# W3 — predictions, committed before the build Method rule 2: the expected result is written down before the instrument is built, with a falsification section for each claim. Lane W3, 2026-09-08. Companion to `docs/W2-watchpoints.md`, whose module this extends. ## 0. What is being armed A second watch mode, `watch.mode=tshn`, in `src/shim/hooks/watchpoints.{h,cpp}`. Same single arming detour on `StrategyServer::ApplyAllTurnCommands`, same canary self-test, same VEH. Four 4-byte data-write watchpoints: | slot | target | how it is reached from `S` | |---|---|---| | 0 | the `NVO` map's **root node + 0x10** on the target system — the dword `{touched:int16, TShn:int16}` | `systems = *(S+4+0x40)`; pick the system with `AFlags == 0` and `NVO._Mysize >= 1`; `head = *(sys+0x274)`; `root = head->_Parent` | | 1 | that system's `NVO._Mysize` = `sys + 0x278` | same | | 2 | trade-route vector `_Mylast` = `*(S+4+0x154) + 0x40` | W2 §8.3 | | 3 | spy-program vector `_Mylast` = `*(S+4+0x158) + 0x14` | W2 §8.3 | Offsets used, and where they come from (all re-derived this lane from the instruction stream of `ServerSystem::Write` `0x00749630`, because two earlier accounts disagreed): `Write` is entered on the **IStreamable subobject at `ServerSystem+0x8`** (the RTTI COL offset for vftable `0x00a2043c` is `+0x8`), so every displacement in its body is 8 less than the `ServerSystem`-relative offset. Reconciled: | field | in `Write` | `ServerSystem`-relative | |---|---|---| | `NVO._Myhead` | `[edi+0x26c]` (`0x0074a195`) | **+0x274** | | `NVO._Mysize` | `[edi+0x270]` (`0x0074a07d`) | **+0x278** | | `NVE._Myhead` / `_Mysize` | `[edi+0x27c]` / `[edi+0x280]` | +0x284 / +0x288 | | `NVs._Myhead` / `_Mysize` | `[edi+0x28c]` / `[edi+0x290]` | +0x294 / +0x298 | | `AFlags` | `[edi+0xcc]` (`0x00749aec`) | **+0xd4** | | `Name` (`std::string`) | `[edi+0xa0]` (`0x00749aa9`) | +0xa8 | | `ltis` | `[edi+0x2c0]` (`0x0074997c`) | +0x2c8 | So the brief's `ServerSystem+0x274` is right and is `NVO._Myhead`; `objects/layouts.md` and lane E3 are both right; and the third reading (`+0x26c`) is the un-adjusted one. **The arming code does not rely on this reconciliation**: it validates both candidate bases at runtime and logs which one passed, because an arm that lands on the wrong object reports a confident nothing (rule 1). `NVO` node layout, instruction-verified from the same loop (`0x0074a0b0..0x0074a195`): `_Left +0x0`, `_Parent +0x4`, `_Right +0x8`, key = **player index** `+0xc`, `touched:int16 +0x10`, `TShn:int16 +0x12`, `OID:int32 +0x14`, `isind:bool +0x18`, `indi` inline from `+0x1c`, `_Isnil` at **`+0x8d`** — so an `NVO` node is about `0x90` bytes, not the `0x20` an `NVE` node is. ## 1. Prediction P1 — `TShn` Workload: load `ref-turn2.sav` (Frame 2), one End Turn. Target system: the run picks it by predicate, and on this save the predicate has **exactly one** solution. **P1.0** The arming log names **Spica**, and reports `AFlags == 0`, `NVO._Mysize == 1`, `NVE._Mysize == 0`. It is the only system in the ref game with `AFlags == 0` and a non-empty `NVO` (measured across `turn1/2/3-state.sav`), so if the picker names anything else the picker is wrong. **P1.1** Slot 0 takes **at least one** trap in the End Turn, and the value after the last trap has `TShn == 3` in its high half-word (`(value >> 16) == 3`). `turn3-state.sav` carries `TShn = 3` for Spica, so this is a prediction against a known post-state, not against the run. **P1.2** The trapping EIP is **not** in `FUN_00743ec0` (the `ltis` writer, driver phase 29) and **not** in `ServerSystem::RecordObservation 0x00756300` (which writes `NVE`, at `+0x284`). Spica's `ltis` is `0x7fffffff` in all three ref saves — never written — while its `TShn` moves every turn, so the two cannot share a writer. **P1.3** Slot 1 (`NVO._Mysize`) takes **zero** traps. `NVO` counts are constant across every consecutive pair in the corpus (1/1/1 at Spica, 2/2/2 at Kea'Pono), so nothing should insert or erase. This slot is not decoration: it is the check that the node armed in slot 0 is still the node being written. If slot 1 fires, slot 0's silence means nothing. **P1.4** The prior this is testing. Across all 11 corpus saves, **every** `NVO` entry has `TShn == Frame`, with exactly one exception: Bismol in `zuul-turn23-fleet23.sav` (`TShn = 22` at `Frame = 23`, `AFlags = 0`, `VFlags = 2`, `FFlags = 0`, `ltis = 22`). The systems whose `TShn` tracks `Frame` despite `AFlags == 0` — Spica, and Altair / Galifrey / Kithrup / Alversi / Dosadi / Hyrakius in the Zuul game — all key their single `NVO` entry to a **Species-4 NPC pseudo-player** (`NPC=true, RebAI=false`: "Alien Menace", "Peacekeeper Enforcer", "Von Neumann", "Independent Colony"). Bismol's entry is keyed to a real empire (index 1). So the model under test is: **`TShn` is refreshed to `Frame` unconditionally for entries belonging to NPC/monster players, and gated for real empires** — and the point of the watchpoint is that the trapped EIP and return addresses name the loop, after which the gate is a static read. ### Falsification of P1 | symptom | what it would mean | |---|---| | slot 0: **0 traps**, slot 1: 0 traps, canary PASS, picker named Spica | `TShn` is written from a thread other than the turn thread — debug registers are per-thread and this instrument would be blind to it (W2 §1). Cheapest next step: arm from a second thread, or bracket with a code hook instead. | | slot 0: 0 traps, slot 1: **fires** | the node was freed and reallocated; the armed address is stale and says nothing. Re-arm on `_Myhead`'s successor after the insert. | | picker names a system other than Spica | `AFlags` is not at `+0xd4`, or the systems vector is not at `S+4+0x40`. The arming log prints both candidate bases and the system count (expect **28**), so this is diagnosable from the log alone. | | slot 0 fires but `(value>>16) != 3` | either the write is not `TShn` (offset wrong) or `TShn` is not the Frame. Compare against `(Autosave).sav`'s Spica record. | | slot 0 fires **many** times | more than one writer, or a per-player loop that touches the same node once per player. That is itself the answer to "what is the gate" and is a better outcome than one trap. | ## 2. Prediction P2 — the trade / spy containers **P2.1** At arming time, on `ref-turn2.sav`, both vectors are **empty** (`_Myfirst == _Mylast`), so the arming log reports `trade routes: 0` and `spy programs: 0`. Two lanes have failed to build this workload; nobody has ever printed these two counts from a live game, and printing them is the cheapest possible confirmation instrument. **P2.2** Slots 2 and 3 take **zero** traps during the End Turn. Nothing in a turn with no trade station and no spy program should grow either vector. **P2.3** If either slot *does* fire, the trapped EIP plus the two `ebp`-chain return addresses name the function that grew it. For the spy vector that is the answer to a question no lane has been able to answer — which UI creates a spy program — because the return addresses name the caller. ### Falsification of P2 - If the arming log reports a **non-zero** count for either vector on `ref-turn2`, then the offsets `S+4+0x154` / `S+4+0x158` do not name what W2 §8.3 says they name, *or* the game creates entries this campaign has never seen. Either is news; the arming log prints the manager pointer and the raw `_Myfirst`/`_Mylast`/`_Myend` triple so the two cases are separable. - If a slot fires with `_Mylast` unchanged, the watch is on the wrong word of the vector. - A zero here does **not** mean "the container is never filled" (rule 20). It means: on this save, in this turn, nothing grew it. The claim being made is only that the *instrument* works and that the workload is *absent*, which is exactly what a workload confirmation is for. ## 3. Prediction P3 — the rule-19 control The armed run is the oracle run. One End Turn from `ref-turn2.sav` with four watchpoints live reproduces the determinism oracle byte for byte: | file | sha256 prefix | |---|---| | `(Autosave EndTurn).sav` | `bb4fd9ac89f41e3b` | | `(Autosave).sav` | `978041acd168b56e` | W2 took this control with a different set of four addresses and it passed; the mechanism (a `#DB` on a data write is a trap taken after the store retires, and no code is modified except the single arming detour that W2 already proved neutral) is unchanged, so this should hold. **If it does not, every number in this lane is void** and the difference between W2's run and this one is only the four addresses — which would be a much more interesting finding than anything above. ## 4. What this run cannot establish, stated in advance (rule 15) - One save, one game, one turn. Nothing here is a claim about a turn with combat, an alliance, or two players observing one system. - **One** system watched, not 28. Spica is chosen because it is the discriminating case; the other 27 systems' `TShn` writes are unobserved and the "every entry is refreshed" half of the model rests on the corpus, not on this run. - Per-thread blind spot: a write from a thread other than the turn thread is invisible. - The gate itself is **not** measured by this run. The run names the writer; the gate is then a static read of that writer's enclosing branch, which is a different kind of claim and will be labelled as one.