# Lane A2 — the alliance mask (`S04`) and `ModCount` Lane A2, 2026-09-08. Branch `wip/alliance`, off `main` `b2bad30` (lane N's output term already merged). Host + static only; lane A2 holds no VM. **This section is the prediction, written before the implementation, per earned rule 2.** Everything below the horizontal rule at §3 is the result. --- ## 1. The prediction — `S04`, the alliance / shared-vision mask The spine's fourth phase is a per-player pre-pass that rebuilds one word of each player's turn record from scratch, every turn, before anything else in the turn can read it. Read from the instruction stream (not the decompiler), the phase is: ``` for i in 0 .. numPlayers-1: # i is the player's POSITION IN THE SERVER VECTOR p = players[i] rec = p.turnRecord rec.allianceMask = 0 rec.allianceMask |= (1 << i) if p.allianceId != -1: rec.allianceMask |= p.allianceMembers ``` so, as one expression, ``` almem[i] = (1u << i) | (alid[i] != -1 ? al[i] : 0) ``` Both inputs are on the wire: they are the two leading ints of the player's second `Team` item (`ALid`, `AL`). The output is on the wire too — it is the `almem` field of the player's turn-statistics element for the turn. **The prediction, in the form the test will check it:** over the whole eleven-save corpus, for every player of every save, for the archive element whose `trn` equals the save's own frame, > `almem == (1 << playerVectorIndex) | (ALid != -1 ? AL : 0)` with **zero** mismatches, and with `playerVectorIndex` being the player's ordinal position in `/Sim/players` — **not** its `PlyrIdx`. Committing this closes the 8 `almem` leaves that lane Y measured as regressing `T36`, and moves `T36` from "right in six fields, wrong in five" to "right in seven fields, wrong in four". ### 1.1 Why the bit index is the vector position and not `PlyrIdx` The shift count is the loop induction variable, which is the index used to subscript the player vector on the same iteration. Nothing loads `PlyrIdx` anywhere in the phase. Lane T recorded this correctly and it is restated here because it is the single most likely place for a reimplementation to be quietly wrong: every *other* per-player index in the tail (the turn-results array, the battle tally, the archive key) uses `PlyrIdx`, and this one does not. ### 1.2 What this phase does NOT do It does not read, and does not write, `NA` or `CF` — the other two ints of the same `Team` item. It does not consult the game's alliance-enabled flag (`EnAl`). It writes exactly one word. ## 2. Falsification Each row is a way the model above can be wrong, and the symptom that would show it. | # | how the model could be wrong | symptom | |---|---|---| | F1 | the bit is `1 << PlyrIdx`, not `1 << vectorIndex` | on any save where the two orders differ, `almem`'s low bit pattern is off by a permutation. **Zero mismatches on a save where they agree proves nothing** — the check must report how many saves actually separate the two | | F2 | the guard is on something other than `ALid != -1` (e.g. on `AL != 0`, or on the game's `EnAl`) | indistinguishable while no player is in an alliance. Only a save with a live alliance can separate them | | F3 | `AL` is a mask over `PlyrIdx` bits while the self-bit is a vector-position bit | `almem` would mix two index spaces. Same blind spot as F2: unobservable without an alliance | | F4 | phase 36 (`FinalizeTurnRecords`) overwrites `almem` after phase 4 sets it | mismatches everywhere, and the stored value would correlate with something else entirely | | F5 | the archived element for the save's own frame was written by the **load** path rather than by the tail (`FinalizeTurnRecords` runs in both) | the self-bit would be missing, because the load path does not run spine phase 4. Expect `almem == 0`, or a value that is stale by one turn | | F6 | the shift wraps | `shl` masks its count to 5 bits, so a 32nd player would set bit 0. No save has more than 8 players; the model is untested above 31 and says so | | F7 | the corpus never exercises the alliance term at all | then this lane has closed the **self-bit** and *nothing else*, and must say so under rule 6 rather than claim the rule | F7 is the one I expect to bite. The prior is that no save in the corpus has a player in an alliance, in which case the measured agreement tests only `1 << i`, and the `|= AL` arm is a **hypothesis** read from the instruction stream with no exercised evidence. That is a real result and it is a smaller one than "the alliance mask is closed", and it will be reported as the smaller one. ## 3. The prediction — `ModCount` `ModCount` is a **modification counter**, not a turn number and not a phase counter, and the count a turn adds is a function of the *command stream*, not of the pre-turn state. **The prediction:** 1. Every writer is an **entry-block increment inside a `StrategySim` command-application method** — the `OnCommand_*` family. The bump is unconditional and happens *before* the handler validates its arguments, so a command that fails validation still advances the counter. 2. The two turn drivers each add exactly 1. 3. `ProcessTurn` phase 1 adds 1 per system that passes its gate; the gate is closed on all eleven saves, so it contributes 0 to every measured turn. 4. Therefore **`ModCount` is not derivable from the pre-turn save**, and `S00`/`T00`'s two bumps are the only part of it this standalone can ever produce without modelling the AI's order stream. The remaining 10–42 bumps a turn are commands. **Falsification for §3:** if the delta were a function of the pre-turn state, then two saves with the same state shape would show the same delta. `turn1→turn2` and `turn2→turn3` both show exactly 12, which is *consistent* with a state function and is the strongest evidence against my reading; a third pair from the same game showing a different delta with an unchanged board would settle it. The counter-evidence I already hold is that `human-turn2→turn3` is 28 and `zuul-turn16→turn17` is 16 on boards whose system and player counts are identical (28 systems, 7 players) — so it is not a function of the board size. If someone finds a closed-form rule over the pre-turn save that fits all eleven saves, this section is wrong. --- # 4. Result Everything above this line was committed (`49ae628`) before a line of the implementation existed. ## 4.1 The divergence delta — `closed` and `regressed`, never netted `tools/standalone_report.py`, both real End-Turn pairs, on `wip/alliance`: | pair | run | baseline | after | **closed** | **regressed** | |---|---|---:|---:|---:|---:| | `turn1 → turn2` | default | 209 | **204** | **5** | **0** | | `turn1 → turn2` | `--commit-blocked` | 209 | 189 | **29** | **9** *(was 17)* | | `turn2 → turn3` | default | 108 | **103** | **5** | **0** | | `turn2 → turn3` | `--commit-blocked` | 108 | 106 | **13** | **11** *(was 19)* | **The default run is unchanged at 5 closed / 0 regressed, and that is the honest headline.** `S04` writes no save leaf of its own: the alliance mask reaches the wire only through `T36`, and `T36` is still blocked. What moved is `T36`'s cost of committing. The 8 `almem` regressions are **gone**, and the regression set on the reference pair is now exactly: ``` inc x3 sav x3 <- the budget: blocked behind the per-system money output shpt[0] shpt[2] satt[0] <- the ship census: needs each design's hull size and 0x400 class flag ``` Nine leaves, from two named blockers, both owned by other lanes in flight. On the reference pair `T36` now closes **all 24** `/Sim/turnstats` leaves. **When the budget and the design catalogue land, `T36` is a clean +24 with nothing left over** — the same conclusion lane Y reached, with the alliance third of it now paid for. One thing lane Y wrote is no longer true and is corrected here: *"which way the net falls depends on the save"*. It does not any more — `--commit-blocked` is net positive on **both** pairs (+20 and +2, against +12 and −9 before). `T36` nevertheless **stays blocked**, because `regressed > 0` means nine leaves would be confidently wrong, and that is the whole rule the status exists to enforce. ## 4.2 The alliance rule — measured `almem == (1 << vectorIndex) | (ALid != -1 ? AL : 0)`, against bytes the game wrote: **11 saves, 80 player-records, 560 fields compared, 0 mismatches** (`app_turn_record`, up from 480 fields). 14 records carry a live alliance id, so falsifier **F7** did not bite — the alliance term is exercised. **F5 fired, and it was the model, not a miss.** All eight `almem` values of `turn1-state.sav` are zero where the rule says otherwise, because the archiving phase also runs on load and the load path does not run the spine. The engine states that as a prediction (`spineRan`), so all 80 records compare rather than 72. **F1, F2 and F3 are all unfalsifiable on this corpus and the run says so every time.** Every player in every save has `PlyrIdx == vectorIndex`; every observed alliance mask already contains its own member's bit; and `AL == 0` exactly when `ALid == -1`. `app_alliance` pins all three with the separating inputs no save provides. ## 4.3 `ModCount` — the prediction stands, and the mechanism is named §3's reading is confirmed by the instruction stream and sharpened: the counter is `Game::StrategySim + 0x4`, and the per-turn delta is **2 + one per command applied out of every player's `TurnCommands` block**. The flush is `StrategyServer::ApplyAllTurnCommands`, called from the same message handler that calls both turn drivers, and it walks a `vector` whose 0x1b4 stride independently confirms the block layout the campaign already recovered. So `S00` + `T00`'s two bumps really are all the standalone can produce, and the remaining 10–42 are downstream of the AI's order generation. `ModCount` is **not** the next leaf to chase. The watchpoint that would settle the residual in one turn is specified in `sots-re/findings/control-flow/alliance-mask-and-modcount.md` §3, with its own written prediction (exactly 12 hits on the reference workload) and four falsifiers.