424 KiB
424 KiB
Campaign board
Status flow: backlog → in-progress → mapped → verified (or blocked).
verified requires re-verifier evidence. Maintained by re-quartermaster.
| Target | Type | Status | Conf | Coverage | Updated | Notes |
|---|---|---|---|---|---|---|
| RTTI class inventory | meta | mapped | high | 100% | 2026-09-07 | findings/objects/00-inventory.md — 1924 types, engine=Mars |
.gob format |
subsystem | mapped | high | 100% | 2026-09-07 | renamed uncompressed ZIP (community-known) |
Mars::AppStartup (entry) |
control-flow | mapped | high | 100% | 2026-09-07 | entry 0x00925794 -> WinMain 0x0089dd30 -> DemoApp ctor 0x0089c950 -> Mars::Application::Initialize 0x008a0e50 (config/affinity, D3D9, window, sound thread, OnStartup -> net thread). findings/control-flow/turn-spine.md |
| main loop / tick dispatch | control-flow | mapped | high | 90% | 2026-09-07 | Application::Run 0x0089f5b0: FrameTimer, PanelManager, DemoApp::OnUpdate 0x00898800 / OnTick 0x0089a640 / OnRender 0x00899210. Turn pipeline: EndTurn 0x00783be0 -> BeginProcessTurn 0x007d98e0 -> StrategyServer::ProcessTurn 0x007dc6c0 -> RunCombatRound 0x007cbe80 -> SETurnResults -> ResumePlaying 0x007ddc90. Lockstep on every machine |
Game::ClientPlayer / AIPlayer |
object | verified | high | 100% | 2026-09-07 | ServerPlayer confirmed strict vs real saves (turns 1-3) |
Game::StarSystem |
object | verified | high | 100% | 2026-09-07 | ServerSystem confirmed strict vs real saves; NVO.indi unconditional; haltv bool; cp1252 names |
Game::Planet / DOPlanet |
object | mapped | high | 100% | 2026-09-07 | Planet : Actor is a RENDER actor, not streamed; all colony state is in ServerSystem (+PlayerView in NVs map) |
Game::ShipDesignDef / DesignList |
object | verified | high | 90% | 2026-09-07 | engine game/design merged: rules + stats in C++; 127/127, 197/197 DOpts, gating 121/127 (hidden riders warn), oracle-identical incl. 9,673 stats; 43 unit tests |
Game::FleetLayout |
object | verified | high | 100% | 2026-09-07 | StarFleet/StarShip confirmed strict vs real saves; PrisonerHold PrNSp gated on PrMax>0 |
| Mars entity chain | object | mapped | high | 100% | 2026-09-07 | EntityBase@0x00a36fd0 (10) -> PhysicalEntity@0x00a3710c (12) -> TargetableEntity -> Actor@0x009e2114 (36); mixins RigidBody/Body/NetworkObject/HandleObject/IScriptable |
Game::GameCombatSim + CombatCommand* |
subsystem | backlog | — | 0% | 2026-09-07 | tactical combat (~40 command types) |
| D3D9 device init | subsystem | backlog | — | 0% | 2026-09-07 | Mars::DrawDevice/Camera → Direct3DCreate9/CreateDevice |
Mars::Buffer::gobio |
subsystem | verified | — | 0% | 2026-09-08 | SUPERSEDED (see engine: mars/vfs row). .gob I/O; anchors the verify/ parser |
| serializable-types vs save structs | verify | mapped | high | 100% | 2026-09-07 | findings/objects/save-editor-structs.md (~130 structs, exact order); verifier: cross-check vs binary next |
| battle-load path | subsystem | mapped | med | 60% | 2026-09-07 | mitigation = affinity pin FUN_0089ee70; sim+load on main thread; see thread-contention row (parked) |
| class hierarchy + key vftables | meta | mapped | high | 100% | 2026-09-07 | findings/objects/ghidra-recon.md - RTTI Base_Class_Array read directly; vftables for 8 core classes |
| data-model (.gob data files) | subsystem | mapped | high | 100% | 2026-09-07 | findings/subsystems/data-model.md - tech/weapons/sections/races/AI all data-driven |
| string / config intel | meta | mapped | high | 100% | 2026-09-07 | findings/subsystems/strings-and-config.md |
| Mars brace-block parser | subsystem | verified | high | 100% | 2026-09-07 | verify/parsers/ (mars_data, flat_kv, manifest, effect_txt, verify.py): 1595/1595 files parse, 0 dangling cross-links; catalogs in verify/results/data-catalogs/ |
| networking (SNM/FNM + GameSpy) | subsystem | mapped | — | 0% | 2026-09-08 | SUPERSEDED (lane G2: direct IP joins without GameSpy; identity + hosts list recovered). ~60 SNM strategy msgs, FNM file-xfer/host-migration; lockstep sim |
| battle-load: thread contention | subsystem | blocked | med | 60% | 2026-09-07 | PARKED (not on reimpl critical path). Profile: findings/subsystems/battle-load-profile.md. Combat not reached (no ships/range/encounters); audio thread inert w/o audio device; candidate = TryEnterCriticalSection->Sleep(1000) job poll @0x0071ea60 + 15.6ms timer. Resume recipe + tools in verify/harness/profiling/ |
| UI screen & flow map | meta | mapped | high | 100% | 2026-09-07 | findings/subsystems/ui-screen-map.md - 36 screens; screens are C++ on Mars controls (NOT data); turn state machine recovered |
Game::StrategyServer (sim block) |
object | verified | high | 100% | 2026-09-07 | full member table (47 rows) confirmed vs real saves under strict parse incl. ID lists, GOWinPly, invasions, zdsc pairs, SvSctOb (present in all 3 saves) |
| stream primitive API | subsystem | mapped | high | 100% | 2026-09-07 | IStreamable vft: +0x18 string, +0x1c bool, +0x20 float, +0x24 int, +0x28 nested, +0x30 raw; FUN_00816490 = NetworkObject handle id |
| real save for verification | verify | verified | high | 100% | 2026-09-07 | 6 saves, 3 distinct turn states in verify/results/saves/ (byte-deterministic per state). Game runs on VM140 via DXVK 3.1 + lavapipe; recipe findings/subsystems/running-the-game.md |
| save_reader.py | verify | verified | high | 100% | 2026-09-07 | --strict exit 0 on all 3 real saves; 0 resyncs / 0 hint-failures; only raw = RNG blob (2503 B). 29 tests. SAVE_FORMAT.md confirmed |
| Ghidra type write-back | meta | verified | high | 100% | 2026-09-07 | structs saved in project (ServerSystem 87f, ServerPlayer 110f, StarFleet, StarShip, StrategyServer partial, 22 nested); 52 serializers + primitives + ~60 spine fns renamed; decompile shows field names |
| strategic turn internals (economy/research/colony/movement/diplomacy) | subsystem | mapped | high | 90% | 2026-09-07 | findings/subsystems/strategic-turn-internals.md: budget/RP/trade/bankruptcy formulas, research rolls (unlisted race=1.0, Zuul x2), colony growth, movement, RNG=MT19937; ~600 g_KEY labels in Ghidra |
| P2-M0 shim bootstrap | phase2 | verified | high | 100% | 2026-09-07 | sots-engine 5f5bc41: proxy binkw32.dll (66 exports identical), MinHook, asm-stub trace hook on Application::Initialize; menu reached; evidence verify/results/shim/. Initialize signature unverified -> asm stubs only |
| P2-M1 flat-KV config loader | phase2 | verified | high | 100% | 2026-09-08 | LIVE: GlobalConsts::LoadFile hooked; trace 19 calls/1088 regions (tracecmp 0); compare 19/19, 0 divergences; REPLACE loaded all 19 files and End-Turn autosaves hashed exactly to the oracle; offline replay 0. 3 extra parsers found (string/vec3/rect), pi/180 is a double. sots-engine 8b231c0, docs/M1.md; traces verify/traces/m1-* |
| P2-M2 manifest/id registry | phase2 | verified | high | 80% | 2026-09-08 | LIVE: trace 22 calls (tracecmp 0); weapons compare 1/1 0 divergences; REPLACE+oracle byte-identical with our weapon loader feeding the game. Proves 123 weapon ids == manifest, DELETED lines are comments, dict sorted by _stricmp on name, +0x14 layout fix correct. GAP: section dictionary is TRACE-ONLY - ours crashes the engine in compare mode (leads in docs/M2.md; hooking LoadSection is the next boundary) |
| P2-M3 Mars brace-block parser | phase2 | verified | — | 0% | 2026-09-08 | SUPERSEDED (engine mars/parse, oracle 1531/1531). Mars::Script pull tokenizer (Open 0x008cd7d0, ReadToken 0x008cd2f0, Next 0x008cd3e0, SkipBlock 0x008cd4b0); whitespace-only delimiting, quotes " ' ` no escapes, EOF ends, stray } ignored, LAST PAIR W/O TRAILING NEWLINE DROPPED; consumers = _stricmp if/else chains (WeaponDef::ParseScript 0x00599070, SectionDef::ParseScript 0x005744e0, MasterTechTree::ParseTech 0x0058b050). .effect = Mars::TextFileStream (Open 0x008cfb90) |
| P2-M4 gobio VFS read | phase2 | verified | — | 0% | 2026-09-08 | SUPERSEDED (engine mars/vfs, 10268 files CRC-clean). choke point: bool __cdecl gobio::ReadFile(const char*, IBuffer**) 0x008d5140; FileSystemSet ctor 0x008d6d60 pushes NativeFileSystem(".") FIRST then sots.ini [Modules] Mount (fallback sots.gob, sots_local_en.gob); zip lookup case+slash-insensitive 0x008d4a50 -> matches mars/vfs NativeFirst |
| loader prototypes (Ghidra) | meta | verified | high | 100% | 2026-09-07 | findings/subsystems/loader-prototypes.md: 80 entries (79 verified) in ghidra/addresses.json; Initialize = bool(this, AppStartup*) RET 4 (M0 crash explained); 93 fns renamed + 9 structs in Ghidra |
| compare harness | verify | mapped | high | 100% | 2026-09-07 | verify/harness/compare/: TRACE_FORMAT.md, tracecmp.py, replay, oracle bridge |
| engine: mars/parse | engine | verified | high | 100% | 2026-09-07 | sots-engine: brace-block + .effect readers; oracle 1531/1531; cross-compiles i686; wired into host+shim builds |
| 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 |
| 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). |
| engine: shim trace/compare emitter | engine | verified | high | 100% | 2026-09-07 | merged (sots-engine 9e110b4): emitter byte-exact vs mkfixture, tracer, snapshot/diff, Hook; ctest 18/18; tracecmp exits 0/1/2 as specified; shim links |
| engine parity: first-wins keys + tokenizer rules | engine | verified | high | 100% | 2026-09-07 | merged: Mars::Script tokenizer rules, first-wins keys, trailing-pair drop in mars/parse+mars/text AND Python oracle; oracles 1531/1531, 64/64; only real-data effects: 2 dropped trailing pairs (engine uses defaults), systemnames.txt nesting |
| save-format tag corrections | verify | verified | high | 100% | 2026-09-07 | byte-confirmed at offsets (otnF x62, nextid, FAIDes/DHide/DWep/DName x43, ords, wpts, paths); reader A()-matches them; 36 tests; strict 3/3, infos 259->197; SAVE_FORMAT §10 changelog |
| tech effects (code-defined) | subsystem | mapped | high | 90% | 2026-09-07 | findings/subsystems/tech-effects.md: g_TechIdNames 196 @0x009ff9e4 (TechId=10000+i), effects in ServerPlayer::OnTechResearched 0x00891790; SpeciesDef table 0x00b10a00; 36 strategic / ~45 gates / ~115 none. Feeds game/sim + game/data |
| strategic formula gaps | subsystem | verified | high | 100% | 2026-09-07 | findings/subsystems/formula-gaps.md: all 8 answered with code (bankruptcy 3.3 on BnkPr, linear hazard, money tail, pbon, slider, [6] net, no speed clamp, decay hits current) |
| engine: game/design | engine | verified | high | 100% | 2026-09-07 | merged; see ShipDesignDef row |
| P2-B1 ComputeBudget (behavioral) | phase2 | verified | med | 75% | 2026-09-08 | LIVE + GREEN on declared regions (4,437 compares, 0 divergences; replace-mode oracle byte-identical). Corrections: out-param int[22]; researchMoneyKept gated on a research target. QUALIFIED BY THE HARNESS AUDIT: replace mode runs the ORIGINAL A SECOND TIME to harvest slots -> a real per-turn double effect (ComputeOutput repairs ships in orbit) that no region reaches - UNRESOLVED; the over-budget int at Budget+0x64 was captured only as an argument and args are never compared. Golden trace needs recapture with guards. Coverage gaps: 8 slots always 0 (expenses/aid/debt) . RECAPTURED WITH GUARDS 2026-09-08 (lane R): 4284 compared, 0 diverged, exit 0; budget_object guard 0 undeclared writes, so Budget+0x64 never took a different value (weaker than 'never written'). COVERAGE IS NARROWER THAN THE NUMBER: only 20 distinct (player, output) states in 4284 calls (4278 are the UI polling one player; the turn pass is ~8 calls), and 13 of the 22 slots are 0 on EVERY call - including 5 of the 6 declared input-boundary slots, which therefore prove nothing twice over. 8 modelled slots ever carry a value |
| P2-B2 OnTechResearched (behavioral) | phase2 | verified | high | 80% | 2026-09-08 | LIVE: 3 completions compared, 0 divergences (incl. one tech absent from the effects table = tail-only, the case an early return would have broken). float32 CONFIRMED ON THE GAME bit-for-bit (con_mod 1.0->0.899999976 = (float)(x+(double)0.1f)). Found B3's rolling effect: ServerPlayer::RollResearchEvent draws one NextFloat at the top of the callback - modelled + RNG region declared, but the branch never fired here so NOT behaviourally proven. KNOWN GAP: ours posts no events and the player event list is still undeclared, so the clean compare bounds the economy fields only and the replace/oracle pass was deliberately run on a no-completion turn (weak check) . RECAPTURED WITH GUARDS 2026-09-08 (lane R): 2 calls, 0 divergences; the player guard reports player+0x2b0 (EvNxID) on BOTH calls, so 'ours posts no events' is now measured, not assumed. RollResearchEvent branch FIRED and matched (see its row). Also caught an undeclared vector<ObservedTech> append at player+0x274. Reachable only after 5 End Turns from ref-turn2 |
| 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 |
| 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 <text> 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. |
| 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 |
| 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 |
| section-loader compare crash | verify | backlog | — | 0% | 2026-09-08 | SectionDictionary compare crashes the engine while the identical weapon path succeeds -> fault is in re-running LoadSection, not the manifest reader. Next boundary: hook LoadSection itself. docs/M2.md has 3 ranked leads |
| P2-B4 colony + movement (behavioral) | phase2 | mapped | med | 70% | 2026-09-08 | LIVE: 36 calls compared, 0 divergences, tracecmp exit 0. Scout's headline: RNG left-delta 0 and mt hash identical on ALL 28 systems (only ProcessRebellion draws; none fired) - fpu_cw 0x127f confirmed. One real fleet move reproduced bit-for-bit; PlanFleetMovement's schedule matched. 3 prototypes + 6 helpers VERIFIED and written back to Ghidra; 22 formula corrections. THREE hook bugs found by reading the trace, not the verdict (stale args from describe_args-before-regions; StrategyServer has TWO bases 4 bytes apart; off_Fleets was a Ghidra-base number used as raw) - each would have given a clean compare that checked nothing. COVERAGE IS THIN: only 3 owned systems, 1 moving fleet, gate traffic all-zero; bats2, plague, rebellion, slaves, terraform, jumps, arrivals untested. No replace mode (input boundary). VM released: main menu, hooks=trace, build b4-fix2-20260908T0615Z . QUALIFIED 2026-09-08 by lane R: with 45 MoveFleet calls over 5 turns instead of 7 over 1, 8 of the 15 moving calls diverge by 1 ULP of position. B4's clean verdict was a ONE-SAMPLE verdict - fleet 34 (its only mover) still matches bit-for-bit; the bug appears once other fleets move. ProcessTurn recaptured at 140 calls / 0 divergences, but only ntdev (15x) and rcex (7x) ever moved: everything else was byte-identical on all 140, so the verdict bounds two counters. Guard found the AI home system's fleet vector growing every turn (the build queue emitting a ship) |
| harness gap: undeclared side-effect lists | verify | verified | high | 100% | 2026-09-08 | FIXED STRUCTURALLY (engine 3f0721f+): compile-time-required Coverage on every descriptor (a hook without one does not compile); Guard regions that catch AND localise undeclared writes (names player+0x2b0, not 'the hash moved'); replace mode now emits records; tracecmp prints coverage on every report + --strict-coverage. A hook claiming 'complete' while a guard caught an undeclared write now counts as a DIVERGENCE. Audit found 23 undeclared side effects: docs/harness-audit.md |
| RollResearchEvent draw (behavioural) | verify | verified | high | 100% | 2026-09-08 | CLOSED by lane R. On turn 6 (IND_TRKSTL, tech 10094) research_roll_pending was true going in; the original drew exactly one word (rng left 375->374, next_index 249->250) and cleared the flag, and ours reproduced both bit-for-bit on the scratch generator. 0 divergences on that call. Needs 5 End Turns from ref-turn2 to reach - the reference turn has no completion at all |
| golden-trace recapture (post-guards) | verify | verified | high | 100% | 2026-09-08 | DONE on the live game, build recap-7584bad-20260908T0615Z (NO source change needed - the audit's machinery did all of it). B3 ProcessResearch: the defect is VISIBLE - side.events.after.v.next_id orig=4 ours=3, one divergent call of 3 on the reference turn and its ONLY divergent field; bit-for-bit the EvNxID 4->3 that previously needed a 609 KB save diff. Over 5 turns 15 calls / 3 diverged, RNG matched 15/15, and the two completion calls miss TWO event ids each. B1 ComputeBudget: verdict held - 4284 compared, 0 diverged, exit 0, budget_object guard caught 0 undeclared writes (Budget+0x64 never changed value). MoveFleet: 8 of 45 diverge by 1 ULP of position (new; see its own row). First guarded captures for OnTechResearched (2 calls), ServerSystem::ProcessTurn (140 calls) and MoveFleet (45). Guards mapped SetResearched live (ConMod[0..2], OutMod, PopMod, ResTNm, TechTree+0x20 order counter) and found an UNDECLARED vector<ObservedTech> otch append at player+0x274. Oracle held on every run's first End Turn. Report findings/subsystems/golden-trace-recapture.md; engine docs/R-recapture.md; traces verify/traces/recap-*, reports verify/results/compare/recap-*. sots-engine branch wip/recapture e50d5e5 (merged with main 82ef52f; ctest 32/32, clean-room OK) |
| MoveFleet position rounding (1 ULP) | verify | verified | high | 100% | 2026-09-08 | CLOSED by lane M. Mechanism read off the instruction stream, not fitted: the engine's Mars_Vec3_Normalize (0x00422520, 123 callers) narrows to float32 FOUR times - sumsq = f32(x*x+y*y+z*z) (products/adds stay in 53-bit regs, only the SUM is stored), len = f32(sqrt(sumsq)), inv = f32(1.0/len) a RECIPROCAL that is MULTIPLIED through rather than three divides, and dir.c = f32(delta.c*inv); and MoveFleet stores each dest.c - pos.c BACK TO A FLOAT32 SLOT before calling it, and takes the leg distance from that same call's return value. ours did all of it in double. The position tail (f32(pos + f32(dir*move))) was already right, which is exactly why the error was a constant ABSOLUTE ~1.2e-7. Confirmed OFFLINE first (an arrival copies the destination verbatim, so calls 115/155 hand you fleet 34's and fleet 50's exact float32 destinations = 8 fully determined legs; the 5-narrowing model reproduces the ORIGINAL bit-for-bit on all 8, the old double model reproduces ours on the 3 divergent ones), then LIVE: control run 8/45 diverged exit 1, fixed run 0/45 diverged exit 0, with identical args, identical pos.before and identical ORIGINAL pos.after on all 45 calls. Report findings/subsystems/movefleet-position-rounding.md, engine docs/M-movefleet.md, branch wip/movefleet 2aa8cba |
| undeclared ObservedTech append | verify | verified | high | 90% | 2026-09-08 | NEW (lane R). A tech completion grows vector<ObservedTech> otch at ServerPlayer+0x274 (all three vector words move = a realloc). Seen as an undeclared write by BOTH the ProcessResearch player guard and the OnTechResearched player guard. It is serialized ServerPlayer state and it is in NO coverage note anywhere - a third list append in the same neighbourhood as the event list. B3's replace oracle never saw it because turn 1 of ref-turn2 has no completion MODELLED AND LIVE-VERIFIED by lane U 2026-09-08. ours now models the append DECISION - it scans the owner's otch element NAMES pre-call (the original appends during the call, so a scan taken afterwards would find the tech present and "agree" with a count it never computed) and reproduces RecordObservedTech's de-duplication by name, then moves the SCRATCH header's byte span by one 0x2c element per append. observed_techs.bytes now matches on every call in all three runs (440->484, 484->528 on the five-turn; 396->440, 440->484 on the Zuul). STILL NOT MODELLED: the element's own fields (turn_first/turn_last/detected/name/with) - no region can see them |
| unnamed offsets from guard hits | verify | backlog | — | 0% | 2026-09-08 | NEW (lane R). Three spans the guards report every run and no addresses.json entry names: ServerSystem+0xd8 (1 B) and ServerSystem+0x238 (4 B) - written by the AI home system on every colony turn, alongside the fleet-vector growth; StarFleet+0xdc (1 B) - written on every moving MoveFleet call, just past Speed (FPsp2 @0xd8). Cheap wins for the contract |
| waypoint types 2-5 have no coverage | verify | mapped | — | 0% | 2026-09-08 | SUPERSEDED (superseded by lane P2 species table). NEW (lane M, promoted from a coverage line to its own row because it is now the biggest gap in MoveFleet). Types 2 (node line), 3 (node route), 4 (gate teleport) and 5 (probabilistic jump) have NEVER fired in any capture, and the node-line step is WRONG BY CONSTRUCTION - sim::NodeLineStep and sim::BuildStutterSegments are written and host-tested but are NOT wired into the hook, which steps every waypoint type as speed x dt. ref-turn2 structurally cannot exercise them: lane M held the VM and tried. The only mover in that save is the AI, which travels straight runs; the player that would travel a node line has DE 00 CR 00 DN 00 at its home system (screenshot verify/results/shim/mf-human-home-no-ships.png), so Move/Manage Fleets are greyed out on every turn. Needs a ship built over several turns, or - much cheaper - a PURPOSE-BUILT SAVE with a fleet already in orbit next to a node line. Same save would unblock the gate-traffic and probabilistic-jump rows. Also owed on that path: sim::Distance is still plain double, and Mars_Vec3_Length (0x004224b0) says every vector length in the engine is float32-narrowed twice, so the stutter geometry is probably 1 ULP out the same way the position update was - deliberately left alone by lane M because there is no behavioural evidence to correct it against |
| ref-turn2 has no tech completion | meta | verified | high | 100% | 2026-09-08 | TRAP for anyone writing a workload (lane R). The documented one-End-Turn recipe produces zero OnTechResearched calls - an empty log that still passes. It takes 5 End Turns (to turn 7) to reach a completion. Also: only the FIRST End Turn is reproducible - it hashed to the oracle on all four runs and its research calls reproduce docs/B3.md exactly, but from turn 4 the AI picks a different research target than B3 recorded while the point totals stay nearly identical. Treat anything past turn 1 as a run, not the run |
| B1 replace double-run | verify | backlog | — | 0% | 2026-09-08 | ComputeBudget replace mode runs the original a second time to harvest budget slots; ComputeOutput repairs ships in orbit as a side effect, so this is a real per-turn double effect on objects no region covers. Needs a design fix (harvest without re-running, or declare+revert) |
| ReVa MCP link drop (workaround) | meta | verified | high | 100% | 2026-09-08 | The ReVa MCP client link dropped mid-session while the CT111 server stayed healthy (systemd active, :8080 listening, valid key -> 200). tools/reva_call.py <tool> '<json>' calls the same server over plain HTTP (initialize -> notifications/initialized -> tools/call; replies are SSE with a leading id: line, initialize is plain JSON). Key is NEVER stored in the repo: $REVA_KEY, else ~/.claude.json, else ssh to the CT properties file. Use this whenever mcp__plugin_ReVa_ReVa__* is unavailable |
| event posting API | subsystem | mapped | high | 90% | 2026-09-08 | RECOVERED (lane E, findings/subsystems/events.md). Container: EventStorage embedded at ServerPlayer+0x29c (0x1c), EvNxID at +0x14 = player+0x2b0 — exactly the guard's byte run. Nested vector<TurnEvents{int EvTurn; vector<PlayerEvent>}>, record 0x74 B, tags EvEID EvDsc EvMsg EvImg EvLoc EvPos EvAct EvCID; layout confirmed field-by-field against turn3-state.sav, which CONTAINS the overbudget record. Entry point int __thiscall EventStorage::PostEvent(this, string BYVAL, string BYVAL, obj*, Vector3*, turn, const char* img, int act) 0x008862b0 RET 0x4c — 161 call sites in 113 functions, the whole sim's event API. B3 defect fully explained: 0x00587b97, in the completion-roll-FAILED branch under !wasDone && nowDone && owner. 3 note corrections (EvPos is FLT_MAX not inf; the save array is turn-bucketed not flat; TECHS_UNLOCKED has no parent clause). 56 entries in addresses.json; 11 prototypes + 13 labels + 12 comments + 2 structs written back to Ghidra. Engine: sots-engine branch wip/events a7348be, src/game/events + 112 checks, ctest 32/32. NOT YET WIRED INTO A HOOK — see docs/E-events.md for the proposed region/Coverage change |
| state-checksum replay harness | verify | verified | high | 90% | 2026-09-08 | Lane C: verify/state-checksum/ (tool, 38 tests, STATE_CHECKSUM.md, evidence in verify/results/state-checksum/). Whole-state digest tree; coverage is PROVED by byte-for-byte re-serialisation, not declared - the answer to empty-region-set green verdicts. Localises: the known load->re-save delta reports as exactly 5 named leaves (/Sim/players/Player[496 "Singularity"]/Status: 4 -> 0, /Summary/Checksum), and one real End Turn as 108 attributed diffs. All 10 saves STABLE + COVERED. Float policy = exact bits by default, canonical for -0.0/NaN only, tolerance deliberately not a hashing mode (it lives in --ulps on the differ); corpus has 0 NaN/-0.0/subnormals so canonical is a no-op today. Chain record/verify validated on the real turn1-3 saves. REMAINING 10%: the VM-driven replay loop is designed (§5) but UNRUN - needs the VM holder. Open question named in §3.5 with the experiment that settles it (force fpu_cw 0x027f/0x127f/0x137f across End Turn, checksum the three autosaves) §3.5 CLOSED 2026-09-08 by lane F (see the fpu_cw row): measured, not assumed — 53-bit == 64-bit, so floats=bits costs the SSE port nothing; 24-bit and round-up each name one witness. The tool localised both to single leaves out of 35,394, with coverage PROVED on all 8 new saves. |
| MoveFleet position ULP divergence | phase2 | verified | high | 100% | 2026-09-08 | DONE (lane M). First arithmetic divergence caught by BEHAVIOURAL compare rather than static reading, and it is fixed by matching the original's precision sequence rather than by fitting numbers - see the MoveFleet position rounding (1 ULP) row for the mechanism. Live 8 -> 0 on the same 45 calls, control run included so the before/after is this lane's own measurement. COVERAGE IS UNCHANGED AND STILL THIN: 15 of 45 calls move and all 15 are the same straight-run waypoint type. Waypoint types 2-5 were ATTEMPTED and could NOT be reached - the only player that would travel a node line has DE 00 CR 00 DN 00 at its home system on this save, so its Move/Manage Fleets buttons are greyed out every turn and there is literally nothing to send along the node lines the map draws. Reaching them needs a ship BUILT over several turns, or (cheaper) a purpose-built save that starts with a fleet in orbit beside a node line. The node-line step is still wrong by construction: NodeLineStep/BuildStutterSegments exist and are unit-tested but are not wired into the hook |
| ObservedTech append (undeclared) | verify | verified | high | 90% | 2026-09-08 | Lane R's guards caught a vector append at player+0x274 during SetResearched. It is SERIALIZED state and appears in NO coverage note anywhere - found only because guards localise rather than just flag a moved hash. Needs a declared region + a model in ours MODELLED AND LIVE-VERIFIED by lane U 2026-09-08. ours now models the append DECISION - it scans the owner's otch element NAMES pre-call (the original appends during the call, so a scan taken afterwards would find the tech present and "agree" with a count it never computed) and reproduces RecordObservedTech's de-duplication by name, then moves the SCRATCH header's byte span by one 0x2c element per append. observed_techs.bytes now matches on every call in all three runs (440->484, 484->528 on the five-turn; 396->440, 440->484 on the Zuul). STILL NOT MODELLED: the element's own fields (turn_first/turn_last/detected/name/with) - no region can see them |
| fpu_cw sensitivity experiment | verify | verified | high | 100% | 2026-09-08 | DONE (lane F): 53-bit vs 64-bit x87 makes NO difference — the SSE port has no double-rounding budget to preserve, STATE_CHECKSUM.md §3.5 CLOSED. 7 End Turns from ref-turn2, 6 control words, whole-state checksum on each. stock / 0x027f / 0x127f / 0x137f (64-bit) all give 978041ac… identical across all 35,394 leaves. Two settings DO move state, each reproduced on a repeat run: 0x007f (true 24-bit) -> /Sim/systems/Sys[112 "Gamma Cephei"]/Pop2/PopG/PopC 540000000->540000002 (+derived Summary/Checksum); 0x1a7f (53-bit, round-UP) -> /Sim/fleets/Flt[34 "Beta Fleet"]/Pos/.[0] and /Pos/.[2], 1 ULP each. So the port must hold intermediates at 53 bits (never compute a chain in float) and use round-to-nearest — both SSE defaults, now measured. THE BRIEFED TRIPLE WAS UNDER-POWERED: 0x027f is 53-bit (differs from 0x127f only in bit 12, infinity control, ignored since the 387) and 0x137f is 64-bit, not a rounding change — run as written, all three come back identical and would have 'proved' something false. PC=bits 8-9, RC=bits 10-11. Setting PROVEN to hold: read-back at each force + 38 in-pipeline hook samples per run spanning turn phases 4/6/8, all at the forced value (verify/results/fpu-cw/cw-census.txt). Mars::Application::Run calls _controlfp(0x50000,0x3070300) at 0x0089f606 EVERY FRAME = 0x127f, so forcing at EndTurn is wiped before BeginProcessTurn; BeginProcessTurn is the point that works. TRAP: under 24-bit the CRT's own %g rendering degrades, so trace TEXT is not a comparison surface — use verify/fpu-cw/trace_bitdiff.py. findings/subsystems/fpu-precision-sensitivity.md |
| Summary.Checksum algorithm | objects | blocked | — | 0% | 2026-09-08 | Lane C RULED OUT two candidates so nobody repeats them: NOT a byte sum over the inflated stream, NOT a sum over the int leaves. Each is consistent with the -16 re-save delta but leaves no constant residual across turns |
| event posting in ours | phase2 | verified | — | 0% | 2026-09-08 | SUPERSEDED (lanes P/V/U: prediction held, cascade closed). Lane P: make ours actually post events so ProcessResearch's side.events.after.v.next_id 4->3 divergence closes. Converts harness-audit row 1 from known-defect to checked, and unbounds B2/B3 whose clean compares currently cover economy fields only |
LAB RULE: no git add -A in sots-re while lanes run |
meta | verified | high | 100% | 2026-09-08 | MY error, caught by lane M: an integrator git add -A in the SHARED sots-re clone swept a running lane's in-progress files into commit 9d385a7 mid-run (remainder landed in f5b37c2). Nothing was lost, but authorship and atomicity were. RULE: while any lane is live, the integrator stages sots-re by explicit PATH only (git add campaign/board.md campaign/DASHBOARD.md), never -A. Lanes own their own subtrees. sots-engine is unaffected - lanes work in per-lane worktrees there, which is exactly why that repo has not had this problem . HAPPENED AGAIN 2026-09-08 (lane V): lane D's commit d7ea0a0 swept lane V's in-flight ghidra/addresses.json edit (the ObservedTech LIVE CONFIRMATION prose) into it. No harm this time - the content was correct and is now on main - but it is the same failure mode a third time, and it means a lane can find its own work already committed under another lane's message. Check git log -- <path> before assuming your edit is still unstaged. |
| MoveFleet waypoint types 2-5 | verify | mapped | — | 0% | 2026-09-08 | SUPERSEDED (lane P2: type 2 = LIIR drive (naming error), 3 covered by lane O, 4/5 need a Hiver game). Still ZERO behavioural coverage after lane M. Not for lack of trying: the only mover in ref-turn2 is the AI (straight runs only), and the player that would travel a node line has DE/CR/DN all 00 at its home system, so Move/Manage Fleets are greyed out every turn - there is nothing to send along the node lines the map draws. Needs a ship built over several turns or a purpose-built save. The type-2 node-line step is still WRONG BY CONSTRUCTION (B4). Also: sim::Distance deliberately left in double (only stutter geometry uses it); Mars_Vec3_Length says it is probably 1 ULP out the same way, but there is zero behavioural evidence to correct it against - do not "fix" it blind |
| P2-P event posting in ours | phase2 | verified | high | 95% | 2026-09-08 | HOST-VERIFIED, VM RUN QUEUED (lane F holds VM140). next_id reaches 4 in a host reproduction of recap-b3 call 0, fixture rebuilt from raw bytes at the real 0x1c/0x18/0x74 strides and cross-checked against turn3-state.sav with lane C's state_checksum --tree. Count-only (lane E option a): ours never calls the game's PostEvent and REPLACE MODE WRITES NOTHING - a bumped EvNxID with no record behind it would corrupt the very save the oracle hashes. Three design points: the event scan is taken in describe_args BEFORE the original (taken after, ours would dedup against the original's own posts and agree for the wrong reason); dedup risk is MEASURED and reported as events_dedup_risk, not assumed; KeylessEventText resolves keys to "%s" so the shim carries no prose. VERIFIED from the instruction stream: SetResearched 0x00581e10 calls owner vft+0x10 with (flags>>2)&1 and ProcessResearch passes flags=2, so silent=false and the completion event IS posted - previously only inferable from "EvNxID moved by two". ctest 33/33, shim cross-builds on CT111 (lane P could only syntax-check) |
| EVENT_TECHS_UNLOCKED / the unlock cascade | verify | verified | high | 100% | 2026-09-08 | Lane P FLAGGED RATHER THAN GUESSED. Trigger IS pinned (SetResearched's sweep sets state=2 + stamps turnAvailable sticky at -1; tail loop collects state==2 && turnAvailable==currentTurn) but evaluating it needs the unlock cascade ours deliberately does not run. The driver takes the unlock list as an INPUT and is handed nullptr ("no list") - deliberately distinct from an empty list ("computed, empty"). PREDICTED RESIDUAL: next_id short by exactly 1 on every completion call. Posting it "whenever something completed" would score on this save and be WRONG the first time a completion unlocks nothing - the exact false-pass shape this project keeps catching CLOSED by lane U 2026-09-08 — by running the cascade, not by posting on completion. sots-engine src/game/sim/techgraph.{h,cpp} implements PrereqsMet (0x0057d8e0, AND of ORs; zero groups = TRUE, an EMPTY group = FALSE), SetResearched (0x00581e10: stamps, child-cost sweep with a SIGNED min against an INT_MAX sentinel, sticky turnAvailable, zero-cost recursion) and the tail collector (0x00587cc3). Wired into the B3 hook in COMPARE MODE ONLY, over the scratch node copies. LIVE: first End Turn 3/3/0 exit 0; five-turn 15/15/0 exit 0; Zuul 20/20/0 exit 0 — all 22 of lane V's divergent fields gone, next_id reaching 7 and 12, and the End-Turn oracle hashes UNCHANGED (bb4fd9ac… / 978041ac…). The unlock list is still an INPUT and still nullptr when it could not be computed, so lane P's distinction survives. findings/subsystems/unlock-cascade.md; engine docs/U-unlock.md (§4 = the advance prediction, §5 = the outcome) |
| sizeof(ObservedTech) unpinned | objects | verified | high | 100% | 2026-09-08 | PINNED (lane X). sizeof(Game::ObservedTech) = 0x2c (44) -- three independent proofs: the magic divide 0x2e8ba2e9 sar 3 (= /44, exact) at 0x0087239f, imul reg,reg,0x2c at 0x0087243a / 0x007b735b, and the search stride add edi,0x2c at 0x007ba257. Append site = RecordObservedTech+0xdf (0x007ba27f): lea ecx,[player+0x274]; call vector_ObservedTech_push_back 0x007b7320 -- a de-duplicating append, direct callee of OnTechResearched 0x00891790; the realloc through 0x007b5820 is why all three vector words move. Element FULLY MAPPED (lane S, from ObservedTech::Write 0x00817cf0 / Read 0x00817c40): +0x00 vptr 0x00a2439c (RTTI .?AVObservedTech@Game@@), +0x04 uint16 otnF, +0x06 uint16 otnL, +0x08 bool odet (1 byte), +0x0c std::string otch (0x1c, so +0x24 is its _Alval, NOT a field), +0x28 int owith -- 0x2c exactly, nothing unaccounted. Same shape as Game::ObservedWeapon (Write 0x00817bc0, tag owep). Built the general tool the row asked for: tools/x86disp.py, an x86 displacement xref scanner (100% code coverage, 0.17% desync). findings/subsystems/observedtech-append.md |
| lea-displacement xref scanner | meta | verified | high | 100% | 2026-09-08 | tools/x86disp.py -- fixes the systemic blind spot that Ghidra does not index ModRM displacements. Full x86-32 length decoder swept from Ghidra's 41,089 function starts: 2,174,504 instructions, 612,166 displacement sites, 100.0% code coverage, 70 desyncs (0.17%), zero unknown opcodes. Validated against ground truth before use (re-finds lea eax,[ecx+0x29c] in GetEventStorage, both OnTechResearched +0x29c sites, and one NEW ProcessTurn site). HONEST LIMITS: it is a recall tool, not an oracle -- class-level precision at 0x274 is ~13% by function (99 sites / 45 functions, ~6 real), i.e. a 900x search-space cut that still needs one call-graph check. The naive byte scan it replaces is not wrong so much as blind: it misses 80/99 real sites at 0x274 and 13,784/14,611 at disp8 0x14. cohort ranking must never be used as a hard filter -- it would have discarded the correct ObservedTech answer. Works off a gitignored local cache in dumps/, so it does not hammer CT111 |
| lea-displacement xref scanner | meta | verified | high | 100% | 2026-09-08 | tools/x86disp.py - full x86-32 length decoder swept from 41,089 Ghidra function starts: 2,174,504 instructions, 612,166 disp sites, 0 unknown opcodes, 100.0% code coverage, 0.17% desyncs. VALIDATED against ground truth before any new claim (rediscovers GetEventStorage's lea ecx+0x29c, EvNxID +0x2b0; positive control: given 50 known ServerPlayer offsets, FUN_0087fac0 scores 50/50 = the serializer, nothing close). BUILD GOTCHA: clipping sweeps at fva+Ghidra sizeInBytes lost 11% of functions to mid-instruction truncation; sweeping to the NEXT function start took coverage 89% -> 100%. HONEST LIMIT: the win is RECALL not precision - naive lea-only scan MISSES 80 of 99 real 0x274 sites; class-level precision ~13% by function. Value = search space 41,411 -> 45 (~900x), then disambiguate by call graph |
| RANKER TRAP: cohort filter discards correct answers | meta | verified | high | 100% | 2026-09-08 | Lane X's cohort ranker WOULD HAVE DISCARDED THE CORRECT ANSWER. RecordObservedTech touches only 0x274/0x278 and nothing else on ServerPlayer, so every --min>=1 cohort filter drops it. What actually closed the case was plain query + ONE call-graph lookup on OnTechResearched's callees. RULE now in the tool docstring: it is a RANKER, NEVER a filter. Displacement scan for recall, call graph for disambiguation; neither alone sufficed |
| ObservedTech struct | objects | verified | high | 100% | 2026-09-08 | sizeof = 0x2c (44) by THREE independent proofs: exact magic divide 0x2e8ba2e9 sar 3 (= ceil(2^35/44), emulated against n=0..1000) at 0x0087239f; imul reg,reg,0x2c at 0x0087243a/0x007b735b; search stride add edi,0x2c at 0x007ba257. APPEND SITE: RecordObservedTech+0xdf (0x007ba27f) lea ecx,[player+0x274]; call vector_ObservedTech_push_back 0x007b7320. RecordObservedTech (0x007ba1a0) is a DIRECT CALLEE of OnTechResearched and DE-DUPLICATES BY TECH NAME before appending - a naive push_back in the reimpl WOULD DIVERGE on re-observation. Realloc through 0x007b5820 explains why lane R saw all three vector words move. Element FULLY MAPPED by lane S via the serializer lane X pointed at: +0x00 vptr 0x00a2439c (RTTI .?AVObservedTech@Game@@), +0x04 uint16 otnF, +0x06 uint16 otnL, +0x08 bool odet (ONE BYTE), +0x0c std::string otch (0x1c -> +0x24 is the string's _Alval, not a field), +0x28 int owith. Matches save_reader.py's on-disk order exactly. Lane P's live byte delta should still read exactly 44 |
| std::string size 0x18 vs 0x1c CONTRADICTION | objects | verified | high | 100% | 2026-09-08 | RESOLVED (lane S): 0x1c was right all along; ONE layout binary-wide. _Bx@0, _Mysize@0x10, _Myres@0x14, _Alval@0x18, sizeof 0x1c. ObservedTech+0x24 is the string's own trailing allocator word, not the unaccounted data field lane X read it as. Settled by three COMPLETE ENUMERATIONS of the element, each of which skips +0x24: ObservedTech::Write 0x00817cf0 (serialises +0x04/+0x06/+0x08/+0x0c/+0x28 and nothing else), ObservedTech_ctor 0x008562a0, and the inlined copy ctor at 0x0079a184. Then generalised: new tools/strfootprint.py recovers every (base,disp,tag) handed to the Mars::Stream string helpers across the whole exe -- 65 std::string members off a non-stack base, ZERO with a sibling field inside the 0x1c span, 51 of the 52 measurable inter-member gaps exactly +0x1c (the one +0x20 is StrategyServer KeyPath, +0x1c on its own Read side -- the writer skips a member). Corroborated by the vector walk stride add esi,0x1c @0x00699c29, PostEvent's by-value strings at [ebp+8]/[ebp+0x24] with RET 0x4c, and MoraleEvent 0x50 = name@0x34 + 0x1c. NO empty-base variant, no custom allocator, no game-local string class. BLAST RADIUS: zero recovered struct tables were wrong -- every string-bearing layout in struct-recovery / save-editor-structs / events / schema-gaps-resolved already used 0x1c, ServerPlayer::pswd @0x2dc..0x2f7 included (Write 0x008563e0 puts the next member exactly 0x1c above). Only 4 prose statements carried the 0x18 number, all corrected. save_reader 36/36 and state_checksum unaffected and still green. LESSON: never size a struct member from the offsets the code TOUCHES -- _Alval is an empty allocator, never loaded or stored, so a touch-scan undercounts every string AND every vector by exactly 4. Size from an enumeration: serializer, ctor, copy ctor, or container stride. observedtech-append.md §9 |
| harness-audit row 11 CORRECTED (Budget+0x64) | verify | verified | high | 100% | 2026-09-08 | Lane X: row 11 is NOT SUPPORTED. ComputeBudget writes its Budget* only through esi into +0x00..+0x54; its only two +0x64 accesses are LOADS OFF A DIFFERENT BASE. And ProcessResearch's int* overbudget is a ProcessTurn STACK LOCAL (lea edx,[ebp-0x14] at 0x008914a5), not Budget+0x64. Agrees with lane R's 0-of-4284 guard result. Reclassified to "nothing shown to write it"; only a watchpoint settles it definitively |
| STANDING RULE: size structs by enumeration, never by touch-scan | meta | verified | high | 100% | 2026-09-08 | Produced by lane S after lane X's 0x18/0x1c scare. _Alval is std::allocator, an EMPTY class: it occupies a word but is NEVER loaded or stored, so it is INVISIBLE to any analysis based on what the code touches. Sizing a member that way undercounts by exactly 4. RULE: size a member from an ENUMERATION - serializer, ctor, copy ctor, or container stride - because an enumeration can show ABSENCE where a touch-scan cannot. Same trap is live for std::vector here: {_Myfirst,_Mylast,_Myend,_Alval} = 0x10, ALLOCATOR-LAST, the opposite of the MSVC _String_val allocator-first shape the textbooks describe. Now in re-windows-2000s-howto.md 1c and struct-recovery.md 0 |
| std::string 0x18 vs 0x1c CONTRADICTION - RESOLVED | objects | verified | high | 100% | 2026-09-08 | 0x1c WAS RIGHT ALL ALONG; lane X mis-attributed ObservedTech+0x24, which is the string's trailing empty-allocator word. ONE layout binary-wide: _Bx@0 (16-byte SSO union), _Mysize@0x10, _Myres@0x14, _Alval@0x18. Proof by three COMPLETE ENUMERATIONS of ObservedTech (Write 0x00817cf0, ctor 0x008562a0, inlined copy ctor 0x0079a184) each of which skips +0x24, then generalised by new tools/strfootprint.py: 65 std::string members off a non-stack base, ZERO with a sibling inside the 0x1c span, 51 of 52 measurable inter-member gaps exactly 0x1c (the one 0x20 is StrategyServer::KeyPath, 0x1c on its own Read side - the WRITER skips a member). No 0x18 instantiation, no EBO variant, no custom allocator, no game-local string class. BLAST RADIUS: 65 layouts audited, ZERO were wrong - including ServerPlayer::pswd, the row lane X flagged. Only PROSE carried the 0x18 number. Separately found+fixed: struct-recovery.md 0 had _Mysize/_Myres TRANSPOSED while every table in the same file used the correct offsets |
| ObservedTech on-disk mapping | objects | verified | high | 100% | 2026-09-08 | READ, NOT GUESSED, from ObservedTech::Write 0x00817cf0 / Read 0x00817c40: +0x00 vptr, +0x04 u16 otnF, +0x06 u16 otnL, +0x08 bool odet (ONE byte, WriteBool), +0x0c std::string otch (0x1c), +0x28 int owith = 0x2c exactly, nothing unaccounted. Lane X's flagged-as-hypothesis first-seen/last-seen pair CONFIRMED by the tag names. Game::ObservedWeapon (0x00817bc0/0x00817b10) is the identical element with tag owep. Matches save_reader.py's on-disk order exactly - independent agreement between disassembly and the save oracle. odet typed int in the reader is BENIGN (for a 4-char tag a bool item and an int item are both 12 bytes, same value); all 60/61/61 real-save values are 00000000 so the SAVES do not discriminate - the binary does |
| MY BRIEFING ERROR: fpu_cw triple was under-powered | meta | verified | high | 100% | 2026-09-08 | I specified the experiment as 0x027f / 0x127f / 0x137f. WRONG ON BOTH AXES: 0x027f is 53-bit (it differs from 0x127f only in bit 12, infinity control, ignored since the 387) and 0x137f is 64-bit EXTENDED, not a rounding change. Run literally it returns "all three identical" - TRUE, but it tests single precision not at all and rounding not at all, so the conclusion drawn would have been wrong. Lane F caught it and added the genuine probes 0x007f (24-bit) and 0x1a7f (round-up). RULE: derive experiment parameters from the ISA definition, never from assumed mnemonics - and brief lanes to challenge the parameters, not just execute them |
| serializer struct recovery (automated) | objects | verified | high | 90% | 2026-09-08 | Lane D: struct recovery is now MECHANICAL. tools/serializers.py decodes the Mars::IStreamable Write idiom for the whole binary in 0.35 s; tools/rtti_map.py walks type-descriptor <- COL <- vftable[-1] (1,924 TDs = the inventory's count, 2,172 COLs/vftables) to attribute each serializer to its class and its COL this-adjustment. VALIDATION RUN BEFORE ANY NEW CLAIM, four ways: (A) 305/307 field offsets+kinds exact across 17 already-known classes, ZERO WRONG vs struct-recovery.md 1-4 (ServerSystem 79/79, StarShip 23/23, StarFleet 16/16, ServerPlayer 101/103, PlayerView 13/13, DiplomacyStats 14/14 - including every trap: ObservedTech+0x24 correctly NOT a field, Bats2/rcex int64, TShn/ETS/DipStats int16, ltis int, pswd string, TRM/CstR/E/T/shrm/RefCap/RepCap/PlayerView.Infra float); (B) sizeof read off the VectorHelper element-stride divide (invert MSVC's magic M = ceil(2^(32+k)/s)) reproduces ObservedTech 0x2c independently of lane X's three proofs, plus MoraleEvent 0x50, PlayerReport 0x30, DiplomacyStats 0x24, ObservedDesign 0x10; (C) 22 of save_reader.py's shapes, recovered tag order identical, 22 agree / 0 disagree (Sys 78 tags, Player 104, CreateParams 25, Ship 22, Slot 18); (D) automated Read/Write cross-check on every class, 437/437 field offsets agree. SCALE: 386 classes with a Write, 1,682 member fields - verified 87 (542 fields) / clean 77 (328) / unnamed 176 (471) / partial 31 / empty 15; 58 sizeofs corroborated by a second line of evidence, the rest reported as LOWER BOUNDS and labelled as such (a serializer enumerates only serialised members - a non-streamed tail is invisible). ~10x more classes with a known layout, reproducible from the exe in under a second. FOUR ENABLERS each worth 10-170 classes: (1) the RTTI ClassHierarchyDescriptor is the only honest "is this an IStreamable" test - a 3-slot vftable also matches TacAISquadRule_*, the CSV row parsers and ~100 others, and using it dropped a bogus 116-class "empty" bucket to 11; (2) mod=0 memory operands carry no displacement so tools/x86disp.py cannot index them - every field at offset 0 was invisible (OutputRates.SRt, every container element read through a bare iterator); (3) the member->id pointer idiom behind every handle field (HomeSys/PlrID/DesID/FltID/SrnTo) - and the base's this-ness must be read BEFORE retiring the destination, because mov edi,[edi+0x1c0] overwrites the this-register with the member it is reading; (4) sub-writers, base-class (StarMapNode inside ServerSystem = Pos) and private-same-class (StrategyServer's six id lists live in FUN_00794cd0 - without it PlayerIDs/DesignIDs/SystemIDs/FleetIDs/ShipIDs/TradeIDs are simply absent), spliced at the call site so disk order survives. FAILURE CLASSES, enumerated: 176 anonymous-tag classes (writer passes NULL, so offsets+types are complete but there are NO on-disk names - a hard limit on names, not on layout; combat commands + network messages); 19 unsized nested types; 9 map/list-node-only; 3 untyped sub-writers; 2 fields lost to a value assembled ACROSS A BRANCH (tech ? tech->name : "", max(v,1)) - left unrecovered on purpose rather than patched with a backward-search heuristic that has no oracle on the other 1,600 classes; std::map/list members recovered only as their _Mysize int; 1 runtime-built tag (Mars::ParticleSystem). Everything but the first is a bounded mechanical fix. WRITE-BACK: 288 structures + 328 labels into Ghidra (0 failures), +201 addresses.json entries, header regenerated with gen_addresses.py. findings/objects/serializer-struct-recovery.md, objects/layouts.{json,md,h} |
| Game::ShipDesign is NOT streamed through IStreamable | objects | mapped | high | 100% | 2026-09-08 | Lane D, from the serializer sweep: ShipDesign's IStreamable slots are inert - Write 0x008747a0 makes no stream call at all. Same for Game::ServerTradeManager (Read and Write are BOTH the inherited no-op 0x924fb0) and 13 others incl. the whole Mars::Particle* family and Mars::DecalFile. So designs persist by some other path (ShipDesignDef is already verified separately and may be the whole answer) - worth confirming before anyone goes looking for a ShipDesign serializer that does not exist |
| SVSOJewelsOfTheCrown writes a DUPLICATE tag | objects | mapped | high | 100% | 2026-09-08 | Lane D: SVSOJewelsOfTheCrown::Write emits the tag JEWELLOCATIONID TWICE, at +0x8 and +0x10. Not a tool error - the Write really does it. Any save reader matching that shape BY NAME will bind the wrong field. Same shape as ServerPlayer's two Team members (int @0xac, PlayerAlliances @0x168), which is why the Read/Write cross-check has to exclude duplicated tags rather than report them as conflicts |
| serializer struct recovery (mass) | objects | verified | high | 95% | 2026-09-08 | Lane D industrialised lane S's enumeration rule: the ~179 Streamable serializers ARE complete field enumerations with 4-char tags. 386 classes with a Write, 1,682 member fields, whole binary in 0.35 s. Verified 87 (542 fields) / clean 77 (328) / unnamed 176 (471) / partial 31 (341) / empty 15 - roughly 10x more classes with a known layout. VALIDATED FOUR WAYS BEFORE ANY NEW CLAIM: (A) 305/307 fields exact vs layouts we already had, 2 unrecovered, ZERO WRONG, every known trap reproduced incl. ObservedTech+0x24 correctly NOT a field; (B) sizeof independently inverted from MSVC's container-stride magic M=ceil(2^(32+k)/s) - ObservedTech 0x2c falls out mechanically, plus MoraleEvent 0x50, PlayerReport 0x30, DiplomacyStats 0x24; (C) 22 on-disk tag orders vs save_reader.py, 22 agree 0 disagree; (D) automated Read/Write cross-check, 437/437 offsets agree, 0 conflicts. 58 sizeofs corroborated by a second line of evidence; the REST ARE REPORTED AS LOWER BOUNDS AND LABELLED - a serializer enumerates only SERIALISED members, so a non-streamed tail is invisible. Ghidra: 288 structures + 328 labels, 0 failures |
| serializer recovery: failure classes | objects | mapped | high | 100% | 2026-09-08 | What defeats the tool, i.e. whether it scales to the remaining ~1,600 classes. 176 anonymous tags (writer passes NULL - offsets/types complete, no on-disk names; combat commands + network messages; NOT fixable, the names do not exist in the binary, layouts still usable). 19 nested member of unknown size (fixable, one ctor size each). 9 map/list node bodies. 3 untyped sub-writer. 2 fields VALUE ASSEMBLED ACROSS A BRANCH (tech ? tech->name : "", max(v,1)) - DELIBERATELY left unrecovered rather than patched with a backward-search heuristic that has no oracle on the other 1,600 classes. 1 runtime-built tag (Mars::ParticleSystem, not fixable). Four enablers each worth 10-170 classes: RTTI ClassHierarchyDescriptor as the only honest "is this IStreamable" test (a 3-slot vftable also matches TacAISquadRule_* and the row parsers; using it dropped a bogus 116-class "empty" bucket to 11), mod=0 memory operands (x86disp.py cannot index them; they hide every field at offset 0), the member->id pointer idiom behind every handle field, and sub-writers (StrategyServer's six id lists live in FUN_00794cd0) |
| Game::ShipDesign has NO serializer | objects | verified | high | 100% | 2026-09-08 | Lane D: Game::ShipDesign::Write (0x008747a0) MAKES NO STREAM CALL, and ServerTradeManager's Read and Write are both the inherited no-op. Designs persist some other way. Recorded so nobody hunts for a serializer that does not exist |
| SVSOJewelsOfTheCrown duplicate tag | objects | verified | med | 100% | 2026-09-08 | Its Write emits the tag JEWELLOCATIONID TWICE (+0x8 and +0x10). A name-matching save reader binds the WRONG field. Ours must bind by ordinal here, not by name |
| addresses.json fragment dir (structural fix) | meta | verified | high | 100% | 2026-09-08 | Three cross-lane sweeps today all traced to ONE shared file. ghidra/addresses.d/<lane>.json fragments are now merged by gen_addresses.py after addresses.json, in sorted order; a DUPLICATE NAME ACROSS FILES IS A HARD ERROR (exit 1), never last-wins, because two lanes disagreeing about an address is precisely what must not be papered over. Self-tested both ways (duplicate exit=1, clean exit=0). Integrator folds fragments back into addresses.json once a lane merges. This replaces the "stage by path" rule with a design that does not depend on remembering it |
| P2-P event posting LIVE-VERIFIED | phase2 | verified | high | 95% | 2026-09-08 | ADVANCE PREDICTION HELD FIELD FOR FIELD. Lane P wrote the expected result into docs/P-events-wiring.md 4 BEFORE the run; lane V tested it. First End Turn: 3 calls, 3 compared, 0 divergent, tracecmp exit 0 - turn=3, bucket_exists=true, next_id_in=3, in_turn_bucket=1, dedup_risk=0, no scan_truncated, next_id 3->4 both sides, node[144] and rng as forecast, observed_techs.bytes unchanged on all three. Five-turn: exit 1, 2 divergent calls (was 3), each short by EXACTLY 1 (orig 7/ours 6, orig 12/ours 11) = the deliberately unmodelled EVENT_TECHS_UNLOCKED. player+0x274/0x278/0x27c LEFT the guards' undeclared list (13 spans -> 10) and now show as observed_techs. STRENGTHENED BY LUCK RUNNING OUT: this was NOT lane R's run - from turn 4 the AI picked different techs - and the predicted id totals landed anyway. End-Turn oracle matched lane R's hashes byte for byte |
| sizeof(ObservedTech)=44 CONFIRMED LIVE | objects | verified | high | 100% | 2026-09-08 | observed_techs.bytes grew by EXACTLY 44 on both completion calls (440->484, 484->528); non-researching players measured 880 = 20 x 44 and never moved. Independent BEHAVIOURAL confirmation of a purely static pin (lane X's three proofs + lane D's magic-divide inversion). Static and dynamic now agree on this struct from four directions |
| CORRECTION: lane R's "RNG matched 15/15" was workload luck | verify | verified | high | 100% | 2026-09-08 | Lane V found rng diverges on call 9 (orig left 374/next_index 250, ours 375/249) = the RollResearchEvent draw in OnTechResearched, a boundary B3 and P-events-wiring 3 both declared out of scope. HONEST STATEMENT: ProcessResearch's rng matches on every call that does NOT complete a roll-triggering tech, and is short by one draw on every call that DOES. Lane R's 15/15 was a property of that workload, not of the model - exactly the kind of claim that survives only until the workload changes. The two "new" guard spans (player+0x196, player+0x3b4) are already-named fields (design-mask B, ResErrRoll) seen from the caller EXPLAINED by lane U 2026-09-08, and it is stronger than "workload luck". The draw is now MODELLED (ServerPlayer::OnTechResearched head: if (ResT == def) { if (ResearchRollPending) RollResearchEvent(); pending=0; ResT=0; }, one unconditional NextFloat), its two inputs are read PRE-CALL and reported in every record (research_target, roll_pending_in), and region:rng compared clean on all 35 of lane U's calls. BUT roll_draws was 0 on every one of them: ResearchRollPending is normally consumed by ServerPlayer::ProcessTurn BEFORE ProcessResearch runs, because that call site fires once the progress ratio crosses its threshold - i.e. exactly the turns approaching completion. In lane U's Zuul run roll_pending_in is true for turns 8-12 and flips FALSE on turn 13, the turn before the tech completes on turn 14. So lane V's call-9 draw was the RARE case (a tech jumping from below the threshold to complete in one turn). Three sessions, three answers: 0 / 1 / 0. The branch is inside the compare but has never been seen to fire - needs a purpose-built save |
| Zuul double roll (behavioural) | verify | verified | high | 100% | 2026-09-08 | CLOSED by lane V. No species-5 save existed so it MADE one: custom game, only Zuul in the Available Species pool, 4 End Turns. 8 calls, 8 compared, 0 divergent, exit 0; all four species=5 researching calls advance the generator by TWO (left 540->538, 522->520, 504->502, 485->483) and ours reproduced every post-state bit-for-bit. Was disassembly-only since 2026-09-08 morning. Save: verify/results/saves/zuul-turn5-species5.sav (and zuul-turn5.sav on the VM). Does not yet contain a Zuul COMPLETION - one more End Turn gets there |
VM click helper: type does not register |
meta | verified | high | 100% | 2026-09-08 | The click helper's type action does not reach the game's text fields. WORKAROUND: accept the default name and rename the .sav afterwards. Costs a save-naming step in every workload-building lane |
| wire-schema channel (engine) | engine | verified | high | 95% | 2026-09-08 | Lane G REJECTED the brief's premise for good reason. objects/layouts.json is a MEMORY-layout projection and was the wrong input: serializers.py's build() sorts fields by off_abs (89 of 386 classes have offset order != WRITE order) and merges duplicate offsets into alt_tags - which is exactly the JewelsOfTheCrown double-tag trap. Lab.layout() already computed true disk order internally and it was being thrown away. New tools/streams.py is a SECOND projection keeping order and repeats and DROPPING EVERY MEMORY FACT (no off/size/sizeof/gaps/strides) - so the original's ABI never enters our runtime types, which was the design constraint. Offsets still have their home as offset entries in addresses.json for the shim, which legitimately needs them. gen_stream_schema.py -> include/generated/sots_stream_schema.h (386 classes, 2,042 items). SUBTLETY: the on-disk primitive is NOT layouts.json's kind - it comes from the stream vftable slot/helper, and a member held as int16/int8 is written by WriteInt and is FOUR BYTES ON THE WIRE |
| schema as CHECK not codec | engine | verified | high | 100% | 2026-09-08 | Lane G's key judgement: the table is a SPECIFICATION, not a program. The recovery is a LINEAR pass over Write, so it cannot see branches (StarShip's BQ2 is gated on hbq but listed unconditionally) and it flattens container loops - a codec driven off it DESYNCHRONISES ON THE FIRST BRANCH. So hand-written io() shapes stay the codec and a new SchemaProbe archive walks them WITH EVERY BRANCH TAKEN (the same view the recovery has), LCS-aligned against the table: 56 shapes bound, 657 items matched, 0 mismatches |
| NAMED coverage 38% -> 97% | engine | verified | high | 100% | 2026-09-08 | All four real saves round-tripped byte-identically BEFORE and AFTER - and lane G refused to call that a coverage claim, because ar.any bodies round-trip trivially by copying bytes nobody understands. New CoverageArchive separates items a FIELD NAMES from items a Node merely CARRIES: turn1/2/3 37.9/38.8/39.4% -> 97.1/97.2/97.2%; zuul-turn5 42.5% -> 97.6%. Ratchet at 95%. Newly typed: TechTree (both NumTechs sections), Events (3 nesting levels), ShipRecs, sprjs, civr, comms, spy2, spymgr, aid, Ojvs, AIEnf, FNG, trdmgr, Des section/gun-bank tree. STILL OPAQUE, honestly: CD custom data (744 items - TurnCommands_v5 is a NO-ORDERS snapshot, needs a save with issued orders), SvSctOb (147, eight EncObj variants), DOpts (94, read_elem lacks a std::string branch), spies2 (56, count 0 everywhere), RNG (2, correctly opaque) |
| FOUR reader defects invisible to round-trip | verify | verified | high | 100% | 2026-09-08 | Present in BOTH readers, none catchable by a round-trip test. (1) SystemParams field 1 is a STRING, not an int - it is the empty string in every save, four zero bytes, byte-identical to int 0; it round-tripped BY LUCK and a named planet would have desynced both parsers. (2) ObservedTech/ObservedWeapon odet is a BOOL not an int (lane D's own golden table already said so); byte-safe only because a 4-char tag makes bool and int items both 12 bytes. (3) SpeciesRatios::nv and (4) ShipRecords::srbd are COUNTS, not fields. Lane G deliberately did NOT patch save_reader.py: every fix is byte-neutral on available saves, and quietly editing the oracle mid-campaign is worse than recording the divergence. THE ENGINE IS NOW THE CORRECTED REFERENCE; the Python oracle carries a logged open item |
| RULE: interface stub -> look for the *Impl | objects | verified | high | 100% | 2026-09-08 | trdmgr RESOLVED, correcting the earlier "ServerTradeManager has no serializer" finding: the no-op Read/Write is real but THE CALL IS VIRTUAL - ServerTradeManagerImpl has the actual serializer. Same shape resolves IServerSpyManager -> ServerSpyManager. GENERAL RULE: when an interface's serializer is the inherited stub, look for the concrete *Impl. (Game::ShipDesign::Write genuinely still makes no stream call - but ShipDesignDef::Section IS recovered, so most of Des is typeable anyway) |
| B3 hook: the completion path is now inside the compare | phase2 | verified | high | 95% | 2026-09-08 | NEW (lane U 2026-09-08). Game::TechTree::ProcessResearch in compare mode now reproduces not only what the function itself writes but the whole SetResearched completion path: the turn/order stamps, the child-cost sweep, the availability sweep, the newly-available collector that decides EVENT_TECHS_UNLOCKED, the de-duplicating ObservedTech append and the one RNG word RollResearchEvent draws. 35 compared calls across three workloads, 0 divergences, tracecmp exit 0 on all three. FOUR pre-call reads make it work and each is a trap that reads back a PLAUSIBLE wrong answer if taken after the original: TechTree+0x20 (order counter, post-incremented -> every order off by one), ServerPlayer+0x294 (ResT, zeroed by the callback -> the extra RNG draw never modelled), +0x3b4 (pending roll, cleared in the same block), and the otch element names (already appended -> the dedup check 'agrees' with a count it never computed). All four are reported as ARGUMENTS so a run is auditable without trusting ours. COMPARE MODE ONLY: in replace mode applying half of OnTechResearched (the append and the roll, but not the ~90 tech-effect field writes) would leave the player in a state no code path produces |
| CLEAN-RESULT DISCIPLINE: how a 0-divergence run was kept honest | meta | verified | high | 100% | 2026-09-08 | NEW (lane U). A clean result was EXPECTED here, which is exactly when a hook that silently models nothing slips through. Three things made that hard to hide and all three fired: (1) the 'did nothing' answer is KNOWN - it is INT_MAX / 0 / -1, precisely what lane V's report shows - so there is no null model that passes; (2) the collector ran on all 35 calls, not just the 4 completions, and had to come back EMPTY on the 31 quiet ones - an over-collecting transcription would have pushed next_id TOO HIGH and made those calls newly divergent; (3) the shim log prints per-call counters (completions= unlocked= otch_appends= roll_draws= failures= depth= name_unreadable=), so a clean compare with zeros on a completion call would have been visible as a clean compare of nothing. Reusable pattern for any lane whose expected outcome is 'no divergence' |
| Prereq semantics come from the CODE, not the tech files | subsystem | verified | high | 100% | 2026-09-08 | NEW (lane U). TechTree::PrereqsMet 0x0057d8e0 (whole function): a prerequisite set is an AND over groups, each group an OR over techs, held as a flat entry array at TechDef+0x88 (stride 8, TechDef* at +0) plus a vector of {int start; int count} groups at TechDef+0x98/+0x9c (stride 8). A tech satisfies its group only when its node exists IN THIS TREE and is state 4. TWO EDGE CASES THE DATA FILES DO NOT SUGGEST: zero groups -> TRUE (a tech with no requires is gated only by its parent edge), and a group with zero entries -> FALSE, failing the whole test (the inner loop cannot break, so the outer one exits with that group uncounted) - a 'vacuously true empty OR' reading gets this exactly backwards. Also TechDef+0xb0: a byte that excludes a node from the availability sweep entirely; named TechDef_off_NoAutoAvailable for what it DOES - the tech-file keyword unlock_explicitly matches the behaviour but ParseTech 0x0058b050 shows no reference to 0xb0, so the link is a HYPOTHESIS. findings/subsystems/unlock-cascade.md |
| per-lane addresses.d fragment (second use) | meta | verified | high | 100% | 2026-09-08 | Lane U wrote ghidra/addresses.d/lane-u.json (12 entries; header 615 -> 627) rather than touching the shared addresses.json, and staged every sots-re change by explicit path. No sweep, no lost authorship. Duplicate-name detection did its job: TechDef_off_Name had to be checked against the shared file before being added |
| P2-U tech unlock cascade | phase2 | verified | high | 95% | 2026-09-08 | RESEARCH SLICE CLOSED. Prediction COMMITTED BEFORE THE BUILD (engine 405ba41, docs/U-unlock.md 4, incl. 4.6 listing five ways the model could be falsified and the symptom of each). Outcome: 35 compared calls across THREE workloads, 0 divergences, tracecmp exit 0 on all three (first End Turn 3/0, five-turn 15/0, Zuul turn 5->15 20/0). End-Turn oracle checked FIRST (bb4fd9ac.../978041ac..., lane R's and lane V's hashes) - a clean compare from a build that moved the game would be worthless. ANTI-VACUITY EVIDENCE, which is the point: call 9 was NOT lane V's call (from turn 5 the AI picked a different target) - it completed tech 9 and unlocked three nodes at costs 13000/35000/4000 that appear in NO earlier report and were predicted by nobody, reproduced with zero divergences, so the model cannot have memorised numbers that did not occur. The collector also ran on all 35 calls and had to come back EMPTY on the 31 quiet ones (over-collecting would have pushed next_id TOO HIGH). Guards 9 undeclared in 2 calls vs 10 predicted - the miss is WORKLOAD not code (player+0x3b4 gone because the pending-roll byte was already 0; +0x196 replaced by +0x130 because a different tech completed) |
| Zuul completion (double roll + cascade) | verify | verified | high | 100% | 2026-09-08 | Lane U took it: call 2, turn 7, species 5 - left 449->447 (DOUBLE ROLL) AND the cascade in the SAME call, next_id 10->12, observed_techs 396->440. Independent instance: the Zuul tree unlocks only 132/136 from tech 144 where the Human tree also unlocks 142 |
| RollResearchEvent draw - why it never fires | verify | mapped | high | 90% | 2026-09-08 | Lane U's honest limit: roll_draws was 0 on ALL 35 calls. The draw is modelled, its inputs are read pre-call and reported in every record, and region:rng compares clean - but THE BRANCH HAS NEVER BEEN SEEN TO FIRE. The reason is a real finding: ResearchRollPending is normally consumed by ServerPlayer::ProcessTurn BEFORE ProcessResearch, because that site fires when the progress ratio crosses its threshold - exactly the turns approaching completion. In the Zuul run it is true for turns 8-12 and flips FALSE on turn 13, the turn before completion on turn 14. So lane V's call-9 draw was the RARE case and lane R's "15/15" the common one. Three sessions: 0 / 1 / 0. Closing it needs a purpose-built save |
| VM: use QEMU screendump, not the click helper's shot | meta | verified | high | 100% | 2026-09-08 | Lane U: qm monitor 140 screendump is more reliable than the click helper's shot. Also the >60 s startup got its FOURTH confirmation (menu at ~t+85 s) |
| wire sections closed (SvSctOb/DOpts/spies2) | engine | verified | high | 95% | 2026-09-08 | Lane W: named coverage 97.1 -> 98.0% (zuul 97.6 -> 98.4), conformance 56 shapes/657 items -> 74/769, STILL 0 MISMATCH, every new binding 0 wire-only 0 shape-only. Ratchet 95.0 -> 97.5. SvSctOb (147/156/156/126 -> 0): body is Game::SVSOSots writing two POLYMORPHIC lists keyed by the item before them (xscn->xsc, EncID->EncObj); NEITHER MAP IS ON THE WIRE - both came out of the binary: EncID from a 23-entry dword JUMP TABLE at 0x0052bf60 indexed by EncID-1 (factory 0x0052bf00, class names read off each ctor's vftable store), xscn from an exhaustive four-way _stricmp chain at 0x005a7050. The eight ids in our saves had been identified by tag-sequence match; the table AGREED on all eight and added four more. DOpts (94 -> 0) and spies2 (56 -> 0) are certain FROM THE HELPER'S OWN DECORATED TYPE - VectorHelperMars::String and VectorHelper - not inferred from bytes |
| spies2/SysMem/mts are HYPOTHESES not results | verify | backlog | — | 0% | 2026-09-08 | Lane W flagged as instructed: spies2's count is 0 in all 28 systems of all four saves, so the 56 items "closed" are 28 x (frame + count) and NO ELEMENT VALUE HAS EVER BEEN OBSERVED. Same for SysMem and mts. A layout you cannot exercise is a hypothesis - needs a workload |
| CD custom data: provable negative | objects | mapped | high | 80% | 2026-09-08 | Lane W's static read gave a NEGATIVE RESULT WORTH HAVING: the recovered 44-item Game::TurnCommands sequence CANNOT be aligned to the save's 35 items even as a SUBSEQUENCE. Item 4 is 8 bytes on disk, which for a 1-char tag can only be a BOOL where the recovery says i32; and the 27 trailing ints have only 22 i32 slots available after index 15. Confirms a save with issued orders is required (lane O). ALSO SPLIT INTO TWO PROBLEMS: TurnCommands_v5 (blocked on a workload) vs THREE AIAgent blocks (~236 items each = the BULK of the 744) which are Game::StrategyAIAgent::Streamable with NAMED tags, statically tractable, ~10 nested shapes - deliberately left for a following lane since it is AI cache state, not orders |
| SvSctOb side findings | objects | verified | high | 100% | 2026-09-08 | indsys = Game::SVSOIndependentSystems, whose Read AND Write are both the shared ret 4 stub - its EMPTY FRAME IS CORRECT OUTPUT, not truncation. SVSOMonitor derives from SVSODerelict (its Write calls Derelict's first). SVSOSots::Read accepts NPCPlr/hastraps that Write NEVER EMITS - read-only back-compat, not a recovery hole. And THE RECOVERY IS WRONG about SVSOCrowDefenders::dsys - it is inside the ndsys loop, which no save can settle. EncIDs 7/8/14/21 and ScnObj have factory entries and recovered serializers but NO OCCURRENCE anywhere - deliberately carried as Nodes, not typed |
| four oracle fixes landed, byte-neutral | verify | verified | high | 100% | 2026-09-08 | Lane W fixed all four OPENLY with 12 new tests: 36 -> 48 tests passing, --strict exit 0 on all four saves, state_checksum coverage PROVED on all four with unchanged rebuilt byte counts. BYTE-NEUTRALITY PROVEN AT ITEM GRANULARITY: dumping every save before and after, the OFFSET SEQUENCES ARE IDENTICAL (38,933/39,843/40,300/35,771 offsets, no item boundary moved); the /CreateParams and /Sim/players digests DO change, correctly, because they hash typed values, and the value-byte deltas balance EXACTLY (odet x3 + p1 x4 = 292/295/295/238, to the byte). NUANCE: defects 3 and 4 were an ABSENCE in save_reader.py, not an error - ShipRecs and civr were both A(...,"any"), so the fix ADDED typed shapes rather than retyping a field, a bigger change than my brief implied. srbd turned out BEHAVIOURALLY CONFIRMED not inferred: it takes 0/1/3/4 across players and every non-zero count is followed by exactly srbd x 5 scalars (turn3 player 4: srbd==4, 20 items) - read as a field those trailing scalars have no explanation. nv is WEAKER: only ever 0 or 1 in these saves, so the multi-pair case is synthetic |
| save_reader silent-agreement failure mode | verify | verified | high | 100% | 2026-09-08 | Forced into the open by lane W's tests: save_reader.py's walker types "." items from its OWN kind catalog, not the schema passed in - so on the real saves the catalog and the old schema agreed with each other AND WERE BOTH WRONG. Its failure mode is SILENT AGREEMENT, not the framing desync the schema-driven engine would have suffered. Two independent-looking checks that share a hidden assumption are one check |
| AIAgent blocks typed | engine | verified | high | 95% | 2026-09-08 | Lane A: NAMED COVERAGE 98.0 -> 99.9% on all four saves; opaque items 748 -> 37 (turn1/2/3) and 521 -> 37 (zuul). Conformance 74 shapes/769 items -> 86/838, STILL 0 MISMATCH, all twelve new bindings 0 wire-only 0 shape-only. Round trip byte-identical throughout; state_checksum --strict still coverage PROVED, 0 error 0 warn, unchanged. Ratchet 97.5 -> 99.8. LOAD-BEARING FINDING: Streamable::Write (0x006c6f00) is UNCONDITIONAL - the decompiler shows a large if wrapping everything from lnat on, which reads exactly like the branch that defeated CD, but it is an INLINED std::vector DESTRUCTOR whose operator delete this database marks noreturn; the disassembly shows both paths converging at 0x006c72e8. So the recovery's all-branches view and a single record are THE SAME VIEW - the opposite of lane W's TurnCommands result, for a concrete reason |
| dsh is a map count, not a scalar | objects | verified | high | 100% | 2026-09-08 | Lane A: dsh is _Mysize at +0x240 of the std::map whose _Myhead is at +0x23c - the same head@0/size@4 layout the campaign pinned on ServerSystem::NVO. Node _Isnil at +0x21 makes the value pair<const int, vector> i.e. pid/trns. THE RECOVERY LISTED IT AS A PLAIN FIELD; typing it that way DESYNCHRONISES THE READER. Also lnat/lat are scalars not elements (recovery marks both [element]); saves confirm - nalat is 0 everywhere and both are present anyway |
| CORRECTION to lane W: StreamableEnum | objects | verified | high | 100% | 2026-09-08 | Lane A found StreamableEnum writes a FRAME CONTAINING ONE INT, not a bare int, so VectorHelper<StreamableEnum> is a framed array of one-int frames. SysMem, mts and nalat had been typed as vector<int32_t>. BYTE-NEUTRAL - all three have count 0 in every save, which is EXACTLY WHY IT WAS INVISIBLE - but the previous typing was wrong. Lane W had flagged those two as hypotheses; this is what checking one looked like, and it is the argument for flagging them |
| Game::AISystem::Write never reads its object | objects | verified | high | 100% | 2026-09-08 | Lane A: it builds an EMPTY AISituation on the stack and writes that. Every AISys body on disk is two empty weight maps REGARDLESS OF GAME STATE. Worth knowing before anyone tries to read AI behaviour out of a save |
| ten AIAgent element layouts UNEXERCISED | verify | backlog | — | 0% | 2026-09-08 | Typed but hypotheses, flagged by lane A: AIAttr entries, all four weight maps, prs2, dsh, NBStab, NMBlst, nalat, CmbR, NumCL, apr - every one count 0 in all ELEVEN AIAgent blocks across our saves. They come from the WRITERS, not from bytes, and conformance agrees, but nothing exercises them. test_stream.cpp::test_aiagent_block populates every one and round-trips (and asserts the trap directly: a two-element nalat must produce two COMPLEX nodes) - that test is the only thing exercising them. Four containers ARE exercised: dnnc (0/1/2), NPrv (1/2), NTecS (2/3), AINumSys (28). Settling the rest needs a longer multi-AI save = turns played |
| Game::TacReport carried, not typed | objects | backlog | — | 0% | 2026-09-08 | Lane A left it deliberately: computed count plus three trailing scalar runs the linear pass cannot resolve, and it sits two levels under NCmbR which is 0 everywhere. Carried as Nodes rather than typed on a guess |
| turn driver map | control-flow | verified | high | 95% | 2026-09-08 | Lane T read BOTH drivers BYTE FOR BYTE from objdump over the raw image, not the decompiler: StrategyServer::ProcessTurn 0x007dc6c0-0x007dcd67 (32 phases) and ServerPlayer::ProcessTurn 0x00891340-0x00891783 (12 phases). CORRECTION TO MY BRIEF: ServerPlayer::ProcessTurn is NOT the phase driver - it is the per-player economy/research half; StrategyServer::ProcessTurn IS the spine. Verified call sites of our pieces: ProcessFleetMovement->MoveFleet phase 8; ServerSystem::ProcessTurn phase 11 (mov ecx,[Systems+i*4]; call, NO STACK ARGS, confirming B4); ServerPlayer::ProcessTurn(dt) phase 13 (dt forwarded by fstp [esp]); ComputeBudget player phase 1; ProcessResearch player phase 5. Also settled: the TWO StrategyServer bases 4 bytes apart (every StrategyServer_off_* in addresses.json is the S+4 frame EXCEPT off_RNG) and TWO per-turn counters - BeginProcessTurn bumps S+0xc (ModCount, what the research code stamps), ProcessTurn bumps S+0x8, which nobody has named |
| CORRECTION: turn-spine.md deferred tail was WRONG | control-flow | verified | high | 100% | 2026-09-08 | turn-spine.md 2.4 step 11 said the end-of-turn tail is deferred when encounters are pending. IT IS NOT. The if at 0x007dcb15 is an INLINED std::vector DESTRUCTOR - the je skips only the operator delete and both arms converge at 0x007dcb38. There is NO branch on the encounter snapshot anywhere in the function. And FUN_00794ad0 never built an encounter list: it builds the fifteen ship-action type ids. Encounter detection is a different function, dead last. THIS IS THE SECOND TIME TODAY the inlined-destructor-looks-like-a-branch trap produced a wrong published finding (lane A hit it on Streamable::Write and got it right). Treat any decompiler if wrapping a whole tail as suspect until the converge point is checked |
| ResearchRollPending SETTLED | verify | verified | high | 100% | 2026-09-08 | Lane T, by full displacement scan of +0x3b4 - only TWO consumers in the image. Site A ServerPlayer::ProcessTurn+0x2ac runs AFTER ProcessResearch (+0x165), NOT before as the board previously said: if (ResT && ResErrRoll && 0.5f < progress/Cost) { roll; ResErrRoll = false; } - threshold is a STRICT < against 0.5f at 0x00a2c788, and THE CLEAR IS INSIDE THE BRANCH, so a failed ratio test leaves the flag set. Site B OnTechResearched+0x51 clears unconditionally given ResT==def. So the flag survives into the next turn IFF progress/Cost <= 0.5 at the end of this one - which is exactly why three sessions gave 0/1/0. SAVE RECIPE: ResErrRoll is a serialized tag right after ResTNm so it can be EDITED INTO a save - set it true, point ResTNm at a high-cost tech with near-zero progress, fund research to >= 1.5x cost |
| REPO-WIDE CORRECTION: roll branch costs ONE OR TWO RNG words | verify | verified | high | 100% | 2026-09-08 | EVERY coverage note in the repo says the research-event roll draws "exactly one NextFloat". Lane T: RollResearchEvent itself draws one, but the branch it fires (ServerPlayer_OnResearchRollSucceeded 0x00889d60) draws a SECOND NextInt on the plague path and posts EVENT_PLAGUE_OUTBREAK; the rebellion path allocates an AIRebellion and CANCELS the research. Any RNG accounting that assumes one word is wrong the first time that branch fires - and it has never fired in three sessions, so nothing has caught it |
| ResErrRoll arming mechanism UNKNOWN | objects | backlog | — | 0% | 2026-09-08 | Lane T flagged rather than papered over: NOTHING was found that ARMS the flag during play. The only writes are the two clears, ServerPlayer::Read, the constructor (mov WORD [esi+0x3b4],1 - born TRUE), and a virtual setter pair with no locatable caller. "Born true, consumed once" fits every observation but IS A HYPOTHESIS. The save recipe does not depend on it |
| unread driver: OnAllCombatDone_Tail | control-flow | verified | — | 0% | 2026-09-08 | SUPERSEDED (lane K read all 36 phases). THE BIGGEST UNREAD BLOCK IS NOT IN ProcessTurn AT ALL. No bankruptcy, no turn results, no turn events, no autosave run in the turn driver - they are all in StrategyServer::OnAllCombatDone_Tail 0x007d92a0, a SECOND driver no lane has read. Highest-value next control-flow target |
| player_turn hook prepared (boundary narrowed) | phase2 | mapped | high | 70% | 2026-09-08 | Lane T built the descriptor with compile-time coverage() (9 notes), Result regions for the three phases needing nothing but the player, roll_flags + rng as observations ours never writes, guards over the whole ServerPlayer and the TechTree header. DELIBERATELY NARROWED: phases 2/3/6 (savings, aid records, refund) depend on ComputeBudget's slots and ProcessResearch's overBudget, both STACK LOCALS of the original. The three ways to reach them are calling ComputeBudget ourselves (repairs ships - audit #6), reading the nested hooks (audit #5, SELF-FULFILLING), or inferring from the Sav delta. They are GUARDED, NOT CHECKED; formulas written and unit-tested but not in the verdict. docs/T-turn-driver.md carries the prediction incl. which checks are WEAK BY CONSTRUCTION on the reference save |
| INTEGRATOR NOTE: two gate failures caught on lane T merge | meta | verified | high | 100% | 2026-09-08 | Both gates failed on merge and both were real, which is the argument for running them SEPARATELY rather than &&-chained: (1) clean-room exit 1 - a raw FUN_00889d60 identifier had reached docs/ in the public-capable engine repo; fixed by using the name the lane itself had already put in addresses.json (ServerPlayer_OnResearchRollSucceeded). (2) The shim CROSS-BUILD failed -Werror=unused-function on player_turn.cpp - the file is WIN32-only and the lane could not build it on the notes host, exactly as it flagged. Host ctest passed both times and would have hidden both. A lane that cannot cross-build its own shim TU must say so, and the integrator must run the CT111 shim build before pushing |
| guides/method-rules.md | meta | verified | high | 100% | 2026-09-08 | The 15 rules this campaign PAID FOR, each traced to the finding that produced it, ranked by damage caused. Written because the integrator was pasting the same hard-won constraints into every lane brief, which invites drift and omission. Lane briefs now REFERENCE it instead of restating it. Covers: green verdict != evidence; predict before the run; static vs behavioural find different bugs; inlined dtor looks like a branch; size by enumeration; unexercised path = hypothesis; round-trip != coverage; two checks sharing an assumption are one check; rankers never filter; derive experiment params from the ISA; correct the record; fix the oracle openly; separate gates; never hand-resolve a generated header; report thin coverage as loudly as divergences |
CORRECTION: click helper type DOES work |
meta | verified | high | 100% | 2026-09-08 | Lane V's gotcha was WRONG and I propagated it in every VM brief since. The click helper's type DOES reach the game's text fields - YOU MUST CLICK THE FIELD FIRST. click 480 234 then type <name> worked every time. Saves no longer need renaming on disk. Also: chained clicks on one button work but DROP ~HALF at sleep 250, so count the result; and a mid-turn AI diplomacy popup can swallow a scripted click batch. Full coordinate set for save/quit/build/research/move is in the VM140 row |
| workload saves built (lane O) | verify | verified | high | 100% | 2026-09-08 | SEVEN saves, all --strict clean and all coverage PROVED under state_checksum --tree. Repo now holds 11 .sav. Saves live in the repo only; scp one back to the VM when a compare needs it |
| ISSUED-ORDERS saves + TurnCommands layout | objects | verified | high | 90% | 2026-09-08 | THE BLOCKER LANE W PROVED IS NOW LIFTED. Three saves taken AFTER issuing orders, BEFORE End Turn. CD[0] measured by state_checksum --tree --depth 1: every prior save (turn1/2/3, zuul-turn5, human-turn3) is BIT-IDENTICAL at 3df7d93164fb1d7d, 35 leaves/122 B - which is why nothing could be learned from them. human-turn2-orders 41/143 B (1 fleet move); zuul-turn15-orders 61/226 B (research target + boost + 5 build + 1 move); zuul-turn17-orders2 129/498 B (20 build + system-rates + colonize x3). Layout in SAVE_FORMAT.md 11: playerId, researchRate float, per-command-type present-flags, then counted lists - build {ordinal, designId, systemId, 0}, system-rates {systemId, {SRs SRt SRsc SRtf SRi SRoh SRnr}} (the ONLY named sub-frame), colonize {shipId, 1}, fleet move {fleetId, 1, destSystemId, 0}. Ids cross-check WITHIN the same file: 384=Gallandro, 432=Octans, 688=the fleet the UI showed as Dest: Octans, 216383 = the exact Imperial-Savings drop the Boost panel caused. KEY INSIGHT: the block is the PENDING CLIENT->SERVER QUEUE - zuul-turn15-orders still shows ResRate 0.25 / ResTNm '' / Sav 2,902,722 on the ServerPlayer while the block already carries 0.97 and the boost |
| research_roll_pending save + the gate constant | verify | verified | high | 100% | 2026-09-08 | zuul-turn17-rollpending.sav: ResErrRoll=True, ResTNm='WEP_GrnLas', node at TResDone 1083 / TResCost 5000 = ratio 0.217. zuul-turn16-noderoute.sav same at ratio 0. AND THE GATE CONSTANT IS PINNED AS A TYPE: 0x00a2c788 is a FLOAT 0.5, not a double - read out of dumps/sots.exe; an 8-byte read gives 5.28e13 garbage and 0x00a2c78c is float 100.0. Confirmed live: flag true entering turns 16 and 17, tech completed turn 21 with the flag already cleared. FIRING the OnTechResearched draw needs ONE TURN TO SUPPLY >HALF the tech's cost = ~2.3x this empire's RP. The in-game Boost Research panel is recorded in the command block but its pool is only ~7.5% of savings and DECAYS GEOMETRICALLY INSIDE THE TURN (197,271 -> 17,551 -> 1,561 of 2.9M), so it cannot bridge it |
| budget tail: partial, three slots shown UNREACHABLE | verify | mapped | high | 60% | 2026-09-08 | DELIVERED: [11] construction and a large [8] maintenance (Maint 14,500 in zuul-turn23-fleet23.sav, 27 DEs, vs 1,000-3,000 in every earlier save); ResRate off default. NOT DELIVERED, WITH REASONS - this is the useful half: [12] expenses - Nexp is [] for every player in ALL 11 saves, turns 1-23, human and AI, and NO 1.8 screen exposes an expense slider (map panel, Build, Research, Ranking, Intel Analysis, Empire Population Manager, in-game menu all checked); LIKELY DEAD IN 1.8. [10] debtInterest / [7] systemIncomeNeg - savings cost is deducted at QUEUE time so the queue cannot be over-committed, and Maint counts only in-service ships arriving at ~4.5 DE/turn; income FLOORS at 78,570 (measured 124,055->78,570), so a debtor needs ~157 destroyers = ~35 more End Turns. Reachable, not cheap. aid [13]/[14]/[19] - the AI DID offer a non-aggression pact (accepted, in human-turn3-noderoute.sav) but it leaves aid count 0, and 1.8 has no player-initiated treaty UI |
| waypoint type 2: NEGATIVE RESULT | verify | verified | high | 100% | 2026-09-08 | Lane O: type 3 is now covered and TYPE 2 APPEARS NOT TO EXIST FOR MOVE ORDERS. Census: turn3-state 1x type 1; zuul-turn5-species5 ALREADY HAD 2x type 3 on disk (nobody had looked); lane O's saves have 6/8/11/11/16 type 3 plus 3 in the Human game. The PLAYER'S OWN fleet is a mover: Flt 688, Wpt 432, Tp 3, nrt{nrp -1, nrf 384, nrt 432} (Gallandro->Octans, in nodespace). A fresh HUMAN game was built specifically to chase type 2 - Human node drive, single directly-connected node line (Downbelow->Delphi) - and it came back Tp 3, nrt{nrp 37, nrf 224, nrt 128}. Across BOTH node-drive races, single-hop and multi-hop, natural and rip-bored lines, planned and in-transit: 20+ instances, ZERO type 2. WaypointKind::NodeLine is NOT what a Move order produces. Types 4/5 still need a Hiver game |
| OnAllCombatDone_Tail mapped | control-flow | verified | high | 90% | 2026-09-08 | Lane K: 36 phases, WHOLE 1587-byte body read from the instruction stream. Base is S (checked against FOUR independent reads). Signature void __thiscall (S, vector<EncounterResults>*), ret 4, called from EXACTLY ONE site - StrategyHost::OnMessage 0x00784d07 on SNMAllCombatDone with msg+4. Strides enumerated from division magics: EncounterResults 0x178, Encounter 0x74, member record 0x44; the arity check between them LOGS AND CONTINUES, no early return. TRAP 1 AVOIDED AND ITS CONVERGE POINT FOUND: phase 7 is encounters.clear(), not a filter - the if is the empty-vector guard erase always carries, both arms meet 3 instructions later at 0x007d96bf, and the IDENTICAL four-argument shape appears inside vector::operator= on its empty-source path. Past that point the function is STRAIGHT-LINE: every jcc in phases 8-36 is a loop bound or one of three null tests. Three functions run TWICE per turn across the two drivers. UNREAD (the lane boundary, stated): FUN_007d5af0, 7499 B, the combat resolver under phase 6 |
| CRITICAL for the standalone milestone: the tail advances the RNG | verify | verified | — | 0% | 2026-09-08 | SUPERSEDED (lane Z: ledger closed, zero residual). LANE K'S MOST IMPORTANT FINDING. The tail draws from the strategic generator and NOTHING MODELS IT: phase 11 draws one NextFloat PER EXPIRED NODE LINE (instruction-verified at 0x007ae095) and phase 6's subtree draws NextInt/Twist. BOTH RUN BEFORE THE AUTOSAVE. So any reimplementation that reproduces ProcessTurn exactly STILL DIVERGES the first turn a node line expires. This directly threatens the "standalone runs one turn and byte-matches the autosave" milestone and must be modelled before that is attempted |
| autosave mechanism read | subsystems | verified | high | 100% | 2026-09-08 | The mechanism our determinism ORACLE rests on, finally read from bytes. StrategyHost::Autosave 0x00895210, called with endTurn=1 from SendEndTurn (pre-turn file) and endTurn=0 from the message handler (post-turn file). The (Autosave Backup) rotation fires ONLY on the post-turn call - the flag byte is (endTurn==0) because the je at 0x00895266 SKIPS the store of 0. FOUR PROPERTIES MAKE IT REPRODUCIBLE: a fixed point in the message sequence; NOTHING IDENTIFYING REACHES THE WRITER (the filename is built here and never passed on); per-player connection state is DETACHED via conn->vft[0x14] before the write and reattached after; and a deterministic gzip container. SaveGame_WriteFile 0x00877070 writes Summary/CreateParams/Sim/CDT + per-agent CD - exactly the root save_reader.py models |
| bankruptcy: 3 formula corrections | subsystems | verified | high | 100% | 2026-09-08 | ProcessBankruptcy 0x007c0a50 at phase 15, per-player UpdateBankruptcyLimits at phase 31 - which INSTRUCTION-VERIFIES formula-gaps.md Q1's ordering claim. Three corrections banked: the divisor is the double -0.15000000596046448 (= (double)(float)-0.15f), NOT -0.15; the per-system income term is max(rate,0) BEFORE summing; and 3.3 IS NOT A BINARY CONSTANT - it lives in .bss and is DB-loaded |
| turn results: half of lane T's note was wrong | control-flow | verified | high | 100% | 2026-09-08 | The records ARE filled here (phases 6, 11, 18 write S+0x2f4[PlyrIdx], found by a whole-image scan for the 0x11c stride) - but ApplyEncounterResults' tail then SWAPS S+0x2f4 into the outbox S+0x304 and refills the accumulator; SynchronizePlayer dispatches S+0x304[i] as strategy event 0x25; GenerateTurnEvents clears it. sizeof(SETurnResults)=0x11c, enumerated five ways. ALSO: DispatchTurnResults and SendTurnResultsToPlayers ARE MISNOMERS - they are vector::resize and _Ufill; NOTHING in that group sends anything |
| turn events are NOT per-turn | control-flow | verified | high | 100% | 2026-09-08 | GenerateTurnEvents is 122 bytes and GENERATES NOTHING (its copy loop at 0x007dc680 is provably dead). BuildTurnEvents is MISNAMED: its whole body is gated on a setup descriptor set in one place on the LOAD/REJOIN path, and it references NO EVENT_* string whatsoever. Also found: TurnEvents_Write/TurnEvents_Read are SWAPPED IN GHIDRA (layouts.json is right); sizeof(TurnEvents)=0x18 enumerated four ways |
| CORRECTION: S+0x8 advances TWICE per turn | control-flow | verified | high | 100% | 2026-09-08 | Lane K corrects lane T: OnAllCombatDone_Tail's FIRST INSTRUCTION increments the same word ProcessTurn does, so lane T's "lockstep" conclusion about the two per-turn counters does not hold |
| A CORRECTION WITHDRAWN BEFORE PUBLISHING | meta | verified | high | 100% | 2026-09-08 | Worth recording as method, not trivia. Lane K DRAFTED an entry claiming off_RNG was wrongly flagged as a frame exception, then checked and WITHDREW it: addresses.json already carries BOTH frames (off_RNG=0x16c for S, off_RNGPtr=0x168 for S+4), which is exactly lane T's rule, and a second call site confirmed them. The duplicate entry is gone and the near-miss is recorded in the finding. Also: lane K fixed the "exactly one NextFloat" claim across 4 notes + 4 engine files but DELIBERATELY LEFT the captured verify/results/compare/* artefacts alone - they are RUN RECORDS of what the hook said at the time, and editing them would falsify the record |
| SNMAllCombatDone on no-combat turns is an INFERENCE | verify | verified | — | 0% | 2026-09-08 | SUPERSEDED (lane Z: tail ran 8/8 turns incl. no-combat). Lane K's stated caveat: that the message is delivered on turns with NO combat is inferred from the determinism note (the post-turn autosave appears on every End Turn and this handler is its only reachable caller), NOT from the instruction stream. One VM run settles it |
| trade manager: nine unidentified virtual calls | subsystems | verified | — | 0% | 2026-09-08 | SUPERSEDED (lane V2 named all eleven). Lane K's stated blind spot (its tier 4): phase 23 is NINE CONSECUTIVE VIRTUAL CALLS on the trade manager and NONE is identified |
| TurnCommands_v5 typed - 100% NAMED COVERAGE | engine | verified | high | 95% | 2026-09-08 | Lane Q. All ELEVEN saves 99.7-99.9% -> 100.0%; opaque 37/40/43/63/132 -> 2 EVERYWHERE (only the MT19937 blob, correctly opaque). Ratchet 99.8 -> 99.99. Verified by the integrator with SOTS_SAVES_DIR set: 11 saves, round trip: tree identical, typed identical, resyncs 0, hint-failures 0. THE RECONCILIATION - lane W's arithmetic AND conclusion were right but the disagreement had THREE causes, only one a branch: (1) the sequence lane W aligned against was OFFSET-SORTED, not write-ordered - the writer is flag-then-payload with each gate bool AFTER the payload it gates, so TurnCommands is one of the 89 classes where offset order != write order, and "item 4 is 8 bytes, which can only be a bool where the recovery says i32" is an ARTIFACT OF THE SORTED VIEW. ALIGN SAVES AGAINST streams.json, NEVER layouts.md. (2) The tail is 27 container CALL SITES, not 27 scalars: twenty-seven std::list members at 0x70..0x1a8 stride 0xc allocator-last, each writing WriteInt(size) then records; the linear recovery keeps one item per call site and DROPS THE COUNT WORD, so 44 = 17 members + 27 call sites and lane W's "27 trailing ints, 22 i32 slots" is the same 27 seen from both sides. (3) All 27 lists are ALWAYS written, so an empty one still costs a zero int: 8 prologue + 27 counts = 35, the bit-identical block every earlier save carried. Item arithmetic closes to the unit on all five workloads (35/38/41/61/123) |
| CORRECTION to lane O: fleet-move element | objects | verified | high | 100% | 2026-09-08 | Lane Q: the element is {fleetId, nHops, nHops x systemId} - a COUNTED ROUTE VECTOR - not {fleetId, 1, destSystemId, 0}. Only the three-item reading balances: a four-item element needs 26 lists on human-turn2 and 28 on zuul-turn15, and the writer has 27. ALSO: NEITHER noderoute save carries a fleet move, so the node-route UI does NOT queue through that list. SAVE_FORMAT 11 corrected in place |
| SECOND defect found at main: NVs PID tag | verify | verified | high | 100% | 2026-09-08 | Rule 6 exactly, and it was live at main. zuul-turn23-fleet23.sav is the FIRST save with a non-empty NVs list; its element's leading id had been typed positionally as "." when the real tag is PID. The typed round trip on that save DIFFERED AT 0x89c14 at engine c883a32, BEFORE any of lane Q's changes - i.e. a real round-trip defect that eleven saves' worth of green tests had never exposed because no earlier save populated the list. Independently corroborated: the recovered table for Game::ServerSystem names that item PID, and the Sys row moves 102 matched/1 wire-only/3 shape-only -> 103/0/2. ALSO: zuul-turn17-orders2 was BELOW the old coverage ratchet, so that gate was already failing on lane O's largest save |
| TurnCommands: what stays a hypothesis | verify | backlog | — | 0% | 2026-09-08 | Lane Q labelled honestly: 22 of the 27 lists are scalar sequences read off the helpers with NO SAVE EXERCISING THEM. Nested bodies not otherwise modelled (ShipDesignDef, FleetLayout, WeaponGroups, DefenceLayout, RaidTargets) are carried as opaque Nodes rather than guessed. Three prologue gates never seen set, one a whole CivilianRatios frame. List 14 is OBSERVED BUT NOT UNDERSTOOD: issuing one fleet move also queues {sameFleetId, 0, true} there. No version word exists - the version is in the CDT id and select() matches the exact .TurnCommands_v5 suffix, so a _v6 falls back to the carried Node (unit-tested) |
| save_reader deliberately NOT mirrored (rule 8) | verify | verified | high | 100% | 2026-09-08 | Lane Q declined to mirror the CD shape into the Python oracle and wrote the reasoning down: it reads CD generically for the AIAgent bodies too; state_checksum's digest tree is built from the GENERIC tree so typing CD adds no named leaf to any diff; nothing consumes the typed dict; and RULE 8 POINTS STRAIGHT AT IT - a layout mirrored into both readers from ONE reading is not two checks. Instead test_save.cpp now asserts on every real save that each block is consumed with NOTHING LEFT OVER (extra.empty()) - the item-granular statement a wrong list count or element width breaks first |
| 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. 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. 0x0088b613 IS NO LONGER UNFIRED (lane AR 2026-09-09): calls=1 words=1 on turn 42, raider on sector 816. 0x00820c1b also fired, but through CreateRaidEncounter's caller, never through B - the species short-circuit blocks B's second draw for Human and Zuul raiders. CORRECTED AND ADVANCED 2026-09-09 by lane AP. 0x00840a3c FIRED (1 word, Frame sdet+3). 0x00840929 and 0x008409c7 are NOT two more sites behind the same gate - they are the MORRIGI branch of SpyManager::Slot13RngCallee (cmp [spyOwner+0x5c],6), mutually exclusive with 0x00840a3c and unreachable in any Human/Hiver/Tarka/Liir/Zuul game. Closing them needs a MORRIGI empire (and, for 0x008409c7, a third surviving empire). Of V2's eight, only 0x0082cdb8 (trade 15) is now both reachable and unfired. |
| 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 |
| Mars::RNG::NextUInt read completely | objects | verified | high | 100% | 2026-09-08 | 0x004f7670, 84 bytes, EVERY INSTRUCTION READ: if (left==0) Twist(&mt); y=*next++; --left; temper(y); return y. EXACTLY ONE WORD, unconditional, no loop, no branch but the lazy twist, no scaling with game state. Ghidra's 84 is CORRECT here (body ends 0x004f76c3, 12 int3 to 0x004f76d0) - worth stating, since rule 17 is about not TRUSTING the size, not about it always being wrong. CONVENTION TRAP: ECX is the RNG OBJECT, not &mt - it does lea esi,[ecx+4] itself, where NextFloat/NextInt are entered at object+4, and ProbabilisticJump uses BOTH conventions 0x6b bytes apart |
| EncounterDetect_AssignContacts + "a doomed roll still moves the generator" | verify | verified | high | 95% | 2026-09-08 | FUN_007aa240 (Ghidra says 944; REAL BODY IS 953 - Ghidra's end lands inside a push at 0x007aa5ee, real end 0x007aa5f9; nine hidden bytes, only a throw stub, no draw-count change, but the range is wrong). Draws ONE INLINED NextFloat PER (contact, detector) TRIAL, BEFORE THE ACCEPT TEST. Threshold 0.25f if the detector's TechTree has tech 0x2728/0x2729 else 0.0f; accept iff thresh >= r, derived from fcompp/test ah,5/jp - EQUALITY ACCEPTS. words = sum over contacts of min(trials-to-first-accept, |
| RNG entry points: SEVEN, not three | objects | verified | high | 100% | 2026-09-08 | Lane I: NextFloat, NextInt, Chance, plus NextUInt, FloatRange 0x0047d8a0 (1 word, NARROWS TWICE, in ProcessTurn's closure at depth 3 with two call sites), IntRangeBell 0x008e6d80 (triangular, >=2 words) and GaussianRange 0x008e6e30 (2 words PER ATTEMPT, UNBOUNDED, both draws inlined). AND IT SCALES BY 2^-32 WHERE NextFloat SCALES BY 1/(2^32-1) - TWO DIVISORS IN ONE IMAGE. GaussianRange documented but deliberately NOT modelled |
| RNG residual: 18-20 words STILL UNEXPLAINED | verify | verified | — | 0% | 2026-09-08 | SUPERSEDED (lane Z: every word attributed; 16 = trade-raid generation behind a virtual edge). THE HONEST HEADLINE. Lane J predicted the two inlined functions would explain lane Z's per-turn gap. NOT CONFIRMED. Both new sources are gated and neither has been measured: 0x007aa240 contributes 0..( |
| CORRECTION: no Mars MT19937 variant | objects | verified | high | 100% | 2026-09-08 | Lane I correcting ITSELF (rule 11). It had written into Ghidra that 0xff3a58ad/0xffffdf8c are a Mars variant of MT19937. THEY ARE NOT - they are the textbook masks applied BEFORE the shift: (y & 0xff3a58ad) << 7 == (y << 7) & 0x9d2c5680, verified over 200k words. mars::rng in sots-engine was NEVER WRONG. Corrected in place in Ghidra and in the fragment. NOTE FOR FUTURE SCANS: a scan for the TEXTBOOK constants finds NOTHING in this image, which is exactly why rule 16's scan must use these pre-shift values |
| STANDALONE SCAFFOLD RUNS | engine | verified | high | 100% | 2026-09-08 | THE NORTH STAR MADE CONCRETE. sots_turn SAVE --roundtrip --phases --out POST.sav --metric m.json: loads a real .sav through mars::stream, PROVES THE FOUNDATION FIRST (re-serialises the untouched parse and byte-compares against the inflated stream; if that fails the run STOPS), walks the published phase order of ALL THREE drivers (host steps, StrategyServer::ProcessTurn's 32 with ServerPlayer::ProcessTurn's 12 nested at phase 13, then OnAllCombatDone_Tail's 37), PRINTS EVERY PHASE IT DOES NOT RUN, writes post-turn state back through write_save + gzip, emits a completion metric. Driven over all 11 saves: 11/11 load, run, re-serialise and re-read cleanly. Integrator-verified on CT111 with real saves: ctest 38/38, and the trace output labels hypotheses INLINE (e.g. "3 judged stable by the owned/not-abandoned stand-in (HYPOTHESIS -- the original asks a callee)") |
| standalone divergence vs the oracle | verify | mapped | high | 100% | 2026-09-08 | Leaves localised by state_checksum.py, coverage PROVED on every save in every comparison. turn1->turn2: baseline 209 diverge -> 204, CLOSED 5, REGRESSED 0. turn2->turn3: 108 -> 103, closed 5, regressed 0. Closed the same five on both pairs: /Summary/Turn, /Sim/Frame, and ntdev on Gamma Cephei / Ke'Dolarra / Koa'Vo. Remaining 204: /Sim/players 82, /Sim/systems 80, /Sim/turnstats 24, /Sim/SvSctOb 8, plus 10 singletons (ModCount, RNG, Checksum, NMnx, cmbtid, four id lists, NumFlts). BYTE MATCH: NO, AND IT CANNOT BE YET - 0 of the 18-20 per-turn generator words are modelled and the generator is saved state |
| standalone completion metric | meta | verified | high | 100% | 2026-09-08 | verify/results/standalone/status.json, read by tools/dashboard.py's new section 6 (regenerate with tools/standalone_report.py). 14 of 44 turn-driver phases modelled, 7 committed; 2 of 37 tail phases. VERIFIED 0 - DELIBERATELY: in that table verified means "compared against the live game", lane S2 held no VM, and app_catalog FAILS THE BUILD if that ever drifts upward silently. Implemented+committed (9): H00 BeginProcessTurn, H01 SaveWriterInvariants, S00 ModCount bump, S11 SystemTurn (tuning-free subset), P07 clear timed-research accumulators, P08 rebellion output decay, P09 timed research bonuses (last->first, ORDER LOAD-BEARING), P10 ResearchRollPending consume, T00 tail ModCount bump. Blocked - evaluated, reported, NOT written (8): P01 P02 P03 P05 P06 all behind the unresolved population->base-output term, plus P11, S31, T31. Stub - named no-ops visible in output: 65 |
| 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. 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. Lane AR 2026-09-09: the tail's cost is k + s + z - one word per parked permitted raider, PLUS one per success on a route-sourcing sector, PLUS one more per success unless the raider's species sets SpeciesDef+0x144 (Human and Zuul only). Measured 4,4,4,4,5,3 over six turns with k = 4,4,4,4,4,3. Lane AZ 2026-09-09: the z term's gate is THREE conjuncts, not one - the route owner must not be HIVER (0x00820bc2), the raider's crew species must not be HIVER (0x00820bd7), and only then does SpeciesDef+0x144 (Human, Zuul) decide. And the short-circuit sets frac = 0.0, NOT 1.0. z is still unobserved; the cheapest state for it is a player-owned route sourced in a sector where an AI raider is already parked, which needs no fleet movement at all. |
| 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 |
| rule 17, third independent instance | meta | verified | high | 100% | 2026-09-08 | Ghidra reports 1532 bytes for GenerateTradeRaidEncounters; the real body is 1546, ending 0x0089389a, and Ghidra's end lands MID-INSTRUCTION. Same failure lane I hit on 0x007aa240 and lane J on the combat resolver. Three lanes, three functions, same defect |
| standalone generator: NO MATCH, residual attributed | verify | mapped | high | 100% | 2026-09-08 | A CLEAN BINARY NO, which is the useful kind. On lane Z's two calibrated pairs: oracle 20 words / standalone 16 / RESIDUAL 4 (left 379 vs 375), and oracle 18 / standalone 16 / residual 2 (left 359 vs 357). mt[624] IDENTICAL both times, block byte-identical (no boundary crossed) - ONLY left differs. The residuals 4 and 2 are EXACTLY lane Z's per-site split for those turns (2 detection words both, +2 research on turn 5, +0 on turn 6), so subtracting a statically-bounded 16 from the FILE oracle corroborates the site ledger THROUGH A ROUTE TOUCHING NO HOOK. Stated caveat: a subtraction confirms the SUM, not each term; the static bound carries the 16. New instrument tools/rng_oracle_check.py. MODELLED: GenerateTradeRaidEncounters, 2 Chance rolls per player-vector entry, counted INSIDE the chance helper past both early-outs, so a data file pushing an odds value to 0 or 1 removes the draw and the ledger says so (one test pins that). NOT modelled and why: the 2 detection words (NO DERIVED RULE - fitting 2/turn to three observations is not a model), the 2 research words (downstream of lane N's blocked budget term), the 0-or-1 raid-success draw |
| T36 FinalizeTurnRecords recovered | control-flow | verified | high | 95% | 2026-09-08 | Lane Y: the archive it writes IS ON THE WIRE (/Sim/turnstats/.../stats[], one element per turn per player, every save carries its own frame). Six fields recovered and checked against bytes THE GAME WROTE: 11 saves, 80 player-records, 480 fields, 0 MISMATCHES, and the same check now runs inside every turn as a phase note. TWO CORRECTIONS to combat-done-tail.md 5: inc/tdinc are THE OPPOSITE WAY ROUND from the obvious reading, and lane K's open population term is Pop + pbon (112/112) not Pop + dcs (101/112). The block is now a MEASUREMENT: --commit-blocked closes 24 turnstats leaves and REGRESSES 17 (almem x8, inc x3, sav x3, 3 census, nothing outside the unmodelled set) - so it is NOT "committing makes it worse", it is right in six fields and wrong in five, and becomes a clean +24 once S04 and the budget land |
| BnkEl INVERTS: a per-save oracle for lane N's blocker | verify | verified | high | 100% | 2026-09-08 | THE HIGHEST-LEVERAGE FIND OF THE LANE. BnkEl is injective in maxIncome (slope ~6.67) so it inverts: 25 player-records recovered across the corpus (e.g. turn1-state player 16 -> 238592). That value IS sum of max(ComputeMaxIncome(s),0) - the exact population->output term lane N is chasing - available as a PER-SAVE ORACLE NEEDING NO VM, read from bytes the game itself wrote. Tool: tools/max_income_oracle.py. The same inversion independently recovers BANKRUPTCY_PROTECTION_LIMIT_FACTOR = 3.3 from saves (all 25 in [3.299994751, 3.299999872]), corroborating lane K's "3.3 is DB-loaded, not a binary constant" WITHOUT READING THE DATA FILES. Routed to lane N mid-flight |
| CORRECTION: the -0.15 divisor defect is much worse than noted | verify | verified | high | 100% | 2026-09-08 | The repo characterised the wrong bankruptcy divisor as roughly "one ulp on large empires". THAT UNDERSTATEMENT IS WRONG - and lane Y corrected its OWN prediction which had repeated it. -0.15 and the real -0.15000000596046448 differ for EVERY maxIncome DIVISIBLE BY 3, from m=3 (-20 vs -19), and the rate climbs: 33.3% below ~1.2M, 66.7% at ~1.4M, 100% above 3M. SIX OF THE 25 CORPUS RECORDS would get a different BnkEl from the engine's current constant than the game wrote. One-line fix in economy.cpp wanting a test at maxIncome=3. Lane Y reported it and did NOT touch the file - it is lane N's |
| re-ranked path to the byte-match | meta | mapped | high | 100% | 2026-09-08 | Lane Y's ranking, superseding lane S2's now that the RNG ledger is closed: (1) PER-SYSTEM MONEY OUTPUT - now has a per-save oracle - unblocks P01/P02/P05/P06/T31 and 5 of 11 record fields, ~82 of 204 leaves and 2 of the 4 missing words; (2) S04 alliance mask - 8 of the 17 leaves stopping T36; (3) encounter detection - the last non-research generator residual, HOOK-SHAPED; (4) ship-design catalogue (hull size + 0x400) - census, ship records, part of T24; (5) ModCount - ONE leaf, but it feeds Summary.Checksum, so NO BYTE-MATCH WITHOUT IT |
| three hook-shaped questions for the VM holder | verify | in-progress | — | 0% | 2026-09-08 | SUPERSEDED (lane H holds VM140). Lane Y raised these per rule 18: bracket DetectEncounters with contact/detector counts (settles the last non-research generator residual); hook CreateRaidEncounter to separate "no roll succeeded" from "candidate list empty" (a word count CANNOT); and run one ZUUL End Turn - THE STANDALONE PREDICTS 14 WORDS THERE, NOT 16, which tests what the raid loop actually iterates over. All three routed to lane N mid-flight |
| CORRECTION: FUN_007d5a00 is RETREAT, not a combat pipeline | control-flow | verified | high | 95% | 2026-09-08 | Lane B5 corrects lane J's characterisation. It is the RETREAT pipeline - and THE LARGEST WRITER OF SAVED STATE ANYWHERE UNDER THE COMBAT RESOLVER. Lane J's "the resolver's own body writes nothing outside its stack context" STANDS; this is where a large part of "its callees write plenty" lives. Seven saved fields/containers move, ALL verified in struct-recovery.md so the autosave oracle sees all of them: StarShip.FltID (ships of a partly-retreating fleet re-parented); the StarFleet master list off S+0x64 (one fleet CREATED per partial group, every emptied fleet DESTROYED); NMnx, the object-id counter, inc [ecx+eax*4+0x10] at 0x008b8b16 once per created fleet; NShips/FtFlg (new fleet born with flag 0x400, retreaters flag 2); LocID/Pos/PrvPos/inline FlightPlan (gate retreat TELEPORTS, normal retreat installs a flight plan); ServerSystem VFlags/EFlags/AFlags/FFlags + NumFlts - AND RETREATING FROM AN UNEXPLORED SYSTEM MARKS IT EXPLORED FOR YOU; ServerPlayer.GTraf + per-system NVs/PlayerView debited and re-credited around every membership and plan change |
| OID allocation CLOSED - R2's x16 is a shl 4 | objects | verified | high | 100% | 2026-09-08 | struct-recovery.md 7's long-open "OID allocation (R2's x16)" is answered. The allocator is IDMap::AllocateID 0x008b8ae0; the counter is PER NETWORK NODE at IDMap->+0x08 + node*0x14 + 0x10, pre-incremented, never issues 0; an id is `(counter << 4) |
| retreat destination rule + a predicted ORIGINAL BUG | control-flow | verified | high | 90% | 2026-09-08 | All six phases instruction-verified, dumped to the next function start. destinations -> group -> classify whole/partial -> split -> execute -> destroy. THE DESTINATION RULE: three INDEPENDENT nearest-system searches in ONE pass - nearest owned, nearest with no hostile presence, nearest at all - first non-null wins. The independence is LOAD-BEARING (each best store sits INSIDE its predicate arm); a single-loop version gives a different system. Squared distance, float32 deltas, strict < so exact ties go to the EARLIER system. New diplomacy rule: A SYSTEM CAPTURED ON THE CURRENT TURN LOSES ITS OWNER'S CEASEFIRE COVER. FALSIFIABLE PREDICTION WRITTEN BEFORE ANY RUN: on the first turn a battle produces a partial retreat, post-turn NMnx rises by exactly the number of groups with a split fleet - one per GROUP, not per ship or per fleet. PLUS A PREDICTED ORIGINAL BUG: the fleet is created BEFORE the ship loop, so a group flagged partial creates a fleet even when every ship turns out to belong to a wholly-retreating one - A ZERO-SHIP FLEET IN THE SAVE |
| retreat pipeline: events and RNG | verify | verified | high | 100% | 2026-09-08 | EXACTLY TWO EVENT_* keys in the 327-function closure - EVENT_FLEET_RETREATED_VIA_TELEPORT (depth 1, and gate retreat is species 1 + mode 1 + a destination, the only path to it) and EVENT_FLEET_INTERCEPT_ABORTED (depth 3, posted to the INTERCEPTOR'S owner when a split empties a fleet). Both were already in lane J's 23 at the right depth; what is NEW is the condition for each and that these two are the ONLY ones this pipeline can post. DRAW-FREE CONFIRMED INDEPENDENTLY: zero RNG calls, zero inlined MT tempering immediates, including the fleet-name generator. The one 0x11c hit is a FALSE POSITIVE (FtMS = -1 in the fleet ctor) - no turn-results write here |
| new failure mode: tail-call thunk in an argument list | meta | verified | high | 100% | 2026-09-08 | Lane B5 correcting ITSELF: it first read the fleet-creation call's arguments wrong, because a ret-N TAIL-CALL THUNK IN THE MIDDLE OF AN ARGUMENT LIST makes three pushes look like they belong to the wrong call. CHECKING ret N AGAINST THE PUSH COUNT CATCHES IT. Worth a rules line - and note the same thunks HID the OID allocation chain from a closure pass entirely |
| duplicate-name check working as intended | meta | verified | high | 100% | 2026-09-08 | Two live demonstrations today. (1) Lane B5 found StarFleet_SetFlag ALREADY in addresses.json from lane B4, same address and same prototype DERIVED FROM A DIFFERENT PATH - entry dropped, agreement recorded as corroboration. (2) The generator HARD-ERRORED on my merge: duplicate address entry 'StrategyServer_off_TradeManager': in lane-t.json and lane-v2.json, catching an in-flight collision between a landed lane and a running one. Exactly the designed behaviour: two lanes disagreeing about an address surfaces instead of being silently last-wins |
| vtable inversion tool | meta | verified | high | 100% | 2026-09-08 | Lane V2, tools/vtable_map.py. VALIDATED BLIND FIRST: 12/12, inputs only the PE + Ghidra function starts + the existing RTTI walk, nothing hand-fed - site 0x007d8469 classified virtual, slot 10 recovered, receiver typed as member +0x158, class Game::ServerTradeManagerImpl, target 0x00893290, zero direct call sites. Member typing came from the StrategyServer CONSTRUCTOR (0x007d78d0), corroborated independently by a method entered on the +4 SUB-OBJECT storing the same two ctor results exactly 4 bytes lower - TWO FRAMES, ONE ANSWER, and it re-derives lane T's 0x154 from a different direction. THE MAP IS EXACT: 2,172 vftables -> class -> sub-object offset -> slot -> target plus the inverse, a vftable accepted only with a COL at [vftable-4] pointing at a real type descriptor; no inference, no FPs. ghidra/vtable-owners.json |
| SCALE OF THE INDIRECT BLIND SPOT | meta | verified | high | 100% | 2026-09-08 | 5,045 of the 5,207 functions named by a vtable slot have ZERO DIRECT CALL SITES. That is the size of what every call-graph result in this campaign was blind to. Additionally, sweeping for jumps into another function's start finds 14,958 MORE EDGES - and three of the nine parked inlined-draw functions have NO E8 CALLER AT ALL and would read as dead code without them (the same tail-call-thunk effect that hid the OID allocation chain from lane B5's first pass) |
| indirect resolution: honest precision | meta | verified | high | 100% | 2026-09-08 | Lane V2 reported this as two things with very different precision rather than one number. Of 17,577 indirect sites: 7,415 are call [disp32] import thunks (not dispatch), 6,398 are PROVEN virtual with an exact slot, 2,948 REPORT UNRESOLVED because the backward resolver refuses to cross a branch target or an unmodelled opcode. RECEIVER TYPING IS NOT SOLVED: only 163 of 6,398 (2.5%) get a class - but on those the falsification test fires on 1 in 163 (0.6%) against a 70% random baseline. THE DISPLACEMENT-ONLY ROUTE MEASURED 81% OUT-OF-RANGE VS 58% FOR A RANDOM VTABLE - WORSE THAN CHANCE - SO IT IS REJECTED OUTRIGHT, NOT FLAGGED (rule 9 with the ranker off). Three real bugs the falsification test caught: [ebp+8] typed as a member of this; constructors given a vftable's sub-object offset instead of 0; carrier spans not ending at the epilogue's pop esi |
| lane K tier-4 blind spot CLOSED | control-flow | verified | high | 100% | 2026-09-08 | The tool reproduces lane K's byte-level transcription of phase 23 EXACTLY AND INDEPENDENTLY, then names all eleven targets: trade slots 14/8/12/11/9/7/13/15 -> 0x008590d0 0x0088e8d0 0x0088e920 0x00848570 0x00868060 0x0088ad60 0x0088ef80 0x0082cca0; the ninth call is on +0x15c = ServerSpyManager slot 13 -> 0x008877b0; phase 33 = spy slots 14/15. All eleven have ZERO direct call sites. ALSO: Game::ServerSpyManager has NO *Impl - it is itself concrete over IServerSpyManager/ISpyManager/IStreamable, the opposite of the ServerTradeManager shape, so the *Impl rule is a pattern to CHECK, not to assume |
| CORRECTION: "the tail draws nothing" is true of 8 TURNS, not of the code | verify | backlog | — | 0% | 2026-09-08 | FOUR of the eleven phase-23/33 targets REACH A DRAW on the strategic generator (StrategyServer+0x16c), verified at the instruction: 0x00887c8a, 0x00840929, 0x00840a3c, 0x008409c7, 0x0088dc43, 0x0082cdb8, 0x00820e18, 0x0088b613. NONE HAS EVER BEEN OBSERVED FIRING - lane Z measured 0 tail words across 8 turns. So tail-rng-ledger.md's "the tail draws nothing" is a PROPERTY OF EIGHT TURNS. RULE-18 ITEM: hook those four callees with a save that has LIVE TRADE ROUTES AND A SPY PROGRAM. Our corpus has neither |
| RNG re-check over indirect edges: nothing contradicted | verify | verified | high | 100% | 2026-09-08 | Lane I's 22-site inventory is intact and lane Z's residual stays zero. CORRECTION to how the nine parked functions were recorded: NONE of the eleven inlined-draw functions is itself in a vftable (no dword equal to any of them exists in the image) - their TOPMOST DIRECT-CALL ANCESTORS are, and all nine resolved. CrowRuinsEncounter/SwarmEncounter are CombatEncounterBase (16 slots), NOT SVScriptObject, so ProcessTurn's slot-11 dispatch is not a route to them. NEW DRAW SURFACE: the SVScriptObject hooks on StrategyServer+0x1b4 (lane T's field), 30 derived classes, 11 of 14 slot-4 overrides / 4 of 7 slot-11 / 2 of 8 slot-25 reach a draw - with TWO LIMITS STATED: the generator arrives as an ARGUMENT at depth 2-4 so its identity is unproven, and +0x1b4's class is a HYPOTHESIS here (no ctor store found; lane T had it verified, lane V2 records the WEAKER claim). STRENGTHENED: over direct + every pinnable indirect edge, ProcessTurn's closure grows 1,430 -> 1,486 and the tail's 1,424 -> 1,668, and the only draws those ~300 functions add are the eight above. The MAXIMAL over-approximation puts half the image (19,697/41,089) in the closure - USELESS, AND LANE V2 SAYS SO rather than dressing it up |
| system output term CLOSED | subsystems | verified | high | 95% | 2026-09-08 | Lane N. OUTPUT POINTS ARE LINEAR IN POPULATION: outputPerHead = typeOutputModifier x 1.8 / 500000 = exactly 3.6e-6 per imperial head with no station. ServerSystem::ComputeTotalOutput (0x00750480, ret 8, SIDE-EFFECT FREE) is a SUM OF THREE TERMS, not a multiplicative chain: base = overHarvestDemand x SpeciesDef+0x50 + (TRes + resAvail) x stripMineFraction x 0.9 + imperial + civilian + slave; total = addictionMod x base x OutMod x sys.OutMod x setupOutMod x RebOutMod x ScOutMod. The station bonus scales ONLY the imperial term; morale ONLY the civilian one. InitPopTypeTable (0x00535ca0) builds the three-row population-type table IN CODE from x87 literals (imperial 1.0, civilian 0.33f, caps 50M/20M) - facts about the binary, not the data files. The data-file surface is a CLOSED LIST OF NINE VALUES, all measured live, incl. SpeciesDef +0x4c/+0x50 = 0/10 Human and Tarkas but 10/40 ZUUL, so a species table is NOT optional |
| output term: live verification | verify | verified | high | 90% | 2026-09-08 | Two builds, two species, both hooks in compare mode: GroupOutput 13,105 calls 0 diverged; ComputeTotalOutput 11,252 calls 1 diverged. 0 UNDECLARED WRITES IN 24,357 GUARDED CALLS turns "side-effect free" from a claim into a MEASUREMENT - which is exactly why this and not ComputeOutputFromRates (repairs ships in orbit) was the target. COVERAGE IS THIN AND LANE N SAYS SO: 24,357 calls is THIRTEEN DISTINCT SYSTEM STATES. The first run's 4,957 divergences were ALL ONE ULP on the civilian row and ALL ONE DEFECT - a 32-bit x87 build skipping the double rounding the original performs; sim::Narrow fixes it. The one residual is a single ulp on Koa'Vo in a value THE CALLER ROUNDS TO AN INTEGER, so it cannot move anything stored - recorded, not fixed |
| THE FIVE PHASES ARE NOT UNBLOCKED - the blocker MOVED | verify | mapped | high | 100% | 2026-09-08 | Lane N's honest headline. ComputeBudget needs a system's money, not its output - a SECOND chain (TradePointsToMoney) with its own population law (incomeModifier / 14000, NO 1.8). Against lane Y's BnkEl oracle via new tools/max_income_predict.py: 6 of 25 player-records match EXACTLY, and those six are every HUMAN-OWNED OR INDEPENDENT record, including turn1/2/3 on a moving workload. The 19 misses are ALL AI-OWNED; the single-system ones are short by exactly x1.1 = DifficultyMods(owner)->+4, which is NOT ON THE WIRE (formula-gaps.md Q3 already names it). Multi-system AI empires differ by more - trade routes, stations, or the suitability money cost. So the output term is CLOSED and the missing input for P01/P02/P03/P05/P06 is now the INCOME TAIL PLUS THE AI DIFFICULTY INCOME MULTIPLIER - a smaller, better-specified target with a 25-record oracle and NO VM NEEDED |
| corrections from lane N | subsystems | verified | high | 100% | 2026-09-08 | strategic-turn-internals.md 3.3 had the SHAPE of the output formula wrong, and 0x007483b0 is the over-harvest RESOURCE demand (its population is Res, not Pop; species +0x4c is ADDED, not a cap) - both corrected in place. AND LANE N CORRECTED ITSELF BEFORE VM TIME: its own 4.2 prediction mis-evaluated the strip-mine branch as a min when it is a SUBSTITUTION, so Gamma Cephei's fraction is 2.0 and its resource term 9000, not 4500 - caught by the FIRST HOST TEST, before any VM run. Static-only and labelled hypotheses (unexercised): the over-harvest branch and its max(v,1) floor (SRoh is 0 on every save), the station factor, the slave term, both morale branches, addiction, the capacity surplus |
| bankruptcy divisor FIXED | engine | verified | high | 100% | 2026-09-08 | kBankruptcyInterestDivisor = -0.15000000596046448 named in the header, with tests at maxIncome 3/6/9 (-19/-39/-59, WHERE IT FIRST BITES) and at 238592 and 3000001. Closes the defect lane Y found and correctly escalated from the repo's "one ulp on large empires" understatement to "6 of 25 corpus records get a different BnkEl than the game wrote" |
| VM AUTOSAVE CONTENTS OVERWRITTEN (recoverable) | meta | verified | high | 100% | 2026-09-08 | Lane N reported this rather than papering over it: its two End Turns overwrote the three autosave CONTENTS on VM140 (they now hold its turn-3 Human and turn-6 Zuul states). THE FILE SET - names and count - IS UNCHANGED, so every documented Load-dialog row position still holds. Lane O's byte-identical autosaves are gone from the VM AND LANE N DID NOT FAKE A RESTORE from lane F's older, differently-sized snapshot. NOT A REAL LOSS: all 11 curated saves live in the REPO (verify/results/saves/), and the determinism-oracle hashes are REPRODUCIBLE by loading ref-turn2 and ending one turn - any VM lane can regenerate them. Record the regenerated hashes when someone next does |
| FUN_007066c0 is NOT a path finder | control-flow | verified | high | 95% | 2026-09-08 | Lane P2, instruction-verified over raw .text to the next function start (Ghidra's size was RIGHT on all four functions this time - recorded because rule 17 exists, not because it was violated). THERE IS NO GRAPH SEARCH: no frontier, no visited set, no relaxation, no recursion. It is a PER-LEG CLASSIFIER AND VALIDATOR over a destination list THE CALLER ALREADY CHOSE - a helper classifies each consecutive pair and returns the waypoint type; this walks the chain, ORs the flags, records the first failing index, draws down fuel and resets it at refuelling systems. CDECL WITH 8 ARGS (not thiscall), and it ALWAYS RETURNS TRUE past the null checks - all outcome data is in out-params. The only graph structure in the whole subtree is a SINGLE-HOP ADJACENCY QUERY over a triangular per-player bit matrix; a multi-hop node route in a save is n waypoints, one per hop. NEW FAILURE MODE: two helpers READ ebx/esi LIVE-IN WITHOUT EVER WRITING THEM, so reading them as plain cdecl gives nonsense (adjacent to lane B5's tail-call-thunk-in-an-argument-list trap) |
| WAYPOINT TYPE 2 EXPLAINED - it is a NAMING ERROR | verify | verified | high | 100% | 2026-09-08 | THREE LANES CIRCLED THIS AND THE ANSWER IS THAT THE NAME WAS WRONG. A 7-entry jump table re-verified from raw bytes maps SPECIES to waypoint type: Human->3, Hiver->0, Tarkas->1, Liir->2, NPC->0, Zuul->3, Morrigi->6. TYPE 2 IS THE LIIR DRIVE. Lane O's 20+ observations were all on the two NODE-DRIVE races, both forced to 3 - so nothing is unreachable and NOTHING NEEDS FIXING. "Node line" is the wrong name for type 2 (its speed profile is the STUTTER_* curve, slowest at a star), which is exactly why IsNodeWaypoint accepts only 3 - CONSISTENT, NOT ANOMALOUS. This also puts B4's "the type-2 node-line step is wrong by construction" in doubt: that claim was made about a type it had misidentified. Exercising type 2 needs a LIIR FLEET, not a node line - far cheaper than the save lane M was contemplating |
| OrderFleetMove's three failure bits | control-flow | verified | high | 100% | 2026-09-08 | test DWORD [ebp-0x10],0x418 at 0x865499 = 0x400 |
| nav precision: one float decides a failure | verify | verified | high | 100% | 2026-09-08 | Lane P2 ranked precision by CONSEQUENCE rather than listing it. FUN_006ffa00 is the only float that decides a failure: three f32 deltas, sum-of-squares narrowed ONCE, range = min(rangeLeft, tankCap) from f32 slots - and then fmul st(0),st computes rr AND NEVER STORES IT. The comparison is f32(sumsq) <= (double)r^2, NON-STRICT. Writing f32(rr) - the natural mirror of every other narrowing here - DISAGREES EXACTLY AT THE BOUNDARY. Leg length is Mars_Vec3_Length INLINED (lane M's five narrowings verbatim, plus a third narrowing on the subtraction). fpu_cw=0x127f throughout |
| P2 predictions: written first, then checked offline | verify | verified | high | 100% | 2026-09-08 | Three predictions written from the disassembly BEFORE checking, then run against all 11 saves: 58 waypoints, 46 flight plans, 0 failures. nrt == Wpt on all 57 type-3 records; the nrf chain closes through pnd across the three-hop plans; nrp splits exactly as the branch structure says - Human (cannot bore) has only real indices, Zuul has a MIX of real indices and -1, which is what the bore path writes. AND A BONUS: turn3-state.sav's Tp=1 with nrt{-1,0,0} can only be a TARKA fleet by species elimination - a live confirmation of a SECOND table row from a save nobody was looking at. Also corrected: type 5 is a HIVER GATE THROW at a gateless system within CstR, NOT the Zuul bore; CstR now has a reader; GFlags(+0xdc) is the per-player GATE MASK (closing lane B5's open item); pnd is the node transit's ORIGIN id; FtTrans is a SECOND SAVED COPY of wpts[0].Tp |
| P4: a ~1-minute VM test of a predicted ORIGINAL bug | verify | backlog | — | 0% | 2026-09-08 | Lane P2, for whoever holds VM140: order a fleet to the system it is ALREADY AT, then onward. The leading-destination drop happens in the solver's frame only, so types[]/routes[] end up SHIFTED BY ONE and the last slot is never written. PREDICTED SYMPTOM: the final waypoint saves as Tp 0 with an empty route. Note the framing - a CLEAN result FALSIFIES the reading rather than merely failing to confirm it |
| P2 honest boundary | meta | verified | high | 100% | 2026-09-08 | Fully read: FUN_007066c0, three siblings and 20 helpers. NOT read: FUN_00705d60's tanker fold past ~0x90 bytes (the input to EVERY range decision, the largest remaining hole in the NUMBERS), the node bore, the ranking term and hash walk inside FindNodeLine, the relation scale, and MoveFleet's two classifier call sites. THREE INDIRECT EDGES ON THE MAIN LINE UNRESOLVED, the most important being (fleet->galaxy)->vft[1](), the node-graph getter the whole node branch hangs off - vtable_map.py plus an image-wide E8/E9 sweep confirm the function has exactly two direct callers and no tail-call thunks, but those three are OUTBOUND edges lane V2's inversion does not reach. AND NOT ONE LEG HAS EVER BEEN OBSERVED EXECUTING UNDER AN INSTRUMENT - only its saved output has. Also caught internally: one test check was VACUOUS (the float32-squaring probe never fired) - found and fixed with an unconditional witness |
| MULTIPLAYER: GameSpy is NOT required to join | subsystems | verified | high | 95% | 2026-09-08 | Lane G2's lead answer - THIS IS A WEEKEND, NOT A PROJECT. Three GameSpy-free paths into a session, all verified from the binary and all reachable in the shipped UI: (1) MANUAL JOIN / FAVORITES - the accept handler at 0x0076ef90 calls App_StartJoin(params, address, serverBrowser = NULL), and a NULL browser makes StrategyJoin's ctor take its DIRECT ARM (state +0x80 = 1) straight to App_ConnectToStrategyHost 0x0089a7e0 and the game's own UDP connection - NO availability check, NO peerInitialize, NO browser, NO NatNeg; (2) COMMAND LINE /join <a.b.c.d[:port]>; (3) LAN BROWSE - ServerBrowserNew(lanBrowse=1) with NO availability gate, broadcasting UDP 3369-3370, and LAN HOSTING takes a branch that calls qr2_init_socket(ispublic=0) and SKIPS GSIStartAvailableCheck ENTIRELY at 0x007c5088. CROSS-CHECK: NNBeginNegotiationWithSocket has exactly two call sites image-wide, NEITHER on the direct-address arm. CORROBORATED OUTSIDE THE BINARY: Kerberos staff 2012-12-08, "host needs to start their game in LAN mode, then other can join direct connect. It will not work in Internet mode" |
| GameSpy identity constants + hosts-redirect list | subsystems | verified | high | 100% | 2026-09-08 | From the .rdata constant table at 0x00a35cd4/0x00a35cd8: gamename swordots, secret key Z5gR9Z, game id 1381 - INDEPENDENTLY CONFIRMED by OpenSpy/UniSpy/333networks/gslist, which all carry row 1381 swordots Z5gR9Z. Hosts-redirect targets: swordots.available.gamespy.com:27900/UDP, swordots.master.gamespy.com:27900/UDP, swordots.ms5.gamespy.com:28910/TCP (the ms5 DERIVED by re-implementing the binary's own hash fold h = tolower(c) - h*0x63306ce7, % 20), natneg1/2.gamespy.com:27901/UDP, peerchat.gamespy.com:6667/TCP. Game's own ports: HostPort 3369, CombatHostPort 3370. LINKED AND CALLED: availability check, serverbrowsing SB v2, QR2, NatNeg, Peer SDK, peerchat, ghttp (MOTD only). ABSENT ENTIRELY - no string, no host, no code: GP/GPCM/GPSP login, motd/gamestats/sake/atlas, keymaster, and CD-KEY VALIDATION OF ANY KIND (no CDKEY substring in 49,781 strings) |
| multiplayer revival plan, ranked | subsystems | mapped | high | 90% | 2026-09-08 | TIER 0 (hours): VPN/LAN + manual join, NO SERVER AT ALL. TIER 1 (afternoon): availability responder, specified BYTE-FOR-BYTE - request 09 00 00 00 00 "swordots" 00, reply FE FD 09 00 00 00 00 - and LIKELY A NO-OP, because the check FAILS OPEN on DNS failure (socket == -1 returns GSIACAvailable); MATCHINGSERVICE_UNSUPPORTED was GameSpy's KILL-SWITCH, not self-detection. TIER 3 COLLAPSES TO CONFIGURATION, NOT CODE: OpenSpy and UniSpyServer both implement the exact service set and both already ship the swordots row; adding a title is one DB row plus SyncToRedis. CAVEATS, STATED: swordots is SEEDED not on OpenSpy's 132-title tested list; the public openspy.net availability responder returns "available" for ANY gamename so it PROVES NOTHING - self-host; the seeded row says queryport 6500 while SOTS reports on 3369; and its keylist omits SOTS's 16 custom QR2 keys (slot0..7, numslots, mapshape, numsys, turn, scenario, settings, slot_, ranks - ids 50-65, recovered here). 333networks RULED OUT: GameSpy v0 only, no SB v2/NatNeg/peerchat |
| two clients on ONE guest: /concurrent | meta | verified | high | 100% | 2026-09-08 | THE GAME SHIPS THE TWO-INSTANCE SWITCH. WinMain compares argv against /concurrent on ERROR_ALREADY_EXISTS and CONTINUES on a match - so /concurrent /join 127.0.0.1:3369 gives two clients on one guest, NO CLONED VM NEEDED. Address parsing is DOTTED-QUAD ONLY (localhost is rejected). Full switch list: /join, /concurrent, /startup:, /motd_, /tell |
| CORRECTIONS from lane G2 | meta | verified | high | 100% | 2026-09-08 | ui-screen-map.md corrected in place on two counts: there are 13 NETERROR_* strings and NONE IS CD-KEY (the "21 incl. 6 CD-key" and "CD-key strings survive" claims were WRONG), and the "2017 build knows GameSpy is dead" inference is replaced with the FAIL-OPEN finding. Also: SOTS1's backend died December 2012, not the May 2014 general shutdown - its agreement ran through Lighthouse Interactive |
| alliance mask rule + ModCount writers | control-flow | verified | high | 95% | 2026-09-08 | Lane A2. THE ALLIANCE RULE, byte for byte at 0x007dc871-0x007dc8c7: `rec->almem = 0; rec->almem |
| ModCount is NOT derivable from a pre-turn save | verify | verified | high | 100% | 2026-09-08 | THE RULE: ΔModCount = 2 + one per command applied out of every player's TurnCommands block (+ one per abandoned system). That is why the delta is not a function of the board - human-turn2->3 is 28 and zuul-turn16->17 is 16 on IDENTICAL 28-system/7-player boards. And the TurnCommands block in turn1/2/3-state is the EMPTY 35-item block, so the 10 non-driver bumps were issued AFTER the autosave. ModCount cannot be derived from the pre-turn save, S00+T00 are all the standalone can produce, and IT IS THE WRONG LEAF TO CHASE BEFORE THE AI. Independently confirms lane Q's block layout: the flush walks a vector at S+0x174 with stride 0x1b4 (last list at 0x1a8, stride 0xc) |
| A2 reported what the corpus CANNOT separate | verify | verified | high | 100% | 2026-09-08 | Rule 15, applied without prompting. THREE parts of the alliance rule this corpus cannot distinguish: every player has PlyrIdx == i (0 of 80 records differ), every observed AL already contains its own member's bit, and AL == 0 exactly when ALid == -1. So 1<<i vs 1<<PlyrIdx, the OR vs an assignment, and the ALid guard vs an AL != 0 guard are INSTRUCTION-STREAM READINGS ONLY. A unit test pins all three with the separating inputs no save provides, and the corpus test PRINTS THAT IT COULD NOT SEPARATE THEM |
| T36 cost measured: net positive on BOTH pairs now | verify | mapped | high | 100% | 2026-09-08 | Default run UNCHANGED at closed 5 / regressed 0 - S04 writes no save leaf of its own, the mask reaches the wire only through T36 which stays blocked. What moved is T36's COST: the 8 almem regressions are GONE, and the remainder is exactly inc x3 + sav x3 (the budget) plus 3 census leaves (lane D2's). T36 now closes ALL 24 /Sim/turnstats leaves on the reference pair, so it becomes a clean +24 the moment those two land. CORRECTION to lane Y: "which way the net falls depends on the save" is NO LONGER TRUE - --commit-blocked is now net positive on BOTH pairs (+20, +2). It stays blocked anyway, because 9 leaves would be CONFIDENTLY WRONG |
| ship designs DO have a serializer - misattributed address | objects | verified | high | 100% | 2026-09-08 | Lane D2 overturns lane D's finding. Game::ShipDesign::Write exists at 0x008325e0 and makes four stream calls. The published "ShipDesign::Write (0x008747a0) makes no stream call at all" was A MISATTRIBUTED ADDRESS, not a fact about the class - lane V2's inversion is decisive: vtable_map.py who 0x008747a0 says it is IN NO VFTABLE, and its body pops a parser stack. WHY THE SWEEP MISSED IT: RTTI shows Game::ShipDesign : Game::ShipDesignDef, Mars::IStreamable, ... and ShipDesignDef is the base and is NOT itself IStreamable - it declares its own three virtuals in a different order; ShipDesign overrides all three and the IStreamable sub-object at +0x9c holds three ADJUSTOR THUNKS back to the primary vftable. A slot-indexed sweep finds the thunk. THE *Impl RULE WITH A TWIST WORTH RECORDING: the concrete serializer can be a DERIVED CLASS, not a separate Impl - lane V2's "check the shape, do not assume it" was right |
| ship design: three sections, not five | objects | verified | high | 100% | 2026-09-08 | THREE CORRECTIONS FORCED. (1) THREE sections, not five - the campaign's "slots 3-4 reserved" was save_reader.py's trailing Rest("sections") sweeping Dtc/Dwgv into the section list and stock_designs.py decoding them as empty slots. RULE 8 EXACTLY: reader and consumer agreed and were both wrong. Two independent enumerations say three (straight-line writer; ctor eh_vector_constructor_iterator(this+0x24, 0x28, 3) closing at 0x9c = sizeof(ShipDesignDef)). (2) DWep/Dwgv are BOOLS, byte-indistinguishable at a 4-char tag - same class as odet. (3) Offset order != write order: array [+0x24,+0x4c,+0x74] = mission, command, engine; WIRE = command, mission, engine. Oracles fixed openly (rule 12): save_reader.py 49/49, stock_designs.json regenerated with the diff ONLY raw_slots 5->3 and dWep int->bool across all 127 designs, design rules still 32/32 and 127/127 |
| hull size and the 0x400 flag | objects | verified | high | 90% | 2026-09-08 | 0x400 = defence_platform, read off the .shipsection parser's own bit setter at 0x005749b7 directly after the strcmp against the literal; the design's word is the OR across its <=3 sections. NOT lane B5's 0x400 - that is a FLEET flag, on the wire as FtFlg (turn1-state fleet 0 has FtFlg 1024). Different word, different object. HULL SIZE = section_class via ParseShipClassName 0x0056e1c0, _stricmp against Destroyer/Cruiser/Dreadnought -> 0/1/2; absent or unrecognised -> 0 WITH A LOG LINE, NOT AN ERROR (16 shipped sections have no section_class); ASSIGNMENT NOT MAX - last resolved section in MEMORY ORDER wins. Both live on ShipDesign, NEITHER ON THE WIRE, both rebuilt by UpdateDerivedStats. Independent corroboration: the same bit picks default hull health (500/3000/15000 without, 100/500/1000 with) - platforms, not ships. CENSUS VERIFIED: 11 saves, 503 designs, 480 leaves, 0 mismatched, computed TWICE independently (Python off save_reader, C++ off the engine's shapes reader - they DISAGREE about section count and still agree on all 480) |
| census coverage: 3 of 6 leaves UNEXERCISED | verify | backlog | — | 0% | 2026-09-08 | Rule 15 again, unprompted. Only 32 of 480 census leaves are NONZERO: cls0 shpt 18, cls0 satt 3, cls1 shpt 0, cls1 satt 0, cls2 shpt 11, cls2 satt 0. Three of the six census leaves are UNEXERCISED, NOT VERIFIED - no cruiser of any kind and no DN platform in the corpus. ONE SAVE WITH A CRUISER CLOSES TWO OF THEM. Also 0 ships with an unresolvable design, 0 unclassifiable designs, 0 designs where first- and last-resolved section disagree on hull size |
| T36 census wired; a NEW blocker found by a wrong prediction | verify | mapped | high | 100% | 2026-09-08 | Lane E2. Default run UNCHANGED at closed 5 / regressed 0 on both pairs, exactly as predicted (T36 does not commit by default). --commit-blocked regressions fell 9 -> 7 (pair 1) and 11 -> 9 (pair 2). PREDICTION WAS 6 AND 8, AND THE MISS IS THE FINDING: falsification hypothesis 1 fired - two census leaves closed (shpt[2]=5 dreadnoughts, satt[0]=10 platforms) but shpt[0] did not, and the archived value is higher than ours by EXACTLY ONE DESTROYER ON BOTH PAIRS (0 vs 1, then 1 vs 2). The self-check on the INPUT turn is exact, so this is NOT a classification error - it is a NEW NAMED BLOCKER: no phase we run builds a ship (S11's build-queue sub-pass is the declared input boundary). Census itself reproduced through the app's own path: 11 saves, 80 records, 1040 fields, 480 census leaves, 0 mismatches - and it visits slots in the ORIGINAL'S IN-MEMORY ORDER (mission, command, engine) rather than the wire's, so it is an independent route to lane D2's 480/480 |
| T36 remains blocked on TWO named things | verify | backlog | — | 0% | 2026-09-08 | Neither is lane E2's: (1) sav/inc x3 = the budget, lane E1's income chain; (2) shpt[0] x1 = unmodelled ship construction. The briefed --commit-blocked-except= knob is NOT IMPLEMENTABLE and lane E2 did not fake one: the archived record is ONE STRUCT ON THE WIRE, so sav/inc cannot be omitted while the other eleven fields are written - the format has no hole. It implemented the same convention at PHASE granularity instead (--commit-blocked=IDS / --commit-blocked-except=IDS), which is what isolated the T36 rows and cleanly separated S31's two Player/Status regressions out of the pair-2 number |
| standalone data root | engine | verified | high | 100% | 2026-09-08 | The standalone grew --data DIR / $SOTS_DATA_DIR. NO GAME DATA EMBEDDED; absence costs exactly the six census counters and SAYS SO. Note for integrators: with a data root set, game_data_realdata fails - and it FAILS IDENTICALLY ON UNMODIFIED MAIN, because the extracted sots.gob is a different data version than the counts that test pins. Not a regression; worth pinning the data version in that test |
| THE STRATEGIC AI IS A StrategyClient, not part of ProcessTurn | control-flow | verified | high | 90% | 2026-09-08 | Lane AI1, instruction-verified. One extra Game::StrategyClient (0x708 B) is built PER AI PLAYER at SNMRunAI (net msg 0x3d) by StrategyApp::RunAI 0x008706f0, carrying a Game::StrategyAIAgent at StrategyClient+0x12c, and it receives the same SE* events the human client does. THE PER-TURN TRIGGER IS SEResumePlaying, client event 0x26, raised by StrategyServer::ResumePlaying 0x007ddc90 to every player with Status==0; it lands in StrategyClient::OnResumePlaying 0x00777480 which does if (this->+0x12c) agent->vt[1](0x26, ev) - and THAT CALL RUNS THE AI'S WHOLE TURN SYNCHRONOUSLY ON THE MAIN THREAD. Order: ProcessTurn -> OnAllCombatDone_Tail -> "(Autosave EndTurn)" [the save is written HERE] -> SNMResumePlaying -> each AI client's ~30 phases -> cl_EndTurn -> human End Turn -> SNMUpdate -> ApplyTurnCommands (ModCount++ per command) -> ProcessTurn. EXACTLY THE SHAPE LANE A2 MEASURED. CORRECTION to turn-spine.md 2.3: RaiseAIPrepareTurn/SEAIPrepareTurn (type 9) is CONSTRUCTION, not a turn hook - two callers, RunAI and CreateGame, both one-shot |
| how an AI decision becomes a command | control-flow | verified | high | 95% | 2026-09-08 | THERE IS NO SEPARATE AI ORDER CHANNEL - the join to everything we already own. StrategyClient+0x160 is an ACCUMULATING TurnCommands; +0x4d8 is the send buffer. ~21 client order methods in 0x00762ca0..0x00763f60 each do lea ecx,[this+0x160] and call a TurnCommands::Add* helper. At End Turn, StrategyClient::BuildTurnCommands 0x00783780 does Clear(+0x4d8); +0x4d8 = +0x160; playerId; SetResearchRate - WHICH INCIDENTALLY EXPLAINS why every save's block has the research-rate gate set and the other five clear. SendEndTurn copies +0x4d8 into SNMEndTurn. 11 of the 21 order methods are called directly from the AI module; 7 of those are also UI-reachable. So ModCount is downstream of StrategyClient+0x160, and RUNG B REDUCES TO FILLING THAT OBJECT THE WAY THE AI DOES |
| AI determinism: every client seeded with ZERO | verify | mapped | high | 85% | 2026-09-08 | All AI randomness uses a PER-CLIENT Mars::RNG at StrategyClient+0x134 (operator_new(0x9cc) + RNG_Seed(ctorArg) in the client ctor); all six direct NextInt sites in the AI band reach it, as do the two facade helpers cl_Chance and cl_RandRange. ZERO AI CALLS TO THE STRATEGIC GENERATOR - which closes lane Z's zero residual FROM THE OTHER SIDE, and neither lane has to weaken anything. The seed comes from NextUInt() on a static g_GlobalRNG at 0x00af6e58 that is NEVER SEEDED (its only static init writes the IStreamable vftable; none of the six RNG_Seed call sites targets it), and AN ALL-ZERO MT STATE IS A FIXED POINT OF THE TWIST - so every AI client is seeded with 0. GOOD: constant and reproducible. BAD: the AI's generator state is in NO SAVE - the StrategyAIAgent::Streamable block's 35 tags carry none. UNVERIFIED LIVE: P1/P2 routed to the VM holder SCOPE, MEASURED 2026-09-09 (lane BP): on the rich turn ad-turn27 pinning does NOT collapse two processes to one. It collapses 94 leaves to 35, and the 35 are a single fleet-id transposition with every decision leaf identical. Pinning explains the AI's CHOICES on a rich turn; it does not explain the ORDER in which two new fleets are matched to their ids. BOUNDED 2026-09-09 (lane BS): a turn that mints new client fleet ids is not thereby a turn whose visit order varies. AR turn 38 minted two (counters 154 and 155, both absent from ar-turn37) through six entries to AssignFleetsAndIssueOrders, and four processes (AR's three hooks=off plus BS's instrumented one) produced 15b99255... byte for byte - outcome set 1 on that turn. |
| P2: the test that decides whether Rung B is stated correctly | verify | in-progress | — | 0% | 2026-09-08 | ROUTED TO LANE H (holds VM140). Load one save twice IN TWO FRESH PROCESSES, End Turn in each, diff the autosaves. Predicted byte-identical incl. ModCount. IF IT HOLDS, Rung B stays as written (implement the AI, the byte-match is reachable from a save alone). IF IT FAILS, Rung B is UNREACHABLE FROM A SAVE ALONE and must be restated as "reproduces a recorded command stream", making --turn-commands injection the primary path instead of writing an AI. A roadmap-level answer for ten minutes of VM time, and it must be TWO SEPARATE PROCESS LAUNCHES to catch per-process seeding - distinct from the oracle restoration, which re-runs the same path |
| CORRECTION: fuzzy::IAIRule is the TACTICAL AI | subsystems | verified | high | 100% | 2026-09-08 | A framing error the roadmap and several briefs carried: Game::fuzzy::IAIRule is the tactical/COMBAT AI - its only four implementors are TacAISquadRule_*. The STRATEGIC AI is 34 IAITask goals + 29 IAIRelationScale/Shift diplomacy terms + AIRulesDB (mostly SHIP DESIGN, and AIRulesDB::SelectRules_T is the ship-design selector, NOT the task selection loop). Only the strategic half is on the Rung-B path |
| AI module boundary: lane AI1 read ~3% | meta | verified | high | 100% | 2026-09-08 | The module is 1,629 functions / ~360 KB (dense core 0x680000-0x6d1000) and lane AI1 read the spine, maybe 3%, and says so. NOT DONE: the 34 IAITask bodies - NOT ONE - and IAITask's 8 pure virtuals are unnamed; THE TASK SELECTION LOOP IS UNFOUND; 19 of the 30 AI Process-Turn phases are unclassified addresses; the order-method -> list mapping (mechanical, one pass); and g_CurrentClientIndex 0x00ae4808 is uninstrumented - who sets it and in what ORDER AI clients are stepped matters, BECAUSE ModCount IS A COUNT. ONE HOP IS INFERRED NOT READ: StrategyAIContext::Broadcast 0x006b3840 delivering to the queued callback that reaches OnAIPacket - the identification is safe (one caller, registered by one function, jump table indexed by exactly the codes that function emits) but a next lane should finish it. ALSO UNSETTLED: AIProcessMinTime + Game::AIProcessingDialog hint at a think-time throttle; no deferral seen in OnAIPacket but no per-frame driver was looked for, and if an AI turn CAN span frames the clean "all AI orders in before the human's End Turn" ordering needs revisiting |
| nve visibility CLOSED - 46 leaves, 0 regressed | engine | verified | high | 95% | 2026-09-08 | THE BIGGEST SINGLE MOVE THE STANDALONE HAS MADE. closed 46 / regressed 0 on turn1->turn2 (204 -> 158) and closed 16 / regressed 0 on turn2->turn3 (103 -> 87); against the do-nothing baseline that is 209 -> 158 (51 closed) and 108 -> 87 (21 closed). The brief's 32-leaf nve target closed IN FULL (8 NVE counts, 8 EPid, 8 ETS, 8 Eid) plus 8 ltis and 6 EFlags on pair 1. PREDICTION COMMITTED IN 55ea86f BEFORE ANY ENGINE FILE EXISTED AND MATCHED LINE FOR LINE. VALIDATED ON THREE PAIRS IT WAS NEVER FITTED TO, all zero regressions: human-turn2->3 353->311 (42 closed, a DIFFERENT game/map/species mix, 21 systems), zuul15->16 276->264 (12), zuul16->17 341->329 (12). 128 leaves across five pairs, 0 regressed |
| THE GATE IS AFlags - and the near-miss is the finding | verify | verified | high | 100% | 2026-09-08 | NVE is a std::map<playerIdx, {int16 ETS, int32 Eid}> - what this player LAST SAW at this system: turn of sighting + the encounter type that was there. Writer 0x00756300 (ETS = server->Frame), called from tail phase 17 under one gate, BYTE-DECODED at 0x007cf7a7..0x007cf7ce, not taken from the decompiler. THE GATE IS AFlags - the DERIVED, NON-STICKY union FFlags |
| ltis writer NAMED (was an unnamed offset) | objects | verified | high | 90% | 2026-09-08 | Lane E3 names a writer this board recorded as unnamed: ltis's writer is 0x00743ec0, if AFlags != 0: ltis = Frame (decompiler-level, not byte-decoded). Also EFlags |
| visibility: what remains and the workloads | verify | backlog | — | 0% | 2026-09-08 | (1) Eid's source is a HYPOTHESIS - the original reads StarSystem+0x184, set once at map generation and NOT ON THE WIRE; the engine recovers it from the encounter fleet's FtEnc and agrees on all six encounter fleets in the corpus. NEEDS: a save where an encounter fleet is DESTROYED at a system that stays visible. (2) NVO.TShn (10 leaves/pair) EVALUATED AND REPORTED, NOT WRITTEN - its gate is demonstrably NOT AFlags (Spica has AFlags==0 and TShn moves anyway); needs a watchpoint on the +0x274 map, with Spica vs Bismol as the discriminating pair. (3) rcex (6 leaves/pair) toggles 0 -> 1<<16 -> 0 with this cluster; UNEXPLAINED, UNASSIGNED. (4) NEVER EXECUTED BY ANY SAVE: the map's multi-entry ordering (every corpus system has a single-bit AFlags) and the alliance intel-sharing rule 0x00754d90 (no save has an alliance) - needs a two-empire-contact save |
| income chain 25/25 - and the roadmap's item 1 was WRONG | subsystems | verified | high | 90% | 2026-09-08 | Lane E1 took the BnkEl oracle from 6/25 to 25/25. THE MULTIPLIER RESOLVES FULLY: StrategyServer::IncomeDifficultyMod 0x0080f470 returns f32(f32(triple[1]) x f32(server->IncMod)) where IncMod is the Sim block's own tag, ON THE WIRE; DifficultyMods::Select 0x0059b490 returns the AI triple iff p->[0xf9] && !p->NPC; ServerPlayer+0x36c is an UNNAMED UNSAVED pointer filled by ServerPlayer::Read via LoadDifficultyRow. THE TABLE IS BUILT IN CODE (BuildDifficultyTable 0x005a3870) from .rdata float literals - no data file, no GlobalConst key, the same shape lane N found for the pop-type table; id 1 (which EVERY corpus save carries) is AI {3.0, 1.1, 1.5}. Q3 WAS WRONG about the third column: it is a RESEARCH multiplier, not a trade one, and the first is a fleet-maintenance DIVISOR. BUT P01/P02/P03/P05/P06 ARE NOT UNBLOCKED AND MY ROADMAP ITEM 1 WAS WRONG: ComputeBudget at 0x008631fd BRANCHES - projected mode uses ComputeMaxIncome, THE TURN PATH USES ComputeOutput 0x00751fb0 WITH THE SYSTEM'S OWN Rts SLIDERS, where the repair pass runs and the unspent-industry/terraform cascades into the money channel are live. A strictly larger function, and nothing in the corpus states its answer. The RNG reason narrowed from "a system's money is unmodelled" to "the PROJECTED-RATE money is" |
| E1's own prediction was wrong, and recorded as wrong | verify | verified | high | 100% | 2026-09-08 | Lane E1 predicted the 12 remaining Zuul misses were the suitability cost. THEY WERE NOT - every corpus colony sits EXACTLY at its species' ideal, so CalcSuitMod is 0 and that whole term is UNEXERCISED. The misses were in the OUTPUT half: SpeciesDef +0x4c/+0x50 are PER SPECIES (0/10 Human & Tarkas, 10/40 Zuul) and the predictor carried them as one global pair; 400 output points = 2000 money per Zuul colony, and the observed 4400 and 5566 deltas fall out TO THE UNIT. FALSIFICATION ACTUALLY RUN: model 25/25; "nobody is AI" 14/25 (the 11 AI records break); "both real players AI" 14/25 (the 11 human records break, each by x1.1); human species pair forced on Zuul 0/2 |
| T31 unblocked in substance, still listed blocked | verify | mapped | high | 90% | 2026-09-08 | closed 0 / regressed 0 - it self-checks every run against the save's own BnkEl (8/8, 7/7 ... on ALL 11 saves) and closes nothing because the limits move between turn1 and turn2 from CIVILIAN POPULATION GROWTH, which is not committed, so our value equals the input's. Still blocked on two named things, NEITHER THE FORMULA: ServerPlayer+0xf9 (is-AI) is a game-setup input not on the wire (--ai-player N supplies it), and BnkPr needs BANKRUPTCY_PROTECTION_LIMIT_FACTOR from the data files - committing it from a zero constant REGRESSED a leaf in the first measurement, so it is now gated on haveTuning |
| NEW WIRE FACT: ISsp/ISsu is server->IdealSuit[] | objects | verified | high | 100% | 2026-09-08 | Lane E1: the Sim block's ISsp/ISsu IS server->IdealSuit[] (0x0080f4b0, raw base +0xf8), randomised per game by the map generator, cross-checking against every ServerPlayer.IdealSuit in all 11 saves. So the suitability cost's ideal needs no data file. struct-recovery.md 5 lists the tags without naming them |
| income chain coverage - read before quoting 25/25 | verify | backlog | — | 0% | 2026-09-08 | Lane E1's own caveat list: UNTESTED are the suitability cost (x0 everywhere - every corpus colony sits at its species' ideal), slaves, addiction, morale, stations, the civilian capacity surplus, difficulty levels 0 and 2, and the maintenance/research columns of all three difficulty rows. aidf is 1 on all 25 records |
| determinism oracle REPRODUCED after everything changed | verify | verified | high | 100% | 2026-09-08 | Lane H, first task. ref-turn2 + one End Turn wrote (Autosave EndTurn) = bb4fd9ac89f41e3b... and (Autosave) = 978041acd168b56e... - IDENTICAL to the historical values, with on-screen savings matching too (289,688 -> 532,369). This revalidates the oracle across EVERYTHING that changed since 2026-09-07: nineteen template hooks and seven RNG detours instead of one asm stub, the x87 module, eight save files instead of four, and a Zuul game played to turn 64 in the same directory. Reproduced TWICE MORE with hooks=off, two fresh processes |
| THE INSTRUMENT PERTURBED THE GAME (rule 19) | verify | verified | high | 100% | 2026-09-08 | NO PREDICTION COVERED THIS. Three End Turns from ref-turn2 gave 19/18/19 words where lane Z recorded 19/18/20 - from a BYTE-IDENTICAL input - producing an autosave 4 bytes different. Six single-End-Turn runs bisected it (lane H added a probes= config key so the set is switchable): hooks=off, all template hooks + 7 draw-site detours with probes=0, +probes 0-3, +probes 4-7 ALL give b2124798...; +probe 8, a MinHook detour on GenerateTradeRaidEncounters 0x00893290, gives 058656.... TWO CONSEQUENCES: lane Z's ledger is behaviour-neutral and its published numbers STAND (never checked before - it came out right by luck, not design), and every number in this lane was re-taken at probes=8. MECHANISM UNDETERMINED: the 5 patched bytes land on a clean boundary (push ebp; mov ebp,esp; push -1) with no branch target inside, and the SUPPRESSED draw is RollResearchEvent's, which runs EARLIER IN THE TURN than the hooked function. Now rule 19 |
| P2 HELD: Rung B stays as written | verify | verified | high | 100% | 2026-09-08 | Lane AI1's priority insert, run by lane H before release. ref-turn2 loaded in TWO SEPARATE PROCESS LAUNCHES (pids 3400, 2028) with hooks=off, one End Turn each -> BYTE-IDENTICAL autosaves, equal to the oracle. Byte-identity subsumes ModCount and Summary.Checksum. So the AI is fully reproducible from a save alone and Rung B does NOT need restating - implement the AI and the byte-match is reachable. Seven fresh processes across two saves agreed today in total. P1 NOT ANSWERABLE AS SPECIFIED: session.log after a full load + End Turn with an AI player is 22 lines with no RunAI:/random seed/AI Process Turn - level-2 logging is not emitted by this build; one suggestive counter-indication to the all-zero seed is recorded as a hypothesis with the decisive test named. P3 not run (no watchpoint armed) |
| tail callees: ENTERED every turn, gated INSIDE | verify | verified | high | 100% | 2026-09-08 | A much stronger negative than the ledger had. All four outer callees (0x008877b0, 0x0088db80, 0x0088ef80, 0x0082cca0) are entered 1x per End Turn, 3 turns, 2 saves; all three inner draw-bearing functions are entered 0x. No draw-site row at any of the eight return addresses; tail cost 0. So "never observed firing" was about A GATE, not reachability. Reading the bodies: slot 15 loops the trade-route vector and spy slot 14 the spy-program vector (empty containers) - but slot 13 loops the FLEETS vector, so lane H committed a competing prediction that a fleet-rich save would draw. FALSIFIED: on zuul-turn23-fleet23 (23 fleets) slot 13 ran and the inner function still ran zero times, so the gate is the PER-FLEET TEST CHAIN, not the loop bound |
| CreateRaidEncounter: hypotheses separated (rule 20) | verify | verified | high | 100% | 2026-09-08 | Entered 2x on Zuul turn 24 and 1x on turn 25, drawing 0 words every time. So rolls succeed at about the predicted rate and THE CANDIDATE LIST IS EMPTY - the two hypotheses a word count could not separate. Lane H also corrected lane Z's prior: three quiet turns at that rate is 1-in-720, not "unremarkable". CAVEAT FOUND AND REPORTED: 0x007d5150 ALSO dispatches slot 17, so lane Z's caller list is incomplete and an entry count alone cannot attribute |
| Zuul word count: 14, exactly as predicted | verify | verified | high | 100% | 2026-09-08 | Lane Y's standalone predicted 14 words on a Zuul turn, not 16. Both Chance sites read 7 calls / 7 words on two consecutive Zuul turns (vs 8/8 on Human), turn total 18. The raid loop iterates StrategyServer::Players and nothing else - lane Y's trade_raid.cpp is right. A NUMBER NO EARLIER RUN PRODUCED |
| DetectEncounters residual is NOT where anyone thought | verify | verified | high | 95% | 2026-09-08 | EncounterDetect_ProcessTeamRecord recorded ZERO calls over five End Turns while an entry probe on EncounterDetect_Run recorded one per turn - Run calls it once per record, so the record vector is empty. The +0xfc gate is never even evaluated; lane I's |
| trade-route + spy workload NOT BUILT, with reasons | verify | backlog | — | 0% | 2026-09-08 | Lane H could not build it and said exactly why: from ref-turn2 (turn 2, 1 colony, 0 techs) trade income needs trade-station construction PLUS its tech - tens of 30-60 s End Turns with a Build/Research click path each; the corpus's newest Zuul save (turn 23) STILL has no trade routes (slot 15's body did not execute on it); and NO LANE HAS IDENTIFIED WHICH UI produces an entry in the spyManager+0x10 vector, so there is no click path to write down. WHAT IT GOT INSTEAD IS BETTER FOR THE NEXT ATTEMPT: two named vectors to watch fill, so a workload can be CONFIRMED BEFORE a turn is spent measuring |
| VM menu detection: use Exit-button redness | meta | verified | high | 100% | 2026-09-08 | The intro screens are bright white so brightness-based menu detection fires EARLY. Detect redness in the Exit button: crop 100x14+462+674, mean.r > 0.2 && mean.g < 0.15. With that, all ten of lane H's launches hit the menu first try at t+48-54 s |
| lane H self-report: touched a shared worktree | meta | verified | high | 100% | 2026-09-08 | Recorded because it was self-reported. Lane H ran git checkout -- include/generated/sots_addresses.h in the SHARED ~/sots-engine main worktree, discarding an uncommitted regeneration another lane had in flight. It is a generated file and was regenerated, so no content was lost - but it was not that lane's tree to touch. Reinforces: lanes work in their OWN worktree, and generated files are regenerated by the integrator at merge |
| AI TASK SELECTION LOOP FOUND - it is not a scorer | control-flow | verified | high | 95% | 2026-09-08 | Lane AI2. 0x006cf630, Process-Turn phase 20, which AI1 had listed only as an address. THERE IS NO SEARCH: PruneTasks -> switch (player->Species) (7-entry table @0x006cf880, FOUR distinct arms) building a FIXED, SOURCE-ORDERED list of per-family task creators -> PruneTasks -> sort TaskList DESCENDING by task->vt10 using MSVC std::list::sort, which is STABLE -> two passes of task->vt[5](agent, pass) -> PruneTasks. And vt[10] is, for 26 of 31 task classes, a CONSTANT LOOKUP on the task's type id through 0x00691f00 - 33 arms, each mov eax,imm32; ret. THAT TABLE IS THE AI'S ENTIRE STRATEGIC ORDERING POLICY and it is in the findings verbatim (DeployGateAt 1400 down to AdvanceIdleShips 0). Five overrides; the two artifact tasks IGNORE their table entries and return 1260/1261. The switch is on player+0x5c = Species, VERIFIED THREE WAYS: arm 1 is the only one building gate families (Hiver), arm 5 the only one building AITNodeBore (Zuul), and arm 4 - species 4, the engine's own Species::NPC - BUILDS NOTHING AT ALL. Both defensive families sit behind player+0x2d8 = the save field plcy |
| order-method -> TurnCommands mapping COMPLETE | objects | verified | high | 95% | 2026-09-08 | Done image-wide (94 lea ecx,[r32+0x160] sites in 76 functions). ALL 27 of lane Q's lists and ALL SIX prologue gates now have a NAMED PRODUCER. AI1's 21-row table was incomplete - there are 26 order methods, and the missed ones include list 5 (system rates, AI-called) and list 7 (colonize, AI-called). LANE Q'S UNEXPLAINED LIST 14 IS THE AI'S FLEET ORDER: 0x007634d0(fleetObj, mode, flag) -> {fleetId, mode, flag}, adder 0x00842a00 at +0x10c keyed on (fleetId, mode); the AI bridge 0x006987e0 calls it TWICE, (f,0,true) then (f,1,true), while the UI emits only mode 0. 24 of 31 tasks' Execute reach it. That is a ModCount prediction: TWO bumps per AI fleet order, not one - against lane A2's measured 10 |
| AI throttle SETTLED - Rung B is safe | control-flow | verified | high | 100% | 2026-09-08 | The open question from lane AI1 (if an AI turn can span frames, the "all AI orders in before the human's End Turn" ordering needed revisiting). StrategyApp::RunPendingAITurns 0x00838c60 is called EVERY FRAME from Update 0x007842b0, walks the pending-AI vector at StrategyApp+0x1c IN INDEX ORDER raising SEResumePlaying on each - NO early exit, NO yield - and THEN does Sleep(AIProcessMinTime - elapsed). AIProcessMinTime is a TRAILING MINIMUM-DURATION SLEEP FOR THE PROGRESS DIALOG, not a compute budget. An AI turn CANNOT span frames; AI1's ordering holds |
| g_CurrentClientIndex is a STACK POINTER | objects | verified | high | 100% | 2026-09-08 | Not an index. Two writers only, Push/PopCurrentClient 0x00578020/40, and StrategyAIAgent::OnEvent BRACKETS THE WHOLE AI TURN in them - that is how every cl_* call lands on the right client AND the right per-client RNG. Also: AI1's one inferred hop is now READ TO THE CALL at 0x006b395b (Broadcast -> OnAIPacket), push order matching the thunk exactly; lane-ai1.json's Broadcast and g_CurrentClientIndex upgraded mapped -> verified in place |
| AI2 corrections to AI1 | meta | verified | high | 100% | 2026-09-08 | 31 task classes, not 34 - slot-1 x slot-8 across 31 vtables gives a gapless type enum 0..0x20 with two retired ids. 26 order methods, not 21. 7 pure virtuals, not 8 - named: GetTypeId, GetTargetA/B, Execute(agent,pass), IsFinished(agent), GetTypeName, Describe. Also a test expectation was wrong on first run (ColonizeAt 1300 outranks NodeBore 1275) and lane AI2 FIXED THE TEST, NOT THE CODE |
| AI2 boundary | meta | verified | high | 100% | 2026-09-08 | Slots 11/12/13 UNNAMED. The nine tasks with no reachable order method are INFERRED to be goal/planner tasks - bodies unread. The two-pass meaning is INFERENCE. The reachability table is a DIRECT-CALL CLOSURE = lower bound (rule 16 bites hard in a vtable system). The StrategyApp+0x1c ENQUEUE SITE WAS NOT FOUND, so AI-stepping order is still open - and that matters because ModCount is a count. The .data invade tunables 0x00a1795c/60 have no traced loader. NOTHING RAN UNDER AN INSTRUMENT; four falsifiable predictions with symptoms are in the finding |
| INTEGRATOR NOTE: do not share a build dir with a background gate | meta | verified | high | 100% | 2026-09-08 | I read "98% tests passed, 1 failed" and started hunting a flaky test in a DETERMINISM project - a serious thing to chase. It was my own error: I ran an ad-hoc ctest against the same CT111 build directory while the BACKGROUND gate job was still writing binaries into it. The gate itself reported 47/47 and the shim built; 13 further clean runs (5 serial, 8 at -j4) confirm. RULE: one build directory per gate invocation, and do not query a dir while a background job owns it - the failure mode looks exactly like nondeterminism in the engine, which is the one thing this project must never misdiagnose |
| ComputeOutput turn path modelled | subsystems | verified | high | 90% | 2026-09-08 | Lane C3. +1 leaf closed, 0 regressed, on BOTH pairs (reference 209 -> 157, closed 52; pair 2 108 -> 86, closed 22); the leaf is /Sim/players/Player[576 "Independent Colony"]/Sav both times. THE FUNCTION IS THIN: 0x00751fb0 zeroes 12 slots, calls ComputeOutputFromRates(out, &sys->Rts) with the system's OWN, UNNORMALISED sliders, then fills three reporting slots - and out[3], the only slot ComputeBudget reads (confirmed at 0x008631fd), is written entirely inside ComputeOutputFromRates and never touched again. THE LOAD-BEARING FACT NOBODY HAD: construction points the build queue and the ship-repair pass do NOT spend are REDISTRIBUTED over trade/terraform/infra, and the trade share is added to the money channel - and unspent infra cascades into the TERRAFORM pool, which cascades into money. TWO HOPS, NOT ONE. So a colony with an empty build queue earns the same money whichever way its sliders point. Which float decides an output value: SRsc and the build-queue demand; everything else in the corpus is suppressed by the normaliser or rounds to nothing |
| the two-path cross-check decomposes TO THE UNIT | verify | verified | high | 100% | 2026-09-08 | Stronger than "the two paths agree". The engine now runs BOTH the projected path (BnkEl-constrained) and the turn path on the same colony state every load and reports the difference - and on all 11 saves every delta decomposes to the unit into the build queue's points priced through the money chain: 5 saves agree exactly (all queues empty); human-turn3 differs by 37 = one five-point block x 5 x IncMod 1.48; zuul-turn23 by 2035 = 370x5x1.1; zuul-turn5 by 39160 = 7120x5x1.1; zuul-turn17 by 21010; zuul-turn16 by 36438 (within the 1 unit two truncations of a .5 allow). The two paths differ by EXACTLY what the disassembly says |
| S11 CIVILIAN GROWTH is the highest-value next item | verify | backlog | — | 0% | 2026-09-08 | Lane C3's headline for the roadmap: the residual on the metric is UPSTREAM, not in the output formula. ComputeBudget runs AFTER S11 SystemTurn (strategic phases 11 then 13), and our S11 DOES NOT COMMIT CIVILIAN GROWTH. The human's savings come out 289,092 against the oracle's 289,688 - the growth gap (597) minus one trade point. ONE INPUT (imperial carrying capacity) GATES the human's Sav, PvSav, BnkEl AND BnkPr on both pairs. The AI's are wrong for a different reason: its ResRate/ResTNm are its own orders, i.e. Rung B |
| the last unmodelled output input: ship repair demand | verify | backlog | — | 0% | 2026-09-08 | Nine inputs to the turn path; EIGHT ARE ON THE WIRE (Rts, dsu, IdealSuit, TerraMod, Infra/ibon, BQ/ords/conleft, NumSnF, the output total). The ninth is the repair demand of damaged ships in orbit, needing Ship::RepairCost 0x00815180, which is UNREAD - it is taken as 0, so a colony with a damaged fleet reads HIGH. Also note this is the same repair pass that makes ComputeOutputFromRates unsafe to call for harvesting (the B1 replace double-run defect) |
| the two RNG words: the blocker MOVED again | verify | mapped | high | 90% | 2026-09-08 | They still cannot enter the generator model and THE REASON HAS CHANGED AGAIN. The allocation is now computable - the money input was the whole gap. What blocks them is one step further out: the only corpus player reaching ProcessResearch with a target is the AI, whose research rate and target are generated during the same turn (Rung B). On turn1-state NO player has a target, so both words cost zero on the reference pair - but rule 20 applies to that sentence: a count cannot separate "not entered" from "entered and drew nothing". AN ENTRY PROBE ON ProcessResearch SETTLES IT IN MINUTES. Ledger unchanged at 16 words modelled |
| two callees with NON-STANDARD conventions | objects | verified | high | 100% | 2026-09-08 | Recorded because they read WRONG as __thiscall and would silently mis-model: 0x0074c6f0 takes this in ESI, and 0x007460b0 takes the system in EBX. Same class of trap as lane P2's two helpers that read ebx/esi live-in without ever writing them |
| shpt[0] CANNOT close from the build queue - the blocker moves to game/ai | verify | verified | high | 100% | 2026-09-08 | Lane B6 closed 0 and regressed 0 and predicted exactly that before writing code. MEASURED, NOT ARGUED: turn1-state and turn2-state each carry three BQ frames (players 16, 32, 576) and ALL THREE HOLD ZERO ORDERS; every hbq in both files is false; the only TurnCommands_v5 block is player 16's byte-identical empty one, the other three CDT blobs being AIAgent caches. AT LOAD THERE IS NO BUILD ORDER ANYWHERE IN THE GAME, so a faithful pass has nothing to advance. The destroyer is the AI's, created inside the turn: NumDes 5->6, NMnx 106->109 with ids issued design(1712), ship(1728), fleet(1744) IN THAT ORDER, FNGNum 0->1, Maint 0->500, srb[0] 0->1. DECISIVE: PvSav 50,000 -> 38,100 - 11,900 left the treasury BETWEEN THE SAVE AND SPINE PHASE 0, the queue-time deduction of an order the input file does not contain. So lane E2's new blocker is Rung B, not S11 |
| build queue read; prototype corrected | control-flow | verified | high | 95% | 2026-09-08 | S11 -> ServerSystem::ProcessTurn -> ProcessBuildQueue -> BuildQueue::ProcessTurn 0x00890d50, between the plague pass and population growth. Ghidra says 1230 bytes; the real body is 1264 and Ghidra's end lands INSIDE THE EPILOGUE (rule 17 again). CORRECTED PROTOTYPE: arg0 is NOT the ServerSystem - it is [sys+0x10]-4, the StrategyServer S frame, so the build-completed event goes on the SIM (S+0x2b0) and the hull's turn stamp is S+0xc = Frame. Per completion: create hull -> tblt = Frame -> vft[10](sim, ship) fleet routing -> owner->ShipRecords.built[hullClass]++ -> find-or-append the per-design record -> post event -> points -= conleft; conleft = 0. inc [owner + class*4 + 0x1b4] has EXACTLY ONE WRITER in the whole image (image-wide byte scan), and the encounter spawner that also creates ships touches no ShipRecords - so srb cleanly discriminates PLAYER-BUILT hulls |
| CORRECTION to lane B5: FtFlg 0x400 is not retreat's | objects | verified | high | 100% | 2026-09-08 | **FtFlg |
| NEW BLIND SPOT: stack-built function-pointer tables | meta | verified | high | 100% | 2026-09-08 | A third way this binary hides call edges, after inlining and vtables. The ship-borne build-queue wrapper 0x00789500 has zero call sites AND no vtable slot - its address is written into a STACK-BUILT function-pointer table at 0x007b9c4b. tools/vtable_map.py cannot see that class, so "no vtable caller" is ALSO a lower bound, not just "no direct caller". (Related: the base BuildQueue is pure from slot 4-10 and SystemBuildQueue binds slot 10 to the function that is literally the "next function start" bounding ProcessTurn - which is why the fleet half looked absent) |
| AN ORACLE THE CAMPAIGN ALREADY OWNED | verify | verified | high | 100% | 2026-09-08 | Lane B6 noticed that zuul-turn16-noderoute -> zuul-turn17-rollpending is a real CONSECUTIVE-TURN PAIR - six completions and one partial advance across two systems - which nobody had used. Its test SOLVES for the point total (0..200,000) rather than assuming it, and asserts the solution is UNIQUE (4182 and 9782): a per-order budget, a non-FIFO drain, or skip-instead-of-stop each make the solution set come back EMPTY. The completions then reproduce the ShipRecs deltas both files carry. That refutes its falsification hypothesis 3 AND independently confirms lane B4's ProcessBuildQueue on all four rules the corpus can reach |
| build-queue hypotheses (unexercised) | verify | backlog | — | 0% | 2026-09-08 | Labelled by lane B6: the slot-9 MONEY CHARGE (no design in the corpus costs money), a ship-borne queue WITH an order, srl/srk/sri (all zero in 11 saves), the four non-default routing handlers, and a corrected colony rule (with points <= 0 the original skips the removal sweep too) that is unobservable in this corpus. THE WORKLOAD THAT CLOSES ALL OF IT: load zuul-turn16-noderoute.sav on VM140, End Turn, keep the AUTOSAVE - that converts the fitted point totals into a full before/after oracle for the pass, the ship birth and the fleet birth at once |
| treaty turn stamps (H02) - 26 leaves | engine | verified | high | 95% | 2026-09-08 | Lane T2. Reference pair 157 -> 131 (closed 52 -> 78), pair 2 86 -> 72 (closed 22 -> 36), regressed 0 - +26 and +14, EXACTLY the numbers predicted in writing before building. Three further pairs the model was NEVER FITTED TO - a different game, species 5, turns 2/15/16 - each closed +12, regressed 0, which is PRECISELY the number of ordered treaty pairs each save holds. 76 leaves closed, 0 regressed, 0 RNG words, across five pairs, and the deltas are identical against both 0f1c007 and 2fd0852 so it is orthogonal to C3's and B6's work. StampTreatyTurns 0x007898c0, instruction-verified: the diplomacy ledger's "this treaty was last in force on turn N" stamp over every ordered player pair, creating the entry on demand; sole caller is ApplyTurnCommands so it is a HOST STEP, before both drivers. Reproduces the ledger of 10 of 11 saves ENTRY-FOR-ENTRY INCLUDING ORDER (the 11th is turn1-state, whose ledger no turn has written) |
| THREE corrections to strategic-turn-internals 5.2 | subsystems | verified | high | 100% | 2026-09-08 | All flagged in place by lane T2: (1) relation codes are 3 = ally, 2 = NAP, 1 = cease-fire, NOT the reverse - and 5.2 had self-labelled MEDIUM on exactly this point while combat-done-tail.md 2A implied the right answer independently; (2) the stamped value is Frame, not ModCount (mov eax,[edi+0xc]), and the stamp equals the save's own Frame on 10 of 11 saves while ModCount runs 0/12/24; (3) the bit is the PlyrIdx field, not the vector position - THE OPPOSITE CONVENTION from the shared-vision mask two files away, and no save separates them |
| rule 17, seventh instance - a clipped sweep would call it DEAD | meta | verified | high | 100% | 2026-09-08 | Lane T2's sole-caller claim EXISTS ONLY because it disassembled past Ghidra's function end. ApplyTurnCommands is sized 2965 B ending 0x007b2445; its real body runs to ret 0x10 at 0x007b248d and the call sits at 0x007b2461. A sweep clipped at fva+size finds NO CALLER AND CALLS THE FUNCTION DEAD. Seven functions, seven lanes |
| ROADMAP ITEM 6 WAS WRONG: there is no such writer | verify | verified | high | 100% | 2026-09-08 | backlog item 6 had Player.Status blocked on "the writer between tail 31 and the autosave - watchpoint". THERE IS NO SUCH WRITER. A whole-image scan for immediate stores to ServerPlayer+0x164 finds THREE: ProcessTurn's encounter loop writes 1; ResumePlaying writes 0 on load (the 4->0 the determinism note recorded); and StrategyServer_MarkPlayerTurnEnded 0x00821a40 writes 4 from the three End-Turn SUBMISSION paths, BEFORE the turn runs. Item 6 needs one predicate, not a VM. Lane T2 did NOT write it: only two corpus saves carry a non-zero Status at all, and Species != 4 fits 8 observations on a 1-BIT PREDICATE THAT NINE SAVES DO NOT EXERCISE - rule 20. An entry probe on that function names the set directly |
| the tail's own list is correct and UNACTIONABLE | meta | verified | high | 100% | 2026-09-08 | Lane T2 ranked the tail by leaves-closed-per-effort against the 158-leaf residual and found every tail phase that moves a leaf in this corpus is blocked on someone else: T24 (Maint/ShipRecs, 9+5 leaves) waits on ship construction; T31 and T36 are already Blocked on the budget; T05 writes deadhome/bty* and NO BATTLE OCCURS ON EITHER PAIR (every encounter result has res->+0x4 != 0); T02/T30 move no leaf at all; the SvSctOb cluster is the tail's tier-4 vtable blind spot; TShn/rcex are lane E3's named residual with a watchpoint already specified. So it took a HOST STEP instead and said so plainly. The ranked table is in findings/subsystems/treaty-turn-stamp.md 5 |
| MULTIPLAYER TIER 0 WORKS - verified end to end | subsystems | verified | high | 95% | 2026-09-08 | Lane W2. A two-player game was created, joined, launched and played for TWO TURNS IN LOCKSTEP on one Windows guest, with zero GameSpy traffic. Capture was tcpdump on spicy's tap140i0 - OUTSIDE the guest, so it cannot perturb it - 44,319 packets, EXACTLY 0 to UDP 27900/27901 or TCP 28910/28900/6667/6500. Instrument removed (hooks=off) per rule 19. Host: Host Multi-Player -> Custom + LAN -> Create Game; joiner: /concurrent /join 127.0.0.1:3369. Joiner appeared in slot 2, Ready -> Launch -> Turn 1, both ended turn -> Turn 2 -> Turn 3, distinct treasuries. SOCKETS: host binds UDP 3369 + ephemeral, client binds ONLY ephemeral, no TCP anywhere, and 3370 is NOT bound at lobby time. All five of lane G2's predictions confirmed, restated in docs/W2-predictions.md BEFORE the run. Also: LAN discovery listed the game on first open with no Refresh, browser-mediated join works and makes NO NatNeg attempt, and G2's own caveat (a) is FALSIFIED - Join Manually is enabled with every gamespy.com name dead |
| the developer shipped the answer in the client | subsystems | verified | high | 100% | 2026-09-08 | Two things nobody had. The MOTD is fetched live on every launch from www.kerberos-productions.com (STILL ANSWERING, Last-Modified 2017-06-16, User-Agent GameSpyHTTP/1.0) - and its text is Kerberos telling players to host in LAN mode and use direct connect, i.e. the developer's own statement of lane G2's +0x2c == 1 predicate, shipped inside the binary. Also: the Windows firewall prompt fires at Create Game, on the host, not at launch. AND a correction to the Tier-1 hosts list: the browser resolves THREE names, not one (available, ms5, peerchat); master/natneg are host-path only; exactly one DNS query each (the SDK's 2 s retry is a SOCKET retry and never runs). Tier 1 is confirmed unnecessary |
| multiplayer: what is NOT covered | verify | backlog | — | 0% | 2026-09-08 | Stated loudly by lane W2: loopback only (no two-box or VPN test), NO COMBAT - 3370 and the combat lockstep are untested and are the likeliest remaining breakage - two turns only, two Human players, and sots_server.exe not run |
| watchpoint module + ModCount CONFIRMED to the address | verify | verified | high | 100% | 2026-09-08 | New src/shim/hooks/watchpoints.{h,cpp}: DR0-DR3 4-byte write watchpoints armed from a SINGLE MinHook detour on ApplyAllTurnCommands, a VEH recording EIP/value/DR6/EBP-chain/stack-scan, a CANARY SELF-TEST (arm a word we own, write it once, require exactly one trap), and `watch=on |
| CORRECTION to lane T2: the Player.Status writer EXISTS | verify | verified | high | 100% | 2026-09-08 | Lane T2 concluded from a whole-image scan that no writer sits between tail 31 and the autosave. It does. Full measured order per turn: MarkPlayerTurnEnded+0x35 writes 4 to each player -> Frame++ -> 10 ModCount -> ProcessTurn+0x5ca writes 1 (local player only) -> StrategyNetworkClient::OnMessage+0xa15 writes 4 -> OnAllCombatDone_Tail -> AUTOSAVE -> ResumePlaying+0xb1 writes 0. A static scan for immediate stores missed it; the watchpoint found it. Roadmap item 6's original premise was right and my correction of it was wrong - restore it. (Lane AI3 separately notes 0x007ddd3f is also a Status writer, at the resume boundary AFTER the autosave) |
| ModCount vs Frame settled | objects | verified | high | 100% | 2026-09-08 | S+0x8 took 12 writes; S+0xc took exactly ONE, from BeginProcessTurn+0x2a, value = the turn number. So S+0x8 IS ModCount and S+0xc is Frame, confirming lane A2 by measurement. addresses.json has the name on the wrong word and lane T's PhaseCounter IS ModCount - fix at the next fragment fold |
| rcex CLOSED - with no VM time at all | objects | verified | high | 100% | 2026-09-08 | It is sixteen 4-bit per-player counters. Nibble p is set to 1 on the turn the system enters player p's AFlags, and the rcex tick in ServerSystem::ProcessTurn decrements it to 0 next turn. 7/7 across the corpus, including human-turn2-orders.sav - a DIFFERENT game, different system, different player index (AFlags bit 3 <-> nibble 3). Falsifiers listed. Bats2 is the same shape and remains a rule-6 hypothesis (0 everywhere) |
| MY BRIEF GAVE THE WRONG ADDRESS for TShn | meta | verified | high | 100% | 2026-09-08 | I briefed the TShn watchpoint at player+0x274. That is observed_techs; the TShn map is ServerSystem+0x274. Lane W2 caught it - arming the briefed address would have watched the wrong object and reported a confident nothing. The real obstacle is separate: a std::map's nodes are heap-allocated so there is no fixed leaf to arm; the fix is to arm the MAP HEADER to trap the insertion, which is the better question anyway, ~20 lines. ALSO: both blocked containers are one add from the existing arming point - *(S+4+0x154)+0x40 (trade routes) and *(S+4+0x158)+0x14 (spy programs) - and for the spy vector the trapped return addresses would name the UI nobody has found |
| P11 event posting - 4 leaves, and the bucket now VERIFIES | engine | verified | high | 95% | 2026-09-08 | Lane EV. Pair 1 131 -> 127 (closed 82), pair 2 72 -> 69 (closed 39), regressed 0 - exactly the paths its prediction named (committed BEFORE the build), nothing else moved. STRONGER THAN THE COUNT: the bucket exists in both saves after the run, so the checksum DESCENDS INTO IT and reports nothing - the posted record matches the oracle on ALL EIGHT FIELDS, text included. FLT_MAX vs +inf and the EvAct rule are therefore ACTIVELY DISCONFIRMED as failure modes, not merely untriggered. WHICH EVENTS A TURN POSTS, verified across all 11 saves: turn1->2 posts two (player 0 EVENT_NO_RESEARCH id 1; player 1 EVENT_SHIPS_BUILT id 1); turn2->3 posts three (NO_RESEARCH id 2; SHIPS_BUILT id 2 then RESEARCH_OVERBUDGET id 3). Order is readable off the ids: the build pass posts BEFORE the research pass. Ids are per player, so no cross-player order is observable and none is assumed. Only TWO players in the entire corpus ever hold an event |
| TWO corrections to events.md | subsystems | verified | high | 100% | 2026-09-08 | (1) events.md said the turn handed to PostEvent is ModCount. IT IS THE FRAME - turn2-state has Frame 2, ModCount 12, and buckets EvTurn=2. (2) The EVENT_NO_RESEARCH gate was read BACKWARDS: 0x00584e50 is TechTree::CollectResearchedTechs (lane T's name; argument order re-read), NOT a ListAvailableTechs, and the middle test is "nothing was researched on this turn or later", not "no affordable tech". zuul-turn23-fleet23 exercises it live: RESEARCH_COMPLETE on turn 22 with NO no-research event that turn, NO_RESEARCH again on turn 23. The engine's P11 had only two of the three tests and OVER-FIRED |
| a CONTROL measurement worth copying | verify | verified | high | 100% | 2026-09-08 | Lane EV did not just report that its phase needs the AI roster - it MEASURED the cost of withholding it: with the roster withheld the phase over-fires and REGRESSES 16 leaves on pair 1 - and ONLY on pair 1, because pair 2's input already carries the AI targets. That is why the phase stays Blocked and phase-granular. The AI research-selection gate itself is labelled a hypothesis with the workload named: across ~90 AI player-turns no AI ever posts NO_RESEARCH while the human posts it on every target-less turn |
| tail phases post NO events (measured negative) | control-flow | verified | high | 100% | 2026-09-08 | Confirming lane K from the other side: T18/T21/T32 post nothing on either reference pair - and this is a MEASURED negative, not an absence of evidence, since other corpus saves DO exercise them. No tail phase manufactures a turn's events |
| standalone_report gains --engine-arg | meta | verified | high | 100% | 2026-09-08 | Lane EV added a repeatable --engine-arg pass-through so a lane can feed the standalone the OPERATOR INPUTS A SAVE DOES NOT CARRY (e.g. --ai-player N, --data DIR) instead of hard-coding them; every run now records what it was given in the header AND in status.json. That is the right shape - the provenance travels with the number |
| headline number: default 131, with operator inputs 127 | meta | verified | high | 100% | 2026-09-08 | Worth keeping distinct rather than quoting the better one. standalone_report.py with NO flags reports 209 -> 131, closed 78 - that is the number for "a save and nothing else". Lane EV's P11 closes 4 more (127) but ONLY when handed --data DIR and --ai-player N, operator inputs the save does not carry, because a save does not record which players are AI-controlled (lane AI3: ServerPlayer+0xf9/+0xfa sit in a HOLE in the serialised layout). Both numbers are real; the dashboard tracks the default because it is the one that needs no human. Lane EV's --engine-arg pass-through records in status.json exactly what a run was given |
| civilian growth: MY BRIEF'S PREMISE WAS WRONG | subsystems | verified | high | 95% | 2026-09-08 | Lane G3. +3 closed on each pair, 0 regressed, by default with no flags (both colonies' Pop2/PopG/PopC and Player[16]/Sav); pair 1 131 -> 128, pair 2 72 -> 69. With --commit-blocked=T31 --ai-player 1 it is 83/0 and 41/0 - the extra two per pair being the human's and the AI's BnkEl, which T31 could not close before. GROWTH IS NOT GATED ON IMPERIAL CARRYING CAPACITY as I briefed: the whole system's civilian delta is CLAMPED TO 20,000,000 (POPTYPE[1]+0x08, an int64 literal in the executable), the uncapped delta is 150,000,005 (7.5x) and the capacity headroom 500,000,000 (25x), so the clamp decides the value and the pass commits with NO TUNING TABLE AT ALL. PRECISION BY CONSEQUENCE: the deciding float is neither the growth curve nor the capacity but the rescale trunc(applied x (clamped/total)) - the product's error exceeds half an ulp of 20,000,000, so landing exactly on the cap is A MEASUREMENT, NOT A THEOREM. The growth fraction, which everyone looks at first, is the one term that CANNOT matter here: any value in [0.04, 1.0] gives the same answer. Also pinned the imperial capacity at exactly Size x 1e8 FROM THE CORPUS ALONE via two independent behaviours of one colony over three turns |
| A LIVE-VERIFIED MODULE WAS WRONG (rule 23) | verify | verified | high | 100% | 2026-09-08 | THE MOST IMPORTANT FINDING OF THE ROUND. Lane G3 predicted Sav closes on both pairs; it closed on pair 2 and came out ONE MONEY HIGH on pair 1. Growth had closed 596 of the 596-money gap - the residual was ComputeBudget's interest literals, which are WIDENED FLOATS in the image ((double)0.01f at 0x009e31c0, (double)0.15f at 0x009ed188) and then truncated, so a treasury of exactly 50,000 earns 499, not 500. The engine used exact decimals; sixteen hand-computed test expectations moved by one. That module had been compared on 4,437 LIVE CALLS WITH 0 DIVERGENCES and missed it, because it presented only 20 DISTINCT STATES, NONE ON A BOUNDARY - exactly the thin-coverage caveat lane R recorded against it at the time, now vindicated by a different lane doing arithmetic the compare never exercised. Now rule 23 |
| Ship::RepairCost read; a stronger negative | objects | verified | high | 100% | 2026-09-08 | max(0, design+0xcc - (ConCap + (flag ? design+0xd0 : 0))), plain integers; Ship::ApplyRepair 0x008151c0 names the fields - ConCap is INVESTED CONSTRUCTION, not a per-turn capacity, and +0xcc is its ceiling. Closes 0 leaves as predicted and the demand stays 0 (the design fields are cached stats off the wire) - but the zero is now EVIDENCED, not silent: the independent colony parks TEN SHIPS over Koa'Vo on both pairs and its Sav closes exactly at zero demand. Rule 20's stronger negative |
| CORRECTION: out[6] is not a repair function | subsystems | verified | high | 100% | 2026-09-08 | output-turn-path.md had 0x007460b0 as a repair function. It is SHIP-CARRIED POPULATION INCOME - its summand 0x0081f8c0 gates on the design's carried-population bit and computes GroupIncome over the ship's own Population, a slot the engine already had. Corrected in place; lane C3's three unread stubs superseded. ALSO: GrowCivilianPops' real end is 0x00754b59 (its group-type loop's back edge is OUTSIDE every decompiler if - rule 17, SEVENTH lane); MaxPopGeneric ends 0x0074a6cd so lanes N and E1's 0x0074a6d0 is A DIFFERENT FUNCTION; and InitPopTypeTable NEVER WRITES the group ceiling - the CRT static initialiser at 0x009abe20 does, to INT64_MAX, and read as zero it would cap every capacity in the game at nothing |
| growth hypotheses (nothing exercises them) | verify | backlog | — | 0% | 2026-09-08 | Named by lane G3: one species per colony everywhere so the proportional rescale is only exercised DEGENERATELY; nothing shrinks (the -50,000,000 floor and negative rescale unexercised); pbon2 empty everywhere so the headroom/write-back asymmetry is invisible; haltv[1] false everywhere; GFlags 0 everywhere so the 1.5x growth BONUS never fires; no species ever seeded; the morale-event half not modelled; and imperial growth DELIBERATELY not committed - a no-op here, and committing it needs a capacity the corpus bounds from below but not above |
| ModCount DECOMPOSED EXACTLY - zero residual | control-flow | verified | high | 95% | 2026-09-08 | Lane AI4. The ten command bumps lane W2 measured now decompose with ZERO RESIDUAL, and all four of W2's unnamed handler EIPs are named plus the two it recorded as inlined: 0x00821a87 x4 (research-rate gate, once per submitted block), 0x0086c3e9 (list 5 system rates), 0x0084946e (list 3 build order - THE DESTROYER LANE B6 COULD NOT FIND), 0x0088bf01 (list 10), 0x0089046f x2 (list 14, INLINED, the AI's fleet order), 0x008657ad (list 8 fleet route). The join came from reading ApplyTurnCommandBatch as an INSTRUCTION SEQUENCE: a flat run of 27 per-list loops in a fixed order (neither list nor offset order), the six prologue gates split across THREE separate per-player loops at three different points, and four bumps INLINED INTO THE BATCH rather than in a handler - which is why call-graph sweeps under-count them. THE COST RULE HAS A SHARP BOUNDARY: lists 1-16 cost one bump per element, lists 17-27 cost NOTHING, no exception either way; four gates bump, one is free, and CivilianRatios has NO APPLIER anywhere in the apply path - reported as UNKNOWN, not zero |
| AI2's two-elements-per-fleet-order CONFIRMED from the counter side | verify | verified | high | 100% | 2026-09-08 | At zero VM cost: list 14's applier fired exactly TWICE on a turn that moved exactly ONE fleet (one list-8 bump), and the UI control (human-turn2-orders.sav) carries ONE list-14 element for its one move. Lane AI2 predicted this from the AI bridge calling the order method twice, (f,0,true) then (f,1,true), where the UI emits only mode 0 |
| THE REFERENCE GAME IS NOT WHAT THE RECORD SAID | meta | verified | high | 100% | 2026-09-08 | Three corrections from the saves plus W2's log. (1) ref-turn2.sav IS turn2-state.sav - same 66,739 bytes, same sha256 - so W2's watchpoint measurement is of THE REFERENCE GAME and is directly comparable to the standalone's reference pair. (2) THREE AI players, not one: 32 (Fane Lao) and the two "dormant" Singularity players 496/512, which DO run - all three set ResRate 0.25->0.8 and picked a research target on turn 1. (3) The four Species==4 monster factions submit NO COMMAND BLOCK AT ALL - exactly four players go Status 0->4 and exactly four rate-gate bumps fire, the first direct evidence they have NO CLIENT rather than an empty task list. That third point also gives the Player.Status predicate a candidate shape (has-a-client vs not) with four positive instances |
| AI order emission scaffold + the submit latch | engine | verified | high | 90% | 2026-09-08 | src/game/ai/orders.{h,cpp} + agent.{h,cpp}, 233 -> 423 checks, ctest 51/51 -> 53/53: the command block, the order API with (fleetId, mode) dedup, the AI fleet-order bridge, BlockModCountCost/TurnModCountDelta, the 34-phase turn spine, and the two-pass walk with pass-0 silence enforced AT THE CLIENT, not by trusting the caller. THE SUBMIT LATCH: EndTurn sets client+0x15c BEFORE BuildTurnCommands, so phases 29-33 are DEAD - a literal port would emit AN EXTRA COLONIZE ORDER PER AI PER TURN. Two more cross-checks: phase 2 is Hiver-only (cmp [player+0x5c],1 - a FIFTH independent confirmation of the species reading, and the reason gate +0x3c has never been seen set), and cl_SetResearchTarget is AI-ONLY surface with exactly one caller in the image. IT DECIDES NOTHING - which tasks exist and what each wants are board questions this module does not model (standing convention) |
| AI4's falsifiable turn1 prediction (probe routed) | verify | in-progress | — | 0% | 2026-09-08 | Committed BEFORE the build: turn1-state should also give 12, out of a DIFFERENT command set - 4 rate gates + 3 research targets + list 1 (new design) + list 3 (build) + list 5 (rates) + 2 drivers, with NO fleet order on turn 1. The predicted trap multiset CONTAINS TWO EIPs W2 HAS NEVER SEEN AND OMITS THREE IT DID, so the SET discriminates and a wrong model fails visibly rather than coincidentally landing on 12. One save swap on W2's unchanged watchpoint module; routed to lane W3. Specific falsifier: does 0x0088291a (list 1) fire? That is the difference between "the engine must emit a design COMMAND" and "a design OBJECT the build order merely names" |
| LAB EXPANDED: five Windows guests, not one | meta | verified | high | 100% | 2026-09-08 | The single-VM constraint was shaping the work. With one guest, VM lanes serialised and I defaulted to static analysis because static parallelises - which is exactly the habit rules 18 and 19 exist to counter, and the user had to say so twice. FIXED STRUCTURALLY: zfs snapshot rpool/data/vm-140-disk-0@clone-base then zfs clone per guest (instant, copy-on-write, ~0 extra space until divergence) + qm create with a fresh MAC. VM140 (16c/12G, the reference, holds the oracle autosaves) + VM141, 144, 145, 146 (8-12c/10-12G each). Host has 40 cores and had 101 GB free. NOTE 142/143 were already CT ids - check qm list AND pct list before choosing. THE ORACLE GUEST IS STILL EXCLUSIVE: VM140 keeps the 8-file SavedGames set with the reference autosave bytes and the exclusivity rule; the clones are for probes, workload manufacture and multiplayer peers. A clone can be destroyed and re-cloned from the snapshot in seconds, which makes destructive experiments cheap for the first time |
| /Sim/players DECOMPOSED - unmodelled, not mismodelled | verify | verified | high | 100% | 2026-09-08 | Lane PL, and this reframes the biggest remaining block. Re-measured (status.json said 55; the real figure is 54 on pair 1, 24 on pair 2): 53 of the 54 leaves on pair 1 and 23 of the 24 on pair 2 are values our turn WRITES BACK UNCHANGED from the input save. Exactly ONE - player 32's Sav - is a number we compute and get wrong. A lane hunting a formula error in /Sim/players will not find one. Mechanism split: M1 AI research orders 12/2 (Rung B), M2 AI ship construction 15/9 (Rung B), M3 observation records refreshed by the AI's own new design 9/1 (Rung B, downstream of M2), M4 events 8/6 (half Rung A, written, wants --data), M5 BnkEl+BnkPr 4/4 (BnkEl CLOSED here), M6 Status 4/0, M7 PvSav 0/2 (CLOSED here), M8 an unexplained one-off 2/0. Rung B is 44 of 54 on pair 1 - and FOUR FIFTHS OF THE BLOCK HANGS OFF TWO DECISIONS BY ONE AI PLAYER: pick a research target, queue one destroyer |
| M3 is not an intel pass | objects | verified | high | 90% | 2026-09-08 | Lane PL: player 32's new odes entry is odid=18, opid=32 - the design IT ITSELF created; owep is that design's weapon and the three otch entries whose otnL moves are that design's techs. AND THE LISTS REFRESH ON DIFFERENT EVENTS: on pair 2 odes/.[1]/otnL moves 2->3 while every otch stays at 2, so odes re-stamps on BUILD and otch on CREATION. Both flagged as hypotheses - no corpus save has a player observing someone ELSE'S design |
| T31's "self-check" was not one | verify | verified | high | 100% | 2026-09-08 | Lane PL correcting lane E1's catalogue claim: T31 compared its POST-turn result against the PRE-turn stored value, so its "6 of 8" covered only the six players whose limit never moves - and printed the same number regardless of --ai-player. The catalog claimed "8 of 8 with the flag supplied", which the binary never printed. Now taken at load and passing for EVERY LIVE PLAYER of all 11 saves. Also: T31 now recovers its difficulty column from the save with NO OPERATOR INPUT - BnkEl has a 6.67x slope and the AI column is worth 10%, so recomputing under both columns against the stored value identifies it (1 AI + 1 non-AI on all eleven saves). Blocked -> Partial |
| rule 23 again: two float widths in ONE routine | verify | verified | high | 100% | 2026-09-08 | Lane PL: BANKRUPTCY_PROTECTION_LIMIT_FACTOR is read fmul dword ptr (D8 /1 = m32fp) while the divisor two instructions earlier is fld qword - different widths in the same routine. Engine now narrows it. 0 leaves on this corpus (all seven BnkPr records land where the two agree) but 3 test expectations moved, and disagreement is at EVERY max income divisible by ten, from 10 upward - lane PL predicted ~1.1% and measured 10%, recorded as a prediction miss |
| the published 128/69 OVERSTATES what is unmodelled | meta | verified | high | 100% | 2026-09-08 | 4+3 of those leaves belong to P11, which is WRITTEN and only wants a game-data root - standalone_report.py passes no --data. Reproduced end to end on CT111 against /srv/re-lab/gob-extract: 87 closed / 0 regressed (pair 1, residual 122) and 46 / 0 (pair 2, residual 62). Ranked remainder by leaves-per-effort: (1) give the metric harness a data root - 7 leaves, ONE LINE; (2) wire StrategyVars.txt into a TuningTable and set opt.haveTuning - 4 leaves, and TurnOptions::haveTuning is READ IN THREE PLACES AND NEVER ASSIGNED, the field is dead; (3) game/ai research selection 14; (4) game/ai build order 24 + M3's 10; (5) T34 10; (6) Status predicate 4, one VM session; (7) M8 2, unexplained |
| Status: eighteen fields split the roster identically | verify | backlog | — | 0% | 2026-09-08 | A negative result lane PL deliberately did NOT fit: eighteen scalar fields of the player record split the roster exactly the same way (ReqCL, AMine, CnTrd, cdp, hgs, TerraMod, pddm, ...) because the two sets are perfectly correlated with species. One observation, one transition, one roster. ReqCL is the most plausible on name and meaning but IS A HYPOTHESIS. The probe is an entry hook on lane W2's writer at OnMessage+0xa15 capturing the player index per call - one VM session |
| TShn CLOSED - writer trapped live, gate named, 158/158 | verify | verified | high | 95% | 2026-09-08 | Lane W3, and MY ADDRESS WAS RIGHT AFTER ALL: ServerSystem+0x274 is NVO._Myhead; the three-way disagreement was ServerSystem::Write running on the IStreamable subobject at +0x8. The arming code did not depend on the reconciliation - it probed BOTH bases and logged +0x274 validating on 9 systems, +0x26c on 0. CHAIN CONFIRMED FRAME BY FRAME from the trapped ebp chain, not inferred: RefreshVisibleOwnerIfKnown 0x0075bd70 -> RecordVisibleOwner -> SetVisibleOwner -> NVO::operator[] 0x0075a890 (exactly one caller in the image, so the writer list is COMPLETE) -> store 0x0075b961. THE GATE IS NOT A SPECIES RULE: `AFlags bit |
| Player.Status predicate SETTLED - and "Species != 4" was wrong | verify | verified | high | 100% | 2026-09-08 | Status == 4 <=> ReqCL(+0xfd) != 0 && Elim(+0xf8) == 0. NO species, NPC or RebAI field is read on ANY path that touches Status - the game states it itself in StrategyNetworkServer::AllPlayersTurnEnded 0x007cfd00, and the _NPC script binding clears ReqCL explicitly. Settled STATICALLY, so the entry probe was not needed and the VM slot went to lane AI4's insert instead. Lane PL had found eighteen fields that split the roster identically and correctly refused to fit one; ReqCL was its most plausible guess and is now confirmed by reading the code that reads it. Also corrects treaty-turn-stamp.md by three more Status=4 sites |
| AI4's prediction PASSED, and the falsifier answered | verify | verified | high | 100% | 2026-09-08 | Same binary, one config key, one save swap. 12 traps, contiguous 1->12, and THE SET MATCHED: two EIPs never seen before appeared and three previously-seen ones were absent - exactly the discriminating shape AI4 committed before building. New: 0x0088fe0d x3 = OnCommand_SetResearchProject, a SECOND inlined bump site in ApplyTurnCommandBatch distinct from W2's; 0x0088291d x1 = OnCommand_CreateDesign. AI4'S FALSIFIER: 0x0088291a FIRES - list 1 emits a DESIGN COMMAND, not just an object the build order names, which changes what Rung B must implement. CROSS-CHECK NEITHER MODEL WAS FITTED TO: 4 rate gates but 3 research projects - the three AI empires picked targets, the human did not, which is why the map banner reads "No Research Project Assigned" - and that 4-vs-3 population is the SAME SET the ReqCL predicate produces, reached independently |
| all ten ModCount handlers NAMED | control-flow | verified | high | 100% | 2026-09-08 | 0x00821a84 (x4) = OnCommand_SetResearchRate; plus SetSystemRates, TransferShips, AddBuildOrder. Lane W2's four StrategySim_ModCountBump_unresolved_* entries should be RETIRED in favour of these - lane W3 left W2's fragment alone rather than editing another lane's file, correctly. W2's caveat stands: 12 is this save's command count, not a constant |
| trade/spy workload: ABSENCE MEASURED, not assumed | verify | verified | high | 100% | 2026-09-08 | Both containers read live: trade routes count=0, spy programs count=0, both managers non-null, all three vector pointers ZERO - default-constructed, not emptied - and zero traps on either _Mylast. Three lanes had failed to build this workload; now the absence is a measurement. The spy-UI question stays open because the vector never grew, but the instrument that would name the UI now exists and is proven to arm |
| CORRECTION: layouts.md ServerSystem NVO rows are wrong | objects | backlog | — | 0% | 2026-09-08 | Lane W3: Game::ServerSystem rows PID@0x274 and NVO@0x278 are wrong - +0x274 is _Myhead, +0x278 is _Mysize, and the "PID" tag belongs to the NODE KEY. Same shape for the NVE/NVs rows. Documented but not fixed because it is a GENERATED file - the fix belongs in the recovery, not the output |
| gen_addresses now detects SAME-ADDRESS duplicates | meta | verified | high | 100% | 2026-09-08 | Lane SV caught by hand what the tooling could not see: it reached 0x004271c0 independently and would have filed RNG_NextIntInclusive, but addresses.json already carries it as RNG_NextInt - and the merger only checked duplicate NAMES, so it would have merged silently and forked the vocabulary for the campaign's most-used RNG primitive. Fixed: gen_addresses.py now errors on same-address-different-name too (offsets excluded - they are only meaningful per owning struct). IT IMMEDIATELY FOUND 13 MORE across 8 lane fragments, including two AI lanes naming one function differently, a boundary marker colliding with the function it bounds, and several lane-suffixed aliases of existing entries. All resolved with the agreement recorded in each fragment's _note. AND THE SWEEP BROKE THE SHIM BUILD - the engine referenced a dropped alias - which the CT111 cross-build caught and the host build could not. Rule 13 earning its keep again |
| standalone: 209 -> 124, closed 85, regressed 0 | engine | verified | high | 100% | 2026-09-08 | Corrected figure after finding a stale-build error of mine (rule 24). Lanes PL and SV had each independently reported 126 from a 128 baseline while main also read 128 - arithmetically impossible, which is what exposed it. Both lanes' work was in the tree all along; the binary I measured predated both merges because the gate rsync excludes build* and rsync preserves mtimes, so cmake relinked nothing. NO ERROR, NO WARNING, AND A PLAUSIBLE NUMBER - the same shape as the build-dir race earlier today that looked exactly like engine nondeterminism. Fresh build dir per measurement from now on |
| THE REFERENCE PAIR IS NOT DETERMINISTIC | verify | verified | high | 100% | 2026-09-08 | Lane L5, unlooked for, and it qualifies the metric every standalone number is measured against. Three runs of turn1-state -> turn2 on VM146 gave three different post-turn autosaves, differing in EXACTLY FOUR LEAVES - one Singularity shadow empire's research pick plus the derived checksum. RULE 19 IS DISCHARGED: two of the three runs carried IDENTICAL hooks and still differed, and the hooks=off run was a THIRD value - so this is the game, not the instrument. The determinism oracle stands for ref-turn2 -> turn3 (the byte-identical hashes, reproduced many times today including in two fresh processes for AI1's P2) but DOES NOT GENERALISE TO ITS NEIGHBOUR. Most likely locus: lane AI4 established there are THREE AI players including the two dormant Singularity empires, all of which pick a target on turn 1, and lane AI1's reading has the AI's per-client RNG seeded from an unseeded global - never confirmed live because this build does not emit the log line. Routed to lane L1, which is on this exact pair |
| interest literals VERIFIED LIVE - with a control that FAILED | verify | verified | high | 100% | 2026-09-08 | Lane L5 did what rule 23 asks. Prediction: the game pays 499, not 500, on a treasury of exactly 50,000. HELD - the shipped widened-float build ran 3,895 calls, 0 diverged, 0 undeclared writes. AND THE CONTROL BUILD FAILED AS REQUIRED: exact decimals gave 1,359 divergences of 2,718 calls, every one on a treasury that is a multiple of 100, while the three non-boundary treasuries diverged ZERO times. Coverage: 5 distinct treasury states, 2 ON THE BOUNDARY - versus the green run this replaces, 4,437 calls over 20 states, NONE on a boundary. Fewer calls, far more evidence. BLAST RADIUS IS BIGGER THAN G3 STATED: the one-money error propagates into available and researchMoney on 1,317 of the 1,359 calls, so it moves the player's RESEARCH SPEND, not just the treasury. The control also resolved a contradiction L5 flagged BEFORE running: slot 5 IS diffed and the harness CAN see it, so the old green run was thin workload, not a broken instrument |
| "widen every literal" is ITSELF a defect | verify | verified | high | 100% | 2026-09-08 | Two MORE rule-23 defects found by operand sweep in ComputeBudget, both corrected and both marked unverified because no reference turn can see them: the research-yield factor is a widened 0.85f (needs research money a multiple of 40,000 - the run presented 9 distinct values, NONE is) and the three research modifiers are summed in SINGLE precision (needs shrm/TRM non-zero; the corpus has both at 0). Both pinned by tests that fail with decimals. CRITICALLY: three constants BESIDE them are genuinely exact doubles - so blanket-widening is its own bug. Read the four bytes, every time |
| BANKRUPTCY factor measured, not assumed | objects | verified | high | 100% | 2026-09-08 | = 3.29999995 = (float)3.3. Its file image is ZERO - the loader fills it at run time - so lane PL had read the operand width and been forced to ASSUME the value. Now measured on the running game: the assumption was right and the narrowing is real. No static reading could have produced this |
L5 kept verified at 0, and defended it |
meta | verified | high | 100% | 2026-09-08 | The column has read 0 all campaign and L5 held it there ON PURPOSE. Every phase its compare touches is Partial for reasons UPSTREAM of the compare: P01's repair demand is taken as 0, P02 needs the AI's research orders, T31's BnkPr needs an unwired tuning constant. None is fixable by a VM. Promoting one because part of it was checked is exactly the drift app_test_catalog asserts against - the ladder's two conditions, COMPLETE and COMPARED, are blocked by different things and the column measures the conjunction. Catalog notes now carry the live evidence and its coverage instead of the older call counts. Also two predictions of its own falsified and recorded: the difficulty record is a HEAP POINTER at ServerPlayer+0x36c not an inline record (correcting the hook's standing "not reachable" note; the fitted pair stays a hypothesis, deliberately not dereferenced), and it predicted 2 divergences and got 1,359 because it counted players where the harness counts calls |
| DECISION: determinism qualified, Rung B restated, reference pair swapped | meta | verified | high | 100% | 2026-09-08 | Adopted after lane L5's finding. (1) The oracle HOLDS where no AI must choose a target and FAILS where one must - never wrong, measured on the idle turn. (2) This is NOT a contradiction of lockstep MP but the reason for its shape: the AI is a StrategyClient deciding ONCE and relaying commands; the sim is deterministic given the stream. (3) Canonical reference pair is now turn2-state -> turn3-state (deterministic, oracle-hashed); turn1 -> turn2 is secondary with its four non-deterministic leaves masked. (4) Rung B = byte-match given a recorded command stream (--turn-commands); a behaviourally-equivalent game/ai is Rung C, verified against the distribution of the original's choices. (5) Mechanism inferred, not proved: one-of-three varying suggests a pointer-ordered tie under ASLR rather than a time seed (which would move all three); probe routed to L1. Documented in backlog.md 2 and determinism-oracle.md |
| new crew member: re-resolver (Fable) | meta | verified | high | 100% | 2026-09-08 | ~/.claude/agents/re-resolver.md. Invoked when a lane reports a SURPRISE - a falsified prediction, two lanes contradicting, a closed finding a new measurement qualifies, a number that does not add up, a possibly-perturbing instrument. It does NOT measure: it exonerates-or-indicts the instrument first, lists every standing claim touched (survives / qualified-with-scope / overturned), names the ONE probe that separates the live hypotheses, and sets the course - lane redirects, milestone restatement, a draft rule. Writes findings/resolutions/-.md and PROPOSES board/backlog text rather than editing them. Runs on the planning model. Calibrated on today's five resolutions (ModCount, non-deterministic reference pair, hook perturbation, widened literals, stale build) |
| Rung C sharpened: the TIE-SET claim | meta | verified | high | 100% | 2026-09-08 | User's framing adopted: if the AI's variation is a per-process TIE, our engine picks canonically and the verification is "our choice is one of exactly k outcomes the original can produce, we can name all k, and every observed run lands in the set" - stronger than behavioural equivalence, and it keeps the byte-match alive wherever the tiebreaks agree. Lane L4 briefed to capture the varying empire's CANDIDATE SET and arrival order; lane L1 to read the AI client's generator seed across two processes. Between them the mechanism gets named |
| standalone on the CANONICAL pair: 108 -> 63, closed 45, regressed 0 | engine | verified | high | 100% | 2026-09-08 | First measurement on the deterministic reference pair turn2-state -> turn3-state, clean build (rule 24). Byte-match distance on the pair that has the oracle hashes. The secondary pair turn1 -> turn2 reads 209 -> 124 (closed 85) with four leaves the original itself cannot pin. Both are in status.json; the dashboard quotes the canonical one |
| Rung B lanes launched: RB (replay) + CB (capture) | phase2 | in-progress | — | 0% | 2026-09-08 | Paired deliberately: a replayer with no real input is untested, a capture with no consumer is a log file. RB builds sots_turn --turn-commands <blocks> - read a recorded block set, apply it in the original's order (AI4: a flat run of 27 per-list loops in an order that is NEITHER list nor offset order), account ModCount per the 1-16/17-27 rule, and byte-match the autosave of the run that produced the stream. CB captures on VM146 at the APPLY side, not the emit side - ApplyAllTurnCommands sees every player's block including the AI's in application order, where the emit side would give only one client's. Priority: the canonical deterministic pair first, then turn1->turn2 WITH its own autosave, then ideally the SAME turn twice - two streams differing only in the varying AI pick, which would be the cleanest possible demonstration that the sim is deterministic given the stream |
| HIVE CREATION DRAWS - lane Z's zero QUALIFIED, not overturned | verify | verified | high | 100% | 2026-09-08 | Lane L1. SVSOSwarmQueen::RegisterHives takes one strategic-generator word per new hive, inside StrategyServer::BeginProcessTurn - inside lane Z's autosave bracket and OUTSIDE EVERY ONE OF ITS SUBTOTALS. Nothing had ever been hooked in that interval. turn1->2 (hives created): BeginProcessTurn 2, ProcessTurn 20, tail 0, total 22, residual outside the drivers = 2. turn2->3 (hives exist): 0/19/0, residual 0. THREE INSTRUMENTS AGREE: the region ledger reads 170->172 across RegisterHives; the return-address draw-site table gains a row at 0x00527714 that is ABSENT the next turn; and predict_new_hives, computed at entry from the original's own two predicates, said 2 BEFORE the run. AND THE DRAWS PRODUCED THE HISTORICAL VALUES: NextQ 30/28 out of RegisterHives, slipped to 31/29 by TickHives = exactly turn2-state, then 32/30 = exactly turn3-state; word positions 192/211 match lane Z's own recorded positions. So tail-rng-ledger's "all of it inside ProcessTurn, residual exactly zero" is RIGHT FOR EVERY TURN IT MEASURED AND FALSE ABOUT THE CODE. For the standalone: the interval to reproduce starts at BeginProcessTurn |
| CDiff edges confirmed by playing 49 turns forward | verify | verified | high | 100% | 2026-09-08 | Lane L1 predicted a store at frame 2 and at frame 50 and nothing else, tier 2 unreachable. Played ref-turn2 forward 49 End Turns: 50 entries, exactly two stores - frame 2 (-1->0) and frame 50 (0->1) - and 47 turns where the writer RAN AND STORED NOTHING, which is rule 20's distinction demonstrated live. Frame >= 100 stays a code read, twice-disassembled: the scan exits with index 3 into the epilogue, so CDiff can only ever hold 0 or 1 |
| NextQ constants READ, not fitted - SV's blocked leaf closes | verify | verified | high | 100% | 2026-09-08 | Lane SV had declined to fit two unknowns from one constraint (r1-r2=2) and left the leaf blocked, correctly. L1 read them live: LO = 20, HI = 30, so NextQ = frame + 20 + NextInt(10) inclusive; gates are 10 (a frame floor), 5 and 3 - no queen can spawn before frame 11 on any save, which is why the spawn arm has never run. TickHives entered both turns, 0 words, inc only |
| SnLv: hypothesis -> verified | verify | verified | high | 100% | 2026-09-08 | One arming line, as lane W3 predicted. Spica: AFlags = 0x0, SnLv = 0x00000200 = contact level 2 for player 4, its NVO key - the AFlags term is false, so the sensor branch is what refreshes Spica. Free extras: level 3 occurs on exactly the eight pairs whose AFlags bit is set, level 2 (never 1) where it is not, correcting ComputeContactLevel's documented "else 1"; SnLv written 20x/turn from W3's site, to the byte. Bismol is not on this map - the same config on zuul-turn23-fleet23 answers it in one End Turn |
| hooks=off is NOT self-consistent | verify | verified | high | 100% | 2026-09-08 | Lane L1 hit the turn1->turn2 nondeterminism independently and reconciled to L5 rather than duplicating. Combined: four outcomes over six runs, including TWO DIFFERENT hooks=off RESULTS - which is stronger than "no hook is responsible", because the control itself varies. Bounded: 4 differing leaves out of 35,032, identical tree structure, on every pairwise comparison. Raises a gap in rule 19: a control that is itself non-deterministic needs a stated policy. Routed to the resolver |
| COMBAT MULTIPLAYER WORKS - the revival is complete | subsystems | verified | high | 95% | 2026-09-08 | Lane L2, VM141. Two humans fought a real PvP battle inside a LAN-mode game. Both got the encounter query, both chose Fight Manually, both loaded the tactical HUD, and both ran the same battle off ONE SHARED LOCKSTEP CLOCK - the round expired as a draw and both instances returned to the strategy screen on Turn 13. No @SYNC ERROR, no hang at COMBAT_WAITINGFORPLAYER, no crash, no divergence. THE CLOCK IS THE PROOF: sampling the two instances alternately, the countdown lies on a single line at ~0.29x real time regardless of which instance is read (3:51 -> 3:31 -> 2:42 -> 2:18 across four samples from alternating instances). One simulation with two views, not two free-running timers. The sim also does NOT stall when a window loses focus. *101,835 packets on tap141i0 across the whole run, 0 to any GameSpy port, 0 DNS for any .gamespy.com - including natneg, which the combat path never touches either. W2's last open question is closed |
| CORRECTION: CombatHostPort 3370 is NEVER bound | subsystems | verified | high | 100% | 2026-09-08 | Lane L2's P1, falsified. Not at lobby time, not across twelve turns, not during combat loading, not while the tactical sim ran with both players in it. The socket table was polled continuously (700 ms inside the guest, then 2 s from outside across the whole battle) and logged EXACTLY ONE UNCHANGING LINE: host 0.0.0.0:3369 + ephemeral, joiner ephemeral, and no TCP at all on either process ever. The combat lockstep rides the strategy connection the host already has open on 3369. W2 correctly observed 3370 unbound at lobby time but inferred "it must be opened later" - that inference is wrong on this arm. 3370 presumably serves the SNMHostCombat case where a DIFFERENT player or a dedicated server hosts the battle, impossible when the strategy host is itself a combatant. Port-forwarding advice is 3369 and only 3369 for a two-player direct join |
| adopting a clone: do not diagnose from ARP | meta | verified | high | 100% | 2026-09-08 | Lane L2, ~15 minutes, and the obvious diagnosis was wrong. No IPv4 in any ARP table, no guest agent, and a 40 s tap capture caught ONE packet - which looks exactly like a clone whose inherited static IP was disabled for a duplicate-address conflict. It was none of that: sendkey into an elevated cmd plus a screendump of ipconfig /all showed a healthy DHCP lease on 192.168.10.143 obtained half an hour earlier. THE GUEST WAS SIMPLY IDLE. Take a screendump first. Four things the clone needed: shim.cfg was a previous lane's hooks=trace; C:\SOTS\launch-b.cmd does not exist though sotsb.xml invokes it; lobby slot 2 defaults to Computer and a /join client cannot take an AI slot; hostname/SID duplicate VM140 (harmless for SSH-by-IP). ALSO: with 10 initial colonies and 0 initial technologies neither player starts with a single ship, so random encounters at 100% never fire - you must build warships first |
| LAB HAZARDS from L2 | meta | verified | high | 100% | 2026-09-08 | (1) pgrep -f "tap141i0" matches QEMU's OWN command line (ifname=tap141i0), so stopping a tcpdump SIGTERMed the VM. Use pgrep -x tcpdump. The guest came back clean (fsutil not dirty, same IP). (2) Driving the starmap needs REAL MOUSE MOTION - SetCursorPos alone does not move the game's cursor - plus wheel zoom; L2's driver is in verify/harness/l2-ui/. (3) From lane L4: move X Y then click X Y - a bare click is delivered at the PREVIOUS cursor position about half the time, which reads as "the click did nothing" then "the next click did the previous thing" |
| FOURTH cross-lane commit sweep - now with plain git commit | meta | verified | high | 100% | 2026-09-08 | Lane L1's board commit swallowed all 22 of lane L2's staged files. Content intact and verified in HEAD; L2 correctly did NOT rewrite history with other lanes live. This is the fourth instance of the hazard addresses.d/README documents, and it now happens with plain git commit too, not just addresses.json - because a lane stages by path but another lane's commit picks up whatever is in the index. The index is shared per-repo; staging by path does not protect against someone else's commit. Needs a real fix, not another rule |
| STRUCTURAL FIX for the commit-sweep hazard (rule 25) | meta | verified | high | 100% | 2026-09-08 | Four instances in one session, the last with plain git commit and NO -A anywhere - every lane had staged by path exactly as instructed and it still happened, because git add <path> puts a file in THE REPO'S ONE INDEX and the next commit takes all of it. Staging by path was never the protection. THE FIX IS A FORM OF THE COMMAND: git commit -m "msg" -- path/one path/two commits only those paths and leaves the rest of the index untouched - verified in a scratch repo (two files staged, pathspec commit took one, the other stayed staged). Lanes now commit with a pathspec, always. Deeper fix if it recurs: a per-lane CLONE of sots-re, mirroring the per-lane worktree of sots-engine - which is precisely why sots-engine has never had this problem |
| RESOLUTION: the byte-match SURVIVES, in a stronger form | meta | verified | high | 100% | 2026-09-08 | First re-resolver run, findings/resolutions/2026-09-08-ai-seed-per-process.md. The AI is not non-deterministic - it is MT19937 from ONE 32-bit word per client, drawn once at construction from a per-process global, and we own that generator bit for bit. So game/ai is a deterministic function of (save, per-client seed). Rung C splits: C-exact - capture the three seeds and our block matches element for element and the autosave byte-matches ON EVERY RUN (the user's framing made literal), precondition being a per-client DRAW LEDGER so we consume the stream in the original's order and count INCLUDING draws whose result never reaches the save; C-set - the fallback for references whose seeds were never logged, like turn2-state.sav, which no process including the original can reproduce. Instrument exonerated: three unrelated 32-bit words per process are successive generator outputs, not clock reads, so AI1's call-site reading STANDS and only its "never seeded" reading falls The shape recurs and is now a PREDICATE (resolver 2026-09-09): pick turns (ResTNm == '' at turn start: turn1-state, as-turn15) and heavy build turns (ad-turn27) EXPOSE the seed; quiet-consumer turns (ar-turn37 -> 39) do not, and agree byte-for-byte across three processes. SCOPE of the C-exact proof: it was demonstrated on turn1-state (early game) only - the rich-turn test is ad-turn27 with aiseed=pin, queued on VM146. A control that agrees means "no exposed decision was reached", never "the AI is deterministic here". C-EXACT FALSIFIED ON A RICH TURN 2026-09-09 (lane BP): with all three AI client seeds pinned, two fresh processes on ad-turn27 still differed. Restated: given (save, per-client seeds) the AI's command block is determined UP TO the assignment of newly allocated fleet ids to newly formed fleets - exactly three words of the 81-element block, and the id counter itself is deterministic. Rung C is DAMAGED, NOT LOST: the assignment is canonicalisable the way C-set canonicalises a pick. SHARPENED 2026-09-09 (lane BQ): a research pick is exposed only when the CANDIDATE LIST has more than one entry. Measured on ar-oracle-A-post -> 40: Player[32] entered with ResTNm == '', producers A and B returned null, the candidate walk ran, candidatesTried=1, and the pick AGREED across two fresh processes. ResTNm == '' predicts a pick, not a divergence. COMPARISON SIDE DELIVERED 2026-09-09 (lane BT, host): state_checksum --relabel-new-ids ships with both acceptance halves green - BP's pinned pair reads IDENTICAL modulo pi = {1970<->1986} (35 leaves -> 0) and AD's unpinned pair is REFUSED and still DIVERGED: 94, byte-identically what the tool reports without the flag. Five guards, every one refuses rather than degrades, and forcing the transposition onto AD's pair with the guard bypassed still leaves 82 leaves - so the tool cannot absorb a real divergence. |
| the tie-set claim: rescued as a STATEMENT, open as a RESULT | verify | mapped | high | 80% | 2026-09-08 | And not on "equal cost" - the resolver's sharpest correction. The 2000-RP tier-1s are not the picks; the picks are tier-2s at 13k-30k. S(D) is the support of a random draw, per save, not a set of equal-score candidates. BIO_GnMod (2 of 6 observations) proves a SECOND ARM whose producer is unidentified - likeliest producer A or B gated by a draw, never entering the walk - so "we can name all k" is TO PROVE. The four-part statement is in the resolution 3.1: derive S before any sweep; canonical pick = lowest tech id; containment; and a pinned-seed sweep produces nothing outside S - part (iv) is what makes it non-tautological |
| THE ONE PROBE: pinned seeds THROUGH the resolver | verify | backlog | — | 0% | 2026-09-08 | The seed SOURCE is not the probe. The resolver probe is only an experiment if the seed is pinned - otherwise two runs with two unknown seeds cannot separate "draws from the stream" from "reads something else per-process". Implementation named: L1's l1seed Seed detour (return address 0x0078304b, three constants) + L4's instrument + a left region over client+0x134 at TryResearchCandidate 0x006c8580; ref-turn2 once as the pin's OWN control; turn1-state twice; then sweep by quit-to-menu/reload (the pin table advances per load, no relaunch needed), logging which producer returned non-null for player 512. Predictions committed in the resolution 4. If the four leaves still differ, extend the pin to the unidentified fourth seeded object |
| NEW OPEN RISK: g_GlobalRNG has four other consumers | verify | backlog | — | 0% | 2026-09-08 | Including RunCombatRound+0x60f - and no battle turn has ever been compared across two processes. Add "twice, two processes" to the battle workload in backlog 5. Lane L2 verified combat lockstep between two clients in ONE game; this is the different question of whether a battle is reproducible across separate runs |
| CORRECTION: neither hooks=off run was a control | verify | verified | high | 100% | 2026-09-08 | Rule 26. Three lanes ran hooks=off on turn1-state and got three different files, so no single hooks=off run on that workload ever exonerated anything. And lane L4's "run R2 is byte-identical to the hooks=off control" is a ~1/k COINCIDENCE, not a control - with an outcome set of size k, agreement by chance is expected. Rule 19 said take the measurement with the instrument removed; it did not say what to do when THE CONTROL ITSELF VARIES |
| ModCount IS NOW REACHABLE - 24 exactly, zero residual | engine | verified | high | 100% | 2026-09-08 | Lane RB. The leaf the campaign has called "not derivable from a save" since lane A2. Our turn writes 14 where the original writes 24; the missing ten ARE the command stream, and sots_turn --turn-commands puts it on 24 exactly - 2 drivers + 4 research-rate gates (one per SUBMITTING block, including the human who ordered nothing) + list 3 + list 5 + list 10 + 2x list 14 + list 8, with the list-23 population element FREE. The 1-16/17-27 cost boundary held on the first workload that ever populated the free half. What it does NOT reproduce: everything else - seven of the ten commands need subsystems we lack, and the three we can apply are all no-ops here (the AI re-issues rates and sliders the save already holds) |
| THE MATCHED TRIPLE - and it correctly FAILS to close one leaf | verify | verified | high | 100% | 2026-09-08 | The campaign's first (input, stream, that run's output) set. Canonical pair 108 -> 63 -> 62 with the stream (closed 46, regressed 0); secondary 209 -> 124 -> 118; matched triple 209 -> 124 -> 117 (closed 92). Against the HISTORICAL oracle it closes six and correctly leaves player 512's ResTNm diverging - the recording picked XNC_TrnsMorr2, turn2-state holds BIO_GnMod. That was predicted before the run: a replay that CLOSED it would have meant something was copying the oracle. This is the C-exact / C-set distinction demonstrated on real data - the stream reproduces its own run, not a different run's tiebreak |
| a falsified prediction found a defect in TWO lanes | verify | verified | high | 100% | 2026-09-08 | RB's P3 predicted regressed 0; the first run regressed 2. Cause: the rates frame is dumped in MEMORY order and its memory field order is not its wire order - the AI's one non-zero slider went into SRt where the oracle holds SRsc = 1.0. Memory member 1 is wire member SRsc; six members unread. Lane CB's JSON decoder has the identical defect - RB's adapter masks it engine-side but the JSON is the capture of record, so the list-5 record should be dropped or emitted in wire order with the mapping stated. Routed to CB mid-flight |
| apply order verified three ways - and honestly unfalsifiable here | control-flow | verified | high | 90% | 2026-09-08 | Thirty steps: lists 6 11 20 19 17 18 5 23 24, gate loop A, 1 4 3 21 2 22 9 10 12 13 14 15 16 7 8 25 27 26, gate loops B and C. Verified three ways: the six inlined bump sites plus the three gate-loop heads are strictly increasing in address in exactly that step order (9 of 30 positions independently re-derived, the other 21 inherited from AI4); bijection of 1..27; and neither the list nor the offset sequence is ascending, so a sorted implementation cannot pass. STATED PLAINLY BY THE LANE: apply order is unfalsifiable on this corpus - every non-empty list is one player's and every command names one system |
| CB's capture replays unchanged; first AI seeds landed | verify | verified | high | 100% | 2026-09-08 | RB did not duplicate CB's tool - tools/tcb_from_json.py joins them, CB's JSON staying the capture of record and .tcb the engine's parser-free input. CB's real VM146 capture replays through it unchanged to the identical canonical result, and brings the first AI seeds (32 -> 0x9dfad635, 496 -> 0xb482b2d8, 512 -> 0xfb81e993 - now Rung C's primary input) plus heap payloads closing two gap rows: list 8's route is [272], list 10's vector is [1728], a ship the input save already contains |
| Rung B completeness gap (the real backlog) | verify | backlog | — | 0% | 2026-09-08 | From RB, full table in its finding: ship construction; the client's ID ALLOCATOR - design 18 and fleet 34 are allocated CLIENT-SIDE before submission, and it is a watchpoint not a week of reading; the rates frame's memory order; list 10's and list 14's EFFECTS; the Population body; techId -> name (the wire carries an id, the save a name; today transcribed from the capture and reported in its own column, with 144/90/288 -> IND_Waldo/DRV_PlsFiss/XNC_TrnsMorr2); the civilian-ratios gate has NO APPLIER ANYWHERE so its cost is unknown NOT zero; and whether the load-time batch charges the counter is untested |
| RATCHET HIT: L3's trade-route saves carry unmodelled content | verify | backlog | — | 0% | 2026-09-08 | A good failure, and the ratchet doing exactly its job. mars_stream_save fails only when lane L3's two new saves are present - isolated by re-running on the 13-save corpus, which passes 1/1 - so this is NOT a regression in lane RB's code. human-turn5-traderoutes.sav drops named coverage to 99.0% (409 opaque) and human-turn8-traderoutes.sav to 98.6% (613 opaque), against a 99.99 ratchet. The reader is sound on both: round trip: tree identical, typed identical - the items are carried correctly, just not NAMED. What is newly opaque: <rest:prep> 284 and 439 (the largest, and almost certainly the trade-route/prep data itself - the thing three lanes could not manufacture), TacReports 123 (which lanes B5/J had already flagged as carried-not-typed), and Lay 49, which is new. DO NOT LOWER THE RATCHET - type the content; that is the drift the campaign guards against. This is the first corpus expansion in the campaign to lower coverage, which is what a ratchet is for |
| L3 DELIVERED THE TRADE-ROUTE SAVES | verify | verified | high | 100% | 2026-09-08 | Visible in the corpus ahead of its report: human-turn5-traderoutes.sav and human-turn8-traderoutes.sav, game MyGamel3trade, 28 systems, 2 players, turns 5 and 8. Three lanes failed to build this workload (H, W2, W3 each accounted for why); lane W3 had measured the absence - trade routes count 0, all three vector pointers zero, default-constructed not emptied. These saves are what unblocks spies2/SysMem/mts (rule-6 hypotheses with count 0 in all 11 previous saves) and the four tail callees that CAN draw RNG but have never fired on an empty container |
| C-EXACT PROVEN ON HARDWARE: pinned seeds collapse three runs to one | verify | verified | high | 100% | 2026-09-08 | The resolver's C-exact ruling, demonstrated. Lane CB pinned the three AI client seeds to values an earlier run observed and ran the creation turn in THREE PROCESSES - nine distinct natural seeds - which produced ONE autosave (d59bb9f2...), with identical gates including player 512's research target 282 and identical element records. The workload three lanes could not reproduce is reproducible given the seeds. Lane L1's mechanism is confirmed from the decision side, and the roadmap's Rung C-exact is no longer a proposal - it is a measured property. What remains for C-exact is the per-client DRAW LEDGER (consume the stream in the original's order and count, including draws whose result never reaches the save) |
| the canonical capture is instrument-neutral | verify | verified | high | 100% | 2026-09-08 | One End Turn on turn2-state with the block dump installed wrote bb4fd9ac... / 978041ac... - byte-identical to the published oracle AND to lane CB's own hooks=off control taken on the same guest through the same click path. Rule 19 discharged properly this time (a self-consistent control, per rule 26). (Autosave).sav IS turn3-state.sav. The capture adds three things no earlier one had: the heap payloads (list 8's route [272], list 10's vector [1728], list 23's Population body 24 bytes - ABSENT, not empty, from every prior capture), the three AI client seeds, and the input save plus both autosaves with sizes and hashes |
| CB dropped its own defective record rather than relabel it | meta | verified | high | 100% | 2026-09-08 | Lane RB found CB's list-5 record carried the memory-order-vs-wire-order defect. CB dropped the record: values ship, names do not, wire: null, .tcb emits ?. That is the right call for a capture of record - a missing field is honest, a mislabelled one propagates. It also corrected lane L4's list-23 reading twice (there is no trailing int - the +1/-1 was an ALLOCATOR COOKIE - and the body is not turn-dependent), and element sizes are now measured rather than read (runs with byte-identical autosaves cannot differ in a field) and agree with the writer's instruction stream on all six lists. List 14's bool is ONE BYTE; the rest of that word is garbage |
| k=6 is NOT closed - a seventh value appeared | verify | backlog | — | 0% | 2026-09-08 | Player 512's research target 282 is a value no prior run produced, and nobody has mapped it to a tech name. Lane L4's XNC arm had k = 6 with four observed; lane CB's pinned runs produced a fifth distinct value. So Rung C-set's "we can name all k" remains TO PROVE, exactly as the resolver ruled - and the outstanding BIO_GnMod arm is still unidentified. The pinned-seed sweep the resolver specified (quit-to-menu/reload advances the pin table, no relaunch needed) is the way to enumerate the set under known seeds |
| CB's own gaps, stated | verify | backlog | — | 0% | 2026-09-08 | C2 (the ModCount trap set) not run - RB's replay reaching ModCount = 24 with zero residual is a STRONGER check than the trap multiset, so per-EIP attribution stays an open cheap hour with shim.cfg.cbtrap staged. C4 (a second unpinned turn-1 run) not run - superseded by the pinned triple, so P5 as written stays unresolved and there is one unpinned turn-1 stream, not two |
| two more lab facts | meta | verified | high | 100% | 2026-09-08 | From lane CB: the vmwatch wall CACHES FRAMES FOR 5 s, which cost it a misdiagnosis of four working clicks before it spotted the staleness - if you are driving a guest and watching the wall, the wall lags you. And chained click batches via schtasks /Run can silently drop one - drive one click per call. Both in verify/harness/cb/README.md, joining L2's real-mouse-motion and L4's move-then-click |
| CORPUS 11 -> 19 SAVES, and the ratchet broke as designed | verify | backlog | — | 0% | 2026-09-08 | The save corpus grew from 11 to 19 in one evening as lanes finally manufactured workloads nobody had. Verified clean on the original 11 (mars_stream_save 1/1); every failure comes from saves added today - L3's trade-route games, which reached LATER TURNS while integration ran, plus CB's captures. Newly opaque and itemised: <rest:prep> 704, TacReports 369, Lay 79, <rest:trdmgr> 9, fwarn 3 - and trdmgr is THE TRADE MANAGER, the container three lanes could not get to fill. Named coverage 97.6% against a 99.99 ratchet. DO NOT MOVE THE RATCHET - the reader is sound on all of them (round trip tree-identical, typed-identical), it just does not NAME the new items. Now rule 27. INTEGRATOR NOTE: my first control excluded *traderoutes* by name and still failed, which briefly looked like CB's merge was at fault - scope a control by an explicit list, not a name filter |
| ROLL PARITY IS FALSE - measured at five scopes | verify | verified | high | 100% | 2026-09-08 | The user's hypothesis - that an AI client draws a fixed number of RNG words regardless of path, the classic lockstep discipline - is false, and not marginally. (1) ACROSS CLIENTS, one turn: 3 / 0 / 0 words for AI players 32/496/512, human 0. (2) ACROSS TURNS, one client: 32 drew 3 on turn 2 and 7 on turn 1. (3) ACROSS PROCESSES WITH THE PATH FIXED: client 512 makes ONE cl_RandRange call - the research tie-break - and it cost 1 word in one process and 3 in another. (4) PER SITE: RNG_Chance costs 0 words at p<=0 and p>=1, and RNG_NextInt is an unbounded rejection loop (E[words] = (mask+1)/(bound+1), exactly 1 only for power-of-two bounds). (5) AT TURN START: client 32 began at block index 9 in one process and 11 in another, from the same save. Twenty-one live draw sites exist in an AI turn (plus two provably dead); two fired on the reference turn; only three are unconditional, and only given their enclosing function was called. Six of client 32's seven turn-1 words come from the ship-design composer 0x006ad700, which also holds the only loop-carried draw - that is where the variance lives and is the obvious next probe |
| so THE CHEAP game/ai DOES NOT EXIST | meta | verified | high | 100% | 2026-09-08 | The hoped-for shortcut - align the stream by count and order without modelling decisions - is closed. Every draw sits behind a branch on game state, so a wrong decision changes WHETHER a draw happens, and a slightly-wrong probability flips Chance between 1 word and 0. Rung B is unaffected (command-stream replay runs no AI). Rung C needs the decisions. REAL POSITIVES though: the AI touches no generator but its own client's - foreign_words = 0 on every bracket, the first LIVE confirmation of what lanes AI1/AI3 read statically - the human client draws nothing at all, and the per-turn cost is single digits |
| WHY only one empire varies - read off the running game | verify | verified | high | 100% | 2026-09-08 | Lane L5 observed it, lane L4 inferred a code-path cause, and PAR measured it: 496's research shortlist has ONE member, so the tie-break cl_RandRange is skipped by cmp eax,[edi+4] / je; 512's does not, so it draws. Not robustness of picks, not ordering - a shortlist length |
| cl_RandFloat: a THIRD facade, hidden by a tail jump | objects | verified | high | 100% | 2026-09-08 | Found twice independently (live, and by a static scan following E9). It reaches RNG_NextFloat by a TAIL JUMP, which is exactly why ai-turn-logic.md 5's "zero NextFloat calls from the AI module" reads as true and is not. All 29 call sites of the three cl_* RNG facades are inside the AI band, so the cl_ RNG facade is AI-only surface*. Tail-call thunks have now hidden: the OID allocation chain (B6), a sole caller that made a function look dead (T2), and this |
| the instrument caught its OWN defect | meta | verified | high | 100% | 2026-09-08 | RNG_Chance calls RNG_NextFloat and both were detoured, so one word was counted twice. left_delta was never affected - which is the reason the lane took two independent measurements, and why the defect was visible at all. A single-instrument run would have reported the wrong per-site count with no way to notice |
| call parity: wire and apply yes, call no (incomplete) | subsystems | mapped | med | 60% | 2026-09-08 | The secondary deliverable, unchallenged but not driven: parity holds at the WIRE and APPLY layers - the server cannot distinguish an AI block from a human one - and breaks at the CALL layer, where there is AI-only surface (the cl_* RNG facade, cl_SetResearchTarget, at least four order methods with no UI caller) and UI-only surface. PAR did not drive the UI-reachable order methods, so lane Q's ~22 hypothesis rows stay unconverted. That workload is still wanted |
| TRADE + SPY WORKLOAD BUILT - and the tail still does not draw | verify | verified | high | 100% | 2026-09-08 | [SUPERSEDED 2026-09-09 by lane AC: THE TAIL DOES DRAW. This row's negative was true of the state L3 built and false about the code - the gate is a TECH, not container contents. See the AC rows below.] Lane L3 cracked the workload three lanes failed at, on one stock unmodified install from a cold main menu in one session: 20 trade routes by turn 4, 21 by turn 13, and a spy program count=1 on turn 14. AND THE MEASUREMENT THAT MATTERS: with 21 routes, 6 trade sectors AND a live spy craft, the four outer tail callees are entered exactly once per turn as always, and the three inner RNG-bearing callees entered ZERO times - that turn and across all seven instrumented turns, every probe installed=true. So the RNG ledger and the standalone's generator model do NOT need extending on account of either container being non-empty. Lane W3's open risk is retired BY EVIDENCE rather than by absence. The next condition is ACTIVE contents, not present contents: tsnumflt=0 on every sector (no freighters) and spyon=0 on the spy (built, never deployed) |
| why three lanes failed: it was the research UI, not the tech cost | meta | verified | high | 100% | 2026-09-08 | Clicking a tech node is a PREVIEW. The bottom-left button - which lane O read as "Cancel research" - is the CONFIRM, and it pops a Confirm Research dialog; Esc leaves without confirming. And the tech tree pans only on a right-button drag of ~600 px: left-drag, edge-scroll and wheel-zoom all do nothing useful, which is exactly why the screen reads as ignoring input. L3 added drag/rdrag/sdrag/wheel verbs to VM144's click helper. TWO PREMISES CORRECTED: a trade station is a +2-routes/+25%-income BONUS, not a prerequisite - NumTradeRoutesSupported has a floor of 1 per owned system, so population thresholds scale the count and never gated it; and the spy tender nobody could find is the Cruiser "Repair and Salvage" mission section, which nothing in the UI calls a tender or associates with spies. The container grows at Build Spy while the craft is still docked, so no enemy colony and no asteroid belt were ever on the critical path |
| CORRECTION WITH TEETH: spies2 is not the spy list | objects | verified | high | 100% | 2026-09-08 | spies2 is 0 in all 28 systems of a save that DOES have a spy. The natural reading of lane W's "count 0 in all 11 saves" was "no save ever had one" - that reading is dead. Either spies2 is not the spy list, or it fills only for a DEPLOYED spy. A rule-6 hypothesis that survived because nothing exercised it, now exercised and found wrong |
| a config that would have reported a CONFIDENT ZERO | meta | verified | high | 100% | 2026-09-08 | Lane L3 found and fixed a real defect in the watchpoint module: hooks=off watch=on printed watch=on and armed NOTHING. A lane using that combination as its rule-19 control would have logged zero traps and concluded the container never grew - the exact failure family as the stale binary, the shared build dir and the double-counted draw: no error, no warning, a plausible number. Its own instrument was byte-neutral: the armed run reproduced the determinism oracle exactly, which doubled as VM144's adoption check |
| four new saves: the first populated trdmgr and spymgr | verify | verified | high | 100% | 2026-09-08 | All --strict clean, 0 errors/warnings: human-turn5-traderoutes, human-turn8-traderoutes, human-turn11-spytechs, and human-turn15-spyprogram - 21 routes, 6 sectors, spymgr{nspy 1, spy{sid 1, sown 16, atto 3728, tdep -1}}, the first populated trdmgr and spymgr in the corpus. These are the saves that broke the coverage ratchet (rule 27) by carrying <rest:prep>, TacReports, Lay, <rest:trdmgr> and fwarn that no schema names |
| RATCHET DEBT PAID IN FULL - 99.50% -> 99.9949% | engine | verified | high | 100% | 2026-09-08 | Lane WS, and the bar did not move (rule 27 honoured). Per save: turn5-traderoutes 99.0003 -> 99.9951 (409 opaque -> 2), turn8 98.6349 -> 99.9955 (613 -> 2), turn11-spytechs 97.6382 -> 99.9959 (1166 -> 2), turn15-spyprogram 96.9412 -> 99.9964 (1706 -> 2); corpus 99.5021 -> 99.9949, 3926 opaque -> 40. All 20 saves return above the ratchet, and the only opaque items left on ANY save are the RNG blob's two - nothing a save exercises is carried any more. 14 new bindings, every one a full match: 100 shapes, 966 items, 0 MISMATCH. No header regeneration and no new addresses - it all came from the already-generated streams.json plus the records the new saves finally carry |
| TacReports: why TWO lanes could not close it | objects | verified | high | 100% | 2026-09-08 | The reason was real, not a miss. TRnc plus three trailing scalars is IDENTICAL in a flattened linear recovery whether it is three separate runs or one loop body of three - and TRnc was 0 in every save, two levels under a count that was also 0. Nothing could distinguish them. The trade saves show the wire directly: TRships TRsats TRshipsL INTERLEAVED, three times, under TRnc == 3. One loop body. Binds 20/20. A textbook case of a workload settling what static reading structurally could not |
| two type corrections, and the coverage figures were OPTIMISTIC too | verify | verified | high | 100% | 2026-09-08 | (1) Game::CombatReport: auto/cdst are bools and dur/cdt/cdi are floats - all five had been ints. Four are 0/1 everywhere so the bytes never moved, but cdt was being read as 1070805848 for what is 1.598. (2) Game::CombatWeaponReport's damage quartet is flat with float dami/damt; the old shape reached them through an obj_flex nested dams frame the binary never writes - which made CoverageArchive charge one phantom typed item per weapon report. The four affected saves have 8/13/21/32 weapon reports and their totals moved by exactly 8/13/21/32, so the pre-fix coverage figures were slightly OPTIMISTIC as well as too low |
| FTPnts stays carried, and names its own workload | verify | backlog | — | 0% | 2026-09-08 | The one item the recovery itself marks unresolved, count 0 in every Lay. It names the element class from the DECORATED HELPER NAME - exactly the SysMem/mts/nalat trap: the framing belongs to the helper, not the class. Carried, with Game::FieldTemplate::Point deliberately left unbound. The workload that settles it: a save with a stored fleet tactical formation - set a fleet's combat layout in the tactical setup screen, then save. Everything else still carried is a never-filled container costing zero items on every save |
| LAB: CT111 direct SSH is publickey-denied | meta | verified | high | 100% | 2026-09-08 | Multiple lanes have now hit this and worked around it differently. Direct ssh 192.168.10.138 is denied; the working route is ssh spicy then pct exec 111. Lane WS could not reach it at all and installed g++-mingw-w64-i686 locally, cross-building against GCC 10-posix rather than CT111's toolchain - so its "shim clean" was against a different compiler and the integrator re-ran the real one. Two facts it surfaced that are worth keeping: TRmin wanted a member named min, which windows.h defines as a macro (it is mine for that reason), and the win32-threads variant of that compiler cannot build zip_archive.cpp at all (std::mutex absent) - the posix variant is required. ReVa also stayed down (CONNECTION_CLOSED) the whole session; the server is active and CT111 is healthy at load ~15 from our own builds |
| THERE IS NO CLIENT-SIDE ALLOCATOR - one allocator, partitioned by node | objects | verified | high | 100% | 2026-09-08 | Lane ID, and it dissolves the question rather than answering it. StrategyServer and StrategyClient are BOTH Game::StrategySim (RTTI; StrategyServer carries vftables at offset 0 AND offset 4 - earned rule 1's "two bases 4 bytes apart", and the +4 one IS the StrategySim subobject). StrategySim owns an IDMap at StrategySim+0x80, so each sim allocates from its own map on its own node index using lane B5's one function. The entire protocol is a single branch in StrategySim::CreateDesign 0x008827e0: explicitId != 0 ? use it : AllocateOnLocalNode(this+0x80) - the client mints and puts the id in the command; the server runs THE SAME CODE and mints nothing. localNode = (PlyrIdx == -1) ? 0 : PlyrIdx + 1 (four instructions at 0x007c8ecb): node 0 is the server's, player k gets node k+1. IDMap::Initialize caps nodes at 16 - that IS the nibble - zeroes every counter and seeds only the local one. NO COLLISION IS POSSIBLE: the low nibble partitions the space; ~2,600 ids across 20 saves and 4 games, zero collisions, with nodes 1, 2 and 3 all occurring and each starting at counter 1 |
| save tags named: NMSz / NMLc / NMnx | objects | verified | high | 100% | 2026-09-08 | Falls straight out of IDMap::Initialize(numNodes, localNode, startId) 0x008b9ad0: NMSz = node count, NMLc = local node, NMnx = that node's counter - confirming lane B5's labelled hypothesis and adding the other two. A client is seeded from the SERVER's counter for its node via IDMap::GetNodeCounter. Zero-residual id account of the reference turn from the Sim block's master lists: ship 1760 (counter 110), fleet 1776 (111), fleet 34 (client counter 2), DesignIDs unchanged 43->43. TWO CORRECTIONS to turn-command-replay.md 4: 1712 was issued in turn 1->2, and 1776 is a fleet, not a design; and design 18 is already in turn2-state.sav, so the canonical replay must mint exactly ONE id (fleet 34), not two |
| techId = the stricmp-SORTED index, closed offline | objects | verified | high | 95% | 2026-09-08 | No capture needed. Read out of MasterTechTree's constructor: it copies the parse-order list to a second vector, std::sorts the copy with an inlined _stricmp(a->name, b->name) < 0, then walks it writing def->techId = i at 0x0058ba29 - and the sorted vector doubles as the lower_bound name index, which is WHY it is sorted. That also explains lane RB's dead end: the parse-order table we held is THE OTHER VECTOR. 282 = XNC_TrnsHum2 - and it was already in our notes, observed live by lane L4 in run R2 and never connected (rule 18's corollary, paid for again). All four observed points hit exactly and it reproduces L4's six-member family as {282, 284, 286, 288, 290, 292}, so Rung C-set's "we can name all k" CAN CLOSE. tools/techid_table.py derives all 293 offline and refuses to print unless the four observed points agree. FLAGGED: the _stricmp half is disassembly-only - 48 of 293 ids differ from byte-wise order and no observed point discriminates |
| ID declined the third target, correctly | meta | verified | high | 100% | 2026-09-08 | It did NOT run the cbtrap set: that needs an End Turn on VM140, which rewrites the three oracle autosaves, and lane CB had already judged it the weaker check next to RB's zero-residual ModCount = 24. Confirmatory value did not justify putting the reference bytes at risk. It stays cheap for anyone who backs up SavedGames first, and the exact hashes are recorded in its finding. VM140 released with the oracle verified intact and the game never launched |
| ID's falsified prediction, with the probe named | verify | backlog | — | 0% | 2026-09-08 | Its prediction that client counters restart at 0 on load is falsified by a corpus save: lane CB's reloaded run produced fleet 34, not 18. Either something restores the counter (six IDMap/InitGame functions read, no such write found) or the client makes one earlier unsaved allocation per turn - the second fits everything and implies the id collision has already happened harmlessly in CB's run. One hook on 0x008b8ae0 logging (map, node, id, return address) separates them |
| LAB TRAP: cmd.exe eats parentheses in save names | meta | verified | high | 100% | 2026-09-08 | ssh ... certutil -hashfile "...(Autosave).sav" fails with "Check the spelling", which reads exactly like a missing file. Lane ID's first attempt reported the three oracle saves as absent, and they were never absent - a false alarm about the campaign's most important bytes |
| MY ERROR: over-corrected rule 24, poisoned a remote build | meta | verified | high | 100% | 2026-09-08 | Implementing "fresh build dir per measurement" I dropped --exclude 'build*' from the gate rsync, which shipped a LOCAL build-host to CT111. A CMakeCache records the absolute path it was created in, so the remote build failed with "is different than the directory /home/alex/sots-engine where CMakeCache.txt was created" and 0% of 48 tests passed. THE CORRECT FORM IS BOTH HALVES: exclude local build dirs from the transfer AND rm -rf the remote ones. AND THE SIGNATURE TELLS YOU WHICH FAILURE YOU HAVE: a real regression changes the PASS count (57/58); a broken configure changes the TOTAL (0% of 48 where healthy is 58). If the denominator moved, stop reading failures and fix the build. Re-gated clean: 58/58, shim OK. Rule 24 amended |
| composer WORD-COST MODEL, and it held under pinned seeds | verify | verified | high | 90% | 2026-09-09 | Lane SD. One call to AIComposeShipBlueprint 0x006ad700 costs W = A+B+C+D+E+F+G+H across eight sites (cl_RandFloat once; three cl_RandRange whose cost is the rejection-loop expectation (mask+1)/n; four cl_Chance at 0.5/0.8/0.3/0.2). All four Chance probabilities are strictly inside (0,1), so NEITHER of RNG_Chance's zero-word early-outs is reachable anywhere in this function - only the three RandRange sites can cost more than one word. The loop-carried draw's trip count: f = 1.00/0.75/0.50 by flags and a coin, M = (int)(N*f), D' = max(1,(N+1)/M), L = #{bank j : PointDefence or j mod D' == 0} - not monotone (at f=0.75, N=4 costs 4 words but N=5 costs 3) and at N=1 with f<1 the loop is entered and draws nothing. HELD UNDER PINNED SEEDS: two runs at the same pin in fresh processes are identical field for field on every composer row |
| the seven words decomposed BEFORE the probe existed | verify | verified | high | 100% | 2026-09-09 | From two push literals at 0x006cda9a/0x006cdb17: two composer calls, the first a costOnly=1 PRICE QUERY costing 3 words and the second the real build costing 4. And the bank counts 3/1/2 match the Tarkas section catalog's bank{} counts exactly - a hooked pointer walk and a parsed data file agreeing from opposite directions |
| THE PRICE QUERY IS NOT FREE | verify | verified | high | 100% | 2026-09-09 | New, beyond the brief, and it matters for any AI model: 0x006cda40 runs the composer TWICE per request, and the first pass draws 3 words it DISCARDS. On turn 15 every composer call was a price query - 10 words computed and thrown away, zero designs built. Also on turn 15 the composer is 10 of 16 words, not all of them: the task system's coin at 0x0069086a fires 4x and never fired on turn 1 |
| PINNING AT BRACKET ENTRY DOES NOT PIN THE AI | meta | verified | high | 100% | 2026-09-09 | Two pinned runs with identical draws produced different autosaves, because Prepare-Turn draws happen BEFORE the bracket (life_in 10/435/422 vs 9/412/437). A lane wanting byte-identical AI runs needs lane CB's construction-time l1seed pin, not a bracket-entry pin. Anyone reusing SD's instrument should read this first |
| corrections to lane PAR | verify | verified | high | 100% | 2026-09-09 | (1) PAR's site table omits a live draw site, 0x00691e9b - which PAR itself measured live at 0x00691ea0 - so the AI turn has 22 live sites, not 21. (2) "Six of client 32's seven turn-1 words come from the composer" is five from its body and two from a helper it calls; all seven are in its subtree. Also: player 512's single cl_RandRange has now cost 1, 2 and 3 words across four observations - the rejection loop, visible |
| 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 -- <path>. 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 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. Lane AD reproduced AC's B-at-zero-words with a SECOND, independent fleet and read the failed conjunct out of its own save: trdmgr's three routes have trfr 18/14/14, sourced in sectors 816 and 768, none in 832 - G_B1b, exactly as lane AG decoded. AD's data does NOT distinguish AG's per-system vs sector-indexed reading of TradeManager+0x0c: on sector 832 both predict the same zero. RESOLVED 2026-09-09 (lane AR): decided BOTH WAYS - FUN_00841700 is the writer and the pick fired live at 0x0088b613 with the raider on sector 816. AD's zero on 832 stands and is reproduced inside lane AR's own run as a within-run control. |
| 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 |
| ratchet warning: AC's saves carry new content | verify | backlog | — | 0% | 2026-09-09 | ac-turn20-raider.sav and ac-turn22-raider.sav (both --strict clean) carry CnRad, tscr = 253, three rt route records, and a StarFleet whose LocID is a TradeSector id - the first fleet in the corpus located anywhere but a star. Expect the coverage ratchet to break (rule 27); that is the content to type. [RESOLVED 2026-09-09, PREDICTION WRONG: the ratchet did NOT break. Both saves score 100.0% typed and round trip byte-identically - the novelty lands in fields that were already typed, so a shape-indexed instrument cannot see it. That blindness is now measured on the other axis: 234 of 724 fields have only ever held one value. See the value-axis rows below.] |
| LAB: jmove reads but does not commit; static-link the shim | meta | verified | high | 100% | 2026-09-09 | jmove (jiggle) is required to READ the star-map hover readout but SILENTLY FAILS TO COMMIT a move - use plain click to confirm, and verify the order from Flt.LocID in the autosave, never from the fleet panel. And on the WSL dev box the mingw toolchain links posix-threads, so the shim DLL imports libwinpthread-1.dll and the game dies at load with 0xC0000135 and NO LOG AT ALL; -DCMAKE_SHARED_LINKER_FLAGS=-static fixes it. AC did not change the toolchain file - flagged for the integrator |
| MY ERROR: Rung B IS affected - replay runs the tail | meta | verified | high | 100% | 2026-09-09 | I told lane RB that command-stream replay was unaffected by a tail draw "because replay runs no AI". Wrong. The resolver: this is a sim draw on S+0x16c keyed on save state, and --turn-commands replay runs the tail (command_replay.h:83). Rung B needs the same generator model as Rung A. The canonical pair happens to be untouched by the predicate (tscr = 252, no fleet at a sector), so nothing measured is invalidated - but the reasoning I gave RB was wrong and would have bitten on any raid-turn replay |
| AC's numbers are at probes=11, which is NOT byte-neutral | verify | verified | high | 100% | 2026-09-09 | The resolver's exoneration is partial and precise. The tail's +1 stands - control 0 / active 1 under identical guest, build and config, with the callee going 0 -> 1 entries on the same turns. But every AC number is at probes=11, which lane H bisected as NOT byte-neutral, and AC's own predictions doc promised a probes=8 primary reading it never took. Consequences: the turn totals 20/18 are not calibration data, and no AC autosave is an oracle pair. ac-turn22-raider.sav remains a valid INPUT state. A reminder that rule 19's control must be at the configuration you are quoting |
| lane Z's ledger: the method is sound, the AUDIT is not | meta | verified | high | 100% | 2026-09-09 | The resolver's structural ruling. A bracket state-difference cannot miss a draw that happens - the instrument is sound. But the audit is indexed by FIRING: a site enters the table when a state reaches it, so it can only grow by luck (hives, now the raid roll - two qualifications in two days). What makes the next audit complete: GATE-INDEXED - one row per statically reachable site (lane I's 22+28, V2's 8, L1's hive, node-line decay, the SVSO overrides), each carrying either a measured firing or a decoded predicate on save fields plus the corpus count satisfying it. Today's unfilled rows are named: 0x0088b613, 0x0082cdb8, 0x0088dc43, the spy-13 family, and the SVSO generator which is still unknown |
| rule 24 earns its keep in the OTHER direction | meta | verified | med | 100% | 2026-09-09 | The AC merge gate, run properly for the first time on fresh build dirs on both halves, failed the shim cross-build: ai_orders.cpp tripped -Werror=array-bounds because gcc reasons about a reinterpret_cast from uintptr_t as an object of its own once the read inlines at a constant offset. Three earlier gate runs on a reused build-shim reported the DLL present and green - they were relinking nothing. Note the direction: rule 24's first instance was a stale dir hiding a fix (a passing test that was really stale); this one is a stale dir hiding a break. A reused build directory does not bias toward good news or bad, it reports the past. Fixed with the asm barrier watchpoints.cpp already uses for its TIB reads; engine 0c6d0a3, host 58/58 and shim both green on fresh dirs |
| 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 |
Mars::RNG::Chance modelled, with the zero-word cases and the NaN word |
engine | verified | high | 100% | 2026-09-09 | Acting on lane AG's warning. Body read in full at 0x008e6dd0 (94 bytes, via the ReVa HTTP fallback - the MCP link was down): p <= 0 returns false and p >= 1 returns true, each WITHOUT a draw; in between exactly one NextFloat compared with a strict <. Now MT19937::chance() in the engine with tests pinning all four cases, the accumulating 0.2f-per-turn pattern (4 words then nothing for ever), and the strict comparison. The NaN case is reproduced, not smoothed: both early-outs are pairs of ordered comparisons that a NaN makes false, so a NaN probability falls through, draws a word, and returns false - hiding that would hide a real divergence. Engine 989c692; docs/mars-rng.md updated. Rationale: a model without the early-outs is one word out of step from the fifth turn of every counter-mission onward, permanently, and one word of drift is the whole determinism claim |
| The trade-raid roll is PER FLEET: 2 raiders on one sector = 2 entries, 2 words; B returns without drawing | control-flow | verified | high | 100% | 2026-09-08 | Lane AD, VM141, findings/control-flow/raid-gate-multiplicity.md. Two single-cruiser fleets parked on sector 6 (TradeID 832) from ac-turn22-raider.sav: TradeManager::Slot13RngCalleeA entered 2x per End Turn on BOTH measured turns against lane AC's 1, OnAllCombatDone_Tail costs 2 words against AC's 1, and the return-address ledger prices the site exactly - 0x00820e18 calls=2 words=2, bracket residual 0. H-sector (one roll per sector) is DEAD; the cost is sum over qualifying fleets. vslot13 is still entered ONCE per turn, so the multiplicity is the caller's loop over StrategyServer+0x64, exactly as AC read it. Instrument probes=8 (the byte-neutral set - NOT 11), shim ad-0c6d0a3-20260909T0159Z, cfg shim.cfg.hp8 unmodified, own build dir /srv/re-lab/build/sots-engine-ad rm -rf'd first. CONSTANTS READ FROM THE IMAGE (rule 23, all WIDENED FLOATS): ODDS_CR 0x009e20d8 = (double)0.2f, ODDS_DE 0x009e5060 = (double)0.05f, PER_SHIP 0x009e31c0 = (double)0.01f, and a per-ship scale 0x009f8d48 = 0.33f that REDUCES the odds when ship+0x18 & 0x100000. One-cruiser fleet odds = 0.21000000275671482; six rolls across AC+AD gave two successes (expectation 1.26) - consistent, not validated. Slot13RngCalleeB entered 1x on turn 28 and cost 0 words by return-address attribution - and lane AG's reading that RNG_NextInt has NO zero-bound early-out turns that into a PROOF that B returned before its target pick, i.e. the candidate list was empty (G_B4), reproduced with a fleet AC never had. trdmgr holds 3 routes, trfr 18/14/14, sourced in sectors 816 and 768 - NONE in 832 |
| 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 EXPLAINED IN PART 2026-09-09 (resolver): the variation is the AI's build / fleet-assignment / colony layer and the id-allocation ORDER downstream of it - NOT the design content (the two bodies are byte-identical apart from DesID) and NOT a research pick (ResTNm held). Consumer unidentified; the pin probe on this state (VM146) decides whether it is the seed at all. PARTLY EXPLAINED 2026-09-09 (lane BP, VM146): with the three AI client seeds pinned, 59 of AD's 94 leaves go away - ModCount, NumFlts, DesignIDs[], FleetIDs[], both build queues, every design and ship record and all four turnstats rows become identical. The 35 that remain are fleets 1970 and 1986 exchanging contents. So AD's attribution to the per-process AI seed is right for the substance of the variation and incomplete: a second, non-seed per-process input orders the fleet-id assignment. |
| 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 REACHED 2026-09-09 (lane AR) exactly as costed: five stock Extended Range destroyers (Range 27.0, 2,252 each) built at Epsilon Eridani over turns 28-31, split into four fleets, ETA 4 turns, parked on sector 5's node turn 35. AD's geometry table and its build costing are exact. Two things AD's recipe did not say: a new ship JOINS an existing fleet and a fleet's Range is the MINIMUM over its ships, so the 9-ly cruiser must be split off before the node is a legal target; and the sector node is clickable even though its cube is not drawn. |
| 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 [AD'S READ WAS WRONG - lane AR re-read the same save and found FIVE, trfr 18/16/14/20/14. So sector 816 had TWO candidate routes at turn 27, not one, and my inference below that it had one was built on AD's bad number. The route set does still move (four at turn 22, five at turn 27), so the standing instruction to re-read trfr on the state you build survives - but the arithmetic I used to justify it did not.]. 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 EXTENDED 2026-09-09 by lane AP: the roll fired on seven consecutive turns and SUCCEEDED on Frame 22; sdo is exactly 0.0084*(f-14) as float32 over NINE points and FREEZES the moment sdet stamps. AS's probability table is one turn late - corrected to 43%/70%/87% at +10/+15/+20. AS's 0x0078c97f is now MEASURED (1 word, ProcessTurn bracket) and cbh reproduced bit-identically across processes and builds. |
| 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 FIRST MEASURED ENTRY IN THAT COLUMN, 2026-09-09 (lane AP): 0x0078c97f fires once per SHIPACTION_DEPLOYSPY command applied - 1 call, 1 word, strategic, in the ProcessTurn bracket, with spy.cbh = 5.033599376678467 bit-identical to lane AS's across a different process, build and session. No predicate over the save can express it; no call-graph sweep can find it. The third cell kind is no longer hypothetical. |
| 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 |
VERDICT: TradeManager+0x0c IS the per-system containing-sector table - the writer is found and the raid target pick FIRES |
control-flow | verified | high | 100% | 2026-09-09 | Lane AR, VM141, findings/control-flow/raid-target-pick-verdict.md. Decides lane AG's §3.3 final paragraph, the one load-bearing inference AG flagged as unproven, and lane AD correctly reported it could not separate on sector 832. BOTH INSTRUMENTS AGREE (rule 3). STATIC: the writer AG's §7 lists as NOT FOUND is FUN_00841700 (214 B, 0x00841700-0x008417d5) - it clears the +0x0c vector, RESIZES it to count(([this+4])+0x40..+0x44) (the SYSTEMS vector, the very container FUN_00841cd0 indexes with rt->trfr at 0x00841d36), then walks the SECTOR vector x each sector's MEMBER SYSTEM vector executing mov [this->+0x0c + m->+0x5c * 4], sector at 0x008417a8. ServerSystem+0x5c is the serialised Idx field (objects/layouts.md, grade verified), so this is literally containingSector[system.Idx] = sector and G_B1a cannot reject a valid trfr, BY CONSTRUCTION. It is called from the DESERIALISER FUN_00858a10 at 0x00858f07, so a loaded save has the table populated. LIVE: with 4 raiders parked (2 CR on sector 832, 2 DE on sector 816, all Pos bit-equal), Slot13RngCalleeA is entered 4x/turn, 4 words, every turn - and on turn 42 Slot13RngCalleeB reached its target pick: NextInt at call 0x0088b613, calls=1 words=1, the FIRST firing of that site in the campaign. OnAllCombatDone_Tail cost 5 words that turn against 4 on every other turn; bracket residual 0 on all six turns. THE SAME RUN CARRIES ITS OWN CONTROL: on turns 38 and 40 B was entered by a cruiser on sector 832 and cost 0 words - same process, same build, same instrument, same function, only the sector differs. AC's and AD's proof-carrying zero is a fact about the SECTOR, not the code path. Instrument probes=8 (NOT 11), shim ar-989c692-20260909T0313Z, cfg shim.cfg.hp8 unmodified, lane worktree + own build dir /srv/re-lab/build/sots-engine-ar rm -rf'd first (rules 21, 24) |
B costs ONE word on a success, not two - and the deciding value is a SPECIES FLAG set by a constructor for exactly two species |
control-flow | verified | high | 100% | 2026-09-09 | Lane AR. Lane AG's committed §3.3 prediction was 3 words on a succeeding turn (A 1 + B's NextInt 1 + FUN_00820af0's NextFloat 1) and §5.2 says B costs 2 on success. MEASURED: the tail cost 5 with k=4 raiders and one success, i.e. B cost 1, and there is NO 0x00820c1b row on that turn. Why: FUN_00820af0's third short-circuit is SpeciesDef_HasInterceptFlag 0x0053baf0 - twelve bytes, cmp [ecx+0x144],0; setne al - and InitTable 0x005453a0 sets that flag word at 0x0054562f/0x00545638 for species 0 (HUMAN) bit 0 and species 5 (ZUUL) bit 1 ONLY. Get 0x00545cc0 returns 0x00b10a00 + k*0x184, the same table InitTable fills. So a Human or Zuul raider intercepts at frac=1.0 and draws nothing; the other five species draw a NextFloat at 0x00820c1b. The general tail model is k + s + z words: k = permitted raiders parked with Pos bit-equal, s = their successes ON A SECTOR THAT SOURCES A ROUTE, z = s again unless the raider's species sets the flag. Measured at k+s with k = 4,4,4,4,4,3 and s = 0,0,0,0,1,0. THE CHEAPEST UNEXERCISED STATE IN THE TRADE HALF IS NOW A NON-HUMAN, NON-ZUUL RAIDER - z has never been observed through B by anyone. Rule 23's lesson pointing a new way: the deciding value was not a float literal but a FLAG WORD SET BY A CONSTRUCTOR, invisible to any amount of reading FUN_00820af0 alone SCOPE CORRECTED (lane AZ 2026-09-09): "every other species draws a word" is wrong for HIVER raiders and for HIVER-owned routes, and Hiver can never research Commerce Raiding anyway (CCC_ComRaid pct 0 on both edges), so that conjunct is unreachable through vslot13 for a player. The drawing raiders are Tarkas, Liir and Morrigi. And this row's frac = 1.0 is inverted: the short-circuit sets 0.0. |
| ORACLE PAIR on the turn-37 raider state - and the probes=8 instrument is BYTE-NEUTRAL, whole-save | meta | verified | high | 100% | 2026-09-09 | Lane AR (rules 19, 26). Two fresh hooks=off processes loading ar-turn37-816raiders.sav and running the same two End Turns produced BYTE-IDENTICAL autosaves at both snapshot points: Frame 38 15b99255e1f03dab… and Frame 39 7a8b3d5eb3a60eba…; state_checksum --floats bits --mask none prints IDENTICAL, root 237020deca931f5a180289592cece7ae, coverage PROVED (1,173,884 B rebuilt, 67,239 leaves), 0 error 0 warn. THEN a fourth process with shim.cfg.hp8 installed produced the SAME TWO HASHES. So the instrument is behaviour-neutral at whole-save byte granularity, not merely on the sub-tree the measurement reads - the stronger form of what lane AD could show. Order matters and it was the right order: the control was reproduced FIRST and shown to be a single outcome, so rule 26's 1/k coincidence caveat does not apply (k=1). THIS CORRECTS THE SCOPE OF LANE AD'S ROW: "no oracle pair for this state / the AI varies per process" is true of ad-turn27-two-raiders.sav and NOT true of the same lineage ten turns later, so it is a fact about that state, not about the engine. No explanation for the difference is offered and none should be inferred. ar-turn37-816raiders.sav + 2 End Turns is the first RUNG-A CALIBRATION PAIR from this lineage and the obvious candidate for the standalone's first trade-raid comparison SCOPED by the resolver 2026-09-09: the agreement is explained (no exposed AI decision on turns 38-39 - target held then completed, no design created, forced ship placement) and it does NOT transfer - Player[32] enters turn 40 with ResTNm == ''. Use the second turn, ar-oracle-A-pre.sav -> 7a8b3d5e..., as the standalone's pair once probe 1b reproduces it by load. The peaceful auto-resolve on turn 38 reproduced across three processes but almost certainly bypasses RunCombatRound; the combat-turn warning stands. LOAD-BOUNDARY SCOPE 2026-09-09 (lane BQ): this pair is certified ONLY as the two-turn continuation from ar-turn37-816raiders.sav. Its second half does NOT survive being reached by load: ar-oracle-A-pre.sav + load + one End Turn gives 4c356f59..., twice, differing from 7a8b3d5e... in 17 leaves of ONE AI fleet's movement (Zeta Fleet V moved in the continuation, stayed put after a load). Use the load->turn hashes for the standalone. MECHANISM FOUND 2026-09-09 (lane BS, VM144): the reason this pair does not survive a load is that a Colonize task alive in Player[32]'s AI agent on turn 39 by continuation is ABSENT after a load - AITColonize::Execute 2 vs 0, AssignFleetsAndIssueOrders 2 vs 0, IssueRouteForFleets 2 vs 0, while RunTaskList is 6 in both. Both routes reproduced their published hashes UNDER INSTRUMENT, so the pair is now certified in both forms with the instrument as well as without it. |
CreateRaidEncounter's candidate list is NOT always empty - 0x008939ee fires, and rule 20's case study gets its positive |
control-flow | verified | high | 100% | 2026-09-09 | Lane AR, turn 43 of verify/traces/ar-r1-turn38-turn43.jsonl. ServerTradeManager::CreateRaidEncounter entered 3x and its subtree drew two words: NextInt 0x008939ee calls=1 words=1 - the site lane H measured at 0 on every turn and that tail-rng-ledger.md has carried as a zero ever since, the campaign's own case study for rule 20 - and NextFloat 0x00820c1b calls=1 words=1 through the OTHER caller 0x0082cf65, i.e. lane AG §5.1's chain CreateRaidEncounter -> 0x00892640 -> 0x0082ce00 -> FUN_00820af0. Slot13RngCalleeB was entered 0 times that turn, so neither word is B's. Two of AG's §5.1/§5.2 unread gates are now read from opposite directions. The honest form of the cost claim is neither "0 or 1 further word" nor "at least six draws" but unbounded above and state-dependent; the first non-zero observation is 2 |
| SEED EXPOSURE IS A PREDICATE ON THE PRE-TURN SAVE - why AR's control agreed where AD's and AS's did not | meta | verified | high | 100% | 2026-09-09 | Resolver, on lane AR's falsified P7 (findings/resolutions/2026-09-09-seed-exposure-is-a-predicate.md). Three lanes, three controls, one mechanism: the per-process AI client seed (row 326) is present on EVERY turn and reaches the save ONLY through a decision whose outcome set is larger than one given the state. Read off the saves on disk: AD 27->28 - Player[32] completed BIO_TerBac (no pick: ResTNm held), created a design (NumDes 46->47; the two "Bravestar Mk 3" bodies are BYTE-IDENTICAL apart from DesID 2002/2018), colonised a system (NumOwn 15->16), completed THIRTEEN ships into five new fleets - and A/B differ in the build choice at Sys[272] (978 vs 962), the fleet split (B one fleet more, ModCount 1496 vs 1500), and id allocation ORDER downstream. AS 15->16 - Player[32] entered with ResTNm == '' and picked (64 vs 62). AR 37->38, 38->39 - Player[32] held BIO_EnvTail then completed it (pick deferred), NumDes constant at 47, two freighters a turn into EXISTING Freighters fleets, one encounter auto-resolved peacefully: ~100 leaves moved per turn, all forced, three processes agree. READING: not luck, not a pinned AI - no EXPOSED decision reached; explicable from the pre-turn save. ResTNm == '' names a pick turn (rule 28's shape); NumDes/NumOwn/batch completions name the others; 0x0069dbb0 has a 3-40-turn fuse and cannot show within two turns of a load. Player[32] enters turn 40 with ResTNm == '' - AR's pair MUST NOT be extended by a turn. RUNG A RULING: AR's pair is certified for its procedure; the single-turn form ar-oracle-A-pre.sav -> 7a8b3d5e... is the standalone's first trade-raid pair ONCE one load->End Turn run reproduces the hash (probe 1b, VM145). OPEN: the build/fleet consumer behind AD's variation is UNIDENTIFIED and reading 3 (a non-seed per-process input) is NOT excluded on rich turns - row 360 pinned an early-game turn only. PROBES: (2) VM146, aiseed=pin on ad-turn27 x2 fresh processes, predict IDENTICAL - if not, resolver case; (1) VM145, ar-oracle-A-post + 1 End Turn x2, predict DIFFER in Player[32] TechTree/ResTNm only; (3) per-client draw ledger on both states - the C-exact prerequisite, now decisive. No rule 29: rules 23/26 produced the right behaviour (AR refused the inference); rule 26 gets corollary (c) PROBE 1 RUN 2026-09-09 (lane BQ, VM145). 1b FALSIFIED: ar-oracle-A-pre + LOAD + one End Turn gives 4c356f59..., not 7a8b3d5e..., reproducibly in two processes - the AI moved Zeta Fleet V in the continuation and not after a load, so the pair may NOT enter determinism-hashes.txt in AR's form; the LOAD form (33e30092.../4c356f59...) is certified instead. The resave canonicalisation is EXONERATED. Probe 1 CONFIRMED "the files differ" and /Sim/RNG + /Sim/trdmgr + player-0 fleets identical, but FALSIFIED the mechanism: the research pick AGREED (both chose BIO_GrvAdpt) and the variation is a COMBAT at Venkman. Producer run: A and B both returned null, Player[32] reached the candidate walk, candidatesTried=1 - the outcome set is a singleton by COUNT, not by producer gate. LANE BS RAN THE VM144 PARAGRAPH 2026-09-09. Candidate 1 (AI agent state the save does not carry) SURVIVES and is MEASURED, not inferred: the missing input is a TASK. Candidate 3 (server-side state) is disfavoured - every diverging save leaf has a block-side counterpart. Candidate 2 (stream position) is not what explains it. Two clauses FALSIFIED, both over-specifications: there is no list-14 pair on turn 39, and AssignFleetsAndIssueOrders is NOT entered at all on the load route. |
| THE SPY ATTRIBUTION ROLL FIRES - and P's "three draws" are TWO SPECIES BRANCHES, only one reachable per game | verify | verified | high | 100% | 2026-09-09 | Lane AP, VM144, build ap-989c692-20260909T0425Z, probes=8 (never 11), own build dir /srv/re-lab/build/sots-engine-ap rm -rf'd first. Continues lane AS from as-turn15-spydeployed.sav: 11 End Turns, 6 SECONDS EACH (the brief budgeted an evening; the cost is the launch and the load, not the turns). DETECTION AT FRAME 22, P RAN AT FRAME 25. Mars::RNG::Chance at 0x00840a3c fired 1 call / 1 word / no_draw_calls=0 / strategic=true at ret_rva 0x00840a41 - a site that had never fired. CORRECTION TO EVERY INVENTORY (row 207, lane AG 6, lane AS 6, and this lane's own brief): 0x00840929 / 0x008409c7 / 0x00840a3c ARE NOT THREE SITES BEHIND ONE GATE. SpyManager::Slot13RngCallee 0x008408e0 opens cmp [spyOwner+0x5c], 6; jne - Species == 6 (MORRIGI) takes Chance(0.75f) at 0x00840929 and then NextInt at 0x008409c7 to blame a RANDOM THIRD EMPIRE (a false flag; the true owner is never named); every other species takes Chance(0.25f) at 0x00840a3c and on success returns the TRUE owner. No call can reach more than one. Species table decoded from the save's own ISsp list and cross-checked on lane V's Zuul save: 0 Human, 1 Hiver, 2 Tarkas, 3 Liir, 4 _NPC, 5 Zuul, 6 Morrigi - which also NAMES lane AS's two unexplained multipliers in AccumulateDetectionOdds (0.75f = ZUUL, 0.5f = MORRIGI). Constants read as the four bytes (rule 23): 0x009e5ac0 = 0.25f, 0x009e5ac4 = 0.75f (the same word AS read as the Zuul multiplier). 0x008409c7 needs BOTH the 0.75f roll to succeed AND a third non-eliminated empire that is neither the spy's owner nor the target's. SECOND SITE CLOSED: 0x0088dc43 (the counter-mission roll, lane AG 3.2, corpus 0 of 22, never fired) fires 1 word/turn from Frame 19; the bootstrap turn (Frame == tdep+3) sets spyon/cm:=1/cmo:=0.2f and DRAWS NOTHING. cm, cmo, spyon, ncp - four of the value-domain census's 234 single-valued fields - are all OFF ZERO with a measured trajectory. TWO CORRECTIONS TO AG 3.2: the CnTrd the stage machine consults at cm 1->2/3 is the TARGET OWNER's not the spy owner's (my player has CnTrd=False and it still took 1->2, because Kepler's owner has CnTrd=True), and ncp is a SCALAR COUNTER of completed stages (0,0,1,1,2,2,3 in lock-step with cm), not a container. THIRD SITE CLOSED: 0x0078c97f, the deploy NextFloat in the SHIPACTION_DEPLOYSPY handler that is in NO inventory - 1 call / 1 word / strategic, in the ProcessTurn bracket, and spy.cbh came out 5.033599376678467 BIT-IDENTICAL to lane AS's from a different process, build and session. Its predicate is on the COMMAND STREAM, and this is the first measured entry in a column the gate-indexed audit does not have. THE POLARITY CASE, MEASURED: on Frames 23 and 24 (sdet+1, sdet+2) vslot13 is ENTERED (probe = 1) and costs ZERO - a third arm of the same function, and sdo FREEZES at 0.0672 in the save, proving branch D stopped independently of the ledger. On Frame 25 the entry probe on SpyManager::Slot13RngCallee 0x008408e0 reads 1 - ITS FIRST NON-ZERO EVER, the complement of lane AS's demonstration that its zero said nothing about the subtree. The spy is DESTROYED at the end of phase 23 on sdet+3 by manager vtable slot 10 0x00838480 -> ServerSystem::RemoveSpy, so vslot14 costs 0 that turn: nspy 1->0, spies2 emptied. sdo measured on NINE points, exactly 0.0084*(f-14) as float32 (3c89a027 3cce703a 3d09a027 3d2c0831 3d4e703b 3d70d845 3d89a027), which also corrects AS's detection-probability table by one turn: 43%/70%/87% at +10/+15/+20, not 38/65/85. Residual 0 on all 12 measured turns, overflow 0. ONE PREDICTION WRONG AND NAMED: PB4 said the tail costs 2 words/turn from Frame 19; it costs 2,4,6,9,8,9,8,8 because the TRADE-RAID roll 0x00820e1d entered the tail at Frame 20 and grew 2,4,7,7,8,7,8 as the AI's navy went 157->251 ships - an independent live re-confirmation of lane AD's per-fleet model on a different map and an AI-owned fleet. The spy halves were predicted exactly; quoting a BRACKET total requires modelling every subsystem in it. Saves ap-turn22-spydetected.sav, ap-turn25-spydestroyed.sav, ap-deploy-turn15.sav; findings findings/subsystems/spy-program-draws.md |
| CONTROL ON as-turn15: THREE OUTCOMES IN SIX PROCESSES, and exposure is SATURATED on this map | verify | verified | high | 100% | 2026-09-09 | Lane AP, VM144. Two fresh hooks=off processes (OA, OB) on MyGameas1spydep.sav + 1 End Turn, added to lane AS's A/B/M and this lane's probes=8 run W. Pre-turn (Autosave EndTurn).sav is byte-identical across four processes, two lanes and TWO DIFFERENT BUILDS (98e45d3745b91450...). Post-turn: three distinct outcomes in six processes - 262f8bda x4 (AS's A and M, AP's W and OB), e34775a7 x1 (AS's B), e296394f x1 (AP's OA). OA != OB, so this is NOT a calibration pair and must not enter determinism-hashes.txt - stated in advance as PD3. OA vs OB diverge in 15 leaves, ALL of them Player[32]'s research pick plus /Summary/Checksum and one turnstats tch cell; three distinct picks were sampled (Overthrusting completed / DRV_NodFoc in progress / DRV_McroFus over budget). probes=8 IS BYTE-NEUTRAL, PROVEN PROPERLY THIS TIME: W is IDENTICAL to OB over 44,438 leaves (state_checksum --floats bits --mask none), while the two un-instrumented controls differ from each other - so the instrument agrees exactly with a control in a leaf class it provably does not touch. Sub-tree digests: Sim/spymgr, Sim/systems, Sim/fleets, Sim/trdmgr all IDENTICAL across the pair; only Sim/players and the derived Summary move. RULE 26(c) EXPOSURE FIELDS RECORDED NEXT TO THE HASHES (coordinator's request): the control input carries Player[32].ResTNm == '' - a pick turn - and it varied in exactly the leaves the predicate names. AND THE PREDICATE IS SATURATED ON THIS MAP: all 13 pre-turn states from Frame 14 to Frame 26 carry at least one exposure signature (9 of 13 a research pick, every single one a batch of ship completions, four a design, four a colonisation; Player[32]'s navy goes 146 -> 251 ships in twelve turns). There is NO quiet turn anywhere in this lineage, so no rung-A pair is obtainable from as-turn15-spydeployed.sav by waiting - the opposite end of the axis from lane AR's turns 38-39, and a lane that wants a pair on this map must pin the client seeds. Counter-nuance: exposure is necessary for variation, not sufficient per pair - W, A and M (three processes, two configurations, two builds) landed on the SAME bytes on this exposed turn, so a single agreeing pair on an exposed turn proves nothing. Saves ap-oa-turn16.sav, ap-ob-turn16.sav |
| predict at the SITE, not at the bracket | meta | verified | med | 100% | 2026-09-09 | Lane AP's PB4, the one prediction it got wrong, and the lesson generalises. AP forecast every spy-half draw exactly and still missed its headline number, because it had predicted the whole OnAllCombatDone_Tail total and an unrelated subsystem entered the bracket: the trade-raid roll 0x00820e1d arrived at Frame 20 and grew 2 -> 8 words as the AI's navy went 157 -> 251 ships. A bracket total is a sum over every subsystem inside it, so quoting one commits you to modelling all of them. Predict and report per site, by return address; let the bracket total be the arithmetic check, not the claim. The consolation: the surprise was itself a result - an independent live re-confirmation of lane AD's per-fleet raid model, on a different map and an AI-owned fleet. Now a corollary to rule 23 |
| PINNED SEEDS DO NOT COLLAPSE A RICH TURN - C-exact is false on ad-turn27, and the residue is ONE FLEET-ID TRANSPOSITION | control-flow | verified | high | 100% | 2026-09-09 | Lane BP, VM146, probe 2 of the 2026-09-09 resolution (findings/control-flow/pinned-seed-rich-turn.md). Build bp-989c692-20260909T0505Z, fresh CT111 tree (rule 24), lane worktree (rule 21). THREE fresh processes on ad-turn27-two-raiders.sav (1c8baa27...), one End Turn each, lane CB's CONSTRUCTOR-ARGUMENT overwrite (aiseed=pin, NOT airng.pin_seed), every template hook off BY NAME (27 reported mode=off), zero unknown keys, zero COVERAGE lines, aiseed call=1/2/3 pinned=1 in both pinned runs. THE BRIEF WAS WRONG ABOUT THE PLAYER COUNT: the save has seven non-human players and the engine builds THREE AI clients - RunAI fires exactly 3x, net ids 32/496/512; 528 Alien Menace, 544 Peacekeeper Enforcer, 560 Von Neumann and 576 Independent Colony get no client, no call and no seed. Nine distinct natural seeds across three processes, none shared (L1's per-process seeding, reproduced a 4th time on a turn-27 state). RESULT: run L (unpinned) and run A (pinned to L's observed seeds) are BYTE-IDENTICAL (e913ff41..., state_checksum IDENTICAL, 61147 leaves, coverage PROVED) - a pinned process reproduced an unpinned run of a rich build turn. Run B, same pinned seeds, fresh process, DIFFERS: 724528ff..., 35 leaves. So P1 IS FALSIFIED and READING 3 IS ALIVE: a per-process input that is NOT the seed reaches a rich turn. BUT THE PIN DID ALMOST ALL OF THE WORK. AD's UNPINNED pair on the same save differed in 94 leaves across designs, ShipRecs, Sav/Maint/PvSav/FNGNum, both AI build queues, ModCount (1496 vs 1500), NumFlts, DesignIDs[], FleetIDs[] and four turnstats rows. BP's PINNED pair differs in 35, and ALL 35 ARE ONE TRANSPOSITION: fleets 1970 "Sigma Fleet VI" and 1986 "Tau Fleet VI" exchange their entire contents (Pos, PrvPos, FtOrig, FPlan, LocID 384<->0, HFPlan, FtTrans, FtFlg 12<->13, Ship[6976]<->Ship[5264]), plus Sys[384]/Flt[1] 1970->1986, /Sim/ShipIDs[] list-reordered and /Summary/Checksum. ModCount, NumFlts, DesignIDs[], FleetIDs[], every build queue, every design, every ship record and /Sim/RNG are IDENTICAL. The AI command block says the same thing from the input side: 9 blocks / 81 elements, same (pid,list,idx) sequence, same per-list counts, and - masked by the noise set the byte-identical L/A pair PROVES (list 1 w1,5,8,11; list 3 w4-11; list 5 w8-11; list 7 w2-11; list 8 w1-11; list 10 w2-11; list 12 w5-11; list 14 w2-11; list 23 w2-11) - EXACTLY THREE WORDS DIFFER: list 8 idx1 word0 fleetId 1986/1970, list 10 idx0 word0 systemId 384/80, list 10 idx1 word0 systemId 80/384. List 10's fleet ids are in the SAME order in both (1970, 1986, 2002, 7072), so the id counter is deterministic and what varies is WHICH NEWLY FORMED SHIP GROUP EACH ID LANDS ON. Design elements identical including the name bytes ("Egg Thief Mk 4", "Bravestar Mk 3"). C-EXACT RESTATED: given (save, per-client seeds) the AI's command block is determined UP TO the assignment of newly allocated fleet ids to newly formed fleets; one residual per-process input decides that assignment and it is not the seed. Rung C is damaged, not lost - the assignment is canonicalisable the way L4's C-set canonicalises a pick. INFERENCE, NOT MEASUREMENT: the residue has the shape of a container walked in pointer order under ASLR - the reading the 09-08 resolution withdrew for the research pick and never re-tested on a build turn. RULE 26 (c): the A/B control does NOT agree with itself, so it certifies nothing; the L/A pair does, and certifies only (save, procedure, hashes) reached by two of three processes - a 1/k result, NOT an oracle pair. Exposure facts on the same line: Player[32] ResTNm=BIO_TerBac HELD, NumDes 46, NumOwn 15, 13 ships completing; 496/512 hold targets with NumOwn=0/NumDes=0; the four NPC factions have ResTNm=='' AND NumOwn==0 AND NO AI CLIENT. NEXT: one more pinned process (run C) sizes the residual outcome set; probe 3's per-client draw ledger says whether the fleet-creation sites draw at all. Artefacts: verify/results/saves/bp-{pinA,pinB}-turn28.sav, bp-turn28-pre.sav, verify/results/shim/bp/ |
| rule 26(c) qualified on its first forward use: the signature is NECESSARY, not sufficient | meta | verified | high | 100% | 2026-09-09 | Lane BP, applying the corollary published hours earlier. Four players on ad-turn27 carry ResTNm == '' and contributed NONE of AD's 94 leaves - they have NumOwn == 0, and more fundamentally the engine constructs only THREE AI clients for the save's SEVEN non-human players (RunAI fires exactly 3x, net ids 32/496/512; 528 Alien Menace, 544 Peacekeeper Enforcer, 560 Von Neumann and 576 Independent Colony get no client, no call and no seed). So an exposure signature must be read against who actually runs, not off the field alone. Corollary (c) amended in place. This is the good case of a rule being used the day it was written and coming back sharper |
| the cbpin trap fixed, and made unrepeatable | engine | verified | med | 100% | 2026-09-09 | Lane BP reported two defects in src/shim/shim.cfg.cbpin rather than working around them, and noted its own first draft reproduced one of them - the definition of a trap. (1) The header comment wrapped onto a live setting, leaving a stray aiseed.values=... with a trailing backtick AHEAD of the real one at the bottom, so the file contradicted its own "filled in per run" note. (2) Six registered hooks went unnamed, and under hooks=trace an unnamed hook defaults back ON - so a determinism probe ran six more detours than it claimed, which is exactly what rule 19 says can move the bytes. Both fixed; cbpin now names all 27. New tools/check_shim_configs.py enforces the two syntax defects across every template and exhaustiveness only where a template opts in with # exhaustive. My first draft of that check was wrong and I threw it away: it enforced exhaustiveness globally and "failed" 40 configs, because hooks=trace legitimately means "trace all but these" - the check was wrong, not the configs. Engine 1654785 |
| PROBE 1: the load->turn pair is NOT the continuation's - AR's second hash does not survive a load, and the load form is its own reproducible pair | meta | verified | high | 100% | 2026-09-09 | Lane BQ, VM145, findings/control-flow/seed-exposure-forward-test.md, probe 1b of the 09-09 resolution. Prediction committed before the run (commit be14266): ar-oracle-A-pre.sav (Frame 38, 15b99255...) + LOAD + one End Turn reproduces 7a8b3d5e.... FALSIFIED. It produces 4c356f5916f0e6e4... (107,732 B), and it produces it in TWO FRESH hooks=off PROCESSES BYTE FOR BYTE, at both snapshot points ((Autosave EndTurn).sav 33e3009254f5acd1... in both). So the load->turn procedure AGREES WITH ITSELF and DISAGREES WITH THE CONTINUATION. 23 leaves apart from AR's file and 17 of them are ONE AI FLEET: Flt[1570 "Zeta Fleet V"] (PID 32) enters turn 39 parked at Alpha Centauri with full range; in AR's continuation the AI issued it a move order (FPlan Wpt 320, FtFlg 12->13, LocID 432->0, both ships Range 9.0->7.0) and in BOTH loads it issued nothing. Plus Sys[432] NumFlts 0->1 and Flt[914 "Nu Fleet III"] HLay True/False. RULING: ar-oracle-A-pre.sav -> 7a8b3d5e... MUST NOT enter determinism-hashes.txt as a load->turn pair. What IS certified, two processes: ar-oracle-A-pre.sav -> (load, one End Turn) -> 33e30092.../4c356f59... - and that is still the standalone's first trade-raid pair (four raiders parked, no encounter). THE RESAVE CANONICALISATION IS EXONERATED: the pre-turn round trip is byte-exact but for ONE leaf, /CD[1]/NPrvVa (+5.00 across a load). CONSEQUENCE: the pre-turn save does not fully determine the next turn's AI fleet-task decisions - something Zeta Fleet V's order depended on is not in the file. Resolver case; probe 3's ledger on ar-oracle-A-pre -> 39 taken BOTH by continuation and by load names the site |
PROBE 1: turn 40 varies - but NOT by the research pick. Auto Resolve Peacefully does NOT bypass combat, and a pick with ONE candidate is not exposed |
control-flow | verified | high | 100% | 2026-09-09 | Lane BQ, VM145. ar-oracle-A-post.sav (7a8b3d5e..., Player[32] ResTNm == '') in two fresh hooks=off processes, one End Turn, Auto Resolve Peacefully in both. The files DIFFER (56980aa0... vs b36ac0ec..., 144 leaves) - the committed prediction's first clause holds, and so do /Sim/RNG identical (2,503-B block byte-equal), /Sim/trdmgr identical and every player-0 fleet identical. THE MECHANISM CLAUSE IS FALSIFIED: ZERO TechTree leaves and ZERO ResTNm leaves differ. Both processes picked BIO_GrvAdpt. The 144 leaves are a COMBAT: the AI's Sigma Fleet VII (PID 32) hit player 16's colony Venkman, the human committed no ships, and the PLANET's defences killed 2 raiders in one process and 1 in the other (50 leaves crep, 41 the AI fleet, 8 Sys[224 "Venkman"], 20 Player[16] - the prediction's "every Player[16] leaf identical" is also FALSIFIED). Both runs completed the SAME two ships (+8848,+8864); only the losses differ (7744+7952 vs 8000). So Auto Resolve Peacefully means "I commit no ships", NOT "no combat occurs" - it is not a determinism-preserving resolution, and resolution 09-08 §6's combat warning is now measured, not just retained. AR's turn-38 peaceful auto-resolve reproduced because the other side had nothing to fight with. WHY THE PICK DID NOT VARY - the producer run, shim.cfg.l4research already on the guest, no build: producers A and B were entered once per AI player and BOTH RETURNED NULL for Player[32]; it reached producer C, the candidate walk; TryResearchCandidate was offered ONE candidate and took it - airesult sel=1 candidatesTried=1 chose='BIO_GrvAdpt' - and the 0x006b36e0 & 7 fallback rotation was never reached (FallbackIndex/FallbackArm both 0). The outcome set is a singleton because the CANDIDATE LIST HAS LENGTH ONE. Not the producer gate the resolution guessed - a COUNTING criterion, and a better one |
| Rule 26 (c) needs four corrections, all measured on one lineage in one session | meta | verified | high | 100% | 2026-09-09 | Lane BQ, from probe 1. (1) ResTNm == '' is NECESSARY, NOT SUFFICIENT for research exposure: it names a turn on which a pick is MADE; exposure needs ` |
| THE PINNED-SEED RESIDUE IS A VISIT ORDER, IT IS WRITTEN INTO THE BLOCK, AND ONE RUN SEPARATES HEAP ORDER FROM A GLOBAL-RNG DRAW | meta | verified | high | 100% | 2026-09-09 | Resolver, on lane BP's falsified P1 (findings/resolutions/2026-09-09-fleet-id-order-residue.md). RULING ON BP'S RESTATEMENT: accepted and sharpened. The ids are NOT the variable - 1970/1986/2002 decode (id-allocation.md) to node 2 = Player[32]'s client, counters 123/124/125, minted in that order in every process. The variable is the ORDER in which AssignFleetsAndIssueOrders (0x006c16c0, pass 1) visits the ship groups needing a new fleet: A visited the group at Sys 384 first, B the group at Sys 80 first; 2002's group third in both. That order changes the id each group gets, the element order of lists 8/10 and the apply order (hence ShipIDs[] reordered) and NO decision on this turn. AND IT IS WRITTEN INTO THE BLOCK: list 10 = StrategyServer::OnCommand_TransferShips {systemId, fleetId, ships[]} records the assignment on every captured run, so C-EXACT SURVIVES LITERALLY with one more free input: (save, seeds, visit order) -> byte match; C-set's shape covers the uncaptured case (canonical order = one member of the outcome set). ENGINE: (1) state_checksum --relabel-new-ids <pre.sav> - bijection on that turn's new node-k ids matched by (location, ship set), master id lists as sets, Summary/Checksum masked WITH REASON, refuses to relabel anything in the pre-turn save; accept = BP's pair IDENTICAL modulo {1970<->1986}, AD's pair still DIVERGED; (2) game/ai visits groups canonically (ascending systemId, then min ship id) and mints in that order; (3) optional captured-order override for exact replay. RULING ON POINTER ORDER: not yet leading - ONE OF TWO. The record already names two unnamed AI-band consumers of the per-process global generator 0x00af6e58 (0x0079ea90+0x73 Chance, 0x005b9f00+0xc0; AI1's list) which the seed pin does not touch. What moved: pointer order now has a MEASURED PRECONDITION - BP's aielem dumps carry node heap addresses, and runs A and B (identical config, identical decisions) show the same run of consecutive same-size allocations in DIFFERENT relative address orders (list 3: A idx1<idx0, idx3<idx2; B idx1>idx0, idx3>idx2) - Win8+ LFH randomisation, live in this process. Precondition, not cause. Wall clock has no mechanism (AIProcessMinTime is a trailing Sleep; AI turn is synchronous in OnResumePlaying). THE PROBE (lane BR, VM146, one fresh process, no build, ~12 min): BP's shim.cfg.bppin + airng=on (pin_seed off) - the OnResumePlaying bracket attributes EVERY draw on EVERY generator in each client's turn by return address and generator (airngcensus rows, foreign_words/foreign_calls); lane I's inlined-draw inventory has NO site in the AI band so the observer is complete there; airng is measured behaviour-neutral (roll-parity.md). It is also run C. PREDICTIONS: P1 autosave in {e913ff41, 724528ff}; P2 foreign_words=0 in all three brackets, no census row on 0xaf6e58 or Seed record #2; P4 aiorders diff = noise set + at most the transposition. Foreign draw -> its retaddr names the consumer, follow-up = global pin (RNG_Seed(0xaf6e58,K) after Initialize) x2, predict identical. None -> RNG excluded by measurement; stage 2 = one static read of 0x006c16c0 + the gathering hub TO PLACE A HOOK on the group walk, log (element addr, systemId, minted id) in visit order x2 processes, predict order = ascending address. RUNG C restated (backlog text in §6); RUNG B not restated - value confirmed, replay is insulated by construction, and a aiorders.deep=on run of BP's pinned procedure landing on e913ff41 is the first rich-turn record. RULE 26(c): BP's wording corrected - the measured fact is "no client", NumOwn==0 is NOT shown to gate (496/512 have NumOwn==0 AND clients). NEW COROLLARY 26(d): when the instrument cannot be removed, pin to a natural run's observed values and require byte-identity with it before reading a pinned pair; the agreement is the pin's control AND a noise mask. No rule 29. STRUCTURAL FIX for BP's shim.cfg trap: an unnamed registered hook under a pin/capture config is a config error; print installed/registered counts. VM144 held for stage 2 / the global pin |
| MY OVER-READ of lane BP, corrected by the resolver | meta | verified | med | 100% | 2026-09-09 | I amended rule 26(c) with "a player that has an AI client and something to decide with ... because they have NumOwn == 0". The NumOwn == 0 half is not measured. The measured fact is only that the four NPC factions get no client, so their ResTNm == '' is inert. Players 496 and 512 have NumOwn == 0 AND have clients - they held research targets, so the pick was not exercised either way, and whether a client with NumOwn == 0 reaches the pick is untested. I read a gate into a correlation in a rule I was in the middle of correcting for over-claiming. Wording replaced with the resolver's |
| RNG IS EXCLUDED BY MEASUREMENT FOR THE AI VISIT ORDER - the global generator is drawn THREE TIMES IN A PROCESS, all three to mint the AI client seeds | control-flow | verified | high | 100% | 2026-09-09 | Lane BR, VM146, the single probe of findings/resolutions/2026-09-09-fleet-id-order-residue.md section 4 (findings/control-flow/ai-visit-order-probe.md). No build: lane BP's dist bp-989c692-20260909T0505Z, shim.cfg.brprobe = shim.cfg.bppin with EXACTLY THREE non-comment lines changed (airng=on, airng.pin_seed=off, airng.out=), diffed twice - once file-to-file on the guest, once from the two runs' runtime config: lines, both giving the same three lines. 28 of 28 registered hooks off BY NAME, zero unknown keys, zero COVERAGE lines, aiseed call=1/2/3 pinned=1, no wildcard hit, airng: bracket ... MH_OK + pin_seed=off. Route: LOAD. RunAI net-id list: 32/496/512 (three clients; the four NPC factions get none) - confirmed a second way by the bracket firing 5x (pid 16 human agent=0, then 32/496/512 with agents, then 16). P1 HOLDS: (Autosave).sav = e913ff41..., BYTE-IDENTICAL to lane BP's runs A and L (state_checksum IDENTICAL, root 8100b289..., 61147 leaves; vs BP's B, the same 35-leaf transposition, leaf for leaf; pre-turn 16f63adb... IDENTICAL). Not a third file, so the resolver's ambiguity caution does not fire - AND this is the airng detour's own rule-26(d) control: an instrumented process reproduced an unpinned natural run byte for byte, so airng's behavioural neutrality is now MEASURED on this state, not argued from roll-parity.md. P2 HOLDS, AND HARDER THAN PREDICTED. foreign_words=0 foreign_calls=0 residual=0 in ALL THREE AI brackets (pid 32: left_delta=79 observed=79 calls=75 sites=4; pid 496 and pid 512: left_delta=0, observed=0 - they run a full AI turn and DRAW NOTHING, rule 20's design working). left_delta == observed on pid 32 so there is NO INLINED DRAW on the AI path either, confirming lane I's inventory from the other side. AND THE LIFETIME CENSUS CLOSES IT: the per-process global generator 0x00af6e58 (runtime 0x00cd6e58) was drawn EXACTLY 3 TIMES, 3 WORDS, FROM ONE RETURN ADDRESS, va=0x00784f9e, entry NextUInt - and the count is 3 at every seq from 2 to 5, i.e. it does not move during the turn at all. 0x00784f9e is INSIDE StrategyNetworkClient::OnMessage 0x00784640 and sits 16 BYTES BEFORE the RunAI call site that lane AI1 recorded at OnMessage+0x96e = 0x00784fae, and it fires exactly three times for exactly three AI clients at the same log timestamps as the three aiseed call= lines: it is the AI client seed mint - board row 326's mechanism, measured live with its consumer address, generator address and call count, and it is the ONLY thing that generator does in the whole process. (Inference on the identification, one instruction from a measurement; this lane was instructed not to read the binary.) The two unnamed AI-band consumers the resolution named as H2 candidates (0x0079ea90+0x73, 0x005b9f00+0xc0) DO NOT APPEAR IN THE CENSUS AT ALL. VERDICT: H2 IS DEAD. H1/H1b (an address-keyed container or an uninitialised-word comparator on the fleet-assignment path) IS THE ONLY HYPOTHESIS LEFT. P4 HOLDS in its strong form: under BP's L/A-proven noise mask (plus unset aiblk gate payloads, lane L4's class), BR vs run A = 9 blocks / 81 elements / identical key sets / identical per-list sizes / identical SET gates and NOT ONE differing word outside the noise set; BR vs run B = EXACTLY the three transposition words (list 8 idx1 w0 1986/1970, list 10 idx0 w0 384/80, list 10 idx1 w0 80/384). The comparator was self-tested against BP's published A/B and L/A results first. P3 FALSIFIED (it was labelled not load-bearing): pid 32 spent 79 words, not single digits - rule 23's corollary again, a bracket total is a sum over every subsystem in it. INCIDENTAL, and it is a rule-28 practice-4 case: the 3-40-turn fuse consumer 0x0069dbb0 DOES draw - on the LOAD path, before the bracket, on each client's own (pinned) generator (census 0x0069dc02 and 0x0069dc2e: 2+1 words for client 32, 1+1 for 496 and 512, all inside life_in). Its turn-time behaviour is still untested. FREE MEASUREMENT FROM THE DUMPS: section 2.2's LFH precondition now holds on FOUR processes (list-3 node addresses in four different relative orders for an identical allocation sequence - including between L and A, whose autosaves are byte-identical), AND a negative that matters for stage 2 - the emitted command nodes' relative address order does NOT track the visit order (BR and A share a visit order and differ in node order; BR and B share a node order and differ in visit order), so the block cannot substitute for the container hook. NEXT (unchanged, now unblocked): one bounded static read of AssignFleetsAndIssueOrders 0x006c16c0 + the gathering hub TO PLACE A HOOK on the group walk, log (element addr, systemId, minted id) in visit order, x2 processes. Artefacts: verify/results/shim/br/ SCOPED 2026-09-09 (lane BS, independently and on a DIFFERENT lineage): the three-words-one-site measurement reproduces exactly (calls=3 words=3, same return address, whole process). BUT "only mints seeds" holds only on a turn with NO ENCOUNTER. On the AR continuation's turn 38 - a deep-space encounter auto-resolved peacefully - the same generator ends the process at 87 words: the 3 seed words plus ~80 Chance at 0x006fb6a1 and two NextInt pairs. Those 84 words fall between the client sweeps, in an interval containing the encounter, and were NOT bracketed, so attributing them to the encounter is a reading, not a measurement. H2's verdict is untouched - the question is whether a foreign draw fires INSIDE an AI bracket, and foreign_words=0 in 14 of 14 brackets across BS's two runs as well. Only the summary sentence needed the scope. Rule 28's shape, applied to a row I wrote. |
| THE FIRST REPLAYABLE RICH-TURN RECORD - a deep AI command block for ad-turn27, on the 80-first branch | control-flow | verified | high | 100% | 2026-09-09 | Lane BR run 2, VM146, the Rung B record of findings/resolutions/2026-09-09-fleet-id-order-residue.md section 6 (findings/control-flow/ai-visit-order-probe.md section 4.2). shim.cfg.brdeep = shim.cfg.bppin with ONE non-comment line changed (aiorders.deep=off -> on); airng stays off. Verified: aicfg words=12 deep=1 research=0 seed=pin pins=4 detours=2, three pinned=1, zero unknown keys. The record: (ad-turn27-two-raiders.sav 1c8baa27..., LOAD -> one End Turn, seeds 32=156ebbbd/496=fe7b2826/512=0ed341d1, build bp-989c692-20260909T0505Z, visit order = list 10's order in the captured block) -> pre 16f63adb..., post 724528ffde9a7514b69ab9e82ed3b8c8ade63d622c0720de30503a73a92df11e - BYTE-IDENTICAL to lane BP's run B (cmp, not just the hash). The resolution's stated target was e913ff41...; it landed on the OTHER branch and that costs the record nothing, because the residual input is written into the block (section 6's own logic): list 10 records the visit order on every captured run. THE DEEP DUMP FOLLOWS EVERYTHING IT WAS SUPPOSED TO: 35 aivec rows and 3 aistr rows. List 10's ship vectors - idx0 (80,1970)<-[5264], idx1 (384,1986)<-[6976], idx2 (384,2002)<-[6992], idx3 (816,7072)<-[11 ship ids]. List 8's routes - (2002)->[256], (1970)->[384], (1538)->[240]. Plus list 1's names as strings ("Egg Thief Mk 4", "Egg Thief Mk 5", "Bravestar Mk 3"), list 3's and list 12's vectors (12 elements resolving 3/15/1/5/4/6/20/22/11/7/1/7 ship ids). Under BP's noise mask the deep run's shallow words are ELEMENT-FOR-ELEMENT IDENTICAL to BP's run B, so deep=on is additive and does not disturb the block. TWO CROSS-CHECKS NOW POSSIBLE FOR THE FIRST TIME, BOTH PASS: (1) the block's ship<->fleet pairing matches the save it produced - list 10 gives fleet 1970 <- ship 5264 at system 80, and bp-pinB-turn28.sav is exactly the save where Flt[1970] holds Ship[5264]; the resolution's "the visit order is written into the block" is now verified END TO END, block -> save. (2) The list-8 move goes to the group at system 80 in BOTH orders (fleet 1970 with route [384] here; fleet 1986 with the same route in run A - both are the system-80 group), which confirms the resolution's section 3 qualification from the command stream instead of by inference. OUTCOME SET AFTER FIVE PROCESSES ON THIS STATE: e913ff41... x3 (BP's L unpinned, BP's A pinned, BR's probe pinned+airng, all 384-first), 724528ff... x2 (BP's B, BR's deep, both 80-first). NO THIRD FILE IN FIVE PROCESSES, ACROSS THREE INSTRUMENT CONFIGURATIONS, with both members reproduced at least twice - the residue is a small enumerable order, which is what C-set's shape needs. Not a proof that k=2. Artefacts: verify/results/shim/br/BR2-deep-aiorders.txt, BR2-shim.log, shim.cfg.brdeep; the save is byte-identical to verify/results/saves/bp-pinB-turn28.sav and is deliberately not duplicated |
| CORRECTION: the raid-intercept species gate has THREE short-circuits, and HIVER is in two of them | control-flow | verified | high | 100% | 2026-09-09 | Lane AZ, static, findings/control-flow/raid-intercept-species-word.md. The campaign's published sentence - lane AR section 4.2, lane AG section 5.2, and my own brief - is "a Human or Zuul raider draws nothing; every other species draws a word". Incomplete on both sides of the relation. FUN_00820af0 short-circuits on THREE disjuncts, decoded to the next function start: SC1 cmp [routeOwner+0x5c],1 at 0x00820bc2 - the ROUTE OWNER's Species, so a HIVER-owned route is intercepted with no draw WHOEVER raids it; SC2 cmp eax,1 at 0x00820bd7 on StarFleet_GetCrewSpeciesForIntercept(raider) - a HIVER raider draws nothing, and this is an ordinary == 1 test with nothing to do with SpeciesDef+0x144; SC3 SpeciesDef_HasInterceptFlag at 0x00820bed - Human (0) and Zuul (5) only, which is the one the campaign had. So 0x00820c1b draws only for a raider of species 2 Tarkas, 3 Liir, 6 Morrigi (or 4 _NPC) against a route owned by a non-Hiver. AND THE POLARITY IS INVERTED IN BOTH PUBLISHED RENDERINGS: the short-circuit arm sets frac = 0.0, not 1.0 (fldz; fstp [ebp+0xc] at 0x00820bf8; the draw arm computes 0.0 + (1.0-0.0)*NextFloat()), so a Human/Zuul/Hiver interception happens at the raided route's SOURCE end. Word counts unaffected; the sentence is wrong (rule 28 practice 6 - read the polarity before the count). Species enum named from the image: SpeciesDef_InitTable 0x005453a0 dispatches through the jump table at 0x00545b60, whose seven entries READ AS BYTES give 0 Human, 1 HIVER, 2 TARKAS, 3 LIIR, 4 _NPC, 5 Zuul, 6 MORRIGI - the case bodies are NOT laid out in case order, so reading the disassembly top-to-bottom gives the wrong enum. This agrees with lane AP's independent decode of the save's ISsp list on all seven. And the tech data closes SC2 for players: CCC_ComRaid is 0% for Hiver on both of its edges (CCC_FTLBrdB->CCC_ComRaid and CCC_FTLEcon->CCC_ComRaid), so a Hiver empire can never raid and SC2 is unreachable through vslot13 for any player |
| FIRST TARKA SAVE IN THE CORPUS - and two AI raiders are already parked on sector nodes | verify | verified | high | 100% | 2026-09-09 | Lane AZ, VM141, verify/results/saves/az-turn23-tarka-comraid.sav (3d5eb826..., 89,285 B, turn 23, --strict 0 error 0 warn, resyncs 0, hint-failures 0). A fresh 2-player custom game with the species pool cut to Tarkas only (lane V's Zuul trick), so both empires are species 2 - the first playable non-Human, non-Zuul empire the campaign has. CCC_FTLBrdB came free in the 15 starting techs, CCC_FTLEcon took 4 turns and CCC_ComRaid 3 more at Research Efficiency 150%, so Commerce Raiding by turn 10. Three AI trade routes (tro 32, trfr 4, 27, 27). FIVE AI TARKA FLEETS ARE PARKED ON TRADE-SECTOR NODES WITH Pos BIT-EQUAL, and two of them clear G0-G4: Rho Fleet (274, 6 ships, LocID 768, FtFlg 0x80c) and Escorts (3344, 6 ships, LocID 752, FtFlg 0xc00). The other three are rejected at G3c exactly as lane AG's decode says - tsct bit 1 is set on 752 and 800 (the AI trades in its own sectors) and they lack FtFlg & 0x800; Escorts sits on the SAME NODE as Freighters 3248 and differs only in that bit. COMMITTED PREDICTION, one probes=8 run to settle: k = 2 on this save with the player owning NO fleets, so Slot13RngCalleeA is entered exactly twice per turn and 0x00820e18 reads calls == words == 2. B will still cost 0 - every route is tro 32 and the qualifying raiders are owned by 32, so PlayerAlliances_Relation returns 3 for self and G_B3 empties the candidate list |
tscr IS NOT A CORPUS CONSTANT - it reads 127 on a fresh map, with the AI's bit SET |
control-flow | verified | high | 100% | 2026-09-09 | Lane AZ. Every one of the 30 earlier corpus saves reads Trade[].tscr = 252 or 253, and lane AC watched bit 0 flip 252->253 the turn Commerce Raiding completed; lane AG's section 3.3 reads the mask as "the NPC slots default set, your bit is the tech". On the first map this lane generated, all five sectors read 127 - seven players instead of eight, and bit 1, the AI's, is SET as well as bit 0. Either the Tarka AI researched Commerce Raiding, or the default for a non-human empire differs from what the AC lineage showed; the writer of TradeSector+0x90 is still unfound and no guess is offered. This is rule 28 practice 3 landing on the lane that quoted it: my own committed prediction P3 said a Tarka AI never rolls because its tscr bit is clear, which is true of 24 saves from one lineage and false on the first new map. The COUNT was right and the REASON was wrong |
| WORKLOAD BLOCKED, and the failed conjunct is G_B3 not the species gate | control-flow | in-progress | — | 60% | 2026-09-09 | Lane AZ. z - the NextFloat at 0x00820c1b charged to Slot13RngCalleeB - is still unobserved, and this lane did not run an instrument at all (hooks=off throughout, no shim.cfg.hp8 ever copied). What it built is a state that satisfies SC1, SC2, SC3 and G0-G4 and fails only G_B3: the qualifying raiders are the AI's and every route is the AI's own, so PlayerAlliances_Relation returns 3 for self, B rejects on > 0 and returns at G_B4 with 0 words. THE CHEAPEST NEXT WORKLOAD IS NAMED FROM THAT CONJUNCT AND NEEDS NO FLEET MOVEMENT AT ALL: give player 0 a trade route sourced at Ku'Paaka (index 17) or Ka'Palum (index 26), both player-0 colonies and both members of sector 768, where the AI's Rho Fleet is already parked and already clears G0-G4. Then a success for Rho Fleet puts a player-0 route in B's candidate list at relation 0 = war, 0x0088b613 fires, and FUN_00820af0 runs with a Tarka raider against a Tarka owner - all three short-circuits fail and z draws. Two failures worth not repeating: the 16-destroyer raider fleet was destroyed staging through Ku'Valt, a sector-752 MEMBER SYSTEM that is an AI colony (order to the NODE, never through a member), and a blind "auto-resolve peacefully" click sequence does not resolve a real battle - peaceful is not offered when both sides have ships |
GUEST GOTCHA: rdrag ROTATES the star map, and that is how you aim at a crowded sector node |
meta | verified | high | 100% | 2026-09-09 | Lane AZ, VM141. Lane AR's recipe - fit screen = (A*wx+B*wz+C, D*wx+E*wz+F) from two known points and land on any node first try - does not generalise to a crowded map. On lane AZ's map the two route-sourcing sector nodes lie 2.4 ly from Ke'Sothram and 2.6 ly from Ka'Sogroth, the Move-mode hover snaps to the nearest system over a radius of well over 100 screen pixels, and twelve grid probes all named a system. The offset from star to node points nearly along the camera axis and foreshortens to ~30 px however far you zoom in. THE FIX IS ONE GESTURE: rdrag on the star map ROTATES the camera (verified, reversible, the whole field re-projects). Left-drag does NOT pan, the arrow keys do nothing, and the camera does not follow the selection - it moves only when the game moves it (a combat, a fleet order). Also: wheel DOES zoom, on the star map AND the research tree, toward the screen centre - board row 374's "wheel-zoom does nothing useful" is true of PANNING the tech tree, not of zooming, and wheel x y -120 3 is what made the top-row CCC nodes clickable when the Research title bar and Savings panel were sitting on top of them. A research node under a HUD panel is not clickable and the click is eaten silently - no preview appears and nothing tells you why |
| MY BRIEF TO LANE AZ NAMED THE WRONG GATE | meta | verified | med | 100% | 2026-09-09 | I told AZ that 0x00820c1b's only species short-circuit was SpeciesDef+0x144 and that five species qualify for the z word. Both halves wrong, and AZ found it statically before touching a guest: there are three short-circuits, and HIVER is in two of them - once as the route owner, once as the raider's crew, the latter a plain cmp eax,1 with nothing to do with SpeciesDef+0x144. Four species qualify, not five. I inherited the incomplete sentence from lanes AG and AR and passed it on as fact in a brief whose whole purpose was to test that gate. The lane checked the gate it was sent to test rather than trusting the brief - which is the behaviour the brief asked for and the reason nothing was measured wrongly |
| THE PER-HISTORY RESIDUE IS A TASK THAT THE SAVE DOES NOT CARRY - a Colonize task alive on turn 39 by continuation and ABSENT after a load, measured end to end | control-flow | verified | high | 100% | 2026-09-09 | Lane BS, VM144, findings/control-flow/per-history-residue.md. Predictions committed before the build was deployed (commit 15c4471). Two instrumented runs of the AR lineage to turn 39, aiorders=on + 16 aiprobes + the airng OnResumePlaying bracket, no pin, all 27 template hooks off by name. INSTRUMENT NEUTRALITY CHECKED AT k = 1 ON BOTH ROUTES AND PASSED BYTE FOR BYTE - continuation 15b99255.../7a8b3d5e... (lane AR's three hooks=off processes), load 33e30092.../4c356f59... (lane BQ's two). THE ANSWER: AITColonize::Execute is entered TWICE (once per pass) in the continuation and ZERO times after a load, while RunTaskList is 6 in both (3 AI clients x 2 passes), AcquireFleetsForTask is 12 in both and six of the eight task Execute entries are identical. It is the FIRST task in pid 32's list on both of the continuation's turns, and it is the task that reaches the pass-1 emission exits: AssignFleetsAndIssueOrders 2 vs 0 and IssueRouteForFleets 2 vs 0 - the fleet-gathering hub is NOT REACHED AT ALL on the load route. Consequence in the block: the continuation's turn-39 pid-32 block carries list=8 idx=0 ints=[ 1570 ... ] (Zeta Fleet V) and one extra list=12 element [ 914 ... ] (Nu Fleet III), and the load's block carries NEITHER - list 8 is entirely absent. EVERY ONE OF THE 23 DIVERGING SAVE LEAVES HAS A BLOCK-SIDE COUNTERPART (15 on Flt[1570], 3 on Sys[432], 2 on Flt[914], plus ModCount/Checksum/NPrvVa), so nothing diverged in the SERVER's application of the block and candidate 3 (server-side state) is disfavoured. RNG IS EXCLUDED BY MEASUREMENT: foreign_words=0 foreign_calls=0 residual=0 in 14 of 14 brackets across both runs, and the per-process global generator 0x00af6e58 took ZERO words inside any AI bracket (it moved 3 -> 87 words across the continuation's turn 38, entirely OUTSIDE the AI brackets - that is the combat turn). Per-client words turn 39: pid 32 = 4 (continuation) vs 5 (load), 496 and 512 = 0 on both; the ONE extra word is in the LOAD, a Chance at ret_rva 0x0029086f inside AITBuildPoliceShips::Execute - downstream of the task difference, not its cost. Pre-turn states differ in EXACTLY ONE LEAF, /CD[1]/NPrvVa +5.00 (BQ's correction re-measured under an instrument). VERDICT: resolution 09-09 §9.2 candidate 1 SURVIVES and is now measured rather than inferred; candidate 2 (stream position) does not explain a task that is not there (and the routes DO differ in stream position, so this run does not exclude it on that ground - the seed-insensitivity of both routes does); candidate 3 is disfavoured. FALSIFIER, one lane-hour: hook the task vector at RunTaskList entry and log {count, per-element vtable} for pid 32 on both routes - under this verdict the continuation's list holds a colonise task at turn-39 entry and the load's does not, and the counts differ by exactly one. SCOPE CONFIRMED: every byte-match claim in every rung is a claim about a load->turn procedure, and that is the definition of the comparison, not a defect to fix |
| TWO PREDICTIONS FAILED, BOTH BECAUSE THE LOAD ROUTE DOES LESS THAN ANYONE EXPECTED - and one block difference nobody predicted | meta | verified | high | 100% | 2026-09-09 | Lane BS, from the same runs. (1) P1's list-14 clause FALSIFIED: the continuation's turn-39 block has NO list 14 AT ALL - list 14 is emitted on turn 38 (six elements, for the two fleets minted there), and on turn 39 IssueRouteForFleets is entered twice and emits nothing. So "a list-8 element and its list-14 pair" was wrong about the shape of a move order. (2) P2's "probe 11 is entered in BOTH routes" FALSIFIED: AssignFleetsAndIssueOrders is entered 2x in the continuation and 0x in the load. The brief and I both expected the load to run the same machinery to a different answer; it does not run the machinery. (3) UNPREDICTED AND IT STRENGTHENS THE RESULT: the block ALSO differs by one list=12 element, fleet 914, which is the counterpart of the OTHER save-side divergence (Flt[914 "Nu Fleet III"] HLay False->True, Lay only-in-load). I predicted half the block difference and the other half was there waiting - which is what turns "the AI emitted a move order" into "every diverging leaf is in the block". List 12's elements are fleet ids (274 290 306 322 626 642 786 882 [914] 930 1746 2338); its meaning is NOT decoded and the causal direction is NOT established (rule 20) - one watchpoint would settle it |
| A TURN THAT MINTED TWO NEW FLEET IDS DID NOT VARY - four processes, byte-identical, on a turn whose visit order was exercised | control-flow | verified | med | 100% | 2026-09-09 | Lane BS, VM144, a by-product of the continuation run. Turn 38 of the AR lineage IS a fleet-assignment turn: AssignFleetsAndIssueOrders entered 6x, list 10 carries four {systemId, fleetId} elements (384,2466) (384,2178) (336,2482) (384,2338), and decoding by `id = (counter << 4) |
THE AI'S FLEET-VISIT ORDER IS AN ADDRESS-KEYED CONTAINER AND THE KEY IS THE FLEET'S Location* - H1 CONFIRMED, H1b DEAD, and the campaign's two-file outcome set IS the two orderings of two heap pointers |
control-flow | verified | high | 100% | 2026-09-09 | Lane BU, VM146, stage 2 of the chain lane BR unblocked (findings/control-flow/fleet-visit-order-mechanism.md). Build bu-55408a9-20260909T071101Z, fresh CT111 tree (rule 24), lane worktree (rule 21), shim.cfg.bupin = shim.cfg.bppin with EXACTLY THREE non-comment lines added (aivisit=on/.out/.ships), diffed against the real bppin PULLED OFF THE GUEST and quoted in the finding, marked # exhaustive so tools/check_shim_configs.py ENFORCES that all 27 registered hooks are named. THREE fresh processes, route LOAD, seeds pinned to lane L's observed values; 28 of 28 hooks off, zero unknown keys, zero COVERAGE, 3/3 pinned=1 in every run, three aivisit ... MH_OK in every run, exe base=0x005e0000 for the 8th process on this guest. THE HOOK: AssignFleetsAndIssueOrders 0x006c16c0 walks a std::vector<StarFleet*> by index (cursor [ebp-0x38], _Mylast REFETCHED every iteration, back-edge jne 0x6c17e0 at 0x006c21be), and that walk is BRACKETED BY TWO LOOPS OVER THE SAME VECTOR that call one function each - StrategyAIAgent::ClaimShipsOfFleet 0x006a4290 and ReleaseShipsOfFleet 0x0069da10, which have EXACTLY ONE CALLER EACH IN THE WHOLE IMAGE and it is that function. So the instrument is two ordinary function-entry detours plus a bracket on 0x006c16c0, with NO return-address filter and NO mid-function patch - the loop body could not be patched anyway, its first instruction 0x006c17e6 IS a branch target. RESULT: the visit order is NOT ascending element address (0 of 6 multi-element sequences over three processes), NOT ascending fleet id (0/6), NOT ascending systemId (0/6), NOT ascending minimum ship id (0/6). It is ASCENDING fleet->Location (StarFleet+0xa0): 6 OF 6, with the fleet pointer breaking the one tie (two fleets at system 384 share one interned Location object and are ordered by element address within it). So the container is address-keyed and the comparator is lexicographic in (Location*, StarFleet*), BOTH heap addresses. P3 - the committed prediction, "the visit order is ascending element address" - IS FALSIFIED, and the hypothesis class it tested is CONFIRMED with a sharper key. THE CROSS-PROCESS PROOF, and it does not rest on one sample: three processes visited THE SAME SIX FLEETS IN THREE DIFFERENT ORDERS (run1 1970/1602/1986/354/1570/1346; run2 1602/1970/1986/354/1346/1570; run3 1970/1986/1602/354/1570/1346) and EVERY difference is a pair whose Location addresses exchanged relative order. Runs 1 and 2 wrote BYTE-IDENTICAL autosaves and still visited in different orders - the reordered pairs were pre-existing fleets, which is why the save did not move. AND THE BRANCH FLIP IS MEASURED: run 3's loc(system-80 group) = 0x333ed4c8 < loc(system-384 group) = 0x333ed710, the OPPOSITE of runs 1 and 2, it visited system 80 first, and its autosave is 724528ff... BYTE-IDENTICAL (cmp) to lane BP's run B - while runs 1 and 2 have loc(384) < loc(80), visit 384 first, and are BYTE-IDENTICAL to BP's runs A and L. So the campaign's two-file outcome set on ad-turn27 IS the two orderings of two heap pointers. H1b (an uninitialised-word comparator) is EXCLUDED for this path: an uninitialised key cannot reproduce a strict total order by a specific live pointer field in 6 of 6 sequences and cannot track that field across a branch flip. A SECOND FINDING THE STATIC READ HAD MISSED: the newly minted fleet ids are ALREADY ON THE FLEET OBJECTS when the head loop first touches them (fid=1970 on the element at idx=0 of the first burst, before the pass gate and before any emission), so AssignFleetsAndIssueOrders INHERITS the id-to-group pairing rather than deciding it; the mint is upstream in the gather (AcquireFleetsForTask 0x006ceef0 + 0x006abf80/0x006b7c90/0x006cb310), and since the delivered vector is Location-ordered and the ids follow that order, the natural reading is that the SAME container orders the mint (INFERENCE - the gather was not hooked). ENGINE CONSEQUENCE, and it MOVES deliverable 2 by one level: the canonical order must be imposed WHERE THE GROUP SET IS GATHERED AND THE IDS ARE MINTED, not at the assignment step, or the engine canonicalises the emission order while leaving the minting order to its own container. On the choice of canonical order this lane has evidence rather than convention: the original's order is NOT ascending systemId and NOT ascending minimum ship id in ANY of the three processes, so THERE IS NO ORIGINAL ORDER TO MATCH and (systemId, min ship id) is as good as any - keep it, and keep the tie-break, because the original's own tie-break is a second pointer. RULE 26(d) AND RULE 19, BOTH MET BY MEASUREMENT: runs 1 and 2 are BYTE-IDENTICAL to lane BP's UNPINNED run L, so a process carrying a seed pin PLUS three new detours inside the AI's fleet-assignment path reproduced a natural run byte for byte, twice; run 3 reproduced BP's other branch byte for byte. aivisit is behaviourally neutral on this state, measured, not argued. P1/P2/P4/P5 all hold; under BP's noise mask every run's block differs from BP's same-branch run in NOTHING outside the noise set and from the other branch in EXACTLY the three transposition words. Rule 17 discharged: 0x006c16c0's body is 0x006c16c0-0x006c247a ret with a FOUR-ENTRY JUMP TABLE living PAST the ret at 0x006c247c (target of jmp [eax*4+0x6c247c] at 0x006c22fb). Six more per-process AI seeds, none shared - L1 reproduced a 6th and 7th time. Artefacts: verify/results/shim/bu/; the three output saves are byte-identical to bp-pinA-turn28.sav x2 and bp-pinB-turn28.sav x1 and are deliberately not duplicated |
| the fleet-gather ordering contract is BOUND IN THE ENGINE, not just recorded | engine | verified | high | 100% | 2026-09-09 | Lane BU's finding turned into a thing the engine cannot get wrong later. game/ai takes fleetId as a parameter - it emits orders and does not form fleets or mint ids - so the gather/mint subsystem does not exist yet, and building it now to satisfy a contract would be speculative. Instead the requirement sits in src/game/ai/orders.h at the emission layer where the gather will attach, with the three consequences that bind: (1) order the groups by a key that is a function of the SAVE and mint in that order, (systemId, min ship id) with a total tie-break - and say plainly that there is NO original order to match (the original's is neither, in any of three processes), so we are choosing one member of its outcome set, not recovering a hidden rule; (2) impose it AT THE GATHER, not at assignment - the fleets already carry their ids when the assignment walk first touches them, so canonicalising there is one level too low and fixes nothing; (3) a byte-exact replay needs the original's order and the block already carries it - list 10 records {systemId, fleetId, ships[]} per group in visit order, so the optional override is three words per new fleet, the same cost class as pinning a seed. Lane BU's aivisit instrument merged; engine 45b16ef, host 59/59 and shim both green EXTENDED 2026-09-09 by lane BT, from contact with the data: the contract needs a fourth consequence. FtName is an ID-ATTACHED LABEL - a Greek ordinal minted by the same pass as the id - and its counter is PER PLAYER and SHARED WITH THE SERVER'S FLEETS: in AD's pair, the run that minted two client fleets names a node-0 fleet "Upsilon" and the run that minted three names the same fleet "Phi". So the canonical gather order determines fleet NAMES too, across a boundary that has nothing to do with the AI. Added to orders.h. |
| STATE_CHECKSUM --relabel-new-ids: BP'S PINNED PAIR IS IDENTICAL MODULO ONE TRANSPOSITION, AD'S UNPINNED PAIR IS REFUSED AND STILL DIVERGES | tooling | verified | high | 100% | 2026-09-09 | Lane BT, HOST ONLY (no VM; 140 untouched, 141/144/146 held by AZ/BS/BR and not approached). Implements the 2026-09-09 fleet-id-order resolution §3 item 1 (findings/subsystems/relabel-new-ids.md). state_checksum.py POST-A POST-B --relabel-new-ids PRE computes the ids NEW in each post-turn save, restricts to node nibble != 0 (client-minted; id-allocation.md id=(counter<<4) |
| RUNG B ON A RICH TURN: THE REPLAY RUNS AND CONSUMES THE WHOLE STREAM; 1092 LEAVES, THIRTEEN NAMED SUBSYSTEMS, ZERO UNGROUPED | engine | verified | high | 100% | 2026-09-09 | Lane CV, HOST ONLY (no VM; 140/141/145/146 untouched). First replay of a RICH turn's command stream: lane BR's deep block (BR2-deep-aiorders.txt) converted to .tcb and replayed by sots_turn --turn-commands against ad-turn27-two-raiders.sav (1c8baa27...), compared with bp-pinB-turn28.sav (724528ff... = BR's own output, cmp-verified). VERDICT: OUTCOME 3 -- a named leaf list, not a byte match. state_checksum --floats bits --mask none = DIVERGED: 1092 leaf difference(s); the do-nothing baseline (input vs target) is 1166, so the turn+replay CLOSES 80 and REGRESSES 6 (never netted). The stream's entire contribution to the state is ONE leaf: /Sim/ModCount 1430 -> 1500 against a target of 1502 (85 commands = 81 elements + 4 gates, 70 bumps charged = 85 - the 15 list-23 population commands the cost table charges 0; 2 driver bumps; residual 2). THE RESIDUAL IS LOCALISED: the same binary on the CANONICAL pair (turn2-state.sav + lane CB's tcb) puts ModCount on 12 -> 24 EXACTLY, zero residual, matching turn3-state.sav and confirming CB's own prediction -- so the cost table is right on lists 3/5/8/10/14/23, and the missing 2 must sit in lists 1 (3 elements), 7 (2), 12 (12), the only three the rich turn adds. The only UNIFORM per-element explanation is list 7 (colonisation) at 2 bumps per command (3 does not divide 2, nor does 12) -- a constant fitted to ONE observation (rule 23), so it is a PREDICTION with a named cheap falsifier: any capture with a colonize count other than 2. GUARD SET, ALL SIX RUN (rule 1): (a) 85/81 matches BR's block element-for-element and per-list; (b) the DEEP payload provably reached the replayer -- shallow vs deep captures of the same run move exactly 7 elements from incomplete to declined (list 8's 3 routes, list 10's 4 ship vectors), i.e. on those seven the gap is now the ENGINE not the record; (c) --replay-count-only produces a BYTE-IDENTICAL save, confirming leaves written by commands 0; (d) deleting one elem line makes the reader REFUSE with exit 2; (e) replaying against another board warns; (f) output != input and != the no-stream output. TWO BLOCKERS PUT OUTCOMES 1 AND 2 OUT OF REACH BEFORE THE TURN RUNS: (1) the engine's typed writer is not byte-exact on this save -- --roundtrip DIFFERS by exactly 12 bytes = one item, at 0x3f02: struct ProjectName reads usnc as a count of ONE item per element (a.any("usc"), commented "usnc is 0 in every save available") but the wire pair is usp THEN usc, and this save has one record with usnc == 1. The generated schema already lists both. 12 of 43 corpus saves fail the same way; mars_stream_save and app_turn fail on it in ctest (57/59, pre-existing, denominator intact). Surfaces as /Sim/sprjs/usp: only-in-B. NAMED, NOT FIXED (schema-verified module, byte-neutrality must be re-proved). (2) --relabel-new-ids REFUSES by guard G3 -- "A minted [], B minted [1970, 1986, 2002]" -- because this engine mints NO client fleet ids at all; the refusal applies nothing and the 1092 is byte-identically the no-flag number. Outcome 2 needs list 10's applier first. THE TAIL'S SIM DRAW FIRES HERE: every trade sector in this save reads tscr = 253, not the corpus's 252 -- this IS lane AD's two-raider state, measured at 2 entries / 2 strategic words at 0x00820e18 -- so the backlog's Rung B correction is load-bearing on this workload and S02/T23 are stubs; --commit-rng leaves the count at 1092 because the 16 words are the wrong words. RANKED WORKLIST (full list in the finding, residual 0): morale event ring cme2 539; colony growth/repair/bonuses 154; fleet objects 150; observed designs/techs 79; player money+counters 30; TShn 23; system<->fleet membership 23; ShipRecs 20; build queues 20; other system 15; crep/CD 13; master id lists+generator 10; turnstats 8; trade manager 6; writer defect 1; Checksum 1. THE 539 MUST BE DECODED BEFORE IT IS RANKED: cme2 is a FIXED 11-SLOT RING (158 events in input, output and target alike), so the turn's 27 appended events across 10 systems shift every slot. The 27 are three kinds only: 10x mtp=12 "Colony established on Avalon." (+2), 10x mtp=20 "Imperial savings of 5,000,000 or more." (+2, down a band from the pre-turn 15,000,000), 7x mtp=16 "Population limits inhibit freedom." (-1). So the ring is the REPORTING SURFACE of colonisation, the savings chain and the population term -- it owns none of them. THE LARGEST INDEPENDENTLY CLOSABLE MODULE IS T34 RecordObservedDesigns (stub): 79 leaves, no upstream dependency. TWO MEASURED CORRECTIONS: Pv* is NOT a previous-turn snapshot -- target PvInfra 1.03 and PvPop 617,999,994 where both pre- and post-turn Infra/Pop are 1.0 / 600,000,000 (= current x 1.03), so Pv* holds the UNCAPPED GROWTH PROJECTION on a capped colony, while PvSuit (5/5) and PvRes (4/4) DO equal the pre-turn current: the family is mixed and must not get one rule. And five of the six regressions are ntdev on Sys[160/400/416/432/448], where the engine writes 1 and both input and target hold 0, while the SAME rule closed 26->27 on thirteen other systems -- a one-predicate bug, the cheapest fix here. Also: the capture already holds the three design NAMES the target creates (Egg Thief Mk 4/5, Bravestar Mk 3 = Des[2018/2034/2050], node-2 counters 126/127/128 continuing the fleets' 123/124/125 -- one counter across object kinds, lane BT §4.3), and list 1 is declined for want of a design-registration path. AND: the one list command_replay.cpp CAN apply -- list 5, the planetary sliders -- fired ZERO times on all 15 elements, because the shim's memory window cannot type 7 of its 8 fields; the settling experiment is one UI run (two different sliders to two different values), not engine work. TOOL CHANGE: tools/aiorders_to_tcb.py now reads the deep dump's aivec/aistr rows, consulting them ONLY at a word the existing field map already types as a vector, cross-checking the begin/end length against the followed count, and recording anything it cannot place as a # comment plus a stderr line (12 list-12 vectors, 15 list-23 vectors, 3 list-1 design names, 1 list-3 vector) rather than inventing a field mapping. REGRESSION-CHECKED against every aiorders log in the repo: 8 shallow logs byte-identical, and the 4 lane-CB logs were deep dumps whose payloads this converter had been silently discarding -- the only field changes are list 8's route and list 10's ship vector going v1 -> v1:272/v1:1728, which is EXACTLY what tools/tcb_from_json.py already emits for the same capture, so the two converters now agree everywhere but list 23 (where the committed cb-turn2to3.tcb predates the current tcb_from_json.py's own UNMAPPED_HEAD[23]; no behavioural difference, recorded not touched). The canonical pair's replay is byte-identical under the committed tcb and a regenerated one. Artefacts: findings/subsystems/rung-b-rich-turn.md, verify/results/turncommands/br2-turn27-{deep,shallow}.tcb, verify/results/standalone/cv/* |
| MY GATE WAS HOLLOW FOR A DAY: the corpus tests were SKIPPING and I reported 59/59 | meta | verified | high | 100% | 2026-09-09 | SOTS_SAVES_DIR was never set on CT111, so mars_stream_save and app_turn printed "unset, skipped" and passed. Every "59/59" I reported today did not run them. Lane CV, running on the host with the corpus, got 57/59 - and locally against all 43 saves the picture is worse: 39 failures, two classes. (1) The coverage ratchet BROKE when the corpus grew 22 -> 43 - pct >= 99.99 fails on the spy saves and others; rule 27 working exactly as written, unseen. (2) A writer defect: ProjectName reads usnc as one item and the wire is a usp/usc pair; 12 saves round-trip 12 bytes short. Rule 1's shape - a green verdict on a harness comparing nothing - delivered by the person whose job was to notice. FIX: tools/gate.sh is now the gate; it syncs the corpus, sets the variable, runs every check as its own command, and fails RED if any corpus test skips. First fresh run: 57/59 and RED on one remaining skip, correctly |
mars/stream: SpecialProjectNameGen is a usp/usc PAIR, not a single usc |
engine | mapped | high | 40% | 2026-09-09 | Lane CV. struct ProjectName reads usnc as a count of ONE item per element, commented "usnc is 0 in every save available" - falsified by ad-turn27-two-raiders.sav, which has one record with usnc == 1. The generated schema already lists both tags. 12 of 43 corpus saves round-trip 12 bytes short, all in the sprjs band; it is why mars_stream_save and app_turn fail. Surfaces in the replay as /Sim/sprjs/usp: only-in-B. Named, not fixed - it is a schema-verified module and byte-neutrality must be re-proved after the change. Track 0a in the backlog |