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:
parent
d738b6b4bb
commit
05095e93da
22 changed files with 4926 additions and 8 deletions
|
|
@ -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 |
|
||||
|
|
|
|||
291
findings/subsystems/golden-trace-recapture.md
Normal file
291
findings/subsystems/golden-trace-recapture.md
Normal 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.
|
||||
470
verify/results/compare/recap-b1-compare.json
Normal file
470
verify/results/compare/recap-b1-compare.json
Normal 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": []
|
||||
}
|
||||
21
verify/results/compare/recap-b1-compare.md
Normal file
21
verify/results/compare/recap-b1-compare.md
Normal 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
|
||||
1088
verify/results/compare/recap-b3-compare-t1-5.json
Normal file
1088
verify/results/compare/recap-b3-compare-t1-5.json
Normal file
File diff suppressed because it is too large
Load diff
47
verify/results/compare/recap-b3-compare-t1-5.md
Normal file
47
verify/results/compare/recap-b3-compare-t1-5.md
Normal 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
|
||||
772
verify/results/compare/recap-b3-compare-t1.json
Normal file
772
verify/results/compare/recap-b3-compare-t1.json
Normal 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": []
|
||||
}
|
||||
26
verify/results/compare/recap-b3-compare-t1.md
Normal file
26
verify/results/compare/recap-b3-compare-t1.md
Normal 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}
|
||||
964
verify/results/compare/recap-golden-trace.json
Normal file
964
verify/results/compare/recap-golden-trace.json
Normal 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": []
|
||||
}
|
||||
51
verify/results/compare/recap-golden-trace.md
Normal file
51
verify/results/compare/recap-golden-trace.md
Normal 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
|
||||
759
verify/results/compare/recap-misc-compare.json
Normal file
759
verify/results/compare/recap-misc-compare.json
Normal 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": []
|
||||
}
|
||||
57
verify/results/compare/recap-misc-compare.md
Normal file
57
verify/results/compare/recap-misc-compare.md
Normal 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]
|
||||
39
verify/results/shim/recap-b1-shim.log
Normal file
39
verify/results/shim/recap-b1-shim.log
Normal 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)
|
||||
39
verify/results/shim/recap-b3-shim.log
Normal file
39
verify/results/shim/recap-b3-shim.log
Normal 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)
|
||||
43
verify/results/shim/recap-misc-shim.log
Normal file
43
verify/results/shim/recap-misc-shim.log
Normal 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
|
||||
BIN
verify/results/shim/recap-misc-turn7.png
Normal file
BIN
verify/results/shim/recap-misc-turn7.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 166 KiB |
43
verify/results/shim/recap-trace-shim.log
Normal file
43
verify/results/shim/recap-trace-shim.log
Normal 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)
|
||||
BIN
verify/results/shim/recap-turn2-loaded.png
Normal file
BIN
verify/results/shim/recap-turn2-loaded.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 163 KiB |
BIN
verify/traces/recap-b1-compare.jsonl.gz
Normal file
BIN
verify/traces/recap-b1-compare.jsonl.gz
Normal file
Binary file not shown.
16
verify/traces/recap-b3-compare.jsonl
Normal file
16
verify/traces/recap-b3-compare.jsonl
Normal file
File diff suppressed because one or more lines are too long
BIN
verify/traces/recap-golden-trace.jsonl.gz
Normal file
BIN
verify/traces/recap-golden-trace.jsonl.gz
Normal file
Binary file not shown.
188
verify/traces/recap-misc-compare.jsonl
Normal file
188
verify/traces/recap-misc-compare.jsonl
Normal file
File diff suppressed because one or more lines are too long
Loading…
Add table
Reference in a new issue