Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions .claude/custom-lint-rules.toml
Original file line number Diff line number Diff line change
Expand Up @@ -530,3 +530,60 @@ rs = [
"no_write_result_discard_skips_named_binding_starting_with_underscore",
"no_write_result_discard_only_targets_rust_extension",
]

# ─── ルール⑪: jj template の `first_line()` メソッド使用禁止 (曖昧性防止) ───
#
# 由来: PR #175 で 順位 155 entry の design を書いた際、`jj log` template に
# `description` の `first_line()` メソッドと filter `description = ""` を組合せたが、
# 両者は意味的に整合せず CR Minor finding で「全 description 空」vs「複数行 description で
# 1 行目だけ空」を区別できない曖昧性を指摘された。ad-hoc fix で `empty` keyword +
# `\u{1f}` Unit Separator 設計に変更したが、別の場所で同型ミスを書く可能性が残る。
# 同パターン自体を lint で禁止し、`empty` keyword 利用を促進する構造化予防
# (Bundle 3 順位 159 = PR #175 T1-#1 採用)。
#
# Bundle Z #B-α と同じ「決定論的防止層」哲学 (ADR-007 の正規表現層)。
#
# Pattern scope: jj template DSL 固有のため false positive 極小。Rust code 中の
# 同名メソッド呼出は理論上 hit するが、extensions = ["toml", "yaml", "md"]
# (= jj template 記述場所) に限定するため .rs file は対象外。
#
# Self-exclusion (重要): 本 TOML は extensions = ["toml"] により対象内のため、
# message / why / example / コメント内で pattern 文字列 (description ドット
# first_line 括弧) を**連続して**書かないこと。「description の first_line()」
# のように間に文字を挟む形で記述する (placeholder 形式は使わない、Rust regex は
# 単純 literal match のため間に何かあれば trigger しない)。

[[rules]]
id = "no-jj-template-first-line"
pattern = 'description\.first_line\(\)'
severity = "error"
message = "jj template で `description` の `first_line` メソッドは曖昧性を生みます (= 「全 description 空」と「複数行 description で 1 行目だけ空」を区別不可)"
why = "PR #175 で order 155 entry 内の jj template / filter logic 不整合が CodeRabbit Minor 指摘で発覚。`empty` keyword (= commit が file change を含まないか) または `if(description, \"DESCRIBED\", \"UNDESCRIBED\")` で意味を明示してください"
extensions = ["toml", "yaml", "md"]

[rules.fix]
strategy = "`empty` keyword または `if(description, ...)` への置換"
steps = [
"commit が file change を含むかで判定したい場合: jj template の `empty` keyword を使う (例: `if(empty, \"EMPTY\", \"CONTENT\")`)",
"commit が description を持つかで判定したい場合: `if(description, \"DESCRIBED\", \"UNDESCRIBED\")` を使う",
"あえて 1 行目のみ抽出が必要な場合 (display 用途等): コメントで意図を明記し、filter logic 側で同じ抽象を使う",
]

[rules.example]
bad = "jj log -T 'change_id ++ DESC_DOT_FIRSTLINE' # placeholder: 実コードでは description . first_line() = 曖昧性"
good = "jj log -T 'change_id ++ if(empty, \"EMPTY\", \"CONTENT\")' # commit 自体が file change を含むかで判定"

[rules.test_coverage]
# rule⑪ は jj template が書かれうる主要 file 形式 (toml/yaml/md) を対象。
# toml/yaml は主要拡張子のため main_ext_tests、md は非主要のため other_ext_tests。
other_ext_tests = ["no_jj_template_first_line_detects_md_pattern"]

[rules.test_coverage.main_ext_tests]
toml = [
"no_jj_template_first_line_detects_toml_pattern",
"no_jj_template_first_line_toml_skips_empty_keyword",
]
yaml = [
"no_jj_template_first_line_detects_yaml_pattern",
"no_jj_template_first_line_yaml_skips_empty_keyword",
]
33 changes: 33 additions & 0 deletions docs/adr/adr-041-test-isolation-patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,38 @@ fn enrich_with_classifier_skips_when_findings_empty() {
- **右 arm test**: `PrMonitorState::new()` の自然な初期値 (`findings` 空) を使うが、**precondition assert** で「左 arm が確実に不発になる」ことを test 内で文書化 + 機械検証
- 両 variant とも `classified_findings` を sentinel で pre-populate し、mutation 不発を survival assert で検出

## 関連 pattern: State Preservation Invariant (write-once 不変式)

ADR-041 本体の Multi-Condition Guards とは **別 pattern class** として、`cli-pr-monitor` の state preservation 設計でも類似の test isolation 問題が発生する。本 section は PR #168/169/170 の連続観測で抽出された pattern を codify し、派生プロジェクトへの transferability を確保する (順位 142 採用、本 PR で land)。

### Context (PR #168/169/170 連続観測)

`state.fix_push_time` 等の **write-once field** (= 「一度 set されたら上書きしない」不変式) を持つ field は、`state.fix_push_time.or_else(|| Some(new))` 形式でパス内の任意の地点で代入されうる。test で「既存値が preservation される」「新値が provision される」両方を検証する必要があるが、両方を 1 test で混ぜると validation 軸が混在し、preservation 失敗時に「新 path 経由で書き換わったのか / preservation guard が失敗したのか」を切り分けられない。

Multi-Condition Guards (本体) との違い:

| 観点 | Multi-Condition Guards | State Preservation Invariant |
|---|---|---|
| 検証対象 | OR/AND 早期 return guard の独立発火 | 一度 set した値の不変性 (write-once) |
| 検出 anti-pattern | guard を消した mutant が test を落とさない | preservation 失敗が新 path 由来か preservation guard 由来か判別不能 |
| 解決 idiom | sentinel pre-populate + 直交 precondition | 3 variant 直交 (既存値 / 値なし / 新値提供時の preservation) |

### 原則: 3 variant 直交 test

write-once field の test は以下 3 variant で直交化する:

1. **既存値 preservation** (既存値あり、関数は値なし pass): state に既存値を pre-set した state で関数を呼び、関数が新値を提供しない場合に既存値が保たれることを assert
2. **新値 provision** (既存値なし、関数が新値 set): state に値なし (`None`) で関数を呼び、新値が set されることを assert
3. **書き換え不可 (preservation 強化)** (既存値あり、関数が新値提供): 既存値を pre-set 状態で「新値を提供しようとする」関数呼び出しでも既存値が変わらないことを assert (= write-once 不変式の本質)

variant 3 が特に重要: variant 1 だけでは「関数が新値を提供しなかったから既存値が保たれた」と解釈の余地が残り、preservation guard が真に機能しているかが test できない。

### 参照実装

- `src/cli-pr-monitor/src/stages/poll.rs` の `finalize_*_preserves_existing_fix_push_time` (variant 1 + 3 を兼ねる)
- `src/cli-pr-monitor/src/monitor.rs` の `resume_returns_fix_push_time_from_state_when_set` (variant 1 の resume path 版)
- 実装側の不変式: `state.fix_push_time.or_else(|| Some(new))` (= `or_else` clause で値なし時のみ新値 set、= variant 2 の path)

## 帰結

### Pros
Expand Down Expand Up @@ -135,3 +167,4 @@ fn enrich_with_classifier_skips_when_findings_empty() {
## 改訂履歴

- 2026-05-22: 初版 (順位 139 採用 PR で land、PR #120 W-001 + PR #168 の 2 PR 横断観測ベース)
- 2026-05-27: § 関連 pattern: State Preservation Invariant 追加 (順位 142 採用、PR #168/169/170 連続観測ベース、cli-pr-monitor の write-once field 設計 + 3 variant 直交 test pattern を codify)
2 changes: 0 additions & 2 deletions docs/todo-summary.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@

| 順位 | Tier | タスク | ファイル | 工数 | 依存 |
|---|---|---|---|---|---|
| 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 実装 — 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 確立済) |
Expand Down Expand Up @@ -64,7 +63,6 @@
| 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 確保) |
| 142 | 💎 Tier 3 | **ADR-041 補強 — "State Preservation Invariant" pattern section 追加 (PR #170 T3-#1 採用) ★ Bundle 171** | todo8.md | S | なし (PR #168/169/170 で連続観測の write-once 不変式 (once-set-never-overwritten) パターンを ADR-041 に追記、`state.fix_push_time.or_else(...)` 形式の 3 点セット test pattern (既存値あり / 新値提供 / preservation 確認) を明文化、参照実装 = poll.rs `finalize_*_preserves_existing_fix_push_time` + monitor.rs `resume_returns_fix_push_time_from_state_when_set`、ADR-041 既存 section (Multi-Condition Guards) とは別 pattern class、`feedback_no_unenforced_rules.md` 例外 = 既存実践 (3 PR で実証) の明文化 + 派生プロジェクト transferability 確保) |
| 143 | 🔧 Tier 2 | **複言語 fixture helper 標準化 (hooks-post-tool-linter-tests) (PR #171 T2-#4 採用) ★ Bundle 171** | todo8.md | S | なし (PR #151/#171 の 2 PR 横断で multi-byte fixture 手動組み立てコストが Frequency Medium で観測、Japanese / emoji / combining chars helper 3 関数を標準化して新規 string-processing 関数追加時の boundary test コスト削減 + silent regression early detection、順位 142 + 144 と同 PR で land 推奨) |
| 145 | 🔧 Tier 2 | **preset matrix test 追加 — default fallback vs config-selectable の 2 軸 classification 検証 (PR #172 T2-#1 採用)** | todo8.md | M | なし (PR #172 Phase 3 で `jj-message-required` が opt-in preset であることを前提とせず test を書き rewrite が必要になった経緯、preset architecture の implicit assumption (always-enabled vs config-selectable) を classification 表として test レベルで codify、新 preset 追加時に matrix 更新を強制する mechanical enforcement で design misalignment を構造的検出、target は main.rs (feedback report の lib.rs 記載は誤り)) |
| 146 | 🚀 Tier 1 | **Secret detection PreToolUse hook 追加 — AWS/OpenAI/GitHub token 等の hardcoded secret 検出 (PR #172 仕組み化方針切替由来、`security.md` § Secret Management 移管) ★ Bundle 既存ルール仕組み化** | todo9.md | M | なし (`~/.claude/rules/common/security.md` § Secret Management 記述のみで機械強制なし、AWS Access Key / OpenAI sk- / GitHub ghp_/gho_/ghs_ / Anthropic sk-ant- 等 6+ 種 pattern を `preset_secret_detection` で regex 検出 + 即 block、順位 144 hook 化 template 踏襲、security-critical かつ漏洩観測前の preventive 層として Tier 1、rule docs § Secret Management を hook block message に集約で縮小) |
Expand Down
44 changes: 0 additions & 44 deletions docs/todo3.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,50 +10,6 @@

## 現在進行中

### AI 生成一時スクリプト pattern の pre-push 検出 (PR #88 T1-2)

> **動機**: PR #85 で Claude が transcript 確認用に作成した `__parse_transcripts.ps1` が `.gitignore` 漏れにより jj auto-snapshot 経由で commit に意図せず混入。CodeRabbit が発見し除去作業が必要となった。同パターン (`__*.ps1` / `_tmp_*.ps1` / `__*.py` / `_tmp_*.py` 等の AI 生成一時スクリプト) を pre-push で機械的に検出し再発を防止する。post-merge-feedback (PR #88) が同事象を transcript から再検出。
>
> **本タスクの位置づけ**: **既存の push 前 untracked `__*` ファイル警告 hook task (PR #85 T1-4) と同一インシデントへの異なるアプローチによる補完**。前者 = working-tree の untracked file 検出 (hook 機構) / 本タスク = pre-push 時の lint ベース検出 (AI 命名 pattern 全体)。両機構を併用するか一方に統合するかは実装時に判断。
>
> **参照**: `.claude/feedback-reports/88.md` の Tier 1 #2 finding
>
> **実行優先度**: 🚀 **Tier 1** — 工数 Small。daily efficiency への影響中 (再発リスクは低いが ADR-007 拡張で確実な再発防止)。**実装前に既存の push 前 untracked `__*` ファイル警告 hook task (PR #85 T1-4) と擦り合わせて重複か補完かを判定すること**。

#### 背景

- PR #85 で `__parse_transcripts.ps1` が混入 (Claude が transcript 解析用に作成、`.gitignore` 漏れ)
- `.gitignore` への `__*` 追加で当面の再発は防止済
- ただし `_tmp_*` 等の他 prefix や、`.gitignore` の管理漏れ自体への保険として機械的検出が望ましい
- post-merge-feedback (PR #88) が PR #85 の transcript を解析し、本提案を独立に再生成 → 提案の妥当性が複数 source で corroborate された

#### 設計決定 (案)

- 候補機構 1: ADR-007 の custom_lint_rule (`.claude/custom-lint-rules.toml`) に AI 生成一時スクリプト pattern を追加
- 候補機構 2: pre-push hook で `jj diff --name-only @` で staged file のうち `__*` / `_tmp_*` パターンに合致するものを検出
- 候補機構 3: 既存の push 前 untracked `__*` ファイル警告 hook (PR #85 T1-4) を拡張し pattern を増やす
- 検出パターン (初稿): `__*.ps1`, `__*.py`, `_tmp_*.ps1`, `_tmp_*.py`, `__*.sh`, `__*.js`, `__*.ts`
- 警告メッセージ: 「AI 生成一時スクリプト pattern を検出: `<file>`. `.gitignore` 漏れの可能性。意図的な commit なら override してください。」

#### 作業計画

- [ ] 既存の push 前 untracked `__*` ファイル警告 hook (PR #85 T1-4) の実装状況を確認
- [ ] 重複なら本タスクは前者の hook 内へ統合 (pattern を拡張するだけ)、補完なら別実装
- [ ] 機構決定後に `.claude/custom-lint-rules.toml` または既存 hook を拡張
- [ ] dogfood: 試しに `__test.py` を作って commit 試行 → 警告が出ることを確認
- [ ] 本 todo3.md エントリを削除 (push 前 untracked hook に統合した場合は description も更新)

#### 完了基準

- AI 生成一時スクリプト pattern が pre-push で検出され警告が出る
- 既存の `__*` ファイル検出 hook と整合性が取れている (重複なし or 明示的補完)

#### 詰まっている箇所

なし (Effort Small、ADR-007 既存パターンを拡張)

---

### `vitest` を devDependencies に固定 (PR #88 T2-3)

> **動機**: Stop hook の `pnpm test` → `npx vitest run` が `pnpm-lock.yaml` に vitest なしのため npx がネット DL を試みて偽陽性 FAIL する事象を観測。ネット環境・キャッシュ依存の不確実性を排除し、Stop gate を deterministic にする。
Expand Down
Loading