diff --git a/campaign/DASHBOARD.md b/campaign/DASHBOARD.md index 66eac32..7c8d3b0 100644 --- a/campaign/DASHBOARD.md +++ b/campaign/DASHBOARD.md @@ -1,16 +1,16 @@ # SotS RE campaign — coverage dashboard -Generated 2026-09-09 01:59 UTC · `sots-re` @ d98f435,2026-09-08 · `sots-engine` @ e62d974,2026-09-08 (238 commits) · regenerate with `tools/dashboard.py` +Generated 2026-09-09 02:29 UTC · `sots-re` @ 48db3cc,2026-09-08 · `sots-engine` @ 35b05eb,2026-09-08 (239 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) -403 targets · mapped-or-better **356/403** `[█████████░] 88%` · verified **314/403** `[████████░░] 78%` +405 targets · mapped-or-better **358/405** `[█████████░] 88%` · verified **316/405** `[████████░░] 78%` | Status | Count | % | |---|---:|---:| -| verified | 314 | 78% | +| verified | 316 | 78% | | mapped | 42 | 10% | | in-progress | 4 | 1% | | backlog | 41 | 10% | @@ -19,7 +19,7 @@ Generated 2026-09-09 01:59 UTC · `sots-re` @ d98f435,2026-09-08 · `sots-engine | Type | verified | mapped | in-progress | backlog | blocked | total | |---|---:|---:|---:|---:|---:|---:| | objects | 46 | 6 | 0 | 3 | 1 | 56 | -| control-flow | 24 | 2 | 0 | 0 | 0 | 26 | +| control-flow | 26 | 2 | 0 | 0 | 0 | 28 | | subsystems | 4 | 8 | 0 | 2 | 1 | 15 | | engine | 29 | 0 | 0 | 0 | 0 | 29 | | verify | 101 | 15 | 3 | 35 | 0 | 154 | @@ -31,7 +31,7 @@ Generated 2026-09-09 01:59 UTC · `sots-re` @ d98f435,2026-09-08 · `sots-engine - 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): **1247**, verified **1122** `[█████████░] 90%` +- Functions: **41,411** (parsed from `01-fingerprint.md`); named/annotated in the **address contract** (`ghidra/addresses.json`, not Ghidra's full rename count): **1269**, verified **1139** `[█████████░] 90%` ## 3. Data layer @@ -133,11 +133,11 @@ Most recent open: ## 9. Delta since previous dashboard -- verified targets: 314 → 314 (+0) · mapped-or-better: 356 → 356 (+0) +- verified targets: 314 → 316 (+2) · mapped-or-better: 356 → 358 (+2) - engine LOC: 58,618 → 58,618 (+0) · test files: 122 → 122 (+0) · checks: 4,244 → 4,244 (+0) -- addresses verified: 1,122 → 1,122 (+0) · recovered layouts: 384 → 384 (+0) · open questions: 26 → 26 (+0) +- addresses verified: 1,122 → 1,139 (+17) · 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 7acf92e..73a88f9 100644 --- a/campaign/board.md +++ b/campaign/board.md @@ -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 | +| 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. | | 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 | +| 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. | | 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 | @@ -395,7 +395,7 @@ Status flow: `backlog → in-progress → mapped → verified` (or `blocked`). | the composer's loop has NEVER RUN | verify | backlog | — | 0% | 2026-09-09 | Stated plainly by the lane: `H_obs = 0` on all six composer calls of all four runs, for two different reasons - on turn 1 the weapon lookups gate it, on turn 15 all three calls were price queries returning nine steps earlier. **Four of eight live sites have never fired; two of nine exits have ever been taken.** D/E/G/H are read, not measured. **THE WORKLOAD: a turn where the AI BUILDS (not prices) a design, on hull size > 0, with a point-defence weapon researched.** SD pushed human-turn15-spyprogram at it and still got only price queries; a follow-up should play that game forward until the AI commits a cruiser. **The instrument for it is already committed and cross-built (`w_alt`/`w_def`) but NO RUN HAS USED IT** | | SD self-reported a rule-21 shaped error | meta | verified | high | 100% | 2026-09-09 | Twice it ran `verify/design-rules/stock_designs.py` inside the **shared** sots-re clone to identify a save's players; **the script writes stock_designs.json as a side effect** and it restored the file with `git checkout -- `. Clean afterwards, but that is exactly rule 21's shape - **and it would not have known if another lane had work in flight there.** Worth generalising: a read-only-looking script can have write side effects, so run analysis tools against a copy, not the shared tree | | REVERSAL: THE TAIL DOES DRAW - W3's risk is REALISED | verify | verified | high | 100% | 2026-09-09 | **Lane AC overturns the "retired by evidence" conclusion I reported.** OnAllCombatDone_Tail consumed **1 RNG word per turn** in the state AC built, against **0 in its own control** (same guest, same build, same probe config, one End Turn on L3's turn-15 save, run immediately after). `TradeManager::Slot13RngCalleeA` - **entered 0 times in every measurement any lane had ever taken** - was entered **1x/turn on both instrumented turns**; CalleeB on the turn the roll succeeded; CreateRaidEncounter once. All probes installed=true, watchpoint canary PASS every turn. **THE RNG LEDGER AND THE STANDALONE'S GENERATOR MODEL NEED EXTENDING BY ONE DRAW SITE** | -| and the gate is a TECH, not container contents | verify | verified | high | 100% | 2026-09-09 | **Neither freighters nor a deployed spy is in the predicate** - AC disassembled the gate chain BEFORE building the state. `ServerTradeManagerImpl::vslot13` does not iterate routes or sectors; it iterates the **FLEET** vector and requires a fleet whose LocID resolves to a node of kind **2 (TradeSector)**, positioned **bit-equal** to the sector, whose owner's bit is set in the sector's **`tscr`** mask. `tscr` was **252 in every corpus save = exactly the six NPC slots**, and AC's own prediction P6 said that excluded real players and the callees would stay 0. **WRONG, AND INSTRUCTIVELY**: the mask is not fixed. Researching **`CCC_ComRaid` (Commerce Raiding, RP 12,000, one turn off CCC_FTLEcon)** takes tscr **252 -> 253** on every sector and sets the ServerPlayer flag serialised as **`CnRad`**. **The game says so in as many words**: before the tech, `Alpha Fleet cannot raid trade sector.`; after it the same click is accepted. **That dialog is the cheapest test of the whole chain** | +| and the gate is a TECH, not container contents | verify | verified | high | 100% | 2026-09-09 | **Neither freighters nor a deployed spy is in the predicate** - AC disassembled the gate chain BEFORE building the state. `ServerTradeManagerImpl::vslot13` does not iterate routes or sectors; it iterates the **FLEET** vector and requires a fleet whose LocID resolves to a node of kind **2 (TradeSector)**, positioned **bit-equal** to the sector, whose owner's bit is set in the sector's **`tscr`** mask. `tscr` was **252 in every corpus save = exactly the six NPC slots**, and AC's own prediction P6 said that excluded real players and the callees would stay 0. **WRONG, AND INSTRUCTIVELY**: the mask is not fixed. Researching **`CCC_ComRaid` (Commerce Raiding, RP 12,000, one turn off CCC_FTLEcon)** takes tscr **252 -> 253** on every sector and sets the ServerPlayer flag serialised as **`CnRad`**. **The game says so in as many words**: before the tech, `Alpha Fleet cannot raid trade sector.`; after it the same click is accepted. **That dialog is the cheapest test of the whole chain** **AND lane AC's "three `rt` route records" is FOUR in `ac-turn22-raider.sav`** (three in `ac-turn20-raider`); corrected by direct read, lane AG. | | CORRECTION: L3's own save falsified the freighter hypothesis | verify | verified | high | 100% | 2026-09-09 | L3 3.5 says "every trade sector has `tsnumflt = 0`". **Wrong about its own save**: `human-turn15-spyprogram.sav` sector TradeID 816 has **`tsnumflt 1`, `tsflt 2736` - the AI's Freighters fleet, on an active route, across all seven of L3's instrumented turns, with the callees at 0.** So "freighters in service" was falsified BEFORE lane AC started. Also **`tsflt` is not a freighter list** - at turn 20 sector 6 reads tsflt 3744, which is AC's WARSHIP fleet. **[CORRECTED 2026-09-09 by the resolver: LANE V2 IS VINDICATED AND WAS MISQUOTED.** `indirect-edges.md` 3.1 lists trade slot 13 as **DRAWING at 0x00820e18 - the exact site that fired.** "Closure 185, draw-free" is V2's row for **slot 9**, the route rebuilder lane L3 trapped. Lane AC carried the number to the wrong slot and I published it. V2's static reading was right all along.] | | spies2 NOT settled, and the deploy state is unreachable in that game | verify | backlog | — | 0% | 2026-09-09 | AC could not deploy a spy and **declined to infer around it**. `ARes2` on ServerSystem **is the asteroid-belt field** (non-zero values 1703-2531 sit inside SYSTEM_MIN/MAX_ASTEROID_RESOURCES 1500-3000); in L3's save only **5 of 28 systems have a belt and NONE is AI-owned** - the textbook deploy target does not exist in that game. **AND L3's "the next condition is spyon" UNDERSTATES IT BY THREE TURNS AND A DETECTION ROLL**: the chain needs `deat != 0`, then `sdet` set by a detection roll on an EARLIER turn, then `turn - sdet >= 3`. **That detection roll is itself an RNG draw, inline in vslot13, not inside P** - so a probe on P reading 0 does not mean the spy half is draw-free | | AC's coverage caveat, stated | verify | backlog | — | 0% | 2026-09-09 | Two turns, one fleet, one sector, one ship. **`Slot13RngCalleeA` has a short-circuit (`design+0x12c > 1` returns true without rolling) that AC's hull did not exercise**, so a different raider could cost **0 words while still firing B**. The one-word-per-turn figure is this raider's, not the site's | @@ -408,3 +408,5 @@ Status flow: `backlog → in-progress → mapped → verified` (or `blocked`). | the coverage ratchet is blind along the value axis | verify | verified | high | 100% | 2026-09-09 | Board row 402 predicted AC's two saves would break the ratchet. **They did not** - both score 100.0% typed and round trip byte-identically, because their novelty (`LocID` at a TradeSector, `tscr = 253`, three `rt` records) lands in fields that were **already typed**. `CoverageArchive` is indexed by **shape**, exactly as lane Z's ledger was indexed by **firing**: both instruments are sound at what they measure and both are blind where the surprises come from. New instrument `DomainArchive` + `test_domains` (engine `e62d974`) measures the other axis | | 234 of 724 typed fields have only ever held one value | verify | verified | high | 100% | 2026-09-09 | The census over 22 saves: **724 fields observed, 490 vary, 234 constant across the whole corpus - 32%**. `tscr` was one of the 234 until a one-turn tech moved it. Clusters: `Sim.Player.dipstats` 11 (no corpus game reached a treaty), `SvSctOb.EncObj` 16 (**no corpus game triggered a random encounter**), `spymgr.spy` 12, `CmbR.CRPlSv2.TacReports` 9 (tactical reports never populated). Individually suspicious because non-zero and non-round: `spy.atto = 3728`, `EncObj.infest.mtrn = INT32_MAX` (a sentinel), `CD.researchBoostFraction = 0.999215`. Rule 23's thin coverage with a number on it, rule 8's eighteen fields generalised. Ratchet at 490 varying; `findings/subsystems/value-domain-census.md` | | the spy sub-object is unexercised: 0 of 22 before lane AS starts | verify | verified | high | 100% | 2026-09-09 | Direct input to lane AS. The census reads `spymgr.spy.sdet = -1` in **all 22 saves**, with `cbh`/`cm`/`cmo`/`deat`/`sdo` all 0 - `sdet = -1` sits where a detection result would go and is a sentinel. So the **corpus count for any spy-path predicate is 0 of 22 before the lane takes a single measurement**, which is exactly what rule 28 requires before a zero may be called a negative. Also gives lane AG a mechanically computable corpus-count column: a predicate resting on any of the 234 constants has count 0 by construction | +| GATE-INDEXED RNG AUDIT: 31 unread-gate rows, 6 new draw sites, the SVSO generator named | control-flow | verified | high | 100% | 2026-09-09 | **Lane AG**, host only, no VM, no Ghidra (`findings/control-flow/gate-indexed-rng-audit.md`). The resolution's §8 item 3, delivered: one row per STATICALLY REACHABLE draw site in `BeginProcessTurn` u `ProcessTurn` u `OnAllCombatDone_Tail` over direct + tail-jump + pinned-virtual edges, each carrying a MEASURED firing or a DECODED PREDICATE on save fields with the 22-save corpus counted against it. **NO TOTAL IS REPORTED - 31 rows (30 distinct sites + one grouped SVSO row) are UNREAD GATES**, and that visible blank column is the deliverable. THREE GATES DECODED. (1) `0x0082cdb8` is the **ADDICTION SPREAD ROLL**: `ServerTradeManagerImpl::vslot15` iterates the SERIALISED route vector (its G1, `BYTE[rt+0x25]!=0`, is byte-for-byte the test `Write` uses to emit an `rt`), and for each route with `trfrs==0 && trtos==0` rolls `Chance(ADDICTION_SPREAD_ODDS=0.2f)` once per species slot the FROM system is addicted to. Corpus **0 of 22**: `nadct` is 0 on all 28 systems of all 22 saves. WRITER FOUND (rule 28 practice 3): `ServerSystem::BeginAddiction 0x0074ef70` has EXACTLY TWO CALLERS, both inside vslot15 - so BLOCK 1 (an INTER-EMPIRE route, `trfow != trtow`, plus bit 4 of `player+0x348+4k`, which `RebuildSpeciesTechFlags` sets from the per-species ADDICT TECH table `FUN_0053b620`) must seed it before the roll can ever happen. Every corpus route is intra-AI (`trfow==trtow==1`), so neither block has run. (2) `0x0088dc43` is the SPY COUNTER-MISSION roll, `p = spy->cmo` starting at 0.2f and gaining 0.2f per failure - so `Chance` costs 0 WORDS from the fifth turn on (`p>=1` early-out), a stage costs at most 4 words. Predicate `deat != 0 AND cm in {1..4}`; corpus **0 of 22** by construction (both fields are among the census's 234 constants). (3) `0x0088b613` is the RAID TARGET PICK, and **`RNG_NextInt` HAS NO ZERO-BOUND EARLY-OUT** (read in full: mask smear then an unconditional draw loop) - so a single-candidate list still costs a word, which PROVES lane AC's `B` returned BEFORE the draw. The failed conjunct is named: AC's raider sat on sector `TradeID 832`, member systems 13/23/0/3, while the four `rt` records' `trfr` are 18/16/14/20, lying in sectors 816/816/768/752. **NEXT WORKLOAD: park the raider on sector 816 instead of 832** - two candidates, `NextInt(bound=1)` cannot reject, `tsct=2` is the AI's bit so no `FtFlg&0x800` is needed, and the AI's Freighters fleet is already there to test the doubled odds. SIX NEW DRAW SITES the firing-indexed table structurally could not hold: five behind `CreateRaidEncounter`'s empty-candidate-list gate (`0x0088f1eb`, `0x0088f356`, `0x0083b460`, `0x00848fd9`, `0x00820c1b` via `0x00892640`) and `0x00820c1b` AGAIN from `Slot13RngCalleeB` at `0x0088b62c`. **SVSO GENERATOR CLOSED**: all 17 overrides that can draw use `GetGame()->+0x16c` = `*(void**)0x00b29f9c` -> +0x16c, the STRATEGIC generator - V2 §4.3 caution (a) retired. Rule 17 bit twice more (`ServerSpyManager::vslot14` real end 0x0088dd2b vs Ghidra's 0x0088dd03, jump table past it) | +| CORRECTION: `CreateRaidEncounter` success is not 0-or-1 words | control-flow | verified | high | 100% | 2026-09-09 | `tail-rng-ledger.md` 11.1's **"a success costs 0 or 1 further word"** is right about the **corpus** and wrong about the **code**. A success with a **non-empty** candidate list reaches **five** further draw sites through `0x00892640` (`0x0088f1eb`, `0x0088f356`, `0x0083b460`, `0x00848fd9`, `0x00820c1b`), each a `NextInt` with its own rejection loop. Lane AG 5.1. The firing-indexed table could not hold these because no corpus state has ever had a non-empty list | diff --git a/findings/control-flow/tail-rng-ledger.md b/findings/control-flow/tail-rng-ledger.md index 9e153c3..a7de5d4 100644 --- a/findings/control-flow/tail-rng-ledger.md +++ b/findings/control-flow/tail-rng-ledger.md @@ -348,6 +348,8 @@ would have said so by construction rather than by anyone noticing. §9.3. * ~~`FUN_00893290` is unidentified.~~ **Identified — §11.1.** What is still open there is whether a *successful* raid roll costs a further word (0 or 1, undetermined on a workload where none succeeded). + **ANSWERED 2026-09-09 (lane AG): neither — a success with a non-empty candidate list reaches five + further draw sites, unbounded above. The rolls that "succeeded" here succeeded into an EMPTY list.** * **A turn with a genuinely empty encounter vector was not observed** (§4). Both saves produce exactly one sighting encounter on every turn. The tail-runs-every-turn claim is settled; the no-encounters variant is still an inference, now a much narrower one. @@ -615,6 +617,15 @@ A word count cannot separate those, and separating them is the cheapest experime > (`findings/control-flow/tail-probes.md` §4). An entry counter on 0x008938a0 recorded **2 calls on > Zuul turn 24 and 1 on turn 25, all drawing 0 words**: the rolls *do* succeed and **the candidate > list is empty**. A successful raid roll costs 0 further words on our corpus. +> +> **CORRECTED IN PLACE 2026-09-09 by lane AG (rule 11).** That sentence is right about the **corpus** +> and wrong about the **code**, and the qualifier "on our corpus" is doing more work than it looks. +> A success with a **non-empty** candidate list reaches **five** further draw sites through +> `0x00892640` — `0x0088f1eb`, `0x0088f356`, `0x0083b460`, `0x00848fd9`, `0x00820c1b` — each a +> `NextInt` with its own rejection loop, so the cost is unbounded above, not "0 or 1". The reason no +> one saw them is structural: **no state we own has ever had a non-empty candidate list**, and a +> firing-indexed ledger cannot contain a site that never fired. See +> `gate-indexed-rng-audit.md` §5.1 and §3.3. > (a) "three quiet turns in a row is unremarkable" understates it by three orders of magnitude — > 11% is the probability of a quiet *turn* (`0.8^8 × 0.95^8`), so three in a row is 0.14%, about > 1 in 720. (b) The caller list below is **incomplete**: `0x007d5150`, the other subtree of diff --git a/ghidra/generated/sots_addresses.h b/ghidra/generated/sots_addresses.h index 3d7d269..f533f38 100644 --- a/ghidra/generated/sots_addresses.h +++ b/ghidra/generated/sots_addresses.h @@ -1,5 +1,5 @@ // GENERATED — do not edit. Facts about Sword of the Stars.exe (GOG 1.8.1). -// Source: sots-re ghidra/addresses.json @ 9e39b00, generated 2026-09-08 by tools/gen_addresses.py +// Source: sots-re ghidra/addresses.json @ 48db3cc, generated 2026-09-08 by tools/gen_addresses.py // Runtime address = (uintptr_t)GetModuleHandle(NULL) + RVA (the exe is ASLR-relocated). #pragma once #include