Compare commits

...

4 commits

Author SHA1 Message Date
alex
f18afadf57 board: lane RB - ModCount reaches 24 with zero residual; matched triple; rates memory-vs-wire order defect 2026-09-08 19:09:00 -04:00
alex
ce8b3e66b4 RB: lane CB's real capture replays to the same result, and brings the seeds and the heap payloads
CB's VM146 capture goes through the JSON->tcb adapter and the replayer unchanged and reproduces
this lane's canonical result from a separate run on a different guest: 108 -> 62, closed 46,
regressed 0, ModCount 24. It carries the first AI seeds any capture has held (32/496/512), and
its deep dump closes two gap rows: list 8's route is [272] and list 10's vector is [1728], a
ship the input save already contains.
2026-09-08 19:07:13 -04:00
alex
7a41246450 RB: bind a .tcb capture to the save it was taken on
Replaying a turn's commands against a different board charges the counter happily and produces
a confidently wrong number, because the blocks name player ids that exist in both. The converter
now records the input save and sots_turn warns when they disagree.
2026-09-08 19:05:33 -04:00
alex
a4a1d373f1 RB: replay a recorded turn's commands -- ModCount is reachable, and the rates frame's memory order is not its wire order
sots_turn --turn-commands puts /Sim/ModCount on the original's 24 with zero residual, closing
the one leaf that has been unreachable from a save all campaign. Canonical pair 108 -> 62,
closed 46, regressed 0, fresh build directory.

The .tcb capture format (line-oriented, parser-free, '?' for a field the instrument could not
read, per-client AI seeds), a converter from lane L4's shim dump, and an adapter from lane CB's
JSON capture -- CB's stays the capture of record, .tcb stays the engine's input, and the two
paths produce byte-identical replays.

A falsified prediction paid for itself: the first run regressed two leaves because list 5's
element is decoded in MEMORY order, and the memory order of the rates frame is NOT its wire
order. Memory member 1 is wire member SRsc; six members unread. Lane CB's decoder has the same
defect and should drop its list-5 record.

Two new addresses (the second and third gate-loop heads) in ghidra/addresses.d/lane-rb.json.
2026-09-08 19:01:35 -04:00
9 changed files with 2273 additions and 0 deletions

View file

@ -349,3 +349,9 @@ Status flow: `backlog → in-progress → mapped → verified` (or `blocked`).
| THE ONE PROBE: pinned seeds THROUGH the resolver | verify | backlog | — | 0% | 2026-09-08 | The seed SOURCE is not the probe. The resolver probe is only an experiment **if the seed is pinned** - otherwise two runs with two unknown seeds cannot separate "draws from the stream" from "reads something else per-process". Implementation named: L1's `l1seed` Seed detour (return address 0x0078304b, three constants) + L4's instrument + a `left` region over client+0x134 at TryResearchCandidate 0x006c8580; ref-turn2 once as the pin's OWN control; turn1-state twice; then sweep by quit-to-menu/reload (the pin table advances per load, **no relaunch needed**), logging which producer returned non-null for player 512. Predictions committed in the resolution 4. If the four leaves still differ, extend the pin to the unidentified fourth seeded object | | THE ONE PROBE: pinned seeds THROUGH the resolver | verify | backlog | — | 0% | 2026-09-08 | The seed SOURCE is not the probe. The resolver probe is only an experiment **if the seed is pinned** - otherwise two runs with two unknown seeds cannot separate "draws from the stream" from "reads something else per-process". Implementation named: L1's `l1seed` Seed detour (return address 0x0078304b, three constants) + L4's instrument + a `left` region over client+0x134 at TryResearchCandidate 0x006c8580; ref-turn2 once as the pin's OWN control; turn1-state twice; then sweep by quit-to-menu/reload (the pin table advances per load, **no relaunch needed**), logging which producer returned non-null for player 512. Predictions committed in the resolution 4. If the four leaves still differ, extend the pin to the unidentified fourth seeded object |
| NEW OPEN RISK: g_GlobalRNG has four other consumers | verify | backlog | — | 0% | 2026-09-08 | Including **RunCombatRound+0x60f** - and **no battle turn has ever been compared across two processes**. Add "twice, two processes" to the battle workload in backlog 5. Lane L2 verified combat lockstep between two clients in ONE game; this is the different question of whether a battle is reproducible across separate runs | | NEW OPEN RISK: g_GlobalRNG has four other consumers | verify | backlog | — | 0% | 2026-09-08 | Including **RunCombatRound+0x60f** - and **no battle turn has ever been compared across two processes**. Add "twice, two processes" to the battle workload in backlog 5. Lane L2 verified combat lockstep between two clients in ONE game; this is the different question of whether a battle is reproducible across separate runs |
| CORRECTION: neither hooks=off run was a control | verify | verified | high | 100% | 2026-09-08 | Rule 26. Three lanes ran hooks=off on turn1-state and got **three different files**, so no single hooks=off run on that workload ever exonerated anything. And lane L4's "run R2 is byte-identical to the hooks=off control" is a **~1/k COINCIDENCE, not a control** - with an outcome set of size k, agreement by chance is expected. Rule 19 said take the measurement with the instrument removed; it did not say what to do when THE CONTROL ITSELF VARIES | | CORRECTION: neither hooks=off run was a control | verify | verified | high | 100% | 2026-09-08 | Rule 26. Three lanes ran hooks=off on turn1-state and got **three different files**, so no single hooks=off run on that workload ever exonerated anything. And lane L4's "run R2 is byte-identical to the hooks=off control" is a **~1/k COINCIDENCE, not a control** - with an outcome set of size k, agreement by chance is expected. Rule 19 said take the measurement with the instrument removed; it did not say what to do when THE CONTROL ITSELF VARIES |
| ModCount IS NOW REACHABLE - 24 exactly, zero residual | engine | verified | high | 100% | 2026-09-08 | Lane RB. **The leaf the campaign has called "not derivable from a save" since lane A2.** Our turn writes 14 where the original writes 24; **the missing ten ARE the command stream**, and `sots_turn --turn-commands` puts it on **24 exactly** - 2 drivers + 4 research-rate gates (one per SUBMITTING block, including the human who ordered nothing) + list 3 + list 5 + list 10 + 2x list 14 + list 8, with the list-23 population element FREE. **The 1-16/17-27 cost boundary held on the first workload that ever populated the free half.** What it does NOT reproduce: everything else - seven of the ten commands need subsystems we lack, and the three we can apply are all no-ops here (the AI re-issues rates and sliders the save already holds) |
| THE MATCHED TRIPLE - and it correctly FAILS to close one leaf | verify | verified | high | 100% | 2026-09-08 | The campaign's first (input, stream, that run's output) set. Canonical pair 108 -> 63 -> **62** with the stream (closed 46, regressed 0); secondary 209 -> 124 -> 118; **matched triple 209 -> 124 -> 117 (closed 92)**. Against the HISTORICAL oracle it closes six and **correctly leaves player 512's ResTNm diverging** - the recording picked XNC_TrnsMorr2, turn2-state holds BIO_GnMod. **That was predicted before the run: a replay that CLOSED it would have meant something was copying the oracle.** This is the C-exact / C-set distinction demonstrated on real data - the stream reproduces its own run, not a different run's tiebreak |
| a falsified prediction found a defect in TWO lanes | verify | verified | high | 100% | 2026-09-08 | RB's P3 predicted regressed 0; **the first run regressed 2**. Cause: the rates frame is dumped in MEMORY order and **its memory field order is not its wire order** - the AI's one non-zero slider went into SRt where the oracle holds SRsc = 1.0. **Memory member 1 is wire member SRsc**; six members unread. **Lane CB's JSON decoder has the identical defect** - RB's adapter masks it engine-side but the JSON is the capture of record, so the list-5 record should be dropped or emitted in wire order with the mapping stated. Routed to CB mid-flight |
| apply order verified three ways - and honestly unfalsifiable here | control-flow | verified | high | 90% | 2026-09-08 | Thirty steps: lists `6 11 20 19 17 18 5 23 24`, gate loop A, `1 4 3 21 2 22 9 10 12 13 14 15 16 7 8 25 27 26`, gate loops B and C. Verified three ways: the six inlined bump sites plus the three gate-loop heads are **strictly increasing in address in exactly that step order** (9 of 30 positions independently re-derived, the other 21 inherited from AI4); bijection of 1..27; and **neither the list nor the offset sequence is ascending, so a sorted implementation cannot pass**. **STATED PLAINLY BY THE LANE: apply order is unfalsifiable on this corpus** - every non-empty list is one player's and every command names one system |
| CB's capture replays unchanged; first AI seeds landed | verify | verified | high | 100% | 2026-09-08 | RB did not duplicate CB's tool - `tools/tcb_from_json.py` joins them, CB's JSON staying the capture of record and `.tcb` the engine's parser-free input. **CB's real VM146 capture replays through it unchanged to the identical canonical result**, and brings the first **AI seeds** (32 -> 0x9dfad635, 496 -> 0xb482b2d8, 512 -> 0xfb81e993 - now Rung C's primary input) plus heap payloads closing two gap rows: list 8's route is [272], list 10's vector is [1728], a ship the input save already contains |
| Rung B completeness gap (the real backlog) | verify | backlog | — | 0% | 2026-09-08 | From RB, full table in its finding: ship construction; **the client's ID ALLOCATOR** - design 18 and fleet 34 are allocated CLIENT-SIDE before submission, and it is a watchpoint not a week of reading; the rates frame's memory order; list 10's and list 14's EFFECTS; the Population body; **techId -> name** (the wire carries an id, the save a name; today transcribed from the capture and reported in its own column, with 144/90/288 -> IND_Waldo/DRV_PlsFiss/XNC_TrnsMorr2); the civilian-ratios gate has **NO APPLIER ANYWHERE** so its cost is unknown NOT zero; and whether the **load-time batch** charges the counter is untested |

View file

@ -0,0 +1,305 @@
# Replaying a recorded turn: what a command stream reproduces, and what it does not
Lane RB, 2026-09-08. Host work, no VM. Engine worktree `wip/rb` off `main` `4f25f1e`; predictions
committed as `sots-engine` `docs/RB-predictions.md` at `1e6474b`, **before** the module existed
(rule 2). Every number below was taken on a **fresh `build-host`** created with `rm -rf` (rule 24).
Consumes: `ai-order-emission.md` (AI4, the apply order and the cost table), `ai-order-capture.md`
(L4, the live dumps), `turncommands-block.md` (Q, the wire shape),
`turn1-to-turn2-nondeterminism.md` (L5). Produces the first end-to-end **turn record** the
campaign holds.
---
## 0. Lead: `ModCount` is reachable, and it is the only leaf a stream closes on the reference turn
`/Sim/ModCount` has been unreachable from a save for the whole campaign, because it counts a
thing a save does not contain. Our standalone wrote **14** where the original writes **24**: the
two driver bumps and nothing else. Replaying the turn's recorded command blocks puts it on **24**,
exactly, with no fitting and no fudge term.
Canonical pair `turn2-state.sav -> turn3-state.sav`, fresh build:
| | leaves diverging | closed | regressed |
|---|---:|---:|---:|
| do nothing | 108 | — | — |
| standalone, no stream | 63 | 45 | 0 |
| **standalone + recorded stream** | **62** | **46** | **0** |
The one leaf the stream closes is `/Sim/ModCount`, and its twenty-four decompose with **zero
residual**:
```
2 turn drivers (S00, T00) -- ours already
4 research-rate gates, one per submitting block (16, 32, 496, 512)
1 list 5 system rates
1 list 3 build order
1 list 10 (unnamed)
2 list 14 fleet task, modes 0 and 1 against ONE fleet
1 list 8 fleet move
0 list 23 population -- the 17..27 half is free
--
12 delta, on a save that carried 12
```
**Nothing else closes, and that is the honest result rather than a disappointment.** Of the ten
commands, three have a handler this engine holds and all three are no-ops on this workload — the
AI re-issues the research rate and the planetary-budget sliders the save already carries. The
other seven need subsystems we do not have (§4).
The secondary pair is where the stream does work, and it produces the campaign's first **matched
triple** — an input save, the stream captured from the run that consumed it, and *that run's own
autosave*:
| oracle | closed by the stream | still diverging |
|---|---:|---|
| the recording's own autosave (`l4-turn1to2-instrumented-autosave.sav`) | **7** | — |
| the historical `turn2-state.sav`, from a different process | **6** | player 512's `ResTNm` |
Both at **regressed 0**. The seven are `ModCount` (0 → 12) plus `ResRate` and `ResTNm` on players
32, 496 and 512. Against the historical oracle only six close, and the one that does not is
**exactly** the leaf lanes L4 and L5 showed is decided per-process: the recording picked
`XNC_TrnsMorr2`, `turn2-state.sav` holds `BIO_GnMod`. That was written down as a prediction before
the run and it is the strongest thing in this lane — a replay that had closed it would have meant
something was copying the oracle instead of the capture.
---
## 1. The on-disk form: `.tcb`
Line-oriented, whitespace-separated, `#` comments, magic first. A shim can emit it with `fprintf`
and a lane can read it without a tool.
```
tcb 1
meta source l4-turn2to3-aiorders.txt
seed <netId> <32-bit word> -- one per AI client; absent means "not recorded"
block <idx> <playerId> -- the batch SLOT, ascending; playerId 0 = a slot no client wrote
gate <idx> rate|target|boost|group4|group5|civilian <payload...>
list <idx> <listNo> <count> -- required for every non-empty list
elem <idx> <listNo> <elemIdx> <field>... -- fields in WIRE order
```
A field is one token: `iN` int, `fN` float, `b0`/`b1` bool, `s:TEXT` string, `vN` a counted vector
whose length is known and whose values are not, `vN:a,b,c` one that is fully read, and `?` a scalar
the instrument could not reach.
Three parts of that are not decoration.
**`?` and bare `vN` are the point.** A dump reads a fixed window of each element and cannot follow
a pointer, so a route arrives as "one element, contents unknown". That is a different fact from
"no element" and from "an element of zeros": the command still costs its bump and its effect still
cannot be applied. Recording the ignorance is what lets the counter be right while the state is
honestly left alone. The reference turn has **four** such elements out of ten.
**A declared count that disagrees with the elements present is an ERROR, not a warning.** A capture
that lost an element would otherwise produce a counter quietly one short, and nothing downstream
could tell that from a turn that really issued one fewer command. Thirteen malformed captures are
in the engine's test as rejection cases.
**`seed` is carried even though nothing consumes it.** Lane L1 established that each AI client
seeds one MT19937 with a single word at construction, so a decision is a function of *(board,
seed)* and a capture that records the seeds can be **re-derived** rather than replayed. The field
exists now so a capture taken today is still the right file when `game/ai` lands. A reference save
whose seeds were never logged — `turn2-state.sav` — is not reproducible by any process, the
original included, and its commands can only ever be replayed.
### 1.1 Two capture tools, one reader — and a defect one of them shares with my first attempt
Lane CB was building `tools/turncommands_capture.py` in parallel, emitting **JSON**. That is the
better *capture of record* and it should stay: raw element words as ground truth, heap vectors and
strings the deep dump followed, the AI seeds, the container self-check, and — the part neither of
my files had — the input save's hash **bound to the output autosaves' hashes**, so a capture cannot
be silently used against the wrong save.
Rather than a second format in the engine, `tools/tcb_from_json.py` joins them: CB's JSON stays the
capture of record, `.tcb` stays the engine's parser-free input, and one narrow script knows both.
Fed CB's tool's own output over the L4 log, the adapter produces a replay **byte-identical** to the
one from `aiorders_to_tcb.py`. Honest bound on that agreement: both decoders read the *same log*,
so it checks the two decoders against each other and not the log.
**And it caught a defect in CB's decoder, which is the same one I shipped and measured (§3):** its
list-5 record maps the element's memory words straight onto the frame's wire order
(`{systemId, ship, terraform, sciences, …}`). That is wrong by at least one position. The adapter
overrides it to `?` and says why in its own docstring; **CB should drop the list-5 record from
`RECORDS` rather than rely on the adapter to mask it**, because the JSON is the capture of record
and a wrong typing in it will outlive this note. CB's `raw_words` are unaffected and remain right.
### 1.2 CB's real capture, run
CB's own VM146 capture landed while this lane was closing and it goes straight through the adapter
and the replayer unchanged: `verify/results/turncommands/cb-turn2to3.tcb`. It reproduces this
lane's canonical result exactly — **108 → 62, closed 46, regressed 0**, `ModCount` 24 — from a
completely separate run of the game on a different guest. It also brings two things L4's dump
could not:
* **The AI seeds, observed live:** netId **32 → `0x9dfad635`**, **496 → `0xb482b2d8`**,
**512 → `0xfb81e993`**, all `pinned=false` (observed, not forced). These are the first seeds any
capture has carried, and they are what makes this a turn record a future `game/ai` can be
*re-derived* against rather than replayed from.
* **The heap payloads**, which close two rows of the gap list below outright:
* **list 8's route is `[272]`** — one hop to system 272. Lane L4 §5.3 recorded this as
"`turn3-state.sav`'s waypoint says 272 but the capture does not prove it". It is proved now.
* **list 10's counted vector is `[1728]`** — a **ship** id, and one that exists in the input
save. `{systemId 288, fleetId 34, ships [1728]}` is now a typed reading with values, and
"assign these ships to this fleet at this system" fits all three. Still not a proof of the
command's *name* — the effect has never been observed — but the payload's type no longer
leaves it open.
Those two elements move from `incomplete` to `declined` in the replay report: the capture is now
complete and the *subsystems* are what is missing.
CB's capture also does the one thing neither of my files did and every future capture must:
it records the input save's hash **bound to the output autosaves' hashes**, both of which match
the published determinism oracle, with a `hooks=off` control run named. That binding is what makes
a capture usable as a byte-match oracle at all.
**The converter.** `tools/aiorders_to_tcb.py` turns lane L4's shim dump into a `.tcb` mechanically:
it applies the per-list field mapping, undoes the one list whose writer runs backwards, reinterprets
the words the record says are floats, turns a vector's begin/end pair into a length, and writes `?`
where the window could not reach. Both existing captures are converted and checked in at
`verify/results/turncommands/l4-turn{1to2,2to3}.tcb`. **Lane CB does not need to write an emitter:**
the existing `aiorders=on` hook plus this converter already produces the file. If CB does emit
`.tcb` directly, the seed and `name` records are the two things the current dump has no field for.
---
## 2. Apply order, and how it was verified
The batch is a flat run of thirty steps: twenty-seven per-**list** steps, each looping over *every*
player's block before the next step begins, with three per-**player** gate loops spliced in.
```
lists 6 11 20 19 17 18 5 23 24
gate loop A { group5 (free), research target (bump), research rate (bump) } 0x0088fdb0
lists 1 4 3 21 2 22 9 10 12 13 14 15 16 7 8 25 27 26
gate loop B { research boost (bump) } 0x008907b1
gate loop C { group 4 (bump) } 0x0089080a
```
So **one player's list-6 commands are applied before another player's list-11 commands**, the list
sequence starts at 6 and is not 1..27, and the member offsets it walks are not ascending either.
**How it was verified, and what the verification cannot show.**
1. **Address monotonicity, nine of thirty positions.** Six appliers are inlined into the batch and
each writes `ModCount` in place, so a watchpoint run recovered their addresses; the three gate
loop heads are known too. In schedule order those nine are `0x0088fdb0`, `0x0088fe0a`,
`0x008902fe`, `0x008903b9`, `0x0089046c`, `0x008905c8`, `0x008907b1`, `0x008907bc`,
`0x0089080a` — **strictly increasing**, at steps 9, 9, 18, 19, 20, 23, 28, 28, 29. That chain is
an independent re-derivation. The other twenty-one lists' handlers are out of line and this lane
has no record of their call-site addresses inside the batch, so their relative order is
**inherited** from AI4's read of the `add edi, imm` chain, not re-derived. The test says nine.
2. **Bijection and non-sortedness**, asserted: every list exactly once, every located gate exactly
once, and neither the list sequence nor the offset sequence ascending. That is what rules out the
two obvious wrong implementations — `for (list = 1..27)` and walking the block in memory order.
3. **The civilian-ratios gate is absent from the schedule on purpose.** It has no located applier
anywhere in the routine, so including it would be claiming a cost of zero for something whose
cost is unknown. It is caught separately and makes the whole count report itself as a lower
bound.
**Stated plainly: apply order is unfalsifiable on every workload the campaign holds.** 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 for the workload that will need it, and
tested against the instruction stream rather than against an outcome. The workload that would make
it falsifiable is **two players commanding the same object in one turn** — the cheapest is a
two-human `/concurrent` game (lane G2's Tier 0), where both clients order fleets at one system.
---
## 3. A falsified prediction, and the finding it paid for
**RB-P3 predicted `regressed 0`. The first run regressed two leaves**, and the cause is a real fact
about the original.
The converter mapped list 5's dumped words straight onto the wire order of the rates frame
(`SRs, SRt, SRsc, SRtf, SRi, SRoh, SRnr`). The replay then wrote the AI's single non-zero slider
into `SRt`, and `Sys[288 "Ke'Dolarra"]` came out with `SRt 1.0 / SRsc 0.0` against an oracle
holding `SRt 0.0 / SRsc 1.0`.
**The memory field order of `Game::StarSystem::OutputRates` is not its wire order.** What is known
precisely:
* the only non-zero word in every dumped element of list 5, on both turns, is at **memory index 2**
of the element (index 0 is the system id, so it is the frame's **second** member);
* the same command **on the wire**, in a save that carries issued orders, puts its only non-zero in
**`SRsc`**, the frame's **third** member (lane Q, `SAVE_FORMAT.md` §11, cross-checked to the
Planetary Budget slider pushed fully to Construction);
* `turn2-state.sav` and `turn3-state.sav` both hold `Sys[288] Rts = {SRs 0, SRt 0, SRsc 1.0, SRtf 0,
SRi 0, SRoh 0, SRnr 0}` — so the AI's command re-issues the state the save already holds, which is
why a *correct* applier is invisible and an incorrect one is immediately visible.
So **memory member 1 is wire member `SRsc`**: one correspondence pinned, six unread. One non-zero
slider cannot determine a permutation of seven, and the converter no longer pretends otherwise — it
emits the system id and seven `?`, and the replayer counts the command and declines it.
Two experiments settle it, both cheap:
* **one UI run** — push two *different* sliders to two *different* values on one system, End Turn
with `aiorders=on`, and read the permutation straight off the element;
* **cheaper, and no VM at all** — a save taken after issuing rates carries the same command on the
**wire**, where every field is NAMED. `zuul-turn17-orders2.sav` has one. A `.tcb` converted from a
save's own `TurnCommands_v5` block needs no memory mapping, and would also give the `.tcb` format
a second, independent producer. **This is the highest-value next step on the capture side** and it
is pure host work.
Two notes on how this was caught, because they generalise. The plain closed/regressed measurement
found it, before the control did — but the control (RB-P4, `--replay-count-only` vs a full replay
must be byte-identical when every modelled handler is a no-op) would have found it too, and it is
what now stands guard: on the canonical pair those two runs are byte-identical, which is the
evidence that the handlers we *do* run write where they claim to rather than agreeing with the
oracle by luck. And the reason a wrong write was visible at all is that the command re-issues
existing state: **a command that re-states the board is the best possible test of an applier**, and
the corpus is full of them.
---
## 4. What a *complete* replay needs that this lane does not have
This is the real Rung B backlog. Each row is the reason one command in the reference turn is counted
but not applied.
| # | needed for | what is missing | shape |
|---|---|---|---|
| 1 | **list 3 build, list 1 design** | **ship construction.** No phase in this engine builds a ship. The command carries a queue ordinal, a design id and a system; the effect is `srb`/`sri`, `Maint`, the savings debit, a hull id and `ShipIDs`. Nine leaves on the canonical pair. | engine work; lane B6/E2 have the map |
| 2 | **list 1, list 3, list 8, list 10, list 14** | **the client's id allocator.** The build order names design **18** and the fleet order names fleet **34** — objects that do not exist in the input save, allocated *client-side before submission*, while the server's own master counter (`NMnx`) issued 1712 and 1776 the same turn. **Two id spaces**, and the small one is part of the wire protocol. A reimplementation that allocates on apply produces a structurally correct save with every AI-created id wrong. | **a watchpoint**, not a week of reading: break on the write that produces 18 and 34 |
| 3 | ~~**list 8 fleet move** — the route's hops~~ | **CLOSED by lane CB's deep capture (§1.2): the route is `[272]`, one hop.** What remains is row 2 — the route belongs to a fleet the input save does not contain. | done |
| 4 | **list 5 system rates** | **the memory field order of the rates frame** (§3). One correspondence pinned, six unread. | one UI run, or one save-sourced capture |
| 5 | **list 10** | **a name and a meaning.** The payload is fully read now (§1.2): `{systemId 288, fleetId 34, ships [1728]}`, and 1728 is a ship the *input* save already contains. "Assign these ships to this fleet at this system" fits every word. The **effect** has still never been observed, so it is still not named. | a hook on `0x0088bed0` |
| 6 | **list 14 fleet task** | **what the two modes do.** Two elements per AI fleet order, modes 0 then 1; the interface deposits one, mode 0 only. The *cost* is settled; the *effect* is not read at all. | a hook on the inlined applier |
| 7 | **list 23 population** | **the `Population` body**, 24 bytes behind a vftable that the capture window does not follow. Free in `ModCount`, so it has never been forced. | one dumper change |
| 8 | **the research-target gate** | **the techId → tech-name map.** The wire carries an integer (144, 90, 288); the save carries a name. The client resolves it *off the command* and passes a `char*`. The ids are not `index * 16` and are not indices into anything we hold. Today the capture can carry the observed name and the replay TRANSCRIBES it — reported in its own column, because that is not a reimplementation. **Three data points now exist**: 144 → `IND_Waldo`, 90 → `DRV_PlsFiss`, 288 → `XNC_TrnsMorr2`. | a watchpoint at the gate payload, or a read of `0x006c8580` |
| 9 | **the research-boost gate** | savings spent to advance research; both halves unmodelled. Never observed set on an AI turn. | engine work |
| 10 | **the group-4 and group-5 gates** | no read semantics; group 5 is Hiver-only and no save carries it. | rule 6 — a manufactured Hiver workload |
| 11 | **the civilian-ratios gate** | **no applier located anywhere in the batch.** Its `ModCount` cost is *unknown*, not zero, and any turn that sets it reports a lower bound. | an image-wide search for the consumer |
| 12 | **the load-time batch** | the process applies a batch at LOAD as well as at End Turn (`seq=1`, `n=1`, the local client's block alone, with the rate gate SET). Whether it charges `ModCount` is **untested**, and this lane excludes it by construction. If it does charge, a save loaded and immediately re-saved reads one higher. | one run: load, save, compare |
| 13 | **the whole thing, on any interesting board** | **two turns, one AI empire, 28 stars, no contact.** Lists 2, 4, 6, 9, 11–13, 15–22 and 24–27 have never been non-empty. Apply order is unfalsifiable (§2). Nothing here generalises past a very quiet game. | manufacture the workload |
Two things NOT on that list, deliberately. `Summary.Checksum` moves whenever anything else does and
its inputs are unread — no command replay will touch it until the rest is right. And nothing in
either capture draws a generator word, so the RNG frame is untouched by the stream; the coordinator's
note that the replay interval begins at `BeginProcessTurn` rather than `ProcessTurn` is already
satisfied here — the batch is drained *before* the first phase runs, which is where the End-Turn
dispatcher calls it (`ApplyAllTurnCommands` at `+0x00784904`, `ProcessTurn` at `+0x0078491c`,
`OnAllCombatDone_Tail` at `+0x00784d07`) — but the **hive-registration draws lane L1 found inside
`BeginProcessTurn` are a separate interval and this lane does not model them.**
---
## 5. What this lane did not do
1. **No VM time and no new instrument.** Every live number here is lane L4's, re-read through a
converter. Nothing new ran under a hook.
2. **The CT111 shim cross-build was not run** — CT111 refused the key from this host. `src/game/ai`
is in the cross build, so per rule 13 **the integrator must run it before pushing**. The two
32-bit-specific hazards were audited by hand and one was real and is fixed: `strtoul` plus a
`> 0xffffffffUL` test is tautological where `unsigned long` is 32 bits, which would both accept
out-of-range input silently and trip `-Wextra`/`-Werror`; both sites now use `strtoull`. `%zu`
appears in the new code and also in `src/app/main.cpp` today, so it is not a new exposure.
3. **The apply order was not tested against an outcome** and cannot be on this corpus (§2).
4. **`ToTurnCommandBlock` reuses the existing, already-verified cost model** rather than counting a
second time — deliberately, so there is one implementation of the arithmetic and not two that
can agree with each other while both being wrong (rule 8).
5. **The `.tcb` format has exactly one producer.** Until the save-sourced converter of §3 exists,
every capture comes through one script and a bug in it is invisible.
6. **Seeds are carried and never used.** No capture in the corpus has any.

View file

@ -0,0 +1,18 @@
{
"entries": [
{
"name": "StrategySim_ApplyTurnCommandBatch_GateLoopB",
"addr": "0x008907b1",
"convention": "label",
"status": "verified",
"prototype": "the SECOND of the three per-player gate loops inside StrategySim::ApplyTurnCommandBatch. `esi = block+0x20`; it tests the research-boost gate at +0x20 and applies its {spend, fraction} payload through 0x00820560, bumping ModCount inline at 0x008907bc. It runs AFTER all twenty-seven list loops, not with the other gates -- the six prologue gates are split across three loops at three points in the routine, which is why a port that applies them together as a prologue gets the order wrong"
},
{
"name": "StrategySim_ApplyTurnCommandBatch_GateLoopC",
"addr": "0x0089080a",
"convention": "label",
"status": "verified",
"prototype": "the THIRD and last per-player gate loop inside StrategySim::ApplyTurnCommandBatch. `esi = block+0x24`; it tests the group-4 gate at +0x2c and applies its {bool, int} payload through 0x00821b90. It is the final step of the whole batch. Together with the loop-A head at 0x0088fdb0 and the loop-B head at 0x008907b1, and the six inlined ModCount bump sites, this gives nine positions of the thirty-step apply schedule an address-monotonicity check -- the only part of the sequence that can be re-derived rather than inherited from the read of the `add edi, imm` chain"
}
]
}

255
tools/aiorders_to_tcb.py Normal file
View file

@ -0,0 +1,255 @@
#!/usr/bin/env python3
"""Turn a live `aiorders` shim dump into a `.tcb` turn-command capture.
The shim's dump is a memory view: for each submitted block it prints the six prologue gates,
the twenty-seven list lengths, and a fixed 48-byte window of every element. That is the right
thing for an instrument to emit -- it commits to nothing -- but it is not a turn record, because
the fields are in MEMORY order (and one list's writer runs them backwards), the floats are still
integers, and a payload behind a pointer is simply absent.
This is the mechanical step in between. It applies the per-list field mapping, reinterprets the
words the element record says are floats, converts a vector's begin/end pair into a length, and
writes `?` wherever the dump window could not reach. The output is what `sots_turn
--turn-commands` reads.
tools/aiorders_to_tcb.py LOG [-o OUT] [--batch SEQ] [--name PID=TECHNAME]... [--seed ID=HEX]...
Two things this tool deliberately will not do.
* It will not invent a payload. A route whose hops live behind a pointer becomes `vN` -- a
vector of known length and unknown contents -- and never `v1:0`. The replayer counts such a
command and refuses to apply it, which is the whole point: a command that was issued and a
command whose effect we can reproduce are different facts.
* It will not pick a batch for you when the choice is ambiguous. A process applies a command
batch at LOAD as well as at End Turn; replaying the load-time one against a save that was
written after it would double-count. The default is the LAST batch in the log, which is the
End-Turn one, and `--batch` overrides it.
`--name PID=TECHNAME` records the tech NAME an instrument observed a research-target gate resolve
to. The wire carries an integer id and the save carries a name; the client resolves one to the
other off the command and that map is unread, so the name can only be recorded, never computed.
`--seed NETID=VALUE` records an AI client's construction seed.
"""
import argparse
import re
import struct
import sys
# Per list: how the 48-byte memory window maps onto the element's wire fields.
#
# `order` is the sequence of word indices to emit; `kind` says how to read each one. The build
# list is the only one that reverses -- its writer emits +0x14, +0x10, +0x0c, +0x08, descending --
# and that is per-list, not a rule. Every other observed list writes ascending.
#
# i an int word
# f a word to reinterpret as a float
# b a word whose low byte is a bool
# v a std::vector: this word and the next hold begin/end, and the length is their difference
# over four. The CONTENTS are behind the pointer and the dump does not follow it, so the
# field is emitted with a length and no values.
# ? a word the element record says is part of an object this window cannot read
LIST_MAP = {
1: [("?", 0)], # a ShipDesignDef object
3: [("i", 3), ("i", 2), ("i", 1), ("i", 0)], # DESCENDING: ordinal, design, system, w
# List 5 carries a system id and then the seven planetary-budget sliders. The id is certain;
# THE SLIDERS ARE NOT, and this used to map them straight onto the wire order and was wrong.
# A replay built on that mapping wrote the AI's one non-zero slider into the wrong member and
# regressed two leaves on the canonical pair -- the command re-issues the rates the save
# already holds, so a correct applier is a no-op and an incorrect one is immediately visible.
#
# What is known: the only non-zero word in every dumped element of this list is at index 2,
# and the same command on the WIRE (where the frame is named) puts its only non-zero in
# `SRsc`, the THIRD member. So memory member 1 is wire member `SRsc` and the frame's memory
# order is not its wire order -- one correspondence pinned, six unread. The `?` below is
# that ignorance, and the replayer counts such a command and refuses to apply it.
#
# The experiment that settles it is one UI run: push two DIFFERENT sliders to two different
# values, capture the block, and read the permutation straight off. Cheaper still, a save
# taken after issuing rates carries the same command on the wire with every field NAMED, so a
# capture converted from a save's own TurnCommands block needs no memory mapping at all.
5: [("i", 0)] + [("?", k) for k in range(1, 8)],
7: [("i", 0), ("i", 1)],
8: [("i", 0), ("v", 1)],
10: [("i", 0), ("i", 1), ("v", 2)],
14: [("i", 0), ("i", 1), ("b", 2)],
23: [("i", 0), ("?", 1)], # a Population object
}
BLK_RE = re.compile(
r"^aiblk seq=(\d+) blk=(\d+)/(\d+) at=\S+ pid=(-?\d+) "
r"rate=(\d):(\S+) target=(\d):(-?\d+) boost=(\d):(-?\d+),(\S+) g4=(\d):(-?\d+),(-?\d+) "
r"f3=(\d):(\S+),(\S+),(\S+) civ=(\d)")
LISTS_RE = re.compile(r"^ailists seq=(\d+) blk=(\d+) pid=(-?\d+) nonEmpty=\d+ sizes\(1\.\.27\)=\[([^\]]*)\]")
ELEM_RE = re.compile(r"^aielem blk=(\d+) pid=(-?\d+) list=(\d+) idx=(\d+) .*? hex=\[([^\]]*)\]")
def f32(word):
"""A dumped word, as the float32 it is, printed so it round-trips exactly."""
return "%.9g" % struct.unpack("<f", struct.pack("<I", word & 0xffffffff))[0]
def parse_log(path):
"""-> {seq: {"n": int, "blocks": {idx: block}}}"""
batches = {}
def batch(seq):
return batches.setdefault(seq, {"n": 0, "blocks": {}})
with open(path, encoding="utf-8", errors="replace") as f:
for line in f:
m = BLK_RE.match(line)
if m:
seq, idx, n = int(m.group(1)), int(m.group(2)), int(m.group(3))
b = batch(seq)
b["n"] = max(b["n"], n)
b["blocks"][idx] = {
"pid": int(m.group(4)),
"rate": (m.group(5) == "1", m.group(6)),
"target": (m.group(7) == "1", int(m.group(8))),
"boost": (m.group(9) == "1", int(m.group(10)), m.group(11)),
"g4": (m.group(12) == "1", int(m.group(13)), int(m.group(14))),
"f3": (m.group(15) == "1", m.group(16), m.group(17), m.group(18)),
"civ": m.group(19) == "1",
"sizes": [0] * 27,
"elems": {},
}
continue
m = LISTS_RE.match(line)
if m:
seq, idx = int(m.group(1)), int(m.group(2))
blk = batch(seq)["blocks"].get(idx)
if blk is not None:
blk["sizes"] = [int(x) for x in m.group(4).split()]
continue
m = ELEM_RE.match(line)
if m:
# An `aielem` line carries no seq, so it belongs to the batch whose block header
# it followed -- which is the most recent one seen.
seq = max(batches) if batches else 0
idx, listno, elemidx = int(m.group(1)), int(m.group(3)), int(m.group(4))
words = [int(x, 16) for x in m.group(5).split()]
blk = batch(seq)["blocks"].get(idx)
if blk is not None:
blk["elems"].setdefault(listno, {})[elemidx] = words
return batches
def element_fields(listno, words):
"""The wire fields of one element, as `.tcb` tokens."""
spec = LIST_MAP.get(listno)
if spec is None:
# An unmapped list: record that the element exists and nothing about it. The command
# still costs whatever its list costs; the replayer will decline to apply it.
return ["?"]
out = []
for kind, k in spec:
if k >= len(words):
out.append("?")
continue
w = words[k]
if kind == "i":
out.append("i%d" % struct.unpack("<i", struct.pack("<I", w))[0])
elif kind == "f":
out.append("f%s" % f32(w))
elif kind == "b":
out.append("b%d" % (1 if (w & 0xff) else 0))
elif kind == "v":
if k + 1 >= len(words):
out.append("?")
else:
out.append("v%d" % ((words[k + 1] - w) // 4))
else:
out.append("?")
return out
def emit(batch, seq, source, names, seeds, input_name):
lines = ["tcb 1",
"meta source %s" % source,
"meta input %s" % input_name,
"meta batch seq=%d n=%d" % (seq, batch["n"]),
"meta note the load-time batch is excluded; this is the End-Turn submission"]
for netid, value in seeds:
lines.append("seed %s %s" % (netid, value))
for idx in sorted(batch["blocks"]):
b = batch["blocks"][idx]
lines.append("block %d %d" % (idx, b["pid"]))
if b["rate"][0]:
lines.append("gate %d rate %s" % (idx, b["rate"][1]))
if b["target"][0]:
name = names.get(b["pid"])
lines.append("gate %d target %d%s" % (idx, b["target"][1],
(" name %s" % name) if name else ""))
if b["boost"][0]:
lines.append("gate %d boost %d %s" % (idx, b["boost"][1], b["boost"][2]))
if b["g4"][0]:
lines.append("gate %d group4 %d %d" % (idx, b["g4"][1], b["g4"][2]))
if b["f3"][0]:
lines.append("gate %d group5 %s %s %s" % (idx, b["f3"][1], b["f3"][2], b["f3"][3]))
if b["civ"]:
lines.append("gate %d civilian" % idx)
for listno in range(1, 28):
n = b["sizes"][listno - 1]
if not n:
continue
lines.append("list %d %d %d" % (idx, listno, n))
have = b["elems"].get(listno, {})
for e in range(n):
words = have.get(e)
fields = element_fields(listno, words) if words is not None else ["?"]
lines.append("elem %d %d %d %s" % (idx, listno, e, " ".join(fields)))
return "\n".join(lines) + "\n"
def main(argv=None):
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("log")
ap.add_argument("-o", "--out")
ap.add_argument("--batch", type=int, help="which seq to convert (default: the last)")
ap.add_argument("--input", default="", metavar="SAVE",
help="the save this turn was run FROM. A capture belongs to one board and "
"replaying it against another charges the counter happily and is wrong; "
"recording it here lets the replayer say so.")
ap.add_argument("--name", action="append", default=[], metavar="PID=TECHNAME",
help="the tech name a research-target gate was observed to resolve to")
ap.add_argument("--seed", action="append", default=[], metavar="NETID=VALUE",
help="an AI client's construction seed")
a = ap.parse_args(argv)
batches = parse_log(a.log)
if not batches:
print("no aiblk records in %s" % a.log, file=sys.stderr)
return 2
seq = a.batch if a.batch is not None else max(batches)
if seq not in batches:
print("no batch seq=%d; the log holds %s" % (seq, sorted(batches)), file=sys.stderr)
return 2
names = {}
for n in a.name:
pid, _, tech = n.partition("=")
if not tech:
print("--name wants PID=TECHNAME", file=sys.stderr)
return 2
names[int(pid)] = tech
seeds = []
for s in a.seed:
netid, _, value = s.partition("=")
if not value:
print("--seed wants NETID=VALUE", file=sys.stderr)
return 2
seeds.append((netid, value))
text = emit(batches[seq], seq, a.log.split("/")[-1], names, seeds, a.input)
if a.out:
with open(a.out, "w") as f:
f.write(text)
print("wrote %s (batch seq=%d, %d blocks)" % (a.out, seq, len(batches[seq]["blocks"])))
else:
sys.stdout.write(text)
return 0
if __name__ == "__main__":
raise SystemExit(main())

232
tools/tcb_from_json.py Normal file
View file

@ -0,0 +1,232 @@
#!/usr/bin/env python3
"""Convert a lane-CB JSON turn-command capture into the `.tcb` file `sots_turn` reads.
TWO FORMATS ON PURPOSE, AND THIS IS THE JOIN.
`tools/turncommands_capture.py` (lane CB) produces the **capture of record**: raw element words as
ground truth, heap vectors and strings the deep dump followed, the input save's hash bound to the
output autosaves' hashes, the per-client AI seeds, and the container self-check. That is what an
experiment should leave behind and none of it belongs in an engine's input file.
`.tcb` is the **engine's input**: line-oriented, no parser, nothing but the commands and their
provenance. `sots_turn --turn-commands` reads it and nothing else, so the engine never grows a
JSON reader and never has an opinion about how a capture was taken.
This script is the only thing that has to know both, and it is deliberately the narrow part:
it takes `decoded.wire` where lane CB's decoder produced one, falls back to raw words where it did
not, and writes `?` for every field neither could reach.
tools/tcb_from_json.py CAPTURE.json [-o OUT.tcb] [--batch SEQ]
ONE FIELD MAPPING IS KNOWN WRONG AND IS OVERRIDDEN HERE, WITH ITS REASON.
List 5 (system rates) is decoded by lane CB as `{systemId, ship, terraform, sciences, ...}` --
the element's memory words mapped straight onto the frame's WIRE order. Lane RB measured that and
it is wrong: replaying it wrote the AI's one non-zero slider into `SRt` and regressed two leaves
on the canonical pair, where the oracle holds `SRsc = 1.0`. The only non-zero word of every list-5
element ever dumped is at memory index 2, and the same command on the wire -- where the frame is
NAMED -- puts its only non-zero in `SRsc`, the third member. So memory member 1 is wire member
`SRsc`, and the frame's memory order is not its wire order: ONE correspondence pinned, six unread.
Rather than carry a mapping that is known to be off by at least one, this converter emits the
system id and seven `?`. The replayer then counts the command -- the count is right either way --
and refuses to apply it, which is the correct behaviour for a payload nobody has read.
Settling it is one run: push two DIFFERENT sliders to two DIFFERENT values and read the
permutation off the element. Cheaper, a save taken after issuing rates carries the same command on
the wire with every field named, and needs no memory mapping at all.
"""
import argparse
import json
import struct
import sys
# Lists whose decoded `wire` this converter trusts. List 5 is deliberately absent (see above);
# lists with no entry are carried as a single `?`, which counts the command and applies nothing.
TRUSTED = {
3: "iiii", # ordinal, designId, systemId, trailing
7: "ii", # shipId, trailing
14: "iib", # fleetId, mode, flag
}
# Lists whose element leads with scalars and then a counted vector the deep dump may have read.
VECTOR_TAIL = {
8: ("i", 1), # fleetId, then the route
10: ("ii", 2), # systemId, fleetId, then a counted vector
}
UNMAPPED_HEAD = {
5: (1, 7), # one trusted leading int (the system id) and seven unread fields
23: (1, 1), # the system id and a Population body behind a vftable
}
def as_int(v):
if isinstance(v, bool):
return 1 if v else 0
if isinstance(v, float):
return int(v)
return int(v)
def tok_i(v):
return "i%d" % as_int(v)
def fields_for(list_no, elem):
"""The `.tcb` field tokens for one element."""
decoded = elem.get("decoded") or {}
wire = decoded.get("wire")
raw = elem.get("raw_words") or []
vectors = {v["at_word"]: v for v in (elem.get("vectors") or [])}
if list_no in UNMAPPED_HEAD:
lead, unread = UNMAPPED_HEAD[list_no]
head = [tok_i(raw[i]) for i in range(min(lead, len(raw)))]
return head + ["?"] * unread if head else ["?"]
if list_no in TRUSTED:
spec = TRUSTED[list_no]
if not wire or len(wire) < len(spec):
return ["?"]
out = []
for i, kind in enumerate(spec):
v = wire[i]
out.append("b%d" % (1 if v else 0) if kind == "b" else tok_i(v))
return out
if list_no in VECTOR_TAIL:
spec, vec_word = VECTOR_TAIL[list_no]
if len(raw) < len(spec):
return ["?"]
out = [tok_i(struct.unpack("<i", struct.pack("<I", raw[i] & 0xffffffff))[0])
for i in range(len(spec))]
v = vectors.get(vec_word)
if v is None:
# The dump did not follow the pointer. Its LENGTH is still derivable from the
# begin/end pair, and a length with no values is exactly the honest field: the
# command is counted and not applied.
if len(raw) > vec_word + 1:
out.append("v%d" % max(0, (raw[vec_word + 1] - raw[vec_word]) // 4))
else:
out.append("?")
elif v.get("truncated"):
out.append("v%d" % v["count"])
else:
vals = [struct.unpack("<i", struct.pack("<I", w & 0xffffffff))[0] for w in v["words"]]
out.append("v%d:%s" % (len(vals), ",".join(str(x) for x in vals)) if vals
else "v0:")
return out
return ["?"]
def emit(cap, batch):
src = cap.get("workload") or cap.get("note") or "lane-CB capture"
lines = ["tcb 1",
"meta source %s" % str(src).replace("\n", " "),
"meta lane %s" % cap.get("lane", "?"),
"meta guest %s" % cap.get("guest", "?"),
"meta build %s" % cap.get("build", "?"),
"meta captured %s" % cap.get("captured_utc", "?"),
"meta batch seq=%s n=%s" % (batch.get("seq"), batch.get("n"))]
binding = cap.get("binding") or {}
if binding.get("input"):
lines.append("meta input %s %s" % (binding["input"].get("file"),
binding["input"].get("sha256")))
for o in binding.get("outputs") or []:
lines.append("meta output %s %s%s" % (o.get("file"), o.get("sha256"),
"" if o.get("matches_oracle") is None
else (" matches_oracle=%s" % o["matches_oracle"])))
if cap.get("WARNING_INCOMPLETE"):
lines.append("meta warning heap-payloads-absent")
# The seeds. Without these the capture is a log file: the AI is one MT19937 per client seeded
# with one word, so the block is the AI's answer and the seed is its input.
for s in cap.get("ai_seeds") or []:
val = s.get("used") or s.get("observed")
if val is None:
continue
lines.append("seed %s %s%s" % (s.get("netId"), val,
""))
for blk in batch.get("blocks") or []:
lines.append("block %d %d" % (blk["index"], blk.get("playerId", blk.get("pid", 0))))
idx = blk["index"]
gates = blk.get("gates") or {}
def payload(name):
g = gates.get(name) or {}
return g.get("payload") if g.get("set") else None
p = payload("researchRate")
if p is not None:
lines.append("gate %d rate %s" % (idx, p))
p = payload("researchTarget")
if p is not None:
name = (blk.get("researchTargetName") or "").strip()
lines.append("gate %d target %s%s" % (idx, p, (" name %s" % name) if name else ""))
p = payload("researchBoost")
if p is not None:
spend, _, frac = p.partition(",")
lines.append("gate %d boost %s %s" % (idx, spend, frac or "0"))
p = payload("group4")
if p is not None:
flag, _, val = p.partition(",")
lines.append("gate %d group4 %s %s" % (idx, flag, val or "0"))
p = payload("group5")
if p is not None:
a, b, c = (p.split(",") + ["0", "0", "0"])[:3]
lines.append("gate %d group5 %s %s %s" % (idx, a, b, c))
if (gates.get("civilianRatios") or {}).get("set"):
lines.append("gate %d civilian" % idx)
for lt in sorted(blk.get("lists") or [], key=lambda l: l["list"]):
n = lt.get("size", 0)
if not n:
continue
lines.append("list %d %d %d" % (idx, lt["list"], n))
by_index = {e["index"]: e for e in lt.get("elements") or []}
for e in range(n):
elem = by_index.get(e)
f = fields_for(lt["list"], elem) if elem else ["?"]
lines.append("elem %d %d %d %s" % (idx, lt["list"], e, " ".join(f)))
return "\n".join(lines) + "\n"
def main(argv=None):
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("capture")
ap.add_argument("-o", "--out")
ap.add_argument("--batch", type=int,
help="which batch seq to convert (default: the last, which is the End-Turn "
"submission; a process also applies a batch at LOAD and replaying that "
"one against a save written after it would double-count)")
a = ap.parse_args(argv)
with open(a.capture) as f:
cap = json.load(f)
batches = cap.get("batches") or []
if not batches:
print("no batches in %s" % a.capture, file=sys.stderr)
return 2
if a.batch is not None:
picked = [b for b in batches if b.get("seq") == a.batch]
if not picked:
print("no batch seq=%d; the capture holds %s"
% (a.batch, [b.get("seq") for b in batches]), file=sys.stderr)
return 2
batch = picked[0]
else:
batch = batches[-1]
text = emit(cap, batch)
if a.out:
with open(a.out, "w") as f:
f.write(text)
print("wrote %s (batch seq=%s, %d blocks)"
% (a.out, batch.get("seq"), len(batch.get("blocks") or [])))
else:
sys.stdout.write(text)
return 0
if __name__ == "__main__":
raise SystemExit(main())

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,38 @@
tcb 1
meta source cb-c1-turn2to3.txt
meta input turn2-state.sav
meta input_sha256 ab4ac2d7e2977260a4de916f65c7ecc6dd5b0094dac4bb51241317232ea17ab4
meta output (Autosave EndTurn).sav 66732 bb4fd9ac89f41e3bc0db2af08b18ce83417521ac4bcee695fc9fa6ce16e30948 oracle=True
meta output (Autosave).sav 67219 978041acd168b56ed8eb3f5e42e78d5e70eae6e6517d75e659a5eb7ca3d60921 oracle=True
meta control c0-hooksoff-turn2 (hooks=off, same guest, same click path; byte-identical to this run)
meta lane CB capture_version 1 build cb2-20260908T2251Z guest VM146
meta extension vN:a,b -- a vector whose contents ARE known; bare vN still means unknown and must still be refused
meta batch seq=2 n=8
seed 32 0x9dfad635
seed 496 0xb482b2d8
seed 512 0xfb81e993
block 0 16
gate 0 rate 0.25
block 1 32
gate 1 rate 0.8
list 1 3 1
elem 1 3 0 i2 i18 i288 i0
list 1 5 1
elem 1 5 0 i288 f0.0 f1.0 f0.0 f0.0 f0.0 f0.0 i0
list 1 8 1
elem 1 8 0 i34 v1:272
list 1 10 1
elem 1 10 0 i288 i34 v1:1728
list 1 14 2
elem 1 14 0 i34 i0 b1
elem 1 14 1 i34 i1 b1
list 1 23 1
elem 1 23 0 i288 ? v6:12422480,1,2,862581960,1000000000,0
block 2 496
gate 2 rate 0.8
block 3 512
gate 3 rate 0.8
block 4 0
block 5 0
block 6 0
block 7 0

View file

@ -0,0 +1,28 @@
tcb 1
meta source l4-turn1to2-aiorders.txt
meta input turn1-state.sav
meta batch seq=2 n=8
meta note the load-time batch is excluded; this is the End-Turn submission
block 0 16
gate 0 rate 0.25
block 1 32
gate 1 rate 0.8
gate 1 target 144 name IND_Waldo
list 1 1 1
elem 1 1 0 ?
list 1 3 1
elem 1 3 0 i1 i18 i288 i0
list 1 5 1
elem 1 5 0 i288 ? ? ? ? ? ? ?
list 1 23 1
elem 1 23 0 i288 ?
block 2 496
gate 2 rate 0.8
gate 2 target 90 name DRV_PlsFiss
block 3 512
gate 3 rate 0.8
gate 3 target 288 name XNC_TrnsMorr2
block 4 0
block 5 0
block 6 0
block 7 0

View file

@ -0,0 +1,30 @@
tcb 1
meta source l4-turn2to3-aiorders.txt
meta input turn2-state.sav
meta batch seq=2 n=8
meta note the load-time batch is excluded; this is the End-Turn submission
block 0 16
gate 0 rate 0.25
block 1 32
gate 1 rate 0.8
list 1 3 1
elem 1 3 0 i2 i18 i288 i0
list 1 5 1
elem 1 5 0 i288 ? ? ? ? ? ? ?
list 1 8 1
elem 1 8 0 i34 v1
list 1 10 1
elem 1 10 0 i288 i34 v1
list 1 14 2
elem 1 14 0 i34 i0 b1
elem 1 14 1 i34 i1 b1
list 1 23 1
elem 1 23 0 i288 ?
block 2 496
gate 2 rate 0.8
block 3 512
gate 3 rate 0.8
block 4 0
block 5 0
block 6 0
block 7 0