--- name: co-evolve-check description: "Detects retention bias in pre-PR diffs where agents add new versions (interface/class/function wrappers like `FooV2` / `getUserNew`) alongside the old ones, when (a) all callers of the old symbol are touched in the same PR and (b) no public consumer markers exist (no public `package.json` exports / no PyPI metadata / no `openapi.yaml` / `*.proto` / `*.graphql` references / no `@deprecated` annotations). Auto-detects language (TS/JS via `package.json`, Python via `pyproject.toml` / `requirements.txt`) and silently skips when detection fails (project-agnostic, requires nothing from the host project). Outputs structured findings with subtractive questions (\"なぜ旧版を残したか?\"). Non-blocking, report-only. Use when about to create a PR (alongside `/comment-sweep`), or when the user mentions リファクタ後の旧版残置 / 後方互換 shim / 同時更新可能 / co-evolution / dead code from version parallelism / internal API retention bias." --- # co-evolve-check 「公開 API でないのに後方互換性を保とうとする」パターンを pre-PR diff から検出する leaf skill ([rules/skills.md](../../../rules/skills.md))。「外部消費者がある = 現状形保持の正当な理由」の**逆ケース** (= 外部消費者なし = 同時更新可能) を機械的に判定する。 `/comment-sweep` と同様、PR 作成前の sweep として動作する。検出のみで PR を block しない (non-blocking report-only)。 ## いつ使うか 主発火点: - `gh pr create` を実行する**前**に `/comment-sweep` と並走で起動 - 既存 PR に追加 push する前 - 「リファクタした」「重複削除した」「型を整理した」等の作業終盤 検出対象となる typical な agent シナリオ: - TypeScript の `interface User` と `interface UserV2` / `UserOld` / `LegacyUser` を並走させる - Python の `class Foo` と `class FooV2` を並走させる - 関数の wrapper 並走 (`getUser` + `getUserNew` / `getUserV2`) - 同一 PR で旧版を消せる場面 (= 全 caller が touched + 外部消費者なし) なのに旧版を温存 検出されないケース (silent skip / low confidence): - 公開 API (`package.json` の public exports / PyPI metadata / `*.proto` / `openapi.yaml` 等から参照される symbol) - `@deprecated` / `Deprecation:` annotation で意図的な段階廃止プロセス中 - 旧 symbol の caller の 1 つでも同 PR で touched されていない場合 (= 外部消費者の可能性) - 言語自動推定不能な project (TS/JS/Python のいずれの marker file もない) - 純粋な revert PR (base..HEAD 全 commit subject が `^Revert "`) ## 引数 | 引数 | 説明 | |------|------| | `BASE_BRANCH` (位置引数、任意) | base ref を明示 (例: `main`, `develop`)。default は `origin/HEAD` → `main` の順で解決 | | `--staged` | `git diff --cached` を見る (commit 前の index) | | `--worktree` | `git diff HEAD` を見る (commit 前の tracked 未 commit 変更) | 引数なし: 現在 branch の `base..HEAD` を sweep。 ## 手順 ```text Co-evolve-check Progress: - [ ] Step 1: project の言語推定 (TS/JS / Python / 推定不能) - [ ] Step 1.5: 純粋 revert PR の auto-skip 判定 - [ ] Step 2: diff から候補 symbol 抽出 (X1 + X2) - [ ] Step 3: 各候補について caller を grep して co-evolution scope を判定 - [ ] Step 4: public marker を推定 (FP 除外) - [ ] Step 5: 構造化 finding を出力 ``` 実装は `scripts/co_evolve_check.py` が SoT で、上記 Step を一括で行う。CLI: ```bash python3 .claude/skills/co-evolve-check/scripts/co_evolve_check.py \ [BASE_BRANCH] [--base BASE_BRANCH] [--staged | --worktree] [--json] ``` `--json` で JSON 出力 (機械可読、CI 連携用)。default は人間向け text 出力。 exit code: - 0: 正常終了 (findings の有無に関わらず) - 1: 引数エラー - 2: git command 失敗 - 3: 言語推定不能 / 純粋 revert PR (silent skip) - 6: その他のエラー ### 各 Step の意味 **Step 1: project の言語推定** — repo root で marker file を確認: - `package.json` 存在 → TypeScript / JavaScript 対象 - `pyproject.toml` / `setup.py` / `setup.cfg` / `requirements.txt` / `requirements*.txt` / `Pipfile` のいずれか → Python 対象 - どれも無い → silent skip `CLAUDE_SKILL_CO_EVOLVE_CHECK_DISABLE=1` が設定されていれば silent skip。 `CLAUDE_SKILL_CO_EVOLVE_CHECK_LANGUAGES=ts,py` が設定されていれば言語自動推定を上書き (csv 形式)。 **Step 1.5: revert PR auto-skip** — `git log --format=%s ..HEAD` の subject が全て `^Revert "` で始まる場合は silent skip。 **Step 2: diff から候補 symbol 抽出** — `git diff ...HEAD` (引数なし時)、`git diff --cached` (`--staged`)、`git diff HEAD` (`--worktree`) で diff を取得。`+` 行から以下を抽出: - **X1: 型・interface 並走** — TS の `interface (\w+)` / `type (\w+) = ...` の追加 + 同一 module 内に version suffix/prefix の対 (`Foo` + `FooV2`、`(V\d+|Old|New|Legacy|Compat|Deprecated)` 命名 regex)。Python の `class (\w+)` 同型。 - **X2: 関数 wrapper 並走** — TS の `function (\w+)` / `const (\w+) = (async)? \(` / `export function (\w+)` の追加 + 既存の同 base name + suffix (`getUser` + `getUserNew` / `getUserV2`)。Python の `def (\w+)` + `_new` / `_v\d+` / `_old` / `_legacy` suffix。 候補 symbol を `(old_symbol, new_symbol, file:line)` の組として記録。 **Step 3: caller 解析と co-evolution scope 判定** — 各候補 `(old, new)` について `grep -rn ""` で全 reference を抽出し、各 reference の touched 状態を `git diff` の touched line と照合: - 全 reference が同 PR で touched + public marker なし → `Co-evolution scope: confirmed` (`Confidence: high`) - 1 件でも未 touched → `Co-evolution scope: uncertain (1+ reference not touched)` (`Confidence: low`) - reference 0 件 → `Co-evolution scope: confirmed (no callers)` (`Confidence: medium`、旧版が完全に dead code) **Step 4: public marker 推定 (FP 回避)** — old_symbol が以下にマッチする場合は `Public marker: detected: ` を記録: - TS: `package.json` の `"private": true` でない + `"exports"` / `"main"` / `"types"` に old_symbol を含む module 宣言、または `export` 付き + `tsconfig.json` の `declaration: true` / `.d.ts` 経由公開 - Python: `pyproject.toml` `[project]` section 存在 (PyPI publish 想定) / `__all__` に含まれる / `_` prefix なしの public symbol - Cross-language: `openapi.yaml` / `swagger.yaml` / `*.proto` / `*.graphql` / `schema.json` から old_symbol 参照、`@deprecated` / `Deprecation:` annotation **Step 5: 構造化 finding を出力** — 各 finding は以下の形式: ```text Co-evolution opportunity: Evidence: (old) + (new) Old symbol: New symbol: Callers of old symbol: - [touched in this PR ✓ / not touched ✗] ... Public marker: > Co-evolution scope: Subtractive question: なぜ を残したか? 全 caller が同 PR 内で touched され、外部消費者がいない場合、旧版を削除して新版に統一できる。 Suggested next action: <具体的なステップ — 例: "src/types/user.ts:10 の `interface UserOld` を削除し、全 caller を `User` に置換する"> Confidence: ``` 最後に summary 1 行: ```text ✅ co-evolve-check: ( high / medium / low confidence) ``` findings 0 件なら `✅ co-evolve-check: no co-evolution opportunities found`。 ## 出力例 ```text Co-evolution opportunity: X1: 型並走 Evidence: src/types/user.ts:10 (old) + src/types/user.ts:20 (new) Old symbol: UserOld New symbol: User Callers of old symbol: 3 references - src/api/legacy.ts:5 [touched in this PR ✓] - src/handlers/old.ts:12 [touched in this PR ✓] - src/types/index.ts:3 [touched in this PR ✓] Public marker: none (no export to package boundary; not referenced in openapi.yaml / *.proto / public docs) Co-evolution scope: confirmed Subtractive question: なぜ UserOld を残したか? 全 caller が同 PR 内で touched され、外部消費者がいない場合、UserOld を削除して User のみに統一できる。 Suggested next action: src/types/user.ts:10 の `interface UserOld` を削除し、src/api/legacy.ts:5 / src/handlers/old.ts:12 / src/types/index.ts:3 の参照を `User` に置換する。 Confidence: high ✅ co-evolve-check: 1 finding (1 high / 0 medium / 0 low confidence) ``` ## 環境変数 (任意 toggle) project 側に必須要求しない。default で動作する。 | 環境変数 | 説明 | |---------|------| | `CLAUDE_SKILL_CO_EVOLVE_CHECK_DISABLE` | `1` で skill を silent disable (特定 project で動作させたくない場合) | | `CLAUDE_SKILL_CO_EVOLVE_CHECK_LANGUAGES` | 言語自動推定を csv で上書き (例: `ts,py`)。default は marker file から推定 | ## PR 作成 flow への統合 `/comment-sweep` と同型の pre-PR 発火点。[CLAUDE.md](../../../CLAUDE.md)「コミット / PR 運用」の autonomy 連鎖で `gh pr create` の**直前**に並走で起動する。`/extension-bloat-sweep` (直交軸: 既存実装の無理な拡張検出) と組み合わせて使うと sweep 範囲がカバーされる。 ## トラブルシューティング | 問題 | 対処 | |------|------| | 言語推定が誤判定する | `CLAUDE_SKILL_CO_EVOLVE_CHECK_LANGUAGES=ts,py` で上書き | | public API なのに finding が出る | public marker 推定が漏れている。`@deprecated` / `__all__` / `package.json exports` 等を確認 | | 内部 symbol なのに `Public marker: detected` で除外される | false negative。具体ケースを issue 化して script の除外条件を狭める | | `grep -rn` の reference 抽出が遅い | repo 規模に比例。`CLAUDE_SKILL_CO_EVOLVE_CHECK_DISABLE=1` で skill 自体を silent disable する選択肢 | | 命名 regex が agent シナリオを取り逃がす | `(V\d+|Old|New|Legacy|Compat|Deprecated)` 以外の suffix/prefix pattern を script 改修で追加 | | revert PR で偽 finding が出る | Step 1.5 の auto-skip が機能していない可能性。`git log --format=%s ..HEAD` で subject を確認 | | 本 workshop repo で動かない | repo root に `package.json` / `pyproject.toml` 等が無いため Step 1 で silent skip するのが正しい挙動 (stage worktree の demo app は marker file を持つので、そちらでは検出が走る) |