EQ_S3B_STAGE_PLAN（P28-02-EQ 段階 S3b。EQ_S3_STAGE_PLAN r01/r02 の S3b を、実際の API に合わせて具体化する。SPEC、TASK、6条件、eq/references、eq/registration、補足 r01/r02/r03、段階の計画 r01/r02 は変更しない）

共通
- 最初に eq/PROGRESS.md と eq/IMPLEMENTATION.md（末尾の S4 の結合の要件を含む）を読み、S1/S2/S3a の型・path・record schema・API・notes を再利用する。再設計しない。
- E3、既存の91件の試験の本体、失敗の履歴は、消さない。書き直さない（失敗した試験が求める修正を除く）。
- E4/E5 の操作は、SPEC §3.3 (b) の専用の scenario の区間でだけ行う。各区間には開始と終了の記録を付ける。check_eq.py の静的な許可は、この区間と、下に列挙した操作に限る。
- 状態は全て r02 の A の typed tree で記録する（深さの制限なし、可逆）。正規化は r03 の閉じた一覧だけを使う。
- 各操作について、前後の S/K/C 層の tree と、全イベントの差分と件数と、scenario の中で出た signal（測定の区間とは別の記録）を記録する。

E4 キャッシュの古さ（R と O。5つの経路を、この順序で行う。各経路の後の状態を、次の経路の開始の状態として記録する）
1. 状態を変えるコマンド（通常の command queue を通す）
   - SimulationRuntime.submit_command(&"build_facility", {definition_id, origin, rotation})（autoload/simulation_runtime.gd:476、:305、:7717-7720）。
   - 引数は固定の規則で選ぶ。定義の一覧（mvp_content_catalog）を ID の文字の順序で、cell を (x, y, z) の辞書順で走査し、製品の公開の配置の判定で、最初に配置できる組を選ぶ。選んだ規則と結果を記録する。施設の値を作らない。配置できる組が無い場合（たとえば O の小さな世界）は、理由つきの NA にする。
   - 記録の時点: submit の直後（queue の状態、command の ID の counter）と、advance_minutes(1) を1回行った後（APPLY_COMMANDS で実行された後）。両方で、create_snapshot() と latest_snapshot の tree を記録する。
2. tick: advance_minutes(1) を1回。直後の create_snapshot() と latest_snapshot を記録する。
3. 直接の restore_snapshot(P)。P は、この経路の開始の時点の create_snapshot() の deep copy。
   - 直後の S が、P を規則どおりに復元したものと、正規化の後で完全一致すること。
   - SAVE_* の event は0件。
4. load_from_path の成功
   - この経路の開始の時点の snapshot を、SaveService.save_snapshot で、固定の user:// の path（例: user://p28_eq/<R|O>/e4_good）に、固定の metadata で保存する。save は静的な API で、Runtime の event を出さない。
   - その path を、Runtime の load_from_path で読む。
   - 直後の状態が X(P, ev_load) と、正規化の後で完全一致すること。SAVE_LOADED はちょうど1件。
5. 状態を変えないコマンド（製品が検証の段階で拒否するコマンド）
   - submit_command(&"build_facility", {definition_id: ""}) を submit して、advance_minutes(1) を1回行う。
   - 戻り値と、command の結果（拒否の code）を記録する。
   - 製品の状態の変化が、command の記帳（次の command の ID、処理済みの結果の記録）と tick の進行の分に限られることを、全ての path の差分で記録する。それ以外の変化があれば、事実として記録する。
- 各経路の後に、create_snapshot() と latest_snapshot を記録する。latest_snapshot は publish（tick）の時にだけ更新される製品の仕様なので、tick を含まない経路の直後に latest_snapshot が以前の値のままであっても、それは記録する事実であって、cache の古さとは呼ばない。古さの判定は、OPT の後の版との、同じ場面での比較（compare_versions.py）で行う。
- この段階での検査: 同じ版の2 PID の間で、5つの経路の記録が、正規化の後で一致すること（harness の決定性）。

E5 所有権（R と O。4つの対象 × 入れ子の dict と配列 × 2つの向き）
- 対象:
  - (1) create_snapshot() の戻り値
  - (2) SimulationRuntime.latest_snapshot
  - (3) snapshot_published の引数
  - (4) state_updated の引数
  - (3) と (4) は、probe の既存の接続で受け取った参照を、handler の外で保持して使う。handler の中で変更しない（他の受け手への影響を避けるため）。
- 変更の場所（固定。R と O の両方に存在するもの）:
  - 入れ子の dict: systems.runtime.id_generator.counters に、key "__p28_eq_probe" と値 1 を追加する。
  - 入れ子の配列: systems.runtime.domain_events.published_ids に、文字列 "__p28_eq_probe" を追加する。
- 外から内:
  - 変更の直前に、内部の create_snapshot() の tree と、latest_snapshot の tree と、latest_digest の文字列を記録する。latest_digest は遅延して計算されるので、変更の前に一度読んで確定させ、その事実を記録する。
  - 変更の直後に、同じ3つを取り直して比べる。
  - 変化があれば、変わった path と値を全て記録する。
- 内から外:
  - 返された値の deep copy を保持する。advance_minutes(1) を1回行った後に、返された値そのものが、保持した deep copy と同じであることを確かめる。
- 予想される既存の不具合:
  - latest_snapshot と、signal の引数 (3)(4) は同じ object である（:12617-12621）。そのため、(2)〜(4) の外から内の変更は、latest_snapshot を変え、まだ計算していない digest にも影響しうる。
  - これは「既存の不具合」として、対象・向き・変わった path・値・digest への影響を全て記録する。PASS で補わない。harness による無断の変更とは区別する（どの scenario の、どの操作かを結びつける）。
  - その後の検査は、advance_minutes(1) を1回行って新しい latest_snapshot が publish された後の状態を、新しい baseline として始める。
- (1) が deep copy であれば、影響が無いことを予想する。影響があれば、同じく事実として記録する。

check_eq.py と試験
- E4 と E5 の記録の形式・必須の欄・矛盾の検査を加える:
  - 経路の順序
  - 区間の連続
  - 件数（restore は0、load の成功は1）
  - X(P, ev_load)
  - 4×2×2 の全ての組の存在
  - NA の理由
- 拒否の試験を加える:
  - 経路の欠落
  - 件数の不一致
  - 既存の aliasing を PASS として扱うこと
  - aliasing の記録の欠落
  - handler の中での変更
  - 区間の外での状態の変更
  - tick を含まない経路の latest_snapshot の据え置きを cache の古さと誤判定すること
  - NA を PASS として数えること

終わり
- engine を使わない python3 test_eq.py の全件 PASS まで直し、生出力を保存する。PROGRESS.md を更新する。正式な JSON の status を blocked にし、findings を「段階 S3b 完了（失敗ではない。次は S4）」とする。提出しない。
- engine の起動、git への書込み、製品の変更、全回帰はしない。通常の実装上の判断は、記録して進める。
