From f1306713d15596aadc83b2bc7e174e2058aba06a Mon Sep 17 00:00:00 2001 From: alex Date: Wed, 9 Sep 2026 04:13:35 -0400 Subject: [PATCH] lane BT merged: --relabel-new-ids ships with both acceptance halves and five refusing guards --- campaign/DASHBOARD.md | 20 ++++++++++---------- campaign/board.md | 5 +++-- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/campaign/DASHBOARD.md b/campaign/DASHBOARD.md index 82880db..cc6c47c 100644 --- a/campaign/DASHBOARD.md +++ b/campaign/DASHBOARD.md @@ -1,16 +1,16 @@ # SotS RE campaign — coverage dashboard -Generated 2026-09-09 07:41 UTC · `sots-re` @ d6f725a,2026-09-09 · `sots-engine` @ 45b16ef,2026-09-09 (244 commits) · regenerate with `tools/dashboard.py` +Generated 2026-09-09 08:13 UTC · `sots-re` @ d46b3f8,2026-09-09 · `sots-engine` @ e7e2bd6,2026-09-09 (245 commits) · regenerate with `tools/dashboard.py` > **North star:** A functional reimplementation of the engine — behavior-equivalent, NOT byte-for-byte ## 1. Map coverage (campaign/board.md) -443 targets · mapped-or-better **395/443** `[█████████░] 89%` · verified **353/443** `[████████░░] 80%` +444 targets · mapped-or-better **396/444** `[█████████░] 89%` · verified **354/444** `[████████░░] 80%` | Status | Count | % | |---|---:|---:| -| verified | 353 | 80% | +| verified | 354 | 80% | | mapped | 42 | 9% | | in-progress | 5 | 1% | | backlog | 41 | 9% | @@ -25,7 +25,7 @@ Generated 2026-09-09 07:41 UTC · `sots-re` @ d6f725a,2026-09-09 · `sots-engine | verify | 105 | 15 | 3 | 35 | 0 | 158 | | phase2 | 13 | 3 | 1 | 0 | 0 | 17 | | meta | 93 | 6 | 0 | 1 | 0 | 100 | -| other | 19 | 2 | 0 | 0 | 0 | 21 | +| other | 20 | 2 | 0 | 0 | 0 | 22 | ## 2. Binary understanding @@ -47,7 +47,7 @@ Generated 2026-09-09 07:41 UTC · `sots-re` @ d6f725a,2026-09-09 · `sots-engine | Module | LOC | Test files | Checks | Wired | Doc | |---|---:|---:|---:|---|---| | `app` | 4,838 | 9 | 207 | yes | N-output-term.md | -| `game/ai` | 2,128 | 8 | 322 | yes | L4-ai-orders.md | +| `game/ai` | 2,138 | 8 | 322 | yes | L4-ai-orders.md | | `game/combat` | 582 | 2 | 34 | yes | L2-predictions.md | | `game/config` | 558 | 4 | 131 | yes | M1.md | | `game/data` | 2,077 | 12 | 462 | yes | game-data.md | @@ -64,7 +64,7 @@ Generated 2026-09-09 07:41 UTC · `sots-re` @ d6f725a,2026-09-09 · `sots-engine | `shim` | 16,709 | 0 | 0 | direct (WIN32) | H-probes.md | | `shim/hooks` | 13,593 | 0 | 0 | direct (WIN32) | L1-predictions.md | | `shim/trace` | 2,258 | 9 | 273 | direct (WIN32) | shim-trace.md | -| **total** | **59,504** | **122** | **4257** | | | +| **total** | **59,514** | **122** | **4257** | | | Board `engine:` rows: verified **32**, mapped 0, in flight 0 (of 32) — verified & merged `[██████████] 100%` @@ -133,11 +133,11 @@ Most recent open: ## 9. Delta since previous dashboard -- verified targets: 352 → 353 (+1) · mapped-or-better: 394 → 395 (+1) -- engine LOC: 58,647 → 59,504 (+857) · test files: 122 → 122 (+0) · checks: 4,257 → 4,257 (+0) +- verified targets: 353 → 354 (+1) · mapped-or-better: 395 → 396 (+1) +- engine LOC: 59,504 → 59,514 (+10) · test files: 122 → 122 (+0) · checks: 4,257 → 4,257 (+0) - addresses verified: 1,163 → 1,163 (+0) · recovered layouts: 384 → 384 (+0) · open questions: 26 → 26 (+0) - standalone leaves closed: 45 → 45 (+0) · leaves still diverging: 63 → 63 (+0) --- -warnings: board.md: unknown types subsystems; mars-rng.md: no oracle total row parsed; mars-stream.md: no oracle total row parsed; mars-vfs.md: no oracle total row parsed - +warnings: board.md: unknown types subsystems, tooling; mars-rng.md: no oracle total row parsed; mars-stream.md: no oracle total row parsed; mars-vfs.md: no oracle total row parsed + diff --git a/campaign/board.md b/campaign/board.md index fdba644..91f2ec5 100644 --- a/campaign/board.md +++ b/campaign/board.md @@ -344,7 +344,7 @@ Status flow: `backlog → in-progress → mapped → verified` (or `blocked`). | LAB HAZARDS from L2 | meta | verified | high | 100% | 2026-09-08 | (1) **`pgrep -f "tap141i0"` matches QEMU's OWN command line** (`ifname=tap141i0`), so stopping a tcpdump SIGTERMed the VM. Use `pgrep -x tcpdump`. The guest came back clean (fsutil not dirty, same IP). (2) Driving the starmap needs **REAL MOUSE MOTION** - SetCursorPos alone does not move the game's cursor - plus wheel zoom; L2's driver is in verify/harness/l2-ui/. (3) From lane L4: **`move X Y` then `click X Y`** - a bare click is delivered at the PREVIOUS cursor position about half the time, which reads as "the click did nothing" then "the next click did the previous thing" | | FOURTH cross-lane commit sweep - now with plain git commit | meta | verified | high | 100% | 2026-09-08 | Lane L1's board commit swallowed all 22 of lane L2's staged files. Content intact and verified in HEAD; L2 correctly did NOT rewrite history with other lanes live. **This is the fourth instance of the hazard addresses.d/README documents, and it now happens with plain `git commit` too, not just addresses.json** - because a lane stages by path but another lane's commit picks up whatever is in the index. The index is shared per-repo; staging by path does not protect against someone else's commit. Needs a real fix, not another rule | | STRUCTURAL FIX for the commit-sweep hazard (rule 25) | meta | verified | high | 100% | 2026-09-08 | Four instances in one session, the last with plain `git commit` and NO `-A` anywhere - **every lane had staged by path exactly as instructed and it still happened**, because `git add ` puts a file in THE REPO'S ONE INDEX and the next commit takes all of it. Staging by path was never the protection. **THE FIX IS A FORM OF THE COMMAND**: `git commit -m "msg" -- path/one path/two` commits only those paths and leaves the rest of the index untouched - verified in a scratch repo (two files staged, pathspec commit took one, the other stayed staged). Lanes now commit with a pathspec, always. Deeper fix if it recurs: a per-lane CLONE of sots-re, mirroring the per-lane worktree of sots-engine - which is precisely why sots-engine has never had this problem | -| RESOLUTION: the byte-match SURVIVES, in a stronger form | meta | verified | high | 100% | 2026-09-08 | First re-resolver run, `findings/resolutions/2026-09-08-ai-seed-per-process.md`. **The AI is not non-deterministic - it is MT19937 from ONE 32-bit word per client, drawn once at construction from a per-process global, and we own that generator bit for bit.** So game/ai is a deterministic function of (save, per-client seed). Rung C splits: **C-exact** - capture the three seeds and our block matches element for element and the autosave byte-matches ON EVERY RUN (the user's framing made literal), precondition being a per-client DRAW LEDGER so we consume the stream in the original's order and count INCLUDING draws whose result never reaches the save; **C-set** - the fallback for references whose seeds were never logged, like turn2-state.sav, which **no process including the original can reproduce**. Instrument exonerated: three unrelated 32-bit words per process are successive generator outputs, not clock reads, so AI1's call-site reading STANDS and only its "never seeded" reading falls **The shape recurs and is now a PREDICATE (resolver 2026-09-09)**: pick turns (`ResTNm == ''` at turn start: `turn1-state`, `as-turn15`) and heavy build turns (`ad-turn27`) **EXPOSE** the seed; quiet-consumer turns (`ar-turn37 -> 39`) do not, and agree byte-for-byte across three processes. **SCOPE of the C-exact proof: it was demonstrated on `turn1-state` (early game) only** - the rich-turn test is `ad-turn27` with `aiseed=pin`, queued on VM146. A control that agrees means "no exposed decision was reached", never "the AI is deterministic here". **C-EXACT FALSIFIED ON A RICH TURN 2026-09-09 (lane BP)**: with all three AI client seeds pinned, two fresh processes on `ad-turn27` still differed. **Restated: given (save, per-client seeds) the AI's command block is determined UP TO the assignment of newly allocated fleet ids to newly formed fleets** - exactly three words of the 81-element block, and the id counter itself is deterministic. **Rung C is DAMAGED, NOT LOST**: the assignment is canonicalisable the way C-set canonicalises a pick. **SHARPENED 2026-09-09 (lane BQ)**: a research pick is exposed only when the **CANDIDATE LIST has more than one entry**. Measured on `ar-oracle-A-post -> 40`: Player[32] entered with `ResTNm == ''`, producers A and B returned null, the candidate walk ran, `candidatesTried=1`, and **the pick AGREED across two fresh processes**. **`ResTNm == ''` predicts a pick, not a divergence.** | +| RESOLUTION: the byte-match SURVIVES, in a stronger form | meta | verified | high | 100% | 2026-09-08 | First re-resolver run, `findings/resolutions/2026-09-08-ai-seed-per-process.md`. **The AI is not non-deterministic - it is MT19937 from ONE 32-bit word per client, drawn once at construction from a per-process global, and we own that generator bit for bit.** So game/ai is a deterministic function of (save, per-client seed). Rung C splits: **C-exact** - capture the three seeds and our block matches element for element and the autosave byte-matches ON EVERY RUN (the user's framing made literal), precondition being a per-client DRAW LEDGER so we consume the stream in the original's order and count INCLUDING draws whose result never reaches the save; **C-set** - the fallback for references whose seeds were never logged, like turn2-state.sav, which **no process including the original can reproduce**. Instrument exonerated: three unrelated 32-bit words per process are successive generator outputs, not clock reads, so AI1's call-site reading STANDS and only its "never seeded" reading falls **The shape recurs and is now a PREDICATE (resolver 2026-09-09)**: pick turns (`ResTNm == ''` at turn start: `turn1-state`, `as-turn15`) and heavy build turns (`ad-turn27`) **EXPOSE** the seed; quiet-consumer turns (`ar-turn37 -> 39`) do not, and agree byte-for-byte across three processes. **SCOPE of the C-exact proof: it was demonstrated on `turn1-state` (early game) only** - the rich-turn test is `ad-turn27` with `aiseed=pin`, queued on VM146. A control that agrees means "no exposed decision was reached", never "the AI is deterministic here". **C-EXACT FALSIFIED ON A RICH TURN 2026-09-09 (lane BP)**: with all three AI client seeds pinned, two fresh processes on `ad-turn27` still differed. **Restated: given (save, per-client seeds) the AI's command block is determined UP TO the assignment of newly allocated fleet ids to newly formed fleets** - exactly three words of the 81-element block, and the id counter itself is deterministic. **Rung C is DAMAGED, NOT LOST**: the assignment is canonicalisable the way C-set canonicalises a pick. **SHARPENED 2026-09-09 (lane BQ)**: a research pick is exposed only when the **CANDIDATE LIST has more than one entry**. Measured on `ar-oracle-A-post -> 40`: Player[32] entered with `ResTNm == ''`, producers A and B returned null, the candidate walk ran, `candidatesTried=1`, and **the pick AGREED across two fresh processes**. **`ResTNm == ''` predicts a pick, not a divergence.** **COMPARISON SIDE DELIVERED 2026-09-09 (lane BT, host)**: `state_checksum --relabel-new-ids` ships with **both** acceptance halves green - BP's pinned pair reads **IDENTICAL modulo pi = {1970<->1986}** (35 leaves -> 0) and AD's unpinned pair is **REFUSED** and still **DIVERGED: 94**, byte-identically what the tool reports without the flag. **Five guards, every one refuses rather than degrades**, and forcing the transposition onto AD's pair with the guard bypassed still leaves **82** leaves - so the tool cannot absorb a real divergence. | | the tie-set claim: rescued as a STATEMENT, open as a RESULT | verify | mapped | high | 80% | 2026-09-08 | And **not on "equal cost"** - the resolver's sharpest correction. The 2000-RP tier-1s are not the picks; the picks are tier-2s at 13k-30k. S(D) is **the support of a random draw**, per save, not a set of equal-score candidates. `BIO_GnMod` (2 of 6 observations) proves a SECOND ARM whose producer is unidentified - likeliest producer A or B gated by a draw, never entering the walk - so **"we can name all k" is TO PROVE**. The four-part statement is in the resolution 3.1: derive S before any sweep; canonical pick = lowest tech id; containment; **and a pinned-seed sweep produces nothing outside S** - part (iv) is what makes it non-tautological | | THE ONE PROBE: pinned seeds THROUGH the resolver | verify | backlog | — | 0% | 2026-09-08 | The seed SOURCE is not the probe. The resolver probe is only an experiment **if the seed is pinned** - otherwise two runs with two unknown seeds cannot separate "draws from the stream" from "reads something else per-process". Implementation named: L1's `l1seed` Seed detour (return address 0x0078304b, three constants) + L4's instrument + a `left` region over client+0x134 at TryResearchCandidate 0x006c8580; ref-turn2 once as the pin's OWN control; turn1-state twice; then sweep by quit-to-menu/reload (the pin table advances per load, **no relaunch needed**), logging which producer returned non-null for player 512. Predictions committed in the resolution 4. If the four leaves still differ, extend the pin to the unidentified fourth seeded object | | NEW OPEN RISK: g_GlobalRNG has four other consumers | verify | backlog | — | 0% | 2026-09-08 | Including **RunCombatRound+0x60f** - and **no battle turn has ever been compared across two processes**. Add "twice, two processes" to the battle workload in backlog 5. Lane L2 verified combat lockstep between two clients in ONE game; this is the different question of whether a battle is reproducible across separate runs | @@ -447,4 +447,5 @@ Status flow: `backlog → in-progress → mapped → verified` (or `blocked`). | TWO PREDICTIONS FAILED, BOTH BECAUSE THE LOAD ROUTE DOES LESS THAN ANYONE EXPECTED - and one block difference nobody predicted | meta | verified | high | 100% | 2026-09-09 | Lane BS, from the same runs. (1) **P1's list-14 clause FALSIFIED**: the continuation's turn-39 block has NO list 14 AT ALL - list 14 is emitted on turn 38 (six elements, for the two fleets minted there), and on turn 39 `IssueRouteForFleets` is entered twice and emits nothing. So "a list-8 element and its list-14 pair" was wrong about the shape of a move order. (2) **P2's "probe 11 is entered in BOTH routes" FALSIFIED**: `AssignFleetsAndIssueOrders` is entered 2x in the continuation and **0x** in the load. The brief and I both expected the load to run the same machinery to a different answer; it does not run the machinery. (3) **UNPREDICTED AND IT STRENGTHENS THE RESULT**: the block ALSO differs by one `list=12` element, fleet **914**, which is the counterpart of the OTHER save-side divergence (`Flt[914 "Nu Fleet III"]` HLay False->True, Lay only-in-load). I predicted half the block difference and the other half was there waiting - which is what turns "the AI emitted a move order" into "**every** diverging leaf is in the block". List 12's elements are fleet ids (274 290 306 322 626 642 786 882 [914] 930 1746 2338); its meaning is NOT decoded and the causal direction is NOT established (rule 20) - one watchpoint would settle it | | A TURN THAT MINTED TWO NEW FLEET IDS DID NOT VARY - four processes, byte-identical, on a turn whose visit order was exercised | control-flow | verified | med | 100% | 2026-09-09 | Lane BS, VM144, a by-product of the continuation run. Turn 38 of the AR lineage IS a fleet-assignment turn: `AssignFleetsAndIssueOrders` entered **6x**, list 10 carries four `{systemId, fleetId}` elements `(384,2466) (384,2178) (336,2482) (384,2338)`, and decoding by `id = (counter << 4) | node` (Player[32] = node 2) all four are CLIENT-MINTED - with **2466 (counter 154) and 2482 (counter 155) absent from `ar-turn37-816raiders.sav`**, i.e. two fleets minted on this turn, in that order. That turn's output is `15b99255...`, produced by **lane AR's three `hooks=off` processes AND this instrumented run - four processes, byte for byte**. So the fleet-group visit order was exercised over two new fleets and its **outcome set was 1**. This does NOT contradict lane BP (different turn, different groups, and BP's outcome set of 2 rests on two observations) but it BOUNDS the claim: **a turn that mints new fleet ids is not thereby a turn whose visit order varies.** One turn, one datum, handed to lane BR for stage 2 - if the container is address-keyed, this turn is a case where two same-size allocations landed in the same relative order in four processes, which the LFH randomisation model has to accommodate | | THE AI'S FLEET-VISIT ORDER IS AN ADDRESS-KEYED CONTAINER AND THE KEY IS THE FLEET'S `Location*` - H1 CONFIRMED, H1b DEAD, and the campaign's two-file outcome set IS the two orderings of two heap pointers | control-flow | verified | high | 100% | 2026-09-09 | Lane BU, VM146, stage 2 of the chain lane BR unblocked (`findings/control-flow/fleet-visit-order-mechanism.md`). Build `bu-55408a9-20260909T071101Z`, fresh CT111 tree (rule 24), lane worktree (rule 21), `shim.cfg.bupin` = `shim.cfg.bppin` with EXACTLY THREE non-comment lines added (`aivisit=on/.out/.ships`), diffed against the real bppin PULLED OFF THE GUEST and quoted in the finding, marked `# exhaustive` so `tools/check_shim_configs.py` ENFORCES that all 27 registered hooks are named. THREE fresh processes, route LOAD, seeds pinned to lane L's observed values; 28 of 28 hooks off, zero unknown keys, zero COVERAGE, 3/3 `pinned=1` in every run, three `aivisit ... MH_OK` in every run, `exe base=0x005e0000` for the 8th process on this guest. THE HOOK: `AssignFleetsAndIssueOrders 0x006c16c0` walks a `std::vector` by index (cursor `[ebp-0x38]`, `_Mylast` REFETCHED every iteration, back-edge `jne 0x6c17e0` at 0x006c21be), and that walk is BRACKETED BY TWO LOOPS OVER THE SAME VECTOR that call one function each - **`StrategyAIAgent::ClaimShipsOfFleet 0x006a4290` and `ReleaseShipsOfFleet 0x0069da10`, which have EXACTLY ONE CALLER EACH IN THE WHOLE IMAGE and it is that function**. So the instrument is two ordinary function-entry detours plus a bracket on 0x006c16c0, with NO return-address filter and NO mid-function patch - the loop body could not be patched anyway, its first instruction 0x006c17e6 IS a branch target. **RESULT: the visit order is NOT ascending element address (0 of 6 multi-element sequences over three processes), NOT ascending fleet id (0/6), NOT ascending systemId (0/6), NOT ascending minimum ship id (0/6). It is ASCENDING `fleet->Location` (StarFleet+0xa0): 6 OF 6**, with the fleet pointer breaking the one tie (two fleets at system 384 share one interned Location object and are ordered by element address within it). So the container is address-keyed and the comparator is lexicographic in (Location*, StarFleet*), BOTH heap addresses. **P3 - the committed prediction, "the visit order is ascending element address" - IS FALSIFIED, and the hypothesis class it tested is CONFIRMED with a sharper key.** THE CROSS-PROCESS PROOF, and it does not rest on one sample: three processes visited THE SAME SIX FLEETS IN THREE DIFFERENT ORDERS (run1 1970/1602/1986/354/1570/1346; run2 1602/1970/1986/354/1346/1570; run3 1970/1986/1602/354/1570/1346) and EVERY difference is a pair whose Location addresses exchanged relative order. Runs 1 and 2 wrote BYTE-IDENTICAL autosaves and still visited in different orders - the reordered pairs were pre-existing fleets, which is why the save did not move. **AND THE BRANCH FLIP IS MEASURED: run 3's `loc(system-80 group) = 0x333ed4c8 < loc(system-384 group) = 0x333ed710`, the OPPOSITE of runs 1 and 2, it visited system 80 first, and its autosave is `724528ff...` BYTE-IDENTICAL (cmp) to lane BP's run B - while runs 1 and 2 have `loc(384) < loc(80)`, visit 384 first, and are BYTE-IDENTICAL to BP's runs A and L.** So the campaign's two-file outcome set on `ad-turn27` IS the two orderings of two heap pointers. **H1b (an uninitialised-word comparator) is EXCLUDED for this path**: an uninitialised key cannot reproduce a strict total order by a specific live pointer field in 6 of 6 sequences and cannot track that field across a branch flip. **A SECOND FINDING THE STATIC READ HAD MISSED: the newly minted fleet ids are ALREADY ON THE FLEET OBJECTS when the head loop first touches them** (`fid=1970` on the element at idx=0 of the first burst, before the pass gate and before any emission), so `AssignFleetsAndIssueOrders` INHERITS the id-to-group pairing rather than deciding it; the mint is upstream in the gather (`AcquireFleetsForTask 0x006ceef0` + 0x006abf80/0x006b7c90/0x006cb310), and since the delivered vector is Location-ordered and the ids follow that order, the natural reading is that the SAME container orders the mint (INFERENCE - the gather was not hooked). ENGINE CONSEQUENCE, and it MOVES deliverable 2 by one level: the canonical order must be imposed WHERE THE GROUP SET IS GATHERED AND THE IDS ARE MINTED, not at the assignment step, or the engine canonicalises the emission order while leaving the minting order to its own container. On the choice of canonical order this lane has evidence rather than convention: the original's order is NOT ascending systemId and NOT ascending minimum ship id in ANY of the three processes, so THERE IS NO ORIGINAL ORDER TO MATCH and `(systemId, min ship id)` is as good as any - keep it, and keep the tie-break, because the original's own tie-break is a second pointer. RULE 26(d) AND RULE 19, BOTH MET BY MEASUREMENT: runs 1 and 2 are BYTE-IDENTICAL to lane BP's UNPINNED run L, so a process carrying a seed pin PLUS three new detours inside the AI's fleet-assignment path reproduced a natural run byte for byte, twice; run 3 reproduced BP's other branch byte for byte. `aivisit` is behaviourally neutral on this state, measured, not argued. P1/P2/P4/P5 all hold; under BP's noise mask every run's block differs from BP's same-branch run in NOTHING outside the noise set and from the other branch in EXACTLY the three transposition words. Rule 17 discharged: 0x006c16c0's body is 0x006c16c0-0x006c247a `ret` with a FOUR-ENTRY JUMP TABLE living PAST the ret at 0x006c247c (target of `jmp [eax*4+0x6c247c]` at 0x006c22fb). Six more per-process AI seeds, none shared - L1 reproduced a 6th and 7th time. Artefacts: `verify/results/shim/bu/`; the three output saves are byte-identical to `bp-pinA-turn28.sav` x2 and `bp-pinB-turn28.sav` x1 and are deliberately not duplicated | -| the fleet-gather ordering contract is BOUND IN THE ENGINE, not just recorded | engine | verified | high | 100% | 2026-09-09 | Lane BU's finding turned into a thing the engine cannot get wrong later. `game/ai` takes `fleetId` as a **parameter** - it emits orders and does not form fleets or mint ids - so **the gather/mint subsystem does not exist yet**, and building it now to satisfy a contract would be speculative. Instead the requirement sits in `src/game/ai/orders.h` at the emission layer where the gather will attach, with the three consequences that bind: **(1) order the groups by a key that is a function of the SAVE and mint in that order**, `(systemId, min ship id)` with a total tie-break - **and say plainly that there is NO original order to match** (the original's is neither, in any of three processes), so we are choosing one member of its outcome set, not recovering a hidden rule; **(2) impose it AT THE GATHER, not at assignment** - the fleets already carry their ids when the assignment walk first touches them, so canonicalising there is one level too low and fixes nothing; **(3) a byte-exact replay needs the original's order and the block already carries it** - list 10 records `{systemId, fleetId, ships[]}` per group in visit order, so the optional override is three words per new fleet, the same cost class as pinning a seed. Lane BU's `aivisit` instrument merged; engine `45b16ef`, host 59/59 and shim both green | +| the fleet-gather ordering contract is BOUND IN THE ENGINE, not just recorded | engine | verified | high | 100% | 2026-09-09 | Lane BU's finding turned into a thing the engine cannot get wrong later. `game/ai` takes `fleetId` as a **parameter** - it emits orders and does not form fleets or mint ids - so **the gather/mint subsystem does not exist yet**, and building it now to satisfy a contract would be speculative. Instead the requirement sits in `src/game/ai/orders.h` at the emission layer where the gather will attach, with the three consequences that bind: **(1) order the groups by a key that is a function of the SAVE and mint in that order**, `(systemId, min ship id)` with a total tie-break - **and say plainly that there is NO original order to match** (the original's is neither, in any of three processes), so we are choosing one member of its outcome set, not recovering a hidden rule; **(2) impose it AT THE GATHER, not at assignment** - the fleets already carry their ids when the assignment walk first touches them, so canonicalising there is one level too low and fixes nothing; **(3) a byte-exact replay needs the original's order and the block already carries it** - list 10 records `{systemId, fleetId, ships[]}` per group in visit order, so the optional override is three words per new fleet, the same cost class as pinning a seed. Lane BU's `aivisit` instrument merged; engine `45b16ef`, host 59/59 and shim both green **EXTENDED 2026-09-09 by lane BT, from contact with the data**: the contract needs a **fourth** consequence. **`FtName` is an ID-ATTACHED LABEL** - a Greek ordinal minted by the same pass as the id - and **its counter is PER PLAYER and SHARED WITH THE SERVER'S FLEETS**: in AD's pair, the run that minted two client fleets names a **node-0** fleet "Upsilon" and the run that minted three names the same fleet "Phi". **So the canonical gather order determines fleet NAMES too, across a boundary that has nothing to do with the AI.** Added to `orders.h`. | +| STATE_CHECKSUM --relabel-new-ids: BP'S PINNED PAIR IS IDENTICAL MODULO ONE TRANSPOSITION, AD'S UNPINNED PAIR IS REFUSED AND STILL DIVERGES | tooling | verified | high | 100% | 2026-09-09 | Lane BT, HOST ONLY (no VM; 140 untouched, 141/144/146 held by AZ/BS/BR and not approached). Implements the 2026-09-09 fleet-id-order resolution §3 item 1 (`findings/subsystems/relabel-new-ids.md`). `state_checksum.py POST-A POST-B --relabel-new-ids PRE` computes the ids NEW in each post-turn save, restricts to node nibble != 0 (client-minted; `id-allocation.md` id=(counter<<4)|node), matches those fleets by a key that does not mention the id - (LocID if non-zero else FPlan/pnd destination, sorted ship-id set), with "at 384" and "bound-for 384" tagged apart - builds the bijection pi, rewrites every fleet reference, compares ShipIDs/FleetIDs/DesignIDs as SETS, masks /Summary/Checksum WITH ITS REASON ON THE LINE, and prints pi. **POSITIVE: `bp-pinA` vs `bp-pinB` = `IDENTICAL modulo pi = {1970<->1986}`, 35 leaves -> 0, exit 0**, with pi recovered from the saves alone and printed with the counters (123/124/125, node 2) and the content keys - the group AT Sys 384 with ship 6976 is 1970 in A and 1986 in B; the group BOUND FOR Sys 80 with ship 5264 is 1986 in A and 1970 in B; 2002's group is 2002 in both. **NEGATIVE: `ad-oracle-A/B-post` REFUSED and `DIVERGED: 94 leaf difference(s)`, exit 1** - byte-identically the 94 the tool reports without the flag, because on a refusal NOTHING is rewritten, masked or set-compared. The refusal is by CARDINALITY (A minted 2 client fleets, B minted 3), so AD's pair does not exercise the content matcher; that is exercised separately, and FORCING the transposition onto AD's pair with the guard bypassed STILL LEAVES 82 LEAVES. FIVE GUARDS, EVERY ONE REFUSES RATHER THAN DEGRADES: G1 only ids absent from the pre-turn save (tested with two PRE-EXISTING fleets exchanging contents - must still report); G2 only non-zero node nibbles (node-0 swap still reports); G3 pi must permute ONE set; G4 content keys correspond one-to-one and are unique per side; G5 NO leaf anywhere may hold a permuted id at an unmodelled site, matched on RAW BYTES not the reader's typed value (a value scan is empty the moment the schema types a reference as a float - this happened during development). Refusal prints its reason and falls back to the ORDINARY comparison, never to a partial relabelling. THREE SPECIFICATION CORRECTIONS FROM CONTACT WITH THE DATA: (1) **FtName is an ID-ATTACHED LABEL** - 1970 is "Sigma Fleet VI" and 1986 "Tau Fleet VI" in BOTH saves, a Greek ordinal minted by the same pass as the id, so "apply pi to every id-valued leaf" would have produced two spurious name diffs; the name stays with the SLOT, gated by a guard requiring it to track the id in both saves. The counter is PER PLAYER AND SHARED WITH THE SERVER'S FLEETS - AD's A (2 client fleets) names node-0 fleet 7120 "Upsilon", B (3) names it "Phi" - which the engine-side canonical visit order must reproduce and §3.2 does not mention. (2) **Applying pi to the Flt[] KEYS introduces a spurious `reordered`**: the fleet table and FleetIDs[] are ID-ordered and IDENTICAL in A and B, so renumbering slots reorders A against B; the artefact-free equivalent is to EXCHANGE THE FLEET BODIES between slots, leaving slot ids, FleetIDs[] and every label as written. (3) **A node's new-id set is NOT fleet-only**: 2002 is a DesignID in ad-oracle-A-post and a FltID in ad-oracle-B-post - the per-node counter is shared across object kinds, and G5 is what keeps a fleet-only matcher safe. NOT DONE, with reasons: lists 8/10/14 (they are aiorders SHIM DUMP words, not save leaves - this tool has no block reader); designs/ships; the two engine-side items. DEFAULT PATH PROVEN UNCHANGED: pre- and post-change modules compared over the whole save corpus under two policies on root digest, coverage, mask hits AND every (path,digest) in the tree - zero mismatches; plus byte-identical CLI stdout on the single-save, BP, AD and resave invocations. The relabelled root is DOMAIN-SEPARATED so a "modulo pi" root can never be read as a strict one. 24 new tests (20 synthetic + 4 real-save acceptance), 7 of them asserting a refusal; suite 38 -> 62, all passing. ALSO FIXED, PRE-EXISTING AND NOT A REGRESSION (the pre-change module fails it identically): `test_known_resave_delta_localises_to_five_named_leaves` enumerated pairs over SORTED FILENAMES and hard-coded the re-save direction 4->0; `cb-turn2to3-endturn.sav` sorts before `turn2-state.sav`, so that pair reads 0->4 and the test failed on a corpus fact. Assertion is now direction-symmetric (all four Status leaves must still move the SAME way, Checksum following by -/+16). Artefacts: `verify/state-checksum/state_checksum.py`, `test_state_checksum.py`, `STATE_CHECKSUM.md` §2.6 |