diff --git a/.claude/skills/review-gate/SKILL.md b/.claude/skills/review-gate/SKILL.md index 247b0cc7..77ea4ad6 100644 --- a/.claude/skills/review-gate/SKILL.md +++ b/.claude/skills/review-gate/SKILL.md @@ -11,7 +11,7 @@ user-invocable: false ## 1. 基本レビュー(CI 待ち・組み込みのレビュー・決定論ゲート実走つき) -**全体のレビューと差分のレビュー(台帳・I197)**: 1 回目はブランチ全体を見て、指摘を台帳(番号・重大度・状態)にする。やり直し(fix-loop の後)は、前の記録の `REVIEWED_COMMIT` からの直した差分と、台帳の止める指摘が直ったかだけを見る(全体を探し直さない)。合否はスクリプトが台帳の FINDING 行の欄(FILE・LINE・STOP・再現)から決定論で出す(I205。code-reviewer の VERDICT 行は合否に使わない): アプリ本体はブランチで変えた行(と前後 3 行)または受け入れ条件の未達の、再現のある Blocker・High・Medium で止める。ハーネスは止める条件の一覧(流れが動かなくなる・秘密情報や個人情報が外に出る・develop/main や GitHub から戻せない設定を壊す・受け入れ条件の未達)に当たる、再現のある指摘だけで止める。GitHub から復旧できない例外(`path_risk_trigger()` の集合)は Blocker・High で止める。差分の外でも、セキュリティの Blocker は止める。止めない指摘は台帳に残り、retro が後続のイシューか軽い指摘の一覧へ回す(正は `docs/runbooks/review-rules.md`「検証の全体設計」)。どちらにするか・範囲・台帳はスクリプト(`scripts/claude/code-review.sh`)が決める(台帳のある最新の記録の `REVIEWED_COMMIT` が HEAD の祖先なら差分のレビュー)。高リスクかどうか(3. を走らせるか)は、計画レビューの判定・パスの判定・台帳の目印(code-reviewer が計画の見立てを超えた高リスクの変更に付ける `RISK=up`・I202)で決まる。一度 YES になったら戻らない。 +**全体のレビューと差分のレビュー(台帳・I197)**: 1 回目はブランチ全体を見て、指摘を台帳(番号・重大度・状態)にする。やり直し(fix-loop の後)は、見終えた最新の記録(`REVIEW_COMPLETE: YES`)の `REVIEWED_COMMIT` からの差分と、台帳の止める指摘が直ったかだけを見る(全体を探し直さない。見終えていない回〔組み込みの `/security-review` が動かなかった・レビュー役の出力が壊れた〕の後は、その前の見終えた記録〔無ければブランチ全体〕から見直す・I212)。合否はスクリプトが台帳の FINDING 行の欄(FILE・LINE・STOP・再現)から決定論で出す(I205。code-reviewer の VERDICT 行は合否に使わない): アプリ本体はブランチで変えた行(と前後 3 行)または受け入れ条件の未達の、再現のある Blocker・High・Medium で止める。ハーネスは止める条件の一覧(流れが動かなくなる・秘密情報や個人情報が外に出る・develop/main や GitHub から戻せない設定を壊す・受け入れ条件の未達)に当たる、再現のある指摘だけで止める。GitHub から復旧できない例外(`path_risk_trigger()` の集合)は Blocker・High で止める。差分の外でも、セキュリティの Blocker は止める。止めない指摘は台帳に残り、retro が後続のイシューか軽い指摘の一覧へ回す(正は `docs/runbooks/review-rules.md`「検証の全体設計」)。どちらにするか・範囲・台帳はスクリプト(`scripts/claude/code-review.sh`)が決める(台帳のある最新の記録の `REVIEWED_COMMIT` が HEAD の祖先なら差分のレビュー)。高リスクかどうか(3. を走らせるか)は、計画レビューの判定・パスの判定・台帳の目印(code-reviewer が計画の見立てを超えた高リスクの変更に付ける `RISK=up`・I202)で決まる。一度 YES になったら戻らない。 まず、ここまで手元でコミットだけしてきたもの(implement・fix-loop・記録)をまとめて送る(`docs/runbooks/workflow.md`「GitHub への書き込みの時点」の 2。CI は送ったコミットで動き、スクリプトは「PR の最新のコミット=手元の HEAD」を確かめる): @@ -33,7 +33,7 @@ bash scripts/claude/code-review.sh "$ARGUMENTS" - exit 1 で出力に `CI_WAIT: NOT_PUSHED` がある(PR の最新のコミットが手元の HEAD と違う。上の送り忘れ・送った後のコミット): 上の送る手順をやり直してから 1 回だけ実行し直す。それでも同じなら止める(止める条件 3) - exit 1 で出力に `CI_WAIT: NO_JOBS` がある(ワークフローのジョブ名を読めない。python3 の PyYAML が無いなど): 原因を直して 1 回だけ実行し直す。直せなければ止める(止める条件 3) - exit 1 で出力に `CI_WAIT: NO_PR` がある(PR を調べられない・開いている PR が無い): 原因(gh の認証・ネットワーク・PR が閉じている)を直して 1 回だけ実行し直す。直せなければ止める(止める条件 3)。CI を確かめないまま先へ進まない -- 出力に `OFFICIAL_SECURITY: FAILED` がある(高リスクの変更で組み込みの `/security-review` が動かず、HIGH になった): 直すものが無い基盤の失敗なので fix-loop に回さず、1 回だけ実行し直す。2 回続けば止める(止める条件 3) +- 出力に `OFFICIAL_SECURITY: FAILED` がある(高リスクの変更で組み込みの `/security-review` が動かず、HIGH になった): 直すものが無い基盤の失敗なので fix-loop に回さず、1 回だけ実行し直す。2 回続けば止める(止める条件 3)。やり直しは見終えた記録から差分を取り直すので、動かなかった回が見るはずだった差分を `/security-review` が見る。「動いた」の判定は、`/security-review` の出力の、行全体が `SECURITY_FINDINGS: <数>` の行だけで行う(I212) - それ以外の exit 1(fetch の失敗・イシューファイルが無いなど): 出力を記録に書き、止める条件 3 に当たるかを判定する(直せる原因なら直して実行し直す) ## 2. 出力の機械判定 diff --git a/docs/issues/open/I212.md b/docs/issues/open/I212.md new file mode 100644 index 00000000..5a9a24c7 --- /dev/null +++ b/docs/issues/open/I212.md @@ -0,0 +1,91 @@ +# I212: 差分のレビューが、組み込みの /security-review が動かなかった回の記録を起点にして、セキュリティレビューを飛ばして OK を出すのを止める + +## 関連資料 +- GitHub Issue: #419 +- Draft PR: #421 +- 旧 GitHub Issue: #358(停止中の bot `osushi0404-bot` が起票したため GitHub で見えない。2026-11-07 に GitHub App の名義で起票し直した。bot の停止が解けたら #358 は「起票し直した先へ移した」と書いて閉じる) +- 起点: I211(#356・PR #357)の review-gate 2 周目(retro からの起票)。**次のハーネスの作業で最優先**(判定の仕組み `scripts/claude/code-review.sh` に関わる穴) +- 記録: `docs/reviews/I211_code_review_20261029_0155.md`(`OFFICIAL_SECURITY: FAILED`・`FINAL_VERDICT: HIGH`)→ `docs/reviews/I211_code_review_20261029_0204.md`(範囲のファイル 0 本・`FINAL_VERDICT: OK`)。I211 の自律判断の記録(`docs/reviews/closed/I211_review.md` の「[review-gate 2]」) +- 差分の起点の選び方: `scripts/claude/code-review.sh:482-506`(`ledger_records`・`latest_ledger_record`・`review_mode`) +- 手順: `.claude/skills/review-gate/SKILL.md`(「出力に `OFFICIAL_SECURITY: FAILED` がある: …1 回だけ実行し直す」) +- 正: `docs/runbooks/review-rules.md`「検証の全体設計」 + +## 同梱: I213 の retro から(2026-10-29・GitHub のコメント https://github.com/osushi0404-dev/study-app-multitenant/issues/358#issuecomment-6485976047 と同じ内容) +I213(#359)の review-gate でこのイシューの穴が実際に起き、近い種類の穴が 2 つ見つかった(記録: `docs/reviews/closed/I213_review.md` の「自律判断の記録」の review-gate の行)。3 つとも「差分だけのやり直しが、前の回の確かめの結果を正しく扱わない」穴。計画で、まとめて扱うか、3 を別イシューに切り出すかを決める。 +1. **このイシューの穴の実例**: `docs/reviews/closed/I213_code_review_20261029_1009.md` が `OFFICIAL_SECURITY: FAILED` で HIGH → 1 回だけやり直した `..._1020.md` は「差分のレビュー・範囲のファイル 0 本」で、組み込みの /security-review を動かさずに OK。I213 では 1 回目の /security-review の本文(ブランチ全体を見て指摘なし)を根拠に進めた +2. **「指摘なし」の取り違え**: 1 回目の /security-review は実際には動き、「報告対象の脆弱性はありませんでした」と日本語の文章で書いていた。`scripts/claude/code-review.sh` の `official_security_verdict()` は、指摘の配列か決まった「指摘なし」の形しか読まないため「読めていない」と判定した。根本原因は、判定が組み込みのレビューの文章の形に頼っていること。予防策の案: 組み込みのレビューへの依頼に「最後の行に機械判定の 1 行(例: `SECURITY_FINDINGS: 0`)を必ず出す」を足し、判定はその行だけで行う +3. **敵対的レビューの差分の周で、中身の変わらない「実験必須」の項目が未検証になる**: `docs/reviews/closed/I213_adversarial_review_20261029_1032.md` で実験により確認済みだった V2〜V4 が、文言だけの差分を見た `..._1044.md` で実験されず「未検証」→ HIGH → 上限到達でユーザーの延長の判断が要った(中身のファイルはその間に 1 行も変わっていない)。根本原因は、前の周の確認結果を、関係するファイルが変わっていなければ引き継ぐ仕組みが無いこと。予防策の案: `scripts/claude/adversarial-review.sh` で、前の周に「確認済み(実験)」だった項目は、その周の後に記録の場所の外のファイルが変わっていなければ確認済みとして引き継ぎ、引き継いだことを記録に書く(決定論) + +## 同梱: I200 の retro から(2026-10-30・GitHub のコメント https://github.com/osushi0404-dev/study-app-multitenant/issues/358#issuecomment-6500741220 と同じ内容) +4. **2 の再発**: I200 の `docs/reviews/I200_code_review_20261029_2240.md`・`..._2357.md` でも、/security-review は動いて「報告する(できる)脆弱性はありません」と書いたのに `OFFICIAL_SECURITY: FAILED` → やり直しは差分なしで /security-review を動かさず OK(I213 と合わせて 3 回) +5. **敵対的レビューの締め切りで実験が 1 本も走らない**(I206 の確かめる 2 点の②): `docs/reviews/I200_adversarial_review_20261029_2318.md` で 3 観点とも 920〜990 秒かかり、実験の案を出した時点で締め切り。「実行していない(締め切り)」2 本・実験必須の項目がすべて未検証で HIGH。根本原因は締め切りがレビュー役と実験役で共有されていること。予防策の案(実験の時間を別に確保する・実験必須の項目は計画で決定論の TC を必須にする 等)は計画で決める。対象: `scripts/claude/adversarial-review.sh`・`.claude/agents/adversarial-reviewer.md`・`docs/runbooks/review-rules.md` + +## 同梱: I226 の retro から(2026-11-06・**次のハーネスの作業で最優先**の印つき。#358 は bot の停止で見えず、GitHub へのコメントは停止が解けてから同じ内容で付ける) +6. **G10 の `\b` の後退(I226 の台帳 F1・Medium・SECURITY=yes・再現あり)**: I226 で `scripts/claude/tests/test_i223_gh_app.sh` の G10 の正規表現を `gh auth (status|token)\b` にした。Python の `\w` は和文も含むので、「gh auth tokenを打つ」のように和文がすぐ続く書き方で `\b` が当たらず、G10 が合格になる(変更前の `gh auth status` は `\b` なしで止めていた)。記録: `docs/reviews/closed/I226_code_review_20261106_2139.md` の F1。根本原因: 語の境目を `\b` に任せ、和文が続く形の FG(失敗の注入)を足さなかった。直し方の案: `\b` を `(?![A-Za-z0-9_-])` などの英数字だけの先読みに替え、`g10_fg "和文が続く auth token は不合格" 1 'gh auth tokenを打つ'` を足す。対象: `scripts/claude/tests/test_i223_gh_app.sh`。このイシューの本題(差分のレビューの起点)とは別の小さな直しなので、計画で同じ PR に入れるか決める + +## 背景/目的 + +**背景**: I211 で python の版を 3.12.3 に揃え直した差分(`.python-version`・`pyproject.toml`・`uv.lock`。CI の設定を含むブランチなので高リスク)を review-gate に通した。1 回目は組み込みの `/security-review` が動かず(基盤の失敗)、手順どおり `FINAL_VERDICT: HIGH` になった。手順どおり 1 回だけやり直したところ、スクリプトは失敗した回の記録の `REVIEWED_COMMIT` を差分の起点にし、「起点の後に記録の場所の外は変わっていない(範囲のファイル 0 本)」と判断して、レビュー役も `/security-review` も呼ばずに台帳を引き継ぎ、`FINAL_VERDICT: OK` を出した。`gate-check.sh` もこの記録で通った。I211 では Claude が気づいて `/security-review` を直接実行して補った(指摘なし)。 +**目的**: レビューが失敗した回(HIGH・BLOCKER・組み込みのレビューが動かなかった回)の後のやり直しで、その回が見るはずだった差分を必ず見直す。セキュリティレビューを一度も通らない変更が test-gate に進む道を無くす。 + +### 現状の問題(証拠・再現手順) +- `review_mode()`(`scripts/claude/code-review.sh:494-506`)は、台帳のある最新の記録(`LEDGER_ROUND:` がある記録)の `REVIEWED_COMMIT` が HEAD の祖先なら差分のレビューにする。記録の `VERDICT`/`FINAL_VERDICT`/`OFFICIAL_SECURITY` は見ていない +- 1 回目の記録(`OFFICIAL_SECURITY: FAILED`・HIGH)にも `LEDGER_ROUND:` と `REVIEWED_COMMIT:` が書かれるため、やり直しの起点になる +- 再現(I211): 高リスクのブランチで、組み込みの `/security-review` が動かない回を作る → 記録が commit される → やり直す → 「差分のレビュー(台帳の 3 周目・範囲のファイル 0 本)」「レビュー役は呼ばずに台帳を引き継ぎます」→ `FINAL_VERDICT: OK` + +### 根本原因 +差分のレビューの起点を「最後にレビューした場所」で選んでいて、「最後に合格した場所」で選んでいない。失敗した回の記録も「見た」ことになり、その回が見るはずだった差分が、やり直しの範囲から外れる。 + +### 解決方針 +- 差分のレビューの起点にするのは、`VERDICT: OK`(台帳の結論が合格)で、高リスクなら組み込みの `/security-review` が動いた(`OFFICIAL_SECURITY: FAILED` が無い)記録だけにする。HIGH・BLOCKER・FAILED の記録は起点にしない(その前の合格した記録から差分を取る。合格した記録が無ければ全体のレビュー) +- 台帳(指摘の番号・状態)の引き継ぎは今までどおり(起点の選び方だけを変える) +- 決定論のテストで固定する(失敗した記録を最新に置いた写しで、差分の範囲が前の合格した記録から取られること。失敗注入: 失敗した記録の後に記録の場所の外の変更が無くても、範囲が 0 本にならない) +- `gate-check.sh` が失敗した回の後の「範囲 0 本・OK」の記録で通らないかも確かめる(必要なら同じ考え方で直す)。詳細は計画で決める + +## 実装対象(既知のもの) +| ファイル | 変更内容 | +|---------|---------| +| `scripts/claude/code-review.sh` | `review_mode()`(と `latest_ledger_record` の使い方)で、差分の起点を合格した記録に限る | +| `scripts/claude/gate-check.sh` | 必要なら、失敗した回の後のやり直しの記録の扱いを揃える(未特定・計画で確認) | +| `scripts/claude/tests/`(既存の code-review.sh のテストへの追加か新規) | 失敗した記録を起点にしないことの決定論のテスト(失敗注入つき) | +| `.claude/skills/review-gate/SKILL.md` | 「`OFFICIAL_SECURITY: FAILED` は 1 回だけやり直す」の手順と、起点の選び方の説明を揃える(必要なら) | +| `scripts/claude/code-review.sh`(同梱 2) | 組み込みのレビューへの依頼に機械判定の 1 行を出させ、`official_security_verdict()` はその行だけで判定する(範囲は計画で決める) | +| `scripts/claude/adversarial-review.sh`・`docs/runbooks/review-rules.md`(同梱 3) | 前の周の「確認済み(実験)」の項目を、関係するファイルが変わっていなければ引き継ぐ(計画で同梱か切り出しかを決める) | + +## スコープ +### 含む +- 差分のレビューの起点の選び方(失敗した記録を起点にしない) +- それを固定する決定論のテスト +- 関係する手順の記述の整合 +### 含まない +- 組み込みの `/security-review` が動かない原因そのもの(基盤側の失敗。再発すれば別途) +- 台帳の振り分け(`--route`)の決まり + +## 関連する品質基準 +- C8 セキュリティ — 高リスクの変更が組み込みのセキュリティレビューを一度も通らずに進む道を無くす + +## 受け入れ条件(Acceptance Criteria) +- [ ] 最新の記録が見終えていない回(組み込みの `/security-review` が動かなかった〔`OFFICIAL_SECURITY: FAILED`〕・レビュー役の出力が壊れていた)のとき、やり直しの差分のレビューは、その前の見終えた記録(無ければ全体)を起点にする(2026-11-08・計画の段階のユーザーの判断「ループエンジニアリング、ハーネスエンジニアリングのベストプラクティスと開発をサクサク進めるという観点で問題ないやり方を選択して」を受けて、「HIGH・BLOCKER の回すべて」から狭めた。台帳に止める指摘が残って HIGH になった回は見終えているので起点にしてよい。理由は plan_I212.md §9) +- [ ] その結果、失敗した回の後に記録の場所の外の変更が無くても、失敗した回が見るはずだった差分をレビュー役と(高リスクなら)組み込みの `/security-review` が見る +- [ ] 上の 2 点を、失敗注入つきの決定論のテストで固定している(失敗した記録を起点にする前の形に戻すとテストが落ちる) +- [ ] 失敗が続いた場合(失敗した記録が 2 つ以上続く)も、遡って最後の合格した記録を起点にする(テストで固定) +- [ ] `gate-check.sh` が、失敗した回の後に「範囲 0 本」で出た OK の記録だけでは通さない(失敗した回が見るはずだった差分を見た記録が要る)ことをテストで固定している(計画で `gate-check.sh` を直す必要が無いと分かったら、その理由を計画書に書き、テストだけ足す) +- [ ] 台帳(指摘の番号・状態)は、起点にした記録ではなく最新の台帳のある記録から引き継ぐ(I206 の番号の合流を壊さない。どの記録から引き継ぐかを計画書に明記する) +- [ ] 見終えたかの判定に使う欄(計画で `REVIEW_COMPLETE:` の行を足すと決めた)と、欄が無い古い記録の扱いを計画書に明記する +- 同梱 3・5(敵対的レビューの「確認済み」の引き継ぎ・締め切りで実験が走らない)は、計画で別イシューに切り出すと決めた(retro で起票。plan_I212.md §9) +- [ ] 合格した記録の後のやり直し(今までの差分のレビュー)の動きは変わらない(既存のテストが通る) +- [ ] 既存のハーネステスト(`bash scripts/claude/tests/run-all.sh`)・CI がすべて pass する + +## 影響範囲(想定) +- Backend: なし +- Frontend: なし +- DB: なし +- Config/Infra: `scripts/claude/code-review.sh`(判定の仕組み)・必要なら `scripts/claude/gate-check.sh`・`.claude/skills/review-gate/SKILL.md`・ハーネステスト。同梱分を入れるなら `scripts/claude/adversarial-review.sh`・`docs/runbooks/review-rules.md` + +## 制約・引き継ぎ情報 +- I211 では、`/security-review` を直接実行して補った(指摘なし)。I211 の PR #357 にはこの穴の影響は残っていない +- 差分のレビュー・台帳の仕組みは I197・I202・I205・I206・I207・I208 で作った。台帳の番号の引き継ぎ(I206)を壊さない +- 判定の仕組みの変更なので、計画レビューで高リスクの扱いになる見込み(未定・計画で確認) + +## Danger Ops(有/無) +- 無 diff --git a/docs/plans/open/plan_I212.md b/docs/plans/open/plan_I212.md new file mode 100644 index 00000000..01aad503 --- /dev/null +++ b/docs/plans/open/plan_I212.md @@ -0,0 +1,281 @@ +# I212 計画書: 差分のレビューの起点を「見終えた最新の記録」にする(失敗した回の後にセキュリティレビューを飛ばさない) + +## 基本情報 +- **計画書ID**: plan_I212 +- **関連イシュー**: #419(旧 #358) +- **Draft PR**: #421 +- **作成根拠資料**: docs/issues/open/I212.md(起点イシュー・同梱 1〜6) +- **実装後評価**: docs/reviews/open/I212_review.md +- **作成日**: 2026-11-08 + +## 1. 背景/目的 +- 高リスクの変更で組み込みの `/security-review` が動かなかった回(`OFFICIAL_SECURITY: FAILED`・HIGH)の後に 1 回だけやり直すと、`code-review.sh` は失敗した回の `REVIEWED_COMMIT` を差分の起点にする。起点の後に記録の場所の外が変わっていなければ「範囲 0 本」でレビュー役も `/security-review` も呼ばずに OK を出す(I211・I213・I200 で計 3 回) +- 目的: やり直しで、失敗した回が見るはずだった差分を必ず見直す。セキュリティレビューを一度も通らない変更が test-gate に進む道を無くす +- 同梱(§9 で採否を決めた): 2・4(`/security-review` の「指摘なし」の読み違い)・6(G10 の `\b` の後退) + +### 調査結果 +- **原因の概要**: 差分のレビューの起点を「最後にレビューした場所」で選んでいて、「最後に最後まで見終えた場所」で選んでいない。見終えていない回も「見た」ことになる +- **詳細**: + - `review_mode()`(`scripts/claude/code-review.sh:497-508`)は、`LEDGER_ROUND:` のある最新の記録(`latest_ledger_record`)の `REVIEWED_COMMIT` が HEAD の祖先なら、その SHA を起点にする。記録の判定の欄は見ていない + - `/security-review` が動かなかった回も、レビュー役(code-reviewer)が正常に終われば `LEDGER_ROUND:` と `REVIEWED_COMMIT:` が書かれるので起点になる(I211 の `docs/reviews/closed/I211_code_review_20261029_0155.md`: `VERDICT: HIGH`・`LEDGER_ROUND: 2`) + - やり直しでは `changed_outside_records "$SINCE" HEAD`(`code-review.sh:988-993`)が「変わっていない」を返し、`NO_DELTA=1` → レビュー役も組み込みのレビューも呼ばず `VERDICT: OK`(`..._0204.md`) +- **記録から「見終えたか」を読めるか**: 記録に残る機械の行は `VERDICT:`(合成した最終の判定)・`RISK:`・`REVIEWED_COMMIT:`・`LEDGER_ROUND:`。`OFFICIAL_SECURITY: FAILED` は標準出力だけで、記録には証跡の文(`- ⛔ 高リスク(パス判定 YES)で組み込みの /security-review が動かなかった…`)しか残らない。`VERDICT: HIGH` は「台帳に止める指摘が残った(見終えている)」でも付くので、`VERDICT` だけでは見終えたかを区別できない → 記録に `REVIEW_COMPLETE: YES|NO` の行を足す(§4-1) +- **同梱 2・4 の原因**: `official_security_verdict()`(`code-review.sh:176-182`)は、`run_official_review()` が付ける「読めていない」の注意(`official_output_readable()` の正規表現に当たらない)で HIGH にする。`/security-review` は日本語の文(「報告する脆弱性はありません。」)で書くことがあり、正規表現に当たらない(I200 の `I200_code_review_20261029_2240.md:129-132` で実例)。主題を直しても、この読み違いが残るとやり直しでも同じ FAILED になり止まる +- **スパイク(2026-11-08・成立)**: このブランチで `GH_APP_READONLY=1 timeout 900 claude -p --permission-mode plan --permission-prompts none --strict-mcp-config --append-system-prompt "このレビューの出力の最後の行には、報告する脆弱性の件数を SECURITY_FINDINGS: <件数> の形(半角数字・0 件なら SECURITY_FINDINGS: 0)で必ず 1 行だけ出す。" "/security-review"` を流すと、本文の最後が `報告する脆弱性はありません。` の後に `SECURITY_FINDINGS: 0` の 1 行で終わった(exit 0)。今の `official_output_readable()` は同じ本文を読めない(「報告する脆弱性はありません」は正規表現に当たらない)。→ 公式の CLI のフラグ(`--append-system-prompt`)で判定の 1 行を出させ、判定はその行(行全体が `SECURITY_FINDINGS: <数>` の行)だけで行う +- **同梱 6**: `scripts/claude/tests/test_i223_gh_app.sh:139` の `re.finditer(r"gh auth (status|token)\b", text)`。Python の `\b` は和文の文字も語の文字に数えるので、`gh auth tokenを打つ` で当たらず G10 が合格になる(I226 の台帳 F1) +- **環境前提**: `bash`・`python3`・`git`・`claude`(スパイクで動作確認)があることを確かめた。ツールの追加なし +- **既存テストのベースライン(2026-11-08・このブランチ)**: `test_i197_review_ledger.sh` pass=64 fail=0・`test_i193_official_flow.sh` pass=174 fail=0・`test_i202_ledger_cleanup.sh` pass=157 fail=0・`test_i223_gh_app.sh` pass=37 fail=0。全体は CI(Harness Tests)が正 +- **消費箇所の grep(概念「差分の起点」の変更)**: `review_mode` の利用は `code-review.sh:967`(本体)とテスト(`test_i197_review_ledger.sh` R1・R9・FG-R1)。`latest_ledger_record` は `review_mode`・`--route`(`code-review.sh:908`)。説明の文は `.claude/skills/review-gate/SKILL.md:14`・`:36`、`docs/runbooks/review-rules.md:59`。`gate-check.sh` は最新の記録の `VERDICT` と `REVIEWED_COMMIT` だけを読み、起点は読まない(§4-4) + +## 2. 受け入れ条件 +- [ ] 最新の記録が「見終えていない回」(組み込みの `/security-review` が動かなかった・レビュー役の出力が壊れていた)のとき、やり直しの差分のレビューは、その前の見終えた記録(無ければブランチの分岐点=ブランチ全体)を起点にする → TC-02(U1〜U3) +- [ ] その結果、失敗した回の後に記録の場所の外の変更が無くても、レビュー役と(高リスクなら)組み込みの `/security-review` が呼ばれる → TC-02(E1) +- [ ] 上の 2 点を失敗注入つきの決定論のテストで固定している(起点の選び方を前の形に戻すとテストが落ちる)→ TC-02(FG) +- [ ] 見終えていない回が 2 つ以上続いても、遡って最後の見終えた記録を起点にする → TC-02(U2) +- [ ] `gate-check.sh` は直さない(理由は §4-4)。失敗した回の記録・そのやり直しの記録での `gate-check.sh` の結果をテストで固定している → TC-02(E1) +- [ ] 台帳(指摘の番号・状態)と台帳の周の数は、起点にした記録ではなく台帳のある最新の記録から引き継ぐ → TC-02(U1・E1) +- [ ] 見終えたかの判定に使う欄(`REVIEW_COMPLETE:`)と、欄が無い古い記録の扱い(見終えていない扱い)を §4-1 に書いている +- [ ] 見終えた記録の後のやり直し(指摘が残って HIGH になった回の後を含む)は、今までどおりその記録から差分を取る → TC-02(U4)・TC-01(既存のテスト) +- [ ] 高リスクの変更で `/security-review` が「指摘なし」を日本語の文で書いても FAILED にならない(判定は `SECURITY_FINDINGS:` の行だけ)→ TC-03・M-01 +- [ ] G10 が `gh auth tokenを打つ`(和文がすぐ続く形)を不合格にする → TC-04 +- [ ] 既存のハーネステスト・CI がすべて pass する → TC-01・CI + +(受け入れ条件 1 の「失敗した回」は、ユーザーの判断(2026-11-08「ループエンジニアリング、ハーネスエンジニアリングのベストプラクティスと開発をサクサク進めるという観点で問題ないやり方を選択して」)を受けて「見終えていない回」に定めた。指摘が残って HIGH になった回は見終えているので起点にしてよい(§9)。イシュー本文の受け入れ条件も同じ意味に合わせて直す) + +## 3. 影響範囲 +- Backend: なし +- Frontend: なし +- DB: なし +- Config/Infra(ハーネス): `scripts/claude/code-review.sh`・`scripts/claude/tests/test_i212_review_base.sh`(新規)・`scripts/claude/tests/test_i197_review_ledger.sh`・`scripts/claude/tests/test_i193_official_flow.sh`・`scripts/claude/tests/test_i223_gh_app.sh`・`.claude/skills/review-gate/SKILL.md`・`docs/runbooks/review-rules.md`・`docs/issues/open/I212.md`(受け入れ条件の文言) +- `scripts/claude/gate-check.sh`・`scripts/claude/adversarial-review.sh`: 変更なし +- サービスの再起動: 不要 + +## 4. 変更点一覧(具体) + +### 修正アプローチ +1. 記録ごとに「レビュー役と(高リスクなら)組み込みの `/security-review` が範囲を見終えたか」を `REVIEW_COMPLETE: YES|NO` の 1 行で残す +2. 差分のレビューの起点は、台帳のある記録のうち `REVIEW_COMPLETE: YES` で `REVIEWED_COMMIT` が HEAD の祖先の最新のもの。無ければブランチの分岐点(比べる相手との merge-base)を起点にして、ブランチ全体を差分のレビューとして見直す。台帳と周の数は今までどおり台帳のある最新の記録から引き継ぐ +3. `/security-review` には公式のフラグ `--append-system-prompt` で最後の行に `SECURITY_FINDINGS: <件数>` を出させ、動いたかの判定は行全体がその形の行があるかだけで行う +4. G10 の `\b` を英数字だけの先読みに替える + +### 4-1. `scripts/claude/code-review.sh`: 見終えたかの行(`REVIEW_COMPLETE`) +修正方針: 判定を合成した後に、見終えたかを決定論で決め、記録に 1 行書く。組み込みのレビュー・レビュー役の本文の中の同じ形の行は無効にする(偽の行で起点を動かさない)。 +- 決め方(本体・`REVIEWER_VERDICT` を決めた直後): + ```bash + # I212: レビュー役と(高リスクなら)組み込みの /security-review が範囲を見終えたか。YES の記録だけが次の差分のレビューの起点になる + # (台帳に止める指摘が残った HIGH は見終えているので YES。/security-review が動かなかった・レビュー役の出力が壊れた・台帳を作れなかったは NO) + REVIEW_COMPLETE=NO + if [ "$REVIEW_OK" = "1" ] && [ "$REVIEWER_VERDICT" = "OK" ] && [ "$SECURITY_VERDICT" = "OK" ]; then REVIEW_COMPLETE=YES; fi + GATE_EVIDENCE+="- 見終えたか(次の差分のレビューの起点にできるか・I212): ${REVIEW_COMPLETE}"$'\n' + ``` + - 組み込みの `/code-review` の失敗(`OFFICIAL_CODE` が「実行できず」など)は条件に入れない(計画レビュー Warning 1)。`/code-review` の結果は合否に使わず、参考としてレビュー役に渡すだけなので、動かなくても範囲を見る役(レビュー役・高リスクなら `/security-review`)は見終えている + - `NO_DELTA=1`(起点の後に記録の場所の外が変わっていない)の回は、レビュー役の代わりの本文で `REVIEW_OK=1`・`REVIEWER_VERDICT=OK`・`SECURITY_VERDICT=OK`(下の 4-3 の「差分なし」)になり YES。起点が見終えた記録なので、引き継いで見終えた扱いでよい +- 書く場所: `add_reviewed_commit` の直後、組み込みのレビューの出力を足す前に `printf 'REVIEW_COMPLETE: %s\n' "$REVIEW_COMPLETE" >> "$REVIEW_FILE"` +- 偽の行を無効にする: `neutralize_machine_lines()` の語の一覧に `REVIEW_COMPLETE` を足す(組み込みのレビューの出力)。`REVIEW_CLEAN` の sed(`code-review.sh:1193`)の `(LEDGER|LEDGER_ROUND)` を `(LEDGER|LEDGER_ROUND|REVIEW_COMPLETE)` にする(レビュー役の本文) +- 読む関数(新規・`record_reviewed_commit` の隣): + ```bash + # I212: 記録が「見終えた」か(REVIEW_COMPLETE: YES の行があれば exit 0)。行が無い古い記録は見終えていない扱い + # (起点にしない側=見直す範囲が広がる側に倒す) + record_complete() { grep -qE '^REVIEW_COMPLETE:[[:space:]]*YES[[:space:]]*$' "$1" 2>/dev/null; } + ``` +- 欄が無い古い記録: 見終えていない扱い。今このブランチより前に作った記録で進行中のイシューは無い(I212 自身の記録は新しい版で作る)ので、影響は「起点がさかのぼり、見直す範囲が広がる」だけ + +### 4-2. `scripts/claude/code-review.sh`: 起点の選び方(`review_mode`) +修正方針: 全体か差分かの決め方(最新の台帳のある記録の `REVIEWED_COMMIT` が祖先でなければ全体=rebase の後)と、台帳・周の数の引き継ぎは変えない。起点だけを「見終えた最新の記録」に替える。 +```bash +# 全体か差分か: "full1" か "delta<起点の SHA><前の記録>"。 +# 差分にするのは、台帳のある最新の記録の REVIEWED_COMMIT が今の HEAD の祖先のときだけ(rebase などで祖先でなければ全体)。 +# I212: 起点は、台帳のある記録のうち見終えた(record_complete)最新の記録の REVIEWED_COMMIT(HEAD の祖先のものだけ)。 +# 無ければブランチの分岐点(比べる相手との merge-base)=ブランチ全体を差分のレビューとして見直す。 +# 台帳と周の数(・<前の記録>)は、起点に関係なく台帳のある最新の記録から引き継ぐ(I206 の番号の合流を壊さない) +review_mode() { # $1=I### [$2=比べる相手(既定 origin/develop)] + local last n sha rec base="" + last=$(latest_ledger_record "$1") + if [ -n "$last" ]; then + n=$(grep -oE '^LEDGER_ROUND:[[:space:]]*[0-9]+[[:space:]]*$' "$last" | tail -1 | grep -oE '[0-9]+' || true) + sha=$(record_reviewed_commit "$last") + if [ -n "$n" ] && [ -n "$sha" ] && git merge-base --is-ancestor "$sha" HEAD 2>/dev/null; then + while IFS= read -r rec; do + sha=$(record_reviewed_commit "$rec") + if record_complete "$rec" && [ -n "$sha" ] && git merge-base --is-ancestor "$sha" HEAD 2>/dev/null; then base="$sha"; fi + done < <(ledger_records "$1") + [ -n "$base" ] || base=$(git merge-base "${2:-origin/develop}" HEAD 2>/dev/null) || base="" + if [ -n "$base" ]; then printf 'delta\t%s\t%s\t%s\n' "$(( n + 1 ))" "$base" "$last"; return 0; fi + fi + fi + printf 'full\t1\t\t\n' +} +``` +- 本体の呼び出し(`code-review.sh:967`): `review_mode "$ISSUE" origin/develop` +- 本体の表示(`code-review.sh:975` の後): 起点が前の記録の `REVIEWED_COMMIT` と違うときに 1 行出す: `(前の記録 <パス> は見終えていない〔REVIEW_COMPLETE: NO〕ため、見終えた最新の記録〔無ければブランチの分岐点〕から見直します)` +- 分岐点を起点にした差分のレビューは、`delta_files`・`delta_hunks`・見せるファイル・`changed_outside_records` がすべてブランチ全体と同じ範囲になる(起点=merge-base)。レビュー役の入力は「差分のレビュー」の形のまま(前の周の止める指摘・台帳の残りも渡る) +- 先頭のコメント(`code-review.sh:495-496`)と `latest_ledger_record` のコメント(492 行目)を合わせて直す + +### 4-3. `scripts/claude/code-review.sh`: `/security-review` の判定の 1 行(同梱 2・4) +修正方針: 組み込みのレビューの文の形に頼らず、公式の CLI のフラグで出させた 1 行だけで「動いた」を判定する。 +- `run_official_review()` に省略できる第 2 引数(足す指示)を付け、あれば `--append-system-prompt "$2"` を付けて起動する(無ければ今と同じ起動) + ```bash + run_official_review() { # $1=プロンプト $2=足す指示(省略可。--append-system-prompt で渡す) + local prompt="$1" out rc=0 extra=() + [ -n "${2:-}" ] && extra=(--append-system-prompt "$2") + out=$(GH_APP_READONLY=1 timeout "${OFFICIAL_REVIEW_TIMEOUT:-900}" claude -p --permission-mode plan --permission-prompts none --strict-mcp-config ${extra[@]+"${extra[@]}"} "$prompt" 2>&1 < /dev/null) || rc=$? + ...(以下は今のまま) + ``` +- 指示の文は定数にする(関数だけを読み込む側でも見えるよう、関数の定義の並びに置く): + ```bash + # I212: /security-review に出させる判定の 1 行(official_security_verdict がこの行だけで「動いた」を決める) + SECURITY_FINDINGS_PROMPT='このレビューの出力の最後の行には、報告する脆弱性の件数を SECURITY_FINDINGS: <件数> の形(半角数字・0 件なら SECURITY_FINDINGS: 0)で必ず 1 行だけ出す。' + ``` +- 呼び出し(`code-review.sh:1045`): `run_official_review "/security-review" "$SECURITY_FINDINGS_PROMPT"` +- `official_security_verdict()`: + ```bash + # I212: 高リスク(パス判定 YES)では、組み込みの /security-review の出力に、行全体が + # `SECURITY_FINDINGS: <数>` の行があるときだけ「動いた」(OK)。差分が無くて動かしていない回は OK。それ以外(実行できず・判定の行が無い)は HIGH + official_security_verdict() { # $1=RISK_PATH(YES/NO) $2=組み込みの /security-review の出力 + [ "$1" = "YES" ] || { echo OK; return; } + [[ "$2" == *"(実行していない: 差分なし)"* ]] && { echo OK; return; } + # here-string で渡す(pipefail の下で grep -q が先に終わって 141 になる誤判定を避ける。official_output_readable と同じ・I193 L1) + if grep -qE '^SECURITY_FINDINGS:[[:space:]]*[0-9]+[[:space:]]*$' <<< "$2"; then echo OK; else echo HIGH; fi + } + ``` + - 「最後の行だけ」に限らない(計画レビュー Warning 3): モデルが判定の行の後に締めの文を付けても FAILED にしない。行全体が固定の形の行だけを読むので、文の中に出てくる語(「SECURITY_FINDINGS: 0 と書く」など)では当たらない + - 指摘の件数は判定に使わない(指摘の中身はレビュー役と台帳が扱う。今までどおり) + - `SECURITY_FINDINGS` は `neutralize_machine_lines` の対象にしない(判定はこの行を読むため。記録の中のこの行を読む処理は無い) +- 本体の `⛔ … /security-review が動きませんでした(実行できず・読めていない)` の文を「(実行できず・判定の行 SECURITY_FINDINGS が無い)」に直す + +### 4-4. `scripts/claude/gate-check.sh`: 変更しない(理由) +- `gate-check.sh` は「最新の記録の `VERDICT` が OK で、その `REVIEWED_COMMIT` の後に記録の場所の外が変わっていない」を確かめる。直した後の `code-review.sh` では: + - 見終えていない回(`/security-review` が動かない・レビュー役の出力が壊れた)は必ず `VERDICT` が HIGH/BLOCKER になる(`SECURITY_VERDICT`・`REVIEWER_VERDICT` が `FINAL_VERDICT` に合成される)ので、その記録では通らない + - そのやり直しの記録は、起点を見終えた記録(無ければ分岐点)にするので、失敗した回が見るはずだった差分をレビュー役と `/security-review` が見る。「範囲 0 本・OK」の記録は、見終えた記録の後に記録の場所の外が変わっていないときにしかできない +- よって gate-check に起点をたどる検査を足しても、`code-review.sh` が作る記録では結果が変わらない。足すと記録の形への依存が 2 か所になる。テスト(TC-02 E1)で、失敗した回の記録では `GATE_CHECK: FAIL REASON=verdict`、やり直しの記録では `GATE_CHECK: OK` で、そのやり直しでレビュー役と `/security-review` が呼ばれたことを固定する + +### 4-5. `scripts/claude/tests/test_i212_review_base.sh`(新規) +- U(関数・一時のリポジトリ。`test_i197_review_ledger.sh` の `rec`・`lib` と同じ作り): + - U1: 記録 A(`LEDGER_ROUND: 1`・`REVIEW_COMPLETE: YES`・`REVIEWED_COMMIT: `)→ 記録 B(`LEDGER_ROUND: 2`・`REVIEW_COMPLETE: NO`・``)→ `review_mode I900 develop` が `delta|3||`(起点は A・周と前の記録は B) + - U2: さらに記録 C(`LEDGER_ROUND: 3`・`NO`・``)→ `delta|4||`(2 つ続いても A まで遡る) + - U3: 見終えた記録が 1 つも無い(A を `NO` にする)→ `delta|4||` + - U4: 最新が `VERDICT: HIGH`・`REVIEW_COMPLETE: YES`(台帳に止める指摘が残った回)→ その記録の `REVIEWED_COMMIT` が起点(今までどおり) + - U5: `REVIEW_COMPLETE` の行が無い記録は見終えていない扱い(起点にしない) + - U6: 見終えた記録の `REVIEWED_COMMIT` が HEAD の祖先でない(rebase の前の枝)なら、その記録は起点にしない + - U7: `official_security_verdict`: YES+最後の行 `SECURITY_FINDINGS: 0` → OK/YES+`SECURITY_FINDINGS: 2` → OK/YES+日本語の「報告する脆弱性はありません。」だけ → HIGH/YES+`SECURITY_FINDINGS: 0` の後に締めの文 → OK/YES+文の中に `SECURITY_FINDINGS: 0` の語がある行だけ(行全体ではない)→ HIGH/YES+`(実行できず: exit=124…)` → HIGH/YES+`(実行していない: 差分なし)` → OK/NO → OK + - U8: `neutralize_machine_lines` が `REVIEW_COMPLETE: YES` を `> REVIEW_COMPLETE: YES` にする +- E(本体の通し。`test_i202_ledger_cleanup.sh` の偽の `gh`・`claude` と同じ作り。偽の `claude` は、プロンプトが `/security-review` なら `$E2E_SEC` の中身〔既定は `SECURITY_FINDINGS: 0` で終わる文〕を返し、呼ばれた種類〔agent/code/security〕を書く): + - E1: `.github/` の下のファイルを変えたブランチ(パス判定 YES)で、1 周目は `/security-review` が判定の行を出さない(`E2E_SEC` に日本語の「報告する脆弱性はありません。」だけ)→ `FINAL_VERDICT: HIGH`・記録に `REVIEW_COMPLETE: NO`・gate-check は `REASON=verdict`。記録だけのコミットの後(記録の場所の外は変えない)に 2 周目(`E2E_SEC` は既定)→ 出力に `範囲のファイル 0 本` も `呼ばずに台帳を引き継ぎます` も無い・偽の `claude` の呼び出しに agent と security がある・`FINAL_VERDICT: OK`・記録に `REVIEW_COMPLETE: YES`・`LEDGER_ROUND: 2`・gate-check は `GATE_CHECK: OK` + - E2: E1 の 2 周目の記録の後に、記録の場所だけを変えて 3 周目 → 今までどおり範囲 0 本でレビュー役を呼ばない・`REVIEW_COMPLETE: YES`(見終えた記録の後は I202 の動きのまま) +- FG(`broken`: 写しの文字列を置き換えて壊す): + - FG-U1: `record_complete "$rec" || continue` を `:` にする(見終えたかを見ない・計画変更記録 3 の形)→ U1 が `` になり不合格 + - FG-E1: 本体の写しの `review_mode` を前の形(最新の記録の SHA を起点)に戻す → E1 の 2 周目が `範囲のファイル 0 本` になり不合格 + - FG-U7: `official_security_verdict` の行全体の固定(`'^SECURITY_FINDINGS:[[:space:]]*[0-9]+[[:space:]]*$'`)を外して `'SECURITY_FINDINGS:[[:space:]]*[0-9]+'` にする → U7 の「文の中に語がある行だけ → HIGH」が OK になり不合格(計画変更記録 2) + - 直す前の版(develop の `official_security_verdict`)で U7 を流し、期待と違う行(例: 「報告する脆弱性はありません。」だけ → 前の版は注意が付かない入力なので OK)が出ることをステップ 1 の記録に残す + - FG-U8: `neutralize_machine_lines` の一覧から `REVIEW_COMPLETE` を外す → U8 が不合格 + +### 4-6. 既存テストの直し(意図した振る舞いの変更に合わせる) +- `test_i197_review_ledger.sh`: + - R1「LEDGER_ROUND: 2・祖先の REVIEWED_COMMIT → delta・3・起点」の記録に `REVIEW_COMPLETE: YES` を足す(見終えた記録が起点、という新しい決まりの例にする) + - FG-R1 の `broken` の置き換え前の文字列を新しい `printf 'delta\t%s\t%s\t%s\n' "$(( n + 1 ))" "$base" "$last"; return 0` に合わせる +- `test_i193_official_flow.sh`: + - C23「YES で読めた結果 → OK」の出力を `確信度の高い脆弱性は見つかりませんでした。` から、その後に `SECURITY_FINDINGS: 0` の行を足した形にする(判定の行が要る、という新しい決まり) + - s8 は `"/security-review"` の文字列を探すだけなので、呼び出しに第 2 引数を足しても変えない(実装で確かめる) + - R1 の他のケース(記録が無い・`LEDGER_ROUND` の無い記録だけ・祖先でない)は、第 2 引数を省いて既定の `origin/develop` が一時のリポジトリに無くても、起点を探す前に `full` で終わるので変わらない(計画レビュー Warning 2)。ステップ 1 で R1 が全部合格することを確かめ、変わるケースがあれば、そのケースに第 2 引数 `develop` を渡す。新しい `test_i212_review_base.sh` は常に `develop` を渡す +- `test_i202_ledger_cleanup.sh`: 偽の `claude` は `/security-review` を呼ばない作り(パス判定 NO)なので変えない。見終えた記録の後の通し(m・n)はそのまま合格するはず(TC-01 で確かめる) + +### 4-7. `scripts/claude/tests/test_i223_gh_app.sh`(同梱 6) +- 139 行目: `r"gh auth (status|token)\b"` → `r"gh auth (status|token)(?![A-Za-z0-9_])"`(前の `\b` と同じく英数字と `_` だけを語の文字にする。`-` は入れない=`gh auth token-foo` も今までどおり当たる・計画変更記録 3) +- FG を 1 つ足す: `g10_fg "和文がすぐ続く auth token は不合格" 1 'gh auth tokenを打つ'` +- 冒頭の G10 の説明(111 行目付近)に「語の境目は英数字だけで見る(和文が続いても当たる・I212)」を足す + +### 4-8. 説明の文 +- `.claude/skills/review-gate/SKILL.md:14`: 「前の記録の `REVIEWED_COMMIT` からの直した差分」→「見終えた最新の記録(`REVIEW_COMPLETE: YES`)の `REVIEWED_COMMIT` からの差分(見終えていない回の後は、その前の見終えた記録〔無ければブランチ全体〕から見直す・I212)」。括弧の中の「台帳のある最新の記録の `REVIEWED_COMMIT` が HEAD の祖先なら差分のレビュー」は残す(全体か差分かの決め方は変えない) +- `.claude/skills/review-gate/SKILL.md:36`: `OFFICIAL_SECURITY: FAILED` の行に「やり直しは、見終えた記録から差分を取り直すので、動かなかった回が見るはずだった差分を `/security-review` が見る(I212)。判定は `/security-review` の出力の、行全体が `SECURITY_FINDINGS: <数>` の行だけで行う」を足す +- `docs/runbooks/review-rules.md:59`: 「前の記録の `REVIEWED_COMMIT` からの直した差分」→「見終えた最新の記録の `REVIEWED_COMMIT` からの差分(組み込みの `/security-review` が動かなかった回などは起点にしない・I212)」 + +### 4-9. `docs/issues/open/I212.md` +- 受け入れ条件 1・2 の「HIGH・BLOCKER・`OFFICIAL_SECURITY: FAILED`」を、§2 の注記のとおり「見終えていない回(組み込みの `/security-review` が動かなかった・レビュー役の出力が壊れていた)」に合わせる。受け入れ条件 7 の欄の名前を `REVIEW_COMPLETE` に合わせる。同梱 3・5 は別イシューに切り出したと書く(§9) + +## 5. 実装手順(ステップ) +1. テストを先に書く: 4-5 の `test_i212_review_base.sh` と 4-6・4-7 のテストの直しを書き、直す前の本体で U1〜U3・U5・U7・U8・E1・G10 の新しい FG が落ちることを記録する → TC-02・TC-03・TC-04 参照 +2. 4-1・4-2(見終えたかの行と起点の選び方)→ TC-02 参照(ステップ 1 が前提) +3. 4-3(`/security-review` の判定の 1 行)→ TC-03 参照(ステップ 2 と並行可) +4. 4-7(G10)→ TC-04 参照(並行可) +5. 4-8・4-9(説明の文)(ステップ 2・3 の後)。文言の有無の決定論テストは作らない(消すのが意図的な変更でしか起きない・`plan-writing-rules.md`「決定論テストの対象の選び方」)。中身はレビューで見る。構文は TC-05 +- サービスの再起動は要らない(次の review-gate から新しい版が動く) + +## 6. テスト計画 +### 自動 +- テストレベル: シェルの単体(関数を読み込む)と本体の通し(一時のリポジトリ・偽の `gh`・`claude`)。本物の GitHub・Claude は使わない +- 再発防止テスト: E1(I211・I213・I200 の再現)・U2(続けて失敗)・U7(I200 の日本語の文)・G10 の FG。どれも FG で不合格になることを確かめる +- 認可・テナント境界: 該当なし(アプリのコードを変えない) +- 詳細は `docs/tests/open/I212_auto_test.md` +### 手動 +- `docs/tests/open/I212_manual_test.md`(すべて Claude。本物の `/security-review` を新しい版の `run_official_review` で動かし、判定が OK になるか) + +## 7. ロールバック +- PR を revert する(設定・データの変更なし)。記録に足した `REVIEW_COMPLETE:` の行は、古い版では読まれないだけで害は無い + +## 8. Risk & 回避策 +- **見終えた記録が無いと、ブランチ全体を見直す(1 周目が `/security-review` の失敗だった I213 の形)**: 見直す範囲は 1 周目と同じで、台帳は引き継ぐので、止める指摘の番号は崩れない。新しい指摘が出うるが、それは 1 周目で見るはずだったもの +- **`/security-review` が判定の行を出し忘れる**: HIGH(FAILED)になり、review-gate の手順どおり 1 回だけやり直す(やり直しは見終えた記録から差分を取り直すので、同じ範囲を見る)。2 回続けば止める(止める条件 3)。スパイクでは出た +- **`--append-system-prompt` が組み込みのレビューの中身を変える**: 足すのは最後の行の形だけで、指摘の基準には触れない。スパイクの本文は今までと同じ形だった +- **偽の `REVIEW_COMPLETE: YES` の行で起点を動かす**: 組み込みのレビュー・レビュー役の本文の中の行は無効にする(4-1)。わざと記録を書き換えるのは対象外(「普通の書き方の事故」まで・I179 の決まり) +- **古い記録(欄が無い)が起点にならない**: 進行中のイシューは無い。あっても見直す範囲が広がる側 + +## 9. 確定事項と自律判断 +イシューに明記されている: +- 差分の起点を、失敗した回ではなく前の合格した記録(無ければ全体)にする。台帳は最新の台帳のある記録から引き継ぐ。決定論のテストと失敗の注入で固定する。`gate-check.sh` は必要なら直し、直さないならその理由を書く +- 同梱 1〜6 の採否は計画で決める + +ユーザーの判断(2026-11-08): +- 「失敗した回」の範囲: ユーザーは「ループエンジニアリング、ハーネスエンジニアリングのベストプラクティスと開発をサクサク進めるという観点で問題ないやり方を選択して」と答えた → **見終えていない回(`/security-review` が動かなかった・レビュー役の出力が壊れた)だけを起点にしない**。台帳に止める指摘が残って HIGH になった回は見終えているので起点にする。理由: 指摘が残った回まで起点から外すと、fix-loop のたびにブランチ全体を見直すことになり、I197 の「差分だけを見て周を収束させる」設計が崩れる(I179・I193・I197 で、全体の見直しは新しい指摘が毎回出て減らないことを実測済み)。今回の穴の原因は「見ていない回を起点にした」ことだけで、ここを塞げば足りる + +Claude が決めた(技術的な選択と理由): +- **見終えたかを記録の新しい行 `REVIEW_COMPLETE` で残す**(仮定・技術的): `VERDICT` は「指摘が残った HIGH」と「見ていない HIGH」を区別できない。証跡の文を読む形は文の形に頼る(同梱 2 と同じ穴) +- **見終えた記録が無いときは、全体のレビューではなく「分岐点を起点にした差分のレビュー」にする**(仮定・技術的): 範囲はブランチ全体で同じだが、台帳(番号・止める指摘)と周の数を引き継げる。全体のレビューにすると台帳を作り直し、I206 の番号の合流の守りが外れる +- **`gate-check.sh` は直さない**(§4-4) +- **同梱 2・4 を入れる**: 主題を直しても、日本語の「指摘なし」で FAILED になる限り、やり直しでも同じ FAILED になって止まる(I200 の実例)。判定の 1 行は公式のフラグで出せることをスパイクで確かめた +- **同梱 6 を入れる**: 1 行の直しと FG 1 つで、ハーネスの最優先の印(I226 の retro) +- **同梱 3・5(敵対的レビューの「確認済み」の引き継ぎ・締め切りで実験が走らない)は別イシューに切り出す**: 対象が `adversarial-review.sh`・`adversarial-reviewer.md` で、差分のレビューの起点(このイシューの本題)とは仕組みが別。どちらも設計の検討(引き継ぐ条件・時間の配分)が要り、同じ PR に入れると高リスクの変更が大きくなる。retro で起票する(`issue-bootstrap`) +- **`--append-system-prompt` は `/security-review` だけに付ける**: `/code-review` の結果は合否に使わない(参考としてレビュー役に渡すだけ)ので、判定の行は要らない +- 要件・基本設計・UI/UX の仮定: なし(受け入れ条件の意味はユーザーの判断で確定) + +台帳の残りの一覧(`grep -rnF` で実装対象のファイルと `FILE=-` に当たった行): +- 一覧 I206 F4・I207 F6・I207 F7・I208 F1・I208 F2・I208 F3・I208 F5(`scripts/claude/code-review.sh`): 残す — どれも台帳の合流・振り分け・一時ファイルの Low で、起点の選び方と `/security-review` の判定に関係しない +- 一覧 I226 F3・I223 F6(`scripts/claude/tests/test_i223_gh_app.sh`): 残す — CRLF のつなぎ・G6 の検出の狭さで、G10 の語の境目とは別 +- 一覧 I224 F9・I223 B1〜B4(`FILE=-`): 残す — push の回数の実測(I224 の retro の後)と bot の停止の後始末で、このイシューと関係しない + +要件適合性・業務ロジック: +- 受け入れ条件の範囲を超えた仕様の追加なし(同梱 2・4・6 はイシューの同梱で、計画で採ると決めた)。マルチテナント・ステータス遷移は該当なし +- エッジケース: 見終えた記録が無い(分岐点)・続けて失敗(遡る)・rebase の後(全体のまま)・欄の無い古い記録(見終えていない扱い)・差分なしの回(見終えた扱い)・比べる相手が無い(全体) + +セキュリティ・ベストプラクティス: +- 入力: 記録の行は行頭の固定の形だけを読む(`^REVIEW_COMPLETE:[[:space:]]*YES[[:space:]]*$`)。組み込みのレビュー・レビュー役の本文の中の同じ形の行は無効にする +- 起動: `--append-system-prompt` の文は定数(差分の中身を入れない)。起動の形(plan モード・承認なし・読み取りだけの鍵・`--strict-mcp-config`)は変えない +- 機密データ・OWASP・依存ライブラリ: 該当なし(依存の追加なし) +- bandit・npm audit: 対象のコードなし(シェルとテストだけ) +- 「普通の書き方の事故」の範囲: 記録をわざと書き換える・組み込みのレビューにわざと偽の行を出させるのは対象外(I179) + +P3/P5/P8 影響なし(DB・外部 API の新規・非同期・インフラの追加なし)。P6 影響なし(UI なし)。P9 影響なし(個人情報・テナントデータを扱わない)。 + +設計品質: +- 判定の本体は 1 か所(`record_complete`・`official_security_verdict`)。起点の選び方は `review_mode` の中だけ +- 例外処理: 読めない・欄が無い・git の失敗は、起点にしない(見直す範囲が広がる側)に倒す +- 設定値: 指示の文は定数 `SECURITY_FINDINGS_PROMPT` + +Danger Ops: 無 + +## 必ず確かめる項目(高リスク時の敵対的レビューが判定に使う) +| 項目 | 観点 | 確かめ方 | 内容 | +|---|---|---|---| +| V1 | 1 | 実験必須 | `/security-review` が動かなかった回の後のやり直しで、記録の場所の外を変えていなくても、レビュー役と `/security-review` が呼ばれ、範囲が 0 本にならない(TC-02 E1) | +| V2 | 1 | 実験必須 | 見終えていない回が 2 つ以上続いても、最後の見終えた記録(無ければ分岐点)が起点になる。台帳と周の数は最新の記録から引き継ぐ(TC-02 U1〜U3) | +| V3 | 2 | 実験必須 | 台帳に止める指摘が残って HIGH になった回の後は、今までどおりその記録が起点(周が収束する動きを壊さない・TC-02 U4・TC-01) | +| V4 | 2 | 実験必須 | 組み込みのレビュー・レビュー役の本文の中の `REVIEW_COMPLETE: YES` の行が記録の判定の行として読まれない(TC-02 U8) | +| V5 | 3 | 実験必須 | `/security-review` が日本語の「指摘なし」で書いても、行全体が `SECURITY_FINDINGS: <数>` の行があれば OK(その後に締めの文があっても OK)、無ければ HIGH(TC-03・M-01)。新しい FG がすべて直す前の版で不合格になる(false-green でない) | + +## 計画変更記録 +1. 計画レビュー(20261108_0044・OK・高リスク判定 No)の Warning 3 件・Info 1 件への対応: (W1) §4-1 に「`/code-review` の失敗は見終えたかの条件に入れない」と理由を足した (W2) §4-6 に、既存の R1 の他のケースが第 2 引数なしでも変わらない理由と、ステップ 1 で確かめることを足した (W3) `official_security_verdict` を「最後の空でない行だけ」から「行全体が `SECURITY_FINDINGS: <数>` の行があるか」に緩めた(判定の行の後に締めの文が付いても FAILED にしない。行全体の固定の形なので文の中の語では当たらない)。U7・FG-U7・TC-03・V5 を合わせて直した (Info 1) 空の配列の展開を `${extra[@]+"${extra[@]}"}` にした。Info 2(欄の無い古い記録)は §8 のとおりで対応なし。計画レビューのやり直し: 不要(findings への対応で、方式・範囲・受け入れ条件は変えない) +2. 実装での形の整理(振る舞いは計画どおり): FG-U7 の壊し方を「行頭の固定を外す」から「行全体の固定(`^…$`)を外す」にした。行頭だけを外しても行末の `$` が残り、文の中の語では当たらないままで、FG が成立しなかった(実走で確かめた)。計画レビューのやり直し: 不要(テストの壊し方の直しで、方式・範囲・受け入れ条件は変えない) +3. /simplify(4 観点)を受けた整理: (a) `review_mode` の起点探しを「記録の一覧を 1 回だけ配列に読み、新しい順に見て、最初に当たった記録で止める」形にし、外側の `sha` を上書きしない別の変数 `rec_sha` にした(振る舞いは同じ。FG-U1 の置き換え前の文字列を `record_complete "$rec" || continue` に合わせた) (b) `official_output_readable()` に「行全体が `SECURITY_FINDINGS: <数>` の行があれば読めた結果」を 1 行足した。判定が OK なのに記録に「読めていない可能性」の注意が残って食い違うのを防ぐ(U7 に 1 ケース足した) (c) G10 の先読みから `-` を外した(計画で `-` を入れた理由が無く、前の `\b` より緩くなっていた)。見送り: `review_mode` の呼び出しの既定値と明示の重なり・案内の再計算・`REVIEW_OK` の条件の重なり(どれも Low で、読みやすさの好みの範囲)、`/code-review` の側の自由文の読み取り・「差分なし」の文字列での判定・記録に「次の起点」を書く別設計(範囲の外・今の形で正しく動く。retro で扱う)、E の土台の写しが 4 本目になった件(ファイルごとの写しは決まりどおり。retro で扱う)。計画レビューのやり直し: 不要(方式・範囲・受け入れ条件は変えない) + +## レビュー結果 +- [20261108_0044 判定: ✅ 完了](../../reviews/I212_plan_review_20261108_0044.md) +(plan-review 段階が追記する) diff --git a/docs/reviews/I212_issue_review_20261029_0226.md b/docs/reviews/I212_issue_review_20261029_0226.md new file mode 100644 index 00000000..45cfafe9 --- /dev/null +++ b/docs/reviews/I212_issue_review_20261029_0226.md @@ -0,0 +1,19 @@ +# イシュー自己完結度レビュー [I212] + +## 判定: 十分 +重要セクションに空欄はなく、重大な論理矛盾もありません。「未特定・計画で確認」は意図的な宣言なので、未記入とは扱いません。下記は計画時に詰めるべき助言です。 + +## 未記入の重要セクション +なし + +## 指摘一覧(助言) +| 重大度 | 観点 | 指摘内容 | 該当箇所 | +|--------|------|---------|---------| +| Warning | 解決方針 | 台帳の引き継ぎ元が曖昧です。起点を「前の合格した記録」に戻すと、失敗した回で採番・更新された指摘(番号・状態)を、どの記録から引き継ぐのかが読み取れません。「今までどおり」とあるだけで、起点の記録と台帳の引き継ぎ元が別になる場合の扱いは未記載です。I206 の番号引き継ぎを壊さないという制約と直結します。 | 解決方針 2 項目目/制約 | +| Warning | スコープ/受け入れ条件 | `gate-check.sh` の確認は解決方針と実装対象に出ていますが、受け入れ条件にはありません。「失敗した回の後の範囲 0 本・OK の記録で通らない」ことを検証可能な形で入れないと、確認だけで終わる恐れがあります。 | 解決方針 4 項目目/受け入れ条件 | +| Info | 解決方針 | 「高リスクなら `OFFICIAL_SECURITY` が動いた記録」の判定方法が曖昧です。非高リスクの記録や、古い形式でこのフィールドが無い記録は、合格扱い(フィールドなし=FAILED でない)にするのかを決める必要があります。 | 解決方針 1 項目目 | +| Info | 受け入れ条件 | `VERDICT: OK`(台帳の結論)と `FINAL_VERDICT` の使い分けが混在しています。合格の判定にどちらを使うかを統一すると、テストが書きやすくなります。 | 解決方針 1 項目目/受け入れ条件 1 | +| Info | 根本原因 | 原因は成立しています(起点が最後にレビューした場所で、最後に合格した場所ではない)。ただし、失敗が連続した場合(FAILED が 2 回続くなど)に、遡って最初の合格記録を探すことを受け入れ条件かテストに入れると堅くなります。 | 根本原因/受け入れ条件 3 | + +## 次のアクションの推奨 +`/grill-me` をスキップして `/plan-issue` に進められます。計画では、上の Warning 2 件(台帳の引き継ぎ元と `gate-check.sh` の受け入れ条件化)を先に決めてください。 diff --git a/docs/reviews/I212_plan_review_20261108_0044.md b/docs/reviews/I212_plan_review_20261108_0044.md new file mode 100644 index 00000000..ff350910 --- /dev/null +++ b/docs/reviews/I212_plan_review_20261108_0044.md @@ -0,0 +1,42 @@ +# 計画書レビュー [I212] ── [2026-11-08 12:00] + +## 判定: ✅ 完了 + +## レビュー指摘一覧 +| 重大度 | 観点 | 指摘内容 | 該当箇所 | 対応 | +|--------|------|---------|---------|------| +| Warning | P4 | `REVIEW_COMPLETE: YES` は、`SECURITY_VERDICT` と `REVIEWER_VERDICT` と `REVIEW_OK` から決める。ところが `/code-review`(`OFFICIAL_CODE`)が失敗した回は見終えた扱いになる。参考としてレビュー役に渡すだけなので意図どおりだが、計画書には「`OFFICIAL_CODE` の失敗は YES の条件に含めない」と一言も書かれていない。 | §4-1、§9(`--append-system-prompt` は `/security-review` だけ) | 実装時に §4-1 へ 1 行足す。 | +| Warning | P4 | `review_mode` の第 2 引数の既定が `origin/develop` になる。既存の `test_i197_review_ledger.sh` の R1 などは引数 1 個で呼ぶ。テスト用リポジトリに `origin/develop` が無いと、`merge-base` が失敗して `base` が空になり、`full` に落ちる。R1 の「見終えた記録が起点」の例は YES の記録があるので動く。ただし、見終えた記録が無い既存ケースがあれば、結果が `full` へ変わる恐れがある。 | §4-2、§4-6 | ステップ 1 で、既存の R 系テストが `full` へ変わらないことを確認し、必要ならテスト側で第 2 引数 `develop` を渡す。 | +| Warning | P4 | `official_security_verdict` は、`/security-review` の出力の「最後の空でない行」だけを見る。モデルが `SECURITY_FINDINGS:` の行の後に締めの文を付ければ HIGH(FAILED)になる。review-gate の手順で 1 回やり直せるが、同梱 2・4 の再発経路が残る。スパイクは 1 回だけ。 | §4-3、§8 | M-01 に加えて、実装後の実走(M-02 の基本レビュー)で連続して出るかを観察する。外れが続くなら、行頭一致で最後に出た `SECURITY_FINDINGS:` を読む形へ緩めるかを検討する。 | +| Info | BP | `extra=()` の空配列を `"${extra[@]}"` で展開している。bash 4.3 以前で `set -u` を使うと未定義エラーになる。この環境は bash 5 系なので実害はない。 | §4-3 | `${extra[@]+"${extra[@]}"}` にすれば安全。任意。 | +| Info | P4 | 欄の無い古い記録を「見終えていない」扱いにするので、進行中のイシューで古い記録が残っていると見直す範囲が広がる。計画書は「進行中は無い」としている。 | §4-1、§8 | 対応は任意。 | + +### 観点別の確認 +- **P1 要件適合性**: 問題なし。範囲を「見終えていない回」に狭めたのは、ユーザーの判断(2026-11-08)に沿っている。台帳と周の数は、台帳のある最新の記録から引き継ぐ。全体か差分かの決め方(rebase 後は全体)は変えない。同梱 3・5 は別イシューに切り出す理由が書かれている。 +- **ベストプラクティス**: 問題なし。判定の本体は `record_complete` と `official_security_verdict` の 1 か所に集めている。指示の文は定数化している。消費箇所(`review_mode` の呼び出し、`--route`、SKILL.md の 14 行目と 36 行目、review-rules.md の 59 行目、`gate-check.sh`)は列挙されている。`neutralize_machine_lines` と `REVIEW_CLEAN` の sed への追加も書かれている。 + - コマンド衛生と破壊的取り消し: 問題なし。 + - 外部挙動(`--append-system-prompt`): スパイクで実証済みで、結果が計画書に記録されている。 +- **セキュリティ**: 認証・認可・テナントは対象外(アプリのコードを変えない)。偽の行による起点の改ざんは、本文中の行を無効化して対策している。わざと書き換えるケースは I179 の方針どおり対象外。依存の追加はない。 +- **P4 テスト**: 問題なし。 + - FG(失敗の注入)が各 TC に付いている。 + - 実装前に、直す前の版で落ちることを確かめる手順がある。 + - 合否は exit 0 基準。 + - 文言の有無を見る過剰な決定論テストは作らない方針も明記されている。 + - M-01 と M-02 は Claude が実施し、実行方法が具体的。 + - 「必ず確かめる項目」の節と表がある。確かめ方はすべて `実験必須`、観点は 1〜3 の範囲内。 +- **P3**: 対象外(DB・マイグレーションなし)。 +- **P5 運用性**: ロールバックは revert で足りる。名義と鍵の変更は無い。`GH_APP_READONLY=1` は維持する。 +- **P8 コスト・保守負荷**: 新しい安全装置としては「見終えたか」の行を足すだけで、文字列でシェルを読む精密な判定ではない。欠けたときは広く見直す側(fail-closed)に倒す。問題なし。 +- **P9 プライバシー**: 対象外。 +- **Claude Code ベストプラクティス**: SKILL.md の説明文の変更だけ。分岐の追加はなく、概念変更の消費箇所は §4-8 に列挙されている。問題なし。 + +## 高リスク判定 +判定: No +該当条件: なし +- 変更対象は `scripts/claude/code-review.sh`、そのテスト、SKILL.md、runbook、イシュー文書。 +- harness 固有の高リスク一覧(`.github/*`・`.pre-commit-config.yaml`・`.secrets.baseline`・`pretooluse_guard.py`・`.claude/settings*.json`)には当たらない。 +- アプリ側の条件を理由に Yes にはしない。 + +推奨: 実装(implement 段階)へ。 + +VERDICT: OK diff --git a/docs/reviews/open/I212_review.md b/docs/reviews/open/I212_review.md new file mode 100644 index 00000000..ba4084d6 --- /dev/null +++ b/docs/reviews/open/I212_review.md @@ -0,0 +1,34 @@ +# I212 レビュー: 差分のレビューの起点を「見終えた最新の記録」にする(失敗した回の後にセキュリティレビューを飛ばさない) + +## 変更概要 +- (実装後に記入) + +## 変更点 +- (実装後に記入) + +## 影響範囲 +- Backend/Frontend/DB: なし +- Config(ハーネス): `scripts/claude/code-review.sh`・テスト(`test_i212_review_base.sh` 新規・`test_i197_review_ledger.sh`・`test_i193_official_flow.sh`・`test_i223_gh_app.sh`)・`.claude/skills/review-gate/SKILL.md`・`docs/runbooks/review-rules.md` + +## テスト結果 +- 自動: +- 手動: + +## 計画との差分 +- (実装後に記入) + +## ロールバック +- PR を revert する(設定・データの変更なし) + +## 自律判断の記録 +- [plan] 受け入れ条件 1 の「失敗した回」を「見終えていない回(/security-review が動かなかった・レビュー役の出力が壊れた)」に定めた — ユーザーの判断「ループエンジニアリング、ハーネスエンジニアリングのベストプラクティスと開発をサクサク進めるという観点で問題ないやり方を選択して」を受けた。指摘が残った HIGH まで外すと fix-loop のたびに全体を見直し、I197 の収束の設計が崩れる +- [plan] 同梱 2・4(/security-review の判定の 1 行)・6(G10)を入れ、3・5(敵対的レビュー)は別イシューに切り出す(retro で起票) — 2・4 は直さないとやり直しも同じ FAILED で止まる。3・5 は別の仕組みで設計の検討が要る +- [plan] gate-check.sh は直さない — 直した後の code-review.sh では見終えていない回は必ず HIGH/BLOCKER になり、やり直しは見終えた記録から差分を取るので、gate-check の結果は変わらない(plan §4-4) +- [plan-review] 計画レビュー OK(高リスク判定 No)の Warning 3 件・Info 1 件を計画書で直し、再レビューはしない — 方式・範囲・受け入れ条件を変えない findings への対応のため(計画変更記録 1)。W3 では /security-review の判定の 1 行を「最後の行だけ」から「行全体がその形の行があるか」に緩めた(締めの文で FAILED にして止まるのを防ぐ) +- [implement] テストを先に書き、直す前の本体で U1〜U3・U5〜U8・E1 が落ちる(E1 では 2 周目が範囲 0 本でレビュー役を呼ばず、gate-check も通る=I211 の穴を再現)ことを確かめてから直した — TC-02・TC-03 の FG の前提 +- [implement] FG-U7 の壊し方を「行全体の固定を外す」に直した — 行頭だけ外しても行末の固定が残り、FG が成立しなかった(計画変更記録 2) +- [implement] /simplify の 4 観点のうち、起点探しの形(新しい順・最初に当たったら止める)・記録の「読めていない」注意との食い違い・G10 の `-` の 3 点を直し、他は見送った — 見送りはどれも Low か範囲の外で、今の形で正しく動く。`/code-review` 側の自由文の読み取り・E の土台の写しは retro で扱う(計画変更記録 3) + +STAGE_DONE: plan NEXT: plan-review +STAGE_DONE: plan-review NEXT: implement +STAGE_DONE: implement NEXT: review-gate diff --git a/docs/runbooks/review-rules.md b/docs/runbooks/review-rules.md index 915f2cc6..0cc9baf5 100644 --- a/docs/runbooks/review-rules.md +++ b/docs/runbooks/review-rules.md @@ -56,7 +56,7 @@ | 4 人 | 価値判断・操作感 | 手動テスト(Human の行)・マージ | test-gate・close | 人の返事 | - **黙って止まらない**: 検証する側の `claude -p` はすべて `--permission-prompts none` で起動する(承認が要る操作は待たずに拒否され、出力の `permission_denials` に残る)。親の会話の中で裏で動くサブエージェントに検証を任せない(確認待ちになっても親から気づけない。I166)。 -- **周が収束する(I197・2026-10-27)**: 基本レビューは 1 回目だけブランチ全体を見て、指摘を台帳(番号・重大度・状態)にする。やり直しは、前の記録の `REVIEWED_COMMIT` からの直した差分と、台帳の止める指摘が直ったかだけを見る(全体を探し直すと、新しいコードが大きいほど毎回新しい指摘が出て減らない。I179・I193・I197 で実測)。fix-loop の中の実装・テストのレビューはやめ、診断だけを残す(同じ差分を重ねて見ない)。高リスク(敵対的レビューを走らせるか)は計画レビューの判定・パスの判定・台帳の目印(`RISK=up`・I202)で決め、一度 YES になったら戻さない。 +- **周が収束する(I197・2026-10-27)**: 基本レビューは 1 回目だけブランチ全体を見て、指摘を台帳(番号・重大度・状態)にする。やり直しは、見終えた最新の記録の `REVIEWED_COMMIT` からの差分(組み込みの `/security-review` が動かなかった回などは起点にしない・I212)と、台帳の止める指摘が直ったかだけを見る(全体を探し直すと、新しいコードが大きいほど毎回新しい指摘が出て減らない。I179・I193・I197 で実測)。fix-loop の中の実装・テストのレビューはやめ、診断だけを残す(同じ差分を重ねて見ない)。高リスク(敵対的レビューを走らせるか)は計画レビューの判定・パスの判定・台帳の目印(`RISK=up`・I202)で決め、一度 YES になったら戻さない。 - **止める基準は変更の種類で分ける(I205・2026-10-27)**: 合否はスクリプト(`code-review.sh` の `blocking()`)が、code-reviewer の FINDING 行の欄(FILE・LINE・STOP・再現)から決定論で出す。code-reviewer の VERDICT 行・直す側の判断は使わない。種類は指摘の FILE のパスで決まる。 | 変更の種類 | 止めるもの | diff --git a/docs/tests/open/I212_auto_test.md b/docs/tests/open/I212_auto_test.md new file mode 100644 index 00000000..b853d6a2 --- /dev/null +++ b/docs/tests/open/I212_auto_test.md @@ -0,0 +1,48 @@ + + +# I212 自動テスト: 差分のレビューの起点を「見終えた最新の記録」にする + +実行コマンド(例): 下の「決定論ゲート(自動実走)」のコマンド(pytest・Jest・E2E は対象外。アプリのコードを変えない) + +結果: +- backend: 対象外 +- frontend: 対象外 + +## テストケース + +| TC | 対象 | 中身と期待値(合格=exit 0) | 失敗の注入(FG) | +|---|---|---|---| +| TC-01 | 既存のハーネステスト | `test_i197_review_ledger.sh`(R1 の記録に `REVIEW_COMPLETE: YES` を足し、FG-R1 の置き換え前の文字列を新しい形に合わせた版)・`test_i193_official_flow.sh`(C23 の「読めた結果 → OK」の出力に `SECURITY_FINDINGS: 0` の行を足した版)・`test_i202_ledger_cleanup.sh`(変更なし)・`test_review_gates.sh`(変更なし)が全 PASS で exit 0 | 既存のテストの FG のまま | +| TC-02 | 起点の選び方・見終えたかの行(`test_i212_review_base.sh` の U1〜U6・U8・E1・E2) | U1: 記録 A(周 1・`REVIEW_COMPLETE: YES`・c1)→ B(周 2・`NO`・c2)で `review_mode I900 develop` が `delta|3|c1|`。U2: さらに C(周 3・`NO`・c2)で `delta|4|c1|`。U3: A も `NO` なら `delta|4||`。U4: 最新が `VERDICT: HIGH`・`REVIEW_COMPLETE: YES` ならその記録の SHA が起点。U5: `REVIEW_COMPLETE` の行が無い記録は起点にしない。U6: 見終えた記録の SHA が HEAD の祖先でなければ起点にしない。U8: `neutralize_machine_lines` が `REVIEW_COMPLETE: YES` を `> REVIEW_COMPLETE: YES` にする。E1: パス判定 YES のブランチで 1 周目の `/security-review` が判定の行を出さない → `FINAL_VERDICT: HIGH`・記録に `REVIEW_COMPLETE: NO`・gate-check が `GATE_CHECK: FAIL REASON=verdict`。記録だけのコミットの後の 2 周目 → 出力に `範囲のファイル 0 本` と `呼ばずに台帳を引き継ぎます` が無い・偽の claude の呼び出しに agent と security がある・`FINAL_VERDICT: OK`・記録に `REVIEW_COMPLETE: YES` と `LEDGER_ROUND: 2`・gate-check が `GATE_CHECK: OK`。E2: その後に記録の場所だけ変えた 3 周目は、範囲 0 本でレビュー役を呼ばない・`REVIEW_COMPLETE: YES`。スクリプト全体が `fail=0` で exit 0 | FG-U1(`record_complete "$rec" || continue` → `:`)で U1 が不合格/FG-E1(本体の写しの起点を最新の記録の SHA に戻す)で E1 の 2 周目が範囲 0 本になり不合格/FG-U8(`neutralize_machine_lines` の一覧から `REVIEW_COMPLETE` を外す)で U8 が不合格。実装の前に、直す前の本体で U1〜U3・U5・U8・E1 が不合格になることを確かめて記録する | +| TC-03 | `/security-review` の判定(`test_i212_review_base.sh` の U7・`test_i193_official_flow.sh` の C23) | `official_security_verdict`: YES+最後の行 `SECURITY_FINDINGS: 0` → OK/YES+`SECURITY_FINDINGS: 2` → OK/YES+「報告する脆弱性はありません。」だけ → HIGH/YES+`SECURITY_FINDINGS: 0` の後に締めの文 → OK/YES+文の中に `SECURITY_FINDINGS: 0` の語がある行だけ(行全体ではない)→ HIGH/YES+`(実行できず: exit=124。124 は時間切れ)` → HIGH/YES+`(実行していない: 差分なし)` → OK/NO → OK | FG-U7(行全体の固定 `^…$` を外す)で「文の中に語がある行だけ → HIGH」が OK になり不合格。直す前の版で「報告する脆弱性はありません。」だけ → HIGH が OK になる(期待と違う)ことを記録する | +| TC-04 | G10 の語の境目(`test_i223_gh_app.sh`) | `g10_fg "和文がすぐ続く auth token は不合格" 1 'gh auth tokenを打つ'` が PASS。既存の G10 と FG はそのまま PASS。スクリプト全体が `fail=0` で exit 0 | 直す前の `\b` の版では新しい `g10_fg` が rc=0 で FAIL になる(実装の前に確かめて記録する) | +| TC-05 | 構文 | `code-review.sh`・`test_i212_review_base.sh` の `bash -n` が exit 0 | — | + +## 実行記録 + +| 日時 | TC | 結果 | メモ | +|---|---|---|---| +| 2026-11-08 | TC-01(計画の時点のベースライン・直す前) | OK | `test_i197_review_ledger.sh` pass=64 fail=0・`test_i193_official_flow.sh` pass=174 fail=0・`test_i202_ledger_cleanup.sh` pass=157 fail=0・`test_i223_gh_app.sh` pass=37 fail=0 | +| 2026-11-08 | TC-02・TC-03(テストを先に書き、本体を直す前) | 期待どおり NG | `test_i212_review_base.sh` pass=11 fail=19: U1〜U3・U5・U6(起点が最新の記録の c2 のまま)・U7 の「日本語の文だけ → HIGH」「文の中の語だけ → HIGH」(直す前は OK)・U8(無効にしない)・E1(1 周目に `REVIEW_COMPLETE` が無い・2 周目が `範囲のファイル 0 本`/`呼ばずに台帳を引き継ぎます` でレビュー役も `/security-review` も呼ばれない・それでも gate-check は OK=I211 の穴の再現)。FG は置き換え前の文字列がまだ無いので未成立 | +| 2026-11-08 | TC-04(直す前) | 期待どおり NG | 直す前の正規表現 `gh auth (status\|token)\b` は `gh auth tokenを打つ` に当たらない(Python で False)=新しい `g10_fg` は直す前の版で rc=0 になり不合格 | +| 2026-11-08 | TC-01〜TC-05(実装の後) | OK | `test_i212_review_base.sh` pass=30 fail=0(FG-U1・FG-E1・FG-U7・FG-U8 がすべて成立)・`test_i197_review_ledger.sh` pass=64・`test_i193_official_flow.sh` pass=174・`test_i202_ledger_cleanup.sh` pass=157・`test_review_gates.sh` PASS=67・`test_i223_gh_app.sh` pass=38(FG の 1 本が増えた)、すべて fail=0 | +| 2026-11-08 | TC-01〜TC-05(/simplify の整理の後) | OK | `test_i212_review_base.sh` pass=31 fail=0(U7 に「判定の行があれば読めた結果」を足した)・`test_i197_review_ledger.sh` pass=64・`test_i193_official_flow.sh` pass=174・`test_i202_ledger_cleanup.sh` pass=157・`test_review_gates.sh` PASS=67・`test_i223_gh_app.sh` pass=38、すべて fail=0。`bash -n` 2 本も exit 0 | + +## 決定論ゲート(自動実走) +```bash +bash scripts/claude/tests/test_i212_review_base.sh +bash scripts/claude/tests/test_i197_review_ledger.sh +bash scripts/claude/tests/test_i193_official_flow.sh +bash scripts/claude/tests/test_i202_ledger_cleanup.sh +bash scripts/claude/tests/test_review_gates.sh +bash scripts/claude/tests/test_i223_gh_app.sh +bash -n scripts/claude/code-review.sh +bash -n scripts/claude/tests/test_i212_review_base.sh +``` diff --git a/docs/tests/open/I212_manual_test.md b/docs/tests/open/I212_manual_test.md new file mode 100644 index 00000000..fb1d121b --- /dev/null +++ b/docs/tests/open/I212_manual_test.md @@ -0,0 +1,14 @@ + + +# I212 手動テスト: 差分のレビューの起点を「見終えた最新の記録」にする + +| No | 手順 | 期待結果 | 実施者 | 実結果 | 備考 | +|---:|------|----------|--------|--------|------| +| M-01 | このブランチで、新しい版の関数を読み込んで本物の `/security-review` を動かし、判定を出す: `REVIEW_LIB_SOURCE_ONLY=1 bash -c '. scripts/claude/code-review.sh; out=$(run_official_review "/security-review" "$SECURITY_FINDINGS_PROMPT"); printf "%s\n" "$out" \| tail -3; official_security_verdict YES "$out"'` | 出力の最後の空でない行が `SECURITY_FINDINGS: <数>`(半角数字)で、最後に `OK` と出る。`(実行できず` は出ない | Claude | | 本物の Claude を 1 回動かす(数分)。読み取りだけの鍵・plan モード(`run_official_review` の起動の形のまま) | +| M-02 | この I212 の review-gate の基本レビュー(`bash scripts/claude/code-review.sh I212`)の記録を開く | 記録に `REVIEW_COMPLETE: YES` か `NO` の行が 1 行あり、注入された証跡に `- 見終えたか(次の差分のレビューの起点にできるか・I212):` の行がある。2 周目以降があれば、出力の「差分のレビュー(台帳の n 周目・起点 …)」の起点が、`REVIEW_COMPLETE: YES` の最新の記録の `REVIEWED_COMMIT` と同じ | Claude | | review-gate の実走の記録で確かめる(別に動かさない) | + +結論: diff --git a/scripts/claude/code-review.sh b/scripts/claude/code-review.sh index 4b3da733..a6e50a47 100644 --- a/scripts/claude/code-review.sh +++ b/scripts/claude/code-review.sh @@ -91,6 +91,8 @@ official_output_readable() { # here-string で渡す(printf | grep -q だと、大きな出力で grep が先に終わり、pipefail の下で 141 になって誤判定する。I193 L1) grep -qE '"file"[[:space:]]*:' <<< "$out" && return 0 grep -qE '^[[:space:]]*\[[[:space:]]*\][[:space:]]*$' <<< "$out" && return 0 + # I212: /security-review に出させた判定の行(official_security_verdict と同じ形)があれば読めた結果(判定と記録の注意を食い違わせない) + grep -qE '^SECURITY_FINDINGS:[[:space:]]*[0-9]+[[:space:]]*$' <<< "$out" && return 0 grep -qE '指摘(は)?(なし|ありません|見つかりません)|(脆弱性|問題|指摘)[^。]*見つかりませんでした|[Nn]o (findings|issues|bugs|vulnerabilities)( were)? (found|reported)|[Nn]o (findings|issues|bugs)\.?$' <<< "$out" && return 0 return 1 } @@ -147,11 +149,12 @@ kill_tree() { # I193: 組み込みのレビュー(/code-review・/security-review)を読み取り専用(plan モード)・時間上限つきで実行する。 # plan モードにするのは、claude -p にはプロジェクトの settings の許可(git push・rm など)がそのまま効くため # (差分の中の文章に誘導された書き込み・push を仕組みで止める)。失敗・時間切れは本文に書いて返す(呼び出し元は止めない)。 -run_official_review() { # $1=プロンプト(例: /code-review medium origin/develop...HEAD) - local prompt="$1" out rc=0 +run_official_review() { # $1=プロンプト(例: /code-review medium origin/develop...HEAD) $2=足す指示(省略可。--append-system-prompt で渡す) + local prompt="$1" out rc=0 extra=() + [ -n "${2:-}" ] && extra=(--append-system-prompt "$2") # I197: 承認が要る操作は待たずに拒否する(--permission-prompts none。黙って止まる経路を作らない) # I223: GitHub へは読み取りだけの App の鍵(GH_APP_READONLY=1。書き込みは GitHub が拒否する) - out=$(GH_APP_READONLY=1 timeout "${OFFICIAL_REVIEW_TIMEOUT:-900}" claude -p --permission-mode plan --permission-prompts none --strict-mcp-config "$prompt" 2>&1 < /dev/null) || rc=$? + out=$(GH_APP_READONLY=1 timeout "${OFFICIAL_REVIEW_TIMEOUT:-900}" claude -p --permission-mode plan --permission-prompts none --strict-mcp-config ${extra[@]+"${extra[@]}"} "$prompt" 2>&1 < /dev/null) || rc=$? if [ "$rc" -ne 0 ]; then printf '(実行できず: exit=%s。124 は時間切れ)\n' "$rc" return 0 @@ -167,18 +170,22 @@ run_official_review() { # $1=プロンプト(例: /code-review medium origin # その中の行(差分の中の文章を引用したものを含む)が判定の行として読まれないようにする。 # I197: 敵対的レビューの記録が読む行(ITEM・EXPERIMENT_BEGIN/END・合計の行)も対象にする neutralize_machine_lines() { - sed -E -e 's/^([[:space:]]*)(VERDICT|RISK|REVIEWED_COMMIT|STAGE_DONE|NEW_CRITICAL|NEW_HIGH|NEW_CRITICAL_TOTAL|NEW_HIGH_TOTAL|UNVERIFIED|ROUNDS|ITEM|FINDING|LEDGER|LEDGER_ROUND|FIX_GATE|FINAL_VERDICT|ADVERSARIAL_STAGE|ADVERSARIAL_VERDICT|CI_WAIT|OFFICIAL_SECURITY):/\1> \2:/' \ + sed -E -e 's/^([[:space:]]*)(VERDICT|RISK|REVIEWED_COMMIT|STAGE_DONE|NEW_CRITICAL|NEW_HIGH|NEW_CRITICAL_TOTAL|NEW_HIGH_TOTAL|UNVERIFIED|ROUNDS|ITEM|FINDING|LEDGER|LEDGER_ROUND|FIX_GATE|FINAL_VERDICT|ADVERSARIAL_STAGE|ADVERSARIAL_VERDICT|CI_WAIT|OFFICIAL_SECURITY|REVIEW_COMPLETE):/\1> \2:/' \ -e 's/^([[:space:]]*)(EXPERIMENT_BEGIN|EXPERIMENT_END)([[:space:]]|$)/\1> \2\3/' } -# I193 7 回目 H2: 高リスク(パス判定 YES)で組み込みの /security-review が動かなかった(実行できず・読めていない)ら HIGH。 -# 公式のセキュリティレビューは高リスクのときの多層防御の 1 枚なので、抜けたまま OK にしない(止まる側) +# I212: /security-review に出させる判定の 1 行(official_security_verdict がこの行だけで「動いた」を決める) +SECURITY_FINDINGS_PROMPT='このレビューの出力の最後の行には、報告する脆弱性の件数を SECURITY_FINDINGS: <件数> の形(半角数字・0 件なら SECURITY_FINDINGS: 0)で必ず 1 行だけ出す。' + +# I193 7 回目 H2: 高リスク(パス判定 YES)で組み込みの /security-review が動かなかったら HIGH。 +# 公式のセキュリティレビューは高リスクのときの多層防御の 1 枚なので、抜けたまま OK にしない(止まる側)。 +# I212: 「動いた」は、出力に行全体が `SECURITY_FINDINGS: <数>` の行があるときだけ(文の形に頼らない。日本語の「指摘なし」を +# 読めずに FAILED にしていた・I200 ほか)。差分が無くて動かしていない回は OK。それ以外(実行できず・判定の行が無い)は HIGH official_security_verdict() { # $1=RISK_PATH(YES/NO) $2=組み込みの /security-review の出力 - if [ "$1" = "YES" ] && [[ "$2" == *"(実行できず"* || "$2" == *"(注意: 出力に指摘の配列も"* ]]; then - echo HIGH - else - echo OK - fi + [ "$1" = "YES" ] || { echo OK; return; } + [[ "$2" == *"(実行していない: 差分なし)"* ]] && { echo OK; return; } + # here-string で渡す(pipefail の下で grep -q が先に終わって 141 になる誤判定を避ける。official_output_readable と同じ・I193 L1) + if grep -qE '^SECURITY_FINDINGS:[[:space:]]*[0-9]+[[:space:]]*$' <<< "$2"; then echo OK; else echo HIGH; fi } # I193: PR で動くワークフローのジョブ名(jobs..name。無ければ id)を 1 行ずつ出す。 @@ -481,6 +488,10 @@ record_reviewed_commit() { # $1=ファイル grep -oE '^REVIEWED_COMMIT:[[:space:]]*[0-9a-f]{7,40}[[:space:]]*$' "$1" 2>/dev/null | tail -1 | grep -oE '[0-9a-f]{7,40}' || true } +# I212: 記録が「見終えた」か(REVIEW_COMPLETE: YES の行があれば exit 0)。行が無い古い記録は見終えていない扱い +# (起点にしない側=見直す範囲が広がる側に倒す) +record_complete() { grep -qE '^REVIEW_COMPLETE:[[:space:]]*YES[[:space:]]*$' "$1" 2>/dev/null; } + # LEDGER_ROUND: のある(=レビュー役が正常に終わった)コードレビュー記録を、ファイル名の時刻順に出す ledger_records() { # $1=I### local f @@ -489,19 +500,31 @@ ledger_records() { # $1=I### done < <(review_records "$1" code_review) return 0 } -# 台帳のある最新のコードレビューの記録(無ければ空)。review_mode と --route(I208)が使う +# 台帳のある最新のコードレビューの記録(無ければ空)。review_mode(台帳と周の数の引き継ぎ)と --route(I208)が使う latest_ledger_record() { ledger_records "$1" | tail -1; } # 全体か差分か: "full1" か "delta<起点の SHA><前の記録>"。 -# 差分にするのは、台帳のある最新の記録の REVIEWED_COMMIT が今の HEAD の祖先のときだけ(rebase などで祖先でなければ全体) -review_mode() { # $1=I### - local last n sha - last=$(latest_ledger_record "$1") +# 差分にするのは、台帳のある最新の記録の REVIEWED_COMMIT が今の HEAD の祖先のときだけ(rebase などで祖先でなければ全体)。 +# I212: 起点は、台帳のある記録のうち見終えた(record_complete)最新の記録の REVIEWED_COMMIT(HEAD の祖先のものだけ)。 +# 無ければブランチの分岐点(比べる相手との merge-base)=ブランチ全体を差分のレビューとして見直す。 +# 台帳と周の数(・<前の記録>)は、起点に関係なく台帳のある最新の記録から引き継ぐ(I206 の番号の合流を壊さない) +review_mode() { # $1=I### [$2=比べる相手(既定 origin/develop)] + local last n sha rec rec_sha i base="" recs=() + mapfile -t recs < <(ledger_records "$1") + last=""; [ "${#recs[@]}" -gt 0 ] && last="${recs[-1]}" if [ -n "$last" ]; then n=$(grep -oE '^LEDGER_ROUND:[[:space:]]*[0-9]+[[:space:]]*$' "$last" | tail -1 | grep -oE '[0-9]+' || true) sha=$(record_reviewed_commit "$last") if [ -n "$n" ] && [ -n "$sha" ] && git merge-base --is-ancestor "$sha" HEAD 2>/dev/null; then - printf 'delta\t%s\t%s\t%s\n' "$(( n + 1 ))" "$sha" "$last"; return 0 + # 新しい順に見て、最初に当たった記録で止める + for (( i = ${#recs[@]} - 1; i >= 0; i-- )); do + rec="${recs[i]}" + record_complete "$rec" || continue + rec_sha=$(record_reviewed_commit "$rec") + if [ -n "$rec_sha" ] && git merge-base --is-ancestor "$rec_sha" HEAD 2>/dev/null; then base="$rec_sha"; break; fi + done + [ -n "$base" ] || base=$(git merge-base "${2:-origin/develop}" HEAD 2>/dev/null) || base="" + if [ -n "$base" ]; then printf 'delta\t%s\t%s\t%s\n' "$(( n + 1 ))" "$base" "$last"; return 0; fi fi fi printf 'full\t1\t\t\n' @@ -964,7 +987,7 @@ PLAN_FILE=$(find_plan_file "$ISSUE") REVIEWED_HEAD=$(git rev-parse HEAD) # I197 §4.8: 全体のレビューか差分のレビューか(台帳のある最新の記録の REVIEWED_COMMIT が HEAD の祖先なら差分) -IFS=$'\t' read -r REVIEW_KIND LEDGER_N SINCE PREV_LEDGER_REC <<< "$(review_mode "$ISSUE")" +IFS=$'\t' read -r REVIEW_KIND LEDGER_N SINCE PREV_LEDGER_REC <<< "$(review_mode "$ISSUE" origin/develop)" SCOPE_FILE="$OFFICIAL_DIR/scope" # 前の周の台帳は、始めた時点で写しておく(以後に記録が変わっても、読んだ台帳を使う) PREV_LEDGER_FILE="" @@ -973,6 +996,9 @@ if [ "$REVIEW_KIND" = "delta" ]; then delta_files origin/develop "$SINCE" > "$SCOPE_FILE" || { echo "⛔ 差分のレビューの範囲を取れません(起点 ${SINCE})"; exit 1; } REVIEW_RANGE="${SINCE}...HEAD" echo "差分のレビュー(台帳の ${LEDGER_N} 周目・起点 ${SINCE}・範囲のファイル $(grep -c . "$SCOPE_FILE" || true) 本)" + if [ "$SINCE" != "$(record_reviewed_commit "$PREV_LEDGER_REC")" ]; then + echo "(前の記録 ${PREV_LEDGER_REC} は見終えていない〔REVIEW_COMPLETE: NO〕ため、見終えた最新の記録〔無ければブランチの分岐点〕から見直します)" + fi else REVIEW_RANGE="origin/develop...HEAD" echo "全体のレビュー(台帳の 1 周目)" @@ -1042,7 +1068,7 @@ else echo "(注意: 手元の origin/HEAD が origin/develop を指していないため、/security-review が develop 以外と比べた可能性がある)" > "$OFFICIAL_DIR/security_note" fi echo "組み込みの /security-review を背景で実行しています..." - run_official_review "/security-review" > "$OFFICIAL_DIR/security" & + run_official_review "/security-review" "$SECURITY_FINDINGS_PROMPT" > "$OFFICIAL_DIR/security" & OFFICIAL_PIDS+=("$!") fi fi @@ -1093,7 +1119,7 @@ $(cat "$OFFICIAL_DIR/security" 2>/dev/null || echo "(実行できず: 結果の fi SECURITY_VERDICT=$(official_security_verdict "$RISK_PATH" "$OFFICIAL_SECURITY") if [ "$SECURITY_VERDICT" != "OK" ]; then - echo "⛔ 高リスクの変更で、組み込みの /security-review が動きませんでした(実行できず・読めていない)。HIGH として記録します。" + echo "⛔ 高リスクの変更で、組み込みの /security-review が動きませんでした(実行できず・判定の行 SECURITY_FINDINGS が無い)。HIGH として記録します。" echo "OFFICIAL_SECURITY: FAILED" GATE_EVIDENCE+="- ⛔ 高リスク(パス判定 YES)で組み込みの /security-review が動かなかった(HIGH。直すものは無いので review-gate が 1 回だけ実行し直す)"$'\n' fi @@ -1190,7 +1216,7 @@ else fi # 出力正規化 -REVIEW_CLEAN=$(printf '%s\n' "$REVIEW" | sed -E 's/[[:space:]]*$//; s/^([[:space:]]*)(LEDGER|LEDGER_ROUND):/\1> \2:/') +REVIEW_CLEAN=$(printf '%s\n' "$REVIEW" | sed -E 's/[[:space:]]*$//; s/^([[:space:]]*)(LEDGER|LEDGER_ROUND|REVIEW_COMPLETE):/\1> \2:/') # ファイル保存。判定(RISK・VERDICT)は自作のレビュー役の本文だけで行い、組み込みのレビューの出力は判定の後に足す # (組み込みの出力の中の表の「| High |」などを、VERDICT 行が無いときの代わりの判定が拾わないように。I193 M6) @@ -1230,6 +1256,12 @@ if [ "$REVIEWER_RAN" = "1" ] && [ "$LLM_VERDICT" != "OK" ] && ! grep -qE '^FINDI GATE_EVIDENCE+="- ⛔ code-reviewer の VERDICT は ${LLM_VERDICT} なのに、読める FINDING 行が無い(BLOCKER)"$'\n' fi GATE_EVIDENCE+="- 台帳(${REVIEW_KIND}・${LEDGER_N} 周目): ${LEDGER_VERDICT}(code-reviewer の VERDICT=${LLM_VERDICT}・合否に使わない)"$'\n' +# I212: レビュー役と(高リスクなら)組み込みの /security-review が範囲を見終えたか。YES の記録だけが次の差分のレビューの起点になる +# (台帳に止める指摘が残った HIGH は見終えているので YES。/security-review が動かなかった・レビュー役の出力が壊れた・台帳を作れなかったは NO。 +# 組み込みの /code-review は合否に使わない参考なので条件に入れない) +REVIEW_COMPLETE=NO +if [ "$REVIEW_OK" = "1" ] && [ "$REVIEWER_VERDICT" = "OK" ] && [ "$SECURITY_VERDICT" = "OK" ]; then REVIEW_COMPLETE=YES; fi +GATE_EVIDENCE+="- 見終えたか(次の差分のレビューの起点にできるか・I212): ${REVIEW_COMPLETE}"$'\n' # I084: 決定論ゲート結果で VERDICT を決定論的に上書きする(LLM 出力非依存)。 # FINAL = max(gate, omission, 台帳, code-reviewer の出力の壊れ, 組み込みのセキュリティ)。証跡を記録先頭へ注入し末尾 VERDICT 行を FINAL に書換え。 @@ -1239,6 +1271,8 @@ inject_gate_result "$REVIEW_FILE" "$GATE_EVIDENCE" "$OMISSION_VERDICT" "$FINAL_V set_risk_line "$REVIEW_FILE" "$RISK_FINAL" # I193(L13): このレビューが見た HEAD を記録する(gate-check.sh が読む) add_reviewed_commit "$REVIEW_FILE" "$REVIEWED_HEAD" +# I212: 見終えたか(review_mode が次の起点を選ぶのに読む)。組み込みのレビューの出力(無効にした行)より前に置く +printf 'REVIEW_COMPLETE: %s\n' "$REVIEW_COMPLETE" >> "$REVIEW_FILE" # 判定の後に、組み込みのレビューの出力を参考として足す(機械が読む行は neutralize_machine_lines で無効にしてある) printf '\n## 組み込みのレビューの出力(公式・参考)\n\n### 組み込み /code-review の指摘\n\n%s\n\n### 組み込み /security-review の指摘\n\n%s\n' \ "$OFFICIAL_CODE" "$OFFICIAL_SECURITY" >> "$REVIEW_FILE" diff --git a/scripts/claude/tests/test_i193_official_flow.sh b/scripts/claude/tests/test_i193_official_flow.sh index 91e0a11c..8068d321 100644 --- a/scripts/claude/tests/test_i193_official_flow.sh +++ b/scripts/claude/tests/test_i193_official_flow.sh @@ -350,7 +350,7 @@ ck "C12 claude の設定の警告行は除く" "本文" "$(printf 'Permission al # C23: 高リスク(パス判定 YES)で組み込みの /security-review が動かなかったら HIGH(I193 7 回目 H2) ck "C23 YES で「実行できず」→ HIGH" HIGH "$(official_security_verdict YES '(実行できず: exit=124。124 は時間切れ)')" ck "C23 YES で「読めていない」注意 → HIGH" HIGH "$(official_security_verdict YES $'(注意: 出力に指摘の配列も「指摘なし」も無く、対象を読めていない可能性がある。この出力は判定の根拠にしない)\n本文')" -ck "C23 YES で読めた結果 → OK" OK "$(official_security_verdict YES '確信度の高い脆弱性は見つかりませんでした。')" +ck "C23 YES で読めた結果(I212: 判定の行 SECURITY_FINDINGS つき)→ OK" OK "$(official_security_verdict YES $'確信度の高い脆弱性は見つかりませんでした。\nSECURITY_FINDINGS: 0')" ck "C23 NO なら(実行していない)→ OK" OK "$(official_security_verdict NO '(実行していない: パス判定 NO)')" ck "C23 FINAL の合成に組み込みのセキュリティの判定が入る(I197: 台帳の合否も)" 0 "$(rc grep -qF 'FINAL_VERDICT=$(combine_verdict "$GATE_VERDICT" "$OMISSION_VERDICT" "$LEDGER_VERDICT" "$REVIEWER_VERDICT" "$SECURITY_VERDICT")' scripts/claude/code-review.sh)" ck "C23 組み込みの出力の OFFICIAL_SECURITY: 行は行頭から外す" "> OFFICIAL_SECURITY: FAILED" "$(printf 'OFFICIAL_SECURITY: FAILED\n' | neutralize_machine_lines)" diff --git a/scripts/claude/tests/test_i197_review_ledger.sh b/scripts/claude/tests/test_i197_review_ledger.sh index f8c6c93c..ba9e9dbf 100644 --- a/scripts/claude/tests/test_i197_review_ledger.sh +++ b/scripts/claude/tests/test_i197_review_ledger.sh @@ -52,10 +52,11 @@ rec 20260101_0000 "# 古い形式 REVIEWED_COMMIT: ${SINCE} VERDICT: HIGH" ck "R1 LEDGER_ROUND の無い(古い形式・失敗した周)記録だけ → full" "full|1||" "$(inR lib "$CR" review_mode I900 | tr '\t' '|')" -rec 20260101_0001 "# 台帳あり +rec 20260101_0001 "# 台帳あり(I212: 起点になるのは見終えた記録だけ) LEDGER_ROUND: 2 REVIEWED_COMMIT: ${SINCE} -VERDICT: HIGH" +VERDICT: HIGH +REVIEW_COMPLETE: YES" ck "R1 LEDGER_ROUND: 2・祖先の REVIEWED_COMMIT → delta・3・起点" "delta|3|${SINCE}" "$(inR lib "$CR" review_mode I900 | cut -f1-3 | tr '\t' '|')" rec 20260101_0002 "# 祖先でない LEDGER_ROUND: 3 @@ -198,7 +199,7 @@ ck "R11 code-reviewer に FINDING 行の形・review-gate に差分のレビュ # ---------------- FG ---------------- ck "FG-R1 「台帳があれば delta」を消すと R1 が不合格(delta にならない)" full \ - "$(inR lib "$(broken 'printf '"'"'delta\t%s\t%s\t%s\n'"'"' "$(( n + 1 ))" "$sha" "$last"; return 0' ':')" review_mode I900 | cut -f1)" + "$(inR lib "$(broken 'printf '"'"'delta\t%s\t%s\t%s\n'"'"' "$(( n + 1 ))" "$base" "$last"; return 0' ':')" review_mode I900 | cut -f1)" B3=$(broken 'sys.exit(2) out.append' 'pass out.append') diff --git a/scripts/claude/tests/test_i212_review_base.sh b/scripts/claude/tests/test_i212_review_base.sh new file mode 100644 index 00000000..df38059f --- /dev/null +++ b/scripts/claude/tests/test_i212_review_base.sh @@ -0,0 +1,180 @@ +#!/usr/bin/env bash +# I212(plan_I212.md §4-5・auto_test TC-02・TC-03): 差分のレビューの起点を「見終えた最新の記録」にする・/security-review の判定の 1 行。 +# U: code-review.sh の関数(review_mode・official_security_verdict・neutralize_machine_lines) +# E: 本体の通し(gh・claude はスタブ)と gate-check(I211・I213・I200 の再現) +# FG: 判定の要を一時コピーで壊すと、対応する項目が不合格になること +# 実行: bash scripts/claude/tests/test_i212_review_base.sh +# shellcheck disable=SC2016 # 単一引用符内の $ は文字どおりの照合用 +# shellcheck disable=SC1090,SC1091 # code-review.sh(と壊した写し)は関数だけを読み込む +set -uo pipefail +REPO_ROOT="$(git rev-parse --show-toplevel)" +cd "$REPO_ROOT" || exit 1 +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT +export GIT_AUTHOR_NAME=t GIT_AUTHOR_EMAIL=t@example.invalid GIT_COMMITTER_NAME=t GIT_COMMITTER_EMAIL=t@example.invalid + +pass=0; fail=0 +ck() { if [ "$2" = "$3" ]; then printf 'PASS %s\n' "$1"; pass=$((pass+1)); + else printf 'FAIL %s expected=%s got=%s\n' "$1" "$2" "$3"; fail=$((fail+1)); fi; } +yn() { if "$@" >/dev/null 2>&1; then echo yes; else echo no; fi; } +CR="$REPO_ROOT/scripts/claude/code-review.sh" +GC="$REPO_ROOT/scripts/claude/gate-check.sh" + +# lib <関数と引数…>: 関数だけを読み込んで 1 つ呼ぶ(cwd はそのまま) +lib() { local src="$1"; shift; REVIEW_LIB_SOURCE_ONLY=1 bash -c '. "$1"; shift; "$@"' _ "$src" "$@"; } +# 壊した写し: <置き換え前> <置き換え後>(置き換え前が無ければ印を残して最後に不合格) +broken() { local f="$TMP/b_$RANDOM$RANDOM.sh" + python3 -I -c 'import sys +s = open(sys.argv[1], encoding="utf-8").read() +if sys.argv[3] not in s: sys.exit(1) +open(sys.argv[2], "w", encoding="utf-8").write(s.replace(sys.argv[3], sys.argv[4]))' "$CR" "$f" "$1" "$2" || : > "$TMP/broken_failed" + echo "$f"; } + +# ---------------- U: 起点の選び方(一時のリポジトリ・記録はファイルを置くだけ) ---------------- +R="$TMP/repo"; mkdir -p "$R/docs/reviews" "$R/scripts" +git -C "$R" init -q -b develop +printf 'a\n' > "$R/scripts/a.sh"; git -C "$R" add -A; git -C "$R" commit -qm base +git -C "$R" checkout -q -b elsewhere; printf 'z\n' > "$R/z.txt"; git -C "$R" add -A; git -C "$R" commit -qm "別の枝" +NOT_ANC=$(git -C "$R" rev-parse HEAD) +git -C "$R" checkout -q -b feature/I900-x develop +printf 'a2\n' >> "$R/scripts/a.sh"; git -C "$R" add -A; git -C "$R" commit -qm c1; C1=$(git -C "$R" rev-parse HEAD) +printf 'a3\n' >> "$R/scripts/a.sh"; git -C "$R" add -A; git -C "$R" commit -qm c2; C2=$(git -C "$R" rev-parse HEAD) +MB=$(git -C "$R" merge-base develop HEAD) +RD=docs/reviews/I900_code_review_20260101 +# mk <番号> <周> [VERDICT]: 台帳のある記録を置く +mk() { local c=""; [ "$3" = "-" ] || c="REVIEW_COMPLETE: $3" + printf '# r\nLEDGER_ROUND: %s\nREVIEWED_COMMIT: %s\nRISK: NO\nVERDICT: %s\n%s\n' "$2" "$4" "${5:-OK}" "$c" > "$R/${RD}_$1.md"; } +clear_recs() { rm -f "$R"/docs/reviews/I900_code_review_*.md; } +mode() { (cd "$R" && lib "${2:-$CR}" review_mode I900 "${1:-develop}" | tr '\t' '|'); } + +ck "U0 記録が無い → full" "full|1||" "$(mode)" +mk 0001 1 YES "$C1"; mk 0002 2 NO "$C2" HIGH +ck "U1 見終えた A(c1)→ 見終えていない B(c2): 起点は A・周と前の記録は B" "delta|3|${C1}|${RD}_0002.md" "$(mode)" +mk 0003 3 NO "$C2" HIGH +ck "U2 見終えていない回が 2 つ続いても A まで遡る" "delta|4|${C1}|${RD}_0003.md" "$(mode)" +mk 0001 1 NO "$C1" +ck "U3 見終えた記録が無い → ブランチの分岐点が起点(台帳と周は最新から)" "delta|4|${MB}|${RD}_0003.md" "$(mode)" +ck "U3 比べる相手が無く、見終えた記録も無い → full" "full|1||" "$(mode nosuch-branch)" +clear_recs; mk 0001 1 YES "$C1"; mk 0002 2 YES "$C2" HIGH +ck "U4 指摘が残って HIGH でも見終えた回は起点(今までどおり)" "delta|3|${C2}|${RD}_0002.md" "$(mode)" +clear_recs; mk 0001 1 YES "$C1"; mk 0002 2 - "$C2" OK +ck "U5 REVIEW_COMPLETE の行が無い記録は起点にしない" "delta|3|${C1}|${RD}_0002.md" "$(mode)" +clear_recs; mk 0001 1 YES "$NOT_ANC"; mk 0002 2 NO "$C2" HIGH +ck "U6 見終えた記録の SHA が HEAD の祖先でなければ起点にしない" "delta|3|${MB}|${RD}_0002.md" "$(mode)" +clear_recs; mk 0001 1 YES "$C1"; mk 0002 2 YES "$NOT_ANC" +ck "U6 最新の記録の SHA が祖先でない(rebase の後)→ full のまま" "full|1||" "$(mode)" + +# ---------------- U7: /security-review の判定の 1 行 ---------------- +osv() { lib "${3:-$CR}" official_security_verdict "$1" "$2"; } +ck "U7 YES+最後の行 SECURITY_FINDINGS: 0 → OK" OK "$(osv YES $'報告する脆弱性はありません。\n\nSECURITY_FINDINGS: 0')" +ck "U7 YES+SECURITY_FINDINGS: 2 → OK" OK "$(osv YES $'[ {"file": "a.sh"} ]\nSECURITY_FINDINGS: 2')" +ck "U7 YES+日本語の「指摘なし」の文だけ → HIGH" HIGH "$(osv YES '報告する脆弱性はありません。')" +ck "U7 YES+判定の行の後に締めの文 → OK" OK "$(osv YES $'SECURITY_FINDINGS: 0\n\n以上です。')" +ck "U7 YES+文の中に語があるだけ(行全体ではない)→ HIGH" HIGH "$(osv YES '最後に SECURITY_FINDINGS: 0 と書く決まりです。')" +ck "U7 YES+実行できず → HIGH" HIGH "$(osv YES '(実行できず: exit=124。124 は時間切れ)')" +ck "U7 YES+差分なしで動かしていない → OK" OK "$(osv YES '(実行していない: 差分なし)')" +ck "U7 NO → OK" OK "$(osv NO '(実行していない: パス判定 NO)')" +ck "U7 判定の行があれば「読めた結果」(記録に「読めていない」の注意を付けない)" yes \ + "$(yn lib "$CR" official_output_readable $'報告する脆弱性はありません。\nSECURITY_FINDINGS: 0')" + +# ---------------- U8: 偽の行を無効にする ---------------- +ck "U8 neutralize_machine_lines が REVIEW_COMPLETE を無効にする" "> REVIEW_COMPLETE: YES" "$(printf 'REVIEW_COMPLETE: YES\n' | lib "$CR" neutralize_machine_lines)" + +# ---------------- E: 本体の通し(gh・claude はスタブ) ---------------- +ES="$TMP/e2e_stub"; mkdir -p "$ES" +printf 'jobs:\n j:\n name: J\n' > "$ES/wf.yml" +cat > "$ES/gh" <<'SH' +#!/usr/bin/env bash +case "$*" in + "pr list"*) echo 7 ;; + *headRefOid*) git rev-parse HEAD ;; + "pr checks"*"--json"*) printf 'pass\tJ\n' ;; + *) exit 0 ;; +esac +SH +cat > "$ES/claude" <<'PY' +#!/usr/bin/env python3 +# code-reviewer(--agent)には $E2E_REVIEW を stream-json で返す。/security-review には $E2E_SEC(無ければ判定の行つきの文)、 +# /code-review には「指摘なし」を返す。呼ばれた種類(agent / security / code)を $E2E_CALLS に 1 行ずつ書く +import json, os, sys +args = sys.argv[1:] +kind = "agent" if "--agent" in args else "security" if "/security-review" in args else "code" +with open(os.environ["E2E_CALLS"], "a") as f: + f.write(kind + "\n") +if kind == "agent": + sys.stdin.read() + body = open(os.environ["E2E_REVIEW"]).read() + print(json.dumps({"type": "system", "subtype": "init", "session_id": "s"})) + print(json.dumps({"type": "result", "subtype": "success", "result": body, "permission_denials": []})) +elif kind == "security": + sec = os.environ.get("E2E_SEC") + print(open(sec).read() if sec else "報告する脆弱性はありません。\nSECURITY_FINDINGS: 0") +else: + print("指摘なし") +PY +chmod +x "$ES/gh" "$ES/claude" +printf '報告する脆弱性はありません。\n' > "$ES/sec_no_line" +B_NONE='# r\n\n## 台帳\n指摘なし\n\nVERDICT: OK\n' +# setup <名前> []: 一時のリポジトリ(feature で a.py と .github/w.yml を変える=パス判定 YES)。計画レビューの判定は No +setup() { local E="$TMP/e_$1" O="$TMP/e_$1.git" + mkdir -p "$E/scripts/claude" "$E/docs/issues/open" "$E/docs/reviews" "$E/.github" + cp "${2:-$CR}" "$E/scripts/claude/code-review.sh"; cp "$REPO_ROOT/scripts/claude/review-process-lib.sh" "$GC" "$E/scripts/claude/" + printf '# I900\n' > "$E/docs/issues/open/I900.md"; printf 'l1\n' > "$E/a.py"; printf 'x: 1\n' > "$E/.github/w.yml" + printf '# 計画レビュー\n\n## 高リスク判定\n判定: No\n' > "$E/docs/reviews/I900_plan_review_20260101_0000.md" + git init -q --bare "$O"; git -C "$E" init -q -b develop; git -C "$E" add -A; git -C "$E" commit -qm base + git -C "$E" remote add origin "$O"; git -C "$E" push -q origin develop 2>/dev/null + git -C "$E" checkout -q -b feature/I900-x; printf 'l2\n' >> "$E/a.py"; printf 'x: 2\n' > "$E/.github/w.yml" + git -C "$E" add -A; git -C "$E" commit -qm change; } +# round <名前> <周の名前> [環境変数…]: 本体を丸ごと動かす。出力は $TMP/out_<周>、呼び出しは $TMP/calls_<周>、記録の名前を出す +round() { local E="$TMP/e_$1" r="$2"; shift 2; printf '%b' "$B_NONE" > "$TMP/review_$r.txt"; : > "$TMP/calls_$r" + (cd "$E" && env PATH="$ES:$PATH" CLAUDE_BIN="$ES/claude" E2E_REVIEW="$TMP/review_$r.txt" E2E_CALLS="$TMP/calls_$r" \ + CI_WORKFLOWS="$ES/wf.yml" CI_WAIT_TIMEOUT=5 CI_POLL_INTERVAL=1 "$@" bash scripts/claude/code-review.sh I900 > "$TMP/out_$r" 2>&1) + (cd "$E" && lib "$CR" review_records I900 code_review | tail -1 | sed "s#^#$E/#"); } +fv() { sed -n 's/^FINAL_VERDICT: //p' "$TMP/out_$1"; } +called() { grep -qx "$2" "$TMP/calls_$1"; } +# rename_rec <名前> <記録> <番号>: 同じ分に何度も動かすので、記録の名前の時刻を <番号> にずらしてコミットし、新しい名前を出す +rename_rec() { local to="${2%_*}_$3.md"; mv "$2" "$to"; git -C "$TMP/e_$1" add -A; git -C "$TMP/e_$1" commit -qm "rename $3"; echo "$to"; } +record_only() { printf 'x\n' >> "$TMP/e_$1/docs/issues/open/I900.md"; git -C "$TMP/e_$1" add -A; git -C "$TMP/e_$1" commit -qm "record only"; } +# 敵対的レビューの記録(パス判定 YES の gate-check が求める)を今の HEAD で置いてコミットする +adv_ok() { local E="$TMP/e_$1" + printf 'REVIEWED_COMMIT: %s\nROUNDS: 1\nVERDICT: OK\n' "$(git -C "$E" rev-parse HEAD)" > "$E/docs/reviews/I900_adversarial_review_20260101_0009.md" + git -C "$E" add -A; git -C "$E" commit -qm adv; } +gate() { (cd "$TMP/e_$1" && GATE_BASE=origin/develop bash scripts/claude/gate-check.sh I900 2>&1 | grep -E '^GATE_CHECK:' | tail -1); } + +setup s +S1=$(round s s1 E2E_SEC="$ES/sec_no_line") +ck "E1 1 周目: /security-review が判定の行を出さない → FINAL HIGH・REVIEW_COMPLETE: NO" "HIGH yes" "$(fv s1) $(yn grep -qx 'REVIEW_COMPLETE: NO' "$S1")" +ck "E1 1 周目の記録では gate-check が REASON=verdict" "GATE_CHECK: FAIL REASON=verdict" "$(gate s)" +S1=$(rename_rec s "$S1" 0001) +S2=$(round s s2) +ck "E1 2 周目(記録の場所の外は変えていない): 範囲 0 本・引き継ぎにならない" "no no" \ + "$(yn grep -qF '範囲のファイル 0 本' "$TMP/out_s2") $(yn grep -qF '呼ばずに台帳を引き継ぎます' "$TMP/out_s2")" +ck "E1 2 周目: レビュー役と /security-review が呼ばれる" "yes yes" "$(yn called s2 agent) $(yn called s2 security)" +ck "E1 2 周目: FINAL OK・REVIEW_COMPLETE: YES・LEDGER_ROUND: 2" "OK yes yes" \ + "$(fv s2) $(yn grep -qx 'REVIEW_COMPLETE: YES' "$S2") $(yn grep -qx 'LEDGER_ROUND: 2' "$S2")" +S2=$(rename_rec s "$S2" 0002) +adv_ok s +ck "E1 2 周目の記録(と敵対的レビューの記録)で gate-check が OK" "GATE_CHECK: OK" "$(gate s)" +record_only s +S3=$(round s s3) +ck "E2 見終えた記録の後に記録の場所だけ変えた 3 周目: 範囲 0 本・claude を呼ばない・REVIEW_COMPLETE: YES" "yes 0 yes" \ + "$(yn grep -qF '範囲のファイル 0 本' "$TMP/out_s3") $(grep -c . "$TMP/calls_s3") $(yn grep -qx 'REVIEW_COMPLETE: YES' "$S3")" + +# ---------------- FG ---------------- +B_U1=$(broken 'record_complete "$rec" || continue' ':') +ck "FG-U1 見終えたかを見ないと U1 が不合格(起点が見終えていない B の c2 になる)" "delta|3|${C2}|${RD}_0002.md" \ + "$(clear_recs; mk 0001 1 YES "$C1"; mk 0002 2 NO "$C2" HIGH; mode develop "$B_U1")" +setup f "$B_U1" +F1=$(round f f1 E2E_SEC="$ES/sec_no_line"); rename_rec f "$F1" 0001 >/dev/null +round f f2 >/dev/null +ck "FG-E1 起点を最新の記録に戻すと E1 が不合格(2 周目が範囲 0 本・レビュー役を呼ばない)" "yes no" \ + "$(yn grep -qF '範囲のファイル 0 本' "$TMP/out_f2") $(yn called f2 agent)" +B_U7=$(broken "grep -qE '^SECURITY_FINDINGS:[[:space:]]*[0-9]+[[:space:]]*\$'" "grep -qE 'SECURITY_FINDINGS:[[:space:]]*[0-9]+'") +ck "FG-U7 行全体の固定を外すと U7 が不合格(文の中の語で OK になる)" OK "$(osv YES '最後に SECURITY_FINDINGS: 0 と書く決まりです。' "$B_U7")" +B_U8=$(broken '|OFFICIAL_SECURITY|REVIEW_COMPLETE):/' '|OFFICIAL_SECURITY):/') +ck "FG-U8 無効にする一覧から外すと U8 が不合格" "REVIEW_COMPLETE: YES" "$(printf 'REVIEW_COMPLETE: YES\n' | lib "$B_U8" neutralize_machine_lines)" +ck "FG の写しがすべて作れた(置き換え前の文字列が本体にある)" no "$(yn test -e "$TMP/broken_failed")" + +echo "---" +echo "pass=${pass} fail=${fail}" +[ "$fail" -eq 0 ] diff --git a/scripts/claude/tests/test_i223_gh_app.sh b/scripts/claude/tests/test_i223_gh_app.sh index f931d86a..7537323f 100644 --- a/scripts/claude/tests/test_i223_gh_app.sh +++ b/scripts/claude/tests/test_i223_gh_app.sh @@ -110,7 +110,7 @@ PY # G10(I225): 決まりの置き場(CLAUDE.md・docs/runbooks・.claude/skills・.claude/agents・scripts/claude〔tests を除く〕・ # docs/*/templates)に、鍵を画面に出す書き方(--json なし、または -t/--show-token 付きの auth status・auth token)が無い。 -# auth status は、出てくるたびに、そのあとから行末か次のバッククォートまでの語を見る(行末の \ で続く行はつなぐ・I226)。auth token は鍵を丸ごと出すので、語を問わず不合格(I226)。個々のイシューの記録は対象外(plan_I225.md §9) +# auth status は、出てくるたびに、そのあとから行末か次のバッククォートまでの語を見る(行末の \ で続く行はつなぐ・I226)。auth token は鍵を丸ごと出すので、語を問わず不合格(I226)。語の境目は英数字だけで見る(和文がすぐ続いても当たる・I212)。個々のイシューの記録は対象外(plan_I225.md §9) g10() { # g10 <ルート> python3 -I - "$1" <<'PY' import os, re, sys @@ -136,7 +136,7 @@ for f in files: text = open(f, encoding="utf-8").read() except (UnicodeDecodeError, OSError): continue - for m in re.finditer(r"gh auth (status|token)\b", text): + for m in re.finditer(r"gh auth (status|token)(?![A-Za-z0-9_])", text): # 次のバッククォートか、行末の \ の付かない改行までを見る(\ で続く行はつなぐ) rest = re.split(r"`|(?