# Lane H — five live probes, predicted before the build Lane H, 2026-09-08. Worktree `wip/probes` off `main` `b2bad30`. Every number below was written **before** the shim was built and before any run (method rule 2). Each section ends with the ways the prediction can fail and the symptom of each, so a wrong prediction is informative rather than merely wrong. The lane exists because of method rule 18: five open questions that are one probe away and that other lanes have been circling statically. --- ## P1 — the determinism oracle, regenerated Lane N's End Turns overwrote the three autosave *contents* on VM140. The file set is unchanged, so every documented Load-dialog row position still holds, and `ref-turn2.sav` on the VM still hashes `ab4ac2d7e2977260…` (checked before the run). What is gone is the byte-identical reference pair. **P1. Loading `ref-turn2.sav` and pressing End Turn once, under build `recap-7584bad-20260908T0615Z` with `hooks=trace`, writes** ``` (Autosave EndTurn).sav 66,732 B bb4fd9ac89f41e3bc0db2af08b18ce83417521ac4bcee695fc9fa6ce16e30948 (Autosave).sav 67,219 B 978041acd168b56ed8eb3f5e42e78d5e70eae6e6517d75e659a5eb7ca3d60921 ``` These are the values `findings/subsystems/determinism-oracle.md` recorded on 2026-09-07 across five runs in two processes, and which lane R reproduced under the recap build. **Why this is worth a run of its own.** Everything since has changed the process the oracle runs in: a different shim build with nineteen template hooks and seven entry-point detours installed, an x87 control-word module, a Zuul game played to turn 64 in the same `SavedGames` directory, and eight save files where there were four. If the pair still reproduces, the oracle is revalidated **across all of that**, and every Phase-2 comparison that leans on it keeps its footing. **Falsification.** * *Either hash differs.* Then the oracle is not stable across the session's changes and nothing downstream of it can be trusted until the cause is found. Candidate causes, in the order they would be checked: the installed `binkw32.dll` is not the recap build (hash it); `shim.cfg` is not the recap trace config (read it); `display.cfg` / `Profile_re.ini` changed (they are inputs to the process, not to the save, so this would be a *new* finding); `ref-turn2.sav` is not the file it was (already excluded — hashed before the run). * *Only `(Autosave).sav` differs.* The pre-turn marker is written from loaded state and the post-turn one from computed state, so a pre-turn match with a post-turn mismatch localises the problem to the turn computation rather than to the load or the writer. * *Only `(Autosave EndTurn).sav` differs.* That would be the more alarming of the two: it is the load → save round trip, which `determinism-oracle.md` proved stable for the *pre-turn* form, and it would mean the loader or the writer moved. --- ## P2 — the four tail callees that can draw, and have never been seen to Lane V2 closed lane K's tier-4 blind spot: of the eleven phase-23/33 vtable targets on `StrategyServer+0x158` / `+0x15c`, **four** reach a draw on the strategic generator at `StrategyServer+0x16c`, at eight instructions — | outer callee | slot | inner | draw sites | |---|---|---|---| | `ServerSpyManager_vslot13` 0x008877b0 | spy 13 | — | `Chance` 0x00887c8a | | " | " | 0x008408e0 | `Chance` 0x00840929, `Chance` 0x00840a3c, `NextInt` 0x008409c7 | | `ServerSpyManager_vslot14` 0x0088db80 | spy 14 | — | `Chance` 0x0088dc43 | | `ServerTradeManagerImpl_vslot15` 0x0082cca0 | trade 15 | — | `Chance` 0x0082cdb8 | | `ServerTradeManagerImpl_vslot13` 0x0088ef80 | trade 13 | 0x00820ca0 | `NextFloat` 0x00820e18 | | " | " | 0x0088b440 | `NextInt` 0x0088b613 | Lane Z measured **0 tail words on 8 turns**, so `tail-rng-ledger.md`'s "the tail draws nothing" is a property of *eight turns*, not of the code. Our corpus has no live trade routes and no spy program. **The distinction this probe exists to make** is the one a word count cannot: *not reached* versus *reached and gated*. "Never observed firing" is currently ambiguous between them, and the two have opposite consequences for a reimplementation — the first says the phase is conditional, the second says the phase always runs and the gate is a data condition our saves never satisfy. **P2a. Each of the four outer callees is entered exactly once per End Turn, on every turn, on every save.** The phase-23 and phase-33 dispatch sites (0x007d97a7 … 0x007d9811, 0x007d989b, 0x007d98a8) are unconditional `call [vptr+n]` in a straight-line block, so entry is not a function of game state at all. Entry counters read `calls = 1` per End Turn for each of 0x008877b0, 0x0088db80, 0x0082cca0, 0x0088ef80. **P2b. On the existing corpus none of the eight draw sites fires.** No `draw_sites` row appears with `ret_rva` in {0x00887c8f, 0x0084092e, 0x00840a41, 0x008409cc, 0x0088dc48, 0x0082cdbd, 0x00820e1d, 0x0088b618} (the return address is the site plus the five bytes of the `call`), and the tail's bracket cost stays 0 — reproducing lane Z's number under a strictly better instrument. **P2c. The three inner functions 0x008408e0, 0x00820ca0 and 0x0088b440 are entered 0 times.** This is the load-bearing half. Together with P2a it says the gate is *inside* the outer callee, above the inner call — i.e. an empty container (no spy programs, no trade routes) short-circuits before any draw is reached — and it localises the condition to one function each rather than to "somewhere in a 200-function closure". **Falsification.** * *An outer callee is entered 0 times.* Then phase 23/33 is itself conditional, and lane V2's "reached ONLY virtually, from ``" needs a gate note added: reachability from that site is not the same as being called. This is the single most useful way for P2 to be wrong. * *An inner is entered but draws 0.* The gate is below the inner call, not above it. The word count is still 0 and lane Z's table is unchanged, but the map moves. * *Any of the eight sites fires on the existing corpus.* Then `tail-rng-ledger.md` §2's "the tail's cost on these turns is 0" is still true of those eight turns and false of ours, and the ledger needs the tail term after all — the standalone's generator model would be short by that many words on exactly the turns that matter. * *An entry counter reads more than 1 per turn.* The dispatch block would then be inside a loop nobody has read, which changes the cost model from "0 or 1 draw per site" to "per iteration". **What this probe cannot settle without a manufactured workload.** Whether the sites draw *when* trade routes or spy programs exist. That needs a save with both, and building one is the expensive part of this lane; if it is not built, P2 downgrades from "the tail never draws" to "the tail never draws on a game with no trade and no espionage", which is a **narrower** claim than the one in the ledger today and must be written that way (rule 6). --- ## P3 — `DetectEncounters`, the last non-research generator residual Lane Y's standalone models 16 of a turn's ~20 words; the residual on its two calibrated pairs is 4 and 2, and **2 words on both are encounter detection** — sites 0x0050329d (`NextFloat`, depth 4) and 0x007929a4 (`NextInt`, depth 3), each 1 call and 1 word per turn on all three measured turns. Lane Y refused to model it: *"fitting 2/turn to three observations is not a model."* Lane I bounds the containing family at `|contacts| × |detectors|` and established that `EncounterDetect_AssignContacts` draws one inlined `NextFloat` **per (contact, detector) trial, before the accept test** — so an untec­hed detector has threshold `0.0f`, burns a word, and can never accept. But lane Z's site run found `AssignContacts` **never ran**: the gate in `EncounterDetect_ProcessTeamRecord` (0x007ca640) was not satisfied on any measured turn. So the probe is on the **caller**. `EncounterDetect_ProcessTeamRecord` is bracketed with the three quantities lane I derived from its instruction stream, computed at hook entry from the record: * `gate` — ∃ entry of `rec->(+0x28 .. +0x2c)`, stride 0x44, with `entry[0]->+0xfc != 0`; * `contacts` — entries whose object has `+0xfc != 0`; * `detectors` — entries whose object has `+0xfc == 0` **and** `+0xfb == 0`; * `bound` = `contacts × detectors`, the `AssignContacts` worst case. **P3a. `ProcessTeamRecord` is called once per 0x74-byte `TeamRecord`** in the vector `EncounterDetect_Run` receives — the same `StrategyServer+0x1e8` vector the tail hooks count as `encounters`. On `ref-turn2` that vector holds **1** record by the time the tail runs, so the expected call count per turn is small and single-digit. **P3b. The turn's 2 detection words are consumed inside `ProcessTeamRecord`.** Both residual sites sit at depth 3 and 4 below `DetectEncounters`, and `ProcessTeamRecord` is the only depth-2 function on that path. The bracket over it therefore reads **2 words per turn**, and the detection residual outside it is **0**. **P3c — the one that changes the map. The 2 words are *not* the `AssignContacts` family.** `gate` reads **false** on every record, `AssignContacts` is not called, and the `|contacts| × |detectors|` bound is **irrelevant to the observed residual**. If that holds, lane Y's 2-word residual and lane I's bound are two different things that have been discussed as one, and the residual is a *fixed* per-record cost (one `NextInt` + one `NextFloat`) somewhere above the gate — which is a formula, and a much cheaper one than the family bound. **Falsification.** * *`ProcessTeamRecord` costs 0 words.* Then the two residual sites are reached by some other path out of `DetectEncounters` and P3b's depth reasoning is wrong; the next probe is a bracket on `DetectEncounters` itself. * *`gate` is true and `AssignContacts` fires.* Excellent outcome, not a failure: `contacts` and `detectors` are then both recorded live and the bound becomes checkable against a measured word count for the first time. P3c is wrong and lane I's family is the residual after all. * *The cost is 2 per record rather than 2 per turn.* Then it scales with the record count and the standalone needs `2 × |records|`, not `+2`. This is distinguishable only if some turn has more than one record — which is exactly why the call count is recorded, not assumed. * *My computed `contacts`/`detectors` disagree with the callee's own when `AssignContacts` does fire.* Then lane I's classification rules are wrong or the record layout is, and the hook says so by printing both numbers side by side rather than only mine. --- ## P4 — `CreateRaidEncounter`: separating two hypotheses a word count cannot A *successful* trade-raid roll calls `ServerTradeManagerImpl` slot 17, `ServerTradeManager_CreateRaidEncounter` 0x008938a0, which draws a `NextInt` at 0x008939ee to pick a target but returns at 0x0089391c **without drawing** when the candidate vector is empty. It cost 0 words on all three of lane Z's measured turns. Lane Z read that as "consistent either with no roll succeeding (≈11% on the defaults, so three quiet turns in a row is unremarkable) or with an empty candidate list every time". **A correction to that reading, before the run (rule 11).** 11% is the probability of a quiet *turn*, not of three. With eight player entries and the image defaults the per-turn probability that no roll at all succeeds is `0.8^8 × 0.95^8 = 0.1113`; three consecutive quiet turns is `0.00138`, or about **1 in 720**. The two hypotheses were never equally likely. The expected number of successes per turn is `8 × 0.2 + 8 × 0.05 = 2.0`. **P4. `CreateRaidEncounter` is entered on essentially every turn — about 2 calls per turn — and draws 0 words because the candidate list is empty.** Entry counter ≥ 1 on at least two of three turns; no `draw_sites` row at `ret_rva` 0x008939f3. **Falsification.** * *0 entries across three turns.* Under the model that is a 1-in-720 event, so it would mean the model is wrong, not that we were unlucky. Two concrete candidates, both checkable next: the odds are not the image defaults on this save (a `StrategyVar` override in the game data), or one of the two per-player skip gates lane Z found in `GenerateTradeRaidEncounters` (the `visitedMask` bit test at 0x00893302, the `>2` pre-filter at 0x008933e0) suppresses the *call* while still spending the `Chance` word — which would also mean the 16 words are not 16 independent rolls. * *Entries > 0 and words > 0.* Then a raid target was picked, the candidate list is non-empty, and the standalone needs the extra word **and** the target-selection draw — the row lane Y flagged as "the one that will bite" becomes live rather than theoretical. * *Entries far above 2 per turn.* The roll is not the only caller, or the odds are higher than the defaults. --- ## P5 — the Zuul word count: what the raid loop actually iterates Lane Z's free prediction, restated by lane Y as a consequence of its model: the two `Chance` sites in `GenerateTradeRaidEncounters` cost one word per entry of `StrategyServer::Players`, which holds **8** on the Human saves and **7** on the Zuul ones (`Sim.NumPlrs`: `#empires + one rebel AI per distinct empire species + 4 NPC pseudo-players`). Lane N ran a Zuul End Turn but with the RNG hooks **off**, so it does not answer this. **P5. On a Zuul save each of 0x00893426 and 0x00893513 records `calls = 7`, `words = 7`, for 14 words at the trade-raid block instead of 16**, and the turn total is `14 + 2 (detection) + 0–2 (research)` = **16–18**, against 18–22 on the Human save. **Falsification, and this is the point of the probe.** * *8 calls per site on a 7-entry player vector.* The loop is not iterating the player vector. The next candidates, in order: the `TeamRecord` vector it is handed as an argument, a species list, or a fixed NPC count plus empires. This is the outcome that would matter most — lane Y's `trade_raid.cpp` counts `NumPlrs` and would be wrong on every save. * *7 calls at one site and 8 at the other.* The two sites have different loop bounds; the NPC site's player-independent gate (`0.0f < S->+0x1a0`) is already known to be all-or-nothing per turn, so this would mean something else again. * *Fewer than 7.* One of the two per-player skip gates fires on the Zuul save and never fired on the Human one — which would make the 16 an upper bound rather than the hard bound lane Z reported. --- ## What is being measured, and what is not Every hook in this lane declares the strategic generator and **nothing else**, exactly as lane Z's family does. None of them makes any claim about whether the turn was computed correctly. The entry counters are register-transparent asm stubs that log and tail-jump to the trampoline: they cannot be wrong about a calling convention because they never assume one, and they cannot report a word count, only a call count — which is deliberate, because the word counts already come from two independent instruments that agree. A clean run here is a statement about RNG accounting and coverage, and about nothing else.