Files
TheTree_mock/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/DRAFT.md
T
TakeshiandClaude Opus 5.5 2132bbe3d3 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>
2026-10-21 00:53:49 +09:00

11 KiB
Raw Blame History

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