Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
179 changes: 179 additions & 0 deletions docs/plans/issue-460.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
# Issue #460 — check-all.py が check-mutation-claims.py を既定の並列数で起こし、入れ子で N×N になる

`check-all.py` は既定で CPU の数(N)だけ並列に回し、その中で `check-mutation-claims.py` を**引数なし**で起こす。
`check-mutation-claims.py` の `--jobs` の既定も CPU の数なので、その中の `red` は親の残り N-1 本と、
作業者 N 人ぶんの `red`(陽性対照と変異)と CPU を取り合う。#449 で `red` が 300 秒の打ち切りに届いた(3 回中 2 回)。

## 変更範囲

触る:

- `scripts/check-all.py` — `check-mutation-claims.py` を起こすときに `--jobs` を渡す(値は親の `--jobs` から決める)。docstring の並列の節
- `scripts/test-check-all.py` — 渡した `--jobs` を観測するケースと、その変異
- `docs/plans/issue-460.md` — この計画
- `scripts/test-worktree-scripts.py` — 197〜199 行のコメントの 1 文だけ(2 パス目の R5。CR5 と同じ根拠の無い因果が残っていた。
広げたのは人間の判断、2026-10-27。挙動は変えない)

触らない:

- `scripts/check-mutation-claims.py`(既定の並列数・`TIMEOUT` は変えない。単独で回すときは CPU の数のままでよい)
- `scripts/test-check-mutation-claims.py`(中で `check-mutation-claims.py` を既定の並列で起こすが、当てる木の `red` は軽い試料だけ)
- CI のワークフロー(CI は `check-mutation-claims.py` を単独のステップとして呼ぶ——入れ子にならない)
- `CLAUDE.md`(`check-all.py` の行は並列の細部を docstring に預けている。`check-mutation-claims.py` の行の「既定は CPU の数」は単独で回すときの話で、偽にならない)

### 同じことを述べている箇所の数

入れ子の並列数を述べる箇所: `check-all.py` の docstring(並列の節)と `GUARDS` の注記の 2 箇所。
`CLAUDE.md` の 2 行は単独のときの既定を言うだけで、入れ子の値は持たない(上)。同じファイルの中なので分けない。

## 乗るデプロイ経路

`scripts/` だけ。プラグインの中身は変えないので版は上げない。公開サイトにも載らない——**反映経路なし**。

## 周の在り処

- ブランチ: `issue/460-nested-mutation-jobs`
- worktree: `/Users/hdknr/Projects/hdknr/claude-code-setup/.claude/worktrees/issue-460`
- 起点: `43b5832`(start-point `origin/main`)
- base: `43b58324945badf38de0c481ebc64e62f44e9339`

## 受入基準

不変条件:

1. **`check-all.py` の中の `check-mutation-claims.py` は、親の `--jobs` より多い作業者を起こさない**——
親の作業者と入れ子の作業者の積が N×N にならない。値は起動の引数で確かめる
2. **`check-all.py --jobs 1` は入れ子まで直列**(`check-mutation-claims.py --jobs 1`)——「`--jobs 1` で直列」が入れ子で破れない
3. **`check-all.py` が全部通る**(`N/N 本を回した(失敗 0)`)
4. **`check-all.py` の wall が大きく悪化しない**——同じツリーで前後を交互に測る
5. **`check-all.py` の既存の観測(並列・直列・中断・収録漏れ)が緑のまま**——`test-check-all.py` が見る
6. (2 パス目に範囲を広げた分)**`test-worktree-scripts.py` はコメントの 1 文しか変わらず、挙動が変わらない**——
`test-worktree-scripts.py` が緑で、差分がコメント行だけ。**直した文は記録(`docs/plans/issue-449.md:46`)にある事実だけを言い、
`check-all.py` の docstring と食い違わない**

経路(`check-mutation-claims.py` を起こす入口):

| 経路 | 入れ子になるか | 守り方 |
| --- | --- | --- |
| `check-all.py` の `GUARDS` | なる | `--jobs` を渡す(この周) |
| CI(`plugins.yml`) | ならない(単独のステップ) | 対象外 |
| 人が直接 | ならない | 既定(CPU の数)のまま |
| `test-check-mutation-claims.py`(`check-all.py` の `TESTS` 経由) | なるが、当てる木は試料だけ | 対象外(上の「触らない」) |
| `check-mutation-claims.py` の `red` が `check-all.py` を起こす | 今の主張には無い | 対象外 |

検証で証明できるもの: 1・2(`run` に渡る引数を観測するテストと変異)、3・5(`check-all.py` を回す)、
4(前後を測る。ただし手元の 10 コアの macOS 1 台での値)。

この環境では証明できないもの(**未証明**): 遅い機械・コア数の違う機械で打ち切りに届かなくなること
(測れるのはこの 1 台だけ)。

生成物: なし(`scripts/` の 2 本は生成物を持たない。図の書き出し・`skill-metrics.py` の生成ブロックは触らない)。

## 未解決の判断

- 渡す値: **親の半分(少なくとも 1)に決めた**(下の実測)。`1` は wall が延びすぎる

## 手順 8

- この周の起票: 0 件
- 焼き戻す学び: リポジトリには無し(CR5 の直し残し(R5)は、手順 3 の「同じことを述べている箇所の数」で、
`check-all.py` の外にある同じ主張を数えていなかったため。規定はもうあるので、足すものは無い)
- 手順 7: PR #467 は CI 緑。マージは人間の承認待ち。反映経路なし

## 実測(著者。関門ではない)

10 コアの macOS 1 台、2026-10-27。**計測は scratchpad のスクリプトで、他の `check-all.py` がいなくなってから起動し、
走っている間の他の `check-all.py` の数と 1 分平均の負荷を 5 秒ごとに記録した。** `red` の所要は、`PATH` の先頭に
記録だけする `python3` を置いて、退避先(worktree の外)で走った `scripts/test-*.py` を拾った。

| 入れ子の `--jobs` | wall(他の `check-all.py` が 0 本の回) | 最長の `red` |
| --- | --- | --- |
| 既定(10。変更前) | 227.2 / 217.0 / 232.4 / 224.0 / 262.2 秒 | 90.4 / 99.1 秒 |
| 親の半分(5) | 237.4 / 252.3 秒 | 77.4 / 79.4 秒 |
| 1 | 373.1 / 368.0 / 388.1 秒 | 約 64 秒 |

- **変更前でも、他の `check-all.py` が重ならなければ打ち切り(300 秒)には届かなかった**(最長 90 秒)。
**打ち切りに届いた 1 回(wall 678 秒・`test-worktree-scripts.py` の陽性対照が 300 秒)は、負荷の 15 分平均が 50 の時間帯で、
別のセッション(`issue-458`)が `check-all.py` を走らせていた**(その回は重なりを記録していないので、重なっていたとまでは言えない)
- **`--jobs 1` は wall が約 +65%** で、受入基準 4 に反する。**親の半分は wall の中央値で約 +8%(227 → 245 秒)で、
最長の `red` が 90〜99 → 77〜79 秒**。標本は半分が 2 回・変更前が 5 回で、負荷の揺れ(1 分平均 41〜71)の中にある
- 半分の回がもう 1 回あったが、途中で他の `check-all.py` が 2 本重なった(1 分平均 216・wall 457 秒)ので捨てた

交互の測り直し(3 セッション目。`/code-review` 1 パス目の CR7 への対応):

同じツリーで `check-all.py` と `test-check-all.py` の 2 本を**両方とも** base の版と HEAD の版に入れ替えて、順に回した。
走っている間、**自分の子孫を除いた** `check-all.py` の数と 1 分平均の負荷を 5 秒ごとに記録した
(最初の試行は `test-check-all.py` が起こす `check-all.py` の写しまで「他」に数える形だったので、止めて捨てた)。

| 順 | 版 | wall | 他の `check-all.py`(最大) | 1 分平均 | 結果 |
| --- | --- | --- | --- | --- | --- |
| 0 | base | 248.8 秒 | 0 | 11〜47 | 51/51(失敗 0) |
| 1 | HEAD | 243.1 秒 | 0 | 15〜46 | 51/51(失敗 0) |

- 標本は 1 組だけ(残りの 1 組は人間の判断で止めた)。この 1 組では悪化していない。**「悪化しない」は 1 組と、上の交互でない表からの言い方**である

捨てた測定(交絡):

- 入れ子の値だけを変えて、契約(`test-check-all.py`)を新しい版のままにした回——契約が落ちて `test-check-all.py` の主張 4 件が
陽性対照で止まり、仕事が減った(wall 237 / 227 / 222 秒)。**比べるときは 2 本とも base の版に戻す**
- 最初の数回(678 / 441 / 520 / 459 秒)——他のセッションの `check-all.py` と重なっていたかを記録していなかった
- 1/3 の回——変異の表が式の文字列(`max(1, jobs // 2)`)を名指ししているので契約が崩れ、さらに他の `check-all.py` が 2 本重なった

## 関門の進捗(再開点)

- 周の在り処は上の節。肯定的な確認済み(再開した 2 セッション目でも `issue/460-nested-mutation-jobs` の上・`dfa754b`)
- 実装: `4d460c9`(入れ子を `--jobs 1`)→ `dfa754b`(親の半分。契約を性質で見る)→ `f95df7b`(実測を docstring に)。**実装は完了**。
`f95df7b` で `check-all.py` 51/51
- 次: 手順 5 の Verifier(1 パス目)から
- 手順 4 の割り目: AskUserQuestion で訊いた → **「ここで割る」が選ばれた**(2026-10-27)
- 手順 5 の Verifier: 1 パス目 **反証 0 件**(3 セッション目・sonnet・`dev-loop:dev-loop-verifier`。`test-check-all.py` 緑・足した変異 3 件は捕まえる・鮮度 2 本緑。
基準 3 は回さず、基準 4 は計画の実測を読んで判定)。注意 1 件は下の採否の表 / `/code-review`: 1 パス目 **指摘 9 件**(high)。採否は下の表
- 1 パス目の指摘対応: CR5 を直した(docstring の 1 文)・CR7 を測り直した。他は残課題。直したあと `check-all.py` 51/51
- 次: **手順 5 の当て直し(Verifier 2 パス目)**と **`/code-review` 2 パス目**の両方が残っている。base は同じ SHA
(2 パス目で見るのは主に `b99fc7f` の docstring の 1 文と、計画の測り直し)
- 手順 6 の割り目(2 パス目の前): AskUserQuestion で訊いた → **「ここで割る」が選ばれた**(2026-10-27)
- 手順 5 の当て直し(Verifier 2 パス目): **反証 0 件**(4 セッション目・sonnet・`dev-loop:dev-loop-verifier`。注意 2 件は基準 4 の証拠の弱さ)
/ `/code-review` 2 パス目: **指摘 10 件**(high)。採否は下の「採否の表(2 パス目)」
- 2 パス目の指摘対応: R5 だけ直す(人間の判断)。**狭いパスの起点: `f258b08e8fa6690aea3eb5caa6e644aab3d4e82b`**(指摘対応を始める直前の HEAD)
- 範囲を広げた分の受入基準: 6(下)
- R5 の修正 `bba67c9` のあと `check-all.py` 51/51(失敗 0)
- 狭いパス(R5): **反証 0 件**(sonnet・`dev-loop:dev-loop-verifier`。コメント以外の差分が空・`test-worktree-scripts.py` 緑・
直した文は #449 の記録と一致)。**Verifier は変異を設計しなかった**(コメントだけの変更なので、変異は前後で同じ結果になるとして)。素通りした変異: なし(当てていない)
- 関門はここで打ち切り(2 パス)。次: PR を作り、残課題・未証明・基準 4 の証拠の弱さを PR コメントに書く
- 交絡を潰した手順: 上の「捨てた測定」。他の `check-all.py` の重なりを記録し、比べる 2 本を base の版に揃えた

## 既知の限界・決着済みの論点

- **他のセッションの `check-all.py` が重なったときの打ち切りは、この周では止められない**——入れ子を絞るのは 1 本の `check-all.py` の中の
取り合いだけで、セッションをまたぐ取り合いは見えない。決着: 採らない(範囲外)。理由: Issue の受入基準は入れ子の N×N
- **`--jobs 1` は採らない**——wall が約 +65%(上の表)で、受入基準 4 に反する

### 採否の表(1 パス目)

| 指摘 | 実物で失敗を示せるか | 破る受入基準 | 採否 |
| --- | --- | --- | --- |
| Verifier: 契約 9 は「親の半分」を固定しない(入れ子を定数 1 にする変異が通るはず。読解のみ) | 示せない(その変異は基準 1・2 を満たし、失敗ではない) | 無し(契約は意図して値ではなく性質を見る——`dfa754b`) | 残課題 |
| CR1: 契約 9 は `--jobs` を明示した起動しか見ず、既定の起動(引数なし)を観測しない(変異 `and "--jobs" in sys.argv` が緑) | 示せない——実物は既定の起動で入れ子に `--jobs 5`(10 コア。scratchpad で `run` を差し替えて記録)。失敗はテストの網の穴で、成果物の失敗ではない | 無し(基準 1 は実物で満たす) | 残課題 |
| CR2: 契約 9 は入れ子以外の歯止めに `--jobs` が渡ることを見ない(変異 `if True:` が緑) | 示せない——実物は他の歯止めに `--jobs` を渡さない(同上の記録で 0 本) | 無し | 残課題 |
| CR3: 契約 9 は親 1 と 4 しか見ない(変異 `1 if jobs < 8 else jobs * 2` が緑) | 示せない——実物は親 1・10・32 で 1・5・16 | 無し | 残課題 |
| CR4: docstring「`--jobs 1` なら入れ子まで直列」は孫(`red` が起こす `test-*.py` の並列)まで直列にしない | 示せない(基準 2 は「`check-mutation-claims.py --jobs 1`」と定義しており、その意味では真) | 無し | 残課題 |
| CR5: docstring「#449 の打ち切りはその時間帯に出た」は根拠が無い | **示せる**——`docs/plans/issue-449.md:46`「#449 の 300 秒超えは 3 回中 2 回で起きた別の日の値」。重なりは記録されていない。この周の 678 秒の回は別の事象で、計画も「重なっていたとまでは言えない」 | 無し(docstring の事実は受入基準に無い) | **直す(人間の判断。2026-10-27)**——根拠の無い因果の 1 文を、記録にある事実だけに直す |
| CR6: 高度——実測では入れ子 N の `red` は打ち切りから遠く(最長 90〜99 秒)、wall +8% を払って原因と裏付けられないものを直している。並列の形も N×N ではなく (N-1)+N | 示せない(打ち切りは重ならない回で再現しない) | 無し(Issue の受入基準は入れ子の並列を絞ること) | 残課題(**決着の理由への反論**なので PR コメントで人間に渡す) |
| CR7: 基準 4 の証拠は「交互に測った」ことを示していない(順序・時刻が無く、変更後は 2 回) | 示せる(計画の表に順序が無い) | 4(証拠の不足) | **測り直した**(コードは変えない)——下の「交互の測り直し」。4 回の予定を 1 組(2 回)で止めた(人間の判断。#464 に CPU を空けるため) |
| CR8: `nested_jobs` を CPU の数で頭打ちにしていない(`--jobs 32` で入れ子 16) | 示せる(親 32 で 16)が、基準 1 は「親より多くない」で、16 < 32 | 無し | 残課題 |
| CR9: FAIL の行・ログの見出しに入れ子に渡した `--jobs` が出ない | 示せない(並列に依る失敗の再現の例は仮定) | 無し | 残課題 |

### 採否の表(2 パス目)

| 指摘 | 実物で失敗を示せるか | 破る受入基準 | 採否 |
| --- | --- | --- | --- |
| Verifier 注意 1・R1: 交互の測り直しは 1 組(248.8 / 243.1 秒)で、予想の +8%(約 19 秒)を判別できない。基準 4 には閾値が無く落ちようがない | 示せる(計画の表のばらつき 217〜262 秒) | 4(証拠の不足。成果物の失敗ではない) | 残課題——**PR コメントに「基準 4 は証拠が弱い」と名指し**(人間の判断。2026-10-27) |
| Verifier 注意 2・R2: 1 分平均 11〜47 で、他の `check-all.py` 以外の負荷は統制していない。順序も固定 | 示せる(計画の記録) | 4(同上) | 残課題(R1 と同じ扱い) |
| R3: 経路表の「`red` が `check-all.py` を起こす——今の主張には無い」は偽。`red` が `test-check-all.py` の主張が 4 件ある | 示せる(`grep` で 4 件)。ただし `test-check-all.py` は `run` を偽物に差し替えて `main()` を回す(`test-check-all.py:281`)ので、重い入れ子にはならない | 無し | 残課題(経路表の言い方の誤り) |
| R4: 孫の `test-check-mutation-claims.py` は既定の `--jobs` で `main()` を回すので、`check-all.py --jobs 1` でも孫は直列でない | 示せる(主張 3 件) | 無し(基準 2 は「`check-mutation-claims.py --jobs 1`」と定義。1 パス目の CR4 と同じ) | 残課題 |
| R5: `test-worktree-scripts.py:197-199` に CR5 と同じ根拠の無い因果が残り、`check-all.py` の docstring と食い違う | 示せる(ファイル:行) | 無し(範囲外だった) | **直す(人間の判断。2026-10-27)**——範囲を広げ、基準 6 を足した |
| R6: 「未証明」が狭い——打ち切りはこの機械でも重ならない回で再現しておらず、「打ち切りを防ぐ」はこの機械でも未証明 | 示せる(計画の実測節) | 無し | 残課題——PR コメントの未証明に足す |
| R7: CR8 の理由「16 < 32」は base との比較を見ていない——10 コアで `--jobs 32` なら入れ子は変更前の 10 から 16 に増える | 示せる(式から) | 無し(基準 1 は「親より多くない」) | 残課題(**決着の理由への反論**なので PR コメントで人間に渡す) |
| R8: 捨てた測定(237 / 227 / 222 秒)と残した表の値(227.2・237.4)が近く、混ざったかもしれない | 示せない(1 セッション目の記録は残っていない) | 無し | 残課題 |
| R9: 高度——名前で 1 段だけ絞っており、下の段は既定の並列のまま。環境変数で継ぐ上限なら全段に効く | 示せない(設計の提案) | 無し | 残課題 |
| R10: 半分の固定は、軽い歯止めが終わった後の尾で約 N/2 コアを空ける | 示せない(wall +8% の原因という推測) | 無し | 残課題 |
Loading
Loading