# P03 保存プリフライト修正の独立レビュー 2026-10-02。本体の変更・Godotの追加実行は行わず、save_service / simulation_snapshot / Runtime previewの差分、test_save_preflight、焦点7本485チェックのログを監査した。 新しい`inspect_snapshot`と寸法previewの接続、manifest/clock envelopeの厳格型検証、既知サイズの読取前制限について、差分が原因の重大不具合は見つからなかった。ただし**既存の復旧経路にP01/P03受入を妨げる原状保持の不足2件**がある。485チェックの合格だけで未知版や全APIの読取専用保証を受け入れられない。 | 判定 | 対象 | 結果 | |---|---|---| | 確認済み | inspect_snapshot / 寸法preview | 通常loadのtransaction reconcileを通らず正式packageを読む。formalなしでもtmp/bakへ昇格しない。previewは隔離Runtimeで検証し、本Runtimeと全ディスクSHAを保つ試験がある。計測カウンタの更新は発生するため全メモリが不変という意味ではない。 | | 確認済み | manifest型 | versionはunsigned範囲の有限整数数値だけを許可。文字列・bool・分数・nullを拒否。JSON由来の厳密な整数floatは許可する。checksumは64桁hex文字列、重複sizeは存在する場合に正の整数かつheaderと一致が必須。 | | 確認済み | clock envelope型 | schema/minute/speed/versionのcoerceを避け、present pausedはbool必須。省略paused/versionのlegacy defaultとx4→x3互換を維持。generic envelopeの整数float互換であり、現Runtimeのbinary schemaは従来通りstrict TYPE_INTを要求する。 | | 確認済み | サイズ | worldの圧縮payload/展開予定値は各256 MiB以下・正数、manifestは4 MiB以下としてbulk read/decompress前に検査する。writerも出力blob/manifestを拒否して正式保存を保持する。 | | 不足 | 未知版+transaction artifact | 下記SFR-01。正式保存の版を識別する前にreconcileして未知版を古いbakへ置換し得る。 | | 不足 | 明示backup選択のread_only | 下記SFR-02。候補選択前のformal reconcileでtmp/marker等が変化し得る。 | ## SFR-01: 未知の正式保存を旧backupで置換する [P1] `save_service.gd:932`の`_reconcile_interrupted_formal_save`は、`target_status.current_loadable == false`のとき、未知形式と壊れた現形式を区別していない。formalが存在し有効transaction markerがあると、現版tmp/bakを選び、formalを削除して置換する(同関数のinvalid formal replacement分岐)。`load_snapshot`、`migrate_save`、`inspect_recovery_candidates`、`load_with_recovery`、明示`load_recovery_candidate`がこの経路を先に通る。 再現条件は、正式保存のmanifestとTTWB headerをformat2へ変え、圧縮payload/checksumを保ったまま、現format1のbak/tmpと有効`.save_transaction`を残すこと。通常loadが正式保存を古い状態へ置換して成功する静的経路を確認した。checksumを破損したfuture版、manifestだけfuture、headerだけfutureでも、識別できる未知版を破損現版として消してよい根拠にはならない。 現128チェックの未知版試験は`inspect_snapshot`での拒否と、transaction markerなしでの未登録migrate拒否を確認する。上の複合条件を覆っていない。レビューではGodotを実行していないので、実再現の結果は未取得と明示する。 ## SFR-02: 明示候補読取のread_onlyと実処理が不一致 [P2] `save_service.gd:313〜336`は明示候補読取をread-onlyと説明し、結果にも`read_only=true`を付けるが、候補pathの選択前に`_reconcile_interrupted_formal_save`を呼ぶ。 formalが正常、有効markerとtmpがある場合、backupを選ぶだけでもtmp/markerを削除する。formalが欠損しtmpがある場合、backupを読むだけでもtmpが正式名へ昇格する。明示候補読取は指定packageだけを検証し、通常load/通常recoveryが復旧を所有する契約へ分ける必要がある。 ## 候補と受入の範囲 `artifacts/original-completion-2026-10-02/p03-recovery-readonly-candidate/combined.diff`、完全候補、原因別diff、fixture driver、receiptを作成した。本体変更0、Godot実行0で、候補は未採用・未検証である。rootが既存全回帰の終了後に別stageで、元本体の実再現→候補修正後試験→保存/復旧/移行の関連回帰を行う。 候補はbounded manifestと固定TTWB prefixのいずれでも未知版を先に識別し、checksum成功を前提にしない。通常load/recoveryの未知版は全artifactを変えず拒否する。migrateは明示登録された過去版→現版の全隣接stepだけを認め、その場合もsourceの検証前に既存artifactを整理しない。既知現版の壊れたtransaction復旧は維持する。明示候補読取からreconcileを除く。 `inspect_recovery_candidates`は既存の説明通り通常復旧を含む一覧APIであり、全体をread-onlyとは呼ばない。formalが未知と識別できた場合の不変拒否を先行する。 485ログは7本すべてexit0・実チェック数とcompletion markerが一致し、4修正/試験ファイルSHAは現在と一致した。source-startの318パスは317一致で、差分は今回焦点外の`tests/performance/test_digest_performance.gd`だけだった。初回失敗ログも保存されており、後続r02と混同しない。全157回帰の終了結果はこのレビューに含めていない。 サイズ上限はserialized world blobの制限であり、総ピークメモリ256 MiB以下の証明ではない。256 MiB一致の試験はmanifest/header不一致でdecompress前に拒否し、巨大な正常full Runtimeを実際に展開した受入ではない。20,000セルはworld-onlyのサイズ測定で、full Runtime受入ではない。これらはP28の性能/規模受入として引き続き未検証にする。 ## 既存移行・復旧契約との照合 `test_save_operations.gd`の349〜450行と`test_runtime_save_lifecycle.gd`の170〜200/230〜380行を候補と照合した。以下は静的整合の確認で、候補の再実行合格ではない。 | 既存契約 | 候補との整合 | |---|---| | registryの1→2→3隣接step・入力コピー・欠損step監査 | registry自体を変更しないため維持。 | | current formatだが不正snapshotの通常load/no-op migrate拒否 | guardは現formatを通し、既存schema検証へ到達するため維持。 | | 明示0→1 package移行、step ID、manifest注記、完全pre_migration.bak | 過去版→現版の全step確認後に既存移行本体へ入る。callbackとbackup/promotion処理は変更しない。 | | 登録済みstepが途中失敗したときformalのworld/manifest不変、backup未作成 | sourceのdecode/registry成功前にreconcileを実行しないため、stale普通saveのtmp/bak/markerも不変になる。追加driverでこの複合条件を要求する。 | | 移行後のtorn pre_migration.bakをdurable .oldから修復 | formalが現formatになった状態では従来のinspect/reconcile枝を維持。 | | `.migration.original`のhold、完成candidateのpromotion、candidateなしrollback | formal欠損では未知版guardに識別値がなく、従来のmigration reconcileへ進む。現format持ちの既存fixtureを維持。 | | 壊れたcurrent formalからbackup/autosaveへ通常復旧 | current版識別は通すため従来の復旧を維持。識別可能な未知formalだけをfallbackの対象から外す。 | | 明示backup/autosave選択がpackageを昇格しない | 候補選択にreconcileが不要。formalなしのbak選択は成功でき、tmp/markerは通常loadに残す。 | 登録0→1の成功時は、formalと専用migration artifactだけを既存手順で更新する。stale普通saveのtmp/bak/markerは前処理で片付けず保持する。次の通常loadは新しい現formalを優先し、marker/tmpの既存cleanupを実行できる。これを未知formalの拒否や明示backupの純読取と混同しない。 追加driverは8入口×5未知形、formal有無のbackup選択、既知現版破損復旧、登録legacy成功の普通artifact保持、登録legacy失敗の全SHA保持を含む。rootによる別stageでの実再現・候補検証待ちである。