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
26 changes: 26 additions & 0 deletions .claude/hooks-config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,32 @@ zero_streak_months = 2 # 連続 2 か月発火 0 で非アクティブ化
trend_months = 6 # レポートが遡る月数 (月別表 / 発火 0 リストの窓)
# extra_roots = ["C:\\path\\to\\other-workspace"] # 環境ごとに必要なら有効化

# 機構レジストリ (設計決定 1、Phase A)。発火 0 リストの母集合を「窓内 rollup に現れた id」から
# 「全機構」に拡張する。rule (custom-lint-rules.toml の全 rule id) と preset ([pre_tool_validate]
# blocked_patterns) は exe が自動列挙するため、自動列挙元が無い hook / nudge 発火 id のみここに静的
# 列挙する。id は hook 名と一致しない例がある (各 hook の lib_telemetry::record 呼び出しで実確認):
# file-length = hooks-post-tool-comment-lint-rust (file-length gate)
# hooks-stop-tool-call-leak = leak 検知 Stop 経路 (block)
# hooks-stop-tool-call-leak/prompt-recovery = leak 回収 UserPromptSubmit 経路 (warn、ADR-061)
# jj-op-verify = hooks-post-tool-jj-op-verify (operation 未記録 warn)
# hooks-stop-quality = Stop 品質ゲート (block)
# pr_monitor_catchup / reaper / staleness / workspace_stale / weekly_review_reminder /
# monthly_review_reminder = hooks-session-start の nudge 群 (warn)
[telemetry_report.registry]
hook_ids = [
"file-length",
"hooks-stop-tool-call-leak",
"hooks-stop-tool-call-leak/prompt-recovery",
"jj-op-verify",
"hooks-stop-quality",
"pr_monitor_catchup",
"reaper",
"staleness",
"workspace_stale",
"weekly_review_reminder",
"monthly_review_reminder",
]

# 初期マッピング 1 件: ADR-053/061 の tool call leak 検知。監視 2 id が連続発火 0 かつ各月
# enabled+配備済みなら非アクティブ化候補として提示する (crate 撤去 revert PR は ADR-053/061
# bounded lifetime 手順に従う)。全試験運用 ADR の網羅登録は将来拡張。
Expand Down
31 changes: 31 additions & 0 deletions docs/adr/adr-062-monthly-harness-roi-review.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,37 @@ weekly-review にあった `.failed` marker / resume 機構は**不採用**と
単体では月内の有効性を証明しないため、**月次 rollup 確定時に当月の snapshot を rollup JSON にも
保存**し、判定はこの月別記録を参照する。

### 2 の amendment (2026-07-31, Phase A): 機構レジストリ + 発火 0 リスト再定義

初回 dogfood (2026-07-30) で、レポート (b) の発火 0 リストが「発火 0 = 削除候補」の中核シグナルとして
機能していないことが判明した。rollup の id entry は発火レコードからしか作られない (`aggregate.rs`
`count_firings` は block/warn のみ加算) ため、(1) 発火が止まって窓外に落ちた id と (2) 一度も発火して
いない機構が**どちらも不可視**だった (§ 決定 4 の「MVP は 1 件 + **発火 0 リスト全般**で足りる」の
後半が実装で満たされていなかった)。機構レジストリで母集合を静的に列挙してこの盲点を塞ぐ:

- **機構レジストリ (3 供給源、すべて exe 隣接 `config_base` 基準)**:
- **rule**: `.claude/custom-lint-rules.toml` の全 rule id。incident 判定 (`incident.rs`) と同じ読み口
(`RulesFile`) を共有する。telemetry の rule firing id は `hooks-post-tool-linter` の
`lib_telemetry::record` が `id: &rule.id` で記録する (= rule の `id` フィールドそのもの)。
- **preset**: `hooks-config.toml` の `[pre_tool_validate] blocked_patterns` 宣言。preset firing は
`hooks-pre-tool-validate` の `record_preset_block` が `hit.source` (= blocked_patterns の宣言文字列)
を id に記録するため、宣言をそのまま列挙すれば発火 id と突き合う。
- **hook / nudge**: 自動列挙元が無いため config `[telemetry_report.registry] hook_ids` を新設
(ADR-039 additive。section 不在でも rule/preset は自動列挙される)。id は hook 名と一致しない例が
ある (`jj-op-verify` / `pr_monitor_catchup` / `hooks-stop-tool-call-leak/prompt-recovery` /
`file-length` / nudge 群等) ため、各 hook の `record` 呼び出しから実確認して列挙する。
- **発火 0 集合 = (レジストリ ∪ 全 rollup 履歴に現れた id) − (窓内に発火した id)**。2 区分で提示:
- **never-fired**: レジストリにあり全履歴で発火 0。
- **went-quiet**: 履歴に発火があるが窓内 0 (**最終発火月を併記**。全 rollup 走査で導出)。

incident (`[rules.incident]`) 維持推奨マークと mechanisms 監視対象マークは新リストにも適用する。
- **degraded 実行時は (b) 全体に「参考値 (root 発見不完全)」注記**を付す (発火が発見漏れ root に
偏在し得るため。verdict の promote 抑止と整合)。
- 供給源単位の読取失敗は fail-open で skip しつつ**レポートに欠落を明示**する (silent fallback 排除、
「読めなかった」と「id が 0 件」を区別)。JSON の zero_firing entry に `provenance`
(`never_fired` / `went_quiet`) と `last_fired_month` を追加、`registry.source_failures` も出力する。
(b) は参考情報でありユーザーゲート (自動削除しない) は不変。

### 3. L1: SessionStart reminder

`hooks-session-start` の `[session_start.monthly_review_reminder]` (`enabled` / `threshold_days`
Expand Down
40 changes: 40 additions & 0 deletions src/cli-telemetry-report/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,26 @@ pub struct TelemetryReportConfig {
/// `jj workspace list` で発見できない root を補う追加 root (絶対パス)。
#[serde(default)]
pub extra_roots: Vec<String>,
/// 発火 0 リストの母集合を与える機構レジストリ (設計決定 1、Phase A)。section 不在でも
/// rule / preset は自動列挙されるため、hook / nudge の静的 id リストのみを持つ (ADR-039 additive)。
#[serde(default)]
pub registry: RegistryConfig,
/// 判定候補マッピング (機構ごと)。
#[serde(default)]
pub mechanisms: Vec<MechanismConfig>,
}

/// `[telemetry_report.registry]` の設定値。自動列挙元が無い hook / nudge 発火 id を静的に列挙する
/// (設計決定 1 § hook / nudge)。id は hook 名と一致しない例がある (`jj-op-verify` /
/// `pr_monitor_catchup` / `hooks-stop-tool-call-leak/prompt-recovery` 等、各 hook の
/// `lib_telemetry::record` 実装で確認)。
#[derive(Debug, Default, Clone, Deserialize)]
pub struct RegistryConfig {
/// hook / nudge 発火 id の静的リスト。
#[serde(default)]
pub hook_ids: Vec<String>,
}

impl TelemetryReportConfig {
/// 有効閾値 (未設定は既定 2)。
pub fn zero_streak_months(&self) -> u64 {
Expand Down Expand Up @@ -132,4 +147,29 @@ proposal = "enabled = false"
let cfg = parse_config("[telemetry_report]\ntrend_months = 0\n");
assert_eq!(cfg.trend_months(), 1);
}

#[test]
fn registry_hook_ids_default_empty_when_absent() {
let cfg = parse_config("[telemetry_report]\n");
assert!(cfg.registry.hook_ids.is_empty());
}

#[test]
fn parses_registry_hook_ids() {
let toml = r#"
[telemetry_report]

[telemetry_report.registry]
hook_ids = ["file-length", "reaper", "hooks-stop-tool-call-leak/prompt-recovery"]
"#;
let cfg = parse_config(toml);
assert_eq!(
cfg.registry.hook_ids,
vec![
"file-length".to_string(),
"reaper".to_string(),
"hooks-stop-tool-call-leak/prompt-recovery".to_string(),
]
);
}
}
37 changes: 37 additions & 0 deletions src/cli-telemetry-report/src/incident.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,23 @@ pub fn incident_ids_from_str(content: &str) -> BTreeSet<String> {
.collect()
}

/// `config_base/custom-lint-rules.toml` から**全** rule id を読む (レジストリの rule 供給源、
/// 設計決定 1)。incident 判定と同じ読み口 ([`RulesFile`]) を共有する。
///
/// ファイル不在 / parse 失敗は `None` を返す。[`incident_ids_from_str`] の fail-open (空集合) とは
/// 異なり、レジストリは供給源欠落を「never-fired 判定不能」としてレポートに明示するため、
/// 「読めなかった」と「rule が 0 本」を区別する必要がある。
pub fn read_all_rule_ids(config_base: &Path) -> Option<Vec<String>> {
let content = std::fs::read_to_string(config_base.join("custom-lint-rules.toml")).ok()?;
all_rule_ids_from_str(&content)
}

/// TOML 文字列から全 rule id を抽出する (pure)。parse 失敗は `None`。
pub fn all_rule_ids_from_str(content: &str) -> Option<Vec<String>> {
let parsed = toml::from_str::<RulesFile>(content).ok()?;
Some(parsed.rules.into_iter().map(|r| r.id).collect())
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -74,4 +91,24 @@ pr = "PR #x"
fn malformed_toml_yields_empty_set() {
assert!(incident_ids_from_str("not = [ toml").is_empty());
}

#[test]
fn all_rule_ids_lists_every_rule_regardless_of_incident() {
let toml = r#"
[[rules]]
id = "no-console-log"

[[rules]]
id = "no-personal-paths"
[rules.incident]
pr = 75
"#;
let ids = all_rule_ids_from_str(toml).unwrap();
assert_eq!(ids, vec!["no-console-log".to_string(), "no-personal-paths".to_string()]);
}

#[test]
fn all_rule_ids_returns_none_on_parse_failure() {
assert!(all_rule_ids_from_str("not = [ toml").is_none());
}
}
4 changes: 4 additions & 0 deletions src/cli-telemetry-report/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ mod config;
mod discover;
mod incident;
mod model;
mod registry;
mod report;
mod snapshot;
mod timekit;
Expand Down Expand Up @@ -125,6 +126,7 @@ fn finish_report(
now: u64,
) -> io::Result<Summary> {
let incident_ids = incident::incident_rule_ids(config_base);
let registry = registry::build_registry(config_base, &config.registry.hook_ids);
let verdicts = verdict::compute_verdicts(
rollups,
&config.mechanisms,
Expand All @@ -147,6 +149,7 @@ fn finish_report(
current_snapshot: snapshot,
mechanisms: &config.mechanisms,
incident_ids: &incident_ids,
registry: &registry,
verdicts: &verdicts,
trend_months: config.trend_months(),
retention_deleted,
Expand Down Expand Up @@ -272,6 +275,7 @@ mod tests {
zero_streak_months: Some(2),
trend_months: Some(6),
extra_roots: Vec::new(),
registry: Default::default(),
mechanisms: vec![config::MechanismConfig {
name: "stop_tool_call_leak".to_string(),
adr: "ADR-053/061".to_string(),
Expand Down
Loading