# Lane SD — predictions, written before the shim build Target: `AIComposeShipBlueprint 0x006ad700`, the AI ship-design composer. Lane PAR localised the AI's whole RNG variance to it: on `turn1-state`, six of client 32's seven words are drawn inside it and the seventh is drawn by a helper it calls. It holds nine live draw sites, including the only loop-carried one. Everything below is derived from the **instruction stream only** (`0x006ad700`–`0x006ae61a`, swept to the next function start per rule 17; the body ends with a `ret` at `0x006ae61a` and `int3` padding to `0x006ae620`, so Ghidra's 3864-byte size is right on this one). Nothing here has been run yet. The instrument is `sots-engine` `wip/sd`, `aidesign=on` in `src/shim/shim.cfg.sd*`. --- ## The model One call is `int AIComposeShipBlueprint(ECX agent, EDX parts[3], STACK req*, STACK costOnly)`. `req = {int hullSize; float budget; int role; uint flags}`. It returns 0 on success and 1..8 for eight bail-outs. Its **only** caller `0x006ae620` runs it once per candidate hull size — 1, 2 or 3 sizes depending on `flags & 1` and `flags & 2` — and **breaks on the first success**, so one design request costs one to three composer calls and every failed attempt has already spent whatever it drew before it failed. Draw sites, in execution order, named by the return address the airng tables know them by: | tag | ret | primitive | words | fires when | |---|---|---|---|---| | A | `0x006ad878` | `cl_RandFloat` → `NextFloat` | **1** | tech latch still ok **and** `(flags & 0x40) == 0` | | B | `0x006ad94c` | `cl_RandRange(0, nH-1)` → `NextInt` | `E=(mask+1)/nH` | `parts[0].section == 0` and `nH > 0` (`nH` capped at 0x32) | | C | `0x00691ea0` | `cl_RandRange(0, nC-1)` → `NextInt` | `E=(mask+1)/nC` | command slot empty, section not fixed, `nC > 0` | | D | `0x006adf35` | `cl_Chance(0.5f)` | **1** | `K > 0`, where `K` = mounts with kind ∈ {0, 0xc, 0x17} and size 3 | | E | `0x006adf44` | `cl_RandRange(0, K-1)` | `E=(mask+1)/K` | D accepted | | — | `0x006adf62`, `0x006adf71` | | **dead** | `xor esi,esi / cmp esi,ebx / jle` with `ebx = 0` — confirmed by Ghidra removing both blocks | | F | `0x006adfca` | `cl_Chance(0.8f)` | **1** | unconditional once the call gets this far | | G | `0x006ae418` | `cl_Chance(0.3f)` | **1** | `(flags & 0x20000) == 0` **and** `req.hullSize > 0` | | H | `0x006ae57a` | `cl_Chance(0.2f)` | **L** | **once per qualifying mount, see below** | `RNG_Chance` costs exactly one word at every one of these `p` values (0.2/0.3/0.5/0.8 are all strictly inside `(0,1)`, so neither of its zero-word early-outs is reachable here). Only B, C and E can cost more than one word per call, and only through `NextInt`'s rejection loop. ### The loop-carried draw's trip count The tail is `for part in 0..2 { for mount in 0..part.mountCount }`. The **outer count is a literal 3** and never varies. The inner draw fires for a mount iff: * `mount.kind == 0 && mount.size == 1` (call these the **qualifying** mounts, `N` of them across all three sections), **and** * the section's name is `DEPointDefence` or `CRPointDefence` (case-insensitive) — those take every qualifying mount — **or** the mount's **global** qualifying index `j` satisfies `j mod D' == 0`, **and** * the restricted weapon lookup at `0x006ae3c1` returned non-null (else the site short-circuits with no call), and the default weapon lookup at `0x006ae3dc` returned non-null (else the whole tail block is skipped), and `M > 0`. with ``` f = 1.00 if (flags & 0x20000) == 0 and req.hullSize > 0 and site G accepted = 0.75 if (flags & 0x20000) != 0, or G was drawn and refused = 0.50 if (flags & 0x20000) == 0 and req.hullSize <= 0 M = (int)(N * f) float32 multiply, floor, truncate — non-negative, so one cast D' = max(1, (N + 1) / M) integer division L = #{ j in [0,N) : section(j) is *PointDefence, or j mod D' == 0 } ``` Consequences that make this falsifiable rather than decorative: * `f = 1.00` ⇒ `D' = 1` ⇒ **`L = N`**. * `f = 0.75` ⇒ `L = 0, 1, 2, 4, 3, 6, 7, 8` for `N = 1..8` — note it is **not** monotone: `N = 5` costs 3 and `N = 4` costs 4, because `D'` jumps. * `f = 0.50` ⇒ `L = 0, 1, 1, 2, 2, 3, 4, 4` for `N = 1..8`. * `N = 1` with `f < 1` gives `M = 0` and **zero** draws — a whole loop that is entered and costs nothing (method rule 20's shape, and the reason the probe logs `N` next to `H_obs`). * The 0.3 coin at G therefore changes the word count **only when `N = 1`, `N = 2`, `N = 3` or `N = 5`**. For `N = 4` and `N ≥ 6` both `f = 0.75` and `f = 1.00` give `D' = 1` and the same `L`. ### Exit taxonomy — what a failed attempt has already paid | rc | where | drawn before it returns | |---|---|---| | 5 / 6 / 7 | a forced-tech lookup for flag bit 4 / 8 / 0x10 | **nothing** | | 2 | no hull-section candidate | A | | 3 | no command section | A + B | | 4 | no engine section | A + B + C | | 1 | over budget | A + B + C | | — | `costOnly != 0` returns here | A + B + C | | 8 | no weapon matched a mount | A + B + C + D + E + F | | 0 | success | everything | --- ## Predictions ### P1 — the model holds, per call On **every** composer row the probe emits, `H_pred == H_obs`, where `H_pred` is computed by the probe from `N`, the three per-section point-defence flags and `f` (with `f` taken from the request flags, the hull size, and G's **observed accept/refuse**, which the extended `cl_Chance` detour now records). *Falsified by:* any row printing `model=WRONG`. A row printing `model=GATED-OR-WRONG` (`H_pred > 0` but `H_obs == 0`) is **not** a falsification on its own — it is the weapon-lookup gate — but it is also not a confirmation, and if every row is `GATED-OR-WRONG` the model is untested and I will say so rather than claim it held. ### P2 — one design request, two composer calls, and the first is a price query `0x006cda40` calls the driver twice: once with `costOnly = 1` (statically read at `0x006cda9a`, `push 0x1`) and, if that succeeded, once with `costOnly = 0` (`0x006cdb17`, `push 0x0`). So a turn in which the AI designs one ship should show **two** composer rows: one with `dry=1` that returns `rc=0` having drawn **A, B, C and nothing else**, and one with `dry=0` that goes further. This is the arithmetic of client 32's seven turn-1 words, and it is the prediction I most want checked because it was derived to explain a number rather than measured: ``` call 1 (dry=1): A 1 + B 1 + C 1 = 3 words call 2 (dry=0): A 1 + B 1 + C 1 + F 1 = 4 words total 7 <- PAR's measured 7 ``` with `D`, `E`, `G`, `H` all zero: `K = 0` (no size-3 mounts on a turn-1 hull), and G silent because `req.hullSize <= 0` on the smallest hull. *Falsified by:* one composer row, or three; or `dry = 0` on both; or `F` firing on the dry call. ### P3 — pinned seeds reproduce exactly Two runs of `turn1-state → turn2` with `airng.pin_seed=5A17C0DE` and `aidesign=on`, in two fresh processes, produce **byte-identical `aidesign*` rows** — every field, every row, in order. *Falsified by:* any differing field. That would mean the composer's path depends on something the bracket-entry re-seed does not pin, which would be a bigger finding than the model. ### P4 — a different pinned seed moves the count, and moves it through the path A run with `airng.pin_seed=B16B00B5` differs from P3's runs in at least one of: * site A's coin (`RandFloat() >= 0.5`) landing the other way, which sets request-flag bit `0x40`, forces a tech section and therefore **changes the sections, `N`, and possibly `H`**; * the number of words `B` or `C` spends, `NextInt`'s rejection loop resolving differently. The total for client 32 is **not** required to differ — PAR's §3.4 already showed a pinned run holding the count at 3 while replacing the whole stream. What is required is that if the *path* changes, `H_pred` tracks it: `model=HOLDS` must survive the seed change. *Falsified by:* `model=WRONG` appearing only under the second seed, which would mean the model is fitted to one design rather than derived. ### P5 — the instrument is armed, and says so `shim.airng.txt` must contain `aidesign: composer 0x006ad700 rva=0x002ad700 va=... create=MH_OK enable=MH_OK`. Lane L3 found a configuration that printed `watch=on` and armed nothing; a missing or failed hook here would produce **no `aidesign` rows at all**, which is indistinguishable from "the composer was never called" unless the arming line is read. If that line is absent or not `MH_OK`, every zero in this lane's output is void and the run is discarded, not interpreted. Second arming check, independent of the log line: the `airngcall`/`airngsite` totals for the bracket must equal the sum of the `aidesign` rows' `sub_words`. If the composer hook silently failed the sub-bracket would be zero while the turn total stayed 7. ### P6 — rule 19: the composer detour does not change the game With `aidesign=off` (and `airng=on`, unpinned), two fresh processes on `turn1-state` must both produce the published post-turn autosave `d59bb9f2fd0eb535`. With `aidesign=on`, unpinned, two more fresh processes must produce the same file. The composer detour patches five bytes at a clean prologue boundary (`push ebp; mov ebp,esp; push -1` is exactly 5 bytes, and the function has one caller and no internal branch target below `0x006ad705`), but lane H's finding is that a *correctly placed* patch changed the autosave anyway and the mechanism is still unknown. So this is measured, not argued. *Falsified by:* any of the four runs producing a different file. If the `aidesign=off` pair disagrees **with each other**, the workload is the problem and no control exists (rule 26); I will say so and fall back to pinned comparisons only. --- ## How this could be wrong 1. **`N` is read after the call, from the design the call produced.** The composer counts `N` during its weapon-assignment pass over slots that *received* a weapon; if a slot was skipped, my post-hoc count is high and `H_pred` is too big. Symptom: `H_pred > H_obs` by a small amount on rows with `rc = 8`. This is why the rc is in the row. 2. **`part.mountCount` is `min(hull mounts, 0x32)`.** I clamp to the hull's own vector length as well; if the two disagree the probe prints both and I will notice. 3. **`f` for the `flags & 0x20000` case is an inference from one `fld` at `0x006ae3f8`.** If bit `0x20000` is never set on this board the term is untested, and I will label it rule-6 rather than claim it. 4. **The dead-code claim on `0x006adf62`/`0x006adf71`** is static. Ghidra independently removed both blocks as unreachable, which is a second reading of the same instructions and not a second instrument. If either ever appears in an `airngcall` row, the claim is wrong. 5. **The measurement could double-count.** PAR's instrument reported one word twice because `Chance` calls `NextFloat` and both were detoured; it caught that only because it took two independent measurements. This lane's sub-bracket takes two as well — `left` read off the generator object (`words`) and the observer sum (`sub_words`) — and prints both on every row. They must agree.