{ "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]", "observed_techs", "overbudget", "rng" ], "guarded_calls": 3, "guards": [ "player", "tree_header" ], "spans": { "compare": [] }, "state": "partial", "undeclared_calls": 0, "undeclared_writes": 0, "unmodelled": [ { "mitigation": "region:events", "risk": "medium", "what": "posts EVENT_RESEARCH_OVERBUDGET on the owner's EventStorage: ours reproduces the decision and the id sequence, so region:events compares next_id, but the composed EvDsc/EvMsg text is not reproduced and no region can see it", "why": "text comes from the game's string table, which the engine must not carry; ours posts into its own EventStorage and writes only the counts into the scratch copy, so no live byte moves and replace mode posts nothing at all" }, { "mitigation": "region:events", "risk": "medium", "what": "posts EVENT_TECHS_UNLOCKED once after the per-node loop, for the nodes SetResearched made available this turn", "why": "the set comes from the child-unlock cascade, which ours does not run; the pass driver takes the unlock list as an input and is given `no list` rather than an empty one, so a missing input cannot look like a modelled negative. Expect region:events to under-count next_id by exactly 1 on every call that completes a tech" }, { "mitigation": "region:observed_techs", "risk": "high", "what": "appends to the owner's vector (ServerPlayer+0x274) on every tech completion", "why": "serialized ServerPlayer state that no coverage note in B2 or B3 mentioned until lane R's guard caught it. The element is now fully pinned (sizeof 44, {u16 turn_first, u16 turn_last, bool detected, string tech_name, int with}) and the append de-duplicates by tech name, but ours still does not append; the region reports the byte span, which must grow by exactly 44 per completion" }, { "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": { "compare": 3 } } }, "inputs": [ "/tmp/claude-1000/-home-alex/ec8e34f8-af37-4ef2-a309-ed6a15293097/scratchpad/out/v-b3-t1.jsonl" ], "invalid": [], "kind": "report", "meta": [ { "build": "eventlive-dd38117-20260908T0916Z", "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", "risk": "medium", "what": "posts EVENT_RESEARCH_OVERBUDGET on the owner's EventStorage: ours reproduces the decision and the id sequence, so region:events compares next_id, but the composed EvDsc/EvMsg text is not reproduced and no region can see it", "why": "text comes from the game's string table, which the engine must not carry; ours posts into its own EventStorage and writes only the counts into the scratch copy, so no live byte moves and replace mode posts nothing at all" }, { "mitigation": "region:events", "risk": "medium", "what": "posts EVENT_TECHS_UNLOCKED once after the per-node loop, for the nodes SetResearched made available this turn", "why": "the set comes from the child-unlock cascade, which ours does not run; the pass driver takes the unlock list as an input and is given `no list` rather than an empty one, so a missing input cannot look like a modelled negative. Expect region:events to under-count next_id by exactly 1 on every call that completes a tech" }, { "mitigation": "region:observed_techs", "risk": "high", "what": "appends to the owner's vector (ServerPlayer+0x274) on every tech completion", "why": "serialized ServerPlayer state that no coverage note in B2 or B3 mentioned until lane R's guard caught it. The element is now fully pinned (sizeof 44, {u16 turn_first, u16 turn_last, bool detected, string tech_name, int with}) and the append de-duplicates by tech name, but ours still does not append; the region reports the byte span, which must grow by exactly 44 per completion" }, { "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-08T09:20:00Z" } ], "totals": { "calls": 3, "compared": 3, "coverage_contradicted": 0, "coverage_unstated": 0, "diverged": 0, "guarded_calls": 3, "invalid_records": 0, "undeclared_calls": 0, "undeclared_writes": 0 }, "warnings": [] }