diff --git a/docs/CB-predictions.md b/docs/CB-predictions.md new file mode 100644 index 0000000..e4ba23b --- /dev/null +++ b/docs/CB-predictions.md @@ -0,0 +1,181 @@ +# 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.