Replays lane BR's deep command block for ad-turn27-two-raiders.sav through sots_turn --turn-commands and compares with bp-pinB-turn28.sav (724528ff). Verdict: outcome 3. DIVERGED: 1092 leaf difference(s) against a 1166-leaf do-nothing baseline; 80 closed, 6 regressed; the stream's whole contribution to the state is /Sim/ModCount (1430 -> 1500, target 1502, residual 2). Two blockers upstream of the turn: the typed writer drops one usp item in Game::SpecialProjectNameGen (12 of 43 corpus saves, exactly 12 bytes each), and --relabel-new-ids refuses by guard G3 because the engine mints no client fleet ids. The tail's tscr gate is TRUE on this save (253, not 252). tools/aiorders_to_tcb.py now reads the deep dump's aivec/aistr rows, only at a word the field map already types as a vector, with the followed count cross-checked against the begin/end pair. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARBgSooAfokKUy6wKUKEyZ
534 lines
36 KiB
Markdown
534 lines
36 KiB
Markdown
# 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 <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 `.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 The runs
|
||
|
||
```sh
|
||
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, unexplained.** No `bumpsExact = false` warning fired, so no gate with an unknown cost
|
||
was set. This is the first measurement of the counter under replay on a rich turn (lane CB's
|
||
canonical-pair prediction was "24 exactly, zero residual"), and 2 is the number to hunt.
|
||
|
||
### 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**, only measured and bounded.
|
||
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 <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, unexplained**). 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. 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.
|