Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 22 additions & 18 deletions .github/workflows/ci-pull.yml
Original file line number Diff line number Diff line change
Expand Up @@ -306,13 +306,19 @@ jobs:
run: ./build/bin/laige-api-scanner --root . --check laige-api.json

detcheck:
# M0-TOOL-02: runs on every PR (no ci:* condition). The determinism
# checker skeleton (FR-11.5): the built-in synthetic scenario
# self-check runs here so a broken checker lands red before the real
# scenarios exist. The real-scenario comparison (two build
# configurations of M1-SAMPLE-01's hello) is skipped until that
# sample lands; M1-DET-04 activates it (see the job in ci.yml for
# the full note).
# M1-DET-04: runs on every PR (no ci:* condition). The determinism
# checker (FR-11.5): the built-in synthetic scenario self-check,
# then the M1-SAMPLE-01 hello scenario asserted against the
# committed per-tick hash baselines (samples/hello/baselines/) on
# BOTH SimMath backends (fixed_point_16_16, float_pinned_32 — ADR
# 0002) with `hello --expect` (the laige-replay --expect contract:
# 0 match, 1 first divergence, 2 baseline read/contract error).
# The PR's P0 OS job's ctest repeats the same baseline assertion on
# its own platform (tests/sample: hello_baseline_fpx /
# hello_baseline_fp32); this tooling job guarantees the check runs
# on every PR, including ones labelled for non-Linux P0 OSes. The
# full two-configuration matrix (g++ vs clang++, Debug+ASan vs
# Release) runs on merges in the detcheck job of ci.yml.
name: Determinism check
runs-on: ubuntu-24.04
timeout-minutes: 10
Expand All @@ -324,15 +330,13 @@ jobs:
run: cmake --build build -j
- name: Synthetic scenario self-check
run: ./build/bin/laige-detcheck --scenario=synthetic
- name: Real scenario (M1-SAMPLE-01)
# Skipped until M1-SAMPLE-01 lands the hello scenario binary;
# M1-DET-04 replaces this step with the two-configuration
# comparison (see the job in ci.yml).
- name: Real scenario vs baselines (M1-SAMPLE-01, both backends)
# The hello template is always built in this job, so both steps
# always execute. Each must exit 0 (a first-divergence report is
# exit 1; a baseline read/contract failure is exit 2) and print
# the 301-line per-tick hash stream.
run: |
if [ -x samples/hello/bin/hello ]; then
./build/bin/laige-detcheck \
--run-a=samples/hello/bin/hello \
--run-b=samples/hello/bin/hello
else
echo "skipped: no real scenario yet (M1-SAMPLE-01)"
fi
./samples/hello/bin/hello \
--expect samples/hello/baselines/fixed_point_16_16/hash_stream.txt
./samples/hello/bin/hello-fp32 \
--expect samples/hello/baselines/float_pinned_32/hash_stream.txt
136 changes: 103 additions & 33 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -330,42 +330,112 @@ jobs:
run: ./build/bin/laige-api-scanner --root . --check laige-api.json

detcheck:
# M0-TOOL-02 (FR-11.5; AGENTS ARCH-010, TEST-004): the determinism
# checker skeleton. This job runs the built-in synthetic scenario
# self-check (two in-process runs, per-tick hash comparison — the
# step's Verify clause), so a broken checker lands red here before
# the real scenarios exist. The real-scenario comparison — two build
# configurations of M1-SAMPLE-01's hello scenario (Debug+ASan vs
# Release, plus g++ vs clang++ on Linux) — is SKIPPED until that
# sample lands; M1-DET-04 activates it and records the result per
# ARCH-010. Like include-lint and api-manifest, the job runs on every
# PR and merge, independent of the ci:* label selector — it is a
# tooling check, not an additional P0 OS build (the PRD §14 cadence
# is unchanged).
# M1-DET-04 (FR-11.5; AGENTS ARCH-010, TEST-004): bit-exactness CI.
# The M1-SAMPLE-01 hello scenario is run headless across the
# determinism matrix and asserted for per-tick hash identity:
#
# vs the committed baselines (samples/hello/baselines/; reference
# build: canonical Debug g++) — the P0 OS jobs' ctest already
# runs this half on every platform (tests/sample:
# hello_baseline_fpx / hello_baseline_fp32); this job pins the
# reference build against its own baselines and adds the
# cross-build pairs no single P0 job can produce:
#
# pair A: g++ Debug vs clang++ Debug (the two Linux
# compilers)
# pair B: Debug+ASan vs g++ Release (the two
# configurations)
#
# ... x both SimMath backends (fixed_point_16_16 and
# float_pinned_32 — ADR 0002). A desynced pair is declared
# unsupported for float_pinned_32 (same-build/same-ISA
# scope); fixed_point_16_16 must always match. The result is
# recorded in docs/benchmarks/determinism-matrix.md (scope
# stated per ARCH-010).
#
# Like include-lint and api-manifest, the job runs on every PR and
# merge, independent of the ci:* label selector — it is a tooling
# check, not an additional P0 OS build (the PRD §14 cadence is
# unchanged).
name: Determinism check
runs-on: ubuntu-24.04
timeout-minutes: 10
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- name: Configure
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
- name: Build
run: cmake --build build -j
# -- The four build configurations (targeted builds: only the
# scenario binaries and the checker — no full project build).
# The hello binaries land at the SOURCE-tree path
# samples/hello/bin/ (shared across build trees), so each
# tree's binaries are copied into detbin/ immediately after
# that tree is built.
- name: Build reference (Debug g++)
run: |
cmake -S . -B build-det-ref \
-DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_COMPILER=g++
cmake --build build-det-ref -j \
--target hello hello-fp32 laige-detcheck
mkdir -p detbin
cp samples/hello/bin/hello detbin/hello-gcc-debug
cp samples/hello/bin/hello-fp32 detbin/hello-fp32-gcc-debug
cp build-det-ref/bin/laige-detcheck detbin/laige-detcheck
- name: Build clang++ Debug
run: |
cmake -S . -B build-det-clang \
-DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_COMPILER=clang++
cmake --build build-det-clang -j --target hello hello-fp32
cp samples/hello/bin/hello detbin/hello-clang-debug
cp samples/hello/bin/hello-fp32 detbin/hello-fp32-clang-debug
- name: Build Debug + ASan (clang++)
run: |
cmake -S . -B build-det-asan \
-DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_COMPILER=clang++ \
-DLAIGE_ASAN=ON
cmake --build build-det-asan -j --target hello hello-fp32
cp samples/hello/bin/hello detbin/hello-asan-debug
cp samples/hello/bin/hello-fp32 detbin/hello-fp32-asan-debug
- name: Build Release (g++)
run: |
cmake -S . -B build-det-release \
-DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER=g++
cmake --build build-det-release -j --target hello hello-fp32
cp samples/hello/bin/hello detbin/hello-gcc-release
cp samples/hello/bin/hello-fp32 detbin/hello-fp32-gcc-release
# -- The reference build must reproduce its own committed baselines
# (a stale baseline is a loud job failure, never a silent
# re-baseline — CORE-008).
- name: Reference baseline sanity
run: |
./detbin/hello-gcc-debug \
--expect samples/hello/baselines/fixed_point_16_16/hash_stream.txt
./detbin/hello-fp32-gcc-debug \
--expect samples/hello/baselines/float_pinned_32/hash_stream.txt
# -- The checker's built-in synthetic scenario self-check (two
# in-process runs; the M0-TOOL-02 step's Verify clause).
- name: Synthetic scenario self-check
run: ./build/bin/laige-detcheck --scenario=synthetic
- name: Real scenario (M1-SAMPLE-01)
# The hello template (samples/hello) is always built in this
# job, so this step always executes: two no-arg runs of the
# scenario — both must exit 0 and print the 301-line per-tick
# hash stream (the guard is defensive only). M1-DET-04 replaces
# this step with the two-configuration comparison, e.g.:
# ./build/bin/laige-detcheck --run-a=build-asan/bin/hello
# --run-b=build/bin/hello
run: ./detbin/laige-detcheck --scenario=synthetic
# -- The two-configuration pairs (the roadmap matrix), both
# backends. Phase 1 spawns the two runs and streams their
# per-tick hash lines into one combined file (the children
# inherit the redirected stdout); phase 2 compares it offline
# (the Windows handle-delivery workaround; this job is
# ubuntu-only, so phase 1 is a plain bash redirection).
- name: Pair A — g++ Debug vs clang++ Debug
run: |
./detbin/laige-detcheck \
--run-a=detbin/hello-gcc-debug --run-b=detbin/hello-clang-debug \
> detbin/combined-a-fpx.txt
./detbin/laige-detcheck --compare-combined=detbin/combined-a-fpx.txt
./detbin/laige-detcheck \
--run-a=detbin/hello-fp32-gcc-debug --run-b=detbin/hello-fp32-clang-debug \
> detbin/combined-a-fp32.txt
./detbin/laige-detcheck --compare-combined=detbin/combined-a-fp32.txt
- name: Pair B — Debug+ASan vs Release
run: |
if [ -x samples/hello/bin/hello ]; then
./build/bin/laige-detcheck \
--run-a=samples/hello/bin/hello \
--run-b=samples/hello/bin/hello
else
echo "skipped: no real scenario yet (M1-SAMPLE-01)"
fi
./detbin/laige-detcheck \
--run-a=detbin/hello-asan-debug --run-b=detbin/hello-gcc-release \
> detbin/combined-b-fpx.txt
./detbin/laige-detcheck --compare-combined=detbin/combined-b-fpx.txt
./detbin/laige-detcheck \
--run-a=detbin/hello-fp32-asan-debug --run-b=detbin/hello-fp32-gcc-release \
> detbin/combined-b-fp32.txt
./detbin/laige-detcheck --compare-combined=detbin/combined-b-fp32.txt
19 changes: 15 additions & 4 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@ temp+rename, size-bounded), `Engine::startReplayRecording`, and
template game (samples/hello: one component, one system, one entity,
the per-tick `World::stateHash` stream on stdout, record → replay
through the scenario's own binary, the PRD §9.4 line budget —
[samples/hello/README.md](../../samples/hello/README.md)).
[samples/hello/README.md](../../samples/hello/README.md));
M1-DET-04: bit-exactness CI — the hello scenario's committed per-tick
hash baselines (both SimMath backends), the `hello --expect` baseline
check in every P0 OS job's ctest, the merge `detcheck` job's
two-configuration pairs (g++ vs clang++, Debug+ASan vs Release), and
the [determinism report](benchmarks/determinism-matrix.md)).
Every section of the AGENTS §13 `docs/` tree exists; each entry below
links what is written and the "not yet written" section marks what is
still to land.
Expand Down Expand Up @@ -142,7 +147,8 @@ still to land.
(M0-CORE-06).
- [Determinism checker](api/detcheck.md) — the `laige-detcheck` tool and
the scenario hash-line contract (`<tick> <hash>` lines, two build
configurations) (M0-TOOL-02).
configurations) (M0-TOOL-02; activated on the hello scenario by
M1-DET-04 — the baseline comparison and the CI matrix).

## Guides

Expand All @@ -166,8 +172,13 @@ still to land.
report fields, the `budgets.json` field mapping, the baseline-file
convention, and the PRD §8.1 regression policy (M0-DOC-01).
- [Baselines](benchmarks/baselines/README.md) — recorded baseline
reports. **Empty so far** (all `budgets.json` entries have
`measured: 0`); the first, `m0-synthetic.md`, lands with M0-EXIT-01.
reports (`m0-synthetic.md`, M0-EXIT-01, and `m1-ecs-stress.md`,
M1-ECS-07; every `budgets.json` entry still has `measured: 0`).
- [Determinism matrix](benchmarks/determinism-matrix.md) — the
M1-DET-04 determinism report: the committed per-tick hash baselines
of the hello scenario (both SimMath backends), the CI matrix (every
P0 OS job's baseline check + the merge detcheck job's cross-compiler
/ sanitizer / Release pairs), and the ARCH-010 scope statement.

## Architecture decisions (ADRs)

Expand Down
85 changes: 71 additions & 14 deletions docs/api/detcheck.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,13 @@ output contract** defined below and compares two such streams. The
real scenario now exists: **M1-SAMPLE-01**'s `hello` (samples/hello)
prints exactly this contract — the tick-0 line plus one
`World::stateHash` line per completed tick — and the CI job's real-
scenario step runs it; **M1-DET-04** activates the two-configuration
comparison of it (the tool's line-by-line comparison is unchanged).
scenario stream in CI: **M1-DET-04** activated the checker on it — the
merge CI job runs the two-configuration matrix (g++ vs clang++ Debug,
Debug+ASan vs Release, both SimMath backends; see [CI status](#ci-status)),
and every P0 OS job's ctest asserts the stream against the committed
per-tick hash baselines (`samples/hello/baselines/`) with `hello
--expect` ([baseline comparison](#baseline-comparison-hello---expect)).
The tool's line-by-line comparison is unchanged.

## Scenario contract

Expand Down Expand Up @@ -186,17 +191,69 @@ early exit / short stream). Each test is a generated `cmake -P` check
script asserting both the exit code and the required output fragments
(same pattern as `tests/api`).

## Baseline comparison (`hello --expect`)

`laige-detcheck` compares **two runs of one build configuration**
against each other; the committed baselines (below) compare a run
against the **reference build**. The scenario's own binary carries that
check — `hello --expect BASELINE` — because a game scenario's log
cannot be replayed by `laige-replay` (different registrations → replay
identity mismatch; ADR 0002). It is the scenario-side equivalent of
`laige-replay --expect` and follows the same contract:

- stdout is the run's 301-line hash stream (unchanged);
- **exit 0** — every line matches the baseline (identity);
- **exit 1** — first divergence, with the `laige-replay --expect`
report on stderr (`hash mismatch at tick N (first divergence)` + the
baseline/run lines, or the stream-length-mismatch report);
- **exit 2** — a baseline read/contract error (missing file, a line
not matching `<tick> <hash>`, the 65536-line bound, the 64-byte
line bound, the 8 MiB read cap) before any tick is run.

The check is O(1) per tick in place (one line compared at a time; no
allocation after the baseline loads).

The committed per-tick hash baselines live in
[`samples/hello/baselines/`](../../samples/hello/baselines/README.md) —
one stream per SimMath backend, generated from the **reference build
(canonical Debug g++)**:

- `fixed_point_16_16/hash_stream.txt` — bit-exact by the C++20
standard; every conforming build must reproduce it;
- `float_pinned_32/hash_stream.txt` — same-build/same-ISA scope (ADR
0002); a desynced build is declared **unsupported** for that backend
on its platform (never re-baselined silently).

## CI status

The `detcheck` CI job (`.github/workflows/ci.yml` and `ci-pull.yml`) runs
the built-in self-check on every PR and merge (like `include-lint` and
`api-manifest`, independent of the `ci:*` label selector — it is a
tooling check, not an additional P0 OS build). It also runs the **real
scenario** (M1-SAMPLE-01): both `--run-a` and `--run-b` point at
`samples/hello/bin/hello` (the no-arg 300-tick headless run — the step
activates once the sample binary exists, which it now does), so every
PR and merge executes the template game's detcheck contract: both runs
must complete (exit 0) and print the 301-line hash stream. **M1-DET-04**
replaces this same-configuration step with the two-configuration
comparison (`build-asan/bin/hello` vs `build/bin/hello`) and records the
result per ARCH-010.
M1-DET-04 wired the determinism matrix into CI (PRD §14 cadence — every
merge; the result is recorded per ARCH-010 in
[benchmarks/determinism-matrix.md](../benchmarks/determinism-matrix.md)):

- **Every P0 OS job** (merge: all five; PR: the labelled one) runs the
full ctest suite, which includes `hello_baseline_fpx` /
`hello_baseline_fp32` — this job's own native build of the scenario
must reproduce the reference baselines bit-exactly, on both backends
(`hello --expect`, above). The failure fixtures (first divergence,
stream length, malformed line, missing file) are pinned by
`hello_baseline_mismatch` / `_truncated` / `_malformed` / `_missing`.
- **The merge `detcheck` job** (`.github/workflows/ci.yml`,
"Determinism check" — a tooling job on every merge, like
`include-lint` and `api-manifest`, independent of the `ci:*` label
selector): builds the scenario in four configurations (Debug g++,
Debug clang++, Debug+ASan clang++, Release g++ — only the scenario
targets + the checker), then runs
1. the **reference-baseline sanity** — the Debug g++ build must
reproduce both committed baselines (a stale baseline is a red
job, CORE-008);
2. the built-in **synthetic self-check** (two in-process runs — the
M0-TOOL-02 Verify clause);
3. the two-configuration **pairs**, both backends, via
`--run-a/--run-b` + `--compare-combined`:
**pair A** g++ Debug vs clang++ Debug (the two Linux compilers),
**pair B** Debug+ASan vs Release (the two configurations).
- **The PR `detcheck` job** (`.github/workflows/ci-pull.yml`) keeps the
single-build shape (the synthetic self-check) plus the
**both-backend baseline comparison** (`hello --expect` /
`hello-fp32 --expect`) — so the check always runs, even on PRs
labelled for a non-Linux P0 OS.
6 changes: 6 additions & 0 deletions docs/benchmarks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ Benchmark methodology, baselines, results, and the regression policy
`baselines/m1-ecs-stress.md`, M1-ECS-07) measure harness and stress
workloads that update no budget. `baselines/m1-profiler-cost.md`
lands with M1-PROF-01.
- [determinism-matrix.md](determinism-matrix.md) — the **determinism
report** for the M1-SAMPLE-01 hello scenario (M1-DET-04): the
committed per-tick hash baselines (both SimMath backends), the CI
matrix (every P0 OS job's ctest baseline check + the merge detcheck
job's cross-compiler / sanitizer / Release pairs), the ARCH-010
scope statement, and the `float_pinned_32` per-platform support list.
- Per-milestone performance results (M1 10k-entity tick, M2 50k-sprite
scene, M6/M7 zone server, …) land here as their milestones close —
see the [roadmap progress board](../../roadmap/README.md).
Expand Down
Loading
Loading