214 lines
160 KiB
Markdown
214 lines
160 KiB
Markdown
# 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 | in-progress | — | 0% | 2026-09-07 | .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 | backlog | — | 0% | 2026-09-07 | ~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 | **LANE O 2026-09-08.** Corpus is now **11 saves / 9 distinct states**: the 4 old ones plus 5 Zuul (turns 15-23) and 2 Human (turns 2-3) made this session, all `--strict` clean and all `coverage: PROVED` under `state_checksum --tree`. See the individual rows for what each unblocks.
|
|
| 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 | backlog | — | 0% | 2026-09-07 | 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 | backlog | — | 0% | 2026-09-07 | choke point: bool __cdecl gobio::ReadFile(const char*, IBuffer**) 0x008d5140; FileSystemSet ctor 0x008d6d60 pushes NativeFileSystem(".") FIRST then sots.ini [Modules] Mount<N> (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` | **LANE O 2026-09-08.** **`CD` `TurnCommands_v5` is no longer sample-starved.** Saves taken AFTER issuing orders and BEFORE End Turn grow the block from the universal empty snapshot (`CD[0]` digest `3df7d93164fb1d7d`, 35 leaves / 122 B in turn1/2/3, zuul-turn5 AND human-turn3) to 41 / 61 / 129 leaves. Field-by-field layout in `verify/save-reader/SAVE_FORMAT.md` **section 11**: playerId, researchRate float, per-command-type present-flags, then counted lists of build orders (ordinal, designId, systemId, 0), system-rate commands (systemId + a named `SRs SRt SRsc SRtf SRi SRoh SRnr` frame - the only NAMED sub-frame in the block), colonize orders (shipId, 1) and fleet moves (fleetId, 1, destSystemId, 0). Ids cross-checked against the same file's `Sys`/`Flt` records and against the UI (384 = Gallandro, 432 = Octans, 688 = the fleet the UI showed as Alpha Fleet with Dest: Octans, 216383 = the exact Imperial Savings drop the Boost Research panel caused). The block is the PENDING client->server queue: `zuul-turn15-orders.sav` still has `ResRate 0.25 / ResTNm '' / Sav 2,902,722` on the ServerPlayer while the block already carries rate 0.97 and the boost. Saves `verify/results/saves/zuul-turn15-orders.sav`, `zuul-turn17-orders2.sav`, `human-turn2-orders.sav`
|
|
| 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<Descriptor>; 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 | . **LANE V 2026-09-08 (build `eventlive-dd38117-20260908T0916Z`): the events divergence is CLOSED on the reference turn** - 3 calls, 3 compared, **0 diverged, exit 0**. Over 5 turns 15 calls / **2** diverged (was 3), both short by exactly 1 `next_id` = the unmodelled `EVENT_TECHS_UNLOCKED`. **CORRECTION to lane R's 'RNG 15/15': that was WORKLOAD LUCK, not a property.** On lane V's call 9 the completed tech had `research_roll_pending` set, so the original drew one word `ours` did not (`left` 374 vs 375, `next_index` 250 vs 249) - the `OnTechResearched` callback draw, a DECLARED out-of-scope boundary (P-events-wiring §3 says so in as many words). Honest statement: the `rng` region reproduces bit-for-bit on every call that does not complete a roll-triggering tech, and is short by exactly one draw on every call that does. Also live: `sizeof(ObservedTech)` = **44**, measured twice (`observed_techs.bytes` 440->484 and 484->528). Zuul double roll now CLOSED on a species-5 save lane V made - see its own row
|
|
| RNG signatures (Ghidra) | meta | verified | high | 100% | 2026-09-08 | Seed/Twist/NextFloat/NextInt verified; draw = y/(2^32-1); NextInt [0,n] inclusive; lazy twist; left@+0x9c4. RUNTIME CONFIRMED: fpu_cw=0x127f (53-bit double, round-nearest) - our next_float model is right, float_from_pc24 is an unused contingency |
|
|
| engine: game/effects | engine | verified | high | 100% | 2026-09-08 | merged: TechId enum (196 slots @10000+i), 44 ids with typed strategic effects, species flag bits, ApplyTechEffect; 254 checks |
|
|
| VM140 exclusivity (lab rule) | meta | verified | high | 100% | 2026-09-08 | one agent at a time. Holder: **H-probes** (claimed 2026-09-08; 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-<lane>, stage dist-<lane>, deploy C:\SOTS\shimdist-<lane>. 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. | **LANE O 2026-09-08.** **VM140 is FREE.** Holder was O-workloads; released 2026-09-08 after ~9 turns of Zuul play (turn 15 -> 23) and a fresh 3-turn Human game. **CORRECTION to lane V's `type` gotcha: the click helper's `type` DOES reach the game's text fields - you must CLICK THE FIELD FIRST.** `click 480 234` (the Save Game File Name box) then `type <name>` worked every time this session, and `click 500 363` + `type` worked for the Create Game name box; lane V's failure was almost certainly an unfocused field. Saves can therefore be named properly and no longer need renaming on disk (the file written is gameName + typedName). Coordinates confirmed this session at 1024x768: in-game menu = round button (1000,714) -> `Save` (937,679) / `Quit to Main Menu` (937,698) -> confirm OK (537,377); Save dialog `Save` (772,233), `Game Successfully Saved` OK (612,383), `Done` (772,542); map panel `Research` (55,124) `Design` (142,124) `Build` (221,124), `Manage Fleets` (60,464) `Move` (155,464) `Special` (229,464), `End Turn` (100,714); Build screen `Add To Queue` (125,714); **Esc leaves the Build and Research screens** - the bottom-left button on the Research screen is `Cancel research`, NOT back, and clicking it pops `Confirm Cancel`. Move mode: send `move X Y` FIRST to read the target's name / Range / ETA off the map, THEN `click`, because that readout is what tells you whether the route is a node route (2-5 turns) or a sublight crawl (37-86 turns). **Chained clicks in one `cmd.txt` DO work for repeated clicks on the SAME button** (`Add To Queue`) but drop about half at `sleep 250` and about 1 in 6 at `sleep 500` - count the result, never assume. End Turn takes 30-45 s under `hooks=trace` with ~25 ships. A mid-turn AI diplomacy popup can steal a scripted click sequence - screenshot after any batch that seems to have done nothing. **VM RESTORED and verified**: quit to the main menu (screenshot), `SavedGames` back to the ORIGINAL 8-file set with the three autosaves restored BYTE-IDENTICAL (sha256 prefixes 0630daaa / e0bf3485 / 1f0dc162), so `ref-turn2` is still (400,436) and `zuul-turn5` still (400,465); lane O's staging dir `C:\SOTS\o-stage` removed; `shim.cfg` still `hooks=trace`, build `recap-7584bad-20260908T0615Z` untouched. Lane O's 7 saves live in the REPO ONLY - push one back with `scp verify/results/saves/<f>.sav re@192.168.10.139:C:/SOTS/SavedGames/` when a compare needs it, and remember that adding a file shifts the Load dialog rows (`MyGame*` sorts BEFORE `MyGameverify1*`)
|
|
| Zuul double-roll (behavioural) | verify | verified | high | 100% | 2026-09-08 | **CLOSED by lane V 2026-09-08.** No species-5 save existed, so lane V MADE one on VM140: a custom game with only Zuul left in the Available Species pool (both players Zuul), 4 End Turns under `shim.cfg.recapb3`. **8 calls, 8 compared, 0 diverged, exit 0.** All four researching calls have `species=5` and the generator advances by TWO, not one: `left` 540->538 / 522->520 / 504->502 / 485->483, `next_index` +2 each, `mt` hash unchanged; `ours` reproduced every post-state bit-for-bit. Save `verify/results/saves/zuul-turn5-species5.sav` (48559ab5b719b332, 59,131 B, turn 5, homeworld Gallandro) and on the VM as `C:\SOTS\SavedGames\zuul-turn5.sav`. NOT yet exercised by it: a Zuul tech COMPLETION (node 144 was at ~4,700 of 5,000 after 5 turns - one more End Turn gets there), so `SetResearched` and the Zuul boarding-pod grant are still untouched. Report `findings/subsystems/eventlive-verification.md` §5 **EXTENDED by lane U 2026-09-08: the double roll and the COMPLETION PATH together.** `zuul-turn5` + 10 more End Turns (turn 5 -> 15) under the same config: **20 calls, 20 compared, 0 diverged, exit 0**, with TWO Zuul completions. Call 2 (turn 7, alloc {144,1376}) advances the generator by two (`left` 449->447, `next_index` 175->177) AND runs the unlock cascade in the same call - node 144 -> state 4 / order 21 / turn_researched 7, nodes 132 and 136 -> state 2 with cost_rp 10000 / 16000 and turn_available 7, `next_id` 10->12, `observed_techs` 396->440. Note the ZUUL tree unlocks only 132 and 136 from tech 144 where the HUMAN tree also unlocks 142, so this is an independent instance of the cascade, not a repeat. Trace `verify/traces/unlock-b3-zuul.jsonl.gz` |
|
|
| budget tail coverage (expenses/aid/debt) | verify | backlog | — | 0% | 2026-09-08 | 8 ComputeBudget slots were always 0 in ref-turn2 (no sliders, no aid, no debt, no handicap). Need a save with expense sliders, a debtor and a research-aid treaty to exercise ExpenseTotal + the aid/bonus tail . CONFIRMED AND WORSE 2026-09-08 (lane R, 4284 calls): **13 of 22 slots are 0 on every call** - tradeIncome, shipCarriedPop, secondaryManager, bonusIncome, systemIncomeNeg, debtInterest, construction, expenses, researchMoneyGiven, savingsGiven, tra, researchPointsGiven, trp | **LANE O 2026-09-08.** **PARTIALLY unblocked, and three of the remaining slots are now shown to be UNREACHABLE through the 1.8 UI.** Delivered: `[11] construction` and a large `[8] maintenance` are live in `zuul-turn17-orders2.sav` (build queue of 20 DEs) and `zuul-turn23-fleet23.sav` (27 DEs in service, `Maint` 14,500 vs 3,000/1,000 in every earlier save); the empire research slider is off default (`ResRate` 0.97 vs 0.25) in all five Zuul saves. NOT delivered, with reasons. **(a) `[12] expenses`**: `Nexp` is `[]` for EVERY player in ALL 11 saves across turns 1-23, human and AI alike, and no screen in the 1.8 UI (map panel, Build, Research, Ranking, Intel Analysis, Empire Population Manager, in-game menu) exposes a per-category expense slider - the only economic sliders are the empire Savings/Research split and the per-system `SRs/SRt/SRsc/SRtf/SRi/SRoh` rates, which land in `Sys`, not `Nexp`. Working hypothesis: slot 12 is dead in 1.8. **(b) `[10] debtInterest` / `[7] systemIncomeNeg`**: a debtor cannot be manufactured quickly - a ship's savings cost is deducted at QUEUE time so the queue cannot be over-committed, and `Maint` counts only ships IN SERVICE, which arrive at construction-budget/ship-cost per turn (~4.5 DE/turn here). With income floored at 78,570 (Planetary Budget pushed fully to Construction; measured 124,055 -> 78,570 on the panel) that is ~157 destroyers, i.e. ~35 more End Turns. Reachable, just not cheap. **(c) `[13]/[14]/[19]` aid**: a treaty offer DOES occur (the AI offered a non-aggression pact on turn 3 of the Human game and it was accepted - `human-turn3-noderoute.sav`), but the accepted pact leaves the player's `aid` count 0, and SotS 1.8 has no player-initiated treaty UI - proposals are AI-driven events. Research aid needs a longer game and luck, not a click path
|
|
| hook GetDifficultyMods | meta | backlog | — | 0% | 2026-09-08 | B1 derived the two difficulty rows from trace values (AI maintenance divisor 3, research x1.5) instead of snapshotting them; hook it properly so they stop being constants |
|
|
| 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 | . RE-OBSERVED by lane V from the CALLER side 2026-09-08: on the B3 hook's own call 9 the same draw shows as an `rng` divergence (`left` 374 vs 375) plus guard spans `player+0x3b4` (`ResErrRoll`) and `player+0x196` (byte 2 of design mask B at `+0x194`). Still NO save that starts with `research_roll_pending` true - the flag has only ever been seen set and cleared inside one turn **LANE O 2026-09-08.** **The 'no save starts with the flag true' gap is CLOSED, and the exact condition is now a number.** `verify/results/saves/zuul-turn17-rollpending.sav` has player 16 `ResErrRoll=True`, `ResTNm='WEP_GrnLas'`, and that node at `TResDone 1083 / TResCost 5000` = ratio 0.217; `zuul-turn16-noderoute.sav` is the same state at ratio 0.000. The gate constant at **0x00a2c788 is a float 0.5**, not a double (read out of `dumps/sots.exe` by PE section walk; an 8-byte read gives 5.28e13 garbage and 0x00a2c78c is float 100.0) - recorded as `ResearchRollProgressThreshold` in `ghidra/addresses.d/lane-o.json`. So `ResErrRoll` survives into `ProcessResearch` exactly while progress/cost <= 0.5 AT THE START of the turn, and the `OnTechResearched` draw can only fire when ONE turn supplies more than half the target's cost. Confirmed live end to end: target set turn 15 (ratio 0), funded 1,083 RP/turn at 97% research, flag still true entering turns 16 and 17, tech completed on turn 21 with the flag ALREADY CLEARED - the ProcessTurn roll had fired when the ratio crossed 0.5. To make the draw fire you need a tech whose remaining cost is under one turn's RP; on this empire (1 colony, income 78-124k, 1,083 RP/turn, cheapest available tech 5,000 RP) that is ~2.3x the available RP, so it needs a bigger empire or a cheaper tech, not a different click path. The in-game **Boost Research** panel (Research screen, `Boost Research` twisty) spends savings on the current target and IS recorded in `TurnCommands_v5`, but its per-turn pool is only ~7.5% of Imperial Savings and decays geometrically within the turn (197,271 then 17,551 then 1,561 out of 2.9M), so it cannot bridge the gap either
|
|
| 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 | **APPEND SITE NOW NAMED (lane X): `RecordObservedTech` 0x007ba1a0, called from OnTechResearched; stride 0x2c.** Still needs a declared region + a model in ours. . PARTLY ADDRESSED (lane P + lane V 2026-09-08): it IS now a declared **Result** region (`observed_techs`, ServerPlayer+0x274), so it has left the guards' undeclared list and appears in the diff instead - lane V measured the span growing by exactly 44 on both completion calls (440->484, 484->528) and `ours` short by that 44 each time. STILL OPEN: `ours` does not append, so the region diverges on every completion. The element and the append site are fully pinned (lane X/S: sizeof 0x2c, RecordObservedTech 0x007ba1a0, DE-DUPLICATES BY TECH NAME), so what is left is modelling it - and that belongs with whoever takes SetResearched
|
|
| 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 | backlog | — | 0% | 2026-09-08 | 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 | **LANE O 2026-09-08.** **TYPE 3 (NodeRoute) IS NOW COVERED - and type 2 is shown NOT to be produced by ordinary movement of EITHER node-drive race.** Waypoint census over the corpus (`Tp` values): turn1/turn2 none, turn3-state 1x type 1, `zuul-turn5-species5` **2x type 3** (already on disk since lane V - nobody had looked), and the new saves 6 / 8 / 11 / 11 / **16** x type 3 (`zuul-turn15-orders` through `zuul-turn23-fleet23`) plus 3x type 3 in `human-turn3-noderoute`. Includes the PLAYER'S OWN fleet: `zuul-turn16-noderoute.sav` has `Flt 688 HFPlan=True wpts` = 1 x (`Wpt 432`, `Tp 3`, `nrt` = (`nrp -1`, `nrf 384`, `nrt 432`)) - Gallandro to Octans, in nodespace - and `human-turn3-noderoute.sav` has (`Wpt 128`, `Tp 3`, `nrt` = (`nrp 37`, `nrf 224`, `nrt 128`)) where `nrp` is the `NdGr2` path id being ridden. **Type 2 negative result**: Zuul (rip-bore route over a natural node line) and Human (node drive over a single directly-connected node line, Downbelow->Delphi, and over multi-hop routes) BOTH emit type 3 - single-hop (`nrp -1`) and multi-hop alike, planned and in-transit alike, 20+ instances, zero type 2. So `WaypointKind::NodeLine` is not what a Move order produces for a node race; the remaining candidates are Hiver gate traffic (4/5) and some non-order path into node space. Types 4 and 5 still need a HIVER game. Mechanic worth knowing for the next lane: a Zuul fleet WITHOUT a Rip Bore is refused node travel outright (dialog: 'cannot reach X using nodes. Do you want to plot a sublight course?'), and node-connected destinations are identifiable from the Move-mode readout by a short ETA (2-5 turns) versus a 37-86 turn sublight crawl
|
|
| 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<ObservedTech> 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 | . PARTLY ADDRESSED (lane P + lane V 2026-09-08): it IS now a declared **Result** region (`observed_techs`, ServerPlayer+0x274), so it has left the guards' undeclared list and appears in the diff instead - lane V measured the span growing by exactly 44 on both completion calls (440->484, 484->528) and `ours` short by that 44 each time. STILL OPEN: `ours` does not append, so the region diverges on every completion. The element and the append site are fully pinned (lane X/S: sizeof 0x2c, RecordObservedTech 0x007ba1a0, DE-DUPLICATES BY TECH NAME), so what is left is modelling it - and that belongs with whoever takes SetResearched
|
|
| 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 | in-progress | — | 0% | 2026-09-08 | 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 | backlog | — | 0% | 2026-09-08 | 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 | **LANE O 2026-09-08.** Superseded in part - see the `waypoint types 2-5 have no coverage` row: **type 3 now has 16 instances in one save** (`zuul-turn23-fleet23.sav`) and the player's own fleet is a type-3 mover in `zuul-turn16-noderoute.sav` and `human-turn3-noderoute.sav`. Type 2 was ATTEMPTED as a Human node-drive move over a single directly-connected node line and still came back type 3, so B4's node-line step is not what a player Move order exercises
|
|
| 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) | . **LIVE-VERIFIED AGAINST AN ADVANCE PREDICTION (lane V 2026-09-08, build `eventlive-dd38117-20260908T0916Z`, main dd38117 unchanged).** Lane P wrote the expected numbers into `docs/P-events-wiring.md` §4 BEFORE the run; every one held. First End Turn: **3 calls, 3 compared, 0 diverged, exit 0** (was 1 diverged) with `turn=3`, `events_turn_bucket_exists=true`, `events_next_id_in=3`, `events_in_turn_bucket=1`, `events_dedup_risk=0`, no `events_scan_truncated`, `next_id` 3->4 both sides, `turns`/`turns_bytes` 2/48 unchanged, node[144] 2879->5768 + flag 1->2, rng identical. Five End Turns: exit 1 with **2 divergent calls instead of 3**, each short by EXACTLY 1 (`next_id` orig 7 ours 6, orig 12 ours 11) = the unmodelled `EVENT_TECHS_UNLOCKED`, exactly as forecast - 0 divergences there would have been suspicious, not good. §4.3 conversion held too: `player+0x274/0x278/0x27c` left the guard's undeclared list (13 spans in 2 calls -> 10) and appear in the diff as `observed_techs.bytes`. End-Turn oracle byte-identical to lane R's. clean_room_check OK, ctest 33/33 (separate commands). Report `findings/subsystems/eventlive-verification.md`; engine `docs/V-eventlive.md`; traces `verify/traces/eventlive-b3-*`, reports `verify/results/compare/eventlive-b3-*`
|
|
| 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` | . **CONFIRMED LIVE (lane V 2026-09-08)**: on the running game the `observed_techs` Result region grew by **exactly 44 bytes** on each of the two tech-completion calls of the 5-End-Turn run (440->484, 484->528), and both non-researching players' vectors measured 880 = 20 x 44 and never moved. Behavioural confirmation of a purely static pin
|
|
| 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 | . **CONFIRMED LIVE (lane V 2026-09-08)**: on the running game the `observed_techs` Result region grew by **exactly 44 bytes** on each of the two tech-completion calls of the 5-End-Turn run (440->484, 484->528), and both non-researching players' vectors measured 880 = 20 x 44 and never moved. Behavioural confirmation of a purely static pin
|
|
| 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<string> 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<char>, 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) | **LANE O 2026-09-08.** The `CD` line is now actionable: three saves with issued orders exist (see the wire-schema row and `SAVE_FORMAT.md` section 11). `SvSctOb`, `DOpts`, `spies2` untouched
|
|
| 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 - VectorHelper<Mars::String> and VectorHelper<int> - 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<int>> 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<T> | objects | verified | high | 100% | 2026-09-08 | Lane A found StreamableEnum<T> writes a FRAME CONTAINING ONE INT, not a bare int, so VectorHelper<StreamableEnum<uint>> 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 | backlog | — | 0% | 2026-09-08 | 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<Encounter>::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 | backlog | — | 0% | 2026-09-08 | 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 | backlog | — | 0% | 2026-09-08 | 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 | backlog | — | 0% | 2026-09-08 | 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<T> 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<T>() 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 |
|
|
| 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, |detectors|); with no detector teched that is |contacts| x |detectors| EXACTLY and NOTHING IS EVER ASSIGNED - **a roll that can never succeed still advances the generator**. The outer repeat-until-no-progress back-edge (which a truncated dump HIDES) cannot redraw a pair: the tried-bitset is filled ABOVE the back-edge target and never cleared, so at most two passes |
|
|
| 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 | backlog | — | 0% | 2026-09-08 | 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..(|contacts|x|detectors|) and whether its +0xfc gate passed is not knowable statically; ProbabilisticJump's second word is 0 unless a type-5 waypoint fails its arrival test. RESIDUAL: 18-20 words, essentially ALL of it. WHAT IS NOW PROVABLE IS THE NEGATIVE: there is NO TWENTY-THIRD MECHANISM - the complete draw-site inventory of the ProcessTurn closure (1,426 functions) is 22 sites (21 entry-point calls + 1 inlined), so the 18-20 words are distributed among exactly those. THE SEARCH SPACE CLOSES; THE COUNT DOES NOT. One extra bracket on EncounterDetect_ProcessTeamRecord with |contacts|/|detectors| in the argument record turns the formula into a one-turn test |
|
|
| 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 |
|
|
| 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 | backlog | — | 0% | 2026-09-08 | 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) | (node & 0xF)`. R2's x16 IS THE shl 4. Found ONLY by following E9 TAIL-CALL THUNKS - an 8-byte jmp thunk hid the whole chain from the first closure pass |
|
|
| 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 |
|