Skip to content

W42: expand world-graph runtime-state contract - #116

Merged
The-Running-Dev merged 23 commits into
mainfrom
feature/w42-world-graph-state-contract
Aug 2, 2026
Merged

W42: expand world-graph runtime-state contract#116
The-Running-Dev merged 23 commits into
mainfrom
feature/w42-world-graph-state-contract

Conversation

@The-Running-Dev

Copy link
Copy Markdown
Owner

Summary

This is W42 (doc-only) for the world-graph kind. 12-world-graph-kind.md now contains the authoritative runtime-state contract for WorldGraphKindState and all transitive types in its closure, including map/entity/state types and WorldGraphView.

What changed

  • Specify all runtime state types in docs/docs/engine/12-world-graph-kind.md (including map, guest, staff, building, queue, construction, finances, incidents, objectives, alerts).
  • Add canonical collection ordering and iterator rules, and clarify nextEntityOrdinal-derived nested entity identity expectations.
  • Add explicit WorldGraphView declaration reconciled with terminal outcome data.
  • Record W42 pending Sun Trap confirmations in docs/docs/engine/OPEN-QUESTIONS.md (entrance basis, rotation model, one-tick duration, and GuestOpinions field-count).

Validation

  • Commit-only, docs-only change as requested for this unit. No src/engine changes.

The-Running-Dev and others added 12 commits August 2, 2026 16:44
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>
@qodo-code-review

Copy link
Copy Markdown
Contributor

PR Summary by Qodo

Expand world-graph runtime-state contract and view specification

📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Define the full WorldGraphKindState runtime-state type closure in the kind contract.
• Specify canonical collection ordering and deterministic nested-entity identity rules.
• Declare WorldGraphView and record pending Sun Trap design gates in open questions.
Diagram

graph TD
T["docs/docs/engine/TODO.md & plans/39"] --> P["plans/42-w42-world-graph-state-contract.md"] --> D["docs/docs/engine/12-world-graph-kind.md"] --> S["State type contract"] --> I["Future W45 implementation"]
D --> V["WorldGraphView contract"] --> I
D --> R["Canonical order & determinism rules"]
D <--> Q["docs/docs/engine/OPEN-QUESTIONS.md"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Generate docs from a canonical schema file (TS/JSON Schema)
  • ➕ Single source of truth once W45 introduces real TypeScript types
  • ➕ Reduces long-term drift between prose/docs and implementation
  • ➕ Enables automated validation/examples derived from schema
  • ➖ Introduces extra tooling/build steps for docs (and likely out of scope for doc-only unit)
  • ➖ Harder to review as a narrative contract document without surrounding prose/rationale
2. Split the contract into smaller dedicated docs (state vs view vs determinism)
  • ➕ Improves navigability; reviewers can focus on one concern at a time
  • ➕ Reduces section size churn in 12-world-graph-kind.md
  • ➖ More cross-document linking and risk of broken references/numbering drift
  • ➖ Loses the “single authoritative seam page” property that helps future W45/W46 work

Recommendation: Keep the PR’s approach (inline authoritative TypeScript-shaped contract in 12-world-graph-kind.md) for now: it matches the programme’s doc-first seam workflow and keeps reviewers/implementers anchored on one canonical contract page before any W45 code exists. Revisit schema-generation once the W45 types exist and a single-source-of-truth problem becomes real rather than hypothetical.

Files changed (5) +934 / -12

Documentation (5) +934 / -12
12-world-graph-kind.mdMake §3 the authoritative runtime-state contract and define 'WorldGraphView' +360/-10

Make §3 the authoritative runtime-state contract and define 'WorldGraphView'

• Updates the document status to Revision 2 and adds a full runtime-state type contract (the transitive closure for 'WorldGraphKindState'). Adds explicit structural answers (guest pruning and finance ‘today’ boundary), canonical collection ordering rules, and a concrete 'WorldGraphView' interface aligned with terminal outcome data.

docs/docs/engine/12-world-graph-kind.md

OPEN-QUESTIONS.mdRecord W42 pending Sun Trap gates for the world-graph state contract +8/-0

Record W42 pending Sun Trap gates for the world-graph state contract

• Adds a new open-questions entry capturing the remaining upstream confirmations needed (entrance basis, rotation model, tick-duration assumption for today-boundaries, and 'GuestOpinions' field count). Provides a clear revisit trigger tied to companion-repo merges.

docs/docs/engine/OPEN-QUESTIONS.md

TODO.mdLink W42 TODO item to the new plan and correct unit sizing expectations +6/-1

Link W42 TODO item to the new plan and correct unit sizing expectations

• Expands the W42 TODO bullet with a direct link to 'plans/42' and notes that W42 includes new design work (six previously-undefined types), not just porting drafted shapes.

docs/docs/engine/TODO.md

39-world-graph-kind-programme.mdAmend W42 programme notes to reflect corrected scope and link the W42 plan +18/-1

Amend W42 programme notes to reflect corrected scope and link the W42 plan

• Adds explicit commentary that W42 is not purely a port and references the W42 plan as evidence. Updates the W42 ledger section to include the plan link and marks the plan-cut step as complete.

plans/39-world-graph-kind-programme.md

42-w42-world-graph-state-contract.mdAdd the W42 plan: scope, decisions, sequence, and done-when checks +542/-0

Add the W42 plan: scope, decisions, sequence, and done-when checks

• Introduces a new, detailed plan document for W42, including the handoff brief, measured inventory of missing types, decisions (e.g., determinism, state vs derived data), execution sequence, and explicit completion criteria. Constrains the unit to doc-only changes and calls out known pitfalls (e.g., treating the work as transcription).

plans/42-w42-world-graph-state-contract.md

Comment thread plans/42-w42-world-graph-state-contract.md
Comment thread docs/docs/engine/12-world-graph-kind.md
Comment thread docs/docs/engine/12-world-graph-kind.md
Comment thread docs/docs/engine/12-world-graph-kind.md Outdated
Comment thread docs/docs/engine/12-world-graph-kind.md Outdated
Comment thread docs/docs/engine/12-world-graph-kind.md Outdated
@qodo-code-review

Copy link
Copy Markdown
Contributor

Qodo Fixer

🍒 Ready to be cherry-picked — ✅ Merged (0) · ☑ Fixed (5)

Grey Divider

🔗 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

This is an automated fix prepared on a separate branch (#117). It is NOT applied to this PR.
To use it: review Fix PR #117 (https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/117), evaluate each change critically against your local context, and cherry-pick the changes that are correct into this branch. Do not accept them blindly.
Process — 5 fixed
  • ☑ Fixed: Id order underspecified
  • ☑ Fixed: Derived entrances persisted
  • ☑ Fixed: plans/42 contradicts contract types
  • ☑ Fixed: Zero-day boundary semantics
  • ☑ Fixed: Status contradicts provisionality

Comment thread src/engine/src/kinds/world-graph/advance.ts Outdated
Comment thread src/engine/src/kinds/world-graph/validate.ts Outdated
Comment thread src/engine/src/kinds/world-graph/available.ts Outdated
Comment thread src/engine/src/kinds/world-graph/available.ts Outdated
Comment thread src/engine/src/kinds/world-graph/advance.ts Outdated
Comment thread src/engine/src/kinds/world-graph/outcome.ts Outdated
Comment thread src/engine/src/kinds/world-graph/advance.ts Outdated
Comment thread src/engine/src/kinds/world-graph/advance.ts Outdated
Comment thread src/engine/src/kinds/world-graph/reasons.ts Outdated
The-Running-Dev and others added 2 commits August 2, 2026 22:18
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>
Comment thread docs/docs/engine/12-world-graph-kind.md Outdated
Comment thread docs/docs/engine/12-world-graph-kind.md
Comment thread docs/docs/engine/12-world-graph-kind.md Outdated
- §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>
Comment thread docs/docs/engine/12-world-graph-kind.md Outdated
Comment thread docs/docs/engine/12-world-graph-kind.md Outdated
Comment thread docs/docs/engine/12-world-graph-kind.md Outdated
The-Running-Dev added a commit that referenced this pull request Aug 2, 2026
- §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>
The-Running-Dev added a commit that referenced this pull request Aug 2, 2026
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>
Comment thread docs/docs/engine/12-world-graph-kind.md Outdated
"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>
Comment thread docs/docs/engine/12-world-graph-kind.md Outdated
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>
Comment thread docs/docs/engine/12-world-graph-kind.md
The-Running-Dev and others added 2 commits August 2, 2026 23:25
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>
Comment thread docs/docs/engine/12-world-graph-kind.md Outdated
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>
@The-Running-Dev
The-Running-Dev enabled auto-merge (squash) August 2, 2026 20:50
@The-Running-Dev
The-Running-Dev merged commit 5f5f8f5 into main Aug 2, 2026
4 checks passed
@The-Running-Dev
The-Running-Dev deleted the feature/w42-world-graph-state-contract branch August 2, 2026 20:52
The-Running-Dev added a commit that referenced this pull request Aug 3, 2026
* 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant