From 1a723784b21dbde603657b7e81911367a6254bda Mon Sep 17 00:00:00 2001 From: aloekun Date: Mon, 3 Aug 2026 05:44:18 +0900 Subject: [PATCH 1/3] =?UTF-8?q?docs(harness-plan):=20WP-17=20=E3=81=AE?= =?UTF-8?q?=E5=AE=9F=E8=A1=8C=E7=8A=B6=E6=B3=81=E3=81=A8=E5=86=8D=E5=88=86?= =?UTF-8?q?=E5=89=B2=E8=A8=88=E7=94=BB=20(2a/2b/2c)=20=E3=82=92=E5=8F=8D?= =?UTF-8?q?=E6=98=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ここまでの実行状況を master 版計画書へ反映し、WP-17 PR 2 の再分割を別セッションの Opus がドキュメントのみで実施できる自己完結記述にする。 反映した実行状況: - PR 1 (ADR-066 / #347): 完了。exe 単体 drill 充足済、観測中への遷移は 2c 後 - PR 2: 実装済・未 land。incident (gut-revert) と事前整備 2 本 (ADR-068 / #348、ADR-069 / #349 = マージ済) の経緯を 3 行で要約 - Actions variable AUTONOMY_ENABLED 設定済みの事実と、2c マージで Phase B が 即 live になる帰結 (マージ前の variable 削除推奨) を明記 再分割計画 (2a/2b/2c) の要点: - 資産表: ローカル jj コミット 8 件を change_id で列挙 (rebase 耐性のため commit_id ではなく change_id を正とする旨を明記)、実在確認コマンド付き - 2a: 計画書 (本コミット) + rename パーサ修正。mxzwmsyp は gut-revert 混入のため rebase 不可 — tests.rs は restore、diff.rs は #348 の pub(crate) 変更と重なるため hunk 単位の手適用、という回収手順を明記 - 2b: lib 抽出 + cli-fix-push-gate を同一 PR (ADR-044 充足 = incident の失敗の裏返し)。 ADR-069 chain 宣言の本文を計画書に事前記載し、2b の diff に載せる手順と 「初回 dogfood の実測結果を ADR-069 へ記帳」まで指示 - 2c: workflow + config 有効化 + ADR-067。マージ後の実走スモーク段 0-2 (ユーザー 操作込み) を順序付きで記載 - jj 運用の注意: describe 適用先事故 (本セッションで 3 回発生) の予防として 「編集前に必ず jj new」を明記。ADR-016 / ADR-028 / VSCode での draft 提示方法も記載 受け入れ基準を再分割構成に合わせて更新 (PR 1 / 2b drill = 充足済、2c = 実走スモーク)。 本コミットは未 push (master の local 子)。再分割 2a の先頭コミットとして次セッションが 流用する (計画書 § 2a 手順 1 に記載)。 Co-Authored-By: Claude Opus 5 (1M context) --- docs/harness-improvement-plan.md | 92 +++++++++++++++++++++++++------- 1 file changed, 74 insertions(+), 18 deletions(-) diff --git a/docs/harness-improvement-plan.md b/docs/harness-improvement-plan.md index 32ddda85..87f1a6c5 100644 --- a/docs/harness-improvement-plan.md +++ b/docs/harness-improvement-plan.md @@ -76,7 +76,7 @@ Anthropic 公式のハーネスエンジニアリング指針(決定論的基 | WP-14 | 3 | PowerShell 3 本の Rust 化 | S-M ×2 | なし | 完了(新規 ADR 不要判断 = 決定は各 crate doc + commit message に記録。実走確認済) | | WP-15 | 3 | Linux バイナリビルド + クラウド setup script | M | WP-13, 14 | 完了([ADR-063](adr/adr-063-linux-portability-release-binaries.md)。クラウド実測は [ADR-060](adr/adr-060-cloud-harness-sessionstart-dispatcher.md) dogfood で達成、以降は ADR-060 の bounded lifetime で管理。追補の陽性証拠設計は [ADR-064](adr/adr-064-monitor-success-positive-evidence.md) → park 実観測は § 残作業) | | WP-16 | 3 | CI matrix(移植退行防止) | S | WP-13, 14 | 観測中([ADR-065](adr/adr-065-ci-matrix-cross-os-regression.md)。2 OS matrix は PR #342 でマージ済・master 稼働中、初回観測期間に実バグ 1 件捕捉(PR #344 で修正)。観測継続と required check 化は → § 残作業) | -| WP-17 | 4 | イベント駆動バックボーン完成(Phase B + routines 移行 + 全体 kill-switch 前倒し) | M-L | WP-09, 10, 11(2026-08-02 充足確認済) | 着手中(PR 1 実装済 = [ADR-066](adr/adr-066-autonomy-global-kill-switch.md)。PR 2-4 未着手 → § WP-17) | +| WP-17 | 4 | イベント駆動バックボーン完成(Phase B + routines 移行 + 全体 kill-switch 前倒し) | M-L | WP-09, 10, 11(2026-08-02 充足確認済) | 着手中(PR 1 = [ADR-066](adr/adr-066-autonomy-global-kill-switch.md) #347 マージ済。PR 2 は incident を経て再分割 2a/2b/2c で実施 → § WP-17 PR 2。事前整備 [ADR-068](adr/adr-068-fix-step-authority-boundary.md) #348 / [ADR-069](adr/adr-069-pr-chain-declaration.md) #349 マージ済) | | WP-18 | 4 | 夜間 todo 消化ループ | M-L | WP-15, 17 | 未着手 | | WP-19 | 4 | 常時性ガード(自主減速 / 監査ループ。全体 kill-switch は WP-17 PR 1 へ前倒し) | S-M | WP-18 | 未着手 | @@ -117,27 +117,81 @@ Anthropic 公式のハーネスエンジニアリング指針(決定論的基 3. Claude GitHub App は未インストール。ユーザーがインストールする方針(確認済み)。routine 移行(PR 4)はユーザーの Web UI 作業とセットのため最後に回す。 - **PR 分割**: 1 WP = 原則 1 PR からの明示的逸脱(kill-switch 前倒しにより 1 PR に収まらない)。PR 1 → 2 → 3 → 4 の順で依存する。 -#### WP-17 PR 1: 全体 kill-switch(WP-19 ステップ 1 前倒し分) — 実装済(2026-08-02) +#### WP-17 PR 1: 全体 kill-switch(WP-19 ステップ 1 前倒し分) — 完了(PR #347、2026-08-02 マージ) -設計・決定・検証記録はすべて [ADR-066](adr/adr-066-autonomy-global-kill-switch.md) へ移管済み(§ 0 の知識移管 3 ステップ)。本節は残作業のみを保持する。 +設計・決定・検証記録はすべて [ADR-066](adr/adr-066-autonomy-global-kill-switch.md) へ移管済み。成果物: `cli-autonomy-gate`(unit test 21 件 + drill 8 シナリオ実測済)、`autonomy-config.toml`(`enabled = false`)、`pnpm autonomy-status`。exe 単体の kill-switch drill は充足済(ADR-066 § 検証記録)。`観測中` への遷移は Phase B(再分割 2c)稼働後、ADR-066 の bounded lifetime(3〜5 run、期限 2026-11-02)が管理する。 -- **成果物**: `cli-autonomy-gate`(純粋判定コア + config/env 読み取り + loud 出力、unit test 21 件)、`autonomy-config.toml`(`[autonomy] enabled`、初期値 `false`)、`templates/autonomy-config.toml`、`pnpm autonomy-status`。 -- **確定した設計**(詳細は ADR-066): 正極性の単一フラグを repo config と外部フラグ `AUTONOMY_ENABLED` の AND で評価。負極性は不採用。背圧契約は操作クラス別で `draft-pr` は WP-18 まで構造的に deny。deny は loud + ADR-055 telemetry。exit コードは 0/1/2 で呼び手は非ゼロを全て拒否として扱う。 -- **PR 2 へ引き渡す前提**: +- **再分割 2b/2c へ引き渡す前提**(原文は #347 時点の記載、現在も有効): 1. workflow 式 `if: ${{ vars.AUTONOMY_ENABLED == 'true' }}` と exe 呼び出しの二層を接続する。 - 2. CI では `autonomy-config.toml` を **master ref から取り出した写し**として `--config` へ渡す(PR ブランチの checkout を渡すと自律 actor が自己解除できる。ADR-066 § 決定 3)。exe はパスの出所を検証できないため workflow 側の契約。 - 3. `autonomy-config.toml` の `enabled` を `true` へ倒すのは PR 2(呼び手と drill が揃ってから)。 + 2. CI では `autonomy-config.toml` を **master ref から取り出した写し**として `--config` へ渡す(PR ブランチの checkout を渡すと自律 actor が自己解除できる。ADR-066 § 決定 3)。 + 3. `autonomy-config.toml` の `enabled` を `true` へ倒すのは呼び手と drill が揃う 2c。 + 4. GitHub 側の Actions variable `AUTONOMY_ENABLED` = `true`(小文字ちょうど。workflow 式は完全一致)は**設定済み**(2026-08-02 ユーザー作業。Settings → Secrets and variables → Actions → Variables)。2c マージまでは fix job 自体が存在しないため作用しない。 -#### WP-17 PR 2: Phase B — pr-monitor.yml の無人 fix push 拡張(旧ステップ 1) +#### WP-17 PR 2: Phase B — pr-monitor.yml の無人 fix push 拡張 — 実装済・未 land(再分割 2a/2b/2c で land する) -- pr-monitor.yml を Phase B へ拡張し、fix push まで無人実行する。実行条件は全 AND 合成(1 つでも欠けたら Phase A 相当 = 分析コメントのみに degrade。fail-closed): - 1. PR 1 の kill-switch が有効(workflow 式 + 操作直前判定の二重)。 - 2. 対象 PR の head ブランチが `claude/` prefix(ADR-052 target 軸)。 - 3. 変更内容が ADR-052 自動実行可クラス(内容軸)。分類は `lib_docs_policy::is_docs_only_summary` を再利用し、分類不能はゲート必須へ(fail-closed)。あわせて ADR-052 実装スコープ節の stale 記述(lib 切り出しが未了である旨)を訂正する。 - 4. ADR-054 scope guard(fix diff の findings 由来 allowlist 検証)を CI 側でも実行。 -- **permissions 昇格の再設計**: Phase A の安全担保の主体は `contents: read`(push が 403 で決定論的に失敗)だった。Phase B では `contents: write` が必要になりこの担保が失われるため、代替の決定論的担保(例: repository ruleset で `claude/` 以外への push を deny)を同 PR で設計し、workflow 冒頭の多層防御コメントを更新する。 -- **適用対象の現実**: 既存のローカル発 PR ブランチは `claude/` prefix ではないため、無人 fix push の作用対象は当面 `claude/` ブランチ PR(= WP-18 の夜間ループ発 PR が本命)に限られる。実走検証は workflow_dispatch + `claude/` テストブランチのスモークで行う。 -- ADR: Phase B 設計を新規 ADR または ADR-022 amendment として記録。起票時に「2. 検証済みの前提事実」の GitHub Actions 課金 / claude-code-action OAuth の事実を最新値へ再確認し永続化する(同節冒頭の必須要件)。 +> **実行者向け(別セッションの Opus を想定)**: 本節と下位 3 小節(2a/2b/2c)だけで作業できるよう自己完結的に書いてある。実装は 2026-08-02 に完了しローカル jj コミットとして全量存在する。残る作業は**履歴の組み替えと land** であり、新規実装はほぼ無い。着手前に「資産の実在確認」のコマンドを必ず実行すること。 + +**経緯(3 行)**: PR 2 は一括実装(1613 行)→ PR size gate で 2 分割 → 先頭 PR が simplicity REJECT → takt fix が lib 抽出 2 crate を丸ごと削除し gate 全 PASS のまま空洞化 push が「成功」する incident が発生(2026-08-02)。原因分析と再発防止は [ADR-068](adr/adr-068-fix-step-authority-boundary.md)(fix 後退検知 backstop、PR #348)と [ADR-069](adr/adr-069-pr-chain-declaration.md)(PR chain 宣言規約、PR #349)として **land 済み**。この 2 つが入った現在は、同じ事故は決定論的に block され、チェーン分割は宣言により REJECT されない。 + +**資産の実在確認(着手時に最初に実行)**: + +```sh +jj log -r 'ylkowqkp | unksnyts | mxzwmsyp | lwpktvpm | lqxzpvuw | utpvkwql | rxvwoxyq | zqlrrurl' --no-graph -T 'change_id.short() ++ " | " ++ description.first_line() ++ "\n"' +``` + +8 行出れば資産は無傷。change_id は rebase 後も安定なので、以降の手順はすべて change_id で参照する(commit_id は組み替えで変わる)。 + +| change_id | 内容 | 行き先 | +|---|---|---| +| `zqlrrurl` | docs: PR 1 完了反映(旧) | **abandon**(内容は本節の更新が包含済み) | +| `ylkowqkp` | `lib-scope-guard` 抽出(ADR-054 判定コア、テスト 11 件) | 2b | +| `unksnyts` | `lib-autonomy-policy` 抽出(ADR-066 判定コア、テスト 21 件維持) | 2b | +| `mxzwmsyp` | rename パーサ修正 + **incident の gut-revert が混入**(後述) | 2a(2 ファイルのみ回収)→ abandon | +| `lwpktvpm` | `cli-fix-push-gate`(4 軸 AND ゲート、テスト 22 件 + drill 7 実測済) | 2b | +| `lqxzpvuw` | pr-monitor.yml の Phase B fix job(12 step) | 2c | +| `utpvkwql` | `autonomy-config.toml` の `enabled = true` 化 | 2c | +| `rxvwoxyq` | ADR-067 起票 + ADR-052 stale 訂正 + CLAUDE.md | 2c | + +チェーン構造: `zqlrrurl → ylkowqkp → unksnyts → {lwpktvpm → lqxzpvuw → utpvkwql → rxvwoxyq, mxzwmsyp}`(旧 master 起点)。remote に stale ブランチ `feat/wp17-pr2a-policy-libs`(mxzwmsyp = 汚染版)が残っており、2a 完了時にユーザーへ GitHub UI での削除を依頼する(`gh` 直呼びは hook が block)。 + +**jj 運用の注意(本セッションで 3 回発生した事故の予防)**: ファイル編集を始める前に必ず `jj new -m "wip: <内容>"` で新コミットを作ること。描述済みコミットが `@` のまま編集すると、後続の `jj describe` が既存コミットのメッセージを上書きし、変更が混入する。`pnpm push` は必ず timeout 600000ms + `run_in_background: true`(ADR-016)。PR 作成・マージは AskUserQuestion または本文提示でユーザー承認を得る(ADR-028。VSCode では AskUserQuestion の preview・同一ターンの本文が見えないことがあるため、**draft はツール呼び出しを伴わない単独メッセージで提示**する)。 + +##### 2a: 計画書更新 + rename パーサ修正(約 130〜200 行) — 実施中(本 PR) + +1. 本計画書の更新コミット(`jj log -r 'master..'` で description が `docs(harness-plan): WP-17 の実行状況と再分割計画` のもの)が既にあれば、それを 2a の先頭として流用する。 +2. **パーサ修正の回収**: `mxzwmsyp` は rename パーサ修正(`src/cli-push-runner/src/stages/diff.rs` + `src/cli-push-runner/src/stages/diff/tests.rs` の 2 ファイル)と incident の gut-revert(lib 削除等)が混在しており、**rebase / duplicate では回収できない**。次の手順で 2 ファイル分だけ取り出す: + - `tests.rs` は #348 以降 master で未変更のため丸ごと取得可: `jj restore --from mxzwmsyp -- src/cli-push-runner/src/stages/diff/tests.rs` + - `diff.rs` は **restore 不可**(#348 が `parse_git_diff_paths` の `pub(crate)` 化を入れており、mxzwmsyp 版で上書きすると `post_takt_regate.rs` が compile error になる)。`jj diff -r mxzwmsyp -- src/cli-push-runner/src/stages/diff.rs` で hunk を確認し、`summary_line_new_path` の R/C 分岐変更と `rename_new_path` 関数追加、関連 doc 更新だけを現ファイルへ手で適用する(`pub(crate)` 行とは重ならない)。 + - 修正の本質: jj の rename summary は `R src\{old => new}\file.rs` の**波括弧形式**で、旧実装の 3 トークン前提が壊れたパスを作り **rename を含む PR が一律 push 不能**だった。詳細は mxzwmsyp のコミットメッセージ参照。 +3. 検証: `cargo test -p cli-push-runner`(rename 系テスト含め全緑)、`cargo clippy -p cli-push-runner --all-targets -- -D warnings`。 +4. push(bookmark 例 `feat/wp17-r2a-docs-parser`)→ PR 作成(承認フロー)→ マージ(ユーザー)。 +5. マージ後、stale remote ブランチ `feat/wp17-pr2a-policy-libs` の削除をユーザーへ依頼。 + +##### 2b: lib 抽出 2 件 + cli-fix-push-gate(約 1,130 行、warning 帯) + +抽出(`lib-scope-guard` / `lib-autonomy-policy`)と最初の呼び手(`cli-fix-push-gate`)を**同一 PR に入れる**ことで ADR-044 層 1 を充足する(incident の初回分割はここを分離して失敗した)。 + +1. rebase: `jj rebase -s ylkowqkp -d master`(2a マージ後の master)。descendants(2c 分と mxzwmsyp)も一緒に移動する。その後 `jj abandon -r mxzwmsyp`(2a で回収済み)と `jj abandon -r zqlrrurl`(stale。※ zqlrrurl が ylkowqkp の親として残っている場合は rebase 前に abandon するか `-s zqlrrurl` ではなく `-s ylkowqkp` 起点で外す)。 +2. conflict 解決指針(rebase 時に発生しうる): `docs/harness-improvement-plan.md` と `CLAUDE.md` は **master 側を正**とし、rxvwoxyq 由来の編集は CLAUDE.md の ADR-067 行(ADR-066 と ADR-068 の行の間に挿入)だけ活かす。`autonomy-config.toml` / `pr-monitor.yml` / ADR-052 は master 未変更のため conflict しない見込み。 +3. **lib module doc の時制修正**(新コミット): `lib-scope-guard/src/lib.rs`・`lib-autonomy-policy/src/lib.rs`・`cli-autonomy-gate/src/main.rs`・`cli-pr-monitor/src/stages/scope_guard.rs` の「cli-fix-push-gate(計画中・本 diff の時点では未実装)」系の文言を、同一 PR に呼び手が存在する現実に合わせて現在形へ直す(incident 後の言い換えの残骸)。 +4. **chain 宣言(ADR-069 の初回 dogfood)**: 本計画書の下記「2b の chain 宣言」を 2b のコミットで**削除せずそのまま残し**、2b の diff に本計画書の状態行更新(例: 2b 見出しへの「実施中」付記)を含めることで宣言を diff に載せる。レビューが宣言を認識して missing-consumer findings を warning に降格することが ADR-069 試験運用の初回実測になる(結果を ADR-069 の試験運用判断基準の記録として残すこと)。 +5. 検証: `cargo test --workspace` 全緑(1936 件規模 + 新規 33 件)、`cargo clippy --workspace --all-targets -- -D warnings`、`pnpm lint:docs` / `lint:md`。 +6. push 時は `jj edit` で `@` を 2b tip(lib module doc 修正コミット)に置く(push-runner のレビュー範囲と bookmark 自動更新は `master..@`)。bookmark 例 `feat/wp17-r2b-libs-gate` → PR → マージ。 + +**2b の chain 宣言**(ADR-069 準拠。2b PR の diff にこの計画書が含まれることで有効になる): 2b が導入する `cli-fix-push-gate`(crate `src/cli-fix-push-gate`)の workflow 呼び手は、**後続 PR 2c の `.github/workflows/pr-monitor.yml` fix job**(step `Gate fix push` が `master-ref/target/release/cli-fix-push-gate` を実行)として land する。`lib-scope-guard` の 2 呼び手(`cli-pr-monitor` scope_guard / `cli-fix-push-gate`)と `lib-autonomy-policy` の 2 呼び手(`cli-autonomy-gate` / `cli-fix-push-gate`)は **2b 自身の diff 内**に存在する。 + +##### 2c: Phase B workflow + config 有効化 + ADR-067(約 470 行) + +1. 2b マージ後、残チェーンを rebase: `jj rebase -s lqxzpvuw -d master`。conflict 指針は 2b と同じ。 +2. 内容: pr-monitor.yml の fix job(agent は push しない / findings と fix の agent 分離 / gate と config は master ref から調達 / degrade は run 失敗にしない — 設計の全文は rxvwoxyq が起票する ADR-067 に記載済み)、`autonomy-config.toml` の `enabled = true`、ADR-067 + ADR-052 訂正 + CLAUDE.md。 +3. workflow の構文検証: js-yaml でのパース(node script を scratchpad に書いて実行。実走は後述スモークで)。 +4. **マージ前の確認**: Actions variable `AUTONOMY_ENABLED` は設定済み(= true)のため、**2c がマージされた瞬間に Phase B が live になる**。意図的に段階を踏むなら、マージ前に variable を削除し、下記スモーク段 1 で再設定する(削除 = 停止が ADR-066 の設計どおり機能する)。 +5. push → PR → マージ(ユーザー)。 +6. **マージ後の実走スモーク**(ユーザー操作込み。順序厳守): + - 段 0: repository ruleset で `claude/` 以外への `GITHUB_TOKEN` push を deny(5 層目の防波堤。ユーザー、GitHub UI)。 + - 段 1: variable `AUTONOMY_ENABLED` = `true` を設定 → 適当な非 `claude/` PR に対し workflow_dispatch → fix job が `[FIX_PUSH_DENY] branch=... claude/ prefix ではない` で degrade することを確認。 + - 段 2: `claude/` prefix のテストブランチで docs 指摘のある PR を作り、allow 経路(gate exit 0 → workflow step が push)と deny 経路(variable 削除で次 run から job skip)を観測。 + - 結果を ADR-067 の検証記録と ADR-066 / ADR-068 の bounded lifetime 観測へ記帳する。 #### WP-17 PR 3: wakeup 機構(CronCreate 系)の廃止(旧ステップ 3) @@ -160,7 +214,9 @@ Anthropic 公式のハーネスエンジニアリング指針(決定論的基 #### WP-17 受け入れ基準 -- PR 1–2: kill-switch drill — `AUTONOMY_ENABLED` 未設定 / false で Phase B が fix push せず loud deny marker が run log / telemetry で観測できること。有効時のみ `claude/` テストブランチへの fix push が通ること(workflow_dispatch スモーク)。 +- PR 1: kill-switch drill(exe 単体) — **充足済**(PR #347。8 シナリオ実測、ADR-066 § 検証記録)。 +- 再分割 2b: `cli-fix-push-gate` の決定論層 drill — **充足済**(7 シナリオ実測、ADR-067 § 検証記録に記載済み。land 時に有効化)。加えて 2b が ADR-069 chain 宣言の初回実測を兼ねる(宣言付き先頭 PR が missing-consumer REJECT を受けないこと)。 +- 再分割 2c: 実走スモーク段 0〜2(§ 2c 手順 6) — `AUTONOMY_ENABLED` 未設定 / false で fix job が起動せず、有効時のみ `claude/` テストブランチへの fix push が通ること。 - PR 3: wakeup 廃止後、レート制限を含む PR イベントが GitHub Actions 経路のみで処理されること(ADR-064 (b) の判定文保証は同経路の検証残として追跡)。 - WP 全体(従来基準): PC 電源オフの週末をまたいで PR イベント・週次レビューが取りこぼしなく処理されること。 From 434ced53e4025beca2135c3f3aaa98dc9a050874 Mon Sep 17 00:00:00 2001 From: aloekun Date: Mon, 3 Aug 2026 13:18:02 +0900 Subject: [PATCH 2/3] =?UTF-8?q?fix(cli-push-runner):=20rename=20summary=20?= =?UTF-8?q?=E3=81=AE=E6=B3=A2=E6=8B=AC=E5=BC=A7=E5=BD=A2=E5=BC=8F=E3=82=92?= =?UTF-8?q?=E3=83=91=E3=83=BC=E3=82=B9=E3=81=A7=E3=81=8D=E3=82=8B=E3=82=88?= =?UTF-8?q?=E3=81=86=E4=BF=AE=E6=AD=A3=20(WP-17=202a)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `jj diff --summary` の rename / copy 行を誤ってパースし、**rename を含む PR が一律 push 不能** になっていた不具合を修正する。WP-17 PR 2a の push が exit 5 で停止して 顕在化した。 ## 実測書式と旧実装のずれ jj (0.42.0 / Windows) は共通 prefix / suffix を括り出した波括弧形式で出す: R src\{cli-autonomy-gate => lib-autonomy-policy}\src\decision.rs 旧実装は ` ` の 3 トークン空白区切りを前提に rsplit_once(' ') していたため、上の行から `lib-autonomy-policy}\src\decision.rs` という壊れたパスを 取り出していた。これが `--git` 側の `b/src/lib-autonomy-policy/src/decision.rs` と 一致せず、常に「未収録」と判定されていた。 判定は block 側に倒れるので fail-closed ではあるが、正当な push を止める誤検知。 ## 根拠が fixture だった問題 3 トークン形式を前提にした根拠は、コード内コメントが引用していた lib_docs_policy の `"R docs/a.md docs/b.md"` というテスト文字列だった。これは jj の出力を観測したもの ではなく手で作った fixture で、実挙動の証拠になっていない。dev-conventions の 「外部 fixture 参照テストは値まで assert」が想定する失敗そのもの。本コミットの テストは実測値を定数 OBSERVED_RENAME_SUMMARY として持ち、それを assert する。 ## 修正方針 個別ケースを潰すのではなく、rename 行の入力空間を 1 バッチで固定する (dev-conventions: パーサ修正は全 branch を一度に堅牢化): - 波括弧形式: prefix + new + suffix を結合して復元 - 矢印形式 (共通部分なし): `old => new` の new 側 - 上記以外 (旧 3 トークン形式・閉じ括弧欠落・矢印欠落) は None → Err で fail-closed 空白区切りへの fallback は残さない。波括弧のパースに失敗した行が fallback で 「それらしいパス」に化けると書式変化を検知できず gate が沈黙するため (既存方針「未知は明示的に reject」と同じ)。 波括弧の全 branch (中間 rename / 上位移動 `{sub/ => }` / 下位移動 `{ => sub/}` / ファイル名のみ / copy) をテーブル駆動で固定した。 ## 回収経緯 本修正の実装は 2026-08-02 の incident コミット (mxzwmsyp) に lib 抽出の gut-revert と 混在していたため、rebase / duplicate では回収できなかった。tests.rs は master 未変更の ため restore、diff.rs は #348 の `parse_git_diff_paths` pub(crate) 化と衝突するため `summary_line_new_path` の R/C 分岐と `rename_new_path` 追加だけを手で適用している (計画書 § WP-17 2a 手順 2)。`summary_line_new_path` の doc は「末尾トークン」前提の 記述が本修正で stale になるため現状に合わせて更新した。 検証: cargo test -p cli-push-runner 291 件 pass、clippy -D warnings 緑。 Co-Authored-By: Claude Opus 5 (1M context) --- src/cli-push-runner/src/stages/diff.rs | 58 ++++++++++++++-- src/cli-push-runner/src/stages/diff/tests.rs | 69 +++++++++++++------- 2 files changed, 97 insertions(+), 30 deletions(-) diff --git a/src/cli-push-runner/src/stages/diff.rs b/src/cli-push-runner/src/stages/diff.rs index 153ff37c..c4fd3ee7 100644 --- a/src/cli-push-runner/src/stages/diff.rs +++ b/src/cli-push-runner/src/stages/diff.rs @@ -162,8 +162,7 @@ fn verify_diff_covers_pr_range( /// 全行が未知のときだけ fail-closed する旧実装 (SIM-NEW-diff-rs-L178) の per-line 版。 /// 全行が空 (末尾改行のみ 等) なら `Ok` の空集合を返す (= 変更なし)。 /// -/// `R`/`C` は ` ` の 3 トークン形式 (`lib_docs_policy` の -/// `is_docs_only_summary` テストが同じ `"R docs/a.md docs/b.md"` 形状を実証)。 +/// `R`/`C` の実際の書式は [`rename_new_path`] を参照 (2026-08-02 実測で訂正)。 /// `parse_git_diff_paths` (`diff --git a/old b/new` の new = `b/` 側のみ拾う) と /// 揃えるため new path 側だけを採用する (SIM-NEW-diff-rs-L146)。 /// @@ -191,22 +190,67 @@ fn parse_summary_paths( /// fail-closed に倒す。 /// /// `None` になるのは: 未知 status (`jj diff --summary` の M/A/D/R/C 以外) / R・C で -/// new path (末尾トークン) が無い崩れた行 / パスが空。catch-all で任意 status や崩れた -/// R/C を通すと、jj の出力書式が変わった行を「妥当なパス」として取り込み、書式変化を -/// 検知できず gate が沈黙する (SIM-NEW-diff-rs-L178)。旧実装は崩れた R/C の生トークンを +/// [`rename_new_path`] が new path を復元できない崩れた行 / パスが空。catch-all で任意 +/// status や崩れた R/C を通すと、jj の出力書式が変わった行を「妥当なパス」として取り込み、 +/// 書式変化を検知できず gate が沈黙する (SIM-NEW-diff-rs-L178)。旧実装は崩れた R/C の生トークンを /// 残して coverage 不一致に頼っていたが、CodeRabbit #313 指摘に従い「未知 status も崩れた /// R/C も明示的に reject」へ統一する (fail-closed の判定を coverage 副作用でなく /// パース時点に前倒しする)。 fn summary_line_new_path(status: &str, rest: &str) -> Option { let trimmed = rest.trim(); let path = match status { - "M" | "A" | "D" => trimmed, - "R" | "C" => trimmed.rsplit_once(' ').map(|(_, new)| new)?, + "M" | "A" | "D" => trimmed.to_string(), + "R" | "C" => rename_new_path(trimmed)?, _ => return None, }; (!path.is_empty()).then(|| path.replace('\\', "/")) } +/// `jj diff --summary` の rename / copy 行から new path を復元する。 +/// +/// # 実測した書式 (2026-08-02) +/// +/// jj は共通 prefix / suffix を括り出した**波括弧形式**で出す: +/// +/// ```text +/// R src\{cli-autonomy-gate => lib-autonomy-policy}\src\decision.rs +/// ``` +/// +/// 共通部分が無い場合は括弧の無い矢印形式 (`old => new`) になる。ただしこちらは実出力を +/// **観測できていない**推定であり、上の波括弧形式と証拠の強さが違う (本コミットが正した +/// 「fixture を実測の証拠と取り違える」誤りを繰り返さないため明記する)。推定が外れていても +/// 未知形は `None` → `Err` に倒れるので、書式変化は沈黙せず push 停止として現れる。 +/// +/// # 旧実装のバグ (本関数が修正するもの) +/// +/// 旧実装は ` ` の 3 トークン空白区切りを前提に `rsplit_once(' ')` して +/// おり、上の実測行からは `lib-autonomy-policy}\src\decision.rs` という壊れたパスを取り出して +/// いた。結果 coverage 検査の期待値が `--git` 側と一致せず、**rename を含む PR が一律 push +/// 不能**になっていた (判定は block 側なので fail-closed ではあるが誤検知)。 +/// +/// 3 トークン形式を前提とした根拠は `lib_docs_policy` の `"R docs/a.md docs/b.md"` という +/// テスト文字列だったが、これは jj の出力を観測したものではなく**手で作った fixture** で、 +/// 実挙動の証拠になっていなかった (dev-conventions「外部 fixture 参照テストは値まで assert」 +/// が想定する失敗そのもの)。本関数のテストは実測値をそのまま assert する。 +/// +/// # 受理する形と倒し方 +/// +/// 波括弧形式と矢印形式のみ受理し、それ以外 (旧 3 トークン形式を含む) は `None` を返して +/// 呼び出し側で `Err` に昇格させる。空白区切りへの fallback を残さないのは、波括弧形式の +/// パースに失敗した行が空白 fallback で「それらしいパス」に化けると、書式変化を検知できず +/// gate が沈黙するため (既存方針「未知は明示的に reject」と同じ、fail-closed)。 +fn rename_new_path(rest: &str) -> Option { + const ARROW: &str = " => "; + let Some(open) = rest.find('{') else { + return rest.split_once(ARROW).map(|(_, new)| new.trim().to_string()); + }; + let close = open + rest[open..].find('}')?; + let (_, new) = rest[open + 1..close].split_once(ARROW)?; + let prefix = &rest[..open]; + let suffix = &rest[close + 1..]; + Some(format!("{prefix}{new}{suffix}")) +} + /// unified diff の `diff --git a/X b/X` ヘッダからパス集合を作る。 /// /// `pub(crate)`: post_takt_regate の fix 後退検知 (ADR-068) が takt 前後の diff の diff --git a/src/cli-push-runner/src/stages/diff/tests.rs b/src/cli-push-runner/src/stages/diff/tests.rs index fc152ceb..a4140cf3 100644 --- a/src/cli-push-runner/src/stages/diff/tests.rs +++ b/src/cli-push-runner/src/stages/diff/tests.rs @@ -73,39 +73,55 @@ fn verify_normalizes_windows_path_separators() { /// SIM-NEW-diff-rs-L146: rename 行 (`R `) は new path だけを /// パス集合に採用すること。旧実装は old+new を 1 個の壊れたパスにしてしまい、 /// coverage 検査が rename ファイルを絶対に "covered" と一致させられなかった。 +/// **実測値** (2026-08-02、jj 0.42.0 / Windows): 共通 prefix / suffix を括り出した +/// 波括弧形式。旧テストは `"R docs/a.md docs/b.md"` という手書き fixture を根拠に +/// 3 トークン空白区切りを前提にしていたが、jj はその形式を出さない。 +const OBSERVED_RENAME_SUMMARY: &str = + "R src\\{cli-autonomy-gate => lib-autonomy-policy}\\src\\decision.rs\n"; + #[test] fn parse_summary_paths_extracts_new_path_for_rename() { - let paths = parse_summary_paths("R docs/a.md docs/b.md\n").expect("valid rename は Ok"); + let paths = parse_summary_paths(OBSERVED_RENAME_SUMMARY).expect("valid rename は Ok"); assert_eq!( paths, - std::collections::BTreeSet::from(["docs/b.md".to_string()]), - "rename は new path のみを採用し、old path や結合パスを含めないこと: {:?}", + std::collections::BTreeSet::from(["src/lib-autonomy-policy/src/decision.rs".to_string()]), + "波括弧を prefix/suffix と結合して new path を復元すること: {:?}", paths ); } -/// copy 行 (`C `) も rename と同じ扱い (new path のみ採用)。 +/// 波括弧形式の全 branch を 1 バッチで固定する (dev-conventions: パーサ修正は +/// 個別ケースを潰すのでなく入力空間全体を一度に堅牢化する)。 #[test] -fn parse_summary_paths_extracts_new_path_for_copy() { - let paths = parse_summary_paths("C docs/a.md docs/c.md\n").expect("valid copy は Ok"); - assert_eq!( - paths, - std::collections::BTreeSet::from(["docs/c.md".to_string()]), - "copy は new path のみを採用すること: {:?}", - paths - ); +fn rename_brace_form_covers_every_shape() { + let cases = [ + ("R docs/{a.md => b.md}\n", "docs/b.md"), + ("R src/{sub/ => }file.rs\n", "src/file.rs"), + ("R src/{ => sub/}file.rs\n", "src/sub/file.rs"), + ("R old.rs => new.rs\n", "new.rs"), + ("C docs/{a.md => c.md}\n", "docs/c.md"), + ]; + for (summary, expected) in cases { + assert_eq!( + parse_summary_paths(summary).unwrap_or_else(|e| panic!("{summary:?}: {e}")), + std::collections::BTreeSet::from([expected.to_string()]), + "{summary:?} の new path は {expected:?}" + ); + } } /// incident 再現 (rename 版): rename されたファイルを含む PR 範囲でも、 /// new path を収録した diff は coverage 検査を通ること。 +/// +/// 実測書式でこれが通らず「rename を含む PR が一律 push 不能」になっていたのが +/// 本修正の契機 (WP-17 PR 2a の push が exit 5 で停止)。 #[test] fn verify_accepts_diff_with_renamed_file() { - let summary = "R docs/a.md docs/b.md\n"; - let diff = "diff --git a/docs/a.md b/docs/b.md\n@@ -1 +1 @@\n-old\n+new\n"; + let diff = "diff --git a/src/cli-autonomy-gate/src/decision.rs \ + b/src/lib-autonomy-policy/src/decision.rs\n@@ -1 +1 @@\n-old\n+new\n"; assert!( - verify_diff_covers_pr_range(diff, || Ok(summary.to_string())).is_ok(), - "rename ファイルの new path が --git ヘッダの b/ 側と一致し、coverage 検査を\ - 通ること (旧実装は old+new の壊れたパスと一致せず常に未収録扱いだった)" + verify_diff_covers_pr_range(diff, || Ok(OBSERVED_RENAME_SUMMARY.to_string())).is_ok(), + "波括弧形式から復元した new path が --git ヘッダの b/ 側と一致すること" ); } @@ -192,13 +208,20 @@ fn parse_summary_paths_rejects_unknown_status() { /// fail-closed の判定をパース時点に前倒しする)。 #[test] fn parse_summary_paths_rejects_malformed_rename_line() { - assert!( - parse_summary_paths("R docs/only-one-token.md\n").is_err(), - "new path (末尾トークン) が無い崩れた rename 行は Err" - ); + for malformed in [ + "R docs/only-one-token.md\n", + "R src/{a => b\n", + "R src/{ab}\n", + "R docs/a.md docs/b.md\n", + ] { + assert!( + parse_summary_paths(malformed).is_err(), + "{malformed:?} は矢印 / 波括弧のどちらの実測書式でもないため Err" + ); + } assert_eq!( - parse_summary_paths("R docs/a.md docs/b.md\n").expect("正常 rename は Ok"), - std::collections::BTreeSet::from(["docs/b.md".to_string()]), + parse_summary_paths(OBSERVED_RENAME_SUMMARY).expect("正常 rename は Ok"), + std::collections::BTreeSet::from(["src/lib-autonomy-policy/src/decision.rs".to_string()]), "正常な rename は new path のみ" ); } From 47af3e8dd77a3172863aa1c87ab53a89b1b6746d Mon Sep 17 00:00:00 2001 From: aloekun Date: Mon, 3 Aug 2026 13:53:53 +0900 Subject: [PATCH 3/3] =?UTF-8?q?fix(cli-push-runner):=20rename=20=E3=83=91?= =?UTF-8?q?=E3=83=BC=E3=82=B5=E3=81=AE=E7=A9=BA=E7=99=BD=E6=89=B1=E3=81=84?= =?UTF-8?q?=E3=82=92=E4=B8=A1=E5=88=86=E5=B2=90=E3=81=A7=E6=8F=83=E3=81=88?= =?UTF-8?q?=E3=82=8B=20+=20=E8=A8=88=E7=94=BB=E6=9B=B8=E3=81=AE=E5=89=8D?= =?UTF-8?q?=E6=8F=90=E3=82=92=E6=98=8E=E8=A8=98=20(WP-17=202a)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #350 の CodeRabbit 指摘 3 件への対応。 ## 1. rename_new_path の矢印分岐から冗長な trim を削除 指摘: 「パスから空白を除去しないでください」(diff.rs、Minor)。 矢印分岐だけが `new.trim()` していて波括弧分岐 (`format!` で結合、trim なし) と 非対称だった。呼び出し元 `summary_line_new_path` が既に `rest.trim()` 済みの文字列を 渡す唯一の呼び手なので、この trim は冗長でもある。 **これはバグ修正ではなく対称性の回復である**: 現実的な入力では両者の結果は一致し、 削除による挙動変化は観測できない (だからこそ安全に削除できる)。狙いは「同じパスが 分岐によって違う結果になり得る」状態を残さないこと。 なお post-pr-review の分析は本指摘を false positive と判定していたが、根拠が誤り だった (指摘位置を `path.replace('\', "/")` と読み「バックスラッシュ変換であって 空白除去ではない」とした)。CodeRabbit は複数行コメントを範囲末尾に anchor するため 実際の対象は関数全体で、trim は確かに存在した。結論 (非ブロッキング) は一致するが 理由が違うので、指摘の芯を採用して直す。 テストには空白を含むパスのケースを 2 件追加した (波括弧 / 矢印の両分岐)。区切りの `" => "` と紛れる位置に空白があっても内部の空白が保持され、両分岐が同じ結果を返す ことを固定する。 ## 2. tests.rs restore の前提を明記 (docs) 指摘: 「復元前に tests.rs の復元前提を明記してください」(計画書 L164、Minor)。 `jj restore` で丸ごと上書きしてよいのは「master 側が未変更」の間だけ。前提の確認 コマンドと、崩れていた場合の切り替え先 (diff.rs と同じ hunk 単位の手適用) を書いた。 前提を確認せず restore すると master 側の変更を無言で巻き戻すため、確認手順まで含めて 残す。 ## 3. workflow_dispatch の ref 選択を明記 + スモーク段 0.5 を追加 (docs) 指摘: 「workflow_dispatch は PR head branch を受け取ると記載してください」 (計画書 L193、Minor)。 dispatch は起動時に ref を選べ、選んだ ref 版の workflow 定義で走る。これは 2c の 実走スモークの前提として重要で、計画書が「マージ後の実走スモーク」としか書いて いなかったのは記述不足だった。fix job はマージ前に 2c ブランチ ref に対して実走できる。 これを踏まえ段 0.5 (マージ前 dispatch) を追加した。gate と config は master ref から 調達される (ADR-066 決定 3) ため、マージ前は config が `enabled = false` のままで fix job は config 層で停止する = **deny 経路 (kill-switch が効く側) をマージ前に検証でき、 allow 経路だけがマージ後に残る**。`AUTONOMY_ENABLED` が既に true で「2c マージ = 即 live」 の一発勝負になる状況に対する段階化として意味がある。 手順 4 (マージ前に variable を削除して段階を踏む案) は段 0.5 が variable 層を通す必要が あるため順序が矛盾していた。削除するとしても段 0.5 の後、と順序を明記して整合させた。 検証: cargo test -p cli-push-runner 291 件 pass、clippy -D warnings 緑。 Co-Authored-By: Claude Opus 5 (1M context) --- docs/harness-improvement-plan.md | 13 +++++++++---- src/cli-push-runner/src/stages/diff.rs | 9 ++++++++- src/cli-push-runner/src/stages/diff/tests.rs | 6 ++++++ 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/docs/harness-improvement-plan.md b/docs/harness-improvement-plan.md index 87f1a6c5..96df9ca1 100644 --- a/docs/harness-improvement-plan.md +++ b/docs/harness-improvement-plan.md @@ -161,6 +161,7 @@ jj log -r 'ylkowqkp | unksnyts | mxzwmsyp | lwpktvpm | lqxzpvuw | utpvkwql | rxv 1. 本計画書の更新コミット(`jj log -r 'master..'` で description が `docs(harness-plan): WP-17 の実行状況と再分割計画` のもの)が既にあれば、それを 2a の先頭として流用する。 2. **パーサ修正の回収**: `mxzwmsyp` は rename パーサ修正(`src/cli-push-runner/src/stages/diff.rs` + `src/cli-push-runner/src/stages/diff/tests.rs` の 2 ファイル)と incident の gut-revert(lib 削除等)が混在しており、**rebase / duplicate では回収できない**。次の手順で 2 ファイル分だけ取り出す: - `tests.rs` は #348 以降 master で未変更のため丸ごと取得可: `jj restore --from mxzwmsyp -- src/cli-push-runner/src/stages/diff/tests.rs` + - **restore の前提(実行前に確認すること)**: 「master 側が未変更」が成立している間だけ丸ごと上書きしてよい。`jj diff --from master --to mxzwmsyp -- ` が incident コミット単体の差分(`jj diff -r mxzwmsyp -- `)と一致すれば前提は満たされている。一致しない = master 側にも変更が入っており、`diff.rs` と同じく hunk 単位の手適用に切り替える(restore すると master 側の変更を無言で巻き戻す)。 - `diff.rs` は **restore 不可**(#348 が `parse_git_diff_paths` の `pub(crate)` 化を入れており、mxzwmsyp 版で上書きすると `post_takt_regate.rs` が compile error になる)。`jj diff -r mxzwmsyp -- src/cli-push-runner/src/stages/diff.rs` で hunk を確認し、`summary_line_new_path` の R/C 分岐変更と `rename_new_path` 関数追加、関連 doc 更新だけを現ファイルへ手で適用する(`pub(crate)` 行とは重ならない)。 - 修正の本質: jj の rename summary は `R src\{old => new}\file.rs` の**波括弧形式**で、旧実装の 3 トークン前提が壊れたパスを作り **rename を含む PR が一律 push 不能**だった。詳細は mxzwmsyp のコミットメッセージ参照。 3. 検証: `cargo test -p cli-push-runner`(rename 系テスト含め全緑)、`cargo clippy -p cli-push-runner --all-targets -- -D warnings`。 @@ -185,11 +186,15 @@ jj log -r 'ylkowqkp | unksnyts | mxzwmsyp | lwpktvpm | lqxzpvuw | utpvkwql | rxv 1. 2b マージ後、残チェーンを rebase: `jj rebase -s lqxzpvuw -d master`。conflict 指針は 2b と同じ。 2. 内容: pr-monitor.yml の fix job(agent は push しない / findings と fix の agent 分離 / gate と config は master ref から調達 / degrade は run 失敗にしない — 設計の全文は rxvwoxyq が起票する ADR-067 に記載済み)、`autonomy-config.toml` の `enabled = true`、ADR-067 + ADR-052 訂正 + CLAUDE.md。 3. workflow の構文検証: js-yaml でのパース(node script を scratchpad に書いて実行。実走は後述スモークで)。 -4. **マージ前の確認**: Actions variable `AUTONOMY_ENABLED` は設定済み(= true)のため、**2c がマージされた瞬間に Phase B が live になる**。意図的に段階を踏むなら、マージ前に variable を削除し、下記スモーク段 1 で再設定する(削除 = 停止が ADR-066 の設計どおり機能する)。 -5. push → PR → マージ(ユーザー)。 -6. **マージ後の実走スモーク**(ユーザー操作込み。順序厳守): +4. **マージ前の確認**: Actions variable `AUTONOMY_ENABLED` は設定済み(= true)のため、**2c がマージされた瞬間に Phase B が live になる**。意図的に段階を踏むなら、下記スモーク段 0.5 を済ませた後・マージ前に variable を削除し、段 1 で再設定する(削除 = 停止が ADR-066 の設計どおり機能する)。段 0.5 は variable 層を通す必要があるため、削除するとしても段 0.5 の後にすること。 +5. push → PR → マージ(ユーザー)。ただしマージ前に下記スモーク段 0.5 を先に済ませる。 +6. **実走スモーク**(ユーザー操作込み。順序厳守)。 + + 前提となる `workflow_dispatch` の挙動: **dispatch は起動時に ref(ブランチ)を選べ、選んだ ref 版の workflow 定義で走る**(workflow ファイル自体が default branch に存在すれば Actions UI の候補に出る。pr-monitor.yml は master 稼働中なので条件を満たす)。したがって 2c の fix job は**マージ前に 2c ブランチ ref に対して実走できる**。なお dispatch 起点の run は PR の Status Check には載らない(pr-monitor.yml 冒頭の設計メモのとおり、対象 SHA が default branch 側になるため)。対象 PR は input `pr_number` で渡す。 + - 段 0: repository ruleset で `claude/` 以外への `GITHUB_TOKEN` push を deny(5 層目の防波堤。ユーザー、GitHub UI)。 - - 段 1: variable `AUTONOMY_ENABLED` = `true` を設定 → 適当な非 `claude/` PR に対し workflow_dispatch → fix job が `[FIX_PUSH_DENY] branch=... claude/ prefix ではない` で degrade することを確認。 + - 段 0.5(**マージ前**): 2c ブランチ ref を選んで workflow_dispatch。`AUTONOMY_ENABLED` は設定済みなので variable 層は通り、gate と `autonomy-config.toml` は **master ref から調達**される(ADR-066 決定 3)= master 側は `enabled = false` のままなので、fix job は config 層で停止するのが期待動作。ここで検証できるのは workflow 構文 / job 配線 / variable 層 / master-ref 調達 / **config 無効時の deny 経路**(= kill-switch が効く側)。allow 経路だけがマージ後に残る。`cli-fix-push-gate` exe を master ref から取るため、**2b マージ済みであることが前提**。 + - 段 1: 適当な非 `claude/` PR に対し workflow_dispatch → fix job が `[FIX_PUSH_DENY] branch=... claude/ prefix ではない` で degrade することを確認(2c マージ後。config が有効化されて初めて prefix 層まで到達する)。 - 段 2: `claude/` prefix のテストブランチで docs 指摘のある PR を作り、allow 経路(gate exit 0 → workflow step が push)と deny 経路(variable 削除で次 run から job skip)を観測。 - 結果を ADR-067 の検証記録と ADR-066 / ADR-068 の bounded lifetime 観測へ記帳する。 diff --git a/src/cli-push-runner/src/stages/diff.rs b/src/cli-push-runner/src/stages/diff.rs index c4fd3ee7..467dc4e4 100644 --- a/src/cli-push-runner/src/stages/diff.rs +++ b/src/cli-push-runner/src/stages/diff.rs @@ -239,10 +239,17 @@ fn summary_line_new_path(status: &str, rest: &str) -> Option { /// 呼び出し側で `Err` に昇格させる。空白区切りへの fallback を残さないのは、波括弧形式の /// パースに失敗した行が空白 fallback で「それらしいパス」に化けると、書式変化を検知できず /// gate が沈黙するため (既存方針「未知は明示的に reject」と同じ、fail-closed)。 +/// +/// # 空白を足し引きしない (CodeRabbit #350) +/// +/// どちらの分岐も new path を trim しない。`ARROW` が前後の空白を込みで区切るため内部の +/// 空白はそのまま残り、行頭・行末の空白は呼び出し側 [`summary_line_new_path`] が既に +/// 落としている。ここで重ねて trim すると矢印分岐だけがパス末尾の空白を削り、波括弧分岐と +/// 非対称になる (両分岐で同じパスが違う結果になる状態を作らない)。 fn rename_new_path(rest: &str) -> Option { const ARROW: &str = " => "; let Some(open) = rest.find('{') else { - return rest.split_once(ARROW).map(|(_, new)| new.trim().to_string()); + return rest.split_once(ARROW).map(|(_, new)| new.to_string()); }; let close = open + rest[open..].find('}')?; let (_, new) = rest[open + 1..close].split_once(ARROW)?; diff --git a/src/cli-push-runner/src/stages/diff/tests.rs b/src/cli-push-runner/src/stages/diff/tests.rs index a4140cf3..6335e864 100644 --- a/src/cli-push-runner/src/stages/diff/tests.rs +++ b/src/cli-push-runner/src/stages/diff/tests.rs @@ -92,6 +92,10 @@ fn parse_summary_paths_extracts_new_path_for_rename() { /// 波括弧形式の全 branch を 1 バッチで固定する (dev-conventions: パーサ修正は /// 個別ケースを潰すのでなく入力空間全体を一度に堅牢化する)。 +/// +/// 末尾 2 ケースは空白を含むパス (CodeRabbit #350)。区切りの `" => "` と紛れる位置に +/// 空白があっても両 branch が同じ結果を返す = 空白の足し引きで非対称にならないことを +/// 固定する。 #[test] fn rename_brace_form_covers_every_shape() { let cases = [ @@ -100,6 +104,8 @@ fn rename_brace_form_covers_every_shape() { ("R src/{ => sub/}file.rs\n", "src/sub/file.rs"), ("R old.rs => new.rs\n", "new.rs"), ("C docs/{a.md => c.md}\n", "docs/c.md"), + ("R docs/{my old.md => my new.md}\n", "docs/my new.md"), + ("R my old.rs => my new.rs\n", "my new.rs"), ]; for (summary, expected) in cases { assert_eq!(