lane E2: T36 measured with the ship census wired in -- 29 closed / 7 regressed, and the two things it is still blocked on

standalone_report.py regenerated from sots-engine wip/turnrecord. The reference pair does
not move by design: T36 does not commit by default, so 209 -> 204, closed 5 / regressed 0
and 108 -> 103, closed 5 / regressed 0 are unchanged. What moved in section 6 is the phase
tally, 14/44 modelled and 8 committed -> 15/44 and 9, from the A2 and D2 merges. verified
stays 0 on both tables.

t36-turnrecord.md carries the measurement the default run cannot show: with
--commit-blocked=T36 and a data root, the reference pair closes 29 leaves and regresses 7
(was 9). Two of the three census leaves close; the third is short by exactly one destroyer
on both pairs, because no phase we run builds a ship. The census model itself reproduces
480/480 archived census leaves over 11 saves with 0 mismatches.

T36 stays blocked: sav and inc on lane E1's budget, and one ship count on unmodelled
construction. A field-granular commit knob is not implementable -- the archived record is
one struct on the wire and has no hole to leave.

Coverage: 32 of 480 census leaves are nonzero anywhere in the corpus; cls1 and cls2's
platform count are unexercised, not verified.
This commit is contained in:
alex 2026-09-08 13:25:31 -04:00
parent 3a44f9178b
commit fbe3c7bd86
3 changed files with 186 additions and 42 deletions

View file

@ -1,8 +1,8 @@
# standalone vs the oracle
generated 2026-09-08T15:35:00Z binary /home/alex/sots-engine-wt-yield/build-host/src/app/sots_turn
generated 2026-09-08T17:23:54Z binary /home/alex/sots-engine-wt-turnrecord/build-host/src/app/sots_turn
phases: 14/44 of the two turn drivers modelled, 8 committed (implemented 2, partial 6, blocked 6, stub 30)
phases: 15/44 of the two turn drivers modelled, 9 committed (implemented 3, partial 6, blocked 6, stub 29)
3/37 of the post-combat tail modelled
## turn1-state.sav -> turn2-state.sav (real End Turn)

View file

@ -1,7 +1,7 @@
{
"schema": "sots-standalone-status/1",
"generated": "2026-09-08T15:35:00Z",
"binary": "/home/alex/sots-engine-wt-yield/build-host/src/app/sots_turn",
"generated": "2026-09-08T17:23:54Z",
"binary": "/home/alex/sots-engine-wt-turnrecord/build-host/src/app/sots_turn",
"reference": {
"input": "turn1-state.sav",
"oracle": "turn2-state.sav",
@ -30,12 +30,12 @@
"phases": {
"total": 44,
"verified": 0,
"implemented": 2,
"implemented": 3,
"partial": 6,
"blocked": 6,
"stub": 30,
"modelled": 14,
"committed": 8
"stub": 29,
"modelled": 15,
"committed": 9
},
"tailPhases": {
"total": 37,
@ -62,16 +62,17 @@
" 591376 inflated bytes, 0 error(s), 0 warning(s)",
" turn 1, frame 1, modCount 0, 8 player(s), 28 system(s), 6 fleet(s)",
"roundtrip (untouched): byte-identical (591376 bytes)",
"data: /tmp/claude-1000/-home-alex/ec8e34f8-af37-4ef2-a309-ed6a15293097/scratchpad/data -- 885 section(s) over 7 race(s), 46 load problem(s)",
"",
"phases",
" turn drivers (the milestone's denominator): 14 of 44 modelled, 8 committed",
" verified 0 implemented 2 partial 6 blocked 6 stub 30",
" turn drivers (the milestone's denominator): 15 of 44 modelled, 9 committed",
" verified 0 implemented 3 partial 6 blocked 6 stub 29",
" post-combat tail (written to the autosave, tracked separately): 3 of 37 modelled",
" verified 0 implemented 1 partial 0 blocked 2 stub 34",
"",
"this run",
" leaves written 7",
" leaves NOT written by a blocked phase 64",
" leaves NOT written by a blocked phase 120",
" generator words consumed 16 (state loaded, left untouched)",
" generator words NOT accounted (never netted off the above):",
" - encounter detection draws one unit value and one bounded integer per turn on every turn measured (2 words), with no derived rule behind the count -- its bound is the product of the contact and detector counts, so it is left unmodelled",
@ -80,8 +81,8 @@
" ! the generator advanced during this run but the save keeps its original state (--commit-rng to write it)",
" ! the modelled words are a LOWER BOUND on the turn's cost, so a committed generator is short by the unaccounted sites below and its drawn VALUES are not the game's",
"",
"wrote /tmp/tmp_cz5dsbf/post-turn1-state.sav (65145 bytes gzipped, 591376 inflated)",
"metric -> /tmp/tmp_cz5dsbf/metric-turn1-state.sav.json"
"wrote /tmp/tmpbodkynxg/post-turn1-state.sav (65145 bytes gzipped, 591376 inflated)",
"metric -> /tmp/tmpbodkynxg/metric-turn1-state.sav.json"
],
"coverage": {
"input": true,
@ -89,9 +90,9 @@
"ours": true
},
"roots": {
"input": "ce36f6e5a2e41ae1a90d5b7fefda8f8f",
"oracle": "b0e2448b83ad1da254d526f4f553ff8d",
"ours": "8836ea25910897ec7a9b7d69fcaf6c87"
"input": "64b836b966299d9b72fc02244ed31e31",
"oracle": "4bed514f853332b9fa35b388c3aa5558",
"ours": "1acc372b1d410b966ab544dffbe43cb5"
},
"baselineDiverging": 209,
"divergingAfterTurn": 204,
@ -151,16 +152,16 @@
"standalone": {
"schema": "sots-standalone-metric/1",
"input": "/home/alex/sots-re/verify/results/saves/turn1-state.sav",
"output": "/tmp/tmp_cz5dsbf/post-turn1-state.sav",
"output": "/tmp/tmpbodkynxg/post-turn1-state.sav",
"spine": {
"total": 44,
"verified": 0,
"implemented": 2,
"implemented": 3,
"partial": 6,
"blocked": 6,
"stub": 30,
"modelled": 14,
"committed": 8
"stub": 29,
"modelled": 15,
"committed": 9
},
"tail": {
"total": 37,
@ -174,7 +175,7 @@
},
"run": {
"leafWrites": 7,
"blockedLeafWrites": 64,
"blockedLeafWrites": 120,
"rngWords": 16,
"rngLoaded": true,
"rngCommitted": false,
@ -239,8 +240,8 @@
"driver": "StrategyServer::ProcessTurn",
"id": "S04",
"name": "RebuildAllianceMasks",
"status": "stub",
"ran": 0,
"status": "implemented",
"ran": 8,
"writes": 0,
"blockedWrites": 0,
"rng": 0
@ -1002,7 +1003,7 @@
"status": "blocked",
"ran": 8,
"writes": 0,
"blockedWrites": 48,
"blockedWrites": 104,
"rng": 0
},
{
@ -1028,16 +1029,17 @@
" 603360 inflated bytes, 0 error(s), 0 warning(s)",
" turn 2, frame 2, modCount 12, 8 player(s), 28 system(s), 7 fleet(s)",
"roundtrip (untouched): byte-identical (603360 bytes)",
"data: /tmp/claude-1000/-home-alex/ec8e34f8-af37-4ef2-a309-ed6a15293097/scratchpad/data -- 885 section(s) over 7 race(s), 46 load problem(s)",
"",
"phases",
" turn drivers (the milestone's denominator): 14 of 44 modelled, 8 committed",
" verified 0 implemented 2 partial 6 blocked 6 stub 30",
" turn drivers (the milestone's denominator): 15 of 44 modelled, 9 committed",
" verified 0 implemented 3 partial 6 blocked 6 stub 29",
" post-combat tail (written to the autosave, tracked separately): 3 of 37 modelled",
" verified 0 implemented 1 partial 0 blocked 2 stub 34",
"",
"this run",
" leaves written 7",
" leaves NOT written by a blocked phase 58",
" leaves NOT written by a blocked phase 114",
" generator words consumed 16 (state loaded, left untouched)",
" generator words NOT accounted (never netted off the above):",
" - encounter detection draws one unit value and one bounded integer per turn on every turn measured (2 words), with no derived rule behind the count -- its bound is the product of the contact and detector counts, so it is left unmodelled",
@ -1046,8 +1048,8 @@
" ! the generator advanced during this run but the save keeps its original state (--commit-rng to write it)",
" ! the modelled words are a LOWER BOUND on the turn's cost, so a committed generator is short by the unaccounted sites below and its drawn VALUES are not the game's",
"",
"wrote /tmp/tmp_cz5dsbf/post-turn2-state.sav (66883 bytes gzipped, 603360 inflated)",
"metric -> /tmp/tmp_cz5dsbf/metric-turn2-state.sav.json"
"wrote /tmp/tmpbodkynxg/post-turn2-state.sav (66883 bytes gzipped, 603360 inflated)",
"metric -> /tmp/tmpbodkynxg/metric-turn2-state.sav.json"
],
"coverage": {
"input": true,
@ -1055,9 +1057,9 @@
"ours": true
},
"roots": {
"input": "b0e2448b83ad1da254d526f4f553ff8d",
"oracle": "7ea50b486c2e9f9f1a8ec5b9646165a8",
"ours": "6523aa25786814380a26f48c046da22b"
"input": "4bed514f853332b9fa35b388c3aa5558",
"oracle": "e9c161e311f8ef8fad6f1aa1903dcf3f",
"ours": "22071ae1303f308a93b805e940b626b5"
},
"baselineDiverging": 108,
"divergingAfterTurn": 103,
@ -1117,16 +1119,16 @@
"standalone": {
"schema": "sots-standalone-metric/1",
"input": "/home/alex/sots-re/verify/results/saves/turn2-state.sav",
"output": "/tmp/tmp_cz5dsbf/post-turn2-state.sav",
"output": "/tmp/tmpbodkynxg/post-turn2-state.sav",
"spine": {
"total": 44,
"verified": 0,
"implemented": 2,
"implemented": 3,
"partial": 6,
"blocked": 6,
"stub": 30,
"modelled": 14,
"committed": 8
"stub": 29,
"modelled": 15,
"committed": 9
},
"tail": {
"total": 37,
@ -1140,7 +1142,7 @@
},
"run": {
"leafWrites": 7,
"blockedLeafWrites": 58,
"blockedLeafWrites": 114,
"rngWords": 16,
"rngLoaded": true,
"rngCommitted": false,
@ -1205,8 +1207,8 @@
"driver": "StrategyServer::ProcessTurn",
"id": "S04",
"name": "RebuildAllianceMasks",
"status": "stub",
"ran": 0,
"status": "implemented",
"ran": 8,
"writes": 0,
"blockedWrites": 0,
"rng": 0
@ -1968,7 +1970,7 @@
"status": "blocked",
"ran": 8,
"writes": 0,
"blockedWrites": 48,
"blockedWrites": 104,
"rng": 0
},
{

View file

@ -0,0 +1,142 @@
# T36 `FinalizeTurnRecords` — the ship census wired in, and what T36 is still blocked on
Lane E2, 2026-09-08. Engine branch `wip/turnrecord` off `sots-engine` main `0592104`.
Measured with `verify/state-checksum/state_checksum.py` leaves, the same currency as
`report.txt`. **`closed` and `regressed` are reported separately on every line; nothing here
is netted.**
`status.json` and `report.txt` in this directory were regenerated from the same binary.
## What changed in the engine
`ShipCensus` (lane D2) is wired into `BuildTurnRecord`. The classification walks every
player's designs — normal and legacy, one id space — resolves each design's sections against
the section catalog, and walks the global fleet list keyed by `Flt.PID` against each player's
**object id**, not its vector position. The record's modelled field count goes 7 → 13.
Two things the standalone did not have before:
* **a data root.** The census cannot come from a save: a design's hull size and its
defence-platform flag are recomputed from the section catalog whenever the design changes
and are never written down. `sots_turn --data DIR` (or `$SOTS_DATA_DIR`) supplies it. No
game data enters the repo, and without a root the six counters are reported unmodelled
rather than written as six zeros.
* **`--commit-blocked=IDS` / `--commit-blocked-except=IDS`.** `--commit-blocked` alone is
all-or-nothing across every blocked phase, which reports one closed count and one regressed
count for all of them together — the netting the campaign does not do. The two lists narrow
it to named phase ids, so a phase's own contribution can be measured. Every number below
that names T36 was measured with `--commit-blocked=T36`.
## Prediction, written before the code (rule 2)
> Default mode unchanged at 209 → 204, closed 5 / regressed 0 and 108 → 103, closed 5 /
> regressed 0. With `--commit-blocked`, the three census leaves close and the regressed list
> falls from 9 to 6 on pair 1 (`inc`×3 + `sav`×3, the budget) and from 11 to 8 on pair 2.
Falsification hypothesis 1 was: *the census is built from the fleet list as it stands, which
is pre-construction; if a real turn's census differs from the input turn's, the leaf does not
close and shows a third value.*
**That is what happened.** Two of the three closed; the third did not, and the symptom was
exactly the one written down.
## Measured — reference pair `turn1-state.sav` → `turn2-state.sav`
| run | closed | regressed |
|---|---:|---:|
| default (no commit flags) — before and after this lane | 5 | 0 |
| `--commit-blocked`, before this lane | 29 | 9 |
| `--commit-blocked`, after | 29 | **7** |
| `--commit-blocked=T36` alone, with a data root | 29 | 7 |
| `--commit-blocked=T36` alone, no data root | 29 | 9 |
Regressed after, named: `hist[0]/stats[1]/{sav,inc}`, `hist[1]/stats[1]/{sav,inc}`,
`hist[7]/stats[1]/{sav,inc}` — the budget, lane E1 — and `hist[1]/stats[1]/shpt[0]`, ours 0,
archived 1.
Closed by the census: `hist[4]/stats[1]/shpt[2]` (5 dreadnoughts) and
`hist[7]/stats[1]/satt[0]` (10 defence platforms).
## Measured — second pair `turn2-state.sav` → `turn3-state.sav`
| run | closed | regressed |
|---|---:|---:|
| default | 5 | 0 |
| `--commit-blocked`, before this lane | 13 | 11 |
| `--commit-blocked`, after | 13 | **9** |
| `--commit-blocked=T36` alone, with a data root | 13 | 7 |
| `--commit-blocked=T36` alone, no data root | 13 | 9 |
The two extra regressions under a full `--commit-blocked` are `Player/Status` on two players.
They are S31's, not T36's, and the new id filter separates them cleanly — which is the whole
reason the filter exists.
## The one census leaf that did not close is not a census error
`app_test_turn_record`, extended this lane, now compares the six counters against the record
the game itself archived, on every save: **11 saves, 80 player-records, 1040 fields, 480 of
them census leaves, 0 mismatches.** That is the same 480/480 lane D2 got, reproduced through
the app's own code path (which visits slots in the original's in-memory order rather than the
wire's) rather than through D2's test.
So the model reproduces the archived census exactly for the turn the save was written at. The
remaining leaf is `shpt[0]` for one player, and the archived value is higher than ours by
exactly one destroyer on **both** pairs (0 vs 1, then 1 vs 2). That player's build queue
completes one ship per turn, and no phase the standalone runs creates a ship — S11's
build-queue sub-pass is the declared input boundary. The leaf is short by the turn's
construction, not wrong about classification.
**This is a new named blocker, and it is not lane E1's and not mine.**
## T36's status: still `Blocked`, on two named things, neither of them in this phase
1. `sav` and `inc` for the new turn come from P01/P02, blocked on the per-system money
output — **lane E1**.
2. `shpt[0]` is short by the ships the turn builds — **ship construction, unmodelled**.
There is **no field-granular knob that could rescue this**, and the brief's
`--commit-blocked-except=<fields>` idea is not implementable as stated: the archived record
is one struct on the wire, so `sav` and `inc` cannot be left out while the other eleven fields
are written. The format has no hole. Committing is all-or-nothing at the record, so the
choice is 29 closed with 7 confidently-wrong leaves, or neither — and 7 wrong leaves is not
worth 29 that a later lane closes for free. What *is* implementable, and is implemented, is
the same convention at **phase** granularity, which is what produced the isolated rows above.
T36 flips the moment E1 lands, minus one leaf that waits on construction.
## Coverage, stated as loudly as the verdict (rules 6 and 15)
Only **32 of the 480** archived census leaves are nonzero anywhere in the 11-save corpus.
Per leaf (cls0 shpt/satt, cls1 shpt/satt, cls2 shpt/satt) = **18/3, 0/0, 11/0**.
* **cls1 (cruisers), ships and platforms both, and `satt` for cls2, have never been observed
nonzero.** Three of the six counters are unexercised hypotheses, not verified. A zero leaf
agrees for free. The corpus needs a cruiser and a dreadnought-hull platform
(roadmap §5 already lists both).
* The other four words of each class group — `shpl`, `shpk`, `satl`, `satk`, losses and kills
— are zero throughout the corpus, and this phase writes zeros with no model behind them.
Listed in the phase's own unmodelled list rather than passed off as agreement.
* Hull size is an **assignment** in slot order, so the visit order is load-bearing in
principle. The original's stats pass walks mission, command, engine; the wire writes
command, mission, engine. This code reproduces the memory order. **Design rule A6 makes
every shipped design class-homogeneous, so no save in the corpus can tell the two orders
apart** — the order is carried from the instruction stream, not confirmed by these bytes.
* `verified` stays **0**. Nothing here was compared against a running game; this lane holds no
VM. The comparison is against bytes the original wrote, which is the strongest check
available without one, and it is not the same thing.
## Gates
Run as separate commands, never chained (rule 13):
* `tools/clean_room_check.sh` — OK
* host `ctest`, no `SOTS_SAVES_DIR` — 45/45
* host `ctest`, `SOTS_SAVES_DIR` set — 45/45
* host `ctest`, `SOTS_SAVES_DIR` + `SOTS_DATA_DIR` set — 44/45. The one failure is
`game_data_realdata`, and it **fails identically on unmodified `main`** with the same data
root: that test pins section and bank counts against the campaign's canonical extraction and
my extraction of `sots.gob` is a different data version (9 pinned counts differ, e.g. a
`human/DEWAR` section the pin says should be absent). It is a data-root mismatch, not a
regression, and the census matched all 480 archived leaves against that same root anyway.
* **Shim cross-build NOT run**: no `i686-w64-mingw32-g++` on this host. No `src/shim` file was
touched, but per rule 13 the integrator should run the CT111 shim build before pushing.