[M1-DET-02] Replay recorder (versioned log format, ReplayRecorder, engine + CLI wiring) - #36
Merged
Merged
Conversation
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Replay recording for M1 (replay execution —
world.state_hash+ thelaige-replayrunner — 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
src/laige-sim/include/laige/sim/replay.h+replay.cpp: 40-byte header (magicLGRP,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 64fileHashover all prior bytes). All integers little-endian (SCALE-005). Every structural violation is aMalformedInput— never a crash, never a silent skip.ReplayRecorder— opt-in, move-only: writespath + ".tmp"(same filesystem — the finalrenameis atomic) and publishespathonly on a successfulfinish(); size-bounded (cap counts header + frames + trailer;0= 128 MiB default; cap breach → stickyBudgetExhausted); no partial file at the final path on interruption/failure.parseReplay(in-memory) andloadReplay(bounded read; oversized file →MalformedInput, the ADR 0003 bound precedent).componentSchemaHash(const World&)(registration-order-sensitive; pure integers, no addresses — ARCH-010) andconfigHash(const EngineConfig&), word-stream FNV-1a 64 (house convention), assembled bymakeReplayIdentity.Engine::startReplayRecording(path, maxBytes): once, after all registration, beforerun_headless; debug builds only (NDEBUG→InvalidArgument+replay/record_disabledwarn); one zero-length frame per completed tick in the loop'sonTickhook; a mid-run recording failure stops the run (run_headlessreturns theStatus, 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).laige-fuzz replay_parsetarget (the parser's malformed-input surface; TEST-005/NFR-8.7) +fuzz_replay_parseCTest entry (1000 deterministic runs; the corpus includes a valid v1 log as a mutate/truncate base).tests/laige-sim/replay_record_tests.cpp(22 tests, CTest entryreplay_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 withrecord_failed/record_aborted; double-start and stopped-engine failures).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.laige-api.jsonregenerated (630 symbols, 20 headers);api-real-treegreen.Verification (local)
ctest -R replay_record(Debug)ctestonbuild(Debug GCC)ctestonbuild-asan(ASan+UBSan)ctestonbuild-tsan(halt_on_error=1)replay_record+fuzz_replay_parse)laige-fuzz replay_parse --runs=1000/json_parse --runs=1000tools/laige-include-lintlaige-api+api-real-treeUntested paths (CI-only / not locally exercisable)
_fsopen/MoveFileExA(MOVEFILE_REPLACE_EXISTING)and AppleClang platform branches of the atomic write (the P0 OS jobs cover them);record_disabledbranch (NDEBUG— a compile-time constant, not reachable from a Debug tree);Compatibility
Additive: the
--replayflag's documented contract (the M1-HEAD-01 stub was explicitly reserved for this step) is now honored; no existing symbol or behavior changed.