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
58 changes: 58 additions & 0 deletions config/custom-lint-rules.toml
Original file line number Diff line number Diff line change
Expand Up @@ -807,3 +807,61 @@ rs = [
"no_unbounded_child_wait_skips_barrier_wait",
"no_unbounded_child_wait_paths_match_integration_tests_only",
]

# ─── ルール⑮: `temp_dir().join(<固定名>)` の弱い一意性検出 ───
#
# 由来: PR #405 で `std::env::temp_dir().join(<固定名>)` を production とテストの両方で
# 踏んだ (順位 455)。production 側は ADR-045 が支える並行 `pnpm push` が互いのスナップショットを
# 上書きし合うレース、テスト側は入力長から名前を作って `/` 版と `\` 版が衝突する形だった。
# 1 つ直した直後に同型を別の場所で作っており、人手の注意では止まらないため決定論層へ移す
# (ADR-007 の正規表現層)。
#
# 検出スコープ (regex 層の限界を先に見積もった結果):
# `temp_dir().join("<文字列リテラル>")` の直呼び (= 引数が呼び出しのたびに不変な literal) だけを
# 検出する。`.join(format!(...))` や `.join(some_var)` は literal 直後に `"` が来ないため
# 対象外 (= 動的組立は「弱い一意性でない」とみなし false positive を避ける)。
# `join` が複数行にまたがる書き方 (`\s*` で改行を許容するため単純な改行分割は追えるが、
# 引数側に変数や関数呼び出しが挟まる形は正規表現では判定できない) はこの層の既知の限界。
#
# `process::id()` を「これを付ければ済む」条件にしないこと (着手時の設計メモ、順位 455):
# プロセス ID が分けるのはプロセス間だけで、同一プロセスが複数の一時ファイルを作る場合は
# 衝突する。本 rule は「literal な固定名」だけを機械検出し、`process::id()` 単体で十分かは
# fix 側のガイダンス (用途名 / カウンタとの組合せ、または `tempfile` crate) に委ねる。

[[rules]]
id = "no-weak-temp-uniqueness"
pattern = 'temp_dir\(\)\s*\.join\(\s*"[^"]*"\s*\)'
severity = "error"
message = "temp_dir() に文字列リテラルの固定名を join しています。呼び出しごとに一意な名前にしてください"
why = "PR #405 で std::env::temp_dir().join(<固定名>) を production (ADR-045 が支える並行 pnpm push によるスナップショット上書きレース) とテスト (入力から名前を作り区切り文字違いで衝突) の両方で踏んだ。1 つ直した直後に同型を別箇所で作っており、人手の注意では止まらない。process::id() 単体では同一プロセス内の複数回呼び出しで不足するため、fix 側で用途名 / カウンタとの組合せ、または tempfile crate への切替を促す"
extensions = ["rs"]

[rules.fix]
strategy = "process ID + 用途名の組み合わせ、または tempfile crate の一時ファイル/ディレクトリ生成に置換"
steps = [
"同一プロセス内で 1 度しか呼ばれない場合: format!(\"<用途名>-{}.json\", std::process::id()) に変える",
"同一プロセスが複数回呼ぶ場合: 単調増加カウンタ (AtomicU64 等) を process id と組み合わせるか、tempfile::Builder / tempfile::NamedTempFile で生成自体に一意性を持たせる",
"テストで衝突する場合: 呼び出しごとに異なる用途名 (test 関数名由来など) を name に含める",
]

[rules.example]
bad = "let path = std::env::temp_dir().join(\"push-runner-snapshot.json\");"
good = "let path = std::env::temp_dir().join(format!(\"push-runner-snapshot-{}.json\", std::process::id()));"

[rules.incident]
pr = 405
bad_fixture = "no-weak-temp-uniqueness.rs"
good_fixture = "no-weak-temp-uniqueness.rs"
adr = "adr-045"

[rules.test_coverage]
# rule⑮ は rs のみ (主要拡張子)。literal 直呼び (positive) と動的組立 / 非 temp_dir join (negative) を網羅。

[rules.test_coverage.main_ext_tests]
rs = [
"no_weak_temp_uniqueness_detects_fixed_literal_join",
"no_weak_temp_uniqueness_detects_bare_env_temp_dir_prefix",
"no_weak_temp_uniqueness_skips_format_with_process_id",
"no_weak_temp_uniqueness_skips_non_temp_dir_join",
"no_weak_temp_uniqueness_only_targets_rust_extension",
]
1 change: 0 additions & 1 deletion docs/claude-code-web-tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,6 @@ close は「この成果物は採らない」という判断であって、「
| 143 | T2 | — | 複言語 fixture helper(日本語 / emoji / combining chars の 3 関数)を標準化して string-processing の境界テストを書きやすくする | `src/hooks-post-tool-linter/src/main.rs` | S | helper の追加のみ。既存テストの書き換えは範囲外。**2026-08-23 auto → human へ変更**: 宣言先 `main.rs` は 73 行の起点のみでテスト 0 件、string 処理のテストは `utf8_integrity.rs` 等の別モジュールにある (漂流)。ただし「複数モジュールが使う共有 fixture helper をどこに置くか」は台帳から一意に決まらず ([ADR-074](adr/adr-074-auto-lane-screening-criteria.md) 決定 2 クラス 5)、パスの機械的な差し替えでは塞げないため人間が実装先を決める | test(post-tool-linter): 複言語 fixture helper を標準化する |
| 428 | T2 | — | PR 番号を取る CLI の不正値(`--pr 0` 等)を弾く検査を足す | `src/cli-merge-pipeline/src/main.rs` | S | 既に `pr_number_zero_is_rejected` があるため、**未カバーの入口(他 exe の同種フラグ)を洗ってから足す**。gh の実実行は成功条件外。**2026-08-23 auto → human へ変更**: 夜間ループでは完了不能と判明したため。理由は 2 つで、いずれも 2026-08-23 の実測にもとづく。(1) **宣言先が既に完成している** — 宣言成果物 `src/cli-merge-pipeline/src/main.rs` は `parse_pr_flag` で `--pr` / `--feedback-only` の 0 を拒否済みで、テスト `pr_number_zero_is_rejected` / `feedback_only_pr_number_zero_is_rejected` も存在する。(2) **洗った結果、未カバーの入口が見つからない** — PR 番号フラグを持つもう一方の入口 `src/check-ci-coderabbit/src/main.rs` も `--pr` 経路で `pr == 0` を弾いており (main.rs:299)、`src/cli-pr-monitor` はそもそも PR 番号フラグを取らない (PR URL から取得する)。この状態で夜間ループが着手すると、正しい実装ほど「足すものが無い」に到達して `[NIGHTLY_DENY] 変更がありません` で停止し、無理に何かを足せば宣言先以外を触って `[LEDGER_CLEANUP_BLOCK]` で停止する。どちらに転んでも完了できない。残る価値は `check-ci-coderabbit` の 0 拒否に専用テストが無い点だが、それは宣言先と別ファイルであり、対象の再定義は人間が行う ([ADR-074](adr/adr-074-auto-lane-screening-criteria.md) 決定 2 クラス 5 = 実装内容が一意に定まらない) | fix(cli): PR 番号フラグの不正値を弾く |
| 454 | T1 | — | 自律実行ガードレールの 3 点同期(workflow の Guard step / agent プロンプト / ADR-072 決定 6 の列挙)を cargo test で機械検証する | `src/cli-nightly-task-select/src/main.rs`(実ファイルを読む既存 2 検査と同じ形。新規 test module でも可) | S | **workflow ファイルは読むだけで書き換えない**。3 箇所からパス集合を抽出し完全一致を要求する。抽出は行指向で足りる。**2026-08-23 auto → human へ変更**: 成果物 `src/cli-nightly-task-select/src/main.rs` 自体が [ADR-072](adr/adr-072-nightly-todo-loop.md) 決定 6 の Guard 禁止パス `src/cli-nightly-task-select/` に当たる([ADR-074](adr/adr-074-auto-lane-screening-criteria.md) 決定 2 クラス 3)。注意欄が守っていたのは「workflow を書き換えない」だけで、成果物側は照合されていなかった。機械検査は順位 486 | test(nightly-todo): ガードレール 3 点同期を cargo test で固定する |
| 455 | T1 | ✅ | 一時ファイルの弱い一意性(PID や ms を含まない固定名)を検知する custom lint rule を追加 | `config/custom-lint-rules.toml` + `tests/fixtures/incidents/{bad,good}/` | S | **rule 定義・rule test・fixture の 3 点セットが要る**(片方だけだと #394 と同型の未完了マージになる)。検証は cargo test で完結。**2026-09-15 に auto lane へ戻した** — 2026-09-03 に外した理由は成果物が `.claude/` 配下で agent の `Edit(work/**)` (ドット始まりに当たらない) から届かなかったこと (夜間 run 33665621808 で 34 ターン・0 変更・`permission_denials_count: 2`)。設定を `config/` へ移設して解消した ([ADR-006](adr/adr-006-config-driven-hooks.md) § 改訂 2026-09-15) | feat(lint): 一時ファイルの弱い一意性を検知する rule を追加する |
| 236 | T1 | — | tempfile mandate(固定名の一時ファイル生成)を検知する custom lint rule を追加 | `config/custom-lint-rules.toml` + `tests/fixtures/incidents/{bad,good}/` | S | 455 と同じ 3 点セット。**455 と検出対象が重なる可能性があるため、着手時にどちらかへ統合するか判断する**(統合する場合は片方を取り下げ)。**2026-08-17 auto → human へ変更**: 上記の「着手時判断」は [ADR-074](adr/adr-074-auto-lane-screening-criteria.md) 決定2 クラス5(判断留保)の除外語彙に該当するため。**2026-09-15**: `.claude/` 権限の障害は設定の `config/` 移設で解消した ([ADR-006](adr/adr-006-config-driven-hooks.md) § 改訂) が、**455 との統合可否が未決**なので human のまま。統合可否が決まり次第 `✅` へ | feat(lint): 固定名の一時ファイル生成を検知する rule を追加する |
| 281 | T1 | ✅ | config 読み hook の `current_dir()` 解決(exe-relative であるべき箇所)を検出する custom lint rule を追加 | `config/custom-lint-rules.toml` + `tests/fixtures/incidents/{bad,good}/` | S | 455 / 236 と同じ 3 点セット。検出対象は「`hooks-*` の `.rs` で `current_dir()` と `hooks-config.toml` が同居する」パターン。照合除外: `current_dir`(lint rule が検出する対象であって本タスクの成果物ではないため、宣言先に実在しなくてよい) **2026-09-15 に auto lane へ戻した** — 外していた理由 (成果物が `.claude/` 配下で agent の権限 glob から届かない) は設定の `config/` 移設で解消した ([ADR-006](adr/adr-006-config-driven-hooks.md) § 改訂 2026-09-15)。**検出対象は「`hooks-*` の `.rs` が `lib_config_path::resolve_config` を経由せず自前で config パスを組み立てる」形**とする (2026-09-15 に同移設で解決口を 1 本化したため、以後は復活防止の回帰ガードにあたる)。 | feat(lint): config 読み hook の current_dir 解決を検出する rule を追加する |
| 368 | T3 | — | `describe_axes()`(deny 行の 4 軸表示)と `evaluate()`(実際の allow/deny 判定)が同一入力で食い違わないことを assert する regression test を追加 | `src/cli-fix-push-gate/src/checks.rs`(既存 `mod tests` 内) | S | 純関数どうしの一貫性検査。Allow / Denied の双方向を書く。誤 allow は起きず観測性の劣化のみなので Severity は中。**2026-08-23 auto → human へ変更**: 成果物 `src/cli-fix-push-gate/src/checks.rs` が [ADR-072](adr/adr-072-nightly-todo-loop.md) 決定 6 の Guard 禁止パス `src/cli-fix-push-gate/` に当たる([ADR-074](adr/adr-074-auto-lane-screening-criteria.md) 決定 2 クラス 3)。機械検査は順位 486 | test(fix-push-gate): describe_axes と evaluate の一貫性を固定する |
Expand Down
1 change: 0 additions & 1 deletion docs/todo-summary3.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@
| 452 | Tier 3 | **本リポ instruction とスキルリポ SKILL.md の同時反映チェックリスト (PR #400 T3-2)** | todo23.md | XS | なし (2026-08-14 採用。ADR-051 の具体化。スキルリポ側に約 110 行のコミット漏れが滞留していた検出も含める) |
| 453 | Tier 2 | **post-merge-feedback 分析 agent の書き込み先制約 (read-only facet の一時ファイル生成)** | todo23.md | S | なし (2026-08-14 起票。analyze_transcript.py の実観測。weekly の workspace-hygiene-scan が backstop、本タスクは上流修正で緊急度低) |
| 454 | Tier 1 | **自律実行ガードレールの 3 点同期を機械検証する (#400-#406 feedback 統合)** | todo23.md | S | なし (2026-08-15 採用。#403/#405 で 3 箇所を手で揃えた。片方漏れで保護が静かに緩み、#403 では実際に抽出で実体が保護外へ出かけた) |
| 455 | Tier 1 | **一時ファイルの弱い一意性を検知する lint (#400-#406 feedback 統合)** | todo23.md | S | なし (2026-08-15 採用。#405 で production/test の両方で踏んだ。1 つ直した直後に同型を別箇所で作っており人手の注意では止まらない。regex 層の限界を先に見積もる) |
| 456 | Tier 1 | **workflow の guard なし `git commit` を検知する (#400-#406 feedback 統合)** | todo23.md | S | なし (2026-08-15 採用。#406 で Critical を 2 度。レビューが無ければ夜間ループが停止していた) |
| 458 | Tier 2 | **`cli-ledger-cleanup` の統合テスト suite (提案 10 件を統合)** | todo23.md | M | なし (2026-08-15 採用。手動実測した安全側 3 ケースの自動化が起点。削除は取り返しがつかないため安全側こそ回り続ける必要がある) |
| 459 | Tier 2 | **weekly-review 周辺の決定論層テスト (提案 4 件を統合)** | todo23.md | S-M | なし (2026-08-15 採用。scan 失敗テストは検証対象が未確定 = shell のままか exe 化か。順位 448 と同じ構図) |
Expand Down
36 changes: 0 additions & 36 deletions docs/todo23.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,42 +54,6 @@

なし

### 順位 455: 一時ファイルの弱い一意性を検知する lint

> **動機**: `std::env::temp_dir().join(<固定名>)` を [#405](https://github.com/aloekun/claude-code-hook-test/pull/405) で **production とテストの両方で踏んだ**。production 側は [ADR-045](adr/adr-045-jj-workspace-parallel-sessions.md) が支える並行 `pnpm push` で互いのスナップショットを上書きし合う race、テスト側は入力長から名前を作って `/` 版と `\` 版が衝突する形だった。1 つ直した直後に同型を別の場所で作っており、**人手の注意では止まらない**。
>
> **統合した提案**: `temp_dir().join(<固定 or 弱い一意性>)` の検知 (#405 Tier1 #4)、テスト用一時ファイルの命名規則 (#405 Tier3 #4 の機械強制部分)。
>
> **参照**: `.claude/feedback-reports/405.md`、[ADR-045](adr/adr-045-jj-workspace-parallel-sessions.md)
>
> **実行優先度**: **Tier 1** — Severity High / Frequency Medium / Effort S / Adoption Risk None。

#### 設計決定 (案)

- custom lint rule (`.claude/custom-lint-rules.toml`)。`temp_dir()` を含む行の近傍に `process::id()` 等の一意化子が無い形を検出する
- **regex 層の限界を先に見積もる** ([ADR-007](adr/adr-007-custom-linter-layer-boundary.md))。`join` が複数行に分かれる書き方は正規表現で追えない。追えない形が現行コードにどれだけあるか grep で測ってから、rule にするか cargo test にするかを決める

#### 作業計画

- [ ] 現行コードの `temp_dir()` 利用箇所を全件洗い、regex で追える形の割合を測る
- [ ] rule 化するなら fixture 3 点セット + dogfood
- [ ] 追えない形が多ければ cargo test (AST でなく実ファイル走査) へ切り替える

#### 完了基準

- 固定名の一時ファイル生成を足すと、その場で機械的に止まる

> **`process::id()` を「これを付ければ済む」条件にしないこと。** プロセス ID が分けるのは
> プロセス間だけで、**同一プロセスが複数の一時ファイルを作る場合は衝突する**。入力値から
> 名前を導くのも不可 — `/` 区切りと `\` 区切りのように、異なる入力が同じ名前になりうる
> (実際に #405 のテストで踏んだ)。検査で要求するのは「呼び出しごとに一意」であり、
> プロセス ID + 用途名、あるいは `tempfile` crate の一時ディレクトリなど、
> **何を一意性の源にするかは着手時に決める**。

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

一意性の担保方法が未確定 (プロセス ID 単体では不足。用途名との組み合わせか crate 導入か)

### 順位 456: workflow の guard なし `git commit` を検知する

> **動機**: [#406](https://github.com/aloekun/claude-code-hook-test/pull/406) で **Critical を 2 度**踏んだ。(1) pathspec 無しの `git commit` が Guard step の `git add -A` で stage された全ツリーを取り込み、後段の commit が空になって **PR が 1 つも作られなくなる**。(2) ステージが空の場合に無条件 commit が非ゼロで落ち、検証済みの実装ごと job が落ちる。どちらも「単体では正しいが前後の文脈で破綻する」型で、レビューが無ければ夜間ループが停止していた。
Expand Down
58 changes: 58 additions & 0 deletions src/hooks-post-tool-linter/src/custom_rules/rule_tests_extras.rs
Original file line number Diff line number Diff line change
Expand Up @@ -698,3 +698,61 @@ fn no_unbounded_child_wait_paths_match_integration_tests_only() {
assert!(!rule_matches_path(&compiled, &abs(rel)), "should not match: {rel}");
}
}

// ─── rule⑮: no-weak-temp-uniqueness ───

fn no_weak_temp_uniqueness_rule() -> CustomRule {
make_test_rule(
"no-weak-temp-uniqueness",
r#"temp_dir\(\)\s*\.join\(\s*"[^"]*"\s*\)"#,
&["rs"],
)
}

fn run_weak_temp_uniqueness_rule_on(source: &str) -> usize {
let dir = tempfile::tempdir().unwrap();
let file = write_file(dir.path(), "snapshot.rs", source);
let rules = compile_test_rules(vec![no_weak_temp_uniqueness_rule()]);
run_custom_rules(file.to_str().unwrap(), &rules).len()
}

#[test]
fn no_weak_temp_uniqueness_detects_fixed_literal_join() {
let source = "let path = std::env::temp_dir().join(\"push-runner-snapshot.json\");\n";
assert_eq!(run_weak_temp_uniqueness_rule_on(source), 1);
}

/// prefix (`std::env::` 等) の有無に関わらず `temp_dir()` の部分一致で検出する。
#[test]
fn no_weak_temp_uniqueness_detects_bare_env_temp_dir_prefix() {
let source = "let path = env::temp_dir().join(\"cli-nightly-task-select-absent\");\n";
assert_eq!(run_weak_temp_uniqueness_rule_on(source), 1);
}

/// `format!` 等の動的組立は literal 直呼びでないため対象外 (`.join(` の直後が `"` でない)。
#[test]
fn no_weak_temp_uniqueness_skips_format_with_process_id() {
let source = "let path = std::env::temp_dir().join(format!(\"snapshot-{}.json\", std::process::id()));\n";
assert_eq!(run_weak_temp_uniqueness_rule_on(source), 0);
}

/// `temp_dir()` を経由しない通常の `.join(\"literal\")` (例: 既存 `PathBuf` への相対結合) は
/// 本 rule のスコープ外 (temp file の一意性問題ではない)。
#[test]
fn no_weak_temp_uniqueness_skips_non_temp_dir_join() {
let source = "let path = repo_root().join(\"config\").join(\"custom-lint-rules.toml\");\n";
assert_eq!(run_weak_temp_uniqueness_rule_on(source), 0);
}

#[test]
fn no_weak_temp_uniqueness_only_targets_rust_extension() {
let dir = tempfile::tempdir().unwrap();
let file = write_file(
dir.path(),
"notes.md",
"let path = std::env::temp_dir().join(\"push-runner-snapshot.json\");\n",
);
let rules = compile_test_rules(vec![no_weak_temp_uniqueness_rule()]);
let violations = run_custom_rules(file.to_str().unwrap(), &rules);
assert!(violations.is_empty());
}
Loading
Loading