docs/COMBAT_EVENT_REPLAY.md

Saved event-local combat replay

tools/scenario/combat_event_replay.py checks saved host native/policy observations against the causal ledger, exact HP packet, friend receive and friend HP store. It never opens a process or changes gameplay. Run:

python -B tools/scenario/combat_event_replay.py --host inject-0.log --client inject-1.log --output event-replay.json
python -B tests/test_combat_event_replay.py
python -B tests/test_combat_event_replay_a03.py

Exit 0 means RECORDED_LOCAL_CHAINS_LINKED, not gameplay acceptance. Exit 1 means an unresolved recorded host hit or queued claim; exit 2 means invocation, file or native-record parsing failure. Every result keeps acceptance=false, exactlyOneSourceProven=false and coverageQualified=false.

The tool independently checks the currently observed host Apply 410EFF → direct Stat 3D37D2 family. It binds copied policy context, source/victim roots, source class and actual decision, input identities, parent/child relationship and HP arithmetic. Host companions require sourceClass4/reason12; canonical host players require sourceClass1/reason11. Other path families explicitly refuse; a causal qualified=1 or generic witness bit alone cannot substitute for native replay. Each raw enemy-damage event also receives a disposition, including a native event with no ledger Hit. This does not authenticate an actor incarnation or reconstruct every possible native HP writer.

Each linked event retains the Apply sequence, source actor/object and attack, host Hit and publication serials, HPseq, exact payload SHA256, friend receive/store serials and HP delta. Local scope, roots and ordering must agree. Cross-peer edges compare exact packets and shared recorded session fields; they never compare clocks or assume equal native addresses across peers.

Every HP publication has an explicit disposition: noop_received, noop_unmatched, changed_received or changed_unmatched, with receive count and source associations. Unmatched entries have transportDisposition=NOT_ESTABLISHED: the logs do not distinguish a packet drop, coalescing or observation omission. Multiple receives are visible in the count and refuse an event-local chain. No-op publications remain in the report; they are not silently discarded to make a graph pass. The unchanged whole-run auditor's status and issues accompany the local result.

The fixture is the exact retained practice-activation-first-A-20261010-01 host/client inject logs from product 2725ff71, losslessly gzip-compressed with their uncompressed hashes in tests/fixtures/combat_event_replay/manifest.json. It records Goofy/P_EX030/object93 attack1161: Apply1 → host151 (20→18) → publication152/HPseq35 → friend receive298 → friend store303. HPseq8/17 each have five unmatched no-op rows. The original audit remains INCONCLUSIVE.

Sealed drain/end-cutoff, authenticated original runtime-session coverage and a complete native-writer coverage certificate are acceptance-phase work. This tool leaves those limits intact and cannot retroactively qualify VUH-1503 AC3. A linked recorded event and a fully covered run are separate conclusions.

Stage A: whole graph and actual claim paths

The replay entry point includes the unchanged combat_causal_audit.audit_texts() result as wholeGraph, not a filtered copy. wholeRunStatus and coverageIssues preserve that original result. Neither the schema-1 parser, lifecycle decoder nor the original CLI changes. receiptDispositions retains every successfully parsed receipt, its fields (including raw payload hex), original line, reason and associated issues. Malformed causal rows remain issues in wholeGraph; malformed or unterminated native/policy rows refuse parsing. rawNativePolicy retains every parsed event/context/actor/input/child row from both views. No incoming, no-op or rejected row is deleted to improve a verdict.

For each recorded host application, events joins its source to exactly one publication, exact packet/HPseq, accepted consumer header, receive and friend store. The entire packet batch must have exactly its decoded targets, with matching scope, sequence, requested HP and native readback. Changing an unchanged sibling target therefore refuses the chain too. One store with multiple source associations refuses the literal one-source chain. Current target roots must still match the original admission/lifecycle history; coherent replacement of a current snapshot does not authenticate a reused address.

Client claims use the real observed Apply 410EFF -> direct Stat 3D37D2 path: a canonical client Sora, original context and target, copied client policy sourceClass1/reason13/action2, successful revalidation, zeroing and exactly one queued claim. Native Stat must change enemy HP by zero. The copied policy snapshot precedes the sync-drop flag; native input has syncDrop=1 and damage becomes0 after zeroing. Those distinct observations must match in every other input identity. Exact claim packet bytes, requester slot1, connection/sequence/epoch/netId/object/attack/damage bind the claim to exactly one host HostApply receipt. That receipt must record attempted/returned/readback/qualified authority and the precise HP decrement, followed by the exact publication/receive/store chain. This reconstructs the recorded claim path; it does not invent an independent native-writer certificate around the host claim call. claims retains unresolved queued claims even when no host application exists.

Typed incoming dispositions independently join the client ledger event/frame/scope/HP to the existing native Take/Stat replay and the copied enemy->canonical-Sora policy/context/input/actor snapshots. A netId0 row alone never qualifies. Actual incoming damage requires the checked Take/Stat ancestry and supported policy reason10. The A-03 already-applied no-op is a separate checked path: unchanged input/actor/context, applied flag already set, no Take/Stat children, unsupported policy reason4, and unchanged HP. It is recorded_incoming_noop_already_applied, not a damage application. Unknown paths refuse as unresolved_incoming; they remain in the original whole graph. clientNativeEvents also lists unlinked native events explicitly.

Consumer rejection/admission dispositions preserve the exact recorded kind/reason/bytes and validation issues; an enqueue or rejection is never promoted into an applied source. The typed label describes what was recorded, and receiptIssues must be read with it. Unmatched no-op publications remain NOT_ESTABLISHED, not inferred delivery or loss. The local RECORDED_LOCAL_CHAINS_LINKED status refers only to recorded host-hit/claim chains and cannot certify incoming coverage, all possible writers, session identity or shutdown.

The second lossless fixture is the exact acceptance-runner-rehearsal-A-03 pair, with original byte counts and SHA256 in tests/fixtures/combat_event_replay/a03/manifest.json. It links both queued claims (host117/128 -> HPseq28/30 -> friend245/269), both canonical host hits (events3/7), and six published companion events1/5/9/11/13/15. Companion event17 still refuses because its publication is absent. Incoming event5 is checked damage24->16; event8 is the checked already-applied no-op16->16. Five initial no-op publications remain unmatched. Thus the local result is UNRESOLVED, original whole graph FAIL, and all acceptance/coverage flags remain false. No original run result is changed.

Stage B is deliberately absent: sealed producer drain/end cutoff, authenticated original session, target-incarnation and complete writer coverage need a separate product-observation design and reviewed follow-up packet. This offline tool cannot PASS AC3, including when all recorded chains link or a schema-1 flag is forged.

Incoming replay also requires one ledger serial across the entire client stream, one consuming ledger Hit per native event, no competing queued claim, and full SCOPE equality with the original five-member admission (including delivery, salt, both connections and both deliveries). Relevant parser/admission/graph contradictions refuse before a linked incoming label is emitted. The only disclosed enemy-only graph limitations allowed for a checked net0 incoming row are target_replacement_or_unmapped, invalid_target, unqualified_receipt, unqualified_reason, invalid_target_net_id, unadmitted_client_native_delta and unresolved_or_duplicate_client_hit; the independent native/policy domain proof still must pass. These issues remain unchanged in wholeGraph. Duplicate serial/event, invalid clocks, loss, foreign scope or broken original admission never fall under that allowance.

Each parsed receipt has a receiptIdentity composed of peer, original line, serial, kind, event, QPC and complete scope. Typed incoming dispositions use that identity rather than serial lookup, and attached receipt issues use the exact original line. Even colliding/duplicated invalid rows remain separate, with explicit refusal instead of inheriting another row's damage/no-op label. Permanent A-03 controls include review3's duplicated450, salt999 and450->528 collision reproductions, distinct-serial event reuse, every scope field, parse contradictions and broken admission. The baseline event5 damage and event8 no-op still qualify only as local recorded dispositions, with all coverage flags false.