diff --git a/.claude/custom-lint-rules.toml b/.claude/custom-lint-rules.toml index a23a2fdf..949aff2b 100644 --- a/.claude/custom-lint-rules.toml +++ b/.claude/custom-lint-rules.toml @@ -704,3 +704,46 @@ rs = [ "no_hardcoded_jj_revset_range_skips_parameterized_format", "no_hardcoded_jj_revset_range_skips_other_branch_literal", ] + +# ─── ルール⑬: config コメント内の workstream 連番名を禁止 ─── + +[[rules]] +id = "no-workstream-seq-names-in-config" +pattern = '\bPR-[0-9]+\b' +severity = "warning" +message = "config コメントに workstream 連番名 (PR-N) が書かれています。恒久的な参照 (GitHub PR 番号 `#NNN` / 日付 / ADR) に置き換えてください" +why = "`PR-N` (N = 数字) 形式の workstream 連番は、その作業パッケージが終わると何を指すか復元できない ephemeral な識別子であり、rule⑥ `no-ephemeral-todo-reference` と同じ「消える参照」クラス。実際に `.claude/hooks-config.toml` のコメントへ書かれ、後から対応 PR を特定できなくなっていた (順位 216)。GitHub PR 番号 `#NNN` は不変なので除外する (pattern が `PR-` + 数字に限定されるため `#NNN` 形式は構造的に非対象)。なお本ルール自身の説明で数字を使わず `PR-N` と書くのは、rule⑥ が `docs/todoN.md` と書くのと同じ自己検出回避" +# plain `json` は対象外。本ルールは config **コメント**内の ephemeral 参照を対象とするが、 +# JSON はコメント構文を持たないため、fire するとしたら data 文字列であり message と食い違う。 +extensions = ["toml", "yaml", "yml", "jsonc"] + +[rules.fix] +strategy = "ephemeral な連番を恒久参照に置換、または削除" +steps = [ + "その `PR-N` が指す実 PR を特定できるなら GitHub PR 番号 `#NNN` に置換", + "特定できないなら日付 / ADR 番号など復元可能な参照に置換", + "計画の記述で参照先が未定なら連番を落として内容だけ残す (例: 「次 PR (PR-N) で移行予定」→「将来移行予定」)", +] + +[rules.example] +bad = '# 次 PR (PR-N、N = 数字) で allow-list 方式に移行予定' +good = '# 将来 allow-list 方式へ移行予定 (由来: PR #216)' + +[rules.incident] +pr = 216 +bad_fixture = "no-workstream-seq-names-in-config.toml" +good_fixture = "no-workstream-seq-names-in-config.toml" +adr = "adr-007" + +[rules.test_coverage] +# rule⑬ は toml / yaml / yml (主要拡張子) + jsonc / json (非主要)。 +# positive は主要拡張子ごとに 1 件、negative は `PR #216` (GitHub 形式) を網羅。 +other_ext_tests = ["no_workstream_seq_names_in_config_detects_jsonc_comment"] + +[rules.test_coverage.main_ext_tests] +toml = [ + "no_workstream_seq_names_in_config_detects_toml_comment", + "no_workstream_seq_names_in_config_skips_github_pr_reference", +] +yaml = ["no_workstream_seq_names_in_config_detects_yaml_comment"] +yml = ["no_workstream_seq_names_in_config_detects_yml_comment"] diff --git a/.claude/hooks-config.toml b/.claude/hooks-config.toml index ac66956f..dc179e05 100644 --- a/.claude/hooks-config.toml +++ b/.claude/hooks-config.toml @@ -43,9 +43,9 @@ stale_check_enabled = true [session_start.weekly_review_reminder] # 試験運用元 (本リポジトリ) では明示的に enable して reminder を実発火させる運用。 # ADR-031 は 2026-06-01 で本採用化済み、Phase E 観測で「reminder 未発火 = silent」だった -# 問題に対処するため試験運用元での enable に切替 (2026-06-23、PR-1)。 +# 問題に対処するため試験運用元での enable に切替 (2026-06-23)。 # 派生プロジェクト deploy 時は default OFF (ADR-039 § 1 opt-in 契約) を維持。 -# 次 PR (PR-3) で `[features].enabled` allow-list 方式に移行予定 = 本 `enabled = true` は暫定。 +# 将来 `[features].enabled` allow-list 方式へ移行予定 = 本 `enabled = true` は暫定。 enabled = true # 7 日は**恒久設定**であり暫定値ではない (2026-08-13 ユーザー判断で確定)。週次レビューは # 毎週実行すること自体に意味があり、「weekly」を冠する運用の reminder が月周期で鳴るなら diff --git a/docs/claude-code-web-tasks.md b/docs/claude-code-web-tasks.md index 86c791a8..1cf08b8d 100644 --- a/docs/claude-code-web-tasks.md +++ b/docs/claude-code-web-tasks.md @@ -102,7 +102,6 @@ | 180 | T2 | — | `escape_markdown_pipe(&str)` を pub 追加 + `format_table` の user field に適用 + 5 variant test(markdown table 破壊の防止 / prompt injection の緩和 = defense-in-depth の一層) | `src/lib-report-formatter/src/lib.rs` | XS-S | 外部依存ゼロの純 lib。既存 private `truncate()` と escape ロジック重複、DRY 整理(共通化 or 役割分担)を検討 | | | 228 | T2 | ✅ | `evaluate_rate_limit_shortcut` の cr_clean 判定(`new_comments` / `actionable_comments` / `unresolved_threads` 3 field × None/Some 境界)の回帰テスト | `src/cli-pr-monitor/src/stages/poll/rate_limit_signal.rs`(末尾 tests) | S | pure 関数、silent-clean 誤認保護。同 crate の `#[ignore]` 統合テストは無関係 | test(check-ci): rate-limit shortcut の cr_clean 判定をテストで固定する | | 178 | T2 | — | `state.rs` の behavioral invariant test を ADR-041 pattern(sentinel 事前投入 + mutation 不在 assert)で 3-5 件追加 | `src/cli-pr-monitor/src/state.rs` | S | **todo 提案の invariant #1/#2 は実挙動と不一致**。`update_state_from_check_result` の実挙動を読んで実在する invariant を再選定する | | -| 239 | T2 | ✅ | `filter_transcripts` の `read_dir` 非決定順を timestamp ソートで決定論化 + 回帰テスト | `src/cli-merge-pipeline/src/feedback/transcript.rs`(`filter_transcripts` + tests) | M | temp-dir に複数 jsonl 生成 → 順序 assert で完結。実 hook 発火不要 | fix(merge-pipeline): transcript の読み取り順を timestamp ソートで決定論化する | ### Batch 2: 新規実装を伴う(○、要設計判断) @@ -111,7 +110,6 @@ cargo test で検証完結するが、新規 module / lint rule / 軽微リフ | 順位 | Tier | 無人可 | 内容 | 対象ファイル | 工数 | 注意 | PRタイトル | |---|---|---|---|---|---|---|---| | 340 | T2 | — | `decide.rs` の rate_limit × positive-evidence 複合境界テスト + `main.rs` の rate_limit threading テスト | `src/check-ci-coderabbit/src/{decide,main}.rs` | S | (a) は純関数で容易。(b) は `main.rs` の呼び出し側を I/O 無しでテスト可能にする小さな合成関数抽出リファクタが要る | | -| 216 | T2 | ✅ | `no-workstream-seq-names-in-config` lint rule 追加(config comment 内 `PR-[0-9]+` を検出、`#NNN` は除外) | `.claude/custom-lint-rules.toml` + `src/hooks-post-tool-linter/src/custom_rules/rule_tests_extras.rs` + `tests/incident_eval.rs` + `tests/fixtures/incidents/{bad,good}/` + (dogfood) `.claude/hooks-config.toml` | S | 確立 12 rule / 11 incident パターン踏襲。Rust regex lookaround 不要(`\bPR-[0-9]+\b`)。dogfood は数行の text 編集 | feat(post-tool-linter): config の workstream 連番名を lint する | | 272 | T1 | — | cli-docs-lint に ADR 重複採番検出 + CLAUDE.md 索引整合チェック(新規 validator module) | `src/cli-docs-lint/src/adr_consistency.rs`(新規)+ `main.rs`(CheckMode dispatch 拡張) | S-M | 中核(validator + fixture test)は cargo test で完結。「pnpm lint:docs 経由の発火確認」は Web 外だが成功条件ではない。CLAUDE.md は docs_dir の親なので TempDir で fake 構造を組む | | | 334 | T1 | — | docs/todo\*.md 本文の順位番号表記を検出する custom lint rule(ADR-033 仕組み化、`paths=["docs/todo*.md"]` scope、table 行除外) | `.claude/custom-lint-rules.toml` + fixtures(216 と同基盤) | M | 検証経路は 216 と同じ cargo test。**regex FP 精緻化**(preamble の「順位 220 以降」等)+ **本文 dogfood cleanup の規模**を着手前に grep 見積り(todo 記載 S だが M 見込み) | | | 179 | T2 | — | rate-limit retry 境界(max_retries=0/1/3)で retry 継続 vs `action_required` 遷移の off-by-one を pin する parameterized テスト | `src/cli-pr-monitor/src/stages/poll/rate_limit.rs`(判定 L52)+ `config.rs`(L143-155) | S-M | **todo の「rstest 使用済」は誤り**(Cargo.lock に不在)。新 dev-dep 追加 or plain 複数 `#[test]` で代替を着手時判断。gh subprocess を踏まない早期 return 経路で構成する | | @@ -178,8 +176,39 @@ cargo test で検証完結するが、新規 module / lint rule / 軽微リフ これで docs-only の採用枠は 0 件になった。ただし本ファイルは retire しない(→ § ライフサイクル)。 +### 2026-08-14 + +| 順位 | 節 | 判定 | 根拠 | +|---|---|---|---| +| 239 | 採用タスク (2) Batch 1 | マージ済みのため削除 | 夜間 PR [#391](https://github.com/aloekun/claude-code-hook-test/pull/391) が 2026-08-14 にマージ。実体も確認済み — `src/cli-merge-pipeline/src/feedback/transcript.rs` に `jsonl_paths.sort_by_key(\|path\| transcript_ordering_key(path))` が存在し、`docs/todo-summary2.md` の順位 table からも削除済み | +| 216 | 採用タスク (2) Batch 2 | 本 PR で完成させたため削除 | 夜間 PR [#394](https://github.com/aloekun/claude-code-hook-test/pull/394) は fixture 2 ファイルのみで rule 本体が無く**未完了だった**(→ [§ 未完了のままマージされた順位](#未完了のままマージされた順位))。本 PR で rule 定義・rule test 5 件・E2E case・dogfood を実装し、完了基準(`.toml`/`.yaml`/`.yml`/`.jsonc` の `PR-` + 数字を warning 検出、`PR #NNN` は非検出)を満たしたうえで削除した | + --- +## 未完了のままマージされた順位 + +夜間 PR がマージされても**タスクが完了しているとは限らない**。マージは「その PR の内容を取り込む」判断であって「台帳の完了基準を満たした」判定ではなく、両者を突き合わせる機構が現状どこにも無い。ここには実際に起きた事例と、そこから設けた機構を記録する。 + +**現在 live な事例は無い**(順位 216 は 2026-08-14 に完成させて削除済み。→ [§ 棚卸し履歴](#棚卸し履歴))。本節を残すのは、下記の失敗モードと対処が再発防止の根拠として参照され続けるため。 + +### 事例: 順位 216(2026-08-14) + +| PR | 入った成果物 | 欠けていた成果物 | +|---|---|---| +| [#394](https://github.com/aloekun/claude-code-hook-test/pull/394) | `tests/fixtures/incidents/{bad,good}/no-workstream-seq-names-in-config.toml`(2 ファイル・計 6 行) | `.claude/custom-lint-rules.toml` の rule 定義、rule test、`tests/incident_eval.rs` の E2E case、dogfood | + +**なぜ CI を通ったのか**: custom lint rule の 3 つの機械チェックは**すべて「rule → fixture/test」の向き**にしか働かなかった。 + +- `rule_test_coverage_check`: toml の各 rule に対応 test が実在するか +- `incident_fixture_coverage_check`: incident 由来 rule に bad/good fixture が実在するか +- `cases_cover_every_incident_rule`: `CASES.len()` == `[rules.incident]` の個数 + +いずれも rule の存在を起点に検査するため、**rule を伴わない孤児 fixture は 3 チェックすべてを素通り**した。「新規 lint rule は 3 つの cargo test 群で機械強制される」という [§ 採用タスク (2)](#採用タスク-2-cargo-test-検証タスククロスプラットフォーム対応後2026-07-23) の記述は、**rule を書いた場合にのみ成立**していた。 + +**対処**: 逆向きの `orphan_fixture_check`(`src/hooks-post-tool-linter/src/custom_rules/coverage.rs`)を追加し、「fixture があるなら必ず rule がある」を fail-closed で強制した。この検査があれば #394 は CI で落ちてマージできなかった。 + +**残る一般的リスク**: 上記は lint rule クラスに固有の対処であり、「マージ ≠ 完了」という失敗モード自体は他のタスククラスに残る。完了基準を機械可読にして削除前に検証する仕組み(push 前セルフレビューでの決定論的な台帳自動削除 + 実装確認)を別途構築する。 + ## 昇格検査履歴 週次レビューの昇格候補チェック(→ [§ 定期更新(週次)](#定期更新週次) 2)で **「検査したが本台帳の採用基準を満たさない」と判定した順位**を記録する。次回以降の検査はここに載っている順位を除外し、**残り全件**を判定する。 diff --git a/docs/todo-summary.md b/docs/todo-summary.md index 03e287cf..247666cd 100644 --- a/docs/todo-summary.md +++ b/docs/todo-summary.md @@ -81,7 +81,6 @@ | 207 | 💎 Tier 3 | **`~/.claude/rules/common/patterns.md` § Experimental Feature 設計時の参照必須 に「mechanical lint は ADR-039 scope 外」境界 case 追加 (PR #204 post-merge-feedback T3-2 採用)** | todo10.md | S | なし (PR #204 で project-local adr-039 § 1.b 追加した知見の global 投影、派生プロジェクトでの同型 over-application 防止、4 条件 (non-blocking / 決定論 / scope 限定 / recovery hint 明確) + 該当例 (file-length / file-size lint) と非該当例 (post-merge-feedback / weekly-review / local-llm) を境界 case として明示、順位 200/202/205 と同 pattern = project-local 知見の global codification、`feedback_global_config_backup` 適用必須) | | 211 | 💎 Tier 3 | **`~/.claude/rules/common/testing.md` に「単複・閾値・時制で出力形式が変わる関数は N=0 / N=1 / N≥2 の 3 境界 variant 必須」guideline 追加 (PR #210 post-merge-feedback T3-2 採用)** | todo10.md | XS | なし (PR #210 で `drain_pipe_capped_reporting_n_plus_1_truncates_one_appends_summary` が当初 `"1 lines truncated"` 誤期待値で takt-fix auto-fix された実観測、N-1/N/N+1 境界値と直交する「N=1 単複境界 + ゼロ近傍」次元の guideline 化、順位 110 pure function test pattern と相補、派生プロジェクトに global 経由で自動波及、`feedback_global_config_backup` 適用必須) | | 215 | 💎 Tier 3 | **`~/.claude/rules/common/coding-style.md` に「Defensive State Reset in State Machines」section 追加 (PR #214 post-merge-feedback T3-1 採用)** | todo18.md | S | なし (PR #214 round 2 で `finalize_initial_review_park` の `state.pr` / `state.repo` / `state.started_at` を `read_state()` 後に無条件上書きする pattern が CR Major #4 の fix として land、CR Major #1 (`head_commit`) + CR Major #2 (`review_recheck_count`) と同型の defensive reset が既に 3 field 適用済 = `review_recheck.rs` 内で複数 instance あり、将来の reviewer が「redundant」と誤判定して削除すると prior cycle の stale state が混入して silent bug 化、global rules への docs 追記で派生プロジェクト (techbook-ledger / auto-review-fix-vc) に自動波及、simplicity-review LLM が同 file を読むため "enforced via review" として機能し memory `feedback_no_unenforced_rules` 例外を満たす、`feedback_global_config_backup` 適用必須) | -| 216 | 🔧 Tier 2 | **`no-workstream-seq-names-in-config` lint rule 追加 — config comment 内 `PR-[0-9]+` ephemeral workstream sequence 検出 (PR #216 post-merge-feedback T1-1 採用) ★ Bundle 216-217** | todo18.md | S | なし (PR #216 で `hooks-config.toml` comment に "PR-1" / "PR-3" workstream sequence を書き込んだ違反を post-merge-feedback T1-1 で捕捉、rule⑥ `no-ephemeral-todo-reference` は `docs/todo*.md` file path のみ catch するが workstream sequence names は対象外、pattern `(?i)\bPR-[0-9]+\b` で `.toml`/`.yaml`/`.yml`/`.jsonc`/`.json` comment を対象、`#NNN` GitHub PR 表記は exception で除外、rule⑫ と同 pattern (TOML `test_coverage` meta field + main.rs test) で Effort S、順位 217 と同根 = 1 PR bundle 推奨。**Tier 列との不整合補足**: analyzer feedback report の `Tier 1: Hooks/Linter 改善` カテゴリは project Tier 1 (🚀 high-impact urgent) ではなく memory `feedback_tier_classification` の re-classification rule (mechanical enforcement = T2 / docs 修正 = T3) に従い project Tier 2 (🔧 tooling improvements) に再分類) | | 217 | 💎 Tier 3 | **`~/.claude/rules/common/coding-style.md` § Cross-File Reference Lifecycle に config file comments の permanent artifact 扱い + workstream sequence 禁止例追加 (PR #216 post-merge-feedback T3-1 採用) ★ Bundle 216-217** | todo18.md | XS | なし (順位 216 lint rule と同根、本 task は文書層 = author 理解促進、機械層 (216) との 2 層防御、coding-style.md 既存 § Cross-File Reference Lifecycle は markdown 内 cross-ref を主想定で config file comments の permanent artifact 扱いが暗黙的、workstream sequence names (`PR-1`/`PR-3` 等) 禁止例 + GitHub PR numbers (`#NNN`) 代替を明示、派生プロジェクト (techbook-ledger / auto-review-fix-vc) へ global 経由で自動波及、`feedback_global_config_backup` 適用必須、順位 216 と 1 PR bundle 推奨) | | 218 | 💎 Tier 3 | **ADR-039 § Bounded Lifetime + `~/.claude/rules/common/patterns.md` に provisional `enabled` 変更時の todo entry 必須化を追加 (PR #216 post-merge-feedback T3-2 採用)** | todo18.md | XS | なし (PR #216 で `weekly_review_reminder.enabled = true` の provisional 状態を config comment のみで tracking した silent aging risk を post-merge-feedback T3-2 で捕捉、ADR-039 6-point design checklist に「provisional `enabled` 変更時は `docs/todo*.md` に移行 tracking entry を作成」を追加、`~/.claude/rules/common/patterns.md` § Experimental Feature 設計時の参照必須 にも同旨 note 追加、Frequency Low (初観測) + Adoption Risk None で早期 codify、派生プロジェクト (techbook-ledger / auto-review-fix-vc) へ global 経由で自動波及、`feedback_global_config_backup` 適用必須) | | 219 | 💎 Tier 3 | **`~/.claude/rules/common/development-workflow.md` § 設計 doc/実装の同期チェック に「commit description 言及 ≠ 実装完了」明文化 (PR #216 post-merge-feedback T3-3 採用)** | todo18.md | XS | なし (本 PR で「commit description で順位 N 言及 = 実装完了」naïve assumption から analyzer が 6 entry 削除計画を立てたが、grep で実体確認した結果 5 entry が正解 (順位 215 救出) の実観測、「PR commit description で順位 N や feature X を言及していても、実際のファイル変更を `jj diff` / `grep` で確認するまで completion 判定しない」guideline を追加、Frequency Low (初観測) + Severity Medium (analyzer / Claude 誤判定リスクが今後も継続)、派生プロジェクト (techbook-ledger / auto-review-fix-vc) へ global 経由で自動波及、`feedback_global_config_backup` 適用必須) | diff --git a/docs/todo-summary2.md b/docs/todo-summary2.md index 49582a8c..975aea92 100644 --- a/docs/todo-summary2.md +++ b/docs/todo-summary2.md @@ -22,7 +22,6 @@ | 236 | 🚀 Tier 1 | **tempfile mandate + PID+ms 命名 block の custom lint (PR #229 post-merge-feedback T1-1 採用)** | todo13.md | S | なし (#227 で修正した temp file collision flaky の再導入防止。#229 で本 flaky が push pipeline の `cargo test` を 3 回ブロックした実害。custom lint で `tempfile::Builder` / `NamedTempFile` を mandate + `gh-pr-body-{PID}-{ms}` 形式の手動命名を block。順位 237 = 検出層と二層防御) | | 237 | 🔧 Tier 2 | **create_pr flaky の高並列 regression test (PR #229 post-merge-feedback T2-1 採用)** | todo13.md | M | なし (#227 flaky fix の再導入検出網。`body_with_literal_newline_converted` を per-test `tempfile::tempdir()` + 高並列 concurrent run で回し collision を恒常 trap。順位 236 = 予防層と二層防御) | | 238 | 🚀 Tier 1 | **`Command::new("gh")` 直叩き禁止 + timeout wrapper 必須の custom lint (PR #230 post-merge-feedback T1-#1 採用)** | todo13.md | M | なし (fetch_pr_time_range / fetch_pr_diff_summary / run_gh_logged / delete_remote_branch の 4 箇所が `Command::new("gh").output()` 同期実行でネットワーク不調時に無期限ハング = ADR-016 違反、CodeRabbit Major #2/#3。custom lint で直叩きを検出し `run_cmd_shell_capped_reporting` 相当の timeout wrapper を促す。extensions=["rs"] 限定で false positive 軽減。Severity High + Frequency High + Effort M。順位 240 と同 crate、bundle 検討可) | -| 239 | 🔧 Tier 2 | **`filter_transcripts` の複数 jsonl 走査を timestamp ソートで deterministic 化 + regression test (PR #230 post-merge-feedback T2-#1 採用)** | todo13.md | M | なし (`fs::read_dir` の非決定順により複数 Claude セッション時にファイル間時系列順が保証されず downstream takt workflow の context 品質が低下 = ADR-030 determinism 目標と乖離。timestamp ソート + regression test。Severity Medium + Effort M + Adoption Risk None) | | 240 | 🔧 Tier 2 | **`takt.rs` の spawn/try_wait `Err(_)` 分岐に eprintln 追加 — 原因握り潰し解消 (PR #230 post-merge-feedback T3-#1 採用)** | todo13.md | XS | なし (spawn/try_wait の `Err(_) =>` が詳細を握り潰し `.failed` marker に実原因 (pnpm 未検出 / 権限エラー等) が残らず L2 recovery の debug 困難。`write_pending_marker_logged` 等の確立 eprintln パターン踏襲で XS。Severity Medium + Effort XS + Adoption Risk None、順位 238 と同 crate bundle 検討可) | | 241 | 💎 Tier 3 | **binary crate の module symbol を `pub(crate)` 限定 + CLAUDE.md 明文化 (PR #230 post-merge-feedback T3-#2 採用)** | todo13.md | S | なし (feedback module 分割で write_failed_marker / fetch_pr_diff_summary / FeedbackInput / run 等が external consumer 不在なのに `pub` export = pub(crate) 方針と乖離。refactor PR ごとに再発する systemic pattern (Frequency Medium)。CLAUDE.md 明文化 + pub→pub(crate) 揃えで Effort S。Adoption Risk None) | | 243 | 💎 Tier 3 | **`pub(crate)` vs `pub` 可視性チェックリストを module split 手順に追加 (PR #231 post-merge-feedback T3-1 採用)** | todo13.md | XS | なし (W-series module split で visibility scoping の判断が都度必要 = crate 内共有は `pub(crate)`、`pub` は同一 crate 内では有効だが library target 公開時のみ外部 API 化 (binary crate では pub(crate) と実質同等ゆえ pub(crate) 推奨) の違いを具体例付きで明示。Frequency Medium (file-length 強制継続で split 継続) + Effort XS + Adoption Risk None。順位 241 (pub(crate) 方針) と相補、追記先の file-length-enforcement-plan.md は W5 land 後削除予定のため coding-style.md / CLAUDE.md への恒久配置を着手時判断) | diff --git a/docs/todo13.md b/docs/todo13.md index 55705f05..281e51ab 100644 --- a/docs/todo13.md +++ b/docs/todo13.md @@ -345,29 +345,6 @@ --- -### `filter_transcripts` の複数 jsonl 走査を timestamp ソートで deterministic 化 + regression test (PR #230 post-merge-feedback T2-#1 採用) - -> **動機**: `filter_transcripts` (transcript.rs) が `fs::read_dir` の非決定的走査順で複数 `.jsonl` を処理しており、複数 Claude セッションが並存する場合にファイル間の時系列順が保証されない。downstream の takt workflow (analyze-session) が受け取る context の順序品質が低下し、ADR-030 の determinism 目標と乖離する (CodeRabbit findings)。走査結果を timestamp ソートして決定論化し、regression test で保護する。 -> -> **本タスクの位置づけ**: PR #230 post-merge-feedback Tier 2 #1 採用 (Medium / Frequency Low / Effort M / Adoption Risk None)。 -> -> **参照**: `.claude/feedback-reports/230.md` Tier 2 #1、PR #230 (`3e7fdf9e`)、`src/cli-merge-pipeline/src/feedback/transcript.rs` (対象)、ADR-030 (determinism 目標)。 -> -> **実行優先度**: 🔧 **Tier 2** — Effort M。 - -#### 作業計画 - -- [ ] `filter_transcripts` の `fs::read_dir` 結果を timestamp (または名前) で sort してから処理するよう変更 -- [ ] 複数 jsonl の順序が入力順に依らず決定論になることを assert する regression test 追加 -- [ ] `cargo test -p cli-merge-pipeline` pass -- [ ] 本 entry 削除 + todo-summary2.md 行削除 - -#### 完了基準 - -- 複数 `.jsonl` 入力時の filter 出力が決定論的順序になり regression test で保護される。 - ---- - ### `takt.rs` の spawn/try_wait `Err(_)` 分岐に eprintln 追加 — 原因握り潰し解消 (PR #230 post-merge-feedback T3-#1 採用) > **動機**: `takt.rs` の `spawn()` / `try_wait()` の `Err(_) =>` 分岐がエラー詳細を握り潰しており、失敗時に `.failed` marker へ実際の原因 (`pnpm` 未検出 / 権限エラー等) が残らず L2 recovery の debugging が困難 (CodeRabbit findings)。同 crate に確立済の `write_pending_marker_logged` 等の `eprintln!` パターンを踏襲して原因を記録する。 diff --git a/docs/todo18.md b/docs/todo18.md index d366b443..fafc84e2 100644 --- a/docs/todo18.md +++ b/docs/todo18.md @@ -51,62 +51,6 @@ --- -### `no-workstream-seq-names-in-config` lint rule 追加 — config comment 内 `PR-[0-9]+` ephemeral workstream sequence 検出 (PR #216 post-merge-feedback T1-1 採用) - -> **動機**: PR #216 で `.claude/hooks-config.toml` の `weekly_review_reminder` section comment に `(2026-06-23、PR-1)` および `次 PR (PR-3) で移行予定` を書き込んだ。これは ephemeral workstream sequence names (= マルチ PR 計画のローカル連番、GitHub PR `#NNN` ではない) を permanent artifact (config file comments) に embed する違反であり、`coding-style.md` § Cross-File Reference Lifecycle の「permanent → ephemeral 禁止」原則と同根。 -> -> 既存の rule⑥ `no-ephemeral-todo-reference` は `docs/todo*.md` file path 直接参照を検出するが、本ケースのような workstream sequence names (`PR-N`) は対象外。PR シリーズ完了後に「PR-3 とは何だったか」が文脈喪失し dead pointer 化するリスクが構造的に残る。 -> -> **本タスクの位置づけ**: PR #216 post-merge-feedback Tier 1 #1 採用 (Severity Medium / Frequency Medium / Effort S / Adoption Risk None、2026-06-23 ユーザー承認)。順位 217 (文書層) と同根 = 1 PR bundle 推奨。analyzer rationale: 「config file comment は permanent artifact であり dead pointer 化の直接トリガ。pattern `(?i)PR-[0-9]+` の FP リスクは軽微 (config コメントで企業コード等との混同は稀)」。Prepush T1-1 + Session T1-1 + Session T1-2 の 3 ソース独立検出。 -> -> **Tier 列との不整合補足**: analyzer feedback report (`.claude/feedback-reports/216.md`) では `Tier 1: Hooks/Linter 改善` カテゴリに分類されているが、本 todo entry の Tier 列および「実行優先度」行では **🔧 Tier 2** に再分類している。memory `feedback_tier_classification` の re-classification rule (= analyzer の Tier 1/3 分類は鵜呑みにせず実体ベース ⟨mechanical enforcement = T1 / docs 修正 = T3⟩ で再分類) に従い、project tier 定義 (🚀 Tier 1 = high-impact urgent / 🔧 Tier 2 = tooling improvements) と整合させた意図的再分類。 -> -> **参照**: `.claude/feedback-reports/216.md` Tier 1 #1、PR #216 commit `65963197e6c0` の hooks-config.toml diff、既存 rule⑥ `no-ephemeral-todo-reference` (template)、rule⑫ `no-hardcoded-jj-revset-range` (TOML meta field test_coverage pattern の template)、`~/.claude/rules/common/coding-style.md` § Cross-File Reference Lifecycle、`.claude/custom-lint-rules.toml` (rule 配置先)。 -> -> **実行優先度**: 🔧 **Tier 2** (project 分類、上記 re-classification 後) — Effort S。rule 追加 (~30 行 TOML + meta field) + test 追加 (~50 行 main.rs) で約 80 行、順位 217 と bundle すれば 1 PR diff < 200 行見込み。 - -#### 設計決定 (案) - -- **rule id**: `no-workstream-seq-names-in-config` -- **pattern**: `(?i)\bPR-[0-9]+\b` -- **extensions**: `["toml", "yaml", "yml", "jsonc"]` (config formats、plain `json` は comment 構文を持たず rule 対象外なので除外) -- **検出範囲**: comment 行のみ (TOML `#`、YAML `#`、JSONC `//` 等)。**実装注**: 多くの project-local lint rule は file 全体に regex match している。本 rule は comment 行検出が本質だが、初期実装は file 全体マッチで MVP として開始し、false positive 観測後に comment 行限定への絞り込みを判断する (= 同 pattern の rule⑥/⑫ と整合的な段階導入) -- **exception**: GitHub PR number `#[0-9]+` 形式は対象外。実装上は positive pattern `(?i)\bPR-[0-9]+\b` が `#NNN` を match しない (`#` prefix 形式は別) ため exception 不要。ただし test で「`#216`」「`PR #216`」のようなケースが fire しないことを negative test で固定 -- **severity**: `warning` (block しない、author への hint 機能優先、rule⑫ と同 pattern) -- **block message**: 「Ephemeral workstream sequence name (`PR-N`) detected in config comment. Permanent artifacts (config files) must not reference ephemeral workstream sequences. Use GitHub PR `#NNN` for stable cite, or inline rationale instead of "PR-3 で移行予定". See coding-style.md § Cross-File Reference Lifecycle.」 -- **TOML meta field** (`test_coverage` schema、rule⑫ と同 pattern): - ```toml - [rules.test_coverage] - other_ext_tests = ["no_workstream_seq_detects_pr_dash_n_in_jsonc_comment"] - - [rules.test_coverage.main_ext_tests] - toml = ["no_workstream_seq_detects_pr_dash_n_in_toml_comment", "no_workstream_seq_skips_github_pr_number"] - yaml = ["no_workstream_seq_detects_pr_dash_n_in_yaml_comment"] - yml = ["no_workstream_seq_detects_pr_dash_n_in_yml_comment"] - ``` - -#### 作業計画 - -- [ ] `.claude/custom-lint-rules.toml` に `[[rules]]` entry 追加 (id / pattern / extensions / severity / message / test_coverage) -- [ ] `src/hooks-post-tool-linter/src/main.rs` の `mod tests` に positive test 4 件 (toml / yaml / yml / jsonc 各 1) + negative test 1 件 (`#216` / `PR #216` が fire しない) を追加 -- [ ] `cargo test -p hooks-post-tool-linter` で rule_test_coverage_check が pass することを確認 -- [ ] dogfood: 本 PR で `hooks-config.toml` から `PR-1` / `PR-3` 表記が削除 or `#216`/`#NNN` 表記に置換されることを確認 -- [ ] 本エントリ削除 + docs/todo-summary.md 行削除 - -#### 完了基準 - -- `no-workstream-seq-names-in-config` rule が `.toml` / `.yaml` / `.yml` / `.jsonc` / `.json` comment 内の `PR-[0-9]+` を warning として検出 -- `#216` / `PR #216` のような GitHub PR number は fire しない (negative test pass) -- rule_test_coverage_check が main_ext_tests / other_ext_tests 整合性を強制 -- 順位 217 (文書層) と同 PR で land した場合、`coding-style.md` への具体例追加と機械強制の 2 層防御が確立される - -#### 詰まっている箇所 - -- comment 行限定 vs file 全体 match: MVP は file 全体 match で開始、false positive 観測後に絞り込み判断 (rule⑥/⑫ と同段階導入)。順位 217 の docs 追加で「config comment」の意図を明示化することで、author が non-comment context での意図的使用を回避できれば file 全体 match でも実用性高い -- `#NNN` vs `PR-NNN` 境界: regex `\bPR-[0-9]+\b` は `#` prefix を含まないため除外可能、ただし将来 `PR-#216` のような mixed 表記が登場した場合は pattern 拡張が必要 - ---- - ### `~/.claude/rules/common/coding-style.md` § Cross-File Reference Lifecycle に config file comments の permanent artifact 扱い明記 + workstream sequence 禁止例追加 (PR #216 post-merge-feedback T3-1 採用) > **動機**: 既存 `coding-style.md` § Cross-File Reference Lifecycle は markdown 内 cross-reference (docs/ADR/README 等) を主に想定して書かれており、**config file comments (`.toml`/`.json`/`.yaml`) も permanent artifact** であることが暗黙的にしか扱われていない。PR #216 で `hooks-config.toml` comment に "PR-1" / "PR-3" ephemeral workstream sequence を embed した違反は、author が「config の comment は注釈であって rule の対象外」と暗黙的に判断していた可能性が高い。 diff --git a/src/hooks-post-tool-linter/src/custom_rules/coverage.rs b/src/hooks-post-tool-linter/src/custom_rules/coverage.rs index a8dff9ef..5d76c3f2 100644 --- a/src/hooks-post-tool-linter/src/custom_rules/coverage.rs +++ b/src/hooks-post-tool-linter/src/custom_rules/coverage.rs @@ -306,3 +306,160 @@ fn incident_fixture_coverage_check() { gaps.join("\n - ") ); } + +/// `[rules.incident]` が宣言する fixture 名を **kind ごとに分けて** 集める。 +/// +/// bad/good を 1 つの集合にまとめてはならない。`bad_fixture = "a"` / `good_fixture = "b"` +/// を宣言する rule があると、実在する `bad/b` が「b は宣言済み」として孤児判定を +/// すり抜ける (kind を跨いだ照合になるため)。実運用では bad/good 同名だが、schema は +/// 別名を許すので照合も kind 単位で行う。 +#[cfg(test)] +fn declared_fixture_names_by_kind( + rules: &[CustomRule], +) -> ( + std::collections::BTreeSet, + std::collections::BTreeSet, +) { + let mut bad = std::collections::BTreeSet::new(); + let mut good = std::collections::BTreeSet::new(); + for rule in rules { + if let Some(incident) = &rule.incident { + bad.insert(incident.bad_fixture.clone()); + good.insert(incident.good_fixture.clone()); + } + } + (bad, good) +} + +/// `tests/fixtures/incidents//` の実ファイル名を集める。 +/// +/// **列挙の失敗は握り潰さない** (ADR-043 fail-closed)。`read_dir` の要素エラーや +/// 非 UTF-8 名を捨てると、読めなかった 1 件が孤児だった場合に「他が読めたので 0 件」と +/// 誤って緑になる。検査そのものが成立していないので panic して落とす。 +#[cfg(test)] +fn existing_fixture_names(kind: &str) -> std::collections::BTreeSet { + let dir = incident_fixtures_dir(kind); + let entries = std::fs::read_dir(&dir) + .unwrap_or_else(|e| panic!("failed to read fixture dir {}: {e}", dir.display())); + let mut names = std::collections::BTreeSet::new(); + for entry in entries { + let entry = entry.unwrap_or_else(|e| { + panic!( + "failed to read a directory entry under {}: {e} (false-green guard: an \ + unreadable entry could be the orphan)", + dir.display() + ) + }); + let path = entry.path(); + if !path.is_file() { + continue; + } + let name = path + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or_else(|| { + panic!( + "non-UTF-8 fixture file name under {} — cannot verify its orphan status", + dir.display() + ) + }) + .to_string(); + names.insert(name); + } + names +} + +/// 1 つの kind について、宣言されていない実ファイルを孤児として列挙する。 +/// +/// I/O を持たない純関数にしてあるのは、kind を跨いだ照合をしていないことを +/// synthetic な集合で固定できるようにするため。 +#[cfg(test)] +fn orphans_for_kind( + kind: &str, + declared: &std::collections::BTreeSet, + existing: &std::collections::BTreeSet, +) -> Vec { + existing + .difference(declared) + .map(|name| { + format!( + "fixture `{}/{}` is referenced by no rule — add the `[[rules]]` entry it was \ + created for (with `[rules.incident]`), or delete the fixture", + kind, name + ) + }) + .collect() +} + +/// 孤児 fixture ゲート ([`incident_fixture_coverage_check`] の**逆向き**)。 +/// +/// # なぜ逆向きの検査が要るのか +/// +/// 既存 3 検査 ([`rule_test_coverage_check`] / [`incident_fixture_coverage_check`] / +/// `incident_eval.rs` の `cases_cover_every_incident_rule`) は**すべて rule を起点**に回る。 +/// したがって「rule を伴わない fixture」= 孤児は 3 検査すべてを素通りする。 +/// +/// 2026-08-14、夜間 todo ループの PR [#394] が **fixture 2 ファイルだけを追加して CI green で +/// マージされた** (rule 定義・rule test・E2E case・dogfood がいずれも無い)。台帳が +/// 「新規 lint rule は 3 つの cargo test 群で機械強制される」と記していた保証は、 +/// **rule を書いた場合にのみ成立**していた。本検査はその非対称を閉じる。 +/// +/// 「fixture があるなら必ず rule がある」は例外なく成立するため、 +/// [`NON_INCIDENT_RULES`] のような allowlist を持たない (順方向にだけ必要な例外)。 +/// +/// [#394]: https://github.com/aloekun/claude-code-hook-test/pull/394 +#[cfg(test)] +#[test] +fn orphan_fixture_check() { + let rules = load_deployed_custom_rules(); + let (declared_bad, declared_good) = declared_fixture_names_by_kind(&rules); + assert!( + !declared_bad.is_empty() && !declared_good.is_empty(), + "no fixtures declared by any `[rules.incident]` — false-green guard" + ); + let mut orphans: Vec = Vec::new(); + for (kind, declared) in [("bad", &declared_bad), ("good", &declared_good)] { + let existing = existing_fixture_names(kind); + assert!( + !existing.is_empty(), + "no fixture files found under {} — false-green guard", + incident_fixtures_dir(kind).display() + ); + orphans.extend(orphans_for_kind(kind, declared, &existing)); + } + assert!( + orphans.is_empty(), + "orphan incident fixtures detected ({} issue(s)):\n - {}", + orphans.len(), + orphans.join("\n - ") + ); +} + +/// bad/good を 1 つの集合にまとめると、kind を跨いだ名前で孤児がすり抜ける。 +/// `bad_fixture = "a"` / `good_fixture = "b"` の rule に対し実在する `bad/b` は +/// 孤児だが、統合集合では「b は宣言済み」として見逃される。 +#[cfg(test)] +#[test] +fn orphans_are_matched_within_the_same_kind_only() { + let declared_bad: std::collections::BTreeSet = ["a.toml".to_string()].into(); + let declared_good: std::collections::BTreeSet = ["b.toml".to_string()].into(); + let existing_bad: std::collections::BTreeSet = + ["a.toml".to_string(), "b.toml".to_string()].into(); + let existing_good: std::collections::BTreeSet = ["b.toml".to_string()].into(); + + let bad_orphans = orphans_for_kind("bad", &declared_bad, &existing_bad); + assert_eq!(bad_orphans.len(), 1, "{bad_orphans:?}"); + assert!(bad_orphans[0].contains("bad/b.toml"), "{bad_orphans:?}"); + + assert!(orphans_for_kind("good", &declared_good, &existing_good).is_empty()); +} + +/// 宣言と実ファイルが一致していれば孤児 0 件 (正常系の固定)。 +#[cfg(test)] +#[test] +fn matching_declarations_yield_no_orphans() { + let declared: std::collections::BTreeSet = + ["a.toml".to_string(), "b.rs".to_string()].into(); + let existing = declared.clone(); + assert!(orphans_for_kind("bad", &declared, &existing).is_empty()); +} diff --git a/src/hooks-post-tool-linter/src/custom_rules/rule_tests_extras.rs b/src/hooks-post-tool-linter/src/custom_rules/rule_tests_extras.rs index 0694e161..e648d234 100644 --- a/src/hooks-post-tool-linter/src/custom_rules/rule_tests_extras.rs +++ b/src/hooks-post-tool-linter/src/custom_rules/rule_tests_extras.rs @@ -540,3 +540,76 @@ fn no_hardcoded_jj_revset_range_skips_other_branch_literal() { let violations = run_custom_rules(file.to_str().unwrap(), &rules); assert!(violations.is_empty()); } + +/// rule⑬ (順位 216): config コメント内の workstream 連番名 (`PR-` + 数字) を検出する。 +/// +/// 連番リテラルは helper 側で組み立てる。テスト本体に直書きすると、この `.rs` 自体は +/// rule の対象拡張子外とはいえ、同じ形の文字列が repo 内へ増えて grep の見通しが悪くなる。 +fn no_workstream_seq_names_in_config_rule() -> CustomRule { + make_test_rule( + "no-workstream-seq-names-in-config", + r"\bPR-[0-9]+\b", + &["toml", "yaml", "yml", "jsonc"], + ) +} + +/// `PR-` 形式 (= 検出対象) のコメント行。 +fn build_workstream_seq_comment(seq: u32) -> String { + format!("# 次 PR (PR-{seq}) で allow-list 方式に移行予定\nenabled = true\n") +} + +/// GitHub PR 番号 `#NNN` 形式 (= 不変参照なので非検出) のコメント行。 +fn build_github_pr_reference_comment(number: u32) -> String { + format!("# 将来 allow-list 方式へ移行予定 (由来: PR #{number})\nenabled = true\n") +} + +#[test] +fn no_workstream_seq_names_in_config_detects_toml_comment() { + let dir = tempfile::tempdir().unwrap(); + let file = write_file(dir.path(), "hooks-config.toml", &build_workstream_seq_comment(3)); + let rules = compile_test_rules(vec![no_workstream_seq_names_in_config_rule()]); + let violations = run_custom_rules(file.to_str().unwrap(), &rules); + assert_eq!(violations.len(), 1); +} + +#[test] +fn no_workstream_seq_names_in_config_detects_yaml_comment() { + let dir = tempfile::tempdir().unwrap(); + let file = write_file(dir.path(), "workflow.yaml", &build_workstream_seq_comment(1)); + let rules = compile_test_rules(vec![no_workstream_seq_names_in_config_rule()]); + let violations = run_custom_rules(file.to_str().unwrap(), &rules); + assert_eq!(violations.len(), 1); +} + +#[test] +fn no_workstream_seq_names_in_config_detects_yml_comment() { + let dir = tempfile::tempdir().unwrap(); + let file = write_file(dir.path(), "workflow.yml", &build_workstream_seq_comment(12)); + let rules = compile_test_rules(vec![no_workstream_seq_names_in_config_rule()]); + let violations = run_custom_rules(file.to_str().unwrap(), &rules); + assert_eq!(violations.len(), 1); +} + +#[test] +fn no_workstream_seq_names_in_config_detects_jsonc_comment() { + let dir = tempfile::tempdir().unwrap(); + let file = write_file(dir.path(), "settings.jsonc", &build_workstream_seq_comment(2)); + let rules = compile_test_rules(vec![no_workstream_seq_names_in_config_rule()]); + let violations = run_custom_rules(file.to_str().unwrap(), &rules); + assert_eq!(violations.len(), 1); +} + +/// GitHub PR 番号は不変な参照であり、本 rule の対象ではない。 +/// この negative が崩れると、正当な PR 参照まで書き換えを迫られる。 +#[test] +fn no_workstream_seq_names_in_config_skips_github_pr_reference() { + let dir = tempfile::tempdir().unwrap(); + let file = write_file( + dir.path(), + "hooks-config.toml", + &build_github_pr_reference_comment(216), + ); + let rules = compile_test_rules(vec![no_workstream_seq_names_in_config_rule()]); + let violations = run_custom_rules(file.to_str().unwrap(), &rules); + assert!(violations.is_empty()); +} diff --git a/src/hooks-post-tool-linter/tests/incident_eval.rs b/src/hooks-post-tool-linter/tests/incident_eval.rs index 5f97e7d9..d790a17f 100644 --- a/src/hooks-post-tool-linter/tests/incident_eval.rs +++ b/src/hooks-post-tool-linter/tests/incident_eval.rs @@ -51,6 +51,7 @@ const CASES: &[Case] = &[ Case { rule_type: "NO_WRITE_RESULT_DISCARD", severity: "error", fixture: "no-write-result-discard.rs", expected_line: 2, workflow_rel: None }, Case { rule_type: "NO_JJ_TEMPLATE_FIRST_LINE", severity: "error", fixture: "no-jj-template-first-line.toml", expected_line: 2, workflow_rel: None }, Case { rule_type: "NO_HARDCODED_JJ_REVSET_RANGE", severity: "warning", fixture: "no-hardcoded-jj-revset-range.rs", expected_line: 2, workflow_rel: None }, + Case { rule_type: "NO_WORKSTREAM_SEQ_NAMES_IN_CONFIG", severity: "warning", fixture: "no-workstream-seq-names-in-config.toml", expected_line: 2, workflow_rel: None }, ]; fn repo_root() -> PathBuf {