# 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.