From a6a68ba572da81a8c739b03827f924b2697d07c9 Mon Sep 17 00:00:00 2001 From: aloekun Date: Tue, 26 May 2026 01:38:53 +0900 Subject: [PATCH 1/5] =?UTF-8?q?docs(todo):=20=E9=80=B1=E6=AC=A1=E3=83=AC?= =?UTF-8?q?=E3=83=93=E3=83=A5=E3=83=BC=20Phase=20B=207=20=E8=A6=B3?= =?UTF-8?q?=E7=82=B9=E8=B2=AC=E5=8B=99=20mapping=20=E7=A2=BA=E5=AE=9A=20+?= =?UTF-8?q?=20Phase=20B+1=20follow-up=20=E7=99=BB=E9=8C=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ADR-031 週次レビュー Phase B (順位 8) の MVP scope を 3 facets に維持しつつ、ユーザー 希望 7 観点を facet prompt に重点配分で対応する方針を確定 (2026-05-26 AskUserQuestion ヒアリング経由)。 - todo.md 順位 8 entry: 7 観点責務 mapping 表追記 (① ハーネス遵守 + ⑥ テストロジック を architecture-whole / simplicity-whole の筆頭 criteria に配置、② ③ は architecture-whole sub criterion、④ は security-whole、⑤ は順位 136 hook へ委譲、 ⑦ は aggregate 前 Rust 機械 pre-step で対応) - todo8.md 順位 136 entry: 週次レビュー ⑤ Todo 妥当性との責務分離を明記 (hook = 編集時 immediate / 週次 = batch 棚卸し) - todo9.md 順位 153 新規: review-harness-whole facet 追加 (Phase B+1、観点 ① 独立 facet 化の選択肢、Phase B dogfood 結果次第) - todo9.md 順位 154 新規: review-todo-whole facet + aggregate 前 file size pre-step 追加 (Phase B+1、観点 ⑤ ⑦ 拡張) - todo-summary.md: 順位 8 row update + 順位 153/154 row 追加 --- docs/todo-summary.md | 5 +- docs/todo.md | 16 +++++ docs/todo8.md | 2 + docs/todo9.md | 142 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 164 insertions(+), 1 deletion(-) diff --git a/docs/todo-summary.md b/docs/todo-summary.md index 015337b..f88664f 100644 --- a/docs/todo-summary.md +++ b/docs/todo-summary.md @@ -15,7 +15,7 @@ | 2 | 🚀 Tier 1 | `cli-push-runner` jj bookmark 未設定 early-exit (PR #85 T1-3) | todo2.md | S | なし | | 5 | 🚀 Tier 1 | **AI 生成一時スクリプト pattern の pre-push 検出 (PR #88 T1-2)** | todo3.md | Small | 順位 1 と関連 (要擦り合わせ) | | 6 | 🚀 Tier 1 | ADR-032 PR-pre: GitHub Branch Protection 整備 | todo2.md | 設定のみ | なし (依存タスクは完了済) | -| 8 | 🔧 Tier 2 | 週次レビュー (ADR-031) Phase B 実装 | todo.md | 中-高 | なし (順位 20 の compensating check 前提) | +| 8 | 🔧 Tier 2 | 週次レビュー (ADR-031) Phase B 実装 — 7 観点責務 mapping 確定 (① ハーネス遵守 + ⑥ テストロジック を MVP 優先、2026-05-26 ユーザー合意) | todo.md | 中-高 | 順位 20 compensating check 前提 + 順位 136 land 先行推奨 (観点 ⑤ 責務分離) | | 10 | 🔧 Tier 2 | ADR-032 PR-broken-link: broken-link-check + 内部アンカー検査 統合 | todo2.md | Small-中 | なし (clean baseline 確立済) | | 11 | 🔧 Tier 2 | `cli-pr-monitor` プロセス正常終了の integration test (PR #85 T2-2) | todo2.md | S | なし | | 16 | 🔧 Tier 2 | **`vitest` を devDependencies に固定 (PR #88 T2-3)** | todo3.md | Small | なし | @@ -77,6 +77,9 @@ | 149 | 🔧 Tier 2 | **Long-running subprocess pipe truncate hook 拡張 — `development-workflow.md` § subprocess pipe truncate 禁止 移管 (PR #172 仕組み化方針切替由来) ★ Bundle 既存ルール仕組み化** | todo9.md | S | なし (既存 `exe-help-block` preset を `cli-*.exe ... \| (head\|tail\|awk)` 等の副作用ある subprocess 出力 truncate にも拡張 or 新 `subprocess-pipe-truncate-block` preset 追加、PR #109 SIGPIPE 事故 root cause の構造化、順位 44 (gh-token-efficiency) との scope 境界整理必要、development-workflow.md § 該当 section 縮小) | | 150 | 🔧 Tier 2 | **Magic number lint 追加 — `coding-style.md` § Magic Numbers 移管 (PR #172 仕組み化方針切替由来、ユーザー判断 2026-05-25 = source folder 限定) ★ Bundle 既存ルール仕組み化** | todo9.md | M | なし (`.claude/custom-lint-rules.toml` に `no-magic-number` rule 追加、source folder paths filter で test/config 除外、時間定数 / リトライ回数 / threshold の 3 category MVP、severity warning で reviewer 判断補助、順位 102 paths filter + 順位 118 適用範囲検討と整合、coding-style.md § Magic Numbers 削除可否は dogfood 後判断) | | 151 | 🔧 Tier 2 | **PR diff lines check 追加 — `git-workflow.md` § Multi-PR chaining 移管 (PR #172 仕組み化方針切替由来、ユーザー判断 2026-05-25 = 条件付き block 3 段階) ★ Bundle 既存ルール仕組み化** | todo9.md | S | なし (`src/cli-push-runner/src/stages/pr_size_check.rs` 新 stage 追加、`push-runner-config.toml` `[pr_size_check]` section で threshold 設定可能化 (default: block 1500 / warning 800)、jj diff stat 計測、大型 refactoring 時の override は config 編集、git-workflow.md § Multi-PR chaining 縮小) | +| 152 | 🔧 Tier 2 | **todo entry 削除時の事前 land 確認手順 — 順位 136 hook 拡張 or 独立 follow-up (PR #173 T2-1 採用、2026-05-26)** | todo9.md | XS-S | 順位 136 (working copy staleness + 既実装 grep) と同型機械強制、lifecycle 補完 = 順位 136 (add/edit 時) + 本タスク (delete 時)。PreToolUse hook で `docs/todo*.md` 削除時に対応 land commit を `jj log` で grep 検証、land 確認なら allow + 証跡出力、未確認なら warning (block しない)。順位 136 hook 統合 (~+15 行) or 独立 (~40 行) のいずれか、ADR-042 § Decision matrix 適用 (mechanizable + FP 低 + Adoption Risk None) | +| 153 | 🔧 Tier 2 | **`review-harness-whole` facet 追加 — 観点 ① 独立 facet 化 (順位 8 follow-up、Phase B+1、2026-05-26 ユーザー合意) ★ 週次拡張** | todo9.md | S | 順位 8 Phase B land + 2-3 週 dogfood 後に着手判断 (extract 不要なら close)、順位 146-151 Bundle 既存ルール仕組み化の継続的発見源、architecture-whole から ① 観点を extract して context 圧迫回避 | +| 154 | 🔧 Tier 2 | **`review-todo-whole` facet + aggregate 前 file size pre-step — 観点 ⑤ ⑦ 拡張 (順位 8 follow-up、Phase B+1、2026-05-26 ユーザー合意) ★ 週次拡張** | todo9.md | M | 順位 136 land + Phase B 2-3 週 dogfood 完了後着手、順位 95 / 147 と scope 整理必要 (CI 即時 vs 週次 batch)、ADR-031 3 層分離原則で file size は LLM 不要の Rust pre-step に分離 | **戦略**: 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/todo.md b/docs/todo.md index 2ad893d..8a20cdf 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -279,6 +279,22 @@ SessionStart hook (hooks-session-start.exe 拡張) | 失敗ポリシー | **best-effort** (`.failed` marker + SessionStart hook reminder で再実行誘導。must-run ではないので決定論ゲート不要) | | アンチパターン | **whole-tree 用 facet を diff 用 facet と共通化しない** (ADR-027 で diff 局所が本質要件のため separation 必須) | +#### 7 観点責務 mapping (2026-05-26 ユーザー合意、AskUserQuestion 経由) + +ユーザー希望 7 観点を ADR-031 設計の 3 facets に **prompt 重点配分** で対応。facet 数は増やさず YAGNI + context 圧迫リスク回避 (MVP 維持)。MVP 優先観点は **① ハーネス遵守 + ⑥ テストロジック** で、各 facet prompt の筆頭 criteria に組み込む。 + +| 観点 | 担当 facet | prompt 重点 | +|---|---|---| +| ① ハーネス遵守 (rule < pipeline < hook 重複) | architecture-whole | **MVP 最優先** — facet criteria の筆頭、rule/pipeline/hook 重複検出、順位 146-151 Bundle 既存ルール仕組み化の継続的発見源 | +| ② docs 内整合性 | architecture-whole の sub criterion | ADR 間 supersedes / cross-reference / todo routing、順位 10 / 95 / 96 と補完 | +| ③ docs-source 矛盾 | architecture-whole の sub criterion | 重要 ADR 限定リスト (ADR-007 / 012 / 021 / 022 等) で context 圧迫回避 | +| ④ セキュリティ | security-whole | ADR-031 設計通り、変更なし | +| ⑤ Todo 妥当性 | **MVP 対象外** (順位 136 hook へ委譲) | hook = 編集時 immediate guard / 週次 = batch 棚卸し で責務分離、Phase B+1 で順位 154 facet として再評価 | +| ⑥ テストロジック (振る舞い vs 実装詳細、境界) | simplicity-whole | **MVP 最優先** — facet criteria の筆頭、TDD anti-pattern + 境界欠落、順位 38 (cargo-mutants L3 weekly) と cross-validate | +| ⑦ ファイルサイズ (50KB) | aggregate 前の Rust 機械 pre-step (Phase B+1) | facet 不要、機械検査で十分。順位 154 で順位 95 / 147 と scope 整理 | + +**Bundle 戦略**: **Phase B 単体で land** (順位 38 / 95 / 96 は別 PR、PR diff 250-800 行に収める方針)。Phase B+1 で観点 ① ⑤ ⑦ を独立 facet / pre-step に extract する余地を残す (順位 153 / 154 を follow-up 登録済)。 + #### 作業計画 ##### Phase B: takt workflow + facets + persona (PR 2) diff --git a/docs/todo8.md b/docs/todo8.md index 55a8c43..034f2e8 100644 --- a/docs/todo8.md +++ b/docs/todo8.md @@ -233,6 +233,8 @@ > > **本タスクの位置づけ**: 本セッション post-merge-feedback 相当の structural defense + 旧 順位 122 機能統合。`feedback_no_unenforced_rules.md` 例外条件 = **2 つの hook で機械強制可能**。案 A (予防層 = session 起動時に状況認識) + 案 B (最終 backstop = stale 状態での編集を hard block + 既実装 grep 提示) のセット二段構え。 > +> **週次レビュー (ADR-031) 観点 ⑤ Todo 妥当性 との責務分離 (2026-05-26 ユーザー合意)**: **本 hook = 編集時 immediate guard / 週次 = 全 entry 横断 batch 棚卸し**。本 hook land 後の週次レビュー Phase B+1 (順位 154 `review-todo-whole` facet) は hook が拾えない broad な観点 (経年劣化 entry / cross-file 重複 / preamble drift) に focus する設計。順位 8 entry の「7 観点責務 mapping」表参照。 +> > **参照**: 本セッション (2026-05-18) PR cleanup-stale-rank-39 root cause 分析 (ユーザー対話)、PR #150 post-merge-feedback Tier 3 #1 (旧 順位 122 由来)、memory `feedback_verify_task_not_already_done.md`、ADR-039 (Experimental feature 標準パターン)、PR #172 (順位 144 hook 化 dogfood 事例) > > **実行優先度**: 🚀 **Tier 1** — Effort Medium-Large (案 A ~80 行 + 案 B ~50 行 = 既実装 grep 拡張で +~20 行)。本セッションの実観測 failure mode に対する直接対策で、並列セッション運用が常態化している現状で再発確率が高い。 diff --git a/docs/todo9.md b/docs/todo9.md index 9cba1ee..278c086 100644 --- a/docs/todo9.md +++ b/docs/todo9.md @@ -305,6 +305,148 @@ --- +### todo entry 削除時の事前 land 確認手順 — 順位 136 hook 拡張 or 独立 follow-up (PR #173 T2-1 採用、2026-05-26) + +> **動機**: PR #173 で land 済 entry (順位 125 / 139 / 141) を todo8.md から削除した際、削除前の land 状態確認は実装 grep ベースの「事後 verify」で実施し全て land 確認できたが、「事前確認」の機械強制はなかった。post-merge-feedback analyzer (T2-1) で「rank 125 / 141 の actual land status を `jj log` で確認、未実装なら todo に復帰」採用判定が成立 (Severity Medium / Frequency Low / Effort XS / Adoption Risk None)。今回 false alarm (実装は全 land 済) だったが、将来「削除前に land 確認」を機械強制すれば誤削除を構造的に防止できる。 +> +> **本タスクの位置づけ**: 順位 136 (working copy staleness hook + stale todo entry 既実装 grep 提示) と **同型の機械強制タスク**、lifecycle 補完関係: +> +> - 順位 136: **add / edit 時**に既実装の commit を grep 提示 (= 「既に実装済では?」warning) +> - **本タスク (順位 152)**: **delete 時** に対応 land commit を grep 検証 (= 「本当に land 済?」warning) +> +> 順位 136 hook 実装時に統合検討 (= 同一 PreToolUse hook で add/edit/delete の edit 種別を判定して分岐)、または独立 hook (= shared utility 経由) で別 task 化のいずれか。ADR-042 § Decision matrix 適用 = **mechanizable + FP 低 + Adoption Risk None** で仕組み化 zone。 +> +> **参照**: `.claude/feedback-reports/173.md` Tier 2 #1、順位 136 entry (本ファイル内)、PR #173 セッションで実施した実装 grep 検証 (rank 125 = `run_custom_rules_line_number_correct_with_multibyte_content` test 存在 / rank 139 = `docs/adr/adr-041-test-isolation-patterns.md` 存在 / rank 141 = `fix_push_time` + `RATE_LIMIT_BUT_MERGEABLE` シグナル存在)、ADR-042 (rule vs mechanism boundary)、memory `feedback_pipeline_over_rules.md` +> +> **実行優先度**: 🔧 **Tier 2** — Effort XS-S。順位 136 に統合する場合は追加 ~15 行 (edit 種別判定 + delete branch)、独立 hook の場合は ~40 行 (構造的に分離)。 + +#### 設計決定 (案) + +- **検出条件**: `docs/todo*.md` への Edit/Write で `### 順位 N ` セクション (or `### ` headed entry) が削除されたパターン + - Edit tool の `old_string` に `### ` で始まる entry header が含まれ、`new_string` に含まれない場合 = 削除と判定 + - Write tool で全文書き換えの場合は old/new file の `### ` header 数を比較 +- **動作**: 削除対象 entry の keyword (見出し title から抽出、順位 prefix / 句読点 除去) を `jj log --limit 30` で grep +- **判定**: + - 関連 commit (= 「順位 N land」「PR #XXX」「<keyword> land 済」等の description) を検出 → 削除を **allow** + 検出 commit を additional context に出力 (削除証跡として残る) + - 関連 commit なし → **warning** (block ではなく feedback) + 「削除前に land 確認推奨。defer / withdraw の場合は commit message に明記推奨」を出力 +- **scope**: 順位 136 hook (PreToolUse on docs/todo*.md edit) に統合する case が推奨。共通の `jj log` grep utility を共有 +- **block vs warning 設計判断**: AI が大量 land 済 entry を一括削除するケース (本 PR #173 でも 3 件削除) を考慮し、warning にとどめる。block にすると mass cleanup PR で UX 阻害 + +#### 作業計画 + +- [ ] 順位 136 hook 実装時に edit 種別判定ロジック (add / edit / delete) を含める設計検討 +- [ ] delete 検出: `old_string` に `### ` entry header あり / `new_string` になし pattern +- [ ] keyword 抽出 (順位 prefix / 句読点 除去) + `jj log --limit 30` grep +- [ ] 結果出力フォーマット (land 確認時 = additional context に commit 列挙 / 未確認時 = warning) +- [ ] test fixture (4 ケース): delete + land あり / delete + land なし / add + 既実装あり / add + 既実装なし +- [ ] 派生プロジェクト deploy 検討 (順位 136 と同タイミング) +- [ ] 本エントリ削除 + todo-summary.md 行削除 + +#### 完了基準 + +- `docs/todo*.md` への delete 操作時、対応 land commit が grep で検出されれば allow + 証跡 output +- land commit なし時は warning (block しない) で AI に再確認を促す +- 順位 136 (add/edit 時 既実装 grep) と統合 or 独立で lifecycle カバレッジ完成 +- 派生プロジェクト transferability + +#### 詰まっている箇所 + +- **edit 種別判定の複雑性**: Edit tool の old/new 比較で削除を判定可能だが、部分削除 + 他箇所改修の混在 edit で false negative リスク。最小単位は「順位 N entry 全体の削除」のみ対象とする MVP が現実的 +- **keyword 抽出の精度**: 順位 prefix 除去後の title 残りで grep するが、title に表記揺れ (例: "ADR-041 Test Isolation Patterns" vs "Test Isolation Patterns ADR") があると false negative。順位 N をそのまま grep する case も併用検討 +- **mass cleanup PR との両立**: 本 PR #173 のように 3 件以上の land 済 entry を一括削除する PR では各削除で warning が累積し UX 阻害。1 PR 内で同 file の delete N 件目以降は output 抑制 等の noise 軽減策必要 + +--- + +### `review-harness-whole` facet 追加 — 観点 ① 独立 facet 化 (順位 8 follow-up、Phase B+1、2026-05-26 ユーザー合意) + +> **動機**: 順位 8 (週次レビュー Phase B) の MVP は 3 facets (simplicity / security / architecture) 構成で start し、観点 ① ハーネス遵守 (rule < pipeline < hook 重複検出) は architecture-whole facet の prompt 重点 criteria として組込。Phase B dogfood で「① 観点が architecture-whole の他 criteria (ADR 整合性 / モジュール境界 / 命名規約 / 循環依存) と context 圧迫」が観測されたら、独立 facet `review-harness-whole` に extract する。 +> +> **本タスクの位置づけ**: 順位 8 の follow-up、Phase B+1。Phase B dogfood 結果を見てから着手判断 (extract 不要なら本 entry close)。順位 146-151 (Bundle 既存ルール仕組み化) の **継続的発見源** として機能し、新 rule → hook 昇格候補を週次で systemic に拾う構造を強化する。 +> +> **参照**: 順位 8 entry (todo.md 「7 観点責務 mapping」表)、順位 146-151 Bundle 既存ルール仕組み化、`feedback_no_unenforced_rules.md`、`feedback_pipeline_over_rules.md`、ADR-031 (週次レビュー設計) +> +> **実行優先度**: 🔧 **Tier 2** — Effort S。順位 8 Phase B land + 2-3 週 dogfood 後に着手判断。 + +#### 設計決定 (案) + +- 配置: `.takt/facets/instructions/review-harness-whole.md` 新規 facet (allowed_tools: Read/Glob/Grep のみ) +- 観点: `~/.claude/rules/common/*.md` の各 rule を全文走査 + `.claude/custom-lint-rules.toml` / `.claude/hooks-config.toml` / `push-runner-config.toml` と突き合わせ → rule docs に記載があるが hook / pipeline 未実装の項目を finding として抽出 +- aggregate-weekly 側で finding category `harness-rule-coverage-gap` として独立 group 化 +- Phase B+1 着手判断条件: Phase B dogfood で architecture-whole の output から ① 観点 finding 数が多く他 criteria の finding 質が劣化、または ① 観点が見落とされていると観測された場合 + +#### 作業計画 + +- [ ] Phase B (順位 8) land + 2-3 週 dogfood 運用 → ① 観点 finding の context 圧迫 / 見落としを観測 +- [ ] facet extract 判断 (extract 不要なら本 entry close) +- [ ] `review-harness-whole.md` instruction 設計 (順位 146-151 land 済 / 未済の状況を踏まえた rule-vs-hook gap 検出ロジック) +- [ ] takt workflow weekly-review.yaml に facet 追加 + `parallel:` block 拡張 +- [ ] aggregate-weekly facet 拡張 (新 category) + pending JSON schema 拡張 +- [ ] dogfood + 本エントリ削除 + todo-summary.md 行削除 + +#### 完了基準 + +- ① ハーネス遵守 観点が独立 facet で週次検出される +- architecture-whole は ADR 整合性 / モジュール境界 / 命名規約 / 循環依存 に集中 +- 新規 rule 追加時の hook 昇格候補が systemic に提案される + +#### 詰まっている箇所 + +- Phase B dogfood 結果次第 (extract 不要なら本 entry close) +- ① 観点と ② docs 内整合性の境界判断 (rule docs 整合 vs その他 docs 整合の cross-cut) + +--- + +### `review-todo-whole` facet + aggregate 前 file size pre-step — 観点 ⑤ ⑦ 拡張 (順位 8 follow-up、Phase B+1、2026-05-26 ユーザー合意) + +> **動機**: 順位 8 (週次レビュー Phase B) の MVP では観点 ⑤ Todo 妥当性 は順位 136 (todo hook 2 段構え) に委譲し、観点 ⑦ ファイルサイズ も対象外とした。順位 136 hook land 後、hook が拾えない broad な観点 (全 todo entry 横断の dead pattern 検出 / cross-todo file の重複 entry / docs/todo*.md preamble drift) を週次の `review-todo-whole` facet で補完する。並行して観点 ⑦ ファイルサイズ (50KB / 800 行) は aggregate-weekly facet 直前の Rust 機械 pre-step で計測し、LLM context を浪費せず ADR-031 の 3 層分離 (Rust 機械 / takt AI / skill ask) に整合させる。 +> +> **本タスクの位置づけ**: 順位 8 の follow-up、Phase B+1。順位 136 hook land 後に着手判断 (= hook の immediate guard が機能している前提で、週次は batch 棚卸しに focus)。`feedback_pipeline_over_rules.md` 適用で、機械検査可能な観点 (file size) を LLM facet に乗せず分離する設計。 +> +> **参照**: 順位 8 entry (todo.md 「7 観点責務 mapping」表)、順位 136 entry (todo8.md、todo hook 2 段構え)、順位 95 (preamble file count CI 自動照合)、順位 147 (file length lint 800 行)、ADR-031 (3 層分離 = Rust 機械 / takt AI / skill ask)、`feedback_pipeline_over_rules.md` +> +> **実行優先度**: 🔧 **Tier 2** — Effort M (facet 新規 + Rust pre-step ~80 行)。順位 136 land + Phase B 2-3 週 dogfood 完了後に着手。 + +#### 設計決定 (案) + +**`review-todo-whole` facet (観点 ⑤ 補完):** + +- 配置: `.takt/facets/instructions/review-todo-whole.md` 新規 facet (allowed_tools: Read/Glob/Grep のみ) +- 観点: 全 todo*.md entry を横断走査 → dead pattern (= 半年以上 stale + 関連 commit なし + 依存 task land 済) / cross-file 重複 entry / preamble routing drift を finding として抽出 +- 順位 136 hook が拾えない範囲: 編集していない entry の経年劣化 / file 跨ぎの重複 / preamble file count drift + +**aggregate 前 Rust 機械 pre-step (観点 ⑦):** + +- 配置: takt workflow weekly-review.yaml の aggregate-weekly facet 直前に新 step 追加 (or aggregate facet 自身が呼び出す Rust binary) +- 計測対象: + - `docs/todo*.md` の file size (50KB 閾値、PR #88 / #96 / #101 / #123 / #172 で実証された分割 trigger) + - `src/**/*.rs` の line count (800 行閾値、順位 147 file length lint と整合) +- 出力: 閾値超過 / 接近 (90% 等) のファイル一覧を aggregate facet の入力として渡す +- 機械検査のため LLM context を浪費しない (ADR-031 3 層分離原則) + +#### 作業計画 + +- [ ] 順位 136 hook land 待ち +- [ ] Phase B 2-3 週 dogfood 完了 + 観点 ⑤ ⑦ の必要性再評価 (順位 95 / 147 land 状況も確認) +- [ ] `review-todo-whole.md` instruction 設計 (順位 136 hook が拾える範囲との境界明示) +- [ ] aggregate 前 Rust pre-step 実装 (新 binary `cli-weekly-review-prep` or aggregate facet 内 step) +- [ ] takt workflow weekly-review.yaml に facet + pre-step 追加 +- [ ] aggregate-weekly facet 拡張 (新 category) + pending JSON schema 拡張 +- [ ] dogfood + 本エントリ削除 + todo-summary.md 行削除 + +#### 完了基準 + +- 全 todo*.md entry の dead pattern / cross-file 重複 / preamble drift が週次検出される +- file size 閾値超過 / 接近が aggregate facet input として通知される +- 順位 136 hook と責務分離 (hook = 編集時 immediate / 週次 = batch 棚卸し) が機能 + +#### 詰まっている箇所 + +- 順位 136 hook 実装次第 (hook が拾える範囲が確定後に週次の補完範囲を確定) +- Phase B dogfood 結果次第 (有用な finding が出るかは運用観察) +- 順位 95 (preamble count CI 自動照合) との scope 重複整理: CI = 機械検査即時 / 週次 pre-step = aggregate 入力、両立可能だが integration 検討 + +--- + ## 既知課題 (記録のみ、本セッションで未対応) (現時点で本ファイルへの既知課題は無し。docs/todo8.md 末尾の post-merge-feedback workflow stale marker 問題を参照。) From 9634e7cb099378bbac5b470f3735c995edfc908f Mon Sep 17 00:00:00 2001 From: aloekun <aloekun.a10e@gmail.com> Date: Tue, 26 May 2026 12:17:29 +0900 Subject: [PATCH 2/5] =?UTF-8?q?feat(cli-push-runner):=20Bundle=201=20?= =?UTF-8?q?=E2=80=94=20=E9=A0=86=E4=BD=8D=201=20scratch=20file=20warning?= =?UTF-8?q?=20hook=20+=20=E9=A0=86=E4=BD=8D=20116/134=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 順位 1 (PR #85 T1-4) push 前 untracked scratch file warning hook を実装。jj auto-snapshot 環境で .gitignore 漏れがあると `__*` 等の scratch ファイルが PR に意図せず混入する事故 (PR #85 で `__parse_transcripts.ps1` 実例) を構造的に 予防する。 ## 実装 (順位 1) - src/cli-push-runner/src/stages/scratch_file_warning.rs (新規): 新 stage - src/cli-push-runner/src/stages/mod.rs: module register - src/cli-push-runner/src/config.rs: ScratchFileWarningConfig 追加 + tests - src/cli-push-runner/src/main.rs: quality_gate 前に stage invoke + EXIT_SCRATCH_FILE_WARNING=6 - push-runner-config.toml: [scratch_file_warning] section 追加 (default-ON) 設計: - 配置: run_pipeline の最早期 (quality_gate より前) — 検出時は無駄な quality_gate 実行を回避 - 検出: `jj file list -r @` で @ commit に含まれるファイルを列挙、basename が config patterns (default: ["__*"]) に match するか検査 - Override: env var SCRATCH_FILE_WARNING_OVERRIDE=1/true/yes/on で意図的バイパス可能 - fail-open: jj 失敗時 (timeout/起動失敗) は warning ログのみで push 続行 - Config-driven patterns: 順位 5 (AI 生成一時スクリプト pattern) は本 stage の patterns 拡張 + ADR-007 連携で補完実装する設計 (重複ではなく補完アプローチ確定) ## Bundled docs - 順位 116: ADR-040 § `step_timeout` 比例係数の根拠 に sublinear / KV cache locality clarification を追記。実測値 600s 採択 vs 線形 derivation 720s 保守上限の使い分けを明示、永続 ADR の数値整合性確保。 - 順位 134: ADR-035 § ❌ 適用しない criteria 表に Magic number / hardcoded value 行を追加。docs-only PR の reviewer 誤適用 (PR #156 観測) の構造的予防。 ## Tests cargo test --manifest-path src/cli-push-runner/Cargo.toml: 122 passed 新規 test 30 件 (scratch_file_warning module 内 + config.rs parse) --- docs/adr/adr-035-doc-evaluation-policy.md | 1 + docs/adr/adr-040-local-llm-context-size.md | 8 +- push-runner-config.toml | 13 + src/cli-push-runner/src/config.rs | 95 ++++ src/cli-push-runner/src/main.rs | 18 +- src/cli-push-runner/src/stages/mod.rs | 2 + .../src/stages/scratch_file_warning.rs | 464 ++++++++++++++++++ 7 files changed, 598 insertions(+), 3 deletions(-) create mode 100644 src/cli-push-runner/src/stages/scratch_file_warning.rs diff --git a/docs/adr/adr-035-doc-evaluation-policy.md b/docs/adr/adr-035-doc-evaluation-policy.md index 0b16dec..6afec2e 100644 --- a/docs/adr/adr-035-doc-evaluation-policy.md +++ b/docs/adr/adr-035-doc-evaluation-policy.md @@ -73,6 +73,7 @@ executable code logic への変更が **無い** こと (= AST 上の関数 body | function length / nesting depth / complexity metrics | docs に関数は無い | | DRY (code logic 視点) | docs hierarchy は意図的な再記述 (summary + detail) を含む。例外列挙は本 ADR で集約 | | YAGNI (code logic 視点) | 計画文書の "future candidates" / "Phase 2 検討" / "rejected alternatives" セクションは speculative ではなく **保管すべき意思決定履歴** | +| Magic number / hardcoded value | docs 中の数値 (閾値説明 / Phase 番号 / バージョン / 行番号引用等) は説明的記述であり code 内の magic value とは性質が異なるため適用しない。code 例として埋め込まれた数値が `const` 化推奨に該当するか否かは、対応する実 code 側 PR で判定する | ### facet instructions への反映方針 diff --git a/docs/adr/adr-040-local-llm-context-size.md b/docs/adr/adr-040-local-llm-context-size.md index 4aeb447..5279353 100644 --- a/docs/adr/adr-040-local-llm-context-size.md +++ b/docs/adr/adr-040-local-llm-context-size.md @@ -45,7 +45,13 @@ ephemeral artifact (旧 analysis.md) には permanent data を残さない原則 - Phase b' (8K): 180s で 12 件 mistral invoke (`cargo test --ignored`) を完走 - Phase C (32K): 269s 観測 (= 180s 超過、cargo test 全体) → 600s に拡大 -- per-invoke latency が num_ctx に対して概ね線形に拡大する経験則 (overflow 解消後の純粋な inference time) +- per-invoke latency は num_ctx に対して**ほぼ線形**だが、KV cache locality 効果でわずかに sublinear (`22 ms/token` → `18.3 ms/token`、17% 改善) + +**実測値 vs 線形 derivation の使い分け** (派生プロジェクトでの porting 時の判断指針): + +- **実測値 (600s) を正規採択**: Phase C cargo test で 269s 観測 → 2x safety margin で 600s。本 ADR が定義する canonical 値。 +- **線形 derivation (= 720s) は保守上限見積もり**: per-token 不変仮定 (`22 ms/token × 32768 = 721s`) は KV cache locality を無視するため過大評価。新規 model / 未測定環境での fallback ceiling として使う。 +- sublinear 性の根拠は KV cache locality 効果 (推定) で model-specific。別 model (llama2:13b 等) では再 calibration 必須。 **reference 値** (派生プロジェクトでの derivation 用): diff --git a/push-runner-config.toml b/push-runner-config.toml index cbcdac9..e420b2b 100644 --- a/push-runner-config.toml +++ b/push-runner-config.toml @@ -3,6 +3,19 @@ # pnpm push で起動される push-runner.exe がこのファイルを読み込む。 # カレントディレクトリ (リポジトリルート) を優先的に検索する。 +# --------------------------------------------------------------------------- +# [scratch_file_warning] — 順位 1 (PR #85 T1-4): scratch ファイル混入の防御層。 +# `@` commit に `__*` 等の scratch ファイルが含まれていないか push 前に検査し、 +# 検出時は push を block する。jj auto-snapshot 環境で .gitignore 漏れがあると +# scratch ファイルが PR に混入する事故 (PR #85 で実証) の構造的予防。 +# section 不在時は default-ON 動作 (enabled=true, patterns=["__*"])。 +# Override: env SCRATCH_FILE_WARNING_OVERRIDE=1 で意図的バイパス可能。 +# 順位 5 で _tmp_* 等の追加 pattern を本 section の patterns に追加して拡張する。 +# --------------------------------------------------------------------------- +[scratch_file_warning] +enabled = true +patterns = ["__*"] + [quality_gate] parallel = true # step_timeout 履歴: 120s (Phase b 当初) → 180s (PR #132、Phase b' で 12 件 mistral invoke が 120s 境界) → diff --git a/src/cli-push-runner/src/config.rs b/src/cli-push-runner/src/config.rs index 597a389..4ec415c 100644 --- a/src/cli-push-runner/src/config.rs +++ b/src/cli-push-runner/src/config.rs @@ -32,6 +32,22 @@ pub(crate) struct Config { pub(crate) lint_screen: Option<LintScreenConfig>, pub(crate) takt: TaktConfig, pub(crate) push: PushConfig, + pub(crate) scratch_file_warning: Option<ScratchFileWarningConfig>, +} + +/// 順位 1 (PR #85 T1-4) — scratch ファイル (`__*` 等) が `@` commit に +/// 混入していないか push 前に検査する stage の config。 +/// +/// `[scratch_file_warning]` section が TOML に**不在**の場合は default-ON 動作 +/// (= `enabled = true`, `patterns = ["__*"]`)。security-critical かつ漏洩観測前の +/// preventive 層のため、明示的な opt-out が無い限り検査する。 +/// +/// `patterns` は順位 5 (AI 生成一時スクリプト pattern の pre-push 検出) で +/// `_tmp_*` 等の追加 pattern を config-driven で拡張可能 (= 補完アプローチ)。 +#[derive(Deserialize)] +pub(crate) struct ScratchFileWarningConfig { + pub(crate) enabled: Option<bool>, + pub(crate) patterns: Option<Vec<String>>, } /// Phase c (§8.E lint screen facet) — pre-push 時に diff を mistral:7b に流して @@ -543,6 +559,7 @@ command = "echo push" }, diff: None, lint_screen: None, + scratch_file_warning: None, takt: TaktConfig { workflow: "w".into(), task: "t".into(), @@ -615,6 +632,7 @@ command = "echo push" }, diff: None, lint_screen: None, + scratch_file_warning: None, takt: TaktConfig { workflow: "w".into(), task: "t".into(), @@ -697,6 +715,82 @@ command = "echo push" ); } + #[test] + fn config_parses_with_scratch_file_warning_full() { + let toml_str = r#" +[quality_gate] +[[quality_gate.groups]] +name = "test" +commands = ["echo ok"] + +[scratch_file_warning] +enabled = true +patterns = ["__*", "_tmp_*"] + +[takt] +workflow = "w" +task = "t" + +[push] +command = "echo push" +"#; + let config: Config = toml::from_str(toml_str).unwrap(); + let s = config + .scratch_file_warning + .expect("[scratch_file_warning] should parse to Some"); + assert_eq!(s.enabled, Some(true)); + assert_eq!( + s.patterns.unwrap(), + vec!["__*".to_string(), "_tmp_*".to_string()] + ); + } + + #[test] + fn config_parses_with_scratch_file_warning_only_enabled_false() { + let toml_str = r#" +[quality_gate] +[[quality_gate.groups]] +name = "test" +commands = ["echo ok"] + +[scratch_file_warning] +enabled = false + +[takt] +workflow = "w" +task = "t" + +[push] +command = "echo push" +"#; + let config: Config = toml::from_str(toml_str).unwrap(); + let s = config.scratch_file_warning.unwrap(); + assert_eq!(s.enabled, Some(false)); + assert!(s.patterns.is_none()); + } + + #[test] + fn config_scratch_file_warning_absent_yields_none() { + let toml_str = r#" +[quality_gate] +[[quality_gate.groups]] +name = "test" +commands = ["echo ok"] + +[takt] +workflow = "w" +task = "t" + +[push] +command = "echo push" +"#; + let config: Config = toml::from_str(toml_str).unwrap(); + assert!( + config.scratch_file_warning.is_none(), + "absent [scratch_file_warning] should yield None (default-ON 動作は stage 側で解決)" + ); + } + #[test] fn validate_rejects_empty_commands() { let config = Config { @@ -711,6 +805,7 @@ command = "echo push" }, diff: None, lint_screen: None, + scratch_file_warning: None, takt: TaktConfig { workflow: "w".into(), task: "t".into(), diff --git a/src/cli-push-runner/src/main.rs b/src/cli-push-runner/src/main.rs index 800583c..6c3b46a 100644 --- a/src/cli-push-runner/src/main.rs +++ b/src/cli-push-runner/src/main.rs @@ -1,6 +1,7 @@ //! Push Runner — takt ベースの pre-push パイプライン //! //! pnpm push から呼び出され、以下のステージを実行する: +//! Stage 0: scratch_file_warning — `__*` 等の scratch ファイル混入を検査 (順位 1) //! Stage 1: quality_gate — TOML で定義されたコマンド群をグループ間で並列実行 //! Stage 1.5: diff — jj diff を取得しファイルに書き出し(reviewers が Read で参照) //! Stage 2: takt — AI レビュー(reviewers → fix loop) @@ -15,6 +16,7 @@ //! 3 - push 失敗 //! 4 - 設定エラー //! 5 - diff 取得失敗 +//! 6 - scratch_file_warning 検出 (override env で bypass 可能) mod config; mod log; @@ -25,7 +27,10 @@ use std::time::Instant; use config::load_config; use log::log_info; -use stages::{run_diff, run_lint_screen, run_push, run_quality_gate, run_takt, DiffResult}; +use stages::{ + run_diff, run_lint_screen, run_push, run_quality_gate, run_scratch_file_warning, run_takt, + DiffResult, +}; const EXIT_SUCCESS: i32 = 0; const EXIT_QUALITY_GATE_FAILURE: i32 = 1; @@ -33,6 +38,7 @@ const EXIT_TAKT_FAILURE: i32 = 2; const EXIT_PUSH_FAILURE: i32 = 3; const EXIT_CONFIG_ERROR: i32 = 4; const EXIT_DIFF_FAILURE: i32 = 5; +const EXIT_SCRATCH_FILE_WARNING: i32 = 6; /// diff stage を実行し lint-screen を呼び出す。 /// Ok(skip_takt) で成功、 Err(exit_code) で pipeline 中断。 @@ -70,11 +76,19 @@ fn run_pipeline() -> i32 { let has_diff = config.diff.is_some(); log_info(&format!( - "パイプライン開始: quality_gate → {} takt ({}) → push", + "パイプライン開始: scratch → quality_gate → {} takt ({}) → push", if has_diff { "diff →" } else { "" }, config.takt.workflow, )); + if !run_scratch_file_warning(config.scratch_file_warning.as_ref()) { + log_info( + "パイプライン中断: scratch ファイル検出。.gitignore 修正 / ファイル削除 / \ + SCRATCH_FILE_WARNING_OVERRIDE=1 のいずれかで再実行してください。", + ); + return EXIT_SCRATCH_FILE_WARNING; + } + if !run_quality_gate(&config.quality_gate) { log_info("パイプライン中断: quality_gate 失敗。問題を修正して再実行してください。"); return EXIT_QUALITY_GATE_FAILURE; diff --git a/src/cli-push-runner/src/stages/mod.rs b/src/cli-push-runner/src/stages/mod.rs index a366853..9dace47 100644 --- a/src/cli-push-runner/src/stages/mod.rs +++ b/src/cli-push-runner/src/stages/mod.rs @@ -3,10 +3,12 @@ mod lint_screen; mod push; mod push_jj_bookmark; mod quality_gate; +mod scratch_file_warning; mod takt; pub(crate) use diff::{run_diff, DiffResult}; pub(crate) use lint_screen::run_lint_screen; pub(crate) use push::run_push; pub(crate) use quality_gate::run_quality_gate; +pub(crate) use scratch_file_warning::run_scratch_file_warning; pub(crate) use takt::run_takt; diff --git a/src/cli-push-runner/src/stages/scratch_file_warning.rs b/src/cli-push-runner/src/stages/scratch_file_warning.rs new file mode 100644 index 0000000..4c708ec --- /dev/null +++ b/src/cli-push-runner/src/stages/scratch_file_warning.rs @@ -0,0 +1,464 @@ +//! Scratch file warning stage — 順位 1 (PR #85 T1-4) +//! +//! `@` commit に scratch-pattern ファイル (default: `__*`) が含まれていないか検査し、 +//! 検出時は warning + block で push を停止する。jj は auto-snapshot で working tree +//! を即 commit に取り込むため、`.gitignore` 漏れがあると scratch ファイルが PR に +//! 意図せず混入する (PR #85 で `__parse_transcripts.ps1` 実例)。 +//! +//! Override: env var `SCRATCH_FILE_WARNING_OVERRIDE=1` で意図的バイパス可能。 +//! +//! Stage 配置: `run_pipeline` の最早期 (quality_gate より前)。検出時は quality_gate +//! や takt review を無駄に走らせず即停止する。 +//! +//! Config-driven pattern: `[scratch_file_warning]` section で `patterns` を拡張可能。 +//! 順位 5 (AI 生成一時スクリプト pattern の pre-push 検出) は本 stage の patterns +//! 拡張 (例: `_tmp_*`) + ADR-007 連携で補完的に実装する。 + +use std::process::Command; + +use crate::config::ScratchFileWarningConfig; +use crate::log::{log_info, log_stage}; + +const JJ_TIMEOUT_SECS: u64 = 30; +const OVERRIDE_ENV_VAR: &str = "SCRATCH_FILE_WARNING_OVERRIDE"; +const DEFAULT_PATTERN: &str = "__*"; + +/// `[scratch_file_warning]` config の有無に応じて検査を実行し、 +/// push を続行してよいか (= violation なし or override active) を返す。 +/// +/// `None` は section 不在を意味し、default-ON 動作 (= 検査を実行、patterns=["__*"])。 +/// `Some(c)` で `c.enabled = Some(false)` の場合のみ完全 skip。 +/// +/// fail-open: jj 不調 (timeout / 起動失敗) 時は warning ログのみで true を返し、 +/// push 自体は止めない。 +pub(crate) fn run_scratch_file_warning(config: Option<&ScratchFileWarningConfig>) -> bool { + let enabled = config.and_then(|c| c.enabled).unwrap_or(true); + if !enabled { + return true; + } + let patterns = effective_patterns(config); + let files = match list_files_in_at() { + Ok(f) => f, + Err(e) => { + log_info(&format!( + "scratch_file_warning: jj file list 失敗、検査を skip して push を続行します: {}", + e + )); + return true; + } + }; + let violations = find_violations(&files, &patterns); + if violations.is_empty() { + log_stage("scratch", "scratch ファイル検出なし"); + return true; + } + log_stage( + "scratch", + &format!( + "scratch ファイル候補 ({} 件) が @ commit に含まれます:", + violations.len() + ), + ); + for v in &violations { + log_info(&format!(" - {}", v)); + } + let raw = std::env::var(OVERRIDE_ENV_VAR).ok(); + if parse_override_env(raw.as_deref()) { + log_info(&format!( + " {}={} により続行します (意図的バイパス)", + OVERRIDE_ENV_VAR, + raw.as_deref().unwrap_or("") + )); + true + } else { + log_info(&format!( + " 対処:\n \ + (a) `.gitignore` に該当 pattern を追加 + `jj abandon @ && jj new` で再記述\n \ + (b) ファイル自体を削除\n \ + (c) 意図的 commit なら env {}=1 を設定して再実行", + OVERRIDE_ENV_VAR + )); + false + } +} + +fn effective_patterns(config: Option<&ScratchFileWarningConfig>) -> Vec<String> { + config + .and_then(|c| c.patterns.as_ref()) + .filter(|p| !p.is_empty()) + .cloned() + .unwrap_or_else(|| vec![DEFAULT_PATTERN.to_string()]) +} + +fn list_files_in_at() -> Result<Vec<String>, String> { + let output = run_jj_file_list_at()?; + Ok(parse_file_list_output(&output)) +} + +fn parse_file_list_output(raw: &str) -> Vec<String> { + raw.lines() + .map(|line| line.trim().to_string()) + .filter(|s| !s.is_empty()) + .collect() +} + +fn extract_basename(path: &str) -> &str { + match path.rfind(['/', '\\']) { + Some(idx) => &path[idx + 1..], + None => path, + } +} + +/// 簡易 glob: `*` (任意長文字列、空マッチ含む) のみサポート。`?` 等は未対応。 +/// パターンに `*` が含まれない場合は完全一致。 +fn matches_glob(name: &str, pattern: &str) -> bool { + if !pattern.contains('*') { + return name == pattern; + } + let parts: Vec<&str> = pattern.split('*').collect(); + match_pattern_parts(name, &parts) +} + +fn match_pattern_parts(name: &str, parts: &[&str]) -> bool { + let Some(after_prefix) = consume_prefix(name, parts.first().copied().unwrap_or("")) else { + return false; + }; + let middle_parts = pattern_middle_slice(parts); + let Some(after_middle) = consume_middle(after_prefix, middle_parts) else { + return false; + }; + if parts.len() > 1 { + let suffix = parts.last().copied().unwrap_or(""); + check_suffix(after_middle, suffix) + } else { + true + } +} + +fn pattern_middle_slice<'a>(parts: &'a [&'a str]) -> &'a [&'a str] { + if parts.len() > 2 { + &parts[1..parts.len() - 1] + } else { + &[] + } +} + +fn consume_prefix<'a>(name: &'a str, prefix: &str) -> Option<&'a str> { + if prefix.is_empty() { + Some(name) + } else if name.starts_with(prefix) { + Some(&name[prefix.len()..]) + } else { + None + } +} + +fn consume_middle<'a>(name: &'a str, middle_parts: &[&str]) -> Option<&'a str> { + let mut remaining = name; + for part in middle_parts { + if part.is_empty() { + continue; + } + let idx = remaining.find(part)?; + remaining = &remaining[idx + part.len()..]; + } + Some(remaining) +} + +fn check_suffix(name: &str, suffix: &str) -> bool { + suffix.is_empty() || name.ends_with(suffix) +} + +fn find_violations(files: &[String], patterns: &[String]) -> Vec<String> { + let mut violations = Vec::new(); + for file in files { + let name = extract_basename(file); + for pattern in patterns { + if matches_glob(name, pattern) { + violations.push(file.clone()); + break; + } + } + } + violations +} + +fn parse_override_env(raw: Option<&str>) -> bool { + let Some(value) = raw else { + return false; + }; + matches!( + value.trim().to_ascii_lowercase().as_str(), + "1" | "true" | "yes" | "on" + ) +} + +fn run_jj_file_list_at() -> Result<String, String> { + use std::process::Stdio; + + let mut child = Command::new("jj") + .args(["file", "list", "-r", "@"]) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .map_err(|e| format!("jj file list 起動失敗: {}", e))?; + + let stdout_handle = + crate::runner::drain_pipe(child.stdout.take().expect("stdout must be piped")); + let stderr_handle = + crate::runner::drain_pipe(child.stderr.take().expect("stderr must be piped")); + + let status = crate::runner::wait_with_timeout("jj file list", &mut child, JJ_TIMEOUT_SECS) + .map_err(|e| format!("jj file list wait 失敗: {}", e))?; + + let stdout = stdout_handle.join().unwrap_or_default(); + let stderr = stderr_handle.join().unwrap_or_default(); + + match status { + None => Err(format!("jj file list タイムアウト ({}s)", JJ_TIMEOUT_SECS)), + Some(s) if s.success() => Ok(stdout), + Some(_) => Err(stderr.trim().to_string()), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_file_list_basic() { + let raw = "src/main.rs\nsrc/lib.rs\n"; + assert_eq!( + parse_file_list_output(raw), + vec!["src/main.rs", "src/lib.rs"] + ); + } + + #[test] + fn parse_file_list_skips_empty_lines() { + let raw = "src/main.rs\n\n\nsrc/lib.rs\n"; + assert_eq!( + parse_file_list_output(raw), + vec!["src/main.rs", "src/lib.rs"] + ); + } + + #[test] + fn parse_file_list_trims_whitespace() { + let raw = " src/main.rs \n\tsrc/lib.rs\t\n"; + assert_eq!( + parse_file_list_output(raw), + vec!["src/main.rs", "src/lib.rs"] + ); + } + + #[test] + fn parse_file_list_empty_returns_empty() { + assert_eq!(parse_file_list_output(""), Vec::<String>::new()); + } + + #[test] + fn extract_basename_forward_slash() { + assert_eq!(extract_basename("src/foo/bar.rs"), "bar.rs"); + } + + #[test] + fn extract_basename_backslash() { + assert_eq!(extract_basename(r"src\foo\bar.rs"), "bar.rs"); + } + + #[test] + fn extract_basename_no_separator() { + assert_eq!(extract_basename("foo.rs"), "foo.rs"); + } + + #[test] + fn extract_basename_mixed_separators() { + assert_eq!(extract_basename(r"src/foo\bar.rs"), "bar.rs"); + assert_eq!(extract_basename(r"src\foo/bar.rs"), "bar.rs"); + } + + #[test] + fn extract_basename_trailing_separator_returns_empty() { + assert_eq!(extract_basename("src/foo/"), ""); + } + + #[test] + fn matches_glob_prefix_wildcard() { + assert!(matches_glob("__foo", "__*")); + assert!(matches_glob("__", "__*")); + assert!(!matches_glob("foo__", "__*")); + assert!(!matches_glob("_foo", "__*")); + } + + #[test] + fn matches_glob_suffix_wildcard() { + assert!(matches_glob("foo.tmp", "*.tmp")); + assert!(matches_glob(".tmp", "*.tmp")); + assert!(!matches_glob("foo.tmpx", "*.tmp")); + } + + #[test] + fn matches_glob_prefix_and_suffix_wildcards() { + assert!(matches_glob("_tmp_file.ps1", "_tmp_*")); + assert!(matches_glob("__file.py", "__*.py")); + assert!(!matches_glob("__file.ps1", "__*.py")); + } + + #[test] + fn matches_glob_single_middle_wildcard() { + assert!(matches_glob("foobazbar", "foo*bar")); + assert!(matches_glob("foobar", "foo*bar")); + assert!(!matches_glob("fooXY", "foo*bar")); + } + + #[test] + fn matches_glob_three_part_pattern() { + assert!(matches_glob("mytest_x.ps1", "*test*.ps1")); + assert!(matches_glob("test.ps1", "*test*.ps1")); + assert!(!matches_glob("foo.ps1", "*test*.ps1")); + } + + #[test] + fn matches_glob_no_wildcard_exact() { + assert!(matches_glob("foo", "foo")); + assert!(!matches_glob("foo.bar", "foo")); + assert!(!matches_glob("foo", "bar")); + } + + #[test] + fn matches_glob_only_wildcard_matches_anything() { + assert!(matches_glob("anything", "*")); + assert!(matches_glob("", "*")); + } + + #[test] + fn matches_glob_empty_pattern_exact() { + assert!(matches_glob("", "")); + assert!(!matches_glob("foo", "")); + } + + #[test] + fn find_violations_detects_default_pattern() { + let files = vec![ + "src/main.rs".to_string(), + "__test.ps1".to_string(), + "docs/__draft.md".to_string(), + "src/__scratch.rs".to_string(), + ]; + let patterns = vec!["__*".to_string()]; + let violations = find_violations(&files, &patterns); + assert_eq!( + violations, + vec![ + "__test.ps1".to_string(), + "docs/__draft.md".to_string(), + "src/__scratch.rs".to_string() + ] + ); + } + + #[test] + fn find_violations_empty_when_no_match() { + let files = vec!["src/main.rs".to_string(), "Cargo.toml".to_string()]; + let patterns = vec!["__*".to_string()]; + assert!(find_violations(&files, &patterns).is_empty()); + } + + #[test] + fn find_violations_multiple_patterns() { + let files = vec![ + "__test.ps1".to_string(), + "_tmp_log.txt".to_string(), + "src/main.rs".to_string(), + ]; + let patterns = vec!["__*".to_string(), "_tmp_*".to_string()]; + let violations = find_violations(&files, &patterns); + assert_eq!(violations.len(), 2); + assert!(violations.contains(&"__test.ps1".to_string())); + assert!(violations.contains(&"_tmp_log.txt".to_string())); + } + + #[test] + fn find_violations_reports_file_only_once_when_matching_multiple_patterns() { + let files = vec!["__test.tmp".to_string()]; + let patterns = vec!["__*".to_string(), "*.tmp".to_string()]; + let violations = find_violations(&files, &patterns); + assert_eq!(violations.len(), 1); + } + + #[test] + fn find_violations_matches_basename_in_any_subdirectory() { + let files = vec![ + "subdir/__hidden.txt".to_string(), + r"win\path\__hidden.txt".to_string(), + "__top.txt".to_string(), + ]; + let patterns = vec!["__*".to_string()]; + assert_eq!(find_violations(&files, &patterns).len(), 3); + } + + #[test] + fn find_violations_ignores_dirname_prefix_match_when_basename_does_not_match() { + let files = vec!["__src/main.rs".to_string()]; + let patterns = vec!["__*".to_string()]; + assert!(find_violations(&files, &patterns).is_empty()); + } + + #[test] + fn parse_override_env_truthy() { + for v in [ + "1", "true", "TRUE", "yes", "YES", "on", "On", " true ", "\tyes\n", + ] { + assert!(parse_override_env(Some(v)), "'{}' should be truthy", v); + } + } + + #[test] + fn parse_override_env_falsy() { + for v in ["0", "false", "no", "off", "", " ", "maybe", "enable"] { + assert!(!parse_override_env(Some(v)), "'{}' should be falsy", v); + } + } + + #[test] + fn parse_override_env_none_is_false() { + assert!(!parse_override_env(None)); + } + + #[test] + fn effective_patterns_default_when_none() { + let p = effective_patterns(None); + assert_eq!(p, vec!["__*".to_string()]); + } + + #[test] + fn effective_patterns_default_when_no_patterns_field() { + let config = ScratchFileWarningConfig { + enabled: Some(true), + patterns: None, + }; + assert_eq!(effective_patterns(Some(&config)), vec!["__*".to_string()]); + } + + #[test] + fn effective_patterns_default_when_empty_list() { + let config = ScratchFileWarningConfig { + enabled: Some(true), + patterns: Some(vec![]), + }; + assert_eq!(effective_patterns(Some(&config)), vec!["__*".to_string()]); + } + + #[test] + fn effective_patterns_uses_config_when_provided() { + let config = ScratchFileWarningConfig { + enabled: Some(true), + patterns: Some(vec!["__*".to_string(), "_tmp_*".to_string()]), + }; + assert_eq!( + effective_patterns(Some(&config)), + vec!["__*".to_string(), "_tmp_*".to_string()] + ); + } +} From f153eda9974d43613a200cecfff804f6c31581f1 Mon Sep 17 00:00:00 2001 From: aloekun <aloekun.a10e@gmail.com> Date: Tue, 26 May 2026 12:29:56 +0900 Subject: [PATCH 3/5] =?UTF-8?q?docs(todo):=20Bundle=201=20=E5=AE=8C?= =?UTF-8?q?=E4=BA=86=E3=81=AB=E4=BC=B4=E3=81=84=20=E9=A0=86=E4=BD=8D=201?= =?UTF-8?q?=20/=20116=20/=20134=20=E3=82=92=E5=89=8A=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/todo-summary.md | 3 -- docs/todo2.md | 34 ---------------------- docs/todo8.md | 68 -------------------------------------------- 3 files changed, 105 deletions(-) diff --git a/docs/todo-summary.md b/docs/todo-summary.md index f88664f..fd7234f 100644 --- a/docs/todo-summary.md +++ b/docs/todo-summary.md @@ -11,7 +11,6 @@ | 順位 | Tier | タスク | ファイル | 工数 | 依存 | |---|---|---|---|---|---| -| 1 | 🚀 Tier 1 | push 前 untracked `__*` ファイル警告 hook (PR #85 T1-4) | todo2.md | Small | なし (PR #85 直接対策) | | 2 | 🚀 Tier 1 | `cli-push-runner` jj bookmark 未設定 early-exit (PR #85 T1-3) | todo2.md | S | なし | | 5 | 🚀 Tier 1 | **AI 生成一時スクリプト pattern の pre-push 検出 (PR #88 T1-2)** | todo3.md | Small | 順位 1 と関連 (要擦り合わせ) | | 6 | 🚀 Tier 1 | ADR-032 PR-pre: GitHub Branch Protection 整備 | todo2.md | 設定のみ | なし (依存タスクは完了済) | @@ -59,12 +58,10 @@ | 108 | 💎 Tier 3 | **CLAUDE.md に「Tier 2 偽装検知 + 却下パターン」table (PR #141 T3-#3 採用)** | todo6.md | S | なし (`~/.claude/CLAUDE.md` に memory `feedback_no_unenforced_rules` の policy をユーザー可視 table として公開、Tier 2 と称した必須化ルール提案を新セッションでも一貫して却下できる構造、memory ファイル閉鎖を補完) | | 110 | 💎 Tier 3 | **pure function test pattern template を `testing.md` に追記 (PR #142 T2-#3 採用)** | todo6.md | S | なし (Phase A の `overflow_hint()` をモデル例とし「境界値 / None / 閾値未満」3 パターンの test テンプレを `~/.claude/rules/common/testing.md` に追記、副作用分離の促進、Rust lib 全般で再利用) | | 111 | 💎 Tier 3 | **`docs-governance.md` に todo5/todo6 routing rule 明文化 (PR #142 T3-#1 採用)** | todo6.md | S | なし (Phase/bundle 関連 → todo6、global rules/lint → todo5 等の routing rule を `~/.claude/rules/common/docs-governance.md` に追記、PR #142 で実証された file pointer bifurcation の構造的予防、CR Minor #2 と同根) | -| 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) と整合させて結論を出す | | 128 | 💎 Tier 3 | **CLAUDE.md § Cross-File Reference Lifecycle に多ファイル同時削除 retirement condition checklist を追加 (PR #153 T3-#2 採用)** | todo8.md | XS | なし (PR #133 (todo.md 分割) + PR #153 (analysis.md 分割) の successful pattern を明文化、`feedback_no_unenforced_rules.md` 例外 = 既存実践の明文化 + guide 効果、順位 122 / 127 と同じロジック、`~/.claude/` global 配下で派生プロジェクトに自動波及) | | 133 | 💎 Tier 3 | **docs-governance §Retirement Workflow に「diff context 由来 false alarm 防止 = grep hit は実ファイル Read で確認」明記 (PR #156 T3 #1 採用)** | todo8.md | XS | なし (PR #156 で 5 件以上の false alarm 発生、`feedback_no_unenforced_rules.md` 例外 = 既存実践の明文化 + guide 効果、順位 122 / 127 と同じロジック、`~/.claude/` global 配下で派生プロジェクトに自動波及) | -| 134 | 💎 Tier 3 | **ADR-035 に docs-only PR 評価の適用外基準リスト追加 (mutation / error handling / DRY / YAGNI / function length / test coverage / magic-number 等) (PR #156 T3 #2 採用)** | todo8.md | S | なし (Severity Medium = reviewer の criteria 誤適用による unnecessary review overhead / 開発体験劣化、ADR-035 は分類基準のみ定義済で適用外基準が未明示、`feedback_no_unenforced_rules.md` 例外 = ADR への追加で機械強制ではなく reviewer / Claude の judgment 補助) | | 135 | 💎 Tier 3 | **todo entry の ADR 番号 hardcode 撤廃 — 「ADR-NNN (採番未確定、land 時に確定)」placeholder 採用 (順位 78 番号 conflict 2026-05-16 観測由来)** | todo8.md | XS | なし (順位 78 (旧 ADR-038 → ADR-041) で番号 conflict が顕在化、queue 滞留 entry の hardcode が後発 PR の採番と衝突する構造リスクを convention で予防、`~/.claude/rules/common/docs-governance.md` に 2-3 行追記。採番予約簿は管理コスト過剰のため見送り、land 時 PR で空き番号確定の軽量運用に統一) | | 136 | 🚀 Tier 1 | **working copy staleness 検出 hook 2 段構え + stale todo entry 既実装 grep 提示 (PR cleanup-stale-rank-39 由来 + PR #150 T3-#1 統合 2026-05-25)** | todo8.md | M-L | なし (本セッション実証 failure mode の structural enforcement + 旧 順位 122 機能統合。案 A SessionStart で jj fetch + lineage 報告、案 B PreToolUse で docs/todo*.md edit 時の stale block + 既実装 grep 自動実行で関連 commit を warning 提示、rule 追加 (= 順位 122 当初案) を仕組み化に切替で session 跨ぎ品質一定化、ADR-039 experimental pattern 適用) | | 140 | 💎 Tier 3 | **順位 135「codified placeholder policy」を正式 ADR に昇格 (PR #169 T3-#2 採用)** | todo8.md | S | なし (順位 135 entry を retire し、ADR-NNN (採番未確定、land 時に確定): ADR Numbering Strategy として永続化。PR #111/#132/#169 の 3+ PR で適用実証済 — PR #169 で「ADR-038 → 041 → NNN」3 段振り直し dogfood が land、ephemeral todo entry 限りでは派生プロジェクトへの transferability 不足、`feedback_no_unenforced_rules.md` 例外 = 既存実践 (3 PR で実証) の明文化 + 後続 entry が同 policy を参照する際の永続 reference 確保) | diff --git a/docs/todo2.md b/docs/todo2.md index 6b2c169..a72d283 100644 --- a/docs/todo2.md +++ b/docs/todo2.md @@ -362,40 +362,6 @@ Phase 観測 (4-6 週) Phase 2 (任意、段階的緩和) ``` -### push 前 untracked `__*` ファイル警告 hook (PR #85 T1-4) - -> **動機**: PR #85 で `__parse_transcripts.ps1` が jj auto-snapshot 経由で commit に意図せず混入。`.gitignore` への `__*` 追加で当面の再発は防止できたが、将来 `.gitignore` 漏れの可能性は残る。push 前に `__*` 命名の untracked file が working directory に残っていないか機械的に検出する安全網が必要。 -> -> **本タスクの位置づけ**: jj 環境では staging area が無く `.gitignore` が唯一のフィルタ。push 前 hook で `__*` パターンの untracked file を検出し警告すれば、`.gitignore` 漏れがあっても気付ける。 -> -> **参照**: `.claude/feedback-reports/85.md` Tier 1 #4 -> -> **実行優先度**: 🚀 **Tier 1** — Small 工数、直近インシデントの直接対策。同種事故 (PR scope 外ファイル混入) の再発防止で、混入時の追加コスト (force-push + 再 review) を回避。 - -#### 設計決定 (案) - -- 配置先: `cli-push-runner` の早期段階 (bookmark check の隣)、または独立 hooks binary -- 検出方法: `jj status` 出力から `Untracked` セクションを parse、`__*` パターンとマッチング -- 失敗時挙動: warning + ユーザー確認待ち (本人が意図的に scratch を残している場合の override を許容) -- config: `[scratch_file_warning] patterns = ["__*"]` で将来の拡張性確保 - -#### 作業計画 - -- [ ] 検出ロジックを `cli-push-runner` または共通ライブラリに実装 -- [ ] config に `[scratch_file_warning]` セクション追加 -- [ ] dogfood: `__test.ps1` を意図的に作って push し、警告を確認 -- [ ] 派生プロジェクトへ deploy -- [ ] 本 todo2.md エントリを削除 - -#### 完了基準 - -- push 前に `__*` 命名の untracked file が存在すると警告が出る -- override コマンド (env var or flag) で意図的バイパスが可能 - -#### 詰まっている箇所 - -なし - ### `cli-push-runner` jj bookmark 未設定 early-exit (PR #85 T1-3) > **動機**: PR #85 の初回 `pnpm push` で bookmark 未設定 → `jj git push` が `Nothing changed` で終了し、158s かけて走った Quality Gate + takt review がすべて無駄になった。jj 環境特有の落とし穴で、決定論的に防止可能。 diff --git a/docs/todo8.md b/docs/todo8.md index 034f2e8..36c6715 100644 --- a/docs/todo8.md +++ b/docs/todo8.md @@ -10,29 +10,6 @@ ## 現在進行中 -### ADR-040 `step_timeout` 説明に sublinear / KV cache locality clarification 追記 (PR #145 T3-#1 採用) - -> **動機**: 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 - -#### 作業計画 - -- [ ] 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 行削除 - -#### 完了基準 - -- ADR-040 の reference table と本文の formula が矛盾なく解釈可能になる -- 派生プロジェクトの porting 時に sublinear の根拠が永続記録から逆引きできる - ---- - ### rule⑧ への paths filter 適用範囲検討 (順位 102 land 時に意図的保留、follow-up) > **動機**: 順位 102 (PR #148 想定で land 中、Phase D D-3) で paths filter が lint runner に実装されたが、当初計画した rule⑧ への `paths = ["docs/**/*.md"]` migration は **意図的に保留**。理由: D-2 (PR #146、順位 101) で追加した「root-level MD (CLAUDE.md / README.md) からの `../docs/` 参照を fire = true positive で扱う」design intent が、`paths = ["docs/**/*.md"]` 適用で scope narrow されて壊れる (root-level MD の実 path が docs/ 配下ではないため rule 対象外になり、broken link 検出を失う)。本タスクで以下のいずれを採用するか検討する: @@ -142,51 +119,6 @@ --- -### ADR-035 に docs-only PR 評価の明示的な適用外基準リストを追加 (PR #156 T3 #2 採用) - -> **動機**: ADR-035 は docs-only PR の **分類基準** (どの PR が docs-only か) は定義しているが、**除外される評価観点** (docs-only PR で適用すべきでない code-logic 系評価項目) が明示されていない。PR #156 (Phase E、docs-only) で reviewer が mutation / error handling / test coverage 等の code-logic criteria を docs-only PR に適用しかけて unnecessary review overhead が発生する潜在リスクが観測された。明示することで将来セッションでの reviewer による criteria 誤適用を防止できる。 -> -> **本タスクの位置づけ**: PR #156 post-merge-feedback Tier 3 #2 採用 (Severity Medium / Frequency Low / Effort S / Adoption Risk None)。Severity Medium の根拠 = 誤適用による unnecessary review overhead / 開発体験劣化。`feedback_no_unenforced_rules.md` 例外条件 = ADR (= 設計判断 doc) への追加で機械強制ではなく reviewer / Claude の judgment 補助。 -> -> **参照**: `.claude/feedback-reports/156.md` Tier 3 #2、`docs/adr/adr-035-doc-evaluation-policy.md` - -#### 設計決定 (案) - -- **配置先**: `docs/adr/adr-035-doc-evaluation-policy.md` 内に新 section 「docs-only PR で適用しない評価観点」を追加 -- **適用外基準リスト (案)**: - - **Mutation / immutability**: docs に code mutation は存在しないため適用しない - - **Error handling**: docs に error path は存在しないため適用しない - - **Test coverage**: docs に test は不要なため適用しない (test 文言の追加自体は除く) - - **Function length / complexity**: docs に関数は存在しないため適用しない - - **DRY / YAGNI**: docs では intentional な重複・冗長な記述が reader にとって有益な場合があるため適用しない (例: 同じ概念を複数 section で説明する) - - **Magic number / hardcoded value**: docs 中の数値は説明的記述で magic ではないため適用しない -- **適用される評価観点** (既存 ADR-035 で定義済みのものを再確認): - - Cross-reference lifecycle (permanent → ephemeral 禁止) - - Markdown syntax / lint - - Anchor link validity - - Retirement workflow 整合 - - 内容の正確性 / typo - -#### 作業計画 - -- [ ] `docs/adr/adr-035-doc-evaluation-policy.md` の構造確認 (既存 section header の慣習) -- [ ] 「適用外基準リスト」section を追加 -- [ ] 既存 ADR の評価観点 section との整合性確認 (重複説明の有無、cross-reference の追加) -- [ ] markdownlint clean 確認 -- [ ] 本エントリ削除 + todo-summary.md 行削除 - -#### 完了基準 - -- docs-only PR の reviewer / Claude が「mutation / DRY 等は適用しない」を ADR から逆引きできる -- 将来の docs-only PR 評価で criteria 誤適用が systemic に発生しなくなる -- markdownlint clean - -#### 詰まっている箇所 - -なし。Effort S、ADR への追記のみで副作用最小。 - ---- - ### todo entry の ADR 番号 hardcode 撤廃 — 「ADR-NNN (採番未確定、land 時に確定)」placeholder 採用 (順位 78 番号 conflict 2026-05-16 観測由来) > **動機**: 順位 78 (旧 ADR-038 Rust timestamp arithmetic safety、PR #115 T3-1) は entry 登録時 (2026 年序盤) に新規 ADR として ADR-038 を予約のつもりで hardcode していたが、queue 滞留中に Bundle Z 系列の連続採用で `ADR-037 / 038 / 039 / 040` がすべて占有され、2026-05-16 セッションで番号 conflict が顕在化 (ADR-041 へ振り直し)。さらに 2026-05-22 に順位 139 (PR #168 follow-up) が ADR-041 を取得したため順位 78 を再 placeholder 化 = **同一 entry が 3 回 (038 → 041 → NNN) 番号変更を経た実証ベース**で、queue 深度と滞留期間の積に比例して同型 conflict が再発する構造リスクを convention で予防する必要がある。 From 0ca9df64d65dc27f540318f98de69b9b59555f41 Mon Sep 17 00:00:00 2001 From: aloekun <aloekun.a10e@gmail.com> Date: Tue, 26 May 2026 15:54:42 +0900 Subject: [PATCH 4/5] =?UTF-8?q?fix(cli-push-runner):=20scratch=5Ffile=5Fwa?= =?UTF-8?q?rning=20=E3=82=92=20ADR-039=20opt-in=20=E8=A8=AD=E8=A8=88?= =?UTF-8?q?=E3=81=AB=E4=BF=AE=E6=AD=A3=20(CR=20#174=20Major)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- push-runner-config.toml | 16 +++++++++--- src/cli-push-runner/src/config.rs | 8 +++--- .../src/stages/scratch_file_warning.rs | 25 +++++++++++++------ 3 files changed, 35 insertions(+), 14 deletions(-) diff --git a/push-runner-config.toml b/push-runner-config.toml index e420b2b..d5090aa 100644 --- a/push-runner-config.toml +++ b/push-runner-config.toml @@ -8,9 +8,19 @@ # `@` commit に `__*` 等の scratch ファイルが含まれていないか push 前に検査し、 # 検出時は push を block する。jj auto-snapshot 環境で .gitignore 漏れがあると # scratch ファイルが PR に混入する事故 (PR #85 で実証) の構造的予防。 -# section 不在時は default-ON 動作 (enabled=true, patterns=["__*"])。 -# Override: env SCRATCH_FILE_WARNING_OVERRIDE=1 で意図的バイパス可能。 -# 順位 5 で _tmp_* 等の追加 pattern を本 section の patterns に追加して拡張する。 +# +# ADR-039 (Experimental feature 標準パターン) 3 点セット: +# - **Config opt-in**: 試験運用のため default OFF。本リポジトリでは明示的に +# `enabled = true` で dogfood を開始。section 不在 / enabled 未設定では完全 skip。 +# - **Kill-switch**: `enabled = false` で完全停止。env `SCRATCH_FILE_WARNING_OVERRIDE=1` +# で個別 push の意図的バイパスも可能。 +# - **Bounded lifetime**: 本リポジトリで 3-5 PR の dogfood 後 (false positive 観測 / +# 検出効果 / override 使用頻度) に default-ON 昇格 or 却下を判定。判定結果は +# `src/cli-push-runner/src/stages/scratch_file_warning.rs` module doc + 本 section +# コメントに反映する。 +# +# 順位 5 (AI 生成一時スクリプト pattern) で `_tmp_*` 等の追加 pattern を本 section +# の `patterns` に追加して拡張する (補完アプローチ、別 PR Bundle 3 で実施)。 # --------------------------------------------------------------------------- [scratch_file_warning] enabled = true diff --git a/src/cli-push-runner/src/config.rs b/src/cli-push-runner/src/config.rs index 4ec415c..57d822c 100644 --- a/src/cli-push-runner/src/config.rs +++ b/src/cli-push-runner/src/config.rs @@ -38,12 +38,14 @@ pub(crate) struct Config { /// 順位 1 (PR #85 T1-4) — scratch ファイル (`__*` 等) が `@` commit に /// 混入していないか push 前に検査する stage の config。 /// -/// `[scratch_file_warning]` section が TOML に**不在**の場合は default-ON 動作 -/// (= `enabled = true`, `patterns = ["__*"]`)。security-critical かつ漏洩観測前の -/// preventive 層のため、明示的な opt-out が無い限り検査する。 +/// ADR-039 (Experimental feature 標準パターン) § 1 Config opt-in 準拠: +/// `[scratch_file_warning]` section 不在 / `enabled` 未設定 / `enabled = false` +/// のいずれも検査を **skip** (= default `enabled = false`)。明示的に `enabled = true` +/// にしたときのみ検査実行 (3-5 PR の dogfood 後に default-ON 昇格 or 却下を判定)。 /// /// `patterns` は順位 5 (AI 生成一時スクリプト pattern の pre-push 検出) で /// `_tmp_*` 等の追加 pattern を config-driven で拡張可能 (= 補完アプローチ)。 +/// `patterns` 未設定時の default は `["__*"]` (= stage 側 `DEFAULT_PATTERN`)。 #[derive(Deserialize)] pub(crate) struct ScratchFileWarningConfig { pub(crate) enabled: Option<bool>, diff --git a/src/cli-push-runner/src/stages/scratch_file_warning.rs b/src/cli-push-runner/src/stages/scratch_file_warning.rs index 4c708ec..2ae5f11 100644 --- a/src/cli-push-runner/src/stages/scratch_file_warning.rs +++ b/src/cli-push-runner/src/stages/scratch_file_warning.rs @@ -1,11 +1,19 @@ //! Scratch file warning stage — 順位 1 (PR #85 T1-4) //! -//! `@` commit に scratch-pattern ファイル (default: `__*`) が含まれていないか検査し、 -//! 検出時は warning + block で push を停止する。jj は auto-snapshot で working tree -//! を即 commit に取り込むため、`.gitignore` 漏れがあると scratch ファイルが PR に -//! 意図せず混入する (PR #85 で `__parse_transcripts.ps1` 実例)。 +//! `@` commit に scratch-pattern ファイル (default pattern: `__*`) が含まれていないか +//! 検査し、検出時は warning + block で push を停止する。jj は auto-snapshot で +//! working tree を即 commit に取り込むため、`.gitignore` 漏れがあると scratch +//! ファイルが PR に意図せず混入する (PR #85 で `__parse_transcripts.ps1` 実例)。 //! -//! Override: env var `SCRATCH_FILE_WARNING_OVERRIDE=1` で意図的バイパス可能。 +//! ADR-039 (Experimental feature 標準パターン) 準拠の 3 点セット: +//! - **Config opt-in**: 試験運用のため default `enabled = false`、`[scratch_file_warning]` +//! section で明示的に `enabled = true` にしないと検査は走らない。section 不在 / +//! enabled 未指定の場合も skip (= 完全 no-op)。 +//! - **Kill-switch**: `enabled = false` (TOML) または env override +//! `SCRATCH_FILE_WARNING_OVERRIDE=1` で意図的バイパス可能。 +//! - **Bounded lifetime**: 3-5 PR の dogfood で false positive / 検出効果を観測後、 +//! default-ON 昇格 or 却下を判定 (詳細は push-runner-config.toml の +//! `[scratch_file_warning]` section コメント参照)。 //! //! Stage 配置: `run_pipeline` の最早期 (quality_gate より前)。検出時は quality_gate //! や takt review を無駄に走らせず即停止する。 @@ -26,13 +34,14 @@ const DEFAULT_PATTERN: &str = "__*"; /// `[scratch_file_warning]` config の有無に応じて検査を実行し、 /// push を続行してよいか (= violation なし or override active) を返す。 /// -/// `None` は section 不在を意味し、default-ON 動作 (= 検査を実行、patterns=["__*"])。 -/// `Some(c)` で `c.enabled = Some(false)` の場合のみ完全 skip。 +/// ADR-039 § 1 Config opt-in 準拠: default `enabled = false` (試験運用)。 +/// section 不在 / `c.enabled = None` / `c.enabled = Some(false)` のいずれも skip。 +/// 明示的に `c.enabled = Some(true)` のときのみ検査を実行。 /// /// fail-open: jj 不調 (timeout / 起動失敗) 時は warning ログのみで true を返し、 /// push 自体は止めない。 pub(crate) fn run_scratch_file_warning(config: Option<&ScratchFileWarningConfig>) -> bool { - let enabled = config.and_then(|c| c.enabled).unwrap_or(true); + let enabled = config.and_then(|c| c.enabled).unwrap_or(false); if !enabled { return true; } From d566e05a6542aba610f551b009ffbacb45d5b92d Mon Sep 17 00:00:00 2001 From: aloekun <aloekun.a10e@gmail.com> Date: Tue, 26 May 2026 17:05:12 +0900 Subject: [PATCH 5/5] =?UTF-8?q?fix(cli-push-runner):=20scratch=5Ffile=5Fwa?= =?UTF-8?q?rning=20effective=5Fpatterns=20=E3=81=A7=E7=A9=BA=E6=96=87?= =?UTF-8?q?=E5=AD=97=E8=A6=81=E7=B4=A0=E3=82=92=20default=20fallback=20?= =?UTF-8?q?=E5=AF=BE=E8=B1=A1=E3=81=AB=20(CR=20#174=20Minor)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/stages/scratch_file_warning.rs | 45 ++++++++++++++++++- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/src/cli-push-runner/src/stages/scratch_file_warning.rs b/src/cli-push-runner/src/stages/scratch_file_warning.rs index 2ae5f11..2ccf9a9 100644 --- a/src/cli-push-runner/src/stages/scratch_file_warning.rs +++ b/src/cli-push-runner/src/stages/scratch_file_warning.rs @@ -94,8 +94,14 @@ pub(crate) fn run_scratch_file_warning(config: Option<&ScratchFileWarningConfig> fn effective_patterns(config: Option<&ScratchFileWarningConfig>) -> Vec<String> { config .and_then(|c| c.patterns.as_ref()) - .filter(|p| !p.is_empty()) - .cloned() + .map(|patterns| { + patterns + .iter() + .map(|p| p.trim().to_string()) + .filter(|p| !p.is_empty()) + .collect::<Vec<_>>() + }) + .filter(|patterns| !patterns.is_empty()) .unwrap_or_else(|| vec![DEFAULT_PATTERN.to_string()]) } @@ -470,4 +476,39 @@ mod tests { vec!["__*".to_string(), "_tmp_*".to_string()] ); } + + #[test] + fn effective_patterns_default_when_only_blank_entries() { + let config = ScratchFileWarningConfig { + enabled: Some(true), + patterns: Some(vec!["".to_string(), " ".to_string()]), + }; + assert_eq!(effective_patterns(Some(&config)), vec!["__*".to_string()]); + } + + #[test] + fn effective_patterns_filters_blank_entries_and_keeps_valid_ones() { + let config = ScratchFileWarningConfig { + enabled: Some(true), + patterns: Some(vec![ + "".to_string(), + "__*".to_string(), + " ".to_string(), + "_tmp_*".to_string(), + ]), + }; + assert_eq!( + effective_patterns(Some(&config)), + vec!["__*".to_string(), "_tmp_*".to_string()] + ); + } + + #[test] + fn effective_patterns_trims_whitespace_in_pattern_values() { + let config = ScratchFileWarningConfig { + enabled: Some(true), + patterns: Some(vec![" __* ".to_string()]), + }; + assert_eq!(effective_patterns(Some(&config)), vec!["__*".to_string()]); + } }