diff --git a/docs/local-llm-offload-analysis.md b/docs/local-llm-offload-analysis.md index c4cab79..5fa7cbd 100644 --- a/docs/local-llm-offload-analysis.md +++ b/docs/local-llm-offload-analysis.md @@ -2,7 +2,7 @@ > **位置づけ**: 本ファイルは「残作業の **次に何をするか** だけ」を持つ実行計画。完了済みの分析・実装・dogfood 計測・retrospective は [local-llm-offload-history.md](local-llm-offload-history.md) に切り出した。 > -> **状態**: 試験運用 (Phase a 完了 = PR #130 land / Phase b 完了 = conditional GO 2026-05-08, PR #131 / Phase c MVP 完了 = PR #132 land 2026-05-08 / **Phase c+ Bundle i 完了 = PR #135 land 2026-05-09 / §8.D 完了 = PR #136 land 2026-05-09 (num_ctx 8192、agreement 86.7% / verdict GO) / Phase d kickoff prep 完了 = 2026-05-10 / Phase d Round 1 完遂 = 2026-05-12 (D-1〜D-3、実 dogfood は D-3 単独 1 data point) / Phase d Round 2 着手 = 2026-05-13 (D-4〜D-7、累積 5 PR で Phase E gate 充足見込み)** ([docs/local-llm-offload-phase-d-guide.md](local-llm-offload-phase-d-guide.md) 参照))。 +> **状態**: 試験運用 (Phase a 完了 = PR #130 land / Phase b 完了 = conditional GO 2026-05-08, PR #131 / Phase c MVP 完了 = PR #132 land 2026-05-08 / **Phase c+ Bundle i 完了 = PR #135 land 2026-05-09 / §8.D 完了 = PR #136 land 2026-05-09 (num_ctx 8192、agreement 86.7% / verdict GO) / Phase d kickoff prep 完了 = 2026-05-10 / Phase d Round 1 完遂 = 2026-05-12 (D-1〜D-3、実 dogfood は D-3 単独 1 data point) / Phase d Round 2 進行中 = 2026-05-13 (D-4 完遂 → PR #150 merged、累積 3 data points / 5 PR 計画、残 D-5/D-6/D-7)** ([docs/local-llm-offload-phase-d-guide.md](local-llm-offload-phase-d-guide.md) 参照))。 > > **引退条件**: 以下のいずれかで本ファイルを削除する (docs-governance.md retirement workflow 準拠)。`local-llm-offload-history.md` も同タイミングで判断する。 > - 残作業 (§8.D / §8.E / §8.F, §1 Phase b/c/d) が **すべて land または却下** された場合 → permanent value (採用された設計判断、却下理由) を ADR-038 に migrate して両ファイルを削除 @@ -229,7 +229,7 @@ Phase A 実装後、PR #141 (P-3 = 187 行 mixed diff) を replay → **`prompt_ `src/cli-push-runner/src/stages/lint_screen.rs` 改修: graceful fallback (exit 0) 時にも classifier stderr を `.takt/lint-screen-report.md` の `## Diagnostic` section に取込。Phase A 診断 warn log が **real pipeline 経由で visible** になる状態を確保。新 struct `ClassifierOutput { stdout, stderr }`、新 helper `render_diagnostic`、新規 smoke test 4 件 (TP / FP / edge case / parse-error path) で contract を seal。lint_screen tests 14/14 pass + workspace 全 cargo test pass。 -##### 🔄 Phase D: Clean dogfood validation (real pipeline 経由、Round 1 完遂 2026-05-12 / Round 2 D-4〜D-7 着手中 2026-05-13) +##### 🔄 Phase D: Clean dogfood validation (real pipeline 経由、Round 1 完遂 2026-05-12 / Round 2 進行中 = D-4 完遂 2026-05-13 / 残 D-5/D-6/D-7) Phase C fix + Phase D 前提整備 (順位 109) 完了で **real pipeline 経由 dogfood の必要十分条件が揃った**。D-1 着手時に session-only opt-in workflow が jj auto-snapshot と本質的に衝突する gap が判明したが、**順位 115 (`LINT_SCREEN_ENABLED` env var override) land で解消**。env var 経路 (`$env:LINT_SCREEN_ENABLED = "true"`) で `push-runner-config.toml` を編集せずに lint_screen を有効化できるため、D-3 で初の実 dogfood が成立し、計画 3 PR + prereq 1 PR がすべて land 完了。 @@ -279,8 +279,8 @@ Round 1 で実 dogfood data point が **1 件のみ** (D-3) に留まり、ADR-0 | Order | 構成 (todo-summary.md priority list より) | Tier / Effort | 推定 diff 行 | Diff Profile | 状態 | |---|---|---|---|---|---| -| **D-4** | 順位 39 単独 = takt workflow `model` 必須化 lint rule + 副次作業 (`.takt/workflows/*.yaml` の `persona:` 行 3 件に `model:` 明示追加で clean baseline 確保) | T1 / S | ~150-280 | Rust lint rule (yaml multi-line regex) + 3-5 unit tests + custom-lint-rules.toml entry + 3 yaml site touch | 未着手 | -| **D-5** | 順位 56 + 119 bundle = comment-lint hook test 拡充 + `MAX_CUSTOM_VIOLATIONS` outer/inner loop break scope explicit test | T2+T2 / S+S | ~200-350 | hooks-post-tool-linter test infra (UTF-8 multi-byte 5 パターン + block boundary 6 パターン + MAX cap regression test) | 未着手 | +| **D-4** ✅ | 順位 39 単独 = takt workflow `model` 必須化 lint rule + 副次作業 (`.takt/workflows/*.yaml` の `persona:` 行 3 件に `model:` 明示追加で clean baseline 確保) + CR Major fix で 4 fields 追加 | T1 / S | 実 ~340 行 (commit 0c2cc07d + 1ec15686) | Rust lint rule (yaml multi-line regex、enumeration 方式) + 6+1 unit tests + custom-lint-rules.toml entry + 3 yaml site touch + CR Major fix | **PR #150 merged 2026-05-13、初 real lint_screen 観測 2 data points (initial + CR fix push)** | +| **D-5** ✅ | 順位 56 + 119 bundle = comment-lint hook test 拡充 + `MAX_CUSTOM_VIOLATIONS` outer/inner loop break scope explicit test + 副産物として `byte_offset_to_line` の char-boundary panic bug fix | T2+T2 / S+S | 実 ~120 行 (test 12 件追加 + production fix 1 行) | hooks-post-tool-comment-lint-rust + hooks-post-tool-linter test infra (UTF-8 multi-byte 5 + block boundary 6 + multi-rule MAX cap 2 + direct unit test 1) | **着手済** | | **D-6** | 順位 51 単独 = `.takt/review-diff.txt` を fix→review iteration 間で refresh | T1 / M | ~400-580 | cli-push-runner Rust impl + iteration logic refactor + integration tests | 未着手 | | **D-7** | Bundle c-1 (順位 63 + 64 + 67) = cli-merge-pipeline Drop guard / signal handler + orphan run reaper + ADR-030 spec amendment | T1×2 + T3 / M+M+XS | ~600-800 | Rust impl + signal trap + reaper logic + ADR markdown | 未着手 | @@ -299,15 +299,51 @@ Round 1 で実 dogfood data point が **1 件のみ** (D-3) に留まり、ADR-0 - **detail 見積もりの精度**: 各 todo`N`.md の詳細 (実装方針 / acceptance criteria) を未参照、着手時に scope 修正の必要あり - **D-4 の re-pivot 経緯 (2026-05-13)**: 当初 D-4 = 順位 47 (`>` vs `>=` boundary lint) を予定していたが、着手直前 (memory rule `feedback_verify_task_not_already_done.md` 適用) で **PR #126 (commit `b677b9d4f54d`) で既に land 済 (`no-time-field-strict-greater` rule、custom-lint-rules.toml line 208-243)** を発見。D-5 から 順位 39 を D-4 に繰上げ、D-5 を 順位 56 + 119 bundle に再構成。stale todo7.md 順位 47 entry は同 PR の docs commit で削除 -**Phase D Round 1 完遂 + Round 2 計画策定後の Phase E 判定材料**: +**D-4 dogfood outcome (Phase D Round 2 初の real lint_screen 観測、PR #150)**: -- ✅ pipeline integration works end-to-end (D-1 #144 smoke test + D-3 real diff 完走) -- ✅ num_ctx 32768 で 270 行 Rust diff overflow なし (Phase C reference values と整合) -- ✅ fallback rate < 50% (D-3 で 0/1、単発) -- ⚠️ agreement: 1 false positive 観測 (Phase b' 75% 想定範囲内、単発観測) -- 🔄 **累積 PR data 充足中**: Round 1 で 1 PR (D-3) 取得済、Round 2 (D-4〜D-7) で 4 PR 追加観測予定 → **累積 5 PR で ADR-038 採用条件「5 PR 以上」+ analysis.md「3-5 PR 累積」前提を充足見込み** +PR #150 は同一 PR 内で **2 push event** が発生し、それぞれ独立した lint_screen dogfood data point を生成した: -Phase E 着手の前提条件は **3-5 PR 累積 dogfood**。D-3 (1 PR 取得済) + Round 2 (D-4〜D-7、4 PR 追加観測予定) で計画上 **累積 5 PR** に到達する見込み。各 PR push 時に `$env:LINT_SCREEN_ENABLED=true` を opt-in で set し、`.takt/lint-screen-report.md` を post-push で記録する運用を継続。 +| Push event | commit | screen_decision | findings | fallback | num_ctx overflow | latency 推定 | +|---|---|---|---|---|---|---| +| 初回 push (D-4 impl) | `0c2cc07d` | **`informational`** | 0 | なし | なし | ~10-15s (pipeline 総 645s) | +| CR Major fix re-push | `1ec15686` | **`auto_fix`** | 1 (FP: TOML に Rust `unused-import` 誤検出) | なし | なし | ~10-15s (pipeline 総 583s) | + +**D-4 観測の意義**: + +1. **`informational` verdict の初観測**: D-3 (`auto_fix` + 1 FP) と異なる「指摘なし」経路を実証。lint_screen の判定空間 2 経路 (auto_fix / informational) を D-3 + D-4 で本セッション内にカバー、特定 verdict への偏りバイアスが無いことを確認 +2. **same-PR 2 push の independent dogfood**: CR Major fix re-push でも pipeline が独立に走り、新 data point を生成。Phase E 累積カウントへの直接寄与は 1 PR = 1 と数えるが、verdict variance 観測材料としては 2 data points として有効 +3. **CR Major auto-fix の構造的成功**: persona 直後の field 列挙不足 (`output_contracts` / `pass_previous_response` / `required_permission_mode` / `parallel`) を CR が指摘 → memory rule `feedback_review_severity_auto_fix.md` 適用で auto-fix → regression test 同梱 land。CR Minor は `resolved:` reply で auto-resolve (memory `project_coderabbit_auto_resolve.md`) +4. **post-merge-feedback 採用 3 件**: 順位 120 (rule comment + ADR-007 case study、Tier 1→3 reclassify) / 順位 121 (3 fields の individual fixture test 追加、Tier 2) / 順位 122 (`development-workflow.md` Step 0 への stale-entry 確認 step 追加、Tier 3) を `docs/todo8.md` に登録 + `docs/todo-summary.md` table 反映済 (commit 0c2cc07d-merged → 39ae2cd1) +5. **副産物 (D-4 セッション知見)**: post-merge-feedback analyzer の Tier 分類が誤りやすい構造を発見 (rule コメント追記を Tier 1 と分類) → memory `feedback_tier_classification.md` に正しい Tier 定義 (mechanical enforcement = T1 / docs 修正 = T3) を codify + +**D-5 dogfood outcome (Phase D Round 2 二件目の real lint_screen 観測、PR #TBD)**: + +D-4 と同様、D-5 も同一 PR 内で **2 push event** が発生し、それぞれ独立した lint_screen dogfood data point を生成: + +| Push event | commit | screen_decision | findings | fallback | num_ctx overflow | lint_screen latency | pipeline 総時間 | +|---|---|---|---|---|---|---|---| +| 初回 push (D-5 impl、~649 行 Rust diff) | `5cbed3c3` | **`auto_fix`** | 1 (FP: comment-lint-rust line 1 を `use std::io::Write;` 誤認、実 line 1 は `//!` doc comment) | なし | なし | 54s | 679s (takt review 5m 23s) | +| 2 回目 push (docs-only outcome record、~67 行 analysis.md 更新) | `9458660b` | **`auto_fix`** | 1 (同 FP 再現: docs-only diff にも関わらず Rust file hallucinate、構造的 root cause) | なし | なし | ~推定 30-50s | 522s (takt review 2m 44s、docs-only scope で executable criteria waived) | + +**D-5 観測の意義**: + +1. **`auto_fix` verdict 4 件目**: D-3 + D-4 CR fix + D-5 (2 push events) と同じ verdict 経路、false positive pattern も file/scope 混同で共通 (mistral:7b の構造的特徴) +2. **reviewer による cross-check の構造的成功**: simplicity-review が "Lint Screen Cross-Check" section で初回 finding を **明示的に false positive と判定** + 根拠 (line 1 直接読取 + use 文の実 location 特定 = `hooks-post-tool-linter` 側 line 1131/1163) を report に記載。Phase b' agreement 75% 設計通りに reviewer が独立判断する advisory consumption が functional +3. **docs-only diff でも同 FP 再現**: 2 回目 push は analysis.md ~67 行のみで Rust file の変更ゼロにも関わらず、mistral:7b が同じ FP を出力。**lint_screen の FP は diff 内容ではなく hook のソース全文を見て hallucinate している強い証拠** → mistral:7b の context window 内に hook source の周辺コードが含まれ、過去 commit の `use std::io::Write;` (test fn 内) を unused と推論。Phase b' scale-aware fixture では捕捉できない failure mode +4. **副産物 (D-5 セッション知見、production bug fix)**: UTF-8 漢字単独 test 着手時に `byte_offset_to_line` の char-boundary panic bug を発見 → 1-line fix で resolve、direct unit test も追加。AI 編集で multi-byte 文字で終わる `new_string` (例: 「漢字のみのコメント」末尾改行なし) を渡すと従来 hook が panic していたのを構造的に修正。test 拡充が production fault detection に直結した事例 +5. **lint_screen agreement の累積観測 (5 観測中 4 FP)**: D-3 / D-4 CR fix / D-5 (×2) はいずれも file-type / scope 混同型 FP で同 root cause 推定。Phase b' agreement 75% 想定からは過大な FP 率 (~80%) だが、severity が全て `minor` で reviewer cross-check による blocking なし → 運用 viable +6. **副産物 (D-5 push workflow 知見)**: 初回 push 後の outcome record 追記を「local @ in-place edit → split」で扱うと force-push 必要になる jj workflow gap を発見。代替策として `jj new @origin` で remote bookmark の child commit を直接作成 → FF push で advance する手順を実証。本 PR の 2 push 構成自体がこの workflow validation の dogfood + +**Phase D Round 1 完遂 + Round 2 D-4 + D-5 完遂後の Phase E 判定材料**: + +- ✅ pipeline integration works end-to-end (D-1 #144 smoke test + D-3 #148 + D-4 #150 + D-5 ×2 で計 5 real diff 完走) +- ✅ num_ctx 32768 で 67-649 行 diff overflow なし (Phase C reference values と整合、D-5 docs-only 67 行 〜 D-5 impl 649 行で size 帯拡大) +- ✅ fallback rate < 50% (D-3 0/1、D-4 initial 0/1、D-4 CR fix 0/1、D-5 ×2 0/2 = 累積 0/5 = 0%) +- ⚠️ agreement: 累積 false positive 4 件観測 (D-3 / D-4 CR fix / D-5 ×2) — いずれも `minor` severity で reviewer cross-check 通過、blocking なし。D-5 観測で「diff 外 context から hallucinate する failure mode」を新発見 +- ✅ verdict variance: `auto_fix` (D-3 + D-4 CR fix + D-5 ×2) と `informational` (D-4 initial) の 2 経路を観測 +- 🔄 **累積 PR data 充足中**: Round 1 (D-3) + Round 2 (D-4 + D-5) で **5 data points (3 PR)** 取得済、累積 5 PR (= ADR-038 採用条件) は **3 PR 段階で先行充足**。残 D-6/D-7 で更に観測予定 + +Phase E 着手の前提条件は **3-5 PR 累積 dogfood**。D-3 (1) + D-4 (1) + 残 D-5/D-6/D-7 (3 PR 計画) で計画上 **累積 5 PR** に到達する見込み。D-4 で「2 push event = 2 data points」が同 PR で発生する pattern を実証したため、Round 2 の残 PR でも CR fix 経由の追加観測が見込まれる。各 PR push 時に `$env:LINT_SCREEN_ENABLED=true` を opt-in で set し、`.takt/lint-screen-report.md` を post-push で記録する運用を継続。 **Phase D 計測手順** (各 PR 共通): diff --git a/docs/todo-summary.md b/docs/todo-summary.md index 00874aa..8c23081 100644 --- a/docs/todo-summary.md +++ b/docs/todo-summary.md @@ -43,7 +43,6 @@ | 49 | 🔧 Tier 2 | **`parse_findings` 系の error-path test infrastructure (PR #101 T2-1) ★ Bundle a Sub-PR 2** | todo7.md | M | 順位 42 / 43 / 46 と同 PR (Sub-PR 2、`unwrap_or_else(\|_\| empty)` silent fail 抑止 + cli-pr-monitor mock infra 流用) | | 51 | 🚀 Tier 1 | **`.takt/review-diff.txt` を fix→review iteration 間で refresh (PR #103 観測)** | todo7.md | M | なし (PR #103 で stale-diff false positive による wasted iter ×2 = ~10 分浪費を実観測、6-iter outlier の構造的根因対策、Bundle Z 3 層では塞げない独立改善) | | 52 | 💎 Tier 3 | **comment-lint hook の MultiEdit 対応 (順位 50 follow-up)** | todo7.md | S | なし (順位 50 で v1 = Edit のみ実装、MultiEdit は whole-file fallback で no-regression、利用頻度低く優先度は低) | -| 56 | 🔧 Tier 2 | **comment-lint hook test 拡充 (PR #104 T2-1+T2-2 bundle)** | todo7.md | S | なし (UTF-8 multi-byte 5 パターン + block comment boundary 6 パターンを `locate_string_line_ranges` / `span_overlaps_ranges` の回帰テストとして体系化、PR #104 Critical/Minor fix の固定化) | | 57 | 🔧 Tier 2 | **Aggregation cap integration test (PR #105 T2-1 採用)** | todo7.md | S | なし (`collect_all_violations` の MAX_VIOLATIONS contract を test 化、将来の lint 追加時に `truncate(MAX)` 削除 regression を防止する explicit 安全網) | | 60 | 💎 Tier 3 | **analyze-session の transcript filter 絞り込み (旧 #A-3)** | todo7.md | M | なし (旧 docs/pipeline-token-efficiency.md #A-3、ADR-036/037 化に伴い計画書削除、本 task のみ todo に移管。analyze-session の input range を PR 作成 commit〜merge に限定して input token 30-50% 削減見込み、dogfood で実測必要) | | 61 | 🔧 Tier 2 | **post-PR 検証フローに CR review.body 手動スキャン step 追加 (PR #108 T2-1 採用)** | todo7.md | XS | なし (PR #108 で analyze-coderabbit が review body の outside diff range comment を検出漏れし line 371/378 の修正が後追い、blind spot の暫定緩和策として手動 checklist を整備) | @@ -75,7 +74,9 @@ | 116 | 💎 Tier 3 | **ADR-040 `step_timeout` 説明に sublinear / KV cache locality clarification 追記 (PR #145 T3-#1 採用)** | todo8.md | XS | なし (L42-48 で「sublinear (3.33x)」と「per-invoke latency が概ね線形」が並存し reference table 600s と formula 720s が乖離。実測値 600s 採択 + 保守上限 720s + sublinear 性の KV cache locality 根拠を 2-3 行追記して整合化、永続 ADR の数値正確性確保) | | 117 | 💎 Tier 3 | **`coding-style.md § Cross-File Reference Lifecycle` に ephemeral → permanent 知識移管 edit order 追記 (PR #145 T3-#3 採用)** | todo8.md | S | なし (PR #145 で lib.rs L128-139 → ADR-040 移管 + Phase C/D empirical data 移管の 2 観測。既存ルール (参照方向制約) と complementary な「① permanent target 先行作成・validate → ② 参照追加 → ③ 参照元削除」3 ステップ原則を `~/.claude/rules/common/coding-style.md` に codify、次回 ephemeral 計画書 retire 時の checklist として再利用) | | 118 | 💎 Tier 3 | **rule⑧ への paths filter 適用範囲検討 (順位 102 land 時の意図的保留、follow-up)** | todo8.md | XS | 順位 102 (PR #148 land 済、Phase D D-3) で paths filter は実装済だが、rule⑧ への `paths = ["docs/**/*.md"]` migration は D-2 (PR #146、順位 101) で追加した root-level MD fire intent を壊すため保留。4 案 (保留継続 / broader glob / explicit list / rule split) の trade-off 評価を ADR-007 amendment (順位 104) と整合させて結論を出す | -| 119 | 🔧 Tier 2 | **`MAX_CUSTOM_VIOLATIONS` outer/inner loop break scope を explicit test で seal (PR #148 T2-1 採用)** | todo8.md | S | なし (PR #148 で `run_custom_rules` refactor 中に発見した bug fix = inner break が outer に伝播しない問題、takt reviewer が "Behavioral change: improvement" と評価。複数 rule 実行時に violation cap が正確に機能することを explicit test で seal、将来 refactor 時の regression 防止) | +| 120 | 💎 Tier 3 | **`takt-workflow-persona-without-model` rule コメント拡張 + ADR-007 case study 追記 (PR #150 T1-#1 採用、実体 Tier 3 — analyzer 誤分類)** | todo8.md | XS | なし (実体は docs/comment 修正のみで mechanical enforcement なし → Tier 3 reclassify。次回 takt yaml schema 拡張時の rule 更新フロー文書化 + enumeration-based pattern の case study 記録、Tier 1 #2 ADR case study と同 PR で land 効率的) | +| 121 | 🔧 Tier 2 | **`takt_workflow_persona_detects_required_permission_mode_violation` doc 修正 + 残り 3 fields 個別 fixture test 追加 (PR #150 T2-#1 採用)** | todo8.md | S | なし (4 fields のうち 3 fields は個別 fixture test 不在、doc comment と実態乖離。pass_previous_response / output_contracts / parallel の individual test を追加して将来 regex 変更時の regression 検知を担保) | +| 122 | 💎 Tier 3 | **`development-workflow.md` Step 0 に「新 todo 着手前の既実装確認」チェックステップ追加 (PR #150 T3-#1 採用)** | todo8.md | XS | なし (memory rule `feedback_verify_task_not_already_done.md` を canonical workflow へ昇格。`jj log --limit 20 ` は決定的コマンドのため `feedback_no_unenforced_rules.md` 例外 = 既存実践の明文化 + 機械実行可能で採用、グローバル設定変更前に `~/.claude/` バックアップ取得必須) | **戦略**: Tier 1 を 2〜3 セッションで片付け → Tier 2 で ADR-032 の前提 + rate-limit + convergence cost 削減を進める → Tier 3 で ADR-032 を land + ドキュメント整備。Tier 4-5 は cleanup / 外部展開で daily efficiency への直接効果は小さい。 diff --git a/docs/todo7.md b/docs/todo7.md index ea23a5a..20f0708 100644 --- a/docs/todo7.md +++ b/docs/todo7.md @@ -133,48 +133,6 @@ --- - -### comment-lint hook test 拡充 (PR #104 T2-1+T2-2 bundle) - -> **動機**: PR #104 で CodeRabbit Critical (UTF-8 byte boundary) + Minor (multi-line block comment boundary) の 2 件を auto-fix で解消したが、いずれも回帰防止テストは 1 パターンのみで脆い。tree-sitter / Rust version 更新で区間交差判定や UTF-8 境界処理が壊れた場合に検出できないリスク。 -> -> **本タスクの位置づけ**: PR #104 post-merge-feedback Tier 2-1 / Tier 2-2 の bundle。コスト低 (S effort)、test additions のみで scope clean、PR #104 の fix を体系的に固定化する。 -> -> **参照**: `.claude/feedback-reports/104.md` Tier 2 #1, #2、PR #104 (`src/hooks-post-tool-comment-lint-rust/src/main.rs` の `locate_string_line_ranges` / `span_overlaps_ranges`) -> -> **実行優先度**: 🔧 **Tier 2** — Effort S。Bundle b と独立、いつでも単独着手可。 - -#### 設計決定 (案) - -- **UTF-8 multi-byte test 拡充** (T2-1): - - 現状: `locate_string_line_ranges_handles_multibyte_utf8` 1 パターン - - 追加 5 パターン: 漢字 + ASCII 混合 / 漢字単独 / emoji / BMP 外文字 (例: 𝕊) / 結合文字 (例: é = e + ́) - - 各パターンで `search_start = (absolute + needle.len()).min(source.len())` の境界処理を検証 -- **Block comment boundary matrix 拡充** (T2-2): - - 現状: `find_violations_multiline_block_comment_spanning_range_boundary` 1 パターン - - 追加 6 パターン: {開始行のみ被覆, 終了行のみ被覆, 内部完全包含} × {単行 block comment, 複数行 block comment} - - `span_overlaps_ranges(start, end, ranges)` の区間交差判定を体系化 - -#### 作業計画 - -- [ ] UTF-8 multi-byte test 5 パターン追加 -- [ ] Block comment boundary test 6 パターン追加 -- [ ] 既存 1 パターンずつのテストは保持 (regression 防止のため削除しない) -- [ ] 派生プロジェクト deploy は不要 (test のみのため) -- [ ] 本 todo7.md エントリを削除 - -#### 完了基準 - -- UTF-8 multi-byte test が 6 パターン以上 -- Block comment boundary test が 7 パターン以上 -- `cargo test -p hooks-post-tool-comment-lint-rust` 全 pass - -#### 詰まっている箇所 - -- 結合文字 (`e + ́`) を `new_string` に含むケースは Edit tool が実環境で発生するか不明 (理論的検証としては有効、実際の回帰防止としては効果薄の可能性)。1 パターンで足る - ---- - ### Aggregation cap integration test (PR #105 T2-1 採用) > **動機**: PR #105 の auto-fix で `collect_all_violations` に `violations.truncate(MAX_VIOLATIONS)` を追加した (CodeRabbit Minor finding 解消) が、これは contract の暗黙化に過ぎない。将来 `find_xxx_violations` を追加する PR で `extend()` の後に `truncate` を入れ忘れる regression を構造的に防ぐ test がない。 diff --git a/docs/todo8.md b/docs/todo8.md index b3cd28c..416ebbb 100644 --- a/docs/todo8.md +++ b/docs/todo8.md @@ -10,54 +10,98 @@ ## 現在進行中 -### ADR-040 `step_timeout` 説明に sublinear / KV cache locality clarification 追記 (PR #145 T3-#1 採用) +### `takt-workflow-persona-without-model` rule コメント拡張 + ADR-007 case study 追記 (PR #150 T1-#1 採用、実体 Tier 3) -> **動機**: ADR-040 L42-48 の `step_timeout` 説明は「sublinear (3.33x)」と記述したが、本文中に「per-invoke latency が num_ctx に対して概ね線形に拡大する経験則」も併記しており、両者の関係が不明瞭。派生プロジェクトが reference table から 32K = 600s を読む際、なぜ formula `(num_ctx/8192)*180` で導出される 720s と乖離するかが直感的に分からない。clarification として「実測値 600s を正規値として採択、computed 720s は保守上限の目安、sublinear 性の根拠は KV cache locality 効果 (大規模 context で per-token efficiency 向上)」の 2-3 行追記が必要。 +> **動機**: PR #150 の Major fix (4 fields 追加) で「enumeration 方式は新規 field 追加時に明示的拡張が必要」という設計判断が再確認された。custom-lint-rules.toml ルール⑨ のコメントに field 拡張手順 (どの workflow を grep して enumeration に追加するかの手順) を明記すれば、次回 takt yaml schema 拡張時の rule 更新漏れリスクを低減できる。同 PR で ADR-007 にも「enumeration-based 正規表現層の好例」として case study 追記すれば、次回 lint rule 設計判断の prior assumption として再利用可能。 > -> **本タスクの位置づけ**: PR #145 post-merge-feedback Tier 3 #1 採用 (Severity Low / Frequency Low / Effort XS / Adoption Risk None)。永続 ADR の数値整合性確保。 +> **本タスクの位置づけ**: PR #150 post-merge-feedback で **Tier 1 #1 として採用** されたが、実体は「コメント追記 + ADR docs 修正」のみで mechanical enforcement なし。**ユーザー判断で Tier 3 に reclassify** (rule 追加 / docs 修正 は judgment-required で機械強制力がないため Tier 1 ではない)。analyzer 分類器に Tier 定義の誤解がある (`feedback_no_unenforced_rules.md` と関連)。Severity Medium / Frequency Low (1 PR) / Effort XS / Adoption Risk None。 > -> **参照**: `.claude/feedback-reports/145.md` Tier 3 #1、`docs/adr/adr-040-local-llm-context-size.md` L42-48 +> **参照**: `.claude/feedback-reports/150.md` Tier 1 #1、`docs/adr/adr-007-custom-linter-layer-boundary.md`、`.claude/custom-lint-rules.toml` ルール⑨ (line 295-) #### 作業計画 -- [ ] ADR-040 § `step_timeout` 比例係数の根拠 に 2-3 行追記: - - 実測値 600s を正規採択、computed 720s は保守上限見積もり - - sublinear 性 (3.33x vs context 4x) の根拠 = KV cache locality 効果 (推定) - - 派生プロジェクトでの derivation 時は実測 cargo test 経過時間の 2x margin を採用 +- [ ] ルール⑨ のコメントに「field 拡張手順 (1) `.takt/workflows/*.yaml` を grep / (2) `persona:` 直後に出現する未列挙 field を pattern alternation に追加 / (3) regression test 追加」を 4-5 行追記 +- [ ] `docs/adr/adr-007-custom-linter-layer-boundary.md` に「Case study: takt-workflow-persona-without-model (enumeration-based 正規表現層、Rust regex lookahead 非対応の pragmatic 対処)」section を追記 - [ ] 本エントリ削除 + todo-summary.md 行削除 #### 完了基準 -- ADR-040 の reference table と本文の formula が矛盾なく解釈可能になる -- 派生プロジェクトの porting 時に sublinear の根拠が永続記録から逆引きできる +- ルール⑨ コメントに field 拡張手順が記載され、次回 takt yaml schema 拡張時の rule 更新フローが文書化される +- ADR-007 に enumeration-based pattern の case study が記録される +- 派生プロジェクト (techbook-ledger / auto-review-fix-vc) への deploy 経由でも同更新が反映される + +--- + +### `takt_workflow_persona_detects_required_permission_mode_violation` doc 修正 + 残り 3 fields 個別 fixture test 追加 (PR #150 T2-#1 採用) + +> **動機**: PR #150 CR Major fix で 4 fields (`output_contracts` / `pass_previous_response` / `required_permission_mode` / `parallel`) を pattern に追加したが、regression test は `required_permission_mode` の 1 case のみ。doc comment は「4 fields regression test」と主張しているが実態と乖離 (`pass_previous_response` は非トリガー位置にあり、`output_contracts` / `parallel` は不在)。将来 regex 変更時に test 漏れに気付けない保守債が累積する。 +> +> **本タスクの位置づけ**: PR #150 post-merge-feedback Tier 2 #1 採用。Severity Low / Frequency Medium (3 独立分析ソースが同一 finding) / Effort S / Adoption Risk None。 +> +> **参照**: `.claude/feedback-reports/150.md` Tier 2 #1、`src/hooks-post-tool-linter/src/main.rs` L2108-2123 + +#### 作業計画 + +- [ ] `takt_workflow_persona_detects_required_permission_mode_violation` の doc comment を「`required_permission_mode` のみの代表 case (PR #150 CR Major 採用) を assert」に修正 +- [ ] `pass_previous_response` 個別 fixture test 追加 (例: `persona: code-reviewer\n pass_previous_response: false`) +- [ ] `output_contracts` 個別 fixture test 追加 (例: `persona: simplicity-reviewer\n output_contracts:`) +- [ ] `parallel` 個別 fixture test 追加 (例: `persona: code-reviewer\n parallel:` または該当箇所の構造に応じて) +- [ ] `cargo test` 全 pass + clean baseline test (`deployed_takt_workflows_have_clean_baseline_for_persona_model_rule`) も pass を確認 +- [ ] 本エントリ削除 + todo-summary.md 行削除 + +#### 完了基準 + +- 4 fields すべてに対応する individual fixture test が存在し、各 field の regex alternation 動作が機械検証される +- doc comment が test 実態と整合する +- 将来 alternation から 1 field を誤って削除した場合に test fail で検出される --- -### `MAX_CUSTOM_VIOLATIONS` outer/inner loop break scope を explicit test で seal (PR #148 T2-1 採用) +### `development-workflow.md` Step 0 に「新 todo 着手前の既実装確認」チェックステップ追加 (PR #150 T3-#1 採用、補足: ユーザー判断採用) -> **動機**: PR #148 (Phase D D-3、順位 102) の `run_custom_rules` refactor で発見した bug fix = inner `for m` loop の break のみで outer `for compiled in rules` loop に break が伝播しない問題。新コードでは `collect_violations_for_rule` 呼出後に `violations.len() >= MAX_CUSTOM_VIOLATIONS` を outer loop でチェックして break する設計に修正された (takt reviewer が "Behavioral change in `MAX_CUSTOM_VIOLATIONS`: improvement" として明示的に評価)。本タスクでは **複数ルール実行時に violation cap が正確に機能する** ことを explicit test で seal し、loop 構造を再 refactor する将来時の regression を防止する。 +> **動機**: PR #150 着手時に「順位 47 は PR #126 で既 land 済」という stale todo entry を memory rule `feedback_verify_task_not_already_done.md` 適用で発見・回避できた。memory にとどまる限り read 漏れリスクが残るため、canonical workflow doc (`~/.claude/rules/common/development-workflow.md`) Step 0 (Research & Reuse) に「新 todo 着手前に `jj log --limit 20 ` で既実装確認」step を正式追加すれば、AI エージェントの workflow 読込時の visibility が向上する。 > -> **本タスクの位置づけ**: PR #148 post-merge-feedback Tier 2 #1 採用 (Severity Medium / Frequency Low / Effort S / Adoption Risk None)。Phase D D-3 で発見済 bug fix の test net 補強。 +> **本タスクの位置づけ**: PR #150 post-merge-feedback Tier 3 #1 採用。rule 追加は本来 `feedback_no_unenforced_rules.md` 適用で却下 zone だが、本 case は「stale entry 発見の具体的 grep コマンドが workflow 内で機械的に実行可能 (`jj log` は決定的)」+「memory rule の昇格 path 実例」としてユーザー判断で採用。Severity Medium / Frequency Medium (memory 既存 + 本 PR で再発) / Effort XS / Adoption Risk None。 > -> **参照**: `.claude/feedback-reports/148.md` Tier 2-1、`src/hooks-post-tool-linter/src/main.rs` の `run_custom_rules` + `collect_violations_for_rule` (PR #148 で extract) +> **参照**: `.claude/feedback-reports/150.md` Tier 3 #1、`~/.claude/rules/common/development-workflow.md` Step 0 (Research & Reuse)、memory `feedback_verify_task_not_already_done.md` + +#### 作業計画 -#### 設計決定の余地 +- [ ] `~/.claude/rules/common/development-workflow.md` Step 0 (Research & Reuse) 末尾または直後に「Stale task verification」サブステップ追加: + - `jj log --limit 20 ` で既実装の有無を確認 + - 既 land を発見した場合は stale todo entry を docs/todo*.md / todo-summary.md から削除する形に re-purpose +- [ ] 既存 memory `feedback_verify_task_not_already_done.md` の content を canonical rule へ昇格させた旨を memory に追記 (or memory を削除して rule に統合) +- [ ] グローバル設定変更前に `~/.claude/` バックアップ取得 (memory `feedback_global_config_backup.md` 適用) +- [ ] 本エントリ削除 + todo-summary.md 行削除 + +#### 完了基準 + +- `development-workflow.md` Step 0 で「stale entry 確認」が canonical workflow として読まれる +- memory ファイルとの責任分離が明確 (rule = 公式手順、memory = session-specific 補足) または memory が rule に統合される +- 次回 todo 着手時に AI エージェントが自然に `jj log` 確認を行う + +--- + +### ADR-040 `step_timeout` 説明に sublinear / KV cache locality clarification 追記 (PR #145 T3-#1 採用) -- **test fixture 設計**: violation cap (`MAX_CUSTOM_VIOLATIONS = 20`) を超える数の違反を 1 fixture で生成、複数 rule を同時に実行 -- **scope 検証**: (a) inner loop break (1 rule で 20 件で打ち切り)、(b) outer loop break (rule A で 20 件達成後、rule B が呼ばれない)、(c) 上限未達時に複数 rule 全実行 -- **既存テスト整合**: `run_custom_rules_respects_max_violations` test が単一 rule の cap 動作を test 済。本 task はそれを multi-rule scenario に拡張 +> **動機**: ADR-040 L42-48 の `step_timeout` 説明は「sublinear (3.33x)」と記述したが、本文中に「per-invoke latency が num_ctx に対して概ね線形に拡大する経験則」も併記しており、両者の関係が不明瞭。派生プロジェクトが reference table から 32K = 600s を読む際、なぜ formula `(num_ctx/8192)*180` で導出される 720s と乖離するかが直感的に分からない。clarification として「実測値 600s を正規値として採択、computed 720s は保守上限の目安、sublinear 性の根拠は KV cache locality 効果 (大規模 context で per-token efficiency 向上)」の 2-3 行追記が必要。 +> +> **本タスクの位置づけ**: PR #145 post-merge-feedback Tier 3 #1 採用 (Severity Low / Frequency Low / Effort XS / Adoption Risk None)。永続 ADR の数値整合性確保。 +> +> **参照**: `.claude/feedback-reports/145.md` Tier 3 #1、`docs/adr/adr-040-local-llm-context-size.md` L42-48 #### 作業計画 -- [ ] test fixture 設計: 20+ 違反を含む test file + 2 rules (例: `rule_a` + `rule_b` の異なる pattern) -- [ ] test ケース追加: (a) `rule_a` 単独で 20 件 → outer break、`rule_b` が実行されないこと assert (b) `rule_a` で 19 件、`rule_b` で 1 件 → 両方実行されて合計 20 件 -- [ ] cargo test 全 pass を確認 (既存 102 tests + 新規 2 tests 程度 = 104 tests) +- [ ] ADR-040 § `step_timeout` 比例係数の根拠 に 2-3 行追記: + - 実測値 600s を正規採択、computed 720s は保守上限見積もり + - sublinear 性 (3.33x vs context 4x) の根拠 = KV cache locality 効果 (推定) + - 派生プロジェクトでの derivation 時は実測 cargo test 経過時間の 2x margin を採用 - [ ] 本エントリ削除 + todo-summary.md 行削除 #### 完了基準 -- `MAX_CUSTOM_VIOLATIONS` の outer/inner loop break scope が test で明示化される -- 将来 `run_custom_rules` を再 refactor した際、cap 動作の regression が即 fail で検出される +- ADR-040 の reference table と本文の formula が矛盾なく解釈可能になる +- 派生プロジェクトの porting 時に sublinear の根拠が永続記録から逆引きできる --- diff --git a/src/hooks-post-tool-comment-lint-rust/src/main.rs b/src/hooks-post-tool-comment-lint-rust/src/main.rs index 77cc92b..c305abe 100644 --- a/src/hooks-post-tool-comment-lint-rust/src/main.rs +++ b/src/hooks-post-tool-comment-lint-rust/src/main.rs @@ -176,7 +176,11 @@ fn locate_string_line_ranges(source: &str, needle: &str) -> Vec<(usize, usize)> fn byte_offset_to_line(source: &str, offset: usize) -> usize { let clamped = offset.min(source.len()); - source[..clamped].bytes().filter(|b| *b == b'\n').count() + 1 + source.as_bytes()[..clamped] + .iter() + .filter(|b| **b == b'\n') + .count() + + 1 } fn span_overlaps_ranges(start: usize, end: usize, ranges: &[(usize, usize)]) -> bool { @@ -440,11 +444,13 @@ fn function_metric(node: Node, source_bytes: &[u8]) -> Option { let length = line_end - line_start + 1; let (body_line_start, body_line_end, max_nesting_depth) = node .child_by_field_name("body") - .map_or((line_start, line_end, 0), |b| ( - b.start_position().row + 1, - b.end_position().row + 1, - max_block_depth_inside_body(b), - )); + .map_or((line_start, line_end, 0), |b| { + ( + b.start_position().row + 1, + b.end_position().row + 1, + max_block_depth_inside_body(b), + ) + }); Some(FunctionMetric { name, line_start, @@ -548,7 +554,11 @@ fn collect_all_violations( line_filter: Option<&[(usize, usize)]>, ) -> Vec { let mut violations = find_violations(file_path, source, line_filter); - violations.extend(find_function_length_violations(file_path, source, line_filter)); + violations.extend(find_function_length_violations( + file_path, + source, + line_filter, + )); violations.truncate(MAX_VIOLATIONS); violations } @@ -961,6 +971,42 @@ mod tests { assert_eq!(ranges, vec![(2, 2), (3, 3)]); } + #[test] + fn locate_string_line_ranges_handles_mixed_ascii_and_kanji() { + let source = + "fn main() {\n let msg = \"hello 世界\";\n let bye = \"hello 世界\";\n}\n"; + let ranges = locate_string_line_ranges(source, "\"hello 世界\""); + assert_eq!(ranges, vec![(2, 2), (3, 3)]); + } + + #[test] + fn locate_string_line_ranges_handles_kanji_only() { + let source = "// 漢字のみのコメント\nfn foo() {}\n// 漢字のみのコメント\n"; + let ranges = locate_string_line_ranges(source, "漢字のみのコメント"); + assert_eq!(ranges, vec![(1, 1), (3, 3)]); + } + + #[test] + fn locate_string_line_ranges_handles_emoji() { + let source = "fn foo() {\n let r = \"🎉\";\n let s = \"🎉\";\n}\n"; + let ranges = locate_string_line_ranges(source, "\"🎉\""); + assert_eq!(ranges, vec![(2, 2), (3, 3)]); + } + + #[test] + fn locate_string_line_ranges_handles_supplementary_plane_char() { + let source = "fn foo() {\n let s = \"𝕊\";\n let t = \"𝕊\";\n}\n"; + let ranges = locate_string_line_ranges(source, "\"𝕊\""); + assert_eq!(ranges, vec![(2, 2), (3, 3)]); + } + + #[test] + fn locate_string_line_ranges_handles_combining_character() { + let source = "fn foo() {\n let s = \"e\u{0301}\";\n let t = \"e\u{0301}\";\n}\n"; + let ranges = locate_string_line_ranges(source, "\"e\u{0301}\""); + assert_eq!(ranges, vec![(2, 2), (3, 3)]); + } + #[test] fn byte_offset_to_line_handles_offsets_correctly() { let s = "abc\ndef\nghi\n"; @@ -970,6 +1016,14 @@ mod tests { assert_eq!(byte_offset_to_line(s, 8), 3); } + #[test] + fn byte_offset_to_line_handles_mid_multibyte_offset() { + let s = "漢字\nfn foo() {}\n"; + assert_eq!(byte_offset_to_line(s, 5), 1); + assert_eq!(byte_offset_to_line(s, 6), 1); + assert_eq!(byte_offset_to_line(s, 7), 2); + } + #[test] fn span_overlaps_ranges_single_line_inclusive_bounds() { let ranges = [(2, 4), (10, 10)]; @@ -995,7 +1049,80 @@ mod tests { fn find_violations_multiline_block_comment_spanning_range_boundary() { let source = "/* line1\n line2\n line3 */\nfn main() {}\n"; let v = find_violations("test.rs", source, Some(&[(3, 4)])); - assert_eq!(v.len(), 1, "block comment starting at line 1 but extending into range should be detected"); + assert_eq!( + v.len(), + 1, + "block comment starting at line 1 but extending into range should be detected" + ); + } + + #[test] + fn find_violations_multiline_block_comment_range_covers_start_line_only() { + let source = "/* line1\n line2\n line3 */\nfn main() {}\n"; + let v = find_violations("test.rs", source, Some(&[(1, 1)])); + assert_eq!( + v.len(), + 1, + "range covering only the start line of a multiline block comment should detect" + ); + } + + #[test] + fn find_violations_multiline_block_comment_range_covers_end_line_only() { + let source = "/* line1\n line2\n line3 */\nfn main() {}\n"; + let v = find_violations("test.rs", source, Some(&[(3, 3)])); + assert_eq!( + v.len(), + 1, + "range covering only the end line of a multiline block comment should detect" + ); + } + + #[test] + fn find_violations_multiline_block_comment_range_covers_middle_line_only() { + let source = "/* line1\n line2\n line3 */\nfn main() {}\n"; + let v = find_violations("test.rs", source, Some(&[(2, 2)])); + assert_eq!( + v.len(), + 1, + "range covering only an internal line of a multiline block comment should detect" + ); + } + + #[test] + fn find_violations_inline_block_comment_range_exact_match() { + let source = + "fn foo() {}\nfn bar() {}\nfn baz() {}\nfn qux() {}\n/* inline */\nfn end() {}\n"; + let v = find_violations("test.rs", source, Some(&[(5, 5)])); + assert_eq!( + v.len(), + 1, + "single-line range exactly on inline block comment line should detect" + ); + } + + #[test] + fn find_violations_inline_block_comment_range_starts_at_comment_line() { + let source = + "fn foo() {}\nfn bar() {}\nfn baz() {}\nfn qux() {}\n/* inline */\nfn end() {}\n"; + let v = find_violations("test.rs", source, Some(&[(5, 7)])); + assert_eq!( + v.len(), + 1, + "range whose start equals inline block comment line should detect" + ); + } + + #[test] + fn find_violations_inline_block_comment_range_ends_at_comment_line() { + let source = + "fn foo() {}\nfn bar() {}\nfn baz() {}\nfn qux() {}\n/* inline */\nfn end() {}\n"; + let v = find_violations("test.rs", source, Some(&[(3, 5)])); + assert_eq!( + v.len(), + 1, + "range whose end equals inline block comment line should detect" + ); } fn tool_input_with(new_string: Option<&str>) -> ToolInput { diff --git a/src/hooks-post-tool-linter/src/main.rs b/src/hooks-post-tool-linter/src/main.rs index 99731e0..8935ce9 100644 --- a/src/hooks-post-tool-linter/src/main.rs +++ b/src/hooks-post-tool-linter/src/main.rs @@ -359,8 +359,7 @@ fn compile_paths_glob(paths: &Option>) -> Result, St } let mut builder = GlobSetBuilder::new(); for pattern in pattern_list { - let glob = Glob::new(pattern) - .map_err(|e| format!("invalid glob '{}': {}", pattern, e))?; + let glob = Glob::new(pattern).map_err(|e| format!("invalid glob '{}': {}", pattern, e))?; builder.add(glob); } builder @@ -470,7 +469,12 @@ fn rule_matches_path(compiled: &CompiledRule, file: &str) -> bool { /// `m.start()` 以前の `\n` 数 + 1 を 1-indexed line number として算出 (`find_iter` の byte /// offset を line 番号に変換するため line-by-line search では捕捉できない multiline pattern /// = 例: PowerShell `} catch {\n}` にも対応)。 -fn build_violation_json(file: &str, rule: &CustomRule, m: regex::Match, content: &str) -> Option { +fn build_violation_json( + file: &str, + rule: &CustomRule, + m: regex::Match, + content: &str, +) -> Option { let line_no = content[..m.start()].bytes().filter(|b| *b == b'\n').count() + 1; let violation = LintViolation { r#type: rule.id.to_uppercase().replace('-', "_"), @@ -483,12 +487,21 @@ fn build_violation_json(file: &str, rule: &CustomRule, m: regex::Match, content: message: rule.message.clone(), why: rule.why.clone(), fix: ViolationFix { - strategy: rule.fix.as_ref().map_or_else(String::new, |f| f.strategy.clone()), + strategy: rule + .fix + .as_ref() + .map_or_else(String::new, |f| f.strategy.clone()), steps: rule.fix.as_ref().map_or_else(Vec::new, |f| f.steps.clone()), }, example: ViolationExample { - bad: rule.example.as_ref().map_or_else(String::new, |e| e.bad.clone()), - good: rule.example.as_ref().map_or_else(String::new, |e| e.good.clone()), + bad: rule + .example + .as_ref() + .map_or_else(String::new, |e| e.bad.clone()), + good: rule + .example + .as_ref() + .map_or_else(String::new, |e| e.good.clone()), }, }; serde_json::to_string(&violation).ok() @@ -993,7 +1006,6 @@ mod tests { ); } - /// テスト用: CustomRule からコンパイル済みルールを生成するヘルパー fn compile_test_rules(rules: Vec) -> Vec { rules.into_iter().filter_map(compile_rule).collect() @@ -1115,6 +1127,73 @@ mod tests { assert_eq!(violations.len(), MAX_CUSTOM_VIOLATIONS); } + #[test] + fn run_custom_rules_outer_break_skips_subsequent_rules() { + use std::io::Write; + let dir = tempfile::tempdir().unwrap(); + let file = dir.path().join("outer_break.ts"); + { + let mut f = std::fs::File::create(&file).unwrap(); + for i in 0..21 { + writeln!(f, "console.log('cl {}');", i).unwrap(); + } + for i in 0..5 { + writeln!(f, "alert('al {}');", i).unwrap(); + } + } + + let rules = compile_test_rules(vec![ + make_test_rule("rule-a", r"console\.log\(", &["ts"]), + make_test_rule("rule-b", r"alert\(", &["ts"]), + ]); + let violations = run_custom_rules(file.to_str().unwrap(), &rules); + + assert_eq!(violations.len(), MAX_CUSTOM_VIOLATIONS); + for raw in &violations { + let v: serde_json::Value = serde_json::from_str(raw).unwrap(); + assert_eq!( + v["type"], "RULE_A", + "rule-b must not run once rule-a exhausts the cap" + ); + } + } + + #[test] + fn run_custom_rules_inner_cap_after_partial_first_rule() { + use std::io::Write; + let dir = tempfile::tempdir().unwrap(); + let file = dir.path().join("inner_cap.ts"); + { + let mut f = std::fs::File::create(&file).unwrap(); + for i in 0..19 { + writeln!(f, "console.log('cl {}');", i).unwrap(); + } + for i in 0..5 { + writeln!(f, "alert('al {}');", i).unwrap(); + } + } + + let rules = compile_test_rules(vec![ + make_test_rule("rule-a", r"console\.log\(", &["ts"]), + make_test_rule("rule-b", r"alert\(", &["ts"]), + ]); + let violations = run_custom_rules(file.to_str().unwrap(), &rules); + + assert_eq!(violations.len(), MAX_CUSTOM_VIOLATIONS); + let mut rule_a_count = 0; + let mut rule_b_count = 0; + for raw in &violations { + let v: serde_json::Value = serde_json::from_str(raw).unwrap(); + match v["type"].as_str() { + Some("RULE_A") => rule_a_count += 1, + Some("RULE_B") => rule_b_count += 1, + other => panic!("unexpected violation type: {other:?}"), + } + } + assert_eq!(rule_a_count, 19); + assert_eq!(rule_b_count, 1); + } + #[test] fn compile_test_rules_filters_invalid_regex() { let rules = vec![ @@ -1301,7 +1380,11 @@ extensions = ["ts", "js"] // --- 新規ルール: PowerShell 空 catch ブロック (no-empty-powershell-catch) --- fn ps_empty_catch_rule() -> CustomRule { - make_test_rule("no-empty-powershell-catch", r"(?i)catch\s*\{\s*\}", &["ps1"]) + make_test_rule( + "no-empty-powershell-catch", + r"(?i)catch\s*\{\s*\}", + &["ps1"], + ) } fn write_file(dir: &std::path::Path, name: &str, content: &str) -> std::path::PathBuf { @@ -1315,11 +1398,7 @@ extensions = ["ts", "js"] #[test] fn ps_empty_catch_detects_violation() { let dir = tempfile::tempdir().unwrap(); - let file = write_file( - dir.path(), - "swallow.ps1", - "try { Get-Item $p } catch {}\n", - ); + let file = write_file(dir.path(), "swallow.ps1", "try { Get-Item $p } catch {}\n"); let rules = compile_test_rules(vec![ps_empty_catch_rule()]); let violations = run_custom_rules(file.to_str().unwrap(), &rules); assert_eq!(violations.len(), 1); @@ -1472,11 +1551,7 @@ extensions = ["ts", "js"] fn md_mutable_anchor_rule() -> CustomRule { // path 部から `:` を除外することで http(s):// など外部 URL を除外 - make_test_rule( - "no-mutable-anchor", - r"\]\([^)#:]*#[^\x00-\x7F)]+", - &["md"], - ) + make_test_rule("no-mutable-anchor", r"\]\([^)#:]*#[^\x00-\x7F)]+", &["md"]) } #[test] @@ -1507,11 +1582,7 @@ extensions = ["ts", "js"] fn md_mutable_anchor_skips_ascii_fragment() { // `[link](#stable-id)` パターン (ASCII fragment、許容) let dir = tempfile::tempdir().unwrap(); - let file = write_file( - dir.path(), - "ascii.md", - "See [section](#stable-ascii-id)\n", - ); + let file = write_file(dir.path(), "ascii.md", "See [section](#stable-ascii-id)\n"); let rules = compile_test_rules(vec![md_mutable_anchor_rule()]); let violations = run_custom_rules(file.to_str().unwrap(), &rules); assert!(violations.is_empty()); @@ -1843,8 +1914,7 @@ extensions = ["ts", "js"] "no-ephemeral-todo-reference", &pattern, &[ - "rs", "toml", "jsonc", "json", "yaml", "yml", "ts", "tsx", "js", "jsx", "py", - "ps1", + "rs", "toml", "jsonc", "json", "yaml", "yml", "ts", "tsx", "js", "jsx", "py", "ps1", ], ) } @@ -1986,11 +2056,7 @@ extensions = ["ts", "js"] #[test] fn powershell_validation_handles_mixed_extension_list() { - let rule = make_test_rule( - "mixed-rule", - r"\bcatch\s*\{\s*\}", - &["js", "ps1", "ts"], - ); + let rule = make_test_rule("mixed-rule", r"\bcatch\s*\{\s*\}", &["js", "ps1", "ts"]); let missing = find_powershell_rules_missing_case_insensitive_flag(&[rule]); assert_eq!(missing, vec!["mixed-rule".to_string()]); } @@ -2020,9 +2086,8 @@ extensions = ["ts", "js"] .join("..") .join(".claude") .join("custom-lint-rules.toml"); - let content = std::fs::read_to_string(&path).unwrap_or_else(|e| { - panic!("failed to read deployed custom-lint-rules.toml: {e}") - }); + let content = std::fs::read_to_string(&path) + .unwrap_or_else(|e| panic!("failed to read deployed custom-lint-rules.toml: {e}")); let config: CustomRulesConfig = toml::from_str(&content).unwrap(); let rules = config.rules.unwrap_or_default(); let missing = find_powershell_rules_missing_case_insensitive_flag(&rules);