P28-01-RESTART: Track SPEC_CLARIFICATION_02 (6-file import closure, execution contract)

Frozen SPEC/DRAFT/baseline/contract unchanged; original acceptance RS1-RS8 unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Takeshi
2026-10-21 01:21:00 +09:00
co-authored by Claude Opus 5.5
parent d908a2c4a2
commit 971996f6d5
@@ -0,0 +1,84 @@
# SPEC_CLARIFICATION_02(P28-01-RESTART:依存の閉包と実行契約の確定)
- 対象: base `2132bbe3d39a0f1b69a97c731bb09213bf9d7a91` 上の SPEC.md / DRAFT.md / TASK_INSTRUCTIONS.md と、SPEC_CLARIFICATION_01 です。
- 変えないもの: 元の受入 RS1〜RS8、baseline、contract、spec hash。製品、セキュリティ、権限は変更しません。
- この文書の役割: repair1 run `facdc513fdd54a5e9899ecc7e4afb817` が blocked になった件(依存の閉包)に答えます。あわせて、残っている実行上の曖昧さを1回でまとめて確定します。
- **これ以外の実装上の選択**(関数の分け方、名前、データ構造、ログの書式など)は、承認された範囲の中での worker の通常の判断です。原本の仕様の不足として blocked にしないでください。
## D1. import の閉包(完全な列挙。それ以外は禁止)
import してよい project の Python は、次の6ファイルだけです(Root の読取専用の監査 `RESTART_DEPENDENCY_CLOSURE_READONLY.json`、SHA `06fdecc471a56d0edab9f58a68b33c433b1a5ab9d9db38024bd8e97e66166766` の6件と一致。第7のファイルはありません)。どれも読取専用で使い、変更しません。SHA は base 時点の値で、実行時に照合します。
| ファイル | SHA256 | 区分 |
|---|---|---|
| `tools/run_all_tests.py` | `320ca668732b4ff72fc4449998af2327d4e5c527b06896740806aaf0487282b4` | 承認済み |
| `tools/regression/owned_outer.py` | `c2e3483b76db1a3ff940f23f88ea0083fc78736e874ebcdb385d766dde8866e7` | 承認済み |
| `tools/regression/owned_diagnostic_process.py` | `e31e93c6e2c5ea46a7d954ef8bc0b4caec1c08b86b8aef19e93b52ef24993087` | 推移(owned_outer.py:9-10 が import 時に読み込む) |
| `tools/regression/p09_owned_engine_process.py` | `c007e712c281573254faa1233c2ac92e100ab87393256a6dc096b35283e43037` | 推移(owned_diagnostic_process.py:10-14 が SHA を照合してから exec する) |
| `docs/plans/original-completion-2026-10-02/execution/p28-01/collector/collect_p28_01.py` | `a43e1f963ef8505ddd5acf8bf747d9bfac22a028cbb805c0b964ee11b9b40a14` | 承認済み |
| `docs/plans/original-completion-2026-10-02/execution/p28-01/collector/check_p28_01_report.py` | `6c0851d498ca80bdc120fad047b072255f48713f63114521f3d4dfc902b2512b` | 推移(collect_p28_01.py:16 が読み込む) |
- **読み込み方**: collector は `importlib.util.spec_from_file_location` で読み込みます。その前に、collector のディレクトリを `sys.path` の先頭に追加します(collect_p28_01.py:16 の `from check_p28_01_report import ...` を解決するため)。run_all_tests.py と owned_outer.py は、collector の `load_tool(EXTERNAL_IMPORTS[i])` で取得してかまいません。
- **使わないもの**: run_all_tests.py の `main()` の中で読み込まれる `tools/strict_test_markers.py`、`tools/regression/diagnostic_gate.py`、`ordinary_proof_gate.py`、`audio_evidence_gate.py` は使いません。run_all_tests.py の `main()` も呼びません。
- **標準ライブラリ**: 通常どおり import してかまいません。新しいものの導入、third-party の package、ネットワークは禁止です。
- **checker の検査**: 上の6ファイル以外の project の `.py` を import しようとしたら、拒否します。拒否試験も置きます。
- 方法は2つ組み合わせます。AST で import 文と、`spec_from_file_location` / `exec` の文字列の path を走査することと、実行時に `sys.modules` の `__file__` を照合することです。
- 6ファイルの SHA が1つでも違えば、起動の前に拒否します。
## D2. CLI と入力
- `restart_pair.py --check|--run --readonly <build/p28-01-readonly> --fixture <reference.variant> --engine <Godot> --output <新しいdir>`。collector と同じ形で、`--check` は計画の argv を出力するだけです。
- guard は `<readonly>/run-r02/profile_guard.gd` を使い、wrapper は collector と同じ形(`extends "<probe>"` + `Guard.activate()`)で生成します。Root はこの project の allocation にも、`build/p28-01-readonly` を同じ SHA で置きます。
- 実行の順序は固定です: import → parse-base → parse-wrapper → R-writer → R-reader → O-writer → O-reader。
- 各 phase は collector の `run_private_engine`(新しい private profile、owned、上限 1800 s)で実行します。
- parse は collector の L2 と同じ規則です(`--check-only`、exit 0、SCRIPT ERROR/Parse Error/ERROR が無いこと)。
- reader は、対応する writer が成功したときだけ起動します。どこかが失敗したら、そこで run 全体を止めます。
- 役割と条件は env で渡します。`THE_TREE_PACK_PROFILE_*` は collector と同じものを使います。追加は `THE_TREE_RESTART_ROLE`、`THE_TREE_RESTART_K=30`、`THE_TREE_RESTART_TRANSFER=<絶対path>`、`THE_TREE_RESTART_TRANSFER_MANIFEST=<json の絶対path>` です。
## D3. 保存 package の受け渡し(profile を事前に書き換えない)
- guard は、runtime profile が事前に新しいことと、guard より前の書込みが0であることを要求します。そのため、reader の profile に事前にファイルを置くことは**禁止**です。
- writer の probe が正規の UI で保存した package を、driver が engine の終了後に writer の private user:// から `out/<R|O>-transfer/good/` へ複製します。全ファイルの SHA を manifest に記録します。
- 改ざん用のコピー `out/<R|O>-transfer/tampered/` は、good の複製の `world.bin` の offset `len//2` の1バイトを XOR 0x01 したものです。offset と、改ざん前後の SHA を記録します。
- reader の probe は guard が有効になった後で、transfer を絶対 path から読み、自分の `user://p28_restart/good/` と `user://p28_restart/tampered/` へ書き出します。manifest の SHA と一致しなければ、止めて失敗にします。そのあと、UI の読込で `user://` の path を使います。
## D4. 一時停止と保存の時点(実際の UI/Clock の経路)
- 時間は実際の Clock(`_set_speed(1)` → `SimulationKernel._process`)だけで進めます。
- 保存の時点は、`GameClock.tick_completed(absolute_minute)`(simulation_kernel.gd:9、:154)に受動接続して判定します。
- R の writer: 開始から10分以上進んだ tick。
- O の writer: 実際の arrival が >0 になった後の最初の tick。上限は120分です。
- その signal の handler の中で `GameClock.set_paused(true)` を呼びます。kernel は tick ごとに paused を確かめるので、catch-up の途中でも止まります。
- `set_paused(true)` は時間を進めない一時停止なので、許可します。`set_paused(false)` は従来どおり禁止です。`_toggle_pause` は使いません(Ops の acknowledge に分岐するため)。
- overload で先に止まった場合は、その状態のまま保存します(RS5 の止まった状態での保存)。
- arrival の判定は、guest_id が `guest-auto-` で始まる客(simulation_runtime.gd:2963、:3421)の実数(>0)と、`get_operations_report()` を併記して記録します。
## D5. 比較の実行場所と記録(役割分担)
- 状態を正確に比べるのは probe です(GDScript、同じ engine の中)。
- X(P, ev)、X(B, ev_fail)(CLARIFICATION_01)、各分の比較は、`var_to_bytes` の完全一致で判定します。
- 一致しなかった場合は、差のある path の一覧(depth ≤ 4)を出力します。
- 大きな状態(R は約11 MB)を全分保存することは避けます。記録するのは次のものです。
- 全 bytes のファイル: P、writer の保存直後、reader の改ざん試行の前(B)、試行の後、読込直後、継続 K の最後の状態。
- 毎分の値: 全状態の `var_to_bytes` の SHA256。
- writer と reader の各分の照合: 両側とも、event_id E の要素1件(窓の中にある間)を、同じ正規形 `{"event_id": E, "replaced_for_cross_pid_compare": true}` に置き換えます。その後の全状態 bytes の SHA256 が、全分で一致することを要求します。両側の E の要素の全内容は記録します。この置き換えは DRAFT §3.3 d の規則と同じもので、広げてはいません。
- SimulationDigest は参考の記録にだけ使い、一致の判定には使いません。全 bytes の SHA256 は bytes の完全一致の代わりとして扱います。
- Python の checker が確かめるのは、report の形式・矛盾・SHA の結合と、両 PID の毎分の SHA の一致、そして D1/D7 の静的検査です(Godot の値の再構成はしません)。probe の実経路は静的レビューで確かめ、実際の実行は Root の owned PID と raw の結合で確かめます。
## D6. 終了と診断
- main を追加した phase は、collector の K1 と同じく `get_root().close_requested.emit()` で終了し、`P28_RESTART_AUDIO_CLOSE` の1行を要求します。
- report は stdout の `P28_RESTART_REPORT=<json>` ちょうど1行です。PID の結合、raw の SHA、owned folder へ書き込まないこと、は collector と同じです。
- probe の自己停止は 1740 s です。
## D7. 静的検査(restart probe 用。SPEC §6 を具体化)
- UI 区間で許可するもの: `_set_speed`、`_save_manual_at_path`、`_load_manual_at_path`、`GameClock.set_paused(true)`(tick 完了の handler の中だけ)、`close_requested.emit()`、受動的な connect。
- 決定性区間: 単一の `func deterministic_step()` の中の `GameClock.advance_minutes(1)` だけを許可します。呼んでよいのは、保存か読込が成功し、clock が止まった後だけです。
- 禁止: 決定性区間の外の `advance_`、`set_paused(false)`、`clear(`、`spawn_guest`、`SimulationRuntime.new`、`initialize(`、`enable_production_`、`_toggle_pause`、決定性区間の中の `Time.get_ticks_usec`。性能値の欄も禁止です。
## D8. 試験(test_restart.py)
- SPEC §4 と CLARIFICATION_01 C4 のすべてに加えて、D1〜D7 それぞれに、正常試験と拒否試験を1つ以上置きます。
- 試験は engine を使いません。合成した report と、collector の J2 と同じ「本物の owned_outer に偽の子プロセス」で動かします。
- 例: 閉包の外の import、6ファイルの SHA の違い、事前に profile へ書き込むこと、transfer の SHA の不一致、`set_paused(false)` や `_toggle_pause`、毎分の SHA の不一致、E 以外の置き換え、writer の失敗の後に reader を起動すること。