P28-01-RESTART: Track spec, worker instructions and Root 4-PID procedure

Finite acceptance RS1-RS8 from the preserved draft (byte-identical DRAFT.md),
pinned to the collector final Q and the integrated full9 receipt on db9d613.
Docs only, outside the product source map; nothing registered or run.
Original credit unchanged (22/88, 4/30).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Takeshi
2026-10-21 00:53:49 +09:00
co-authored by Claude Opus 5.5
parent db9d6132a8
commit 2132bbe3d3
4 changed files with 283 additions and 0 deletions
@@ -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 の結合つきで実行します。