EQ_S3_STAGE_PLAN（P28-02-EQ。EQ_STAGED_IMPLEMENTATION_PLAN r01/r02 の段階 S3 を、S3a と S3b に分ける。理由: S2 は 846.159 秒で、Runner の900秒の上限に近かったため。要求の範囲は変えない。SPEC、TASK、6条件、eq/references、eq/registration、補足 r01/r02/r03 は変更しない）

共通
- 最初に eq/PROGRESS.md と eq/IMPLEMENTATION.md を読み、S1/S2 の型・path・record schema・API・notes を再利用する。再設計しない。
- S1/S2 のコードと試験の本体を消さない。書き直さない（S3 で直すと決めた checker の2点と、失敗した試験が求める修正を除く）。
- 失敗の履歴は、PROGRESS.md と UNIT_OUTPUT に残す。各段階の終わりに、engine を使わない python3 test_eq.py の全件 PASS まで直し、生出力を保存する。
- 状態を変える呼出しは、SPEC §3.3 (b) の、列挙した scenario の専用の区間の中でだけ許可する。check_eq.py の静的な許可も、その区間と列挙した操作に限る。
- engine の起動、git への書込み、製品の変更、全回帰はしない。SPEC、TASK、eq/references、eq/registration は変更しない。

S3a（次の run）
1. checker の修正 1（eq/check_eq.py:109）
   - 各 checkpoint の immutable snapshot の phase の key の集合が、正確に {0,…,10} であることを要求する（空の dict や、一部の欠落を拒否する）。
   - 負の試験を加える: 空、1つの欠落、範囲外の key、重複した key の表現。
2. checker の修正 2（eq/check_eq.py:147-150）
   - 測定の区間の30 tick の各分について、製品の _publish_snapshot（autoload/simulation_runtime.gd:12616-12621）が出す snapshot_published → state_updated を、実際の回数と順序で記録したものを、その分の checkpoint と結びつけて検査する。
     - 各分に、ちょうど1組であること（snapshot_published に接続した受け手が無い場合は、製品の条件 :12619 のとおり state_updated だけになるので、probe の側で接続を記録し、期待値をその実際の接続の状態から決める）。
     - 順序は snapshot_published が先。
     - 分の番号は、checkpoint の minute と一致すること。
   - scenario の区間で出た追加の signal は、別の区間の記録に入れる。測定の区間の記録には混ぜない。
   - 負の試験を加える: 欠落、一部の分だけ、重複、順序の改ざん、分のずれ、scenario の signal の混入。
3. E3 失敗時の拒否（専用の scenario の区間。R と O の両方）
   - restore_snapshot、SimulationSnapshot.validate、load_from_path の3つの API に、次の不正な入力を、それぞれ1件以上与える:
     - 欠けた key
     - 違う型
     - 範囲外の値
     - 重複した ID（published_ids、facility の ID）
     - world.bin の改ざん（r03 の D3 の固定の規則）
   - 不正な入力は、B1 の snapshot の deep copy から、固定の規則で作る。乱数や時刻を使わない。
   - 記録すること:
     - 戻り値と code
     - 前後の S/K/C 層（同じ PID の中で完全に比べる）
     - 全イベントの差分と、その件数（r02 の T4: 直接の restore は0件、load の失敗は SAVE_LOAD_FAILED がちょうど1件で、X(B, ev_fail) と r03 の正規化の後で完全一致すること）
   - check_eq.py: E3 の記録の形式と矛盾の検査を加える。製品の失敗（不正な入力が受理された、状態が変わった、など）は、「既存の不具合」として記録する。PASS で補わない。
   - 試験を加える: E3 の正常の記録と、拒否の場合（code の欠落、件数の不一致、拒否の前後の状態の変化を見逃すこと）。
4. 終わり: PROGRESS.md を更新し、正式な JSON の status を blocked にし、findings を「段階 S3a 完了（失敗ではない。次は S3b）」とする。

S3b（S3a の後の run）
1. E4 キャッシュの古さ（専用の区間。R と O）
   - 場面: 状態を変えるコマンド（建設のコマンドを1件、通常の command queue を通す）、tick（advance_minutes(1)）、restore_snapshot、load_from_path、状態を変えないコマンド。
   - 各場面の直後の create_snapshot() と latest_snapshot を、完全な typed tree（r02 の A）で記録する。版の間の比較は compare_versions.py で行う。
   - 件数: 直接の restore は SAVE_* が0件、load の成功は SAVE_LOADED がちょうど1件（X(P, ev_load)）。
2. E5 所有権（専用の区間）
   - 対象: create_snapshot() の戻り値、latest_snapshot、snapshot_published と state_updated の引数。
   - 外から内: 入れ子の dict と配列を変更した後に、内部の状態、次の create_snapshot()、latest_digest が変わっていないかを、完全な typed tree で確かめる。
   - 内から外: 次の tick の後に、既に返した値が変わっていないかを確かめる。
   - 予想される aliasing（latest_snapshot と signal の引数が同じ object であること）による live の変化は、「既存の不具合」として、変わった path と値を全て記録する。その後の検査は、新しい baseline から始める。harness による無断の変更とは区別する。
3. check_eq.py と試験: E4/E5 の記録の検査と、正常と拒否の試験を加える。
4. 終わり: PROGRESS.md を更新し、正式な JSON の status を blocked にし、findings を「段階 S3b 完了（失敗ではない。次は S4）」とする。

その後
- S4: run_eq.py と verify_eq.py、bundle の試験、全件 PASS の後に正式に提出する。
- その後に、Root の native（7 phase）→ 両方の validation → 別の新しい Opus による全6条件のレビュー → 通常の統合と final、の順序で進める。
