sots-engine/docs/RB-predictions.md
alex 1e6474b31c RB: predictions for the recorded-command replay, before the module exists
The headline is one number: our turn writes ModCount 14 where the oracle writes 24, and the
missing ten are the turn's command stream. P1 decomposes the ten and names what each near
miss would mean; P5 predicts the secondary pair closes five leaves and must NOT close the
sixth, because the recording is from a run that diverged there.
2026-09-08 18:33:17 -04:00

182 lines
9.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.