tools/standalone_report.py drives sots-engine's sots_turn over each consecutive-turn save pair and diffs the result against the game's own post-turn save with state_checksum.py, which localises to named leaves and proves its own coverage by re-serialisation. turn1-state -> turn2-state baseline 209 diverging, after 204, closed 5 turn2-state -> turn3-state baseline 108 diverging, after 103, closed 5 regressed 0 on both `regressed` is reported next to `closed` and never netted off. It earned its place immediately: committing the phase-31 player-status restore turned two agreeing leaves into disagreeing ones, because the phase writes 1 and the file carries 4. The stable-system stand-in feeding the colony pass is a labelled hypothesis and it survived a changed workload -- the same 3 ntdev leaves closed on both pairs, six agreements, zero disagreements. Two things deliberately NOT implemented: the TShn/ltis counters (18 leaves, a `+1` would close them, but "+1 across one observed turn" is a hypothesis, not a reading), and the RNG state write-back (an advanced-but-incomplete generator is wrong in a different way from an untouched one). dashboard.py gains section 6, reading verify/results/standalone/status.json: phases modelled/committed per driver, baseline vs after, closed vs regressed, the subsystem breakdown of what still differs, and the RNG gap. Sections 6-8 renumbered to 7-9; the delta footer tracks the two new counts. DASHBOARD_README.md documents every number. findings/control-flow/standalone-scaffold.md has the ranked blocker list.
7 KiB
The standalone, and the measured distance to the byte-match
Lane S2, 2026-09-08. Engine branch wip/standalone; full documentation in
sots-engine/docs/S-standalone.md. This note records what the lane measured, what it declined
to implement, and the two numbers the campaign should track from here.
0. The headline
sots_turn loads a real save through the engine's own reader, walks the published phase
order of all three turn drivers, runs what we hold, prints what we do not, and writes a save
through the engine's own writer.
turn1-state.sav -> turn2-state.sav (a real End Turn)
baseline (a standalone that does nothing) 209 leaves diverge
after one standalone turn 204 leaves diverge
closed 5, regressed 0
turn2-state.sav -> turn3-state.sav
baseline 108 -> 103, closed 5, regressed 0
Phases: 14 of 44 turn-driver phases modelled, 7 committing anything; 2 of 37 of the post-combat tail. Generator words modelled per turn: 0 of the 18–20 consumed.
Leaves are verify/state-checksum/state_checksum.py's named leaves; every one of the six
saves involved reported coverage: PROVED on the same run, so the diff cannot be hiding
anything.
Regenerate with tools/standalone_report.py; outputs land in verify/results/standalone/
and the dashboard's new section 6 reads status.json.
1. What the scaffold is for
Everything the milestone needs already existed in pieces — a save reader with 100 % named coverage, a verified budget roll-up, a verified research slice, a mechanism-verified movement model, and byte-for-byte maps of both turn drivers. What did not exist was a place to put them and a number that says how far they get. That is the whole of this lane.
The phase catalog (src/app/phase_catalog.cpp) is the roadmap: all 32 + 12 + 37 phases, each
carrying a status and a note. Running sots_turn --phases prints the turn, and every
unimplemented phase prints itself. There is no way for a phase to be silently absent, and
app_catalog fails the build if a table develops a gap or if anything claims to be verified
(which in that table means "compared against the live game" — lane S2 held no VM).
2. The finding that justifies the design: a committed phase can make things worse
The first version of this lane implemented StrategyServer::ProcessTurn phase 31's
player-status restore — Status = 1 — and committed it. The comparison tool immediately
reported two regressed leaves on the turn2 → turn3 pair: two Player.Status words that
agreed with the oracle before the turn and disagreed after it.
The phase writes 1. The post-turn file carries 4. A load resets it to 0. So a writer between phase 31 and the autosave is unaccounted, and the input save happened to already carry the right answer.
This is the same shape as the campaign's oldest lesson in a new place: running more code is
not the same as knowing more. The standalone therefore separates modelled from
committed, and a phase whose inputs are not modelled is evaluated, reported and not
written unless --commit-blocked is passed. regressed is reported next to closed in every
run, never netted off.
Five phases are blocked behind one unresolved formula (below); S31 and T31 are blocked
behind their own; P11 is blocked behind the event-text table.
3. Two things this lane declined to implement
The TShn / ltis counters. 18 leaves of the remaining 204 are TShn and ltis moving
1 -> 2 on 8–10 systems, on both turn pairs. They look exactly like per-turn counters and a
+1 would close 18 leaves in ten lines of code. Nothing in the campaign names their writer, so
"+1 per turn across one observed turn" is a hypothesis, not a reading, and rule 6 says label it
as one. They are the cheapest measured target on the board and they are named here so the
next lane can close them properly rather than plausibly.
The RNG state. The generator advances during a real turn; the standalone leaves the blob
byte-identical by default. An advanced-but-incomplete state is wrong in a different way from an
untouched one, and the untouched one at least reports the truth. --commit-rng is there for
the day lane Z's ledger closes.
4. One hypothesis under test, and it survived a changed workload
ProcessColonyTurn takes stable as an input; in the original it is a callee's verdict. The
standalone stands in owned && !abandoned && !destroyed, labelled a hypothesis in the code and
printed as one in the run log.
It drives ntdev, which is a named leaf, so it is falsifiable. On turn1 → turn2 it judged 3
of 28 systems stable and closed exactly the 3 ntdev leaves the oracle moved. On
turn2 → turn3 — a different turn, a different set of orders — it closed the same 3 again.
Six agreements, zero disagreements, across two workloads. Not proof; recorded as such.
5. What is now measurably in the way
Ordered by what must be solved, not by size.
| # | blocker | cost in leaves on the reference pair | who can close it |
|---|---|---|---|
| 1 | the RNG ledger — 18–20 words/turn, none modelled, and the generator is saved state | 1 leaf, and it makes a byte-match arithmetically impossible | lane Z (in flight); nothing in src/app |
| 2 | the population → base-output term — one unresolved formula that blocks P01 P02 P05 P06 T31 |
~10 directly (Sav, BnkPr, BnkEl on 4 players), and it gates 5 of the 44 phases |
a formula lane against the live game |
| 3 | the post-combat tail, 37 phases, none implemented, and the driver the autosave is written from | 24 (turnstats) + the bankruptcy limits + observed designs + player reports |
its own milestone |
| 4 | the per-player system-visibility record (nve) |
32 — one mechanism, eight repetitions | spine phase 24 or tail phase 21 |
| 5 | the event pipeline — buckets, ids and localised text | ~20 across the players | needs the string table |
| 6 | Summary.Checksum — algorithm unknown |
1, and it is the last leaf to fall | — |
| 7 | the Player.Status writer — phase writes 1, file carries 4 |
4 | small, self-contained |
| 8 | ModCount — advances 12–44 times a turn from writers across both drivers; we model 2 |
1 | falls out of implementing the other phases |
6. Gates run, separately
tools/clean_room_check.sh -> clean-room check: OK (with src/app + tests/app staged)
ctest --preset host -> 100% tests passed out of 38 (was 36; +app_catalog, +app_turn)
app_turn with SOTS_SAVES_DIR set -> 11 saves driven, 0 failures
src/shim/ was not touched, so no CT111 cross-build was required.
7. What the corpus cannot answer
The two pairs above are the only true End-Turn transitions we hold. The other nine saves are
single states: app_turn drives a turn over each of them and asserts the file survives, but
there is no oracle to diff against. A third and fourth consecutive-turn pair — especially on a
Zuul game, where the research and node-travel paths differ — would make every number in §0
sturdier for the cost of two End Turns on VM140.