# 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 の結合つきで実行します。