Skip to content

feat(cli-pr-monitor): 重複起動 file lock + ポーリング間隔延長 (Phase 3 / 順位 12) + Bundle V 登録 - #96

Merged
aloekun merged 3 commits into
masterfrom
feat/cli-pr-monitor-lock-and-poll-extension
Apr 30, 2026
Merged

feat(cli-pr-monitor): 重複起動 file lock + ポーリング間隔延長 (Phase 3 / 順位 12) + Bundle V 登録#96
aloekun merged 3 commits into
masterfrom
feat/cli-pr-monitor-lock-and-poll-extension

Conversation

@aloekun

@aloekun aloekun commented Apr 30, 2026

Copy link
Copy Markdown
Owner

Summary

トークン削減ロードマップ Phase 3 (順位 12: cli-pr-monitor ポーリング延長 + 重複起動ロック) の本実装に加え、PR #95 post-merge-feedback 由来の Bundle V 登録 (順位 31-33) を同梱します。両者とも token-reduction 系で論理的近接、PR 数最小化方針 (MEMORY.md: feedback_minimize_pr_count_during_rate_limit) により同 PR で land。

Why

PR #88 で実証された rate-limit 浪費 (Claude Code Max を 1 時間で 40% 消費) への直接対策。複数セッションが同時に cli-pr-monitor の polling を回すと、Claude API への並走 polling で rate-limit を急速消費する事象が観測されていました。

本 PR で rate-limit 抑制の 3 層のうち 2 層を完成させます:

対策 状態
1. Claude 側 polling 禁止 (preventive) Polling anti-pattern 検出 hook (PR #86 T1-1) ✅ 完了済
2. tool 側ポーリング頻度削減 (corrective) 本 PR: 重複起動 file lock + 間隔延長 ✅ 本 PR で完成
3. review 単位の自動再トリガー post-pr-review に rate-limit 自動検出 + 再トリガー Tier 2 残

Scope (2 論理ユニット同梱)

Phase 3 本実装 (順位 12)

重複起動 file lock — 新 module src/cli-pr-monitor/src/lock.rs

設計要素 内容
取得方式 OpenOptions::create_new による atomic create-or-fail (TOCTOU race を排除)
Busy 判定 AlreadyExists 時のみ既存 lock の stale 判定にフォールバック
Stale 閾値 1800s (max_duration_secs 600s の 3x 安全マージン)
Stale 時挙動 overwrite で takeover (複数 takeover 並走しても last write wins、 cleanup は確実)
Cleanup RAII Drop guard (ADR-025 同型パターン) で正常終了時削除
Crash recovery Drop が走らなくても次インスタンスが stale 経由で takeover
Guard 範囲 start_monitoring のみ。--observe (read-only) と --mark-notified (one-shot) は対象外で並走可

ポーリング間隔延長 src/cli-pr-monitor/src/config.rs

  • DEFAULT_POLL_INTERVAL: 120s → 180s
  • 単独セッションの polling 回数を 5 → ~3 サイクル/監視に削減 (約 40% 削減)
  • max_duration_secs (600s) は維持 (検出窓は変えない)

ISO 8601 parser (lock.rs 内蔵)

chrono 依存を増やさず、手書き parser で start_time の age を計算。util::utc_now_iso8601() 出力との format alignment は test で round-trip 検証済 (advisor 指摘反映)。

Bundle V 登録 (PR #95 post-merge-feedback)

docs/todo.md table への 順位 31-33 追加 + docs/todo3.md に詳細 3 件追加。実装は別 PR で予定。

順位 内容
31 review-security.md docs-only fast-approve から .takt/**.claude/** を明示除外
32 docs/todo.md ヘッダの「新規タスクは追加しない」表記を実態整合
33 code-review.md に新 verdict 経路追加時の 3 点チェックリスト追記

Test plan

  • cargo test -p cli-pr-monitor: 106 passed (lock 9 件含む)
  • cargo fmt --check / cargo clippy --all-targets -- -D warnings: 全 clean
  • release ビルド成功 + .claude/cli-pr-monitor.exe 配備
  • takt pre-push-review: 3 iterations / 11m 26s / aggregate=approved
  • concurrent_acquire_only_one_wins: 8 thread 同時 acquire で 1 つだけが Acquired になることを検証 (advisor 指摘の TOCTOU race を真に検証する test)
  • advisor pre-push 検証で TOCTOU race 指摘を反映、OpenOptions::create_new に書き換え

Design notes

  • stale 閾値 1800s の根拠: max_duration 600s の 3x マージン。プロセス crash 後 30 分待てば takeover できる。長すぎる場合は pr-monitor-config.toml で将来 override 可能 (本 PR では非導入)。
  • PID liveness check は非導入: timestamp-based stale で十分カバー。Windows/Unix の platform-specific 実装を避け、依存追加もなし。30 min 待機が許容できないユースケース観測時に follow-up で検討。
  • fail-open ポリシー: lock I/O エラー時は監視を継続 (権限不足等で lock が機能しなくても polling 自体は走る)。

Phase

Token 削減 Phase 3 完了。次は Phase 4 (順位 13: post-pr-review に rate-limit 自動検出 + 再トリガー、rate-limit critical 系の最終 1 つ)。

Summary by CodeRabbit

  • バグ修正

    • 同一リポジトリでの重複起動を防ぐプロセスロックを導入しました。既存ホルダー情報を通知して重複時は正常終了、古いロックは上書きで復旧。ロック取得不能時はロック無しで継続します。
    • ポーリング間隔の既定値を120秒から180秒に調整しました。
  • ドキュメント

    • 優先度表と戦略記述を更新(Tier表の再編、"rate-limit 抑制の3層"モデルへの変更)と、複数ドキュメントの整合性修正・チェックリスト追加を行いました。

@coderabbitai

coderabbitai Bot commented Apr 30, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 80a44c16-20d5-401f-96d2-99541e279eca

📥 Commits

Reviewing files that changed from the base of the PR and between 0a6b2cd and 66246c1.

📒 Files selected for processing (2)
  • src/cli-pr-monitor/src/lock.rs
  • src/cli-pr-monitor/src/stages/monitor.rs

📝 Walkthrough

Walkthrough

cli-pr-monitor のデフォルトポーリング間隔を120秒→180秒に延長し、プロセス重複を防ぐファイルベースのリポジトリロックを追加。関連ドキュメントの優先度表記を更新し、Tier 3 のドキュメント整合タスクを追加。

Changes

Cohort / File(s) Summary
Documentation Updates
docs/todo.md, docs/todo3.md
recommended-order/Tier 表を再構成して PR#88 完了を反映し、PR#95 による3件のTier 3 ドキュメント整合タスクを追加。
Polling Interval Configuration
src/cli-pr-monitor/src/config.rs
DEFAULT_POLL_INTERVAL120180 秒に変更し、doc comment を追加。
Repository Lock Implementation
src/cli-pr-monitor/src/lock.rs
.claude/pr-monitor.lock を用いるファイルベースのロック実装を追加(PID/UTC start_time/mode を TOML 書込、Acquired/Busy/Unavailable 列挙、RAII 解除、古いロックの上書きロジック、ISO8601 パーサとテスト群)。
Lock Integration
src/cli-pr-monitor/src/main.rs, src/cli-pr-monitor/src/stages/monitor.rs
mod lock; を導入し、start_monitoring がリポジトリロックを取得するよう変更。Busy の場合はホルダー情報をログして早期終了、Unavailable の場合はロック無しで継続、Acquired の場合はスコープ保持(_lock_guard)。

Sequence Diagram(s)

sequenceDiagram
    participant CLI as "CLI / main"
    participant Lock as "lock module"
    participant FS as "Filesystem (.claude/pr-monitor.lock)"
    participant Monitor as "start_monitoring"

    CLI->>Lock: acquire("start_monitoring")
    Lock->>FS: create_new() or read lock file
    alt created (Acquired)
        FS-->>Lock: wrote lock (PID, start_time, mode)
        Lock-->>CLI: Acquired(guard)
        CLI->>Monitor: proceed with monitoring (holds lock)
        Monitor-->>Lock: release on Drop at end
    else exists and fresh (Busy)
        FS-->>Lock: read holder PID + age
        Lock-->>CLI: Busy(holder_pid, age)
        CLI->>CLI: log holder and exit (code 0)
    else stale or corrupt (Overwrite / takeover)
        FS-->>Lock: overwrite with new lock
        Lock-->>CLI: Acquired(guard)
        CLI->>Monitor: proceed with monitoring (holds lock)
    else io error (Unavailable)
        FS-->>Lock: I/O error
        Lock-->>CLI: Unavailable(reason)
        CLI->>Monitor: proceed without lock
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PRタイトルは、重複起動ファイルロック機能、ポーリング間隔延長、およびBundle V登録という3つの主要な変更を正確に反映しており、変更セット全体の核となる部分を明確に要約しています。
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 60 minutes.

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/cli-pr-monitor/src/lock.rs`:
- Around line 167-205: parse_iso8601 currently forwards parsed
month/day/hour/minute/second into days_from_epoch/unix_timestamp without
validating ranges, causing panics for malformed timestamps like
"2026-99-01T00:00:00Z"; update parse_iso8601 to validate that month is 1..=12,
hour is 0..=23, minute and second are 0..=59, and that day is within the valid
number of days for the parsed month (accounting for is_leap for February), and
return None if any check fails so callers treat invalid parse as stale instead
of panicking; keep days_from_epoch/unix_timestamp unchanged but rely on
parse_iso8601 to guarantee valid inputs.
- Around line 75-109: The code currently returns LockResult::Acquired even when
filesystem ops fail (create_dir_all, write_all, create_new, or takeover write),
which falsely signals a held lock; change this to return a distinct failure
state (e.g. add LockResult::Unavailable or LockResult::IoError) instead of
Acquired in all error branches where we couldn't create or write the lock (refer
to functions/builders: build_lock_content, read_fresh_lock, MonitorLock and the
match arms handling OpenOptions::new().create_new and std::fs::write); update
the match arms so any I/O error paths (create_dir_all failure, f.write_all
error, create_new non-AlreadyExists errors, and takeover write failure) return
the new LockResult variant, and then update the caller in monitor.rs to treat
that variant as "skip/explicit failure" rather than "Acquired."
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 57f5b89e-8257-4e4b-8b8b-eecde6288f2e

📥 Commits

Reviewing files that changed from the base of the PR and between c299aa2 and 073de72.

📒 Files selected for processing (6)
  • docs/todo.md
  • docs/todo3.md
  • src/cli-pr-monitor/src/config.rs
  • src/cli-pr-monitor/src/lock.rs
  • src/cli-pr-monitor/src/main.rs
  • src/cli-pr-monitor/src/stages/monitor.rs

Comment thread src/cli-pr-monitor/src/lock.rs Outdated
Comment thread src/cli-pr-monitor/src/lock.rs
PR #88 で実証された rate-limit 浪費 (Claude Code Max を 1 時間で 40% 消費) への
直接対策。tool 側の polling 動作を 2 軸で改善し、Polling anti-pattern 検出 (PR
#86 T1-1, 完了済) と組み合わせて 3 層の rate-limit 抑制を成立させる。

## 順位 12 本実装

### 重複起動 file lock (新 module: src/cli-pr-monitor/src/lock.rs)

- `start_monitoring` の polling + takt 並走を `.claude/pr-monitor.lock` で
  1 アクティブ監視にゲート
- atomic create-or-fail (`OpenOptions::create_new`) で TOCTOU race を排除
- AlreadyExists 時のみ既存 lock の stale 判定にフォールバック
  - fresh (start_time が threshold 内): Busy 返却 → caller は no-op exit
  - stale (threshold 超過 / parse 不能): overwrite で takeover
- stale_threshold = 1800s (max_duration_secs 600s の 3x 安全マージン)
- Drop guard で正常終了時 cleanup、crash 時は次インスタンスが stale 経由で takeover
- guard 対象は `start_monitoring` のみ。`--observe` (read-only) と
  `--mark-notified` (one-shot mutation) は対象外で並走可能

### ポーリング間隔延長 (config.rs)

- DEFAULT_POLL_INTERVAL を 120s → 180s
- 単独セッションでも polling 回数を 5 → ~3 サイクル/監視に削減 (~40% 削減)
- max_duration_secs (600s) は維持

### ISO 8601 parser (lock.rs 内蔵)

- chrono 依存を増やさず手書き parse で start_time の age を計算
- うるう年・月日・時分秒を正確にハンドル (`is_leap` / `days_from_epoch` テスト済)
- util::utc_now_iso8601() の出力 format と round-trip することを test で検証

## テスト

- 9 件の lock test 追加 (cli-pr-monitor 全体 106 passed):
  - acquire / drop の基本動作
  - fresh lock が second acquire を block
  - stale lock の takeover (1980 timestamp で確実に stale)
  - corrupt lock (parse 不能) の takeover
  - **concurrent_acquire_only_one_wins**: 8 thread 同時 acquire で 1 つだけが
    Acquired になることを検証 (advisor 指摘の TOCTOU race を真に検証する test)
  - lock_format_matches_util_iso8601: util との format alignment 確認
  - parse_iso8601 / is_leap 単体動作

## docs/todo 系列の更新 (Bundle V 登録 + Phase 3 完了反映、同 PR 同梱)

ユーザー方針 (memory: feedback_minimize_pr_count_during_rate_limit) により、
直前 session の Bundle V 登録 (順位 31-33 の table 追加 + 詳細エントリ) と本
Phase 3 完了反映 (順位 12 削除 + narrative 整合化) を同 PR で land する。

- docs/todo.md: 順位 12 削除 + Bundle V 順位 31-33 追加 + narrative 三段構え
  (Polling anti-pattern + cli-pr-monitor lock + post-pr-review rate-limit) に更新
- docs/todo3.md: 順位 12 詳細エントリ削除 + Bundle V 詳細 3 件追加

## 期待効果

- 重複起動セッション間の polling 並走を確実に防止
- DEFAULT_POLL_INTERVAL 延長で単独セッションの polling 総回数を ~40% 削減
- rate-limit 抑制 3 層 (Claude 側 polling 禁止 + tool 側ポーリング頻度削減 +
  review 単位の自動再トリガー [Tier 2 残]) のうち 2 層を完成
aloekun added a commit that referenced this pull request Apr 30, 2026
Resolved findings:
- [Major] src/cli-pr-monitor/src/lock.rs:109 `Acquired` を「実際には lock できていない」ケースにも返しています。
- [Major] src/cli-pr-monitor/src/lock.rs:205 壊れた `start_time` で panic します。
@aloekun
aloekun force-pushed the feat/cli-pr-monitor-lock-and-poll-extension branch from 073de72 to 0a6b2cd Compare April 30, 2026 04:23
Resolved findings:
- [Major] src/cli-pr-monitor/src/lock.rs:109 `Acquired` を「実際には lock できていない」ケースにも返しています。
- [Major] src/cli-pr-monitor/src/lock.rs:205 壊れた `start_time` で panic します。
@aloekun

aloekun commented Apr 30, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Apr 30, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@aloekun

aloekun commented Apr 30, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Apr 30, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@aloekun

aloekun commented Apr 30, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Apr 30, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
src/cli-pr-monitor/src/lock.rs (1)

90-110: ⚠️ Potential issue | 🟠 Major

書き込み失敗後も Acquired を返してしまっています。

Line 95 / Line 110 では、実際には lock file を正常に残せていないのに caller が「lock 保持中」と解釈します。空/部分書き込みの file は次回 parse 失敗で stale 扱いになるので、重複起動防止がここで抜けます。write_all / takeover write の失敗は Unavailable に寄せ、新規作成側は部分 file を削除して戻す方が安全です。

差分案
         Ok(mut f) => {
             if let Err(e) = f.write_all(content.as_bytes()) {
-                log_info(&format!("[lock] 新規 lock 書き込み失敗 (継続): {}", e));
+                log_info(&format!("[lock] 新規 lock 書き込み失敗: {}", e));
+                let _ = std::fs::remove_file(&path);
+                return LockResult::Unavailable {
+                    reason: e.to_string(),
+                };
             }
             LockResult::Acquired(MonitorLock { path })
         }
@@
             if let Err(e) = std::fs::write(&path, content) {
-                log_info(&format!("[lock] takeover 書き込み失敗 (継続): {}", e));
+                log_info(&format!("[lock] takeover 書き込み失敗: {}", e));
+                return LockResult::Unavailable {
+                    reason: e.to_string(),
+                };
             }
             LockResult::Acquired(MonitorLock { path })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli-pr-monitor/src/lock.rs` around lines 90 - 110, The code returns
LockResult::Acquired even when writes fail (in the OpenOptions branch using
f.write_all and in the takeover branch using std::fs::write), which can leave a
corrupt/empty lock file; change the logic so that any write error causes the
function to return LockResult::Unavailable (or equivalent) instead of Acquired,
and for the new-create branch delete the partial file if write_all fails before
returning Unavailable; keep the existing fresh-check using
read_fresh_lock/stale_threshold_secs and only return MonitorLock with
LockResult::Acquired after a successful write.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/cli-pr-monitor/src/lock.rs`:
- Around line 354-356: このテストは thread 内で matches!(acquire_at(p, "concurrent",
1800), LockResult::Acquired(_)) を直接評価しているため取得した MonitorLock が即時 drop されて flaky
です。修正するには各 thread が acquire_at の戻り値(LockResult)を保持してから全 thread を join し、その後で各
LockResult を調べて exactly one が Acquired であることをアサートしてください; 該当する識別子は acquire_at,
LockResult::Acquired, MonitorLock, handles, thread::spawn,
b.wait()(および同様のブロックが存在する 359–369 範囲)です。
- Around line 163-170: The current parse_age_secs function uses
now.saturating_sub(then) which produces age=0 for future timestamps and prevents
treating future/broken timestamps as stale; modify parse_age_secs (and its use
of parse_iso8601) to detect if then > now and return None in that case,
otherwise return Some(now - then) as i64—ensure you compare the same integer
types (after converting SystemTime to i64 seconds) and avoid saturating_sub so
future timestamps are treated as parse failures/stale.

---

Duplicate comments:
In `@src/cli-pr-monitor/src/lock.rs`:
- Around line 90-110: The code returns LockResult::Acquired even when writes
fail (in the OpenOptions branch using f.write_all and in the takeover branch
using std::fs::write), which can leave a corrupt/empty lock file; change the
logic so that any write error causes the function to return
LockResult::Unavailable (or equivalent) instead of Acquired, and for the
new-create branch delete the partial file if write_all fails before returning
Unavailable; keep the existing fresh-check using
read_fresh_lock/stale_threshold_secs and only return MonitorLock with
LockResult::Acquired after a successful write.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 23020d50-2f44-416f-9f41-79d2abd5b487

📥 Commits

Reviewing files that changed from the base of the PR and between 073de72 and 0a6b2cd.

📒 Files selected for processing (6)
  • docs/todo.md
  • docs/todo3.md
  • src/cli-pr-monitor/src/config.rs
  • src/cli-pr-monitor/src/lock.rs
  • src/cli-pr-monitor/src/main.rs
  • src/cli-pr-monitor/src/stages/monitor.rs
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/cli-pr-monitor/src/main.rs
  • src/cli-pr-monitor/src/stages/monitor.rs
  • docs/todo.md

Comment thread src/cli-pr-monitor/src/lock.rs Outdated
Comment thread src/cli-pr-monitor/src/lock.rs Outdated
Finding D (Major, parse_age_secs): 未来日時 (時計巻き戻し / 破損 future timestamp)
が saturating_sub で age=0 となり永続 fresh 扱いになる bug を修正。明示的に
then > now を None (stale 扱い) で返し、crash recovery が機能するようにする。
test future_timestamp_lock_is_taken_over を追加。

Finding E (Minor, concurrent test flakiness): 1 barrier 構成だと先行 thread の
guard が判定後に即 drop されて後続 thread が逐次 Acquired する race window が
存在した。start barrier + finish barrier の 2 barrier 構成にし、全 thread が
判定終了まで Acquired guard を保持するよう修正。

両者とも CR の suggestion 準拠。lock test 全 13 件 pass、clippy clean。
@aloekun
aloekun force-pushed the feat/cli-pr-monitor-lock-and-poll-extension branch from 0a6b2cd to 66246c1 Compare April 30, 2026 05:21
@aloekun

aloekun commented Apr 30, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Apr 30, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@aloekun
aloekun merged commit 5f5c79b into master Apr 30, 2026
1 check passed
@aloekun
aloekun deleted the feat/cli-pr-monitor-lock-and-poll-extension branch April 30, 2026 08:54
aloekun added a commit that referenced this pull request Apr 30, 2026
cli-pr-monitor の重複起動防止 lock file (PR #88 / #96 で導入) が
.gitignore 漏れで PR snapshot に混入する事故を防ぐ。
aloekun added a commit that referenced this pull request Apr 30, 2026
CR review @ commit e7b8d49 で検出された 3 Major + 1 Nitpick を修正。

## 修正内容

### Finding 1 (Major): parse_rate_limit に push_time フィルタ追加
過去セッションの rate-limit comment を新セッション開始時に誤って拾うバグを修正。
`comment.created_at >= push_time` フィルタを追加し、parse_new_comments / parse_findings
と同じ規則に揃える。これにより本 PR 検証中に観測された 22 分遅延の根因の一つが解消される。

### Finding 2 (Major): parse_new_comments で rate-limit comment 除外
rate-limit comment が new_comments にカウントされると `decide()` が action_required
を早期 return し、poll.rs の rate-limit retry 経路に入らずに監視終了するバグ。
"Rate limit exceeded" を含む comment を新規コメント集計から除外する。22 分遅延のもう
一つの根因。

### Finding 3 (Major): write_state 失敗時の自動 retry 停止
retrigger 後の state 永続化に失敗すると、次 iteration で `rate_limit_retries` と
`rate_limit_last_retriggered_at` の復元に失敗し、dedup と max_retries が壊れて
`@coderabbitai review` 重複投稿の可能性がある。失敗時は action_required で抜ける。

### Finding 4 (Nitpick): docs/todo.md 順位 絶対参照削除
ADR-033 の「絶対番号は table のみに保持」原則に従い、Bundle W/X 説明文から
`(順位 34)` `(順位 35)` 等の絶対参照を削除しタスク名参照に置換。

### .gitignore: pr-monitor.lock 除外
cli-pr-monitor の重複起動防止 lock file (PR #88 / #96 で導入) が .gitignore
漏れで PR snapshot に混入する事故を防ぐ。

## テスト

- check-ci-coderabbit: 59 tests pass (rate_limit_filters_out_past_session_comments,
  rate_limit_includes_comment_at_exact_push_time, comments_excludes_rate_limit を追加)
- cli-pr-monitor: 118 tests pass
- clippy + fmt clean (変更パッケージ)
aloekun added a commit that referenced this pull request Apr 30, 2026
CR review @ commit 79b7c3d で検出された 3 Major + 1 Minor のうち
Finding 1 (updated_at) を除く 3 件を修正。

## 修正内容

### Finding 2 (Major): rate-limit sleep を max_duration で cap
`handle_rate_limit_retry` に `remaining_monitor_secs` 引数を追加し、
sleep が監視残り予算を超える場合は Err を返して retry を停止する。
これまでは max_duration を素通りして 30 分超ブロックする可能性があった。

### Finding 3 (Major): handle_rate_limit_retry を Result 化
gh pr comment 投稿失敗 / PR 番号未確定の場合に retries++ や
last_retriggered_at 更新を実施しないよう、関数を Result<(), String> に
変更。caller は Ok 時のみ dedup key を更新し、Err 時は action_required
で抜ける。これにより失敗 retrigger が dedup で perma-skip 化する
silent failure を防ぐ。

### Finding 4 (Minor): docs/todo.md 第2層 PR 参照に #96 追記
"(PR #88 T2-4、完了済)" → "(PR #88 T2-4 / #96、完了済)" に履歴整合。

## テスト追加 (2 件)

- rate_limit_retry_returns_err_when_sleep_exceeds_budget:
  remaining=60s だが sleep=600s 必要なケースで Err を返し state 不変を確認
- rate_limit_retry_returns_err_when_pr_number_missing:
  PR 番号未確定で Err を返し state 不変を確認

## 未対応 (任意)

Finding 1 (updated_at): CR が rate-limit comment を編集する挙動は実観測なし。
将来的な堅牢性向上のため todo 化候補。
aloekun added a commit that referenced this pull request Apr 30, 2026
* feat(cli-pr-monitor): rate-limit 自動検出 + 再トリガーロジック (Phase 4 / 順位 13)

PR #89 T2-1 の自動化。CodeRabbit が `Rate limit exceeded` コメントを投稿した
場合、reset 時刻 + 60s buffer まで sleep し `@coderabbitai review` を再投稿する。

## 実装

- check-ci-coderabbit: rate-limit comment を検出し reset 時刻を計算する
  `parse_rate_limit` を追加。`Please wait N minutes M seconds` の正規表現抽出 +
  ISO 8601 → unix epoch 秒の手動パース (chrono 依存追加なし)。

- cli-pr-monitor: poll loop に rate-limit retry ブランチを追加
  - `RateLimitConfig`: auto_retry_enabled (default true) + max_retries (default 3)
  - `state.rate_limit_retries`: 累積 retry 回数を state.json で persist
  - `state.rate_limit_last_retriggered_at`: dedup key (advisor 指摘対応)
  - `handle_rate_limit_retry`: sleep → gh pr comment 投稿 → counter++

## dedup の必要性 (advisor finding)

`comment_created_at` で dedup しないと、同一 rate-limit comment が iteration を
跨いで PR コメント一覧に残り、`(until_unix_secs - now).max(0) = 0` となって
即時 retrigger を繰り返す。結果 max_retries=3 が数秒で消費され誤って
`action_required` で抜ける。`rate_limit_last_retriggered_at` で同じ created_at
を skip し、CR が新しい rate-limit comment を投稿した時点で再度対象にする。

## テスト

- check-ci-coderabbit: rate-limit detection / parsing 11 tests 追加 (56 passed)
- cli-pr-monitor: poll dedup + state persistence 4 tests 追加 (116 passed)

## 完了タスク

- 順位 13 を docs/todo.md table から削除、todo3.md detail 削除
- 順位 19 (REJECT-ESCALATE) が rate-limit critical 系の最後の Tier 2 残

* fix(cli-pr-monitor): CodeRabbit 指摘 (PR #97) 反映 + 誤混入 lock 削除

- handle_rate_limit_retry の log format 修正: retry={}/{} の 2 番目に
  rl.until_unix_secs (unix 秒) が渡されていたバグ。max_retries に修正。
  以前は "retry=1/1735689600" のような意味不明な出力になっていた。
- RateLimitConfig の parse test 追加 (defaults + custom)。既存
  config_fix_defaults / config_fix_custom と同パターンで網羅。
- 前 commit で誤って snapshot された .claude/pr-monitor.lock を削除
  (.gitignore 追加は後続 commit で実施)。

* fix(cli-pr-monitor): CodeRabbit round 2 指摘 (PR #97) 反映 + .gitignore 整備

CR review @ commit e7b8d49 で検出された 3 Major + 1 Nitpick を修正。

## 修正内容

### Finding 1 (Major): parse_rate_limit に push_time フィルタ追加
過去セッションの rate-limit comment を新セッション開始時に誤って拾うバグを修正。
`comment.created_at >= push_time` フィルタを追加し、parse_new_comments / parse_findings
と同じ規則に揃える。これにより本 PR 検証中に観測された 22 分遅延の根因の一つが解消される。

### Finding 2 (Major): parse_new_comments で rate-limit comment 除外
rate-limit comment が new_comments にカウントされると `decide()` が action_required
を早期 return し、poll.rs の rate-limit retry 経路に入らずに監視終了するバグ。
"Rate limit exceeded" を含む comment を新規コメント集計から除外する。22 分遅延のもう
一つの根因。

### Finding 3 (Major): write_state 失敗時の自動 retry 停止
retrigger 後の state 永続化に失敗すると、次 iteration で `rate_limit_retries` と
`rate_limit_last_retriggered_at` の復元に失敗し、dedup と max_retries が壊れて
`@coderabbitai review` 重複投稿の可能性がある。失敗時は action_required で抜ける。

### Finding 4 (Nitpick): docs/todo.md 順位 絶対参照削除
ADR-033 の「絶対番号は table のみに保持」原則に従い、Bundle W/X 説明文から
`(順位 34)` `(順位 35)` 等の絶対参照を削除しタスク名参照に置換。

### .gitignore: pr-monitor.lock 除外
cli-pr-monitor の重複起動防止 lock file (PR #88 / #96 で導入) が .gitignore
漏れで PR snapshot に混入する事故を防ぐ。

## テスト

- check-ci-coderabbit: 59 tests pass (rate_limit_filters_out_past_session_comments,
  rate_limit_includes_comment_at_exact_push_time, comments_excludes_rate_limit を追加)
- cli-pr-monitor: 118 tests pass
- clippy + fmt clean (変更パッケージ)

* fix(cli-pr-monitor): CodeRabbit round 3 指摘 (PR #97) 反映

CR review @ commit 79b7c3d で検出された 3 Major + 1 Minor のうち
Finding 1 (updated_at) を除く 3 件を修正。

## 修正内容

### Finding 2 (Major): rate-limit sleep を max_duration で cap
`handle_rate_limit_retry` に `remaining_monitor_secs` 引数を追加し、
sleep が監視残り予算を超える場合は Err を返して retry を停止する。
これまでは max_duration を素通りして 30 分超ブロックする可能性があった。

### Finding 3 (Major): handle_rate_limit_retry を Result 化
gh pr comment 投稿失敗 / PR 番号未確定の場合に retries++ や
last_retriggered_at 更新を実施しないよう、関数を Result<(), String> に
変更。caller は Ok 時のみ dedup key を更新し、Err 時は action_required
で抜ける。これにより失敗 retrigger が dedup で perma-skip 化する
silent failure を防ぐ。

### Finding 4 (Minor): docs/todo.md 第2層 PR 参照に #96 追記
"(PR #88 T2-4、完了済)" → "(PR #88 T2-4 / #96、完了済)" に履歴整合。

## テスト追加 (2 件)

- rate_limit_retry_returns_err_when_sleep_exceeds_budget:
  remaining=60s だが sleep=600s 必要なケースで Err を返し state 不変を確認
- rate_limit_retry_returns_err_when_pr_number_missing:
  PR 番号未確定で Err を返し state 不変を確認

## 未対応 (任意)

Finding 1 (updated_at): CR が rate-limit comment を編集する挙動は実観測なし。
将来的な堅牢性向上のため todo 化候補。

* fix(check-ci-coderabbit): rate-limit 計算を updated_at 基準に変更 (PR #97 round 3 Finding 1 実観測対応)

CR が rate-limit comment を編集して wait 時間を更新するケースが本 PR の dogfood で
実観測された (created_at=2026-04-30T11:11:51Z, updated_at=2026-04-30T14:38:32Z で
"wait 21 minutes" に編集)。created_at 基準だと reset 時刻を 3 時間以上前として誤算定し、
premature retrigger → CR 再 rate-limit → comment edit のループに陥る危険があった。

## 修正内容

- GhComment に updated_at フィールドを追加
- rate_limit_event_time(): updated_at fallback created_at を返すヘルパー追加
- parse_rate_limit: 計算基準を event_time に変更 (フィルタ / sort / until 計算)
- RateLimitInfo.comment_created_at: 値の意味を「event_time」に拡張 (フィールド名は維持)
- state.rs RateLimitState.comment_created_at の doc 更新

## テスト追加 (3 件)

- rate_limit_uses_updated_at_when_present:
  実観測ケース (created_at != updated_at) で updated_at 基準計算を確認
- rate_limit_falls_back_to_created_at_when_updated_at_missing:
  既存挙動の後方互換性を確認
- rate_limit_edited_comment_yields_new_dedup_key:
  編集前後で dedup key が変化することを確認 (新 wait 時間で再 trigger 可能)
aloekun added a commit that referenced this pull request Jun 8, 2026
…除) (#199)

* feat(cli-pr-monitor): PastTime newtype + proptest for parse_iso8601/from_parts (Bundle W)

Bundle W (順位 34 PBT + 順位 35 PastTime newtype) を Tier 1 として独立 land。
PR #96 で実証された Finding D class (saturating_sub silent semantic mismatch) を
型層で再発不能化し、proptest で regression net を確立。

## 順位 35: PastTime newtype

src/cli-pr-monitor/src/lock.rs に PastTime { epoch_secs, captured_now } を導入。
`from_iso8601_now` / `from_parts` で「parse 成功 + then <= now」の 2 ステップを
construction に閉じ込め、`age_secs()` が常に非負となる invariant を構造的に保証。

旧 `parse_age_secs(iso8601) -> Option<i64>` は削除し、`read_fresh_lock` を
PastTime ベースに refactor。public API への影響なし (lock module 内 closure)。

## 順位 34: proptest properties

src/cli-pr-monitor/Cargo.toml の [dev-dependencies] に proptest = "1" を追加。
lock.rs の #[cfg(test)] mod proptests に 5 properties を記述:

- P1 past_time_age_is_correct_when_in_past: then <= now で age_secs == now - then
- P2 past_time_rejects_future: then > now で必ず None (Finding D 直接対応)
- P3 parse_iso8601_never_panics: 任意 string で panic しない
- P4 parse_iso8601_rejects_pre_epoch_year: year < 1970 を必ず reject
- P5 parse_iso8601_accepts_well_formed: 有効範囲内の正規 ISO 8601 を必ず accept

PastTime 単体 unit test 4 件 (accepts_past / accepts_equal / rejects_future /
accepts_unix_epoch_origin / rejects_far_future_year_9999) も追加。

実行時間: cargo test -p cli-pr-monitor lock:: で 0.04s (todo4.md 完了基準
「pre-push pipeline +1 秒以内」を充足)。

## 依存解除

Bundle W は元来「順位 19 (REJECT-ESCALATE) land 後着手」とユーザー指示があったが、
順位 19 自身が Status update 2026-06-06 で「5-10 PR baseline 観測フェーズ」に移行
した結果、Tier 1 → Tier 2 の priority inversion + 待機解除タイミング未定でデッド
ロックに近い状態だった。

依存解除の根拠 (advisor 検証済):
- ADR-037 (fix-trust shortcut, PR #106 land): `convergence_verdict: fully_resolved`
  で COMPLETE 直行 → iteration が verdict ベースで有限化、無限 loop 経路は解消済
- ADR-043 (fail-closed 原則, PR #194 land): security/quality gate の semantic を構造化
- PR #194 (mechanical gate 強化): merge 前の検証層を拡充

これにより PBT 由来の test 失敗を fix loop が処理しきれず暴走する旧懸念は解消済。
docs/todo-summary.md 順位 34 row および docs/todo4.md 順位 34 実行優先度行に
依存解除の根拠を明記。

## scope 外

- Finding E (concurrency race): proptest は data generation には強いが thread
  interleaving 網羅は苦手 (todo4.md 詰まっている箇所に既記載)。Bundle X
  (順位 36 cargo-mutants + 順位 37 stress runner) に委譲
- 派生プロジェクト deploy 計画: Bundle W land 後の別 task として todo 登録予定

## 完了確認

- cargo build -p cli-pr-monitor: 成功
- cargo test -p cli-pr-monitor lock::: 23 passed (proptest 5 + PastTime 4 + 既存 14)
- cargo clippy -p cli-pr-monitor --all-targets -- -D warnings: clean

Refs: 順位 34, 35 (todo-summary.md), todo4.md Bundle W entries

* docs(todo): Bundle W (順位 34 + 35) land 完了に伴い削除
aloekun added a commit that referenced this pull request Jun 8, 2026
PR #199 (Bundle W) の post-merge-feedback report (.claude/feedback-reports/199.md)
で analyzer が ✅ 採用候補とした 2 件を、ユーザー承認 (2026-06-08) を経て
docs/todo*.md 系列に登録する。

## 順位 197: hooks-session-start orphan age 計算に proptest 追加 (T2-1)

Bundle W (PR #199) で `cli-pr-monitor::lock` に PastTime newtype + proptest properties を
導入し、`saturating_sub` silent semantic mismatch (Finding D) を構造的に排除した。
同じ bug class が `src/hooks-session-start/src/main.rs:236` の orphan age 計算
(`now_unix.saturating_sub(start_unix)`) にも存在し、clock rewind / future timestamp で
age=0 → orphan reaper が「young」判定でスキップ → `.failed` marker 未生成 → ADR-030
L2 recovery 停止という具体的 failure chain が確認済。Bundle W の pattern を
hooks-session-start に展開する。

- Tier 2 / Effort M / Severity High / Frequency Medium / Adoption Risk None
- proptest 1.x は既存 dev-dependencies、新規依存追加は hooks-session-start のみ

## 順位 198: ADR-NNN Timestamp invariant safety (T3-2)

PR #96 Finding D + PR #199 Bundle W で同型 bug class が 2 件観測 (Frequency Medium)。
「時刻計算における silent failure class と型レベル防御」を永続化し、派生プロジェクト
(techbook-ledger / auto-review-fix-vc) への transferability を確保する ADR を新設。

- Tier 3 / Effort M / Severity Medium / Frequency Medium / Adoption Risk None
- 順位 135 codified placeholder policy 適用 (ADR 番号は land 時 PR で確定)
- CLAUDE.md ADR list 追記

## 変更ファイル

- docs/todo10.md: 順位 197/198 詳細 entry を 2 セクション追加
- docs/todo-summary.md: table に順位 197/198 行を追加
aloekun added a commit that referenced this pull request Jun 8, 2026
PR #199 (Bundle W) の post-merge-feedback report (.claude/feedback-reports/199.md)
で analyzer が ✅ 採用候補とした 2 件を、ユーザー承認 (2026-06-08) を経て
docs/todo*.md 系列に登録する。

## 順位 197: hooks-session-start orphan age 計算に proptest 追加 (T2-1)

Bundle W (PR #199) で `cli-pr-monitor::lock` に PastTime newtype + proptest properties を
導入し、`saturating_sub` silent semantic mismatch (Finding D) を構造的に排除した。
同じ bug class が `src/hooks-session-start/src/main.rs:236` の orphan age 計算
(`now_unix.saturating_sub(start_unix)`) にも存在し、clock rewind / future timestamp で
age=0 → orphan reaper が「young」判定でスキップ → `.failed` marker 未生成 → ADR-030
L2 recovery 停止という具体的 failure chain が確認済。Bundle W の pattern を
hooks-session-start に展開する。

- Tier 2 / Effort M / Severity High / Frequency Medium / Adoption Risk None
- proptest 1.x は既存 dev-dependencies、新規依存追加は hooks-session-start のみ

## 順位 198: ADR-NNN Timestamp invariant safety (T3-2)

PR #96 Finding D + PR #199 Bundle W で同型 bug class が 2 件観測 (Frequency Medium)。
「時刻計算における silent failure class と型レベル防御」を永続化し、派生プロジェクト
(techbook-ledger / auto-review-fix-vc) への transferability を確保する ADR を新設。

- Tier 3 / Effort M / Severity Medium / Frequency Medium / Adoption Risk None
- 順位 135 codified placeholder policy 適用 (ADR 番号は land 時 PR で確定)
- CLAUDE.md ADR list 追記

## 変更ファイル

- docs/todo10.md: 順位 197/198 詳細 entry を 2 セクション追加
- docs/todo-summary.md: table に順位 197/198 行を追加
aloekun added a commit that referenced this pull request Jun 9, 2026
…-1/T3-2 採用 (#200)

* feat(cli-docs-lint): priority_inversion validator — Tier 1→Tier 2 依存を機械検知

PR #199 (Bundle W) で実観測した「Tier N が Tier N+k に依存 + 待ち先 Tier N+k 自身が
観測フェーズ」パターンを cli-docs-lint に validator 化。決定論的検出層で Claude 判断
介入なし (`feedback_pipeline_over_rules.md` 適用)。

## 実装

- `src/cli-docs-lint/src/priority_inversion.rs` (新規): todo-summary.md table を parse し
  - 順位/Tier 抽出 (`parse_row`, `parse_tier`)
  - 依存記述から 順位 NN 参照を抽出 (`extract_referenced_ranks`、複合 `順位 NN/MM` 対応)
  - 「なし」プレフィックスで context note を排除 (`has_no_dependency_prefix`)
  - 依存先 Tier > 自分の Tier かつ resolved-marker なしを violation 化
- `src/cli-docs-lint/src/lib.rs`: `priority_inversion` module を export
- `src/cli-docs-lint/src/main.rs`: `--check priority-inversion` mode + `all` 経路統合 +
  help / describe_mode に追記

## resolved-marker 設計

依存記述内で「順位 NN ... land 済 / 完了 / retired / 採用昇格済」が 80 char window
以内に現れる場合 resolved 扱い (= inversion check skip)。「land 済 (2026-06-07)」のような
日付付きマーカーや「Bundle X land 済」のような bundle-level マーカーをカバー。

## 性能 (F-2 対応、pre-push reviewer non-blocking finding)

`parse_tier` / `extract_referenced_ranks` の regex は `std::sync::LazyLock<Regex>` で
module 初期化時に 1 度だけ compile (per-row 再 compile を回避)。既存 codebase の
OnceLock 利用 (cli-pr-monitor) と整合する std::sync::* family。

## テスト fixture 設計 (F-3 対応、pre-push reviewer non-blocking finding)

`check_content_skips_when_referenced_rank_missing_from_table` テストの fixture を
`"順位 19 land 後推奨"` (resolved-marker 非含有) に変更。missing-rank 経路を厳密に
exercise する形に修正し、将来 fixture に rank 19 行を追加した際の test 経路 silent
shift (missing-rank → resolved-marker) を防止。

## prefix-match 防御 (pre-push takt-fix で auto 補正)

`has_resolved_marker_after` に `c.is_ascii_digit()` ガードを追加し、`"順位 19"` が
`"順位 195"` 等の prefix match で誤検出するバグを防止。専用 unit test 追加。

## テスト & dogfood

- 38 unit tests pass (priority_inversion 18 + preamble 9 + cross_ref 11)
- 現 docs/todo-summary.md に対し 0 violations (false positive なし)
- pnpm lint:docs OK (preamble + cross-ref + priority-inversion)

## 統合

- `pnpm lint:docs` 経由で `--check all` 実行時に自動実行
- `.claude/cli-docs-lint.exe` rebuild + deploy 済 (release profile)
- ADR-039 試験運用パターン継承 (kill-switch `CLI_DOCS_LINT_DISABLE=1`、3-5 PR dogfood
  後に default-ON 昇格 / 却下を判定)

* docs(todo): 順位 19 (REJECT-ESCALATE) retire — ADR-037/043 + PR #194 land で代替経路実現済、PR #194-後 8 runs で escalation 候補 0 件

PR #199 (Bundle W) session 内合意 (ユーザー指示 2026-06-07): 「A の PR マージまで進ん
だら、B. 順位 19 自体の Tier 再評価 のタスク化を進める」を実行し、実測値に基づき
(C) retire を決定。

## 決定根拠 (empirical data)

PR #194 (mechanical gate 強化) land 後の takt run logs を集計:

| Run | iteration 上限到達 | supervise step 到達 | escalation 候補 |
|---|---|---|---|
| 8 runs (pre-push-review 7 + post-pr-review 1) | 0/8 | 0/8 | 0/8 |

REJECT-ESCALATE が解決するはずの「iteration 上限到達 + supervise/fix_supervisor の
無限ループ」が **PR #194 land 以降 1 件も観測されていない**。代替経路で十分実用に
耐えていることが定量的に確認できた:

- ADR-037 (fix-trust shortcut、PR #106 land): `convergence_verdict: fully_resolved` で
  COMPLETE 直行 → iteration が verdict ベースで有限化
- ADR-043 (fail-closed 原則、PR #194 land): security/quality gate semantics 構造化
- PR #194 (mechanical gate 強化): merge 前 clippy + 空 commit sweep の決定論層追加

## 変更内容

- docs/todo3.md: 順位 19 entry block を削除 (-46 行)
- docs/todo-summary.md: table から 順位 19 row を削除 (-1 行)

順位 19 を gate にしていた Tier 1 (旧 順位 34/35) は既に PR #199 で priority
inversion 解除済のため、本 retire で残るブロッキングはない。

## 関連 PR / ADR

- PR #199 (Bundle W、priority inversion 解除)
- ADR-037 fix-trust shortcut
- ADR-043 Security/Quality Gate Fail-Closed 原則
- PR #194 mechanical gate 強化 (clippy + 空 commit sweep)

* docs(todo): post-merge-feedback T2-1/T3-2 採用 — 順位 197/198 追加

PR #199 (Bundle W) の post-merge-feedback report (.claude/feedback-reports/199.md)
で analyzer が ✅ 採用候補とした 2 件を、ユーザー承認 (2026-06-08) を経て
docs/todo*.md 系列に登録する。

## 順位 197: hooks-session-start orphan age 計算に proptest 追加 (T2-1)

Bundle W (PR #199) で `cli-pr-monitor::lock` に PastTime newtype + proptest properties を
導入し、`saturating_sub` silent semantic mismatch (Finding D) を構造的に排除した。
同じ bug class が `src/hooks-session-start/src/main.rs:236` の orphan age 計算
(`now_unix.saturating_sub(start_unix)`) にも存在し、clock rewind / future timestamp で
age=0 → orphan reaper が「young」判定でスキップ → `.failed` marker 未生成 → ADR-030
L2 recovery 停止という具体的 failure chain が確認済。Bundle W の pattern を
hooks-session-start に展開する。

- Tier 2 / Effort M / Severity High / Frequency Medium / Adoption Risk None
- proptest 1.x は既存 dev-dependencies、新規依存追加は hooks-session-start のみ

## 順位 198: ADR-NNN Timestamp invariant safety (T3-2)

PR #96 Finding D + PR #199 Bundle W で同型 bug class が 2 件観測 (Frequency Medium)。
「時刻計算における silent failure class と型レベル防御」を永続化し、派生プロジェクト
(techbook-ledger / auto-review-fix-vc) への transferability を確保する ADR を新設。

- Tier 3 / Effort M / Severity Medium / Frequency Medium / Adoption Risk None
- 順位 135 codified placeholder policy 適用 (ADR 番号は land 時 PR で確定)
- CLAUDE.md ADR list 追記

## 変更ファイル

- docs/todo10.md: 順位 197/198 詳細 entry を 2 セクション追加
- docs/todo-summary.md: table に順位 197/198 行を追加

* fix(cli-docs-lint): priority_inversion の resolved-marker window を char-based 化 (CR Major #1)

PR #200 CodeRabbit review で指摘された Major finding を修正。

## Bug

`has_resolved_marker_after` の window 計算が以下の 2 点で spec とズレていた:

1. **byte vs char**: `(abs_pos + RESOLUTION_WINDOW_CHARS)` は **byte** 演算。日本語 1 文字 = 3 bytes
   なので「80 文字 window」のつもりが実質 ~27 文字 window に縮退
2. **window 開始位置**: `&haystack[abs_pos..]` で needle (順位 NN) 自体を window に含めていた。
   spec は「順位参照の **直後** から N 文字以内」

具体 failure 例 (CR review 提示):
- `"順位 19" + "あ"*40 + " land 済"` で、marker "land 済" は char-distance 41 (window 80 内)
- 旧 byte-based: 80 bytes window は ~26 chars のみカバー → marker 圏外 → resolved 扱いされず
  → **false negative (= inversion 誤検出 → reviewer に noise)**

## Fix

```rust
let window_end = haystack[after..]
    .char_indices()
    .nth(RESOLUTION_WINDOW_CHARS)
    .map(|(i, _)| after + i)
    .unwrap_or(haystack.len());
let window = &haystack[after..window_end];
```

- 起点を `after` (needle 直後) に変更
- `char_indices().nth(N)` で N 文字目の byte offset を取得 → char-based window

不要になった `next_char_boundary` ヘルパー関数を削除。

## Test

regression test 追加 (`is_resolved_detects_marker_across_multibyte_gap`):
- `"順位 19" + "あ".repeat(40) + " land 済"` で resolved 判定が true になることを assert
- 旧実装ではこの test は fail する (byte window で marker 圏外)

## Doc update

`RESOLUTION_WINDOW_CHARS` の doc に「**文字数** (バイト数ではない)」を明記。
multi-byte でも spec 通り動作することを comment で保証。

## 検証

- cargo test -p cli-docs-lint: 19 priority_inversion tests pass (新規 regression 1 含む)
- cargo clippy -p cli-docs-lint --all-targets -- -D warnings: clean
- pnpm lint:docs: OK (preamble + cross-ref + priority-inversion)
- .claude/cli-docs-lint.exe rebuild + deploy 済

Refs: CR comment on PR #200 (src/cli-docs-lint/src/priority_inversion.rs:182)
aloekun added a commit that referenced this pull request Jun 12, 2026
…exception codify (PR #203 follow-up) (#204)

* docs(todo): 順位 198 を PR #203 T3-1 採用で 3 観測目に昇格

PR #203 post-merge-feedback Tier 3 #1 (ADR-NNN: Timestamp invariant safety) を採用。
analyzer は新規 entry 提案だが、順位 198 が既に同 ADR 提案として登録済 (PR #199 T3-2)
のため、新規追加ではなく既存 entry の data point 強化として merge した。

主な変更:
- 動機: 2 件観測 (Medium) → 3 件観測 (High) に Frequency 昇格
- 本タスクの位置づけ: PR #203 T3-1 採用情報 + 既存 entry 強化の判断根拠を追記
- 参照: .claude/feedback-reports/203.md Tier 3 #1 + PR #203 を追加
- 設計決定 § 1 コンテキスト: PR #203 hooks-session-start port を観測実例に追加
- 派生プロジェクト適用: "順位 197 で実装予定" → "PR #203 で実装済" に更新
- 作業計画: PR #96 / #199 / #203 の 3 観測すべてを ADR 実装時に inline cite

順位 194 (task 着手前 grep 確認 rule、PR #196 採用) の初実践例となる。
analyzer の重複提案を運用層で吸収する明示的 pattern。

* docs(adr-039): mechanical lint exception を § 1.b として明記 + checklist 上位判定追加

PR #203 post-merge-feedback で「順位 177 file_size_check が ADR-039 機械適用で
default OFF にされ、user 期待と乖離した」事象を発見。順位 147 file_length lint
(default ON 固定) と順位 177 file_size_check (default OFF) の asymmetry が
標準パターンの over-application を示した。

主な変更:

§ 1 (Config opt-in) の改訂
- 「適用対象を明示」する section に再構成
- 「behavior の妥当性が不確定な experimental feature」と適用範囲を限定
- 「採否判定 (採用 / 却下 / 継続) のフェーズが必要なもの」を判定基準として追加

§ 1.b 新設 (mechanical lint default ON 許容)
- 4 条件 (non-blocking / 決定論 / scope 限定 / recovery hint 明確) すべて満たす機能を
  § 1 対象外として default ON 配布を許容
- 該当する実装例: 順位 147 file_length lint / 順位 177 file_size_check
- 該当しない例: post-merge-feedback (ADR-014/030) / weekly-review (ADR-031) /
  local-llm-finding-classification (ADR-038)
- PR #197 順位 177 の誤適用を本 PR (PR #203 由来) で訂正と明記

§ 新規 feature 追加時 checklist (4 点 → 5 点に拡張)
- § 0 「上位判定」を最初に追加: 「そもそも § 1 適用対象か?」
- § 1.b 4 条件すべて満たす → default ON で配布、4 点 checklist は skip
- 1 つでも欠ける → 従来通り 4 点 mechanical checklist 実施
- 判断に迷う場合は conservative default (default OFF) を選択
- 本判定を skip して機械適用すると order-application 発生 (PR #197 で実観測)

由来: PR #203 post-merge-feedback で発見された systemic 問題への対応。
派生プロジェクトへの自動波及はなし (本 ADR は本リポジトリ専用、`~/.claude/rules/`
配下ではないため)。

* docs(adr-007): Layer 0.5 file_size_check 追記を削除

順位 177 file_size_check は ADR-007 で扱う「正規表現層 / AST 層」の判断フロー対象外で
あり、metadata-only check (`std::fs::metadata.len()`) という性質上、独立した Layer
区分を設ける積極的理由がない。「Layer 0.5」概念を ADR に codify することで:

- 後続の metadata-only check 追加時に Layer 0.5 への配置判断を毎回迫る
- ADR-007 本体の Q1/Q2/Q3 判断フロー (regex / AST) との整合性が複雑化
- ADR-039 opt-in pattern 言及が「導入リスク」未定義のまま記載されている

という systemic な over-abstraction の温床になっていた。本 PR で「順位 177 は単純な
custom linter の一つとして扱う」方針 (ユーザー判断、2026-06-12) に従い、Layer 0.5
追記を削除する。今後 file_size_check 系の linter を追加する場合は ADR-007 の通常
判断フローに従い、必要なら都度 ADR 改訂で対応する。

* fix(hooks-config): file_size_check を default ON 化 + ADR 参照表記修正

ADR-039 § 1.b (mechanical lint 例外、本 PR で同時 codify) に従い、順位 177
file_size_check を default ON で配布する。順位 147 file_length lint と同 pattern。

主な変更:

enabled = false → enabled = true
- 4 条件 (non-blocking / 決定論的閾値 / scope 限定 / recovery hint 明確) すべて満たすため
- additionalContext warning のみで block しない (failure mode が無害)
- paths glob で scope 宣言的に限定
- todo*.md / Rust source に明示的 recovery hint

コメント修正:
- "ADR-039 § 3 opt-in pattern" → "ADR-039 § 1.b mechanical lint 例外"
  (§ 3 は bounded lifetime、opt-in は § 1。元コメントは誤参照)
- "Layer 0.5" → "custom linter" (ADR-007 Layer 0.5 追記削除に追従)
- 4 条件 (1.b 適用根拠) を明示
- 順位 147 file_length lint を同類例として cite
- bounded lifetime dogfood の記述を削除 (mechanical lint は dogfood phase 不要)

影響:
- 既存 grandfather (>50KB 既存ファイル) は touch されるまで warning なし
- 触られた >50KB ファイル (例: docs/todo10.md) は次の Edit/Write で warning が出る
- 本 PR で todo10.md の split (Commit 5) を同時実施し、初回 dogfood も完了させる

* docs(todo): todo10.md を分割して file_size_check 50KB threshold 内に収める

本 PR で順位 177 file_size_check を default ON 化したことにより、touched で 50KB
超のファイル (= 本 PR 着手時の docs/todo10.md = 57KB) に warning が出る状態になった。
本 commit で todo10.md から PR #185 〜 PR #196 era の 8 エントリを新規 docs/todo12.md
に分離し、todo10.md を 27KB まで縮小して threshold 内に収める。同時に hook の dogfood
としても機能 (順位 177 が想定する recovery flow = 新 todo<N+1>.md 新設 + entry 移管
が実際に機能することを実観測)。

主な変更:

docs/todo12.md (新規 158 行)
- 順位 176 (PR #185 T2-#4): check-ci-coderabbit format variant fixture 追加
- 順位 178 (週次レビュー S02): state.rs behavioral invariant test
- 順位 179 (週次レビュー S03): rate-limit retry decision boundary test
- 順位 180 (週次レビュー C01): lib-report-formatter markdown pipe escape
- 順位 181 (Phase D D-A): aggregate-weekly findings.json raw JSON
- 順位 182 (Phase D D-B): /weekly-review skill 重複検出 (簡易 grep)
- 順位 193 (PR #196 T2-1): Companion helper group 署名整合 compile-time test
- 順位 194 (PR #196 T3-5): development-workflow.md grep step 追記
- 専用ファイル (新規追加先ではない)、todo11.md と同 role

docs/todo10.md (-377 行、57KB → 27KB)
- 上記 8 エントリを削除
- preamble に todo12.md 分離の経緯を記述
- 新セッション確認対象を「12 file」→「13 file」に更新

docs/todo-summary.md
- preamble に todo12.md の説明を追記
- 8 行の「ファイル」列を todo10.md → todo12.md に変更 (sed 一括置換)

由来: 本 PR (PR #204) の hooks-config.toml 変更 (commit 4) で file_size_check
default ON 化に伴う初回 dogfood。順位 177 設計の recovery flow が機能した実証
データとなる。

技術メモ: sed -i '13,390d' で 8 entries 削除、Edit tool で 380 行の old_string
構築は実用的でないため Bash 経路を選択 (ユーザーの「適切な粒度」要件と整合、
独立 commit に集約)。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant