W42: expand world-graph runtime-state contract - #116
Conversation
The next unit after W41, planned to the same depth: measured against both repositories rather than recalled, with the handoff brief built into the plan so picking it up needs no instructions beyond the link. The audit found this unit is not what plans/39 sized it as. That document calls W42 a port of eight drafted types from Sun Trap's content-and-systems.md. In fact six further types in WorldGraphKindState's transitive closure are defined in neither repository -- Incident, ObjectiveProgress, Alert, TerrainCell, PathCell and Zone -- and Sun Trap's own two design documents disagree about a seventh: GuestOpinions is typed with seven fields in content-and-systems.md and described with ten in game-design.md 3.2, with neither document acknowledging the other. Alert is the sharpest case: 3 argues for why it must be state, 6 defines a dismiss_alert action taking an alertId, and no document declares the type. It is load-bearing in three places and specified in none. Two structural questions the draft does not answer, both raised in the plan: departed guests have lifecycle values but nothing says whether they are pruned from state (unbounded growth across a scenario, and serialize() output is a determinism concern, not just a performance one); and Finances carries revenueTodayCents while 3 collapses the clock to tick alone with day derived on read, so nothing defines which tick resets "today". Four Sun Trap decision gates due before its own M2 are unchecked, two of which determine field shapes here. The plan's Decision 2 draws the line deliberately: specify permissively where the answer cannot change the seam (rotation -- declare all four, let validation narrow), block where it changes what a field means (entrances -- absolute versus footprint-relative makes every placement test different). Those are Sun Trap's calls; the unit's job is to ask them precisely. Also corrects plans/39's split table and ledger in place, so the programme document no longer describes W42 as transcription. Second time this optimistic sizing has happened -- plans/36 Finding 3 caught the same thing for simulation. Planning and doc-only.
The Handoff section said "branch off main", copied from plans/40's pattern where the plan and the implementation were deliberately two PRs. This unit is set up the other way: feature/w42-world-graph-state-contract carries the plan and the contract edits together, one PR at the end. An agent following the old wording would have split the work off the branch created to hold it. Replaced with an explicit "commit onto this branch, do not create a new one", a check to run if unsure which branch is current, and the reason the two units differ. Also says not to open the PR until Done-When is satisfied, since the branch accumulates rather than being ready on first push.
…brief Found while reconciling W41's status against the same two Sun Trap documents this plan ports from. Added where they are actionable — Current State, Decisions, Sequence, Done-When — rather than appended as a note the executing agent would read once and lose. Four are defects nobody has to ask Sun Trap about. Queue records its containment twice, nested inside Building and also carrying buildingId back, which is the objection §3 already makes to a persisted rng one level down; buildingId is dropped and Queue.id stays, because Guest.queueId needs a referent. dismiss_alert is missing from §4's action split where §6's table has it, and the eight-versus-nine undercount has since been copied into plans/39's W45 row and TODO.md's W45 bullet — fixed here because this unit is what gives Alert a type to operate on. Three open-keyed Record<string, number> fields need N6 reconciling in 10 §6.2's existing wording, not a copy-paste that reads on the page exactly like the loose bag N6 bans. And most of these fields are outside mvp.md §4's scope with nothing saying which, so they get marked at the field. GuestConditions disagrees 6-vs-7 across the two documents the same way GuestOpinions disagrees 7-vs-10 — one field apart, easy to miss, and it belongs in the same question rather than a second one. Added a test to apply before asking: a field no §4 system updates, no §11 reason code reads and no §10 projection carries is not state. That turns "seven or ten?" into "is a system coming for these?", which is answerable. The rename is the one that could not wait, so it is a decision rather than a finding. ResortMap is themed naming in a kind whose §1 rejects a name specifically because a colony sim or a transport network would run on it unchanged; Guest, Staff and Building stay, since they name roles rather than a theme. Today it is a find-and-replace in one document, after §3 is written it is a rewrite of the section, and after W45 it crosses a kind, its fixtures and its replay corpus. Also widened the handoff's doc-only file list to match Sequence step 9, which now touches TODO.md and plans/39 for the reducer count and nothing else — the two statements would otherwise contradict each other. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adding the GuestConditions disagreement left "Why This Is Not the Port plans/39 Called It" saying the two Sun Trap documents "disagree about a seventh" type, when they now disagree about two — GuestOpinions and GuestConditions. Decision 1 had the same problem one level down, arguing only about "the seven-field list" when the same reasoning covers both field sets. Exactly the defect CLAUDE.md's envelope-duplication ledger records about itself: a count written from memory of the finding rather than from the list, and four documents ended up carrying four different numbers. Caught by re-reading the file as a whole instead of trusting the edits that produced it. Also states plainly that the consumer test added alongside does not override Decision 1 — it sharpens the question, it does not answer it for Sun Trap. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…heirs The plan filed four "Before M2" decision gates as Sun Trap's and two as blocking, which left W42 unable to reach its own Done-When without an answer from another repository. Re-reading them against this contract's own rules, three are not content-design questions at all. Building.entrances was the hard blocker: absolute or footprint-relative? The contract answers a prior question first. An entrance position is position + rotation + the definition's offsets — derived — and §3's clock callout bans derived values from serialized state in the same words it bans the persisted rng sitting four fields away. So the field leaves Building entirely, offsets are authored on BuildingDefinition (W43), and the rotation transform is stated in §3 because rotating an integer offset is a determinism concern. Neither original answer applies, and the remaining question has a home in the unit that owns BuildingDefinition. The GuestOpinions 7-vs-10 and GuestConditions 6-vs-7 disagreements are not disagreements. game-design.md says guests *evaluate* ten things; evaluating is something the utility model does at decision time from world state, and it does not require the guest to carry a field. The drafted seven and six are the stored impressions; the rest are evaluation inputs, which is W44's subject. Both documents end up right about different things, which beats one of them losing. The extras go to OPEN-QUESTIONS with the condition that would admit them: a §4 system that writes the value between ticks. The tick duration does not gate the revenueTodayCents reset either. The accumulators stay — today's revenue is not recoverable from cash — and the boundary is the first tick of a new day via floor(tick / ticksPerDay), a pure function of tick and campaign data. The value of ticksPerDay is balance and stays Sun Trap's; the rule does not depend on it. Decision 1 was rewritten rather than kept, because "ask Sun Trap, do not resolve unilaterally" directly contradicted the three new decisions. It now carries the test that separates the two: would a different answer change what the engine may store, or only what the game contains? Entrances read like the second and were the first. Sequence step 2 becomes a message that informs rather than a wait, and explicitly says not to block on the reply. Also removed a Done-When box the new one superseded, and marked the second structural question answered where it is raised. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The prompt to start this unit existed only in a chat transcript, which is the one place the plan's own handoff says nothing should depend on. Now it is on the branch it refers to. Deliberately short. It carries only what an agent cannot read off the branch itself — which branch to work on, that the branch is already synced so rebasing would be wrong, and that nothing is waiting on a Sun Trap answer. The file allowlist is repeated because it is the constraint most easily lost between reading the prompt and starting work; everything else is left to the sections below rather than duplicated into a block that would drift from them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
plans/41-roadmap-page.md merged to main while the W42 plan sat on this branch under the same number. Both would have landed: they are different filenames, so git merges them cleanly with zero conflicts. I checked with a simulated merge rather than assuming — and the clean merge is exactly what makes this worth fixing now. Nothing surfaces it. The directory would simply have had two 41s, against a numbering convention CLAUDE.md calls positional. The roadmap plan merged first and keeps 41. This one becomes 42, which also lines it up with the unit it plans. Four references followed the file: one in TODO.md, two in plans/39, and one inside the plan's own starting prompt. Merged main in first, so the collision was visible in one tree rather than argued about across two branches. Also reworded the prompt's branch-currency line. It said "it is already synced with main — do not rebase," which was true when written and false twice within the same day, because another branch kept merging to main. It now tells the agent to check `git log origin/main..HEAD` and merge if needed. A claim about a moving branch has a shelf life; an instruction to check does not — the same failure the roadmap plan's hard-coded work-unit count had, in a different shape. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PR Summary by QodoExpand world-graph runtime-state contract and view specification
AI Description
Diagram
High-Level Assessment
Files changed (5)
|
Qodo Fixer🍒 Ready to be cherry-picked — ✅ Merged (0) · ☑ Fixed (5) 🔗 Fix PR: #117 This fix PR was closed automatically. Its branch is preserved so you can cherry pick the changes into the original PR. Prompt for coding agent Process — 5 fixed
|
W42's §3.2 and the kind code on this branch disagreed eight ways, and on four of them the code was right against this plan's own Decisions. Fix the document, then fix the code where it was the one that drifted. Contract (12-world-graph-kind.md): - ResortMap -> WorldMap; a themed name in engine-owned code contradicts §1 - Building.entrances leaves state as derived, with the rotation transform stated - GuestOpinions is the seven stored impressions; the other three are evaluation inputs, and GuestConditions resolves the same way - Building.pricesCents added — set_price had nowhere to write — with the N6 reconciliation in 10 §6.2's wording - Staff.zoneId and GuestConditions.arrivalTick removed as duplication one level down: inside kindState rather than against the envelope - the "today" boundary is floor(tick / ticksPerDay), per Decision 7 - dismiss_alert joins §4's split; TODO.md and plans/39 say nine reducers - id order compares ordinals numerically, or building:10 sorts before building:2 - MVP-inert fields marked at the field; every number states its scale Code: - a campaign with no objectives stays active rather than resolving vacuously, and Tier 2 warns about it and about one already resolved at tick 0 - placement_out_of_bounds is emitted rather than folded into terrain; a capped definition reports action_not_available, not unknown_entity - every reducer returns its StateChange; §13 fixes the grain, not whether - demolish detaches guests and dismisses alerts instead of deleting them - assign_staff validates zoneId against map.zones - buildOptions and availableActions share one predicate with the reducer - Tier 1 checks pre-placed footprints, ticksPerDay, and default prices in band - each verb gets its own labelKey; eventNames match §12 plans/42 records that this branch carries W45's skeleton against a doc-only plan. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
W42 is doc-only. This branch had acquired src/engine/src/kinds/world-graph/ — state types, the nine immediate reducers, projection, outcome, validation and the kind assembly — which is W45's unit, executed without its own plan. The code moves to feature/w45-world-graph-kind-skeleton, branched from the commit that reconciled it with the contract, so every fix that review produced is preserved and W45 starts from a reviewed draft rather than from nothing. W42's PR now reviews as one thing. Not deleted, because writing it is what found two defects the contract now carries fixed: Building had no price field for set_price to write to, and §3.4's canonical id order would have compared ordinals lexicographically — building:10 before building:2, a determinism defect that bites only past nine entities of one prefix. plans/39's W45 ledger points at the branch; plans/42 records the episode. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- §13's table wrote collection-level rows (`path: "buildings"`, `op: "set"`), which 04 §12 cannot represent: StateChange.value is string|number|boolean, so there is nothing legal to put in `value`. Paths are entity-scoped now, with appearance and removal as a boolean on the entity's own `.exists` path, and the `value` column stated so the constraint is visible rather than implied. - §10's blockedBy promised a reason for a definition at its scenario cap and §11 defined no code for it. Adds `building_limit_reached`, the twin of the `staff_limit_reached` that already existed for roles; §10 now names the three codes it may carry and says it never invents one. - The MVP-inert convention cited `mvp.md`, which reads as this repository's MVP.md. It is Sun Trap's, in its own repository — disambiguated rather than recased, since the reviewer read it the way any reader would. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- §13's table wrote collection-level rows (`path: "buildings"`, `op: "set"`), which 04 §12 cannot represent: StateChange.value is string|number|boolean, so there is nothing legal to put in `value`. Paths are entity-scoped now, with appearance and removal as a boolean on the entity's own `.exists` path, and the `value` column stated so the constraint is visible rather than implied. - §10's blockedBy promised a reason for a definition at its scenario cap and §11 defined no code for it. Adds `building_limit_reached`, the twin of the `staff_limit_reached` that already existed for roles; §10 now names the three codes it may carry and says it never invents one. - The MVP-inert convention cited `mvp.md`, which reads as this repository's MVP.md. It is Sun Trap's, in its own repository — disambiguated rather than recased, since the reviewer read it the way any reader would. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Six of qodo's findings landed on files that moved to this branch, so they show as outdated on that PR. Three were already fixed; three were not, and were true until now. - build and hire_staff subtracted cash without touching expensesTodayCents or expensesTotalCents, so the accumulators disagreed with the cash movement that produced them. One `spend` helper does both, and a test asserts they agree. - resolveOutcome returned null while any objective was active, even with another already failed — an active game carrying a non-null failureId, which the player could keep acting in after losing. Failure now resolves immediately. - placement required allowedTerrain AND a hard-coded empty|path check, so a definition declaring water or restricted could never be placed on the terrain it declares. allowedTerrain is now the whole rule; reachability moved to asking whether a walkable cell borders the footprint, since a pier has no walkable cell of its own and guests reach an edge, not an interior. Also matches the contract fixes on that PR: building_limit_reached replaces the generic base code for a capped definition, and StateChange paths are entity-scoped (`buildings.<id>.exists`) because 04 §12's value cannot hold a collection. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- `op: decrement` with `value: cash after` is ambiguous by construction: half a table's consumers read a decrement's value as the amount deducted. 04 §12 offers increment/decrement but defines no value semantics for them, and its own worked examples use `set` with value + previous — as does 03 §5, which is explicit that op stays `set` regardless of the arithmetic that ran. This kind follows that: always `set`, value is the state after, previous the state before, and a consumer wanting a delta subtracts. - §6 lets `build` open a construction site instead of placing a building; the table covered only the immediate path. Both rows are listed now. - The table omitted `reason`, which the core type requires. Added as a column, with a note that these are StateChange vocabulary and not §11 additions — §11's codes are what a rejected action returns. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"Every path is entity-scoped" was over-broad — `tick` and `finances.cashCents` sit in the same table and belong to no collection. The rule that is actually forced by 04 §12 is that a collection is never a path; there are two shapes under it, singleton and entity-scoped, and both are now stated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The singleton shape was illustrated by two examples and never closed, so nothing said whether a third top-level path was legal. Rather than list the valid singletons — a list that would drift from the fields it describes, which is the defect this document keeps catching elsewhere — the set is derived: a path is the dotted traversal of WorldGraphKindState down to the scalar that changed, and it is valid iff it resolves against §3.2. That gives consumers a check rather than a convention, since 04 §12 types path as an unconstrained string and nothing structural stops a producer inventing one. Also names the two fields the rule reaches but that are never audited — nextEntityOrdinal is an id source, and map.* changes only when authored topology does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Appearing and disappearing are not fields of any type in §3.2 — a removed entity has no field left to carry the news — so the traversal rule did not literally cover the `.exists` rows the same table specifies. Defined as a membership assertion, and closed: a second synthetic leaf may not be added without amending that paragraph, or the grammar is back where it started. Found by writing the test that checks the grammar (feature/w45, 712afd9). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
§3.2 called identifiers opaque, and opacity of meaning implied freedom of shape — but §13's paths are dot-separated, so a productId of `water.sparkling` makes `buildings.b:3.pricesCents.water.sparkling` parse two ways: one segment or two, resolving to a price or to nothing depending on who read it. No path-addressable identifier may contain a `.` — authored content ids, the keys of nested records like pricesCents (which are product ids), and entity ids, which satisfy it by construction since §9 formats them <prefix>:<ordinal>. Tier 1 checks the authored ones, because content is exactly what a contract cannot assume about. §3.2 now states the one shape constraint alongside the opacity claim that otherwise contradicts it. A canonical escaping grammar was the alternative and buys nothing: nothing needs a dot inside an id, and every producer and consumer would have to implement the unescaping identically or reintroduce the divergence. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Tier 1 checklist enumerated building/product definitions, staff roles, objectives and zones, which omitted Guest.archetypeId and would have been wrong again the moment W43 adds incidents and scenarios. Stated over every authored id the kind reads instead, so W43's ids are bound on the same terms without amending the sentence. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Plan W42 — the world-graph runtime-state contract (plans/41) The next unit after W41, planned to the same depth: measured against both repositories rather than recalled, with the handoff brief built into the plan so picking it up needs no instructions beyond the link. The audit found this unit is not what plans/39 sized it as. That document calls W42 a port of eight drafted types from Sun Trap's content-and-systems.md. In fact six further types in WorldGraphKindState's transitive closure are defined in neither repository -- Incident, ObjectiveProgress, Alert, TerrainCell, PathCell and Zone -- and Sun Trap's own two design documents disagree about a seventh: GuestOpinions is typed with seven fields in content-and-systems.md and described with ten in game-design.md 3.2, with neither document acknowledging the other. Alert is the sharpest case: 3 argues for why it must be state, 6 defines a dismiss_alert action taking an alertId, and no document declares the type. It is load-bearing in three places and specified in none. Two structural questions the draft does not answer, both raised in the plan: departed guests have lifecycle values but nothing says whether they are pruned from state (unbounded growth across a scenario, and serialize() output is a determinism concern, not just a performance one); and Finances carries revenueTodayCents while 3 collapses the clock to tick alone with day derived on read, so nothing defines which tick resets "today". Four Sun Trap decision gates due before its own M2 are unchecked, two of which determine field shapes here. The plan's Decision 2 draws the line deliberately: specify permissively where the answer cannot change the seam (rotation -- declare all four, let validation narrow), block where it changes what a field means (entrances -- absolute versus footprint-relative makes every placement test different). Those are Sun Trap's calls; the unit's job is to ask them precisely. Also corrects plans/39's split table and ledger in place, so the programme document no longer describes W42 as transcription. Second time this optimistic sizing has happened -- plans/36 Finding 3 caught the same thing for simulation. Planning and doc-only. * W42 plan — point the handoff at this branch, not a new one off main The Handoff section said "branch off main", copied from plans/40's pattern where the plan and the implementation were deliberately two PRs. This unit is set up the other way: feature/w42-world-graph-state-contract carries the plan and the contract edits together, one PR at the end. An agent following the old wording would have split the work off the branch created to hold it. Replaced with an explicit "commit onto this branch, do not create a new one", a check to run if unsure which branch is current, and the reason the two units differ. Also says not to open the PR until Done-When is satisfied, since the branch accumulates rather than being ready on first push. * W42 plan — six findings from the W41 reconciliation, folded into the brief Found while reconciling W41's status against the same two Sun Trap documents this plan ports from. Added where they are actionable — Current State, Decisions, Sequence, Done-When — rather than appended as a note the executing agent would read once and lose. Four are defects nobody has to ask Sun Trap about. Queue records its containment twice, nested inside Building and also carrying buildingId back, which is the objection §3 already makes to a persisted rng one level down; buildingId is dropped and Queue.id stays, because Guest.queueId needs a referent. dismiss_alert is missing from §4's action split where §6's table has it, and the eight-versus-nine undercount has since been copied into plans/39's W45 row and TODO.md's W45 bullet — fixed here because this unit is what gives Alert a type to operate on. Three open-keyed Record<string, number> fields need N6 reconciling in 10 §6.2's existing wording, not a copy-paste that reads on the page exactly like the loose bag N6 bans. And most of these fields are outside mvp.md §4's scope with nothing saying which, so they get marked at the field. GuestConditions disagrees 6-vs-7 across the two documents the same way GuestOpinions disagrees 7-vs-10 — one field apart, easy to miss, and it belongs in the same question rather than a second one. Added a test to apply before asking: a field no §4 system updates, no §11 reason code reads and no §10 projection carries is not state. That turns "seven or ten?" into "is a system coming for these?", which is answerable. The rename is the one that could not wait, so it is a decision rather than a finding. ResortMap is themed naming in a kind whose §1 rejects a name specifically because a colony sim or a transport network would run on it unchanged; Guest, Staff and Building stay, since they name roles rather than a theme. Today it is a find-and-replace in one document, after §3 is written it is a rewrite of the section, and after W45 it crosses a kind, its fixtures and its replay corpus. Also widened the handoff's doc-only file list to match Sequence step 9, which now touches TODO.md and plans/39 for the reducer count and nothing else — the two statements would otherwise contradict each other. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * W42 plan — fix two counts my own amendment made stale Adding the GuestConditions disagreement left "Why This Is Not the Port plans/39 Called It" saying the two Sun Trap documents "disagree about a seventh" type, when they now disagree about two — GuestOpinions and GuestConditions. Decision 1 had the same problem one level down, arguing only about "the seven-field list" when the same reasoning covers both field sets. Exactly the defect CLAUDE.md's envelope-duplication ledger records about itself: a count written from memory of the finding rather than from the list, and four documents ended up carrying four different numbers. Caught by re-reading the file as a whole instead of trusting the edits that produced it. Also states plainly that the consumer test added alongside does not override Decision 1 — it sharpens the question, it does not answer it for Sun Trap. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * W42 plan — none of the four Sun Trap gates blocks; three were never theirs The plan filed four "Before M2" decision gates as Sun Trap's and two as blocking, which left W42 unable to reach its own Done-When without an answer from another repository. Re-reading them against this contract's own rules, three are not content-design questions at all. Building.entrances was the hard blocker: absolute or footprint-relative? The contract answers a prior question first. An entrance position is position + rotation + the definition's offsets — derived — and §3's clock callout bans derived values from serialized state in the same words it bans the persisted rng sitting four fields away. So the field leaves Building entirely, offsets are authored on BuildingDefinition (W43), and the rotation transform is stated in §3 because rotating an integer offset is a determinism concern. Neither original answer applies, and the remaining question has a home in the unit that owns BuildingDefinition. The GuestOpinions 7-vs-10 and GuestConditions 6-vs-7 disagreements are not disagreements. game-design.md says guests *evaluate* ten things; evaluating is something the utility model does at decision time from world state, and it does not require the guest to carry a field. The drafted seven and six are the stored impressions; the rest are evaluation inputs, which is W44's subject. Both documents end up right about different things, which beats one of them losing. The extras go to OPEN-QUESTIONS with the condition that would admit them: a §4 system that writes the value between ticks. The tick duration does not gate the revenueTodayCents reset either. The accumulators stay — today's revenue is not recoverable from cash — and the boundary is the first tick of a new day via floor(tick / ticksPerDay), a pure function of tick and campaign data. The value of ticksPerDay is balance and stays Sun Trap's; the rule does not depend on it. Decision 1 was rewritten rather than kept, because "ask Sun Trap, do not resolve unilaterally" directly contradicted the three new decisions. It now carries the test that separates the two: would a different answer change what the engine may store, or only what the game contains? Entrances read like the second and were the first. Sequence step 2 becomes a message that informs rather than a wait, and explicitly says not to block on the reply. Also removed a Done-When box the new one superseded, and marked the second structural question answered where it is raised. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * W42 plan — put the starting prompt in the handoff The prompt to start this unit existed only in a chat transcript, which is the one place the plan's own handoff says nothing should depend on. Now it is on the branch it refers to. Deliberately short. It carries only what an agent cannot read off the branch itself — which branch to work on, that the branch is already synced so rebasing would be wrong, and that nothing is waiting on a Sun Trap answer. The file allowlist is repeated because it is the constraint most easily lost between reading the prompt and starting work; everything else is left to the sections below rather than duplicated into a block that would drift from them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Renumber the W42 plan to 42 — plans/41 was claimed twice plans/41-roadmap-page.md merged to main while the W42 plan sat on this branch under the same number. Both would have landed: they are different filenames, so git merges them cleanly with zero conflicts. I checked with a simulated merge rather than assuming — and the clean merge is exactly what makes this worth fixing now. Nothing surfaces it. The directory would simply have had two 41s, against a numbering convention CLAUDE.md calls positional. The roadmap plan merged first and keeps 41. This one becomes 42, which also lines it up with the unit it plans. Four references followed the file: one in TODO.md, two in plans/39, and one inside the plan's own starting prompt. Merged main in first, so the collision was visible in one tree rather than argued about across two branches. Also reworded the prompt's branch-currency line. It said "it is already synced with main — do not rebase," which was true when written and false twice within the same day, because another branch kept merging to main. It now tells the agent to check `git log origin/main..HEAD` and merge if needed. A claim about a moving branch has a shelf life; an instruction to check does not — the same failure the roadmap plan's hard-coded work-unit count had, in a different shape. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * W42: expand world-graph runtime-state contract types * Implement world-graph W42 kind runtime scaffolding * Harden world-graph immediate action runtime * Reconcile the world-graph contract with its implementation W42's §3.2 and the kind code on this branch disagreed eight ways, and on four of them the code was right against this plan's own Decisions. Fix the document, then fix the code where it was the one that drifted. Contract (12-world-graph-kind.md): - ResortMap -> WorldMap; a themed name in engine-owned code contradicts §1 - Building.entrances leaves state as derived, with the rotation transform stated - GuestOpinions is the seven stored impressions; the other three are evaluation inputs, and GuestConditions resolves the same way - Building.pricesCents added — set_price had nowhere to write — with the N6 reconciliation in 10 §6.2's wording - Staff.zoneId and GuestConditions.arrivalTick removed as duplication one level down: inside kindState rather than against the envelope - the "today" boundary is floor(tick / ticksPerDay), per Decision 7 - dismiss_alert joins §4's split; TODO.md and plans/39 say nine reducers - id order compares ordinals numerically, or building:10 sorts before building:2 - MVP-inert fields marked at the field; every number states its scale Code: - a campaign with no objectives stays active rather than resolving vacuously, and Tier 2 warns about it and about one already resolved at tick 0 - placement_out_of_bounds is emitted rather than folded into terrain; a capped definition reports action_not_available, not unknown_entity - every reducer returns its StateChange; §13 fixes the grain, not whether - demolish detaches guests and dismisses alerts instead of deleting them - assign_staff validates zoneId against map.zones - buildOptions and availableActions share one predicate with the reducer - Tier 1 checks pre-placed footprints, ticksPerDay, and default prices in band - each verb gets its own labelKey; eventNames match §12 plans/42 records that this branch carries W45's skeleton against a doc-only plan. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Answer PR #116's three live review threads - §13's table wrote collection-level rows (`path: "buildings"`, `op: "set"`), which 04 §12 cannot represent: StateChange.value is string|number|boolean, so there is nothing legal to put in `value`. Paths are entity-scoped now, with appearance and removal as a boolean on the entity's own `.exists` path, and the `value` column stated so the constraint is visible rather than implied. - §10's blockedBy promised a reason for a definition at its scenario cap and §11 defined no code for it. Adds `building_limit_reached`, the twin of the `staff_limit_reached` that already existed for roles; §10 now names the three codes it may carry and says it never invents one. - The MVP-inert convention cited `mvp.md`, which reads as this repository's MVP.md. It is Sun Trap's, in its own repository — disambiguated rather than recased, since the reviewer read it the way any reader would. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Apply PR #116's code findings to the skeleton Six of qodo's findings landed on files that moved to this branch, so they show as outdated on that PR. Three were already fixed; three were not, and were true until now. - build and hire_staff subtracted cash without touching expensesTodayCents or expensesTotalCents, so the accumulators disagreed with the cash movement that produced them. One `spend` helper does both, and a test asserts they agree. - resolveOutcome returned null while any objective was active, even with another already failed — an active game carrying a non-null failureId, which the player could keep acting in after losing. Failure now resolves immediately. - placement required allowedTerrain AND a hard-coded empty|path check, so a definition declaring water or restricted could never be placed on the terrain it declares. allowedTerrain is now the whole rule; reachability moved to asking whether a walkable cell borders the footprint, since a pier has no walkable cell of its own and guests reach an edge, not an interior. Also matches the contract fixes on that PR: building_limit_reached replaces the generic base code for a capped definition, and StateChange paths are entity-scoped (`buildings.<id>.exists`) because 04 §12's value cannot hold a collection. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Answer the second review round on §13's StateChange table - `op: decrement` with `value: cash after` is ambiguous by construction: half a table's consumers read a decrement's value as the amount deducted. 04 §12 offers increment/decrement but defines no value semantics for them, and its own worked examples use `set` with value + previous — as does 03 §5, which is explicit that op stays `set` regardless of the arithmetic that ran. This kind follows that: always `set`, value is the state after, previous the state before, and a consumer wanting a delta subtracts. - §6 lets `build` open a construction site instead of placing a building; the table covered only the immediate path. Both rows are listed now. - The table omitted `reason`, which the core type requires. Added as a column, with a note that these are StateChange vocabulary and not §11 additions — §11's codes are what a rejected action returns. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Emit StateChange as set-with-previous, never increment/decrement 04 §12 offers increment/decrement but defines no value semantics for them, so a `decrement` row whose value was the resulting balance reads to half its consumers as the amount deducted. 04's own examples and 03 §5 both use `set` with value + previous; this follows them, and a test asserts every emitted change is a `set`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Name both StateChange path shapes, not just the entity one "Every path is entity-scoped" was over-broad — `tick` and `finances.cashCents` sit in the same table and belong to no collection. The rule that is actually forced by 04 §12 is that a collection is never a path; there are two shapes under it, singleton and entity-scoped, and both are now stated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Make the StateChange path grammar normative and checkable The singleton shape was illustrated by two examples and never closed, so nothing said whether a third top-level path was legal. Rather than list the valid singletons — a list that would drift from the fields it describes, which is the defect this document keeps catching elsewhere — the set is derived: a path is the dotted traversal of WorldGraphKindState down to the scalar that changed, and it is valid iff it resolves against §3.2. That gives consumers a check rather than a convention, since 04 §12 types path as an unconstrained string and nothing structural stops a producer inventing one. Also names the two fields the rule reaches but that are never audited — nextEntityOrdinal is an id source, and map.* changes only when authored topology does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Test the StateChange path grammar, and name .exists as its one synthetic leaf Writing the check found a hole in the wording it was checking: "a path is the dotted traversal of the state down to the scalar that changed" does not cover `.exists`, because appearing and disappearing are not fields of any type in §3.2 — a removed entity has no field left to carry the news. §13 now defines `.exists` as a membership assertion, as the only synthetic leaf, and says a second one may not be added without amending that paragraph. The test resolves every emitted path against the state the action produced, so an `.exists` row is checked against the membership it claims rather than merely parsed. A second test asserts the three shapes §13 bans — a collection path, an invented leaf, an array index — do not resolve, so the first cannot pass vacuously. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Ban the dot from path-addressable ids, so §13's grammar is unambiguous §3.2 called identifiers opaque, and opacity of meaning implied freedom of shape — but §13's paths are dot-separated, so a productId of `water.sparkling` makes `buildings.b:3.pricesCents.water.sparkling` parse two ways: one segment or two, resolving to a price or to nothing depending on who read it. No path-addressable identifier may contain a `.` — authored content ids, the keys of nested records like pricesCents (which are product ids), and entity ids, which satisfy it by construction since §9 formats them <prefix>:<ordinal>. Tier 1 checks the authored ones, because content is exactly what a contract cannot assume about. §3.2 now states the one shape constraint alongside the opacity claim that otherwise contradicts it. A canonical escaping grammar was the alternative and buys nothing: nothing needs a dot inside an id, and every producer and consumer would have to implement the unescaping identically or reintroduce the divergence. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Enforce the no-dot id rule at Tier 1 §13's audit paths are dot-separated, so an authored id carrying a dot makes a path parse two ways. Tier 1 now rejects one, across every authored id the kind reads — building and product definitions (which are also the keys of pricesCents), staff roles, objectives and zones. Entity ids need no check; §9 constructs them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Plan W43 world-graph content contract * Plan W44 world-graph resolution contract * Clarify W43 map ownership and W45 handoff * docs(world-graph): define W43 content contract * docs(world-graph): record W43 review status * docs(world-graph): define W44 resolution contract * docs(world-graph): record W44 review status * Plan W45 world-graph kind skeleton * Plan W46 world-graph tick pipeline * Consolidate W43 review into PR 119 * Implement W45 world-graph kind skeleton * Resolve world-graph review findings --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Summary
This is W42 (doc-only) for the world-graph kind.
12-world-graph-kind.mdnow contains the authoritative runtime-state contract forWorldGraphKindStateand all transitive types in its closure, including map/entity/state types andWorldGraphView.What changed
docs/docs/engine/12-world-graph-kind.md(including map, guest, staff, building, queue, construction, finances, incidents, objectives, alerts).nextEntityOrdinal-derived nested entity identity expectations.WorldGraphViewdeclaration reconciled with terminal outcome data.docs/docs/engine/OPEN-QUESTIONS.md(entrance basis, rotation model, one-tick duration, andGuestOpinionsfield-count).Validation
src/enginechanges.