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
221 lines
14 KiB
Markdown
221 lines
14 KiB
Markdown
# 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::BeginProcessTurn` **0x007d98e0 .. 0x007d9ae8** (`ret`, thiscall, no stack args).
|
||
`inc [esi+0xc]` at **0x007d990a** is `Frame++`. The last thing the function does before its
|
||
epilogue is the script two-step at **0x007d9ab8**: `vft[0x10](0x13, 0)` then `vft[0x60]()`. `+0x60`
|
||
is exactly what lane SV's jump table gives for event 0x13. Everything else in the body is two
|
||
container walks (`S+0x44` systems, `S+0x64` fleets) and an event post (`push 0x24`).
|
||
* `SVSOSwarmQueen_OnTurnBegin` **0x00529930 .. 0x0052999f** (`ret`): `RegisterHives` →
|
||
prune-by-`EggScio` → `TickHives` → `0x00505100`.
|
||
* `SVSOSwarmQueen_RegisterHives` **0x00527630 .. 0x00527761** (`ret`). Per system in
|
||
`GetGame()->+0x44..+0x48`, `[sys+0x184] == this->+0x4` and no existing hive whose `+0x4` is that
|
||
system; then
|
||
```
|
||
5276e1 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_back
|
||
```
|
||
**Correction to `svsctob-writers.md` §4.3:** the draw call is at **0x0052770f** (return address
|
||
0x00527714), not 0x0052770c — 0x0052770c is the `mov [ebp-0x14],esi` that stores the bound. The
|
||
substance is unchanged and `GetGame()` (0x00578050) returns the **`S` frame**: `[+0xc]` is `Frame`
|
||
and `[+0x16c]` is the strategic generator, the same object lane Z's ledger watches.
|
||
* `SVSOSwarmQueen_TickHives` **0x00527770 .. 0x005279a1** (`ret`). Four gate-failure edges
|
||
(`0x005277e3`, `0x005277fe`, `0x00527848`, `0x0052784f`) all land on **`inc [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_UpdateDifficultyTier` **0x00515820**. 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
|
||
```
|
||
515893 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
|
||
```
|
||
and the class's ctor at **0x0051a820** stores `[eax+0x38] = 0xffffffff`, which is exactly the
|
||
`CDiff = -1` that `turn1-state.sav` carries. **Lane SV's off-by-one reading is confirmed
|
||
independently: `CDiff` can only ever hold 0 or 1.**
|
||
* `SVSOSlaversRefuel_HandleEvent` **0x0051a800** 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`:
|
||
|
||
1. `SVSOSwarmQueen_RegisterHives` is **entered exactly once**, inside `BeginProcessTurn`, before
|
||
`StrategyServer::ProcessTurn` runs.
|
||
2. It reports `hives_in = 0` and `hives_out = 2`.
|
||
3. Its measured word cost is **2** — one `RNG_NextInt` per new hive. (`NextInt` is a
|
||
reject-on-`(y & mask) > n` loop, so 2 is the *minimum*; see the falsifier.)
|
||
4. `SVSOSwarmQueen_OnTurnBegin`'s cost is the same 2 (`TickHives` adds nothing).
|
||
5. `StrategyServer::BeginProcessTurn`'s cost is the same 2.
|
||
6. **The bracket residual is 2, not 0**: `Autosave(endTurn=1) → Autosave(endTurn=0)` total minus
|
||
`ProcessTurn` minus the tail equals **2**.
|
||
7. The two hives written carry `NextQ` **31** and **29**, matching `turn2-state.sav` exactly.
|
||
|
||
**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):
|
||
|
||
* `RegisterHives` is **entered**, finds both systems already referenced, and costs **0 words**.
|
||
* `TickHives` is **entered**, takes a gate-failure edge on both hives, costs **0 words**, and
|
||
`NextQ` reads **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).** `UpdateDifficultyTier` is **entered once per End Turn** on
|
||
`turn1-state.sav`, stores `0` on the first (frame 2, `CDiff` −1 → 0) and stores **nothing** on the
|
||
second (frame 3, unchanged). The `this+0x38` region 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**; `CDiff` stays 1 forever. Tier 2 is unreachable.
|
||
* **P4d.** The `frame ≤ 0` edge is unreachable in a real game: `Frame` starts at 1 and the tail
|
||
never runs before the first End Turn, which is exactly why `turn1-state.sav` carries 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.
|