diff --git a/docs/RB-predictions.md b/docs/RB-predictions.md new file mode 100644 index 0000000..c0034e8 --- /dev/null +++ b/docs/RB-predictions.md @@ -0,0 +1,182 @@ +# Rung B — predictions, written before the replay module exists + +Lane RB, 2026-09-08. Committed **before** `src/game/ai/command_capture.*`, +`src/game/ai/apply_order.*` or `src/app/command_replay.*` were written, and before any +`--turn-commands` run. Rule 2. + +Baseline taken on a **fresh** `build-host` in this worktree (rule 24), `main` at `4f25f1e`, +ctest 54/54, header regenerated from `~/sots-re/ghidra` (1,217 entries, no diff): + +``` +turn2-state.sav -> turn3-state.sav : baseline 108, after our turn 63, closed 45, regressed 0 + our /Sim/ModCount = 14 oracle = 24 input = 12 +``` + +The whole lane is aimed at that one arithmetic gap: our turn contributes the **two driver +bumps** and nothing else, because a command stream is the only other writer and a save does +not carry the AI's. + +--- + +## The inputs this lane replays + +Lane L4's live dumps at `StrategySim::ApplyTurnCommandBatch`, already in the RE repo: +`verify/results/shim/aiorders/l4-turn{1to2,2to3}-aiorders.txt`. Each holds two batches; the +**second** (`seq=2`, `n=8`) is the End-Turn submission. The first (`seq=1`, `n=1`) is the +load-time batch and is deliberately **excluded** — our standalone loads from disk and never +runs it, and the save's on-disk `ModCount` is the value before it. + +--- + +## P1 — `ModCount` lands on exactly 24 on the canonical pair + +Replaying the `turn2to3` block set against `turn2-state.sav` makes `/Sim/ModCount` read +**24**, closing that leaf. The decomposition, item by item, from AI4's cost table: + +| term | count | +|---|---:| +| turn drivers (`S00`, `T00`) — already ours | 2 | +| research-rate gate, blocks 0–3 (pids 16, 32, 496, 512) | 4 | +| list 5 system rates ×1 | 1 | +| list 3 build ×1 | 1 | +| list 10 ×1 | 1 | +| list 14 fleet task ×2 | 2 | +| list 8 fleet move ×1 | 1 | +| list 23 population ×1 — **free half** | 0 | +| **total** | **24** | + +**Falsified if** the run reports anything but 24, and each near miss names its own defect: + +* **25** — list 23 charged; the 17..27 free half is wrong and AI4 §1 must be reopened. +* **23** — the AI's fleet order counted as the interface's single list-14 element; AI2's P1 + and the `(fleetId, mode)` dedup key are wrong. +* **28** — the four uninitialised monster slots' garbage rate payloads read as *set* gates. + This is the parser failing, not the model: those slots carry `pid == 0` with the gate bit + clear and a garbage float behind it. +* **22** — the human's block (pid 16, every list empty) charged 0 instead of 1. The + research-rate gate is set unconditionally by the send-buffer build; a block that ordered + nothing still costs one. This is the boundary case rule 23 asks for and it carries four of + the ten. + +## P2 — the apply order is this permutation, and it is neither list order nor offset order + +The batch is a flat run of 27 per-list loops with three per-player gate loops spliced in: + +``` +lists 6 11 20 19 17 18 5 23 24 +gate loop A { group5(+0x3c, free), target(+0x14, bump), rate(+0x0c, bump) } +lists 1 4 3 21 2 22 9 10 12 13 14 15 16 7 8 25 27 26 +gate loop B { boost(+0x20, bump) } +gate loop C { group4(+0x2c, bump) } +``` + +Each list step is `for each player block: for each element: apply` — so **every player's +list-6 elements are applied before any player's list-11 element**. Order is per list, not +per player. + +**How I will verify it, three ways, and what each cannot show.** + +1. **Address monotonicity of the six inlined bump sites.** `ApplyTurnCommandBatch` inlines + six appliers and each writes `ModCount` in place: `0x0088fe0a` (gate A target), + `0x008902fe` (list 12), `0x008903b9` (list 13), `0x0089046c` (list 14), `0x008905c8` + (list 7), `0x008907bc` (gate B boost). Their positions in the permutation above are + 10th, 18th, 19th, 20th, 23rd and 28th. Adding the three gate-loop heads (A `0x0088fdb0`, + B `0x008907b1`, C `0x0089080a`) gives a nine-point chain that must be strictly increasing + in both address and position. **This is a check on 9 of 30 positions and no more** — the + other 21 lists' handlers are out-of-line and this lane has no record of their call-site + addresses inside the batch, so their relative order rests on AI4's direct read of the + `add edi, imm` sequence and is inherited, not re-derived. + *Falsified if* any of those nine is out of order. +2. **The permutation is a bijection of 1..27**, asserted at compile time. Catches a + transcription slip, catches nothing about the order being right. +3. **Neither sorted.** Asserted: the list sequence is not ascending, and the member-offset + sequence implied by it is not ascending either. A naive implementation that loops + `for (list = 1..27)` or walks the block's members in memory order cannot pass this — which + is the only reason the test earns its place, since on every workload the corpus holds the + *outcome* is order-independent (no two commands in either capture touch the same object + through a modelled handler). + +**Stated plainly: apply order is unfalsifiable on this workload.** Both captures put every +non-empty list on one player and every command on one system, so any permutation produces the +same save and the same count. The order is implemented because a later workload will need it, +and it is tested against the instruction stream rather than against an outcome. + +## P3 — a replay closes `ModCount` and nothing else, and regresses nothing + +Predicted canonical-pair result: **closed 46 (45 + `/Sim/ModCount`), regressed 0, remaining 62.** + +Of the ten commands in the `turn2to3` block set, only three have a handler this lane can +write, and all three are **no-ops on this workload**: + +* the research-rate gate — `ServerPlayer.ResRate` already reads 0.25/0.8 in `turn2-state`; +* list 5 system rates — Ke'Dolarra's rates already read `{0, 1.0, 0, 0, 0, 0, 0}`; +* the research-target gate — not set on any block this turn. + +The other seven need subsystems we do not have (P4). So the replay's entire contribution to +the byte-match distance is the counter. + +**Falsified if:** any further leaf closes — which would mean one of the seven is more +modellable than I claimed, or that a "no-op" write is not one; or any leaf regresses — which +would mean a modelled handler writes the wrong field, and is the failure this prediction +exists to catch. + +## P4 — the no-op control + +Running `--turn-commands` with the three modelled handlers enabled must change **zero** leaves +relative to the same run with them suppressed. That is the control on "these are no-ops here": +a rate written to the wrong player, or rates written into the wrong system's frame, shows up +as a regression against a run that wrote nothing, even though both agree with the oracle by +luck. Rule 1 — the outcome agreeing is not evidence the write went to the right place. + +## P5 — the secondary pair, where the stream actually does something + +`turn1-state.sav` carries `ModCount 0`; the oracle `turn2-state.sav` carries 12. The +`turn1to2` capture holds 4 rate gates, **3 research-target gates** (ids 144, 90, 288 on +players 32, 496, 512), one list-1 design, one list-3 build, one list-5 rates and one list-23 +population. + +**Prediction:** `ModCount` 0 → **12** (2 + 4 + 3 + 1 + 1 + 1 + 0), and — if and only if the +capture carries the resolved tech *names* — the replay closes `ResRate` on players 32, 496 and +512 and `ResTNm` on 32 (`IND_Waldo`) and 496 (`DRV_PlsFiss`). + +**And it must NOT close `ResTNm` on player 512.** The capture records target id **288**, which +that run resolved to `XNC_TrnsMorr2`; `turn2-state.sav` holds `BIO_GnMod`. The recording is +from a *different process* than the oracle, and 512's pick is the one leaf lanes L4 and L5 +both showed is not reproducible. So the recorded stream and the reference save genuinely +disagree there, and a replay that closed it would mean I had fitted something. + +*This is the sharpest prediction in the set*, because it is the one where the replay's output +is determined by the recording and the recording is known to be from a divergent run. If 512 +closes, something is copying the oracle rather than the capture. + +*Falsified if:* `ModCount` ≠ 12; or `ResRate` fails to close on all three; or 512's `ResTNm` +closes; or 32/496 fail to close while the capture does carry names. + +## P6 — the wire carries an id, the save carries a name, and we cannot bridge them + +The research-target gate's payload is an `int techId` (`block+0x10`). `ResTNm` on the wire is +a string. Process Turn phase 18 resolves the chosen object's `std::string` at `+0x4` and +passes a `char*` to `cl_SetResearchTarget`, so the resolution happens **client-side, off the +command**, and the applier receives an id whose mapping to a name this campaign has not read. +`144`, `90` and `288` are not `index * 16` and are not indices into anything we hold. + +**Prediction:** the target-gate handler cannot be modelled from the id alone, and the capture +format must therefore be able to carry the *observed* name alongside the id. When it does, the +leaf is closed **by recorded payload, not by model**, and the report must say so in a separate +column — otherwise a future reader will mistake a transcription for a reimplementation. + +*Falsified if:* someone finds the id→name map, at which point this becomes a modelled handler +and the column empties. + +--- + +## What this lane is not predicting + +* **Nothing about `Summary.Checksum`.** Its inputs are unread; it moves whenever anything else + does. +* **Nothing about the RNG frame.** No command in either capture draws. +* **Nothing about the load-time batch.** `seq=1` is excluded by construction (above), and + whether the original charges `ModCount` for it is untested — a save loaded and immediately + re-saved would settle it in one run and this lane does not do it. +* **Nothing about ordering effects between two commands on the same object.** No workload has + one.