Names the six regions (four result, two guard) that are the classifier's whole
output surface, and then names every predicate the decision consumes next to
which side computed it -- because the module takes those predicates as given, so
reading them off the live objects is its interface rather than a shortcut. Also
records that the node-graph indirect edge path-solver.md could not resolve
statically is resolved DYNAMICALLY here, by making the same call from the hook.
The observed-design list is a MOVE-TO-BACK vector capped at 20 records per
DESIGN OWNER, and that is why lane CV's 55 odes leaves looked unrelated: a turn's
output is a permutation of its input, not an append.
rich turn ad-turn27 + BR's deep tcb vs bp-pinB-turn28 1092 -> 1058
36 closed / 2 regressed vs CV's run; 116 / 8 vs the 1166 baseline
canonical turn2-state + CB's tcb vs turn3-state 62 -> 61
1 closed / 0 regressed, set-differenced by leaf PATH
All five committed predictions held; P5 held in direction and was wrong in
magnitude (9 leaves, not 4 -- I predicted spurious records, not leaves).
CV's "no upstream dependency" is right for the design half and wrong for the
other 24 leaves of the 79, two separate ways, and both are named rather than
attempted: the otch/owep arms need three undecoded design->tech/weapon set
builders (the wire's own DOpts list covers 13 of the 18 tech names the turn
moves), and the design half is itself capped by SHIP CONSTRUCTION -- two designs
get their first ships that turn. Fed the true post-turn ship list the same code
leaves 1 leaf of 55 instead of 21, so the mechanism is not the residual.
Control, run: with the NPC-owner guard compiled out the canonical pair goes
61 -> 70 and the rich turn stays at 1058. The guard is load-bearing on one pair
and inert on the other, which is why both were run.
Coverage, stated as loudly as the divergence: 8 players swept, TWO produce any
record; the canonical pair offers 16 (player, ship) pairs of which the guard
drops 15, leaving one record call on one design. No foreign observation moves on
either pair, so the visibility gate -- a two-bit-per-player word at Ship+0x54
that is not on the wire, stood in for with ownership -- is entirely unexercised
and is labelled a hypothesis.
ghidra/addresses.d/dt.json: 10 entries (the sweep 0x007c2350, the recorder
0x007be340, RecordObservedWeapon, the vector helpers, sizeof(ObservedDesign) =
0x10, the vftable, and ServerPlayer +0x254 / +0xfb). gen_addresses.py merges it
clean at 1320 entries, no duplicate name or address.
The classifier is a real callable unit (StarFleet::ClassifyLeg 0x00703730, thiscall,
ret 0x14, two direct callers), so the hook goes on the classifier and not on a proxy
for it. Nine predictions with their falsification symptoms, the workload that reaches
both route classes, and the three input gaps that are counted per call rather than
argued away. addresses.d/dn.json carries the eleven offsets the hook needs plus the
vtable slot that resolves path-solver.md's node-graph indirect edge.
Track 0 items 0a and 0b. The finding has the full account; the two things
that matter for anyone reading the gate afterwards:
gate.sh was taking TAIL's exit code from `ctest ... | tail -15`, so it printed
"GATE ok host-ctest" directly above "2 tests failed out of 59". The same
masking was on the host build and on the shim cross-build. All three now
capture the real status and trim the output afterwards.
The corpus-skip check counted every "unset, skipped" line, two of which are not
corpus tests, so it could never reach zero. It is now scoped to the tests gated
on the corpus, SOTS_DATA_DIR is set so game_design_census actually runs against
it (43 saves, 2728 designs, 0 mismatched -- a test that had been skipping), and
every remaining skip is printed by name so none can be invisible again.
Replace mode was tried live on a turn that actually completes a tech, with a
two-process hooks=off oracle established first on that exact (save, procedure,
route). Verdict: game/sim/research stays compared.
What displaced: all 13 tech-tree leaves the turn moves -- 2 from the pass itself
and 11 from the SetResearched cascade -- produced by our code in live game memory,
with the original's ProcessResearch never executing.
What did not: 16 leaves, every one written by ServerPlayer::OnTechResearched.
Five player tech-effect fields (OutMod, ConMod[0..2], ResTNm), one ObservedTech
element, two event records plus EvNxID, and five derived leaves behind them.
Also: ref-turn2 + one End Turn does NOT complete a tech, so every research oracle
before this one was taken on a quiet turn; and a config that names all 27 registered
hooks off and passes check_shim_configs.py still installs six detours, because the
M0 stub and the FPU module's four sampling detours have no hook. key.
The phase is FUN_007c2350 (tail phase 34): for every player, over every ship of
every fleet, gated on a two-bit-per-player field at Ship+0x54 that is NOT on the
wire, call RecordObservedDesign(player, ship.design). That function dedups on
odid alone, and on a hit it ERASES and PUSH_BACKS rather than updating in place --
so the list is ordered by last observation and otnF survives the move -- then caps
the list at 20 entries per DESIGN-OWNER, counted from the back, after every single
record call. sizeof(ObservedDesign) = 0x10.
Predictions, computed from a Python simulation of that mechanism over the corpus,
committed before any engine code is written:
P1 rich turn odes: 55 -> 21, i.e. 34 closed / 0 regressed
P2 otch+owep (24 leaves): 0 closed / 0 regressed, blocked (see the finding)
P3 rich-turn total 1092 -> 1058
P4 canonical pair: 1 closed / 0 regressed
P5 the NPC-owner guard is worth 4 leaves of regression on the canonical pair
The canonical pair on the same binary puts ModCount on 12 -> 24 exactly (zero
residual), so the cost table is right on the lists that pair exercises and the
rich turn's residual of 2 must sit in lists 1, 7 or 12. The only uniform
per-element explanation is list 7 (colonisation) at 2 bumps -- fitted to one
observation, recorded as a prediction with its falsifier.
Also records the regression check on tools/aiorders_to_tcb.py: 8 shallow logs
byte-identical, 4 lane-CB logs corrected (they were deep dumps whose payloads
the converter had been discarding), and the canonical pair's replay unchanged.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARBgSooAfokKUy6wKUKEyZ
Replays lane BR's deep command block for ad-turn27-two-raiders.sav through
sots_turn --turn-commands and compares with bp-pinB-turn28.sav (724528ff).
Verdict: outcome 3. DIVERGED: 1092 leaf difference(s) against a 1166-leaf
do-nothing baseline; 80 closed, 6 regressed; the stream's whole contribution to
the state is /Sim/ModCount (1430 -> 1500, target 1502, residual 2).
Two blockers upstream of the turn: the typed writer drops one usp item in
Game::SpecialProjectNameGen (12 of 43 corpus saves, exactly 12 bytes each), and
--relabel-new-ids refuses by guard G3 because the engine mints no client fleet
ids. The tail's tscr gate is TRUE on this save (253, not 252).
tools/aiorders_to_tcb.py now reads the deep dump's aivec/aistr rows, only at a
word the field map already types as a vector, with the followed count
cross-checked against the begin/end pair.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARBgSooAfokKUy6wKUKEyZ
Decodes the route-creation gate first (rule 28 practice 2): TradeSector.tssec
gates which player may source a trade route from a sector, and sector 768's
tssec is 0 for every player on az-turn23-tarka-comraid.sav -- so the briefed
Target 1 cannot be built there at all. The three sector masks are decoded and
reproduce all five sectors x seven players from the save; tscr turns out to be
the CnRad roster, which closes lane AZ's open item.
The workload switches to the alternative: player 0's own Tarka raiders parked
on the node of a sector that sources an AI route, which satisfies G_B3 by war
instead of by ownership.
Implements the 2026-09-09 fleet-id-order resolution, section 3 item 1. Given the
pre-turn save, compute the ids new in each post-turn save, match the client-minted
(node nibble != 0) new fleets by a key that does not mention the id -- (LocID or
FPlan destination, sorted ship-id set) -- build the bijection pi, rewrite every
fleet reference, compare the master id lists as sets, mask /Summary/Checksum with
its reason on the line, and print pi.
Acceptance, both halves:
bp-pinA vs bp-pinB IDENTICAL modulo pi = {1970<->1986} (35 leaves -> 0)
ad-oracle-A vs -B REFUSED, then DIVERGED: 94 leaves (unchanged)
Five guards, every one refusing rather than degrading: only ids absent from the
pre-turn save; only non-zero node nibbles; pi must permute one set; content keys
must correspond one-to-one and be unique per side; and no leaf anywhere may hold a
permuted id at an unmodelled site (matched on raw bytes, not the reader's typed
value). A refusal rewrites nothing and falls back to the ordinary comparison.
Three corrections to the specification from contact with the data, in
findings/subsystems/relabel-new-ids.md section 4: FtName is an id-attached label
and needs the same treatment as the id; relabelling the Flt[] keys is the wrong
operation (exchange the bodies -- the fleet table is id-ordered and identical in
both saves); a node's new-id set spans object kinds.
Default path proven unchanged: pre- and post-change modules agree on the root
digest, coverage, mask hits and every (path, digest) in the tree over all 43 saves
under two policies, and on 5,602 lines of CLI stdout across every mode.
Also fixes a pre-existing, unrelated test failure: the re-save localisation test
enumerated pairs over sorted filenames and hard-coded the direction 4 -> 0, which
a later corpus addition reversed. Suite 38 -> 62 tests, all passing.
Three fresh processes on ad-turn27 with the seeds pinned, hooking the two
single-caller per-element callees of the assignment walk.
* ascending fleet->Location: 6 of 6 multi-element sequences
* ascending element address (the committed prediction P3): 0 of 6
* ascending fleet id / systemId / minimum ship id: 0 of 6 each
Three processes gave three different orders of the same six fleets, and every
difference is a pair whose Location addresses exchanged relative order. Runs 1
and 2 wrote byte-identical autosaves and still visited differently. Run 3
flipped the branch and its two Location addresses flipped with it: it is
byte-identical to lane BP's run B, runs 1 and 2 to BP's A and L.
So P3 is falsified as written and its hypothesis class, H1, is confirmed with
a sharper key. H1b is excluded on this path.
Second finding the static read had missed: the fleets already carry their new
ids when the walk first touches them, so the mint is upstream in the gather.
That moves the engine's canonicalisation point by one level.
rule 26(d) and rule 19 are both met by measurement rather than argument: an
instrumented pinned run reproduced an unpinned natural run byte for byte,
twice.
Run 1 falsified P3 as written -- the walk is NOT ascending element address,
0 of 5 sequences -- and handed back a sharper key: it is lexicographically
ascending in (fleet->Location*, fleet*), 5 of 5. Both are heap addresses.
Run 1 also found something the static read had missed: the newly minted fleet
ids are ALREADY on the fleet objects when the assignment walk claims them, so
the id-to-group pairing is decided upstream of AssignFleetsAndIssueOrders.
The cross-process test that separates 'the Location pointer is the key' from
'the Location pointer is a correlate' is committed here, while run 2 was still
loading and before its autosave existed.
Stage 2 of the chain lane BR unblocked. H2 (an RNG draw) is dead by
measurement, so the visit order over the ship groups is either a function of
the walked elements' addresses (H1) or of nothing in particular (H1b).
The committed prediction is P3: the visit order is ascending element address,
and across two processes the element order differs exactly where the visit
order differs.
Also the bounded static read this lane took to place the hook: the walk is a
plain index walk of a std::vector<StarFleet*> whose _Mylast is refetched every
iteration, bracketed by two loops over the same vector that call one function
each -- and those two functions have exactly ONE caller each in the whole
image. So the hook is two ordinary function entries plus a bracket, not a
mid-function patch of a loop body whose first instruction is a branch target.
Rule 17 discharged: the body is 0x006c16c0-0x006c247a ret, with a four-entry
jump table living past the ret at 0x006c247c.
ghidra/addresses.d/bu.json carries the two new names and the agent offset; it
deliberately does not re-declare AssignFleetsAndIssueOrders, which lane AI3
owns.
az-turn23-tarka-comraid.sav is 89,285 B not 73,385 (that number was the
turn-11 file's). Both saves are now on the guest as well as in the repo.
az-turn11-16destroyers.sav is the same game with the 16-destroyer fleet
still alive and Commerce Raiding already in hand - it saves ten turns of
play for anyone retrying the player's own raider, though the AI's first
routes do not appear until turn 16.
Static half, verified and correcting two published sentences:
FUN_00820af0 short-circuits on the ROUTE OWNER being Hiver (0x00820bc2),
on the RAIDER's crew species being Hiver (0x00820bd7), and only then on
SpeciesDef+0x144 (Human, Zuul). The campaign's 'Human or Zuul draws
nothing, every other species draws' is incomplete on both sides. And the
short-circuit sets frac = 0.0, not 1.0 - corrected in ag.json in place.
Live half NOT taken. hooks=off throughout; no instrument was armed. The
workload was built to within one conjunct: az-turn23-tarka-comraid.sav is
the corpus's first Tarka empire, has Commerce Raiding, three AI routes and
five AI Tarka fleets parked on sector nodes with Pos bit-equal, two of
which clear G0-G4. The failed conjunct is G_B3 - the qualifying raiders
own every route, so relation-to-self is 3 and B empties its candidate
list. The next workload is named from that: give player 0 a route sourced
in sector 768, where the AI raider already stands.
tscr reads 127 on this map, not 252/253 - the mask is not a corpus
constant and my own P3 gave the right count for the wrong reason.
BP has a per-PROCESS input (two pinned processes disagree); this lane has a
per-HISTORY input (two load processes agree with each other and disagree
with the continuation). One datum against the ASLR reading as stated: the
exe base was identical in all five VM145 processes (0x00f40000, delta
+11796480), so any image-base hypothesis has to name which allocation it
means. Probe 3's ledger taken twice on ar-oracle-A-pre - once by
continuation, once by load - now speaks to both.
Both committed predictions are wrong, in different and useful ways.
1b FALSIFIED: ar-oracle-A-pre.sav + load + one End Turn gives 4c356f59...,
not 7a8b3d5e... - and gives it byte-for-byte in two fresh hooks=off
processes. 23 leaves apart from AR's continuation, 17 of them one AI fleet
(Zeta Fleet V moved in the continuation and stayed put after a load). So
AR's second hash may NOT enter determinism-hashes.txt as a load->turn pair;
the load form is certified instead. The resave canonicalisation is
exonerated: the pre-turn round trip differs in exactly one leaf,
/CD[1]/NPrvVa, +5.00 per load - which also corrects determinism-oracle.md's
"nothing else varies".
Probe 1: the two files DIFFER, and /Sim/RNG, /Sim/trdmgr and every player-0
fleet are identical as predicted - but ZERO TechTree and ZERO ResTNm leaves
differ. Both processes picked BIO_GrvAdpt. The 144 leaves are a combat at
Venkman: Auto Resolve Peacefully means the human commits no ships, not that
no combat occurs, and the planet's defences killed two raiders in one
process and one in the other.
The producer run (config already on the guest, no build) names why the pick
agreed: producers A and B both returned null, Player[32] reached the
candidate walk, and candidatesTried=1. The outcome set is a singleton by
COUNT, not by producer gate - so ResTNm == '' is necessary for research
exposure, not sufficient.
VM145 restored byte-for-byte and released.
shim.cfg back to hooks=off, binkw32.dll back to the pre-BP file, both autosaves
restored byte-identical, game process left not running as found.
ad-turn27-two-raiders.sav is deliberately left on disk, so the Load-dialog rows
have moved on this guest.
P1 falsified. Two processes with identical pinned AI client seeds produced
different turn-28 autosaves from ad-turn27-two-raiders.sav, so a per-process
input other than the seed reaches a rich turn and C-exact is false on this
state. Resolution trigger, handed back rather than chased.
But the pin did almost all the work. AD's unpinned pair differed in 94 leaves
across designs, ship records, money, both build queues, ModCount, NumFlts,
DesignIDs[] and FleetIDs[]. This pinned pair differs in 35 of 61147, and all 35
are one transposition: fleets 1970 and 1986 exchange their entire contents.
Every decision leaf is identical.
The command block says the same from the input side. Runs L (unpinned) and A
(pinned to L's observed seeds) wrote BYTE-IDENTICAL autosaves, which gives a
noise mask no previous lane could build for this state; against it, A and B
differ in exactly three words - one fleet-move fleetId and two systemIds - and
list 10's fleet ids are in the same order in both. The id counter is
deterministic; what varies is which newly formed ship group each id lands on.
Also measured: the save has seven non-human players and the engine builds THREE
AI clients (RunAI fires 3x, ids 32/496/512); the four NPC factions get no client
and no seed.
From as-turn15-spydeployed.sav, 11 End Turns under probes=8. Detection at Frame
22, P at Frame 25: 0x00840a3c 1 call / 1 word, the first firing of any of P's
sites. 0x00840929 and 0x008409c7 are the MORRIGI branch and cannot be reached in
a Human game - correcting every inventory that lists them as three sites behind
one gate. The counter-mission roll 0x0088dc43 fires from Frame 19 (corpus was 0
of 22) and the deploy NextFloat 0x0078c97f is measured with cbh reproducing lane
AS's value bit for bit. Species table decoded, which also names AS's 0.75f as
Zuul and 0.5f as Morrigi. Two corrections to lane AG 3.2 (CnTrd is the target's;
ncp is a scalar counter). PB4 was wrong and is named: the trade-raid roll entered
the tail at Frame 20, re-confirming lane AD's per-fleet model. Control pair
disagrees (three outcomes in six processes) - not a calibration pair - and
probes=8 is byte-identical to a control over 44,438 leaves. Exposure fields
recorded per turn: saturated on this map.