Skip to content

[M1-DET-05] Replay diff: componentStateHash, World::stateDiff, diffReplays, laige-replay --diff - #44

Merged
offdev merged 1 commit into
masterfrom
m1-det-05-replay-diff
Sep 20, 2026
Merged

offdev merged 1 commit into
masterfrom
m1-det-05-replay-diff

Conversation

@offdev

@offdev offdev commented Sep 20, 2026

Copy link
Copy Markdown
Owner

Summary

M1-DET-05 (roadmap/M1-heartbeat.md): laige-replay --diff <logA> <logB> — replay both logs, align by tick, report the first divergent tick + the diffed state components (bounded report, not a full dump). FR-11.3; later consumed by the editor replay viewer (M5-ED-15).

New public surfaces

  • World::componentStateHash(tick) (src/laige-sim/include/laige/sim/entity.h, src/laige-sim/state_hash.cpp) — the state hash's canonical steps 1-4 (tick, live handles, archetype assignment, component bytes) with the PRNG substream state EXCLUDED. It is the diff's tick-alignment key: a different-seed replay diverges in substream state from tick 0 while its components may match for hundreds of ticks, so aligning on the full stateHash would always report tick 0 and hide the first real component divergence. stateHash now shares steps 1-4 with this via one extracted helper (single canonical implementation).
  • World::stateDiff(other, maxItems, fn) (entity.h) — bounded per-(entity, component) comparison in canonical order (slots ascending; entity-presence item before component items; components ascending in the union of the two worlds' sets). Returns the exact total difference count; the callback runs for the first maxItems items. Item bytesA/bytesB are non-owning views into the worlds' component columns (PERF-005; valid until the next mutation of the involved entities).
  • laige::diffReplays(logA, logB, worldA, worldB, configA, configB, maxEntries = 16) (src/laige-sim/replay_diff.{h,cpp}) — identity check per log (both checked, both reported; replay/diff_identity_mismatch warn), determinism check (replay/diff_determinism_disabled warn), lock-step beginFrame()/runSystems() walk aligned on the component-state hash, honest fullStateDivergent flag from the full stateHash (never silent, CORE-008), bounded state diff at the first divergence, length divergence (firstDivergentTick = alignedTicks + 1). maxEntries: 0 = count only, >64 clamped (kMaxReplayDiffEntries); default 16 (kReplayDiffDefaultEntries).

CLI (tools/replay/laige-replay.cpp)

laige-replay --diff <logA> <logB> --config <cfg> [--entries N] — per-log config derived from the base config with that log's header seed. Stdout is the stable structured report only (machine-greppable; the editor viewer's source):

replay_diff result=<identical|divergent|length_divergence>
frames_a=<n>
frames_b=<n>
first_divergent_tick=<n>
length_divergence=<true|false>
full_state_divergent=<true|false>
differing_items=<n>
reported_items=<n>
item slot=<s> component=<c> presence=<both|a_only|b_only> a=<hex|-> b=<hex|->

Stderr: human summary, the identity-mismatch field report, the PRNG note when full_state_divergent, usage errors. Exit codes: 0 identical; 1 divergent or length-divergent; 2 usage/config/log/identity/engine error. --diff is mutually exclusive with --log/--expect; --entries takes an integer in 0..64.

Tests

  • ctest -R replay_diff — new StateDiff.* + ReplayDiff.* suites in tests/laige-sim/replay_diff_tests.cpp (13 tests): identical worlds report nothing; component-byte differences with canonical slot/component order; entity- and component-presence items; bounded report vs exact count; the component-state hash's PRNG-excluded scope; the roadmap integration scenario — two different-seed replays whose first component divergence lands at tick 37 report tick 37 and the diverged component (machine-greppable replay-diff-37 line); tick-0 initial-state divergence; length divergence; the identical case; identity-mismatch rejection (both logs named, per field); determinism-disabled rejection (recorded under the disabled identity, so the determinism check — not the identity check — fires); draw-path KAT against an independent laige::Prng.
  • CLI (tests/replay/) — the check-script template gains an optional second setup step (SETUP2, backward compatible) plus a <DIFFBUDGET> placeholder; new fixture replay_smoke_budget128.json; four entries: replay_diff_identical (two 16-tick recordings, same fixture: result=identical, exit 0), replay_diff_length (16 vs 10 ticks: result=length_divergence, first_divergent_tick=11, exit 1), replay_diff_identity (budget-128 config against a budget-64 log: config_hash DIFFERS report, exit 2), replay_diff_usage (--diff with one log path, exit 2).

Docs

  • docs/api/replay.md — intro updated; new "The diff mode" section (API, result field table, steps, performance, misuse warnings: fresh identically-registered worlds, scenario logs need the scenario binary, entry byte views die with the worlds) and CLI report grammar + exit codes; Testing and CI bullets for both new suites.
  • docs/api/entity.md — new "The component-state hash and the state diff" section; Roadmap context bullet for M1-DET-05.
  • roadmap/M1-heartbeat.md — M1-DET-05 checkbox flipped to [x].
  • laige-api.json regenerated (cmake --build build --target laige-api); api-check-fresh green.

Verification (all green)

  • build (Debug, g++): ctest -R replay_diff (13 unit tests + 4 CLI entries), ctest -R replay 16/16, full laige-sim_tests, include-lint/API/determinism-lint suites 11/11, hello sample suite.
  • build-asan (Debug, ASan): ctest -R replay_diff, full laige-sim_tests (70 s, no findings), all 20 replay entries.
  • build-clang (Debug, Clang, -Werror): clean compile; ctest -R replay_diff, full laige-sim_tests.
  • build-tsan (Debug, TSan, TSAN_OPTIONS=halt_on_error=1): ctest -R replay_diff, all 20 replay entries.

Notes:

  • The tick-37 scenario uses a per-entity tick-count component (not a shared global) — the diff driver runs two worlds in lock step, and a shared counter would double-advance (one bump per world per tick pair). The fixed seeds (0x11111111 / 0x22222222) make the draw values a deterministic property of the test, printed as the machine-greppable line.
  • No ENGINE-RULE-EXCEPTION added. No dependencies added.
  • Not covered locally: macOS/Windows CI platforms (the check scripts are platform-independent: no {n} regexes, CRLF normalization, gtest temp dir), and scenario-log diffs from the scenario binary (documented constraint, identity check rejects schema-mismatched logs).

…plays, laige-replay --diff

FR-11.3 (diff two replays by frame/state). Two new public surfaces:

- World::componentStateHash(tick): the state hash's canonical steps
  1-4 (tick, live handles, archetype assignment, component bytes)
  with the PRNG substream state EXCLUDED — the diff's tick-alignment
  key (a different-seed replay diverges in substream state from tick
  0 while its components may match for hundreds of ticks).
- World::stateDiff(other, maxItems, fn): the bounded per-(entity,
  component) comparison in canonical order (slots ascending; presence
  item before component items; components ascending in the union of
  the two worlds' sets); exact total count, callback for the first
  maxItems; non-owning byte views into the worlds' columns.
- diffReplays(logA, logB, worldA, worldB, configA, configB,
  maxEntries): identity check per log (both reported), determinism
  check, lock-step beginFrame/runSystems walk aligned on the
  component-state hash, honest fullStateDivergent flag from the full
  state hash, bounded state diff at the first divergent tick, length
  divergence, maxEntries 0/count-only, >64 clamped.

CLI: laige-replay --diff <logA> <logB> --config <cfg> [--entries N]:
stable structured stdout report (replay_diff result=..., frames,
first_divergent_tick, full_state_divergent, differing_items,
reported_items, per-item slot/component/presence/hex lines), human
summary + diagnostics on stderr, exit 0 identical / 1 divergent or
length / 2 usage-identity-IO.

Tests: ctest -R replay_diff (StateDiff.* + ReplayDiff.* suites — the
bounded diff contract, the tick-37 integration scenario: two
different-seed replays first diverging at tick 37 report tick 37 and
the diverged component, the identity/determinism rejections, the
draw-path KAT vs an independent Prng) + four laige-replay CLI entries
(replay_diff_identical/length/identity/usage; template gains SETUP2).
Docs: replay.md (diff section, report grammar, testing), entity.md
(componentStateHash/stateDiff section, roadmap). laige-api.json
regenerated.
@offdev
offdev merged commit 6b63465 into master Sep 20, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant