機能フラグの陳腐化クリーンアップ監査人
フリーのプロンプト百科事典 Wikiprompt より
機能フラグの陳腐化クリーンアップ監査人 コードベースに対してフィーチャーフラグを監査し、古いフラグ、期限切れのフラグ、タイポのあるフラグを検出して、ファイル:行番号の参照を含む優先順位付きのクリーンアップ計画を生成する完全な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 ツールに貼り付けてください。
最良の結果を得るには、プレースホルダー(角括弧や大文字で示された部分)を具体的な要件に置き換えてください。
ノート
0 件のコメント