Skip to content

[M1-DET-02] Replay recorder (versioned log format, ReplayRecorder, engine + CLI wiring) - #36

Merged
offdev merged 2 commits into
masterfrom
m1-det-02-replay-recorder
Sep 16, 2026
Merged

offdev merged 2 commits into
masterfrom
m1-det-02-replay-recorder

Conversation

@offdev

@offdev offdev commented Sep 16, 2026

Copy link
Copy Markdown
Owner

Replay recording for M1 (replay execution — world.state_hash + the laige-replay runner — is M1-DET-03). Scope: roadmap/M1-heartbeat.md M1-DET-02, nothing else (FR-1.4, FR-11.3, PRD Appendix A — replay = input log + seed — ADR 0002, ARCH-007, SCALE-005).

What lands

  • The versioned replay log format (v1) — src/laige-sim/include/laige/sim/replay.h + replay.cpp: 40-byte header (magic LGRP, formatVersion, reserved, then the ADR 0002 replay identity: seed, tick rate, componentSchemaHash, math backend id, configHash), per-tick frame records (tick u64 strictly from 1; byteLength u32 ≤ 1 MiB; the payload an opaque byte blob — M1 frames are zero-length, the input data shape lands with M3-INPUT-03), and a 16-byte trailer (frameCount + the canonical byte-stream FNV-1a 64 fileHash over all prior bytes). All integers little-endian (SCALE-005). Every structural violation is a MalformedInput — never a crash, never a silent skip.
  • ReplayRecorder — opt-in, move-only: writes path + ".tmp" (same filesystem — the final rename is atomic) and publishes path only on a successful finish(); size-bounded (cap counts header + frames + trailer; 0 = 128 MiB default; cap breach → sticky BudgetExhausted); no partial file at the final path on interruption/failure.
  • The readers — parseReplay (in-memory) and loadReplay (bounded read; oversized file → MalformedInput, the ADR 0003 bound precedent).
  • The identity hashes — componentSchemaHash(const World&) (registration-order-sensitive; pure integers, no addresses — ARCH-010) and configHash(const EngineConfig&), word-stream FNV-1a 64 (house convention), assembled by makeReplayIdentity.
  • Engine wiring — Engine::startReplayRecording(path, maxBytes): once, after all registration, before run_headless; debug builds only (NDEBUG → InvalidArgument + replay/record_disabled warn); one zero-length frame per completed tick in the loop's onTick hook; a mid-run recording failure stops the run (run_headless returns the Status, no partial log); successful runs finalize (record_finished); the ordered shutdown abandons unfinished recordings (record_aborted); replayRecordingActive()/replayBytesWritten() accessors.
  • laige-run --replay <path> — the M1-HEAD-01 stub is now real recording: atomically published on a clean run, debug-only, exit 2 on a start failure / exit 1 on a mid-run failure (no partial log either way).
  • Fuzz — laige-fuzz replay_parse target (the parser's malformed-input surface; TEST-005/NFR-8.7) + fuzz_replay_parse CTest entry (1000 deterministic runs; the corpus includes a valid v1 log as a mutate/truncate base).
  • Tests — tests/laige-sim/replay_record_tests.cpp (22 tests, CTest entry replay_record, TSan property list): the round trip (record → parse → identical bytes, incl. PRNG payloads and the exact 1 MiB frame boundary), the full malformed table (every truncation cut of a valid log, bad magic/version, length overrun, tick sequence, trailer count, fileHash, trailing garbage), the recorder contract (atomic publish, no partial file, the size limit at the exact boundary — cap 56 and the at-finish cap 67 — sticky failure, move semantics), the identity hashes (order stability, per-field sensitivity), and the engine integration (8-tick run → 8 empty frames + matching identity; the failure stop with record_failed/record_aborted; double-start and stopped-engine failures).
  • Docs (same change, DOC-007) — new docs/api/replay.md (format spec, API, engine + CLI integration, Performance section); docs/api/engine.md (new replay section, de-stubbed CLI bullet), docs/concepts/determinism.md (recording landed; execution = M1-DET-03), docs/README.md, docs/compatibility/README.md (the replay log is the first persistent engine format — formats table), docs/testing.md, tools/README.md, src/laige-sim/README.md.
  • API manifest — laige-api.json regenerated (630 symbols, 20 headers); api-real-tree green.
  • Roadmap — M1-DET-02 box checked; progress board M1 14→15 of 25 (total 34→35); change-log line in the second commit of this branch (the hash reference is the first commit, per the two-commit convention).

Verification (local)

check result
ctest -R replay_record (Debug) 22/22 green
full ctest on build (Debug GCC) 57/57
full ctest on build-asan (ASan+UBSan) 57/57 (leak-free — the interrupted-recorder cleanup)
full ctest on build-tsan (halt_on_error=1) 57/57 (incl. replay_record + fuzz_replay_parse)
laige-fuzz replay_parse --runs=1000 / json_parse --runs=1000 clean
tools/laige-include-lint OK (36 source files, 1/10 vendored)
laige-api + api-real-tree green (630 symbols)
zero new warnings under NFR-8.10 yes

Untested paths (CI-only / not locally exercisable)

  • the MSVC _fsopen/MoveFileExA(MOVEFILE_REPLACE_EXISTING) and AppleClang platform branches of the atomic write (the P0 OS jobs cover them);
  • the release-build record_disabled branch (NDEBUG — a compile-time constant, not reachable from a Debug tree);
  • a real > 128 MiB run (the size-limit branches are boundary-tested at caps 56/67 instead).

Compatibility

Additive: the --replay flag's documented contract (the M1-HEAD-01 stub was explicitly reserved for this step) is now honored; no existing symbol or behavior changed.

…gine + CLI wiring)

M1-DET-02 (roadmap/M1-heartbeat.md, FR-1.4/FR-11.3, PRD Appendix A, ADR 0002,
ARCH-007, SCALE-005): the replay RECORDING half (replay execution —
world.state_hash + the laige-replay runner — is M1-DET-03).

- src/laige-sim/include/laige/sim/replay.h + replay.cpp: the versioned
  replay log format (v1, magic LGRP, little-endian; header = format
  version + the ADR 0002 replay identity — seed, tick rate, component
  schema hash, math backend id, config hash — + per-tick frames as
  opaque byte blobs + the trailer frameCount + FNV-1a fileHash), the
  ReplayRecorder (opt-in, atomic temp+rename, size-bounded, strict
  tick sequence, sticky failure), the parseReplay/loadReplay readers
  (every structural violation a MalformedInput — never a crash), and
  the identity hashes (word-stream FNV-1a 64, big-endian per u64 — the
  house convention).
- src/laige-sim/engine.{h,cpp}: Engine::startReplayRecording (once,
  after all registration, before the run; debug builds only — NDEBUG
  rejects with a record_disabled warn), one zero-length frame per
  completed tick in the loop's onTick hook (M1: no input system yet),
  a mid-run recording failure STOPS the run (run_headless returns the
  Status, no partial log), finalization in the successful run
  (record_finished), abandonment in the ordered shutdown
  (record_aborted); replayRecordingActive/replayBytesWritten accessors.
- tools/run/laige-run.cpp: --replay <path> wired (it was the M1-HEAD-01
  stub): the run is recorded and the log atomically published on a
  clean run; debug builds only; failure exits 2 (start) / 1 (mid-run).
- tools/fuzz: the replay_parse target (the parser's malformed-input
  surface, TEST-005/NFR-8.7) + the fuzz_replay_parse CTest entry
  (1000 deterministic runs; the corpus includes a valid v1 log);
  laige-fuzz now links laige-sim.
- tests/laige-sim/replay_record_tests.cpp (22 tests, CTest entry
  replay_record, TSan property list): the round trip (record -> parse
  -> identical bytes, incl. PRNG payloads and the 1 MiB frame
  boundary), the full malformed table (every truncation cut, bad
  magic/version, length overrun, tick sequence, trailer count,
  fileHash, trailing garbage), the recorder contract (atomic publish,
  no partial file, the size limit at the exact boundary, sticky
  failure, move semantics), the identity hashes (registration-order
  stability, per-field sensitivity), and the engine integration
  (8-tick run -> 8 empty frames + matching identity; the failure stop
  with record_failed/record_aborted; double-start and stopped-engine
  failures).
- CMake: replay.cpp into laige-sim; replay_record entry; the fuzz
  wiring. laige-api.json regenerated (630 symbols, 20 headers).
- Docs (DOC-007, same change): new docs/api/replay.md (format spec,
  API, engine + CLI integration, Performance section); engine.md,
  concepts/determinism.md, README.md, compatibility/README.md
  (the replay log is the first persistent engine format),
  testing.md, tools/README.md, src/laige-sim/README.md updated.
- Roadmap: M1-DET-02 box checked; progress board M1 15/25, total 35.

Verify (local): ctest -R replay_record green (22/22); full ctest
57/57 on build (Debug GCC), build-asan (ASan+UBSan, leak-free), and
build-tsan (TSan halt_on_error, incl. replay_record + fuzz_replay_parse);
laige-fuzz replay_parse/json_parse 1000 runs each clean;
tools/laige-include-lint OK; laige-api regenerated + api-real-tree green.
@offdev
offdev merged commit 544a8a4 into master Sep 16, 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