diff --git a/.claude/settings.local.json.template b/.claude/settings.local.json.template index 0e98ad0..49be4a5 100644 --- a/.claude/settings.local.json.template +++ b/.claude/settings.local.json.template @@ -52,6 +52,16 @@ "timeout": 30 } ] + }, + { + "matcher": "Write|Edit|Replace", + "hooks": [ + { + "type": "command", + "command": "\"{{PROJECT_DIR}}\\.claude\\hooks-post-tool-comment-lint-rust.exe\"", + "timeout": 10 + } + ] } ], "Stop": [ diff --git a/Cargo.lock b/Cargo.lock index 6fe0c17..845294d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,6 +23,16 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +[[package]] +name = "cc" +version = "1.2.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" +dependencies = [ + "find-msvc-tools", + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -103,6 +113,12 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + [[package]] name = "foldhash" version = "0.1.5" @@ -143,6 +159,17 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hooks-post-tool-comment-lint-rust" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "tempfile", + "tree-sitter", + "tree-sitter-rust", +] + [[package]] name = "hooks-post-tool-linter" version = "0.1.0" @@ -412,6 +439,12 @@ dependencies = [ "serde", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "syn" version = "2.0.117" @@ -477,6 +510,26 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" +[[package]] +name = "tree-sitter" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df7cc499ceadd4dcdf7ec6d4cbc34ece92c3fa07821e287aedecd4416c516dca" +dependencies = [ + "cc", + "regex", +] + +[[package]] +name = "tree-sitter-rust" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "277690f420bf90741dea984f3da038ace46c4fe6047cba57a66822226cde1c93" +dependencies = [ + "cc", + "tree-sitter", +] + [[package]] name = "unicode-ident" version = "1.0.24" diff --git a/Cargo.toml b/Cargo.toml index 604dfca..250dcf5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ members = [ "src/cli-pr-monitor", "src/cli-push-pipeline", "src/cli-push-runner", + "src/hooks-post-tool-comment-lint-rust", "src/hooks-post-tool-linter", "src/hooks-pre-tool-validate", "src/hooks-session-start", diff --git a/docs/pipeline-token-efficiency.md b/docs/pipeline-token-efficiency.md index e0ecb65..06862c8 100644 --- a/docs/pipeline-token-efficiency.md +++ b/docs/pipeline-token-efficiency.md @@ -159,91 +159,147 @@ reviewers (parallel: simplicity + security) → fix → (loop, threshold 2) → - explanatory output style mode が "include in conversation, not in code" と指示しているにも関わらず、私 (Claude) はコメントを書く習性がある - 簡潔に書かせる Stop hook / lint rule が **不在** +### 再評価 (2026-05-01: PR #98 セッション後) + +ユーザーフィードバック (PR #98 セッション末) により、本セクションの当初提案 (旧 #B-1〜#B-4) は **構造的に不適切** と判定し全面再編した。論点は以下: + +- **6 iter = worst path は「たまたま」ではなく構造的必然**: `非決定 reviewer × 非制約 fix × 短い loop threshold = 高確率で最大 path 到達` +- **LLM 検証器の追加では収束しない**: LLM review → LLM fix → LLM review の連鎖は完全性保証も再現性もない (ask-based の本質的限界) +- **解くべき問題の捉え直し**: 「iter を減らす」ではなく **「iter を不要にする」** = LLM 通過前に決定論層で止める + +旧 #B-1〜#B-4 はすべて「LLM 検証器を増設する案」で、批判は以下: + +| 旧案 | 批判 | 移行先 | +|---|---|---| +| #B-1 fix self-check | LLM self-check は信頼できない (検出漏れと同じ問題を内包) | 取り下げ → #B-β (制約付き fix、機械的指標 diff) | +| #B-2 reviewer exhaustive scan | recall は上がるが 100% 保証なし、deterministic lint の下位互換 | 取り下げ → #B-γ (reviewer 役割を異常検知に再定義) | +| #B-3 regex lint hook | regex で「意味」を取るのは危険 (Why/What 区別不能、多言語対応で破綻) | 取り下げ → #B-α (AST/トークンベース、コメント存在自体を禁止) | +| #B-4 coding-style.md 強化 | 「文化」であって「制御」ではない (ルール増 → 読まれない、モデル変わる → 崩れる) | **完全削除** (補助層としても採用しない) | + +新案 #B-α / #B-β / #B-γ で「決定論レイヤー → 制約付き修正 → 異常検知レビュアー」のアーキテクチャ 3 層を構築する。 + ### 改善案 -#### #B-1: fix step に "no new violations" self-check を追加 +#### #B-α: 決定論 comment lint hook (Rust 限定 PoC) -`fix.md` instruction の「Completion criteria」に以下を追加: +**設計思想**: regex で "What っぽい文章を検出" するのではなく、**コメントの存在自体を制約する** (例外マーカーのみ許可)。意味解析を回避することで言語非依存な shell を確保する。 -```markdown -- 変更後のコードが **他の criteria** (nesting depth ≤ 4、function length ≤ 50、等) を新たに violate していないことを Read で再確認する -- 特に新パターン (match / nested if let / closure 等) を導入する場合、nesting depth が増えていないか確認する -``` +**検出ロジック**: -**期待効果**: Run 2 タイプの「fix が nest 深度を悪化させる」waste を削減。1 PR あたり 1-2 iter 削減 (= 3-6 分)。 +- 原則: Rust ソース内のすべての comment (`//`, `/* */`, `///`) を count +- 例外マーカー (実装 `src/hooks-post-tool-comment-lint-rust/src/main.rs` の `ALLOWED_LINE_PREFIXES` / `ALLOWED_BLOCK_PREFIXES` 定数が **single source of truth**): + - **line comment**: `///` (rustdoc outer) / `//!` (rustdoc inner) / `// TODO:` / `// FIXME:` / `// SAFETY:` / `// NOTE:` / `// HACK:` / `// XXX:` + - **block comment**: `/**` (block rustdoc outer) / `/*!` (block rustdoc inner) +- 上記以外のコメントは **REJECT** (count > 0 で hook が block) +- マーカー追加・削除時は実装定数と本箇所を必ず同期させる (docs と実装の乖離は次フェーズの誤誘導要因) -**リスク**: fix step の instruction 肥大化。指示を読み飛ばされる可能性あり。 +**配置 (ADR-002 / ADR-006 / ADR-007 整合)**: -#### #B-2: simplicity-review に "exhaustive comment scan" instruction 追加 +- 新 crate `src/hooks-post-tool-comment-lint-rust/` (PoC は Rust 限定、将来 ts/py を独立 crate で並列追加) +- 既存の **PreToolUse hooks** (`hooks-pre-tool-validate.exe` 等) とは **別エントリ** として配置 (言語別 plugin の独立性確保、ユーザー指示) +- PostToolUse タイミング (Edit/Write 後) で発火、書かれた直後に block して即時修正させる +- ADR-002 (PostToolUse の Biome + oxlint 二段構成) には統合せず、独立 hook entry として並列 +- ADR-007 の **AST 層** に位置づけ (正規表現層ではない)。`tree-sitter` / `tree-sitter-rust` で `(line_comment)` / `(block_comment)` ノードを query で抽出 -`review-simplicity.md` の Judgment procedure に追加: +**期待効果**: -```markdown -1. Read `.takt/review-diff.txt` -2. **Diff 中の全 comment 行 (`//`, `/*`, `///`) を enumerate する** -3. 各 comment が Why / What/How / PR-reference のどれに該当するか分類 -4. What/How に該当するもの全てを finding に列挙 -5. その他の 6 criteria を順にチェック -``` +- AI が What/How コメントを書いた瞬間に hook が block → 修正させる +- takt 起動時にはコメント問題が解決済みのため、reviewer は ALL APPROVE に近い動作 +- 長期的に **6-iter run を 1-iter に構造的に圧縮** + +**リスク**: + +- 例外マーカーリストの保守 (新例外 `// LICENSE:` 等を追加するたびに list 更新) +- false positive (合理的な Why コメントが誤 block されると開発体験悪化) → 例外マーカー充実で回避 +- 派生プロジェクト展開時の言語拡張作業が **言語別に Effort M** ずつ加算 (本 PoC は Rust のみ) + +#### #B-β: 制約付き fix instruction -**期待効果**: Run 1 タイプの「Iter 1 検出漏れ」を抑制。1 PR あたり 1 iter 削減 (= 3-5 分)。 +**設計思想**: fix step の self-check を LLM 判断ではなく **機械的指標の diff 比較** に置き換える。指標増加で fix 自身がやり直しを self-trigger。 -**リスク**: false positive 増加 (PR 参照行を What と誤判定する等)。Why/What 分類は LLM 判断のままで本質的解決にならない可能性。 +**実装方法**: -#### #B-3: What/How コメント検出を **決定論的 lint hook** に移行 +- `.takt/facets/instructions/fix.md` の Completion criteria に追加: -`hooks-stop-quality.exe` または `hooks-post-tool-linter.exe` に regex-based 検出を追加。Stop hook で push 前に検出 → 私が書いた直後に修正させる (takt 起動前)。 + ```markdown + ## Pre-completion deterministic check -**検出パターン (案)**: -- `// .*?する/させる/実行` (動詞終止形を含む日本語コメント) -- `// (Sets?|Gets?|Updates?|Returns?) ` (英語 What 動詞) -- 上記コメントの直後に同じ動作を表す関数呼び出しがある場合 → flag + Compare metrics between pre-fix and post-fix file states for each modified file: + - max nesting depth (within modified function or change site) + - function length (lines) + - non-doc comment count (excluding `///`, `// TODO:`, `// SAFETY:` 等の例外マーカー) -**期待効果**: AI が書いた直後に検出されて修正されれば、takt simplicity-review は ALL APPROVE に近づく。長期的に **6-iter run を 1-2 iter に圧縮**。 + Run helper script: + + scripts/fix-metrics-check.ps1 + + If any metric increased, REJECT own fix and try alternative approach + (e.g., extract function, use early return, simplify match arm). + ``` + +- helper script `scripts/fix-metrics-check.ps1`: `rust-code-analysis` (Rust) を呼び出し metrics を JSON 出力 → diff 計算 → 増加 detected なら exit 非ゼロ +- `.takt/runs//fix-metrics.log` に記録 (audit 用) + +**期待効果**: + +- Run 2 タイプ (fix が新 violation を introduce) の **構造的排除** +- LLM 判断ではなく数値比較なので、attention drift や指示読み飛ばしの影響を受けない **リスク**: -- regex の精度: false positive 発生で開発体験悪化 -- 全言語 (Rust / TS / Python) 対応のメンテナンスコスト -- 「Why コメント」を誤検知する境界が曖昧 -- ADR-007 (custom-lint-rule の正規表現層/AST 層の線引き) との整合確認必要 -#### #B-4: AI 自身への事前防止 — `~/.claude/rules/common/coding-style.md` に明示 +- 「適切な refactor で一時的に行数増加」のケース判定 (関数分割で個別関数長は減るが modified function 範囲では増える) + → 対策: function 単位ではなく **diff 内の change site 周辺** に scope を絞る +- metric tool の Rust 限定 (PoC は `rust-code-analysis`、将来言語拡張で別 tool 評価) -既に「Default to writing no comments」ルールはあるが、簡潔さの問題で読み飛ばされている可能性。**強い表現で再記述**: +#### #B-γ: reviewer の役割を「検査」から「異常検知」へ -```markdown -## コメント絶対禁止パターン (即 REJECT) - -以下のパターンに該当するコメントは **絶対に書いてはならない** (simplicity-review が REJECT する): - -1. **直後の代入文・関数呼び出しを言葉で paraphrase するコメント** (What) - ```rust - // 成功時のみ更新する ← REJECT - state.value = Some(x); - ``` -2. **Type/match arm/early return が既に表現していることを再説明** (How) - ```rust - // Optional なので None の可能性がある ← REJECT - let x: Option = ...; - ``` - -書きたくなったら **代わりに識別子名で表現**せよ。 -``` +**設計思想**: 決定論層 (#B-α + #B-β) を通過した状態を前提に、reviewer の責務を **「lint で防げない高次違反のみ flag」** に再定義する。enumerate 義務を削除して attention drift 問題を解消。 -**期待効果**: AI 自身の writing pattern を fundamental に変える。**長期的には #B-1〜#B-3 が不要になる可能性**。 +**変更内容**: -**リスク**: ルール量が増えると AI が読み込まないリスク (現状 CLAUDE.md は既に大きい)。 +- `.takt/facets/instructions/review-simplicity.md` を書き換え: + - 旧: 「6 criteria を順次チェック + ALL findings を列挙」 + - 新: 「決定論層 (#B-α + #B-β) で防げない **異常パターンのみ flag**」 + - 例: 巨大な closure / 不自然な抽象化 / 命名 ambiguity / 設計上の concern + - 例外: comment count / nesting depth / function length は **lint が保証済み** として skip +- `.takt/facets/instructions/review-security.md` も同様の方針 (security lint で防げる範囲は skip、higher-order な脅威のみ flag) + +**期待効果**: + +- attention drift 問題が消滅 (検出対象が absolute に narrow に) +- 1 iter ALL APPROVE 率が **90% 超** に到達 (決定論層で大半が intercept されるため) +- review 所要時間も短縮 (現 baseline 1m 30s〜3m → 30s〜1m 期待) + +**リスク**: + +- deterministic 層 (#B-α / #B-β) の coverage gap で漏れた違反が reviewer もスルー → 二重 miss の可能性 + → 対策: reviewer の異常検知 instruction に「過度に narrow にしすぎない」ガイドラインを残す +- 「異常」の定義が LLM 主観で false positive が出る場合、決定論層 update で対応 (lint rule 追加) ### 採用判定 | 改善案 | ROI | 実装コスト | 推奨 | |---|---|---|---| -| #B-1 fix self-check | ★★★★ | XS (instruction 数行) | **即実施推奨** | -| #B-2 reviewer exhaustive scan | ★★★ | XS (instruction 数行) | 即実施推奨 | -| #B-4 coding-style.md 強化 | ★★★★ | S (rules 編集) | 即実施推奨 | -| #B-3 決定論的 lint hook | ★★★ | M-L (regex + ADR-007 整合) | dogfood 後 | - -**Bundle 案 (Bundle Z?)**: #B-1 + #B-2 + #B-4 を 1 PR で land 推奨。共通テーマは "What/How comment 防止の三層構造" (AI 自身 = #B-4 / reviewer = #B-2 / fix = #B-1)。effort 合計 XS+XS+S。 +| **#B-α 決定論 comment lint hook (Rust)** | ★★★★★ | M (新 crate + AST + hook 登録) | **Phase 1 (PoC)** | +| **#B-β 制約付き fix instruction** | ★★★★ | M (helper script + facet update) | **Phase 2** | +| **#B-γ reviewer 役割変更** | ★★★ | S (facet instruction 書き換え) | **Phase 3** | + +**Bundle 案 (Bundle Z 再編)**: アーキテクチャ 3 層を **3 Phase 分割** で順次実装し、各 Phase の dogfood で次 Phase 着手判断。 + +- **Phase 1 — #B-α (Rust 限定 PoC)**: + - 新 crate `src/hooks-post-tool-comment-lint-rust/` を Cargo workspace (ADR-026) に追加 + - `tree-sitter` / `tree-sitter-rust` で `(line_comment)` / `(block_comment)` ノードを抽出 + 例外マーカー判定 + - PostToolUse hook として独立配置 (既存 PreToolUse hooks とは別エントリ、ADR-006 整合) + - dogfood 1〜2 PR: 例外マーカー漏れ / false positive を観測 → list 拡充 +- **Phase 2 — #B-β (制約付き fix)**: + - `scripts/fix-metrics-check.ps1` + `rust-code-analysis` 統合 + - `.takt/facets/instructions/fix.md` に deterministic check ブロック追加 + - dogfood 1〜2 PR: 「適切 refactor 誤 reject」の頻度を観測 → scope 調整 +- **Phase 3 — #B-γ (reviewer 異常検知化)**: + - `review-simplicity.md` / `review-security.md` 書き換え + - dogfood 1〜2 PR: 二重 miss 発生率と 1-iter APPROVE 率を観測 → 本採用判断 + +**期待累積効果**: pre-push iter 分布 `{1×3, 3×2, 6×1}` (PR #97 ベースライン) → `{1×N}` (1-iter ALL APPROVE 構造化)。outlier 率 1/6 (16.7%) → 0% 達成試算。 --- @@ -276,7 +332,7 @@ analyze → fix → analyze (loop, threshold 2) → supervise → fix_supervisor | 14:38 | 1 | 1m 22s | approved | | 15:30 | 1 | 2m 0s | approved | -**8 runs 中 7 runs が 1-iter で完了**。outlier 1 件のみ (12.5%)。pre-push-review の outlier 率 (33%) と比べて健全。 +**8 runs 中 7 runs が 1-iter で完了**。outlier 1 件のみ (12.5%)。pre-push-review の outlier 率 (1/6 = 16.7%) と比べて同水準だが、3-iter 中央値は post-pr-review の方が低い。 #### 3-iter outlier の中身 (PR #96, 10m 19s) @@ -552,7 +608,7 @@ $ check-ci-coderabbit.exe --list-findings --pr 97 **期待累積効果 (Bundle Y2 + Z2 統合)**: - #A-1 + #C-1 (haiku 化): session あたり 15-20 分削減 + token 大削減 - #D-1 + #D-4 (gh + 応答ルール): cache_creation **3-4M tokens 削減** (全体の 25-30%) -- Bundle Z (#B-1 + #B-2 + #B-4): pre-push iter 数削減で間接的に大効果 +- Bundle Z 再編 (#B-α + #B-β + #B-γ、3 Phase 分割): pre-push iter 数を **1-iter 固定** に構造化 (outlier 率 1/6 (16.7%) → 0% 試算) - **合計**: rate-limit 90% 消費が 60-70% に下がる試算 (要 dogfood 確認) --- @@ -566,7 +622,7 @@ $ check-ci-coderabbit.exe --list-findings --pr 97 | Bundle | 内容 | effort | 即効性 | |---|---|---|---| | **Bundle Y2** | #A-1 + #C-1 (analyze facets を haiku 化、aggregate/fix/supervise は sonnet 維持) | XS (yaml 4 行) | 最即効 | -| **Bundle Z** | #B-1 + #B-2 + #B-4 (What/How comment 防止の 3 層: AI 自身 / reviewer / fix) | S (instruction + rules) | 中期 | +| **Bundle Z (再編)** | #B-α + #B-β + #B-γ (アーキテクチャ 3 層: 決定論 comment lint / 制約付き fix / 異常検知 reviewer)、Phase 1〜3 で順次 dogfood | M+M+S (3 Phase 分割) | 段階的 (Phase 1 から) | | **Bundle Z2** | #D-1 + #D-4 (gh CLI 使用規則 + Claude 応答スタイル簡素化 rules) | S (rules 追記) | 即効 | ### 期待効果 (Bundle 別) @@ -574,7 +630,7 @@ $ check-ci-coderabbit.exe --list-findings --pr 97 | Bundle | 削減対象 | 想定削減量 | 検証指標 | |---|---|---|---| | Y2 | analyze step の sonnet 利用 | session あたり 15-20 分 + sonnet → haiku で **当該 step の token cost 1/3** | post-pr-review / post-merge-feedback の avg time、当該 facets の billable input tokens | -| Z | pre-push-review iter 数 | outlier 6-iter run を 2-3 iter に圧縮、avg iter 2.5 → 1.5 期待 | pre-push-review iter 分布、6-iter outlier 発生率 | +| Z (再編) | pre-push-review iter 数を構造的に固定化 | **outlier 0% 達成 + 1-iter ALL APPROVE 90% 超** (旧推定: avg iter 2.5 → 1.5 だったが、決定論層導入で 1-iter 固定が target に格上げ) | pre-push-review iter 分布 (`{1×N}` 集中度)、6-iter outlier 発生率 (target 0%)、1-iter ALL APPROVE 率 | | Z2 | gh CLI noise + text-only response | cache_creation **3-4M tokens 削減** (現在 13.6M の 25-30%) | gh tool_result avg/max chars、text-only turn の cache_creation 占有率 | ### 統合効果試算 @@ -659,8 +715,8 @@ done #### ④ pre-push-review iter 分布比較 (#B 効果検証) -ベースライン: `{1×3, 3×2, 6×1}` = 6 runs (うち 6-iter outlier 1 件) -目標: `{1×N, 2×N}` で 6-iter outlier 消失、avg iter 2.5 → 1.5 期待 +ベースライン: `{1×3, 3×2, 6×1}` = 6 runs (うち 6-iter outlier 1 件、avg iter 2.5) +目標 (Bundle Z 再編後): `{1×N}` (1-iter 固定) で 6-iter outlier 消失 (outlier 率 1/6 (16.7%) → 0%)、avg iter 2.5 → 1.0、1-iter ALL APPROVE 率 90% 超 (L302 / L633 と統一) #### ⑤ サンプル CR review listing token 量比較 (#D-4 効果検証) @@ -700,10 +756,13 @@ docs/pipeline-token-efficiency.md の「全体統合: Bundle 群の累積効果 | #A-1 analyze facets haiku 化 | 採用済 (Bundle Y2) | 2026-05-01 | #98 | | #A-2 trivial PR skip | 計画 | - | - | | #A-3 transcript filter | 計画 | - | - | -| #B-1 fix self-check | 計画 | - | - | -| #B-2 reviewer exhaustive scan | 計画 | - | - | -| #B-3 決定論的 lint hook | 検討 | - | - | -| #B-4 coding-style.md 強化 | 計画 | - | - | +| ~~#B-1 fix self-check~~ | 取り下げ (Bundle Z 再編 → #B-β) | 2026-05-01 | — | +| ~~#B-2 reviewer exhaustive scan~~ | 取り下げ (Bundle Z 再編 → #B-γ) | 2026-05-01 | — | +| ~~#B-3 決定論的 lint hook~~ | 取り下げ (Bundle Z 再編 → #B-α) | 2026-05-01 | — | +| ~~#B-4 coding-style.md 強化~~ | 完全削除 (Bundle Z 再編、補助層としても不採用) | 2026-05-01 | — | +| #B-α 決定論 comment lint hook (Rust 限定 PoC) | 採用済 (Bundle Z Phase 1) | 2026-05-02 | #99 | +| #B-β 制約付き fix instruction | 計画 (Bundle Z Phase 2) | 2026-05-01 | - | +| #B-γ reviewer 役割変更 (異常検知化) | 計画 (Bundle Z Phase 3) | 2026-05-01 | - | | #C-1 analyze haiku 化 | 採用済 (Bundle Y2) | 2026-05-01 | #98 | | #C-2 Iter 3 短絡 | 検討 | - | - | | #C-3 rate-limit skip | 計画 | - | - | diff --git a/docs/todo.md b/docs/todo.md index e517764..44db56a 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -52,6 +52,9 @@ | 36 | 🔧 Tier 2 | **cargo-mutants を post-PR pipeline に統合 — test ⇄ impl 制約の機械測定 (PR #96 T2-flaky) ★ Bundle X** | todo4.md | M | Bundle W land 後推奨 (PBT properties の後付け検証層、変更 crate + 1-hop 依存 scope) | | 37 | 🔧 Tier 2 | **pre-push concurrency stress runner (N=100) — scheduling space の random sampling (PR #96 T2-flaky) ★ Bundle X** | todo4.md | S | 順位 36 と同 PR (Bundle X、cli-push-runner に +~1 秒 step 追加) | | 38 | 💎 Tier 3 | **L3 weekly: cargo-mutants workspace 全体 + stress N=1000 を ADR-031 週次レビューに統合 (PR #96 T3-flaky)** | todo4.md | S | ADR-031 Phase B (順位 8) と同 bundle 化推奨、long-tail flake と coverage 全体監査 | +| 39 | 🚀 Tier 1 | **takt workflow `model` フィールド必須化 lint rule (PR #98 T1-1)** | todo4.md | S | なし (Bundle Y2 完全性: post-pr-review.yaml supervise step の `model:` 欠落を契機に決定論的防止層を追加) | +| 40 | 🚀 Tier 1 | **prepare-pr skill Step 1 bookmark 存在チェック強化 (PR #98 T1-2)** | todo4.md | XS | なし (本セッション再現の push 失敗を Step 1 fallback で早期検出。skill repo 側更新) | +| 41 | 🔧 Tier 2 | **Bundle Y2 効果の定量計測 — post-merge-feedback / post-pr-review の avg time 比較 (PR #98 T2-2)** | todo4.md | M | なし (PR #97 sonnet baseline vs PR #98 以降 haiku の実測比較。Bundle Z / Z2 の ROI 判断材料、PR #98 merge 後 3-5 PR の観察ベース) | **戦略**: Tier 1 を 2〜3 セッションで片付け → Tier 2 で ADR-032 の前提 + rate-limit + convergence cost 削減を進める → Tier 3 で ADR-032 を land + ドキュメント整備。Tier 4-5 は cleanup / 外部展開で daily efficiency への直接効果は小さい。 @@ -68,6 +71,7 @@ **Bundle V (verdict path 整合性 + docs-only fast-approve gap 補強) は PR #95 post-merge-feedback 直接対策**。review-security.md の `.takt/**` / `.claude/**` 除外、docs/todo.md ヘッダ整合、code-review.md の 3 点チェックリスト追記の 3 件を 1 PR で land 推奨。共通テーマは「PR #95 で実証された不整合パターンへの retroactive 対策」、effort 合計 XS×3 で完結。 **Bundle W (PBT + 型強化) は PR #96 で実証された flaky 実装防御の最上層**。Finding D (`saturating_sub` の silent semantic mismatch) と E (concurrency test の guard 即 drop) はどちらも「Rust 的に正しいコードがドメイン的に間違う」典型例で、advisor + takt-fix の 2 layer も貫通した。**仕様を proptest properties で明文化 + `PastTime` 等の型で invalid state を unrepresentable に** することで、ルール (ask-based) では塞げない bug class を構造的に排除する。**rate-limit 自動検出 (Phase 4 で land 済) / takt REJECT-ESCALATE を先行**し、その後 Bundle W に着手する流れがユーザー指示。 **Bundle X (cargo-mutants + stress runner) は Bundle W の後付け検証層**。L2 post-PR で変更 crate + 1-hop 依存に cargo-mutants を走らせ test の弱さを直接測定、L1 pre-push で concurrency stress N=100 を回し scheduling race を sampling。Bundle W で書いた spec / 型を後段で機械的に検証する補完関係。**L3 weekly cargo-mutants workspace 全体 + stress N=1000 は ADR-031 Phase B 週次レビューと bundle 化** することで long-tail flake と coverage 全体監査を week 単位で audit する layer に統合。 +**PR #98 (Bundle Y2) post-merge-feedback 反映 (2026-05-01)**: 3 件の follow-up task を追加。**takt workflow `model` フィールド必須化 lint rule** と **Bundle Y2 効果の定量計測** は Bundle Y2 完全性確保 + ROI 検証で同系列 (lint rule 着手時に post-pr-review.yaml supervise step への `model: sonnet` 明示追加を同 PR に含める想定)。**prepare-pr skill Step 1 bookmark 存在チェック強化** は本セッション運用痛 (bookmark 未作成 push 失敗) から派生した独立 task で skill repository 側の更新となる。 --- diff --git a/docs/todo4.md b/docs/todo4.md index 0eaaea2..435cd98 100644 --- a/docs/todo4.md +++ b/docs/todo4.md @@ -259,3 +259,125 @@ #### 詰まっている箇所 - ADR-031 Phase B の実装計画 (順位 8) が未着手のため、本 task の inception は順位 8 の進捗に依存。 + +--- + +### takt workflow `model` フィールド必須化 lint rule (PR #98 T1-1) + +> **動機**: Bundle Y2 (PR #98) で post-pr-review.yaml の analyze step に `model: haiku` を明示追加した結果、post-merge-feedback で同 yaml の `supervise` step (line 106-124) に `model:` フィールドが未指定であることが指摘された。`persona:` を持つ step で `model:` 未指定は default `sonnet` に落ちるため、Bundle Y2 ゴール (analyze 系 haiku / supervise・fix は sonnet 維持) では現時点で偶然合致しているが、将来 default 変更や persona 追加で意図せぬモデル選択が混入しうる。 +> +> **本タスクの位置づけ**: Bundle Y2 完全性 follow-up + 決定論的防止層の追加。`persona:` を持つ step に `model:` がないパターンを `.claude/custom-lint-rules.toml` の正規表現 lint rule として検出する。ADR-007 (custom-lint-rule の正規表現層 / AST 層線引き) の正規表現層に該当。 +> +> **参照**: `.claude/feedback-reports/98.md` Tier 1 #1 +> +> **実行優先度**: 🚀 **Tier 1** — Effort Small。yaml 設定変更のみで lint rule 追加可能。Bundle Y2 の完全性 (post-pr-review.yaml supervise step への `model: sonnet` 明示追加) も同 PR で land する想定。 + +#### 設計決定 (案) + +- **配置先**: `.claude/custom-lint-rules.toml` の新規 rule entry +- **検出ロジック (正規表現案)**: yaml ファイル内で `persona:` 行を見つけ、その同 step block 内に `model:` がない場合を検出する。yaml の階層を厳密に解析する場合は ADR-007 の AST 層昇格を検討 (Tree-sitter / yaml-rust) +- **適用対象**: `.takt/workflows/*.yaml` のみ (extensions: ["yaml"] + path filter) +- **副次作業**: post-pr-review.yaml supervise step に `model: sonnet` を明示追加 (Bundle Y2 完全性)。lint rule 導入と同 commit で実施することで、新 rule が clean baseline を保つ +- **rule 名 (案)**: `takt-workflow-persona-without-model` + +#### 作業計画 + +- [ ] 既存 `.claude/custom-lint-rules.toml` の構造を確認 +- [ ] 正規表現 + path filter を新 rule として記述 +- [ ] PostToolUse hook の lint runner で post-pr-review.yaml supervise step が検出されることを確認 +- [ ] post-pr-review.yaml supervise step に `model: sonnet` を明示追加 (Bundle Y2 完全性) +- [ ] pre-push-review.yaml / post-merge-feedback.yaml も全 step に `model:` が揃っているか確認 +- [ ] `pnpm deploy:hooks` で派生プロジェクトに rule を配布 +- [ ] 本 todo4.md エントリを削除 + +#### 完了基準 + +- `.claude/custom-lint-rules.toml` に新 rule が追加され `.takt/workflows/*.yaml` 全 step で `persona:` ⇔ `model:` 対応が確立 +- post-pr-review.yaml supervise step に `model: sonnet` 明示追加 (Bundle Y2 完全性確保) +- lint rule が将来の workflow 編集時に欠落を検出可能 + +#### 詰まっている箇所 + +- yaml の階層構造を正規表現のみで完全表現するのは難しい。実 workflow ファイルで false positive がないか着手時に確認。多発する場合は ADR-007 の AST 層昇格判断。 + +--- + +### prepare-pr skill Step 1 bookmark 存在チェック強化 (PR #98 T1-2) + +> **動機**: PR #98 セッションで、Bundle Y2 commit の `jj describe` 後の `pnpm push` がローカル bookmark 未作成のまま実行され、`jj git push` の default revset (`remote_bookmarks(remote=origin)..@`) で対象 0 件 → "Nothing changed" warning となり実質 push 失敗。push-runner は bookmark 自動採番ロジックを持たず、prepare-pr skill の Step 1 fallback (bookmark `/` 自動採番) でリカバリしたが、Step 1 の state 確認コマンド一覧に `jj bookmark list` の output 確認が明示されておらず、検出が「Step 1 fallback 表の `local_bookmarks` 空判定」に依存していた。 +> +> **本タスクの位置づけ**: prepare-pr skill Step 1 の state 確認フローに bookmark 存在チェックを明示追加し、push 失敗を事前検出。skill 自体は global (`~/.claude/skills/prepare-pr/`) なので本リポジトリの patch ではなく skill repository (`E:\work\claude-code-skills`) で更新する。 +> +> **参照**: `.claude/feedback-reports/98.md` Tier 1 #2 +> +> **実行優先度**: 🚀 **Tier 1** — Effort XS。SKILL.md Step 1 に確認コマンド 1 行 + fallback 表への明示マッピング追加のみ。 + +#### 設計決定 (案) + +- **追加場所**: `~/.claude/skills/prepare-pr/SKILL.md` Step 1 「現状確認 + 前提工程 fallback」セクション +- **追加内容**: state コマンド一覧に `jj bookmark list 2>&1 | head -20` を追加し、output に `:` 行が含まれない場合を fallback 表「local bookmark なし」行に明示マッピング +- **既存 fallback 表との関係**: `local_bookmarks` template での判定は引き続き primary signal。本タスクは「読み手 (Claude / 人間) の state 確認 step で見落とさない」ための明示化 +- **evals 補強**: 「bookmark 未作成 → fallback で bookmark 作成 → push 成功」の Scenario を `evals/evals.json` に追加 (feedback-report Tier 2 #1 相当、同 PR で land 推奨) + +#### 作業計画 + +- [ ] `E:\work\claude-code-skills\prepare-pr\SKILL.md` の Step 1 を編集 (state コマンド + fallback 表強化) +- [ ] `~/.claude/skills/prepare-pr/SKILL.md` に sync (claude-code-skills repo の deploy 経路に従う) +- [ ] `~/.claude/skills/prepare-pr/evals/evals.json` に新 Scenario 追加 (bookmark 未作成正常 path) +- [ ] 本 todo4.md エントリを削除 + +#### 完了基準 + +- prepare-pr skill Step 1 の state 確認コマンドに bookmark 存在チェックが明示 +- 新 Scenario が evals.json に追加され、bookmark 未作成 fallback の正常動作が検証される +- 本セッション類似の push 失敗が再現した場合、Step 1 で fallback 実行が即時発火 + +#### 詰まっている箇所 + +- skill repository (`E:\work\claude-code-skills`) の deploy / sync 経路の確認が必要 (本リポジトリの `deploy:hooks` とは別経路)。 + +--- + +### Bundle Y2 効果の定量計測 — post-merge-feedback / post-pr-review の avg time 比較 (PR #98 T2-2) + +> **動機**: Bundle Y2 (PR #98) で analyze 系 step を sonnet → haiku に変更したが、ROI 根拠は `docs/pipeline-token-efficiency.md` の推定値 (PR #78 dogfood 12m13s → 並列化想定 7m30s) のみ。PR #98 セッション内観測 (post-pr-review takt 1m 13s / post-merge-feedback 8m 9s) は単発データで baseline (PR #97 セッション、avg 8.9 分) との比較が systematic にドキュメント化されていない。Bundle Z (#B-*) / Bundle Z2 (#D-*) の ROI 判断材料として PR #97 (sonnet baseline) vs PR #98 以降 (haiku) の実測比較を 3-5 PR 分集計し記録する。 +> +> **本タスクの位置づけ**: Bundle Y2 効果検証層。`docs/pipeline-token-efficiency.md` の「検証方法」セクション (① jsonl セッションメトリクス + ② takt run meta.json 集計) を実行し、結果を計画書末尾に「実測検証データ」セクションとして追記。想定削減量達成時は計画書の Bundle Y2 セクションを retire し ADR 化判断の材料とする (計画書ヘッダー L5 方針)。 +> +> **参照**: `.claude/feedback-reports/98.md` Tier 2 #2、`docs/pipeline-token-efficiency.md` 「検証方法」セクション +> +> **実行優先度**: 🔧 **Tier 2** — Effort Medium。3-5 PR の merge 経過後のデータ集計タスクで、即時着手ではなく観察ベース。Bundle Z / Z2 着手前のベースライン整理として有用。 + +#### 設計決定 (案) + +- **計測対象**: + - takt パイプライン別 avg time (post-merge-feedback / post-pr-review / pre-push-review) + - 一意 cache_creation tokens (jsonl usage 集計) + - 該当 step の billable input token 削減幅 (haiku は sonnet の約 1/3 cost 想定) +- **比較期間**: + - baseline: PR #97 セッション (2026-04-30 〜 2026-05-01 JST) — `docs/pipeline-token-efficiency.md` の「観測データ」セクション既値 + - 計測期間: PR #98 merge 後 3-5 PR (Bundle Z / Z2 着手前まで) +- **記録先**: + - `docs/pipeline-token-efficiency.md` 末尾に「実測検証データ」セクションを追加 (計画書が retire される前の最終 update) + - 想定削減量の 70% 以上達成 → 計画書の Bundle Y2 セクション削除 → ADR 化判断材料、未達 → 原因分析を計画書末尾に追記し追加 Bundle 提案 + +#### 作業計画 + +- [ ] PR #98 merge 後 3-5 PR 経過するまで観察 (本タスクの inception は条件待ち) +- [ ] 検証方法 ① (jsonl セッションメトリクス集計) を実行 +- [ ] 検証方法 ② (takt run meta.json 集計) を実行 +- [ ] baseline (PR #97) と比較し削減幅を表に記録 +- [ ] 想定削減量 (session あたり 15-20 分削減) との乖離を分析 +- [ ] 結果を `docs/pipeline-token-efficiency.md` 末尾に追記 +- [ ] 想定削減量達成判定に基づき計画書 retire / 追加 Bundle 提案 +- [ ] 本 todo4.md エントリを削除 + +#### 完了基準 + +- PR #98 merge 後 3-5 PR の実測値が `docs/pipeline-token-efficiency.md` に記録される +- baseline (PR #97) との削減幅が Bundle Y2 の想定削減量と比較され、達成 / 未達の判定がある +- Bundle Z / Z2 の ROI 判断材料として活用可能なデータが揃う + +#### 詰まっている箇所 + +- 計測期間 3-5 PR の間に rate-limit 不安定期 / 大規模変更 PR / docs-only PR が混在すると平均値の比較ノイズが大きい。中央値での比較や PR 性質による normalization 方式を着手時に検討。 diff --git a/package.json b/package.json index f944d45..c33d085 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "build": "npx tsc --noEmit --pretty || true", "build:hooks-pre-tool-validate": "cargo build --release -p hooks-pre-tool-validate && cp target/release/hooks-pre-tool-validate.exe .claude/hooks-pre-tool-validate.exe", "build:hooks-post-tool-linter": "cargo build --release -p hooks-post-tool-linter && cp target/release/hooks-post-tool-linter.exe .claude/hooks-post-tool-linter.exe", + "build:hooks-post-tool-comment-lint-rust": "cargo build --release -p hooks-post-tool-comment-lint-rust && cp target/release/hooks-post-tool-comment-lint-rust.exe .claude/hooks-post-tool-comment-lint-rust.exe", "build:hooks-stop-quality": "cargo build --release -p hooks-stop-quality && cp target/release/hooks-stop-quality.exe .claude/hooks-stop-quality.exe", "build:hooks-stop-feedback-dispatch": "cargo build --release -p hooks-stop-feedback-dispatch && cp target/release/hooks-stop-feedback-dispatch.exe .claude/hooks-stop-feedback-dispatch.exe", "build:hooks-user-prompt-feedback-recovery": "cargo build --release -p hooks-user-prompt-feedback-recovery && cp target/release/hooks-user-prompt-feedback-recovery.exe .claude/hooks-user-prompt-feedback-recovery.exe", @@ -19,7 +20,7 @@ "build:hooks-session-start": "cargo build --release -p hooks-session-start && cp target/release/hooks-session-start.exe .claude/hooks-session-start.exe", "build:cli-merge-pipeline": "cargo build --release -p cli-merge-pipeline && cp target/release/cli-merge-pipeline.exe .claude/cli-merge-pipeline.exe", "build:hooks-settings": "node -e \"const fs=require('fs');const t=fs.readFileSync('.claude/settings.local.json.template','utf8');const p=process.cwd().replace(/\\\\/g,'\\\\\\\\');fs.writeFileSync('.claude/settings.local.json',t.replace(/\\{\\{PROJECT_DIR\\}\\}/g,p))\" && echo settings.local.json generated", - "build:all": "pnpm build:hooks-session-start && pnpm build:hooks-pre-tool-validate && pnpm build:hooks-post-tool-linter && pnpm build:hooks-stop-quality && pnpm build:hooks-stop-feedback-dispatch && pnpm build:hooks-user-prompt-feedback-recovery && pnpm build:cli-push-runner && pnpm build:cli-pr-monitor && pnpm build:cli-merge-pipeline && pnpm build:check-ci-coderabbit && pnpm build:hooks-settings", + "build:all": "pnpm build:hooks-session-start && pnpm build:hooks-pre-tool-validate && pnpm build:hooks-post-tool-linter && pnpm build:hooks-post-tool-comment-lint-rust && pnpm build:hooks-stop-quality && pnpm build:hooks-stop-feedback-dispatch && pnpm build:hooks-user-prompt-feedback-recovery && pnpm build:cli-push-runner && pnpm build:cli-pr-monitor && pnpm build:cli-merge-pipeline && pnpm build:check-ci-coderabbit && pnpm build:hooks-settings", "push": ".\\.claude\\cli-push-runner.exe && .\\.claude\\cli-pr-monitor.exe --monitor-only", "create-pr": ".\\.claude\\cli-pr-monitor.exe", "observe-pr": ".\\.claude\\cli-pr-monitor.exe --observe", diff --git a/scripts/deploy-hooks.ts b/scripts/deploy-hooks.ts index 73143fb..2aba07a 100644 --- a/scripts/deploy-hooks.ts +++ b/scripts/deploy-hooks.ts @@ -22,6 +22,7 @@ const CLAUDE_DIR = join(ROOT, ".claude"); const EXE_FILES = [ "hooks-pre-tool-validate.exe", "hooks-post-tool-linter.exe", + "hooks-post-tool-comment-lint-rust.exe", "hooks-stop-quality.exe", "hooks-stop-feedback-dispatch.exe", "hooks-user-prompt-feedback-recovery.exe", diff --git a/src/hooks-post-tool-comment-lint-rust/Cargo.toml b/src/hooks-post-tool-comment-lint-rust/Cargo.toml new file mode 100644 index 0000000..2eb836e --- /dev/null +++ b/src/hooks-post-tool-comment-lint-rust/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "hooks-post-tool-comment-lint-rust" +version = "0.1.0" +edition = "2021" + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +tree-sitter = "0.22" +tree-sitter-rust = "0.21" + +[dev-dependencies] +tempfile = "3" + +# [profile.release] は workspace root (Cargo.toml) に集約 (ADR-026) diff --git a/src/hooks-post-tool-comment-lint-rust/src/main.rs b/src/hooks-post-tool-comment-lint-rust/src/main.rs new file mode 100644 index 0000000..a0644ff --- /dev/null +++ b/src/hooks-post-tool-comment-lint-rust/src/main.rs @@ -0,0 +1,441 @@ +//! PostToolUse comment lint hook (Rust 限定 PoC) +//! +//! Bundle Z Phase 1 (#B-α): 決定論 comment lint hook。 +//! Rust ファイルに対してコメント存在自体を制約し、例外マーカーのみ許可する。 +//! "Why コメント" "What コメント" の意味的区別は試みず、構造的に防止する。 +//! +//! ADR 整合: +//! - ADR-001: Rust 実装 +//! - ADR-002: PostToolUse の Biome+oxlint 二段構成とは独立 entry として配置 +//! - ADR-007: AST 層 (tree-sitter)、正規表現層ではない +//! - ADR-026: Cargo workspace member +//! +//! 例外マーカー一覧は `ALLOWED_LINE_PREFIXES` / `ALLOWED_BLOCK_PREFIXES` 参照。 + +use serde::{Deserialize, Serialize}; +use std::io::{self, Read}; +use std::path::Path; +use tree_sitter::{Parser, Query, QueryCursor}; + +#[derive(Deserialize)] +struct HookInput { + tool_input: Option, +} + +#[derive(Deserialize)] +struct ToolInput { + file_path: Option, + path: Option, +} + +#[derive(Serialize)] +struct HookOutput { + #[serde(rename = "hookSpecificOutput")] + hook_specific_output: HookSpecificOutput, +} + +#[derive(Serialize)] +struct HookSpecificOutput { + #[serde(rename = "hookEventName")] + hook_event_name: String, + #[serde(rename = "additionalContext")] + additional_context: String, +} + +#[derive(Serialize)] +struct LintViolation { + r#type: String, + severity: String, + location: ViolationLocation, + message: String, + why: String, + fix: ViolationFix, + example: ViolationExample, +} + +#[derive(Serialize)] +struct ViolationLocation { + file: String, + line: usize, + symbol: String, +} + +#[derive(Serialize)] +struct ViolationFix { + strategy: String, + steps: Vec, +} + +#[derive(Serialize)] +struct ViolationExample { + bad: String, + good: String, +} + +const MAX_VIOLATIONS: usize = 20; + +/// 例外マーカー (line_comment): 行頭スペース除去後にこれらのいずれかで始まれば許可 +const ALLOWED_LINE_PREFIXES: &[&str] = &[ + "///", + "//!", + "// TODO:", + "// FIXME:", + "// SAFETY:", + "// NOTE:", + "// HACK:", + "// XXX:", +]; + +/// 例外マーカー (block_comment): rustdoc 形式のみ許可 +const ALLOWED_BLOCK_PREFIXES: &[&str] = &["/**", "/*!"]; + +fn is_allowed_comment(comment_text: &str) -> bool { + let trimmed = comment_text.trim_start(); + if trimmed.starts_with("//") { + ALLOWED_LINE_PREFIXES + .iter() + .any(|prefix| trimmed.starts_with(prefix)) + } else if trimmed.starts_with("/*") { + ALLOWED_BLOCK_PREFIXES + .iter() + .any(|prefix| trimmed.starts_with(prefix)) + } else { + false + } +} + +fn find_violations(file_path: &str, source: &str) -> Vec { + let mut parser = Parser::new(); + let language = tree_sitter_rust::language(); + if parser.set_language(&language).is_err() { + return Vec::new(); + } + + let tree = match parser.parse(source, None) { + Some(t) => t, + None => return Vec::new(), + }; + + let query_source = "[(line_comment) (block_comment)] @comment"; + let query = match Query::new(&language, query_source) { + Ok(q) => q, + Err(_) => return Vec::new(), + }; + + let mut cursor = QueryCursor::new(); + let mut violations = Vec::new(); + let source_bytes = source.as_bytes(); + + let matches = cursor.matches(&query, tree.root_node(), source_bytes); + 'outer: for m in matches { + for capture in m.captures { + let node = capture.node; + let comment_text = match node.utf8_text(source_bytes) { + Ok(t) => t, + Err(_) => continue, + }; + + if is_allowed_comment(comment_text) { + continue; + } + + let start = node.start_position(); + let snippet = comment_text + .lines() + .next() + .unwrap_or(comment_text) + .to_string(); + + violations.push(LintViolation { + r#type: "RUST_COMMENT_FORBIDDEN".to_string(), + severity: "error".to_string(), + location: ViolationLocation { + file: file_path.to_string(), + line: start.row + 1, + symbol: snippet, + }, + message: "非 doc コメントは禁止です (Bundle Z #B-α)".to_string(), + why: "コメントの存在自体を制約する決定論層 (Bundle Z #B-α)。\ + コメントを書きたくなったら識別子名 / 関数分割で意図を表現すること。" + .to_string(), + fix: ViolationFix { + strategy: "コメントを削除し、識別子名や関数分割で意図を表現".to_string(), + steps: vec![ + "コメントを削除する".to_string(), + "(必要なら) 関数を分割して名前で意図を伝える".to_string(), + "(必要なら) 変数名を意図を表す名前にリネーム".to_string(), + "Why コメントが本当に必要なら // SAFETY: / // NOTE: 等のマーカー付きで書き直す".to_string(), + ], + }, + example: ViolationExample { + bad: "// 成功時のみ更新する\nstate.value = Some(x);".to_string(), + good: "if let Ok(updated) = result { state.value = Some(updated); }" + .to_string(), + }, + }); + + if violations.len() >= MAX_VIOLATIONS { + break 'outer; + } + } + } + + violations +} + +fn emit_feedback(message: &str) { + let output = HookOutput { + hook_specific_output: HookSpecificOutput { + hook_event_name: "PostToolUse".to_string(), + additional_context: message.to_string(), + }, + }; + if let Ok(json) = serde_json::to_string(&output) { + println!("{}", json); + } +} + +fn is_rust_file(file_path: &str) -> bool { + Path::new(file_path) + .extension() + .and_then(|e| e.to_str()) + .map(|e| e.eq_ignore_ascii_case("rs")) + .unwrap_or(false) +} + +fn main() { + let mut input = String::new(); + if io::stdin().read_to_string(&mut input).is_err() { + return; + } + + let hook_input: HookInput = match serde_json::from_str(&input) { + Ok(v) => v, + Err(_) => return, + }; + + let file_path = hook_input + .tool_input + .and_then(|t| t.file_path.filter(|s| !s.is_empty()).or(t.path)) + .unwrap_or_default(); + + if file_path.is_empty() || !is_rust_file(&file_path) { + return; + } + + let source = match std::fs::read_to_string(&file_path) { + Ok(c) => c, + Err(_) => return, + }; + + let violations = find_violations(&file_path, &source); + if violations.is_empty() { + return; + } + + let serialized: Vec = violations + .iter() + .filter_map(|v| serde_json::to_string(v).ok()) + .collect(); + + let feedback = format!( + "[comment-lint-rust] {} violation(s) found:\n{}", + serialized.len(), + serialized.join("\n") + ); + emit_feedback(&feedback); +} + +#[cfg(test)] +mod tests { + use super::*; + + fn lint(source: &str) -> Vec { + find_violations("test.rs", source) + } + + #[test] + fn empty_file_no_violations() { + let violations = lint(""); + assert!(violations.is_empty()); + } + + #[test] + fn no_comments_no_violations() { + let source = "fn main() {\n let x = 1;\n}\n"; + let violations = lint(source); + assert!(violations.is_empty()); + } + + #[test] + fn line_comment_detected() { + let source = "fn main() {\n // 値を更新する\n let x = 1;\n}\n"; + let violations = lint(source); + assert_eq!(violations.len(), 1); + assert_eq!(violations[0].location.line, 2); + } + + #[test] + fn block_comment_detected() { + let source = "fn main() {\n /* これは説明 */\n let x = 1;\n}\n"; + let violations = lint(source); + assert_eq!(violations.len(), 1); + } + + #[test] + fn rustdoc_outer_allowed() { + let source = "/// Public doc\nfn foo() {}\n"; + let violations = lint(source); + assert!(violations.is_empty()); + } + + #[test] + fn rustdoc_inner_allowed() { + let source = "//! Module doc\n"; + let violations = lint(source); + assert!(violations.is_empty()); + } + + #[test] + fn block_rustdoc_outer_allowed() { + let source = "/** Public doc */\nfn foo() {}\n"; + let violations = lint(source); + assert!(violations.is_empty()); + } + + #[test] + fn block_rustdoc_inner_allowed() { + let source = "/*! Module doc */\n"; + let violations = lint(source); + assert!(violations.is_empty()); + } + + #[test] + fn todo_marker_allowed() { + let source = "fn main() {\n // TODO: implement later\n}\n"; + let violations = lint(source); + assert!(violations.is_empty()); + } + + #[test] + fn fixme_marker_allowed() { + let source = "// FIXME: race condition\nfn main() {}\n"; + let violations = lint(source); + assert!(violations.is_empty()); + } + + #[test] + fn safety_marker_allowed() { + let source = "// SAFETY: ptr is non-null\nlet _x = 1;\n"; + let violations = lint(source); + assert!(violations.is_empty()); + } + + #[test] + fn note_marker_allowed() { + let source = "// NOTE: temporary workaround\nfn main() {}\n"; + let violations = lint(source); + assert!(violations.is_empty()); + } + + #[test] + fn hack_marker_allowed() { + let source = "// HACK: workaround for issue\nfn main() {}\n"; + let violations = lint(source); + assert!(violations.is_empty()); + } + + #[test] + fn xxx_marker_allowed() { + let source = "// XXX: investigate\nfn main() {}\n"; + let violations = lint(source); + assert!(violations.is_empty()); + } + + #[test] + fn marker_must_be_at_start() { + let source = "// 説明 TODO: not at start\nfn main() {}\n"; + let violations = lint(source); + assert_eq!(violations.len(), 1); + } + + #[test] + fn comment_inside_string_not_detected() { + // NOTE: tree-sitter は文字列リテラル内の `//` を comment と認識しない (regress 防止) + let source = "fn main() {\n let s = \"// not a comment\";\n}\n"; + let violations = lint(source); + assert!(violations.is_empty()); + } + + #[test] + fn multiple_violations_collected() { + let source = "// foo\n// bar\n// baz\nfn main() {}\n"; + let violations = lint(source); + assert_eq!(violations.len(), 3); + } + + #[test] + fn mixed_doc_and_forbidden() { + let source = "/// Public doc\n// 禁止コメント\nfn foo() {}\n"; + let violations = lint(source); + assert_eq!(violations.len(), 1); + assert_eq!(violations[0].location.line, 2); + } + + #[test] + fn max_violations_capped() { + let mut source = String::new(); + for i in 0..30 { + source.push_str(&format!("// comment {}\n", i)); + } + source.push_str("fn main() {}\n"); + let violations = lint(&source); + assert_eq!(violations.len(), MAX_VIOLATIONS); + } + + #[test] + fn is_rust_file_accepts_rs() { + assert!(is_rust_file("main.rs")); + assert!(is_rust_file("src/lib.rs")); + assert!(is_rust_file(r"e:\work\project\src\app.rs")); + } + + #[test] + fn is_rust_file_case_insensitive() { + assert!(is_rust_file("file.RS")); + assert!(is_rust_file("file.Rs")); + } + + #[test] + fn is_rust_file_rejects_other() { + assert!(!is_rust_file("main.ts")); + assert!(!is_rust_file("style.css")); + assert!(!is_rust_file("Makefile")); + assert!(!is_rust_file("")); + } + + #[test] + fn violation_json_has_all_fields() { + let source = "// 説明\nfn main() {}\n"; + let violations = lint(source); + let json = serde_json::to_string(&violations[0]).unwrap(); + let v: serde_json::Value = serde_json::from_str(&json).unwrap(); + assert_eq!(v["type"], "RUST_COMMENT_FORBIDDEN"); + assert_eq!(v["severity"], "error"); + assert!(v.get("location").is_some()); + assert!(v["location"].get("file").is_some()); + assert!(v["location"].get("line").is_some()); + assert!(v["location"].get("symbol").is_some()); + assert!(v.get("message").is_some()); + assert!(v.get("why").is_some()); + assert!(v.get("fix").is_some()); + assert!(v.get("example").is_some()); + } + + #[test] + fn is_allowed_comment_handles_leading_whitespace() { + assert!(is_allowed_comment(" /// doc")); + assert!(is_allowed_comment("\t// TODO: x")); + assert!(!is_allowed_comment(" // forbidden")); + } +}