diff --git a/findings/subsystems/rung-b-rich-turn.md b/findings/subsystems/rung-b-rich-turn.md index d49905e..74edc88 100644 --- a/findings/subsystems/rung-b-rich-turn.md +++ b/findings/subsystems/rung-b-rich-turn.md @@ -113,7 +113,31 @@ 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 +### 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 @@ -203,9 +227,20 @@ 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. +**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 @@ -450,7 +485,10 @@ capture has now made testable, and it is a precondition for `NumDes 46 → 49` a 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. +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`. @@ -503,7 +541,7 @@ Artefacts: `verify/results/turncommands/br2-turn27-{deep,shallow}.tcb`, 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/*` | +| 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: