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
4 changes: 2 additions & 2 deletions docs/adr/adr-018-pr-monitor-takt-migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,8 +182,8 @@ PR #120 (cli-finding-classifier 統合) の dogfood で観測された **「rate

| Pattern | Detection 基準 | Auto-retry 状態 | Notes |
|---|---|---|---|
| **Rate limit (待機型)** | `Rate limit exceeded` + 未来 reset_time | ✅ 実装済 (`RateLimitOutcome::Parked`) | reset まで `until_unix_secs` で park、再投稿不要 |
| **Rate limit (即時型)** | `Rate limit exceeded` + 過去 reset_time | ✅ 実装済 (`RateLimitOutcome::Posted` + 順位 80 fix) | `@coderabbitai review` 投稿後、`review_recheck_wait_secs` で park (順位 80 fix で導入、silent exit 防止) |
| **Rate limit (待機型)** | `RATE_LIMIT_MARKERS` のいずれか + 未来 reset_time | ✅ 実装済 (`RateLimitOutcome::Parked`、順位 167-169 で multi-variant 対応) | reset まで `until_unix_secs` で park、再投稿不要。format detection 詳細は ADR-034 § 既知 CR rate-limit format 一覧 参照 |
| **Rate limit (即時型)** | `RATE_LIMIT_MARKERS` のいずれか + 過去 reset_time | ✅ 実装済 (`RateLimitOutcome::Posted` + 順位 80 fix) | `@coderabbitai review` 投稿後、`review_recheck_wait_secs` で park (順位 80 fix で導入、silent exit 防止)。format detection 詳細は ADR-034 § 既知 CR rate-limit format 一覧 参照 |
| **CR 投稿エラー** (`Failed to post review comments`) | walkthrough overlay の error message | ⏳ 未実装 (順位 81、1 観測のみ低頻度) | 頻度が確認できるまで実装を defer (memory: `feedback_no_unenforced_rules` 系の判断) |
| **Wakeup 未予約 fallback** | rate-limit 検出ありで polling 終端時に next_wakeup_at_unix 未設定 | ⏳ 将来候補 | 順位 80 fix で `Posted` 経路は塞がれた、他経路の同型 silent exit が再観測されたら追加 |

Expand Down
28 changes: 25 additions & 3 deletions docs/adr/adr-034-coderabbit-auto-monitoring.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,35 @@ PR #99 セッションで以下の運用痛が観測された:

既存の `state.rate_limit` 検出は review state ベースで、`not_found` 時に rate-limit overlay を見逃す gap がある。

**改善**: walkthrough comment (PR の最初の CR comment) の `body` + `updated_at` を直接 polling し、`Rate limit exceeded` パターンを regex 検出する
**改善**: walkthrough comment (PR の最初の CR comment) の `body` + `updated_at` を直接 polling し、`RATE_LIMIT_MARKERS` (multi-variant) のいずれかに match するかで rate-limit を判定する (実装: `src/check-ci-coderabbit/src/main.rs` の `is_rate_limit_comment` + `extract_wait_time`、順位 167-169 で multi-variant 対応済)

参照: memory `project_coderabbit_rate_limit_overlay.md` (PR #99 で実証された CR の rate-limit overlay 仕様)
参照: memory `project_coderabbit_rate_limit_overlay.md` (PR #99 で実証された CR の rate-limit overlay 仕様) + 下記 § 既知 CR rate-limit format 一覧

### 既知 CR rate-limit format 一覧 (順位 169 採用、2026-05-29 追加)

CR は format を時間経過で変更するため、本リポジトリの実装は multi-variant marker 配列 + 複数 regex pattern で対応する。発見時期昇順:

| 発見時期 | body marker | wait time regex |
|---|---|---|
| ~2026 年初頃 (旧 format) | `Rate limit exceeded` (本文先頭、heading なし) | `Please wait \*?\*?(\d+) minutes? and (\d+) seconds?` / 短縮形 `Please wait \*?\*?(\d+) minutes?` |
| 2026-05 観測 (新 format) | `rate limited by coderabbit.ai` (HTML コメント `<!-- ... -->` 内、`## Review limit reached` heading 併設) | `More reviews will be available in (\d+) minutes? and (\d+) seconds?` / 短縮形 `More reviews will be available in (\d+) minutes?` |

**HTML マーカー優先**: walkthrough body の HTML コメント (`<!-- ... rate limited by coderabbit.ai -->`) は heading 文言や本文より stable な可能性が高いため、新 format 検出の優先 source とする (CR 側で UI 文言は変えても internal marker は維持する傾向、本リポジトリ未検証だが post-merge-feedback で再評価)。

### 検出 logic 更新手順 (CR が format を変更した場合)

format drift で `is_rate_limit_comment` が常時 false を返す symptom (= 30+ 分 polling 継続、`RateLimitOutcome::Parked` 経路に乗らない、PR #182 で実観測) を発見した場合の標準対応:

1. **観測**: 該当 PR で `gh api repos/.../issues/<N>/comments --jq '.[] | select(.user.login == "coderabbitai[bot]")' | head` で walkthrough body を確認
2. **grep**: 新 marker 候補 (HTML コメント / heading / 本文文言) を特定
3. **marker 配列 append**: `src/check-ci-coderabbit/src/main.rs` の `RATE_LIMIT_MARKERS` 配列に新 marker を追加
4. **regex 追加**: 同 main.rs の `extract_wait_time` から呼ばれる `extract_<format-name>_format_wait_time` ヘルパーを新規追加 (旧 + 新の共存パターン)
5. **fixture 追加**: 同 `#[cfg(test)]` mod に新 format fixture を 2-3 variant 追加 (順位 168 と同 pattern)。既存 fixture は backward compat のため維持
6. **ADR-034 update**: 本 ADR の § 既知 CR rate-limit format 一覧 table に新 format 行を append

### auto-trigger 投稿

- body 内 `Please wait N minutes and M seconds` を regex 抽出
- body 内 wait time 表現を regex 抽出 (`extract_wait_time` が multi-variant 自動判定)
- `updated_at` + N min M s = 解除予定時刻
- 解除 + **1 分** の安全マージン後 `gh api -X POST issues/N/comments -f body='@coderabbitai review' > /dev/null 2>&1` を投稿
- 1 分マージンは PR #99 セッション末で実証済 (本セッション内手動再現で確認)
Expand Down
1 change: 1 addition & 0 deletions docs/todo-summary.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
| 171 | 💎 Tier 3 | **`docs-governance.md` に「Operational reference vs Pointer reference」区別 section を追加 (PR #183 T3-#2 採用) ★ Bundle DG-RULES** | todo9.md | S | 順位 172 と同 PR 推奨、PR #183 A01 修正で実適用した判定ロジック (operational = workflow 動作記述 = 保持可 / pointer = section 名・順位番号参照 = 置換必要) を `~/.claude/rules/common/docs-governance.md` § Cross-File Reference Lifecycle に新 sub-section として codify、ADR-031 lines 79-302 中 line 270 のみが真の pointer だった実例を inline cite、派生プロジェクトへ自動波及、`feedback_global_config_backup` 適用必須 |
| 172 | 💎 Tier 3 | **CR ephemeral artifact Nitpick の統一 skip 基準を memory に codify (PR #183 T3-#3 採用) ★ Bundle DG-RULES** | todo9.md | XS | 順位 171 と同 PR 推奨、CR が `docs/todo*.md` 系 ephemeral artifact 内の行番号参照を Nitpick 指摘した場合は skip 推奨という判断基準を新 memory `feedback_coderabbit_ephemeral_nitpick.md` に codify、既存 memory `feedback_coderabbit_no_actionable_merge_signal` の補完、本リポジトリ専用 (派生プロジェクトには波及しない)、`feedback_global_config_backup` 適用推奨 |
| 173 | 🔧 Tier 2 | **`combine_output` 5 crate 重複を `lib-runner-utils` (or 既存 lib-*) に extract (PR #182 dry-run S01 採用)** | todo9.md | S-M | なし (`src/cli-pr-monitor/src/runner.rs:80-89` の `combine_output` 8 行関数が `#[allow(dead_code)]` 付与で生産未使用、同関数が 4 他 crate (cli-push-runner, cli-push-pipeline, cli-merge-pipeline, hooks-post-tool-linter) にも複製 = 5 crate 横断 systemic duplication、ADR-026 Cargo workspace + ADR-012 lib-* naming で解決、Phase B dogfood の最初の実体ベース finding (A01 と並ぶ)、A01 は PR #183 で fix 済) |
| 174 | 💎 Tier 3 | **ADR-039 experimental feature lifecycle checklist 拡張 — 新規 feature 追加時の 4 点整合確認 (PR #184 T3-#2 採用)** | todo9.md | S | なし (PR #184 CR Major M-2 (`weekly_review_reminder` の `enabled = true` が ADR-039 opt-in 違反) の self-review 捕捉化、`docs/adr/adr-039-experimental-feature-standard-pattern.md` に「新規 feature 追加時の self-review checklist」section を追加、config schema ↔ feature flag default OFF ↔ docs / config example ↔ test coverage の 4 点 mechanical 確認、PR #184 fix 前後を OK/NG 実例として inline cite、T3-1 不採用根拠 (analyzer rubric は推奨で user 判断と完全一致は構造的に困難) との対比で discretionary 部分なしの mechanical checklist のため採用条件成立) |

**戦略**: Tier 1 を 2〜3 セッションで片付け → Tier 2 で ADR-032 の前提 + rate-limit + convergence cost 削減を進める → Tier 3 で ADR-032 を land + ドキュメント整備。Tier 4-5 は cleanup / 外部展開で daily efficiency への直接効果は小さい。

Expand Down
45 changes: 45 additions & 0 deletions docs/todo9.md
Original file line number Diff line number Diff line change
Expand Up @@ -1118,6 +1118,51 @@ extract 先の crate 選定: 既存 lib-* に追加するか新規 `lib-runner-u

---

### ADR-039 experimental feature lifecycle checklist 拡張 — 新規 feature 追加時 4 点整合確認 (PR #184 T3-#2 採用)

> **動機**: PR #184 で CR Major M-2 (`weekly_review_reminder` の `enabled = true` が ADR-039 opt-in 契約に違反) が CR re-review で検出された。本 finding は self-review の段階で捕捉可能だったはずだが、ADR-039 は「3 点原則 (config opt-in + kill-switch + bounded lifetime)」を記述しているのみで、**新規 feature 追加時の具体的確認手順 (checklist)** が未整備だった。本タスクは ADR-039 に「新規 experimental feature 追加時の self-review checklist」を追加し、**config schema ↔ feature flag ↔ docs ↔ test の 4 点整合** を mechanical に確認できる手順を codify する。
>
> **本タスクの位置づけ**: PR #184 post-merge-feedback Tier 3 #2 採用 (Severity Medium / Frequency Low / Effort S / Adoption Risk None、2026-05-29 ユーザー承認)。**T3-1 不採用根拠との対比** (2026-05-29 ユーザー判断): 「analyzer rubric は推奨でしかなく user 判断と完全一致は構造的に困難」のため、discretionary 部分を含む rule は不採用、本 T3-2 は mechanical な 4 点整合 checklist のため採用条件成立。
>
> **参照**: `.claude/feedback-reports/184.md` Tier 3 #2、`docs/adr/adr-039-experimental-feature-standard-pattern.md` (既存 3 点原則、checklist 追加対象)、PR #184 CR M-2 thread (id 3323337089) の実例、本 PR で実施した M-2 fix commit (= `enabled = false` 化)

#### 設計決定 (案)

ADR-039 に新 section「新規 experimental feature 追加時の self-review checklist」を追加。以下 4 点の整合を確認する:

- **1. config schema**: 該当 hook / module の config struct (例: `WeeklyReviewReminderConfig`) で `enabled: Option<bool>` を持つ
- **2. feature flag default**: 該当 config の `enabled` field の default が **OFF** (= `unwrap_or(false)`) になっている。`unwrap_or(true)` は ADR-039 違反 (PR #184 M-2 の症状)
- **3. docs / config example**: `.claude/hooks-config.toml` 等の repo config example で `enabled = false` を明示しつつ、enable した場合の挙動説明をコメントで添える (= opt-in 運用の guidance)
- **4. test coverage**: `enabled = false` (= disabled state) の test case が含まれ、feature が完全 skip されることを assert する (= kill-switch が機能することの regression gate)

実例の cite:

- **OK** (PR #184 fix 後の `weekly_review_reminder`): `WeeklyReviewReminderConfig::enabled` Option + `unwrap_or(false)` + `.claude/hooks-config.toml` で `enabled = false` + `compute_weekly_review_reminder_nudge_returns_none_when_disabled` test
- **NG** (PR #184 fix 前): `enabled = true` で 4 点整合の (2) と (3) が違反、CR Major で検出
- **既存 grandfathered case** (`[session_start.staleness]`): pre-existing で `enabled = true` だが本 PR scope 外、別 PR で cleanup 候補

#### 作業計画

- [ ] `docs/adr/adr-039-experimental-feature-standard-pattern.md` に新 section「新規 experimental feature 追加時の self-review checklist」を追加 (~15-20 行)
- [ ] 4 点整合の確認手順を箇条書きで明文化
- [ ] OK / NG 実例を inline cite (PR #184 M-2 fix の前後)
- [ ] 既存 grandfathered case の扱いを補足記述 (本 ADR は新規 feature 追加時の checklist であり、既存 feature の retro-cleanup は別判断)
- [ ] markdownlint clean 確認
- [ ] 本エントリ削除 + todo-summary.md 行削除

#### 完了基準

- ADR-039 に新 section が codify される
- 将来の新規 experimental feature 実装時、self-review で 4 点整合を mechanical に確認可能
- PR #184 M-2 と同型の config opt-in 違反が self-review で捕捉可能になる
- markdownlint clean

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

なし。Effort S、ADR の section 追加のみで副作用最小。

---

## 既知課題 (記録のみ、本セッションで未対応)

(現時点で本ファイルへの既知課題は無し。docs/todo8.md 末尾の post-merge-feedback workflow stale marker 問題を参照。)
92 changes: 79 additions & 13 deletions src/check-ci-coderabbit/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -248,12 +248,22 @@ struct GhPullComment {

// ─── パース関数 (テスト可能な純粋関数) ───

const RATE_LIMIT_MARKER: &str = "Rate limit exceeded";
/// CodeRabbit rate-limit body markers。complete list of known formats.
///
/// CR は format を時間経過で変更するため multi-variant 配列で対応する
/// (PR #182/#184 で silent regression を実体観測)。新 format 検出時は本配列に
/// append + extract_wait_time() に対応する regex を追加することで対応する。
/// 詳細な進化と更新手順は ADR-034 § CR rate-limit format evolution 参照。
///
/// 既知 format (発見時期昇順):
/// - "Rate limit exceeded" (旧 format、~2026 年初頃まで)
/// - "rate limited by coderabbit.ai" (新 format、HTML マーカー、2026-05 観測)
const RATE_LIMIT_MARKERS: &[&str] = &["Rate limit exceeded", "rate limited by coderabbit.ai"];

fn is_rate_limit_comment(c: &GhComment) -> bool {
c.body
.as_deref()
.map(|b| b.contains(RATE_LIMIT_MARKER))
.map(|b| RATE_LIMIT_MARKERS.iter().any(|m| b.contains(m)))
.unwrap_or(false)
}

Expand Down Expand Up @@ -400,8 +410,8 @@ fn rate_limit_event_time(c: &GhComment) -> Option<&str> {
///
/// 検出条件:
/// - 投稿者が `coderabbitai[bot]`
/// - body に `Rate limit exceeded` を含む
/// - body に `Please wait <N> minutes? and <M> seconds?` 表現を含む
/// - body に `RATE_LIMIT_MARKERS` のいずれかを含む (旧: `Rate limit exceeded` / 新: `rate limited by coderabbit.ai`)
/// - body に wait time 表現を含む (旧: `Please wait <N> minutes? and <M> seconds?` / 新: `More reviews will be available in <N> minutes? and <M> seconds?`)
/// - event_time (= updated_at fallback created_at) が `push_time` 以降
///
/// `push_time` フィルタは過去セッションの古い rate-limit comment が現セッションで
Expand Down Expand Up @@ -454,29 +464,45 @@ fn parse_rate_limit(json: &str, push_time: &str) -> Option<RateLimitInfo> {
})
}

/// `Please wait **N minutes? and M seconds?**` から (minutes, seconds) を抽出
/// 旧 format (`Please wait **N minutes? and M seconds?**` / 短縮形 `Please wait **N minutes?**`) を抽出
///
/// CodeRabbit の rate-limit メッセージは複数のフォーマット variant がある:
/// 受理する具体例:
/// - `Please wait **5 minutes and 13 seconds**`
/// - `Please wait 1 minute and 7 seconds`
/// - `Please wait **30 minutes**` (seconds 省略)
fn extract_wait_time(body: &str) -> Option<(u64, u64)> {
// 標準形: "Please wait **N minutes? and M seconds?**"
fn extract_old_format_wait_time(body: &str) -> Option<(u64, u64)> {
let re_full = regex::Regex::new(r"Please wait \*?\*?(\d+) minutes? and (\d+) seconds?").ok()?;
if let Some(caps) = re_full.captures(body) {
let m: u64 = caps.get(1)?.as_str().parse().ok()?;
let s: u64 = caps.get(2)?.as_str().parse().ok()?;
return Some((m, s));
}

// フォーマット2: "Please wait **N minutes?**" (seconds 省略)
let re_min = regex::Regex::new(r"Please wait \*?\*?(\d+) minutes?").ok()?;
if let Some(caps) = re_min.captures(body) {
let caps = re_min.captures(body)?;
let m: u64 = caps.get(1)?.as_str().parse().ok()?;
Some((m, 0))
}

/// 新 format (`More reviews will be available in N minutes? and M seconds?` /
/// 短縮形 `More reviews will be available in N minutes?`) を抽出。
/// PR #182/#184 で実観測した CR 新フォーマット。
fn extract_new_format_wait_time(body: &str) -> Option<(u64, u64)> {
let re_full =
regex::Regex::new(r"More reviews will be available in (\d+) minutes? and (\d+) seconds?")
.ok()?;
if let Some(caps) = re_full.captures(body) {
let m: u64 = caps.get(1)?.as_str().parse().ok()?;
return Some((m, 0));
let s: u64 = caps.get(2)?.as_str().parse().ok()?;
return Some((m, s));
}
let re_min = regex::Regex::new(r"More reviews will be available in (\d+) minutes?").ok()?;
let caps = re_min.captures(body)?;
let m: u64 = caps.get(1)?.as_str().parse().ok()?;
Some((m, 0))
}

None
fn extract_wait_time(body: &str) -> Option<(u64, u64)> {
extract_old_format_wait_time(body).or_else(|| extract_new_format_wait_time(body))
}

/// ISO 8601 (`YYYY-MM-DDTHH:MM:SSZ` 形式) を unix epoch 秒に変換
Expand Down Expand Up @@ -2113,4 +2139,44 @@ mod tests {
assert_eq!(item["summary"], "signature mismatch");
assert_eq!(item["url"], "https://github.com/o/r/pull/1#r1");
}

#[test]
fn rate_limit_detected_from_new_format_with_html_marker_and_full_wait_time() {
let json = r#"[{
"user": {"login": "coderabbitai[bot]"},
"body": "<!-- This is an auto-generated comment: summarize by coderabbit.ai -->\n<!-- This is an auto-generated comment: rate limited by coderabbit.ai -->\n\n> [!WARNING]\n> ## Review limit reached\n> \n> More reviews will be available in 36 minutes and 52 seconds. [Learn more](https://docs.coderabbit.ai/management/plans).",
"created_at": "2026-05-29T08:16:12Z"
}]"#;
let result = parse_rate_limit(json, "2026-05-29T00:00:00Z")
.expect("new format with HTML marker + full wait time must be detected");
assert_eq!(result.wait_minutes, 36);
assert_eq!(result.wait_seconds, 52);
let base = parse_iso8601_to_unix("2026-05-29T08:16:12Z").unwrap();
assert_eq!(result.until_unix_secs, base + 36 * 60 + 52 + 60);
}

#[test]
fn rate_limit_detected_from_new_format_with_minutes_only() {
let json = r#"[{
"user": {"login": "coderabbitai[bot]"},
"body": "<!-- This is an auto-generated comment: rate limited by coderabbit.ai -->\n\nMore reviews will be available in 30 minutes.",
"created_at": "2026-05-29T08:00:00Z"
}]"#;
let result = parse_rate_limit(json, "2026-05-29T00:00:00Z")
.expect("new format minutes-only variant must be detected");
assert_eq!(result.wait_minutes, 30);
assert_eq!(result.wait_seconds, 0);
}

#[test]
fn rate_limit_picks_latest_when_mixed_old_and_new_formats() {
let json = r#"[
{"user": {"login": "coderabbitai[bot]"}, "body": "Rate limit exceeded\nPlease wait 5 minutes and 0 seconds", "created_at": "2026-05-28T00:00:00Z"},
{"user": {"login": "coderabbitai[bot]"}, "body": "<!-- rate limited by coderabbit.ai -->\nMore reviews will be available in 15 minutes and 30 seconds.", "created_at": "2026-05-29T00:00:00Z"}
]"#;
let result = parse_rate_limit(json, "2026-05-28T00:00:00Z")
.expect("mixed old/new formats must resolve to newest comment");
assert_eq!(result.wait_minutes, 15);
assert_eq!(result.wait_seconds, 30);
}
}