From fdea0fde081fa4ca251e7b45c5bb7f3b01bb6db8 Mon Sep 17 00:00:00 2001 From: alex Date: Wed, 9 Sep 2026 10:13:49 -0400 Subject: [PATCH] lanes CR and DW merged: research stays compared (16 leaves, all OnTechResearched); writer byte-exact on 43 saves; gate locked; exhaustive-config blind spot --- campaign/DASHBOARD.md | 36 ++++++++++++++++++------------------ campaign/board.md | 6 ++++++ tools/displacement.py | 9 +++++++-- tools/gate.sh | 9 +++++++++ 4 files changed, 40 insertions(+), 20 deletions(-) diff --git a/campaign/DASHBOARD.md b/campaign/DASHBOARD.md index cc49b48..123264a 100644 --- a/campaign/DASHBOARD.md +++ b/campaign/DASHBOARD.md @@ -1,17 +1,17 @@ # SotS RE campaign — coverage dashboard -Generated 2026-09-09 13:40 UTC · `sots-re` @ c4ea347,2026-09-09 · `sots-engine` @ e7e2bd6,2026-09-09 (245 commits) · regenerate with `tools/dashboard.py` +Generated 2026-09-09 14:13 UTC · `sots-re` @ d4e9b58,2026-09-09 · `sots-engine` @ 52db23c,2026-09-09 (250 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) -447 targets · mapped-or-better **399/447** `[█████████░] 89%` · verified **356/447** `[████████░░] 80%` +453 targets · mapped-or-better **404/453** `[█████████░] 89%` · verified **361/453** `[████████░░] 80%` | Status | Count | % | |---|---:|---:| -| verified | 356 | 80% | -| mapped | 43 | 10% | +| verified | 361 | 80% | +| mapped | 43 | 9% | | in-progress | 5 | 1% | | backlog | 41 | 9% | | blocked | 2 | 0% | @@ -22,16 +22,16 @@ Generated 2026-09-09 13:40 UTC · `sots-re` @ c4ea347,2026-09-09 · `sots-engine | control-flow | 41 | 2 | 1 | 0 | 0 | 44 | | subsystems | 4 | 8 | 0 | 2 | 1 | 15 | | engine | 33 | 1 | 0 | 0 | 0 | 34 | -| verify | 105 | 15 | 3 | 35 | 0 | 158 | -| phase2 | 13 | 3 | 1 | 0 | 0 | 17 | -| meta | 94 | 6 | 0 | 1 | 0 | 101 | -| other | 20 | 2 | 0 | 0 | 0 | 22 | +| verify | 106 | 15 | 3 | 35 | 0 | 159 | +| phase2 | 15 | 3 | 1 | 0 | 0 | 20 | +| meta | 95 | 6 | 0 | 1 | 0 | 102 | +| other | 21 | 2 | 0 | 0 | 0 | 23 | ## 2. Binary understanding - RTTI type descriptors: **1,924** (`Game::` 1,404, `Mars::` 194; serializable types 179) - Classes with recovered member layouts: **384** / 1,598 named classes `[██░░░░░░░░] 24%` — `objects/layouts.json` (serializer recovery) plus classes recovered by hand in `struct-recovery.md` + `schema-gaps-resolved.md`. Note 179 types are *serializable*; the recovery also reaches non-serializable ones, so this is not a subset of that -- Functions: **41,411** (parsed from `01-fingerprint.md`); named/annotated in the **address contract** (`ghidra/addresses.json`, not Ghidra's full rename count): **1298**, verified **1167** `[█████████░] 90%` +- Functions: **41,411** (parsed from `01-fingerprint.md`); named/annotated in the **address contract** (`ghidra/addresses.json`, not Ghidra's full rename count): **1310**, verified **1179** `[█████████░] 90%` ## 3. Data layer @@ -58,13 +58,13 @@ Generated 2026-09-09 13:40 UTC · `sots-re` @ c4ea347,2026-09-09 · `sots-engine | `game/sim` | 4,780 | 14 | 1038 | yes | game-sim.md | | `mars/parse` | 875 | 12 | 277 | yes | mars-parse.md | | `mars/rng` | 273 | 0 | 0 | yes | mars-rng.md | -| `mars/stream` | 5,834 | 8 | 329 | yes | mars-stream.md | +| `mars/stream` | 6,021 | 8 | 331 | yes | mars-stream.md | | `mars/text` | 899 | 8 | 245 | yes | mars-text.md | | `mars/vfs` | 788 | 9 | 140 | yes | mars-vfs.md | -| `shim` | 16,709 | 0 | 0 | direct (WIN32) | H-probes.md | -| `shim/hooks` | 13,593 | 0 | 0 | direct (WIN32) | L1-predictions.md | +| `shim` | 16,777 | 0 | 0 | direct (WIN32) | H-probes.md | +| `shim/hooks` | 13,658 | 0 | 0 | direct (WIN32) | L1-predictions.md | | `shim/trace` | 2,258 | 9 | 273 | direct (WIN32) | shim-trace.md | -| **total** | **59,514** | **122** | **4257** | | | +| **total** | **59,834** | **122** | **4259** | | | Board `engine:` rows: verified **33**, mapped 1, in flight 0 (of 34) — verified & merged `[██████████] 97%` @@ -133,11 +133,11 @@ Most recent open: ## 9. Delta since previous dashboard -- verified targets: 354 → 356 (+2) · mapped-or-better: 396 → 399 (+3) -- engine LOC: 59,514 → 59,514 (+0) · test files: 122 → 122 (+0) · checks: 4,257 → 4,257 (+0) -- addresses verified: 1,163 → 1,167 (+4) · recovered layouts: 384 → 384 (+0) · open questions: 26 → 26 (+0) +- verified targets: 356 → 361 (+5) · mapped-or-better: 399 → 404 (+5) +- engine LOC: 59,514 → 59,834 (+320) · test files: 122 → 122 (+0) · checks: 4,257 → 4,259 (+2) +- addresses verified: 1,167 → 1,179 (+12) · 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, 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 - +warnings: board.md line 459: unknown status 'open' for '**A replace of `ProcessResearch` is gated on `ServerPlayer::OnTechResearched`, and partly on a policy question**'; board.md: unknown types engine + gate, 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 265cf63..1665311 100644 --- a/campaign/board.md +++ b/campaign/board.md @@ -452,3 +452,9 @@ Status flow: `backlog → in-progress → mapped → verified` (or `blocked`). | RUNG B ON A RICH TURN: THE REPLAY RUNS AND CONSUMES THE WHOLE STREAM; 1092 LEAVES, THIRTEEN NAMED SUBSYSTEMS, ZERO UNGROUPED | engine | verified | high | 100% | 2026-09-09 | Lane CV, HOST ONLY (no VM; 140/141/145/146 untouched). First replay of a RICH turn's command stream: lane BR's deep block (`BR2-deep-aiorders.txt`) converted to `.tcb` and replayed by `sots_turn --turn-commands` against `ad-turn27-two-raiders.sav` (`1c8baa27...`), compared with `bp-pinB-turn28.sav` (`724528ff...` = BR's own output, cmp-verified). **VERDICT: OUTCOME 3 -- a named leaf list, not a byte match.** `state_checksum --floats bits --mask none` = **DIVERGED: 1092 leaf difference(s)**; the do-nothing baseline (input vs target) is **1166**, so the turn+replay CLOSES 80 and REGRESSES 6 (never netted). The stream's entire contribution to the state is ONE leaf: `/Sim/ModCount` 1430 -> 1500 against a target of 1502 (85 commands = 81 elements + 4 gates, 70 bumps charged = 85 - the 15 list-23 population commands the cost table charges 0; 2 driver bumps; **residual 2**). **THE RESIDUAL IS LOCALISED:** the same binary on the CANONICAL pair (`turn2-state.sav` + lane CB's tcb) puts `ModCount` on **12 -> 24 EXACTLY, zero residual**, matching `turn3-state.sav` and confirming CB's own prediction -- so the cost table is right on lists 3/5/8/10/14/23, and the missing 2 must sit in lists **1 (3 elements), 7 (2), 12 (12)**, the only three the rich turn adds. The only UNIFORM per-element explanation is **list 7 (colonisation) at 2 bumps per command** (3 does not divide 2, nor does 12) -- a constant fitted to ONE observation (rule 23), so it is a PREDICTION with a named cheap falsifier: any capture with a colonize count other than 2. GUARD SET, ALL SIX RUN (rule 1): (a) 85/81 matches BR's block element-for-element and per-list; (b) the DEEP payload provably reached the replayer -- shallow vs deep captures of the same run move exactly 7 elements from `incomplete` to `declined` (list 8's 3 routes, list 10's 4 ship vectors), i.e. on those seven the gap is now the ENGINE not the record; (c) `--replay-count-only` produces a BYTE-IDENTICAL save, confirming `leaves written by commands 0`; (d) deleting one `elem` line makes the reader REFUSE with exit 2; (e) replaying against another board warns; (f) output != input and != the no-stream output. TWO BLOCKERS PUT OUTCOMES 1 AND 2 OUT OF REACH BEFORE THE TURN RUNS: **(1) the engine's typed writer is not byte-exact on this save** -- `--roundtrip` DIFFERS by exactly 12 bytes = one item, at 0x3f02: `struct ProjectName` reads `usnc` as a count of ONE item per element (`a.any("usc")`, commented "usnc is 0 in every save available") but the wire pair is `usp` THEN `usc`, and this save has one record with `usnc == 1`. The generated schema already lists both. 12 of 43 corpus saves fail the same way; `mars_stream_save` and `app_turn` fail on it in ctest (57/59, pre-existing, denominator intact). Surfaces as `/Sim/sprjs/usp: only-in-B`. NAMED, NOT FIXED (schema-verified module, byte-neutrality must be re-proved). **(2) `--relabel-new-ids` REFUSES by guard G3** -- "A minted [], B minted [1970, 1986, 2002]" -- because this engine mints NO client fleet ids at all; the refusal applies nothing and the 1092 is byte-identically the no-flag number. Outcome 2 needs list 10's applier first. **THE TAIL'S SIM DRAW FIRES HERE:** every trade sector in this save reads `tscr = 253`, not the corpus's 252 -- this IS lane AD's two-raider state, measured at 2 entries / 2 strategic words at 0x00820e18 -- so the backlog's Rung B correction is load-bearing on this workload and `S02`/`T23` are stubs; `--commit-rng` leaves the count at 1092 because the 16 words are the wrong words. RANKED WORKLIST (full list in the finding, residual 0): morale event ring `cme2` **539**; colony growth/repair/bonuses **154**; fleet objects **150**; observed designs/techs **79**; player money+counters **30**; `TShn` **23**; system<->fleet membership **23**; ShipRecs **20**; build queues **20**; other system **15**; crep/CD **13**; master id lists+generator **10**; turnstats **8**; trade manager **6**; writer defect **1**; Checksum **1**. **THE 539 MUST BE DECODED BEFORE IT IS RANKED:** `cme2` is a FIXED 11-SLOT RING (158 events in input, output and target alike), so the turn's **27 appended events across 10 systems** shift every slot. The 27 are three kinds only: 10x `mtp=12` "Colony established on Avalon." (+2), 10x `mtp=20` "Imperial savings of 5,000,000 or more." (+2, down a band from the pre-turn 15,000,000), 7x `mtp=16` "Population limits inhibit freedom." (-1). So the ring is the REPORTING SURFACE of colonisation, the savings chain and the population term -- it owns none of them. **THE LARGEST INDEPENDENTLY CLOSABLE MODULE IS `T34 RecordObservedDesigns` (stub): 79 leaves, no upstream dependency.** TWO MEASURED CORRECTIONS: **`Pv*` is NOT a previous-turn snapshot** -- target `PvInfra` 1.03 and `PvPop` 617,999,994 where both pre- and post-turn `Infra`/`Pop` are 1.0 / 600,000,000 (= current x 1.03), so `Pv*` holds the UNCAPPED GROWTH PROJECTION on a capped colony, while `PvSuit` (5/5) and `PvRes` (4/4) DO equal the pre-turn current: the family is mixed and must not get one rule. And **five of the six regressions are `ntdev`** on Sys[160/400/416/432/448], where the engine writes 1 and both input and target hold 0, while the SAME rule closed 26->27 on thirteen other systems -- a one-predicate bug, the cheapest fix here. Also: the capture already holds the three design NAMES the target creates (`Egg Thief Mk 4/5`, `Bravestar Mk 3` = Des[2018/2034/2050], node-2 counters 126/127/128 continuing the fleets' 123/124/125 -- one counter across object kinds, lane BT §4.3), and list 1 is declined for want of a design-registration path. AND: **the one list `command_replay.cpp` CAN apply -- list 5, the planetary sliders -- fired ZERO times on all 15 elements**, because the shim's memory window cannot type 7 of its 8 fields; the settling experiment is one UI run (two different sliders to two different values), not engine work. TOOL CHANGE: `tools/aiorders_to_tcb.py` now reads the deep dump's `aivec`/`aistr` rows, consulting them ONLY at a word the existing field map already types as a vector, cross-checking the begin/end length against the followed count, and recording anything it cannot place as a `#` comment plus a stderr line (12 list-12 vectors, 15 list-23 vectors, 3 list-1 design names, 1 list-3 vector) rather than inventing a field mapping. REGRESSION-CHECKED against every aiorders log in the repo: 8 shallow logs byte-identical, and **the 4 lane-CB logs were deep dumps whose payloads this converter had been silently discarding** -- the only field changes are list 8's route and list 10's ship vector going `v1` -> `v1:272`/`v1:1728`, which is EXACTLY what `tools/tcb_from_json.py` already emits for the same capture, so the two converters now agree everywhere but list 23 (where the committed `cb-turn2to3.tcb` predates the current `tcb_from_json.py`'s own `UNMAPPED_HEAD[23]`; no behavioural difference, recorded not touched). The canonical pair's replay is byte-identical under the committed tcb and a regenerated one. Artefacts: `findings/subsystems/rung-b-rich-turn.md`, `verify/results/turncommands/br2-turn27-{deep,shallow}.tcb`, `verify/results/standalone/cv/*` | | MY GATE WAS HOLLOW FOR A DAY: the corpus tests were SKIPPING and I reported 59/59 | meta | verified | high | 100% | 2026-09-09 | `SOTS_SAVES_DIR` was never set on CT111, so `mars_stream_save` and `app_turn` printed "unset, skipped" and **passed**. Every "59/59" I reported today did not run them. Lane CV, running on the host with the corpus, got **57/59** - and locally against all 43 saves the picture is worse: **39 failures**, two classes. (1) **The coverage ratchet BROKE when the corpus grew 22 -> 43** - `pct >= 99.99` fails on the spy saves and others; rule 27 working exactly as written, unseen. (2) **A writer defect**: `ProjectName` reads `usnc` as one item and the wire is a `usp`/`usc` pair; 12 saves round-trip 12 bytes short. Rule 1's shape - a green verdict on a harness comparing nothing - delivered by the person whose job was to notice. FIX: `tools/gate.sh` is now the gate; it syncs the corpus, sets the variable, runs every check as its own command, and **fails RED if any corpus test skips**. First fresh run: 57/59 and RED on one remaining skip, correctly | | `mars/stream`: `SpecialProjectNameGen` is a `usp`/`usc` PAIR, not a single `usc` | engine | mapped | high | 40% | 2026-09-09 | Lane CV. `struct ProjectName` reads `usnc` as a count of ONE item per element, commented "usnc is 0 in every save available" - **falsified by `ad-turn27-two-raiders.sav`**, which has one record with `usnc == 1`. The generated schema already lists both tags. **12 of 43 corpus saves round-trip 12 bytes short**, all in the `sprjs` band; it is why `mars_stream_save` and `app_turn` fail. Surfaces in the replay as `/Sim/sprjs/usp: only-in-B`. **Named, not fixed** - it is a schema-verified module and byte-neutrality must be re-proved after the change. Track 0a in the backlog | +| THE TYPED WRITER IS BYTE-EXACT ON ALL 43 SAVES; THE RATCHET WAS HONOURED, NOT MOVED; AND THE GATE ITSELF WAS THROWING AWAY CTEST'S EXIT CODE | engine + gate | verified | high | 100% | 2026-09-09 | Lane DW, HOST ONLY (no VM; 140/141/145/146 untouched; CT111 only as the gate's build host). Track 0 items 0a and 0b, both closed. **`tools/gate.sh --fresh` is GREEN: 59/59 with the 43-save corpus, 0 corpus tests skipped, shim cross-build present.** `mars_stream_test_save` goes 39 failures across 15 of 43 saves -> 0; all 43 round-trip BYTE-IDENTICALLY through the typed writer and through `sots_turn --roundtrip`; `SchemaProbe` still reports 0 MISMATCH with SIX new shapes bound, every one matching item-for-item. **(0a) `usp` IS NOW READ, NOT GUESSED.** From `Game::SpecialProjectNameGen::Write` (0x008147e0): each 88-byte name record opens with a 32-BYTE TABLE OF PER-SUFFIX USE COUNTS; the unrolled loop at 0x814850 counts the NON-ZERO slots and writes that as `usnc`; the loop at 0x814895 then walks slots 0..31 and for each non-zero one writes the SLOT INDEX as `usp` (0x8148b1) and the slot's COUNT BYTE, movzx-widened, as `usc` (0x8148c2) -- both through WriteInt, so both i32 on the wire (rule 5); `Nm` at record+0x20 and `Ntg` at +0x3c follow. Tag literals at 0xa2bbf8/0xa2bbf4 confirm the ORDER independently of the one save that shows it. This kills the ambiguity the corpus alone cannot: `usnc=1,usp=0,usc=1` also fits "usp is a nested count", and the disassembly refutes that -- `usp` is the induction variable, written unconditionally inside `usc`'s own if-body. 12 saves were 12 bytes short = exactly one i32 item (4 len + 3 tag + 4 value + 1 pad). **(0b) THE RATCHET WAS CATCHING THREE BODIES, NOT ONE -- CV NAMED ONLY `usc`.** Reading the per-save opaque tags rather than the percentage shows `Sprj=14` (12 saves), `usc=1` (12), `FTPnts=6/12/18` (6 saves, THREE OF WHICH HAVE NO WRITER DEFECT AT ALL), `RNG=2` (43, deliberate). All three typed. **`Sprj` IS A POLYMORPHIC FRAME AND ITS DISCRIMINATOR MAPPING IS NOW MEASURED IN BOTH DIRECTIONS:** ServerPlayer::Write (0x856fe4) emits `SprjT` from the PLAIN MEMBER at project+0x3c, not a vfunc; ServerPlayer::Read (0x881aa7) feeds it to the factory at 0x8610a0 whose 4-entry table (0x8611e8) is 0 -> BackEngProject (vft 0xa3185c), 1 -> MonitorProject (0xa3180c), 2 -> JewelsProject (0x9fa438), 3 -> TechOfferProject (0xa31884) -- and EVERY ONE OF THOSE CONSTRUCTORS STORES ITS OWN INDEX BACK AT +0x3c, closing the loop. `Game::TechProject` (0xa31834) has NO factory entry, cannot come off a save, and shares BackEngProject's serializer byte for byte anyway. ONLY SprjT 0 IS EXERCISED (one back-engineering project on "Magonian Prophicies"/`DRV_RecFiss`, identical in all 12 saves); arms 1/2/3 are typed from schema+factory and LABELLED AS HYPOTHESES (rule 6), with an unknown SprjT falling to `rest()` so it breaks the ratchet instead of being mis-read. **`FTPnts`: the workload the old comment asked for was ALREADY IN THE CORPUS** (rule 28 practice 5) -- six saves carry stored tactical formations (`ap-turn22` 2 points, `az-turn23` 3, four others 1 each), the framing is the ordinary CArr, and `Game::FieldTemplate::Point` is now bound 5/5. Caveat kept: FTPPosX/FTPPosY/FTPSqd read 0 in EVERY observed point, so their i32 disk type is the schema's word and not the corpus's. **THE RATCHET WAS NOT LOWERED AND COULD NOT USEFULLY BE RAISED -- THAT IS A RESULT.** With only the MT19937 block left opaque, `pct` still ranges 99.99400 (`zuul-turn5-species5`, 33,318 stream items) to 99.99746 (`ar-r1-turn43-post`, 78,633 items), because the number is 2/items: raising the bar to 99.995 would FAIL A SAVE THAT TYPES PERFECTLY and pass a save twice its size carrying a whole new opaque body. A percentage ratchet on a fixed-size debt is a ratchet on SAVE SIZE. So `pct >= 99.99` stays and a STRICTLY STRONGER line is added beside it: `cov.opaque == 2` and the opaque tag set is exactly {RNG}. It breaks on the save that has the new body, and it names the body. DOMAIN CENSUS (43 saves): 736 fields / 536 vary / 200 constant BEFORE -> **755 / 538 / 217** AFTER; the 19 new fields are the three bodies, and **only two of them (`FTPShID`, `FTPDesID`) have ever been seen to move** -- typing a body honestly adds far more unexercised fields than exercised ones. `kVaryingBaseline` raised 490 -> **538** deliberately, with date and count. **AND THE GATE WAS NOT HONEST, IN TWO WAYS.** (a) `ctest ... | tail -15` returns TAIL's exit code, so the baseline run prints `GATE ok host-ctest` DIRECTLY ABOVE `2 tests failed out of 59` -- rule 1's failure inside the script written to prevent it; the same masking was on the build step; both now capture the real status and trim after. (b) the skip check counted every `unset, skipped` line, including two tests that are not corpus tests, so it could never reach zero; it is now scoped to the corpus-gated tests AND every remaining skip is printed by name on its own `GATE info skip:` line. The fix makes the gate do MORE: `SOTS_DATA_DIR=/srv/re-lab/gob-extract` is now set, so **`game_design_census` runs against the corpus for the first time in this gate -- 43 saves, 2,728 designs, 1,962 census leaves, 0 mismatched**, a test that was silently skipping and is now a real check. One skip remains and is NAMED, not hidden: `smoke_real_save` wants `SOTS_SAVES_JSON` from the RE repo's `save_reader.py`, and it is a PRINT-ONLY TEST WITH NO ASSERTIONS, so satisfying it would add no check and would couple the engine gate to the reader rule 8 warns about. Artefacts: `findings/subsystems/writer-byte-exact.md`; engine commits `076f714` (the three bodies) and `00e9266` (the ratchets) on `main`; `tools/gate.sh`. | +| **`ProcessResearch` replace: our code ran instead of the original's, and the oracle missed by 16 leaves — all of them `OnTechResearched`'s** | phase2 | verified | high | 95% | 2026-09-09 | **Lane CR, VM145**, `findings/subsystems/research-replace.md`. Predictions committed before the build (`sots-re` 4b3cc82); engine `wip/cr` 618ccb1 adds `research.replace_cascade=on\|off` (default off) so the shipped and extended behaviours differ by **a config line, not a binary**. New certified pair `turn3-state.sav` → one End Turn, **load** route, `e00eed0c…`/`79df5047…`, 2 `hooks=off` processes + 1 compare. Compare run **3/3/0 exit 0** and byte-identical to the control (rule 19 satisfied), undeclared writes exactly the 6 predicted spans. **Replace, cascade on: 16 diverging leaves; cascade off: 27.** The 11-leaf delta is the whole `SetResearched` cascade and it is **ours**; with the pass's own 2 words that is **13 of 13 tech-tree leaves the turn moves, produced live by our code**. The 16 residual leaves are `OutMod`, `ConMod[0..2]`, `ResTNm`, one `ObservedTech` element, two event records + `EvNxID`, and 5 derived. **Verdict: stays `compared`.** | +| **`ref-turn2` + one End Turn does NOT exercise a research completion** | verify | verified | high | 100% | 2026-09-09 | Lane CR. The campaign's most-reproduced oracle is a **quiet turn for the completion path**: `unlock-b3-t1.md` reports `0 undeclared write(s) in 0 call(s)` and `unlock-shim.log`'s first three lines read `completions=0`. It *is* a real workload for the pass (one RNG word, `flag 1 → 2`, one over-budget event) — but a replace-mode oracle taken there would be rule 1's green verdict on a hook comparing nothing. The completing turn is the **next** one, from `turn3-state.sav`. | +| **`# exhaustive` is exhaustive over the 27 template hooks only — six detours are installed, not one** | phase2 | verified | high | 100% | 2026-09-09 | Lane CR. A config naming all 27 registered hooks `off` except one, passing `tools/check_shim_configs.py`, still installs **6** detours: the M0 `Application::Initialize` asm stub (unconditional whenever `hooks != off`) and the **FPU-force module's four sampling detours** (`force=off value=0x0000 sample_ticks=on sample_turn=on`, on by default). `check_shim_configs.py` cannot see either group — no `hook.` key names them. What made lane CR's runs safe was the **neutrality check** (compare run byte-identical to two `hooks=off` controls), not the config check. A lane reading "exhaustive, therefore one detour" is wrong by five. | +| **A replace of `ProcessResearch` is gated on `ServerPlayer::OnTechResearched`, and partly on a policy question** | phase2 | open | high | – | 2026-09-09 | Lane CR. To reach `replaced`, three things are needed: the ~90-field tech-effect write-back applied live (B2 has `game/effects/tech_effects` host-tested), the `ObservedTech` element constructed (`ours` already decides the append), and the two research event **records** written. The third is not an implementation gap: their `EvDsc`/`EvMsg` text comes from the game's string table, which the engine must not carry, so a byte-identical oracle on any completion turn requires calling the game's own `PostEvent` and accepting a `ComputeBudget`-shaped **QUALIFIED** `replaced`. Settle that before spending a lane. | +| `# exhaustive` WAS EXHAUSTIVE OVER THE WRONG LIST - five determinism probes ran with four extra detours | meta | verified | high | 100% | 2026-09-09 | Lane CR found it: a config naming all 27 template hooks `off`, passing `tools/check_shim_configs.py`, still installed **six** detours - the M0 `Application::Initialize` asm stub (unconditional whenever `hooks != off`, nameable by nothing) and the **FPU-force module's four sampling detours** (`fpu.sample_turn`/`fpu.sample_ticks` default ON, keys that do not start with `hook.`). The checker I built this morning could not see either. **So `bupin`, `cbpin`, `crcompare`, `crreplace0`, `crreplace1` all ran with four detours they did not declare.** The measurements STAND - every one was proved byte-neutral against a control (rule 26(d)), which is what made them safe; the config check was not. Fixed: the checker now requires both FPU keys `off` in an exhaustive config and names the M0 stub in its OK line; all five configs amended. A rerun of any of them is a slightly different instrument (four fewer detours) and should say so | diff --git a/tools/displacement.py b/tools/displacement.py index a5f67e5..e6c7e3f 100644 --- a/tools/displacement.py +++ b/tools/displacement.py @@ -58,8 +58,13 @@ ROWS = [ "every call; only 20 distinct states across 4,437 calls"), ("TechTree::ProcessResearch + unlock cascade", "compared", "35 calls across 3 workloads, 0 divergences, tracecmp exit 0; advance prediction held on a " - "changed workload (unlock costs no earlier report contained)", - "compare only, never replaced"), + "changed workload (unlock costs no earlier report contained); REPLACE ATTEMPTED live (lane " + "CR): ours ran instead of the original on a real completion and produced all 13 tech-tree " + "leaves the turn moves, but the save oracle missed by 16 leaves", + "compare only. The replace attempt failed on ServerPlayer::OnTechResearched, not on the " + "research model: 5 player tech-effect fields, 1 ObservedTech element and 2 event records, " + "plus 5 derived leaves. Gated on B2. Thin: 1 completion, 1 tech, 2 of 3 calls allocate zero " + "points, and the RNG region did not move at all on the replace workload"), ("ServerPlayer::OnTechResearched", "compared", "3 completions, 0 div; float32 confirmed bit-for-bit on the game", "compare only; RollResearchEvent's branch has fired once in four sessions"), diff --git a/tools/gate.sh b/tools/gate.sh index aed03b2..1046f89 100755 --- a/tools/gate.sh +++ b/tools/gate.sh @@ -15,6 +15,15 @@ # Usage: tools/gate.sh [--fresh] --fresh: rm -rf the remote build dirs first (rule 24) set -u +# One gate at a time. Lane DW saw up to FOUR `gate.sh --fresh` runs concurrently, all rsyncing +# into the same remote tree and rm -rf'ing its build dirs -- rule 24's shape with no lock, and a +# way to report a build that is half someone else's. Every lane runs from this host, so a local +# flock is sufficient. Waits up to 30 min, then fails loudly rather than running unlocked. +if [ -z "${SOTS_GATE_LOCKED:-}" ]; then + exec env SOTS_GATE_LOCKED=1 flock -w 1800 /tmp/sots-gate.lock "$0" "$@" \ + || { echo "GATE FAIL could not acquire /tmp/sots-gate.lock within 30 min"; exit 2; } +fi + ENGINE="${ENGINE:-$HOME/sots-engine}" RE="${RE:-$HOME/sots-re}" CT=111