# CB — predictions for the command-stream capture, written before the module exists Lane CB, 2026-09-08. Worktree `wip/cb` off `main` `4f25f1e` (the brief said `7444c3b`; the integrator merged L1 in the meantime, so this branch is based on the newer main and says so). Guest **VM146** (`192.168.10.146`). Committed **before** `ai_orders.cpp` is touched and before anything is built. Lane RB is building `sots_turn --turn-commands `. This lane supplies its input. A replay with no real stream is untested and a stream with no autosave to check it against is a log file, so every prediction below is either *about the stream* or *about the binding between the stream and the autosave of the run that produced it*. --- ## 0. What is being changed, and why it is small Lane L4 already dumps the block set at `StrategySim::ApplyTurnCommandBatch` — one register-transparent entry stub, read-only, measured behaviour-neutral on the canonical pair. This lane does **not** rebuild that. It adds three things and nothing else: 1. **`aiorders.words=`** — the element window, today a fixed 12 words. List 1 carries a polymorphic `ShipDesignDef` whose id sits past 48 bytes, so the turn-1 workload cannot be captured completely at 12. 2. **`aiorders.deep=on`** — follow the heap. Three payloads in the canonical block are behind pointers and are therefore *absent* from L4's capture: list 8's route vector, list 10's counted vector, and list 23's `Population` body. A replayer cannot reconstruct a fleet move without the route. The follower is deliberately **generic** — it scans the element window for anything shaped like `{_Myfirst,_Mylast,_Myend}` and for anything shaped like an MSVC `std::string`, and dumps the bytes. It does **not** know which list it is looking at. 3. Nothing in the instrument types an element. L4's design point — *the dump reads element bytes, not element types; decoding happens offline so a wrong record shows up as a wrong value instead of being baked into the instrument* — is kept, and the typed decode moves to a host-side Python step that can be corrected without another VM run. So `aiorders=on aiprobes=off` remains **exactly one MinHook detour**, and the added work inside it is guarded reads of memory the game already owns. --- ## 1. The capture format (defined here because RB may not have landed first) One JSON file per run under `verify/results/turncommands/`. Mechanical, and the raw words are the ground truth: * `binding` — the input save and both output autosaves, each with size and sha256, plus whether they match the published determinism oracle. **A capture whose `binding.outputs` is absent or whose hashes do not match the run is worthless for a byte-match and must be treated as such.** * `batches[].blocks[]` — one entry per block slot, `playerId`, the six gates with their payloads, and all 27 lists. * Each element carries `raw_words` (the window, verbatim), `vectors` (each followed heap vector, with its element words), `strings` (each decoded `std::string`), and `decoded` — a named record **only where the campaign has one**, with a `record` field naming it and a `wire` array giving the values in the order the writer emits them. * `decoded` is regenerable from `raw_words` by `tools/turncommands_capture.py`. RB should prefer `decoded.wire` and fall back to `raw_words`; if the two ever disagree, `raw_words` wins. The one thing the decoder must get right and that is easy to get wrong: **list 3's in-memory element is in the opposite order from its wire record.** Lane L4 read that off the writer (`0x00822870` emits `+0x14, +0x10, +0x0c, +0x08`, descending); lists 5, 8, 10, 14 and 23 all write ascending. That is per-list, not a rule, and the decoder encodes it as such. --- ## 2. Runs, and what each is for | run | save | `hooks` | `aiorders` | `watch` | purpose | |---|---|---|---|---|---| | **C0** | `ref-turn2.sav` | `off` | — | `off` | rule-19 control: the oracle bytes with no instrument at all | | **C1** | `ref-turn2.sav` | `trace` | `on`, words=32, deep | `off` | **the deliverable** — canonical pair capture | | **C2** | `ref-turn2.sav` | `trace` | `on`, words=32, deep | `on` (modcount) | the trap multiset, as RB's second check | | **C3** | `turn1-state.sav` | `trace` | `on`, words=32, deep | `off` | the non-deterministic pair, block + *its own* autosave | | **C4** | `turn1-state.sav` | `trace` | `on`, words=32, deep | `off` | the same turn twice | --- ## 3. Predictions ### P1 — C0 reproduces the oracle. (control) `(Autosave EndTurn).sav` = 66,732 B, `bb4fd9ac89f41e3b`; `(Autosave).sav` = 67,219 B, `978041acd168b56e`. If this fails, **the guest is not the reference guest** and nothing else in this lane means anything; lane L4 already reproduced these on VM145 and lane L5 held VM146, so a failure here is a lab fact, not a finding. ### P2 — C1's autosaves are byte-identical to C0's, so the capture binds. The deliverable stands or falls on this. Lane L4 ran **seventeen** detours on this same workload and got the oracle bytes; this run has **one**, doing strictly more reads inside it. If C1's autosaves differ from C0's, the capture is a stream that changed the turn it recorded, it is unusable for a byte-match, and the report must lead with that (rule 19). ### P3 — the block set on the canonical pair is exactly lane L4's, plus the three payloads. Two batches: `seq=1` at load (`n=1`, pid 16, all 27 lists empty) and `seq=2` at End Turn (`n=8`). Slots 0–3 are pids **16, 32, 496, 512**; slots 4–7 are pid **0** with uninitialised gate payloads. Only pid 32 is non-empty, with list sizes `3:1, 5:1, 8:1, 10:1, 14:2, 23:1`. Every rate gate on the four real players is **set**; no research-target gate is set on any player this turn. New, and the reason for the run: * **P3a — list 8's route vector has count 1.** Its single value is the destination system id. L4 could not read it and noted that `turn3-state.sav`'s waypoint says **272**. Predict **272**. *Falsifier:* any other id, in which case the waypoint in the output save is not the route in the command and the replayer has to carry both. * **P3b — list 10's counted vector has count 1.** L4's reading is "at system 288, fleet 34, [one object]". Predict the value is a **ship id that already exists in `ref-turn2.sav`** — the pre-existing ship being folded into the newly allocated fleet 34. *Falsifier:* a small client-allocated id in the 18/34 space, which would mean the client allocates the ship too and the id-allocation hole L4 left open is wider than one counter. * **P3c — list 23's `Population` vector is 24 bytes, i.e. six words**, at a stride the dump does not name. Predict the six words are **not all zero** and that they **differ between the turn-2 and turn-1 captures** (L4 saw the element's trailing int go `+1` on turn 1 and `−1` on turn 2, so the body is turn-dependent and not a constant the replayer can ignore). ### P4 — C2's trap multiset is exactly ten bumps, and they attribute. The paying half of pid 32's block is `3:1 + 5:1 + 8:1 + 10:1 + 14:2` = **6 elements**, list 23 being free; plus **4** research-rate gates, one per submitted block, set unconditionally. Total **10** — which is the number lane W2 measured on this turn from the other end, before anyone had seen the block. Predicted per-EIP: | trap EIP | count | what | |---|---:|---| | `0x00821a87` | 4 | prologue gate `+0x0c`, research rate, one per submitted block | | `0x0084946e` | 1 | list 3, build | | `0x0086c3e9` | 1 | list 5, system rates | | `0x0088bf01` | 1 | list 10 | | inlined at `ApplyTurnCommandBatch+0xabc` | 2 | list 14, the AI's two `(fleet, mode)` elements | | list 8's site | 1 | fleet move | *Falsifier:* any total other than 10, or any bump attributed to a list this capture reports empty. That would mean the counter is not the length of the command stream and the whole `ModCount`-from-the-block claim in `src/game/ai/orders.h` is wrong. *Rule 19 for C2 specifically:* the watchpoint module has never been run in the same process as the block dump. If C2's autosaves move off the oracle, C2 is reported as perturbing and **C1 remains the deliverable** — the trap set is a bonus check, not the capture. ### P5 — C3 and C4 differ in exactly one word of the stream. Both from `turn1-state.sav`. Predicted block: pid 32 with lists `1:1, 3:1, 5:1, 23:1`, three research-**target** gates set (pids 32, 496, 512) and the human's clear, four rate gates set. The prediction that matters: **C3's and C4's block sets are byte-identical except for player 512's research-target payload at `block+0x10`**, and each run's autosave differs from the other's only in `p512.ResTNm` and the derived checksum. That is Rung B's claim in its sharpest form — if the two streams differ *only* where the two autosaves differ, then the sim is a function of the stream and the decision layer is the only thing that is not. *Falsifiers, and each is worth more than the prediction:* * *The two blocks are identical and the two autosaves differ.* Then the stream does **not** determine the save, Rung B is not reachable by replay alone, and the divergence is downstream of the command — which contradicts L4 §3.1, which saw it in the block. * *The two blocks differ somewhere else as well* (an id, an ordinal, a route). Then the client-allocated id space is also per-process and a replay has to carry ids it cannot derive. * *Player 512's target lands on a tech outside the six-member `XNC` set L4 named.* Then k is larger than six and Rung C's tie-set claim has to start one level up. `BIO_GnMod` has already been observed once, so this is not unlikely; a second `BIO` value would settle that the resolver leaves the family. ### P6 — list 1's design element yields its id and its name at words=32. `"Honor Lance"` is a short-string-optimised `std::string` inside the element, and design id **18** is named by list 3 in the same block. Predict the widened window shows the string *and* a word equal to **18** inside the list-1 element. *Falsifier:* no 18 anywhere in 128 bytes, in which case L4 §1 P3 stands as it is — the design command does not carry its own id and the id is inferred. ### P7 — what this lane will **not** be able to say. Stated in advance so a green capture is not read as more than it is (rule 15): * Two turns, one AI empire with anything to do, a board with no contact. Lists 2, 4, 6, 7, 9, 11–22, 24–27 will be **empty in every block of every run**, exactly as they have been in all eleven corpus saves and both of L4's runs. The capture exercises **six** of twenty-seven lists and **one** of six gates on the canonical pair, two of six on turn 1. Everything the replayer does with the other twenty-one lists is untested by this lane and must be labelled as such. * The generic vector follower can produce a **false positive** — three consecutive words that happen to look like a vector. The count bound and the readability probe make that unlikely, not impossible. Any `aivec` line on a list whose record has no vector is to be read as noise until a second run reproduces it. * Nothing here says what list 10 or list 23 *mean*. It says what bytes they carry.