sots-re/findings/subsystems/system-visibility-prediction.md
alex d639c9a112 lane E3: the prediction's outcome, leaf for leaf
204 -> 158 and 103 -> 87, closed 46 and 16, regressed 0 -- the predicted table
line for line, including the 32-leaf target in full. Three further pairs the
model was never fitted to close 42, 12 and 12 with zero regressions; 128 leaves
across five pairs.

The AFlags-vs-VFlags falsification is decided on a real save rather than in a
comment: running the engine on zuul-turn23-fleet23 reproduces the frozen stamp
at the one system whose active mask is clear while its sticky and explored masks
are not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARBgSooAfokKUy6wKUKEyZ
2026-09-08 13:39:30 -04:00

7.6 KiB

Prediction: the visibility phase, before it is built

Lane E3, 2026-09-08. Written and committed before the engine change exists (rule 2). Model: findings/subsystems/system-visibility-record.md.

Baseline, measured on sots-engine main 0592104 this morning:

turn1-state -> turn2-state   204 leaf differences
turn2-state -> turn3-state   103 leaf differences

The model, as three rules

  1. T17 RecordObservation. For every player p and system s with s.AFlags & (1 << p.PlyrIdx): set NVE[p] = { EPid = p's handle id, ETS = frame, Eid = the encounter id at s }. frame is the post-increment frame, i.e. the turn number the autosave carries. Entries are ordered by PlyrIdx ascending (a std::map). Nothing is ever erased.
  2. T21 SetExploredBy. s.EFlags |= s.AFlags.
  3. S29 ltis. If s.AFlags != 0 then s.ltis = frame.

Eid is taken from an existing NVE entry when the system already has one (the field it mirrors is fixed for the life of the game); otherwise from a fleet at the system with FtEnc != 0; otherwise -1.

AFlags is read from the save, not recomputed. Justification, and it is checkable: neither reference pair moves an AFlags leaf, so the loaded value is the value the phase would see.

Predicted leaves closed

turn1-state -> turn2-state (the 32-leaf target)

Eight systems — 64 Hyperion, 112 Gamma Cephei, 288 Ke'Dolarra, 304 Koa'Vo, 336 Kaa'Vaalu, 400 Markab, 448 Kea'Pono, 480 Ko'Rorkor — are exactly the systems with AFlags != 0.

leaves rule count
Sys[*]/NVE count 0 -> 1 1 8
Sys[*]/EPid only-in-B 1 8
Sys[*]/ETS only-in-B 1 8
Sys[*]/Eid only-in-B 1 8
the brief's target 32
Sys[*]/EFlags 0 -> {16,128,16,16,16,16} on 64, 304, 336, 400, 448, 480 2 6
Sys[*]/ltis 1 -> 2 on all eight 3 8
total predicted closed 46

Predicted EPid values, per system, in order: 528, 16, 32, 576, 528, 528, 528, 528. Predicted Eid values: 5, -1, -1, -1, 3, 3, 4, 4. Predicted ETS: 2 everywhere.

Expected result: 204 -> 158, closed 46, regressed 0.

turn2-state -> turn3-state

The entries already exist, so only their stamps move.

leaves rule count
Sys[*]/ETS 2 -> 3 on the same eight 1 8
Sys[*]/EFlags 2 0 (already equal to AFlags)
Sys[*]/ltis 2 -> 3 on the same eight 3 8
total predicted closed 16

Expected result: 103 -> 87, closed 16, regressed 0.

EPid and Eid must be re-emitted unchanged; if either moves, the model has regressed a leaf that agreed by construction, and that is a failure of rule 3, not a rounding detail.

Falsification

Each of these is a way the model is wrong and the symptom that would show it.

  • The gate is VFlags, not AFlags. On both reference pairs VFlags == AFlags on every system, so the pairs cannot separate them. The discriminator already in the corpus is zuul-turn23-fleet23.sav Bismol: VFlags = 2, AFlags = 0, ETS frozen at 22 while the save is turn 23. A VFlags model refreshes it to 23. Symptom: a host test that replays Bismol's row fails. That test is written.
  • The gate is EFlags. Same corpus discriminator: Bismol has EFlags = 2.
  • ETS is the sighting turn, set once, not refreshed. Then turn2 -> turn3 closes 0 of its 8, and the eight ETS 2 -> 3 leaves stay. Symptom: pair 2 closes 8 instead of 16.
  • Eid is re-derived from the fleet every turn rather than parked on the system. Invisible on this corpus — no encounter fleet dies in it. Symptom: none available; that is why §8 of the model doc names the workload instead of claiming the rule.
  • The Eid test is FtFlg & 0x10, not FtEnc != 0. Invisible on this corpus; the six encounter fleets agree on both. Symptom: none available.
  • EPid is the player index, not the handle id. Then all eight EPid leaves regress to 0..7 instead of closing. Symptom: 8 closed becomes 8 regressed, and the run's closed count drops to 38 / 16.
  • The map is ordered by insertion, not by key. Invisible: every system in the corpus has exactly one entry. Symptom: none available; the ordering is asserted from the container type, and §8 names the two-empire-contact workload that would exercise it.
  • ltis is not the frame but something that merely equals it here. ltis is 1 at turn 1 and 2 at turn 2 on the same systems, which is consistent with a great many counters. The reading is a 41-byte function body; if it is wrong the 8 ltis leaves regress rather than close.
  • The phase runs before the frame increment. Then every stamp is one low and 24 leaves regress across the two pairs.

What the model deliberately does not write

  • NVO.TShn (10 leaves per pair). Its gate is not AFlags — Spica has AFlags == 0 and TShn still moves. Formula not held; evaluated and reported, not written (lane S2's rule).
  • rcex (6 leaves per pair). Unexplained.
  • NVs / PlayerView. Count 0 on both reference pairs; the PlayerView refresh loop is a separate phase and no leaf of it diverges on either pair.

Measured

Run after the build, against the prediction above, which was committed in 55ea86f before any engine file existed. Engine branch wip/visibility, host ctest 46/46, clean-room OK, shim cross-built on CT111 with exports byte-identical.

pair baseline after closed regressed
turn1-state -> turn2-state 204 158 46 0
turn2-state -> turn3-state 103 87 16 0

Closed, by leaf name — the prediction's table, line for line:

turn1 -> turn2   EPid 8   ETS 8   Eid 8   NVE 8   (= the brief's 32)   ltis 8   EFlags 6
turn2 -> turn3   ETS 8    ltis 8

tools/standalone_report.py's own numbers, whose baseline is the do-nothing distance from the input save rather than from main, are 209 -> 158 (closed 51, regressed 0) and 108 -> 87 (closed 21, regressed 0); the five-leaf difference in each is what main had already closed.

Three pairs the model was never fitted to

Rule 2's strong form. None of these was looked at while the model was being built; the human-* pair is a different game on a different map with a different species mix.

pair baseline after closed regressed
human-turn2-orders -> human-turn3-noderoute 353 311 42 (21 ETS, 21 ltis) 0
zuul-turn15-orders -> zuul-turn16-noderoute 276 264 12 (6 ETS, 6 ltis) 0
zuul-turn16-noderoute -> zuul-turn17-orders2 341 329 12 (6 ETS, 6 ltis) 0

128 leaves closed across five pairs, 0 regressed.

The falsification that mattered

The AFlags-vs-VFlags case is the one the reference pairs could not decide, and it is decided. Running the engine on zuul-turn23-fleet23.sav reproduces the freeze on the save itself: every observed system's stamp moves to 24 and Bismol's — sticky mask 2, explored mask 2, active mask 0 — stays at 22, in both ETS and ltis. A VFlags or EFlags model moves it to 24. That row is also a host test (tests/game_sim/test_visibility.cpp, test_bismol_freeze), which asserts the freeze and asserts what the wrong gate would have produced, so a later edit that swaps the mask fails loudly rather than quietly agreeing with the five pairs above.

None of the other falsification cases fired. The remaining ones — the two Eid derivations and the map ordering — are exactly the ones the section marked "symptom: none available", and they are still not settled; §8 of the model doc names the workloads.