`ComputeBudget`'s savings-interest term is now compared against the running game at
a treasury the corpus actually contains. Three runs on VM146 from turn1-state.sav:
A (widened floats, as shipped) 3,895 calls, 0 diverged, 0 undeclared writes
B (exact decimals, the control) 2,718 calls, 1,359 diverged
The game fills savingsInterest with 499 at a treasury of 50,000, and with 380 at
38,100 -- the exact decimals pay 500 and 381. Every divergence in B lands on a
treasury that is a multiple of 100 and no other state diverges at all, which is
exactly the arithmetic. G3's rule-23 reading is now measured, not inferred, and the
one-money error is shown to propagate into `available` and `researchMoney` too.
The control also settles why the earlier 4,437-call green run was green: slot 5 IS
diffed and the harness CAN see it, so that run simply presented no boundary state.
Coverage is therefore reported as distinct states, not calls: 5 distinct treasuries,
2 of them on the boundary.
Two further rule-23 constants found in the same routine by an operand-width sweep,
corrected, and honestly marked UNVERIFIED because no reference turn can see them:
- the research-yield factor is a widened 0.85f while its two neighbours in the
same product are exact doubles. Boundary: research money a multiple of 40,000;
the run presented 9 distinct values and none is.
- the three research modifiers are summed in single precision, not double.
Boundary: two of the three non-zero; the corpus has shrm = TRM = 0.
Both are pinned by boundary cases in test_economy.cpp that fail with the decimals.
Also verified live, in the same run:
- T31's difficulty-column recovery. The live ServerPlayer+0xf9 / NPC flags on all
eight players are exactly what lane PL's save-only inversion claims, including
the awkward system-owning player that is still ambiguous because it is an NPC.
- BANKRUPTCY_PROTECTION_LIMIT_FACTOR reads 3.29999995 = (float)3.3. Its file image
is zero because the loader fills it at run time, so lane PL-3 had to assume the
value; it is now measured and the assumption was right.
Falsified, and recorded as such: the difficulty-mods record does NOT sit inline at
ServerPlayer+0x36c -- that field is a heap pointer on all eight players. The row IS
reachable from a ServerPlayer (which corrects the hook's standing coverage note),
but the fitted {3.0,1.5}/{1.0,1.0} pair remains unverified. The hook logs the
pointer and does not follow it.
The `verified` column stays 0, deliberately. Every phase this compare touches is
Partial for reasons upstream of it, and promoting one because part of it was checked
is the drift app_test_catalog exists to catch. What moved is models; see
docs/L5-live-verification.md for each one with its coverage.
Gates run separately: clean-room OK, host ctest 54/54, CT111 shim cross-build exit 0.
428 lines
25 KiB
Markdown
428 lines
25 KiB
Markdown
# L5 — the standalone's newest models, checked against the running game
|
||
|
||
Lane L5, 2026-09-08. Worktree `wip/l5` off `main` `0117495`. Guest **VM146** (`192.168.10.146`),
|
||
a ZFS clone of the reference guest VM140.
|
||
|
||
The `verified` column of `src/app/phase_catalog.cpp` has read **0** all campaign, by construction:
|
||
`app_test_catalog` asserts it, because in that table `verified` means *compared against the live
|
||
game* and no lane that wrote a phase held a VM. This lane holds one. Everything below is written
|
||
**before** the builds and before any run.
|
||
|
||
---
|
||
|
||
## 0. What a live compare can settle that a host test cannot
|
||
|
||
Three of this lane's four targets are claims about **values the executable never stores in a file**
|
||
or about **rounding the host cannot reproduce**:
|
||
|
||
* a data-file constant whose file image is zero because the loader fills it at run time
|
||
(`BANKRUPTCY_PROTECTION_LIMIT_FACTOR`, lane PL);
|
||
* the per-player "is AI" flag at `ServerPlayer+0xf9`, which no save carries and which lane PL's
|
||
T31 recovers *by inference from the stored `BnkEl`*;
|
||
* the game's own x87 rounding of a product whose error exceeds half an ulp of the answer
|
||
(lane G3's rescale).
|
||
|
||
And one is a claim about a **boundary the corpus contains but the existing compare never reported
|
||
on** (rule 23, `ComputeBudget`'s interest literals).
|
||
|
||
---
|
||
|
||
## 1. E1 — the interest literals, at the boundary, with a control that must fail
|
||
|
||
### The setting
|
||
|
||
Lane G3 corrected `ComputeBudget` to multiply the treasury by **widened float** literals —
|
||
`kSavingsInterestRate = 0.009999999776482582` = `(double)0.01f` and
|
||
`kDebtInterestRate = 0.15000000596046448` = `(double)0.15f` — because the image loads
|
||
`0x009e31c0` and `0x009ed188`, which hold those widened values, not the exact decimals. The
|
||
consequence G3 states is that **a treasury of exactly 50,000 earns 499, not 500.**
|
||
|
||
G3 did not measure that against the game. It inferred it from a one-money residual on the human's
|
||
savings, and it wrote down, honestly, that the behavioural compare had not caught it:
|
||
`ComputeBudget` had run **4,437 calls with 0 divergences** and "that run presented only 20 distinct
|
||
states and none of them sat on a rounding boundary."
|
||
|
||
### The fact that makes this decidable in one End Turn
|
||
|
||
`turn1-state.sav` (= `Autosave EndTurn - turn2.sav`, 64,967 B) carries **exactly two distinct
|
||
treasuries across its eight players**:
|
||
|
||
| `Sav` | players | multiple of 100? |
|
||
|---:|---:|---|
|
||
| 50,000 | 2 (the human at `@0000f2a8`, the AI empire at `@0001b3d8`) | **yes** |
|
||
| 0 | 6 | yes, trivially |
|
||
|
||
`SavingsInterest` gates on `savings >= 0 && ownsSystems`, and the interest term diverges between
|
||
the two constants exactly when `savings` is a **multiple of 100**:
|
||
|
||
* exact decimal: `50000 x 0.01 = 500.0` → `trunc` = **500**
|
||
* image widened: `50000 x 0.009999999776482582 = 499.9999888241291` → `trunc` = **499**
|
||
|
||
So the corpus **does** sit on the boundary, and slot 5 (`kSlotSavingsInterest`) is **not** in the
|
||
declared input boundary — `IsInputSlot` names slots 1, 2, 3, 4, 7 and 11, and 5 is not among them,
|
||
so `FillSlots` writes `b.savingsInterest` from *our* arithmetic and the harness diffs it.
|
||
|
||
That is a contradiction worth stating plainly before the run: **if slot 5 is compared, and the
|
||
boundary is in the corpus, the 4,437-call run should have diverged and did not.** Either the
|
||
4,437 calls were not on this save, or something about the compare does not reach slot 5. The run
|
||
below distinguishes those two, and that is the point of the control build.
|
||
|
||
### E1 predictions
|
||
|
||
**E1-a.** Under build **A** (`main`, G3's widened constants), `hook.Game::ServerPlayer::ComputeBudget=compare`,
|
||
one End Turn from `turn1-state.sav`: **0 divergences**, and the original fills slot 5 with **499**
|
||
for the human and **499** for the AI empire, **0** for the other six.
|
||
|
||
**E1-b (the control that must fail).** Under build **B** — byte-identical to A except
|
||
`kSavingsInterestRate = 0.01` and `kDebtInterestRate = 0.15`, the exact decimals — the same run
|
||
reports **exactly 2 divergences**, both on `side.budget.after.v.savingsInterest`, both
|
||
`ours = 500` against `orig = 499`, on the two players whose `Sav` is 50,000 and who own systems.
|
||
Every other slot and every other player agrees.
|
||
|
||
**E1-c (rule 19).** Build B changes only `ours()`. `compare` mode never lets ours touch game
|
||
memory. So the **autosave written by build B must be byte-identical to the one written by build A,
|
||
and both must be byte-identical to a `hooks=off` control run.** Three files, one hash.
|
||
|
||
**E1-d (coverage, rule 1 / rule 15).** The run will report a call count in the low tens, not
|
||
thousands, and a **distinct-state count of 2** on the treasury input. That number goes next to the
|
||
verdict. A green compare over 2 distinct states is worth reporting as 2, not as "clean".
|
||
|
||
### Falsification
|
||
|
||
* *The original fills slot 5 with 500.* Then G3's rule-23 reading is **wrong**, the correction
|
||
introduced a defect, and the residual it closed did so for another reason. This is the outcome
|
||
that would matter most, and it is why the run is worth making.
|
||
* *Build B also reports 0 divergences.* Then slot 5 is not actually reaching the diff — an
|
||
**instrument** defect, not an engine one, and it would retroactively explain the 4,437 green
|
||
calls. Symptom to check: whether the record carries `side.budget.after` at all, and whether the
|
||
human's call appears in the log.
|
||
* *Build B diverges on more than 2 calls, or on a player whose `Sav` is 0.* Then `SavingsInterest`
|
||
is reached on a path that ignores its `ownsSystems` gate, or the UI's `projected=true` calls are
|
||
in the log with treasuries the save does not carry — in which case the distinct-state count is
|
||
larger than 2 and the whole coverage claim above has to be restated from the log.
|
||
* *The autosaves differ between A, B and the `hooks=off` control.* Then compare mode is leaking
|
||
into game memory, which would invalidate every compare result the campaign has recorded, not
|
||
just this one.
|
||
|
||
---
|
||
|
||
## 2. E2 — T31's difficulty column, checked against the flag the save does not carry
|
||
|
||
Lane PL's T31 identifies which difficulty column applies **from the save alone**, with no operator
|
||
input, on all 11 saves, by computing `BnkEl` under both columns and matching the stored value. Its
|
||
own resolution is "**1 AI and 1 non-AI on every one of the eleven saves**, with the rest ambiguous",
|
||
and §7 records that "ambiguous" has two causes — a player that owns nothing, and player 576, an
|
||
**NPC**, for which the row selector `isAI && !npc` hands both columns the same row.
|
||
|
||
The running game holds the answer directly. `ServerPlayer_off_IsAI = 0xf9` and
|
||
`ServerPlayer_off_NPC` are **already snapshotted** by `compute_budget.cpp` and already emitted into
|
||
every trace record as `inputs.isAI` / `inputs.npc`. So E1's run produces, per player per turn, the
|
||
live values of the two bits T31 infers — **at no extra cost and in the same run**.
|
||
|
||
**E2-a.** The live flags on `turn1-state`, for the eight players, are: `isAI=false, npc=false` for
|
||
the human; `isAI=true, npc=false` for exactly one player (the AI empire); and for the remaining
|
||
six, at least one with `npc=true` — the one T31 calls ambiguous-because-NPC (player 576, the
|
||
Independent Colony).
|
||
|
||
**E2-b.** T31's save-only identification agrees with the live flags on **every player it does not
|
||
call ambiguous**, and every player it *does* call ambiguous is either `maxIncome == 0` or
|
||
`npc == true`. If both hold, T31's recovery is verified against the game rather than against its
|
||
own inversion.
|
||
|
||
### Falsification
|
||
|
||
* *A player T31 identifies as AI reads `isAI=false` live.* T31's inversion is fitting noise; the
|
||
two columns are being distinguished by something other than the ×1.1, and PL-2's leaf closures
|
||
are coincidence.
|
||
* *More than one player reads `isAI=true && npc=false`.* Then "1 AI and 1 non-AI on every save" is
|
||
a property of the inversion, not of the roster, and the ambiguous bucket is hiding real AI
|
||
players whose max income happens to be 0.
|
||
* *No player reads `npc=true`.* Then §7's explanation of the sixth ambiguous player is wrong and
|
||
the ambiguity has a third cause.
|
||
|
||
---
|
||
|
||
## 3. E3 — the constants the file image cannot hold
|
||
|
||
Two of the campaign's live claims rest on values that are **zero in the executable on disk**
|
||
because the data-file loader writes them at run time, and on a table the shim reads at fitted
|
||
constants:
|
||
|
||
* `g_BANKRUPTCY_PROTECTION_LIMIT_FACTOR` at `0x00b23e28`, reached through the pointer slot
|
||
`0x00aedfdc`. Lane PL-3 read its *width* off the instruction stream (`D8 09` = `fmul m32fp`, a
|
||
float32, two instructions after `DD 05` = `fld m64fp`, a double) and could not read its *value*
|
||
at all: "the file image of the slot is zero: the loader fills it at run time". PL-3 assumed 3.3
|
||
to compute the disagreement rate and said so.
|
||
* the difficulty-mods triple. `budget_inputs.h` carries `kDifficultyAI{3.0, 1.5}` and
|
||
`kDifficultyHuman{1.0, 1.0}` as **fitted constants "measured from the B1 trace rather than
|
||
snapshotted inputs"**, with the coverage note "not reachable from a ServerPlayer".
|
||
|
||
**E3-a.** Read live, the protection factor is a finite positive float32, and it is **exactly
|
||
representable as a float32** — i.e. `(float)value == value` — which is the whole content of PL-3's
|
||
width claim. Its value is **not** predicted here; PL-3's 3.3 is an assumption and this run replaces
|
||
it with a measurement.
|
||
|
||
**E3-b.** The difficulty row the game hands the AI empire has its fleet-maintenance divisor and its
|
||
research multiplier equal to the fitted `{3.0, 1.5}`, and the human's equal to `{1.0, 1.0}`.
|
||
|
||
**E3-c.** `ServerPlayer+0x8` points into the `StrategyServer` frame (the address table records
|
||
`ServerPlayer+0x8 == S+4`), so the difficulty record **is** reachable from a `ServerPlayer` and the
|
||
coverage note "not reachable from a ServerPlayer" is wrong as written.
|
||
|
||
### Falsification
|
||
|
||
* *The slot reads zero or unreadable at the turn gate.* Then the loader has not run, or the key is
|
||
absent from this install's data files, and PL-3's whole disagreement analysis is about a constant
|
||
the shipped game does not have. Symptom: `BnkPr` is 0 for every player, which the corpus can be
|
||
checked against.
|
||
* *The fitted difficulty pair disagrees with the live row.* Then B1's fit was on a workload that
|
||
happens to match and `ComputeBudget`'s maintenance and research terms are wrong off-corpus.
|
||
* *`ServerPlayer+0x8` does not resolve to a plausible server frame.* Then E3-c is wrong and the
|
||
coverage note stands; the value has to come from a hook on the server instead.
|
||
|
||
---
|
||
|
||
## 4. What is deliberately NOT attempted, and why
|
||
|
||
**G3's civilian growth is not compared on a stock End Turn.** The instrument would be a new
|
||
`compare` hook on `ServerSystem::GrowCivilianPops` (`0x00754220`, verified thiscall, no stack
|
||
args, `void`), whose verdict is the `Population` rows behind `ServerSystem+0x1a0`. On the reference
|
||
save that run presents ~28 calls of which 2–3 have civilians, **all single-species, all at ideal
|
||
suitability, none blockaded, none seeding, none at the settle limit** — the degenerate case in
|
||
which G3's rescale `trunc(applied x (clamped/total))` is *exact by construction* and the clamp is
|
||
the only thing being tested. That is the same shape as the 4,437-call green run this lane exists to
|
||
distrust: it would come back clean and license nothing.
|
||
|
||
Recording it as a measured decision rather than an omission: **the civilian-growth compare is worth
|
||
making only on a workload the corpus lacks** — a multi-species colony (the rescale's truncation and
|
||
its lack of renormalisation), a blockaded colony (`haltv[1]`), a colony over its settle limit (the
|
||
`-50,000,000` floor), or a newly landed colony ship (the seeding loop). Each of those turns one row
|
||
of G3 §4.4's falsification table from "invisible here" into a measurement. Building such a save is
|
||
a game-play session, not an instrument change, and it is the right next lane's work.
|
||
|
||
---
|
||
|
||
*Outcomes are recorded in §5 onward, after the runs, next to the predictions above.*
|
||
|
||
---
|
||
|
||
# 5. Outcomes
|
||
|
||
Three runs on **VM146**, all from `turn1-state.sav` (the Turn-1 save, human and AI empire both at
|
||
a treasury of exactly 50,000), all through the identical Load → Single Player → OK → row → OK →
|
||
Launch → End Turn path:
|
||
|
||
| run | DLL | `hooks` | `ComputeBudget` |
|
||
|---|---|---|---|
|
||
| **control** | `l5A-widened` | `off` | not installed |
|
||
| **A** | `l5A-widened` | `trace` | `compare` |
|
||
| **B** | `l5B-decimal-CONTROL` | `trace` | `compare` |
|
||
|
||
A and B differ **only** in `kSavingsInterestRate` / `kDebtInterestRate` — widened floats in A, the
|
||
exact decimals in B — which is our side of the compare and never touches game memory.
|
||
|
||
## 5.1 E1 — held, and the control fired exactly as designed
|
||
|
||
**E1-a held.** Run A: **3,895 calls, 3,895 compared, 0 diverged, 0 undeclared writes.** The
|
||
original filled `savingsInterest` with **499** at a treasury of 50,000 — not 500.
|
||
|
||
**E1-b held in substance and was wrong in its units.** I predicted "exactly 2 divergences". Run B
|
||
reported **1,359 divergences over 2,718 calls**. The prediction was right about *which states*
|
||
diverge and wrong about the counting: I counted players, and the harness counts calls —
|
||
`ComputeBudget` is invoked several hundred times per player per turn by the AI's own evaluation
|
||
loop, not once. Recorded rather than quietly corrected, because the same mistake would have
|
||
made a call-count look like a coverage claim.
|
||
|
||
What the control shows, per treasury:
|
||
|
||
| treasury | multiple of 100? | calls in B | diverged | game's `savingsInterest` | exact decimal |
|
||
|---:|---|---:|---:|---:|---:|
|
||
| 0 | — | 303 | **0** | 0 | 0 |
|
||
| 38,100 | **yes** | 1 | **1** | 380 | 381 |
|
||
| 50,000 | **yes** | 1,358 | **1,358** | **499** | 500 |
|
||
| 92,651 | no | 28 | **0** | 926 | 926 |
|
||
| 289,688 | no | 1,028 | **0** | 2,896 | 2,896 |
|
||
|
||
Every divergence is on a multiple of 100 and no other state diverges at all — which is exactly the
|
||
arithmetic: the two constants differ in their truncated product only when the treasury is a
|
||
multiple of 100. **The game agrees with the widened float and disagrees with the decimal, at two
|
||
independent treasuries.** G3's rule-23 correction is verified against the running game.
|
||
|
||
**A blast radius larger than "one money".** The diff on a diverging call is three fields, not one:
|
||
|
||
```
|
||
side.budget.after.v.savingsInterest orig 499 ours 500
|
||
side.budget.after.v.available orig 239091 ours 239092
|
||
side.budget.after.v.researchMoney orig 59772 ours 59773
|
||
```
|
||
|
||
The interest is an income line, so the error propagates into `available` and then into the money
|
||
routed to research — 1,317 of the 1,359 diverging calls also move `researchMoney`. A one-money
|
||
interest defect moves the player's research spend too.
|
||
|
||
**E1-c held.** All three runs wrote a byte-identical **pre-turn** autosave
|
||
(`a3f9dc4b49fc669c…`, 64,967 B). Load and save are deterministic. The post-turn autosaves are
|
||
discussed in §5.4 and the answer is *not* the hook.
|
||
|
||
**E1-d — coverage, and my prediction was wrong here too.** I predicted 2 distinct treasury states;
|
||
the run presented **5** (the trace spans the post-turn UI as well as the turn), of which **2 are
|
||
on the interest boundary**. The honest coverage line for run A is therefore:
|
||
|
||
> `ComputeBudget`: 3,895 calls (3,888 turn-path + 7 `projected=true`), **5 distinct treasury
|
||
> states, 2 of them on the rounding boundary**, 0 divergences, 0 undeclared writes.
|
||
|
||
Compare that with the number this lane was sent to distrust — "4,437 calls, 0 divergences, 20
|
||
distinct states, none on a boundary". The difference between the two runs is not the call count.
|
||
|
||
**The contradiction in §1 is resolved.** Slot 5 *is* compared and the harness *can* see it: the
|
||
control build produced 1,359 divergences on it. So the earlier 4,437-call green run genuinely did
|
||
not present a boundary treasury. The instrument was never broken; the workload was thin.
|
||
|
||
## 5.2 E2 — T31's difficulty-column recovery, checked against the flag the save does not carry
|
||
|
||
Held on every point. The live `ServerPlayer+0xf9` / NPC flags, read on all eight players:
|
||
|
||
| PlyrIdx | `isAI` | `npc` | owns systems | T31's verdict from the save alone |
|
||
|---:|---|---|---:|---|
|
||
| 0 (human) | false | false | 1 | non-AI column |
|
||
| 1 (AI empire) | **true** | false | 1 | AI column |
|
||
| 2–6 | true | **true** | 0 | ambiguous (zero income) |
|
||
| 7 (Independent Colony) | true | **true** | **1** | ambiguous (both columns agree) |
|
||
|
||
**E2-a held**: exactly one player is `isAI && !npc`, the human is neither, and six are NPCs.
|
||
**E2-b held**: every player T31 calls ambiguous is either income-less or an NPC, and the one
|
||
awkward case — a player that owns a system and is *still* ambiguous — reads `npc = true` live,
|
||
which is precisely the explanation lane PL §7 gave after its own prediction missed it. T31
|
||
identifies the column from the save with no operator input, and the running game agrees.
|
||
|
||
`aidf` (`ServerPlayer+0x368`) reads **1** on all eight players across 3,895 records — the
|
||
difficulty level the corpus carries, and the level whose AI column holds the ×1.1.
|
||
|
||
## 5.3 E3 — one confirmed, one falsified
|
||
|
||
**E3-a held, and it is the one result no static reading could have produced.** The
|
||
`BANKRUPTCY_PROTECTION_LIMIT_FACTOR` slot at `0x00aedfdc` was non-null on every call and the
|
||
float32 behind it reads **3.29999995231628417968750** — exactly `(float)3.3`. So:
|
||
|
||
* the shipped data file does carry 3.3, and **PL-3's assumption was right**;
|
||
* the narrowing PL-3 read off the instruction stream is **real**: the constant the game multiplies
|
||
by is not the decimal 3.3 but the nearest float32 below it. PL-3 had to write "the file image of
|
||
the slot is zero: the loader fills it at run time" and estimate from an assumed value. It is now
|
||
measured.
|
||
|
||
The three `.rdata` doubles read identically in the running process to their file images
|
||
(`0.009999999776482582`, `0.15000000596046448`, `0.8500000238418579`), so nothing patched or
|
||
relocated them.
|
||
|
||
**E3-c was FALSIFIED, in the way its own falsifier described.** I predicted the difficulty record
|
||
sits inline at `ServerPlayer+0x36c`. It does not: the dword there is a heap pointer on all eight
|
||
players (`0x0da0ec20`…`0x0da13360`, within 18 KB of the `ServerPlayer` allocations themselves) and
|
||
the "floats" behind it decode as denormal garbage. The record is **one dereference further on**.
|
||
|
||
What survives is the part the reading was actually for: the row **is** reachable from a
|
||
`ServerPlayer`, so `compute_budget.h`'s note "not reachable from a ServerPlayer" is wrong as
|
||
written and has been corrected. **E3-b is therefore not settled**: the fitted pair
|
||
`{3.0, 1.5}` / `{1.0, 1.0}` remains unverified against the running game. The hook now logs the
|
||
pointer *as a pointer* and does not follow it — dereferencing an unvalidated pointer inside a hook
|
||
that runs in the turn pipeline is how a shim crashes the game, and this lane could not re-run to
|
||
validate it. `ServerPlayer_off_DiffModsPtr` is in the address table as a **hypothesis** with the
|
||
measurement attached, and the next run is one guarded dereference.
|
||
|
||
## 5.4 A determinism result that was not being looked for
|
||
|
||
The three post-turn autosaves are all different, and **the hook is not the cause**:
|
||
|
||
| run | hooks | `(Autosave).sav` | `Player[512 "Singularity"]/ResTNm` |
|
||
|---|---|---|---|
|
||
| control | **off** | `62061918176e3441` | `XNC_TrnsLir2` |
|
||
| A | compare | `7fc9e6ab46b47794` | `XNC_TrnsHvr2` |
|
||
| B | compare | `ab4ac2d7e2977260` | `BIO_GnMod` |
|
||
|
||
Each pair differs by **exactly four leaves**: one shadow empire's research-target name, two of its
|
||
tech-tree status words, and the derived `/Summary/Checksum`. Nothing else in the state moves.
|
||
|
||
Runs A and B carry **identical hooks** and differ only in our side's arithmetic, which compare
|
||
mode never lets touch game memory — so a hook-induced perturbation cannot explain A ≠ B. And run
|
||
B reproduced `ab4ac2d7e2977260…`, which is the hash `findings/subsystems/running-the-game.md`
|
||
records for the original live session's `Autosave - turn2.sav`. So the instrument is exonerated
|
||
(rule 19 discharged) and the real finding is about the game:
|
||
|
||
> **The `turn1-state → turn2` transition is not reproducible across runs.** The determinism
|
||
> oracle was established on `ref-turn2 → turn3`, where five runs in two processes gave one hash.
|
||
> It does not extend to this pair: three runs gave three hashes, differing only in one Singularity
|
||
> shadow empire's AI research pick.
|
||
|
||
The pre-turn autosave is byte-identical across all three runs, so the loader and the writer are
|
||
deterministic; only the turn's AI decision is not. This is worth knowing before any lane uses
|
||
`turn1-state → turn2` as an oracle pair, and it corroborates lane PL's placing `ResTNm`/
|
||
`ResErrRoll` on Rung B — those leaves are not merely AI-dependent, they are *unstable*.
|
||
|
||
## 5.5 Two more rule-23 defects, found by reading and invisible to the corpus
|
||
|
||
The operand-width sweep of the money chain turned up two more constants in `ComputeBudget`
|
||
itself, both now corrected and both **unobservable on any reference turn**:
|
||
|
||
1. **The research-yield factor is a widened float.** `0x00a2d818` holds `(double)0.85f`, while its
|
||
two neighbours in the same product — the 1.15 and the 0.5 — are exact doubles. Three literals,
|
||
one expression, two widths. The exact-decimal product is integral only when the research money
|
||
is a multiple of 40,000, and the image's constant lands one *above* it there: at 40,000 the
|
||
game gives 391 research points and the decimal gives 390.
|
||
**Measured coverage: the run presented 9 distinct research-money values and none is a multiple
|
||
of 40,000.** That is why the compare stayed green with the wrong constant in place.
|
||
2. **The three research modifiers are summed in single precision**, with a float32 store after
|
||
every add, not in double. The corpus has `shrm = TRM = 0`, so the sum is exactly `ResMod` and
|
||
the two agree by construction.
|
||
|
||
Both are pinned by boundary cases in `tests/game_sim/test_economy.cpp` that fail with the decimals
|
||
and pass with the image's constants. Neither is verified against the game, and the doc says so —
|
||
the sweep also confirmed that `1.15`, `0.5`, `500`, `0.01` (in the infrastructure term) and `1.65`
|
||
really *are* exact doubles in the image, so "widen every literal" would be its own defect.
|
||
|
||
## 5.6 G3's clamp, observed on the un-instrumented game
|
||
|
||
The control run's own strategy-map panel, with no hooks installed at all, shows Gamma Cephei's
|
||
civilian population going **500,000,000 → 520,000,000** across the End Turn: exactly the
|
||
**+20,000,000** step cap that lane G3 read out of the population-type table. That is the clamp
|
||
claim confirmed on the game's own readout, independent of any instrument.
|
||
|
||
It is *not* a verification of G3's rescale, which is the part G3 flagged as "a measurement, not a
|
||
theorem". The rescale is exact by construction in the one-species case, and every colony on this
|
||
save is single-species. §6 says what workload would settle it.
|
||
|
||
---
|
||
|
||
# 6. What the `verified` column reads after this lane, and why
|
||
|
||
**It still reads 0, and moving it would have been the wrong answer.**
|
||
|
||
In this table `verified` means "implemented here **and** compared against the live game", and
|
||
`app_test_catalog` asserts it stays 0 so the claim cannot drift upward by accident. Every phase
|
||
this lane's compare touches is `Partial` — `P01 ComputeBudget` because the repair demand of ships
|
||
in orbit is still taken as 0; `P02 ApplyNetToSavings` because an AI's research rate is set by its
|
||
own orders during the turn; `T31 UpdateBankruptcyLimits` because `BnkPr` needs a tuning constant
|
||
that is not wired. Those are all blocked **upstream of the compare**, and none of them is
|
||
something a VM can fix. Promoting a `Partial` phase to `verified` because part of it was checked
|
||
is exactly the drift the assertion exists to catch.
|
||
|
||
So the ladder's two conditions — *complete* and *compared* — are currently blocked by different
|
||
things, and the column measures the conjunction. What this lane moved is **models**, and those are
|
||
listed with their coverage in §5. The catalog notes for `P01`, `P02` and `T31` have been updated
|
||
to carry the live evidence and its measured coverage instead of the older, weaker call counts.
|
||
|
||
# 7. The workload the next lane needs
|
||
|
||
Not more analysis. Two saves that the corpus does not contain:
|
||
|
||
1. **A multi-species colony**, which is the only thing that exercises G3's rescale
|
||
`trunc(applied x (clamped/total))` outside its degenerate exact form — the one float G3 says is
|
||
"a measurement, not a theorem". Also a blockaded colony (`haltv[1]`), a colony over its settle
|
||
limit (the −50,000,000 floor) and a newly landed colony ship (the seeding loop): four rows of
|
||
G3 §4.4's falsification table that read "invisible here" today.
|
||
2. **A treasury that is a multiple of 40,000 in research money**, which is the only thing that can
|
||
observe the yield-factor width live. It is reachable without editing memory: the research
|
||
slider changes `ResRate` and therefore the research money, and each slider position is a
|
||
separate `ComputeBudget` call — sweeping it turns the UI into a boundary generator, which is
|
||
the cheapest way to raise the distinct-state count that §5.1 shows is the thing that actually
|
||
decides whether a compare means anything.
|
||
|
||
Both are game-play sessions, not instrument changes.
|