From bbde02de59a06acdab610bf7cf39d2221887d16a Mon Sep 17 00:00:00 2001 From: aloekun Date: Mon, 6 Jul 2026 22:38:55 +0900 Subject: [PATCH] =?UTF-8?q?feat(hooks-post-tool-linter):=20WP-08=20inciden?= =?UTF-8?q?t=E2=86=92eval=20=E5=9B=9E=E5=B8=B0=E3=82=B9=E3=82=A4=E3=83=BC?= =?UTF-8?q?=E3=83=88=20=E2=80=94=20=E7=94=B1=E6=9D=A5=20incident=20?= =?UTF-8?q?=E5=86=8D=E7=8F=BE=20fixture=20+=20=E5=AE=9F=20exe=20E2E=20+=20?= =?UTF-8?q?fixture=20=E5=BF=85=E9=A0=88=20fail-closed=20gate=20(ADR-049)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/custom-lint-rules.toml | 66 ++++++ .markdownlint-cli2.jsonc | 5 + CLAUDE.md | 1 + .../adr-049-incident-eval-regression-suite.md | 107 +++++++++ docs/harness-improvement-plan.md | 4 +- src/hooks-post-tool-linter/Cargo.toml | 4 + .../src/custom_rules/coverage.rs | 71 ++++++ .../src/custom_rules/deployed_tests.rs | 1 + .../src/custom_rules/engine_tests.rs | 1 + .../src/custom_rules/rule_tests.rs | 1 + .../src/custom_rules/rule_tests_extras.rs | 1 + .../src/custom_rules/types.rs | 28 +++ .../tests/incident_eval.rs | 224 ++++++++++++++++++ .../bad/no-docs-relative-back-to-docs.md | 2 + .../bad/no-empty-powershell-catch.ps1 | 2 + .../bad/no-ephemeral-todo-reference.rs | 2 + .../bad/no-hardcoded-jj-revset-range.rs | 2 + .../bad/no-jj-template-first-line.toml | 2 + .../incidents/bad/no-mutable-anchor.md | 2 + .../incidents/bad/no-personal-paths.md | 2 + .../incidents/bad/no-silent-error-action.ps1 | 2 + .../bad/no-time-field-strict-greater.rs | 2 + .../incidents/bad/no-write-result-discard.rs | 2 + .../takt-workflow-persona-without-model.yaml | 5 + .../good/no-docs-relative-back-to-docs.md | 2 + .../good/no-empty-powershell-catch.ps1 | 2 + .../good/no-ephemeral-todo-reference.rs | 2 + .../good/no-hardcoded-jj-revset-range.rs | 2 + .../good/no-jj-template-first-line.toml | 2 + .../incidents/good/no-mutable-anchor.md | 2 + .../incidents/good/no-personal-paths.md | 2 + .../incidents/good/no-silent-error-action.ps1 | 2 + .../good/no-time-field-strict-greater.rs | 2 + .../incidents/good/no-write-result-discard.rs | 2 + .../takt-workflow-persona-without-model.yaml | 6 + 35 files changed, 564 insertions(+), 1 deletion(-) create mode 100644 docs/adr/adr-049-incident-eval-regression-suite.md create mode 100644 src/hooks-post-tool-linter/tests/incident_eval.rs create mode 100644 tests/fixtures/incidents/bad/no-docs-relative-back-to-docs.md create mode 100644 tests/fixtures/incidents/bad/no-empty-powershell-catch.ps1 create mode 100644 tests/fixtures/incidents/bad/no-ephemeral-todo-reference.rs create mode 100644 tests/fixtures/incidents/bad/no-hardcoded-jj-revset-range.rs create mode 100644 tests/fixtures/incidents/bad/no-jj-template-first-line.toml create mode 100644 tests/fixtures/incidents/bad/no-mutable-anchor.md create mode 100644 tests/fixtures/incidents/bad/no-personal-paths.md create mode 100644 tests/fixtures/incidents/bad/no-silent-error-action.ps1 create mode 100644 tests/fixtures/incidents/bad/no-time-field-strict-greater.rs create mode 100644 tests/fixtures/incidents/bad/no-write-result-discard.rs create mode 100644 tests/fixtures/incidents/bad/takt-workflow-persona-without-model.yaml create mode 100644 tests/fixtures/incidents/good/no-docs-relative-back-to-docs.md create mode 100644 tests/fixtures/incidents/good/no-empty-powershell-catch.ps1 create mode 100644 tests/fixtures/incidents/good/no-ephemeral-todo-reference.rs create mode 100644 tests/fixtures/incidents/good/no-hardcoded-jj-revset-range.rs create mode 100644 tests/fixtures/incidents/good/no-jj-template-first-line.toml create mode 100644 tests/fixtures/incidents/good/no-mutable-anchor.md create mode 100644 tests/fixtures/incidents/good/no-personal-paths.md create mode 100644 tests/fixtures/incidents/good/no-silent-error-action.ps1 create mode 100644 tests/fixtures/incidents/good/no-time-field-strict-greater.rs create mode 100644 tests/fixtures/incidents/good/no-write-result-discard.rs create mode 100644 tests/fixtures/incidents/good/takt-workflow-persona-without-model.yaml diff --git a/.claude/custom-lint-rules.toml b/.claude/custom-lint-rules.toml index e05d1e60..a23a2fdf 100644 --- a/.claude/custom-lint-rules.toml +++ b/.claude/custom-lint-rules.toml @@ -82,6 +82,12 @@ steps = [ bad = '具体的なファイル所在: `C:\Users\alice\.claude\projects\\` 配下' good = '具体的なファイル所在: `%USERPROFILE%\.claude\projects\\` 配下' +[rules.incident] +pr = 75 +bad_fixture = "no-personal-paths.md" +good_fixture = "no-personal-paths.md" +adr = "adr-007" + [rules.test_coverage] # rule② は md/txt (= 全て非主要拡張子)。順位 137 PR で test gap 補填 (3 tests 新規追加)。 other_ext_tests = [ @@ -121,6 +127,12 @@ steps = [ bad = 'try { Get-Item $path } catch {}' good = 'try { Get-Item $path } catch { Write-Verbose "expected miss: $_"; $null }' +[rules.incident] +pr = 85 +bad_fixture = "no-empty-powershell-catch.ps1" +good_fixture = "no-empty-powershell-catch.ps1" +adr = "adr-007" + [rules.test_coverage] # rule③ は ps1 のみ (非主要拡張子)。複数 case-variant の安定性 seal。 other_ext_tests = [ @@ -161,6 +173,12 @@ steps = [ bad = '$data = ConvertFrom-Json $raw -ErrorAction SilentlyContinue' good = 'try { $data = ConvertFrom-Json $raw -ErrorAction Stop } catch { Write-Error "Invalid JSON: $_"; throw }' +[rules.incident] +pr = 85 +bad_fixture = "no-silent-error-action.ps1" +good_fixture = "no-silent-error-action.ps1" +adr = "adr-007" + [rules.test_coverage] # rule④ は ps1 のみ (非主要拡張子)。PowerShell case-insensitive variant の安定性 seal。 other_ext_tests = [ @@ -207,6 +225,12 @@ steps = [ bad = 'See [推奨実行順序](todo-summary.md#推奨実行順序サマリー)' good = 'See [推奨実行順序](todo-summary.md#recommended-order-summary) ' +[rules.incident] +pr = 89 +bad_fixture = "no-mutable-anchor.md" +good_fixture = "no-mutable-anchor.md" +adr = "adr-007" + [rules.test_coverage] # rule⑤ は md のみ (非主要拡張子)。GFM anchor の positive / negative variant を網羅。 other_ext_tests = [ @@ -265,6 +289,12 @@ steps = [ bad = 'BLOCK_MESSAGE: &str = "詳細: docs/todoN.md (N = 数字) の \"
\" 参照";' good = 'BLOCK_MESSAGE: &str = "詳細: docs/adr/adr-NNN-feature.md 参照";' +[rules.incident] +pr = 94 +bad_fixture = "no-ephemeral-todo-reference.rs" +good_fixture = "no-ephemeral-todo-reference.rs" +adr = "adr-007" + [rules.test_coverage] # rule⑥ は主要拡張子 4 つ (rs/toml/yaml/yml) + 非主要 8 つを extensions に含む。 # 順位 137 PR で yaml/yml の test gap を補填 (positive 2 + negative 1 新規追加)。 @@ -325,6 +355,12 @@ steps = [ bad = "comments.iter().filter(|c| c.created_at > push_time)" good = "comments.iter().filter(|c| c.created_at >= push_time)" +[rules.incident] +pr = 101 +bad_fixture = "no-time-field-strict-greater.rs" +good_fixture = "no-time-field-strict-greater.rs" +adr = "adr-007" + [rules.test_coverage] # rule⑦ は rs のみ (主要拡張子)。時刻フィールド variant + 境界 case の網羅的 seal。 @@ -392,6 +428,12 @@ steps = [ bad = "[ADR-036](DOTDOT/docs/adr/adr-036-...) " good = "[ADR-036](adr/adr-036-...)" +[rules.incident] +pr = 133 +bad_fixture = "no-docs-relative-back-to-docs.md" +good_fixture = "no-docs-relative-back-to-docs.md" +adr = "adr-007" + [rules.test_coverage] # rule⑧ は md のみ (非主要拡張子)。docs 配下 + root-level 両方の back-reference を網羅。 other_ext_tests = [ @@ -463,6 +505,12 @@ good = ''' persona: supervisor model: sonnet instruction: loop-monitor-reviewers-fix''' +[rules.incident] +pr = 98 +bad_fixture = "takt-workflow-persona-without-model.yaml" +good_fixture = "takt-workflow-persona-without-model.yaml" +adr = "adr-007" + [rules.test_coverage] # rule⑨ は yaml のみ (主要拡張子)。multi-line regex variant + paths filter は別 test で網羅。 @@ -517,6 +565,12 @@ steps = [ bad = "let _ = write_state(&state);" good = "if let Err(e) = write_state(&state) { log_warn(&format!(\"state write failed: {}\", e)); }" +[rules.incident] +pr = 155 +bad_fixture = "no-write-result-discard.rs" +good_fixture = "no-write-result-discard.rs" +adr = "adr-007" + [rules.test_coverage] # rule⑩ は rs のみ (主要拡張子)。Drop / if-let-Err / named-binding 等の variant を網羅。 @@ -573,6 +627,12 @@ steps = [ bad = "jj log -T 'change_id ++ DESC_DOT_FIRSTLINE' # placeholder: 実コードでは description . first_line() = 曖昧性" good = "jj log -T 'change_id ++ if(empty, \"EMPTY\", \"CONTENT\")' # commit 自体が file change を含むかで判定" +[rules.incident] +pr = 175 +bad_fixture = "no-jj-template-first-line.toml" +good_fixture = "no-jj-template-first-line.toml" +adr = "adr-007" + [rules.test_coverage] # rule⑪ は jj template が書かれうる主要 file 形式 (toml/yaml/md) を対象。 # toml/yaml は主要拡張子のため main_ext_tests、md は非主要のため other_ext_tests。 @@ -628,6 +688,12 @@ steps = [ bad = 'let revset = "master..@";' good = 'let revset = format!("{}..@", default_branch);' +[rules.incident] +pr = 195 +bad_fixture = "no-hardcoded-jj-revset-range.rs" +good_fixture = "no-hardcoded-jj-revset-range.rs" +adr = "adr-021" + [rules.test_coverage] # rule⑫ は rs のみ (主要拡張子)。positive (hardcode 検出) + negative (parameterized 形式 skip) の variant を網羅。 diff --git a/.markdownlint-cli2.jsonc b/.markdownlint-cli2.jsonc index 1d880d2f..87a40b52 100644 --- a/.markdownlint-cli2.jsonc +++ b/.markdownlint-cli2.jsonc @@ -15,6 +15,11 @@ ".claude/skills", ".takt/runs", ".pytest_cache", + // WP-08: incident-eval fixtures are synthetic test data (intentionally reproduce + // past incidents) — quarantined from markdownlint so their deliberate "bad" content + // does not trip style gates. See docs/adr/adr-049-incident-eval-regression-suite.md. + "tests/fixtures/incidents", + "tests/fixtures/incidents/**", "__*", "__*/**" ] diff --git a/CLAUDE.md b/CLAUDE.md index 22385c8d..426ab28b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -49,6 +49,7 @@ - [ADR-046: ローカル LLM pre-push レビュアー — 選定スパイクと不採用判断](docs/adr/adr-046-local-llm-review-spike.md) *(却下)* - [ADR-047: pre-push review の反証(refute)facet](docs/adr/adr-047-prepush-refute-facet.md) *(試験運用)* - [ADR-048: reviewers→fix findings handoff の output-contract 標準化(markdown 統一・JSON 却下)](docs/adr/adr-048-facet-findings-handoff-markdown-contract.md) *(試験運用)* +- [ADR-049: incident→eval 回帰スイート(カスタムルールの由来 incident 再現テスト)](docs/adr/adr-049-incident-eval-regression-suite.md) *(試験運用)* ## Build diff --git a/docs/adr/adr-049-incident-eval-regression-suite.md b/docs/adr/adr-049-incident-eval-regression-suite.md new file mode 100644 index 00000000..71c3f43a --- /dev/null +++ b/docs/adr/adr-049-incident-eval-regression-suite.md @@ -0,0 +1,107 @@ +# ADR-049: incident→eval 回帰スイート (カスタムルールの由来 incident 再現テスト) + +## ステータス + +試験運用 (2026-07-06) + +## コンテキスト + +カスタムリントルール ([.claude/custom-lint-rules.toml](../../.claude/custom-lint-rules.toml)、`hooks-post-tool-linter` が適用) は +12 本あり、うち **11 本は実 incident (過去 PR で発生した具体的な事故) を由来**とする ([ADR-007](adr-007-custom-linter-layer-boundary.md) の正規表現層)。 +既存の `rule_test_coverage_check` は「各ルールが対応 test 関数を宣言していること」をゲート化するが、 +**ルールが由来 incident を今も検出できるか (= ハーネス自身の退行)** を機械検出する仕組みは無かった。 + +WP-08 (`docs/harness-improvement-plan.md`) は、各ルールを生んだ実 incident を再現する fixture を整備し、 +それを hook に食わせて block/warn を assert する回帰スイートで「ハーネス自体の退行」を機械検出する。 + +## 決定 + +**incident→eval 回帰スイート**を導入する。標準的な 2 つの手法 — 回帰テスト (バグ修正には再現テストを付ける) と +linter の fixture corpus (ESLint / Clippy 等の「引っかかる例 / clean な例」) — の応用で、これに +**provenance ポリシー** (各ルールは由来 incident とその再現 fixture を機械可読に持つ) を重ねる。 + +### 1. provenance の構造化 ([rules.incident]) + +`[.claude/custom-lint-rules.toml](../../.claude/custom-lint-rules.toml)` の incident 由来 11 ルールに +`[rules.incident]` meta field を追加し、`CustomRule.incident: Option` として parse する: + +```toml +[rules.incident] +pr = 75 # 由来 incident の PR 番号 +bad_fixture = "no-personal-paths.md" # tests/fixtures/incidents/bad/ 配下 (fire すべき入力) +good_fixture = "no-personal-paths.md" # tests/fixtures/incidents/good/ 配下 (fire しない clean 入力) +adr = "adr-007" # 設計根拠 ADR (任意) +``` + +追跡鎖: **incident (PR) → rule (id) → fixture (bad/good) → regression test → ADR**。数年後に +「このルールは消してよいか」を問う開発者が全て辿れる。section を持たないルール (rule① `no-console-log` += 汎用サンプル、incident 由来でない) は fixture 要求から免除する (`NON_INCIDENT_RULES` allowlist)。 +これにより「12 ルール中 11 が incident 由来」という齟齬も明示的に扱える。 + +### 2. fixture 設計 (1 fixture = 1 failure mode + good/bad) + +`tests/fixtures/incidents/{bad,good}/` に配置。設計原則: + +- **1 fixture = 1 failure mode**: 各 bad fixture は該当ルールの incident パターン**のみ**含む + (LLVM / rustc の UI test 流儀)。将来「何が検出できなくなったか」を一点に絞れる。 +- **good (negative) fixture 必須**: bad は fire する、good (clean な対応) は fire しないことを両方保証し、 + 検出退行だけでなく **false positive 退行**も防ぐ (linter では同等に重要)。 +- **テストデータ明示**: 各 fixture 冒頭コメントで synthetic test data であることと由来 PR を明示。 + +### 3. Hook E2E test (実 exe spawn) + +[src/hooks-post-tool-linter/tests/incident_eval.rs](../../src/hooks-post-tool-linter/tests/incident_eval.rs) は +内部関数呼び出しではなく **実 exe を `CARGO_BIN_EXE_*` で spawn** し、`PostToolUse` JSON を stdin に流して +stdout を parse する。これで **arg/stdin パース → config → rule → feedback JSON → stdout** の全経路を通す +真の E2E になる (内部 API だけ呼ぶと exe の shell を通らない)。assert は **`type` / `severity` / `line` のみ**に +限定し (feedback 全文は固定しない)、文言修正で test が壊れない。 + +- パス filter付きルール (rule⑨ `takt-workflow-persona-without-model`、`paths = [".takt/workflows/*.yaml"]`) + は temp CWD 配下の `.takt/workflows/` に fixture を stage し相対パスで invoke して path filter も検証する。 +- exe は `custom-lint-rules.toml` を自身の隣から解決するため、test は deploy 済 toml を build 先へ copy する。 + +### 4. coverage gate (fail-closed) + +既存 `rule_test_coverage_check` と同じ crate 内 cargo test として `incident_fixture_coverage_check` を追加。 +各 incident 由来ルールが `[rules.incident]` を持ち bad/good fixture が**実在**することを **fail-closed** で強制する +(hard assert、[ADR-043](adr-043-security-gates-fail-closed.md))。fixture 欠落 / ルールが incident を検出しなくなった +場合は **cargo test が失敗**する — これは開発者向けの test 失敗であって、本番の Edit/Write を止めるものではない。 + +### 5. fixture の隔離 (ハーネス運用を壊さない) + +fixture は意図的に「悪い」内容を含むため、ハーネス自身のゲートから隔離する: + +- `src/**` の外 (repo-root `tests/fixtures/`) に置き、`deployed_tests.rs` の clean-baseline 走査 (`src/**/*.rs` / + `.takt/workflows/*.yaml`) に触れない。 +- `[.markdownlint-cli2.jsonc](../../.markdownlint-cli2.jsonc)` の ignores に追加。 +- `.rs` fixture のヘッダは comment-lint ([ADR-036](adr-036-bundle-z-three-layer-review.md) #B-α) に触れないよう + doc コメント (`//!`) を使う。 +- カスタムリンター自体は非致命 (exit 0、feedback のみ) のため、fixture を編集しても運用は止まらない。 + +## ADR-039 との関係 + +prompt/test 資産の追加であり、[ADR-039](adr-039-experimental-feature-standard-pattern.md) の config opt-in は +そのままは適用しない。bounded lifetime として、dogfood 期間で「fixture 追加忘れ / ルール検出退行を実際に +機械検出できた」ことを確認したら `試験運用` を解除する。可逆性は fixture / gate の revert で担保。 + +## 帰結 + +### 利点 + +- ルールの検出力退行と false positive 退行を cargo test で機械検出 (ハーネス自身の回帰スイート)。 +- 各ルールが由来 incident と再現 fixture を機械可読に持ち、削除可否判断が追跡可能。 +- 実 exe E2E で hook の全経路 (stdin/config/feedback/exit) を保証。 +- 本 repo 初の exe-spawn integration test パターンを確立 (WP-16 CI smoke test で流用可能)。 + +### 欠点 / 留意点 + +- fail-closed のため、incident 由来 11 ルール分の fixture を揃えて一括で land する必要がある (部分導入は gate 赤)。 +- exe-spawn は内部関数テストより遅い (22 spawn)。回帰網羅性とのトレードオフとして許容。 +- ルールの extensions / pattern を変更する際は対応 fixture も更新が必要 (既存 `rule_test_coverage_check` と同じ保守義務)。 + +### 関連 ADR + +- [ADR-007](adr-007-custom-linter-layer-boundary.md) — custom-linter 正規表現層 (対象 11 ルールの居所 + per-rule test checklist) +- [ADR-036](adr-036-bundle-z-three-layer-review.md) — Bundle Z 3 層 review (comment-lint #B-α、fixture ヘッダの制約源) +- [ADR-042](adr-042-rule-vs-mechanism-boundary.md) — rule vs 仕組み化 (「11 custom lint rule」の由来カウント) +- [ADR-043](adr-043-security-gates-fail-closed.md) — ゲートの fail-closed 原則 diff --git a/docs/harness-improvement-plan.md b/docs/harness-improvement-plan.md index f1b1fe4b..190f36ce 100644 --- a/docs/harness-improvement-plan.md +++ b/docs/harness-improvement-plan.md @@ -67,7 +67,7 @@ Anthropic 公式のハーネスエンジニアリング指針(決定論的基 | WP-05 | 1-A | Stop hook 高速化(nextest + 変更 crate 限定) | M | なし | 実装済(ADR-004 amendment: 逐次→並列で ~8s→~2s 約75%削減。nextest/変更crate は実測無効 → push pipeline 向け nextest を順位257へ) | | WP-06 | 1-B | 反証(refute)facet 追加 | S-M | なし | 実装済(ADR-047、導入は default OFF、dogfood 有効化はマージ後の別 PR) | | WP-07 | 1-B | facet 間受け渡しの JSON 化 | M | なし | 実装済(ADR-048: 公式調査で JSON 却下→markdown 契約標準化。simplicity-review 契約新設・reviewer 間で列統一) | -| WP-08 | 1-B | incident→eval 回帰スイート | S | なし | 未着手 | +| WP-08 | 1-B | incident→eval 回帰スイート | S | なし | 実装済(ADR-049: 11 incident 由来ルールに bad/good fixture + 実 exe E2E + fixture 必須 fail-closed gate) | | WP-09 | 1-C | PR 監視の GitHub Actions 化 Phase A(読み取り専用) | M | なし | 未着手 | | WP-10 | 1-C | 自律境界ポリシー ADR(ADR-028 の 2 段化) | S | なし | 未着手 | | WP-11 | 2 | prompt injection 信頼境界の 3 層防御 | M-L | WP-08 | 未着手 | @@ -170,6 +170,8 @@ Anthropic 公式のハーネスエンジニアリング指針(決定論的基 ### WP-08: incident→eval 回帰スイート +> **実装済 (2026-07-06、[ADR-049](adr/adr-049-incident-eval-regression-suite.md))**: 12 ルール中 **11 が実 incident 由来**(rule① no-console-log は汎用サンプルで免除)と判明。`[rules.incident]` (pr/bad_fixture/good_fixture/adr) を toml に構造化し、`tests/fixtures/incidents/{bad,good}/` に **1 fixture = 1 failure mode** の bad/good ペア(good = false positive 退行ガード)を整備。**実 exe を spawn する E2E** (`tests/incident_eval.rs`、rule⑨ は temp-CWD で paths filter も検証、assert は type/severity/line のみ) と、`incident_fixture_coverage_check` (incident 由来ルールに fixture 必須を **fail-closed** で強制) を追加。fixture は src/ 外・markdownlint 除外・`//!` doc ヘッダで隔離しハーネス運用を壊さない。追跡鎖 incident→rule→fixture→test→ADR を閉じた。以下は当初ステップ (記録用)。 + - **目的**: 「ハーネス自体の退行」を機械検出する。カスタムルール 12 本は全て実 incident(PR 番号)由来なので、逆方向の検証を仕組み化する。 - **ステップ**: 1. `tests/fixtures/incidents/` に由来 incident を再現する fixture を整備(例: rule② の由来である PR #75 の PII パス混入)。 diff --git a/src/hooks-post-tool-linter/Cargo.toml b/src/hooks-post-tool-linter/Cargo.toml index 3cc7c4ab..2ba0d0ca 100644 --- a/src/hooks-post-tool-linter/Cargo.toml +++ b/src/hooks-post-tool-linter/Cargo.toml @@ -15,5 +15,9 @@ lib-subprocess = { path = "../lib-subprocess" } [dev-dependencies] tempfile = "3" +# serde_json: WP-08 incident-eval E2E test (tests/incident_eval.rs) parses the hook's +# stdout HookOutput/LintViolation JSON. serde_json is also a [dependencies] entry but +# integration tests only see [dev-dependencies], so it is re-declared here. +serde_json = "1.0" # [profile.release] は workspace root (Cargo.toml) に集約 (ADR-026) 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 151390c7..a8dff9ef 100644 --- a/src/hooks-post-tool-linter/src/custom_rules/coverage.rs +++ b/src/hooks-post-tool-linter/src/custom_rules/coverage.rs @@ -235,3 +235,74 @@ fn rule_test_coverage_check() { gaps.join("\n - ") ); } + +/// WP-08 (ADR-049): incident 由来でない汎用ルール (現状 no-console-log = サンプル) を +/// fixture 要求から免除する allowlist。新規ルールは [rules.incident] + fixture を用意するか +/// ここに追加するかの二択を強制する (= 学習機会化)。 +#[cfg(test)] +const NON_INCIDENT_RULES: &[&str] = &["no-console-log"]; + +#[cfg(test)] +fn incident_fixtures_dir(kind: &str) -> std::path::PathBuf { + std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("..") + .join("..") + .join("tests") + .join("fixtures") + .join("incidents") + .join(kind) +} + +#[cfg(test)] +fn collect_incident_gaps(rule: &CustomRule) -> Vec { + let is_exempt = NON_INCIDENT_RULES.contains(&rule.id.as_str()); + let mut gaps: Vec = Vec::new(); + match (&rule.incident, is_exempt) { + (None, true) => {} + (None, false) => gaps.push(format!( + "rule `{}` has no `[rules.incident]` — every incident-derived rule must record its \ + originating PR + bad/good fixtures (or be added to NON_INCIDENT_RULES with justification)", + rule.id + )), + (Some(_), true) => gaps.push(format!( + "rule `{}` is listed in NON_INCIDENT_RULES yet declares `[rules.incident]` — remove one", + rule.id + )), + (Some(incident), false) => { + for (kind, name) in [("bad", &incident.bad_fixture), ("good", &incident.good_fixture)] { + let path = incident_fixtures_dir(kind).join(name); + if !path.exists() { + gaps.push(format!( + "rule `{}` (PR #{}) declares {}_fixture `{}` but no file exists at {}", + rule.id, + incident.pr, + kind, + name, + path.display() + )); + } + } + } + } + gaps +} + +/// WP-08 (ADR-049) incident→fixture ゲート: 各 incident 由来ルールが [rules.incident] で +/// 実 incident (PR) と bad/good fixture を宣言し、その fixture ファイルが実在することを +/// fail-closed で強制する。incident_eval.rs E2E test はその fixture を実 exe に食わせて +/// 検出/誤検知ゼロを検証し、両者で「ルール ⇔ incident ⇔ fixture ⇔ 回帰 test」の鎖を閉じる。 +#[cfg(test)] +#[test] +fn incident_fixture_coverage_check() { + let rules = load_deployed_custom_rules(); + let mut gaps: Vec = Vec::new(); + for rule in &rules { + gaps.extend(collect_incident_gaps(rule)); + } + assert!( + gaps.is_empty(), + "incident-eval fixture coverage gaps detected ({} issue(s)):\n - {}", + gaps.len(), + gaps.join("\n - ") + ); +} diff --git a/src/hooks-post-tool-linter/src/custom_rules/deployed_tests.rs b/src/hooks-post-tool-linter/src/custom_rules/deployed_tests.rs index 998ff1e0..dbe008fd 100644 --- a/src/hooks-post-tool-linter/src/custom_rules/deployed_tests.rs +++ b/src/hooks-post-tool-linter/src/custom_rules/deployed_tests.rs @@ -24,6 +24,7 @@ fn make_test_rule(id: &str, pattern: &str, extensions: &[&str]) -> CustomRule { good: "good code".into(), }), test_coverage: None, + incident: None, } } diff --git a/src/hooks-post-tool-linter/src/custom_rules/engine_tests.rs b/src/hooks-post-tool-linter/src/custom_rules/engine_tests.rs index de64fe5e..044997c8 100644 --- a/src/hooks-post-tool-linter/src/custom_rules/engine_tests.rs +++ b/src/hooks-post-tool-linter/src/custom_rules/engine_tests.rs @@ -28,6 +28,7 @@ pub(super) fn make_test_rule(id: &str, pattern: &str, extensions: &[&str]) -> Cu good: "good code".into(), }), test_coverage: None, + incident: None, } } diff --git a/src/hooks-post-tool-linter/src/custom_rules/rule_tests.rs b/src/hooks-post-tool-linter/src/custom_rules/rule_tests.rs index e3ec6064..b1578731 100644 --- a/src/hooks-post-tool-linter/src/custom_rules/rule_tests.rs +++ b/src/hooks-post-tool-linter/src/custom_rules/rule_tests.rs @@ -25,6 +25,7 @@ fn make_test_rule(id: &str, pattern: &str, extensions: &[&str]) -> CustomRule { good: "good code".into(), }), test_coverage: None, + incident: None, } } 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 d7927634..0694e161 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 @@ -24,6 +24,7 @@ fn make_test_rule(id: &str, pattern: &str, extensions: &[&str]) -> CustomRule { good: "good code".into(), }), test_coverage: None, + incident: None, } } diff --git a/src/hooks-post-tool-linter/src/custom_rules/types.rs b/src/hooks-post-tool-linter/src/custom_rules/types.rs index 6d653daa..b2940b75 100644 --- a/src/hooks-post-tool-linter/src/custom_rules/types.rs +++ b/src/hooks-post-tool-linter/src/custom_rules/types.rs @@ -51,6 +51,9 @@ pub(crate) struct CustomRule { #[serde(default)] #[allow(dead_code)] pub(crate) test_coverage: Option, + #[serde(default)] + #[allow(dead_code)] + pub(crate) incident: Option, } #[derive(Deserialize, Clone)] @@ -65,6 +68,31 @@ pub(crate) struct CustomRuleExample { pub(crate) good: String, } +/// `[rules.incident]` meta field。WP-08 (ADR-049) で導入。 +/// +/// ルールを生んだ実 incident (PR 番号) と、その incident を再現する回帰 fixture +/// (bad = ルールが fire すべき入力 / good = fire してはいけない clean 入力) を +/// 機械可読に記録する。`incident_fixture_coverage_check` cargo test が +/// 「incident 由来ルール ⇒ bad/good fixture が実在」を fail-closed で強制し、 +/// `tests/incident_eval.rs` E2E test が fixture を実 exe に stdin JSON で食わせて +/// 検出 (severity/type/line) と誤検知ゼロ (good) を検証する。 +/// +/// この section を持たないルール (例: no-console-log = 汎用サンプルで incident 由来 +/// でない) は fixture 要求から免除される (coverage check の NON_INCIDENT_RULES allowlist)。 +#[derive(Deserialize, Clone, Debug)] +#[allow(dead_code)] +pub(crate) struct CustomRuleIncident { + /// ルールを生んだ実 incident の PR 番号。 + pub(crate) pr: u64, + /// tests/fixtures/incidents/bad/ 配下の fixture ファイル名 (ルールが fire すべき入力)。 + pub(crate) bad_fixture: String, + /// tests/fixtures/incidents/good/ 配下の fixture ファイル名 (fire してはいけない clean 入力)。 + pub(crate) good_fixture: String, + /// 設計根拠となる ADR (例: "adr-007" = custom-lint 正規表現層)。任意。 + #[serde(default)] + pub(crate) adr: Option, +} + /// `[rules.test_coverage]` meta field。順位 137 (PR #163 T1-#1 採用) で導入。 /// /// 各 rule が「主要拡張子 (`rs` / `toml` / `yaml` / `yml`) のうち targets するもの」に対して diff --git a/src/hooks-post-tool-linter/tests/incident_eval.rs b/src/hooks-post-tool-linter/tests/incident_eval.rs new file mode 100644 index 00000000..f4ab9998 --- /dev/null +++ b/src/hooks-post-tool-linter/tests/incident_eval.rs @@ -0,0 +1,224 @@ +//! WP-08 (ADR-049) incident-eval E2E regression suite. +//! +//! Spawns the real `hooks-post-tool-linter` binary (via `CARGO_BIN_EXE_*`) and feeds +//! each incident fixture as a `PostToolUse` stdin JSON, then parses stdout. This +//! exercises the whole chain end-to-end (stdin parse -> config -> custom rules -> +//! feedback JSON -> stdout), unlike the in-crate unit tests that call `run_custom_rules` +//! directly. +//! +//! For every incident-derived rule: +//! - the `bad/` fixture MUST fire the rule (assert type + severity + line), and +//! - the `good/` fixture MUST fire nothing (false-positive regression guard). +//! +//! Fixtures live under `tests/fixtures/incidents/{bad,good}/` and are synthetic test +//! data reproducing the real incident each rule was created for (see the fixture +//! headers and `[rules.incident]` in `.claude/custom-lint-rules.toml`). + +use std::io::Write; +use std::path::{Path, PathBuf}; +use std::process::{Command, Stdio}; + +/// One incident-derived rule's E2E expectation. +struct Case { + /// Expected `LintViolation.type` (rule id upper-cased, hyphens -> underscores). + rule_type: &'static str, + /// Expected severity of the bad-fixture violation. + severity: &'static str, + /// Fixture file name, shared by `bad/` and `good/`. + fixture: &'static str, + /// 1-indexed line the bad fixture fires on. + expected_line: u64, + /// `Some(rel)` when the rule has a `.takt/workflows/*.yaml` path filter (rule 9): + /// the fixture must be staged at this repo-relative path under a temp CWD so the + /// path filter is exercised. `None` for rules without a path filter. + workflow_rel: Option<&'static str>, +} + +const CASES: &[Case] = &[ + Case { rule_type: "NO_PERSONAL_PATHS", severity: "error", fixture: "no-personal-paths.md", expected_line: 2, workflow_rel: None }, + Case { rule_type: "NO_EMPTY_POWERSHELL_CATCH", severity: "error", fixture: "no-empty-powershell-catch.ps1", expected_line: 2, workflow_rel: None }, + Case { rule_type: "NO_SILENT_ERROR_ACTION", severity: "warning", fixture: "no-silent-error-action.ps1", expected_line: 2, workflow_rel: None }, + Case { rule_type: "NO_MUTABLE_ANCHOR", severity: "warning", fixture: "no-mutable-anchor.md", expected_line: 2, workflow_rel: None }, + Case { rule_type: "NO_EPHEMERAL_TODO_REFERENCE", severity: "warning", fixture: "no-ephemeral-todo-reference.rs", expected_line: 2, workflow_rel: None }, + Case { rule_type: "NO_TIME_FIELD_STRICT_GREATER", severity: "warning", fixture: "no-time-field-strict-greater.rs", expected_line: 2, workflow_rel: None }, + Case { rule_type: "NO_DOCS_RELATIVE_BACK_TO_DOCS", severity: "error", fixture: "no-docs-relative-back-to-docs.md", expected_line: 2, workflow_rel: None }, + Case { rule_type: "TAKT_WORKFLOW_PERSONA_WITHOUT_MODEL", severity: "error", fixture: "takt-workflow-persona-without-model.yaml", expected_line: 4, workflow_rel: Some(".takt/workflows/incident-eval.yaml") }, + 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 }, +]; + +fn repo_root() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..").join("..") +} + +fn exe_path() -> PathBuf { + PathBuf::from(env!("CARGO_BIN_EXE_hooks-post-tool-linter")) +} + +fn fixture_path(kind: &str, name: &str) -> PathBuf { + repo_root() + .join("tests") + .join("fixtures") + .join("incidents") + .join(kind) + .join(name) +} + +/// The exe resolves `custom-lint-rules.toml` next to its own binary. Under `cargo test` +/// the freshly built binary lives in `target/debug/` without the deployed toml, so copy +/// it beside the exe before spawning (false-green guard on a missing source toml). +fn ensure_rules_toml_beside_exe() { + let src = repo_root().join(".claude").join("custom-lint-rules.toml"); + assert!( + src.exists(), + "deployed custom-lint-rules.toml missing at {} (false-green guard)", + src.display() + ); + let dst = exe_path() + .parent() + .expect("exe has a parent dir") + .join("custom-lint-rules.toml"); + std::fs::copy(&src, &dst) + .unwrap_or_else(|e| panic!("copy rules toml beside exe failed: {e}")); +} + +/// Stage a fixture for invocation. Returns a kept-alive temp dir (or `None`), the CWD to +/// spawn under, and the `file_path` to send in the hook JSON. +/// +/// - No path filter: pass the fixture's absolute path directly; CWD is irrelevant. +/// - Path filter (rule 9): copy the fixture to `/` and invoke the +/// relative path under `` so `paths = [".takt/workflows/*.yaml"]` matches. +fn stage(fixture: &Path, workflow_rel: Option<&str>) -> (Option, PathBuf, String) { + match workflow_rel { + None => ( + None, + repo_root(), + fixture.to_string_lossy().replace('\\', "/"), + ), + Some(rel) => { + let tmp = tempfile::tempdir().expect("create temp dir"); + let dst = tmp.path().join(rel); + std::fs::create_dir_all(dst.parent().expect("staged path has parent")) + .expect("create staged workflow dir"); + std::fs::copy(fixture, &dst).expect("stage workflow fixture"); + let cwd = tmp.path().to_path_buf(); + (Some(tmp), cwd, rel.to_string()) + } + } +} + +/// Spawn the linter exe with a `PostToolUse` stdin payload and return the custom-lint +/// `LintViolation` JSON objects parsed out of its stdout. +fn run_linter(cwd: &Path, invoke_path: &str) -> Vec { + let payload = serde_json::json!({ "tool_input": { "file_path": invoke_path } }).to_string(); + let mut child = Command::new(exe_path()) + .current_dir(cwd) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("spawn hooks-post-tool-linter"); + child + .stdin + .take() + .expect("child stdin") + .write_all(payload.as_bytes()) + .expect("write stdin payload"); + let out = child.wait_with_output().expect("wait for linter exe"); + let stdout = String::from_utf8_lossy(&out.stdout); + parse_custom_lint_violations(&stdout) +} + +/// Extract the `LintViolation` JSON objects embedded in the custom-lint layer's +/// `additionalContext` (other layers, if any, are ignored). +fn parse_custom_lint_violations(stdout: &str) -> Vec { + let mut violations = Vec::new(); + for line in stdout.lines() { + let Ok(envelope) = serde_json::from_str::(line.trim()) else { + continue; + }; + let Some(ctx) = envelope + .get("hookSpecificOutput") + .and_then(|h| h.get("additionalContext")) + .and_then(|c| c.as_str()) + else { + continue; + }; + if !ctx.contains("[custom-lint]") { + continue; + } + for inner in ctx.lines() { + let inner = inner.trim(); + if !inner.starts_with('{') { + continue; + } + if let Ok(v) = serde_json::from_str::(inner) { + violations.push(v); + } + } + } + violations +} + +/// bad fixture: the rule MUST fire, with the expected severity and line. +fn assert_bad_fixture_fires(case: &Case) { + let bad = fixture_path("bad", case.fixture); + assert!( + bad.exists(), + "bad fixture missing: {} (false-green guard)", + bad.display() + ); + let (_keep, cwd, invoke) = stage(&bad, case.workflow_rel); + let violations = run_linter(&cwd, &invoke); + let hit = violations + .iter() + .find(|v| v["type"] == case.rule_type) + .unwrap_or_else(|| { + panic!( + "rule {} did NOT fire on its bad fixture {} — harness regression! \ + custom-lint violations seen: {:?}", + case.rule_type, case.fixture, violations + ) + }); + assert_eq!( + hit["severity"], case.severity, + "rule {} fired with wrong severity on {}", + case.rule_type, case.fixture + ); + assert_eq!( + hit["location"]["line"].as_u64(), + Some(case.expected_line), + "rule {} fired on wrong line on {}", + case.rule_type, + case.fixture + ); +} + +/// good fixture: the rule MUST NOT fire (false-positive regression guard). +fn assert_good_fixture_clean(case: &Case) { + let good = fixture_path("good", case.fixture); + assert!( + good.exists(), + "good fixture missing: {} (false-green guard)", + good.display() + ); + let (_keep, cwd, invoke) = stage(&good, case.workflow_rel); + let violations = run_linter(&cwd, &invoke); + assert!( + violations.is_empty(), + "good fixture {} unexpectedly produced custom-lint violations \ + (false-positive regression): {:?}", + case.fixture, + violations + ); +} + +#[test] +fn incident_eval_all_incident_rules() { + ensure_rules_toml_beside_exe(); + for case in CASES { + assert_bad_fixture_fires(case); + assert_good_fixture_clean(case); + } +} diff --git a/tests/fixtures/incidents/bad/no-docs-relative-back-to-docs.md b/tests/fixtures/incidents/bad/no-docs-relative-back-to-docs.md new file mode 100644 index 00000000..e4845d4d --- /dev/null +++ b/tests/fixtures/incidents/bad/no-docs-relative-back-to-docs.md @@ -0,0 +1,2 @@ + +See [ADR-036](../docs/adr/adr-036-bundle-z.md) for the layering. diff --git a/tests/fixtures/incidents/bad/no-empty-powershell-catch.ps1 b/tests/fixtures/incidents/bad/no-empty-powershell-catch.ps1 new file mode 100644 index 00000000..43792d24 --- /dev/null +++ b/tests/fixtures/incidents/bad/no-empty-powershell-catch.ps1 @@ -0,0 +1,2 @@ +# WP-08 incident-eval fixture (synthetic test data). Reproduces PR #85 (no-empty-powershell-catch). +try { Get-Item $path } catch {} diff --git a/tests/fixtures/incidents/bad/no-ephemeral-todo-reference.rs b/tests/fixtures/incidents/bad/no-ephemeral-todo-reference.rs new file mode 100644 index 00000000..5c32a089 --- /dev/null +++ b/tests/fixtures/incidents/bad/no-ephemeral-todo-reference.rs @@ -0,0 +1,2 @@ +//! WP-08 incident-eval fixture (synthetic test data). Reproduces PR #94 (no-ephemeral-todo-reference). +//! See docs/todo5.md for the originating task entry. diff --git a/tests/fixtures/incidents/bad/no-hardcoded-jj-revset-range.rs b/tests/fixtures/incidents/bad/no-hardcoded-jj-revset-range.rs new file mode 100644 index 00000000..c96b87f1 --- /dev/null +++ b/tests/fixtures/incidents/bad/no-hardcoded-jj-revset-range.rs @@ -0,0 +1,2 @@ +//! WP-08 incident-eval fixture (synthetic test data). Reproduces PR #195 (no-hardcoded-jj-revset-range). +let revset = "master..@"; diff --git a/tests/fixtures/incidents/bad/no-jj-template-first-line.toml b/tests/fixtures/incidents/bad/no-jj-template-first-line.toml new file mode 100644 index 00000000..f1862f5e --- /dev/null +++ b/tests/fixtures/incidents/bad/no-jj-template-first-line.toml @@ -0,0 +1,2 @@ +# WP-08 incident-eval fixture (synthetic test data). Reproduces PR #175 (no-jj-template-first-line). +template = "change_id ++ description.first_line()" diff --git a/tests/fixtures/incidents/bad/no-mutable-anchor.md b/tests/fixtures/incidents/bad/no-mutable-anchor.md new file mode 100644 index 00000000..391853ad --- /dev/null +++ b/tests/fixtures/incidents/bad/no-mutable-anchor.md @@ -0,0 +1,2 @@ + +See [推奨順序](todo-summary.md#推奨実行順序) for details. diff --git a/tests/fixtures/incidents/bad/no-personal-paths.md b/tests/fixtures/incidents/bad/no-personal-paths.md new file mode 100644 index 00000000..6f949575 --- /dev/null +++ b/tests/fixtures/incidents/bad/no-personal-paths.md @@ -0,0 +1,2 @@ + +Config lives at `C:\Users\alice\.claude\projects\demo\` on this machine. diff --git a/tests/fixtures/incidents/bad/no-silent-error-action.ps1 b/tests/fixtures/incidents/bad/no-silent-error-action.ps1 new file mode 100644 index 00000000..c802a0cf --- /dev/null +++ b/tests/fixtures/incidents/bad/no-silent-error-action.ps1 @@ -0,0 +1,2 @@ +# WP-08 incident-eval fixture (synthetic test data). Reproduces PR #85 (no-silent-error-action). +$data = Get-Item $path -ErrorAction SilentlyContinue diff --git a/tests/fixtures/incidents/bad/no-time-field-strict-greater.rs b/tests/fixtures/incidents/bad/no-time-field-strict-greater.rs new file mode 100644 index 00000000..82ea0c5c --- /dev/null +++ b/tests/fixtures/incidents/bad/no-time-field-strict-greater.rs @@ -0,0 +1,2 @@ +//! WP-08 incident-eval fixture (synthetic test data). Reproduces PR #101 (no-time-field-strict-greater). +let recent = comments.iter().filter(|c| c.created_at > push_time); diff --git a/tests/fixtures/incidents/bad/no-write-result-discard.rs b/tests/fixtures/incidents/bad/no-write-result-discard.rs new file mode 100644 index 00000000..7d4b1f7b --- /dev/null +++ b/tests/fixtures/incidents/bad/no-write-result-discard.rs @@ -0,0 +1,2 @@ +//! WP-08 incident-eval fixture (synthetic test data). Reproduces PR #155 (no-write-result-discard). +let _ = write_state(&state); diff --git a/tests/fixtures/incidents/bad/takt-workflow-persona-without-model.yaml b/tests/fixtures/incidents/bad/takt-workflow-persona-without-model.yaml new file mode 100644 index 00000000..7ea33928 --- /dev/null +++ b/tests/fixtures/incidents/bad/takt-workflow-persona-without-model.yaml @@ -0,0 +1,5 @@ +# WP-08 incident-eval fixture (synthetic test data). Reproduces PR #98 (takt-workflow-persona-without-model). +steps: + - name: supervise + persona: supervisor + instruction: loop-monitor-reviewers-fix diff --git a/tests/fixtures/incidents/good/no-docs-relative-back-to-docs.md b/tests/fixtures/incidents/good/no-docs-relative-back-to-docs.md new file mode 100644 index 00000000..a0c3ef1d --- /dev/null +++ b/tests/fixtures/incidents/good/no-docs-relative-back-to-docs.md @@ -0,0 +1,2 @@ + +See [ADR-036](adr/adr-036-bundle-z.md) for the layering. diff --git a/tests/fixtures/incidents/good/no-empty-powershell-catch.ps1 b/tests/fixtures/incidents/good/no-empty-powershell-catch.ps1 new file mode 100644 index 00000000..fc93b8b8 --- /dev/null +++ b/tests/fixtures/incidents/good/no-empty-powershell-catch.ps1 @@ -0,0 +1,2 @@ +# WP-08 incident-eval fixture (synthetic test data). Clean counterpart for no-empty-powershell-catch (must NOT fire). +try { Get-Item $path } catch { Write-Verbose "expected miss: $_" } diff --git a/tests/fixtures/incidents/good/no-ephemeral-todo-reference.rs b/tests/fixtures/incidents/good/no-ephemeral-todo-reference.rs new file mode 100644 index 00000000..d2c79478 --- /dev/null +++ b/tests/fixtures/incidents/good/no-ephemeral-todo-reference.rs @@ -0,0 +1,2 @@ +//! WP-08 incident-eval fixture (synthetic test data). Clean counterpart for no-ephemeral-todo-reference (must NOT fire). +//! See docs/adr/adr-030-deterministic-post-merge-feedback.md for the rationale. diff --git a/tests/fixtures/incidents/good/no-hardcoded-jj-revset-range.rs b/tests/fixtures/incidents/good/no-hardcoded-jj-revset-range.rs new file mode 100644 index 00000000..19ec318b --- /dev/null +++ b/tests/fixtures/incidents/good/no-hardcoded-jj-revset-range.rs @@ -0,0 +1,2 @@ +//! WP-08 incident-eval fixture (synthetic test data). Clean counterpart for no-hardcoded-jj-revset-range (must NOT fire). +let revset = format!("{}..@", default_branch); diff --git a/tests/fixtures/incidents/good/no-jj-template-first-line.toml b/tests/fixtures/incidents/good/no-jj-template-first-line.toml new file mode 100644 index 00000000..e087280b --- /dev/null +++ b/tests/fixtures/incidents/good/no-jj-template-first-line.toml @@ -0,0 +1,2 @@ +# WP-08 incident-eval fixture (synthetic test data). Clean counterpart for no-jj-template-first-line (must NOT fire). +template = "change_id ++ if(empty, \"EMPTY\", \"CONTENT\")" diff --git a/tests/fixtures/incidents/good/no-mutable-anchor.md b/tests/fixtures/incidents/good/no-mutable-anchor.md new file mode 100644 index 00000000..de4aa44e --- /dev/null +++ b/tests/fixtures/incidents/good/no-mutable-anchor.md @@ -0,0 +1,2 @@ + +See [order](todo-summary.md#recommended-order) for details. diff --git a/tests/fixtures/incidents/good/no-personal-paths.md b/tests/fixtures/incidents/good/no-personal-paths.md new file mode 100644 index 00000000..fc8f1936 --- /dev/null +++ b/tests/fixtures/incidents/good/no-personal-paths.md @@ -0,0 +1,2 @@ + +Config lives at `%USERPROFILE%\.claude\projects\demo\` on this machine. diff --git a/tests/fixtures/incidents/good/no-silent-error-action.ps1 b/tests/fixtures/incidents/good/no-silent-error-action.ps1 new file mode 100644 index 00000000..4df82cee --- /dev/null +++ b/tests/fixtures/incidents/good/no-silent-error-action.ps1 @@ -0,0 +1,2 @@ +# WP-08 incident-eval fixture (synthetic test data). Clean counterpart for no-silent-error-action (must NOT fire). +$data = Get-Item $path -ErrorAction Stop diff --git a/tests/fixtures/incidents/good/no-time-field-strict-greater.rs b/tests/fixtures/incidents/good/no-time-field-strict-greater.rs new file mode 100644 index 00000000..165c9a72 --- /dev/null +++ b/tests/fixtures/incidents/good/no-time-field-strict-greater.rs @@ -0,0 +1,2 @@ +//! WP-08 incident-eval fixture (synthetic test data). Clean counterpart for no-time-field-strict-greater (must NOT fire). +let recent = comments.iter().filter(|c| c.created_at >= push_time); diff --git a/tests/fixtures/incidents/good/no-write-result-discard.rs b/tests/fixtures/incidents/good/no-write-result-discard.rs new file mode 100644 index 00000000..4cf040e6 --- /dev/null +++ b/tests/fixtures/incidents/good/no-write-result-discard.rs @@ -0,0 +1,2 @@ +//! WP-08 incident-eval fixture (synthetic test data). Clean counterpart for no-write-result-discard (must NOT fire). +if let Err(e) = write_state(&state) { log_warn(&format!("state write failed: {e}")); } diff --git a/tests/fixtures/incidents/good/takt-workflow-persona-without-model.yaml b/tests/fixtures/incidents/good/takt-workflow-persona-without-model.yaml new file mode 100644 index 00000000..4bc86707 --- /dev/null +++ b/tests/fixtures/incidents/good/takt-workflow-persona-without-model.yaml @@ -0,0 +1,6 @@ +# WP-08 incident-eval fixture (synthetic test data). Clean counterpart for takt-workflow-persona-without-model (must NOT fire). +steps: + - name: supervise + persona: supervisor + model: sonnet + instruction: loop-monitor-reviewers-fix