The spine's fourth phase, read byte-for-byte and implemented:
almem[i] = (1 << i) | (ALid != -1 ? AL : 0)
with i the player's POSITION IN THE PLAYER VECTOR, not its index field. Both
inputs are on the wire and so is the output, through the turn-record archive,
so the phase is checkable against bytes the original wrote:
app_turn_record: 11 saves, 80 player-records, 560 fields, 0 mismatches
(was 480 fields over six fields; almem is the seventh)
The eight zero masks of the corpus's earliest archived turn are PREDICTED, not
excluded: the archiving phase also runs on load, and the load path does not run
the spine. BuildTurnRecord takes spineRan and models it, so all 80 records are
compared.
Three parts of the rule the corpus cannot separate -- the bit index, the OR,
and the ALid guard -- are pinned in app_alliance with the separating inputs no
save provides, and app_turn_record prints that it could not separate them.
Divergence, closed and regressed reported separately:
turn1->turn2 default 209 -> 204 closed 5, regressed 0
turn1->turn2 --commit-blocked 209 -> 189 closed 29, regressed 9 (was 17)
turn2->turn3 default 108 -> 103 closed 5, regressed 0
turn2->turn3 --commit-blocked 108 -> 106 closed 13, regressed 11 (was 19)
T36 stays blocked: nine leaves would still be wrong (inc x3, sav x3 behind the
budget; three census leaves behind the design catalogue). It now closes all 24
turnstats leaves on the reference pair, so it becomes a clean +24 once those
two land.
Prediction and falsification committed first in 49ae628.
Gates run separately: clean-room OK; host ctest 43/43. No src/shim touched.
10 KiB
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:
- Every writer is an entry-block increment inside a
StrategySimcommand-application method — theOnCommand_*family. The bump is unconditional and happens before the handler validates its arguments, so a command that fails validation still advances the counter. - The two turn drivers each add exactly 1.
ProcessTurnphase 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.- Therefore
ModCountis not derivable from the pre-turn save, andS00/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<TurnCommands> 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.