讨论

功能标志过期清理审计器

来自 Wikiprompt,自由的提示词百科全书

Fatih Kadir Akın

2026年11月5日

功能标志过期清理审计器 一个完整的AI技能,用于审计代码库中的功能开关,发现过期、失效或拼写错误的开关,并生成带有文件:行号引用的优先清理计划。包含完整的系统提示词、参考文档、模板、示例以及一个独立的Python扫描脚本。

提示词内容收藏

🌐
--- name: feature-flag-stale-cleanup-auditor description: 对照你的代码库审计功能开关并规划其清理工作,,找出已100%开启或关闭数周的发布开关、卡在部分百分比的灰度发布、从未结束的实验、已过期、已归档和未被引用的开关、代码中使用但开关服务中缺失(或拼写错误)的键、预发布与生产环境漂移以及没有负责人的开关。列出每个需要修改的file:line及工作量。包含一个离线标准库Python扫描器。 --- # 功能开关过期清理审计器 你帮助工程团队偿还功能开关债务。开关添加成本低,但没人会移除它们:一个发布开关在100%状态持续数月,旧代码路径仍被编译在内;灰度发布停在25%,一半用户得到不同的产品;A/B测试运行半年;代码中拼写错误的键悄悄返回SDK默认值,导致"已上线"的功能从未展示。你将开关清单与代码进行对比,为每个开关决定是移除(保留ON或OFF路径)、完成、决策还是归档,并交回一份包含确切修改引用的清理计划。 你基于导出或手写的开关清单以及给定的源码文件夹工作。你不会在开关服务中更改开关:团队只有在代码变更部署到所有环境后才会归档开关。 ## 此技能中的文件 - `scripts/flag_audit.py` - 读取开关JSON(键、类型release/experiment/ops/kill-switch/permission、负责人、创建时间、过期时间、是否归档、各环境on/rollout/updated),并扫描源码文件夹(.ts .tsx .js .jsx .py .go .rb .java .kt .cs .php .swift .vue .svelte;跳过node_modules、dist、vendor)以查找SDK调用、字符串字面量和`flags.camelCase`属性;报告FF01-FF14及带file:line引用和工作量的排序清理计划(仅Python 3.8+标准库) - `references/flag-lifecycle-and-removal.md` - 开关类型及其预期生命周期、如何安全移除开关(保留ON/保留OFF)、从常见开关服务导出清单,以及防止开关债务复发的策略 - `references/checks-and-codes.md` - 每个发现代码、其严重级别及处理方法,以及输入格式 - `templates/cleanup-ticket.md` - 每个开关的清理工单/PR描述 - `examples/flags.json` - 一个网店的九个开关(release、experiment、kill-switch、permission、archived) - `examples/src/checkout.ts`、`examples/src/Header.tsx`、`examples/src/search.py` - 使用它们的代码,包括一个拼写错误的键、一个不存在的键和一个运行时构建的键 - `examples/cleanup-report.md` - 扫描器在示例上的真实输出 ## 工作流程 ### 1. 收集清单和代码 询问他们使用哪个开关服务,并要求导出所有开关,包括类型、负责人、创建日期、过期时间或工单,以及各环境状态(开/关、灰度百分比、最后变更时间)。将其映射为类似`examples/flags.json`的JSON(参见LaunchDarkly、Unleash、Flagsmith、GrowthBook和ConfigCat字段名的参考)。询问读取开关的仓库或文件夹,以及如果开关检查函数不是常见SDK调用时的名称。 ### 2. 运行扫描器 ``` python3 scripts/flag_audit.py examples/flags.json examples/src python3 scripts/flag_audit.py flags.json web/src api/app --today 2026-11-05 --stale-days 21 python3 scripts/flag_audit.py flags.json src --call featureOn --call can python3 scripts/flag_audit.py flags.json src --json python3 scripts/flag_audit.py --self-test ``` 退出码0 = 无高严重级别发现,1 = 有高严重级别发现,2 = 输入错误。如果没有shell,手动搜索代码中的每个键(`git grep -n "<key>"`)并应用`references/checks-and-codes.md`中的规则。 ### 3. 阅读结果 - 首先处理高严重级别:FF01代码中使用但开关服务不认识的键(拼写错误读取默认值,因此功能实际关闭)和FF14已归档但仍被代码引用的开关。 - 中严重级别:FF02完全开启和FF03完全关闭超过过期窗口,FF12卡住的灰度发布,FF09长时间实验,FF05已过期,FF08生产环境领先于预发布环境。 - 低/信息级别:未被引用的开关(先检查其他仓库)、缺少负责人或过期时间、长期存在的开关(kill switches、permissions)列出但不判定为过期、搜索无法找到的运行时构建键。 ### 4. 规划清理 按计划表向下工作:先修复拼写错误,然后移除引用较少的开关(工作量S)以建立势头。对于"保留ON",删除检查、OFF分支以及仅其使用的代码;对于"保留OFF",删除受保护的代码。卡住的灰度发布和实验需要负责人在代码变更前做出决定。为每个开关填写`templates/cleanup-ticket.md`。 ### 5. 交付 返回判定、带负责人和file:line列表的排序计划、工单,以及两三条策略建议(每个临时开关设置过期日期、每月清理时段、对未知键失败的lint规则或CI检查)。 ## 示例结果 三个高严重级别发现:`new-checkout-flow-v3`在checkout.ts中被读取但不存在,`recs_carousel`在search.py中是`recs-carousel`的拼写错误,,因此"100%开启"170天的轮播从未展示过,,以及已归档的`legacy-pdf-invoices`仍被检查。`new-checkout-flow`已100%开启86天并于9月过期(2个引用,保留ON),2025年假日横幅已关闭303天(保留OFF),`search-v2`卡在25%达63天,定价实验已运行199天。`dark-mode`在生产环境开启但在预发布环境关闭。保留kill switch和权限开关。 ## 规则 - 绝不因kill switch、ops或权限开关老旧而建议移除;它们按设计是长期存在的。 - 扫描文件夹中无引用的开关可能在其他地方使用(移动应用、其他服务、配置);在建议归档前说明这一点。 - 先移除代码、部署,然后在服务中归档开关,,绝不能反过来。 - 卡住的灰度发布或运行中的实验需要负责人的决定;不要替他们选择赢家。 - 将`today`和过期窗口视为输入;说明你使用了哪些值。 FILE:references/flag-lifecycle-and-removal.md # 开关生命周期与安全移除 ## 开关类型及预期生命周期 | 类型 | 目的 | 预期生命周期 | 完成条件 | |---|---|---|---| | release | 隐藏未完成的工作,逐步推出 | 100%后数天至数周 | 所有环境100%且在过期窗口内稳定 | | experiment | A/B或多变量测试 | 测试持续时间(通常2-6周) | 做出决定 | | ops | 运维开关(缓存开/关、批处理大小) | 长期 | 很少;每年审查 | | kill-switch | 快速关闭有风险的依赖 | 永久 | 绝不因时间而移除 | | permission | 权限、beta访问、套餐功能 | 永久(或移至计费/权限系统) | 产品决定 | ## 安全移除开关 1. 与负责人确认状态:完全开启(保留ON)或完全关闭(保留OFF),在所有环境和所有读取它的服务中。 2. 找到所有引用(扫描器列表加上其他仓库、移动应用、基础设施配置、分析仪表板和测试中的`git grep -n "<key>"`)。 3. 保留ON:用ON分支替换检查,删除OFF分支,然后删除仅OFF分支使用的代码(旧组件、端点、样式、迁移保护)。保留OFF:删除受保护的代码及仅其使用的任何内容。 4. 删除开关特定的测试和夹具;保留保留行为的测试。 5. 部署到所有环境。移动应用:等待读取该开关的旧应用版本低于你的支持阈值。 6. 然后在开关服务中归档开关(归档,不删除,以保留历史)。 7. 用PR链接关闭工单。 ## 常见陷阱 - 拼写错误和重命名键:SDK对未知键返回默认值而不报错,因此读取`recs_carousel`的代码永远不会看到`recs-carousel`。将字面键与清单比较的CI检查可捕获此问题。 - 先归档:已归档的开关在所有仍读取它的地方返回默认值,,通常是OFF路径,即已上线功能的即时回滚。 - 卡住的灰度发布:25%持续两个月意味着需要支持和测试两个产品;完成或回滚。 - 没有结束日期的实验持续扭曲指标和分析事件。 - 运行时构建的键(`"export-format-" + region`)无法通过搜索找到;保留列出所有可能键的常量文件。 ## 导出清单 - LaunchDarkly: REST `GET /api/v2/flags/{projectKey}?summary=0` -> `key`、`creationDate`(毫秒)、`temporary`、`archived`、`maintainerId`/`_maintainer`、`tags`、`environments.<env>.on`、`.lastModified`、`fallthrough.rollout.variations`中的灰度权重(weight/1000 = 百分比)。将`temporary: false`映射为`permanent: true`。 - Unleash: `GET /api/admin/projects/<p>/features` -> `name`、`type`(release、experiment、operational、kill-switch、permission)、`createdAt`、`stale`、`environments[].enabled`、渐进式发布策略`parameters.rollout`。 - Flagsmith: `/api/v1/projects/<id>/features/` + 各环境`featurestates`(`enabled`、`updated_at`),通过分段覆盖的百分比。 - GrowthBook: `/api/v1/features` -> `id`、`owner`、`dateCreated`、`archived`、`environments.<env>.enabled`、发布规则`coverage`(0-1 -> 百分比)。 - ConfigCat: 管理API设置 + 各环境值;百分比选项给出灰度。 - 内部配置(YAML/JSON):每个开关一个条目;从git blame手动添加created/owner/expires。 ## 保持开关债务低的策略 - 每个临时开关在创建时获得负责人、工单和过期日期。 - 清理是完成定义的一部分:移除工单与开关一起创建。 - 每月30分钟使用此审计的开关审查;每个团队的活动发布开关上限。 - CI:清单中不存在的键失败;仍被引用的过期开关警告。 FILE:references/checks-and-codes.md # 检查与发现代码 | 代码 | 严重级别 | 检查 | 处理方法 | |---|---|---|---| | FF01 | 高 | 开关检查调用(isEnabled、variation、useFlag、is_enabled...)使用清单中不存在的键;建议近似匹配(拼写错误、`_` vs `-`) | 修复键或创建开关;在此之前SDK返回默认值 | | FF02 | 中 | 发布开关在主环境100%达`stale_days`或更久 | 移除开关,保留ON路径 | | FF03 | 中 | 发布或实验开关在主环境关闭/0%达`stale_days`或更久 | 移除开关和受保护代码,或决定上线 | | FF04 | 低 | 扫描文件中无引用 | 检查其他仓库和服务;如未使用则归档 | | FF05 | 中 | `expires`日期已过(临时开关) | 移除或与负责人商定新日期 | | FF06 | 信息 | ops、kill-switch、permission(或`permanent: true`)开关,,不判定为过期 | 每年审查 | | FF07 | 低 | 无负责人 | 指定一个 | | FF08 | 中 | 主环境领先于staging/qa/test/dev,或主环境无状态 | 对齐环境 | | FF09 | 中 | 实验超过`experiment_max_days` | 决策、发布赢家、移除 | | FF10 | 信息 | 开关键在运行时构建(变量、拼接、模板字符串) | 使用字面键或常量文件 | | FF11 | 低 | 临时开关无`expires` | 设置过期时间 | | FF12 | 中 | 发布灰度在1-99%之间且`stale_days`未变 | 完成(100%)或回滚,然后移除 | | FF13 | 中 | 清单中键定义两次 | 合并 | | FF14 | 高 | 开关在服务中已归档但仍被代码引用 | 移除引用 | ## 判定 - 存在FF01/FF14时:**N个高严重级别发现**;否则:**N个开关可移除**;否则:**无过期开关**。 ## 计划操作(按此顺序) 移除代码(已归档)-> 移除,保留ON -> 移除,保留OFF -> 完成灰度 -> 决策实验 -> 归档(无引用)。在同一操作内,引用多的开关优先。工作量:S 1-2个引用,M 3-6个,L 7个以上。 ## 如何找到引用 1. 调用:`<name>(`带字面第一个参数(或一个前导参数如client或user后的字面参数),针对内置名称加上`call_names`/`--call`。 2. 任何等于已知键的字符串字面量(常量文件、配置映射)。 3. `flags.`、`features.`、`featureFlags.`、`toggles.`后跟键或其camelCase形式(`new-search` -> `flags.newSearch`)。 注释中包含带引号的键也被计数,,检查列出的行。 ## 开关JSON 顶层:`today, primary_env (production), environments, stale_days (30), experiment_max_days (90), call_names[], flags[]`。 开关:`key, type (release|experiment|ops|kill-switch|permission), owner, created, expires, permanent, archived, description, ticket, environments{name: {on, rollout 0-100, updated}}`。`rollout`在开启时默认为100,关闭时默认为0;`updated`是该环境的最后变更时间(回退到`created`)。 FILE:templates/cleanup-ticket.md # 移除功能开关 `<key>` **负责人:** <team-payments> **类型:** <release> **工单:** <PAY-412> **操作:** <移除,保留ON> **状态:** <自2026-08-11起生产环境100%开启(86天),预发布环境100%> **已过期:** <2026-09-30> ## 原因 <已完全推出并在86天内稳定;OFF路径(旧结账)是死代码。> ## 变更 - [ ] `<examples/src/checkout.ts:4>` - 用ON分支替换检查(`renderOnePageCheckout`) - [ ] `<examples/src/checkout.ts:14>` - 始终使用`oneClickButton()` - [ ] 删除仅OFF路径使用的代码:<renderLegacyCheckout、classicButton、旧结账样式> - [ ] 删除开关特定的测试和夹具 - [ ] 搜索其他仓库/移动应用:`git grep -n "<key>"` ## 发布 1. 合并并部署到预发布环境,然后生产环境。 2. 观察<结账转化率、错误率>持续<2天>。 3. 在<开关服务>中归档`<key>`(归档,不删除)。 ## 风险/问题 - <移动应用是否读取此开关?最低支持版本?> - <是否有以开关为键的仪表板或警报?> FILE:examples/flags.json { "today": "2026-11-05", "primary_env": "production", "stale_days": 30, "experiment_max_days": 90, "flags": [ {"key": "new-checkout-flow", "type": "release", "owner": "team-payments", "created": "2026-06-02", "expires": "2026-09-30", "ticket": "PAY-412", "description": "新单页结账", "environments": {"production": {"on": true, "rollout": 100, "updated": "2026-08-11"}, "staging": {"on": true, "rollout": 100, "updated": "2026-07-20"}}}, {"key": "search-v2", "type": "release", "owner": "team-search", "created": "2026-07-15", "expires": "2026-12-31", "description": "基于Typesense的搜索", "environments": {"production": {"on": true, "rollout": 25, "updated": "2026-09-03"}, "staging": {"on": true, "rollout": 100, "updated": "2026-08-01"}}}, {"key": "holiday-banner-2025", "type": "release", "owner": "team-growth", "created": "2025-11-10", "expires": "2026-01-10", "description": "2025年圣诞促销横幅", "environments": {"production": {"on": false, "rollout": 0, "updated": "2026-01-06"}, "staging": {"on": false, "rollout": 0, "updated": "2026-01-06"}}}, {"key": "pricing-page-test", "type": "experiment", "owner": "team-growth", "created": "2026-04-20", "expires": "2026-07-01", "description": "A/B测试:年度套餐优先", "environments": {"production": {"on": true, "rollout": 50, "updated": "2026-04-22"}, "staging": {"on": true, "rollout": 50, "updated": "2026-04-22"}}}, {"key": "dark-mode", "type": "release", "owner": "team-web", "created": "2026-10-01", "expires": "2026-12-15", "description": "深色主题", "environments": {"production": {"on": true, "rollout": 100, "updated": "2026-10-28"}, "staging": {"on": false, "rollout": 0, "updated": "2026-10-02"}}}, {"key": "payments-kill-switch", "type": "kill-switch", "owner": "team-payments", "created": "2025-03-01", "description": "PSP中断期间关闭银行卡支付", "environments": {"production": {"on": true, "rollout": 100, "updated": "2025-03-01"}, "staging": {"on": true, "rollout": 100, "updated": "2025-03-01"}}}, {"key": "beta-reports-export", "type": "permission", "owner": "", "created": "2025-09-12", "description": "为beta客户提供CSV导出", "environments": {"production": {"on": true, "rollout": 100, "updated": "2025-09-12"}}}, {"key": "legacy-pdf-invoices", "type": "release", "owner": "team-billing", "created": "2025-05-01", "archived": true, "description": "旧PDF渲染器", "environments": {"production": {"on": false, "rollout": 0, "updated": "2026-02-01"}}}, {"key": "recs-carousel", "type": "release", "owner": "team-growth", "created": "2026-03-03", "description": "产品页面上的推荐轮播", "environments": {"production": {"on": true, "rollout": 100, "updated": "2026-05-19"}, "staging": {"on": true, "rollout": 100, "updated": "2026-05-01"}}} ] } FILE:examples/src/checkout.ts import { flags } from "./flags"; export async function renderCheckout(user: User, cart: Cart) { if (await flags.isEnabled("new-checkout-flow", user)) { return renderOnePageCheckout(user, cart); } return renderLegacyCheckout(user, cart); } export async function payButton(user: User) { if (await flags.isEnabled("payments-kill-switch", user)) { return disabledButton("银行卡支付已暂停。请稍后再试。"); } const oneClick = await flags.isEnabled("new-checkout-flow", user); return oneClick ? oneClickButton() : classicButton(); } export async function invoiceLink(order: Order) { // TODO 在PDF迁移后移除 if (await flags.isEnabled("legacy-pdf-invoices")) { return legacyPdfUrl(order.id); } return invoiceServiceUrl(order.id); } export async function trackCheckoutVariant(user: User) { const variant = await flags.variation("new-checkout-flow-v3", user, "control"); analytics.track("checkout_variant", { variant }); } FILE:examples/src/Header.tsx import { useFlag } from "@acme/flags-react"; export function Header() { const darkMode = useFlag("dark-mode"); const showBanner = useFlag("holiday-banner-2025"); return ( <header className={darkMode ? "header header--dark" : "header"}> {showBanner && <PromoBanner text="假日促销:所有商品20%折扣" />} <SearchBox /> </header> ); } export function PricingPage({ user }: { user: User }) { const annualFirst = useFlag("pricing-page-test"); return annualFirst ? <PlansAnnualFirst /> : <PlansMonthlyFirst />; } FILE:examples/src/search.py from app.flags import flags def search(query, user): if flags.is_enabled("search-v2", user): return typesense_search(query) return postgres_search(query) def export_reports(user, region): if not flags.is_enabled("beta-reports-export", user): raise PermissionError("导出功能处于beta阶段") # 按地区的实验键,例如 "export-format-eu" if flags.is_enabled("export-format-" + region, user): return export_parquet(user) return export_csv(user) def recommendations(product): if flags.is_enabled("recs_carousel"): return top_related(product, limit=12) return [] FILE:examples/cleanup-report.md # 功能开关清理审计 今天2026-11-05,主环境`production`,30天后过期。清单中有9个开关,在examples/src中扫描了3个源文件。 **判定:3个高严重级别发现,,代码中使用但开关服务不认识的开关** 清理候选:6个(4个现在可移除,4个引用需更改)。发现:3个高严重级别,9个中严重级别,2个低严重级别,3个信息级别。 ## 清理计划 | # | 开关 | 类型 | 负责人 | 主环境 | 操作 | 原因 | 引用(文件) | 工作量 | 位置 | |---|---|---|---|---|---|---|---|---|---| | 1 | `legacy-pdf-invoices` | release | team-billing | 已归档 | **移除代码** | 已归档但仍被代码引用 | 1 (1) | S | `examples/src/checkout.ts:20` | | 2 | `new-checkout-flow` | release | team-payments | 100% | **移除,保留ON** | 100%开启86天 | 2 (1) | S | `examples/src/checkout.ts:4`、`examples/src/checkout.ts:14` | | 3 | `recs-carousel` | release | team-growth | 100% | **移除,保留ON** | 100%开启170天 | 0 (0) | - | - | | 4 | `holiday-banner-2025` | release | team-growth | 0% | **移除,保留OFF** | 关闭303天 | 1 (1) | S | `examples/src/Header.tsx:5` | | 5 | `search-v2` | release | team-search | 25% | **完成灰度** | 卡在25% | 1 (1) | S | `examples/src/search.py:5` | | 6 | `pricing-page-test` | experiment | team-growth | 50% | **决策实验** | 运行199天 | 1 (1) | S | `examples/src/Header.tsx:15` | 工作量:S = 1-2个引用,M = 3-6个,L = 7个或更多。"保留ON" = 删除开关检查和OFF分支;"保留OFF" = 删除受保护的代码。仅在代码变更部署到所有环境后,才在开关服务中归档开关。 ## 发现 | 严重级别 | 代码 | 开关 | 发现 | 修复 | |---|---|---|---|---| | 高 | FF01 | `new-checkout-flow-v3` | 代码中使用(examples/src/checkout.ts:27)但开关清单中未定义,,SDK返回默认值,因此此代码路径读取了错误的键 | 是new-checkout-flow的拼写错误?使用确切键 | | 高 | FF01 | `recs_carousel` | 代码中使用(examples/src/search.py:20)但开关清单中未定义,,SDK返回默认值,因此此代码路径读取了错误的键 | 是recs-carousel的拼写错误?使用确切键 | | 高 | FF14 | `legacy-pdf-invoices` | 开关在开关服务中已归档但仍被1处引用,,现在返回默认值 | 移除引用 | | 中 | FF02 | `new-checkout-flow` | 发布开关在生产环境100%开启86天 | 移除开关并保留ON路径;删除OFF分支和旧代码 | | 中 | FF02 | `recs-carousel` | 发布开关在生产环境100%开启170天 | 移除开关并保留ON路径;删除OFF分支和旧代码 | | 中 | FF03 | `holiday-banner-2025` | 发布开关在生产环境关闭303天,,受保护代码是死代码 | 移除开关和受保护代码(或决定上线) | | 中 | FF05 | `holiday-banner-2025` | 已于2026-01-10过期(299天前) | 立即移除或与负责人商定新日期 | | 中 | FF05 | `new-checkout-flow` | 已于2026-09-30过期(36天前) | 立即移除或与负责人商定新日期 | | 中 | FF05 | `pricing-page-test` | 已于2026-07-01过期(127天前) | 立即移除或与负责人商定新日期 | | 中 | FF08 | `dark-mode` | 生产环境100%但预发布环境0%,,生产环境运行预发布环境未测试的路径 | 将预发布环境与生产环境对齐 | | 中 | FF09 | `pricing-page-test` | 实验运行199天(限制90) | 调用结果、发布赢家、移除开关 | | 中 | FF12 | `search-v2` | 灰度卡在生产环境25%达63天,,用户得到两个版本 | 完成灰度(100%)或回滚到0,然后移除 | | 低 | FF04 | `recs-carousel` | 3个扫描文件中无引用 | 检查其他仓库/服务;如未使用,在开关服务中归档 | | 低 | FF07 | `beta-reports-export` | 无负责人 | 指定负责人团队或个人 | | 信息 | FF06 | `beta-reports-export` | 权限开关,,按设计长期存在,不检查过期 | 每年审查;保留描述和负责人 | | 信息 | FF06 | `payments-kill-switch` | kill-switch开关,,按设计长期存在,不检查过期 | 每年审查;保留描述和负责人 | | 信息 | FF10 | `-` | 在examples/src/search.py:14运行时构建开关键,,搜索无法找到其引用 | 优先使用字面键(或常量文件)以便清理开关 | ## 每个开关的引用 | 开关 | 引用 | |---|---| | `beta-reports-export` | 1 | | `dark-mode` | 1 | | `holiday-banner-2025` | 1 | | `legacy-pdf-invoices` | 1 | | `new-checkout-flow` | 2 | | `payments-kill-switch` | 1 | | `pricing-page-test` | 1 | | `recs-carousel` | 0 | | `search-v2` | 1 | 搜索仅覆盖扫描文件夹中的字符串字面量、常见SDK调用和`flags.camelCase`属性;归档开关前检查其他服务、移动应用和配置仓库。 FILE:scripts/flag_audit.py #!/usr/bin/env python3 """功能开关过期清理审计器(仅标准库,Python 3.8+)。 读取功能开关清单(从LaunchDarkly、Unleash、Flagsmith、GrowthBook、ConfigCat或内部配置导出或手写的JSON),并扫描源码树以查找开关引用。找出已完全开启(或关闭)数周且可移除的发布开关、卡住的部分灰度、已过期和孤立的开关、从未结束的实验、代码中使用但开关服务中缺失的开关(它们静默回退到默认值)、环境漂移和无负责人的开关,,并写出带确切file:line引用的优先清理计划。 用法: flag_audit.py FLAGS.json SRC_DIR [SRC_DIR ...] [--today YYYY-MM-DD] [--stale-days N] [--call NAME ...] [--json] flag_audit.py --self-test 退出:0 = 无高严重级别发现,1 = 至少一个高严重级别发现,2 = 输入错误。 """ import argparse, datetime, difflib, json, os, re, sys SEV = {"high": 0, "medium": 1, "low": 2, "info": 3} TYPES = {"release", "experiment", "ops", "kill-switch", "permission"} LONG_LIVED = {"ops", "kill-switch", "permission"} EXTS = {".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".go", ".rb", ".java", ".kt", ".cs", ".php", ".swift", ".vue", ".svelte"} SKIP_DIRS = {"node_modules", ".git", "dist", "build", "vendor", "__pycache__", ".venv", "venv", "coverage"} CALLS = ["isEnabled", "isFeatureEnabled", "variation", "boolVariation", "stringVariation", "useFlag", "useFeatureFlag", "useFeatureIsOn", "getFlag", "getFeatureValue", "isOn", "feature_enabled", "is_enabled", "flag_enabled", "enabled?", "Enabled", "IsEnabled", "getBooleanValue", "hasFeature"] TOP_KEYS = {"today", "stale_days", "experiment_max_days", "primary_env", "environments", "flags", "call_names", "source"} FLAG_KEYS = {"key", "type", "owner", "created", "expires", "permanent", "description", "environments", "archived", "ticket"} class InputError(Exception): pass def pdate(s, what): try: return datetime.date.fromisoformat(str(s).strip()[:10]) except ValueError: raise InputError("%s必须是YYYY-MM-DD,得到%r" % (what, s)) def camel(k): parts = re.split(r"[-_.]", k) return parts[0] + "".join(p[:1].upper() + p[1:] for p in parts[1:]) # ---------------------------------------------------------------- load def load_flags(cfg, today=None, stale_days=None): if not isinstance(cfg, dict) or not isinstance(cfg.get("flags"), list): raise InputError("flags文件必须是带\"flags\"列表的JSON对象") bad = set(cfg) - TOP_KEYS if bad: raise InputError("未知顶层键:%s" % ", ".join(sorted(bad))) c = {"today": pdate(today or cfg.get("today") or datetime.date.today().isoformat(), "today"), "stale": int(stale_days if stale_days is not None else cfg.get("stale_days", 30)), "exp_max": int(cfg.get("experiment_max_days", 90)), "primary": cfg.get("primary_env", "production"), "envs": cfg.get("environments") or [], "flags": {}, "dups": [], "calls": cfg.get("call_names") or []} for i, f in enumerate(cfg["flags"], 1): if not isinstance(f, dict) or not f.get("key"): raise InputError("第%d个开关无键" % i) badk = set(f) - FLAG_KEYS if badk: raise InputError("开关%s:未知键%s" % (f["key"], ", ".join(sorted(badk)))) t = (f.get("type") or "release").lower() if t not in TYPES: raise InputError("开关%s:类型必须是%s之一,得到%r" % (f["key"], ", ".join(sorted(TYPES)), t)) envs = {} for e, st in (f.get("environments") or {}).items(): if not isinstance(st, dict): raise InputError("开关%s环境%s必须是对象{on, rollout, updated}" % (f["key"], e)) r = st.get("rollout", 100 if st.get("on") else 0) if not isinstance(r, (int, float)) or not 0 <= r <= 100: raise InputError("开关%s环境%s:rollout必须是0-100,得到%r" % (f["key"], e, r)) envs[e] = {"on": bool(st.get("on")), "rollout": float(r), "updated": pdate(st["updated"], "开关%s环境%s updated" % (f["key"], e)) if st.get("updated") else None} fl = {"key": f["key"], "type": t, "owner": f.get("owner") or "", "ticket": f.get("ticket", ""), "created": pdate(f["created"], "开关%s created" % f["key"]) if f.get("created") else None, "expires": pdate(f["expires"], "开关%s expires" % f["key"]) if f.get("expires") else None, "permanent": bool(f.get("permanent")) or t in LONG_LIVED, "archived": bool(f.get("archived")), "description": f.get("description", ""), "envs": envs} if fl["key"] in c["flags"]: c["dups"].append(fl["key"]) c["flags"][fl["key"]] = fl return c def eff(st): """获得开关的用户有效份额:0-100""" return st["rollout"] if st["on"] else 0.0 # ---------------------------------------------------------------- scan def scan(dirs, keys, calls): """返回refs {key: [(path, line, text)]}、unknown {literal: [(path,line)]}、dynamic [(path,line,text)]、files""" calls = sorted(set(CALLS + list(calls)), key=len, reverse=True) alt = "|".join(re.escape(c) for c in calls) call_re = re.compile(r"(?:\b|\.)(%s)\s*\(\s*(?:\w+\s*,\s*)?([\"'`])([^\"'`$]+)\2\s*[,)]" % alt) dyn_re = re.compile(r"(?:\b|\.)(%s)\s*\(\s*(?![\"'`][^\"'`$]*[\"'`]\s*[,)])([^,)]+)[,)]" % alt) lit_re = re.compile(r"([\"'`])([A-Za-z0-9][A-Za-z0-9_.:-]{2,80})\1") camels = {camel(k): k for k in keys if camel(k) != k} prop_re = re.compile(r"\b(?:flags|features|featureFlags|toggles)\.([A-Za-z_]\w*)") refs, unknown, dynamic, nfiles = {k: [] for k in keys}, {}, [], 0 for d in dirs: if not os.path.isdir(d): raise InputError("源目录未找到:%s" % d) for root, ds, fs in os.walk(d): ds[:] = sorted(x for x in ds if x not in SKIP_DIRS) for fn in sorted(fs): if os.path.splitext(fn)[1] not in EXTS: continue p = os.path.join(root, fn) nfiles += 1 try: lines = open(p, encoding="utf-8", errors="replace").read().splitlines() except OSError as e: raise InputError("无法读取%s:%s" % (p, e)) for i, ln in enumerate(lines, 1): seen = set() for m in call_re.finditer(ln): lit = m.group(3) if lit in refs: refs[lit].append((p, i, ln.strip())); seen.add(lit) else: unknown.setdefault(lit, []).append((p, i, ln.strip())) seen.add(lit) for m in dyn_re.finditer(ln): if m.group(2).strip() not in ("true", "false", "None", "null", ""): dynamic.append((p, i, ln.strip())) for m in lit_re.finditer(ln): lit = m.group(2) if lit in refs and lit not in seen: refs[lit].append((p, i, ln.strip())); seen.add(lit) for m in prop_re.finditer(ln): k = camels.get(m.group(1)) or (m.group(1) if m.group(1) in refs else None) if k and k not in seen: refs[k].append((p, i, ln.strip())); seen.add(k) return refs, unknown, dynamic, nfiles # ---------------------------------------------------------------- audit def audit(c, refs, unknown, dynamic, nfiles): F, plan = [], [] today, stale, prim = c["today"], c["stale"], c["primary"] add = lambda sev, code, key, msg, fix: F.append((sev, code, key, msg, fix)) for k in c["dups"]: add("medium", "FF13", k, "清单中开关定义两次(使用最后一个)", "合并两个条目") keys = list(c["flags"]) norm = {re.sub(r"[-_.]", "", k.lower()): k for k in keys} for lit, where in sorted(unknown.items()): close = norm.get(re.sub(r"[-_.]", "", lit.lower())) or (difflib.get_close_matches(lit, keys, 1, 0.8) or [None])[0] locs = ", ".join("%s:%d" % (p, i) for p, i, _ in where[:3]) add("high", "FF01", lit, "代码中使用(%s)但开关清单中未定义,,SDK返回默认值,因此此代码路径%s" % (locs, "可能从未开启" if not close else "读取了错误的键"), ("是%s的拼写错误?使用确切键" % close) if close else "创建开关或移除检查") for p, i, txt in dynamic[:20]: add("info", "FF10", "-", "在%s:%d运行时构建开关键,,搜索无法找到其引用" % (p, i), "优先使用字面键(或常量文件)以便清理开关") for k, f in c["flags"].items(): r = refs.get(k, []) files = sorted(set(p for p, _, _ in r)) st = f["envs"].get(prim) age = (today - f["created"]).days if f["created"] else None action, why = None, "" if not f["owner"]: add("low", "FF07", k, "无负责人", "指定负责人团队或个人") if f["archived"]: if r: add("high", "FF14", k, "开关在开关服务中已归档但仍被%d处引用,,现在返回默认值" % len(r), "移除引用") plan.append({"key": k, "type": f["type"], "owner": f["owner"] or "?", "state": "已归档", "action": "移除代码", "why": "已归档但仍被代码引用", "refs": len(r), "files": len(files), "locs": ["%s:%d" % (p, i) for p, i, _ in r], "ticket": f["ticket"]}) continue if st is None and not f["archived"]: add("medium", "FF08", k, "主环境%r无状态" % prim, "添加%s状态或修复primary_env" % prim) if f["permanent"]: add("info", "FF06", k, "%s开关,,按设计长期存在,不检查过期" % f["type"], "每年审查;保留描述和负责人") if not r: add("low", "FF04", k, "%d个扫描文件中无引用" % nfiles, "检查其他仓库/服务;如未使用,在开关服务中归档") action, why = action or "归档", "无引用" elif st is not None: since = st["updated"] or f["created"] days = (today - since).days if since else None e = eff(st) all_on = all(eff(s) == 100 for s in f["envs"].values()) if f["expires"] and f["expires"] < today: add("medium", "FF05", k, "已于%s过期(%d天前)" % (f["expires"], (today - f["expires"]).days), "立即移除或与负责人商定新日期") if f["type"] == "release" and e == 100 and days is not None and days >= stale: add("medium", "FF02", k, "发布开关在%s 100%%开启%d天%s" % (prim, days, "" if all_on else "(并非所有环境100%)"), "移除开关并保留ON路径;删除OFF分支和旧代码") action, why = "移除,保留ON", "100%%开启%d天" % days elif e == 0 and days is not None and days >= stale and f["type"] in ("release", "experiment"): add("medium", "FF03", k, "%s开关在%s关闭%d天,,受保护代码是死代码" % (f["type"], prim, days), "移除开关和受保护代码(或决定上线)") action, why = "移除,保留OFF", "关闭%d天" % days elif 0 < e < 100 and days is not None and days >= stale and f["type"] == "release": add("medium", "FF12", k, "灰度卡在%s的%g%%达%d天,,用户得到两个版本" % (prim, e, days), "完成灰度(100%)或回滚到0,然后移除") action, why = "完成灰度", "卡在%g%%" % e if f["type"] == "experiment" and age is not None and age > c["exp_max"]: add("medium", "FF09", k, "实验运行%d天(限制%d)" % (age, c["exp_max"]), "调用结果、发布赢家、移除开关") action, why = action or "决策实验", "运行%d天" % age if not r: add("low", "FF04", k, "%d个扫描文件中无引用" % nfiles, "检查其他仓库/服务;如未使用,在开关服务中归档") action, why = action or "归档", why or "无引用" for en, s2 in f["envs"].items(): if en != prim and eff(s2) < e and (en in ("staging", "qa", "test", "dev", "development")): add("medium", "FF08", k, "%s为%g%%但%s为%g%%,,生产环境运行预发布环境未测试的路径" % (prim, e, en, eff(s2)), "将%s与%s对齐" % (en, prim)) if f["expires"] is None and f["type"] in ("release", "experiment") and action is None: add("low", "FF11", k, "临时%s开关无过期日期" % f["type"], "设置expires(例如完全推出后30-60天)") if action: plan.append({"key": k, "type": f["type"], "owner": f["owner"] or "?", "state": ("%g%%" % eff(st)) if st else "-", "action": action, "why": why, "refs": len(r), "files": len(files), "locs": ["%s:%d" % (p, i) for p, i, _ in r], "ticket": f["ticket"]}) rank = {"移除代码": 0, "移除,保留ON": 1, "移除,保留OFF": 2, "完成灰度": 3, "决策实验": 4, "归档": 5} plan.sort(key=lambda x: (rank[x["action"]], -x["refs"], x["key"])) F.sort(key=lambda x: (SEV[x[0]], x[1], x[2])) return {"today": today.isoformat(), "primary": prim, "stale_days": stale, "flags": len(c["flags"]), "files": nfiles, "refs": {k: len(v) for k, v in refs.items()}, "plan": plan, "findings": F} def effort(n): return "S" if n <= 2 else "M" if n <= 6 else "L" def report(r, src): c = {s: 0 for s in SEV} for f in r["findings"]: c[f[0]] += 1 removable = [p for p in r["plan"] if p["action"].startswith("移除")] out = ["# 功能开关清理审计", "", "今天%s,主环境`%s`,%d天后过期。清单中有%d个开关,在%s中扫描了%d个源文件。" % ( r["today"], r["primary"], r["stale_days"], r["flags"], r["files"], ", ".join(src)), ""] if c["high"]: v = "%d个高严重级别发现,,代码中使用但开关服务不认识的开关" % c["high"] elif removable: v = "%d个开关可移除" % len(removable) else: v = "无过期开关" out += ["**判定:%s**" % v, "", "清理候选:%d个(%d个现在可移除,%d个引用需更改)。发现:%d个高严重级别,%d个中严重级别,%d个低严重级别,%d个信息级别。" % ( len(r["plan"]), len(removable), sum(p["refs"] for p in removable), c["high"], c["medium"], c["low"], c["info"]), ""] out += ["## 清理计划", "", "| # | 开关 | 类型 | 负责人 | 主环境 | 操作 | 原因 | 引用(文件) | 工作量 | 位置 |", "|---|---|---|---|---|---|---|---|---|---|"] for i, p in enumerate(r["plan"], 1): locs = ", ".join("`%s`" % x for x in p["locs"][:4]) + (" +%d更多" % (len(p["locs"]) - 4) if len(p["locs"]) > 4 else "") out.append("| %d | `%s` | %s | %s | %s | **%s** | %s | %d (%d) | %s | %s |" % (i, p["key"], p["type"], p["owner"], p["state"], p["action"], p["why"], p["refs"], p["files"], effort(p["refs"]) if p["refs"] else "-", locs or "-")) out += ["", "工作量:S = 1-2个引用,M = 3-6个,L = 7个或更多。\"保留ON\" = 删除开关检查和OFF分支;\"保留OFF\" = 删除受保护的代码。仅在代码变更部署到所有环境后,才在开关服务中归档开关。", ""] out += ["## 发现", "", "| 严重级别 | 代码 | 开关 | 发现 | 修复 |", "|---|---|---|---|---|"] for s, code, k, msg, fix in r["findings"]: out.append("| %s | %s | `%s` | %s | %s |" % (s, code, k, msg.replace("|", "\\|"), fix.replace("|", "\\|"))) out += ["", "## 每个开关的引用", "", "| 开关 | 引用 |", "|---|---|"] for k, n in sorted(r["refs"].items()): out.append("| `%s` | %d |" % (k, n)) out += ["", "搜索仅覆盖扫描文件夹中的字符串字面量、常见SDK调用和`flags.camelCase`属性;归档开关前检查其他服务、移动应用和配置仓库。"] return "\n".join(out) + "\n" def run(flags_path, dirs, today=None, stale=None, calls=()): try: cfg = json.load(open(flags_path, encoding="utf-8")) except (OSError, ValueError) as e: raise InputError("无法读取%s:%s" % (flags_path, e)) c = load_flags(cfg, today, stale) refs, unknown, dynamic, n = scan(dirs, list(c["flags"]), list(c["calls"]) + list(calls)) return audit(c, refs, unknown, dynamic, n) # ---------------------------------------------------------------- self-test def self_test(): import tempfile tests = [] def t(name, cond): tests.append((name, bool(cond))) def mk(flags, files, **kw): d = tempfile.mkdtemp() for name, body in files.items(): os.makedirs(os.path.dirname(os.path.join(d, name)) or d, exist_ok=True) open(os.path.join(d, name), "w").write(body) cfg = {"today": "2026-11-05", "flags": flags} cfg.update(kw) c = load_flags(cfg) refs, unk, dyn, n = scan([d], list(c["flags"]), c["calls"]) return audit(c, refs, unk, dyn, n) def codes(r): return [(f[0], f[1], f[2]) for f in r["findings"]] P = lambda on, r, up: {"production": {"on": on, "rollout": r, "updated": up}} rel = lambda k, on=True, r=100, up="2026-09-01", **x: dict({"key": k, "type": "release", "owner": "web", "created": "2026-08-01", "expires": "2027-01-01", "environments": P(on, r, up)}, **x) src = {"a.ts": "if (client.isEnabled('new-checkout')) {}\nconst x = useFlag(\"dark-mode\")\n"} r = mk([rel("new-checkout"), rel("dark-mode", up="2026-10-30")], src) t("100%开启65天 -> FF02", ("medium", "FF02", "new-checkout") in codes(r)) t("100%开启6天 -> 不过期", ("medium", "FF02", "dark-mode") not in codes(r)) t("计划操作保留ON", [p["action"] for p in r["plan"] if p["key"] == "new-checkout"] == ["移除,保留ON"]) t("通过调用找到引用", r["refs"]["new-checkout"] == 1 and r["refs"]["dark-mode"] == 1) r = mk([rel("old-banner", on=False, r=0)], {"b.py": "if flags.is_enabled(\"old-banner\"):\n pass\n"}) t("关闭65天 -> FF03保留OFF", ("medium", "FF03", "old-banner") in codes(r) and r["plan"][0]["action"] == "移除,保留OFF") r = mk([rel("half", r=50)], {"c.js": "isEnabled('half')"}) t("卡住的灰度 -> FF12", ("medium", "FF12", "half") in codes(r)) r = mk([rel("new-checkout")], {"a.ts": "isEnabled('new_checkout')\n"}) t("代码中未定义的键 -> FF01高", ("high", "FF01", "new_checkout") in codes(r)) t("FF01建议近似键", any("是new-checkout的拼写错误" in f[4] for f in r["findings"])) t("FF01单独意味着真实键0引用 -> FF04", ("low", "FF04", "new-checkout") in codes(r)) r = mk([rel("ghost")], {"a.ts": "const a = 1\n"}) t("无引用 -> FF04 + 归档", ("low", "FF04", "ghost") in codes(r)) r = mk([rel("exp1", type="experiment", created="2026-05-01", r=50, up="2026-10-20")], {"a.ts": "variation('exp1', false)"}) t("实验超过90天 -> FF09", ("medium", "FF09", "exp1") in codes(r)) r = mk([rel("exp2", expires="2026-10-01", up="2026-10-30")], {"a.ts": "isEnabled('exp2')"}) t("已过期 -> FF05", ("medium", "FF05", "exp2") in codes(r)) r = mk([dict(rel("ks"), type="kill-switch", expires=None)], {"a.ts": "isEnabled('ks')"}) t("kill-switch跳过 -> 仅FF06信息", [x[1] for x in codes(r)] == ["FF06"]) r = mk([dict(rel("p", up="2026-10-30"), owner="")], {"a.ts": "isEnabled('p')"}) t("无负责人 -> FF07", ("low", "FF07", "p") in codes(r)) r = mk([dict(rel("d", up="2026-10-30"), environments={"production": {"on": True, "rollout": 100, "updated": "2026-10-30"}, "staging": {"on": False}})], {"a.ts": "isEnabled('d')"}) t("生产领先于预发布 -> FF08", ("medium", "FF08", "d") in codes(r)) r = mk([rel("x")], {"a.ts": "const k = 'x' + region\nisEnabled(flagName)\n"}) t("动态键 -> FF10信息", ("info", "FF10", "-") in codes(r)) r = mk([rel("x")], {"a.py": "is_enabled('export-' + region)\n"}) t("字面量+变量是动态,不是未定义", ("info", "FF10", "-") in codes(r) and not [f for f in r["findings"] if f[1] == "FF01"]) r = mk([rel("new-search", up="2026-10-30")], {"s.tsx": "if (flags.newSearch) {}\n"}) t("flags.camelCase属性计数", r["refs"]["new-search"] == 1) r = mk([rel("new-search", up="2026-10-30")], {"node_modules/x.js": "isEnabled('new-search')", "a.md": "isEnabled('new-search')"}) t("跳过node_modules和非代码文件", r["refs"]["new-search"] == 0) r = mk([rel("t", up="2026-10-30", expires=None)], {"a.ts": "isEnabled('t')"}) t("无过期时间 -> FF11", ("low", "FF11", "t") in codes(r)) r = mk([rel("arch", archived=True)], {"a.ts": "isEnabled('arch')"}) t("已归档但被引用 -> FF14高", ("high", "FF14", "arch") in codes(r) and r["plan"][0]["action"] == "移除代码") r = mk([rel("dup", up="2026-10-30"), rel("dup", up="2026-10-30")], {"a.ts": "isEnabled('dup')"}) t("重复键 -> FF13", ("medium", "FF13", "dup") in codes(r)) r = mk([rel("cust", up="2026-10-30")], {"a.ts": "myFlags.check('cust')\nmyFlags.check('nope')\n"}, call_names=["check"]) t("自定义调用名找到未定义键", ("high", "FF01", "nope") in codes(r)) r = mk([rel("stale14", up="2026-10-20")], {"a.ts": "isEnabled('stale14')"}, stale_days=14) t("尊重stale_days设置", ("medium", "FF02", "stale14") in codes(r)) for bad, name in (({"flags": [{"key": "a", "type": "temp"}]}, "错误类型"), ({"flags": [{"key": "a", "environments": {"production": {"on": True, "rollout": 150}}}]}, "rollout > 100"), ({"flags": [{"key": "a", "colour": 1}]}, "未知开关键"), ({"flag": []}, "缺少flags列表")): try: load_flags(bad); t("拒绝:" + name, False) except InputError: t("拒绝:" + name, True) r = mk([rel("new-checkout")], src) t("报告有判定和计划", "**判定:" in report(r, ["x"]) and "## 清理计划" in report(r, ["x"])) for n, ok in tests: print("%s %s" % ("PASS" if ok else "FAIL", n)) p = sum(ok for _, ok in tests) print("%d/%d通过" % (p, len(tests))) return 0 if p == len(tests) else 1 def main(argv=None): ap = argparse.ArgumentParser(description="查找过期功能开关并规划其移除。") ap.add_argument("flags", nargs="?") ap.add_argument("src", nargs="*") ap.add_argument("--today") ap.add_argument("--stale-days", type=int) ap.add_argument("--call", action="append", default=[], help="额外的开关检查函数名(可重复)") ap.add_argument("--json", action="store_true") ap.add_argument("--self-test", action="store_true") a = ap.parse_args(argv) if a.self_test: return self_test() if not a.flags or not a.src: ap.error("给出FLAGS.json和至少一个源目录") try: r = run(a.flags, a.src, a.today, a.stale_days, a.call) except InputError as e: print("错误:%s" % e, file=sys.stderr) return 2 if a.json: print(json.dumps({**r, "findings": [dict(zip(("severity", "code", "flag", "finding", "fix"), f)) for f in r["findings"]]}, indent=2)) else: sys.stdout.write(report(r, a.src)) return 1 if any(f[0] == "high" for f in r["findings"]) else 0 if __name__ == "__main__": sys.exit(main())

登录以查看完整提示词

继续使用:

登录即表示你同意我们的 使用条款 和 隐私政策

用法

此提示词专为 coding 设计。复制上方内容并粘贴到你常用的 AI 工具中。

为获得最佳效果,可将占位符(方括号或大写字母标示)替换为你的具体需求。

参考资料

分类:coding| prompts.chat| feature-flags| code-cleanup

讨论