The trade-raid block is 16 of a measured turn's 18-22 generator words and it is the
first thing the standalone can model as a COUNT rather than as a formula: two chance
rolls per entry of the player vector, neither site inside a back edge, both
probabilities strictly inside (0,1) so neither early-out fires. src/app/trade_raid
implements it with the word cost reported rather than assumed, so a tuning table that
pushes an odds value to 0 or 1 removes the draw and the ledger says so.
Against lane Z's two calibrated oracle pairs the standalone now consumes 16 words and
lands 4 and 2 short, which is exactly the per-call-site ledger's split for those two
turns. It does NOT match the oracle's state, and the report says which sites are
missing instead of netting them off the total.
The tail's last phase -- the per-player turn record -- is modelled for the six fields
that are recoverable from the wire, and self-checked every run against the record the
input save already carries for its own turn: 480 fields over 80 player-records across
the corpus, 0 mismatches. It is not committed. Under --commit-blocked it closes 24
container-shaped divergences on the reference pair and opens 17 leaf-shaped ones, all
of them in the five fields the model does not hold, so the block is a measurement now
rather than an argument.
phases 14/44 of the two turn drivers (8 committed), 3/37 of the tail.
divergence unchanged: 209->204 and 108->103, 5 closed / 0 regressed on both.
`sots_turn` loads a 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 the result back through the engine's own writer.
The phase catalog carries all 32 + 12 + 37 phases whether or not they are
implemented, so an unimplemented phase is a named no-op that appears in the run
log rather than a silent absence. 14 of the 44 turn-driver phases are modelled,
7 commit anything, 2 of the 37 tail phases are modelled.
Modelled but NOT committed is a first-class state. A phase whose formula we hold
and whose inputs we do not is evaluated, reported, and left unwritten unless
--commit-blocked is passed. That distinction was earned: committing phase 31's
player-status restore regressed two leaves that had agreed with the oracle
before the turn, because the phase writes 1 and the file carries 4.
Measured against the game's own post-turn saves, leaves localised by
state_checksum.py with coverage proved by re-serialisation:
turn1-state -> turn2-state 209 -> 204 diverging, closed 5, regressed 0
turn2-state -> turn3-state 108 -> 103 diverging, closed 5, regressed 0
Two tests: app_catalog (the tables stay complete and nothing claims to be
verified against a live game) and app_turn (11 saves driven; an untouched load
re-serialises byte-identically, a turn leaves the file re-readable, and no
blocked or stub phase writes anything). Skips cleanly without SOTS_SAVES_DIR.
ctest 38/38, clean-room OK. src/shim untouched. docs/S-standalone.md has the
full gap list.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARBgSooAfokKUy6wKUKEyZ