# 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 save `bp-pinB-turn28.sav` (`724528ff…`, byte-identical to BR's run 2, verified by BR with `cmp`), lane BT's `state_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. **No `sots-engine` code 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 none` reports **`DIVERGED: 1092 leaf difference(s)`** against > `bp-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 /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 `.tcb` as a `#` comment and reported on stderr — a real observation that this tool cannot place. - The begin/end pointer pair and the followed `count` are **cross-checked**; a disagreement keeps the length-only form rather than guessing. - `aistr` rows (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 flip `CapturedElement::complete` on a payload we do not have. That this change reached the replayer is **measured**, not assumed (§3.2). ```sh 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 ```sh T=/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: ```cpp 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 `ntdev` leaves**, `Sys[160/400/416/432/448]`: the engine writes **1** where both the input and the target hold **0**. On the same run `ntdev` was *closed* on thirteen other systems (`26 → 27`). So `S11`'s `ntdev` rule 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 1. **Nothing about a turn other than this one**, a route other than **load**, a build other than `e7e2bd6`, or a target other than `724528ff…`. 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. 2. **No engine phase was implemented and no engine code was changed.** The `usp` codec defect (§4.1) is named, not fixed. 3. **The `ModCount` residual 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. 4. **The morale ring's three event rules are named from their `mdsc` strings and their `mtp` codes, not decoded from the binary.** `mtp` 12 / 16 / 20 are three of the codes; the full table is not read, and `src/game/sim/colony.h` names only the addiction codes `0x1b`–`0x1d`. 5. **The list-5 slider permutation was not resolved** (§3.3). The experiment that settles it is a UI run, not analysis. 6. **`/Summary/Checksum` remains unmodelled** — masked elsewhere, reported here, not solved. 7. **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 ```sh 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 \ --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 against `724528ff...` 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 one `usp` item in `Game::SpecialProjectNameGen` (12 saves of 43), and `--relabel-new-ids` refuses because the engine mints no client fleet ids. **And the tail's `tscr` gate 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-commands` path 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 (`S11` civilian growth)** — append: ``Pv*` is not a previous-turn snapshot: on `ad-turn27` it holds the uncapped growth projection (current x 1.03) for `PvPop` and `PvInfra`, while `PvSuit`/`PvRes` do hold the pre-turn current. See rung-b-rich-turn.md §5.2.` - **`backlog.md` §4 / new engine item** — `T34 RecordObservedDesigns` is the largest independently closable stub measured against a rich turn: **79 leaves**, no upstream dependency. - **A new `mars/stream` row** — the `Game::SpecialProjectNameGen` element is a `usp`/`usc` PAIR, not a single `usc`; the codec's "usnc is 0 in every save available" comment is falsified by `ad-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.