sots-engine/docs/W3-predictions.md

9.2 KiB

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.