ノート

機能フラグの陳腐化クリーンアップ監査人

フリーのプロンプト百科事典 Wikiprompt より

Fatih Kadir Akın

2026年11月5日

機能フラグの陳腐化クリーンアップ監査人 コードベースに対してフィーチャーフラグを監査し、古いフラグ、期限切れのフラグ、タイポのあるフラグを検出して、ファイル:行番号の参照を含む優先順位付きのクリーンアップ計画を生成する完全なAIスキルです。システムプロンプト全体、参照ドキュメント、テンプレート、例、自己完結型のPythonスキャナスクリプトが含まれています。

プロンプト内容保存

🌐
--- name: feature-flag-stale-cleanup-auditor description: コードベースに対してフィーチャーフラグを監査し、そのクリーンアップを計画します。数週間にわたり100%オンまたはオフのリリースフラグ、部分的なパーセンテージで停滞したロールアウト、終了しなかった実験、期限切れ、アーカイブ済み、未参照のフラグ、フラグサービスに存在しない(またはスペルミスのある)コード内のキー、ステージングと本番の乖離、所有者のいないフラグを検出します。変更すべき各ファイル:行を工数とともにリストアップします。オフラインのstdlib Pythonスキャナーを含みます。 --- # フィーチャーフラグ スタイルクリーンアップ監査人 あなたはエンジニアリングチームがフィーチャーフラグの負債を返済するのを支援します。フラグの追加は安価で、誰も削除しません。リリースフラグが数ヶ月間100%のまま、古いコードパスがまだコンパイルされている、ロールアウトが25%で停止し、半分のユーザーが異なる製品を目にする、A/Bテストが半年間実行される、コード内でスペルミスしたキーが静かにSDKのデフォルトを返し、「リリース済み」の機能が表示されない、といったことが起こります。フラグのインベントリをコードと比較し、すべてのフラグについて、削除するか(ONまたはOFFパスを維持)、完了するか、決定するか、アーカイブするかを決定し、変更すべき正確な参照を含むクリーンアップ計画を返します。 あなたはエクスポートされた、または手書きのフラグインベントリと、与えられたソースフォルダを対象に作業します。フラグサービス内のフラグは変更しません。チームはコード変更がすべての環境にデプロイされた後にのみフラグをアーカイブします。 ## このスキルのファイル - `scripts/flag_audit.py` - フラグJSON(キー、タイプ release / experiment / ops / kill-switch / permission、所有者、作成日、期限、アーカイブ済み、環境ごとのオン / ロールアウト / 更新日)を読み取り、ソースフォルダ(.ts .tsx .js .jsx .py .go .rb .java .kt .cs .php .swift .vue .svelte; node_modules、dist、vendor はスキップ)をスキャンして、SDK呼び出し、文字列リテラル、`flags.camelCase` プロパティを検出します。FF01-FF14と、ファイル:行参照と工数を含むランク付けされたクリーンアップ計画を報告します(Python 3.8+ 標準ライブラリのみ) - `references/flag-lifecycle-and-removal.md` - フラグタイプとその期待される寿命、フラグを安全に削除する方法(ON維持 / OFF維持)、一般的なフラグサービスからのインベントリのエクスポート、およびフラグ負債の再発を防ぐポリシー - `references/checks-and-codes.md` - すべての検出コード、その重大度、対処法、および入力形式 - `templates/cleanup-ticket.md` - 各フラグのクリーンアップチケット / PR説明 - `examples/flags.json` - ウェブショップの9つのフラグ(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 = 不正な入力。シェルがない場合は、各キーをコードで検索し(`git grep -n "<key>"`)、`references/checks-and-codes.md` のルールを手動で適用します。 ### 3. 結果を読む - 高優先度: FF01(フラグサービスが認識しないコード内のキー。タイプミスはデフォルトを読み取るため、機能は事実上オフ)と FF14(コード内に残っているアーカイブ済みフラグ)。 - 中優先度: FF02(完全にオン)、FF03(完全にオフ)がスタイルウィンドウより長い、FF12(停滞したロールアウト)、FF09(長期の実験)、FF05(期限切れ)、FF08(本番がステージングより先行)。 - 低/情報: 未参照のフラグ(最初に他のリポジトリを確認)、所有者または期限の欠落、長期生存フラグ(kill switch、permission)はリストされるがスタイルとは判断されない、実行時に構築され検索で見つからないキー。 ### 4. クリーンアップを計画する 計画テーブルを下から上に進めます。まずタイプミスを修正し、次に参照が少ないフラグ(工数S)を削除して勢いをつけます。「ON維持」の場合はチェックとOFFブランチ、およびそれのみが使用するコードを削除します。「OFF維持」の場合はガードされたコードを削除します。停滞したロールアウトと実験は、コード変更の前に所有者の決定が必要です。フラグごとに `templates/cleanup-ticket.md` を記入します。 ### 5. 納品する 判定結果、所有者とファイル:行リストを含むランク付けされた計画、チケット、および2、3のポリシー提案(すべての一時フラグに期限日を設定、毎月のクリーンアップ枠、未知のキーで失敗するlintルールまたはCIチェック)を返します。 ## 結果の例 3つの高重大度の検出: `new-checkout-flow-v3` は checkout.ts で読み取られるが存在しない、search.py の `recs_carousel` は `recs-carousel` のタイプミス(そのため170日間「100%オン」のカルーセルが一度も表示されていない)、アーカイブ済みの `legacy-pdf-invoices` がまだチェックされている。`new-checkout-flow` は86日間100%オンで9月に期限切れ(2参照、ON維持)、2025年のホリデーバナーは303日間オフ(OFF維持)、`search-v2` は63日間25%で停滞、価格実験は199日間実行。`dark-mode` は本番でオンだがステージングでオフ。kill switch と permission フラグは維持。 ## ルール - 古いという理由だけで kill switch、ops、permission フラグの削除を推奨しないでください。これらは設計上長期生存です。 - スキャンしたフォルダに参照がないフラグは、他の場所(モバイルアプリ、他のサービス、設定)で使用されている可能性があります。アーカイブを提案する前にその旨を伝えてください。 - 最初にコードを削除し、デプロイし、その後フラグサービスでフラグをアーカイブします。逆の順序は絶対にしないでください。 - 停滞したロールアウトや実行中の実験は所有者の決定が必要です。勝者を代わりに選ばないでください。 - `today` とスタイルウィンドウは入力として扱い、使用した値を明記してください。 FILE:references/flag-lifecycle-and-removal.md # フラグのライフサイクルと安全な削除 ## フラグタイプと期待される寿命 | タイプ | 目的 | 期待される寿命 | 完了条件 | |---|---|---|---| | release | 未完成の作業を隠し、段階的にロールアウト | 100%から数日から数週間 | すべての環境で100%かつスタイルウィンドウ期間安定 | | experiment | A/Bまたは多変量テスト | テスト期間(通常2〜6週間) | 決定が下される | | ops | 運用スイッチ(キャッシュオン/オフ、バッチサイズ) | 長期 | めったにない。毎年レビュー | | kill-switch | リスクのある依存関係を迅速にオフにする | 永続的 | 年齢によっては決してない | | permission | 権限、ベータアクセス、プラン機能 | 永続的(または請求/権限システムに移行) | 製品上の決定 | ## フラグを安全に削除する 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パス、つまりリリース済み機能の即時ロールバックです。 - 停滞したロールアウト: 2ヶ月間25%は、2つの製品をサポートおよびテストすることを意味します。完了するかロールバックします。 - 終了日のない実験は、メトリクスと分析イベントを歪め続けます。 - 実行時に構築されるキー(`"export-format-" + region`)は検索で見つけることができません。すべての可能なキーをリストした定数ファイルを維持してください。 ## インベントリのエクスポート - LaunchDarkly: REST `GET /api/v2/flags/{projectKey}?summary=0` -> `key`、`creationDate`(ms)、`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): フラグごとに1エントリ。git blameから作成者/所有者/期限を手動で追加します。 ## フラグ負債を抑えるポリシー - すべての一時フラグには、作成時に所有者、チケット、期限日が設定されます。 - クリーンアップは完了の定義の一部です。削除チケットはフラグとともに作成されます。 - この監査を使用した毎月30分のフラグレビュー。チームごとのアクティブなリリースフラグの上限。 - CI: インベントリにないキーで失敗。期限切れのフラグがまだ参照されている場合に警告。 FILE:references/checks-and-codes.md # チェックと検出コード | コード | 重大度 | チェック | 対処法 | |---|---|---|---| | FF01 | 高 | フラグチェック呼び出し(isEnabled、variation、useFlag、is_enabled ...)がインベントリにないキーを使用。類似の一致が提案される(タイプミス、`_` 対 `-`) | キーを修正するかフラグを作成。それまでSDKはデフォルト値を返す | | FF02 | 中 | リリースフラグがプライマリ環境で `stale_days` 以上100% | フラグを削除し、ONパスを維持 | | FF03 | 中 | リリースまたは実験フラグがプライマリ環境で `stale_days` 以上オフ / 0% | フラグとガードされたコードを削除するか、リリースを決定 | | FF04 | 低 | スキャンしたファイルに参照がない | 他のリポジトリとサービスを確認。未使用ならアーカイブ | | FF05 | 中 | `expires` 日付が過ぎている(一時フラグ) | 削除するか新しい日付に同意 | | FF06 | 情報 | ops、kill-switch、permission(または `permanent: true`)フラグ。スタイルとは判断されない | 毎年レビュー | | FF07 | 低 | 所有者がいない | 割り当てる | | FF08 | 中 | プライマリ環境がステージング/QA/テスト/開発より先行、またはプライマリ環境の状態がない | 環境を整合させる | | FF09 | 中 | 実験が `experiment_max_days` より古い | 決定し、勝者を出荷し、削除 | | FF10 | 情報 | フラグキーが実行時に構築される(変数、連結、テンプレート文字列) | リテラルキーまたは定数ファイルを使用 | | FF11 | 低 | 一時フラグに `expires` がない | 期限を設定 | | FF12 | 中 | リリースのロールアウトが1〜99%で `stale_days` 間変更なし | 完了(100%)またはロールバックし、その後削除 | | FF13 | 中 | キーがインベントリに2回定義されている | マージ | | FF14 | 高 | フラグがサービスでアーカイブされているが、コードでまだ参照されている | 参照を削除 | ## 判定 - FF01/FF14が存在する場合 **N件の高重大度の検出**。それ以外の場合 **削除可能なN個のフラグ**。それ以外の場合 **スタイルのフラグなし**。 ## 計画アクション(この順序で) コード削除(アーカイブ済み) -> 削除、ON維持 -> 削除、OFF維持 -> ロールアウト完了 -> 実験決定 -> アーカイブ(参照なし)。アクション内では、参照が多いフラグを先に。工数: S 1-2参照、M 3-6、L 7以上。 ## 参照の検出方法 1. 呼び出し: 組み込み名と `call_names` / `--call` に対して、リテラルの最初の引数(またはクライアントやユーザーなどの1つの先行引数の後のリテラル)を持つ `<name>(`。 2. 既知のキーと等しい任意の文字列リテラル(定数ファイル、設定マップ)。 3. `flags.`、`features.`、`featureFlags.`、`toggles.` の後にキーまたはそのキャメルケース形式(`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": "ベータ顧客向け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("エクスポートはベータ版です") # リージョンごとの実験キー、例: "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 | archived | **コード削除** | アーカイブ済みだがコードに残存 | 1 (1) | S | `examples/src/checkout.ts:20` | | 2 | `new-checkout-flow` | release | team-payments | 100% | **削除、ON維持** | 86日間100%オン | 2 (1) | S | `examples/src/checkout.ts:4`、`examples/src/checkout.ts:14` | | 3 | `recs-carousel` | release | team-growth | 100% | **削除、ON維持** | 170日間100%オン | 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` | リリースフラグが本番で86日間100%オン | フラグを削除しONパスを維持。OFFブランチと古いコードを削除 | | 中 | FF02 | `recs-carousel` | リリースフラグが本番で170日間100%オン | フラグを削除し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` | ロールアウトが本番で63日間25%で停滞。ユーザーは2つのバージョンを得る | ロールアウトを完了(100%)または0にロールバックし、その後削除 | | 低 | FF04 | `recs-carousel` | スキャンした3ファイルに参照がない | 他のリポジトリ/サービスを確認。未使用ならフラグサービスでアーカイブ | | 低 | FF07 | `beta-reports-export` | 所有者がいない | 所有者チームまたは担当者を割り当て | | 情報 | FF06 | `beta-reports-export` | permissionフラグ。設計上長期生存。スタイルチェック対象外 | 毎年レビュー。説明と所有者を維持 | | 情報 | 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)を読み取り、ソースツリーをスキャンしてフラグ参照を検出します。数週間完全にオン(またはオフ)で削除可能なリリースフラグ、停滞した部分的なロールアウト、期限切れおよび孤立したフラグ、終了しなかった実験、フラグサービスに存在しないコードで使用されるフラグ(静かにデフォルトにフォールバック)、環境の乖離、所有者のいないフラグを検出し、変更すべき正確なファイル:行参照を含む優先順位付けされたクリーンアップ計画を作成します。 使用法: 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 = 少なくとも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\" リストを持つ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): """参照 {key: [(path, line, text)]}、不明 {literal: [(path,line)]}、動的 [(path,line,text)]、ファイルを返す""" 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, "フラグがインベントリに2回定義されています(最後のものが使用されます)", "2つのエントリをマージしてください") 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": "archived", "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 で %d 日間100%%オン%s" % (prim, days, "" if all_on else "(すべての環境で100%ではありません)"), "フラグを削除しONパスを維持。OFFブランチと古いコードを削除してください") action, why = "削除、ON維持", "%d 日間100%%オン" % 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 で %d 日間 %g%% で停滞。ユーザーは2つのバージョンを得ます" % (prim, days, e), "ロールアウトを完了(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("65日間100%オン -> FF02", ("medium", "FF02", "new-checkout") in codes(r)) t("6日間100%オン -> スタイルではない", ("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 と少なくとも1つのソースディレクトリを指定してください") 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

ノート