706 lines
45 KiB
Markdown
706 lines
45 KiB
Markdown
# 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) := <the float argument>
|
||
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 <ServerSpyManager::vslot13>+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 → <ship> → 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.`
|