lane R: golden-trace recapture with guards on the live game

Five hooks recaptured, build recap-7584bad-20260908T0615Z, no source change.

- ProcessResearch: the over-budget event is now a compare divergence
  (side.events.after.v.next_id orig=4 ours=3) instead of a 609 KB save diff.
  15 calls over 5 turns, 3 diverged, RNG matched 15/15.
- ComputeBudget: 4284 compared, 0 diverged, guard quiet - but only 20 distinct
  states and 13 of 22 slots always zero.
- MoveFleet: 8 of 45 calls diverge by 1 ULP of position. B4's clean verdict was
  a one-sample verdict.
- First guarded captures for OnTechResearched (RollResearchEvent draw fired and
  matched), the colony turn and MoveFleet.
- Guards mapped SetResearched live and found an undeclared ObservedTech append.

VM140 released at the main menu, hooks=trace.
This commit is contained in:
alex 2026-09-08 03:06:43 -04:00
parent d738b6b4bb
commit 05095e93da
22 changed files with 4926 additions and 8 deletions

View file

@ -53,20 +53,24 @@ Status flow: `backlog → in-progress → mapped → verified` (or `blocked`).
| tech effects (code-defined) | subsystem | mapped | high | 90% | 2026-09-07 | findings/subsystems/tech-effects.md: g_TechIdNames 196 @0x009ff9e4 (TechId=10000+i), effects in ServerPlayer::OnTechResearched 0x00891790; SpeciesDef table 0x00b10a00; 36 strategic / ~45 gates / ~115 none. Feeds game/sim + game/data |
| strategic formula gaps | subsystem | verified | high | 100% | 2026-09-07 | findings/subsystems/formula-gaps.md: all 8 answered with code (bankruptcy 3.3 on BnkPr, linear hazard, money tail, pbon, slider, [6] net, no speed clamp, decay hits current) |
| engine: game/design | engine | verified | high | 100% | 2026-09-07 | merged; see ShipDesignDef row |
| P2-B1 ComputeBudget (behavioral) | phase2 | verified | med | 75% | 2026-09-08 | LIVE + GREEN on declared regions (4,437 compares, 0 divergences; replace-mode oracle byte-identical). Corrections: out-param int[22]; researchMoneyKept gated on a research target. QUALIFIED BY THE HARNESS AUDIT: replace mode runs the ORIGINAL A SECOND TIME to harvest slots -> a real per-turn double effect (ComputeOutput repairs ships in orbit) that no region reaches - UNRESOLVED; the over-budget int at Budget+0x64 was captured only as an argument and args are never compared. Golden trace needs recapture with guards. Coverage gaps: 8 slots always 0 (expenses/aid/debt) |
| P2-B2 OnTechResearched (behavioral) | phase2 | verified | high | 80% | 2026-09-08 | LIVE: 3 completions compared, 0 divergences (incl. one tech absent from the effects table = tail-only, the case an early return would have broken). float32 CONFIRMED ON THE GAME bit-for-bit (con_mod 1.0->0.899999976 = (float)(x+(double)0.1f)). Found B3's rolling effect: ServerPlayer::RollResearchEvent draws one NextFloat at the top of the callback - modelled + RNG region declared, but the branch never fired here so NOT behaviourally proven. KNOWN GAP: ours posts no events and the player event list is still undeclared, so the clean compare bounds the economy fields only and the replace/oracle pass was deliberately run on a no-completion turn (weak check) |
| P2-B3 ProcessResearch (behavioral, RNG) | phase2 | mapped | high | 85% | 2026-09-08 | LIVE, PARTIAL PASS: 15 calls compared, 13 zero-divergence; RNG post-state matched 14/15 incl. every roll (validates MT19937 + draw mapping + odds together). 2 divergences are the declared SetResearched boundary. ORACLE FAILS by exactly one item across 40,300: an unposted EVENT_RESEARCH_OVERBUDGET - compare was blind because the event list was never a declared region. fpu_cw=0x127f => 53-bit double, x87 question SETTLED. No Zuul in the save: double roll still disassembly-only |
| P2-B1 ComputeBudget (behavioral) | phase2 | verified | med | 75% | 2026-09-08 | LIVE + GREEN on declared regions (4,437 compares, 0 divergences; replace-mode oracle byte-identical). Corrections: out-param int[22]; researchMoneyKept gated on a research target. QUALIFIED BY THE HARNESS AUDIT: replace mode runs the ORIGINAL A SECOND TIME to harvest slots -> a real per-turn double effect (ComputeOutput repairs ships in orbit) that no region reaches - UNRESOLVED; the over-budget int at Budget+0x64 was captured only as an argument and args are never compared. Golden trace needs recapture with guards. Coverage gaps: 8 slots always 0 (expenses/aid/debt) . RECAPTURED WITH GUARDS 2026-09-08 (lane R): 4284 compared, 0 diverged, exit 0; `budget_object` guard 0 undeclared writes, so Budget+0x64 never took a different value (weaker than 'never written'). COVERAGE IS NARROWER THAN THE NUMBER: only **20 distinct (player, output) states** in 4284 calls (4278 are the UI polling one player; the turn pass is ~8 calls), and **13 of the 22 slots are 0 on EVERY call** - including 5 of the 6 declared input-boundary slots, which therefore prove nothing twice over. 8 modelled slots ever carry a value |
| P2-B2 OnTechResearched (behavioral) | phase2 | verified | high | 80% | 2026-09-08 | LIVE: 3 completions compared, 0 divergences (incl. one tech absent from the effects table = tail-only, the case an early return would have broken). float32 CONFIRMED ON THE GAME bit-for-bit (con_mod 1.0->0.899999976 = (float)(x+(double)0.1f)). Found B3's rolling effect: ServerPlayer::RollResearchEvent draws one NextFloat at the top of the callback - modelled + RNG region declared, but the branch never fired here so NOT behaviourally proven. KNOWN GAP: ours posts no events and the player event list is still undeclared, so the clean compare bounds the economy fields only and the replace/oracle pass was deliberately run on a no-completion turn (weak check) . RECAPTURED WITH GUARDS 2026-09-08 (lane R): 2 calls, 0 divergences; the `player` guard reports **player+0x2b0 (EvNxID) on BOTH calls**, so 'ours posts no events' is now measured, not assumed. **RollResearchEvent branch FIRED and matched** (see its row). Also caught an undeclared `vector<ObservedTech>` append at player+0x274. Reachable only after 5 End Turns from ref-turn2 |
| P2-B3 ProcessResearch (behavioral, RNG) | phase2 | mapped | high | 85% | 2026-09-08 | LIVE, PARTIAL PASS: 15 calls compared, 13 zero-divergence; RNG post-state matched 14/15 incl. every roll (validates MT19937 + draw mapping + odds together). 2 divergences are the declared SetResearched boundary. ORACLE FAILS by exactly one item across 40,300: an unposted EVENT_RESEARCH_OVERBUDGET - compare was blind because the event list was never a declared region. fpu_cw=0x127f => 53-bit double, x87 question SETTLED. No Zuul in the save: double roll still disassembly-only . RECAPTURED WITH GUARDS 2026-09-08 (lane R): the oracle gap is now a COMPARE DIVERGENCE - `side.events.after.v.next_id orig=4 ours=3` on call 0, its only divergent field, with node[144] progress 2879->5768 and flag 1->2 both reproduced and the single RNG draw identical. 15 calls over 5 turns: 3 diverged, **RNG 15/15** (better than the original 14/15 - no tech-effect draw in this session). Guards on the two completion calls map SetResearched: ConMod[0..2]/OutMod/PopMod, ResTNm, TechTree+0x20 order counter, and the undeclared otch vector |
| RNG signatures (Ghidra) | meta | verified | high | 100% | 2026-09-08 | Seed/Twist/NextFloat/NextInt verified; draw = y/(2^32-1); NextInt [0,n] inclusive; lazy twist; left@+0x9c4. RUNTIME CONFIRMED: fpu_cw=0x127f (53-bit double, round-nearest) - our next_float model is right, float_from_pc24 is an unused contingency |
| engine: game/effects | engine | verified | high | 100% | 2026-09-08 | merged: TechId enum (196 slots @10000+i), 44 ids with typed strategic effects, species flag bits, ApplyTechEffect; 254 checks |
| VM140 exclusivity (lab rule) | meta | verified | high | 100% | 2026-09-08 | one agent at a time. Holder: R-recapture (window open 2026-09-08). QUEUE: empty. Windows Update now DISABLED/paused on the VM (was rebooting mid-run). Non-holders build /srv/re-lab/build/sots-engine-<lane>, stage dist-<lane>, deploy C:\SOTS\shimdist-<lane> |
| VM140 exclusivity (lab rule) | meta | verified | high | 100% | 2026-09-08 | one agent at a time. Holder: **FREE** (R-recapture released 2026-09-08 03:00 local). QUEUE: empty. VM left at the MAIN MENU, `hooks=trace`, build `recap-7584bad-20260908T0615Z` deployed from `C:\SOTS\shimdist-recap` (that dist also carries `shim.cfg.recap{trace,b3,b1,misc}`). Windows Update DISABLED/paused on the VM. Non-holders build /srv/re-lab/build/sots-engine-<lane>, stage dist-<lane>, deploy C:\SOTS\shimdist-<lane>. GOTCHA found this window: after `schtasks /Run /TN SOTS` the main menu can take >60 s - SCREENSHOT AND VERIFY before clicking, or the click path lands in Credits |
| Zuul double-roll (behavioural) | verify | backlog | — | 0% | 2026-09-08 | CONFIRMED NEEDED: ref-turn2 has only species 0 and 2, so the double roll is verified by disassembly + host tests only. Needs one compare from a species-5 save; the check is just that `left` drops by 2 not 1 |
| budget tail coverage (expenses/aid/debt) | verify | backlog | — | 0% | 2026-09-08 | 8 ComputeBudget slots were always 0 in ref-turn2 (no sliders, no aid, no debt, no handicap). Need a save with expense sliders, a debtor and a research-aid treaty to exercise ExpenseTotal + the aid/bonus tail |
| budget tail coverage (expenses/aid/debt) | verify | backlog | — | 0% | 2026-09-08 | 8 ComputeBudget slots were always 0 in ref-turn2 (no sliders, no aid, no debt, no handicap). Need a save with expense sliders, a debtor and a research-aid treaty to exercise ExpenseTotal + the aid/bonus tail . CONFIRMED AND WORSE 2026-09-08 (lane R, 4284 calls): **13 of 22 slots are 0 on every call** - tradeIncome, shipCarriedPop, secondaryManager, bonusIncome, systemIncomeNeg, debtInterest, construction, expenses, researchMoneyGiven, savingsGiven, tra, researchPointsGiven, trp |
| hook GetDifficultyMods | meta | backlog | — | 0% | 2026-09-08 | B1 derived the two difficulty rows from trace values (AI maintenance divisor 3, research x1.5) instead of snapshotting them; hook it properly so they stop being constants |
| section-loader compare crash | verify | backlog | — | 0% | 2026-09-08 | SectionDictionary compare crashes the engine while the identical weapon path succeeds -> fault is in re-running LoadSection, not the manifest reader. Next boundary: hook LoadSection itself. docs/M2.md has 3 ranked leads |
| P2-B4 colony + movement (behavioral) | phase2 | verified | high | 100% | 2026-09-08 | LIVE: 36 calls compared, 0 divergences, tracecmp exit 0. Scout's headline: RNG left-delta 0 and mt hash identical on ALL 28 systems (only ProcessRebellion draws; none fired) - fpu_cw 0x127f confirmed. One real fleet move reproduced bit-for-bit; PlanFleetMovement's schedule matched. 3 prototypes + 6 helpers VERIFIED and written back to Ghidra; 22 formula corrections. THREE hook bugs found by reading the trace, not the verdict (stale args from describe_args-before-regions; StrategyServer has TWO bases 4 bytes apart; off_Fleets was a Ghidra-base number used as raw) - each would have given a clean compare that checked nothing. COVERAGE IS THIN: only 3 owned systems, 1 moving fleet, gate traffic all-zero; bats2, plague, rebellion, slaves, terraform, jumps, arrivals untested. No replace mode (input boundary). VM released: main menu, hooks=trace, build b4-fix2-20260908T0615Z |
| P2-B4 colony + movement (behavioral) | phase2 | mapped | med | 70% | 2026-09-08 | LIVE: 36 calls compared, 0 divergences, tracecmp exit 0. Scout's headline: RNG left-delta 0 and mt hash identical on ALL 28 systems (only ProcessRebellion draws; none fired) - fpu_cw 0x127f confirmed. One real fleet move reproduced bit-for-bit; PlanFleetMovement's schedule matched. 3 prototypes + 6 helpers VERIFIED and written back to Ghidra; 22 formula corrections. THREE hook bugs found by reading the trace, not the verdict (stale args from describe_args-before-regions; StrategyServer has TWO bases 4 bytes apart; off_Fleets was a Ghidra-base number used as raw) - each would have given a clean compare that checked nothing. COVERAGE IS THIN: only 3 owned systems, 1 moving fleet, gate traffic all-zero; bats2, plague, rebellion, slaves, terraform, jumps, arrivals untested. No replace mode (input boundary). VM released: main menu, hooks=trace, build b4-fix2-20260908T0615Z . **QUALIFIED 2026-09-08 by lane R:** with 45 MoveFleet calls over 5 turns instead of 7 over 1, **8 of the 15 moving calls diverge by 1 ULP of position**. B4's clean verdict was a ONE-SAMPLE verdict - fleet 34 (its only mover) still matches bit-for-bit; the bug appears once other fleets move. ProcessTurn recaptured at 140 calls / 0 divergences, but only `ntdev` (15x) and `rcex` (7x) ever moved: everything else was byte-identical on all 140, so the verdict bounds two counters. Guard found the AI home system's fleet vector growing every turn (the build queue emitting a ship) |
| harness gap: undeclared side-effect lists | verify | verified | high | 100% | 2026-09-08 | FIXED STRUCTURALLY (engine 3f0721f+): compile-time-required Coverage on every descriptor (a hook without one does not compile); Guard regions that catch AND localise undeclared writes (names player+0x2b0, not 'the hash moved'); replace mode now emits records; tracecmp prints coverage on every report + --strict-coverage. A hook claiming 'complete' while a guard caught an undeclared write now counts as a DIVERGENCE. Audit found 23 undeclared side effects: docs/harness-audit.md |
| RollResearchEvent draw (behavioural) | verify | backlog | — | 0% | 2026-09-08 | ServerPlayer::RollResearchEvent draws one NextFloat when the completing def is the current target and +0x3b4 is set. Region declared, ours reproduces it, but the branch never fired on ref-turn2 - needs a workload that sets research_roll_pending |
| golden-trace recapture (post-guards) | verify | in-progress | — | 0% | 2026-09-08 | region sets changed, so goldens must be recaptured: ProcessResearch (expect NEW divergences - the event defect becoming visible), ComputeBudget (verdict should stay 0), and first captures for OnTechResearched / ServerSystem::ProcessTurn / MoveFleet |
| RollResearchEvent draw (behavioural) | verify | verified | high | 100% | 2026-09-08 | **CLOSED by lane R.** On turn 6 (`IND_TRKSTL`, tech 10094) `research_roll_pending` was true going in; the original drew exactly one word (rng `left` 375->374, `next_index` 249->250) and cleared the flag, and `ours` reproduced both bit-for-bit on the scratch generator. 0 divergences on that call. Needs 5 End Turns from ref-turn2 to reach - the reference turn has no completion at all |
| golden-trace recapture (post-guards) | verify | verified | high | 100% | 2026-09-08 | DONE on the live game, build `recap-7584bad-20260908T0615Z` (NO source change needed - the audit's machinery did all of it). **B3 ProcessResearch: the defect is VISIBLE** - `side.events.after.v.next_id orig=4 ours=3`, one divergent call of 3 on the reference turn and its ONLY divergent field; bit-for-bit the `EvNxID 4->3` that previously needed a 609 KB save diff. Over 5 turns 15 calls / 3 diverged, **RNG matched 15/15**, and the two completion calls miss TWO event ids each. **B1 ComputeBudget: verdict held** - 4284 compared, 0 diverged, exit 0, `budget_object` guard caught 0 undeclared writes (Budget+0x64 never changed value). **MoveFleet: 8 of 45 diverge by 1 ULP of position** (new; see its own row). First guarded captures for OnTechResearched (2 calls), ServerSystem::ProcessTurn (140 calls) and MoveFleet (45). Guards mapped SetResearched live (ConMod[0..2], OutMod, PopMod, ResTNm, TechTree+0x20 order counter) and found an UNDECLARED `vector<ObservedTech> otch` append at player+0x274. Oracle held on every run's first End Turn. Report `findings/subsystems/golden-trace-recapture.md`; engine `docs/R-recapture.md`; traces `verify/traces/recap-*`, reports `verify/results/compare/recap-*`. sots-engine branch `wip/recapture` e50d5e5 (merged with main 82ef52f; ctest 32/32, clean-room OK) |
| MoveFleet position rounding (1 ULP) | verify | backlog | — | 0% | 2026-09-08 | NEW (lane R). 8 of 45 `StrategyServer::MoveFleet` compares diverge on one position component by 1 ULP (worst 64 ULP at a near-zero result, but the ABSOLUTE error is ~1.2e-7 in every case = half an ULP of the INPUTS, not of the result). Step length right, every ship-range region matches, x/y/z each diverge on some call -> one rounding too many or too few in the position update (`sim::AdvanceAlongDirection` narrowing to float32 where the original keeps the x87 53-bit double, or the reverse). Needs pinning against the instruction stream. Evidence: `verify/traces/recap-misc-compare.jsonl` calls 42/79/80/116/118/154/156/157 |
| undeclared ObservedTech append | verify | backlog | — | 0% | 2026-09-08 | NEW (lane R). A tech completion grows `vector<ObservedTech> otch` at ServerPlayer+0x274 (all three vector words move = a realloc). Seen as an undeclared write by BOTH the ProcessResearch `player` guard and the OnTechResearched `player` guard. It is serialized ServerPlayer state and it is in NO coverage note anywhere - a third list append in the same neighbourhood as the event list. B3's replace oracle never saw it because turn 1 of ref-turn2 has no completion |
| unnamed offsets from guard hits | verify | backlog | — | 0% | 2026-09-08 | NEW (lane R). Three spans the guards report every run and no addresses.json entry names: **ServerSystem+0xd8 (1 B)** and **ServerSystem+0x238 (4 B)** - written by the AI home system on every colony turn, alongside the fleet-vector growth; **StarFleet+0xdc (1 B)** - written on every moving MoveFleet call, just past Speed (FPsp2 @0xd8). Cheap wins for the contract |
| ref-turn2 has no tech completion | meta | verified | high | 100% | 2026-09-08 | TRAP for anyone writing a workload (lane R). The documented one-End-Turn recipe produces **zero** `OnTechResearched` calls - an empty log that still passes. It takes 5 End Turns (to turn 7) to reach a completion. Also: only the FIRST End Turn is reproducible - it hashed to the oracle on all four runs and its research calls reproduce docs/B3.md exactly, but from turn 4 the AI picks a different research target than B3 recorded while the point totals stay nearly identical. Treat anything past turn 1 as *a* run, not *the* run |
| B1 replace double-run | verify | backlog | — | 0% | 2026-09-08 | ComputeBudget replace mode runs the original a second time to harvest budget slots; ComputeOutput repairs ships in orbit as a side effect, so this is a real per-turn double effect on objects no region covers. Needs a design fix (harvest without re-running, or declare+revert) |
| ReVa MCP link drop (workaround) | meta | verified | high | 100% | 2026-09-08 | The ReVa MCP client link dropped mid-session while the CT111 server stayed healthy (systemd active, :8080 listening, valid key -> 200). `tools/reva_call.py <tool> '<json>'` calls the same server over plain HTTP (initialize -> notifications/initialized -> tools/call; replies are SSE with a leading `id:` line, initialize is plain JSON). Key is NEVER stored in the repo: $REVA_KEY, else ~/.claude.json, else ssh to the CT properties file. Use this whenever mcp__plugin_ReVa_ReVa__* is unavailable |
| event posting API | subsystem | mapped | high | 90% | 2026-09-08 | RECOVERED (lane E, `findings/subsystems/events.md`). Container: `EventStorage` embedded at `ServerPlayer+0x29c` (0x1c), `EvNxID` at +0x14 = player+0x2b0 — exactly the guard's byte run. Nested `vector<TurnEvents{int EvTurn; vector<PlayerEvent>}>`, record 0x74 B, tags `EvEID EvDsc EvMsg EvImg EvLoc EvPos EvAct EvCID`; layout confirmed field-by-field against turn3-state.sav, which CONTAINS the overbudget record. Entry point `int __thiscall EventStorage::PostEvent(this, string BYVAL, string BYVAL, obj*, Vector3*, turn, const char* img, int act)` 0x008862b0 RET 0x4c — **161 call sites in 113 functions, the whole sim's event API**. B3 defect fully explained: 0x00587b97, in the completion-roll-FAILED branch under `!wasDone && nowDone && owner`. 3 note corrections (EvPos is FLT_MAX not inf; the save array is turn-bucketed not flat; TECHS_UNLOCKED has no parent clause). 56 entries in addresses.json; 11 prototypes + 13 labels + 12 comments + 2 structs written back to Ghidra. Engine: `sots-engine` branch `wip/events` a7348be, `src/game/events` + 112 checks, ctest 32/32. NOT YET WIRED INTO A HOOK — see `docs/E-events.md` for the proposed region/Coverage change |

View file

@ -0,0 +1,291 @@
# Golden-trace recapture with guards (lane R, 2026-09-08)
Every golden trace went stale when the harness grew `Coverage` + guard regions
(`sots-engine` 3f0721f+, `docs/harness-audit.md`). This is the recapture on the live game.
**Read the coverage, not the verdict.** Two of the five hooks now report divergences that
their previous "verified" rows did not, and both are real.
* Build: `recap-7584bad-20260908T0615Z` (sots-engine `7584bad` + four `shim.cfg.recap*`
variants; no source change). Staged `C:\SOTS\shimdist-recap`, VM140.
* Workload: `ref-turn2.sav` → Launch → **End Turn**. The B3/misc runs continue to turn 7
(5 End Turns) to reach a research completion, which turn 1 does not contain.
* Oracle held on every run's first End Turn: `(Autosave EndTurn).sav` = `bb4fd9ac89f41e3b…`,
`(Autosave).sav` = `978041acd168b56e…` — trace and compare mode do not perturb the game.
* Artefacts: `verify/traces/recap-*.jsonl[.gz]`, reports in `verify/results/compare/recap-*`,
shim logs + screenshots in `verify/results/shim/recap-*`.
## 1. Headline table (coverage first)
| hook | run | calls | compared | diverged | exit | coverage line |
|---|---|---|---|---|---|---|
| `TechTree::ProcessResearch` | `recap-b3-compare` (t2→t7) | 15 | 15 | **3** | **1** | 15 guarded calls, **13 undeclared writes in 2 calls**, 0 unstated, 0 contradicted |
| `TechTree::ProcessResearch` | first End Turn only | 3 | 3 | **1** | **1** | 3 guarded calls, 0 undeclared writes |
| `ServerPlayer::ComputeBudget` | `recap-b1-compare` | 4284 | 4284 | 0 | 0 | 4284 guarded calls, **0 undeclared writes** |
| `ServerPlayer::OnTechResearched` | `recap-misc-compare` | 2 | 2 | 0 | (1)¹ | 2 guarded calls, **6 undeclared writes in 2 calls** |
| `ServerSystem::ProcessTurn` | `recap-misc-compare` | 140 | 140 | 0 | (1)¹ | 140 guarded calls, **15 undeclared writes in 6 calls** |
| `StrategyServer::MoveFleet` | `recap-misc-compare` | 45 | 45 | **8** | (1)¹ | 45 guarded calls, **42 undeclared writes in 15 calls** |
| all five | `recap-golden-trace` (trace mode) | 3630 | — | — | 0 | 3630 guarded calls, 9 undeclared writes in 4 calls |
¹ the three misc hooks share one log; its exit code 1 comes from `MoveFleet` alone.
## 2. `TechTree::ProcessResearch` — the over-budget event is now visible
**Yes. The defect became a divergence, on exactly the call the audit predicted.**
On the first End Turn, one call of three diverges, and in exactly one field:
```
### Game::TechTree::ProcessResearch: first 1 of 1 divergent call(s)
- call_id 0 (recap-b3-compare.jsonl:2)
side.events.after.v.next_id [exact] orig={"t":"i32","v":4} ours={"t":"i32","v":3}
```
That is bit-for-bit the same signal B3's save-file oracle produced (`docs/B3.md`:
`EvNxID 4 → 3`, one `EVENT_RESEARCH_OVERBUDGET` missing across 40,300 items). The 609 KB
save diff has been replaced by one line in the compare report.
Corroboration that this is the right branch (lane E: the post site is 0x00587b97, reachable
only from the completion-roll-**failed** path, gated `!wasDone && nowDone && owner`, and it
sets `node->flag = 2` in the same branch):
| region | before | after (orig) | after (ours) |
|---|---|---|---|
| `node[144].progress` | 2879 | 5768 | 5768 ✓ |
| `node[144].flag` | 1 | **2** | **2** ✓ |
| `events.next_id` (= `ServerPlayer+0x2b0`, `EvNxID`) | 3 | **4** | **3** ✗ |
| `rng` (`left` 432→431, `mt[624]` hash, `next_index` 192→193) | | | identical ✓ |
| `overbudget` (the caller's `int*`) | 0 | 0 | 0 ✓ |
So the node arithmetic, the flag and the single RNG draw are all reproduced; the *only*
thing missing is the event. Alloc for that call was `{tech 144 (IND_Waldo), 2889 points}`,
species 2 — B3's call 0 exactly.
### Across five turns: 15 calls, 3 divergent
| call | alloc | draws | events `next_id` orig → ours | guard hits |
|---|---|---|---|---|
| 0 | {144, 2889} | 1 | **4 → 3** | none |
| 1, 2 | {90,0}, {9,0} | 0 | 0 → 0 | none |
| **3** | {144, 2898} | 0 (ceiling ⇒ guaranteed) | **7 → 5** (two events) | 9 |
| 4, 5 | zero-spend | 0 | 0 → 0 | none |
| 6 | {9, 3064} | 1 (rolled, failed) | 8 → 8 | none |
| 7, 8 | zero-spend | 0 | 0 → 0 | none |
| **9** | {9, 3074} | 0 | **12 → 10** (two events) | 4 |
| 10, 11 | zero-spend | 0 | 0 → 0 | none |
| 12 | {18, 3087} | 1 (rolled, failed) | 14 → 14 | none |
| 13, 14 | zero-spend | 0 | 0 → 0 | none |
* **RNG matched on 15 of 15** (`mt[624]` hash, `left` and `next_index` identical on every
call) — stronger than B3's 14/15, because in this session no tech-effect callback drew.
* Calls 3 and 9 are the completions. Their non-`events` divergences are exactly the
`SetResearched`-owned fields B3 declared out of scope (`cost_rp`/`state`/`turn_available`
on unlocked children; `ours` leaves them at `INT_MAX`/0/−1). Nothing new there.
* Note the dedup rule (lane E): a duplicate post inside a turn bucket returns the existing
id **without** advancing `EvNxID`, so `next_id` not moving is not proof nothing was posted.
Calls 6 and 12 rolled and failed *without* completing by cost, so no post is expected there.
### What the guards mapped: `SetResearched`, measured
This is the "cheapest map of what `SetResearched` actually touches" the audit asked for.
Undeclared byte runs inside `guard:player` / `guard:tree_header` on the two completion calls,
resolved against the recovered `ServerPlayer` table:
| offset | len | field | call 3 | call 9 |
|---|---|---|---|---|
| `player+0x10c/0x110/0x114` | 3 each | `ConMod[0..2]` (construction cost per hull class) | ✓ | |
| `player+0x124` | 3 | `OutMod` (industrial output multiplier) | ✓ | |
| `player+0x130` | 3 | `PopMod` | | ✓ |
| `player+0x274 / 0x278 / 0x27c` | 3 each | **`vector<ObservedTech> otch`** — all three vector words move (a realloc) | ✓ | ✓ (+0x278 only, 1 byte) |
| `player+0x294` | 4 | `ResTNm` — the current research target pointer is cleared | ✓ | ✓ |
| `tree_header+0x20` | 1 | **the tree's completion-order counter** | ✓ | ✓ |
Two things worth carrying forward:
1. `tree_header+0x20` is harness-audit row 9, confirmed on the game: the per-node `order`
word is compared but the counter it is read from moves outside every Result region.
2. **`otch` (`vector<ObservedTech>`) grows on a tech completion, and no coverage note in
either B2 or B3 mentions it.** It is a third list append in the same neighbourhood as the
event list, and it *is* save state (`ServerPlayer` serializer). B3's replace-mode oracle
never saw it because turn 1 contains no completion.
3. The `player+0x10c…0x124` hits are precisely `OnTechResearched`'s own declared regions
(§4) — a clean cross-check that the guard is reporting the callee, not noise.
## 3. `ServerPlayer::ComputeBudget` — verdict held, and the guard is quiet
**4284 calls, 4284 compared, 0 divergences, `tracecmp` exit 0.** The `budget_object` guard
(Budget+0x00..0x68) caught **0 undeclared writes in 4284 calls**. In particular the
over-budget int at **`Budget+0x64`** (harness-audit row 11 — the word B1 captured only as an
argument, and arguments are never compared) **never changed value on any call**. That does
not prove the original never writes it, only that it never writes a *different* value here.
Coverage, honestly:
* **Only 20 distinct `(player, budget-output)` states exist across the 4284 calls.** 4278 of
the calls are the strategy-map UI polling the human player (one pointer accounts for 4115
of them); the End-Turn pass itself is ~8 calls, one per player in server order. The
headline number is repetition, not evidence.
* **13 of the 22 named slots are 0 on every single call**: `tradeIncome`, `shipCarriedPop`,
`secondaryManager`, `bonusIncome`, `systemIncomeNeg`, `debtInterest`, `construction`,
`expenses`, `researchMoneyGiven`, `savingsGiven`, `tra`, `researchPointsGiven`, `trp`.
The board's "budget tail coverage" backlog row is confirmed, and it is worse than stated:
five of the six *input-boundary* slots (1,2,3,4,7,11 = `systemIncomePos`, `tradeIncome`,
`shipCarriedPop`, `secondaryManager`, `systemIncomeNeg`, `construction`) are also always
zero, so they prove nothing twice over. Only `systemIncomePos` carries a value.
* Nine slots ever moved: `savings`, `systemIncomePos` (input), `savingsInterest`,
`maintenance`, `researchMoneyKept`, `available`, `researchMoney`, `researchPoints`,
`totalResearchPoints`. **Eight of those are genuinely modelled.** That is the real width
of the B1 result.
* Cross-check with B3: the End-Turn call for the AI Tarkas player yields
`researchPoints = 2889`, which is exactly the allocation `ProcessResearch` received.
* The B1 replace-mode double-run defect was **not** exercised: this lane ran compare only.
## 4. `ServerPlayer::OnTechResearched` — first guarded capture; the roll branch finally fired
**2 calls, 2 compared, 0 divergences.** The workload had to run to turn 7; the reference
turn contains no completion at all, which is why B2 never captured this hook on it.
| call | tech | branch reached | declared regions the original moved |
|---|---|---|---|
| 71 | 10001 `IND_Waldo`, node 144, `silent=false` | economy modifiers | `modifiers` (`con_mod[0..2]` 1.0 → 0.899999976), `research_target` (ptr → null) |
| 147 | 10094 `IND_TRKSTL`, node 142 | **design-option mask + the pending research roll** | `design_masks` (`b` 64 → 2097216), `research_target`, `roll`, **`rng`** |
**The `RollResearchEvent` draw is now behaviourally verified** — the board's backlog row.
On call 147 `research_roll_pending` was `true` going in; the original drew exactly one word
(`left` 375 → 374, `next_index` 249 → 250) and cleared the flag, and `ours` reproduced both
on the scratch generator bit-for-bit. That closes "region declared, ours reproduces it, but
the branch never fired".
Guard `player` (0x3e0 span) findings — **B2's known gap is now measured, not assumed**:
| offset | len | field | calls |
|---|---|---|---|
| `player+0x2b0` | 1 | **`EvNxID`** (`EventStorage+0x14`, `EventStorage` embedded at `ServerPlayer+0x29c`) | both |
| `player+0x274/0x278/0x27c` | 3 | `vector<ObservedTech> otch` — the vector grew | 71 |
| `player+0x278` | 1 | same vector, end pointer only | 147 |
So the completion events (`EVENT_RESEARCH_COMPLETE` / `_UNDERBUDGET` / `_TEMPERANCE`) are
confirmed to post on **every** completion, and `ours` posts none. The guard names the byte
rather than leaving it to a save hash that `docs/B2.md` gotcha 4 explicitly told readers to
ignore.
Not exercised at all: `silent = true`, a Zuul boarding-pod grant, a plague cure, a
node-bore allocation, the temperance sweep, the arcology re-evaluation, or any of the
writes to other objects. **Two calls, two branches. This is a thin capture.**
## 5. `ServerSystem::ProcessTurn` — first guarded capture
**140 calls (28 systems × 5 turns), 140 compared, 0 divergences.**
The coverage is the finding:
* **Only two declared regions ever moved in 140 calls**: `ntdev` (15 times) and `rcex`
(7 times). `infra`, `ibon`, `pbon`, `tres`, `haltv`, `bats2`, `bats_mask`, `rcex_mask`
and `rng` were byte-identical before and after on **every one of the 140 calls**. A
0-divergence verdict here means "two integer counters agreed and nothing else moved" —
it does not exercise the decay, the bonus applies, or the battle countdown.
* `rng` unchanged on all 140 confirms B4's headline over five turns: no colony drew a word,
i.e. no rebellion fired. `fpu_cw = 0x127f` on every call.
* Owners: 3 owned systems (`owner` 0, 1, 7) × 5 turns = 15 calls; the other 125 are unowned.
Guard `system` (0x2d8 span) hits, 15 writes on 6 calls:
| offset | len | plausible field | when |
|---|---|---|---|
| `system+0x1a4` | 12 | inside `Pop2` (civilian `Population`) at 0x1a0 — the population-group vector's three words | sys 4, 15, 16 on **turn 1 only** |
| `system+0x170` | 1 | inside `Fleets` (`vector<StarFleet*>` at 0x16c) — the end pointer | sys 15, every turn |
| `system+0xd8` | 1 | **unnamed in the contract** | sys 15, every turn |
| `system+0x238` | 4 | **unnamed in the contract** | sys 15, every turn |
System 15 is the AI's home world and it emits one new fleet per turn (see §6: fleets 50, 66,
98, 114 appear one per turn) — so `system+0x170` is the build queue pushing the new fleet
into the system's fleet vector, harness-audit row 14 observed live. `+0xd8` and `+0x238` are
unidentified and worth an addresses.json entry.
**The morale-event append (audit row 3) never fired** — no addiction on this save — so that
risk is still unmeasured, not cleared.
## 6. `StrategyServer::MoveFleet` — 8 of 45 diverge; B4's clean verdict was a one-sample verdict
**45 calls, 45 compared, 8 divergent, `tracecmp` exit 1.** Every divergence is a 1-ULP
position component.
```
- call_id 42 side.pos.after.v.y [exact] orig=3.1515913 ours=3.15159106
- call_id 79 side.pos.after.v.y [exact] orig=1.58417809 ours=1.58417821
- call_id 116 side.pos.after.v.y [exact] orig=0.0167649984 ours=0.0167651176
```
Structure of the run: 30 of the 45 calls are the six waypointless fleets
(`wpt_type = -1`, early-out, no guard hit). **15 calls do real work and every one of them is
`wpt_type = 1`** (a straight run at `speed 2 × dt 1.0`). Types 2/3/4/5 never occurred, so
node-line travel, node routes, gate teleports and the probabilistic jump remain untested,
and the generator never moved in this hook.
Of the 15 moving calls, ULP deltas (orig − ours) on the position:
| call | fleet | Δx | Δy | Δz |
|---|---|---|---|---|
| 6, 41, 78, 115, 154 | 34 | 0 | 0 | 0 / 0 / 0 / 0 / **+4** |
| 42, 79, 116 | 50 | 0 | **+1 / −1 / −64** | 0 |
| 80, 117 | 66 | 0 | **+1** / 0 | 0 |
| 118, 157 | 98 | 0 | **+1 / −1** | 0 / **+1** |
| 155, 156, 158 | 50/66/114 | 0 / **−1** / 0 | 0 / **−1** / 0 | 0 |
The 64-ULP case is a near-zero result (0.0167…): the **absolute** error is ~1.2e-7 in every
case, i.e. half an ULP at the magnitude of the *inputs* (y ≈ 1.6–4.7), not of the result.
That is the signature of one rounding too many (or too few) in the position update —
`sim::AdvanceAlongDirection` narrowing an intermediate to float32 where the original keeps
the x87 53-bit double, or the reverse. It is not a formula error: x, y and z all diverge in
some call, the step length is right, and every ship-range region matched.
**Why B4 missed it:** B4 had exactly one moving call, fleet 34 — and fleet 34 matches
bit-for-bit on four of its five moves here too. The bug only shows once other fleets start
moving. A reader of B4's trace would have seen "1 straight move, verified"; the verdict said
"MoveFleet verified".
Guard `fleet` (0x120 span), 42 writes on the 15 moving calls, resolved against the
`StarFleet` table:
| offset | len | field | count |
|---|---|---|---|
| `fleet+0x10c` | 1 | `Flags` (bit 0x100 = held this turn) | 15 — every moving call |
| `fleet+0xdc` | 1 | unnamed, just past `Speed` (`FPsp2` @0xd8) | 13 |
| `fleet+0xa0` | 4 | **`Location*`** — the fleet's Location object pointer is replaced | 8 |
| `fleet+0xcc` | 1 | `Waypoints` vector `last` pointer — **a waypoint was popped** | 2 |
| `fleet+0xdb` | 2 | straddles `Speed`/0xdc | 2 |
| `fleet+0xe0` | 26 | `FPogn2` @0xe0 … `DestPos` @0xec — the destination position rewrite | 2 |
The two calls carrying `+0xcc` and `+0xe0` (115 and 155) are the **two arrivals** in the
whole run — and both compared clean, because none of the arrival machinery is declared. B4
said "an arriving call was simply clean"; that is still true and still means nothing.
## 7. What a reader of the trace would have noticed that no verdict did
* `ProcessResearch` fires **3 times per End Turn**, not 15 — B3's 15 was five turns. Turn 1
of `ref-turn2` contains **no tech completion at all**, which is why `OnTechResearched`
produced zero records on the reference workload and B2's three completions must have come
from a longer run. A lane that captures `OnTechResearched` on the documented one-turn
recipe will get an empty log and a green report.
* **The multi-turn continuation is not reproducible across sessions.** Turn 1 is
byte-identical to the oracle every time, and calls 0–3 reproduce `docs/B3.md` exactly
(`{144,2889}`, `{90,0}`, `{9,0}`, `{144,2898}`), but from turn 4 the AI picks a different
research target than B3 recorded (`{9,3064}` here vs `{142,3064}` there, `{18,3087}` vs
`{9,3086}`) while the point totals stay nearly identical. Anything past the first End Turn
should be treated as *a* run, not *the* run.
* `ComputeBudget` is called ~4280 times per session by the UI and ~8 times by the turn pass.
Any "N compares" headline for that hook is dominated by UI polling of one player.
## 8. Still owed
* The count-only `events` region for `ProcessResearch` recommended by lane E's
`docs/E-events.md` (converts audit row 1 from "known defect" to "checked"). Not wired —
this lane's captures predate that module and re-capturing a good trace was not worth it.
* `MoveFleet`'s 1-ULP position divergence needs the rounding in `AdvanceAlongDirection`
pinned against the instruction stream.
* `ServerSystem+0xd8` and `ServerSystem+0x238` are unnamed and are written every turn by the
AI home system. `StarFleet+0xdc` likewise, on every move.
* `vector<ObservedTech> otch` (`ServerPlayer+0x274`) is undeclared everywhere and is save
state.
* Nothing here exercises: replace mode (any hook), a Zuul double roll, a rebellion, an
addiction sweep, a plague, gate traffic, waypoint types 2–5, `silent = true` completions,
the budget expense/aid/debt tail, or the `Budget+0x64` over-budget write.

View file

@ -0,0 +1,470 @@
{
"coverage_contradicted": [],
"coverage_unstated": [],
"format": 1,
"hooks": {
"Game::ServerPlayer::ComputeBudget": {
"calls": 4284,
"compared": 4284,
"coverage": {
"checked_regions": [
"budget",
"inputs",
"research_alloc"
],
"guarded_calls": 4284,
"guards": [
"budget_object"
],
"spans": {
"compare": []
},
"state": "partial",
"undeclared_calls": 0,
"undeclared_writes": 0,
"unmodelled": [
{
"mitigation": "declared input boundary; see budget_inputs.h",
"risk": "high",
"what": "slots 1, 2, 3, 4, 7 and 11 are produced by callees this milestone does not model (per-system output, trade, ship-carried population, a second manager, the build-queue spend)",
"why": "they are copied out of the original's own output and back into the same slots, so they match BY CONSTRUCTION and prove nothing"
},
{
"mitigation": "guard:budget_object does not reach the ships; unverified",
"risk": "high",
"what": "ServerSystem::ComputeOutput repairs damaged ships in orbit",
"why": "replace mode runs the original a second time on a scratch Budget to harvest the six unmodelled slots, so that repair happens TWICE per turn in replace mode and nothing in the trace would show it"
},
{
"mitigation": "",
"risk": "medium",
"what": "the difficulty-mods row from StrategyServer::GetDifficultyMods",
"why": "not reachable from a ServerPlayer, so the two relevant entries are fitted constants measured from the B1 trace rather than snapshotted inputs"
},
{
"mitigation": "",
"risk": "low",
"what": "the research-allocation vector's heap block",
"why": "only the element count is compared; the three words are heap pointers the default policy ignores"
}
],
"verdict": "partial",
"why": ""
},
"diffs": [],
"diverged": 0,
"diverged_call_ids": [],
"errors": 0,
"modes": {
"compare": 4284
}
}
},
"inputs": [
"/tmp/claude-1000/-home-alex/ec8e34f8-af37-4ef2-a309-ed6a15293097/scratchpad/recap-b1-compare.jsonl"
],
"invalid": [],
"kind": "report",
"meta": [
{
"build": "recap-7584bad-20260908T0615Z",
"exe_sha256": "970b7de729956a53094c7eb98aba4270aee98e2fed5daf0d39e290013c90c841",
"format": 1,
"hooks": {
"Game::SectionDictionary::SectionDictionary": {
"coverage": {
"state": "partial",
"unmodelled": [
{
"mitigation": "see docs/M2.md; compare mode for this hook is not safe to run",
"risk": "high",
"what": "LoadSection registers each section with the string table and the live TechTree, and may append to the dictionary's own vector",
"why": "M3 scope; ours delegates to the game's LoadSection after the original has already built all 885 definitions, so the second pass registers duplicates -- the leading hypothesis for this hook's compare-mode crash"
},
{
"mitigation": "",
"risk": "medium",
"what": "post-load validation pass over every definition's @-token against the string table",
"why": "runs after the loop and touches no declared region"
},
{
"mitigation": "",
"risk": "low",
"what": "allocates 885 SectionDef objects (0x3d8 bytes each) on the game heap",
"why": "they do not exist at hook entry; compared by index/species/id/token"
},
{
"mitigation": "guard:dict",
"risk": "low",
"what": "the word at dictionary+0x14",
"why": "not modelled; emitted as an ignored pointer"
},
{
"mitigation": "",
"risk": "medium",
"what": "the before-snapshot of the object is uninitialised heap",
"why": "the hook is on the constructor, so `before` is meaningless and only `after` carries information"
}
],
"why": ""
},
"ftol": 0,
"ftol_kind": "abs",
"ptr": "ignore"
},
"Game::ServerPlayer::ComputeBudget": {
"coverage": {
"state": "partial",
"unmodelled": [
{
"mitigation": "declared input boundary; see budget_inputs.h",
"risk": "high",
"what": "slots 1, 2, 3, 4, 7 and 11 are produced by callees this milestone does not model (per-system output, trade, ship-carried population, a second manager, the build-queue spend)",
"why": "they are copied out of the original's own output and back into the same slots, so they match BY CONSTRUCTION and prove nothing"
},
{
"mitigation": "guard:budget_object does not reach the ships; unverified",
"risk": "high",
"what": "ServerSystem::ComputeOutput repairs damaged ships in orbit",
"why": "replace mode runs the original a second time on a scratch Budget to harvest the six unmodelled slots, so that repair happens TWICE per turn in replace mode and nothing in the trace would show it"
},
{
"mitigation": "",
"risk": "medium",
"what": "the difficulty-mods row from StrategyServer::GetDifficultyMods",
"why": "not reachable from a ServerPlayer, so the two relevant entries are fitted constants measured from the B1 trace rather than snapshotted inputs"
},
{
"mitigation": "",
"risk": "low",
"what": "the research-allocation vector's heap block",
"why": "only the element count is compared; the three words are heap pointers the default policy ignores"
}
],
"why": ""
},
"ftol": 0,
"ftol_kind": "abs",
"ptr": "ignore"
},
"Game::ServerPlayer::OnTechResearched": {
"coverage": {
"state": "partial",
"unmodelled": [
{
"mitigation": "guard:player (EventStorage is inline at ServerPlayer+0x29c)",
"risk": "high",
"what": "posts EVENT_RESEARCH_COMPLETE / _UNDERBUDGET / _TEMPERANCE on the owner's EventStorage when !silent",
"why": "the same class of write as B3's defect, and this hook has no replace-mode oracle that could catch it: gotcha 4 in docs/B2.md says a changed save hash on a completion turn is expected and therefore not a finding"
},
{
"mitigation": "",
"risk": "high",
"what": "writes every owned system's AI flag (CCC_AIVrus / CCC_AISlv), re-evaluates the arcology civilian cap, cures addiction and clears plague across systems AND ships",
"why": "writes through pointers to other objects; compare mode must not touch live state, and no region reaches them"
},
{
"mitigation": "this is the extra draw B3 observed on a completion",
"risk": "high",
"what": "the pending plague-cure roll (ServerPlayer::RollResearchEvent)",
"why": "it draws exactly one word from the strategic generator unconditionally; running it in compare mode would consume real randomness. The two words it guards are still cleared and the record says whether it would have fired"
},
{
"mitigation": "",
"risk": "medium",
"what": "TechTree::SetResearched for the Zuul boarding-pod grant",
"why": "it would mutate the live tree, and it recurses"
},
{
"mitigation": "region:node_bore, declared only when the block already exists",
"risk": "medium",
"what": "allocates or frees the node-bore block at ServerPlayer+0x308",
"why": "ours has no allocator the game's runtime could free, so replace mode calls the game's own updater -- which means replace mode never exercises our node-bore selection at all"
}
],
"why": ""
},
"ftol": 0,
"ftol_kind": "abs",
"ptr": "ignore"
},
"Game::ServerSystem::ProcessTurn": {
"coverage": {
"state": "partial",
"unmodelled": [
{
"mitigation": "guard:system",
"risk": "high",
"what": "the addiction sweep raises MoraleEvents, which are constructed and appended to the system's capped morale history",
"why": "the same class of write as B3's defect. sim::ProcessColonyTurn does compute the morale events (ColonyTurnResult), but the hook never emits them: DescribeMoraleEvents is dead code, so they are neither compared nor logged"
},
{
"mitigation": "guard:system covers the system object only, not the other objects",
"risk": "high",
"what": "every callee: the plague pass, imperial and civilian growth, the resource debit, in-orbit refuel, slaves, rebellion and the build queue",
"why": "declared input boundary -- ProcessTurn is a dispatcher and only the words it writes itself are modelled. The callees raise EVENT_SLAVES_DEAD, EVENT_SYSTEM_REBELLION_CONTINUES, the plague events and SEBuildCompleted, create ships and bump per-player ShipRecords counters"
},
{
"mitigation": "",
"risk": "medium",
"what": "ApplyInfraBonus / ApplyPopBonus read the owner's home-system id, and the build queue writes the owning ServerPlayer",
"why": "writes through a pointer to another object; no region reaches the player"
},
{
"mitigation": "",
"risk": "low",
"what": "ProcessRebellion is the pass's only RNG consumer and its draw count is data-dependent",
"why": "the generator IS a declared region, so a moved post-state is visible and names the system whose rebellion fired -- it is reported, not modelled"
},
{
"mitigation": "",
"risk": "medium",
"what": "replace mode is refused for this hook",
"why": "our side models the dispatcher's own writes and none of the callees, so a replace run would silently skip a colony's whole turn. There is therefore no oracle layer behind the compare for this hook"
}
],
"why": ""
},
"ftol": 0,
"ftol_kind": "abs",
"ptr": "ignore"
},
"Game::StrategyServer::MoveFleet": {
"coverage": {
"state": "partial",
"unmodelled": [
{
"mitigation": "guard:fleet sees the fleet's own words; the event and the system do not",
"risk": "high",
"what": "on arrival: dispatches SEFleetArrived and runs one of three arrival handlers by destination kind (enter system / join fleet / stop at point)",
"why": "declared input boundary -- an arriving call is expected to differ in all of it, and none of it is declared, so the compare says nothing about arrivals"
},
{
"mitigation": "",
"risk": "high",
"what": "on departure: cancels every still-acting ship (with a log line each) and calls ServerSystem::FleetDeparts, which rewrites the system's ownership bits",
"why": "writes through pointers to ships and to the system"
},
{
"mitigation": "",
"risk": "medium",
"what": "the tanker top-up refuels other ships in the fleet",
"why": "the per-ship range regions would show it, but ours does not model it, so a fleet with a tanker diverges for a known reason"
},
{
"mitigation": "declared gap: docs/B4.md",
"risk": "medium",
"what": "a node-line waypoint's step comes from the stutter profile",
"why": "NodeLineStep / BuildStutterSegments are written and unit-tested but not wired in; the hook steps every waypoint type as speed x dt, so a node-line leg is knowingly mis-stepped and only its type is recorded"
},
{
"mitigation": "",
"risk": "medium",
"what": "a missed probabilistic jump scatters the fleet in a random direction",
"why": "the direction is a second draw whose mapping is not modelled; ours leaves the position alone and reports the scatter distance, so the generator region diverges by one word on a miss"
},
{
"mitigation": "",
"risk": "medium",
"what": "the route revalidation and the waypoint list itself",
"why": "declared input boundary; the waypoint vector is not a region"
}
],
"why": ""
},
"ftol": 0,
"ftol_kind": "abs",
"ptr": "ignore"
},
"Game::StrategyServer::ProcessFleetMovement": {
"coverage": {
"state": "partial",
"unmodelled": [
{
"mitigation": "",
"risk": "high",
"what": "`ours` re-reads the LIVE fleet list after the original has run",
"why": "the gate-traffic total is computed by the original at the very end of the pass, so a pre-call snapshot would diverge for the wrong reason. It breaks the compare invariant that ours never touches live memory, and it makes this hook's verdict partly self-fulfilling: the input to our arithmetic is the original's own post-move state"
},
{
"mitigation": "",
"risk": "high",
"what": "drives MoveFleet up to five times per fleet",
"why": "every undeclared effect of MoveFleet happens inside this call too; the pass schedule is recorded in the arguments but never compared"
},
{
"mitigation": "",
"risk": "high",
"what": "writes FPdpos into every fleet and clears flags 0x2 and 0x100 on every fleet",
"why": "no region covers the fleets, only the players' gate-traffic words"
},
{
"mitigation": "",
"risk": "high",
"what": "OnFleetArrived posts EVENT_FLEET_ARRIVED",
"why": "the same class of write as B3's defect, and there is no replace mode for this hook, so nothing behind the compare could catch it either"
},
{
"mitigation": "",
"risk": "medium",
"what": "the original accumulates by player->index but writes back by the player's position in the server vector, into a fixed 32-int array with no bounds check",
"why": "a real latent bug in the original that our side reproduces only while index == position; the reference save never separates them"
},
{
"mitigation": "",
"risk": "medium",
"what": "PassSchedule() is never called by the hook, and FleetSummary::targetFleetId / relation are never filled",
"why": "the header claims ours predicts the call order for a trace to check; that prediction is not actually emitted"
}
],
"why": ""
},
"ftol": 0,
"ftol_kind": "abs",
"ptr": "ignore"
},
"Game::TechTree::ProcessResearch": {
"coverage": {
"state": "partial",
"unmodelled": [
{
"mitigation": "region:events (EvNxID now diverges instead of passing silently)",
"risk": "high",
"what": "posts EVENT_RESEARCH_OVERBUDGET on the owner's EventStorage in the same branch that sets node.flag = 2",
"why": "the message text is composed from the tech name, so ours cannot synthesise it; it would have to be posted through the game's own event API. This is the defect that made a clean compare false: replace mode's autosave differed from the oracle by exactly this one event"
},
{
"mitigation": "region:events",
"risk": "medium",
"what": "posts EVENT_TECHS_UNLOCKED for nodes that became available this turn",
"why": "the trailing unlock loop makes no draw and writes no node, but it does build a names list and post an event"
},
{
"mitigation": "guard:player, guard:tree_header",
"risk": "high",
"what": "TechTree::SetResearched on completion: the turn/order stamps, the child unlock cascade, the recursive research of zero-cost children, and the owner's OnTechResearched callback",
"why": "its own milestone (B2); the callback writes live player state that compare mode must not touch, and it consumes one extra RNG word"
},
{
"mitigation": "guard:tree_header",
"risk": "medium",
"what": "bumps the tree's completion-order counter (TechTree+0x20)",
"why": "part of SetResearched; the per-node `order` word is compared but the counter it comes from was not a region"
},
{
"mitigation": "",
"risk": "low",
"what": "writes a completion line to the game log",
"why": "log text is not simulation state"
}
],
"why": ""
},
"ftol": 0,
"ftol_kind": "abs",
"ptr": "ignore"
},
"Game::WeaponDictionary::Init": {
"coverage": {
"state": "partial",
"unmodelled": [
{
"mitigation": "suspected cause of the sibling section hook's compare crash (docs/M2.md)",
"risk": "high",
"what": "LoadWeapon -> WeaponDef::ParseScript registers each weapon's name with the string table and resolves `requires` against the live TechTree",
"why": "per-file parsing is M3 scope; ours delegates to the game's own LoadWeapon, so a compare run performs the registration a SECOND time and neither the string table nor the tech tree is a declared region"
},
{
"mitigation": "",
"risk": "low",
"what": "allocates 123 WeaponDef objects (0x278 bytes each) on the game heap",
"why": "the definitions do not exist when the hook is entered, so they cannot be a before-snapshot; the dictionary region compares them by id/name/path"
},
{
"mitigation": "guard:dict",
"risk": "low",
"what": "the word at dictionary+0x14",
"why": "not modelled; emitted as an opaque pointer, which the default policy ignores -- a change is visible in a trace but never a divergence"
},
{
"mitigation": "",
"risk": "low",
"what": "writes lines to the game log for a missing manifest",
"why": "log text is not simulation state"
},
{
"mitigation": "",
"risk": "low",
"what": "std::sort tie order for equal weapon names",
"why": "msvc_sort.h replays MSVC 2010's introsort, but the shipped data has no tied names, so the tie rule is unexercised rather than verified"
}
],
"why": ""
},
"ftol": 0,
"ftol_kind": "abs",
"ptr": "ignore"
},
"Mars::GlobalConsts::LoadFile": {
"coverage": {
"state": "partial",
"unmodelled": [
{
"mitigation": "LoadAll's post-state would have to be hooked to see it",
"risk": "medium",
"what": "erases each consumed key from the caller's std::map",
"why": "the map is a LoadAll temporary; declaring a red-black tree as a region is not possible before the call. First-occurrence-wins is reproduced in game::config::apply instead, so the *effect* is modelled, the container is not"
},
{
"mitigation": "",
"risk": "low",
"what": "writes three kinds of line to the game log (unrecognised key, applied key, expected-but-not-found)",
"why": "log text is not part of the simulation state"
},
{
"mitigation": "",
"risk": "low",
"what": "opens the file through the VFS and allocates/releases a refcounted buffer",
"why": "ours performs the same two calls, so allocation behaviour matches by construction rather than by comparison"
},
{
"mitigation": "",
"risk": "low",
"what": "String slots assign through the engine's own std::string, leaking one heap block per long string in compare mode",
"why": "start-up only; documented in docs/M1.md"
}
],
"why": ""
},
"ftol": 0,
"ftol_kind": "abs",
"ptr": "ignore"
},
"Shim::SelfTest::Fill": {
"coverage": {
"state": "complete",
"unmodelled": [],
"why": "Fill writes buf[0..n) and nothing else; the whole range is a declared region"
},
"ftol": 0,
"ftol_kind": "abs",
"ptr": "ignore"
}
},
"inline_max": 256,
"started": "2026-09-08T06:40:02Z"
}
],
"totals": {
"calls": 4284,
"compared": 4284,
"coverage_contradicted": 0,
"coverage_unstated": 0,
"diverged": 0,
"guarded_calls": 4284,
"invalid_records": 0,
"undeclared_calls": 0,
"undeclared_writes": 0
},
"warnings": []
}

View file

@ -0,0 +1,21 @@
## tracecmp report: recap-b1-compare.jsonl
- build: recap-7584bad-20260908T0615Z started: 2026-09-08T06:40:02Z inline_max: 256
- calls: 4284 compared: 4284 diverged: 0 invalid records: 0 warnings: 0
- coverage: 4284 guarded call(s), 0 undeclared write(s) in 0 call(s); 0 hook(s) unstated, 0 contradicted
| hook | calls | modes | compared | diverged | errors |
|---|---|---|---|---|---|
| Game::ServerPlayer::ComputeBudget | 4284 | compare:4284 | 4284 | 0 | 0 |
### coverage
| hook | verdict | compared regions | guards | undeclared writes | unmodelled |
|---|---|---|---|---|---|
| Game::ServerPlayer::ComputeBudget | partial | budget, inputs, research_alloc | budget_object | 0 | 4 |
#### Game::ServerPlayer::ComputeBudget — not checked by this run
- (high) slots 1, 2, 3, 4, 7 and 11 are produced by callees this milestone does not model (per-system output, trade, ship-carried population, a second manager, the build-queue spend) — they are copied out of the original's own output and back into the same slots, so they match BY CONSTRUCTION and prove nothing [declared input boundary; see budget_inputs.h]
- (high) ServerSystem::ComputeOutput repairs damaged ships in orbit — replace mode runs the original a second time on a scratch Budget to harvest the six unmodelled slots, so that repair happens TWICE per turn in replace mode and nothing in the trace would show it [guard:budget_object does not reach the ships; unverified]
- (medium) the difficulty-mods row from StrategyServer::GetDifficultyMods — not reachable from a ServerPlayer, so the two relevant entries are fitted constants measured from the B1 trace rather than snapshotted inputs
- (low) the research-allocation vector's heap block — only the element count is compared; the three words are heap pointers the default policy ignores

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,47 @@
## tracecmp report: recap-b3-compare5.jsonl
- build: recap-7584bad-20260908T0615Z started: 2026-09-08T06:28:40Z inline_max: 256
- calls: 15 compared: 15 diverged: 3 invalid records: 0 warnings: 0
- coverage: 15 guarded call(s), 13 undeclared write(s) in 2 call(s); 0 hook(s) unstated, 0 contradicted
| hook | calls | modes | compared | diverged | errors |
|---|---|---|---|---|---|
| Game::TechTree::ProcessResearch | 15 | compare:15 | 15 | 3 | 0 |
### coverage
| hook | verdict | compared regions | guards | undeclared writes | unmodelled |
|---|---|---|---|---|---|
| Game::TechTree::ProcessResearch | partial | events, node[0], node[105], node[106], node[107], node[108], +270 | player, tree_header | 13 in 2 call(s) | 5 |
#### Game::TechTree::ProcessResearch — not checked by this run
- (high) posts EVENT_RESEARCH_OVERBUDGET on the owner's EventStorage in the same branch that sets node.flag = 2 — the message text is composed from the tech name, so ours cannot synthesise it; it would have to be posted through the game's own event API. This is the defect that made a clean compare false: replace mode's autosave differed from the oracle by exactly this one event [region:events (EvNxID now diverges instead of passing silently)]
- (medium) posts EVENT_TECHS_UNLOCKED for nodes that became available this turn — the trailing unlock loop makes no draw and writes no node, but it does build a names list and post an event [region:events]
- (high) TechTree::SetResearched on completion: the turn/order stamps, the child unlock cascade, the recursive research of zero-cost children, and the owner's OnTechResearched callback — its own milestone (B2); the callback writes live player state that compare mode must not touch, and it consumes one extra RNG word [guard:player, guard:tree_header]
- (medium) bumps the tree's completion-order counter (TechTree+0x20) — part of SetResearched; the per-node `order` word is compared but the counter it comes from was not a region [guard:tree_header]
- (low) writes a completion line to the game log — log text is not simulation state
- guard hits in compare mode: player+0x10c:3, player+0x110:3, player+0x114:3, player+0x124:3, player+0x274:3, player+0x278:3, player+0x27c:3, player+0x294:4, tree_header+0x20:1, player+0x130:3, player+0x278:1
### Game::TechTree::ProcessResearch: first 3 of 3 divergent call(s)
- call_id 0 (recap-b3-compare5.jsonl:2)
side.events.after.v.next_id [exact] orig={"t":"i32","v":4} ours={"t":"i32","v":3}
- call_id 3 (recap-b3-compare5.jsonl:5)
side.events.after.v.next_id [exact] orig={"t":"i32","v":7} ours={"t":"i32","v":5}
side.node[132].after.v.cost_rp [exact] orig={"t":"i32","v":10000} ours={"t":"i32","v":2147483647}
side.node[132].after.v.state [exact] orig={"t":"i32","v":2} ours={"t":"i32","v":0}
side.node[132].after.v.turn_available [exact] orig={"t":"i32","v":4} ours={"t":"i32","v":-1}
side.node[136].after.v.cost_rp [exact] orig={"t":"i32","v":16000} ours={"t":"i32","v":2147483647}
side.node[136].after.v.state [exact] orig={"t":"i32","v":2} ours={"t":"i32","v":0}
side.node[136].after.v.turn_available [exact] orig={"t":"i32","v":4} ours={"t":"i32","v":-1}
side.node[142].after.v.cost_rp [exact] orig={"t":"i32","v":8000} ours={"t":"i32","v":2147483647}
… 4 more
- call_id 9 (recap-b3-compare5.jsonl:11)
side.events.after.v.next_id [exact] orig={"t":"i32","v":12} ours={"t":"i32","v":10}
side.node[12].after.v.cost_rp [exact] orig={"t":"i32","v":35000} ours={"t":"i32","v":2147483647}
side.node[12].after.v.state [exact] orig={"t":"i32","v":2} ours={"t":"i32","v":0}
side.node[12].after.v.turn_available [exact] orig={"t":"i32","v":6} ours={"t":"i32","v":-1}
side.node[18].after.v.cost_rp [exact] orig={"t":"i32","v":4000} ours={"t":"i32","v":2147483647}
side.node[18].after.v.state [exact] orig={"t":"i32","v":2} ours={"t":"i32","v":0}
side.node[18].after.v.turn_available [exact] orig={"t":"i32","v":6} ours={"t":"i32","v":-1}
side.node[3].after.v.cost_rp [exact] orig={"t":"i32","v":13000} ours={"t":"i32","v":2147483647}
… 4 more

View file

@ -0,0 +1,772 @@
{
"coverage_contradicted": [],
"coverage_unstated": [],
"format": 1,
"hooks": {
"Game::TechTree::ProcessResearch": {
"calls": 3,
"compared": 3,
"coverage": {
"checked_regions": [
"events",
"node[0]",
"node[105]",
"node[106]",
"node[107]",
"node[108]",
"node[109]",
"node[10]",
"node[110]",
"node[111]",
"node[112]",
"node[113]",
"node[114]",
"node[115]",
"node[116]",
"node[117]",
"node[118]",
"node[119]",
"node[11]",
"node[120]",
"node[121]",
"node[122]",
"node[123]",
"node[124]",
"node[125]",
"node[126]",
"node[127]",
"node[128]",
"node[129]",
"node[12]",
"node[130]",
"node[131]",
"node[132]",
"node[133]",
"node[134]",
"node[135]",
"node[136]",
"node[137]",
"node[138]",
"node[139]",
"node[13]",
"node[140]",
"node[141]",
"node[142]",
"node[143]",
"node[144]",
"node[145]",
"node[146]",
"node[147]",
"node[148]",
"node[149]",
"node[14]",
"node[151]",
"node[152]",
"node[153]",
"node[154]",
"node[155]",
"node[156]",
"node[157]",
"node[158]",
"node[159]",
"node[15]",
"node[160]",
"node[161]",
"node[162]",
"node[163]",
"node[164]",
"node[165]",
"node[166]",
"node[167]",
"node[168]",
"node[169]",
"node[16]",
"node[170]",
"node[171]",
"node[172]",
"node[173]",
"node[174]",
"node[175]",
"node[177]",
"node[178]",
"node[179]",
"node[17]",
"node[180]",
"node[181]",
"node[182]",
"node[183]",
"node[184]",
"node[185]",
"node[186]",
"node[187]",
"node[188]",
"node[189]",
"node[18]",
"node[190]",
"node[191]",
"node[192]",
"node[193]",
"node[194]",
"node[195]",
"node[196]",
"node[197]",
"node[198]",
"node[199]",
"node[19]",
"node[1]",
"node[200]",
"node[201]",
"node[202]",
"node[203]",
"node[204]",
"node[205]",
"node[206]",
"node[207]",
"node[208]",
"node[209]",
"node[20]",
"node[210]",
"node[211]",
"node[212]",
"node[213]",
"node[214]",
"node[215]",
"node[216]",
"node[217]",
"node[218]",
"node[219]",
"node[21]",
"node[220]",
"node[221]",
"node[222]",
"node[223]",
"node[224]",
"node[225]",
"node[226]",
"node[227]",
"node[228]",
"node[229]",
"node[22]",
"node[230]",
"node[231]",
"node[232]",
"node[233]",
"node[234]",
"node[235]",
"node[236]",
"node[237]",
"node[238]",
"node[239]",
"node[23]",
"node[240]",
"node[241]",
"node[242]",
"node[243]",
"node[244]",
"node[245]",
"node[246]",
"node[247]",
"node[248]",
"node[249]",
"node[24]",
"node[250]",
"node[251]",
"node[252]",
"node[253]",
"node[254]",
"node[255]",
"node[256]",
"node[257]",
"node[258]",
"node[259]",
"node[25]",
"node[260]",
"node[261]",
"node[262]",
"node[263]",
"node[264]",
"node[265]",
"node[266]",
"node[267]",
"node[268]",
"node[269]",
"node[26]",
"node[270]",
"node[271]",
"node[272]",
"node[273]",
"node[274]",
"node[275]",
"node[276]",
"node[277]",
"node[278]",
"node[279]",
"node[27]",
"node[280]",
"node[281]",
"node[282]",
"node[283]",
"node[284]",
"node[285]",
"node[286]",
"node[287]",
"node[288]",
"node[289]",
"node[28]",
"node[290]",
"node[291]",
"node[292]",
"node[29]",
"node[2]",
"node[30]",
"node[31]",
"node[32]",
"node[33]",
"node[34]",
"node[35]",
"node[36]",
"node[37]",
"node[38]",
"node[39]",
"node[3]",
"node[40]",
"node[41]",
"node[42]",
"node[43]",
"node[44]",
"node[45]",
"node[46]",
"node[47]",
"node[48]",
"node[49]",
"node[4]",
"node[50]",
"node[51]",
"node[52]",
"node[53]",
"node[54]",
"node[55]",
"node[56]",
"node[57]",
"node[58]",
"node[5]",
"node[60]",
"node[61]",
"node[62]",
"node[63]",
"node[64]",
"node[65]",
"node[66]",
"node[67]",
"node[68]",
"node[69]",
"node[6]",
"node[70]",
"node[72]",
"node[74]",
"node[77]",
"node[78]",
"node[7]",
"node[80]",
"node[81]",
"node[85]",
"node[86]",
"node[87]",
"node[88]",
"node[89]",
"node[8]",
"node[90]",
"node[91]",
"node[92]",
"node[94]",
"node[98]",
"node[99]",
"node[9]",
"overbudget",
"rng"
],
"guarded_calls": 3,
"guards": [
"player",
"tree_header"
],
"spans": {
"compare": []
},
"state": "partial",
"undeclared_calls": 0,
"undeclared_writes": 0,
"unmodelled": [
{
"mitigation": "region:events (EvNxID now diverges instead of passing silently)",
"risk": "high",
"what": "posts EVENT_RESEARCH_OVERBUDGET on the owner's EventStorage in the same branch that sets node.flag = 2",
"why": "the message text is composed from the tech name, so ours cannot synthesise it; it would have to be posted through the game's own event API. This is the defect that made a clean compare false: replace mode's autosave differed from the oracle by exactly this one event"
},
{
"mitigation": "region:events",
"risk": "medium",
"what": "posts EVENT_TECHS_UNLOCKED for nodes that became available this turn",
"why": "the trailing unlock loop makes no draw and writes no node, but it does build a names list and post an event"
},
{
"mitigation": "guard:player, guard:tree_header",
"risk": "high",
"what": "TechTree::SetResearched on completion: the turn/order stamps, the child unlock cascade, the recursive research of zero-cost children, and the owner's OnTechResearched callback",
"why": "its own milestone (B2); the callback writes live player state that compare mode must not touch, and it consumes one extra RNG word"
},
{
"mitigation": "guard:tree_header",
"risk": "medium",
"what": "bumps the tree's completion-order counter (TechTree+0x20)",
"why": "part of SetResearched; the per-node `order` word is compared but the counter it comes from was not a region"
},
{
"mitigation": "",
"risk": "low",
"what": "writes a completion line to the game log",
"why": "log text is not simulation state"
}
],
"verdict": "partial",
"why": ""
},
"diffs": [
{
"call_id": 0,
"diff": [
{
"orig": {
"t": "i32",
"v": 4
},
"ours": {
"t": "i32",
"v": 3
},
"path": "side.events.after.v.next_id",
"why": "exact"
}
],
"file": "recap-b3-compare.jsonl",
"line": 2
}
],
"diverged": 1,
"diverged_call_ids": [
0
],
"errors": 0,
"modes": {
"compare": 3
}
}
},
"inputs": [
"/tmp/claude-1000/-home-alex/ec8e34f8-af37-4ef2-a309-ed6a15293097/scratchpad/recap-b3-compare.jsonl"
],
"invalid": [],
"kind": "report",
"meta": [
{
"build": "recap-7584bad-20260908T0615Z",
"exe_sha256": "970b7de729956a53094c7eb98aba4270aee98e2fed5daf0d39e290013c90c841",
"format": 1,
"hooks": {
"Game::SectionDictionary::SectionDictionary": {
"coverage": {
"state": "partial",
"unmodelled": [
{
"mitigation": "see docs/M2.md; compare mode for this hook is not safe to run",
"risk": "high",
"what": "LoadSection registers each section with the string table and the live TechTree, and may append to the dictionary's own vector",
"why": "M3 scope; ours delegates to the game's LoadSection after the original has already built all 885 definitions, so the second pass registers duplicates -- the leading hypothesis for this hook's compare-mode crash"
},
{
"mitigation": "",
"risk": "medium",
"what": "post-load validation pass over every definition's @-token against the string table",
"why": "runs after the loop and touches no declared region"
},
{
"mitigation": "",
"risk": "low",
"what": "allocates 885 SectionDef objects (0x3d8 bytes each) on the game heap",
"why": "they do not exist at hook entry; compared by index/species/id/token"
},
{
"mitigation": "guard:dict",
"risk": "low",
"what": "the word at dictionary+0x14",
"why": "not modelled; emitted as an ignored pointer"
},
{
"mitigation": "",
"risk": "medium",
"what": "the before-snapshot of the object is uninitialised heap",
"why": "the hook is on the constructor, so `before` is meaningless and only `after` carries information"
}
],
"why": ""
},
"ftol": 0,
"ftol_kind": "abs",
"ptr": "ignore"
},
"Game::ServerPlayer::ComputeBudget": {
"coverage": {
"state": "partial",
"unmodelled": [
{
"mitigation": "declared input boundary; see budget_inputs.h",
"risk": "high",
"what": "slots 1, 2, 3, 4, 7 and 11 are produced by callees this milestone does not model (per-system output, trade, ship-carried population, a second manager, the build-queue spend)",
"why": "they are copied out of the original's own output and back into the same slots, so they match BY CONSTRUCTION and prove nothing"
},
{
"mitigation": "guard:budget_object does not reach the ships; unverified",
"risk": "high",
"what": "ServerSystem::ComputeOutput repairs damaged ships in orbit",
"why": "replace mode runs the original a second time on a scratch Budget to harvest the six unmodelled slots, so that repair happens TWICE per turn in replace mode and nothing in the trace would show it"
},
{
"mitigation": "",
"risk": "medium",
"what": "the difficulty-mods row from StrategyServer::GetDifficultyMods",
"why": "not reachable from a ServerPlayer, so the two relevant entries are fitted constants measured from the B1 trace rather than snapshotted inputs"
},
{
"mitigation": "",
"risk": "low",
"what": "the research-allocation vector's heap block",
"why": "only the element count is compared; the three words are heap pointers the default policy ignores"
}
],
"why": ""
},
"ftol": 0,
"ftol_kind": "abs",
"ptr": "ignore"
},
"Game::ServerPlayer::OnTechResearched": {
"coverage": {
"state": "partial",
"unmodelled": [
{
"mitigation": "guard:player (EventStorage is inline at ServerPlayer+0x29c)",
"risk": "high",
"what": "posts EVENT_RESEARCH_COMPLETE / _UNDERBUDGET / _TEMPERANCE on the owner's EventStorage when !silent",
"why": "the same class of write as B3's defect, and this hook has no replace-mode oracle that could catch it: gotcha 4 in docs/B2.md says a changed save hash on a completion turn is expected and therefore not a finding"
},
{
"mitigation": "",
"risk": "high",
"what": "writes every owned system's AI flag (CCC_AIVrus / CCC_AISlv), re-evaluates the arcology civilian cap, cures addiction and clears plague across systems AND ships",
"why": "writes through pointers to other objects; compare mode must not touch live state, and no region reaches them"
},
{
"mitigation": "this is the extra draw B3 observed on a completion",
"risk": "high",
"what": "the pending plague-cure roll (ServerPlayer::RollResearchEvent)",
"why": "it draws exactly one word from the strategic generator unconditionally; running it in compare mode would consume real randomness. The two words it guards are still cleared and the record says whether it would have fired"
},
{
"mitigation": "",
"risk": "medium",
"what": "TechTree::SetResearched for the Zuul boarding-pod grant",
"why": "it would mutate the live tree, and it recurses"
},
{
"mitigation": "region:node_bore, declared only when the block already exists",
"risk": "medium",
"what": "allocates or frees the node-bore block at ServerPlayer+0x308",
"why": "ours has no allocator the game's runtime could free, so replace mode calls the game's own updater -- which means replace mode never exercises our node-bore selection at all"
}
],
"why": ""
},
"ftol": 0,
"ftol_kind": "abs",
"ptr": "ignore"
},
"Game::ServerSystem::ProcessTurn": {
"coverage": {
"state": "partial",
"unmodelled": [
{
"mitigation": "guard:system",
"risk": "high",
"what": "the addiction sweep raises MoraleEvents, which are constructed and appended to the system's capped morale history",
"why": "the same class of write as B3's defect. sim::ProcessColonyTurn does compute the morale events (ColonyTurnResult), but the hook never emits them: DescribeMoraleEvents is dead code, so they are neither compared nor logged"
},
{
"mitigation": "guard:system covers the system object only, not the other objects",
"risk": "high",
"what": "every callee: the plague pass, imperial and civilian growth, the resource debit, in-orbit refuel, slaves, rebellion and the build queue",
"why": "declared input boundary -- ProcessTurn is a dispatcher and only the words it writes itself are modelled. The callees raise EVENT_SLAVES_DEAD, EVENT_SYSTEM_REBELLION_CONTINUES, the plague events and SEBuildCompleted, create ships and bump per-player ShipRecords counters"
},
{
"mitigation": "",
"risk": "medium",
"what": "ApplyInfraBonus / ApplyPopBonus read the owner's home-system id, and the build queue writes the owning ServerPlayer",
"why": "writes through a pointer to another object; no region reaches the player"
},
{
"mitigation": "",
"risk": "low",
"what": "ProcessRebellion is the pass's only RNG consumer and its draw count is data-dependent",
"why": "the generator IS a declared region, so a moved post-state is visible and names the system whose rebellion fired -- it is reported, not modelled"
},
{
"mitigation": "",
"risk": "medium",
"what": "replace mode is refused for this hook",
"why": "our side models the dispatcher's own writes and none of the callees, so a replace run would silently skip a colony's whole turn. There is therefore no oracle layer behind the compare for this hook"
}
],
"why": ""
},
"ftol": 0,
"ftol_kind": "abs",
"ptr": "ignore"
},
"Game::StrategyServer::MoveFleet": {
"coverage": {
"state": "partial",
"unmodelled": [
{
"mitigation": "guard:fleet sees the fleet's own words; the event and the system do not",
"risk": "high",
"what": "on arrival: dispatches SEFleetArrived and runs one of three arrival handlers by destination kind (enter system / join fleet / stop at point)",
"why": "declared input boundary -- an arriving call is expected to differ in all of it, and none of it is declared, so the compare says nothing about arrivals"
},
{
"mitigation": "",
"risk": "high",
"what": "on departure: cancels every still-acting ship (with a log line each) and calls ServerSystem::FleetDeparts, which rewrites the system's ownership bits",
"why": "writes through pointers to ships and to the system"
},
{
"mitigation": "",
"risk": "medium",
"what": "the tanker top-up refuels other ships in the fleet",
"why": "the per-ship range regions would show it, but ours does not model it, so a fleet with a tanker diverges for a known reason"
},
{
"mitigation": "declared gap: docs/B4.md",
"risk": "medium",
"what": "a node-line waypoint's step comes from the stutter profile",
"why": "NodeLineStep / BuildStutterSegments are written and unit-tested but not wired in; the hook steps every waypoint type as speed x dt, so a node-line leg is knowingly mis-stepped and only its type is recorded"
},
{
"mitigation": "",
"risk": "medium",
"what": "a missed probabilistic jump scatters the fleet in a random direction",
"why": "the direction is a second draw whose mapping is not modelled; ours leaves the position alone and reports the scatter distance, so the generator region diverges by one word on a miss"
},
{
"mitigation": "",
"risk": "medium",
"what": "the route revalidation and the waypoint list itself",
"why": "declared input boundary; the waypoint vector is not a region"
}
],
"why": ""
},
"ftol": 0,
"ftol_kind": "abs",
"ptr": "ignore"
},
"Game::StrategyServer::ProcessFleetMovement": {
"coverage": {
"state": "partial",
"unmodelled": [
{
"mitigation": "",
"risk": "high",
"what": "`ours` re-reads the LIVE fleet list after the original has run",
"why": "the gate-traffic total is computed by the original at the very end of the pass, so a pre-call snapshot would diverge for the wrong reason. It breaks the compare invariant that ours never touches live memory, and it makes this hook's verdict partly self-fulfilling: the input to our arithmetic is the original's own post-move state"
},
{
"mitigation": "",
"risk": "high",
"what": "drives MoveFleet up to five times per fleet",
"why": "every undeclared effect of MoveFleet happens inside this call too; the pass schedule is recorded in the arguments but never compared"
},
{
"mitigation": "",
"risk": "high",
"what": "writes FPdpos into every fleet and clears flags 0x2 and 0x100 on every fleet",
"why": "no region covers the fleets, only the players' gate-traffic words"
},
{
"mitigation": "",
"risk": "high",
"what": "OnFleetArrived posts EVENT_FLEET_ARRIVED",
"why": "the same class of write as B3's defect, and there is no replace mode for this hook, so nothing behind the compare could catch it either"
},
{
"mitigation": "",
"risk": "medium",
"what": "the original accumulates by player->index but writes back by the player's position in the server vector, into a fixed 32-int array with no bounds check",
"why": "a real latent bug in the original that our side reproduces only while index == position; the reference save never separates them"
},
{
"mitigation": "",
"risk": "medium",
"what": "PassSchedule() is never called by the hook, and FleetSummary::targetFleetId / relation are never filled",
"why": "the header claims ours predicts the call order for a trace to check; that prediction is not actually emitted"
}
],
"why": ""
},
"ftol": 0,
"ftol_kind": "abs",
"ptr": "ignore"
},
"Game::TechTree::ProcessResearch": {
"coverage": {
"state": "partial",
"unmodelled": [
{
"mitigation": "region:events (EvNxID now diverges instead of passing silently)",
"risk": "high",
"what": "posts EVENT_RESEARCH_OVERBUDGET on the owner's EventStorage in the same branch that sets node.flag = 2",
"why": "the message text is composed from the tech name, so ours cannot synthesise it; it would have to be posted through the game's own event API. This is the defect that made a clean compare false: replace mode's autosave differed from the oracle by exactly this one event"
},
{
"mitigation": "region:events",
"risk": "medium",
"what": "posts EVENT_TECHS_UNLOCKED for nodes that became available this turn",
"why": "the trailing unlock loop makes no draw and writes no node, but it does build a names list and post an event"
},
{
"mitigation": "guard:player, guard:tree_header",
"risk": "high",
"what": "TechTree::SetResearched on completion: the turn/order stamps, the child unlock cascade, the recursive research of zero-cost children, and the owner's OnTechResearched callback",
"why": "its own milestone (B2); the callback writes live player state that compare mode must not touch, and it consumes one extra RNG word"
},
{
"mitigation": "guard:tree_header",
"risk": "medium",
"what": "bumps the tree's completion-order counter (TechTree+0x20)",
"why": "part of SetResearched; the per-node `order` word is compared but the counter it comes from was not a region"
},
{
"mitigation": "",
"risk": "low",
"what": "writes a completion line to the game log",
"why": "log text is not simulation state"
}
],
"why": ""
},
"ftol": 0,
"ftol_kind": "abs",
"ptr": "ignore"
},
"Game::WeaponDictionary::Init": {
"coverage": {
"state": "partial",
"unmodelled": [
{
"mitigation": "suspected cause of the sibling section hook's compare crash (docs/M2.md)",
"risk": "high",
"what": "LoadWeapon -> WeaponDef::ParseScript registers each weapon's name with the string table and resolves `requires` against the live TechTree",
"why": "per-file parsing is M3 scope; ours delegates to the game's own LoadWeapon, so a compare run performs the registration a SECOND time and neither the string table nor the tech tree is a declared region"
},
{
"mitigation": "",
"risk": "low",
"what": "allocates 123 WeaponDef objects (0x278 bytes each) on the game heap",
"why": "the definitions do not exist when the hook is entered, so they cannot be a before-snapshot; the dictionary region compares them by id/name/path"
},
{
"mitigation": "guard:dict",
"risk": "low",
"what": "the word at dictionary+0x14",
"why": "not modelled; emitted as an opaque pointer, which the default policy ignores -- a change is visible in a trace but never a divergence"
},
{
"mitigation": "",
"risk": "low",
"what": "writes lines to the game log for a missing manifest",
"why": "log text is not simulation state"
},
{
"mitigation": "",
"risk": "low",
"what": "std::sort tie order for equal weapon names",
"why": "msvc_sort.h replays MSVC 2010's introsort, but the shipped data has no tied names, so the tie rule is unexercised rather than verified"
}
],
"why": ""
},
"ftol": 0,
"ftol_kind": "abs",
"ptr": "ignore"
},
"Mars::GlobalConsts::LoadFile": {
"coverage": {
"state": "partial",
"unmodelled": [
{
"mitigation": "LoadAll's post-state would have to be hooked to see it",
"risk": "medium",
"what": "erases each consumed key from the caller's std::map",
"why": "the map is a LoadAll temporary; declaring a red-black tree as a region is not possible before the call. First-occurrence-wins is reproduced in game::config::apply instead, so the *effect* is modelled, the container is not"
},
{
"mitigation": "",
"risk": "low",
"what": "writes three kinds of line to the game log (unrecognised key, applied key, expected-but-not-found)",
"why": "log text is not part of the simulation state"
},
{
"mitigation": "",
"risk": "low",
"what": "opens the file through the VFS and allocates/releases a refcounted buffer",
"why": "ours performs the same two calls, so allocation behaviour matches by construction rather than by comparison"
},
{
"mitigation": "",
"risk": "low",
"what": "String slots assign through the engine's own std::string, leaking one heap block per long string in compare mode",
"why": "start-up only; documented in docs/M1.md"
}
],
"why": ""
},
"ftol": 0,
"ftol_kind": "abs",
"ptr": "ignore"
},
"Shim::SelfTest::Fill": {
"coverage": {
"state": "complete",
"unmodelled": [],
"why": "Fill writes buf[0..n) and nothing else; the whole range is a declared region"
},
"ftol": 0,
"ftol_kind": "abs",
"ptr": "ignore"
}
},
"inline_max": 256,
"started": "2026-09-08T06:28:40Z"
}
],
"totals": {
"calls": 3,
"compared": 3,
"coverage_contradicted": 0,
"coverage_unstated": 0,
"diverged": 1,
"guarded_calls": 3,
"invalid_records": 0,
"undeclared_calls": 0,
"undeclared_writes": 0
},
"warnings": []
}

View file

@ -0,0 +1,26 @@
## tracecmp report: recap-b3-compare.jsonl
- build: recap-7584bad-20260908T0615Z started: 2026-09-08T06:28:40Z inline_max: 256
- calls: 3 compared: 3 diverged: 1 invalid records: 0 warnings: 0
- coverage: 3 guarded call(s), 0 undeclared write(s) in 0 call(s); 0 hook(s) unstated, 0 contradicted
| hook | calls | modes | compared | diverged | errors |
|---|---|---|---|---|---|
| Game::TechTree::ProcessResearch | 3 | compare:3 | 3 | 1 | 0 |
### coverage
| hook | verdict | compared regions | guards | undeclared writes | unmodelled |
|---|---|---|---|---|---|
| Game::TechTree::ProcessResearch | partial | events, node[0], node[105], node[106], node[107], node[108], +270 | player, tree_header | 0 | 5 |
#### Game::TechTree::ProcessResearch — not checked by this run
- (high) posts EVENT_RESEARCH_OVERBUDGET on the owner's EventStorage in the same branch that sets node.flag = 2 — the message text is composed from the tech name, so ours cannot synthesise it; it would have to be posted through the game's own event API. This is the defect that made a clean compare false: replace mode's autosave differed from the oracle by exactly this one event [region:events (EvNxID now diverges instead of passing silently)]
- (medium) posts EVENT_TECHS_UNLOCKED for nodes that became available this turn — the trailing unlock loop makes no draw and writes no node, but it does build a names list and post an event [region:events]
- (high) TechTree::SetResearched on completion: the turn/order stamps, the child unlock cascade, the recursive research of zero-cost children, and the owner's OnTechResearched callback — its own milestone (B2); the callback writes live player state that compare mode must not touch, and it consumes one extra RNG word [guard:player, guard:tree_header]
- (medium) bumps the tree's completion-order counter (TechTree+0x20) — part of SetResearched; the per-node `order` word is compared but the counter it comes from was not a region [guard:tree_header]
- (low) writes a completion line to the game log — log text is not simulation state
### Game::TechTree::ProcessResearch: first 1 of 1 divergent call(s)
- call_id 0 (recap-b3-compare.jsonl:2)
side.events.after.v.next_id [exact] orig={"t":"i32","v":4} ours={"t":"i32","v":3}

View file

@ -0,0 +1,964 @@
{
"coverage_contradicted": [],
"coverage_unstated": [],
"format": 1,
"hooks": {
"Game::ServerPlayer::ComputeBudget": {
"calls": 3592,
"compared": 0,
"coverage": {
"checked_regions": [
"budget",
"inputs",
"research_alloc"
],
"guarded_calls": 3592,
"guards": [
"budget_object"
],
"spans": {
"trace": []
},
"state": "partial",
"undeclared_calls": 0,
"undeclared_writes": 0,
"unmodelled": [
{
"mitigation": "declared input boundary; see budget_inputs.h",
"risk": "high",
"what": "slots 1, 2, 3, 4, 7 and 11 are produced by callees this milestone does not model (per-system output, trade, ship-carried population, a second manager, the build-queue spend)",
"why": "they are copied out of the original's own output and back into the same slots, so they match BY CONSTRUCTION and prove nothing"
},
{
"mitigation": "guard:budget_object does not reach the ships; unverified",
"risk": "high",
"what": "ServerSystem::ComputeOutput repairs damaged ships in orbit",
"why": "replace mode runs the original a second time on a scratch Budget to harvest the six unmodelled slots, so that repair happens TWICE per turn in replace mode and nothing in the trace would show it"
},
{
"mitigation": "",
"risk": "medium",
"what": "the difficulty-mods row from StrategyServer::GetDifficultyMods",
"why": "not reachable from a ServerPlayer, so the two relevant entries are fitted constants measured from the B1 trace rather than snapshotted inputs"
},
{
"mitigation": "",
"risk": "low",
"what": "the research-allocation vector's heap block",
"why": "only the element count is compared; the three words are heap pointers the default policy ignores"
}
],
"verdict": "partial",
"why": ""
},
"diffs": [],
"diverged": 0,
"diverged_call_ids": [],
"errors": 0,
"modes": {
"trace": 3592
}
},
"Game::ServerSystem::ProcessTurn": {
"calls": 28,
"compared": 0,
"coverage": {
"checked_regions": [
"bats2",
"bats_mask",
"haltv",
"ibon",
"infra",
"inputs",
"ntdev",
"pbon",
"rcex",
"rcex_mask",
"rng",
"tres"
],
"guarded_calls": 28,
"guards": [
"system"
],
"spans": {
"trace": [
"system+0x1a4:12",
"system+0xd8:1",
"system+0x170:1",
"system+0x238:4"
]
},
"state": "partial",
"undeclared_calls": 3,
"undeclared_writes": 6,
"unmodelled": [
{
"mitigation": "guard:system",
"risk": "high",
"what": "the addiction sweep raises MoraleEvents, which are constructed and appended to the system's capped morale history",
"why": "the same class of write as B3's defect. sim::ProcessColonyTurn does compute the morale events (ColonyTurnResult), but the hook never emits them: DescribeMoraleEvents is dead code, so they are neither compared nor logged"
},
{
"mitigation": "guard:system covers the system object only, not the other objects",
"risk": "high",
"what": "every callee: the plague pass, imperial and civilian growth, the resource debit, in-orbit refuel, slaves, rebellion and the build queue",
"why": "declared input boundary -- ProcessTurn is a dispatcher and only the words it writes itself are modelled. The callees raise EVENT_SLAVES_DEAD, EVENT_SYSTEM_REBELLION_CONTINUES, the plague events and SEBuildCompleted, create ships and bump per-player ShipRecords counters"
},
{
"mitigation": "",
"risk": "medium",
"what": "ApplyInfraBonus / ApplyPopBonus read the owner's home-system id, and the build queue writes the owning ServerPlayer",
"why": "writes through a pointer to another object; no region reaches the player"
},
{
"mitigation": "",
"risk": "low",
"what": "ProcessRebellion is the pass's only RNG consumer and its draw count is data-dependent",
"why": "the generator IS a declared region, so a moved post-state is visible and names the system whose rebellion fired -- it is reported, not modelled"
},
{
"mitigation": "",
"risk": "medium",
"what": "replace mode is refused for this hook",
"why": "our side models the dispatcher's own writes and none of the callees, so a replace run would silently skip a colony's whole turn. There is therefore no oracle layer behind the compare for this hook"
}
],
"verdict": "partial",
"why": ""
},
"diffs": [],
"diverged": 0,
"diverged_call_ids": [],
"errors": 0,
"modes": {
"trace": 28
}
},
"Game::StrategyServer::MoveFleet": {
"calls": 7,
"compared": 0,
"coverage": {
"checked_regions": [
"pos",
"prev_pos",
"rng",
"ship[0].range",
"ship[1].range",
"ship[2].range",
"ship[3].range",
"ship[4].range",
"ship[5].range",
"ship[6].range",
"ship[7].range",
"ship[8].range",
"ship[9].range"
],
"guarded_calls": 7,
"guards": [
"fleet"
],
"spans": {
"trace": [
"fleet+0xa0:4",
"fleet+0xdc:1",
"fleet+0x10c:1"
]
},
"state": "partial",
"undeclared_calls": 1,
"undeclared_writes": 3,
"unmodelled": [
{
"mitigation": "guard:fleet sees the fleet's own words; the event and the system do not",
"risk": "high",
"what": "on arrival: dispatches SEFleetArrived and runs one of three arrival handlers by destination kind (enter system / join fleet / stop at point)",
"why": "declared input boundary -- an arriving call is expected to differ in all of it, and none of it is declared, so the compare says nothing about arrivals"
},
{
"mitigation": "",
"risk": "high",
"what": "on departure: cancels every still-acting ship (with a log line each) and calls ServerSystem::FleetDeparts, which rewrites the system's ownership bits",
"why": "writes through pointers to ships and to the system"
},
{
"mitigation": "",
"risk": "medium",
"what": "the tanker top-up refuels other ships in the fleet",
"why": "the per-ship range regions would show it, but ours does not model it, so a fleet with a tanker diverges for a known reason"
},
{
"mitigation": "declared gap: docs/B4.md",
"risk": "medium",
"what": "a node-line waypoint's step comes from the stutter profile",
"why": "NodeLineStep / BuildStutterSegments are written and unit-tested but not wired in; the hook steps every waypoint type as speed x dt, so a node-line leg is knowingly mis-stepped and only its type is recorded"
},
{
"mitigation": "",
"risk": "medium",
"what": "a missed probabilistic jump scatters the fleet in a random direction",
"why": "the direction is a second draw whose mapping is not modelled; ours leaves the position alone and reports the scatter distance, so the generator region diverges by one word on a miss"
},
{
"mitigation": "",
"risk": "medium",
"what": "the route revalidation and the waypoint list itself",
"why": "declared input boundary; the waypoint vector is not a region"
}
],
"verdict": "partial",
"why": ""
},
"diffs": [],
"diverged": 0,
"diverged_call_ids": [],
"errors": 0,
"modes": {
"trace": 7
}
},
"Game::TechTree::ProcessResearch": {
"calls": 3,
"compared": 0,
"coverage": {
"checked_regions": [
"events",
"node[0]",
"node[105]",
"node[106]",
"node[107]",
"node[108]",
"node[109]",
"node[10]",
"node[110]",
"node[111]",
"node[112]",
"node[113]",
"node[114]",
"node[115]",
"node[116]",
"node[117]",
"node[118]",
"node[119]",
"node[11]",
"node[120]",
"node[121]",
"node[122]",
"node[123]",
"node[124]",
"node[125]",
"node[126]",
"node[127]",
"node[128]",
"node[129]",
"node[12]",
"node[130]",
"node[131]",
"node[132]",
"node[133]",
"node[134]",
"node[135]",
"node[136]",
"node[137]",
"node[138]",
"node[139]",
"node[13]",
"node[140]",
"node[141]",
"node[142]",
"node[143]",
"node[144]",
"node[145]",
"node[146]",
"node[147]",
"node[148]",
"node[149]",
"node[14]",
"node[151]",
"node[152]",
"node[153]",
"node[154]",
"node[155]",
"node[156]",
"node[157]",
"node[158]",
"node[159]",
"node[15]",
"node[160]",
"node[161]",
"node[162]",
"node[163]",
"node[164]",
"node[165]",
"node[166]",
"node[167]",
"node[168]",
"node[169]",
"node[16]",
"node[170]",
"node[171]",
"node[172]",
"node[173]",
"node[174]",
"node[175]",
"node[177]",
"node[178]",
"node[179]",
"node[17]",
"node[180]",
"node[181]",
"node[182]",
"node[183]",
"node[184]",
"node[185]",
"node[186]",
"node[187]",
"node[188]",
"node[189]",
"node[18]",
"node[190]",
"node[191]",
"node[192]",
"node[193]",
"node[194]",
"node[195]",
"node[196]",
"node[197]",
"node[198]",
"node[199]",
"node[19]",
"node[1]",
"node[200]",
"node[201]",
"node[202]",
"node[203]",
"node[204]",
"node[205]",
"node[206]",
"node[207]",
"node[208]",
"node[209]",
"node[20]",
"node[210]",
"node[211]",
"node[212]",
"node[213]",
"node[214]",
"node[215]",
"node[216]",
"node[217]",
"node[218]",
"node[219]",
"node[21]",
"node[220]",
"node[221]",
"node[222]",
"node[223]",
"node[224]",
"node[225]",
"node[226]",
"node[227]",
"node[228]",
"node[229]",
"node[22]",
"node[230]",
"node[231]",
"node[232]",
"node[233]",
"node[234]",
"node[235]",
"node[236]",
"node[237]",
"node[238]",
"node[239]",
"node[23]",
"node[240]",
"node[241]",
"node[242]",
"node[243]",
"node[244]",
"node[245]",
"node[246]",
"node[247]",
"node[248]",
"node[249]",
"node[24]",
"node[250]",
"node[251]",
"node[252]",
"node[253]",
"node[254]",
"node[255]",
"node[256]",
"node[257]",
"node[258]",
"node[259]",
"node[25]",
"node[260]",
"node[261]",
"node[262]",
"node[263]",
"node[264]",
"node[265]",
"node[266]",
"node[267]",
"node[268]",
"node[269]",
"node[26]",
"node[270]",
"node[271]",
"node[272]",
"node[273]",
"node[274]",
"node[275]",
"node[276]",
"node[277]",
"node[278]",
"node[279]",
"node[27]",
"node[280]",
"node[281]",
"node[282]",
"node[283]",
"node[284]",
"node[285]",
"node[286]",
"node[287]",
"node[288]",
"node[289]",
"node[28]",
"node[290]",
"node[291]",
"node[292]",
"node[29]",
"node[2]",
"node[30]",
"node[31]",
"node[32]",
"node[33]",
"node[34]",
"node[35]",
"node[36]",
"node[37]",
"node[38]",
"node[39]",
"node[3]",
"node[40]",
"node[41]",
"node[42]",
"node[43]",
"node[44]",
"node[45]",
"node[46]",
"node[47]",
"node[48]",
"node[49]",
"node[4]",
"node[50]",
"node[51]",
"node[52]",
"node[53]",
"node[54]",
"node[55]",
"node[56]",
"node[57]",
"node[58]",
"node[5]",
"node[60]",
"node[61]",
"node[62]",
"node[63]",
"node[64]",
"node[65]",
"node[66]",
"node[67]",
"node[68]",
"node[69]",
"node[6]",
"node[70]",
"node[72]",
"node[74]",
"node[77]",
"node[78]",
"node[7]",
"node[80]",
"node[81]",
"node[85]",
"node[86]",
"node[87]",
"node[88]",
"node[89]",
"node[8]",
"node[90]",
"node[91]",
"node[92]",
"node[94]",
"node[98]",
"node[99]",
"node[9]",
"overbudget",
"rng"
],
"guarded_calls": 3,
"guards": [
"player",
"tree_header"
],
"spans": {
"trace": []
},
"state": "partial",
"undeclared_calls": 0,
"undeclared_writes": 0,
"unmodelled": [
{
"mitigation": "region:events (EvNxID now diverges instead of passing silently)",
"risk": "high",
"what": "posts EVENT_RESEARCH_OVERBUDGET on the owner's EventStorage in the same branch that sets node.flag = 2",
"why": "the message text is composed from the tech name, so ours cannot synthesise it; it would have to be posted through the game's own event API. This is the defect that made a clean compare false: replace mode's autosave differed from the oracle by exactly this one event"
},
{
"mitigation": "region:events",
"risk": "medium",
"what": "posts EVENT_TECHS_UNLOCKED for nodes that became available this turn",
"why": "the trailing unlock loop makes no draw and writes no node, but it does build a names list and post an event"
},
{
"mitigation": "guard:player, guard:tree_header",
"risk": "high",
"what": "TechTree::SetResearched on completion: the turn/order stamps, the child unlock cascade, the recursive research of zero-cost children, and the owner's OnTechResearched callback",
"why": "its own milestone (B2); the callback writes live player state that compare mode must not touch, and it consumes one extra RNG word"
},
{
"mitigation": "guard:tree_header",
"risk": "medium",
"what": "bumps the tree's completion-order counter (TechTree+0x20)",
"why": "part of SetResearched; the per-node `order` word is compared but the counter it comes from was not a region"
},
{
"mitigation": "",
"risk": "low",
"what": "writes a completion line to the game log",
"why": "log text is not simulation state"
}
],
"verdict": "partial",
"why": ""
},
"diffs": [],
"diverged": 0,
"diverged_call_ids": [],
"errors": 0,
"modes": {
"trace": 3
}
}
},
"inputs": [
"/tmp/claude-1000/-home-alex/ec8e34f8-af37-4ef2-a309-ed6a15293097/scratchpad/recap-trace.jsonl"
],
"invalid": [],
"kind": "report",
"meta": [
{
"build": "recap-7584bad-20260908T0615Z",
"exe_sha256": "970b7de729956a53094c7eb98aba4270aee98e2fed5daf0d39e290013c90c841",
"format": 1,
"hooks": {
"Game::SectionDictionary::SectionDictionary": {
"coverage": {
"state": "partial",
"unmodelled": [
{
"mitigation": "see docs/M2.md; compare mode for this hook is not safe to run",
"risk": "high",
"what": "LoadSection registers each section with the string table and the live TechTree, and may append to the dictionary's own vector",
"why": "M3 scope; ours delegates to the game's LoadSection after the original has already built all 885 definitions, so the second pass registers duplicates -- the leading hypothesis for this hook's compare-mode crash"
},
{
"mitigation": "",
"risk": "medium",
"what": "post-load validation pass over every definition's @-token against the string table",
"why": "runs after the loop and touches no declared region"
},
{
"mitigation": "",
"risk": "low",
"what": "allocates 885 SectionDef objects (0x3d8 bytes each) on the game heap",
"why": "they do not exist at hook entry; compared by index/species/id/token"
},
{
"mitigation": "guard:dict",
"risk": "low",
"what": "the word at dictionary+0x14",
"why": "not modelled; emitted as an ignored pointer"
},
{
"mitigation": "",
"risk": "medium",
"what": "the before-snapshot of the object is uninitialised heap",
"why": "the hook is on the constructor, so `before` is meaningless and only `after` carries information"
}
],
"why": ""
},
"ftol": 0,
"ftol_kind": "abs",
"ptr": "ignore"
},
"Game::ServerPlayer::ComputeBudget": {
"coverage": {
"state": "partial",
"unmodelled": [
{
"mitigation": "declared input boundary; see budget_inputs.h",
"risk": "high",
"what": "slots 1, 2, 3, 4, 7 and 11 are produced by callees this milestone does not model (per-system output, trade, ship-carried population, a second manager, the build-queue spend)",
"why": "they are copied out of the original's own output and back into the same slots, so they match BY CONSTRUCTION and prove nothing"
},
{
"mitigation": "guard:budget_object does not reach the ships; unverified",
"risk": "high",
"what": "ServerSystem::ComputeOutput repairs damaged ships in orbit",
"why": "replace mode runs the original a second time on a scratch Budget to harvest the six unmodelled slots, so that repair happens TWICE per turn in replace mode and nothing in the trace would show it"
},
{
"mitigation": "",
"risk": "medium",
"what": "the difficulty-mods row from StrategyServer::GetDifficultyMods",
"why": "not reachable from a ServerPlayer, so the two relevant entries are fitted constants measured from the B1 trace rather than snapshotted inputs"
},
{
"mitigation": "",
"risk": "low",
"what": "the research-allocation vector's heap block",
"why": "only the element count is compared; the three words are heap pointers the default policy ignores"
}
],
"why": ""
},
"ftol": 0,
"ftol_kind": "abs",
"ptr": "ignore"
},
"Game::ServerPlayer::OnTechResearched": {
"coverage": {
"state": "partial",
"unmodelled": [
{
"mitigation": "guard:player (EventStorage is inline at ServerPlayer+0x29c)",
"risk": "high",
"what": "posts EVENT_RESEARCH_COMPLETE / _UNDERBUDGET / _TEMPERANCE on the owner's EventStorage when !silent",
"why": "the same class of write as B3's defect, and this hook has no replace-mode oracle that could catch it: gotcha 4 in docs/B2.md says a changed save hash on a completion turn is expected and therefore not a finding"
},
{
"mitigation": "",
"risk": "high",
"what": "writes every owned system's AI flag (CCC_AIVrus / CCC_AISlv), re-evaluates the arcology civilian cap, cures addiction and clears plague across systems AND ships",
"why": "writes through pointers to other objects; compare mode must not touch live state, and no region reaches them"
},
{
"mitigation": "this is the extra draw B3 observed on a completion",
"risk": "high",
"what": "the pending plague-cure roll (ServerPlayer::RollResearchEvent)",
"why": "it draws exactly one word from the strategic generator unconditionally; running it in compare mode would consume real randomness. The two words it guards are still cleared and the record says whether it would have fired"
},
{
"mitigation": "",
"risk": "medium",
"what": "TechTree::SetResearched for the Zuul boarding-pod grant",
"why": "it would mutate the live tree, and it recurses"
},
{
"mitigation": "region:node_bore, declared only when the block already exists",
"risk": "medium",
"what": "allocates or frees the node-bore block at ServerPlayer+0x308",
"why": "ours has no allocator the game's runtime could free, so replace mode calls the game's own updater -- which means replace mode never exercises our node-bore selection at all"
}
],
"why": ""
},
"ftol": 0,
"ftol_kind": "abs",
"ptr": "ignore"
},
"Game::ServerSystem::ProcessTurn": {
"coverage": {
"state": "partial",
"unmodelled": [
{
"mitigation": "guard:system",
"risk": "high",
"what": "the addiction sweep raises MoraleEvents, which are constructed and appended to the system's capped morale history",
"why": "the same class of write as B3's defect. sim::ProcessColonyTurn does compute the morale events (ColonyTurnResult), but the hook never emits them: DescribeMoraleEvents is dead code, so they are neither compared nor logged"
},
{
"mitigation": "guard:system covers the system object only, not the other objects",
"risk": "high",
"what": "every callee: the plague pass, imperial and civilian growth, the resource debit, in-orbit refuel, slaves, rebellion and the build queue",
"why": "declared input boundary -- ProcessTurn is a dispatcher and only the words it writes itself are modelled. The callees raise EVENT_SLAVES_DEAD, EVENT_SYSTEM_REBELLION_CONTINUES, the plague events and SEBuildCompleted, create ships and bump per-player ShipRecords counters"
},
{
"mitigation": "",
"risk": "medium",
"what": "ApplyInfraBonus / ApplyPopBonus read the owner's home-system id, and the build queue writes the owning ServerPlayer",
"why": "writes through a pointer to another object; no region reaches the player"
},
{
"mitigation": "",
"risk": "low",
"what": "ProcessRebellion is the pass's only RNG consumer and its draw count is data-dependent",
"why": "the generator IS a declared region, so a moved post-state is visible and names the system whose rebellion fired -- it is reported, not modelled"
},
{
"mitigation": "",
"risk": "medium",
"what": "replace mode is refused for this hook",
"why": "our side models the dispatcher's own writes and none of the callees, so a replace run would silently skip a colony's whole turn. There is therefore no oracle layer behind the compare for this hook"
}
],
"why": ""
},
"ftol": 0,
"ftol_kind": "abs",
"ptr": "ignore"
},
"Game::StrategyServer::MoveFleet": {
"coverage": {
"state": "partial",
"unmodelled": [
{
"mitigation": "guard:fleet sees the fleet's own words; the event and the system do not",
"risk": "high",
"what": "on arrival: dispatches SEFleetArrived and runs one of three arrival handlers by destination kind (enter system / join fleet / stop at point)",
"why": "declared input boundary -- an arriving call is expected to differ in all of it, and none of it is declared, so the compare says nothing about arrivals"
},
{
"mitigation": "",
"risk": "high",
"what": "on departure: cancels every still-acting ship (with a log line each) and calls ServerSystem::FleetDeparts, which rewrites the system's ownership bits",
"why": "writes through pointers to ships and to the system"
},
{
"mitigation": "",
"risk": "medium",
"what": "the tanker top-up refuels other ships in the fleet",
"why": "the per-ship range regions would show it, but ours does not model it, so a fleet with a tanker diverges for a known reason"
},
{
"mitigation": "declared gap: docs/B4.md",
"risk": "medium",
"what": "a node-line waypoint's step comes from the stutter profile",
"why": "NodeLineStep / BuildStutterSegments are written and unit-tested but not wired in; the hook steps every waypoint type as speed x dt, so a node-line leg is knowingly mis-stepped and only its type is recorded"
},
{
"mitigation": "",
"risk": "medium",
"what": "a missed probabilistic jump scatters the fleet in a random direction",
"why": "the direction is a second draw whose mapping is not modelled; ours leaves the position alone and reports the scatter distance, so the generator region diverges by one word on a miss"
},
{
"mitigation": "",
"risk": "medium",
"what": "the route revalidation and the waypoint list itself",
"why": "declared input boundary; the waypoint vector is not a region"
}
],
"why": ""
},
"ftol": 0,
"ftol_kind": "abs",
"ptr": "ignore"
},
"Game::StrategyServer::ProcessFleetMovement": {
"coverage": {
"state": "partial",
"unmodelled": [
{
"mitigation": "",
"risk": "high",
"what": "`ours` re-reads the LIVE fleet list after the original has run",
"why": "the gate-traffic total is computed by the original at the very end of the pass, so a pre-call snapshot would diverge for the wrong reason. It breaks the compare invariant that ours never touches live memory, and it makes this hook's verdict partly self-fulfilling: the input to our arithmetic is the original's own post-move state"
},
{
"mitigation": "",
"risk": "high",
"what": "drives MoveFleet up to five times per fleet",
"why": "every undeclared effect of MoveFleet happens inside this call too; the pass schedule is recorded in the arguments but never compared"
},
{
"mitigation": "",
"risk": "high",
"what": "writes FPdpos into every fleet and clears flags 0x2 and 0x100 on every fleet",
"why": "no region covers the fleets, only the players' gate-traffic words"
},
{
"mitigation": "",
"risk": "high",
"what": "OnFleetArrived posts EVENT_FLEET_ARRIVED",
"why": "the same class of write as B3's defect, and there is no replace mode for this hook, so nothing behind the compare could catch it either"
},
{
"mitigation": "",
"risk": "medium",
"what": "the original accumulates by player->index but writes back by the player's position in the server vector, into a fixed 32-int array with no bounds check",
"why": "a real latent bug in the original that our side reproduces only while index == position; the reference save never separates them"
},
{
"mitigation": "",
"risk": "medium",
"what": "PassSchedule() is never called by the hook, and FleetSummary::targetFleetId / relation are never filled",
"why": "the header claims ours predicts the call order for a trace to check; that prediction is not actually emitted"
}
],
"why": ""
},
"ftol": 0,
"ftol_kind": "abs",
"ptr": "ignore"
},
"Game::TechTree::ProcessResearch": {
"coverage": {
"state": "partial",
"unmodelled": [
{
"mitigation": "region:events (EvNxID now diverges instead of passing silently)",
"risk": "high",
"what": "posts EVENT_RESEARCH_OVERBUDGET on the owner's EventStorage in the same branch that sets node.flag = 2",
"why": "the message text is composed from the tech name, so ours cannot synthesise it; it would have to be posted through the game's own event API. This is the defect that made a clean compare false: replace mode's autosave differed from the oracle by exactly this one event"
},
{
"mitigation": "region:events",
"risk": "medium",
"what": "posts EVENT_TECHS_UNLOCKED for nodes that became available this turn",
"why": "the trailing unlock loop makes no draw and writes no node, but it does build a names list and post an event"
},
{
"mitigation": "guard:player, guard:tree_header",
"risk": "high",
"what": "TechTree::SetResearched on completion: the turn/order stamps, the child unlock cascade, the recursive research of zero-cost children, and the owner's OnTechResearched callback",
"why": "its own milestone (B2); the callback writes live player state that compare mode must not touch, and it consumes one extra RNG word"
},
{
"mitigation": "guard:tree_header",
"risk": "medium",
"what": "bumps the tree's completion-order counter (TechTree+0x20)",
"why": "part of SetResearched; the per-node `order` word is compared but the counter it comes from was not a region"
},
{
"mitigation": "",
"risk": "low",
"what": "writes a completion line to the game log",
"why": "log text is not simulation state"
}
],
"why": ""
},
"ftol": 0,
"ftol_kind": "abs",
"ptr": "ignore"
},
"Game::WeaponDictionary::Init": {
"coverage": {
"state": "partial",
"unmodelled": [
{
"mitigation": "suspected cause of the sibling section hook's compare crash (docs/M2.md)",
"risk": "high",
"what": "LoadWeapon -> WeaponDef::ParseScript registers each weapon's name with the string table and resolves `requires` against the live TechTree",
"why": "per-file parsing is M3 scope; ours delegates to the game's own LoadWeapon, so a compare run performs the registration a SECOND time and neither the string table nor the tech tree is a declared region"
},
{
"mitigation": "",
"risk": "low",
"what": "allocates 123 WeaponDef objects (0x278 bytes each) on the game heap",
"why": "the definitions do not exist when the hook is entered, so they cannot be a before-snapshot; the dictionary region compares them by id/name/path"
},
{
"mitigation": "guard:dict",
"risk": "low",
"what": "the word at dictionary+0x14",
"why": "not modelled; emitted as an opaque pointer, which the default policy ignores -- a change is visible in a trace but never a divergence"
},
{
"mitigation": "",
"risk": "low",
"what": "writes lines to the game log for a missing manifest",
"why": "log text is not simulation state"
},
{
"mitigation": "",
"risk": "low",
"what": "std::sort tie order for equal weapon names",
"why": "msvc_sort.h replays MSVC 2010's introsort, but the shipped data has no tied names, so the tie rule is unexercised rather than verified"
}
],
"why": ""
},
"ftol": 0,
"ftol_kind": "abs",
"ptr": "ignore"
},
"Mars::GlobalConsts::LoadFile": {
"coverage": {
"state": "partial",
"unmodelled": [
{
"mitigation": "LoadAll's post-state would have to be hooked to see it",
"risk": "medium",
"what": "erases each consumed key from the caller's std::map",
"why": "the map is a LoadAll temporary; declaring a red-black tree as a region is not possible before the call. First-occurrence-wins is reproduced in game::config::apply instead, so the *effect* is modelled, the container is not"
},
{
"mitigation": "",
"risk": "low",
"what": "writes three kinds of line to the game log (unrecognised key, applied key, expected-but-not-found)",
"why": "log text is not part of the simulation state"
},
{
"mitigation": "",
"risk": "low",
"what": "opens the file through the VFS and allocates/releases a refcounted buffer",
"why": "ours performs the same two calls, so allocation behaviour matches by construction rather than by comparison"
},
{
"mitigation": "",
"risk": "low",
"what": "String slots assign through the engine's own std::string, leaking one heap block per long string in compare mode",
"why": "start-up only; documented in docs/M1.md"
}
],
"why": ""
},
"ftol": 0,
"ftol_kind": "abs",
"ptr": "ignore"
},
"Shim::SelfTest::Fill": {
"coverage": {
"state": "complete",
"unmodelled": [],
"why": "Fill writes buf[0..n) and nothing else; the whole range is a declared region"
},
"ftol": 0,
"ftol_kind": "abs",
"ptr": "ignore"
}
},
"inline_max": 256,
"started": "2026-09-08T06:18:02Z"
}
],
"totals": {
"calls": 3630,
"compared": 0,
"coverage_contradicted": 0,
"coverage_unstated": 0,
"diverged": 0,
"guarded_calls": 3630,
"invalid_records": 0,
"undeclared_calls": 4,
"undeclared_writes": 9
},
"warnings": []
}

View file

@ -0,0 +1,51 @@
## tracecmp report: recap-trace.jsonl
- build: recap-7584bad-20260908T0615Z started: 2026-09-08T06:18:02Z inline_max: 256
- calls: 3630 compared: 0 diverged: 0 invalid records: 0 warnings: 0
- coverage: 3630 guarded call(s), 9 undeclared write(s) in 4 call(s); 0 hook(s) unstated, 0 contradicted
| hook | calls | modes | compared | diverged | errors |
|---|---|---|---|---|---|
| Game::ServerPlayer::ComputeBudget | 3592 | trace:3592 | 0 | 0 | 0 |
| Game::ServerSystem::ProcessTurn | 28 | trace:28 | 0 | 0 | 0 |
| Game::StrategyServer::MoveFleet | 7 | trace:7 | 0 | 0 | 0 |
| Game::TechTree::ProcessResearch | 3 | trace:3 | 0 | 0 | 0 |
### coverage
| hook | verdict | compared regions | guards | undeclared writes | unmodelled |
|---|---|---|---|---|---|
| Game::ServerPlayer::ComputeBudget | partial | budget, inputs, research_alloc | budget_object | 0 | 4 |
| Game::ServerSystem::ProcessTurn | partial | bats2, bats_mask, haltv, ibon, infra, inputs, +6 | system | 6 in 3 call(s) | 5 |
| Game::StrategyServer::MoveFleet | partial | pos, prev_pos, rng, ship[0].range, ship[1].range, ship[2].range, +7 | fleet | 3 in 1 call(s) | 6 |
| Game::TechTree::ProcessResearch | partial | events, node[0], node[105], node[106], node[107], node[108], +270 | player, tree_header | 0 | 5 |
#### Game::ServerPlayer::ComputeBudget — not checked by this run
- (high) slots 1, 2, 3, 4, 7 and 11 are produced by callees this milestone does not model (per-system output, trade, ship-carried population, a second manager, the build-queue spend) — they are copied out of the original's own output and back into the same slots, so they match BY CONSTRUCTION and prove nothing [declared input boundary; see budget_inputs.h]
- (high) ServerSystem::ComputeOutput repairs damaged ships in orbit — replace mode runs the original a second time on a scratch Budget to harvest the six unmodelled slots, so that repair happens TWICE per turn in replace mode and nothing in the trace would show it [guard:budget_object does not reach the ships; unverified]
- (medium) the difficulty-mods row from StrategyServer::GetDifficultyMods — not reachable from a ServerPlayer, so the two relevant entries are fitted constants measured from the B1 trace rather than snapshotted inputs
- (low) the research-allocation vector's heap block — only the element count is compared; the three words are heap pointers the default policy ignores
#### Game::ServerSystem::ProcessTurn — not checked by this run
- (high) the addiction sweep raises MoraleEvents, which are constructed and appended to the system's capped morale history — the same class of write as B3's defect. sim::ProcessColonyTurn does compute the morale events (ColonyTurnResult), but the hook never emits them: DescribeMoraleEvents is dead code, so they are neither compared nor logged [guard:system]
- (high) every callee: the plague pass, imperial and civilian growth, the resource debit, in-orbit refuel, slaves, rebellion and the build queue — declared input boundary -- ProcessTurn is a dispatcher and only the words it writes itself are modelled. The callees raise EVENT_SLAVES_DEAD, EVENT_SYSTEM_REBELLION_CONTINUES, the plague events and SEBuildCompleted, create ships and bump per-player ShipRecords counters [guard:system covers the system object only, not the other objects]
- (medium) ApplyInfraBonus / ApplyPopBonus read the owner's home-system id, and the build queue writes the owning ServerPlayer — writes through a pointer to another object; no region reaches the player
- (low) ProcessRebellion is the pass's only RNG consumer and its draw count is data-dependent — the generator IS a declared region, so a moved post-state is visible and names the system whose rebellion fired -- it is reported, not modelled
- (medium) replace mode is refused for this hook — our side models the dispatcher's own writes and none of the callees, so a replace run would silently skip a colony's whole turn. There is therefore no oracle layer behind the compare for this hook
- guard hits in trace mode: system+0x1a4:12, system+0xd8:1, system+0x170:1, system+0x238:4
#### Game::StrategyServer::MoveFleet — not checked by this run
- (high) on arrival: dispatches SEFleetArrived and runs one of three arrival handlers by destination kind (enter system / join fleet / stop at point) — declared input boundary -- an arriving call is expected to differ in all of it, and none of it is declared, so the compare says nothing about arrivals [guard:fleet sees the fleet's own words; the event and the system do not]
- (high) on departure: cancels every still-acting ship (with a log line each) and calls ServerSystem::FleetDeparts, which rewrites the system's ownership bits — writes through pointers to ships and to the system
- (medium) the tanker top-up refuels other ships in the fleet — the per-ship range regions would show it, but ours does not model it, so a fleet with a tanker diverges for a known reason
- (medium) a node-line waypoint's step comes from the stutter profile — NodeLineStep / BuildStutterSegments are written and unit-tested but not wired in; the hook steps every waypoint type as speed x dt, so a node-line leg is knowingly mis-stepped and only its type is recorded [declared gap: docs/B4.md]
- (medium) a missed probabilistic jump scatters the fleet in a random direction — the direction is a second draw whose mapping is not modelled; ours leaves the position alone and reports the scatter distance, so the generator region diverges by one word on a miss
- (medium) the route revalidation and the waypoint list itself — declared input boundary; the waypoint vector is not a region
- guard hits in trace mode: fleet+0xa0:4, fleet+0xdc:1, fleet+0x10c:1
#### Game::TechTree::ProcessResearch — not checked by this run
- (high) posts EVENT_RESEARCH_OVERBUDGET on the owner's EventStorage in the same branch that sets node.flag = 2 — the message text is composed from the tech name, so ours cannot synthesise it; it would have to be posted through the game's own event API. This is the defect that made a clean compare false: replace mode's autosave differed from the oracle by exactly this one event [region:events (EvNxID now diverges instead of passing silently)]
- (medium) posts EVENT_TECHS_UNLOCKED for nodes that became available this turn — the trailing unlock loop makes no draw and writes no node, but it does build a names list and post an event [region:events]
- (high) TechTree::SetResearched on completion: the turn/order stamps, the child unlock cascade, the recursive research of zero-cost children, and the owner's OnTechResearched callback — its own milestone (B2); the callback writes live player state that compare mode must not touch, and it consumes one extra RNG word [guard:player, guard:tree_header]
- (medium) bumps the tree's completion-order counter (TechTree+0x20) — part of SetResearched; the per-node `order` word is compared but the counter it comes from was not a region [guard:tree_header]
- (low) writes a completion line to the game log — log text is not simulation state

View file

@ -0,0 +1,759 @@
{
"coverage_contradicted": [],
"coverage_unstated": [],
"format": 1,
"hooks": {
"Game::ServerPlayer::OnTechResearched": {
"calls": 2,
"compared": 2,
"coverage": {
"checked_regions": [
"abilities",
"capture_designs",
"design_masks",
"inc_mod",
"modifiers",
"node_bore_ptr",
"res_mod",
"research_target",
"rng",
"roll",
"species_flags",
"suit",
"translation",
"vaccines"
],
"guarded_calls": 2,
"guards": [
"player"
],
"spans": {
"compare": [
"player+0x274:3",
"player+0x278:3",
"player+0x27c:3",
"player+0x2b0:1",
"player+0x278:1"
]
},
"state": "partial",
"undeclared_calls": 2,
"undeclared_writes": 6,
"unmodelled": [
{
"mitigation": "guard:player (EventStorage is inline at ServerPlayer+0x29c)",
"risk": "high",
"what": "posts EVENT_RESEARCH_COMPLETE / _UNDERBUDGET / _TEMPERANCE on the owner's EventStorage when !silent",
"why": "the same class of write as B3's defect, and this hook has no replace-mode oracle that could catch it: gotcha 4 in docs/B2.md says a changed save hash on a completion turn is expected and therefore not a finding"
},
{
"mitigation": "",
"risk": "high",
"what": "writes every owned system's AI flag (CCC_AIVrus / CCC_AISlv), re-evaluates the arcology civilian cap, cures addiction and clears plague across systems AND ships",
"why": "writes through pointers to other objects; compare mode must not touch live state, and no region reaches them"
},
{
"mitigation": "this is the extra draw B3 observed on a completion",
"risk": "high",
"what": "the pending plague-cure roll (ServerPlayer::RollResearchEvent)",
"why": "it draws exactly one word from the strategic generator unconditionally; running it in compare mode would consume real randomness. The two words it guards are still cleared and the record says whether it would have fired"
},
{
"mitigation": "",
"risk": "medium",
"what": "TechTree::SetResearched for the Zuul boarding-pod grant",
"why": "it would mutate the live tree, and it recurses"
},
{
"mitigation": "region:node_bore, declared only when the block already exists",
"risk": "medium",
"what": "allocates or frees the node-bore block at ServerPlayer+0x308",
"why": "ours has no allocator the game's runtime could free, so replace mode calls the game's own updater -- which means replace mode never exercises our node-bore selection at all"
}
],
"verdict": "partial",
"why": ""
},
"diffs": [],
"diverged": 0,
"diverged_call_ids": [],
"errors": 0,
"modes": {
"compare": 2
}
},
"Game::ServerSystem::ProcessTurn": {
"calls": 140,
"compared": 140,
"coverage": {
"checked_regions": [
"bats2",
"bats_mask",
"haltv",
"ibon",
"infra",
"inputs",
"ntdev",
"pbon",
"rcex",
"rcex_mask",
"rng",
"tres"
],
"guarded_calls": 140,
"guards": [
"system"
],
"spans": {
"compare": [
"system+0x1a4:12",
"system+0xd8:1",
"system+0x170:1",
"system+0x238:4"
]
},
"state": "partial",
"undeclared_calls": 6,
"undeclared_writes": 15,
"unmodelled": [
{
"mitigation": "guard:system",
"risk": "high",
"what": "the addiction sweep raises MoraleEvents, which are constructed and appended to the system's capped morale history",
"why": "the same class of write as B3's defect. sim::ProcessColonyTurn does compute the morale events (ColonyTurnResult), but the hook never emits them: DescribeMoraleEvents is dead code, so they are neither compared nor logged"
},
{
"mitigation": "guard:system covers the system object only, not the other objects",
"risk": "high",
"what": "every callee: the plague pass, imperial and civilian growth, the resource debit, in-orbit refuel, slaves, rebellion and the build queue",
"why": "declared input boundary -- ProcessTurn is a dispatcher and only the words it writes itself are modelled. The callees raise EVENT_SLAVES_DEAD, EVENT_SYSTEM_REBELLION_CONTINUES, the plague events and SEBuildCompleted, create ships and bump per-player ShipRecords counters"
},
{
"mitigation": "",
"risk": "medium",
"what": "ApplyInfraBonus / ApplyPopBonus read the owner's home-system id, and the build queue writes the owning ServerPlayer",
"why": "writes through a pointer to another object; no region reaches the player"
},
{
"mitigation": "",
"risk": "low",
"what": "ProcessRebellion is the pass's only RNG consumer and its draw count is data-dependent",
"why": "the generator IS a declared region, so a moved post-state is visible and names the system whose rebellion fired -- it is reported, not modelled"
},
{
"mitigation": "",
"risk": "medium",
"what": "replace mode is refused for this hook",
"why": "our side models the dispatcher's own writes and none of the callees, so a replace run would silently skip a colony's whole turn. There is therefore no oracle layer behind the compare for this hook"
}
],
"verdict": "partial",
"why": ""
},
"diffs": [],
"diverged": 0,
"diverged_call_ids": [],
"errors": 0,
"modes": {
"compare": 140
}
},
"Game::StrategyServer::MoveFleet": {
"calls": 45,
"compared": 45,
"coverage": {
"checked_regions": [
"pos",
"prev_pos",
"rng",
"ship[0].range",
"ship[1].range",
"ship[2].range",
"ship[3].range",
"ship[4].range",
"ship[5].range",
"ship[6].range",
"ship[7].range",
"ship[8].range",
"ship[9].range"
],
"guarded_calls": 45,
"guards": [
"fleet"
],
"spans": {
"compare": [
"fleet+0xa0:4",
"fleet+0xdc:1",
"fleet+0x10c:1",
"fleet+0xcc:1",
"fleet+0xdb:2",
"fleet+0xe0:26"
]
},
"state": "partial",
"undeclared_calls": 15,
"undeclared_writes": 42,
"unmodelled": [
{
"mitigation": "guard:fleet sees the fleet's own words; the event and the system do not",
"risk": "high",
"what": "on arrival: dispatches SEFleetArrived and runs one of three arrival handlers by destination kind (enter system / join fleet / stop at point)",
"why": "declared input boundary -- an arriving call is expected to differ in all of it, and none of it is declared, so the compare says nothing about arrivals"
},
{
"mitigation": "",
"risk": "high",
"what": "on departure: cancels every still-acting ship (with a log line each) and calls ServerSystem::FleetDeparts, which rewrites the system's ownership bits",
"why": "writes through pointers to ships and to the system"
},
{
"mitigation": "",
"risk": "medium",
"what": "the tanker top-up refuels other ships in the fleet",
"why": "the per-ship range regions would show it, but ours does not model it, so a fleet with a tanker diverges for a known reason"
},
{
"mitigation": "declared gap: docs/B4.md",
"risk": "medium",
"what": "a node-line waypoint's step comes from the stutter profile",
"why": "NodeLineStep / BuildStutterSegments are written and unit-tested but not wired in; the hook steps every waypoint type as speed x dt, so a node-line leg is knowingly mis-stepped and only its type is recorded"
},
{
"mitigation": "",
"risk": "medium",
"what": "a missed probabilistic jump scatters the fleet in a random direction",
"why": "the direction is a second draw whose mapping is not modelled; ours leaves the position alone and reports the scatter distance, so the generator region diverges by one word on a miss"
},
{
"mitigation": "",
"risk": "medium",
"what": "the route revalidation and the waypoint list itself",
"why": "declared input boundary; the waypoint vector is not a region"
}
],
"verdict": "partial",
"why": ""
},
"diffs": [
{
"call_id": 42,
"diff": [
{
"orig": {
"t": "f32",
"v": 3.1515913
},
"ours": {
"t": "f32",
"v": 3.15159106
},
"path": "side.pos.after.v.y",
"why": "exact"
}
],
"file": "recap-misc-compare.jsonl",
"line": 44
},
{
"call_id": 79,
"diff": [
{
"orig": {
"t": "f32",
"v": 1.58417809
},
"ours": {
"t": "f32",
"v": 1.58417821
},
"path": "side.pos.after.v.y",
"why": "exact"
}
],
"file": "recap-misc-compare.jsonl",
"line": 81
},
{
"call_id": 80,
"diff": [
{
"orig": {
"t": "f32",
"v": 4.21149492
},
"ours": {
"t": "f32",
"v": 4.21149445
},
"path": "side.pos.after.v.y",
"why": "exact"
}
],
"file": "recap-misc-compare.jsonl",
"line": 82
},
{
"call_id": 116,
"diff": [
{
"orig": {
"t": "f32",
"v": 0.0167649984
},
"ours": {
"t": "f32",
"v": 0.0167651176
},
"path": "side.pos.after.v.y",
"why": "exact"
}
],
"file": "recap-misc-compare.jsonl",
"line": 118
},
{
"call_id": 118,
"diff": [
{
"orig": {
"t": "f32",
"v": 3.14307928
},
"ours": {
"t": "f32",
"v": 3.14307904
},
"path": "side.pos.after.v.y",
"why": "exact"
}
],
"file": "recap-misc-compare.jsonl",
"line": 120
}
],
"diverged": 8,
"diverged_call_ids": [
42,
79,
80,
116,
118,
154,
156,
157
],
"errors": 0,
"modes": {
"compare": 45
}
}
},
"inputs": [
"/tmp/claude-1000/-home-alex/ec8e34f8-af37-4ef2-a309-ed6a15293097/scratchpad/recap-misc-compare.jsonl"
],
"invalid": [],
"kind": "report",
"meta": [
{
"build": "recap-7584bad-20260908T0615Z",
"exe_sha256": "970b7de729956a53094c7eb98aba4270aee98e2fed5daf0d39e290013c90c841",
"format": 1,
"hooks": {
"Game::SectionDictionary::SectionDictionary": {
"coverage": {
"state": "partial",
"unmodelled": [
{
"mitigation": "see docs/M2.md; compare mode for this hook is not safe to run",
"risk": "high",
"what": "LoadSection registers each section with the string table and the live TechTree, and may append to the dictionary's own vector",
"why": "M3 scope; ours delegates to the game's LoadSection after the original has already built all 885 definitions, so the second pass registers duplicates -- the leading hypothesis for this hook's compare-mode crash"
},
{
"mitigation": "",
"risk": "medium",
"what": "post-load validation pass over every definition's @-token against the string table",
"why": "runs after the loop and touches no declared region"
},
{
"mitigation": "",
"risk": "low",
"what": "allocates 885 SectionDef objects (0x3d8 bytes each) on the game heap",
"why": "they do not exist at hook entry; compared by index/species/id/token"
},
{
"mitigation": "guard:dict",
"risk": "low",
"what": "the word at dictionary+0x14",
"why": "not modelled; emitted as an ignored pointer"
},
{
"mitigation": "",
"risk": "medium",
"what": "the before-snapshot of the object is uninitialised heap",
"why": "the hook is on the constructor, so `before` is meaningless and only `after` carries information"
}
],
"why": ""
},
"ftol": 0,
"ftol_kind": "abs",
"ptr": "ignore"
},
"Game::ServerPlayer::ComputeBudget": {
"coverage": {
"state": "partial",
"unmodelled": [
{
"mitigation": "declared input boundary; see budget_inputs.h",
"risk": "high",
"what": "slots 1, 2, 3, 4, 7 and 11 are produced by callees this milestone does not model (per-system output, trade, ship-carried population, a second manager, the build-queue spend)",
"why": "they are copied out of the original's own output and back into the same slots, so they match BY CONSTRUCTION and prove nothing"
},
{
"mitigation": "guard:budget_object does not reach the ships; unverified",
"risk": "high",
"what": "ServerSystem::ComputeOutput repairs damaged ships in orbit",
"why": "replace mode runs the original a second time on a scratch Budget to harvest the six unmodelled slots, so that repair happens TWICE per turn in replace mode and nothing in the trace would show it"
},
{
"mitigation": "",
"risk": "medium",
"what": "the difficulty-mods row from StrategyServer::GetDifficultyMods",
"why": "not reachable from a ServerPlayer, so the two relevant entries are fitted constants measured from the B1 trace rather than snapshotted inputs"
},
{
"mitigation": "",
"risk": "low",
"what": "the research-allocation vector's heap block",
"why": "only the element count is compared; the three words are heap pointers the default policy ignores"
}
],
"why": ""
},
"ftol": 0,
"ftol_kind": "abs",
"ptr": "ignore"
},
"Game::ServerPlayer::OnTechResearched": {
"coverage": {
"state": "partial",
"unmodelled": [
{
"mitigation": "guard:player (EventStorage is inline at ServerPlayer+0x29c)",
"risk": "high",
"what": "posts EVENT_RESEARCH_COMPLETE / _UNDERBUDGET / _TEMPERANCE on the owner's EventStorage when !silent",
"why": "the same class of write as B3's defect, and this hook has no replace-mode oracle that could catch it: gotcha 4 in docs/B2.md says a changed save hash on a completion turn is expected and therefore not a finding"
},
{
"mitigation": "",
"risk": "high",
"what": "writes every owned system's AI flag (CCC_AIVrus / CCC_AISlv), re-evaluates the arcology civilian cap, cures addiction and clears plague across systems AND ships",
"why": "writes through pointers to other objects; compare mode must not touch live state, and no region reaches them"
},
{
"mitigation": "this is the extra draw B3 observed on a completion",
"risk": "high",
"what": "the pending plague-cure roll (ServerPlayer::RollResearchEvent)",
"why": "it draws exactly one word from the strategic generator unconditionally; running it in compare mode would consume real randomness. The two words it guards are still cleared and the record says whether it would have fired"
},
{
"mitigation": "",
"risk": "medium",
"what": "TechTree::SetResearched for the Zuul boarding-pod grant",
"why": "it would mutate the live tree, and it recurses"
},
{
"mitigation": "region:node_bore, declared only when the block already exists",
"risk": "medium",
"what": "allocates or frees the node-bore block at ServerPlayer+0x308",
"why": "ours has no allocator the game's runtime could free, so replace mode calls the game's own updater -- which means replace mode never exercises our node-bore selection at all"
}
],
"why": ""
},
"ftol": 0,
"ftol_kind": "abs",
"ptr": "ignore"
},
"Game::ServerSystem::ProcessTurn": {
"coverage": {
"state": "partial",
"unmodelled": [
{
"mitigation": "guard:system",
"risk": "high",
"what": "the addiction sweep raises MoraleEvents, which are constructed and appended to the system's capped morale history",
"why": "the same class of write as B3's defect. sim::ProcessColonyTurn does compute the morale events (ColonyTurnResult), but the hook never emits them: DescribeMoraleEvents is dead code, so they are neither compared nor logged"
},
{
"mitigation": "guard:system covers the system object only, not the other objects",
"risk": "high",
"what": "every callee: the plague pass, imperial and civilian growth, the resource debit, in-orbit refuel, slaves, rebellion and the build queue",
"why": "declared input boundary -- ProcessTurn is a dispatcher and only the words it writes itself are modelled. The callees raise EVENT_SLAVES_DEAD, EVENT_SYSTEM_REBELLION_CONTINUES, the plague events and SEBuildCompleted, create ships and bump per-player ShipRecords counters"
},
{
"mitigation": "",
"risk": "medium",
"what": "ApplyInfraBonus / ApplyPopBonus read the owner's home-system id, and the build queue writes the owning ServerPlayer",
"why": "writes through a pointer to another object; no region reaches the player"
},
{
"mitigation": "",
"risk": "low",
"what": "ProcessRebellion is the pass's only RNG consumer and its draw count is data-dependent",
"why": "the generator IS a declared region, so a moved post-state is visible and names the system whose rebellion fired -- it is reported, not modelled"
},
{
"mitigation": "",
"risk": "medium",
"what": "replace mode is refused for this hook",
"why": "our side models the dispatcher's own writes and none of the callees, so a replace run would silently skip a colony's whole turn. There is therefore no oracle layer behind the compare for this hook"
}
],
"why": ""
},
"ftol": 0,
"ftol_kind": "abs",
"ptr": "ignore"
},
"Game::StrategyServer::MoveFleet": {
"coverage": {
"state": "partial",
"unmodelled": [
{
"mitigation": "guard:fleet sees the fleet's own words; the event and the system do not",
"risk": "high",
"what": "on arrival: dispatches SEFleetArrived and runs one of three arrival handlers by destination kind (enter system / join fleet / stop at point)",
"why": "declared input boundary -- an arriving call is expected to differ in all of it, and none of it is declared, so the compare says nothing about arrivals"
},
{
"mitigation": "",
"risk": "high",
"what": "on departure: cancels every still-acting ship (with a log line each) and calls ServerSystem::FleetDeparts, which rewrites the system's ownership bits",
"why": "writes through pointers to ships and to the system"
},
{
"mitigation": "",
"risk": "medium",
"what": "the tanker top-up refuels other ships in the fleet",
"why": "the per-ship range regions would show it, but ours does not model it, so a fleet with a tanker diverges for a known reason"
},
{
"mitigation": "declared gap: docs/B4.md",
"risk": "medium",
"what": "a node-line waypoint's step comes from the stutter profile",
"why": "NodeLineStep / BuildStutterSegments are written and unit-tested but not wired in; the hook steps every waypoint type as speed x dt, so a node-line leg is knowingly mis-stepped and only its type is recorded"
},
{
"mitigation": "",
"risk": "medium",
"what": "a missed probabilistic jump scatters the fleet in a random direction",
"why": "the direction is a second draw whose mapping is not modelled; ours leaves the position alone and reports the scatter distance, so the generator region diverges by one word on a miss"
},
{
"mitigation": "",
"risk": "medium",
"what": "the route revalidation and the waypoint list itself",
"why": "declared input boundary; the waypoint vector is not a region"
}
],
"why": ""
},
"ftol": 0,
"ftol_kind": "abs",
"ptr": "ignore"
},
"Game::StrategyServer::ProcessFleetMovement": {
"coverage": {
"state": "partial",
"unmodelled": [
{
"mitigation": "",
"risk": "high",
"what": "`ours` re-reads the LIVE fleet list after the original has run",
"why": "the gate-traffic total is computed by the original at the very end of the pass, so a pre-call snapshot would diverge for the wrong reason. It breaks the compare invariant that ours never touches live memory, and it makes this hook's verdict partly self-fulfilling: the input to our arithmetic is the original's own post-move state"
},
{
"mitigation": "",
"risk": "high",
"what": "drives MoveFleet up to five times per fleet",
"why": "every undeclared effect of MoveFleet happens inside this call too; the pass schedule is recorded in the arguments but never compared"
},
{
"mitigation": "",
"risk": "high",
"what": "writes FPdpos into every fleet and clears flags 0x2 and 0x100 on every fleet",
"why": "no region covers the fleets, only the players' gate-traffic words"
},
{
"mitigation": "",
"risk": "high",
"what": "OnFleetArrived posts EVENT_FLEET_ARRIVED",
"why": "the same class of write as B3's defect, and there is no replace mode for this hook, so nothing behind the compare could catch it either"
},
{
"mitigation": "",
"risk": "medium",
"what": "the original accumulates by player->index but writes back by the player's position in the server vector, into a fixed 32-int array with no bounds check",
"why": "a real latent bug in the original that our side reproduces only while index == position; the reference save never separates them"
},
{
"mitigation": "",
"risk": "medium",
"what": "PassSchedule() is never called by the hook, and FleetSummary::targetFleetId / relation are never filled",
"why": "the header claims ours predicts the call order for a trace to check; that prediction is not actually emitted"
}
],
"why": ""
},
"ftol": 0,
"ftol_kind": "abs",
"ptr": "ignore"
},
"Game::TechTree::ProcessResearch": {
"coverage": {
"state": "partial",
"unmodelled": [
{
"mitigation": "region:events (EvNxID now diverges instead of passing silently)",
"risk": "high",
"what": "posts EVENT_RESEARCH_OVERBUDGET on the owner's EventStorage in the same branch that sets node.flag = 2",
"why": "the message text is composed from the tech name, so ours cannot synthesise it; it would have to be posted through the game's own event API. This is the defect that made a clean compare false: replace mode's autosave differed from the oracle by exactly this one event"
},
{
"mitigation": "region:events",
"risk": "medium",
"what": "posts EVENT_TECHS_UNLOCKED for nodes that became available this turn",
"why": "the trailing unlock loop makes no draw and writes no node, but it does build a names list and post an event"
},
{
"mitigation": "guard:player, guard:tree_header",
"risk": "high",
"what": "TechTree::SetResearched on completion: the turn/order stamps, the child unlock cascade, the recursive research of zero-cost children, and the owner's OnTechResearched callback",
"why": "its own milestone (B2); the callback writes live player state that compare mode must not touch, and it consumes one extra RNG word"
},
{
"mitigation": "guard:tree_header",
"risk": "medium",
"what": "bumps the tree's completion-order counter (TechTree+0x20)",
"why": "part of SetResearched; the per-node `order` word is compared but the counter it comes from was not a region"
},
{
"mitigation": "",
"risk": "low",
"what": "writes a completion line to the game log",
"why": "log text is not simulation state"
}
],
"why": ""
},
"ftol": 0,
"ftol_kind": "abs",
"ptr": "ignore"
},
"Game::WeaponDictionary::Init": {
"coverage": {
"state": "partial",
"unmodelled": [
{
"mitigation": "suspected cause of the sibling section hook's compare crash (docs/M2.md)",
"risk": "high",
"what": "LoadWeapon -> WeaponDef::ParseScript registers each weapon's name with the string table and resolves `requires` against the live TechTree",
"why": "per-file parsing is M3 scope; ours delegates to the game's own LoadWeapon, so a compare run performs the registration a SECOND time and neither the string table nor the tech tree is a declared region"
},
{
"mitigation": "",
"risk": "low",
"what": "allocates 123 WeaponDef objects (0x278 bytes each) on the game heap",
"why": "the definitions do not exist when the hook is entered, so they cannot be a before-snapshot; the dictionary region compares them by id/name/path"
},
{
"mitigation": "guard:dict",
"risk": "low",
"what": "the word at dictionary+0x14",
"why": "not modelled; emitted as an opaque pointer, which the default policy ignores -- a change is visible in a trace but never a divergence"
},
{
"mitigation": "",
"risk": "low",
"what": "writes lines to the game log for a missing manifest",
"why": "log text is not simulation state"
},
{
"mitigation": "",
"risk": "low",
"what": "std::sort tie order for equal weapon names",
"why": "msvc_sort.h replays MSVC 2010's introsort, but the shipped data has no tied names, so the tie rule is unexercised rather than verified"
}
],
"why": ""
},
"ftol": 0,
"ftol_kind": "abs",
"ptr": "ignore"
},
"Mars::GlobalConsts::LoadFile": {
"coverage": {
"state": "partial",
"unmodelled": [
{
"mitigation": "LoadAll's post-state would have to be hooked to see it",
"risk": "medium",
"what": "erases each consumed key from the caller's std::map",
"why": "the map is a LoadAll temporary; declaring a red-black tree as a region is not possible before the call. First-occurrence-wins is reproduced in game::config::apply instead, so the *effect* is modelled, the container is not"
},
{
"mitigation": "",
"risk": "low",
"what": "writes three kinds of line to the game log (unrecognised key, applied key, expected-but-not-found)",
"why": "log text is not part of the simulation state"
},
{
"mitigation": "",
"risk": "low",
"what": "opens the file through the VFS and allocates/releases a refcounted buffer",
"why": "ours performs the same two calls, so allocation behaviour matches by construction rather than by comparison"
},
{
"mitigation": "",
"risk": "low",
"what": "String slots assign through the engine's own std::string, leaking one heap block per long string in compare mode",
"why": "start-up only; documented in docs/M1.md"
}
],
"why": ""
},
"ftol": 0,
"ftol_kind": "abs",
"ptr": "ignore"
},
"Shim::SelfTest::Fill": {
"coverage": {
"state": "complete",
"unmodelled": [],
"why": "Fill writes buf[0..n) and nothing else; the whole range is a declared region"
},
"ftol": 0,
"ftol_kind": "abs",
"ptr": "ignore"
}
},
"inline_max": 256,
"started": "2026-09-08T06:47:07Z"
}
],
"totals": {
"calls": 187,
"compared": 187,
"coverage_contradicted": 0,
"coverage_unstated": 0,
"diverged": 8,
"guarded_calls": 187,
"invalid_records": 0,
"undeclared_calls": 23,
"undeclared_writes": 63
},
"warnings": []
}

View file

@ -0,0 +1,57 @@
## tracecmp report: recap-misc-compare.jsonl
- build: recap-7584bad-20260908T0615Z started: 2026-09-08T06:47:07Z inline_max: 256
- calls: 187 compared: 187 diverged: 8 invalid records: 0 warnings: 0
- coverage: 187 guarded call(s), 63 undeclared write(s) in 23 call(s); 0 hook(s) unstated, 0 contradicted
| hook | calls | modes | compared | diverged | errors |
|---|---|---|---|---|---|
| Game::ServerPlayer::OnTechResearched | 2 | compare:2 | 2 | 0 | 0 |
| Game::ServerSystem::ProcessTurn | 140 | compare:140 | 140 | 0 | 0 |
| Game::StrategyServer::MoveFleet | 45 | compare:45 | 45 | 8 | 0 |
### coverage
| hook | verdict | compared regions | guards | undeclared writes | unmodelled |
|---|---|---|---|---|---|
| Game::ServerPlayer::OnTechResearched | partial | abilities, capture_designs, design_masks, inc_mod, modifiers, node_bore_ptr, +8 | player | 6 in 2 call(s) | 5 |
| Game::ServerSystem::ProcessTurn | partial | bats2, bats_mask, haltv, ibon, infra, inputs, +6 | system | 15 in 6 call(s) | 5 |
| Game::StrategyServer::MoveFleet | partial | pos, prev_pos, rng, ship[0].range, ship[1].range, ship[2].range, +7 | fleet | 42 in 15 call(s) | 6 |
#### Game::ServerPlayer::OnTechResearched — not checked by this run
- (high) posts EVENT_RESEARCH_COMPLETE / _UNDERBUDGET / _TEMPERANCE on the owner's EventStorage when !silent — the same class of write as B3's defect, and this hook has no replace-mode oracle that could catch it: gotcha 4 in docs/B2.md says a changed save hash on a completion turn is expected and therefore not a finding [guard:player (EventStorage is inline at ServerPlayer+0x29c)]
- (high) writes every owned system's AI flag (CCC_AIVrus / CCC_AISlv), re-evaluates the arcology civilian cap, cures addiction and clears plague across systems AND ships — writes through pointers to other objects; compare mode must not touch live state, and no region reaches them
- (high) the pending plague-cure roll (ServerPlayer::RollResearchEvent) — it draws exactly one word from the strategic generator unconditionally; running it in compare mode would consume real randomness. The two words it guards are still cleared and the record says whether it would have fired [this is the extra draw B3 observed on a completion]
- (medium) TechTree::SetResearched for the Zuul boarding-pod grant — it would mutate the live tree, and it recurses
- (medium) allocates or frees the node-bore block at ServerPlayer+0x308 — ours has no allocator the game's runtime could free, so replace mode calls the game's own updater -- which means replace mode never exercises our node-bore selection at all [region:node_bore, declared only when the block already exists]
- guard hits in compare mode: player+0x274:3, player+0x278:3, player+0x27c:3, player+0x2b0:1, player+0x278:1
#### Game::ServerSystem::ProcessTurn — not checked by this run
- (high) the addiction sweep raises MoraleEvents, which are constructed and appended to the system's capped morale history — the same class of write as B3's defect. sim::ProcessColonyTurn does compute the morale events (ColonyTurnResult), but the hook never emits them: DescribeMoraleEvents is dead code, so they are neither compared nor logged [guard:system]
- (high) every callee: the plague pass, imperial and civilian growth, the resource debit, in-orbit refuel, slaves, rebellion and the build queue — declared input boundary -- ProcessTurn is a dispatcher and only the words it writes itself are modelled. The callees raise EVENT_SLAVES_DEAD, EVENT_SYSTEM_REBELLION_CONTINUES, the plague events and SEBuildCompleted, create ships and bump per-player ShipRecords counters [guard:system covers the system object only, not the other objects]
- (medium) ApplyInfraBonus / ApplyPopBonus read the owner's home-system id, and the build queue writes the owning ServerPlayer — writes through a pointer to another object; no region reaches the player
- (low) ProcessRebellion is the pass's only RNG consumer and its draw count is data-dependent — the generator IS a declared region, so a moved post-state is visible and names the system whose rebellion fired -- it is reported, not modelled
- (medium) replace mode is refused for this hook — our side models the dispatcher's own writes and none of the callees, so a replace run would silently skip a colony's whole turn. There is therefore no oracle layer behind the compare for this hook
- guard hits in compare mode: system+0x1a4:12, system+0xd8:1, system+0x170:1, system+0x238:4
#### Game::StrategyServer::MoveFleet — not checked by this run
- (high) on arrival: dispatches SEFleetArrived and runs one of three arrival handlers by destination kind (enter system / join fleet / stop at point) — declared input boundary -- an arriving call is expected to differ in all of it, and none of it is declared, so the compare says nothing about arrivals [guard:fleet sees the fleet's own words; the event and the system do not]
- (high) on departure: cancels every still-acting ship (with a log line each) and calls ServerSystem::FleetDeparts, which rewrites the system's ownership bits — writes through pointers to ships and to the system
- (medium) the tanker top-up refuels other ships in the fleet — the per-ship range regions would show it, but ours does not model it, so a fleet with a tanker diverges for a known reason
- (medium) a node-line waypoint's step comes from the stutter profile — NodeLineStep / BuildStutterSegments are written and unit-tested but not wired in; the hook steps every waypoint type as speed x dt, so a node-line leg is knowingly mis-stepped and only its type is recorded [declared gap: docs/B4.md]
- (medium) a missed probabilistic jump scatters the fleet in a random direction — the direction is a second draw whose mapping is not modelled; ours leaves the position alone and reports the scatter distance, so the generator region diverges by one word on a miss
- (medium) the route revalidation and the waypoint list itself — declared input boundary; the waypoint vector is not a region
- guard hits in compare mode: fleet+0xa0:4, fleet+0xdc:1, fleet+0x10c:1, fleet+0xcc:1, fleet+0xdb:2, fleet+0xe0:26
### Game::StrategyServer::MoveFleet: first 5 of 8 divergent call(s)
- call_id 42 (recap-misc-compare.jsonl:44)
side.pos.after.v.y [exact] orig={"t":"f32","v":3.1515913} ours={"t":"f32","v":3.15159106}
- call_id 79 (recap-misc-compare.jsonl:81)
side.pos.after.v.y [exact] orig={"t":"f32","v":1.58417809} ours={"t":"f32","v":1.58417821}
- call_id 80 (recap-misc-compare.jsonl:82)
side.pos.after.v.y [exact] orig={"t":"f32","v":4.21149492} ours={"t":"f32","v":4.21149445}
- call_id 116 (recap-misc-compare.jsonl:118)
side.pos.after.v.y [exact] orig={"t":"f32","v":0.0167649984} ours={"t":"f32","v":0.0167651176}
- call_id 118 (recap-misc-compare.jsonl:120)
side.pos.after.v.y [exact] orig={"t":"f32","v":3.14307928} ours={"t":"f32","v":3.14307904}
other divergent call_ids: [154, 156, 157]

View file

@ -0,0 +1,39 @@
02:40:02.429 [tid 5208] ==== sots-engine shim (binkw32 proxy) build recap-7584bad-20260908T0615Z ====
02:40:02.429 [tid 5208] exe: C:\SOTS\Sword of the Stars.exe
02:40:02.429 [tid 5208] exe base=0x00e80000 (link-time image base 0x00400000, ASLR delta +11010048) pid=7572 shim=696b0000
02:40:02.429 [tid 5208] addresses: Source: sots-re ghidra/addresses.json @ ff67ec0, generated 2026-09-08 by tools/gen_addresses.py
02:40:02.429 [tid 5208] config: hooks=trace
02:40:02.429 [tid 5208] config: hook.Shim::SelfTest::Fill=off
02:40:02.429 [tid 5208] config: hook.Mars::GlobalConsts::LoadFile=off
02:40:02.429 [tid 5208] config: hook.Game::WeaponDictionary::Init=off
02:40:02.429 [tid 5208] config: hook.Game::SectionDictionary::SectionDictionary=off
02:40:02.429 [tid 5208] config: hook.Game::TechTree::ProcessResearch=off
02:40:02.429 [tid 5208] config: hook.Game::ServerPlayer::OnTechResearched=off
02:40:02.429 [tid 5208] config: hook.Game::ServerSystem::ProcessTurn=off
02:40:02.429 [tid 5208] config: hook.Game::StrategyServer::MoveFleet=off
02:40:02.429 [tid 5208] config: hook.Game::StrategyServer::ProcessFleetMovement=off
02:40:02.429 [tid 5208] config: hook.Game::ServerPlayer::ComputeBudget=compare
02:40:02.429 [tid 5208] config: trace.path=C:\SOTS\shim.trace.jsonl
02:40:02.429 [tid 5208] config: trace.inline_max=256
02:40:02.429 [tid 5208] config: trace.flush=always
02:40:02.554 [tid 5208] trace: C:\SOTS\shim.trace.jsonl (default mode trace, inline_max 256, flush always)
02:40:02.554 [tid 5208] hook: Mars_Application_Initialize rva=0x004a0e50 -> va=01320e50
02:40:02.554 [tid 5208] hook: MH_Initialize -> MH_OK
02:40:02.554 [tid 5208] hook: MH_CreateHook -> MH_OK (trampoline=01af0fe0)
02:40:02.569 [tid 5208] hook: MH_EnableHook -> MH_OK
02:40:02.569 [tid 5208] cfg: GlobalConsts hook ready (scale constant 0.017453292519943295)
02:40:02.569 [tid 5208] hook: Mars::GlobalConsts::LoadFile rva=0x004b73c0 mode=off (not installed)
02:40:02.569 [tid 5208] dict: dictionaries hook ready (crt new=6adc232b delete=6adc0174)
02:40:02.569 [tid 5208] hook: Game::WeaponDictionary::Init rva=0x0019a4c0 mode=off (not installed)
02:40:02.569 [tid 5208] hook: Game::SectionDictionary::SectionDictionary rva=0x00176f40 mode=off (not installed)
02:40:02.569 [tid 5208] research: ProcessResearch hook ready (Cost=00ffda00, node=0x34, rng=0x9cc, fpu_cw=0x027f)
02:40:02.569 [tid 5208] hook: Game::TechTree::ProcessResearch rva=0x001876c0 mode=off (not installed)
02:40:02.569 [tid 5208] techfx: OnTechResearched hook ready (regions=15, gate=0/0, fpu_cw=0x027f)
02:40:02.569 [tid 5208] hook: Game::ServerPlayer::OnTechResearched rva=0x00491790 mode=off (not installed)
02:40:02.569 [tid 5208] hook: Game::ServerPlayer::ComputeBudget rva=0x00463030 -> va=012e3030 MH_CreateHook -> MH_OK (trampoline=01af0fc0)
02:40:02.585 [tid 5208] hook: Game::ServerPlayer::ComputeBudget MH_EnableHook -> MH_OK mode=compare
02:40:02.585 [tid 5208] hook: Game::ServerSystem::ProcessTurn rva=0x003598e0 mode=off (not installed)
02:40:02.585 [tid 5208] hook: Game::StrategyServer::MoveFleet rva=0x003d9ee0 mode=off (not installed)
02:40:02.585 [tid 5208] hook: Game::StrategyServer::ProcessFleetMovement rva=0x003da9a0 mode=off (not installed)
02:40:02.585 [tid 5208] selftest: Shim::SelfTest::Fill mode=off checksum=075ef0c3 records=0
02:40:02.976 [tid 5208] Application::Initialize called (this=020c8128)

View file

@ -0,0 +1,39 @@
02:28:40.170 [tid 1412] ==== sots-engine shim (binkw32 proxy) build recap-7584bad-20260908T0615Z ====
02:28:40.170 [tid 1412] exe: C:\SOTS\Sword of the Stars.exe
02:28:40.170 [tid 1412] exe base=0x00e80000 (link-time image base 0x00400000, ASLR delta +11010048) pid=2320 shim=696b0000
02:28:40.170 [tid 1412] addresses: Source: sots-re ghidra/addresses.json @ ff67ec0, generated 2026-09-08 by tools/gen_addresses.py
02:28:40.170 [tid 1412] config: hooks=trace
02:28:40.170 [tid 1412] config: hook.Shim::SelfTest::Fill=off
02:28:40.170 [tid 1412] config: hook.Mars::GlobalConsts::LoadFile=off
02:28:40.170 [tid 1412] config: hook.Game::WeaponDictionary::Init=off
02:28:40.170 [tid 1412] config: hook.Game::SectionDictionary::SectionDictionary=off
02:28:40.170 [tid 1412] config: hook.Game::ServerPlayer::ComputeBudget=off
02:28:40.170 [tid 1412] config: hook.Game::ServerPlayer::OnTechResearched=off
02:28:40.170 [tid 1412] config: hook.Game::ServerSystem::ProcessTurn=off
02:28:40.170 [tid 1412] config: hook.Game::StrategyServer::MoveFleet=off
02:28:40.170 [tid 1412] config: hook.Game::StrategyServer::ProcessFleetMovement=off
02:28:40.170 [tid 1412] config: hook.Game::TechTree::ProcessResearch=compare
02:28:40.170 [tid 1412] config: trace.path=C:\SOTS\shim.trace.jsonl
02:28:40.170 [tid 1412] config: trace.inline_max=256
02:28:40.170 [tid 1412] config: trace.flush=always
02:28:40.232 [tid 1412] trace: C:\SOTS\shim.trace.jsonl (default mode trace, inline_max 256, flush always)
02:28:40.232 [tid 1412] hook: Mars_Application_Initialize rva=0x004a0e50 -> va=01320e50
02:28:40.232 [tid 1412] hook: MH_Initialize -> MH_OK
02:28:40.232 [tid 1412] hook: MH_CreateHook -> MH_OK (trampoline=01b00fe0)
02:28:40.248 [tid 1412] hook: MH_EnableHook -> MH_OK
02:28:40.248 [tid 1412] cfg: GlobalConsts hook ready (scale constant 0.017453292519943295)
02:28:40.248 [tid 1412] hook: Mars::GlobalConsts::LoadFile rva=0x004b73c0 mode=off (not installed)
02:28:40.248 [tid 1412] dict: dictionaries hook ready (crt new=6adc232b delete=6adc0174)
02:28:40.248 [tid 1412] hook: Game::WeaponDictionary::Init rva=0x0019a4c0 mode=off (not installed)
02:28:40.248 [tid 1412] hook: Game::SectionDictionary::SectionDictionary rva=0x00176f40 mode=off (not installed)
02:28:40.248 [tid 1412] research: ProcessResearch hook ready (Cost=00ffda00, node=0x34, rng=0x9cc, fpu_cw=0x027f)
02:28:40.248 [tid 1412] hook: Game::TechTree::ProcessResearch rva=0x001876c0 -> va=010076c0 MH_CreateHook -> MH_OK (trampoline=01b00fc0)
02:28:40.264 [tid 1412] hook: Game::TechTree::ProcessResearch MH_EnableHook -> MH_OK mode=compare
02:28:40.264 [tid 1412] techfx: OnTechResearched hook ready (regions=15, gate=0/0, fpu_cw=0x027f)
02:28:40.264 [tid 1412] hook: Game::ServerPlayer::OnTechResearched rva=0x00491790 mode=off (not installed)
02:28:40.264 [tid 1412] hook: Game::ServerPlayer::ComputeBudget rva=0x00463030 mode=off (not installed)
02:28:40.264 [tid 1412] hook: Game::ServerSystem::ProcessTurn rva=0x003598e0 mode=off (not installed)
02:28:40.264 [tid 1412] hook: Game::StrategyServer::MoveFleet rva=0x003d9ee0 mode=off (not installed)
02:28:40.264 [tid 1412] hook: Game::StrategyServer::ProcessFleetMovement rva=0x003da9a0 mode=off (not installed)
02:28:40.264 [tid 1412] selftest: Shim::SelfTest::Fill mode=off checksum=075ef0c3 records=0
02:28:40.279 [tid 1412] Application::Initialize called (this=01b18128)

View file

@ -0,0 +1,43 @@
02:47:07.341 [tid 7656] ==== sots-engine shim (binkw32 proxy) build recap-7584bad-20260908T0615Z ====
02:47:07.341 [tid 7656] exe: C:\SOTS\Sword of the Stars.exe
02:47:07.341 [tid 7656] exe base=0x00e80000 (link-time image base 0x00400000, ASLR delta +11010048) pid=4552 shim=696b0000
02:47:07.341 [tid 7656] addresses: Source: sots-re ghidra/addresses.json @ ff67ec0, generated 2026-09-08 by tools/gen_addresses.py
02:47:07.341 [tid 7656] config: hooks=trace
02:47:07.341 [tid 7656] config: hook.Shim::SelfTest::Fill=off
02:47:07.341 [tid 7656] config: hook.Mars::GlobalConsts::LoadFile=off
02:47:07.341 [tid 7656] config: hook.Game::WeaponDictionary::Init=off
02:47:07.341 [tid 7656] config: hook.Game::SectionDictionary::SectionDictionary=off
02:47:07.341 [tid 7656] config: hook.Game::TechTree::ProcessResearch=off
02:47:07.341 [tid 7656] config: hook.Game::ServerPlayer::ComputeBudget=off
02:47:07.341 [tid 7656] config: hook.Game::StrategyServer::ProcessFleetMovement=off
02:47:07.341 [tid 7656] config: hook.Game::ServerPlayer::OnTechResearched=compare
02:47:07.341 [tid 7656] config: hook.Game::ServerSystem::ProcessTurn=compare
02:47:07.341 [tid 7656] config: hook.Game::StrategyServer::MoveFleet=compare
02:47:07.341 [tid 7656] config: trace.path=C:\SOTS\shim.trace.jsonl
02:47:07.341 [tid 7656] config: trace.inline_max=256
02:47:07.341 [tid 7656] config: trace.flush=always
02:47:07.403 [tid 7656] trace: C:\SOTS\shim.trace.jsonl (default mode trace, inline_max 256, flush always)
02:47:07.403 [tid 7656] hook: Mars_Application_Initialize rva=0x004a0e50 -> va=01320e50
02:47:07.403 [tid 7656] hook: MH_Initialize -> MH_OK
02:47:07.403 [tid 7656] hook: MH_CreateHook -> MH_OK (trampoline=038f0fe0)
02:47:07.434 [tid 7656] hook: MH_EnableHook -> MH_OK
02:47:07.434 [tid 7656] cfg: GlobalConsts hook ready (scale constant 0.017453292519943295)
02:47:07.434 [tid 7656] hook: Mars::GlobalConsts::LoadFile rva=0x004b73c0 mode=off (not installed)
02:47:07.434 [tid 7656] dict: dictionaries hook ready (crt new=6adc232b delete=6adc0174)
02:47:07.434 [tid 7656] hook: Game::WeaponDictionary::Init rva=0x0019a4c0 mode=off (not installed)
02:47:07.434 [tid 7656] hook: Game::SectionDictionary::SectionDictionary rva=0x00176f40 mode=off (not installed)
02:47:07.434 [tid 7656] research: ProcessResearch hook ready (Cost=00ffda00, node=0x34, rng=0x9cc, fpu_cw=0x027f)
02:47:07.434 [tid 7656] hook: Game::TechTree::ProcessResearch rva=0x001876c0 mode=off (not installed)
02:47:07.434 [tid 7656] techfx: OnTechResearched hook ready (regions=15, gate=0/0, fpu_cw=0x027f)
02:47:07.434 [tid 7656] hook: Game::ServerPlayer::OnTechResearched rva=0x00491790 -> va=01311790 MH_CreateHook -> MH_OK (trampoline=038f0fc0)
02:47:07.450 [tid 7656] hook: Game::ServerPlayer::OnTechResearched MH_EnableHook -> MH_OK mode=compare
02:47:07.450 [tid 7656] hook: Game::ServerPlayer::ComputeBudget rva=0x00463030 mode=off (not installed)
02:47:07.450 [tid 7656] hook: Game::ServerSystem::ProcessTurn rva=0x003598e0 -> va=011d98e0 MH_CreateHook -> MH_OK (trampoline=038f0fa0)
02:47:07.466 [tid 7656] hook: Game::ServerSystem::ProcessTurn MH_EnableHook -> MH_OK mode=compare
02:47:07.466 [tid 7656] hook: Game::StrategyServer::MoveFleet rva=0x003d9ee0 -> va=01259ee0 MH_CreateHook -> MH_OK (trampoline=038f0f80)
02:47:07.481 [tid 7656] hook: Game::StrategyServer::MoveFleet MH_EnableHook -> MH_OK mode=compare
02:47:07.481 [tid 7656] hook: Game::StrategyServer::ProcessFleetMovement rva=0x003da9a0 mode=off (not installed)
02:47:07.481 [tid 7656] selftest: Shim::SelfTest::Fill mode=off checksum=075ef0c3 records=0
02:47:07.497 [tid 7656] Application::Initialize called (this=03c68128)
02:52:42.722 [tid 7656] techfx: ours mode=compare id=10001 granted=10197 plague=0x00 systems_ai=0 civcaps=0 temperance=0x00 bore_changed=0 bore_present=0 roll=0 value=-1
02:53:45.910 [tid 7656] techfx: ours mode=compare id=10094 granted=10197 plague=0x00 systems_ai=0 civcaps=0 temperance=0x00 bore_changed=0 bore_present=0 roll=1 value=0.316456825

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

View file

@ -0,0 +1,43 @@
02:18:02.517 [tid 8008] ==== sots-engine shim (binkw32 proxy) build recap-7584bad-20260908T0615Z ====
02:18:02.517 [tid 8008] exe: C:\SOTS\Sword of the Stars.exe
02:18:02.517 [tid 8008] exe base=0x00e80000 (link-time image base 0x00400000, ASLR delta +11010048) pid=2856 shim=696b0000
02:18:02.517 [tid 8008] addresses: Source: sots-re ghidra/addresses.json @ ff67ec0, generated 2026-09-08 by tools/gen_addresses.py
02:18:02.517 [tid 8008] config: hooks=trace
02:18:02.517 [tid 8008] config: hook.Shim::SelfTest::Fill=off
02:18:02.517 [tid 8008] config: hook.Mars::GlobalConsts::LoadFile=off
02:18:02.517 [tid 8008] config: hook.Game::WeaponDictionary::Init=off
02:18:02.517 [tid 8008] config: hook.Game::SectionDictionary::SectionDictionary=off
02:18:02.517 [tid 8008] config: hook.Game::StrategyServer::ProcessFleetMovement=off
02:18:02.517 [tid 8008] config: hook.Game::TechTree::ProcessResearch=trace
02:18:02.517 [tid 8008] config: hook.Game::ServerPlayer::ComputeBudget=trace
02:18:02.517 [tid 8008] config: hook.Game::ServerPlayer::OnTechResearched=trace
02:18:02.517 [tid 8008] config: hook.Game::ServerSystem::ProcessTurn=trace
02:18:02.517 [tid 8008] config: hook.Game::StrategyServer::MoveFleet=trace
02:18:02.517 [tid 8008] config: trace.path=C:\SOTS\shim.trace.jsonl
02:18:02.517 [tid 8008] config: trace.inline_max=256
02:18:02.517 [tid 8008] config: trace.flush=always
02:18:02.579 [tid 8008] trace: C:\SOTS\shim.trace.jsonl (default mode trace, inline_max 256, flush always)
02:18:02.579 [tid 8008] hook: Mars_Application_Initialize rva=0x004a0e50 -> va=01320e50
02:18:02.579 [tid 8008] hook: MH_Initialize -> MH_OK
02:18:02.579 [tid 8008] hook: MH_CreateHook -> MH_OK (trampoline=01c00fe0)
02:18:02.595 [tid 8008] hook: MH_EnableHook -> MH_OK
02:18:02.595 [tid 8008] cfg: GlobalConsts hook ready (scale constant 0.017453292519943295)
02:18:02.595 [tid 8008] hook: Mars::GlobalConsts::LoadFile rva=0x004b73c0 mode=off (not installed)
02:18:02.595 [tid 8008] dict: dictionaries hook ready (crt new=6adc232b delete=6adc0174)
02:18:02.595 [tid 8008] hook: Game::WeaponDictionary::Init rva=0x0019a4c0 mode=off (not installed)
02:18:02.595 [tid 8008] hook: Game::SectionDictionary::SectionDictionary rva=0x00176f40 mode=off (not installed)
02:18:02.595 [tid 8008] research: ProcessResearch hook ready (Cost=00ffda00, node=0x34, rng=0x9cc, fpu_cw=0x027f)
02:18:02.595 [tid 8008] hook: Game::TechTree::ProcessResearch rva=0x001876c0 -> va=010076c0 MH_CreateHook -> MH_OK (trampoline=01c00fc0)
02:18:02.610 [tid 8008] hook: Game::TechTree::ProcessResearch MH_EnableHook -> MH_OK mode=trace
02:18:02.610 [tid 8008] techfx: OnTechResearched hook ready (regions=15, gate=0/0, fpu_cw=0x027f)
02:18:02.610 [tid 8008] hook: Game::ServerPlayer::OnTechResearched rva=0x00491790 -> va=01311790 MH_CreateHook -> MH_OK (trampoline=01c00fa0)
02:18:02.626 [tid 8008] hook: Game::ServerPlayer::OnTechResearched MH_EnableHook -> MH_OK mode=trace
02:18:02.626 [tid 8008] hook: Game::ServerPlayer::ComputeBudget rva=0x00463030 -> va=012e3030 MH_CreateHook -> MH_OK (trampoline=01c00f80)
02:18:02.642 [tid 8008] hook: Game::ServerPlayer::ComputeBudget MH_EnableHook -> MH_OK mode=trace
02:18:02.642 [tid 8008] hook: Game::ServerSystem::ProcessTurn rva=0x003598e0 -> va=011d98e0 MH_CreateHook -> MH_OK (trampoline=01c00f60)
02:18:02.642 [tid 8008] hook: Game::ServerSystem::ProcessTurn MH_EnableHook -> MH_OK mode=trace
02:18:02.642 [tid 8008] hook: Game::StrategyServer::MoveFleet rva=0x003d9ee0 -> va=01259ee0 MH_CreateHook -> MH_OK (trampoline=01c00f40)
02:18:02.657 [tid 8008] hook: Game::StrategyServer::MoveFleet MH_EnableHook -> MH_OK mode=trace
02:18:02.657 [tid 8008] hook: Game::StrategyServer::ProcessFleetMovement rva=0x003da9a0 mode=off (not installed)
02:18:02.657 [tid 8008] selftest: Shim::SelfTest::Fill mode=off checksum=075ef0c3 records=0
02:18:02.673 [tid 8008] Application::Initialize called (this=03c98128)

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long