Lane SV recovered the script-object subsystem statically and predicted that
SVSOSwarmQueen::RegisterHives takes one RNG_NextInt per new hive inside
StrategyServer::BeginProcessTurn -- which runs inside lane Z's autosave bracket
and outside every one of its subtotals, so a draw there had never been
attributed by anything.
Five nested trace hooks, each declaring the strategic generator as a region and
each carrying a model evaluated at entry so the record can disagree with it:
StrategyServer::BeginProcessTurn the unhooked interval, plus a region
over Frame so the increment is a fact
SVSOSwarmQueen::OnTurnBegin evt 0x13, vtable slot +0x60
SVSOSwarmQueen::RegisterHives predict_new_hives from the original's
own two predicates; reads the LO/HI
config pointers live
SVSOSwarmQueen::TickHives the NextQ slip and its three gates
SVSOSlaversRefuel::UpdateDifficultyTier tail phase 20; a 4-byte region over
CDiff so a store and its ABSENCE are
distinguishable (method rule 20)
Also watch.mode=snlv: one arming line moves slot 1 from the NVO map's _Mysize to
the target system's SnLv, and the arming sweep now prints SnLv with its decoded
per-player 2-bit levels for every system, which costs no debug register.
Measured on VM140: hive creation costs 2 words in BeginProcessTurn and the
residual outside the two turn drivers is 2, not 0; the next turn it is 0 again.
LO=20 HI=30. Spica's SnLv reads 0x200 with AFlags 0. The oracle reproduced
byte for byte with every one of these detours live.
Predictions and outcomes: docs/L1-predictions.md.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARBgSooAfokKUy6wKUKEyZ
14 KiB
Lane L1 — predictions, committed before the build
Written 2026-09-08 against main 0117495, before any code was written and before any VM run.
Instrument: src/shim/hooks/tail_rng.cpp (five new nested hooks), src/shim/hooks/watchpoints.cpp
(one new arming mode). Workloads: turn1-state.sav (hives), ref-turn2.sav (oracle control and
SnLv).
The point of writing this first is that every number below can be wrong in a way that is visible in the trace. Each prediction carries its falsifier.
0. What the static reading says, re-derived independently
Lane SV recovered all of this without running anything. Before predicting, its instruction reading
was re-done from dumps/sots.exe with objdump -M intel at real instruction boundaries (rule 17 —
disassembled past Ghidra's reported end to the int3 padding in every case). Everything below was
confirmed byte for byte, with two small corrections of address, not of substance.
StrategyServer::BeginProcessTurn0x007d98e0 .. 0x007d9ae8 (ret, thiscall, no stack args).inc [esi+0xc]at 0x007d990a isFrame++. The last thing the function does before its epilogue is the script two-step at 0x007d9ab8:vft[0x10](0x13, 0)thenvft[0x60]().+0x60is exactly what lane SV's jump table gives for event 0x13. Everything else in the body is two container walks (S+0x44systems,S+0x64fleets) and an event post (push 0x24).SVSOSwarmQueen_OnTurnBegin0x00529930 .. 0x0052999f (ret):RegisterHives→ prune-by-EggScio→TickHives→0x00505100.SVSOSwarmQueen_RegisterHives0x00527630 .. 0x00527761 (ret). Per system inGetGame()->+0x44..+0x48,[sys+0x184] == this->+0x4and no existing hive whose+0x4is that system; then
Correction to5276e1 mov ecx,ds:0xae0204 ; &LO 5276e7 mov edx,ds:0xae0208 ; &HI 5276ed mov eax,[eax+0xc] ; frame (eax = GetGame()) 5276f0 mov edi,[ecx] ; LO 5276f2 mov esi,[edx] ; HI 5276f4 add edi,eax ; add esi,eax 5276f8 call GetGame ; mov eax,[eax+0x16c] 527707 sub esi,edi ; bound = HI - LO 527709 lea ecx,[eax+4] ; ECX = &mt 52770f call 0x4271c0 ; RNG_NextInt <-- THE DRAW, one per new hive 527717 add eax,edi ; nextQ = frame + LO + draw 52771f call 0x523080 ; push_backsvsctob-writers.md§4.3: the draw call is at 0x0052770f (return address 0x00527714), not 0x0052770c — 0x0052770c is themov [ebp-0x14],esithat stores the bound. The substance is unchanged andGetGame()(0x00578050) returns theSframe:[+0xc]isFrameand[+0x16c]is the strategic generator, the same object lane Z's ledger watches.SVSOSwarmQueen_TickHives0x00527770 .. 0x005279a1 (ret). Four gate-failure edges (0x005277e3,0x005277fe,0x00527848,0x0052784f) all land oninc [esi+0x8]at 0x0052785a. Gates read[0x00ae0210](a frame floor), a queens-vector cap over(this->+0x24 − this->+0x20)/12, and[0x00ae0228]; the spawn arm reads[0x00ae0220]. No RNG primitive is called anywhere in the body.SVSOSlaversRefuel_UpdateDifficultyTier0x00515820. The stack table, read off the stores:{1,1,1} {50,2,3} {100,2,5}at[ebp-0x34],[ebp-0x28],[ebp-0x1c]. Only the threshold column is ever loaded (cmp [ecx],edx); the other two are dead in this function. The scan is
and the class's ctor at 0x0051a820 stores515893 xor eax,eax ; lea ecx,[ebp-0x34] 515898 cmp [ecx],edx ; jg 0x5158c2 ; first threshold > frame 51589c add eax,ebx ; add ecx,0xc ; cmp eax,3 ; jl 0x515898 <fall through to 0x5158a6 = THE EPILOGUE, NO STORE> 5158c2 test eax,eax ; jle 0x5158a6 ; index 0 -> NO STORE 5158c6 dec eax ; cmp eax,-1 ; je ... ; (redundant with the jle) 5158cc cmp [edi+0x38],eax ; je ... ; unchanged -> NO STORE 5158d1 mov [edi+0x38],eax ; CDiff = index - 1[eax+0x38] = 0xffffffff, which is exactly theCDiff = -1thatturn1-state.savcarries. Lane SV's off-by-one reading is confirmed independently:CDiffcan only ever hold 0 or 1.SVSOSlaversRefuel_HandleEvent0x0051a800 is nine instructions,ret 8:if (evt == 0x14) UpdateDifficultyTier(this).
1. The corpus fact this lane is built on
turn1-state.sav (Frame 1) has SvSctOb / EncID 10 / Hives count 0. turn2-state.sav
(Frame 2) has two hives, (HiveID 336, QueenID 0, NextQ 31) and (400, 0, 29).
turn3-state.sav has the same two with NextQ 32 / 30. The two systems with EggScio == 3 in
turn1-state.sav are 336 and 400.
So the End Turn taken from turn1-state.sav is the hive-creation turn, and the End Turn after it
is the slip turn. Both are one click.
P1 — hive creation draws, and lane Z's "residual exactly zero" is a statement about its workload
On the End Turn taken from turn1-state.sav:
SVSOSwarmQueen_RegisterHivesis entered exactly once, insideBeginProcessTurn, beforeStrategyServer::ProcessTurnruns.- It reports
hives_in = 0andhives_out = 2. - Its measured word cost is 2 — one
RNG_NextIntper new hive. (NextIntis a reject-on-(y & mask) > nloop, so 2 is the minimum; see the falsifier.) SVSOSwarmQueen_OnTurnBegin's cost is the same 2 (TickHivesadds nothing).StrategyServer::BeginProcessTurn's cost is the same 2.- The bracket residual is 2, not 0:
Autosave(endTurn=1) → Autosave(endTurn=0)total minusProcessTurnminus the tail equals 2. - The two hives written carry
NextQ31 and 29, matchingturn2-state.savexactly.
What this would mean. tail-rng-ledger.md's headline — "the residual outside the two turn
drivers is exactly zero" — is correct for turns 4, 5, 6, 18, 19, 20, 52-55 and 62-64 of two saves
whose hives already existed, and wrong as a statement about the code. That is the same shape as
"the tail draws nothing", which was true of eight turns and false of turn 64.
Falsifiers
| symptom | what it would mean |
|---|---|
RegisterHives not entered |
the evt-0x13 delivery does not reach SVSOSwarmQueen on this save. SV's evt -> slot map or its class table would need correcting. The entry probe separates this from "entered and drew nothing" (rule 20). |
entered, hives_in = 0, 0 words |
the draw is not taken — e.g. the bound HI-LO is 0, in which case NextInt may return without consuming. Lane Z's zero would stand and SV's claim would be wrong. |
entered, hives_in = 2 already |
the hives exist before BeginProcessTurn, so turn1-state.sav is not the pre-creation state I read it as. |
| cost > 2 | RNG_NextInt's rejection loop fired. Expected: with bound = HI-LO, mask = 2^ceil(log2(bound+1)) − 1, cost per draw is (mask+1)/(bound+1) words on average. A cost of 3 on two hives is unremarkable and does not falsify the claim; it falsifies only the literal "2". |
residual 2 but BeginProcessTurn cost 0 |
the draw is between the autosave markers but outside BeginProcessTurn. |
NextQ ≠ 31/29 |
the state I loaded is not the state turn2-state.sav came from (different queued orders). Does not falsify the draw; does kill P3. |
P2 — the control: the second End Turn costs 0 outside the drivers
On the End Turn taken from the state P1 produced (frame 2 → 3):
RegisterHivesis entered, finds both systems already referenced, and costs 0 words.TickHivesis entered, takes a gate-failure edge on both hives, costs 0 words, andNextQreads 32 / 30 afterwards — SV's slip rule, live.BeginProcessTurn's cost is 0 and the bracket residual returns to 0.
This is what makes P1 mean "creation draws" rather than "BeginProcessTurn always draws". If
P2's residual is also 2, the draw is not hive creation and P1's attribution is wrong even if its
number is right.
Falsifier for the slip specifically: if NextQ moves by anything other than +1 per hive, or if
TickHives costs words, the inc-only model is wrong.
P3 — LO and HI, which no save can show
The shim prints *(int*)0x00ae0204 and *(int*)0x00ae0208 at RegisterHives entry. Lane SV could
not follow these to an initialiser and correctly refused to fit them from one two-hive observation.
Reading them live closes the leaf outright.
Prediction: NextQ = frame + LO + NextInt(HI − LO) with frame = 2 reproduces 31 and 29, i.e.
LO + r₁ = 29 and LO + r₂ = 27, with both r in [0, HI − LO]. Falsifier: any (LO, HI) for
which those two are impossible.
P4 — CDiff: two edges, one of them unreachable by playing forward
Model, transcribed above:
| frame at the tail's evt 0x14 | scan index | stored |
|---|---|---|
| ≤ 0 | 0 | nothing |
| 1 … 49 | 1 | CDiff = 0 |
| 50 … 99 | 2 | CDiff = 1 |
| ≥ 100 | falls off the end | nothing |
Predictions, in increasing cost:
- P4a (free, this run).
UpdateDifficultyTieris entered once per End Turn onturn1-state.sav, stores0on the first (frame 2,CDiff−1 → 0) and stores nothing on the second (frame 3, unchanged). Thethis+0x38region shows the store and its absence. This is rule 20's distinction applied to a store: "entered and stored nothing" is not "not entered". - P4b (~48 End Turns). The first End Turn whose frame lands in 50…99 stores
CDiff = 1, and every later turn below 100 stores nothing. - P4c (~98 End Turns). At frame ≥ 100 the function is still entered every turn and stores
nothing;
CDiffstays 1 forever. Tier 2 is unreachable. - P4d. The
frame ≤ 0edge is unreachable in a real game:Framestarts at 1 and the tail never runs before the first End Turn, which is exactly whyturn1-state.savcarries the ctor's −1. This is recorded as a code property, not a workload gap.
Falsifier for P4b/P4c: CDiff reading 2 at any frame falsifies the whole reading.
P5 — SnLv, one arming line
nvo-tshn-visible-owner.md §6 marks "the sensor branch is what refreshes Spica" as [H] because
SnLv (ServerSystem+0x24) is not serialised and has never been measured. The watchpoint module's
tshn mode already finds Spica by predicate at arm time; the new snlv mode points slot 1 at
sys+0x24 instead of the map's _Mysize, and additionally logs SnLv for all 28 systems at arm
time, which costs no debug register at all.
Prediction: Spica's SnLv has a non-zero 2-bit field for the player index its NVO record is
keyed on (4 on ref-turn2.sav), i.e. (SnLv >> 8) & 3 != 0; and Bismol's is zero for player 1.
That makes the sensor branch the live refresher of Spica ([H] → [V]) and settles which conjunct
fails at Bismol.
Falsifiers: Spica's field reads 0 → the refresh comes from somewhere else entirely and the model in
§5 of that document is wrong about the mechanism even though it fits the corpus. Bismol's field
reads non-zero → the failing conjunct is HasResearched(CCC_AdvSens) alone, which the corpus already
proves true, and the "outside the sensor envelope" alternative is dead.
P6 — the instrument is behaviour-neutral (rule 19), and I expect it might not be
Five new MinHook detours are installed. Lane H proved one detour on a clean prologue boundary changed the autosave by 4 bytes. So this is a genuine prediction and not a formality:
One End Turn from ref-turn2.sav with the full L1 hook set live reproduces the oracle:
(Autosave EndTurn).sav = bb4fd9ac89f41e3b…, (Autosave).sav = 978041acd168b56e….
If it does not, every number in P1–P4 is void and the correct report is which hook broke it, found
by bisecting the five. The hooks=off run on the same workload is taken as well, so the
hive-creation autosave has an uninstrumented twin to compare against.
Outcomes, appended after the runs (2026-09-08)
Recorded here beside the predictions rather than only in the finding, so the pair can be read
together. Full account: sots-re/findings/control-flow/hive-creation-rng.md and
sots-re/findings/subsystems/snlv-measured.md.
| prediction | outcome |
|---|---|
P1 hive creation draws 2 words in BeginProcessTurn; residual 2 |
held exactly. RegisterHives entered once at depth 2, predict_new_hives = 2, words 170 → 172; BeginProcessTurn 2, ProcessTurn 20. The draw-site ledger independently shows a new row at return address 0x00527714, NextInt, 2 calls / 2 words. |
P1 NextQ 31 / 29 |
held, via a route the prediction did not spell out: RegisterHives writes 30 / 28 and TickHives' slip takes them to 31 / 29 on the same turn. |
| P2 the next turn costs 0 outside the drivers | held. RegisterHives entered, predict_new_hives = 0, 0 words; BeginProcessTurn 0; bracket 192 → 211 = ProcessTurn's 19; residual 0. NextQ 32 / 30. |
P3 LO/HI read live |
LO = 20, HI = 30 ⇒ NextQ = frame + 20 + NextInt(10), inclusive. Both draws land in range (8 and 6). The three TickHives gate constants are 10, 5 and 3. |
P4a UpdateDifficultyTier entered every turn, stores on frame 2 only |
held. Frame 2: predict_path = store, region cdiff −1 → 0. Frame 3: predict_path = no_store_unchanged, 0 → 0, entered and stored nothing. |
| P4b / P4c the frame-50 and frame-100 edges | see the finding; the table scan was re-read independently and CDiff can only hold 0 or 1. |
P5 Spica's SnLv non-zero for its NVO key player |
held. AFlags = 0x0, SnLv = 0x00000200, level 2 for player 4. Bismol is not on this map, so that half is still open. |
| P5 the level is 1 | wrong, and informatively. The observed non-visible level is 2; ComputeContactLevel has four return tails and the documented "else 1" names only one. |
| P6 the instrument is behaviour-neutral | held. ref-turn2.sav + one End Turn with all five new detours live reproduced bb4fd9ac… / 978041ac… exactly, and again with the four watchpoints armed on top. |
The one thing no prediction anticipated, and it is the most important by-product: loading
turn1-state.sav and ending one turn produces a different post-turn autosave in every process,
including with hooks=off and nothing installed. One field moves — player 3's research target — and
the strategic generator does not. See the finding's §6.1.