# P28-02「意味を保つ軽量化」仕様(候補 r02。未登録・未実行) - 状態: 候補です。Root が原本の条件と照合して main に commit し、その実 SHA を base にしてから、通常の登録をします。 - 前提: P28-01 は正式に完了しています(Q `ae9ae47646434dd57fc21f3e8566d0d35b242ab1`、`execution/p28-01/P28_01_FORMAL_COMPLETION_AE9AE47.json` `f778ac8d…`)。その測定(full9 `db9d613`、r02、RESTART)と、製品 source 1407 を引き継ぎます。 ## 1. 原本の条件(変更しない) - 成果単位 P28-02(`work_packages.json` の P28.remaining_deliverables) - work: 失敗時拒否を含む同等性を先に固定し、snapshot変換・検証・コピー・経路接続の実測上位から改善する。 - acceptance: 同条件の反復比較と全状態・全イベント一致を確認し、キャッシュの古さ・所有権・不正入力を検証する。 - P28 の横断条件6件(原文): 原本条件とUI操作が一致 / Runtimeの実利用・会計・行動へ接続 / 途中保存・再起動で再現 / 異常/境界/中断を検証 / 関連回帰PASS / 要件別ログとソースハッシュを保存。 - パッケージの依存: P28 は P27 に依存し、P27 は in_progress です。この依存は、P28 パッケージを最終的に完了と判定するときの前提です。P28-02 の診断と作業を止める理由にはしません(既存の、人間による順序変更の許可を保持します)。P27 が完了したとは主張しません。 - 分母は 30/88 で、部分点はありません。現在の正式完了は 23/88、パッケージは 4/30 です。予算の false 6件(R0 の2件、P1 の2件、P2 の2件)は、そのまま保持します。 ## 2. 事実と不足(数値を結びつけない) - 実測(P28-01 から引き継ぐ。それぞれの区間の値で、互いに結びつけない): - full9 の R: kernel の `process_real_delta` の1回の呼出し(frame)が、P1 x1 で平均 479.210 ms、P2 x3 で P95 5062.697 ms。 - r02(headless・Ops OFF・x1。別の区間): 外側の1分 advance が平均 826.6 ms、外側の snapshot が 77.6 ms(count 5)、保存は約 300 ms、読込は約 3.2 s。 - source から確定していること: 毎 tick の PUBLISH_SNAPSHOT で `create_snapshot()` が呼ばれます(`autoload/simulation_runtime.gd:12616-12621`)。ただし、r02 の 77.6 ms を、P1 の 479 ms の中の同じ呼出しの固定の費用として扱うことはしません(区間も条件も違うため)。 - 不足: 現在の source での、tick の中の phase ごとの wall 区間、snapshot・検証・コピーの内訳、経路の費用。推測で埋めません。 ## 3. 段階A: `P28-02-DIAG`(診断。製品は変更しない。原本の加算は0) ### 3.1 live の状態の扱い - live の Runtime の状態を変えてよいのは、次の2つだけです。それ以外の、状態を変える API の呼出し、製品の hook や関数の差し替えは禁止です。 - fixture の最初の導入(`restore_snapshot` で入力 R を読み込む)。 - 決められた `GameClock.advance_minutes(1)` による進行。 - 公開されている読取だけの API(`create_snapshot()` など)は、tick の外で呼んでかまいません。その後、live の全状態の `var_to_bytes` が、呼ぶ前と完全一致することを確かめます。一致しなければ失敗です。 ### 3.2 tick の中の区間(受動的な観測。`scripts/core/simulation_kernel.gd:122-157`) - `tick_started(m)` で tick の ID と、進行の前の分 m を記録します。 - APPLY_COMMANDS の `tick_phase_started` は、進行の前の分 m で発行されます。TIME_BOUNDARIES 以降の phase と `tick_completed` は、進行の後の分 m+1 で発行されます。phase を分で区切らず、tick の ID で結びつけます。 - 区間は、隣り合う信号の間の **inclusive な wall 区間**として記録します。各 handler の純粋な排他時間だとは主張しません。区間ごとの内容は次のとおりです。 - I0: `tick_started` → APPLY_COMMANDS の開始 - I1: APPLY_COMMANDS → TIME_BOUNDARIES(clock の進行、`_sync_calendar_fields`、`_handle_time_boundaries` を含む) - I2〜I10: TIME_BOUNDARIES → MANA → … → PUBLISH_SNAPSHOT の各開始の間 - I11: PUBLISH_SNAPSHOT → `tick_completed`(`create_snapshot`、同期の signal である `snapshot_published` / `state_updated` の受け手、`snapshot_version` の更新を含む) - observer 自身の費用: 各 callback の中で、自分の時間を別に計測して記録します。区間からは差し引きません(二重に数えないために、区間は「observer を含む」と明記します)。 - 経路: `PathService.path_query_measured(duration_us)`(`scripts/world/path_service.gd:4`、`:985`)に受動的に接続し、query の件数と時間を、その時点の区間に結びつけます。これは query の費用だけで、connector の更新や再構築の費用は含みません。query の時間を、connector 全体の費用へ広げて解釈しません。 ### 3.3 tick の外の内訳(独立した inclusive の計測。合算して100%にしない) - snapshot の変換: live で `create_snapshot()` 全体を N=5 回、各 service の `create_snapshot()` をそれぞれ N=5 回、別々に計測します。各値は独立した inclusive の計測で、排他的な内訳として足し合わせることはしません。N=5 は r02 の試行数に揃えたもので、R で1回約 80 ms のため負荷が小さいことが根拠です(製品の値ではありません)。 - 検証・コピー・保存・読込: live の状態は使いません。使い捨ての scratch で、公開されている restore・save・load・validator だけを使います。 - scratch に渡すのは、live の snapshot の deep copy です。保存先は private user:// の scratch の path です。 - 各操作を N=5 回計測します。 - 終わった後に、live の全状態が一致すること、scratch から live への参照の漏れや副作用が無いことを確かめます。 - 公開の API だけで scratch を作れない場合(autoload に触れる必要がある場合)は、その項目を理由つきの未測定にして、判断に戻します。 - 粒度: 製品の内部で分けられない変換・検証・コピーは、測れる粒度(たとえば「restore 全体」)のまま記録します。分けたことにはしません。 - 値: 正当に測れた 0 は 0 として記録します。測れなかったものは null と理由にします。欠損を 0 で補うことは拒否します。 ### 3.4 条件と順序 - 入力・engine・machine は P28-01 の R と同じです(入力 `eb4f7700…`、engine `c7cccbf8…`、Mac16,10)。headless で実行します。 - 区間: warm 30分、測定30分。別の PID で3回行います。3回の間で、区間の順位が一致したかを記録します。 - 実行の順序(P08/P18 の通常の方式を再利用する): 1. worker が S を提出します(局所の unit 試験の全件 PASS つき)。 2. Root が、凍結した S を隔離した stage で、import・parse・DIAG の3 PID を native で実行します。 3. Root が raw を結合し、bundle を作ります。 4. 登録した validation(unit と、bundle に対する native-verify の両方)を実行します。 5. 別の新しい Opus が、全4条件と全 raw をレビューします。 6. prepare → 候補の source と測定した S の一致の資格確認 → Root の委任による承認 → integration → final。 - native は、測定した実際の S のものとして記録します。最終候補 Q で測ったとは表記しません。資格確認で、Q と S の source・設定・engine・入力が一致することを確かめます。 ## 4. 段階B: `P28-02-OPT`(DIAG の後に、根拠をもとに登録する) - 対象: DIAG で実測の上位になったもののうち、原 work の4つの分類(snapshot の変換・検証・コピー・経路接続)に入るものだけです。選ぶのは管制で、DIAG の表を根拠に記録します。上位が4分類の外にあった場合は、原 work の範囲の外なので、判断に戻します。 - 同等性を先に固定します(変更の前に、試験として凍結する): - **E1 全状態**: - まず `var_to_bytes` の完全一致を確かめます。 - 一致しない場合に限り、RESTART の修正8で確定した完全な typed の正規表現と比べます。これは、dict の key を文字で並べ(衝突は拒否)、全ての key と値の `typeof` と配列の順序と値を保つものです。それで一致すれば「dict の列挙の順序だけの差」として、その path を記録します。 - それ以外の差は不一致です。SimulationDigest だけでの判定は不可です。 - **E2 全イベント**: - `DomainEventStream.subscribe(&"*", &"p28_02_equivalence_observer", …)`(`scripts/core/domain_event_stream.gd:5`、`:17`、配送は `:76-85`)で受動的に記録し、全件を上限なく、順序・型・payload・ID・counter とともに保持します。 - subscriber の登録が snapshot を変えないこと(`create_snapshot` の範囲の外)を確かめ、終わったら `unsubscribe` します。 - recent_events(直近256件)の一致だけを、全イベントの一致として扱うことはしません。 - **E3 失敗時の拒否**: 不正な入力(改ざん、型の違い、欠けた key、範囲外、重複した ID)が、変更の前と同じ code で、原子的に拒否されること。 - **E4 キャッシュの古さ**: 無効化の全ての経路(コマンド、tick、restore、load)で、古い値を返さないこと。 - **E5 所有権**: 公開した値を外から変更しても内部に影響せず、その逆も無いこと。 - 比較は2種類に分けて固定します: - (a) phase の比較: DIAG と同じ区間(headless、`advance_minutes`)で、変更の前と後を別の PID で各3回。`advance_minutes` では `performance_budget.record_simulation_frame` が呼ばれず、`_samples` が空になるため、この比較では kernel の予算の値を使いません。 - (b) frame の予算の比較: 製品の経路 `process_real_delta` を通る、受入済みの collector の windowed の P1(x1)と P2(x3)を、変更の前(現在の Q)と後で、別の PID で各3回。kernel 定義の標本で比べます。 - RESTART の native の pair を1回行い、保存と再開の再現が保たれることを確かめます。製品の source が変わるので、最終候補で正式の全 roster を1回実行します。 - 予算は P28-02 の受入の条件ではありません。実測をそのまま記録し、false は合格に補いません。同等性 E1〜E5 が1つでも崩れた変更は採用しません。 - 独立レビュー: 原 work と原 acceptance、横断条件6件、補助の U1/U2 を、whole-unit として判定します。 ## 5. 共通の制約 - 予算値、原本の分母、横断条件の原文は変更しません。Runner 本体、権限、認証、課金も変更しません。修正回数の上限は null で、API の課金はしません。容量待ちは再導入しません。