diff --git a/campaign/DASHBOARD.md b/campaign/DASHBOARD.md index c6ea288..d5a8602 100644 --- a/campaign/DASHBOARD.md +++ b/campaign/DASHBOARD.md @@ -1,16 +1,16 @@ # SotS RE campaign — coverage dashboard -Generated 2026-09-09 03:02 UTC · `sots-re` @ 6d33a74,2026-09-08 · `sots-engine` @ 989c692,2026-09-08 (240 commits) · regenerate with `tools/dashboard.py` +Generated 2026-09-09 04:14 UTC · `sots-re` @ aef3d48,2026-09-09 · `sots-engine` @ 989c692,2026-09-08 (240 commits) · regenerate with `tools/dashboard.py` > **North star:** A functional reimplementation of the engine — behavior-equivalent, NOT byte-for-byte ## 1. Map coverage (campaign/board.md) -410 targets · mapped-or-better **363/410** `[█████████░] 89%` · verified **321/410** `[████████░░] 78%` +414 targets · mapped-or-better **367/414** `[█████████░] 89%` · verified **325/414** `[████████░░] 79%` | Status | Count | % | |---|---:|---:| -| verified | 321 | 78% | +| verified | 325 | 79% | | mapped | 42 | 10% | | in-progress | 4 | 1% | | backlog | 41 | 10% | @@ -22,23 +22,23 @@ Generated 2026-09-09 03:02 UTC · `sots-re` @ 6d33a74,2026-09-08 · `sots-engine | control-flow | 29 | 2 | 0 | 0 | 0 | 31 | | subsystems | 4 | 8 | 0 | 2 | 1 | 15 | | engine | 30 | 0 | 0 | 0 | 0 | 30 | -| verify | 101 | 15 | 3 | 35 | 0 | 154 | +| verify | 102 | 15 | 3 | 35 | 0 | 155 | | phase2 | 13 | 3 | 1 | 0 | 0 | 17 | -| meta | 79 | 6 | 0 | 1 | 0 | 86 | +| meta | 82 | 6 | 0 | 1 | 0 | 89 | | other | 19 | 2 | 0 | 0 | 0 | 21 | ## 2. Binary understanding - RTTI type descriptors: **1,924** (`Game::` 1,404, `Mars::` 194; serializable types 179) - Classes with recovered member layouts: **384** / 1,598 named classes `[██░░░░░░░░] 24%` — `objects/layouts.json` (serializer recovery) plus classes recovered by hand in `struct-recovery.md` + `schema-gaps-resolved.md`. Note 179 types are *serializable*; the recovery also reaches non-serializable ones, so this is not a subset of that -- Functions: **41,411** (parsed from `01-fingerprint.md`); named/annotated in the **address contract** (`ghidra/addresses.json`, not Ghidra's full rename count): **1276**, verified **1145** `[█████████░] 90%` +- Functions: **41,411** (parsed from `01-fingerprint.md`); named/annotated in the **address contract** (`ghidra/addresses.json`, not Ghidra's full rename count): **1283**, verified **1152** `[█████████░] 90%` ## 3. Data layer - Catalogs: **1,595/1,595** files parsed (91 block kinds in `schema_stats.json`), dangling cross-refs **0** (`crosslink.json`) - Oracle `mars-parse`: **1,531/1,531** files agree `[██████████] 100%` - Oracle `mars-text`: **64/64** files agree `[██████████] 100%` -- Saves: **25/25** real saves strict-clean — strict exit 0, 0 errors, 0 warnings +- Saves: **29/29** real saves strict-clean — strict exit 0, 0 errors, 0 warnings - Design rules: **127/127** stock designs pass `[██████████] 100%` - Value domains: **490/724** typed fields have been seen to vary `[███████░░░] 68%` — the other **234** have only ever held one value across the corpus, so their typing is untested (`value-domain-census.md`) @@ -112,7 +112,7 @@ Detail: `verify/results/standalone/report.txt`. ## 7. Verification ledger -- ✅ Saves strict: 25/25 (strict exit 0, 0 errors, 0 warnings) +- ✅ Saves strict: 29/29 (strict exit 0, 0 errors, 0 warnings) - ✅ Design rules: 127/127 - ✅ oracle mars-parse 1531/1531 · ✅ oracle mars-text 64/64 - ✅ Compare harness present (`verify/harness/compare/`) @@ -133,11 +133,11 @@ Most recent open: ## 9. Delta since previous dashboard -- verified targets: 317 → 321 (+4) · mapped-or-better: 359 → 363 (+4) +- verified targets: 321 → 325 (+4) · mapped-or-better: 363 → 367 (+4) - engine LOC: 58,647 → 58,647 (+0) · test files: 122 → 122 (+0) · checks: 4,257 → 4,257 (+0) -- addresses verified: 1,139 → 1,145 (+6) · recovered layouts: 384 → 384 (+0) · open questions: 26 → 26 (+0) +- addresses verified: 1,145 → 1,152 (+7) · recovered layouts: 384 → 384 (+0) · open questions: 26 → 26 (+0) - standalone leaves closed: 45 → 45 (+0) · leaves still diverging: 63 → 63 (+0) --- warnings: board.md: unknown types subsystems; mars-rng.md: no oracle total row parsed; mars-stream.md: no oracle total row parsed; mars-vfs.md: no oracle total row parsed - + diff --git a/campaign/board.md b/campaign/board.md index c0e736b..0ad4508 100644 --- a/campaign/board.md +++ b/campaign/board.md @@ -44,7 +44,7 @@ Status flow: `backlog → in-progress → mapped → verified` (or `blocked`). | engine: mars/text | engine | verified | high | 100% | 2026-09-07 | flat-kv, id-manifest, csv; oracle 64/64 (Strings.csv 5722 rows); ctest green | | engine: game/sim formulas | engine | verified | high | 100% | 2026-09-08 | sim-pin merged: all 5 low-confidence formulas pinned (3.3 on protection limit, old-state bankruptcy decisions, hazard curve, money tail, pop bonus); tests 356->466; + game/effects (196 TechIds, 44 typed effects, 254 checks) | | engine: mars/stream + rng | engine | verified | high | 100% | 2026-09-07 | merging: 100% exact dump agreement on 3 saves; typed shapes round-trip byte-identical whole file; RNG = seed(RSeed)+2 twists confirmed; 4 tag-name fixes for SAVE_FORMAT | -| wire-schema channel (layouts -> engine) | engine | verified | high | 97% | 2026-09-08 | **Lane G.** Lane D's serializer recovery now reaches `sots-engine` as a GENERATED WIRE SCHEMA, not struct layouts: `tools/streams.py` -> `objects/streams.json` (386 classes / 2,042 items, disk order preserved, duplicate tags preserved, **every memory fact dropped** - no off/size/sizeof/gaps/strides) -> `tools/gen_stream_schema.py` -> `include/generated/sots_stream_schema.h`. WHY layouts.json was the wrong input: `build()` sorts fields by `off_abs` (**89 of 386 classes** have offset order != write order) and merges duplicate offsets into `alt_tags` (that IS the JewelsOfTheCrown double-tag trap). The table is a SPEC, not a program - the recovery is a linear pass so it cannot see `Write`'s branches and lists conditional fields (StarShip `BQ2`/`hbq`) unconditionally, and flattens container loops; a codec driven off it desynchronises. So hand-written `io()` stays the codec and a new `SchemaProbe` archive + `test_wire_schema.cpp` CHECK it: **56 shapes bound, 657 items matched, 0 mismatches**. FOUND 4 REAL DEFECTS present in BOTH `save_reader.py` and the engine, invisible to any round-trip test: `SystemParams` field 1 is a **string** not an int (empty string == 4 zero bytes == int 0, so it round-tripped by luck; a named planet would have desynced both readers); `ObservedTech/ObservedWeapon.odet` is a **bool** not an int (byte-safe only because a 4-char tag makes bool and int items both 12 bytes); `SpeciesRatios.nv` and `ShipRecords.srbd` are **counts**, not fields. TWO TRAPS RESOLVED: `ServerTradeManager`'s no-op Read/Write is real but the call is **virtual** - `ServerTradeManagerImpl` has the real serializer (`NumTradeSectors`/`TradeID`/`Trade`/`SctSize`), same shape resolves `IServerSpyManager` -> `ServerSpyManager`; general rule: **when an interface's serializer is the inherited stub, look for the concrete `*Impl`**. `ShipDesign::Write` really does make no stream call, but `ShipDesignDef::Section` is recovered so most of `Des` is typeable anyway. COVERAGE, measured by a new `CoverageArchive` that separates items a field NAMES from items a `Node` merely CARRIES (a byte-identical round trip is not a coverage claim): **37.9% -> 97.1%** typed on turn1 (97.2/97.2/97.6 on the others), round trip still byte-identical on all 4 saves. Typed this round: `TechTree` (both NumTechs sections - the 2nd is per-tech state, field list from `SpyReportTechTree` which streams the same record), `Events` (3 nesting levels), `ShipRecs`, `sprjs`, `civr`, `comms`, `spy2`, `spymgr`, `aid`, `Ojvs`, `AIEnf`, `FNG`, `trdmgr`, `Des` sections + gun banks. STILL OPAQUE: `CD` custom data 744 items (TurnCommands_v5 is a no-orders snapshot - **needs a save with issued turn commands**), `SvSctOb` 147 (8 EncObj variants), `DOpts` 94 (`read_elem` has no std::string branch), `spies2` 56, `RNG` 2 (correctly opaque). Ratchet at 95% so typing can't regress. ctest **34/34**, clean-room OK, `test_save` skips cleanly unset. `findings/objects/wire-schema-channel.md`; sots-engine branch `wip/layouts` | **LANE O 2026-09-08.** **`CD` `TurnCommands_v5` is no longer sample-starved.** Saves taken AFTER issuing orders and BEFORE End Turn grow the block from the universal empty snapshot (`CD[0]` digest `3df7d93164fb1d7d`, 35 leaves / 122 B in turn1/2/3, zuul-turn5 AND human-turn3) to 41 / 61 / 129 leaves. Field-by-field layout in `verify/save-reader/SAVE_FORMAT.md` **section 11**: playerId, researchRate float, per-command-type present-flags, then counted lists of build orders (ordinal, designId, systemId, 0), system-rate commands (systemId + a named `SRs SRt SRsc SRtf SRi SRoh SRnr` frame - the only NAMED sub-frame in the block), colonize orders (shipId, 1) and fleet moves (fleetId, 1, destSystemId, 0). Ids cross-checked against the same file's `Sys`/`Flt` records and against the UI (384 = Gallandro, 432 = Octans, 688 = the fleet the UI showed as Alpha Fleet with Dest: Octans, 216383 = the exact Imperial Savings drop the Boost Research panel caused). The block is the PENDING client->server queue: `zuul-turn15-orders.sav` still has `ResRate 0.25 / ResTNm '' / Sav 2,902,722` on the ServerPlayer while the block already carries rate 0.97 and the boost. Saves `verify/results/saves/zuul-turn15-orders.sav`, `zuul-turn17-orders2.sav`, `human-turn2-orders.sav` +| wire-schema channel (layouts -> engine) | engine | verified | high | 97% | 2026-09-08 | **Lane G.** Lane D's serializer recovery now reaches `sots-engine` as a GENERATED WIRE SCHEMA, not struct layouts: `tools/streams.py` -> `objects/streams.json` (386 classes / 2,042 items, disk order preserved, duplicate tags preserved, **every memory fact dropped** - no off/size/sizeof/gaps/strides) -> `tools/gen_stream_schema.py` -> `include/generated/sots_stream_schema.h`. WHY layouts.json was the wrong input: `build()` sorts fields by `off_abs` (**89 of 386 classes** have offset order != write order) and merges duplicate offsets into `alt_tags` (that IS the JewelsOfTheCrown double-tag trap). The table is a SPEC, not a program - the recovery is a linear pass so it cannot see `Write`'s branches and lists conditional fields (StarShip `BQ2`/`hbq`) unconditionally, and flattens container loops; a codec driven off it desynchronises. So hand-written `io()` stays the codec and a new `SchemaProbe` archive + `test_wire_schema.cpp` CHECK it: **56 shapes bound, 657 items matched, 0 mismatches**. FOUND 4 REAL DEFECTS present in BOTH `save_reader.py` and the engine, invisible to any round-trip test: `SystemParams` field 1 is a **string** not an int (empty string == 4 zero bytes == int 0, so it round-tripped by luck; a named planet would have desynced both readers); `ObservedTech/ObservedWeapon.odet` is a **bool** not an int (byte-safe only because a 4-char tag makes bool and int items both 12 bytes); `SpeciesRatios.nv` and `ShipRecords.srbd` are **counts**, not fields. TWO TRAPS RESOLVED: `ServerTradeManager`'s no-op Read/Write is real but the call is **virtual** - `ServerTradeManagerImpl` has the real serializer (`NumTradeSectors`/`TradeID`/`Trade`/`SctSize`), same shape resolves `IServerSpyManager` -> `ServerSpyManager`; general rule: **when an interface's serializer is the inherited stub, look for the concrete `*Impl`**. `ShipDesign::Write` really does make no stream call, but `ShipDesignDef::Section` is recovered so most of `Des` is typeable anyway. COVERAGE, measured by a new `CoverageArchive` that separates items a field NAMES from items a `Node` merely CARRIES (a byte-identical round trip is not a coverage claim): **37.9% -> 97.1%** typed on turn1 (97.2/97.2/97.6 on the others), round trip still byte-identical on all 4 saves. Typed this round: `TechTree` (both NumTechs sections - the 2nd is per-tech state, field list from `SpyReportTechTree` which streams the same record), `Events` (3 nesting levels), `ShipRecs`, `sprjs`, `civr`, `comms`, `spy2`, `spymgr`, `aid`, `Ojvs`, `AIEnf`, `FNG`, `trdmgr`, `Des` sections + gun banks. STILL OPAQUE: `CD` custom data 744 items (TurnCommands_v5 is a no-orders snapshot - **needs a save with issued turn commands**), `SvSctOb` 147 (8 EncObj variants), `DOpts` 94 (`read_elem` has no std::string branch), `spies2` 56, `RNG` 2 (correctly opaque). Ratchet at 95% so typing can't regress. ctest **34/34**, clean-room OK, `test_save` skips cleanly unset. `findings/objects/wire-schema-channel.md`; sots-engine branch `wip/layouts` | **LANE O 2026-09-08.** **`CD` `TurnCommands_v5` is no longer sample-starved.** Saves taken AFTER issuing orders and BEFORE End Turn grow the block from the universal empty snapshot (`CD[0]` digest `3df7d93164fb1d7d`, 35 leaves / 122 B in turn1/2/3, zuul-turn5 AND human-turn3) to 41 / 61 / 129 leaves. Field-by-field layout in `verify/save-reader/SAVE_FORMAT.md` **section 11**: playerId, researchRate float, per-command-type present-flags, then counted lists of build orders (ordinal, designId, systemId, 0), system-rate commands (systemId + a named `SRs SRt SRsc SRtf SRi SRoh SRnr` frame - the only NAMED sub-frame in the block), colonize orders (shipId, 1) and fleet moves (fleetId, 1, destSystemId, 0). Ids cross-checked against the same file's `Sys`/`Flt` records and against the UI (384 = Gallandro, 432 = Octans, 688 = the fleet the UI showed as Alpha Fleet with Dest: Octans, 216383 = the exact Imperial Savings drop the Boost Research panel caused). The block is the PENDING client->server queue: `zuul-turn15-orders.sav` still has `ResRate 0.25 / ResTNm '' / Sav 2,902,722` on the ServerPlayer while the block already carries rate 0.97 and the boost. Saves `verify/results/saves/zuul-turn15-orders.sav`, `zuul-turn17-orders2.sav`, `human-turn2-orders.sav **CLOSED 2026-09-09 by lane AS**, from the writer and then live. `spies2` (`ServerSystem+0x1cc`) is the per-system vector of **DEPLOYED spy ids**: `ServerSystem::AddSpy 0x007514c0` sets `SpyCraft+0x10` (`deat`) to the system handle and `push_back`s `SpyCraft+0x4` (`sid`) into `+0x1cc` **in the same two instructions**; `RemoveSpy 0x0074f550` is the exact mirror. Measured non-empty for the first time in the corpus: `count=1, element=1`, in the deploy system only. `SysMem` and `mts` are untouched and their flags stay up. | | engine: game/data catalogs | engine | verified | high | 100% | 2026-09-07 | merged: WeaponDef/ShipSectionDef/TurretTable/IdRegistry/TechTree/StringTable + cross_check; oracle 229,042 values 0 diffs; crosslink set reproduced exactly; 34 malformed shipped tokens pinned | | engine: mars/vfs (gob) | engine | verified | high | 100% | 2026-09-07 | merged: ZIP reader + native override; 8352+2035 entries = unzip -l; all 10268 files CRC-clean; byte-equal spot checks; ctest 11/11 | | determinism oracle | verify | verified | high | 100% | 2026-09-07 | BYTE-IDENTICAL across 5 runs incl. cross-process: (Autosave).sav 978041ac…, (Autosave EndTurn).sav bb4fd9ac…; gzip MTIME=0; only loaded-post-turn re-save differs (Player.Status 4->0, Summary.Checksum). findings/subsystems/determinism-oracle.md RE-CONFIRMED 2026-09-08 on engine `cef889e` (lane M's movement fix included) by lane F: `bb4fd9ac…` / `978041ac…` unchanged, and reproduced a further 3x under forced control words that leave the arithmetic alone (0x027f/0x127f/0x137f). | @@ -59,7 +59,7 @@ Status flow: `backlog → in-progress → mapped → verified` (or `blocked`). | P2-B3 ProcessResearch (behavioral, RNG) | phase2 | mapped | high | 85% | 2026-09-08 | LIVE, PARTIAL PASS: 15 calls compared, 13 zero-divergence; RNG post-state matched 14/15 incl. every roll (validates MT19937 + draw mapping + odds together). 2 divergences are the declared SetResearched boundary. ORACLE FAILS by exactly one item across 40,300: an unposted EVENT_RESEARCH_OVERBUDGET - compare was blind because the event list was never a declared region. fpu_cw=0x127f => 53-bit double, x87 question SETTLED. No Zuul in the save: double roll still disassembly-only . RECAPTURED WITH GUARDS 2026-09-08 (lane R): the oracle gap is now a COMPARE DIVERGENCE - `side.events.after.v.next_id orig=4 ours=3` on call 0, its only divergent field, with node[144] progress 2879->5768 and flag 1->2 both reproduced and the single RNG draw identical. 15 calls over 5 turns: 3 diverged, **RNG 15/15** (better than the original 14/15 - no tech-effect draw in this session). Guards on the two completion calls map SetResearched: ConMod[0..2]/OutMod/PopMod, ResTNm, TechTree+0x20 order counter, and the undeclared otch vector | . **LANE V 2026-09-08 (build `eventlive-dd38117-20260908T0916Z`): the events divergence is CLOSED on the reference turn** - 3 calls, 3 compared, **0 diverged, exit 0**. Over 5 turns 15 calls / **2** diverged (was 3), both short by exactly 1 `next_id` = the unmodelled `EVENT_TECHS_UNLOCKED`. **CORRECTION to lane R's 'RNG 15/15': that was WORKLOAD LUCK, not a property.** On lane V's call 9 the completed tech had `research_roll_pending` set, so the original drew one word `ours` did not (`left` 374 vs 375, `next_index` 250 vs 249) - the `OnTechResearched` callback draw, a DECLARED out-of-scope boundary (P-events-wiring §3 says so in as many words). Honest statement: the `rng` region reproduces bit-for-bit on every call that does not complete a roll-triggering tech, and is short by exactly one draw on every call that does. Also live: `sizeof(ObservedTech)` = **44**, measured twice (`observed_techs.bytes` 440->484 and 484->528). Zuul double roll now CLOSED on a species-5 save lane V made - see its own row | RNG signatures (Ghidra) | meta | verified | high | 100% | 2026-09-08 | Seed/Twist/NextFloat/NextInt verified; draw = y/(2^32-1); NextInt [0,n] inclusive; lazy twist; left@+0x9c4. RUNTIME CONFIRMED: fpu_cw=0x127f (53-bit double, round-nearest) - our next_float model is right, float_from_pc24 is an unused contingency | | engine: game/effects | engine | verified | high | 100% | 2026-09-08 | merged: TechId enum (196 slots @10000+i), 44 ids with typed strategic effects, species flag bits, ApplyTechEffect; 254 checks | -| VM140 exclusivity (lab rule) | meta | verified | high | 100% | 2026-09-08 | one agent at a time. Holder: **VM140=FREE, oracle bytes verified intact), VM141=FREE (L2 released, 192.168.10.143, hooks=off), VM144=FREE (L3 released; workload saves left on disk, so Load-dialog row positions have moved on that guest), VM145=FREE, save set restored), VM146=FREE (CB released, stopped, restored)** (five parallel investigations 2026-09-08). VM140 remains the reference/oracle guest. Was: FREE (W3 released 2026-09-08, oracle bytes verified; **AND THE LAB IS NOW FIVE GUESTS** - VM141/144/145/146 are ZFS clones for probes and workloads, VM140 remains the exclusive reference; W2 released 2026-09-08, VM restored + oracle bytes verified; H released 2026-09-08, oracle reproduced + VM restored; N released 2026-09-08; Z released 2026-09-08; O-workloads released 2026-09-08 08:30 local, see the LANE O note at the end of this row; U-unlock released 2026-09-08 06:55 local; V-eventlive before it; F-fpucw before it, M-movefleet before that). QUEUE: empty. VM left at the MAIN MENU, `hooks=trace`, build `recap-7584bad-20260908T0615Z` restored from `C:\SOTS\shimdist-recap` (that dist also carries `shim.cfg.recap{trace,b3,b1,misc}`). Lane M also left `C:\SOTS\shimdist-mf` + `C:\SOTS\ui\mf{deploy,release}.ps1` in place - harmless, and a working template for the next lane. Windows Update DISABLED/paused on the VM. Non-holders build /srv/re-lab/build/sots-engine-, stage dist-, deploy C:\SOTS\shimdist-. GOTCHA (lane R): after `schtasks /Run /TN SOTS` the main menu can take >60 s - SCREENSHOT AND VERIFY before clicking, or the click path lands in Credits. GOTCHA (lane M): drive the load dialog ONE rui.ps1 CALL PER CLICK with a screenshot between - a single chained cmd.txt loses sync and silently ends up somewhere else. And the Load Game dialog does NOT pre-select Single Player on a fresh launch: the documented path really is Load Game (512,536) -> Single Player (512,290) -> OK (551,523) -> row -> OK (682,624) -> Launch (511,663). ref-turn2 row is at (400,436) GOTCHA (lane F, confirms lane R): the >60 s startup is REAL and cost a whole wasted run — do not sleep-and-click, **verify the main menu from a screenshot** (`verify/fpu-cw/` run scripts poll a screenshot until the Load Game / Exit buttons are bright red; 3 probes ≈ 25 s was typical). TIP (lane F): reset `SavedGames\` to a fixed file set before every run — the Load dialog row positions depend on how many files are listed, so a constant set means the click path never has to be re-derived (with the 4-file set ref-turn2 sits at (400,348), not (400,436)). PowerShell over SSH mangles quoting badly: send snippets base64 as `powershell -EncodedCommand`, or use `-ExecutionPolicy Bypass -File`. Lane F left `C:\SOTS\shimdist-fpu` + `C:\SOTS\ui\f{deploy,grab,fpu}.ps1` + `C:\SOTS\ui\preF\` (the pre-lane-F SavedGames snapshot, restored) in place. VM RESTORED: recap build `recap-7584bad-20260908T0615Z`, `hooks=trace`, SavedGames back to the 7-file pre-F set, main menu verified by screenshot. GOTCHA (lane V, third confirmation): the >60 s startup is REAL - the Kerberos intro was still on screen at t+30 s and the main menu appeared at ~t+70 s. GOTCHA (lane V): `type ` via the click helper did **NOT** register in the game's text fields this session (neither the Create Game name nor the Save Game file name took SendKeys); the workaround is to accept the default name and **rename the `.sav` on disk** - the Load dialog lists files by FILENAME. SAVEDGAMES IS NOW **8 FILES**: the pre-existing 7 plus `zuul-turn5.sav` (a species-5 save, see the Zuul row). Row positions are UNCHANGED for existing lanes because `zuul-turn5` sorts last: **ref-turn2 is still (400,436)**, zuul-turn5 is the new row 8 at (400,465), rows 29 px apart from y=262 - verified by screenshot `verify/results/shim/eventlive/load-dialog-8files.png`. The three autosaves now belong to the Zuul game. Lane V left `C:\SOTS\shimdist-v` + `C:\SOTS\ui\v{deploy,release,click}.ps1`. VM RESTORED: recap build `recap-7584bad-20260908T0615Z`, `hooks=trace`, main menu verified by screenshot. LANE U (2026-09-08): left `C:\SOTS\shimdist-u` + `C:\SOTS\ui\u{deploy,release}.ps1` in place. SavedGames is STILL 8 FILES (the three autosaves now belong to the Zuul game at turn 15) - **ref-turn2 is still (400,436)**, zuul-turn5 still (400,465). A QEMU-level screenshot (feed `screendump /tmp/vm140.ppm` to `qm monitor 140` on spicy, then scp the .ppm) is more reliable than the click helper's `shot` action and does not need the game window. FOURTH confirmation of the >60 s startup: the Kerberos intro was still on screen at t+45 s, the main menu at ~t+85 s. VM RESTORED: recap build `recap-7584bad-20260908T0615Z`, `hooks=trace`, main menu verified by screenshot. | **LANE O 2026-09-08.** **VM140 is FREE.** Holder was O-workloads; released 2026-09-08 after ~9 turns of Zuul play (turn 15 -> 23) and a fresh 3-turn Human game. **CORRECTION to lane V's `type` gotcha: the click helper's `type` DOES reach the game's text fields - you must CLICK THE FIELD FIRST.** `click 480 234` (the Save Game File Name box) then `type ` worked every time this session, and `click 500 363` + `type` worked for the Create Game name box; lane V's failure was almost certainly an unfocused field. Saves can therefore be named properly and no longer need renaming on disk (the file written is gameName + typedName). Coordinates confirmed this session at 1024x768: in-game menu = round button (1000,714) -> `Save` (937,679) / `Quit to Main Menu` (937,698) -> confirm OK (537,377); Save dialog `Save` (772,233), `Game Successfully Saved` OK (612,383), `Done` (772,542); map panel `Research` (55,124) `Design` (142,124) `Build` (221,124), `Manage Fleets` (60,464) `Move` (155,464) `Special` (229,464), `End Turn` (100,714); Build screen `Add To Queue` (125,714); **Esc leaves the Build and Research screens** - the bottom-left button on the Research screen is `Cancel research`, NOT back, and clicking it pops `Confirm Cancel`. Move mode: send `move X Y` FIRST to read the target's name / Range / ETA off the map, THEN `click`, because that readout is what tells you whether the route is a node route (2-5 turns) or a sublight crawl (37-86 turns). **Chained clicks in one `cmd.txt` DO work for repeated clicks on the SAME button** (`Add To Queue`) but drop about half at `sleep 250` and about 1 in 6 at `sleep 500` - count the result, never assume. End Turn takes 30-45 s under `hooks=trace` with ~25 ships. A mid-turn AI diplomacy popup can steal a scripted click sequence - screenshot after any batch that seems to have done nothing. **VM RESTORED and verified**: quit to the main menu (screenshot), `SavedGames` back to the ORIGINAL 8-file set with the three autosaves restored BYTE-IDENTICAL (sha256 prefixes 0630daaa / e0bf3485 / 1f0dc162), so `ref-turn2` is still (400,436) and `zuul-turn5` still (400,465); lane O's staging dir `C:\SOTS\o-stage` removed; `shim.cfg` still `hooks=trace`, build `recap-7584bad-20260908T0615Z` untouched. Lane O's 7 saves live in the REPO ONLY - push one back with `scp verify/results/saves/.sav re@192.168.10.139:C:/SOTS/SavedGames/` when a compare needs it, and remember that adding a file shifts the Load dialog rows (`MyGame*` sorts BEFORE `MyGameverify1*` **VM141 = FREE (lane AD released 2026-09-08; guest restored, SavedGames back to AC's 13-file set byte-identical PLUS `ad-turn27-two-raiders.sav`, so Load-dialog rows have MOVED AGAIN - screenshot the dialog).** Lane AD left `C:\SOTS\shimdist-ad` + `C:\SOTS\ui\ad-*.ps1`; `click_helper.ps1` unmodified. **GOTCHA: startup on this guest is ~4.5 min through THREE intro movies** (fg + `key {ESC}` x3 via the click helper, then ~90 s), and loading a turn-27 save is a further 3-4 min - budget **~9 min per run** before the first End Turn. **GOTCHA: the first End Turn click after leaving Move mode is swallowed exiting the mode**; the turn starts on the second click. | +| VM140 exclusivity (lab rule) | meta | verified | high | 100% | 2026-09-08 | one agent at a time. Holder: **VM140=FREE, oracle bytes verified intact), VM141=FREE (L2 released, 192.168.10.143, hooks=off), VM144=FREE (L3 released; workload saves left on disk, so Load-dialog row positions have moved on that guest), VM145=FREE, save set restored), VM146=FREE (CB released, stopped, restored)** (five parallel investigations 2026-09-08). VM140 remains the reference/oracle guest. Was: FREE (W3 released 2026-09-08, oracle bytes verified; **AND THE LAB IS NOW FIVE GUESTS** - VM141/144/145/146 are ZFS clones for probes and workloads, VM140 remains the exclusive reference; W2 released 2026-09-08, VM restored + oracle bytes verified; H released 2026-09-08, oracle reproduced + VM restored; N released 2026-09-08; Z released 2026-09-08; O-workloads released 2026-09-08 08:30 local, see the LANE O note at the end of this row; U-unlock released 2026-09-08 06:55 local; V-eventlive before it; F-fpucw before it, M-movefleet before that). QUEUE: empty. VM left at the MAIN MENU, `hooks=trace`, build `recap-7584bad-20260908T0615Z` restored from `C:\SOTS\shimdist-recap` (that dist also carries `shim.cfg.recap{trace,b3,b1,misc}`). Lane M also left `C:\SOTS\shimdist-mf` + `C:\SOTS\ui\mf{deploy,release}.ps1` in place - harmless, and a working template for the next lane. Windows Update DISABLED/paused on the VM. Non-holders build /srv/re-lab/build/sots-engine-, stage dist-, deploy C:\SOTS\shimdist-. GOTCHA (lane R): after `schtasks /Run /TN SOTS` the main menu can take >60 s - SCREENSHOT AND VERIFY before clicking, or the click path lands in Credits. GOTCHA (lane M): drive the load dialog ONE rui.ps1 CALL PER CLICK with a screenshot between - a single chained cmd.txt loses sync and silently ends up somewhere else. And the Load Game dialog does NOT pre-select Single Player on a fresh launch: the documented path really is Load Game (512,536) -> Single Player (512,290) -> OK (551,523) -> row -> OK (682,624) -> Launch (511,663). ref-turn2 row is at (400,436) GOTCHA (lane F, confirms lane R): the >60 s startup is REAL and cost a whole wasted run — do not sleep-and-click, **verify the main menu from a screenshot** (`verify/fpu-cw/` run scripts poll a screenshot until the Load Game / Exit buttons are bright red; 3 probes ≈ 25 s was typical). TIP (lane F): reset `SavedGames\` to a fixed file set before every run — the Load dialog row positions depend on how many files are listed, so a constant set means the click path never has to be re-derived (with the 4-file set ref-turn2 sits at (400,348), not (400,436)). PowerShell over SSH mangles quoting badly: send snippets base64 as `powershell -EncodedCommand`, or use `-ExecutionPolicy Bypass -File`. Lane F left `C:\SOTS\shimdist-fpu` + `C:\SOTS\ui\f{deploy,grab,fpu}.ps1` + `C:\SOTS\ui\preF\` (the pre-lane-F SavedGames snapshot, restored) in place. VM RESTORED: recap build `recap-7584bad-20260908T0615Z`, `hooks=trace`, SavedGames back to the 7-file pre-F set, main menu verified by screenshot. GOTCHA (lane V, third confirmation): the >60 s startup is REAL - the Kerberos intro was still on screen at t+30 s and the main menu appeared at ~t+70 s. GOTCHA (lane V): `type ` via the click helper did **NOT** register in the game's text fields this session (neither the Create Game name nor the Save Game file name took SendKeys); the workaround is to accept the default name and **rename the `.sav` on disk** - the Load dialog lists files by FILENAME. SAVEDGAMES IS NOW **8 FILES**: the pre-existing 7 plus `zuul-turn5.sav` (a species-5 save, see the Zuul row). Row positions are UNCHANGED for existing lanes because `zuul-turn5` sorts last: **ref-turn2 is still (400,436)**, zuul-turn5 is the new row 8 at (400,465), rows 29 px apart from y=262 - verified by screenshot `verify/results/shim/eventlive/load-dialog-8files.png`. The three autosaves now belong to the Zuul game. Lane V left `C:\SOTS\shimdist-v` + `C:\SOTS\ui\v{deploy,release,click}.ps1`. VM RESTORED: recap build `recap-7584bad-20260908T0615Z`, `hooks=trace`, main menu verified by screenshot. LANE U (2026-09-08): left `C:\SOTS\shimdist-u` + `C:\SOTS\ui\u{deploy,release}.ps1` in place. SavedGames is STILL 8 FILES (the three autosaves now belong to the Zuul game at turn 15) - **ref-turn2 is still (400,436)**, zuul-turn5 still (400,465). A QEMU-level screenshot (feed `screendump /tmp/vm140.ppm` to `qm monitor 140` on spicy, then scp the .ppm) is more reliable than the click helper's `shot` action and does not need the game window. FOURTH confirmation of the >60 s startup: the Kerberos intro was still on screen at t+45 s, the main menu at ~t+85 s. VM RESTORED: recap build `recap-7584bad-20260908T0615Z`, `hooks=trace`, main menu verified by screenshot. | **LANE O 2026-09-08.** **VM140 is FREE.** Holder was O-workloads; released 2026-09-08 after ~9 turns of Zuul play (turn 15 -> 23) and a fresh 3-turn Human game. **CORRECTION to lane V's `type` gotcha: the click helper's `type` DOES reach the game's text fields - you must CLICK THE FIELD FIRST.** `click 480 234` (the Save Game File Name box) then `type ` worked every time this session, and `click 500 363` + `type` worked for the Create Game name box; lane V's failure was almost certainly an unfocused field. Saves can therefore be named properly and no longer need renaming on disk (the file written is gameName + typedName). Coordinates confirmed this session at 1024x768: in-game menu = round button (1000,714) -> `Save` (937,679) / `Quit to Main Menu` (937,698) -> confirm OK (537,377); Save dialog `Save` (772,233), `Game Successfully Saved` OK (612,383), `Done` (772,542); map panel `Research` (55,124) `Design` (142,124) `Build` (221,124), `Manage Fleets` (60,464) `Move` (155,464) `Special` (229,464), `End Turn` (100,714); Build screen `Add To Queue` (125,714); **Esc leaves the Build and Research screens** - the bottom-left button on the Research screen is `Cancel research`, NOT back, and clicking it pops `Confirm Cancel`. Move mode: send `move X Y` FIRST to read the target's name / Range / ETA off the map, THEN `click`, because that readout is what tells you whether the route is a node route (2-5 turns) or a sublight crawl (37-86 turns). **Chained clicks in one `cmd.txt` DO work for repeated clicks on the SAME button** (`Add To Queue`) but drop about half at `sleep 250` and about 1 in 6 at `sleep 500` - count the result, never assume. End Turn takes 30-45 s under `hooks=trace` with ~25 ships. A mid-turn AI diplomacy popup can steal a scripted click sequence - screenshot after any batch that seems to have done nothing. **VM RESTORED and verified**: quit to the main menu (screenshot), `SavedGames` back to the ORIGINAL 8-file set with the three autosaves restored BYTE-IDENTICAL (sha256 prefixes 0630daaa / e0bf3485 / 1f0dc162), so `ref-turn2` is still (400,436) and `zuul-turn5` still (400,465); lane O's staging dir `C:\SOTS\o-stage` removed; `shim.cfg` still `hooks=trace`, build `recap-7584bad-20260908T0615Z` untouched. Lane O's 7 saves live in the REPO ONLY - push one back with `scp verify/results/saves/.sav re@192.168.10.139:C:/SOTS/SavedGames/` when a compare needs it, and remember that adding a file shifts the Load dialog rows (`MyGame*` sorts BEFORE `MyGameverify1*` **VM141 = FREE (lane AD released 2026-09-08; guest restored, SavedGames back to AC's 13-file set byte-identical PLUS `ad-turn27-two-raiders.sav`, so Load-dialog rows have MOVED AGAIN - screenshot the dialog).** Lane AD left `C:\SOTS\shimdist-ad` + `C:\SOTS\ui\ad-*.ps1`; `click_helper.ps1` unmodified. **GOTCHA: startup on this guest is ~4.5 min through THREE intro movies** (fg + `key {ESC}` x3 via the click helper, then ~90 s), and loading a turn-27 save is a further 3-4 min - budget **~9 min per run** before the first End Turn. **GOTCHA: the first End Turn click after leaving Move mode is swallowed exiting the mode**; the turn starts on the second click. **VM144 = FREE (lane AS released 2026-09-09**; restored to L3's build + `shim.cfg` + the three pre-AS autosaves byte-identical, main menu verified by screenshot). **SavedGames is now 15 FILES** - AS added `MyGameas1predeploy` (400,347) and `MyGameas1spydep` (400,376), so **ROW POSITIONS HAVE MOVED AGAIN**; screenshot the dialog. AS left `C:\SOTS\shimdist-as` + `ui\as{go,hash}.ps1` + `ui\preAS-*`; `click_helper.ps1` untouched. | | Zuul double-roll (behavioural) | verify | verified | high | 100% | 2026-09-08 | **CLOSED by lane V 2026-09-08.** No species-5 save existed, so lane V MADE one on VM140: a custom game with only Zuul left in the Available Species pool (both players Zuul), 4 End Turns under `shim.cfg.recapb3`. **8 calls, 8 compared, 0 diverged, exit 0.** All four researching calls have `species=5` and the generator advances by TWO, not one: `left` 540->538 / 522->520 / 504->502 / 485->483, `next_index` +2 each, `mt` hash unchanged; `ours` reproduced every post-state bit-for-bit. Save `verify/results/saves/zuul-turn5-species5.sav` (48559ab5b719b332, 59,131 B, turn 5, homeworld Gallandro) and on the VM as `C:\SOTS\SavedGames\zuul-turn5.sav`. NOT yet exercised by it: a Zuul tech COMPLETION (node 144 was at ~4,700 of 5,000 after 5 turns - one more End Turn gets there), so `SetResearched` and the Zuul boarding-pod grant are still untouched. Report `findings/subsystems/eventlive-verification.md` §5 **EXTENDED by lane U 2026-09-08: the double roll and the COMPLETION PATH together.** `zuul-turn5` + 10 more End Turns (turn 5 -> 15) under the same config: **20 calls, 20 compared, 0 diverged, exit 0**, with TWO Zuul completions. Call 2 (turn 7, alloc {144,1376}) advances the generator by two (`left` 449->447, `next_index` 175->177) AND runs the unlock cascade in the same call - node 144 -> state 4 / order 21 / turn_researched 7, nodes 132 and 136 -> state 2 with cost_rp 10000 / 16000 and turn_available 7, `next_id` 10->12, `observed_techs` 396->440. Note the ZUUL tree unlocks only 132 and 136 from tech 144 where the HUMAN tree also unlocks 142, so this is an independent instance of the cascade, not a repeat. Trace `verify/traces/unlock-b3-zuul.jsonl.gz` | | budget tail coverage (expenses/aid/debt) | verify | backlog | — | 0% | 2026-09-08 | 8 ComputeBudget slots were always 0 in ref-turn2 (no sliders, no aid, no debt, no handicap). Need a save with expense sliders, a debtor and a research-aid treaty to exercise ExpenseTotal + the aid/bonus tail . CONFIRMED AND WORSE 2026-09-08 (lane R, 4284 calls): **13 of 22 slots are 0 on every call** - tradeIncome, shipCarriedPop, secondaryManager, bonusIncome, systemIncomeNeg, debtInterest, construction, expenses, researchMoneyGiven, savingsGiven, tra, researchPointsGiven, trp | **LANE O 2026-09-08.** **PARTIALLY unblocked, and three of the remaining slots are now shown to be UNREACHABLE through the 1.8 UI.** Delivered: `[11] construction` and a large `[8] maintenance` are live in `zuul-turn17-orders2.sav` (build queue of 20 DEs) and `zuul-turn23-fleet23.sav` (27 DEs in service, `Maint` 14,500 vs 3,000/1,000 in every earlier save); the empire research slider is off default (`ResRate` 0.97 vs 0.25) in all five Zuul saves. NOT delivered, with reasons. **(a) `[12] expenses`**: `Nexp` is `[]` for EVERY player in ALL 11 saves across turns 1-23, human and AI alike, and no screen in the 1.8 UI (map panel, Build, Research, Ranking, Intel Analysis, Empire Population Manager, in-game menu) exposes a per-category expense slider - the only economic sliders are the empire Savings/Research split and the per-system `SRs/SRt/SRsc/SRtf/SRi/SRoh` rates, which land in `Sys`, not `Nexp`. Working hypothesis: slot 12 is dead in 1.8. **(b) `[10] debtInterest` / `[7] systemIncomeNeg`**: a debtor cannot be manufactured quickly - a ship's savings cost is deducted at QUEUE time so the queue cannot be over-committed, and `Maint` counts only ships IN SERVICE, which arrive at construction-budget/ship-cost per turn (~4.5 DE/turn here). With income floored at 78,570 (Planetary Budget pushed fully to Construction; measured 124,055 -> 78,570 on the panel) that is ~157 destroyers, i.e. ~35 more End Turns. Reachable, just not cheap. **(c) `[13]/[14]/[19]` aid**: a treaty offer DOES occur (the AI offered a non-aggression pact on turn 3 of the Human game and it was accepted - `human-turn3-noderoute.sav`), but the accepted pact leaves the player's `aid` count 0, and SotS 1.8 has no player-initiated treaty UI - proposals are AI-driven events. Research aid needs a longer game and luck, not a click path | hook GetDifficultyMods | meta | backlog | — | 0% | 2026-09-08 | B1 derived the two difficulty rows from trace values (AI maintenance divisor 3, research x1.5) instead of snapshotting them; hook it properly so they stop being constants | @@ -168,7 +168,7 @@ Status flow: `backlog → in-progress → mapped → verified` (or `blocked`). | conformance check honest about what it cannot check | engine | verified | high | 100% | 2026-09-08 | Lane Q: the generic check() could NOT be used, and that is the finding not an obstacle - every tag in this block is ".", so the LCS degenerates to a strict positional compare where any primitive disagreement is fatal, AND the table's tail describes ELEMENTS where the wire has COUNTS. The dedicated check states only what is checkable: prologue 17/17 ITEM FOR ITEM (SchemaProbe takes every branch, so this is real evidence that the conditional structure read from the instruction stream is the one the recovery flattened) and tail 27 lists vs 27 table items. Tail reported as WIRE-ONLY, never claimed as matched. 86 shapes/838 items -> 87/856, still 0 MISMATCH | | INLINED RNG DRAWS ARE INVISIBLE TO CALL-GRAPH SWEEPS | verify | verified | high | 100% | 2026-09-08 | LANE J'S HEADLINE, and it invalidates a class of prior analysis. The combat resolver's NextFloat is INLINED, so its only call-graph edge is `caller -> RNG_Twist`, which reads as a bare Twist and is NOT one - it is the lazy twist INSIDE NextFloat. That is why lane K reported "no NextFloat in that subtree". An image-wide scan for the MT TEMPERING IMMEDIATES AT INSTRUCTION BOUNDARIES finds **14 game functions with inlined draws no RNG sweep can see**, and TWO OF THEM - FUN_004f7670 (84 bytes) and FUN_007aa240 - ARE IN ProcessTurn'S CLOSURE AT DEPTH 4. Any RNG accounting built from the call graph alone is a LOWER BOUND. Now rule 16 | | CORRECTION: Ghidra's function size is not the function | meta | verified | high | 100% | 2026-09-08 | Ghidra reports 7,499 bytes for the combat resolver; the real body is 0x007d5af0..0x007d78c8 = 7,641 and Ghidra's end lands MID-INSTRUCTION. Lane J first read a callee as straight-line because it dumped at Ghidra's size and the outer BACK-EDGE FELL OUTSIDE - it is a loop over combatants, which changes the draw count of the very site it was measuring. A delegated sweep made the same mistake independently. Same defect lane X hit from the other side (clipping at fva+sizeInBytes lost 11% of functions; sweeping to the next function start took coverage 89% -> 100%). Now rule 17: never trust a function's end, disassemble to the next function start | -| combat resolver mapped | control-flow | verified | high | 85% | 2026-09-08 | Lane J read all 7,641 bytes. THE RESOLVER DRAWS NOTHING ITSELF - exactly three sites in its subtree, each behind a function whose callerCount is 1 (the resolver), so nothing else in the image can trigger them: R1 NextInt @0x007bb69b (node cannon, once per battle, gated on something having been flung AND a non-empty candidate list) mean 4/3 words; R2 an INLINED NextFloat @0x007a84bd, one per back-engineering candidate per combatant with a non-zero salvage slot, exactly 1 each; R3 NextInt @0x00852ec7, one per successful R2 roll if the project list is non-empty, mean 2^ceil(log2 n)/n. Structure: only EIGHT non-stack stores in the whole function and EXACTLY ONE indirect call (in a _CxxThrowException path) - it composes and posts events and delegates every mutation to its 70 callees. Also: the subtree DOES write the turn-results accumulator at S->+0x2f4[PlyrIdx*0x11c]+0x90 on the surrender arm (a DIFFERENT member from the +0x24 lane K found, and that writer has SEVEN callers); 23 EVENT_* keys in the closure vs lane K's 7, five new, INCLUDING PLAGUE BEING DECIDED INSIDE COMBAT; sizeof(Game::TacReport)=0x94 enumerated twice **THREE OF THE FOUR UNREAD GATES ARE NOW DECODED (lane AG):** `0x0082cdb8` = addiction spread (corpus 0 of 22, `nadct` 0 everywhere, and its seed path needs an INTER-EMPIRE route plus a species addict tech); `0x0088dc43` = spy counter-mission, p = `cmo` accumulating 0.2f/turn so `Chance` costs **0 words once p>=1** (corpus 0 of 22 by `deat`/`cm`); `0x0088b613` = raid target pick, and **`NextInt` has no zero-bound early-out** so `B` provably returned before it - the failed conjunct is the raider's **SECTOR**, not any container. Still unread: `0x00887c8a`'s `FUN_00743f80 & 1` conjunct and the three sites inside P. **PRICED 2026-09-08 (lane AD): `0x00820e18` costs ONE WORD PER QUALIFYING FLEET, not one per sector** - two raiders on one sector give calls=2 words=2. `0x0088b613` (B's NextInt) still UNFIRED and now a **proof-carrying zero**: `RNG_NextInt` has no zero-bound early-out (lane AG), so `B` costing 0 words means `B` never reached the pick. | +| combat resolver mapped | control-flow | verified | high | 85% | 2026-09-08 | Lane J read all 7,641 bytes. THE RESOLVER DRAWS NOTHING ITSELF - exactly three sites in its subtree, each behind a function whose callerCount is 1 (the resolver), so nothing else in the image can trigger them: R1 NextInt @0x007bb69b (node cannon, once per battle, gated on something having been flung AND a non-empty candidate list) mean 4/3 words; R2 an INLINED NextFloat @0x007a84bd, one per back-engineering candidate per combatant with a non-zero salvage slot, exactly 1 each; R3 NextInt @0x00852ec7, one per successful R2 roll if the project list is non-empty, mean 2^ceil(log2 n)/n. Structure: only EIGHT non-stack stores in the whole function and EXACTLY ONE indirect call (in a _CxxThrowException path) - it composes and posts events and delegates every mutation to its 70 callees. Also: the subtree DOES write the turn-results accumulator at S->+0x2f4[PlyrIdx*0x11c]+0x90 on the surrender arm (a DIFFERENT member from the +0x24 lane K found, and that writer has SEVEN callers); 23 EVENT_* keys in the closure vs lane K's 7, five new, INCLUDING PLAGUE BEING DECIDED INSIDE COMBAT; sizeof(Game::TacReport)=0x94 enumerated twice **THREE OF THE FOUR UNREAD GATES ARE NOW DECODED (lane AG):** `0x0082cdb8` = addiction spread (corpus 0 of 22, `nadct` 0 everywhere, and its seed path needs an INTER-EMPIRE route plus a species addict tech); `0x0088dc43` = spy counter-mission, p = `cmo` accumulating 0.2f/turn so `Chance` costs **0 words once p>=1** (corpus 0 of 22 by `deat`/`cm`); `0x0088b613` = raid target pick, and **`NextInt` has no zero-bound early-out** so `B` provably returned before it - the failed conjunct is the raider's **SECTOR**, not any container. Still unread: `0x00887c8a`'s `FUN_00743f80 & 1` conjunct and the three sites inside P. **PRICED 2026-09-08 (lane AD): `0x00820e18` costs ONE WORD PER QUALIFYING FLEET, not one per sector** - two raiders on one sector give calls=2 words=2. `0x0088b613` (B's NextInt) still UNFIRED and now a **proof-carrying zero**: `RNG_NextInt` has no zero-bound early-out (lane AG), so `B` costing 0 words means `B` never reached the pick. **FIRED 2026-09-09: `0x00887c8a` (spy 13), lane AS, 1 word**, behind a deployed spy at a foreign colony with a usable asteroid belt. **V2 vindicated a second time.** Five of the eight still unfired: P's three (`0x00840929`/`0x008409c7`/`0x00840a3c`, gate now `sdet != -1` and turn-sdet>=3, ~20 End Turns from `as-turn15-spydeployed.sav`), `0x0088dc43` (spy 14, gate `cm in 1..4`), `0x0082cdb8` (trade 15). **AND THE INVENTORY IS SHORT ONE**: `0x0078c97f` in the `SHIPACTION_DEPLOYSPY` handler draws a `NextFloat` on the strategic generator and is in **no sweep**, because it hangs off the stack-built ship-action table. | | resolver formula is UNTESTED (stated) | verify | backlog | — | 0% | 2026-09-08 | Lane J's honest limit: every encounter in lane Z's workload had res->+0x4 set, so ApplyEncounterResult was a no-op and THE RESOLVER HAS NEVER EXECUTED UNDER AN INSTRUMENT. The formula is untested. combat-resolver.md 10.3 specifies the workload; the first cheap PREDICTION is that a plain fleet battle with no node cannon and no salvage should cost the SAME 18-20 words as a peaceful turn, because the resolver has no unconditional draw | | lane J errors caught before publishing | meta | verified | high | 100% | 2026-09-08 | Two, both corrected pre-publication and both worth keeping: (1) read a callee as straight-line from a truncated Ghidra range, hiding the outer loop that sets R2's count - now rule 17; (2) drafted a 0x11c site as a false positive after converting its address BY HAND, wrongly - re-disassembling from a known boundary showed a real SETurnResults write. THE SCANNER WAS RIGHT AND THE READER WAS NOT | | inlined-draw inventory COMPLETE | verify | verified | high | 100% | 2026-09-08 | Lane I re-ran the tempering-immediate scan ITSELF at real instruction boundaries (all 41,089 functions decoded TO THE NEXT FUNCTION START, never to Ghidra's size). SIXTEEN functions carry the masks inside a decoded instruction, 67 occurrences - and RECALL IS PROVED COMPLETE: a brute byte scan over the executable sections finds the SAME 67 and ZERO ORPHANS. 70 functions desync mid-decode, every one inside int3 padding, none hiding a site. **LANE J'S 14 WAS WRONG IN TWO WAYS**: one is a FALSE POSITIVE (FUN_008cca30's four bytes are the rel32 displacement of a call, not a temper chain) and FOUR are RNG ENTRY POINTS, not game code - including 0x004f7670 which lane J counted as a game function with an inlined draw and which is actually Mars::RNG::NextUInt, a primitive. CORRECTED FIGURE: **eleven game functions, 28 sites**. Only TWO are reachable from the turn drivers by direct edges: FUN_007aa240 (StrategyServer::ProcessTurn, depth 4) and FUN_007a7f30 (OnAllCombatDone_Tail depth 3 / CombatResolver_Run depth 1). The nine others top out at vtable slots with no direct caller - and lane I states AS LOUDLY AS THE RESULT that "not in the closure" is proved FOR DIRECT EDGES ONLY | @@ -183,7 +183,7 @@ Status flow: `backlog → in-progress → mapped → verified` (or `blocked`). | THE FINDING THAT SHAPED THE DESIGN: report regressed, never net it off | meta | verified | high | 100% | 2026-09-08 | Lane S2 first implemented AND COMMITTED S31's player-status restore. The comparison tool immediately reported TWO REGRESSED LEAVES on turn2->turn3: two Player.Status words that AGREED with the oracle before the turn and DISAGREED after. The phase writes 1, the file carries 4, a load resets to 0 - so a writer between phase 31 and the autosave is unaccounted. S31 is now blocked, and `regressed` is reported NEXT TO `closed` in every run, NEVER NETTED OFF. Consequent design rule: a phase whose FORMULA we hold but whose INPUTS we do not is EVALUATED AND REPORTED, NOT WRITTEN, unless --commit-blocked. Same for the generator (--commit-rng) | | standalone: two things deliberately NOT implemented | verify | backlog | — | 0% | 2026-09-08 | (1) TShn/ltis - 18 of the remaining 204 leaves, moving 1->2 on 8-10 systems on BOTH pairs. A `+1` closes them in ten lines. NOTHING NAMES THEIR WRITER, so that is a hypothesis not a reading; named in the docs as the cheapest measured target. (2) RNG write-back - an advanced-but-incomplete state is WRONG DIFFERENTLY from an untouched one. One hypothesis IS under test and survived a changed workload: `stable = owned && !abandoned && !destroyed` (the original asks a callee) closed the same 3 ntdev leaves on both pairs - six agreements, zero disagreements - and is labelled a hypothesis in code, log AND docs | | RANKED blockers to the byte-match | meta | mapped | high | 100% | 2026-09-08 | Lane S2's ranking, which is now the project's critical path: (1) THE RNG LEDGER - lane Z; nothing in src/app can close it. (2) The population->base-output term - ONE FORMULA GATING 5 OF THE 44 PHASES. (3) The 37-phase post-combat tail, which is the driver THE AUTOSAVE IS WRITTEN FROM. (4) The `nve` visibility record (32 leaves, one mechanism x 8). (5) The event pipeline. (6) Summary.Checksum. (7) The Player.Status writer. (8) ModCount - the real turn advances it 12-44 times from writers spread across BOTH drivers | -| RNG LEDGER CLOSED - zero residual | verify | verified | high | 100% | 2026-09-08 | THE BLOCKER IS GONE. 18-22 words per turn, EVERY ONE ATTRIBUTED TO A CALL SITE, summing to the independently-measured total with ZERO RESIDUAL on three turns. THREE INSTRUMENTS AGREE on turn 5's 20 words: boundary hook, return-address detours, and the two save files - and the file check has `twists = 0`, so it does NOT route through anyone's twist implementation. Two calibrated oracle pairs are in the repo for the standalone to test against. Also settled live: the tail RUNS ON EVERY End Turn (8/8, two saves), confirming lane K's inference (its stronger no-encounter form is narrowed, not closed); node-line decay fired on turn 64 for EXACTLY 1 WORD, matching a prediction committed THIRTY TURNS EARLIER, and exercised the twist path live on the same turn; and COMBAT IS FREE - the first instrumented battle cost 0 words **GATE-INDEXED SUCCESSOR 2026-09-09 (lane AG): the site set is now enumerated by GATE rather than by firing** - 43 entry-point call sites plus 2 inlined sites inside the two turn drivers' closures over direct + tail-jump + pinned-virtual edges, of which **30 distinct sites are UNREAD GATES**. Six were not in any earlier inventory. No per-turn total is claimed. **Lane AD 2026-09-08: the tail site's cost is not a constant at all** - it is **one word per permitted raider parked on a sector node**, measured at 2 with two fleets. Any per-turn tail figure is a statement about the fleet count in that state. | +| RNG LEDGER CLOSED - zero residual | verify | verified | high | 100% | 2026-09-08 | THE BLOCKER IS GONE. 18-22 words per turn, EVERY ONE ATTRIBUTED TO A CALL SITE, summing to the independently-measured total with ZERO RESIDUAL on three turns. THREE INSTRUMENTS AGREE on turn 5's 20 words: boundary hook, return-address detours, and the two save files - and the file check has `twists = 0`, so it does NOT route through anyone's twist implementation. Two calibrated oracle pairs are in the repo for the standalone to test against. Also settled live: the tail RUNS ON EVERY End Turn (8/8, two saves), confirming lane K's inference (its stronger no-encounter form is narrowed, not closed); node-line decay fired on turn 64 for EXACTLY 1 WORD, matching a prediction committed THIRTY TURNS EARLIER, and exercised the twist path live on the same turn; and COMBAT IS FREE - the first instrumented battle cost 0 words **GATE-INDEXED SUCCESSOR 2026-09-09 (lane AG): the site set is now enumerated by GATE rather than by firing** - 43 entry-point call sites plus 2 inlined sites inside the two turn drivers' closures over direct + tail-jump + pinned-virtual edges, of which **30 distinct sites are UNREAD GATES**. Six were not in any earlier inventory. No per-turn total is claimed. **Lane AD 2026-09-08: the tail site's cost is not a constant at all** - it is **one word per permitted raider parked on a sector node**, measured at 2 with two fleets. Any per-turn tail figure is a statement about the fleet count in that state. **THIRD site outside the firing-indexed table has now fired**: the spy detection roll (tail T23c9, lane AS). **The pattern is three for three** - hives, the raid roll, the detection roll - and **each fired on the first state built for it**. The gate-indexed audit is the fix, not a longer table. | | the dominant RNG consumer: trade-raid generation | subsystems | verified | high | 100% | 2026-09-08 | Sixteen of the ~20 words per turn are ServerTradeManager::GenerateTradeRaidEncounters (0x00893290) - ServerTradeManagerImpl vftable 0x00a31b74 SLOT 10, looping StrategyServer::Players and rolling per player: +0x196 player raid TRADE_RAID_ODDS_PLAYER 0.2f (8/8 fired), +0x283 NPC raid 0.05f (gate is player-INDEPENDENT so all-or-nothing per turn, 8/8), +0x33e refugee raid 0.05f (0/8). All three probabilities are strictly inside (0,1) so `Chance` takes NEITHER early-out - exactly one word each, matching the measurement. NO BACK-EDGE CONTAINS ANY OF THE THREE SITES, so one word per player per site is a HARD BOUND, not an observation. STILL UNBOUNDED, flagged: a SUCCESSFUL roll calls slot 17 (CreateRaidEncounter 0x008938a0) which draws a NextInt but returns without drawing on an empty candidate list, so 0 or 1 further word; it cost 0 on all three turns, consistent EITHER with no roll succeeding (~11% on the defaults) OR with the candidate list being empty every time - a word count cannot separate those | | WHY NO SWEEP FOUND IT: the virtual edge | meta | verified | high | 100% | 2026-09-08 | ZERO direct calls to 0x00893290 exist in the image; its only reference anywhere is the vtable slot. The dispatch is `call edx` through slot 10 at 0x007d8469, inside DetectEncounters - ONE INSTRUCTION BEFORE the direct call lane I's closure DID follow. LANE I'S INVENTORY IS NOT WRONG: its tempering-scan recall claim is intact and its 22-site list is EXPLICITLY a direct-edge closure, with the indirect-call caveat stated as loudly as the result. This is the demonstration that the caveat was LOAD-BEARING - the single largest RNG consumer of a strategic turn hangs off a virtual edge inside a function the closure already contained. THE LESSON: a closure over direct edges is a lower bound, and saying so is what let the next lane find this in one step | | the repo had already met this function | meta | verified | high | 100% | 2026-09-08 | strategic-turn-internals.md line 153 ALREADY listed 0x00893290 as "raid encounter generation" against these exact three StrategyVars. What was missing was not the identification but THE CONNECTION TO THE LEDGER - nobody knew it was where a turn's RNG goes. Worth remembering when the next "unknown" turns up: search the notes before the binary | @@ -415,3 +415,7 @@ Status flow: `backlog → in-progress → mapped → verified` (or `blocked`). | NO ORACLE PAIR for the turn-27 raider state - the control varies, but ONLY in the AI's decisions, and /Sim/RNG is bit-identical across three processes | meta | verified | high | 100% | 2026-09-08 | Lane AD (rule 26). Two `hooks=off` runs of one End Turn from ad-turn27-two-raiders.sav in two fresh processes gave DIFFERENT post-turn autosaves (a634510e / 676b1778); the pre-turn autosave is byte-identical in all three processes (16f63adb), so the load path is deterministic and the divergence is inside the turn. So NO rung-A calibration pair came out of this state and none is proposed. BUT the state-checksum localises the variation completely: 94 leaves, ALL of them Player[32 "The Eternal Empire"] designs/ShipRecs/odes/build queues/fleet ids and positions, plus ModCount, NumFlts, the three id lists and Summary.Checksum. `/Sim/RNG` = 1e458a997f51bbdf and `/Sim/trdmgr` = 89643da6fabb386b in ALL THREE post-turn saves (two hooks=off and one probes=8), and NO trade-sector, trdmgr or player-0 fleet leaf appears in either diff. Two consequences: (a) every number lane AD reports is read off a sub-tree the control reproduces bit-for-bit; (b) RULE 19, on this state: the probes=8 + draw_sites + boundary-ledger instrument is behaviour-neutral on every leaf OUTSIDE the AI-decision sub-tree, including the serialised strategic generator. The named next step is a RUNG B pair - `--turn-commands` replay runs no AI, so a captured command stream should make this state reproducible where Rung A cannot | | Sector 816 is UNREACHABLE from lane AC's empire - the AG probe needs a 27 ly hull, and one is already buildable | control-flow | verified | high | 100% | 2026-09-08 | Lane AD, measured from ad-turn27-two-raiders.sav geometry + the Build screen. Node distances from the human's only two colonies (Epsilon Eridani idx 0, Downbelow idx 12) against a 9.0 ly cruiser range: sector 816 = 15.66 / 12.12, sector 768 = 11.53 / 10.14, sector 752 = 12.85 / 8.43, sector 800 = 9.37 / 11.77, sector 784 = 9.36 / 3.49, sector 832 = 4.99 / 4.56. The only two sectors IN range (832, 784) source no route; the only two that source routes (816 via trfr 18, 768 via trfr 14 x2) are out of range. With Trade View on, only sector 6's cube renders at any zoom, so no other sector node is even a clickable move target. THE FIX IS ALREADY UNLOCKED: the stock `Extended Range` DESTROYER has Range 27.0, construction cost 2,252, node speed 4.0 - ~1-2 turns to build, ~4 turns to sector 816's node, so AG's probe is ~6 turns of play and needs no research. TWO WARNINGS: a destroyer takes the nDE odds branch, 0.05f + 0.01f = 0.06 (0.12 if the AI Freighters fleet in 816 satisfies the doubling), i.e. a 1-in-8 success per fleet per turn - send several fleets or measure many turns; and sector 816's tsct = 2 is the AI's bit, not player 0's, so no FtFlg & 0x800 is required there, exactly as on 832 | | the route set MOVES between turns - AG's 816 prediction needs restating for AD's state | control-flow | verified | med | 100% | 2026-09-09 | Not a contradiction between the lanes, but it would read like one. Lane AG read **four** routes out of `ac-turn22-raider.sav` with `trfr` 18/16/14/20 (sectors 816/816/768/752); lane AD read **three** out of `ad-turn27-two-raiders.sav` with `trfr` 18/14/14 (sectors 816/768/768). Different saves, five turns apart - **the AI's route set is not static.** Consequence for the sector-816 probe: on AC's turn-22 route set 816 had **two** candidates (`NextInt(bound=1)`, mask 1, no rejection possible); on AD's turn-27 set it has **one** (`NextInt(bound=0)`, mask 0, accepted on the first draw). **Both still cost exactly one word**, so AG's committed prediction of 1 word on a failing turn and 3 on a succeeding one survives - but whoever runs the probe must re-read `trfr` on the state they actually build rather than inheriting either number | +| THE SPY DETECTION ROLL FIRES - 1 word, gated on an asteroid belt; `spies2` and `deat` closed | verify | verified | high | 100% | 2026-09-09 | **Lane AS, VM144, build `as-c172c99-20260909T0205Z`, `probes=8` (never 11).** `Mars::RNG::Chance` at **0x00887c8a**, in the BODY of `ServerSpyManager::vslot13` (tail phase 23 call 9), fired **1 call / 1 word / no_draw_calls=0 / strategic=true** at `ret_rva 0x00887c8f`; `OnAllCombatDone_Tail` went **363 -> 364**, against 0 on every turn any lane has ever measured. Turn total 19, per-site sum 19, **residual 0**. THE ENTRY PROBE ON `SpyManager::Slot13RngCallee 0x008408e0` READ **0 ON THE SAME TURN** - that zero was never a statement about the subtree, only about which function the call sits in (rule 28 practice 4, demonstrated rather than argued). **CORRECTION: the roll is NOT inline** (the resolution and AC §2.2 both say so) - it is a plain `E8` to the `Chance` entry point in the caller. **THE PREDICATE**, decoded to save fields: `spy.deat != 0 && spy.sdet == -1 && sys(deat).ARes2 > 0 && (sys(deat).TerrFl & 1)`; **corpus count 0 of 22, failed conjunct `deat != 0`**. `ServerSystem_BeltUsableFlags 0x00743f80` (16 bytes) returns `TerrFl` with bit 0 cleared when `ARes2 <= 0`; `ARes2>0` and `TerrFl&1` agree on all 616 corpus system records AND on the new map, which also has `TerrFl = 2` and `3`, so bit 0 is the belt specifically. **POLARITY: belt gone => NO DRAW AND THE SPY IS DETECTED ANYWAY** (`je` past the roll to `sdet := turn`) - a zero here is two different results. Cost: `sdo` starts at 0, `SpyCraft_AccumulateDetectionOdds 0x0081f570` ACCUMULATES `0.7*(0.01 + [cA]0.01 + [cB]0.02 + 0.001*cC) * species(1.0/0.75/0.5)` and CLAMPS AT 1.0, so `Chance` costs 1 word until sdo reaches 1 and 0 thereafter. Measured `sdo` 0 -> 0.0084 -> 0.0168 (= 0.7*0.012 exactly, a value no run had produced). **`spies2` IS CLOSED**: `ServerSystem::AddSpy 0x007514c0` sets `spy.deat = system handle` and pushes `spy.sid` into `ServerSystem+0x1cc` in the same two instructions - `spies2` is the per-system DEPLOYED-spy id vector; L3's "not the spy list" was half right, AC's P4 was right. Measured: `count=1 element=1` at Kepler, 0 in the other 27. **NEW DRAW SITE NOBODY HAS**: `0x0078c97f` NextFloat in the SHIPACTION_DEPLOYSPY handler `0x0078c930` (`cbh = NextFloat()*2pi`), strategic generator, in the ProcessTurn bracket - decoded, NOT measured, and its predicate is on the COMMAND STREAM not the save, a column the gate-indexed audit lacks. Oracle: input byte-identical in 3 processes; A vs B differ in **22 leaves, all inside one AI player's research pick** (the known per-process client seed, resolution 2026-09-08) - so this is NOT a calibration pair and must not enter `determinism-hashes.txt`. `probes=8` run came out byte-identical to control A. Saves: `as-turn15-spydeployed.sav`, `as-turn14-predeploy.sav`; findings `findings/subsystems/spy-detection-roll.md` | +| the gate-indexed audit needs a THIRD column: predicates on the COMMAND STREAM | meta | verified | high | 100% | 2026-09-09 | Lane AS found a draw site in **no inventory**: `0x0078c97f`, a strategic `NextFloat` in the `SHIPACTION_DEPLOYSPY` handler (`cbh = NextFloat() * 2pi`), inside the `ProcessTurn` bracket. It escapes every sweep because it hangs off a **stack-built ship-action table** - no static call edge to find (rule 16's cousin). But the structural point is the schema, not the site: **its gate is a predicate on the COMMAND STREAM, not on save fields**, and lane AG's audit has exactly two kinds of cell - a measured firing or a predicate over the save. A site reachable only when a player issues a particular order fits neither. **The audit's blank-cell discipline is right and its vocabulary is one column short.** Consequence for the rungs: a command-stream-gated draw is invisible to a save-only corpus count no matter how large the corpus grows, and it is exactly the class `--turn-commands` replay DOES exercise - so it bears on Rung B specifically | +| a fact about ONE MAP generalised into a fact about the game | meta | verified | high | 100% | 2026-09-09 | Lane AC reported that "an enemy colony with an asteroid belt does not exist in this game", and my brief to lane AS inherited it as a workload cost - regenerate maps until one appears. **It was true of AC's map only.** All 22 corpus saves have a usable belt; `human-turn2-orders.sav` and `human-turn3-noderoute.sav` already hold **four AI-owned** ones; and AS's **first** generated map had two, **zero regenerations**. This is rule 28 practice 3's sibling and is now practice 5: **before building a state to satisfy a condition, count the corpus against it - the state you need may already be in the save set.** The value-domain census exists to make exactly this cheap | +| rule 28 practice 4, demonstrated rather than argued | meta | verified | high | 100% | 2026-09-09 | Lane AS ran both instruments on the same turn: the **entry probe** on `SpyManager::Slot13RngCallee 0x008408e0` read **0**, while the **return-address ledger** recorded `0x00887c8a` firing for **one word** in the caller's body. Four lanes had previously read that zero as a statement about the subtree; it was only ever a statement about which function the call sits in. **Polarity matters too and is now practice 6**: the `je` jumps PAST the roll, so belt-gone means **no draw AND the spy detected anyway** - a zero at that site means two opposite things, and the intuitive reading is backwards | diff --git a/guides/method-rules.md b/guides/method-rules.md index 2d0932e..b364d48 100644 --- a/guides/method-rules.md +++ b/guides/method-rules.md @@ -413,5 +413,23 @@ So, four practices: 252 everywhere looked like a fact about the game; it was a fact about our saves. Compare rule 8's eighteen fields that split a roster identically because nothing exercised them. 4. **Pair entry probes with the return-address ledger before calling a subtree draw-free.** An entry - probe on an inner function reading zero says nothing about an *inline* draw in its caller — which - is exactly where the spy half's detection roll lives. + probe on an inner function reading zero says nothing about a draw sited in its *caller* — which is + exactly where the spy half's detection roll lives. + + **Confirmed live, 2026-09-09.** Lane AS measured both instruments on the same turn: the entry probe + on `SpyManager::Slot13RngCallee` read **0**, while the return-address ledger recorded the detection + roll firing for **one word** in the caller's body. The zero was never a statement about the subtree — + only about which function the call happens to sit in. (The roll is a plain call, not an inlined one; + the earlier claim that it was inlined was wrong, and the practice holds either way.) + +5. **A fact about one map is not a fact about the game.** Practice 3's sibling, and it cost a lane a + whole workload plan. One lane reported that "an enemy colony with an asteroid belt does not exist in + this game", which was true of *its* map. All twenty-two corpus saves have a usable belt, two of them + already hold four AI-owned ones, and the next lane's very first generated map had two — **zero + regenerations**. Before building a state to satisfy a condition, count the corpus against it: the + state you need may already be sitting in the save set. + +6. **Read the polarity before reading the count.** In the same chain, the jump goes *past* the roll: + with the belt gone there is **no draw and the spy is detected anyway**. A zero at that site therefore + means two opposite things, and a lane that assumed the intuitive polarity would have reported the + detection case as "did not fire".