`SvSctOb` is not written by direct calls. Every update goes through an event bus: a
driver notifies the root object with an integer id, the root fans the delivery out to
every child, and each delivery is two steps -- a generic handler that takes the id, then
one event-specific vtable slot that does not. The id -> slot map is a 33-entry jump table
in the image, so which class reacts to which event is recovered and exhaustive rather than
inferred from what the saves happen to show. Five of the 33 rows are not in slot order,
including two the tail sends.
Three handlers write the eight leaves that diverged:
* the slavers' difficulty tier, on the tail's end-of-turn delivery -- a three-record
stack table scanned against the frame, boundaries 1/50/100, stored only on a change,
and at frame 100 and above the scan runs off the end and stores nothing, so the tier
can never reach 2;
* the refugees' one-shot latch, on the turn-begin delivery, with a design instantiation
behind the same latch that nothing here can do;
* the swarm queen's hives, also at turn begin, registered on the systems carrying the
SWARM's scenario tag (the queen's constructor stores 3 for that and 10 for its own
encounter id) and then ticked -- and the tick is the whole explanation of a target
turn that reads 31 after one turn and 32 after the next. It is not re-rolled; it slips
forward by one every turn the spawn gates stay shut.
New host phase H03 for the turn-begin delivery, run right after the frame counter where
the original sends it, and tail phase T20 implemented. Rules are pure in game/sim.
Measured on CT111, closed and regressed stated separately:
default turn1->turn2 209 -> 126 (was 128) closed 83, regressed 0
turn2->turn3 108 -> 67 (was 69) closed 41, regressed 0
--commit-blocked=H03 turn1->turn2 209 -> 124 closed 87, regressed 2
turn2->turn3 108 -> 67 closed 41, regressed 0
Registering a hive closes the four leaves that say which systems have hives and that they
have no queens, and opens two carrying a target turn known to be wrong: the original draws
it from the strategic generator inside the turn-begin step, outside both turn drivers, and
neither the two data-file constants nor the generator's position there is settled. That
trade is a flag, not a default.
The prediction in docs/SV-script-objects.md was committed before the build, and P5 was
wrong: it called the second pair a null control, and the second pair is where the slip
rule is tested EXACTLY -- two hives, two target turns, both landing on the oracle with no
draw and no fitting.
Gates run separately: clean-room OK, host ctest 51/51, CT111 shim cross-build clean.
H02 StampTreatyTurns -- the diplomacy ledger's "this treaty was last in force on turn
N" stamp, over every ordered pair of players that holds one, creating the entry on
demand. It is a host step, not a phase of either turn driver: its sole caller is the
command-application step, which runs after the frame counter has advanced and before
both drivers. On a turn with no combat and no diplomatic command it is the only writer
of these fields, which is why a whole per-player dipstats vector is its output.
Read from the instruction stream; three things an earlier reading had wrong are
corrected in findings/subsystems/treaty-turn-stamp.md: the stamped value is the TURN and
not the modification counter, the relation codes are 3=allied / 2=NAP / 1=cease-fire and
not the reverse, and the bit is the player's index field rather than its position in the
player vector -- the opposite convention from the shared-vision mask two files away.
Measured, closed and regressed reported separately and never netted:
turn1 -> turn2 (reference) 209 -> 132 closed 77 (was 51), regressed 0
turn2 -> turn3 108 -> 73 closed 35 (was 21), regressed 0
human-turn2 -> turn3 closed 76 (was 64), regressed 0
zuul-turn15 -> turn16 closed 30 (was 18), regressed 0
zuul-turn16 -> turn17 closed 29 (was 17), regressed 0
The last three are pairs from a different game at turns 2, 15 and 16 that the model was
never fitted to, and it closes exactly the twelve ordered treaty pairs each of them holds.
The rule also reproduces the ledger of ten of the eleven corpus saves entry for entry,
including each entry's order and every stamped value; the eleventh is the turn-1 save
whose ledger no turn has yet written, and it is the reference pair's input.
app_test_treaty pins the five things the corpus cannot separate: the relation codes (no
save exercises cease-fire), the bit's source, the missing alliance-id guard, the -1
initialiser on a fresh entry, and the append-at-the-end order that makes re-running the
step idempotent instead of duplicating rows.
The betrayal half of the same function needs the turn's diplomatic commands; with no
command stream it is provably a no-op and is not modelled.
`sots_turn` loads a save through the engine's own reader, walks the published
phase order of all three turn drivers, runs what we hold, prints what we do
not, and writes the result back through the engine's own writer.
The phase catalog carries all 32 + 12 + 37 phases whether or not they are
implemented, so an unimplemented phase is a named no-op that appears in the run
log rather than a silent absence. 14 of the 44 turn-driver phases are modelled,
7 commit anything, 2 of the 37 tail phases are modelled.
Modelled but NOT committed is a first-class state. A phase whose formula we hold
and whose inputs we do not is evaluated, reported, and left unwritten unless
--commit-blocked is passed. That distinction was earned: committing phase 31's
player-status restore regressed two leaves that had agreed with the oracle
before the turn, because the phase writes 1 and the file carries 4.
Measured against the game's own post-turn saves, leaves localised by
state_checksum.py with coverage proved by re-serialisation:
turn1-state -> turn2-state 209 -> 204 diverging, closed 5, regressed 0
turn2-state -> turn3-state 108 -> 103 diverging, closed 5, regressed 0
Two tests: app_catalog (the tables stay complete and nothing claims to be
verified against a live game) and app_turn (11 saves driven; an untouched load
re-serialises byte-identically, a turn leaves the file re-readable, and no
blocked or stub phase writes anything). Skips cleanly without SOTS_SAVES_DIR.
ctest 38/38, clean-room OK. src/shim untouched. docs/S-standalone.md has the
full gap list.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARBgSooAfokKUy6wKUKEyZ