The canonical pair on the same binary puts ModCount on 12 -> 24 exactly (zero residual), so the cost table is right on the lists that pair exercises and the rich turn's residual of 2 must sit in lists 1, 7 or 12. The only uniform per-element explanation is list 7 (colonisation) at 2 bumps -- fitted to one observation, recorded as a prediction with its falsifier. Also records the regression check on tools/aiorders_to_tcb.py: 8 shallow logs byte-identical, 4 lane-CB logs corrected (they were deep dumps whose payloads the converter had been discarding), and the canonical pair's replay unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARBgSooAfokKUy6wKUKEyZ
40 KiB
Rung B on a rich turn — the replay runs, consumes the whole stream, and closes one leaf
- Type: subsystem / engine measurement
- Owner / date: lane CV · 2026-09-09 · HOST ONLY. No VM was touched. VM140 (reference, exclusive), VM141, VM145 and VM146 were not approached, not pinged and not logged into.
- Trigger:
campaign/backlog.md§2 Rung B — "byte-match given a recorded command stream" — against the campaign's first replayable rich-turn record,findings/control-flow/ai-visit-order-probe.md§4.2 (lane BR, run 2). - Consumes: lane BR's deep capture (
verify/results/shim/br/BR2-deep-aiorders.txt), lane BP's output savebp-pinB-turn28.sav(724528ff…, byte-identical to BR's run 2, verified by BR withcmp), lane BT'sstate_checksum --relabel-new-ids(findings/subsystems/relabel-new-ids.md), the 2026-09-09 fleet-id-order resolution §6. - Touches:
tools/aiorders_to_tcb.py(a deep-record reader, §2.2),verify/results/turncommands/br2-turn27-*.tcb,verify/results/standalone/cv/*, this file. Nosots-enginecode was changed. No engine phase was implemented (scope discipline).
0. Verdict
Outcome 3 of the four: a short, named leaf list. The replay runs, it consumes the whole recorded stream (85 commands over 8 blocks, 81 elements — element-for-element the block BR captured), and it is not byte-identical and not identical modulo the relabelling.
state_checksum --floats bits --mask nonereportsDIVERGED: 1092 leaf difference(s)againstbp-pinB-turn28.sav. The do-nothing baseline (the input save compared with the same target) is 1166. So the engine's turn plus the replay closes 80 leaves and regresses 6, and the command stream's own contribution to the state is exactly one leaf:/Sim/ModCount, which moves from 1430 (drivers alone) to 1500 against a target of 1502.Outcomes 1 and 2 were not merely missed — they are unreachable on this save for two reasons that have nothing to do with the turn, and both are named below (§4).
The 1092 leaves group into thirteen subsystems with nothing left over; the ranked worklist is §5, and it is the point of this document.
1. Predictions, and how they fared
Written before the first run, from reading command_replay.cpp and the capture format (rule 2).
They are weak predictions — this lane is a measurement, not a hypothesis test — and they are
recorded so the numbers below cannot be mistaken for a fit.
| # | prediction | verdict |
|---|---|---|
| P1 | The replay will run, not fail to parse: --turn-commands reads a .tcb, and tools/aiorders_to_tcb.py already converts an aiorders dump. The deep dump adds records, and adding records to a line-oriented format does not break a reader that skips what it does not know. |
HELD. Outcome 4 does not apply. The existing converter parses the deep log unchanged; what it drops is the deep payload, which §2.2 fixes. |
| P2 | The result will be outcome 3, because command_replay.cpp holds an applier for exactly one of the twenty-seven lists (list 5, system rates) and one gate (research rate). |
HELD, and worse than predicted: list 5's applier never fires, because the capture cannot type seven of its eight fields (§3.3). |
| P3 | --relabel-new-ids will refuse, by guard G3, because our engine mints no client-side fleet ids at all. |
HELD, verbatim (§4.2). |
| P4 | ModCount will land close to the target: the counter is a property of the stream and the stream is complete. |
HELD with a residual of 2 (§3.4). |
2. What was run
2.1 Build (rule 21, rule 24)
A fresh detached worktree of sots-engine at e7e2bd6 (ai: the gather order decides fleet names too, and not only ours), in this lane's own scratch directory. No other lane's worktree or
build directory was touched, and build-host/build-shim were rm -rf'd before configuring.
git -C ~/sots-engine worktree add <scratch>/cv-engine HEAD --detach
rm -rf build-host build-shim
cmake --preset host
cmake --build --preset host -j 8 # 253/253, clean
ctest --preset host with SOTS_SAVES_DIR pointed at the corpus is 57/59, and the two failures
are pre-existing and not this lane's — mars_stream_save and app_turn, both failing on the
same thing: the typed writer is not byte-exact on 12 of the 43 corpus saves (§4.1). The denominator
is 59, not a smaller number, so this is a real-failure signature and not a broken configure (rule 24).
2.2 The capture: one reader change, and why it was in scope
tools/aiorders_to_tcb.py parsed lane BR's deep dump without complaint, but it read only the
shallow aielem window. The deep run's extra records — 35 aivec rows and 3 aistr rows — were
silently dropped, so every route and ship vector arrived as vN (a length with no values), which is
exactly the shape a shallow capture produces. A deep capture that converts to a shallow .tcb
is a harness comparing nothing (rule 1), so the reader was extended:
aivec blk= pid= list= idx= at=wN … count=N ints=[…]is consulted only at a word the existing field map already types as a vector (("v", k)), so it lands on the field the map has, and does not invent one. Where it lands anywhere else it is written into the.tcbas a#comment and reported on stderr — a real observation that this tool cannot place.- The begin/end pointer pair and the followed
countare cross-checked; a disagreement keeps the length-only form rather than guessing. aistrrows (list 1's design names) are likewise recorded as comments. They are not promoted to fields: only the design object's name was followed, so the element is still incomplete, and claiming otherwise would flipCapturedElement::completeon a payload we do not have.
That this change reached the replayer is measured, not assumed (§3.2).
uv run python3 tools/aiorders_to_tcb.py verify/results/shim/br/BR2-deep-aiorders.txt \
-o verify/results/turncommands/br2-turn27-deep.tcb \
--input ad-turn27-two-raiders.sav \
--seed 32=0x156ebbbd --seed 496=0xfe7b2826 --seed 512=0x0ed341d1
The shallow conversion of the same log is kept beside it as
verify/results/turncommands/br2-turn27-shallow.tcb, because the difference between the two is the
control in §3.2.
2.3 The data root
--data needs an extracted tree, not a .gob, and none existed on the host. 1,103 text files were
extracted from /tmp/sots-gob/sots.gob (Weapons/, Species/*/sections/, Species/*/weapons/,
TechTree/MasterTechList.tech) plus Locale/EN/Strings.csv from sots_local_en.gob, into the
lane's scratch directory. The engine reports 885 section(s) over 7 race(s), 5196 string(s), 46 load problem(s). Nothing was written into ~/sots-engine (clean-room).
Without a data root the run reports 71 blocked leaves instead of 119; with it, the census phase evaluates. Both were run; the numbers in this document are the with-data ones.
2.4 Regression check on the shared tool — it changes four other lanes' logs, and it should
tools/aiorders_to_tcb.py is used by other lanes, so the change of §2.2 was run against every
aiorders log in the repo and diffed against the pre-change module:
| logs | result |
|---|---|
bp/{A,B,L}-aiorders.txt, br/BR-aiorders.txt, aiorders/l4-*.txt (8 logs) |
byte-identical output — they are shallow dumps and carry no deep rows |
aiorders/cb-c{1,3,5a,5b}-*.txt (4 logs) |
changed, and the change is a correction |
The four CB logs were deep dumps all along, and the converter had been discarding their
payloads. The only field changes are list 8's route and list 10's ship vector going from v1 to
v1:272 / v1:1728 — which is exactly what tools/tcb_from_json.py already emits for the same
capture, so the two converters now agree on every block, list, gate and element line but one. That
one is list 23: tcb_from_json.py's committed output for the canonical pair carries a
v6:12422480,1,2,… where its own current UNMAPPED_HEAD[23] = (1, 1) would emit ? — i.e. the
committed cb-turn2to3.tcb predates the current tool. It makes no behavioural difference (both
shapes leave a ? in the element, so it is incomplete either way) and it is another lane's
artefact: recorded here, not touched.
The canonical pair is provably unaffected. sots_turn turn2-state.sav --turn-commands with the
committed cb-turn2to3.tcb and with a freshly regenerated one produce a byte-identical save,
with identical dispositions (11 commands, 10 bumps, 4 applied / 5 declined / 2 incomplete).
2.5 The runs
T=<scratch>/cv-engine/build-host/src/app/sots_turn
S=~/sots-re/verify/results/saves
# the measurement
$T $S/ad-turn27-two-raiders.sav --data ./sotsdata \
--turn-commands verify/results/turncommands/br2-turn27-deep.tcb \
--out r1-deep.sav --metric r1.json
# the controls
$T $S/ad-turn27-two-raiders.sav --data ./sotsdata --out r0-nocmd.sav # no stream
$T $S/ad-turn27-two-raiders.sav --data ./sotsdata --turn-commands …deep.tcb \
--replay-count-only --out r4-countonly.sav
$T $S/ad-turn27-two-raiders.sav --data ./sotsdata --turn-commands …shallow.tcb --out r5-shallow.sav
$T $S/ad-turn27-two-raiders.sav --data ./sotsdata --turn-commands …deep.tcb --commit-blocked --out r2.sav
$T $S/ad-turn27-two-raiders.sav --data ./sotsdata --turn-commands …deep.tcb --commit-rng --out r3.sav
3. The guard set — this harness is not comparing nothing
Rule 1 is the failure this lane was most exposed to. Six checks, every one run, every one reported.
3.1 The stream was consumed, and its size is the block's size
turn-commands: br2-turn27-deep.tcb -- 8 block(s), 3 seed(s)
turn commands: 8 block(s), 4 submitting, 85 command(s)
ModCount bumps charged 70
applied 4 transcribed 0 declined 36 incomplete 45
leaves written by commands 0
85 = 81 elements + 4 gates. 81 is lane BR's own element count for the End-Turn batch, and the
per-list breakdown the replay walks — {1:3, 3:19, 5:15, 7:2, 8:3, 10:4, 12:12, 14:8, 23:15} for
player 32 — is BR's §1 P4 list, element for element. The four gates are the research-rate gate in
blocks 0/1/2/3 (players 16, 32, 496, 512). Nothing in the block was dropped and nothing was invented.
3.2 The deep payload reached the replayer — the dispositions move
The deep and shallow captures of the same run, replayed identically:
| capture | applied | transcribed | declined | incomplete |
|---|---|---|---|---|
shallow (vN, length only) |
4 | 0 | 29 | 52 |
deep (vN:a,b,c) |
4 | 0 | 36 | 45 |
Exactly seven elements move from incomplete to declined: list 8's three routes and list 10's
four ship vectors. That is the whole of what the deep dump followed onto a mapped field, and it is
the substantive result of the deep capture for this rung: on those seven commands the gap is now
the engine, not the record. ("Declined" means the engine names the missing subsystem;
"incomplete" means the capture could not carry the payload.)
3.3 The one modelled applier is starved by the capture, and that is a finding
command_replay.cpp holds exactly one list applier — list 5, the per-system planetary-budget
sliders — and it fired zero times, on all fifteen elements:
6 1 32 1 incomplete list 5 system rates [0..14]
the capture does not carry all eight fields of the rates frame
The reason is in the converter's own comment: the shim's 48-byte memory window shows only one
non-zero word in a list-5 element, at index 2, and the wire frame's member order is not the memory
order, so six of the seven sliders are ?. The only list this engine can apply is the only list
the memory dump cannot type. The fix is not engine work — it is one UI run (push two different
sliders to two different values and read the permutation off), or a capture converted from a save's
own TurnCommands block where every field is named.
3.4 The output is not the input, and it is not the no-stream output
| file | sha256 | /Sim/ModCount |
|---|---|---|
ad-turn27-two-raiders.sav (input) |
1c8baa27… |
1428 |
bp-turn28-pre.sav (the game's own pre-turn resave) |
16f63adb… |
1428 |
r0-nocmd.sav (engine turn, no stream) |
1437c5a0… |
1430 |
r1-deep.sav (engine turn + replay) |
99ee2437… |
1500 |
bp-pinB-turn28.sav (target) |
724528ff… |
1502 |
cmp r0-nocmd.sav r1-deep.sav differs; state_checksum r1 r0 reports exactly one leaf,
/Sim/ModCount: 1500 -> 1430. So the stream reached the save, and its entire effect on the state
is the counter.
The counter's arithmetic, in full. Target delta 1502 - 1428 = 74. Engine: 2 driver bumps
(T00 IncrementModCount, measured as the r0 delta) + 70 charged by the stream = 72. The 70 is
85 - 15: the fifteen list-23 population commands are charged 0 by the engine's cost table.
Residual: 2. No bumpsExact = false warning fired, so no gate with an unknown cost was set.
The residual is localised, by running the canonical pair on the same binary. turn2-state.sav
with lane CB's cb-turn2to3.tcb gives ModCount 12 → 24, and turn3-state.sav holds 24:
exact, zero residual, confirming CB's own prediction. So the cost table is right on every list
that pair exercises. That pair carries lists 3, 5, 8, 10, 14, 23; the rich turn adds lists
1 (3 elements), 7 (2), and 12 (12), and nothing else. The missing 2 bumps must therefore
sit in lists 1, 7 or 12 — and the only uniform per-element explanation is list 7 (colonisation) at
2 bumps per command, because 3 ∤ 2 and 12 ∤ 2.
That is a constant fitted to one observation (rule 23), so it is a prediction, not a result, and its falsifier is cheap and named: any capture with a colonize count other than 2. If list 7 costs 2, its residual scales with that count; if the extra 2 is a mixed defect across lists 1 and 12, it will not.
3.5 --replay-count-only produces the identical save
cmp r1-deep.sav r4-countonly.sav → byte-identical. That is the control
ReplayOptions::countOnly was built for, and it confirms the report's own
leaves written by commands 0 rather than taking it on trust.
3.6 The reader refuses a capture that lost an element, and flags the wrong board
Deleting one elem line from the .tcb:
turn-commands: block 1 list 10 declares 4 element(s) and carries 3
refusing to replay a capture that did not parse; a lost element is a counter that is
quietly short by one
$ echo $?
2
Replaying the capture against turn2-state.sav:
turn-commands: ! this capture was taken on 'ad-turn27-two-raiders.sav' and is being replayed
against 'turn2-state.sav'. A capture belongs to ONE board; the counter will be charged either
way and the number will be wrong.
Both guards fire. A silently-empty comparison is not the failure mode here.
4. Why outcomes 1 and 2 are unreachable on this save, independent of the turn
4.1 The engine's own writer is not byte-exact on this save family — one item, 12 bytes
$ sots_turn ad-turn27-two-raiders.sav --roundtrip
roundtrip (untouched): DIFFERS (1052288 bytes) # the file inflates to 1052300
mars_stream_test_save localises it: round trip: tree identical, typed DIFFERS, first divergence
at 0x3f02, 1052288 vs 1052300 — exactly 12 bytes, exactly one item. The bytes at that
offset are a Game::SpecialProjectNameGen record:
0x3ef2 00 00 75 73 6e 63 01 00 00 00 03 00 00 00 75 73 ..usnc.........us
0x3f02 70 00 00 00 00 00 03 00 00 00 75 73 63 01 00 00 p.........usc...
ad-turn27-two-raiders.sav has 103 name-gen records; 102 carry usnc = 0 and one carries
usnc = 1, and that one is followed by a usp item and then a usc item. The codec
(src/mars/stream/shapes.h, struct ProjectName) reads the usnc count and then one item per
element:
ar.narr(A("usnc"), used, [](Ar& a, Node& e) { a.any(A("usc"), e); });
// comment on the member: "usnc is 0 in every save available"
That comment is rule 6's hypothesis, and this save falsifies it: measured, usnc == 0 in 102 of
the 103 records (and the writer's Nm follows immediately), usnc == 1 in one, and that one is
followed by usp and usc before Nm. The generated schema already lists both
(k_Game__SpecialProjectNameGen = NNms2, usnc, usp, usc, Nm, Ntg, with usnc and usp both
marked computed). The engine never reads or writes the usp item, and an independent reader —
state_checksum.py's, which does name it — reports it in the comparison as a leaf of its own,
/Sim/sprjs/usp: only-in-B. Two implementations agree on which item is missing.
All 12 failing corpus saves lose exactly 12 bytes, each at an offset in the sprjs band
(0x3f02–0x43a6), so it is one defect, one item, twelve files — not twelve problems.
Consequence for this rung. Byte-identity (outcome 1) is impossible on ad-turn27 and on the 11
other corpus saves with the same shape, before a single turn phase runs. This is a two-line fix in
the wire codec — but it is a change to a schema-verified, 100%-coverage module that several lanes
and the determinism oracle depend on, and re-proving byte-neutrality across the corpus is the job,
not the edit. Named and stopped, per scope discipline. It is item 0 of the worklist.
(This also explains one of the six "regressed" leaves in §0: /Sim/sprjs/usp agreed between the
input and the target and is dropped by our writer.)
4.2 --relabel-new-ids refuses, correctly, and the refusal is the informative result
$ uv run python3 verify/state-checksum/state_checksum.py \
r1-deep.sav verify/results/saves/bp-pinB-turn28.sav --floats bits --mask none \
--relabel-new-ids verify/results/saves/ad-turn27-two-raiders.sav
relabel: pre-turn save verify/results/saves/ad-turn27-two-raiders.sav
relabel: REFUSED -- the new client-minted fleet ids differ between the two saves -- A minted [],
B minted [1970, 1986, 2002]; only in B [1970, 1986, 2002]. That is a difference in what
was allocated, not in how it was labelled, so no relabelling is defensible
relabel: NO relabelling was applied; the comparison below is the ordinary one, and nothing has
been absorbed.
DIVERGED: 1092 leaf difference(s)
Identical output with bp-turn28-pre.sav as the pre-turn save. Guard G3, by cardinality: our
engine minted zero client fleets. Outcome 2 ("identical modulo the relabelling") is not available
because there is nothing to relabel — the relabelling quotients out which new fleet got which
id, and this engine creates no new fleet at all. Lane BT's tool did exactly what §5 of its own
write-up says it will: it refused rather than degrading, printed the reason, and ran the ordinary
comparison, so nothing was absorbed. The 1092 above is byte-for-byte the number the tool reports
without the flag.
The residue really is insulated from the replay, as the resolution says — it just cannot be demonstrated yet, because demonstrating it requires the engine to mint the ids in the first place. When list 10 has an applier that takes the ids off the wire in block order (the resolution §6's "replay applies ids off the wire and is insulated by construction"), this comparison becomes the one that either passes or refuses for a real reason.
4.3 The tail's sim draw does fire on this workload
The backlog's 2026-09-09 correction warns that replay runs the post-combat tail and that the tail
carries a sim draw gated on tscr, and notes the canonical pair is untouched because tscr = 252
there. On ad-turn27-two-raiders.sav every trade sector reads tscr = 253 — measured, six
sectors, Trade[752…832]. This is lane AD's own two-raider state, for which
findings/control-flow/raid-gate-multiplicity.md measured
TradeManager::Slot13RngCalleeA entered 2× and OnAllCombatDone_Tail costing 2 strategic
words at 0x00820e18.
So this is precisely the case the correction was written for: the generator model matters on this
replay and the engine does not model the site (S02 TradeManagerTurn and T23 TradeManagerEndOfTurnHooks are stubs). sots_turn consumes 16 words on S31 and leaves the saved
generator untouched by default; --commit-rng writes its own 16 words and the leaf count is
unchanged at 1092, because the words are the wrong words. /Sim/RNG/. is 1 leaf and it stays.
5. The ranked worklist — 1092 leaves, thirteen subsystems, nothing ungrouped
Full listing: verify/results/standalone/cv/cv-leaf-worklist.txt. Every leaf is in exactly one
group and the residual is 0.
| rank | leaves | subsystem | phase(s) / tag | independently closable? |
|---|---|---|---|---|
| 1 | 539 | per-system morale event ring (cme2) |
S01 SystemPrePassMoraleAndAbandon (stub), T13 UpdateTreasuryMorale (stub), T14 UpdateForeignFleetMorale (stub) |
No — see below |
| 2 | 154 | colony growth, repair, bonuses (Pv*, Pop, Infra, RepCur/Max, pbon, ibon, ntdev, Rts) |
S11 SystemTurn (partial), T19 DrainInfraTerraformQueue (stub), T21 (partial) |
Mostly yes |
| 3 | 150 | fleet objects: position, route, range, layout, health | S07 NodeSpaceTravel, S08 FleetMovement, S09/S10, T10 — all stubs |
Partly |
| 4 | 79 | observed designs / techs / weapons (odes, otch, owep) |
T34 RecordObservedDesigns (stub) |
Yes |
| 5 | 30 | player money, events, designs, counters (Sav, BnkEl/Pr, Maint, NumDes, NumOwn, ResTNm, nmeid, lboid, FNGNum, designs/Des[]) |
P01/P02 (partial, budget-blocked), P05 ProcessResearch (blocked), command list 1 |
No |
| 6 | 23 | visibility remainder (TShn) |
known open item from lane E3 — gate proven NOT AFlags |
No (watchpoint specified) |
| 6= | 23 | system↔fleet membership (Sys.Flt[], NumFlts) |
command list 10 + S08 |
No |
| 8 | 20 | ship census records (ShipRecs/sr*) |
S17 ShipStatCacheRefresh, T33 ShipManagerEndOfTurnHooks — stubs |
Partly |
| 8= | 20 | build queues (BQ/ords) |
command list 3 — no phase in this engine builds a ship | No |
| 10 | 15 | other system leaves (Bats2, DefF, flags, OID/PID) |
S15/S16, T28/T30 — stubs |
— |
| 11 | 13 | combat reports (crep) and /CD |
partly the known re-save delta (§5.3) | — |
| 12 | 10 | master id lists, counters, generator (ShipIDs/FleetIDs/DesignIDs, ModCount, NMnx, Act, cmbtid, RNG) |
id allocation + the generator model | No |
| 13 | 8 | turn record archive (turnstats) |
T36 FinalizeTurnRecords (blocked, would=104) |
No |
| 14 | 6 | trade manager (fwarn, tsflt) |
S02, T23, T18/T32 — stubs |
— |
| 15 | 1 | save-writer defect (sprjs/usp) |
§4.1 | Yes, two lines |
| 16 | 1 | derived /Summary/Checksum |
unmodelled; masked by --mask resave elsewhere |
No |
5.1 Which single module would close the most — and the honest caveat
By leaf count the answer is the morale event ring, at 539 leaves — half the total. But the
count needs decoding before it is used as a ranking, because cme2 is a fixed-size rolling
window: every system carries exactly 11 slots (158 events across 28 systems in the input, the
target and our output alike). Appending one event shifts all eleven, so a handful of appended events
produces hundreds of positional leaves.
Decoded against the target, the turn appended 27 events across 10 systems (7 systems got 3, 3 got
2), all stamped mtr = 28, of exactly three kinds:
| × | mtp |
mfx.mv |
mdsc |
what it needs |
|---|---|---|---|---|
| 10 | 12 | +2 | Colony established on Avalon. |
colonisation actually happening — command list 7 + T16 ResolveArrivedColonizers |
| 10 | 20 | +2 | Imperial savings of 5,000,000 or more. |
the imperial savings figure — P01/P02, backlog item 1 (the same system's pre-turn event was the 15,000,000 band, so the AI's spend moved it down a band) |
| 7 | 16 | −1 | Population limits inhibit freedom. |
the colony population/limit term — S11, backlog item 1b |
So: the morale ring is the reporting surface of three subsystems it does not own. The ring
mechanics (append, cap at 11, the mid counter — which reaches 324 in the target from 315 in the
input, and mtr = turn) are trivial; the event rules are the work, and they are the same three
things the backlog already ranks first.
The single module with the best leaves-per-unit-of-new-work is therefore rank 4:
T34 RecordObservedDesigns — 79 leaves, one stub phase, no upstream dependency. The observed-design
and observed-tech lists are derived from state we already hold; nothing in them waits on the budget,
on ship construction or on the AI. It is the largest independently closable item in the list.
Second-best on that measure is S11's colony half (rank 2, 154 leaves), which the backlog already
has as item 1b, and which this run gives a sharp new fact about — see §5.2.
5.2 A measured correction: Pv* is not "last turn's value"
The Pv-prefixed system fields read like a previous-turn snapshot, which would make them cheap
(copy before the turn). They are not. On the ten systems where our value disagrees:
Sys[80 "Kam’Kir"]/PvInfra target 1.03 pre-turn Infra 1.0 post-turn Infra 1.0
Sys[80 "Kam’Kir"]/PvPop target 617,999,994 pre-turn Pop 600,000,000 post-turn Pop 600,000,000
600,000,000 × 1.03 = 618,000,000, and 1.0 × 1.03 = 1.03. Pv* holds the growth result before
whatever clamps it back — the uncapped projection — on a colony sitting at its cap. PvSuit (5/5)
and PvRes (4/4) do equal the pre-turn current value, so the field family is mixed and must not be
modelled with one rule. This belongs in id-allocation.md's neighbour, the colony notes, and it is a
prerequisite for closing rank 2.
5.3 Closed and regressed, reported separately (never netted)
Against the do-nothing baseline (input vs target, 1166 leaves):
- closed: 80
- regressed: 6 — and all six are named:
/Sim/sprjs/usp— the writer defect of §4.1, not a turn phase.- five
ntdevleaves,Sys[160/400/416/432/448]: the engine writes 1 where both the input and the target hold 0. On the same runntdevwas closed on thirteen other systems (26 → 27). SoS11'sntdevrule is right for the systems that develop and wrong for five that do not — a one-predicate bug with a five-leaf price, and the cheapest single fix in this document.
The stream itself closed 0 additional leaves and regressed 0 relative to the no-stream run; its whole contribution is the counter (§3.4).
5.4 What the capture already holds that the engine declines
Three of the target's leaves are /Sim/players/Player[32]/designs/Des[2018 "Egg Thief Mk 4"],
Des[2034 "Egg Thief Mk 5"], Des[2050 "Bravestar Mk 3"] — exactly the three names lane BR's
aistr rows recorded, and their ids are the same node-2 counter that minted the fleets (123/124/125
for 1970/1986/2002; 126/127/128 for 2018/2034/2050 — one counter across object kinds, as lane BT's
§4.3 warned). The capture carries the design names and the engine declines list 1 for want of a
design-registration path and an id allocator. That is a small, well-evidenced item that the deep
capture has now made testable, and it is a precondition for NumDes 46 → 49 and for the three
DesignIDs[] entries.
5.5 Variants, for the record
| run | leaves vs target |
|---|---|
--turn-commands (the measurement) |
1092 |
| no stream | 1092 |
--turn-commands --commit-rng |
1092 |
--turn-commands --commit-blocked |
1097 (worse by 5 — the blocked phases' outputs are blocked for a reason) |
| input, untouched | 1166 |
6. What this lane did NOT establish
- Nothing about a turn other than this one, a route other than load, a build other than
e7e2bd6, or a target other than724528ff…. The other member of the outcome set (e913ff41…/bp-pinA-turn28.sav) was not compared; on the evidence of §4.2 it would refuse identically and the leaf count would differ only inside BP's 35-leaf transposition. - No engine phase was implemented and no engine code was changed. The
uspcodec defect (§4.1) is named, not fixed. - The
ModCountresidual of 2 is not explained. It is measured, bounded to lists 1/7/12 by the canonical pair's zero residual, and given one uniform candidate (list 7 at 2 bumps) that is fitted to a single observation and must be tested against a capture with a different colonize count before it is believed. - The morale ring's three event rules are named from their
mdscstrings and theirmtpcodes, not decoded from the binary.mtp12 / 16 / 20 are three of the codes; the full table is not read, andsrc/game/sim/colony.hnames only the addiction codes0x1b–0x1d. - The list-5 slider permutation was not resolved (§3.3). The experiment that settles it is a UI run, not analysis.
/Summary/Checksumremains unmodelled — masked elsewhere, reported here, not solved.- The 46 data-root load problems were not triaged. They do not affect any leaf in §5, but they are unexamined.
7. Reproducing every number here
cd ~/sots-re
# 1. capture -> .tcb (already committed; this regenerates it)
uv run python3 tools/aiorders_to_tcb.py verify/results/shim/br/BR2-deep-aiorders.txt \
-o /tmp/br2-deep.tcb --input ad-turn27-two-raiders.sav \
--seed 32=0x156ebbbd --seed 496=0xfe7b2826 --seed 512=0x0ed341d1
diff /tmp/br2-deep.tcb verify/results/turncommands/br2-turn27-deep.tcb
# 2. build (fresh worktree, fresh build dir -- rules 21 and 24)
git -C ~/sots-engine worktree add /tmp/cv-engine e7e2bd6 --detach
cd /tmp/cv-engine && rm -rf build-host && cmake --preset host && cmake --build --preset host
# 3. replay
build-host/src/app/sots_turn ~/sots-re/verify/results/saves/ad-turn27-two-raiders.sav \
--data <extracted-data-root> \
--turn-commands ~/sots-re/verify/results/turncommands/br2-turn27-deep.tcb \
--out /tmp/r1-deep.sav
# 4. compare
cd ~/sots-re
uv run python3 verify/state-checksum/state_checksum.py \
/tmp/r1-deep.sav verify/results/saves/bp-pinB-turn28.sav \
--floats bits --mask none --limit 100000 # DIVERGED: 1092
uv run python3 verify/state-checksum/state_checksum.py \
/tmp/r1-deep.sav verify/results/saves/bp-pinB-turn28.sav \
--floats bits --mask none --relabel-new-ids verify/results/saves/ad-turn27-two-raiders.sav
# REFUSED (G3), then DIVERGED: 1092
Artefacts: verify/results/turncommands/br2-turn27-{deep,shallow}.tcb,
verify/results/standalone/cv/{cv-replay-vs-bp-pinB,cv-input-vs-bp-pinB,cv-noreplay-vs-bp-pinB,cv-leaf-worklist,cv-replay-run.log}.
Proposed board rows
New row:
| RUNG B ON A RICH TURN: THE REPLAY RUNS AND CONSUMES THE WHOLE STREAM; 1092 LEAVES, THIRTEEN NAMED SUBSYSTEMS, ZERO UNGROUPED | engine | verified | high | 100% | 2026-09-09 | Lane CV, HOST ONLY (no VM; 140/141/145/146 untouched). First replay of a RICH turn's command stream: lane BR's deep block (`BR2-deep-aiorders.txt`) converted to `.tcb` and replayed by `sots_turn --turn-commands` against `ad-turn27-two-raiders.sav` (`1c8baa27...`), compared with `bp-pinB-turn28.sav` (`724528ff...` = BR's own output, cmp-verified). **VERDICT: OUTCOME 3 -- a named leaf list, not a byte match.** `state_checksum --floats bits --mask none` = **DIVERGED: 1092 leaf difference(s)**; the do-nothing baseline (input vs target) is **1166**, so the turn+replay CLOSES 80 and REGRESSES 6 (never netted). The stream's entire contribution to the state is ONE leaf: `/Sim/ModCount` 1430 -> 1500 against a target of 1502 (85 commands = 81 elements + 4 gates, 70 bumps charged = 85 - the 15 list-23 population commands the cost table charges 0; 2 driver bumps; **residual 2**). **THE RESIDUAL IS LOCALISED:** the same binary on the CANONICAL pair (`turn2-state.sav` + lane CB's tcb) puts `ModCount` on **12 -> 24 EXACTLY, zero residual**, matching `turn3-state.sav` and confirming CB's own prediction -- so the cost table is right on lists 3/5/8/10/14/23, and the missing 2 must sit in lists **1 (3 elements), 7 (2), 12 (12)**, the only three the rich turn adds. The only UNIFORM per-element explanation is **list 7 (colonisation) at 2 bumps per command** (3 does not divide 2, nor does 12) -- a constant fitted to ONE observation (rule 23), so it is a PREDICTION with a named cheap falsifier: any capture with a colonize count other than 2. GUARD SET, ALL SIX RUN (rule 1): (a) 85/81 matches BR's block element-for-element and per-list; (b) the DEEP payload provably reached the replayer -- shallow vs deep captures of the same run move exactly 7 elements from `incomplete` to `declined` (list 8's 3 routes, list 10's 4 ship vectors), i.e. on those seven the gap is now the ENGINE not the record; (c) `--replay-count-only` produces a BYTE-IDENTICAL save, confirming `leaves written by commands 0`; (d) deleting one `elem` line makes the reader REFUSE with exit 2; (e) replaying against another board warns; (f) output != input and != the no-stream output. TWO BLOCKERS PUT OUTCOMES 1 AND 2 OUT OF REACH BEFORE THE TURN RUNS: **(1) the engine's typed writer is not byte-exact on this save** -- `--roundtrip` DIFFERS by exactly 12 bytes = one item, at 0x3f02: `struct ProjectName` reads `usnc` as a count of ONE item per element (`a.any("usc")`, commented "usnc is 0 in every save available") but the wire pair is `usp` THEN `usc`, and this save has one record with `usnc == 1`. The generated schema already lists both. 12 of 43 corpus saves fail the same way; `mars_stream_save` and `app_turn` fail on it in ctest (57/59, pre-existing, denominator intact). Surfaces as `/Sim/sprjs/usp: only-in-B`. NAMED, NOT FIXED (schema-verified module, byte-neutrality must be re-proved). **(2) `--relabel-new-ids` REFUSES by guard G3** -- "A minted [], B minted [1970, 1986, 2002]" -- because this engine mints NO client fleet ids at all; the refusal applies nothing and the 1092 is byte-identically the no-flag number. Outcome 2 needs list 10's applier first. **THE TAIL'S SIM DRAW FIRES HERE:** every trade sector in this save reads `tscr = 253`, not the corpus's 252 -- this IS lane AD's two-raider state, measured at 2 entries / 2 strategic words at 0x00820e18 -- so the backlog's Rung B correction is load-bearing on this workload and `S02`/`T23` are stubs; `--commit-rng` leaves the count at 1092 because the 16 words are the wrong words. RANKED WORKLIST (full list in the finding, residual 0): morale event ring `cme2` **539**; colony growth/repair/bonuses **154**; fleet objects **150**; observed designs/techs **79**; player money+counters **30**; `TShn` **23**; system<->fleet membership **23**; ShipRecs **20**; build queues **20**; other system **15**; crep/CD **13**; master id lists+generator **10**; turnstats **8**; trade manager **6**; writer defect **1**; Checksum **1**. **THE 539 MUST BE DECODED BEFORE IT IS RANKED:** `cme2` is a FIXED 11-SLOT RING (158 events in input, output and target alike), so the turn's **27 appended events across 10 systems** shift every slot. The 27 are three kinds only: 10x `mtp=12` "Colony established on Avalon." (+2), 10x `mtp=20` "Imperial savings of 5,000,000 or more." (+2, down a band from the pre-turn 15,000,000), 7x `mtp=16` "Population limits inhibit freedom." (-1). So the ring is the REPORTING SURFACE of colonisation, the savings chain and the population term -- it owns none of them. **THE LARGEST INDEPENDENTLY CLOSABLE MODULE IS `T34 RecordObservedDesigns` (stub): 79 leaves, no upstream dependency.** TWO MEASURED CORRECTIONS: **`Pv*` is NOT a previous-turn snapshot** -- target `PvInfra` 1.03 and `PvPop` 617,999,994 where both pre- and post-turn `Infra`/`Pop` are 1.0 / 600,000,000 (= current x 1.03), so `Pv*` holds the UNCAPPED GROWTH PROJECTION on a capped colony, while `PvSuit` (5/5) and `PvRes` (4/4) DO equal the pre-turn current: the family is mixed and must not get one rule. And **five of the six regressions are `ntdev`** on Sys[160/400/416/432/448], where the engine writes 1 and both input and target hold 0, while the SAME rule closed 26->27 on thirteen other systems -- a one-predicate bug, the cheapest fix here. Also: the capture already holds the three design NAMES the target creates (`Egg Thief Mk 4/5`, `Bravestar Mk 3` = Des[2018/2034/2050], node-2 counters 126/127/128 continuing the fleets' 123/124/125 -- one counter across object kinds, lane BT §4.3), and list 1 is declined for want of a design-registration path. AND: **the one list `command_replay.cpp` CAN apply -- list 5, the planetary sliders -- fired ZERO times on all 15 elements**, because the shim's memory window cannot type 7 of its 8 fields; the settling experiment is one UI run (two different sliders to two different values), not engine work. TOOL CHANGE: `tools/aiorders_to_tcb.py` now reads the deep dump's `aivec`/`aistr` rows, consulting them ONLY at a word the existing field map already types as a vector, cross-checking the begin/end length against the followed count, and recording anything it cannot place as a `#` comment plus a stderr line (12 list-12 vectors, 15 list-23 vectors, 3 list-1 design names, 1 list-3 vector) rather than inventing a field mapping. REGRESSION-CHECKED against every aiorders log in the repo: 8 shallow logs byte-identical, and **the 4 lane-CB logs were deep dumps whose payloads this converter had been silently discarding** -- the only field changes are list 8's route and list 10's ship vector going `v1` -> `v1:272`/`v1:1728`, which is EXACTLY what `tools/tcb_from_json.py` already emits for the same capture, so the two converters now agree everywhere but list 23 (where the committed `cb-turn2to3.tcb` predates the current `tcb_from_json.py`'s own `UNMAPPED_HEAD[23]`; no behavioural difference, recorded not touched). The canonical pair's replay is byte-identical under the committed tcb and a regenerated one. Artefacts: `findings/subsystems/rung-b-rich-turn.md`, `verify/results/turncommands/br2-turn27-{deep,shallow}.tcb`, `verify/results/standalone/cv/*` |
Edits to existing rows:
backlog.md§2, the Rung B bullet — append:2026-09-09, lane CV, measured. The replay RUNS on the first rich-turn record and consumes it whole (85 commands / 81 elements / 70 bumps), and the result is a worklist, not a match: 1092 leaves against724528ff...from a 1166-leaf do-nothing baseline, 80 closed / 6 regressed, and the stream's entire effect on the state is/Sim/ModCount(1430 -> 1500, target 1502, residual 2). Two blockers sit UPSTREAM of the turn and must be cleared before any byte-match claim on this save family: the typed writer drops oneuspitem inGame::SpecialProjectNameGen(12 saves of 43), and--relabel-new-idsrefuses because the engine mints no client fleet ids. **And the tail'stscrgate is TRUE here (253, not 252) -- the correction above is load-bearing on exactly this workload.**backlog.md§3 item 8 (Rung B) — append:The--turn-commandspath is built and works; what it lacks is appliers. 26 of 27 lists decline or are incomplete, and the one that has an applier (list 5) cannot fire because the memory capture cannot type its fields.backlog.md§3 item 1b (S11civilian growth) — append: ``Pv*is not a previous-turn snapshot: onad-turn27it holds the uncapped growth projection (current x 1.03) forPvPopandPvInfra, whilePvSuit/PvResdo hold the pre-turn current. See rung-b-rich-turn.md §5.2.backlog.md§4 / new engine item —T34 RecordObservedDesignsis the largest independently closable stub measured against a rich turn: 79 leaves, no upstream dependency.- A new
mars/streamrow — theGame::SpecialProjectNameGenelement is ausp/uscPAIR, not a singleusc; the codec's "usnc is 0 in every save available" comment is falsified byad-turn27-two-raiders.sav. 12 of 43 corpus saves round-trip 12 bytes short. Fixing it must re-prove byte-neutrality over the whole corpus. verify/results/saves/certified-pairs.md— no new row. This lane certifies nothing; it measures an engine against an already-recorded pair.- Row 62 (guest ledger) — VM140/141/145/146 unchanged by this lane; it took no guest.