diff --git a/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/DRAFT.md b/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/DRAFT.md new file mode 100644 index 0000000..be4f4bf --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/DRAFT.md @@ -0,0 +1,123 @@ +# P28-01-RESTART 最終草案(未登録・未実行) + +- 状態: Desktop 管制による草案です。collector の final が出るまで、登録、worker の起動、engine の実行はしません。 +- 目的: 原 P28 の横断条件「途中保存・再起動で再現」を、R と O のそれぞれについて、writer と reader を別PIDにした1組で示します。性能の数値はここでは比べません。 +- 原本の扱い: collector だけでは原本に加算しません(22/88 のまま)。P28-01 の加算には、次の4つがすべて必要です。 + 1. collector の final + 2. 実測 + 3. RESTART の pair の PASS + 4. 証拠ゲート +- 予算の未達、200客での Operations ON、Windows は、未達のまま残します。 + +## 1. 範囲と入力 + +| | writer(PID A) | reader(PID B、別の engine プロセス) | +|---|---|---| +| R(602分、Ops OFF) | SPEC §4.3 と同じ起動(autoload への復元 → main の追加 → `_set_speed(1)`)。実際の clock 経路で10ゲーム分、または `simulation_overloaded` までの短い方を進め、clock が止まった状態で保存する | 新規開始で main を追加(`_ready` の Ops 開始結果をそのまま記録)→ UI の読込で R の保存を読む | +| O(新規開始、Ops ON) | 新規開始の main で実際の x1 を進める。最初の**実際の arrival**(下の §4)が起きた後の最初の分で、clock を止めて保存する。上限は120ゲーム分 | R と同じ | + +- 保存には、main の実際の手動保存 `main._save_manual_at_path(path, name)` を使います(main.gd:2243 → `save_to_path` :4195 → `_perform_checkpoint_save`)。 +- 読込には、main の実際の読込 `main._load_manual_at_path(path, true)` を使います(main.gd:2288 → `load_from_path` :4223)。 +- 保存先は private user:// だけで、通常 player の保全を前後で照合します。 + +## 2. 成否の取り方(`_save_manual_at_path` は bool、`_load_manual_at_path` は void) + +- `_save_manual_at_path`(main.gd:2243)は bool を返し、`_load_manual_at_path`(main.gd:2288)は void です。 +- 保存の bool 戻り値は記録し、signal の `ok` と一致することを要求します。ただし、bool の true だけで成功とはしません。 +- 保存も読込も、成否は下の signal、`latest_save_operation_result`、実状態の3つで判定します。 + +- 呼出しの前に、`SimulationRuntime.save_operation_completed`(:9)へ受動的に接続しておきます。 +- 成否は、次の3つがそろったときに成功とします。 + 1. signal の result(ok、code、operation、save_id、saved_absolute_minute) + 2. 直後に読んだ `latest_save_operation_result`。§3 の IO イベントの payload と一致すること + 3. 実際の状態。clock の `absolute_minute` が saved_absolute_minute と一致し、§3 の比較が成立すること +- 読込の void 戻り値も、保存の bool 戻り値の true も、それだけでは成功とみなしません。signal が来なければ、失敗として原記録を残します。保存の bool と signal の ok が食い違えば、不整合として失敗にします。 +- `latest_save_operation_result` は `create_snapshot()`(simulation_runtime.gd:521-583)に含まれません。そのため状態比較の対象にはせず、別欄に記録して IO イベントの payload との一致だけを確かめます。 + +## 3. IOイベントの狭い対応規約(広い削除は禁止) + +### 3.1 事実 + +- snapshot の中の場所 + - `systems.runtime.domain_events = {version, published_ids, recent_events}`(scripts/core/domain_event_stream.gd:96-112) + - `published_ids` は、これまでに公開した全 ID を文字列で sort した配列で、上限はありません。 + - `recent_events` は直近の最大256件(`RECENT_EVENT_LIMIT`、:6)。append のとき、256件を超えると先頭が1件落ちます(:58-60)。 + - `systems.runtime.id_generator.counters["domain_event"]`(キーは String、値は int) +- 正式な保存と読込の完了時には、`_complete_save_operation`(:4734)が `_publish_actor_event`(:12558)を1回だけ呼びます。このとき採番される ID は1つで、`E = "domain_event.%012d" % (c + 1)` です(id_generator :13-18)。ここで c は package の snapshot P の counter です。 + - writer のイベントの型は `SAVE_CHECKPOINT_SUCCEEDED`、reader は `SAVE_LOADED` です。 + - どちらも `source_system=save_service`、`entity_ids=[]`、payload は `_save_event_payload(result)` です。 + - writer と reader は同じ P から採番するので、E は同じ ID になります。 + +### 3.2 期待状態の構成と、全状態の完全一致 + +除外して合わせるのではなく、P に IO イベント ev を1件だけ規則どおり適用した期待値 `X(P, ev)` を作り、実際の snapshot 全体と完全一致(typed `var_to_bytes`)させます。 + +- `id_generator.counters["domain_event"]` = P の値 + 1。他の counter はすべて P と同じ。 +- `published_ids` = sort(P.published_ids ∪ {E})。E は P に含まれていてはいけません。 +- `recent_events` は P.recent_events の長さで分けます。 + - 256 未満のとき: P.recent_events に ev を末尾に足したもの + - 256 のとき: P.recent_events の先頭1件を落とし、ev を末尾に足したもの。落ちた先頭1件は、全内容を原記録に残します。 +- ev は、event_id が E、型・source・entity_ids・minute は実際の値で、payload は signal の result から作ったもの(= `latest_save_operation_result`)です。 +- 上記3つのパス以外は、P とバイト単位で同じでなければなりません。 + +### 3.3 比較の種類 + +| 比較 | 規約 | +|---|---| +| a. writer の保存直前 `create_snapshot()` と P(`SaveService.load_snapshot(path)` で読んだもの。runtime は変えない) | 完全一致。不一致なら、差のあるパスの全一覧を残して止め、判断に戻す | +| b. writer の保存直後と `X(P, ev_save)` | 完全一致 | +| c. reader の読込直後と `X(P, ev_load)` | 完全一致 | +| d. 継続 K 分の各分 t について、writer_t と reader_t | 全状態の完全一致。違ってよいのは、`recent_events` の中の event_id E の要素1件だけです。E がまだ直近256件の窓の中にある間は、reader 側のその要素を writer 側のものに置き換えた状態が、writer_t と完全一致しなければなりません。窓から落ちた後は、置き換えなしで完全一致です。`published_ids` と counter は、置き換えなしで一致します。置き換えた両方の要素は全内容を記録します | + +- digest は `SimulationDigest.compute` で計算し、上の比較が成立した状態について記録します。digest 同士の一致だけで比較の代わりにはしません。 +- 規約の外で違う箇所が1つでもあれば失敗です。差のあるパスの一覧を返します。規約を広げるには、新しい判断が必要です。 + +## 4. Operations の境界と、実際の進行 + +- O の writer は、390分の時点で次のものを記録します。 + - 実際の clock + - `get_operations_report()`(coordinator の current_minute、population) + - 処理済みの arrival 分 + - 実際に来館した guest-auto の数 + - その間の domain_events +- 保存できるのは、実際の arrival(来館数 > 0)が起きた後だけです。境界に到達しただけで、arrival を処理したことにはしません。120分までに arrival が起きなければ `no_arrival_within_cap` とし、Ops 境界の項目は null と理由にします。 +- 継続 K: + - 各側で clock を止めた状態にして、`advance_minutes(1)` を1回ずつ呼びます(止めた状態での手動の stepping、simulation_kernel.gd:104-119)。 + - 呼出しの前後で `absolute_minute` を照合し、進まなければ `stop_reason=no_tick_advance` で止めます。 + - 実際の K は、実際に進んだ分の数です。両側の実際の K が一致することを要求し、K=30 のときだけ30分と表記します。 + +## 5. 異常と境界(最小限) + +1. **改ざんした保存の原子的な拒否**: package のコピーで `world.bin` の1バイトを反転させます。UI の読込が拒否され、試行の前後で runtime の全 snapshot が完全に一致することを確かめます。 +2. **止まった状態での保存(R)**: overload で止まった writer の保存は、読込後の reader も止まった状態であること、backlog を消費しないことを確かめます。 +3. **Ops の境界をまたぐ(O)**: 継続 K の d. の比較が、次の arrival 境界で重複も欠落もないことを示します。 + +## 6. 静的な禁止検査(collector の check_probe は丸ごと import しない) + +| 区間 | 許可するもの | 禁止するもの | 用途 | +|---|---|---|---| +| UI 区間(起動、実際の clock、UI での保存と読込) | `_set_speed`、`_save_manual_at_path`、`_load_manual_at_path`、受動的な接続 | `advance_`、`set_paused(false)`、`clear(`、`spawn_guest`、`SimulationRuntime.new`、`initialize(`、`enable_production_` | 状態と IO の観測だけ。性能値は出さない | +| 決定性区間 | 単一の `func deterministic_step()` の中の `GameClock.advance_minutes(1)` だけ。呼び出せるのは UI 区間が終わって clock が止まった後だけ | `Time.get_ticks_usec` と `*_us` の統計欄、および上の UI 区間の禁止 token(許可した1つを除く) | writer と reader の比較だけ。性能・予算・速度には使わない | + +checker の拒否試験は、少なくとも次のものです。 + +- `deterministic_step` の外での advance +- 決定性区間から出た timing 値 +- 呼出し回数で K を数えること +- 規約外のパスの除外や置き換え +- cap が256のときに「末尾1件の除外」として扱うこと +- E 以外のイベントの置き換え +- 読込の void 戻り値や、保存の bool の true だけで成功とみなすこと +- 保存の bool と signal の ok の食い違いを見逃すこと +- 境界に到達しただけで arrival を処理したとみなすこと +- signal が来ていないのに成功とすること + +## 7. 実装と実行 + +- 置き場所: `docs/plans/original-completion-2026-10-02/execution/p28-01/restart/**`(source map の外)。 +- 前後で source map が1407件のまま一致することを照合し、正式全192の証拠を維持します。製品を変えた場合は全 roster をやり直す必要があり、旧証拠は流用できません。 +- 登録: collector の baseline C1〜C9 は凍結されているので、別の小さな project と task(`P28-01-RESTART`)として登録します。 + - spec_ref はこの草案を Git で追跡する文書にしたものです。 + - worker は通常の Runner worker、レビューは独立した Opus です。 + - 統合済みの collector の owned-phase の実行、guard、private profile は、読取専用で import します。 +- 実行: Root が、実測と並べて R に2プロセス、O に2プロセスを、owned PID と raw の結合つきで実行します。 diff --git a/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/ROOT_NATIVE_PROCEDURE.md b/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/ROOT_NATIVE_PROCEDURE.md new file mode 100644 index 0000000..0584ac3 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/ROOT_NATIVE_PROCEDURE.md @@ -0,0 +1,43 @@ +# P28-01-RESTART:Root の実測4 PID の手順 + +RESTART が final に達した後に、統合済みの main で1回だけ実行します。 + +## 事前の照合(いずれかが不一致なら起動しない) + +- 実際の main commit を記録します。source map は1407件で、collector の4ファイルは Q と一致し、restart の4ファイルは RESTART の final の S と一致すること。 +- engine の SHA、代表入力の SHA(`eb4f7700…`)。 +- 動いている engine が無いこと。空き容量が 1.2 GB 以上あること。 +- `restart_pair.py --check` の計画の argv を保存し、実行するものと一致することを確かめます。 + +## 実行(`restart_pair.py --run`、全体で1回) + +1. import を1回、owned で行います。 +2. parse を行います。`restart_probe.gd` と wrapper のそれぞれについて `--check-only` を owned で実行します。exit 0 で、SCRIPT ERROR/Parse Error/ERROR が無いことを要求します。 +3. **R-writer**(PID 1) + - autoload への復元 → main の追加 → `_set_speed(1)` の順で始めます。 + - 実際の clock で10分、または overload まで進めます。 + - clock を止めた状態で、UI から保存します。 + - その後、決定性区間で K=30 を進めます。 +4. driver が package を byte 一致・SHA で照合して、reader の private user:// へ複製します。改ざん用のコピーは、`world.bin` の1バイトを反転したものにします。 +5. **R-reader**(PID 2) + - 新規開始の main で始めます。 + - 改ざんコピーを UI から読み、拒否されること、状態が完全に不変であることを確かめます。 + - 正しい保存を UI から読みます。 + - 決定性区間で K=30 を進めます。 +6. **O-writer**(PID 3) + - 新規開始の main で始め、実際の x1 で進めます。 + - 実際の arrival が >0 になった後の最初の分で(上限120分)、clock を止めて UI から保存します。 + - 決定性区間で K=30 を進めます。 +7. **O-reader**(PID 4): R-reader と同じ手順です。 +8. 各 PID には、collector と同じ guard の bootstrap、新しい private profile、owned の Popen を使います。phase の上限は 1800 s です。 + +## 判定(engine を使わない `check_restart.py`) + +- RS1〜RS7 のすべてと、4 PID の PID・raw の結合。 +- source、通常 player、stage の inventory、guard、cleanup が前後で一致すること。 +- 失敗した場合は原記録を残して止め、判断に戻します。そのまま再実行はしません。 +- R の overload での停止は、実際の値として記録します。O の arrival が無い場合は、null と理由にします。 + +## 証拠ゲート(その後) + +collector の final(Q)、統合版の全9 phase の実測、RESTART の final、この4 PID の pair を結合したときに限り、P28-01 を 22→23 とします。予算の未達、200客での Operations ON、Windows は、未達として残します。 diff --git a/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/SPEC.md b/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/SPEC.md new file mode 100644 index 0000000..1bb3cea --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/SPEC.md @@ -0,0 +1,88 @@ +# P28-01-RESTART 仕様(spec_ref) + +- 状態: 登録用の確定版です。この文書を Git で追跡し、それを含む commit を base にします。 +- 元の草案: 同じディレクトリの `DRAFT.md`(SHA `a4a8261bcc14d38de8df8d9024321dd8fdb6c6356964ff43472d2bf84eff1334`。Control handoff の `RESTART_DRAFT.md` とバイト一致)。§番号は草案と同じです。この仕様は草案を契約の形にしたもので、規約を広げてはいません。 + +## 1. 原本との対応(変更しない) + +- 対象は原 P28 の横断条件6件のうち、「途中保存・再起動で再現」です(`work_packages.json` P28.cross_cutting_acceptance[2])。 +- 成果単位は P28-01「現HEADの性能基準」です。work と acceptance は collector の SPEC §1 と同じです。 +- 分母は原30/88で、部分点はありません。この task が完了しても、原本は加算0です。P28-01 の加算(22→23)には、次の4つがすべて必要です。 + 1. collector の final(完了済み、Q `0f53a672…`) + 2. 統合版で全9 phase を実測すること(完了済み。実際の commit `db9d6132a8349928560dc4a4a99306d27a81e332`、`artifacts/original-completion-2026-10-02/p28-01-runner-preflight-r01/full-measurement-db9d613-r01/run/execution.json` SHA `a4d10e9b1a75a17b981792265338d64fac61178cb6f87dd646c5b486292db73c`、Root の結合 `ROOT_RAW_ACCEPTANCE_JOIN.json` SHA `a76082f767e0a0d52cb70e2f66fa371b62a88754e9be319da3c08d4b48e78ba0`) + 3. この RESTART の final と、Root の実測4 PID の pair の PASS + 4. 証拠ゲート +- 次のものは、未達のまま残します。 + - 予算の未達 + - 200客での Operations ON(製品の上限100) + - Windows +- 予算値、製品の値、停止契約は変更しません。 + +## 2. 成果物(worker、allowed_paths `docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/**`) + +| ファイル | 役割 | +|---|---| +| `restart_pair.py` | Root が実行する driver です。統合済みの collector から、`no_live_engine`、`source_map`、`run_private_engine`、`run_engine` と、guard、private profile、owned phase の仕組みを、読取専用で import して使います。import してよい外部ファイルは `tools/run_all_tests.py`、`tools/regression/owned_outer.py`、統合済みの `execution/p28-01/collector/collect_p28_01.py` だけです。`--check` は計画だけを出力し、`--run` で4 PID を実行します。 | +| `restart_probe.gd` | writer と reader の probe(extends SceneTree)です。草案 §1〜§5 を実装します。 | +| `check_restart.py` | report と receipt の checker です。§4 の拒否をすべて実装します。 | +| `test_restart.py` | engine を使わない unittest です。 | + +## 3. 受入グループ(baseline。有限で、草案の各節に対応する) + +- **RS1(草案§1〜2)保存・読込の成否** + - 保存は main の `_save_manual_at_path`(bool)、読込は `_load_manual_at_path(path, true)`(void)で行います。 + - 成否は、`save_operation_completed` の signal、`latest_save_operation_result`、実際の状態の3つで判定します。 + - 保存の bool と signal の ok は一致しなければなりません。 +- **RS2(§3.2、3.3 a〜c)IO の厳密な対応** + - a: P が保存直前の状態と完全一致すること。 + - b: 保存後の状態が X(P, ev_save) と完全一致すること。 + - c: 読込後の状態が X(P, ev_load) と完全一致すること。 + - X の構成は §3.2 のとおりで、counter+1、published_ids への E の sort 挿入、recent_events(上限256、溢れたら先頭を1件落として記録)です。 +- **RS3(§3.3 d、§4)継続の一致** + - 実際に進んだ K(=30)分の各分で、writer と reader が全状態で完全一致すること。 + - 置き換えてよいのは、窓の中にある event_id E の要素1件だけです。 + - K は実際に進んだ分の数で数えます。 +- **RS4(§4)Operations の境界** + - O は、実際の arrival(>0)が起きた後に保存します。 + - 120分までに arrival が無ければ、null と理由にします。 + - 境界に到達しただけで、arrival を処理したことにはしません。 +- **RS5(§5)異常と境界** + - 改ざんした保存の原子的な拒否(reader の PID 内で、正しい保存の前に試します)。 + - R の止まった状態での保存と、読込後も止まっていること。 + - O の次の arrival の境界に、重複も欠落もないこと。 +- **RS6(§6)区間の静的検査** + - UI 区間と決定性区間の、それぞれの禁止と許可を守ること。 + - 性能値を一切出さないこと。 +- **RS7(§7、collector C6/C7 と同じ)分離** + - private user://、owned PID、guard、cleanup、通常 player の保全、全 raw と SHA の結合。 + - source map が1407件のまま前後で一致すること。 + - collector の4ファイルが Q と一致すること。 +- **RS8 横断条件の記録** + - 6件の横断条件それぞれについて、適用するかどうかと理由を記録します。 + - 「途中保存・再起動で再現」は、RS1〜RS5 で適用します。 + - それ以外の5件は、collector 側の既存の受入を参照し、適用外の理由を記録します。 + +## 4. checker が必ず拒否するもの(すべて test_restart.py に拒否試験を置く) + +1. 読込の void 戻り値、または保存の bool の true だけで、成功とすること。保存の bool と signal の ok の食い違いを見逃すこと。signal が無いのに成功とすること。 +2. X(P, ev) 以外の差を許すこと。たとえば次のものです。 + - 2件以上のイベントの差 + - E 以外の ID + - counter が+1でない + - published_ids の欠落や重複 + - recent の上限での先頭の落とし方の違い + - 規約の外のパスの除外や置き換え + - 「末尾1件の除外」として扱うこと +3. 継続で、E 以外の要素を置き換えること。窓から落ちた後も置き換えること。digest の一致だけで比較の代わりにすること。 +4. advance の呼出し回数で K を数えること。writer と reader で実際の K が違うのに合格とすること。 +5. 境界に到達しただけで arrival を処理したとみなすこと。arrival が0なのに RS4 を合格とすること。 +6. 改ざんした読込のあとで状態が変わっているのに合格とすること。止まった状態での保存のあとに backlog を消費すること。 +7. 決定性区間の外での `advance_minutes`。決定性区間の中の `Time.get_ticks_usec` や `*_us`。UI 区間の禁止 token。 +8. PID、raw、source map、通常 player、guard の不一致。owned folder への書込み。 + +## 5. 依存と前提 + +- 登録済みの task への depends_on はありません。collector は別 project なので、前提は追跡済みの記録として SHA で固定します。 + - collector の final の receipt(Q) + - 統合版の全9 phase の実測の receipt(上の §1 の2つの SHA) +- collector の baseline C1〜C9 は凍結されているので、追加しません。RESTART は別 project として登録します。 diff --git a/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/TASK_INSTRUCTIONS.md b/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/TASK_INSTRUCTIONS.md new file mode 100644 index 0000000..fa346e1 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/TASK_INSTRUCTIONS.md @@ -0,0 +1,29 @@ +# P28-01-RESTART worker 指示 + +仕様は同じディレクトリの `SPEC.md` と、その元になった `DRAFT.md` です。この指示と仕様は変更しないでください。 + +## 作るもの(allowed_paths `docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/**` の中だけ) + +`restart_pair.py`、`restart_probe.gd`、`check_restart.py`、`test_restart.py` の4つです(SPEC §2)。 + +## 必須事項 + +- RS1〜RS8 を実装し、SPEC §4 の拒否それぞれに拒否試験を置きます。 +- import してよいのは `tools/run_all_tests.py`、`tools/regression/owned_outer.py`、統合済みの `execution/p28-01/collector/collect_p28_01.py` だけです。いずれも読取専用で使い、変更しません。collector の `check_probe` は丸ごと流用せず、SPEC §4-7 の区間別の検査を自分で実装します。 +- probe について: + - 既定の autoload の世界を使い、main は実際の UI 経路で動かします。 + - 決定性区間は、単一の `func deterministic_step()` の中の `GameClock.advance_minutes(1)` だけにします。 + - 性能値は出しません。 + - 製品の dictionary の key は、collector と同じ `field()` の方式で読みます。 +- 4 PID の役割は次のとおりです。 + - R-writer、R-reader、O-writer、O-reader + - reader の PID は、正しい保存を読む前に、改ざんしたコピーの読込を試します。 + - writer の private user:// にある保存 package は、driver が byte 一致を SHA で確かめてから、reader の private user:// へ複製します。 +- 製品、source map の対象、予算値、停止契約、IO の正規化の規約は変更しません。草案 §3 より広い除外は禁止です。 +- engine の起動、全回帰、fixture の生成、commit はしません。 +- 提出前に、engine を使わない `python3 test_restart.py` を局所実行して全件 PASS を確認し、その生出力をそのまま報告します。局所の PASS は、正式な validation の代わりにはなりません。 +- 決めきれない不足は、具体的な箇所を示した場合に限り blocked にします。 + +## 検証 + +Runner の validation `p28-01-restart-unit` が `test_restart.py` を実行します。そのあと、独立した Opus が全条件をレビューします。自己申告や終了コード0は、合格の証拠になりません。