# The spy detection roll — decoded, its gate turned into a save predicate, and measured - **Type:** subsystem (static decode + live measurement) - **Owner / date:** lane AS · 2026-09-09 · guest **VM144** (`sots-re-win10-144`, `re@192.168.10.144`) - **Instrument:** lane Z's `draw_sites` return-address ledger at **`probes=8`**, shim build `as-c172c99-20260909T0205Z`, dist `C:\SOTS\shimdist-as` - **Predictions, committed before the build:** `sots-engine` `docs/AS-predictions.md` (commit `c172c99`, addendum `61875a4`) - **Answers:** the resolution `2026-09-09-tail-draws.md` §8 item 2 (the spy half of the tail gate) - **Corrects:** the resolution's "the detection roll is inline in `vslot13`" (it is a direct call); lane L3 §6's "`spies2` is not the spy list"; board row 399's V2 misquote is *not* touched here *(Sections 1–3 were written and committed before any measurement. Sections 4 onward are the measurement.)* --- ## 0. Headline > **The detection roll fires, it costs one word, and its gate is an asteroid belt.** > > `Mars::RNG::Chance` at **`0x00887c8a`** — in the body of `ServerSpyManager::vslot13`, phase 23 > call 9 of 9 of `OnAllCombatDone_Tail` — rolls once per deployed, undetected spy per turn, on the > strategic generator, at a probability `sdo` that starts at **0.0084 as measured** and accumulates > by the same amount every turn. `OnAllCombatDone_Tail` went **363 → 364**: one word, against 0 on > every turn any lane has ever measured. > > The predicate is `spy.deat != 0 && spy.sdet == -1 && sys(deat).ARes2 > 0 && (sys(deat).TerrFl & 1)`. > **Corpus count: 0 of 22.** The failed conjunct is **`deat != 0`** — every spy this campaign has > ever built was still docked to its tender. Not trade routes, not freighters, not `spyon`, and not > the asteroid belt: **all 22 corpus saves already had a usable belt somewhere**, and two of them > already had four AI-owned ones. > > And the polarity is the opposite of the intuitive one: **when the belt is gone the roll does not > happen and the spy is detected anyway.** A zero at this site is two different results. --- ## 1. The chain, decoded from the instruction stream Program `sots` / "Sword of the Stars.exe", ImageBase `0x00400000`, all addresses VAs. Every body below was disassembled from its start **to the next function start** (rule 17); no Ghidra size was used. ReVa/CT111's MCP endpoint was down for this session (`CONNECTION_CLOSED`), so nothing was read in the decompiler — this is all instruction stream, which for these bodies is a feature. `Game::ServerSpyManager::vslot13` = **`0x008877b0`** (lane V2's name), dispatched at `0x007d9811` from `OnAllCombatDone_Tail` on receiver `S+0x15c` slot 13. Body `0x008877b0 .. 0x00887f30`. Throughout: `ebx = 0`, `edi = this` (the manager), `esi` = the current `Game::SpyCraft`. The loop is over the vector at `this+0x10 .. this+0x14` — the same vector lane L3 watched grow to 1 at **Build Spy**. ``` 0088781d je 0x887e92 G1 spy.deat (+0x10) == 0 -> next spy 0088782d call 0x8b9240 sysA = registry(server+0x84, spy.deat) -> [ebp-0xbc] 00887845 call 0x8b9240 ownerO = registry(server+0x84, spy.sown) -> [ebp-0xc0] 00887856 je 0x887c5b G2 spy.sdet (+0x40) == -1 -> BRANCH D (detection) 00887867 jl 0x887e92 (server.turn - sdet) < 3 -> next spy 0088787a je 0x887c4f sysA.PID == 0 -> sdet := -1, next spy 00887891 je 0x887c4f sysA.PID == ownerO -> sdet := -1, next spy 00887af2 call 0x8408e0 <-- P, the false-flag draw (three sites inside) ``` **Branch D — the detection roll:** ``` 00887c5b push eax ; ownerO 00887c62 push [ebp-0xbc] ; sysA 00887c63 push esi ; the spy 00887c66 call 0x81f570 D1 SpyCraft_AccumulateDetectionOdds -> writes spy.sdo (+0x3c) 00887c6b mov ecx,[ebp-0xbc] 00887c71 call 0x743f80 D2 t = ServerSystem_BeltUsableFlags(sysA) 00887c76 test al,1 00887c78 je 0x887c97 (t & 1) == 0 -> SKIP THE ROLL, straight to D4 00887c7a mov ecx,[edi+8] ; the StrategyServer 00887c7d fld [esi+0x3c] ; spy.sdo 00887c80 mov ecx,[ecx+0x16c] ; THE STRATEGIC GENERATOR 00887c86 push ecx 00887c87 fstp [esp] 00887c8a call 0x8e6dd0 D3 Mars::RNG::Chance(strategic, sdo) <-- THE DETECTION ROLL return address 0x00887c8f 00887c8f test al,al 00887c91 je 0x887e92 roll failed -> next spy, still undetected 00887c97 mov edx,[edi+8] 00887c9a mov eax,[edx+0xc] ; the current turn 00887c9d mov [esi+0x40],eax D4 spy.sdet := turn (DETECTED; then the event strings) ``` ### 1.1 It is a direct call, not an inlined draw — the instrument advice was right for the wrong reason The resolution that created this lane says the roll is *inline* in `vslot13`. It is not. The bytes are `e8 41 f1 05 00` — a plain `E8 rel32` to the `Chance` **entry point** `0x008e6dd0`. Nothing resembling rule 16's inlined tempering sequence is present. What *is* true, and is the whole reason `draw_sites` is the right instrument, is that the call sits in the **caller** rather than in `SpyManager::Slot13RngCallee 0x008408e0`. An entry probe on `0x008408e0` therefore reads zero whether or not the roll fires — but that is instrument *choice*, not inlining, and the distinction matters because an inlined draw would be invisible to `draw_sites` too, and this one is not. Lane AG read this independently and reached the same conclusion; the correction is recorded in both places. ### 1.2 The gate is an asteroid belt, and it is sixteen bytes ``` 00743f80 cmp dword ptr [ecx + 0x6c], 0 00743f84 mov eax, dword ptr [ecx + 0x19c] 00743f8a jg 0x743f8f 00743f8c and eax, 0xfffffffe ; clear bit 0 when [ecx+0x6c] <= 0 00743f8f ret ``` On `Game::ServerSystem` (`objects/layouts.md`): **`+0x6c` = `ARes2`**, **`+0x19c` = `TerrFl`**, `+0x100` = `PID` (which `ServerSystem_GetOwner 0x007437e0`, used three times above, returns). So > **bit 0 of the return = `(TerrFl & 1) AND (ARes2 > 0)` — "this system still has a usable > asteroid belt".** `ARes2` is already the field lane AC identified as the asteroid-belt resource (values 1500–3000, matching `SYSTEM_MIN/MAX_ASTEROID_RESOURCES`). `TerrFl` bit 0 is its flag; §2 checks that reading against the corpus, which is the only place a static reading of an unnamed bit can be checked without a debugger. **The polarity, stated because everyone's intuition gets it backwards.** The branch is a `je` that jumps **past** the roll. Belt usable ⇒ roll. Belt gone ⇒ **no draw at all, and `sdet := turn` unconditionally.** The spy hides in the belt; if the belt is mined out it has nowhere to hide and is spotted for free. So at this site, a `draw_sites` row with `calls = 0` and a `sdet` that has moved is a *positive* result about a different arm, and must never be reported as "the roll did not fire". ### 1.3 `sdo` — where the probability comes from, and why the roll should cost a word `0x0081f570` (`SpyCraft_AccumulateDetectionOdds(spy, sys, spyOwner)`, `__stdcall`, `ret 0xc`, 512 bytes) is called **unconditionally** on branch D, immediately before the gate, and writes `spy.sdo (+0x3c)`: * if `sys.PID == 0` (unowned): `fldz; fstp [esi+0x3c]` — **`sdo := 0.0` and return**. A spy at an unowned system rolls at p = 0, which costs no word. * otherwise it walks the system's fleet vector accumulating three counters (`[ebp-4]`, `[ebp-8]`, `[ebp-0xc]`) and computes, with every literal read as the **four bytes in the image** (rule 23 — these are widened floats, not decimals): ``` p = 0.01f (0x009e31c0) p += (countA > 0) ? 0.01f : 0 (0x009e3e14) p += (countB > 0) ? 0.02f : 0 (0x00a1d048) p += 0.001 * countC (0x009e23c0) p *= (spyOwner->[0x5c] == 5) ? 0.75f (0x009e5ac4) : (spyOwner->[0x5c] == 6) ? 0.5f (0x00a2c788) : 1.0f p *= 0.7 (0x009e5df8) sdo = min(sdo + p, 1.0) (0x009e1ef0) ``` Two properties matter more than the arithmetic. It **accumulates** onto the previous `sdo`, and it is **clamped at 1.0**. `Mars::RNG::Chance` costs **0 words at `p <= 0` and at `p >= 1`** (board row 367). So: * a fresh deploy at a quiet foreign system gives `sdo = 0.7 × 0.01 = 0.007` — strictly inside (0, 1), so the roll costs **exactly one word**; * `sdo` climbs by ~0.007/turn, so a spy left in place for ~143 turns reaches `p >= 1`, at which point the site **costs nothing and always succeeds**. That is a second way a zero at this site is not a negative, and it is the same shape as lane AG's `cmo` early-out. *(Measured afterwards: **0.0084**, i.e. `0.7 × (0.01 + 0.001 × 2)` — `countC = 2` rather than the 0 assumed here. The one-word conclusion is unaffected and the form of the expression reproduced exactly. §4.2a.)* ### 1.4 What a deploy actually writes — and one draw site nobody had `ServerSpyManager::DeploySpy` = **`0x00887410`**, vftable `0x00a3073c` slot 7, zero direct call sites, reached only from the `SHIPACTION_DEPLOYSPY` handler `0x0078c930` through `[[dispatcher+0x15c]+0x1c]` — the stack-built ship-action function-pointer table that lane B6 identified as a distinct indirection class. On the success path it writes: ``` spy->Reset() (0x00838070) spy->sdo (+0x3c) := 0.0f spy->sdet (+0x40) := -1 spy->cbh (+0x18) := spy->tdep (+0x14) := server->[+0xc] ; the current turn spy->atto (+0xc) := 0 ; carrierShip->[+0xa8] := 0 ; detach from the tender ServerSystem_AddSpy(system, spy) at 0x008874d4 ``` and `AddSpy` is where `deat` is born (§1.5). Because the deploy leaves `sdet == -1` and `sdo == 0`, **the detection branch is entered on the very next `OnAllCombatDone_Tail`** — which is the same End Turn that applied the order, since `ApplyAllTurnCommands` runs before the tail. The float argument is computed in the handler, and it is a draw: ``` 0078c975 mov ecx,[esi+0x16c] ; esi = StrategyServer -- THE STRATEGIC GENERATOR 0078c97c add ecx,4 ; the +4 sub-object, same one the trade-raid roll uses 0078c97f call 0x47d830 ; Mars::RNG::NextFloat <-- 1 word, ret 0x0078c984 0078c984 fld qword [0x009e21b0] ; 6.283185482025146 == 2*pi cbh = NextFloat() * 2*pi ``` > **`0x0078c97f` is a strategic-generator draw site in no existing inventory.** It is not one of > lane V2's eight, and it is not in lane I's 22, because it hangs off the ship-action table and no > call-graph sweep reaches it. It fires **once per Deploy Spy order applied**, inside > `ApplyAllTurnCommands` — so it lands in the `ProcessTurn` bracket, not the tail's. Any gate-indexed > audit (lane AG) should carry it, and its predicate is "a `SHIPACTION_DEPLOYSPY` command in the > turn's command stream", which is a *stream* predicate rather than a save predicate — a category > the audit does not yet have a column for. ### 1.5 `deat` and `spies2` are written by the same two instructions — `spies2` is closed `ServerSystem::AddSpy` = **`0x007514c0`**, 48 bytes, complete: ``` 007514c3 mov edx,[ebp+8] ; the spy 007514c6 test edx,edx / je ret 007514ca test ecx,ecx / jne 007514ce xor eax,eax ; this == NULL -> store 0 007514d2 mov eax,[ecx+4] ; the system's own registry handle 007514d5 mov [edx+0x10],eax ; *** spy->deat = system handle *** 007514d8 add edx,4 ; &spy->sid 007514db push edx 007514dc add ecx,0x1cc ; &system->spies2 007514e2 call 0x59f1a0 ; push_back (lane AI3's wrapper) ``` and its exact mirror `ServerSystem::RemoveSpy` **`0x0074f550`** sets `spy->deat = 0` and erases `spy->sid` from the same `+0x1cc` vector. `ServerSystem +0x1cc` is `spies2` in `objects/layouts.md`. `AddSpy` has exactly one caller (`0x008874d4`, inside `DeploySpy`) and no vtable slot, so **this is the only way `deat` ever becomes non-zero at runtime.** > **`spies2` is the per-system vector of DEPLOYED spy ids**, and `deat` is its reciprocal > back-pointer. They are written by the same function, two instructions apart. > > - Lane L3 §6: *"`spies2` is therefore not the spy list"* — **half right and worth correcting in > place.** It is not the spy *manager's craft* list; it is the *system's deployed-spy* list. L3's > inference from "this save has a spy and `spies2` is still empty" was sound about what `spies2` > is not, and the alternative it offered second ("or it only fills for a deployed spy") is the > right one. > - Lane AC's P4 (*"fills only for a deployed spy"*) is **confirmed, and now from the writer rather > than by elimination.** > > This retires one third of lane W's rule-6 flag on `spies2` / `SysMem` / `mts` **statically**, and > §5 exercises it live. ### 1.6 `vslot14`'s roll is a different gate — and it excludes independent colonies For the record, because target choice depends on it. `ServerSpyManager::vslot14` `0x0088db80` (phase 33) loops the same vector and requires `spy.deat != 0`, `sys.PID != 0`, and **`sys.indi (+0x1c8) == 0`** (`ServerSystem_IsIndependent 0x00743fa0`) — *not* an independent colony — plus `spy.cm (+0x24)` in 1..4, before its `Chance` at `0x0088dc43`. Lane AG has decoded that site fully; the consequence here is only that **an AI-empire target exercises strictly more than an Independent Colony**, which is why this lane went looking for an AI-owned belt rather than taking the easy NPC one. --- ## 2. The corpus, counted before measuring (rule 28b) `verify/save-reader/save_reader.py --json` over all **22** saves in `verify/results/saves/`. | | count | |---|---| | saves with any `spymgr.spy` record | **3** (L3's turn-15, AC's turn-20 and turn-22) | | of those, `deat != 0` | **0** | | saves satisfying the full detection-roll predicate | **0 of 22** | | **the failed conjunct** | **`spy.deat != 0`** | The integrator's value-domain census (`value-domain-census.md`) reaches the same 0 independently and more strongly: `deat`, `sdet`, `sdo`, `cbh`, `cm`, `cmo` are all in the 234 fields that have only ever held one value, so the count is 0 **by construction**. **Two things fell out of the same pass, and the first is the load-bearing one.** * **`ARes2 > 0` and `TerrFl & 1` agree on every one of 616 system records** (22 saves × 28 systems), with no exception in either direction. That is a save-side confirmation of §1.2's reading of an unnamed flag bit, obtained without a debugger, over a sample large enough to be worth something. It got stronger on the state this lane built (§4): there, `TerrFl` also takes the values **2** and **3**, and `ARes2 > 0` still coincides exactly with `TerrFl & 1` — so bit 1 is a second, independent terrain feature and bit 0 is not merely "TerrFl is non-zero". * **The belt was never the missing conjunct.** **All 22** corpus saves have at least one system with a usable belt (5 to 15 of 28), and **two of them — `human-turn2-orders.sav` and `human-turn3-noderoute.sav` — already have four AI-owned belt systems.** Lane AC's map had none, and the campaign generalised from it. Those two saves are `PTech 0 / ResM 1.0`, so the spy tech chain is tens of turns away and they were not a usable shortcut; but "no AI colony can have a belt" was a fact about lane L3's one map, exactly the rule-28 practice-3 trap. --- ## 3. Predictions Committed to `sots-engine` `docs/AS-predictions.md` at `c172c99` before the build, with the addendum at `61875a4` before the measurement. Summarised here; the falsification table is in that file. | | prediction | |---|---| | **P1** | an AI-owned belt system exists within ≤ 5 map regenerations at lane L3's custom settings | | **P2** | the predicate of §2; corpus count 0 of 22; failed conjunct `deat != 0` | | **P3** | the roll fires: one `draw_sites` row at `ret_rva 0x00887c8f`, `Chance`, `calls = 1`, `strategic = true` | | **P4** | it costs **1** word; `no_draw_calls = 0` | | **P5** | the tail bracket goes 0 → **1**; the trade half contributes 0 (`tscr` 252, no fleet at a sector node) | | **P6** | `sdet` most likely stays −1 (p ≈ 0.993); **`sdo` moves off its corpus-constant 0 to ≈ 0.007**, and that is the save-side confirmation | | **P7** | `spies2` non-empty in the target system only | | **P8** | the `hooks=off` pair reproduces byte-identically in two fresh processes | | **P9** | `probes=8` is byte-neutral on this workload too | --- ## 4. The workload — built from a cold main menu in one session Shim build **`as-c172c99-20260909T0205Z`**, cross-built on CT111 in this lane's own directory `/srv/re-lab/build/sots-engine-as` (`rm -rf build-host build-shim` before configure — rule 24), staged to `/srv/re-lab/shim/dist-as`, deployed to `C:\SOTS\shimdist-as` on VM144. Exports checked identical to the real `binkw32.dll` (66 names). **Guest adoption check, first, before anything else.** `ref-turn2.sav`, one End Turn, `hooks=off`, this lane's own build: ``` (Autosave EndTurn).sav bb4fd9ac89f41e3bc0db2af08b18ce83417521ac4bcee695fc9fa6ce16e30948 66,732 (Autosave).sav 978041acd168b56ed8eb3f5e42e78d5e70eae6e6517d75e659a5eb7ca3d60921 67,219 ``` Both **identical to the published oracle** in `verify/results/saves/determinism-hashes.txt`. The binary and the guest are sane. ### 4.1 The map — zero regenerations were needed New custom game at lane L3's settings (28 stars, 5 ly, 2 players Human vs Tarkas, treasury 9,850,000, **10 initial colonies, 15 initial technologies**, econ 148 %, research 146 %), species set explicitly to Human on the launch card (it defaults to Random — L3's note holds). **The first map generated had two AI-owned belt systems.** The brief expected several regenerations; P1's estimate of ≤ 5 was never tested because none were needed. | SysID | name | PID | `ARes2` | `TerrFl` | note | |---|---|---|---|---|---| | **400** | **Kepler** | **32 (AI)** | **2435** | **1** | the target — 3.5 ly from my colony Midway | | 224 | Delta Pavonis | 32 (AI) | 1537 | 1 | the spare | | 272 | Solaris | 0 | 2773 | 1 | unowned — `sdo` would be forced to 0 here (§1.3) | | 160 / 480 | Piscium / Pascal | 16 (me) | 2448 / 2522 | 1 | mine | | 336 | Copernicus | 0 | 2359 | **3** | bit 1 also set | | 128 | Downbelow | 0 | 1965 | 1 | | | 384 / 448 / 464 | Nunki / Mu Bootis / Jinx | — | **0** | **2** | **bit 1 set, bit 0 clear, no belt** | That last row is the one that matters for §1.2. In the whole 22-save corpus `TerrFl` had only ever been 0 or 1, so "bit 0" and "non-zero" were indistinguishable. This map has three systems at `TerrFl = 2` with `ARes2 = 0` and one at `TerrFl = 3` with `ARes2 = 2359`. **`ARes2 > 0` still coincides exactly with `TerrFl & 1`, and now it is bit 0 specifically, not "TerrFl is set".** ### 4.2 The path to a deployed spy, with what each turn cost The four techs came faster than lane L3's recipe suggested because `CCC_FTLBrdB` was in the 15-technology roll again (it appears to be common) and because the top-of-screen **Savings ↔ Research slider at its right end turns a 2-turn tech into a 1-turn tech.** | turn | what | |---|---| | 2 | new game; research `CCC_SpyBm` (`CCC_FTLBrdB` already researched from the initial roll) | | 4 | `CCC_SpyBm` done; research `IND_OrbFound` | | 6 | done; research `IND_CruisCon` (2 turns → 1 with the research slider at max) | | 7 | done; research `IND_SlvgTech` | | 8 | **all four techs**; design a Cruiser with the `Repair and Salvage` mission section; queue it | | 9 | the tender exists; fleet panel's `Special` → `Build Spy` → `Select All` → `OK` | | 10 | **the spy craft exists**: `nspy 1`, `deat 0`, `sdet -1`, `sdo 0`, `atto` = the tender — the corpus state | | 10 | `Move` the fleet to Kepler (`Dest: Kepler, 4 Turns`) | | 14 | **fleet arrived**: `Flt.LocID = 400`, read from the autosave, not the panel | | 14 | `Special` → **`Deploy Spy`** — the menu entry is present, which is itself the belt gate answering | | 15 | order applied. **`deat = 400`, `sdet = -1`, `sdo = 0.0084`, `spies2 = [1]` at Kepler** | Two things about turn 15 are worth stating separately, because they are results rather than steps. **(a) `sdo = 0.0084`, and that is §1.3's formula evaluated, not fitted.** The prediction was 0.007 for a quiet system (`0.7 × 0.01`). The measured value is `0.7 × 0.012 = 0.0084`, i.e. `countC = 2` in the third term. So the *form* — a 0.7 multiplier over a 0.01 base plus 0.001 per unit of a counter — reproduces exactly, at a value no run of this campaign has ever produced, on a state nobody had built. The identity of `countC` is not established (it is 2 at a system holding one foreign colony and my one-ship fleet); the constant and the shape are. **(b) `spies2` filled, in exactly the system the writer says.** Reading all 28 systems of the turn-15 autosave: `spies2` is `count = 0` in 27 of them and `count = 1, element = 1` at **Kepler**, where `1` is the spy's `sid`. This is the **first non-empty `spies2` in the corpus** and it matches `ServerSystem::AddSpy` instruction for instruction (§1.5). --- ## 5. The measurement All three runs load the **same file**, `MyGameas1spydep.sav` (sha256 `854a10fa1ea602f0f7909f9bf9154942e56ea5fa1db4cce47d6d32bf53952e08`, 76,705 B, turn 15, `--strict` clean, 0 errors / 0 warnings), in a **freshly launched process**, and press End Turn **once**. Repo copy: `verify/results/saves/as-turn15-spydeployed.sav`. ### 5.1 The oracle pair (rule 26) — and what it says honestly | run | config | `(Autosave EndTurn).sav` (the input, re-saved) | `(Autosave).sav` (post-turn) | |---|---|---|---| | **A** | `hooks=off` | `98e45d3745b91450…` 76,706 | `262f8bda97c9511d…` 77,685 | | **B** | `hooks=off`, fresh process | `98e45d3745b91450…` **identical** | `e34775a757e6fb3d…` 77,649 **differs** | | **M** | `probes=8` | `98e45d3745b91450…` **identical** | `262f8bda97c9511d…` **identical to A** | **The pre-turn save is byte-identical in all three**, which proves the input state — including any pending order — was the same in every run. The post-turn saves are not. **A vs B differs in exactly 22 leaves, and every one of them is inside one AI player.** `state_checksum --no-audit` localises them: ``` /Summary/Checksum 985948837 -> 985948925 /Sim/turnstats/history/hist[1]/stats[15]/tch 46 -> 45 /Sim/players/Player[32 "Revenge Fleet"]/TechTree/St[62] 2 -> 3 /Sim/players/Player[32 "Revenge Fleet"]/TechTree/TResDone[62] 0 -> 22469 /Sim/players/Player[32 "Revenge Fleet"]/TechTree/St[64] 4 -> 2 /Sim/players/Player[32 "Revenge Fleet"]/TechTree/TResDone[64] 22469 -> 0 /Sim/players/Player[32 "Revenge Fleet"]/TechTree/TAcq[64] 16 -> -1 /Sim/players/Player[32 "Revenge Fleet"]/TechTree/TiAcq[64] 45 -> -1 /Sim/players/Player[32 "Revenge Fleet"]/ResTNm '' -> 'DRV_NodFoc' … plus that player's Events/EvNxID/otch bookkeeping for the same pick ``` In A the AI completed `Overthrusting`; in B it is part-way through `DRV_NodFoc`. **Nothing else moved** — not one leaf of my empire, not `deat`, `sdet`, `sdo`, `spies2`, `trdmgr` or any fleet. That is precisely the mechanism `2026-09-08-ai-seed-per-process.md` established: each AI `StrategyClient`'s generator is seeded with a word that differs in every process, so its research pick is a per-process outcome. Board row 326 recorded the same signature on `turn1-state → turn2` — *"one shadow empire's research pick plus the derived checksum"*. **So the control agrees with itself modulo the known non-deterministic leaves, which is the bar the brief set — and it is a weaker bar than byte-identity, which must be said plainly.** What is established is: on this input, in two fresh un-instrumented processes, every leaf outside one AI player's research bookkeeping reproduced exactly. What is **not** established is byte-identity, and this workload cannot establish it until the client seeds are pinned (the resolution's Rung C). **And the instrumented run M came out byte-identical to A.** Read carefully, that is: * strong for the thing that matters — `probes=8` changed **no leaf** of the sim, the spy, or my empire, on the same input, which is the byte-neutrality claim P9 makes; * and, for the AI-pick leaf specifically, a **coincidence of size 1/k** (rule 26's corollary): M happened to draw the same AI seed outcome as A. It is not evidence about the instrument in that one leaf class, and I am not treating it as such. ### 5.2 The instrument was armed, and one probe reading zero is the point `shim.log`: seven draw-site detours `create=MH_OK enable=MH_OK`; `probe: installing 8 of 12`; all eight `MH_OK`; **no `COVERAGE:` line**. `draw_site_overflow = 0`. | probe | entries this turn | |---|---| | `Game::ServerSpyManager::vslot13` `0x008877b0` | **1** | | `Game::ServerSpyManager::vslot14` `0x0088db80` | 1 | | `Game::ServerTradeManagerImpl::vslot13` `0x0088ef80` | 1 | | `Game::ServerTradeManagerImpl::vslot15` `0x0082cca0` | 1 | | **`Game::SpyManager::Slot13RngCallee` `0x008408e0`** | **0** | | `Game::TradeManager::Slot13RngCalleeA` `0x00820ca0` | 0 | | `Game::TradeManager::Slot13RngCalleeB` `0x0088b440` | 0 | | `Game::ServerTradeManager::CreateRaidEncounter` `0x008938a0` | 0 | | indices 8–11 (`GenerateTradeRaidEncounters`, three controls) | **NOT INSTALLED** — `probes=8` installs 0–7 | > **That table is the lane's methodological point in one line.** The detection roll fired, and the > entry probe on `SpyManager::Slot13RngCallee` read **0** on the same turn. Four lanes read that > zero and concluded the spy subtree was draw-free. It was never a statement about the subtree; it > was a statement about which function the call sits in. ### 5.3 The detection roll — it fires, and it costs one word `draw_sites`, the post-turn autosave marker, the row verbatim: ``` entry = Chance ret_rva = 0x0048_7c8f (VA 0x00887c8f — the instruction after `call 0x8e6dd0` at 0x00887c8a) calls = 1 words = 1 no_draw_calls = 0 strategic = true ``` and the same site in the per-call-site report, with the owner resolved: ``` * STRAT Chance call 0x00887c8a +0x4da calls=1 words=1 ``` The boundary ledger for the same turn: ``` BeginProcessTurn 345 -> 345 0 ProcessTurn 345 -> 363 18 OnAllCombatDone_Tail 363 -> 364 1 Autosave 364 -> 364 0 ``` > **`OnAllCombatDone_Tail` costs 1 word, against 0 on every turn any lane has ever measured, and the > whole of it is the detection roll.** Turn total 19 words; the per-site sum after removing helper-internal rows is **19**; **residual 0**. The tail's one word is the only site in the tail that fired: no row at `0x00820e18` or `0x0088b613` (trade 13 — `tscr` is 252 in this game and no fleet stands on a sector node, so lane AC's predicate says 0 and it is 0), none at `0x0082cdb8` (trade 15), none at `0x0088dc43` (spy 14 — `cm = 0`, lane AG's failed conjunct), and none of `P`'s three. **Predictions, scored.** P1 held trivially (0 regenerations, not ≤ 5). **P2 held.** **P3 held exactly.** **P4 held exactly** — one word, `no_draw_calls = 0`, and the reasoning behind it (four widened-float literals plus `Chance`'s cost at interior *p*) was written down before the run. **P5 held** — 0 → 1. **P6 held in both halves**: `sdet` stayed −1 (the roll failed, as a 1.68 % roll should), and `sdo` moved from its corpus-constant 0 to **0.0084** on the deploy turn and to **0.0168** after the measured turn — an increment of exactly 0.0084 both times, which is the accumulator running. **P7 held.** **P8 held only modulo the AI-seed leaves** (§5.1). **P9 held.** ### 5.4 The second instrument agreed — which is what makes this more than one number The coordinator asked for a save-visible confirmation independent of the bracket. There are two: * **`sdo` moved and by the right amount.** 0 → 0.0084 → 0.0168. Only branch D writes `sdo`, and it writes it immediately before the gate, so a moving `sdo` proves the branch ran; the *equal* increments prove the accumulator, not a one-off. * **`spies2` filled in exactly one system.** That is the deploy half rather than the roll, but it confirms the `deat` reading the whole predicate rests on. `sdet` did **not** move, and per P6 that was the predicted outcome, not a failure. --- ## 6. Coverage — what this did not touch, said as loudly as what it did * **One measured turn, one spy, one target system, one fleet of one ship.** The cost is 1 word on that turn. The loop is over the spy vector, so the cost should be one word per deployed, undetected spy at a system with a usable belt — **that is a reading, not a measurement.** Rule 20: do not fit a constant to one observation. A second spy at a second belt system separates per-spy from per-turn and costs one more deploy. * **The `sdo` arithmetic is validated only in its first two terms.** `countA`, `countB` and the species multiplier were 0/0/1.0 here, and `countC = 2` is inferred from the value, not observed. A Zuul or Liir owner (`+0x5c` = 5 or 6) would exercise the 0.75/0.5 branches; a system with enemy ships present would exercise `countA`/`countB`. * **The auto-detect arm was never taken.** `(TerrFl & 1) == 0` at the target has not been reached — it would need a belt to be mined out from under a deployed spy. So §1.2's polarity claim rests on the instruction stream plus the deploy-time refusal, not on a measured firing. It is the single most load-bearing unmeasured branch in this document. * **`P` (`0x008408e0`) and its three draws stay unfired**, and the failed conjunct is now `sdet`, not `deat`. From this save the recipe is arithmetic rather than a guess: `sdo` grows 0.0084/turn, so `P(detected)` is 12 % after 5 more End Turns, **38 % after 10, 65 % after 15, 85 % after 20** — then `P` runs 3 turns after `sdet` is stamped, provided the system is still foreign-owned. About 20 End Turns from `as-turn15-spydeployed.sav`, no clicking. * **`0x0078c97f` was decoded and not measured** (§6.1). * **`probes=11` was not used anywhere in this lane**, and no number here is comparable to lane AC's turn totals, which were taken at that configuration. * **No `Guard` region is declared by any hook in this family**, the same gap lanes Z and H reported: `undeclared = 0` in this trace is vacuous. * **One control run was lost to the harness and is reported rather than hidden.** A third `hooks=off` run mis-detected the post-turn End Turn button as "turn still running" and clicked End Turn three times, advancing the line to turn 18. It was discarded; nothing in §5 uses it. The defect was in this lane's own click automation (the brightness test had the wrong polarity: after a completed turn the button is *brighter*, not dimmer), which is rule 19 pointing at the operator rather than at the detour. Its lesson is worth carrying: **verify the turn number changed, not a button's colour.** ### 6.1 A draw site nobody has — the deploy's own `NextFloat` Decoded here and **not measured**, so it is a rule-6 hypothesis with a hook site attached: ``` 0078c975 mov ecx,[esi+0x16c] ; esi = StrategyServer -- the STRATEGIC generator 0078c97c add ecx,4 0078c97f call 0x0047d830 ; Mars::RNG::NextFloat -- 1 word, return address 0x0078c984 0078c984 fld qword [0x009e21b0] ; 6.283185482025146 == 2*pi spy.cbh = NextFloat() * 2*pi 0078c9bb call edx ; -> ServerSpyManager::DeploySpy, vtable slot 7 ``` inside the `SHIPACTION_DEPLOYSPY` handler `0x0078c930`, which is entry 9 of the stack-built ship-action function-pointer table (lane B6's indirection class — no direct callers, no vtable slot, invisible to every call-graph sweep this campaign has run). It fires **once per Deploy Spy order applied**, inside `ApplyAllTurnCommands`, so it lands in the **`ProcessTurn`** bracket, not the tail's. Its predicate is not a predicate on save fields at all — it is *"a `SHIPACTION_DEPLOYSPY` command in this turn's command stream"*, a **stream** predicate, which lane AG's gate-indexed audit has no column for and should grow one. To measure it costs one run: load `verify/results/saves/as-turn14-predeploy.sav` under `probes=8`, select Kepler, `Special → Deploy Spy → → OK`, End Turn, and read `draw_sites` at `ret_rva 0x0078c984`. This lane attempted it and lost the run to the map UI (Kepler and Midway sit within four pixels of each other at the zoom the map opens at, and Midway wins the hit test), then chose to report the site honestly rather than rush a fourth process. Prediction, committed here for whoever takes it: **one call, one word, `strategic = true`, in the `ProcessTurn` bracket**, and the same turn's tail still costs exactly 1 — the deploy and the first detection roll happen on the same End Turn, because `ApplyAllTurnCommands` runs before `OnAllCombatDone_Tail`. --- ## 7. VM144 as left **Restored and verified by screenshot at the main menu**, profile `re`, 2026-09-09 00:03 local. * `C:\SOTS\binkw32.dll` restored to lane L3's build from `C:\SOTS\shimdist-l3\binkw32.dll` (sha256 `479B8614D2417603…`, byte-identical to what this lane found in place). * `C:\SOTS\shim.cfg` restored from `C:\SOTS\ui\preAS-shim.cfg` (L3's `shim.cfg.l3probe`). * The three autosaves restored **byte-identical** from `C:\SOTS\ui\preAS-SavedGames` (`1985E6F4…` / `24B2E072…` / `5EC80C1E…`). * `C:\SOTS\shim.trace.jsonl` removed. **`SavedGames` is now 15 files, not 13**, so **every Load-dialog row position has moved again** — screenshot the dialog, do not reuse a remembered row. The two additions are this lane's, and they are worth leaving because they are the only deployed-spy states in existence: | file | turn | why it is worth keeping | |---|---|---| | `MyGameas1predeploy.sav` | 14 | fleet + tender + docked spy standing **at** an AI belt colony, one click from a deploy — the input for §6.1 | | `MyGameas1spydep.sav` | 15 | **the deployed spy**: `deat 400`, `sdet -1`, `sdo 0.0084`, `spies2 = [1]` at Kepler | At the 15-file set, verified by screenshot this session: `MyGameas1predeploy` is at **(400, 347)** and `MyGameas1spydep` at **(400, 376)**; `OK` is unmoved at (682, 624). Also left in place, all harmless: `C:\SOTS\shimdist-as\` (build `as-c172c99-20260909T0205Z`, 64 files), `C:\SOTS\ui\asgo.ps1`, `C:\SOTS\ui\ashash.ps1`, `C:\SOTS\ui\preAS-shim.cfg`, `C:\SOTS\ui\preAS-SavedGames\` (13 files), `C:\SOTS\ui\asfetch.sav` / `asgrab.jsonl` / `asgrab.log` (scratch). **`click_helper.ps1` was not touched** — this lane needed no new verbs, which is the first VM lane in a while that did not. ### 7.1 Guest notes worth carrying forward * **The map's hover readout works with a plain `move`** on VM144 — lane AC's `jmove` jiggle was not needed here. Enter Move mode first; the readout renders at the hovered star, not at the cursor. * **Finding a named system on the star map is solvable arithmetically instead of by hunting.** Hover four systems, read their names, and least-squares-fit an affine map from the save's `Pos` vectors to screen coordinates; the residuals came out ≤ 8 px on 5 points, which is enough to click a specific star first time. The script is in this lane's scratch notes and the method is the reusable part. Its failure mode is systems that project within a few pixels of each other (Kepler and Midway), where the nearer one wins the hit test. * **The End Turn button is BRIGHTER after a turn completes, not dimmer.** Any "is the turn still running?" test built on that button's colour has the polarity backwards. Poll the **autosave mtime** and the **turn number in the ticker**, never the button. * Accepting the AI's non-aggression offer (turn 6 here) stopped the combat encounters that were interrupting every third End Turn, and cost nothing this lane needed. --- ## 8. Corrections to earlier findings (rule 11) * **`2026-09-09-tail-draws.md` §0.7 and §7, and lane AC §2.2: "the detection roll is inline in `vslot13`".** It is a direct `E8` call to the `Chance` entry point sitting in `vslot13`'s body. The instrument advice that followed from it is right; the stated reason is not. §1.1. * **Lane L3 §6: "`spies2` is therefore not the spy list".** Half right. It is not the spy manager's craft list; it is the **system's deployed-spy id vector**, written by `ServerSystem::AddSpy` in the same two instructions that set `deat`. L3's own second alternative was the correct one. §1.5. * **Lane AC §5: "the textbook target — an enemy colony with a belt — does not exist in this game."** True of that map and read across the campaign as if it were true of the game. Two corpus saves already had four AI-owned belt systems, and the first map this lane generated had two. §2. * **Lane V2 §3.1's spy-13 row** listed `0x00887c8a` correctly and is **vindicated**, like its trade-13 row before it. Its "never observed firing" note is now discharged for this one site. * **Ghidra's size for `0x008877b0` is 1,869 bytes; the next function starts at `0x00887f30`, i.e. 1,920.** The detection roll at `0x00887c8a` is inside both, but the 51-byte shortfall is rule 17 again, on the very function this lane exists to read. * **This lane's own pre-registered §1.1, corrected here rather than edited there (rule 12).** `docs/AS-predictions.md` calls `0x0083ce50` "the deploy validator" and says its `0x1000000` refusal bit *is* the `Can't deploy spy: Spy requires an asteroid belt to hide.` message. Read properly afterwards, `0x0083ce50` is a **shared placement validator** with ~21 refusal bits (colonise, mine, dump ore, too many things present, …), reached from `0x00760ce0` and the order path `0x00849460`, and it is **not on `ServerSpyManager::DeploySpy`'s call chain at all** — that chain is `SHIPACTION_DEPLOYSPY 0x0078c930` → vtable slot 7 `0x00887410` → `AddSpy`, and it never calls `0x00743f80`. What survives, and is all §1.2 needs, is that **`0x00743f80` bit 0 gates a belt-shaped refusal there and the detection roll here**; *which* action that refusal belongs to was over-claimed and is not established. The empirical half is unaffected: `Deploy Spy` was offered and accepted at a belt system, and the corpus agreement of `ARes2 > 0` with `TerrFl & 1` is 616 of 616. --- ## 9. Artifacts | what | where | |---|---| | **the deployed-spy save** — first `deat != 0` and first non-empty `spies2` in the corpus | `verify/results/saves/as-turn15-spydeployed.sav` (76,705 B, `--strict` 0 errors / 0 warnings, sha256 `854a10fa1ea602f0…`) | | the pre-deploy save, one click from a deploy at an AI belt colony | `verify/results/saves/as-turn14-predeploy.sav` (75,908 B, sha256 `53986f8511cbf992…`) | | the instrumented trace (`draw_sites` + `probe_entries` + the boundary ledger) | `verify/traces/as-probes8-turn15-turn16.jsonl.gz` | | shim log for that run (probe and detour install status) | `verify/results/shim/as/as-probes8-run.shim.log` | | addresses this lane mints | `ghidra/addresses.d/as.json` (7 entries; 6 `verified`, 1 `mapped`) | | predictions, committed before the build | `sots-engine` `docs/AS-predictions.md` (`c172c99`, addendum `61875a4`) | | instrument | `sots-engine` `src/shim/shim.cfg.hp8` and `shim.cfg.hoff`, **both unchanged** — this lane wrote no engine code | ### 9.1 The hashes, for the record ``` input MyGameas1spydep.sav 854a10fa1ea602f0f7909f9bf9154942e56ea5fa1db4cce47d6d32bf53952e08 A/B/M (Autosave EndTurn).sav 98e45d3745b914506fc4c409ad5a5ad0938a2003ab4c845421a99a8065149c3a A (Autosave).sav 262f8bda97c9511d8d2b41e6a8a0582d6f5364c2b01b1caf99e639d0e4f53ebe B (Autosave).sav e34775a757e6fb3df647d7a589e8b1819b90bea094196a9e3cc819e51ed63257 M (Autosave).sav 262f8bda97c9511d8d2b41e6a8a0582d6f5364c2b01b1caf99e639d0e4f53ebe ``` **This is not a calibration pair for the standalone**, and it must not be entered in `determinism-hashes.txt` as one. A ≠ B, in the AI-research-pick leaf class, so the turn is reproducible only once the client seeds are pinned. What it *is* is a reproducible **input** and a turn whose every non-AI leaf reproduces across three processes and two configurations. --- ## Proposed `campaign/board.md` rows New row: ``` | THE SPY DETECTION ROLL FIRES - 1 word, gated on an asteroid belt; `spies2` and `deat` closed | verify | verified | high | 100% | 2026-09-09 | **Lane AS, VM144, build `as-c172c99-20260909T0205Z`, `probes=8` (never 11).** `Mars::RNG::Chance` at **0x00887c8a**, in the BODY of `ServerSpyManager::vslot13` (tail phase 23 call 9), fired **1 call / 1 word / no_draw_calls=0 / strategic=true** at `ret_rva 0x00887c8f`; `OnAllCombatDone_Tail` went **363 -> 364**, against 0 on every turn any lane has ever measured. Turn total 19, per-site sum 19, **residual 0**. THE ENTRY PROBE ON `SpyManager::Slot13RngCallee 0x008408e0` READ **0 ON THE SAME TURN** - that zero was never a statement about the subtree, only about which function the call sits in (rule 28 practice 4, demonstrated rather than argued). **CORRECTION: the roll is NOT inline** (the resolution and AC §2.2 both say so) - it is a plain `E8` to the `Chance` entry point in the caller. **THE PREDICATE**, decoded to save fields: `spy.deat != 0 && spy.sdet == -1 && sys(deat).ARes2 > 0 && (sys(deat).TerrFl & 1)`; **corpus count 0 of 22, failed conjunct `deat != 0`**. `ServerSystem_BeltUsableFlags 0x00743f80` (16 bytes) returns `TerrFl` with bit 0 cleared when `ARes2 <= 0`; `ARes2>0` and `TerrFl&1` agree on all 616 corpus system records AND on the new map, which also has `TerrFl = 2` and `3`, so bit 0 is the belt specifically. **POLARITY: belt gone => NO DRAW AND THE SPY IS DETECTED ANYWAY** (`je` past the roll to `sdet := turn`) - a zero here is two different results. Cost: `sdo` starts at 0, `SpyCraft_AccumulateDetectionOdds 0x0081f570` ACCUMULATES `0.7*(0.01 + [cA]0.01 + [cB]0.02 + 0.001*cC) * species(1.0/0.75/0.5)` and CLAMPS AT 1.0, so `Chance` costs 1 word until sdo reaches 1 and 0 thereafter. Measured `sdo` 0 -> 0.0084 -> 0.0168 (= 0.7*0.012 exactly, a value no run had produced). **`spies2` IS CLOSED**: `ServerSystem::AddSpy 0x007514c0` sets `spy.deat = system handle` and pushes `spy.sid` into `ServerSystem+0x1cc` in the same two instructions - `spies2` is the per-system DEPLOYED-spy id vector; L3's "not the spy list" was half right, AC's P4 was right. Measured: `count=1 element=1` at Kepler, 0 in the other 27. **NEW DRAW SITE NOBODY HAS**: `0x0078c97f` NextFloat in the SHIPACTION_DEPLOYSPY handler `0x0078c930` (`cbh = NextFloat()*2pi`), strategic generator, in the ProcessTurn bracket - decoded, NOT measured, and its predicate is on the COMMAND STREAM not the save, a column the gate-indexed audit lacks. Oracle: input byte-identical in 3 processes; A vs B differ in **22 leaves, all inside one AI player's research pick** (the known per-process client seed, resolution 2026-09-08) - so this is NOT a calibration pair and must not enter `determinism-hashes.txt`. `probes=8` run came out byte-identical to control A. Saves: `as-turn15-spydeployed.sav`, `as-turn14-predeploy.sav`; findings `findings/subsystems/spy-detection-roll.md` | ``` Edits to existing rows: - **Row 62** (guest holders) — `VM144 = FREE (lane AS released 2026-09-09; restored to L3's build + shim.cfg + the three pre-AS autosaves byte-identical, main menu verified by screenshot). SavedGames is now 15 FILES - AS added MyGameas1predeploy (400,347) and MyGameas1spydep (400,376), so ROW POSITIONS HAVE MOVED AGAIN; screenshot the dialog. AS left C:\SOTS\shimdist-as + ui\as{go,hash}.ps1 + ui\preAS-*; click_helper.ps1 untouched.` - **Row 207** (V2's eight sites) — append: `FIRED 2026-09-09: 0x00887c8a (spy 13), lane AS, 1 word, behind a deployed spy at a foreign colony with a usable asteroid belt. V2 vindicated a second time. Five of the eight still unfired: P's three (0x00840929 / 0x008409c7 / 0x00840a3c, gate now `sdet != -1` and turn-sdet>=3, ~20 End Turns from as-turn15-spydeployed.sav), 0x0088dc43 (spy 14, gate `cm in 1..4`, lane AG), 0x0082cdb8 (trade 15). AND THE INVENTORY IS SHORT ONE: 0x0078c97f in the SHIPACTION_DEPLOYSPY handler draws a NextFloat on the strategic generator and is in no sweep, because it hangs off the stack-built ship-action table.` - **Row 373 / the `spies2` rule-6 flag** — `CLOSED 2026-09-09 by lane AS, from the writer and then live. spies2 (ServerSystem+0x1cc) is the per-system vector of DEPLOYED spy ids: ServerSystem::AddSpy 0x007514c0 sets SpyCraft+0x10 (`deat`) to the system handle and push_backs SpyCraft+0x4 (`sid`) into +0x1cc; RemoveSpy 0x0074f550 is the exact mirror. Measured non-empty for the first time in the corpus: count=1, element=1, in the deploy system only. `SysMem` and `mts` are untouched and their flags stay up.` - **Row 186 / `tail-rng-ledger.md`** — append to the existing qualification: `THIRD site outside the firing-indexed table has now fired: the spy detection roll (tail T23c9, lane AS). The pattern is now three for three - hives, the raid roll, the detection roll - and each fired on the first state built for it. The gate-indexed audit is the fix, not a longer table.`