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
2 changes: 1 addition & 1 deletion .claude/custom-lint-rules.toml
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ good = 'See [推奨実行順序](todo.md#recommended-order-summary) <!-- with <

[[rules]]
id = "no-ephemeral-todo-reference"
pattern = 'docs/todo[0-9]*\.md'
pattern = '(?i)docs/todo[0-9]*\.md'
severity = "warning"
message = "永続成果物から ephemeral todo entry への literal 参照を検出。stale pointer 化リスクあり"
why = "Cross-File Reference Lifecycle ルール (~/.claude/rules/common/coding-style.md) の決定論的防止層。todo entry が完了削除されると参照が壊れる。安定 anchor (ADR / PR 番号 / 安定 docs パス) に置き換えてください"
Expand Down
4 changes: 3 additions & 1 deletion .markdownlint-cli2.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
".claude/feedback-reports",
".claude/skills",
".takt/runs",
".pytest_cache"
".pytest_cache",
"__*",
"__*/**"
]
}
7 changes: 4 additions & 3 deletions docs/local-llm-offload-analysis.md
Original file line number Diff line number Diff line change
Expand Up @@ -411,11 +411,12 @@ jq '[.classified_findings[] | select(.normalized_issue) | .normalized_issue | te
(各 PR 完了ごとに以下を埋める。全て master ベース個別 PR 記法)

```text
P-0 (config opt-in): PR #___ (本セッション land), merged ___ ✅ (本ファイル §10 governance + [classifier] enabled=true 同梱)
P-0 (config opt-in): PR #123, merged 2026-05-07 ✅ (本ファイル §10 governance + [classifier] enabled=true 同梱)
- smoke test: mistral:7b で `unused import` finding → action=auto_fix / confidence=1.0
- cli-pr-monitor は [classifier] section を読み込み可、compile 通過
P-1 (Bundle g-1): PR #___, merged ___, findings: __, agreement: __/__, token Δ: __, latency: __s/件, fallback: __/__
P-2 (順位 47): PR #___, merged ___, findings: __, agreement: __/__, token Δ: __, latency: __s/件, fallback: __/__
P-1 (Bundle g-1): PR #125, merged 2026-05-07, findings: 0 (CR APPROVE no comments), classifier 未起動 (input data なし), 計測 N/A — dogfood 不発
P-2 (順位 47): PR #126, merged 2026-05-07, findings: 1 (Nitpick, CR review body 内 `<details>` block), agreement: 1/1 (100%, 私評価=human_review と一致), latency: 6.4s/件 (>5s 目標), fallback: 1/1 (normalized_issue length 100>80)
- 既知 gap: check-ci-coderabbit が review body の `<details>` block 内 Nitpick を抽出しない (post-pr-monitor が classifier に渡せず、手動で synthetic finding 構築して classifier 実行)
P-3 (順位 7): PR #___, merged ___, findings: __, agreement: __/__, token Δ: __, latency: __s/件, fallback: __/__
P-4 (順位 76+77): PR #___, merged ___, findings: __, agreement: __/__, token Δ: __, latency: __s/件, fallback: __/__
P-5 (Bundle f-1): PR #___, merged ___, findings: __, agreement: __/__, token Δ: __, latency: __s/件, fallback: __/__
Expand Down
1 change: 0 additions & 1 deletion docs/todo.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
| 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 | 設定のみ | なし (依存タスクは完了済) |
| 7 | 🚀 Tier 1 | **PowerShell custom-lint-rule の `(?i)` フラグ自動検証 (PR #91 T1-1)** | todo3.md | S | なし (PR #91 直接対策、code-review.md 追記も同 PR で land) |
| 8 | 🔧 Tier 2 | 週次レビュー (ADR-031) Phase B 実装 | todo.md | 中-高 | なし (順位 20 の compensating check 前提) |
| 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 | なし |
Expand Down
46 changes: 0 additions & 46 deletions docs/todo3.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,52 +226,6 @@ prompt and prompt Claude to re-run the workflow.

---

### PowerShell custom-lint-rule の `(?i)` フラグ自動検証 (PR #91 T1-1)

> **動機**: PR #91 で `no-empty-powershell-catch` / `no-silent-error-action` の regex に `(?i)` が欠落し、`Catch {}` / `-erroraction silentlycontinue` 等の大文字バリアントを見逃して CodeRabbit Major 指摘を受けた。PowerShell は言語仕様として keyword + parameter 名 case-insensitive だが、AI 生成 regex はデフォルトで case-sensitive になる構造的な落とし穴がある。本 PR で fix 済 (commit a15b263) だが、次回ルール追加時に同種の漏れが起きないよう自動検証を追加する。
>
> **本タスクの位置づけ**: hooks-post-tool-linter の起動時 (or 専用 test) で「`extensions = ["ps1"]` を含む全ルールの `pattern` に `(?i)` が含まれる」アサーションを追加。同 PR で `~/.claude/rules/common/code-review.md` に「case-insensitive 言語向け lint rule は `(?i)` 必須」のチェックリスト項目を追記 (report Tier 3 #1 を統合)。
>
> **参照**: `.claude/feedback-reports/91.md` の Tier 1 #1 + Tier 3 #1 (統合採用)
>
> **実行優先度**: 🚀 **Tier 1** — 工数 S。決定論的な再発防止で本 PR の主要 finding に直結。Bundle 戦略の継続として code-review.md ルール追記も同 PR で land。

#### 設計決定 (案)

- 配置先: `src/hooks-post-tool-linter/src/main.rs` の `load_custom_rules()` か専用 unit test
- 検証ロジック (案 A: 起動時 check):
```rust
for rule in &rules {
if rule.extensions.iter().any(|e| e == "ps1") && !rule.pattern.contains("(?i)") {
eprintln!("[post-tool-linter] WARN: rule '{}' targets ps1 but lacks (?i) flag", rule.id);
}
}
```
- 案 B: cargo test で全 TOML rule をパースして同等検証 (CI で fail させる)
- 推奨: 案 A + 案 B 併用 (起動時 warn は本番運用、test は CI 検出)
- 同 PR 同梱の code-review.md ルール追記 (案):
> **case-insensitive 言語向け lint rule の正規表現には `(?i)` フラグ必須**: PowerShell, Bash 等の case-insensitive 言語向けルールを追加する際、regex pattern に `(?i)` を付与する。テストで小文字 / 大文字 / 混在ケースを最低 1 ずつ検証する。

#### 作業計画

- [ ] hooks-post-tool-linter に起動時 check 実装 (案 A)
- [ ] cargo test に rule バリデーション test 追加 (案 B)
- [ ] `~/.claude/rules/common/code-review.md` に case-insensitive ルール追記
- [ ] dogfood: 意図的に `(?i)` を外したルールを TOML に追加して warn 発火を確認
- [ ] 本 todo3.md エントリを削除

#### 完了基準

- `.claude/custom-lint-rules.toml` に新規 ps1 ルールを追加した際、`(?i)` 欠落を起動時 warn または cargo test fail で検出できる
- code-review.md に case-insensitive 言語の lint rule 規約が明記される
- 既存 ps1 ルール 2 件 (`no-empty-powershell-catch`, `no-silent-error-action`) が validation を pass する

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

なし (Effort S、既存 load_custom_rules 拡張のみ)

---

### takt ハーネスの `REJECT-ESCALATE` terminal verdict 実装 (PR #91 T2-2)

> **動機**: PR #91 の post-pr-review で `supervise` step が 4 回、`fix_supervisor` step が 4 回の計 8 ステップを「修正不可能な制約あり」と繰り返し報告したにもかかわらず、takt harness はループを継続した。`.claude/` filter + ADR-030 制約明記 task (PR #91 T2-1 + T3-2 Bundle) が path-based に解決するのに対し、本 task は **iteration 上限到達前に「人間判断に委譲する」と AI 自身が宣言できる verdict** を提供する一般解。
Expand Down
90 changes: 90 additions & 0 deletions src/hooks-post-tool-linter/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,13 @@ fn load_custom_rules() -> Vec<CompiledRule> {
Err(_) => return Vec::new(),
};

for missing_id in find_powershell_rules_missing_case_insensitive_flag(&rules) {
eprintln!(
"[post-tool-linter] Warning: rule '{}' targets ps1 but lacks (?i) flag (PowerShell is case-insensitive — see ~/.claude/rules/common/code-review.md)",
missing_id
);
}

rules
.into_iter()
.filter_map(|rule| match Regex::new(&rule.pattern) {
Expand All @@ -343,6 +350,15 @@ fn load_custom_rules() -> Vec<CompiledRule> {
.collect()
}

fn find_powershell_rules_missing_case_insensitive_flag(rules: &[CustomRule]) -> Vec<String> {
rules
.iter()
.filter(|r| r.extensions.iter().any(|e| e.eq_ignore_ascii_case("ps1")))
.filter(|r| !r.pattern.contains("(?i)"))
.map(|r| r.id.clone())
.collect()
}

/// カスタムルール違反の最大出力件数 (外部ツール診断の20行制限と同等)
const MAX_CUSTOM_VIOLATIONS: usize = 20;

Expand Down Expand Up @@ -1519,4 +1535,78 @@ extensions = ["ts", "js"]
let violations = run_custom_rules(file.to_str().unwrap(), &rules);
assert!(violations.is_empty());
}

fn ps_rule_with_pattern(id: &str, pattern: &str) -> CustomRule {
make_test_rule(id, pattern, &["ps1"])
}

#[test]
fn powershell_validation_flags_rule_without_case_insensitive_flag() {
let rules = vec![ps_rule_with_pattern("ps-bad", r"\bcatch\s*\{\s*\}")];
let missing = find_powershell_rules_missing_case_insensitive_flag(&rules);
assert_eq!(missing, vec!["ps-bad".to_string()]);
}

#[test]
fn powershell_validation_passes_rule_with_case_insensitive_flag() {
let rules = vec![ps_rule_with_pattern("ps-good", r"(?i)\bcatch\s*\{\s*\}")];
let missing = find_powershell_rules_missing_case_insensitive_flag(&rules);
assert!(missing.is_empty());
}

#[test]
fn powershell_validation_ignores_non_ps1_rules() {
let rule = make_test_rule("rs-rule", r"\bfn\s+main", &["rs"]);
let missing = find_powershell_rules_missing_case_insensitive_flag(&[rule]);
assert!(missing.is_empty());
}

#[test]
fn powershell_validation_handles_mixed_extension_list() {
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()]);
}

#[test]
fn powershell_validation_treats_extension_case_insensitively() {
let rule = make_test_rule("upper-ext", r"\bcatch\s*\{\s*\}", &["PS1"]);
let missing = find_powershell_rules_missing_case_insensitive_flag(&[rule]);
assert_eq!(missing, vec!["upper-ext".to_string()]);
}

#[test]
fn powershell_validation_returns_multiple_violators() {
let rules = vec![
ps_rule_with_pattern("ps-a", r"\bcatch"),
ps_rule_with_pattern("ps-b", r"\berroraction"),
ps_rule_with_pattern("ps-c-ok", r"(?i)\bwrite-host"),
];
let missing = find_powershell_rules_missing_case_insensitive_flag(&rules);
assert_eq!(missing, vec!["ps-a".to_string(), "ps-b".to_string()]);
}

#[test]
fn deployed_custom_rules_pass_powershell_case_insensitive_validation() {
let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("..")
.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 config: CustomRulesConfig = toml::from_str(&content).unwrap();
let rules = config.rules.unwrap_or_default();
let missing = find_powershell_rules_missing_case_insensitive_flag(&rules);
assert!(
missing.is_empty(),
"PowerShell rules without (?i) flag detected: {:?}",
missing
);
}
}