diff --git a/CLAUDE.md b/CLAUDE.md index fe33a52e..4c2c3e62 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -63,7 +63,7 @@ The agent-kit files are canonical. Edit these, never their generated copies unde | `design/00-brief.md` | Product vision, scope, non-goals, MVP, and definition of done | | `design/10-design.md` | Architecture plus observability, extensibility, replay, capture, client, and content-pack design | | `design/20-contract.md` | Core and all kind contracts, including exact types and invariants | -| `design/30-slices.md` | The W-numbered vertical delivery ledger; W ids are retained because repository history already cites them | +| `design/30-slices.md` | The vertical delivery ledger in the kit's format: live `## S` slices above `## Landed`, where all 125 W-numbered units are retired to one row each | | `design/90-decisions.md` | Decision history, deferred items, and judgement calls to revisit | Each file contains marked human-document blocks. `build/ConvertTo-HumanDocumentation.ps1` @@ -93,7 +93,7 @@ they are outputs, not editing surfaces. | `06-extensibility.md` | Where the engine can be extended: the **ports** a host supplies (`IdSource`, stores, `Emitter`, `Clock`, `ExperimentSource`), the two composition roots, and the rule that decides — *a host may supply anything that cannot change `serialize()` output*, which makes the determinism boundary the trust boundary. Kinds stay engine-owned per architecture N2 | | `05-observability.md` | Logging and tracing as a **separate channel** from `StateChange`: the clock-free `EngineEvent`, the per-resolution emitter handle, sinks, and the boundary that stamps time and trace ids. The invariant is that dropping every event changes nothing | | `MVP.md` | The smallest slice that proves the platform, + Definition of Done (finalized) | -| `TODO.md` | The MVP as ordered **units of work** (W0–W19), each with contract refs, dependencies, and done-criteria; the MVP boundary is marked | +| `TODO.md` | The delivery ledger: open items carried forward, and one Landed row per retired W unit with its pull requests and criteria range | | `OPEN-QUESTIONS.md` | Living register of unknowns, gaps, deferred decisions. §1 is now a **decision log** — the eight MVP-blocking gaps are all resolved; start here for the reasoning behind a contract | ## The Code — `src/engine/` @@ -331,8 +331,8 @@ Two distinctions that are easy to get wrong: - **Non-goals are binding.** Anything listed as a non-goal in `01-vision.md` §5 is out of scope even if it looks trivial, even if you are already touching that file. -- **One unit at a time.** Do not start W because you noticed something while doing - W. Write it to `90-decisions.md`'s open register instead. +- **One unit at a time.** Do not start S because you noticed something while doing + S. Write it to `90-decisions.md`'s open register instead. - **No new dependencies** without a decision-log entry naming the alternatives rejected and why. - **No new public interfaces** that are not in `20-contract.md`. If you need one, stop and @@ -388,8 +388,10 @@ separate `slice`, `check`, `pr`, `resolve`, `track`, `docs`, `hold` or `resume`. `design/00-brief.md`, `10-design.md`, `20-contract.md`, `30-slices.md`, and `90-decisions.md` are the canonical inputs. This repository extends the generic kit in two deliberate ways: -- Existing W identifiers remain the slice ids. Renumbering them to S would break plans, tests, - issues, changelog entries, and merged history without changing the work. +- W identifiers are history. Every W unit is retired to a row under `## Landed` with an + `` anchor, so existing links still resolve; plans, tests, issues, changelog + entries and merged history keep citing them unchanged. New slices are `## S`, numbered from + S121 so no S id is ever mistaken for a W id. - The design and contract files contain marked, independently generated human pages. Commands must read the complete canonical file, including every marked block relevant to the task. diff --git a/agent.md b/agent.md index 5fe3b1c9..eea8768b 100644 --- a/agent.md +++ b/agent.md @@ -68,7 +68,7 @@ README, whose status is deliberately coarse. - **After a work-unit PR merges or the changelog gains an entry:** update the delivered count, completed chapter grouping, current checkpoint, and evidence URL in `roadmapData.ts`. Count - `### [x] W…` headings in `design/30-slices.md`, then explicitly account for any merged + Landed rows in `design/30-slices.md` that are not cancelled, plus `Status: done` S slices, then explicitly account for any merged units that the ledger has not yet recorded in the `completedBeyondTodo` exception list. Update the page before describing the new unit as delivered anywhere public. Reconcile `README.md`'s coarse status at the same time if it has become misleading, then regenerate diff --git a/build/Test-Documentation.ps1 b/build/Test-Documentation.ps1 index b1584ec2..55a294e6 100644 --- a/build/Test-Documentation.ps1 +++ b/build/Test-Documentation.ps1 @@ -360,6 +360,12 @@ function Get-DocumentationAnchor { $counts = @{} foreach ($line in $masked) { + # An inline is an anchor too: the retired-slice rows in + # design/30-slices.md carry one each, so links to #w outlive the headings. + foreach ($inline in [regex]::Matches($line, '')) { + $null = $anchors.Add($inline.Groups[1].Value) + } + $match = [regex]::Match($line, '^\s{0,3}#{1,6}\s+(.+?)\s*$') if (-not $match.Success) { continue diff --git a/design/30-slices.md b/design/30-slices.md index b8abe061..5be853f1 100644 --- a/design/30-slices.md +++ b/design/30-slices.md @@ -1,8 +1,9 @@ # Slices -> Canonical delivery ledger for the agent-kit workflow. This repository retains its established -> W identifiers because plans, tests, issues, changelog entries, and merged history already cite -> them. In this repository a W-numbered work unit is the agent kit's vertical slice. +> Canonical delivery ledger for the agent-kit workflow, in the kit's current format. New slices +> are `## S` sections above *Landed*, numbered from S121. The W-numbered units that preceded +> that format have all landed or been cancelled and are retired to rows under *Landed*. Their ids +> are history, kept because plans, tests, issues, changelog entries and merged history cite them. --- @@ -11,5374 +12,198 @@ slug: todo # TODO -**Status:** Living task list, ordered. The MVP is broken into **units of work** — each one -a single responsibility with its own contract references, dependencies, and done-criteria, -sized to be picked up in a fresh session. The MVP boundary is marked; everything below it -is post-MVP. - -> The MVP's Definition of Done is [`MVP.md`](MVP.md) §5 — every unit below rolls up to it. -> The contracts are [`04-core.md`](04-core.md), -> [`03-story-graph-kind.md`](03-story-graph-kind.md) and -> [`05-observability.md`](05-observability.md). Nothing unsettled remains for the -> MVP: [`OPEN-QUESTIONS.md`](OPEN-QUESTIONS.md) §1 is now a decision log. -> -> **Unit numbering is positional, like the doc numbering.** A unit inserted between two -> existing ones takes a letter suffix — `W3a` — rather than renumbering everything after -> it and invalidating every reference in `plans/`. Same convention as architecture §4a. - -Legend: `[ ]` not started · `[~]` in progress · `[x]` done - ---- - -## Done — Specification and Scaffold - -- [x] **[`03-story-graph-kind.md`](03-story-graph-kind.md)** — `Node`, `Choice`, - `Requirement` (reuses the `Condition` tree verbatim), `Consequence`, `Ending`, - `VariableSchema`, `AchievementDefinition`, seeded random-transition node, turn/settle - semantics, projection, worked Bureaucracy-arc example. -- [x] **[`04-core.md`](04-core.md)** — the Kind seam and the platform types, so the build - runs against contracts rather than decisions. Forced the `03` state reconciliation - (envelope vs kind-state). -- [x] **`executeAction` removed.** No client called it; the plan flow covers execution - (`games/05-text-client.md` §6). A method with no caller is a hypothesis. -- [x] **MVP contracts finalized** — campaign/content identity split ([04 §10.1](04-core.md#101-content-registry)), `visited` - semantics + start-of-game RNG stream ([03 §8.2](03-story-graph-kind.md), [04 §4](04-core.md#4-registration-and-the-pure-engine)/[§8](04-core.md#8-randomness)), `AdvanceResult` tightened, - Definition of Done agreed ([`MVP.md`](MVP.md) §5). -- [x] **All eight MVP-blocking gaps decided** — profile store, base reason strings, - authoring→registry builder, zero-choice campaigns, `InitialStateResult`, `params` to - `advance`, story-graph reason codes, the two format versions - ([`OPEN-QUESTIONS.md`](OPEN-QUESTIONS.md) §1). -- [x] Project scaffold: `src/engine/` package ([Engine Package](/docs/guide/engine-package)), - TypeScript strict, vitest, eslint with the - determinism guard (bans `Math.random`, `Math.pow/exp/log/sin/cos/tan`, `Date.now`). -- [x] Version control: this repo (engine **source + specs**). Companions: the games - [SubZeroDev.GameOfLife](https://github.com/The-Running-Dev/SubZeroDev.GameOfLife) and - [SubZeroDev.SunTrap](https://github.com/The-Running-Dev/SubZeroDev.SunTrap), and - the hosting layer [SubZeroDev.Platform](https://github.com/The-Running-Dev/SubZeroDev.Platform). - This repository and Sun Trap are **public** — verified 2026-08-02, when the engine - package's own visibility raised the question. This line previously said "All private." -- [x] **Seeded PRNG (PCG32)** + `deriveStream` substreams, serializable state. - `src/engine/src/core/determinism/pcg32.ts` — verified bit-identical to the reference vectors. -- [x] **Canonical serialization** (sorted keys, rejects non-finite). - `src/engine/src/core/persistence/canonical.ts`. -- [x] **Toolchain runs green** — `npm install && npm test && npm run lint && npm run - typecheck`; 15 tests across `pcg32` and `canonical`. - ---- - -# The MVP — Units of Work - -> **MVP DONE.** W1–W19 are all checked. [`MVP.md`](MVP.md) §5's Definition of Done is -> checked box-by-box against a named test for each one — the platform is proven. What -> follows below (Post-MVP) is depth and breadth building on it, not the MVP itself. - -Ordered by dependency. **W1–W8** are core (shared by every kind), **W9–W14** the -story-graph kind, **W15–W19** content, clients, and proof. A unit is done when its -done-criteria are demonstrated by a test, not by inspection. - -## Core - -### [x] W0 — CI and Documentation Gates -**Author** `.github/workflows/ci.yml` with one `engine` job (install / typecheck / lint / -test), and **install** the documentation system from the published container image, which -brings `docs-ci.yml` (link-and-terminology gate + production build) and `docs-deploy.yml` -(build + GitHub Pages) ready-made. Every unit below is then guarded from the first commit -rather than the last. The docs half is not optional garnish — `docs/Dockerfile` runs a dev -server, and Docusaurus enforces `onBrokenLinks` only during a production build, so without -CI that pass never runs at all. Both Docusaurus link checks are `'warn'` by design; the hard -gate is `build/Test-Documentation.ps1`, which fails on every relative link and heading -anchor. Also pins the Node floor (`engines`) so CI and local cannot drift, generates the -site homepage from `README.md`, and publishes the site. -- **Depends on:** nothing. -- **Status:** Done — [PR #3](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/3) - (workflow + docs system; landed across a few follow-up PRs — see the evidence below). -- **Done when:** `engine` plus the gate and the docs build all run green on a push; a - newer run for the same repository branch cancels its superseded push/PR run; Pages is - enabled and a push to `main` has deployed to the real published URL; the three - pull-request checks are required on the default branch (deploy runs only on `main`, so - requiring it would leave every PR pending); `engines.node` establishes Node 24 as the - floor while CI runs Node 24; and three deliberate failures — a failing test, a broken - README link, a broken spec link — have each turned their own check red, with run URLs - recorded. - - [x] Workflow authored, docs system installed, README converted, Node 24 aligned — all - green remotely on [PR #3](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/3). - - [x] Required checks configured on the `main` ruleset (`engine`, - *Documentation links and terminology*, *Verify Documentation Build*; deploy - excluded). - - [x] Red-path proof captured and reverted to green, with run URLs — full evidence in - [`plans/04-w0-phase-1-implementation.md`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/blob/main/plans/04-w0-phase-1-implementation.md). - - [x] First deploy to `main` — green twice: `47342b3` - ([run](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/actions/runs/30303045991), - PR #3) and `4e3effc` - ([run](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/actions/runs/30316383318), - PR #5). `https://game-engine.subzerodev.com/docs/` serves the generated homepage. - - [x] HTTPS enforcement — enabled in *Settings* → *Pages* by the repository owner. Note - for future reference: the domain is Cloudflare-fronted, so the `http` → `https` - redirect observable from outside is Cloudflare's "Always Use HTTPS" and is not by - itself evidence of this setting — the checkbox state is. - - **Known, deliberate end state:** `routeBasePath` stays `'docs'`, so the specs serve - from `https://game-engine.subzerodev.com/docs/` and the bare - `https://game-engine.subzerodev.com/` serves the README, generated into - `docs/src/pages/index.md` by the docs installer. That is a real route, so both - broken-link checks are `'throw'`. They were briefly `'warn'` while a static file held - the root instead — a static file serves the request but never satisfies a route - checker, so the navbar brand's link to `/` failed the build under `'throw'`. The - `/docs/` landing page lists the specs in reading order and adds one top-level sidebar - entry above the `engine` category; ordering inside `engine/` is unaffected. -- **Plan:** [`plans/02-w0-ci-workflow.md`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/blob/main/plans/02-w0-ci-workflow.md), [`plans/04-w0-phase-1-implementation.md`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/blob/main/plans/04-w0-phase-1-implementation.md) - -### [x] W1 — Core Contract Types and Module Skeleton -Create the module tree of 04 §1.1 (`kernel`, `session`, `persistence`, `projection`, -`validation`, `registry`, `localization`, `determinism`, `observability`, `composition`) and -put each declared type in the module that owns it. Types only — no behaviour. -- **Spec:** [04 §§1.1–3, 5–12, §17](04-core.md); [05 §§3–4](05-observability.md) for the - `observability` types; [`06-extensibility.md`](06-extensibility.md) §4–§5 for - `composition` — the two host roots and the `IdSource` and `Clock` port interfaces. -- **Depends on:** nothing. -- **Status:** Done — [PR #17](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/17). -- **Done when:** `npm run typecheck` passes with `exactOptionalPropertyTypes`; a dependency - scan shows no core module importing `kinds/`, `clients/`, or `mcp/`; `kindState` is - `unknown`, not a union; `GameState` carries no clock, profile, or kind state; - `EngineEvent` carries no timestamp and no trace id — both are added at the boundary - ([05 §6](05-observability.md#6-the-boundary--stamping-and-tracing)); every port is an interface with a working default, supplied only through - `EngineHost` or `SessionHost`, and no core module reads a clock or generates an id - itself ([06 §4](06-extensibility.md#4-the-composition-root)). - -### [x] W2 — RNG Handle and Stream Derivation -Wrap the built `Pcg32` behind `RngHandle`, and implement the normative `StreamId` → string -encoding. No generator state is persisted anywhere. -- **Spec:** [04 §8](04-core.md#8-randomness). -- **Depends on:** [W1](#x-w1--core-contract-types-and-module-skeleton). -- **Status:** Done — [PR #22](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/22). -- **Done when:** all four encoding forms round-trip exactly as specified; the same - `(seed, streamId)` yields identical draws across runs; different stream ids are - independent; `GameState` contains no RNG field. - -### [x] W3 — Pure Engine Kernel -`createEngine`, `createGame` (consuming `InitialStateResult`), `submitAction` (passing -`params`, returning the new state in `value`), `scene`, `availableActions`, `serialize`, -and a **validating** `deserialize` returning `CommandResult`. -- **Spec:** [04 §§2–5, §12](04-core.md). -- **Depends on:** [W1](#x-w1--core-contract-types-and-module-skeleton), - [W2](#x-w2--rng-handle-and-stream-derivation). -- **Status:** Done — [PR #33](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/33). -- **Done when:** a successful action appends exactly one monotonic `LoggedAction`; a - rejected action leaves serialized state byte-identical and does not advance the log; - every operation returns a new envelope and leaves its input untouched; `deserialize` - rejects a malformed envelope instead of casting; unknown kind, unknown campaign, - ended session, and unknown action each have a test. - -### [x] W3a — Observability: Emitter, Events, and Sinks -The **core half** of the operational event channel. `Emitter`, the per-resolution -`ResolutionEmitter` handle on `KindContext`, the `GameEvent`/`SystemEvent` split, the core -event set ([05 §8](05-observability.md#8-core-events)), and two sinks — `nullEmitter` and `recordingEmitter`. Numbered `3a` -rather than inserted, so no existing unit renumbers — the same convention architecture §4a -uses. - -**The boundary half is W7's**, because it cannot exist before the session store does: -stamping, spans, `attempt`, and `jsonlEmitter` all belong to the layer that owns a clock. -- **Spec:** [`05-observability.md`](05-observability.md) §§1–5, §7–§10, §12; - [04 §3.1](04-core.md#31-kindcontext), [§4](04-core.md#4-registration-and-the-pure-engine), - [§14](04-core.md#14-determinism-harness). -- **Depends on:** [W1](#x-w1--core-contract-types-and-module-skeleton), - [W3](#x-w3--pure-engine-kernel). (Kind events come with the kind units — - [W11](#x-w11--nodes-turn-and-settle) and [W12](#x-w12--scene-actions-projection-reason-codes).) -- **Status:** Done — [PR #34](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/34). -- **Done when:** `emit` returns `void` and no core code path reads anything back from a - sink; the core isolates every `emit`, so a sink that throws on every call does not - fail a game; a fixture replays byte-identically under `nullEmitter` and - `recordingEmitter`; the same fixture twice under `recordingEmitter` yields the - identical event sequence including ordinals, comparing modulo `gameId`; ordinals - restart at 0 each resolution, so a stream does not depend on how many games ran - before; no `EngineEvent` field is populated from a clock or an RNG draw; - `core.validation.completed` and `core.deserialize.rejected` are `scope: "system"` and - carry no `gameId`; a rejected unknown action id is absent from the emitted `data`; a - name outside `core.*` emitted by the core, or outside `kind..*` by a kind, - fails. - -### [x] W4 — Registry, Authoring Builder, Localization -The frozen in-memory `ContentRegistry`; `AuthoredText` → `BuiltCampaign` pure builder; the -protected `core.reason.*` string merge; `LocKey` resolution. Parsing and file I/O stay in -an outer adapter. -- **Spec:** [04 §10.1](04-core.md#101-content-registry), - [§12](04-core.md#12-reason-codes-state-changes-messages), - [§17](04-core.md#17-identifier-conventions). -- **Depends on:** [W1](#x-w1--core-contract-types-and-module-skeleton). -- **Status:** Done — [PR #35](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/35). -- **Done when:** identical key/text pairs deduplicate and conflicting ones fail; a write - into `core.reason.*` is rejected; a registered reason code with no message fails - construction; the engine package performs no filesystem or network I/O. - -### [x] W5 — Tiered Validation -The Tier 1 / Tier 2 framework, identifier and `LocKey` rules, delegating kind checks to -`validateCampaign`. -- **Spec:** [04 §11](04-core.md#11-tiered-validation), [§17](04-core.md#17-identifier-conventions). -- **Depends on:** [W4](#x-w4--registry-authoring-builder-localization). -- **Status:** Done — [PR #36](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/36). -- **Done when:** a Tier 1 error fails registry construction with a path; a Tier 2 warning - loads and is reported; duplicate and malformed identifiers fail; an unvalidated - registry can never be frozen. - -### [x] W6 — Projection -`Engine.view`, the `player` / `ai` audiences, and the `kind.project` seam. -- **Spec:** [04 §9](04-core.md#9-projection). -- **Depends on:** [W3](#x-w3--pure-engine-kernel). -- **Status:** Done — [PR #37](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/37). -- **Done when:** `seed`, `actionLog`, and raw `kindState` cannot reach a client by any - path; the `ai` audience is not wider than `player` by default. - -### [x] W7 — Session Store -The in-memory store: `listCampaigns`, `getScene`, `getView`, `createSession`, -`resumeSession`, `submitAction`, `saveGame`, `loadGame`. Persist canonical blobs, not live -objects. **Owns the observability boundary** ([05 §6](05-observability.md#6-the-boundary--stamping-and-tracing)) — the half W3a deliberately leaves -out, because stamping needs the layer that has a clock. -- **Spec:** [04 §7](04-core.md#7-the-session-store-and-the-platform-api), - [§10.2](04-core.md#102-save-envelope-and-migration); - [05 §6](05-observability.md#6-the-boundary--stamping-and-tracing), - [§6.1](05-observability.md#61-how-per-command-context-reaches-an-event), - [§11](05-observability.md#11-incident-forensics--a-bug-report-is-a-fixture). -- **Depends on:** [W3a](#x-w3a--observability-emitter-events-and-sinks), - [W3](#x-w3--pure-engine-kernel), [W6](#x-w6--projection). -- **Status:** Done — [PR #38](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/38). -- **Done when:** save mid-session → load → continue loses no state; two sessions cannot - mutate each other; `savedAt`, owner ids, and other host metadata never appear in a - serialized `GameState`; every command wraps the base emitter per call via - `withEmitter` and stamps `emittedAt`, `traceId`, `spanId`, `attempt` and `sessionId`; - two concurrent commands never cross-attribute an event, verified with interleaved - sessions rather than asserted; `attempt` increments on **rejected** submissions too, - so repeated invalid actions are distinguishable where `seq` repeats ([05 §5](05-observability.md#5-correlation-without-a-clock)); - `jsonlEmitter` writes one stamped record per line. - -### [x] W8 — Profile Store -`PlayerProfile`, `ProfileStore`, `profileId` on `CreateSessionConfig`, and the post-action -idempotent upsert. -- **Spec:** [04 §7.1](04-core.md#71-the-profile-store). -- **Depends on:** [W7](#x-w7--session-store). -- **Status:** Done — [PR #39](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/39). -- **Done when:** an unlock survives a new session with the same `profileId`; no `profileId` - means no read and no write; missing and corrupt both load an empty profile with the - right warning; a write failure warns without rolling back the game action; a profile - read can be shown never to affect resolution. - -## The Story-Graph Kind - -### [x] W9 — Variables and Consequences -`VariableSchema`, typed `set` / `increment` / `decrement`, clamp-after-all-effects, sorted -iteration of state-affecting records. -- **Spec:** [03 §2](03-story-graph-kind.md#2-variable-schema--fully-typed-n6), - [§5](03-story-graph-kind.md#5-consequences--typed-effects), [§8.1](03-story-graph-kind.md#81-state). -- **Depends on:** [W1](#x-w1--core-contract-types-and-module-skeleton). -- **Status:** Done — [PR #41](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/41). -- **Done when:** undeclared and mistyped writes are rejected; `+5` then `-5` on a clamped - int nets zero rather than clipping; a save/load round trip cannot reorder a `Record`. - -### [x] W10 — Conditions and Requirements -The frozen `Condition` evaluator plus this kind's field namespace (`var.*`, `turn`, -`visited.*`, `achieved.*`, `ending`). -- **Spec:** [03 §6](03-story-graph-kind.md#6-requirements-and-conditions); - [04 §18](04-core.md#18-frozen-primitives). -- **Depends on:** [W9](#x-w9--variables-and-consequences). -- **Status:** Done — [PR #43](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/43). -- **Done when:** only the frozen operator set evaluates; every `field` path is checked at - load against the schema and node set; an unknown path is a Tier 1 error. - -### [x] W11 — Nodes, Turn, and Settle -The four node kinds, `enter(nodeId)`, the settle loop, the `SETTLE_STEPS` guard, and -`initialState` returning `InitialStateResult`. -- **Spec:** [03 §3](03-story-graph-kind.md#3-nodes--the-single-content-type-n7), - [§8.1](03-story-graph-kind.md#81-state), [§8.2](03-story-graph-kind.md), - [§8.4](03-story-graph-kind.md#84-events). -- **Depends on:** [W2](#x-w2--rng-handle-and-stream-derivation), - [W3a](#x-w3a--observability-emitter-events-and-sinks), - [W9](#x-w9--variables-and-consequences), [W10](#x-w10--conditions-and-requirements). -- **Status:** Done — [PR #44](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/44). -- **Done when:** an auto/random chain settles to a choice or ending; every entry increments - its visit count, including the start node and pass-throughs; a 64-step - non-terminating chain fails with `settle_guard_tripped`; a start that settles onto an - ending reports `status: "ended"`; random transitions reproduce from seed + action log; - the settle loop emits `settle.step`, `node.entered` (with `visitCount`) and - `random.picked`, and a stream diff localizes a seeded divergence to one transition. - -### [x] W12 — Scene, Actions, Projection, Reason Codes -`availableActions` (omit on `showWhen`, disable with a reason on `requirements`), `scene`, -the slim `StoryGraphView`, and the kind's reason codes. -- **Spec:** [03 §4](03-story-graph-kind.md#4-choices-and-transitions), - [§8.3](03-story-graph-kind.md#83-reason-codes), [§8.4](03-story-graph-kind.md#84-events), - [§9](03-story-graph-kind.md#9-projection--what-a-client-sees); [04 §6](04-core.md#6-scenes-and-actions-generic). -- **Depends on:** [W6](#x-w6--projection), [W11](#x-w11--nodes-turn-and-settle). -- **Status:** Done — [PR #47](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/47). -- **Done when:** a `showWhen`-hidden choice is absent from the view **and** returns - `unknown_action` when submitted — indistinguishable from a nonexistent id; a gated - choice renders disabled with its `requirementFailKey`; hidden variables and visit - counts never appear in a projection; `StoryGraphView` repeats nothing the generic - `Scene` already carries. - -### [x] W13 — Endings and Achievements -Ending resolution, achievement evaluation after every turn, unlock-once into `kindState` -plus an `achievement_unlocked` `StateChange`. -- **Spec:** [03 §7](03-story-graph-kind.md#7-achievements), [§8.2](03-story-graph-kind.md). -- **Depends on:** [W8](#x-w8--profile-store), [W11](#x-w11--nodes-turn-and-settle). -- **Status:** Done — [PR #51](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/51). -- **Done when:** an achievement fires exactly once across repeated turns; the unlock is - readable as `achieved.` in a later condition; `advance` performs no I/O. - -### [x] W14 — Story-Graph Validation -The kind's Tier 1 and Tier 2 checks via `validateCampaign`. -- **Spec:** [03 §11](03-story-graph-kind.md#11-validation-story-graph-specific). -- **Depends on:** [W5](#x-w5--tiered-validation), [W11](#x-w11--nodes-turn-and-settle). -- **Status:** Done — [PR #55](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/55). -- **Done when:** dangling `goto`, undeclared variable, duplicate id, missing `LocKey`, - non-visible variable in text, and a non-positive-integer `weight` each fail Tier 1 - with a path; unreachable nodes, exitless cycles, and `no_reachable_choice` warn at - Tier 2 without blocking the load. - -## Content, Clients, Proof - -### [x] W15 — The Bureaucracy Campaign and Broken Fixtures -Author 03 §12 in the W4 source form with all its strings, plus four deliberately broken -copies: dangling node, undeclared variable, unreachable node, settlement cycle. -- **Spec:** [03 §12](03-story-graph-kind.md#12-worked-example--the-mvp-bureaucracy-arc); - `games/bulgaria.md`; [MVP §3](MVP.md#3-in-scope). -- **Depends on:** [W4](#x-w4--registry-authoring-builder-localization), - [W14](#x-w14--story-graph-validation). -- **Status:** Done — [PR #60](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/60). -- **Done when:** the valid campaign loads with no Tier 1 errors; the loop reaches its - `office_visits >= 3` gate; the seeded clerk transition reproduces; each broken fixture - produces its expected tier and path; every authored string resolves through the - registry. - -### [x] W16 — Text Client -The plain proving instrument, over `SessionStore` only. -- **Spec:** [04 §§6–7](04-core.md); [`09-clients.md`](09-clients.md) — the contract, and - [§4](09-clients.md#4-the-api-coverage-checklist) the checklist; - [MVP §5](MVP.md#5-definition-of-done--the-mvp) "Honest." -- **Depends on:** [W7](#x-w7--session-store), [W12](#x-w12--scene-actions-projection-reason-codes). -- **Status:** Done — [PR #63](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/63). -- **Done when:** the **API coverage checklist** ([09 §4](09-clients.md#4-the-api-coverage-checklist)) is complete for the text-client - column — all nine operations that existed when W16 shipped exercised by automated - tests, not by inspection (W48 later adds and proves the tenth); it - imports nothing from `kinds/` and never reads a persisted `GameState`; requirement - failures render from reason codes, never matched English; an unknown reason code - renders rather than crashing ([09 §5](09-clients.md#5-reason-codes-and-messages)). - -### [x] W17 — MCP Server -The same operations as tools — a sibling adapter, no AI-specific path. -- **Spec:** [04 §13](04-core.md#13-the-mcp-surface); [09 §7](09-clients.md#7-mcp-is-a-sibling-not-a-special-case) - — MCP is a sibling, not a special case; the tool table itself lives in - [SubZeroDev.ServiceContract's `mcp-tool-contract.md`](https://github.com/The-Running-Dev/SubZeroDev.ServiceContract/blob/main/mcp-tool-contract.md). -- **Depends on:** [W7](#x-w7--session-store), [W12](#x-w12--scene-actions-projection-reason-codes). -- **Status:** Done — [PR #66](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/66). -- **Done when:** every tool matches its documented args and results and maps - one-to-one onto a store operation, with no tool that is not one ([09 §4](09-clients.md#4-the-api-coverage-checklist)); the MCP column of - the coverage checklist is complete; an agent completes the arc; the same seed and - choices, under the same counting `IdSource`, produce **byte-identical** `serialize()` - output to W16's run — the client contract's proof ([09 §1](09-clients.md#1-the-rule-made-testable)); an agent sees no more than a human client does, including - getting `unknown_action` for a hidden choice. - -### [x] W18 — Determinism Harness -The `PlaythroughFixture` runner, committed golden files, property tests, and the -sink-independence pass. -- **Spec:** [04 §14](04-core.md#14-determinism-harness); [05 §12](05-observability.md#12-validation-and-tests). -- **Depends on:** [W3a](#x-w3a--observability-emitter-events-and-sinks), - [W15](#x-w15--the-bureaucracy-campaign-and-broken-fixtures). -- **Status:** Done — [PR #70](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/70). -- **Done when:** the same seed + action log serializes byte-identically; a one-byte golden - edit fails the suite; N random seeds run twice match; `deserialize(serialize(state))` - round-trips; every fixture replays byte-identically under `nullEmitter` and - `recordingEmitter`; the event stream is golden-filed and a stream diff fails the suite - on an unintended behavioural change; the suite passes in Node with no DOM, network, or - AI adapter installed. - -### [x] W19 — MVP Acceptance -Walk [`MVP.md`](MVP.md) §5 and attach test evidence to each box. -- **Depends on:** every unit above. -- **Status:** Done — [PR #71](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/71). -- **Done when:** every box is checked with a named test. **MVP DONE.** - ---- - -## Post-MVP — Depth - -### Rigour: The Replay Regression Oracle - -Replaying committed fixtures across **engine versions**, per -[`07-replay.md`](07-replay.md). Distinct from W18, which compares a build against itself. - -**Not MVP.** It compares versions, and before W19 there is only one. Sequenced here so the -contract is settled while the reasoning is fresh, which is the same call observability took. -Broken into four units — see `plans/27-replay-oracle-programme.md` for the reasoning behind -the split and the decisions each one resolved. - -### [x] W20 — Engine Versioning and Release Tags -Set a real version on the engine package and define the tagging scheme the oracle's -cross-version comparison depends on. -- **Spec:** [07 §2](07-replay.md#2-fixtures-are-inputs-not-state), [§8](07-replay.md#8-where-this-runs). -- **Depends on:** nothing. -- **Status:** Done — [PR #73](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/73). -- **Done when:** `src/engine/package.json` carries a real semver; a documented tag scheme - exists; the version is readable from code without a runtime dependency; the first tag - is cut at the current MVP-done commit. - -### [x] W21 — Replay Oracle: Outcome and the Runner -The `Outcome`/`Decision` projection and the three-verdict runner (07 §3, §6), driven by a -counting `IdSource` ([06 §5.1](06-extensibility.md)) so `createGame` itself replays. Core-owned -and kind-agnostic, the same split `core/determinism/harness.ts` (W18) used, proved first -against a synthetic kind. Composed directly against `Engine` and `ProfileStore`, not -`createInMemorySessionStore` — its client-facing `SessionStore` surface never returns the raw -`GameState` `finalStatus`/`terminal` need (07 §3.2, revised from this unit's original plan -during implementation). -- **Spec:** [07 §2–§3](07-replay.md#2-fixtures-are-inputs-not-state), [§5](07-replay.md#prerequisite-a-controllable-idsource), [§6](07-replay.md#6-the-runner-and-its-verdicts). -- **Depends on:** [W20](#x-w20--engine-versioning-and-release-tags). -- **Status:** Done — [PR #73](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/73). -- **Done when:** all three verdicts (`match`/`diverged`/`unrunnable`) are reachable and - tested; `at` is a `Decision.index`, never a `seq`; a rejected submission records - `seq: null` and does not stop the replay; achievements are read from an in-memory - `ProfileStore` after the last submission. - -### [x] W22 — Replay Oracle: The Corpus -The committed `fixtures/replay/*.{fixture,outcome}.json` set (07 §4) against the real -Bureaucracy campaign: every MVP §5 playable box, plus a deliberate edge case. Also promotes -`createCountingIds` to `core/determinism/counting-ids.ts` and extracts the single real -`story-graph` kind assembly (`kinds/story-graph/kind.ts`), de-duplicating five byte-identical -copies test files accumulated across W16–W19 that a sixth (this unit's own corpus test) would -otherwise have joined. -- **Spec:** [07 §4](07-replay.md#4-the-corpus), [§5](07-replay.md#prerequisite-a-controllable-idsource). -- **Depends on:** [W21](#x-w21--replay-oracle-outcome-and-the-runner). -- **Status:** Done — [PR #73](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/73). -- **Done when:** every MVP §5 *Playable* box has a fixture (the arc, the gated choice, the - seeded transition, the achievement, the loop gate); at least one deliberate edge-case - fixture exists (a rejection, an unknown action); `kinds/story-graph/kind.ts` is the - single kind assembly and the five duplicates are gone; a hand-edited `.outcome.json` - produces `diverged` with the right `at`. - -### [x] W23 — Replay Oracle: CI Wiring -No `paths` filter was actually kept on `pull_request` — one was tried and reverted (`ci.yml`'s -own comment: a path-filtered required check that never starts leaves a PR waiting on a report -that never arrives). The equivalent skip lives *inside* the `engine` job instead ("Determine -whether the engine package changed"), so it always reports while skipping the expensive steps -on a documentation-only PR ([07 §8](07-replay.md#8-where-this-runs)). `push` to `main` stays -unfiltered regardless, since path filters and tag pushes don't reliably combine. A new -`release-tag-replay` job runs only on `v*` tags, extracts the previous tag's committed -`.outcome.json` files via `git show`, and runs the corpus test against them via -`REPLAY_BASELINE_DIR` — the actual cross-version comparison. -Regenerating a committed `.outcome.json` is a deliberate, reviewed, single-fixture step — never -an automatic sweep ([07 §7](07-replay.md#7-intended-change-versus-regression)). -- **Spec:** [07 §7–§8](07-replay.md#7-intended-change-versus-regression). -- **Depends on:** [W20](#x-w20--engine-versioning-and-release-tags), [W22](#x-w22--replay-oracle-the-corpus). -- **Status:** Done — [PR #73](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/73). -- **Done when:** the suite runs on engine-package changes and on release tags; it does not - run on documentation-only changes (verified live: a `plans/`-only PR reported `engine` - green in 22s rather than not running at all). Regenerating an outcome file is a - documented, deliberate per-fixture command, never a sweep. **The release-tag job's - comparison branch is now verified live**, not just by local shell-logic testing: cutting - `v0.2.0` (the first commit with a replay corpus) found the job's checkout never fetched - sibling tags (`actions/checkout`'s `fetch-tags` defaults to `false`), so the comparison - always fell through to "nothing to compare yet" regardless of what tags existed — fixed - in `fetch-tags: true`, then proven end to end with a disposable tag: `4 passed | 5 - skipped` against `v0.2.0`'s real fixtures (PR #76). Milestone M3 - (`plans/27-replay-oracle-programme.md`) needed restating too — it said the *second* tag - would prove this, which undercounts by one: `v0.1.0` predates the corpus entirely, so a - second tag would have hit the same fixture-free skip on correct code. `v0.2.0` is the - actual milestone M3 needed (`plans/35-w26-toolchain-upgrade.md`, Decision 4). - -### Rigour: Spec and Toolchain Debt - -Not new capability — the documentation and tooling debt six units (W8–W13) each correctly -deferred as "not blocking," none since picked up because no unit owned them. Named and -sequenced in [`plans/33-post-mvp-programme.md`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/blob/main/plans/33-post-mvp-programme.md), Tranche A. - -### [x] W24 — Core Spec Reconciliation -Codifies in `04-core.md` §12 and §18 the conventions the code has implemented and depended on -since W8–W13 (two `StateChange` shapes, the `.reason.*` messageKey namespace, the -`Condition` shape itself), fixes one intra-document contradiction in `03-story-graph-kind.md` -§9, closes the doc-tree numbering item, ticks all eighteen boxes of `09-clients.md` §4's API -coverage checklist with a real test cited per box, and corrects six entries `TODO.md` itself -had gotten wrong. -- **Spec:** [04 §12](04-core.md#12-reason-codes-state-changes-messages), [§18](04-core.md#18-frozen-primitives); [03 §9](03-story-graph-kind.md#9-projection--what-a-client-sees); [09 §4](09-clients.md#4-the-api-coverage-checklist). -- **Depends on:** nothing. -- **Status:** Done — [PR #77](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/77). -- **Done when:** every convention is stated where the interface it belongs to is defined, not - only where it is used; `09-clients.md`'s checklist reflects real, run, passing tests - rather than an unticked table beside a closed MVP claim; every corrected `TODO.md` entry - cites the code or plan that proves the correction; no file under `src/engine/` changes. -- **Plan:** [`plans/34-w24-core-spec-reconciliation.md`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/blob/main/plans/34-w24-core-spec-reconciliation.md) - -### [x] W25 — Simulation Kind Seam Reconciliation -Brings `10-simulation-kind.md` up to date against the conventions W24 codified and closes gaps -against the `Kind` interface it never addressed: folds `initialState`/`InitialStateResult` into -§3, adds a deferred §14 Validation (matching the `history` deferral's own idiom), narrows the -`GameStatus` mapping in §2 to match `12-world-graph-kind.md` §8's identical resolution, and -replaces `outcome()`'s shape — dropping an `endingId` this kind never had a concept for, -widening `resolution` to three values, and adding `goalsFailed`. Completes §15 ("What Remains -Upstream"), which accounted for one of `SimulationKindState`'s ten fields before this. -- **Spec:** [10 §2](10-simulation-kind.md#2-kindstate--what-belongs-here), [§3](10-simulation-kind.md#3-the-turn-is-a-week), - [§12](10-simulation-kind.md#12-terminal-identity), [§14](10-simulation-kind.md#14-validation), - [§15](10-simulation-kind.md#15-what-was-ported-and-what-was-found-along-the-way). -- **Depends on:** [W24](#x-w24--core-spec-reconciliation). -- **Status:** Done — [PR #80](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/80). -- **Done when:** every field `SimulationKindState` names has a row in §15; `outcome()`'s shape - is justified against the upstream source rather than assumed (confirmed by full-text - search that this kind has no ending concept); a review finding that the - `week_limit_reached`/goal precedence is unresolved is flagged explicitly rather than - guessed at; no file under `src/engine/` changes. -- **Plan:** [`plans/32-w25-simulation-kind-seam-reconciliation.md`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/blob/main/plans/32-w25-simulation-kind-seam-reconciliation.md) - -### [x] W26 — Toolchain Upgrade -`vitest` 2.1.9 → 4.1.10, `eslint` 9.39.5 → 10.8.0. `typescript-eslint` did **not** need to -move — 8.65.0 already declares `eslint: "^8.57.0 || ^9.0.0 || ^10.0.0"` as a valid peer, so the -plan's anticipated three-package bump was actually two. `npm audit` went from 6 vulnerabilities -(3 moderate, 2 high, 1 critical) to **0** — better than the "partial clear is fine" outcome the -plan allowed for. The determinism guard's specific rules (`no-restricted-properties`, -`no-restricted-globals`, `no-restricted-imports`) are unchanged in eslint 10's migration guide; -Node 24 already satisfies its new floor (`>=20.19`/`>=22.13`/`>=24`). -- **Spec:** none — tooling only. -- **Depends on:** [W18](#x-w18--determinism-harness), [W22](#x-w22--replay-oracle-the-corpus) — - both done, the entry's own stated precondition, met twice over once W20–W23 added the - replay oracle as a second instrument. -- **Status:** Done — [PR #82](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/82). -- **Done when:** 39 files / 445 tests pass unchanged; the determinism guard is proven still - live by a deliberate red-path test (a `Math.random()` and a banned core→kind import each - independently fail lint, both reverted); the replay corpus and the W18 event-stream golden - are verified byte-unmodified by the upgrade, not merely unregenerated; `v0.2.0` — tagged - ahead of this unit specifically so a real predecessor corpus would exist — precedes the - tag this unit cuts. -- **Plan:** [`plans/35-w26-toolchain-upgrade.md`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/blob/main/plans/35-w26-toolchain-upgrade.md) - -### Rigour: Session Capture - -Turning a played session into a fixture, per -[`08-session-capture.md`](08-session-capture.md). **Gated on the hosting layer**, which -MVP §4 defers — there is nothing to capture from a local client the developer drives -themselves. - -- [ ] Capture emits a `ReplayFixture` and no new format ([08 §2](08-session-capture.md#2-what-is-captured)). -- [ ] The refusal rules hold under test: no identity, only kind-declared params, no timing - ([08 §3](08-session-capture.md#3-what-is-refused)). A fixture built from a submission carrying undeclared keys drops them. -- [ ] Capture triggers only on an `error`-severity event or an explicit report — never as - background collection ([08 §5](08-session-capture.md#5-when-a-capture-may-be-taken)). -- [ ] Promotion into the replay corpus is a reviewed human step, never automatic ([08 §7](08-session-capture.md#7-promotion-is-a-one-way-door)). - -> **Not sliced, and the reason changed.** The stated gate above — "there is nothing to capture -> from a local client the developer drives themselves" — has quietly stopped being true: -> [SubZeroDev.Adventures](https://github.com/The-Running-Dev/SubZeroDev.Adventures) is a real -> hosted deployment with an API, Postgres persistence and accounts. The hosting precondition is -> met. -> -> **What blocks it now is a missing contract surface, which is `/contract`'s work and not a -> slice's.** [08 §3.2](08-session-capture.md#3-what-is-refused) requires capture to keep "only -> the parameters the kind declares," dropping every other key — but no kind declares its -> parameters anywhere the core can read. [04 §6](04-core.md#6-scenes-and-actions-generic) states -> the opposite outright: `AvailableAction` "describes a verb, not its parameter space," and -> pushes the parameter domain into the kind's projection, which the core holds as `unknown` by -> design. So §3.2 is unimplementable as written, and a slice for it would have to invent a -> `Kind` signature the contract does not have, which no slice may do. The same gap makes -> [08 §9](08-session-capture.md#9-deferred)'s first deferral stale for a second reason: it -> defers simulation-kind capture because "the kind does not exist yet," and W32–W57 built it. -> -> **Route to `/contract`:** either the `Kind` interface gains a declared-parameter surface, or -> §3.2 is restated against something a host can actually enforce. Everything else in this -> section is buildable once that is settled. - -### Depth: Life in the Fast Lane (The `simulation` Kind) - -- [x] **Specify the kind first, in this repository.** Done across W25 (seam reconciliation) - and the four-unit programme W32–W35 (field-level port). The simulation equivalent of - [`03-story-graph-kind.md`](03-story-graph-kind.md) now exists in - [`10-simulation-kind.md`](10-simulation-kind.md), reconciled with `04` §3, the - `GameState` envelope, `Kind.outcome`, and the `kind.simulation.*` event namespace - ([05 §9](05-observability.md#9-kind-events)) — see - [`plans/36-simulation-kind-programme.md`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/blob/main/plans/36-simulation-kind-programme.md) - for the full split and findings. -- [x] **Build the engine-owned simulation kind.** Done across W36–W40: state and variables, - the weekly pipeline, content definitions, the vertical slice, validation, and the real - `Kind` assembly all run through the core engine seam. -- [x] **Engine-owned "Stable Life" fixture reaches a win and a loss through the replay - oracle.** W40 commits both paths and records the remaining honest gap: simulation - projection is not implemented, so neither path is playable through the text client or - MCP yet. That is separate from the engine/replay milestone and remains part of the full - game Definition of Done below. -- [ ] **From a proven loop to a played game — W50–W57 below.** W36–W40 proved the kind - through the engine seam and the replay oracle; it is still not playable by a person, - and eleven of §3's fifteen end-of-week systems, twenty-eight of §4.2's thirty - `ActionType`s, §6.1's derived-value layer and §9's projection are all specified and - unbuilt. Those eight units are that gap, sliced. -- [ ] Its Definition of Done: `games/life-in-the-fast-lane.md`. - -### Depth: Sun Trap (The `world-graph` Kind) - -The third kind, and the first spatial one. **Specified —** -[`12-world-graph-kind.md`](12-world-graph-kind.md) fixes the seam; the game it serves lives -in [SubZeroDev.SunTrap](https://github.com/The-Running-Dev/SubZeroDev.SunTrap) ([12 §17](12-world-graph-kind.md#17-what-remains-in-the-game-repository)). - -**Now has its own programme doc**, the same milestone `simulation` reached with `plans/36`: -[`plans/39-world-graph-kind-programme.md`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/blob/main/plans/39-world-graph-kind-programme.md), -proposing **W41–W49** (one consumer-boundary unit, three contract units, five build units), -cut once Sun Trap's own -design docs (`content-and-systems.md`, `game-design.md`, `mvp.md`, `client-specification.md`) -existed to size it against — the precondition `plans/33`'s own Decision 3 named as missing -when it declined a programme doc for this kind. - -- [x] **`KindContext.derive` and the `tick` stream — already built, since W1/W2.** Not a gap - this kind needs to close: `KindContext.derive` (04 §3.1) and all four `StreamId` - variants, including `tick` and `agent`, exist in `core/kernel/types.ts`, - `core/kernel/engine.ts`, `core/determinism/types.ts` and `core/determinism/rng.ts` - (the encoder, exhaustiveness-guarded). `simulation`'s NPC draws and this kind's tick - draws already have a reachable home — 04 §3.1's own callout box already documents - `derive` closing exactly the reachability gap this checkbox describes as still open. - - -### [x] W41 — Engine Consumer Boundary {#w41} - -[PR #108](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/108) landed the -whole boundary: the package renamed to `@the-running-dev/game-engine`, one root export -(`src/engine/src/index.ts`, explicit named re-exports only), `exports` with `types` and -`import` targets, `files: ["dist"]` plus a `tsconfig.build.json` that excludes tests and -`campaigns/` from the emit, a `consumer-smoke/` project that installs the **packed -tarball** rather than linking the source, three new required-CI steps (pack, tarball -inspection, consumer smoke — all three verified running green on `main` at `db9c62a`), -and `release-engine-package.yml` publishing on a `v*` tag with `packages: write` and no -stored credential. Review also corrected a drift this unit made live: `package.json` -had never tracked the release tags (`v0.1.0` shipped `0.0.0`; `v0.2.0` and `v0.3.0` both -shipped `0.1.0`), harmless while private and unpublished, fatal once `npm publish` ships -what the manifest says. Set to `0.3.0`; tag and manifest move together from here. -**Now complete.** `@the-running-dev/game-engine@0.4.0` published on the `v0.4.0` tag -(2026-08-02), verified against the packages API rather than inferred from the workflow's -exit status; the coordinate is recorded in `plans/40`'s Done-When, and milestone **T0** -is reached. One deviation, recorded rather than waved through: it published **public** -where `plans/39` and `plans/40` specify private. That also surfaced a stale claim in -this file's own introduction, which described every companion repository as private when -this one and Sun Trap are public — corrected in the same change -([`OPEN-QUESTIONS.md`](OPEN-QUESTIONS.md) §2). No read-access grant was needed as a -result. - -- **Plan:** [`plans/40-w41-engine-consumer-boundary.md`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/blob/main/plans/40-w41-engine-consumer-boundary.md) - -### [x] W42 — World-Graph Runtime State Contract {#w42} - -Merged in [PR #116](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/116). -Authoritative map, guest, building, queue, staff, construction and finance shapes now live in -[`12-world-graph-kind.md`](12-world-graph-kind.md) §3. The plan remains the evidence trail: -[`plans/42-w42-world-graph-state-contract.md`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/blob/main/plans/42-w42-world-graph-state-contract.md), -including the correction to `plans/39`'s original sizing: six types in -`WorldGraphKindState`'s closure -(`Incident`, `ObjectiveProgress`, `Alert`, `TerrainCell`, `PathCell`, `Zone`) are -drafted in neither repository, so this is design work, not a port. - -### [x] W43 — World-Graph Content Definition Contract {#w43} - -Merged in [PR #119](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/119). -Maps, archetypes, buildings, products, terrain, incidents, scenarios, objectives, policies, -and achievements now have their complete source/runtime schema, W42 reconciliation, -validation tiers, and worked fixtures in `design/20-contract.md`'s -`engine/12-world-graph-kind.md` block and its generated reader copy. The execution -record and evidence checklist live in -[`plans/43-w43-world-graph-content-contract.md`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/blob/main/plans/43-w43-world-graph-content-contract.md), -which is historical rather than a second contract authority. - -### [x] W44 — World-Graph Resolution Contract {#w44} - -Merged in [PR #120](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/120). -The 20-system pipeline, utility scoring, canonical pathfinding, queue/service and staff-task -semantics, simultaneous terminal precedence, and deep batch invariance are in -`design/20-contract.md`'s `engine/12-world-graph-kind.md` block and its generated -reader copy. Its execution record is -[`plans/44-w44-world-graph-resolution-contract.md`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/blob/main/plans/44-w44-world-graph-resolution-contract.md), -which is historical rather than a second contract authority. - -### [x] W45 — World-Graph Kind Skeleton and Immediate Actions {#w45} - -Merged in [PR #125](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/125). -The source/runtime builder, total validation, shared spatial substrate, deterministic initial state, read surfaces, -production assembly, package exports, and nine no-time-passes reducers are reconciled -in [PR #125](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/125), with -[PR #124](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/124) retained as -the original implementation review. The execution record is -[`plans/45-w45-world-graph-kind-skeleton.md`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/blob/main/plans/45-w45-world-graph-kind-skeleton.md), -which is historical rather than a second contract authority. - -### [x] W46 — World-Graph Deterministic Tick Pipeline {#w46} - -Fixed-order systems, bounded `advance_ticks`, derived streams and batch invariance are delivered in -[PR #128](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/128), merged to -`main` at `6301a49`. Its implementation plan is -[`plans/46-w46-world-graph-tick-pipeline.md`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/blob/main/plans/46-w46-world-graph-tick-pipeline.md); -it follows the canonical world-graph contract and does not supersede it. - -### [x] W47 — World-Graph MVP Vertical Slice {#w47} - -The synthetic guest journey — spawn → walk → queue → buy → litter → clean → objective → win/lose — is delivered in -[PR #131](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/131), merged -to `main` at `2390750`. It follows the canonical world-graph contract and does not -supersede it. - -### [x] W48 — Preview/Client Parity {#w48} - -`previewAction` across Engine, session, text and MCP surfaces, with 09 §4 and `MVP.md` §5 amended in the same unit, is delivered in -[PR #133](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/133). - -### [x] W49 — World-Graph Validation, Scenario and Replay Guard {#w49} - -The canonical engine-owned MVP fixture and its Tier 1/Tier 2 validation landed in -[PR #134](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/134); deterministic -winning and losing replay pairs and release-corpus coverage landed in -[PR #136](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/136). -Session-parity replay cases, a clean-build serialization proof, and a consumer-smoke -rerun landed in [PR #138](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/138). -W49's engineering scope is complete. Package version selection and publication remain -external release actions, not evidence this unit claims. - -- [ ] **T4 programme gate:** batch invariance and determinism beyond the seed pass; an - immutable package version must carry the replay-guarded third kind so Sun Trap can - install it without a sibling checkout. - -- **Plan:** [`plans/39-world-graph-kind-programme.md`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/blob/main/plans/39-world-graph-kind-programme.md) - -### Depth: Finish the Bulgaria Adventure - -### [x] W27 — Bulgaria Adventure: The Driving Arc -The second real arc: `src/engine/src/campaigns/bulgaria-driving.ts`, authored from -`games/bulgaria.md`'s "Driving" and "BMW Ownership" scenes, mirroring -`bulgaria-bureaucracy.ts`'s established pattern. Picked over the other three remaining arcs -because reading the actual built Bureaucracy campaign against the design doc surfaced two real -discrepancies affecting Enterprise and Return specifically — both recorded in -[`OPEN-QUESTIONS.md`](OPEN-QUESTIONS.md) §2, neither resolved here. Demonstrates a branching -ending (two endings, gated by an earlier choice via `showWhen`) that Bureaucracy's single-ending -design never exercised. -- **Spec:** [03 §4](03-story-graph-kind.md#4-choices-and-transitions) (`showWhen`); - `games/bulgaria.md`, `games/bulgaria-adventure.md`. -- **Depends on:** nothing engine-side — `story-graph` is fully built; this is content only. -- **Status:** Done — [PR #86](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/86). -- **Done when:** both endings are reachable and the wrong-branch choice is absent, not merely - disabled, verified by test; `serialize()` output for each path is golden-filed and - round-trips; the determinism harness's sink-independence and replay-byte-identity checks - pass; test count grows from 39 files/445 tests. Replay-corpus fixtures were found not to - be cheap for a second campaign — the existing harness assumes one campaign per corpus - directory (`OPEN-QUESTIONS.md` §2) — so this unit has none; not blocking. -- **Plan:** [`plans/37-w27-bulgaria-driving-arc.md`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/blob/main/plans/37-w27-bulgaria-driving-arc.md) - -### [x] W28 — Bulgaria Adventure: The Return Arc -The third real arc: `src/engine/src/campaigns/bulgaria-return.ts`, authored from -`games/bulgaria.md`'s single "Expat Returns" scene. No separate plan file — the pattern was -proven twice by W27, and this arc's own design had no open question to resolve: unlike Driving, -`games/bulgaria-adventure.md` names no mechanic for Return beyond "seeds variables the other -arcs read," already found (`OPEN-QUESTIONS.md` §2, W27) not to be mechanically achievable. The -correct minimal design is therefore a single `choice` node whose four options (matching the -source scene's own four reactions) all converge on one shared `ending` — no invented flag or -branch. Deliberately the smallest arc built so far: one node, no variables, no achievement. -- **Spec:** `games/bulgaria.md`, `games/bulgaria-adventure.md`. -- **Depends on:** nothing engine-side — content only, same as W27. -- **Status:** Done — [PR #87](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/87). -- **Done when:** all four choices reach the one ending; `serialize()` output is golden-filed and - round-trips; the determinism harness's sink-independence and replay-byte-identity checks - pass; test count grows from 41 files/478 tests. - -### [x] W29 — Bulgaria Adventure: The Inheritance Arc -The fourth real arc: `src/engine/src/campaigns/bulgaria-inheritance.ts`, authored from -`games/bulgaria.md`'s three scenes ("Property Inheritance", "Village Life", "Family Meeting"). -Larger than the two before it — `games/bulgaria-adventure.md` names this arc's own exercise as -"branching on prior choices, relationship variables, an ending," which needed real design, not -just transcription. Two variables carry it: `family_tension` (int, visible) accumulates the way -Bureaucracy's own counters do, without gating anything; `has_documentation` (bool, set only by -`request_records` or `consult_lawyer` in the first scene) gates the one choice at the climax -that actually resolves the plot (`bring_out_documents`, via `showWhen`) — that option does not -exist at all for a player who never sought documentation, the clearest possible instance of -"branching on prior choices." A second, different kind of branch: `pretend_never_inherited` in -the second scene skips the climax entirely via an immediate `goto`, rather than a gate. Three -endings result: an early opt-out, an ungated "nothing resolved," and the one gated "documents -settle it" (`outcome: "win"`). -- **Spec:** `games/bulgaria.md`, `games/bulgaria-adventure.md`; - [03 §4](03-story-graph-kind.md#4-choices-and-transitions) (`showWhen`). -- **Depends on:** nothing engine-side — content only, same as W27/W28. -- **Status:** Done — [PR #88](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/88). -- **Done when:** the gated choice is confirmed absent (not disabled) without prior - documentation, and present and leading to the win ending with it; the early-opt-out branch - is confirmed to skip the climax node entirely; `family_tension` accumulates and clamps at - its floor; `serialize()` output for each of the four fixtures is golden-filed and - round-trips; the determinism harness's sink-independence and replay-byte-identity checks - pass; test count grows from 43 files/497 tests. - -### [x] W30 — Bulgaria Adventure: The Enterprise Arc -The fifth and final real arc: `src/engine/src/campaigns/bulgaria-enterprise.ts`, authored from -`games/bulgaria.md`'s "Starting a Business" and "Entrepreneur" scenes only — the arc's own third -scene, "Ultimate Reward," and its "It Builds Character" achievement, are already spent by -Bureaucracy (`OPEN-QUESTIONS.md` §2, found during W27). Design proposed for sign-off before -implementation, since inventing closing content is a bigger step than transcribing existing -scenes: `debt_cents` (int, visible) carries what remains of the named exercise -("accumulating debt"), as a running stat in the same idiom as Bureaucracy's own counters, not a -gate; no achievement, since the game's Definition of Done needs only "at least one" across the -whole game; one shared ending rather than a branch, since `games/bulgaria-adventure.md` names -"an ending" for this arc singular, same as the others, and with the achievement gone nothing -remaining calls for more structure. The ending's own prose is new, not adapted from any -`bulgaria.md` scene, since the one this arc was assigned is unavailable. - -**All five Bulgaria Adventure arcs are now built** — Bureaucracy (MVP), Driving (W27), Return -(W28), Inheritance (W29), Enterprise (W30). -- **Spec:** `games/bulgaria.md`, `games/bulgaria-adventure.md`. -- **Depends on:** nothing engine-side — content only, same as W27–W29. -- **Status:** Done — [PR #89](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/89). -- **Done when:** all four `starting_a_business` choices proceed to `entrepreneur`; each - `entrepreneur` choice's `debt_cents` effect is confirmed independently (0, 5000, or - 20000) and all four reach the one ending; `serialize()` output for the high-debt and - no-debt paths is golden-filed and round-trips; the determinism harness's - sink-independence and replay-byte-identity checks pass; test count grows from 45 - files/537 tests. - -- [ ] Its full Definition of Done beyond content: `games/bulgaria-adventure.md` lists MCP - parity, gated choices, seeded random, achievement persistence, save/load, validation, and - byte-identical replay as separate checkboxes, but all seven are platform capabilities - already proven generically (W7, W14, W16–W18) against Bureaucracy and re-exercised by - every arc's own test suite since — not separate work per arc. - -### [x] W31 — Save Migration -Builds the real save-migration mechanism `04-core.md` §10.2 specifies, closing the gap this -list carried since W3. `Kind` gains `version` and an optional `migrateState`; `Campaign` gains -an optional `migrateState` for content-id renames — neither is a new port -([06 §6](06-extensibility.md#6-adding-a-port)'s own rule rules that out, since a migration -function's whole purpose is to change what `serialize()` produces. `SessionStore.saveGame` -now stamps a real `SaveEnvelope` (`core/persistence/envelope.ts`) instead of a bare blob; -`loadGame` verifies its checksum and all five stamped fields, dispatching to `Kind.migrateState` -then `Campaign.migrateState` on a version mismatch, and failing loudly -(`save_requires_migration` / `migration_failed`) when no migration path resolves one. Proven -against a synthetic kind/campaign fixture, not a real Bulgaria campaign republish — every -shipped campaign is still at `1.0.0`. -- **Spec:** [04 §3](04-core.md#3-the-kind-interface--the-seam), [§10.2](04-core.md#102-save-envelope-and-migration); [06 §6](06-extensibility.md#6-adding-a-port). -- **Depends on:** nothing engine-side. -- **Status:** Done — [PR #92](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/92). -- **Done when:** every base-case save/load test still passes unmodified (the envelope is - transparent to `SessionStore`'s public surface); a kind-version mismatch with a - registered migration succeeds and flips `replayCompatible: false`; the same with no - migration registered fails loudly; a campaign-version mismatch does the same; both axes - moving at once run kind migration before campaign migration, proven by an ordering - guard, not just an assertion; a `saveFormatVersion`/`serializationVersion` mismatch fails - loudly (neither has ever moved); an `engineVersion` mismatch never gates a load; `npm run - typecheck && npm run lint && npm test` all pass. -- **Plan:** [`plans/38-save-migration-programme.md`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/blob/main/plans/38-save-migration-programme.md) - -### [x] W32 — Simulation Kind: State Types -The first contract unit of the simulation-kind programme -([`plans/36-simulation-kind-programme.md`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/blob/main/plans/36-simulation-kind-programme.md), -proposed there as W27). Doc-only — ports upstream §5.1, §5.3–§5.6 and §9.1 into -`10-simulation-kind.md` as new §2.1–§2.5 and §4.1: `CalendarState`, `WorldState` (with its -world-strangeness and chain-scope subsections), `StatusEffect`/`Opportunity`/`ScheduledEvent`/ -`PendingEventResponse` (with both lifecycle subsections), `GoalState`, `EconomyState`, and -`WeeklyActionPlan`'s own shape. Introduces this kind's `Cents`/`BasisPoints` primitives and its -sorted-`Record`-iteration rule, both reused by later units. Eight of `SimulationKindState`'s ten -fields are now specified in this repository; only `PlayerState` (W28, next) and the `GameAction` -schema `WeeklyActionPlan.actions` holds (W30) remain. -- **Spec:** [10 §2](10-simulation-kind.md#2-kindstate--what-belongs-here), [§4.1](10-simulation-kind.md#41-the-weekly-action-plan), - [§15](10-simulation-kind.md#15-what-was-ported-and-what-was-found-along-the-way). -- **Depends on:** [W25](#x-w25--simulation-kind-seam-reconciliation) (the seam must be complete - before field detail hangs off it, per `plans/36`). -- **Status:** Done — [PR #94](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/94). -- **Done when:** every field named in `SimulationKindState` (§2) except `PlayerState` has a - full type restated in this repository, reconciled against envelope-duplication and - forward-referencing not-yet-ported types (`NPCState`, `AgentState`, `GameAction`, - `Modifier`, `OpportunityDefinition`) by name rather than inventing placeholder shapes; - §15's table drops the rows this unit closes and gains no new ones; a genuine new open - item found during the port (`ChainScope`'s `"profile"` value has nowhere to persist) is - recorded in `OPEN-QUESTIONS.md`, not silently absorbed or dropped; no file under - `src/engine/` changes; `build/Test-Documentation.ps1` passes. -- **Plan:** [`plans/36-simulation-kind-programme.md`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/blob/main/plans/36-simulation-kind-programme.md) - -### [x] W33 — Simulation Kind: Actor State -The second contract unit of the simulation-kind programme -([`plans/36-simulation-kind-programme.md`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/blob/main/plans/36-simulation-kind-programme.md), -proposed there as W28). Doc-only — ports upstream §7 and §8.1–§8.9 into `10-simulation-kind.md` -as new §6.1–§6.11: the base/derived-value layer (`DerivedPath`, application order, stacking, -expiry), the shared `ActorState`/`PlayerState` shape, and its nine areas (identity, finances, -needs, attributes, education, career, housing, inventory, relationships). `ActorState` comes -over whole, shared verbatim by the player and every rival (`plans/36` Finding 1) — not ported -"for the player" with rival support deferred. Every field `SimulationKindState` (§2) names now -has a full shape in this repository. -- **Spec:** [10 §6](10-simulation-kind.md#6-player-state), [§15](10-simulation-kind.md#15-what-was-ported-and-what-was-found-along-the-way). -- **Depends on:** [W32](#x-w32--simulation-kind-state-types). -- **Status:** Done — [PR #95](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/95). -- **Done when:** `ActorState`'s open-keyed `Record` fields (`skills`, `reputation`, `flags`, - `counters`) are explicitly reconciled against `02-architecture.md` N6 ("the loose bag is - banned") rather than left to look like an unexamined exception; not-yet-ported types - (`AgentState`, `NPCState`, `JobDefinition`, `CourseDefinition`, `HousingDefinition`, - `ItemDefinition`, `BackgroundDefinition`) are forward-referenced by name; the integer - `0–100` range rule for needs/skills/attributes/reputation is stated somewhere, not - silently dropped; §15's table drops every row this unit closes; no file under - `src/engine/` changes; `build/Test-Documentation.ps1` passes. -- **Plan:** [`plans/36-simulation-kind-programme.md`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/blob/main/plans/36-simulation-kind-programme.md) - -### [x] W34 — Simulation Kind: Content Definition Types -The third and largest contract unit of the simulation-kind programme -([`plans/36-simulation-kind-programme.md`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/blob/main/plans/36-simulation-kind-programme.md), -proposed there as W29). Doc-only — ports upstream §13.3–§13.4 and §14.1–§14.9 into -`10-simulation-kind.md` as new §7.1–§7.10: `Modifier`/`Reward`, then jobs, courses, housing, -items, events, NPCs, goals/scenarios/difficulty, supporting definitions (opportunities, -achievements, headlines, employers, locations, backgrounds, traits, skills), and agents. -Corrects a factual error in `plans/32`'s Finding H and `plans/36`'s own Finding 2 — both claimed -upstream specifies no rounding rule for `Modifier.operation: "multiply"` against integer-cents -money; checked directly against the primary source while drafting §7.1, and it does ("rounded -half-away-from-zero after the full chain"). Both plan documents corrected in place. Every -upstream section this contract needs except the last unit's own (`ActionType`/`GameAction`, -`Requirement`, end-of-week ordering) is now specified in this repository. -- **Spec:** [10 §7](10-simulation-kind.md#7-content-definition-types), [§8](10-simulation-kind.md#8-conditions-and-requirements), - [§15](10-simulation-kind.md#15-what-was-ported-and-what-was-found-along-the-way). -- **Depends on:** [W33](#x-w33--simulation-kind-actor-state). -- **Status:** Done — [PR #96](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/96). -- **Done when:** every content type upstream §14.1–§14.9 names has a full shape in this - repository, with identity fields (`id`/`version`/`titleKey`) excluded per the - envelope-duplication rule; `Modifier`'s `multiply` semantics are stated precisely - (basis-points-shaped `value`, round-half-away-from-zero once after the full chain), not - merely flagged as unresolved; the `plans/32`/`plans/36` correction is recorded in both - plan documents, not only in the new spec content; not-yet-ported types (`Requirement`, - `GameAction`, `ActionOutcome`) are forward-referenced by name; §15's table drops every - row this unit closes and states that exactly one contract unit remains; no file under - `src/engine/` changes; `build/Test-Documentation.ps1` passes. -- **Plan:** [`plans/36-simulation-kind-programme.md`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/blob/main/plans/36-simulation-kind-programme.md) - -### [x] W35 — Simulation Kind: Resolution and Systems -The fourth and final contract unit of the simulation-kind programme -([`plans/36-simulation-kind-programme.md`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/blob/main/plans/36-simulation-kind-programme.md), -proposed there as W30). Doc-only — ports upstream §9, §10, §12.2–§12.3 and §13.2 into -`10-simulation-kind.md`: `ActionType`/`GameAction` (§4.2), the resolver dispatch mechanism and -per-action outcome (§5.1–§5.3, reconciled against `04-core.md`'s already-adopted -`StateChange`/`ValidationError`/`ValidationWarning` rather than restating upstream's own -divergent pre-adoption shapes), end-of-week system order and goal/failure precedence (§3), and -`Requirement`/`RequirementType` (§8.1). Closes **S1** of the programme's milestones — every -type `SimulationKindState` names, every content definition type, and the mechanics that -dispatch on both are now specified in this repository; `10-simulation-kind.md` stops being -"the seam only." - -Three reconciliation findings, not plain transcription: this kind's own runtime-validation -result needed a name (`ActionValidation`) distinct from `04-core.md`'s load-time -`ValidationResult` — upstream never had to disambiguate the two, having no campaign-validation -concept of its own to collide with. `ResolutionDebugInfo` (upstream §3.3) is superseded, not -ported — this platform's `trace`-severity observability channel already serves its purpose, and -the `metadata.transparency` field it would gate on lives outside `SimulationKindState` entirely. -And `wrong_location` — load-bearing in `LocationDefinition`'s own prose since W34 — was missing -from §10's reason-code table, a real gap caught while finishing this unit, not a pre-existing -one reported from elsewhere. -- **Spec:** [10 §3](10-simulation-kind.md#3-the-turn-is-a-week), [§4.2](10-simulation-kind.md#42-action-types), - [§5.1](10-simulation-kind.md#51-resolver-dispatch), [§8.1](10-simulation-kind.md#81-requirements), - [§15](10-simulation-kind.md#15-what-was-ported-and-what-was-found-along-the-way). -- **Depends on:** [W34](#x-w34--simulation-kind-content-definition-types). -- **Status:** Done — [PR #97](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/97). -- **Done when:** `ActionType`/`GameAction` are specified with no client-supplied cost fields; - the resolver dispatch mechanism (`ActionResolver`, `ResolverTable`) is reconciled against - `KindContext` (04 §3.1) rather than upstream's own bespoke `ResolutionContext`; per-action - outcome types reuse `04-core.md`'s `StateChange`/`OutcomeMessage`/`ValidationError`/ - `ValidationWarning` verbatim rather than restating upstream's divergent versions; - end-of-week ordering states plainly that `weekLimit` has no scheduled check anywhere in - it, reinforcing rather than merely repeating §12's existing open callout; every stale - "§7/§9, once ported" forward reference written across W32–W34 (16+ instances) is fixed to - point at a real section; §15 is rewritten from "what remains" to a historical record with - no open rows; no file under `src/engine/` changes; `build/Test-Documentation.ps1` passes. -- **Plan:** [`plans/36-simulation-kind-programme.md`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/blob/main/plans/36-simulation-kind-programme.md) - -### [x] W36 — Simulation Kind: State, Variables, and the Plan -First **build** unit of the simulation-kind programme (`plans/36-simulation-kind-programme.md`, -proposed there as W31, mirroring story-graph's W9). `SimulationKindState` and every nested -runtime-state type (`src/engine/src/kinds/simulation/state.ts`, `actor.ts`) as real TypeScript, -plus `ActionType`/`GameAction`/`WeeklyActionPlan` and pure `plan.add`/`remove`/`clear` reducers -(`plan.ts`) — standalone functions, not wired into `Kind.advance` yet, the same precedent W9 -set for `applyConsequences`. `removeAction` rejects an out-of-range index as a genuine runtime -rejection (`action_not_planned`), not a throw — a stale client index is ordinary play, unlike -W9's undeclared-variable case. -- **Spec:** [10 §2](10-simulation-kind.md#2-kindstate--what-belongs-here), [§4.1](10-simulation-kind.md#41-the-weekly-action-plan), - [§4.2](10-simulation-kind.md#42-action-types), [§6](10-simulation-kind.md#6-player-state). -- **Depends on:** [W35](#x-w35--simulation-kind-resolution-and-systems). -- **Status:** Done — [PR #98](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/98). -- **Done when:** every field `SimulationKindState`/`ActorState` names in the contract has a - real TypeScript type; `addAction`/`clearPlan` never mutate their input plan; - `removeAction` rejects a negative index, an index equal to the plan's length, and a - non-integer index, all with `action_not_planned`; no `Kind.advance`/`kernel/engine.ts` - wiring exists yet; `npm run typecheck && npm run lint && npm test` all pass. -- **Plan:** [`plans/36-simulation-kind-programme.md`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/blob/main/plans/36-simulation-kind-programme.md) - -### [x] W37 — Simulation Kind: The Week -Second build unit of the simulation-kind programme (`plans/36-simulation-kind-programme.md`, -proposed there as W32, mirroring story-graph's W11+W12 combined). `Kind.initialState`, -`Kind.advance` (`plan.add`/`remove`/`clear`/`end_week`), the resolver dispatch table, and -the full 4-step start-of-week + 14-step end-of-week system pipeline, all real and wired -through `createEngine`/`submitAction` for the first time. - -Most end-of-week systems (`employment`, `education`, `housing`, `finance_*`, `opportunities`' -offer/revoke, `events`, `headline`, `goals`, `failure`, `achievements`) are explicit, -individually-documented stubs — each needs a content type (`JobDefinition`, -`CourseDefinition`, …) that doesn't exist until the content-definition-types build unit, a -genuine dependency this unit's own research surfaced rather than one story-graph's W11/W12 -ever had to solve (its "content" — the node graph — already existed by then). `needs` drift -and opportunity expiry are real logic; every stub is documented at its own definition site, -not silently doing nothing. The `ResolverTable` (`resolvers.ts`) uses one shared stub -resolver for all 30 `ActionType`s, built as a real object literal (not `Object.fromEntries` + -a cast) specifically so TypeScript's own exhaustiveness check has teeth — verified directly -by temporarily deleting an entry and confirming the compiler catches it. -- **Spec:** [10 §3](10-simulation-kind.md#3-the-turn-is-a-week), [§5](10-simulation-kind.md#5-resolution-and-statechange), - [§5.1](10-simulation-kind.md#51-resolver-dispatch). -- **Depends on:** [W36](#x-w36--simulation-kind-state-variables-and-the-plan). -- **Status:** Done — [PR #99](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/99). -- **Done when:** `initialState` builds week-one state from a synthetic `SimulationCampaign`; - start-of-week increments the week, resets spent time, and expires effects correctly; - end-of-week runs all fourteen named systems in the documented order (verified via - `kind.simulation.system.ran`'s own emitted order, since most systems are stubs and - can't be distinguished by their state effects alone); needs drift clamps to `0–100` - and emits one `StateChange` per touched need; a real `createEngine`/`submitAction` - round trip runs `plan.add` then `end_week` and lands on the next week with a fresh, - empty plan; `npm run typecheck && npm run lint && npm test` all pass (641 tests, was - 607). -- **Plan:** [`plans/36-simulation-kind-programme.md`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/blob/main/plans/36-simulation-kind-programme.md) - -### [x] W38 — Simulation Kind: Content-Definition Types -Third build unit of the simulation-kind programme — but no longer the single unit -`plans/36-simulation-kind-programme.md` originally proposed as W33. That unit assumed -story-graph's own precedent — *author content against already-coded types* — held here; it -doesn't, since this kind's content-definition types (contract §7) were deferred to the -doc-only contract phase (W34) rather than built as code the way story-graph's were across -W9–W13. `plans/36` now splits W33 into W38–W40; see that document's own callout under *Build* -for the reasoning. This unit is the first: port contract §7 to -`src/engine/src/kinds/simulation/content.ts` — `Reward`, `JobDefinition` (+6 nested types), `CourseDefinition`, -`HousingDefinition`, `ItemDefinition` (+1 nested), `EventDefinition` (+5 nested), -`NPCDefinition`, `GoalDefinition`/`ScenarioDefinition`/`DifficultyDefinition`, -`OpportunityDefinition`/`AchievementDefinition`/`HeadlineDefinition`/`EmployerDefinition`/ -`LocationDefinition`/`BackgroundDefinition`/`TraitDefinition`/`SkillDefinition`, and -`AgentStrategy`. `NPCState`/`AgentState`/`NPCMemory`/`NPCRelationship`/`AvailabilityRule`/ -`Modifier` already exist (`state.ts`, W36) as runtime state — this unit ports only their -content-side counterparts, and everything else §7 names. No system/resolver wiring here — -that's W39's job, against a settled type surface, the same "contract before code" discipline -the four contract units (W32–W35) already used one level up. -- **Spec:** [10 §7](10-simulation-kind.md#7-content-definition-types), [§7.1](10-simulation-kind.md#71-modifiers-and-rewards)–[§7.10](10-simulation-kind.md#710-agents--engine-owned-strategy-definition-and-runtime-state). -- **Depends on:** [W37](#x-w37--simulation-kind-the-week). -- **Status:** Done — [PR #100](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/100). -- **Done when:** every type §7 names that isn't already runtime state in `state.ts`/`actor.ts` - exists in `content.ts`; `AvailabilityRule.condition` — `unknown` since W36, deferred per - its own comment there — is narrowed to the real core `Condition` type, this unit's - natural import point for it; `npm run typecheck && npm run lint && npm test` all pass - with no new runtime logic — this unit is types only. -- **Plan:** [`plans/36-simulation-kind-programme.md`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/blob/main/plans/36-simulation-kind-programme.md) - -### [x] W39 — Simulation Kind: Wiring the "Stable Life" Vertical Slice {#w39} -Fourth build unit — the second half of the W33 split. Wires real logic into exactly the -systems and `RESOLVER_TABLE` entries a goal-driven win/loss loop needs, against W38's -content types — not all twelve stubbed end-of-week systems or all thirty `ActionType`s. -Real now: `goals` and `failure` (evaluate `GoalDefinition.conditions`/`failureConditions` -via a new `conditions.ts`, tracking `GoalState`'s persistent-goal fields per §2.4 — -`consecutiveWeeksSatisfied` resets to zero on any unsatisfied week, `status` becomes -`"completed"` once it reaches `requiredDurationWeeks`, default 1); `eat`/`rest` (the two -resolvers give the player any way to counter `needs` drift at all — without them no -needs-based goal could ever be won); `Kind.outcome` (`outcome.ts`, §12's terminal-identity -shape); and `advance.ts`'s `end_week` now reports `status: "ended"` once `outcome()` -resolves non-null. Everything else (`employment`, `education`, `housing`, `finance_*`, -`inventory`, `relationships`, `opportunities`' offer/revoke, `events`, `headline`, -`achievements`) stays an honest, documented stub — the same discipline W37 established. - -**This is deliberately a smaller loop than the real game's own "Stable Life," not a first -pass at the whole thing.** The real scenario (`games/03-game-design.md` §16.3 in the -companion `SubZeroDev.GameOfLife` repo) needs six completion criteria across employment, -education, housing and finance together — wiring all of that is its own multi-unit depth -effort, not this unit's job. This mirrors story-graph's own W15 Bureaucracy-campaign -precedent: the engine repo proves the mechanism with a synthetic fixture; the real, -full-depth flagship content is a companion-repo concern layered on afterward. - -Two acknowledged, documented gaps carried from `outcome.ts`: `week_limit_reached` is never -returned (`state` alone carries no `weekLimit` to compare against, and §12 itself calls the -precedence question upstream-unresolved), and a mixed multi-goal outcome (some completed, -some failed) resolves conservatively to `"failed"` — verified only against this unit's own -single-goal tests, not a settled rule. - -`SimulationCampaign` (`campaign.ts`) gained exactly two fields for this: `goals: readonly -GoalDefinition[]` and `goalFailurePrecedence` — still not the real authoring surface -(`ScenarioDefinition` integration is W40's job), just what this unit's own wiring needs. -- **Spec:** [10 §5](10-simulation-kind.md#5-resolution-and-statechange), [§3](10-simulation-kind.md#3-the-turn-is-a-week), - [§12](10-simulation-kind.md#12-terminal-identity). -- **Depends on:** [W38](#x-w38--simulation-kind-content-definition-types). -- **Status:** Done — [PR #101](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/101). -- **Done when:** a goal with no `requiredDurationWeeks` completes the first week its - condition is met; a persistent goal requires that many consecutive satisfied weeks, - resetting on any miss; `goalFailurePrecedence` resolves a goal whose completion and - failure conditions trip the same week (`"goals_win"`, the default, completes it - anyway; `"failure_wins"` fails it instead); `eat`/`rest` restore the needs they target, - clamped; `advance`'s `end_week` reports `status: "ended"` once `outcome()` resolves; - every stub is still individually documented; `npm run typecheck && npm run lint && - npm test` all pass (669 tests, was 644). -- **Plan:** [`plans/36-simulation-kind-programme.md`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/blob/main/plans/36-simulation-kind-programme.md) - -### [x] W40 — Simulation Kind: The "Stable Life" Scenario, Validation, and Corpus -Fifth build unit — the third and last piece of the W33 split, mirroring story-graph's -W14+W15+W22 combined (the original mirror `plans/36` gave W33 as a whole), now that W38/W39 -give it real types and real logic to author and validate against. Authors the "Stable Life" -fixture `SimulationCampaign` (`campaigns/stable-life.ts` — one goal, "Well Rested": maintain -`player.needs.energy` at or above 70 for two consecutive weeks, failing outright below 40), -Tier 1/2 `validateCampaign` for the simulation kind (`kinds/simulation/validate.ts`), the -real `Kind` assembly (`kinds/simulation/kind.ts`, mirroring -`kinds/story-graph/kind.ts`'s own role), and commits replay-corpus fixtures -(`fixtures/replay/stable-life-{win,loss}.*.json`) for both paths — folded together per -`plans/36`'s own original reasoning: the scenario *is* the test subject. Reaches this -programme's **S3**/**S4** milestones — with one criterion honestly short, below. - -**Validation is scoped to what `SimulationCampaign` actually carries** (`goals`, -`goalFailurePrecedence`) — not §14's full list across every content-definition type, most -of which this campaign shape has no field for yet. `kinds/simulation/validate.ts`'s own -header names exactly what's checked (goal id uniqueness, `LocKey` resolution) and defers -the rest to whichever future unit adds the collection each check needs. - -**Two committed fixtures, both captured by running the real engine once, not hand-typed:** -`stable-life-win` (three weeks of `rest`, completing the goal — two consecutive satisfied -weeks, not one, is the actual proof of `endOfWeek.ts`'s persistence tracking) and -`stable-life-loss` (four weeks of nothing, tripping `failureConditions`). Fixing these -fixtures also surfaced a real, previously-latent bug: `bulgaria-bureaucracy.replay.test.ts` -(W22) enumerated *every* `*.fixture.json` in the shared `fixtures/replay/` directory, -having never anticipated a second kind landing fixtures beside its own — it then tried to -replay `stable-life-*` through the story-graph-only registry and failed with -`campaign_withdrawn`. Fixed by prefix-filtering both suites (`bureaucracy-`/`stable-life-`) -and wiring `stable-life.replay.test.ts` into `ci.yml`'s release-tag-replay job alongside -bureaucracy's, with the same `skipIf`-style guard for a baseline tag that predates this -corpus entirely. - -**Honest scope gap: win/loss are not reachable through the text client or MCP.** The -original "Done when" below asked for that; `scene`/`availableActions`/`project` -(`kinds/simulation/kind.ts`) are placeholders, because §9 (Projection) is still prose-only -in the contract — no `SimulationView`/`PublicWorldState` shape exists to implement against. -This unit's actual consumer, the replay oracle, never calls those three methods (only -`createGame`/`submitAction`), so it doesn't need them — but a text-client/MCP playthrough -does, and that's a real, separate future unit (story-graph's own equivalent, W16/W17, came -*after* its turn loop already worked, not bundled into W14/W15). Recorded here rather than -quietly dropped from the criteria. -- **Spec:** [10 §14](10-simulation-kind.md#14-validation), [07 §4](07-replay.md#4-the-corpus). -- **Depends on:** [W39](#w39). -- **Status:** Done — [PR #102](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/102). -- **Done when:** Tier 1/2 validation rejects a duplicate goal id and an unresolved `LocKey` - for this kind's own content; one win fixture and one loss fixture are committed under - `fixtures/replay/` and pass the replay oracle, including the release-tag-replay CI job; - `npm run typecheck && npm run lint && npm test` all pass (677 tests, was 669). Win/loss - through the text client and MCP is **not** met — see the gap noted above. -- **Plan:** [`plans/36-simulation-kind-programme.md`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/blob/main/plans/36-simulation-kind-programme.md) - -### Depth: Life in the Fast Lane Becomes Playable - -Eight units taking the `simulation` kind from *proven through the replay oracle* to *played by -a person*. W36–W40 built the state, the week pipeline, the content types and a minimal -goal-driven loop; everything below is contract that exists and code that does not. - -**Ordered so the riskiest assumption goes first.** The platform's central claim is that a -client is a projection of the session store (09 §1) — and one of three kinds currently cannot -be projected at all, which W40 recorded honestly rather than dropping. W50 tests that claim. -W51 comes second because the derived-value layer is what every unit after it reads through, -and its rounding rule is the most replay-sensitive line in the kind. - -### [x] W50 — Simulation Kind: Projection and Client Parity {#w50} - -**Delivers:** Makes Life in the Fast Lane something a person can actually play. Today the -simulation kind runs only inside the replay harness — a game starts, a week is planned and -ended, and nothing can show you the result, because `scene`, `availableActions` and `project` -are placeholders returning empty values. This unit fixes what a client is allowed to see and -builds it, so the "Stable Life" scenario can be won and lost through the text client and -through MCP. - -W40 named this gap in its own done-criteria rather than quietly meeting a weaker bar. -[10 §9](10-simulation-kind.md#9-projection) is prose-only: it states the rule — `SimulationView` -carries **only what the generic surface does not**, the rule -[03 §9](03-story-graph-kind.md#9-projection--what-a-client-sees) already follows — but declares -no shape. `PublicWorldState`, which -[§7.10](10-simulation-kind.md#710-agents--engine-owned-strategy-definition-and-runtime-state)'s -`AgentStrategy.selectActions` takes as its first parameter, is referenced in the contract and -declared nowhere at all. - -**This unit amends §9 in the same change that implements it**, the precedent W48 set when it -amended [09 §4](09-clients.md#4-the-api-coverage-checklist) and -`MVP.md` §5 alongside `previewAction`. That is deliberate and narrow: §9's *rule* is settled, -and what is missing is the field list it implies — not a design decision. If drafting the shape -turns out to require one, that is `/contract`'s call, not this unit's. - -**The envelope-duplication ledger is the live risk, and it has bitten the view side before** — -entry 3, `StoryGraphView` duplicating scene and status fields. `SimulationView` has ten -`SimulationKindState` fields to choose from, next to a `GameState` envelope and a generic -`Scene`/`PlayerView` that already carry identity, status and turn. -- **Spec:** [10 §9](10-simulation-kind.md#9-projection), - [§2](10-simulation-kind.md#2-kindstate--what-belongs-here), - [§4](10-simulation-kind.md#4-actions--one-model-richer-verbs), - [§10](10-simulation-kind.md#10-reason-codes), [§11](10-simulation-kind.md#11-events); - [03 §9](03-story-graph-kind.md#9-projection--what-a-client-sees) (the rule to follow); - [04 §6](04-core.md#6-scenes-and-actions-generic), [§9](04-core.md#9-projection); - [09 §4](09-clients.md#4-the-api-coverage-checklist), - [§6](09-clients.md#6-projection-is-not-optional). -- **Touches:** `design/20-contract.md` (the `engine/10-simulation-kind.md` block, §9 and §11); - `src/engine/src/kinds/simulation/` — new `view.ts`, `scene.ts`, `available.ts`, and - `kind.ts`'s three placeholders; `src/engine/src/clients/text/render.ts`; - `src/engine/src/mcp/server.ts`; `design/10-design.md` (09 §4's checklist). -- **Depends on:** [W40](#x-w40--simulation-kind-the-stable-life-scenario-validation-and-corpus). -- **Status:** Not started. -- **Done when:** - - W50.1 Contract §9 declares `SimulationView` and `PublicWorldState` as complete TypeScript - interfaces with every field named, and `AgentStrategy.selectActions` (§7.10) resolves - against the declared `PublicWorldState` rather than an undeclared name. - - W50.2 No `SimulationView` field repeats one the `GameState` envelope, the generic `Scene`, - or `PlayerView` already carries — asserted by a test that names the envelope fields and - checks each is absent, not by review. - - W50.3 `project` never emits `seed`, `actionLog`, raw `kindState`, `AgentState.strategy`, - `RelationshipState.resentment`, or an unrevealed `Opportunity`, for either audience; - each is asserted by name, and the `ai` audience is not wider than `player`. - - W50.4 `availableActions` returns `plan.add`, `plan.remove`, `plan.clear` and `end_week` - with the params §4 declares for each; where the campaign forbids an empty plan, - `end_week` renders disabled with `plan_empty` rather than being hidden. - - W50.5 `scene` renders from registry strings only; a `LocKey` it references but the registry - does not resolve fails registry construction rather than rendering a raw key at play. - - W50.6 Playing `stable-life` to its committed win through the text client and through MCP, - under the same seed and the same counting `IdSource`, produces **byte-identical** - `serialize()` output — 09 §1's proof, now exercised for the first kind whose actions - carry declared `params`. - - W50.7 [09 §4](09-clients.md#4-the-api-coverage-checklist)'s coverage checklist has a - simulation column complete for all ten operations, each ticked against a named passing - test rather than an assertion of intent. - - W50.8 All eight events [§11](10-simulation-kind.md#11-events) declares are emitted at the - points that section names and listed in `Kind.eventNames`; a golden event stream for a - full `stable-life` win covers their order, and a name outside `kind.simulation.*` fails. -- **Out of scope:** wiring any additional resolver or end-of-week system — the projection shows - what the kind already computes, and W51–W57 add behaviour behind it. Also out of scope: - a rival agent actually running. `PublicWorldState` is declared here because - `AgentStrategy` cannot typecheck without it, not because this unit builds agents — how a - scenario configures rivals is an open gap §7.10 states outright. -- **Status:** Done — [PR #166](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/166). - -### [x] W51 — Simulation Kind: Derived Values, Modifiers, and Effects {#w51} - -**Delivers:** Makes a status effect or a trait actually change what the player can do — a -"reduced hours" effect really lowers the time available that week, a bonus really shifts a -cost — instead of every number in the game being the raw stored one. - -[§6.1](10-simulation-kind.md#61-base-and-derived-values)'s base/derived layer is the substrate -every resolver and every end-of-week system in W52–W57 reads through, and none of it exists: -`DerivedPath`, `DerivedValueResolver`, application order, stacking and expiry are absent from -`src/engine/src/kinds/simulation/` entirely. -[§7.1](10-simulation-kind.md#71-modifiers-and-rewards)'s `Modifier`/`Reward` application is -likewise unbuilt — including the `multiply` rounding rule W34 checked against the primary -source and corrected two plan documents over (basis-points `value`, round half-away-from-zero -**once** after the full chain), which is the single most replay-sensitive line in this kind. - -A substrate on its own is not a vertical slice, so this one is paired with the place it is -already observable without any new content: start-of-week `time_commit` -([§3](10-simulation-kind.md#3-the-turn-is-a-week)), whose two-phase split exists precisely so -an expiring effect changes committed time correctly. That makes the unit provable end to end -rather than a layer added on faith. -- **Spec:** [10 §6.1](10-simulation-kind.md#61-base-and-derived-values), - [§7.1](10-simulation-kind.md#71-modifiers-and-rewards), - [§2.3](10-simulation-kind.md#23-effects-opportunities-and-scheduled-events), - [§3](10-simulation-kind.md#3-the-turn-is-a-week), - [§13](10-simulation-kind.md#13-determinism). -- **Touches:** `src/engine/src/kinds/simulation/` — new `derived.ts` and `modifiers.ts`, plus - `startOfWeek.ts`, `endOfWeek.ts`, `state.ts`; `src/engine/fixtures/replay/`. -- **Depends on:** [W50](#w50). -- **Status:** Not started. -- **Done when:** - - W51.1 Every `DerivedPath` §6.1 names resolves through `DerivedValueResolver`; a path that - is not a declared derived path fails at load with its path, not at read. - - W51.2 Modifier application follows §6.1's stated order and stacking rule: two modifiers - touching the same path produce the identical result in either registration order, - proven by a test that applies them both ways round. - - W51.3 `Modifier.operation: "multiply"` against integer `Cents` rounds half-away-from-zero - exactly once after the full chain — a test shows a three-multiply chain differing from - three separately-rounded multiplies, so the rule is proven rather than assumed. - - W51.4 An `activeEffect` reducing committed time changes the budget in `time_commit` and - not in `time_advance`; a fixture where that effect expires the same week shows the - un-reduced budget, proving the two phases have not been collapsed. - - W51.5 A derived value is visible through `SimulationView` (W50) and never persisted — - `serialize()` output contains base values only, asserted over the canonical string. - - W51.6 A replay fixture covering an effect applying and expiring is committed and passes - the oracle; the determinism harness's sink-independence and byte-identity checks pass. -- **Out of scope:** the content that grants effects. Jobs, courses and items arrive with their - own units; this one proves the mechanism against a hand-authored effect on the existing - "Stable Life" fixture, the same way W39 proved goals against a single synthetic goal. -- **Status:** Done — [PR #204](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/204), - superseding [PR #194](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/194). - -### [x] W52 — Simulation Kind: The Scenario Campaign and Full Validation {#w52} - -**Delivers:** Replaces the hand-assembled test fixture with a real authored scenario — a -starting background, home, job, location, inventory and week cap that a content author writes -— and makes the loader reject a broken one with a path to the mistake instead of failing at -play. - -`SimulationCampaign` carries five starting-state blobs plus `goals` and -`goalFailurePrecedence`, and its own header says it is "deliberately minimal, not the real -authoring surface." Every [§7](10-simulation-kind.md#7-content-definition-types) content -collection exists as a type and has nowhere to live. There is no source/runtime split for this -kind at all — `story-graph` and `world-graph` each have one — so -[04 §10.1](04-core.md#101-content-registry)'s authoring boundary is asserted for two kinds and -proven for two of three. And [§14](10-simulation-kind.md#14-validation)'s Tier 1 list names -seventeen content types whose ids must be unique and whose cross-references must resolve; -what is implemented is goal-id uniqueness and one `LocKey`. -- **Spec:** [10 §7.8](10-simulation-kind.md#78-goals-scenarios-and-difficulty), - [§7.9](10-simulation-kind.md#79-supporting-definitions), - [§14](10-simulation-kind.md#14-validation); - [04 §10.1](04-core.md#101-content-registry), [§11](04-core.md#11-tiered-validation), - [§17](04-core.md#17-identifier-conventions). -- **Touches:** `src/engine/src/kinds/simulation/` — `campaign.ts`, `validate.ts`, `initial.ts`, - and a new `source.ts`; `src/engine/src/campaigns/stable-life.ts` and new broken fixtures - beside it. -- **Depends on:** [W50](#w50). -- **Status:** Not started. -- **Done when:** - - W52.1 `SimulationCampaignSource` → `SimulationCampaign` is a pure builder performing no - file or network I/O, and every §7 collection §14 validates has a field on the runtime - root. - - W52.2 `initialState` builds week one from a `ScenarioDefinition` — starting backgrounds, - housing, location, inventory and week cap — rather than from five literal state blobs. - - W52.3 Each of §14's Tier 1 checks fails with a path: a duplicate id within every one of - the seventeen content types independently, and each named cross-reference - (`PromotionPath.toJobId`, `ScenarioDefinition.startingBackgroundIds`, - `startingHousingId`, `startingLocationId`, `goalIds`, - `startingInventory[].definitionId`) pointing at an id that does not exist. - - W52.4 Each of §14's Tier 2 checks warns and still loads, reported rather than swallowed. - - W52.5 A deliberately broken campaign fixture exists per Tier 1 family — the same - instrument W15 built for `story-graph` — and each produces its expected tier and path. - - W52.6 The re-authored "Stable Life" campaign reaches its committed win and loss fixtures - byte-identically; if a fixture legitimately changes, it is regenerated by the - deliberate per-fixture command ([07 §7](07-replay.md#7-intended-change-versus-regression)) - and named in the PR — never an automatic sweep. -- **Out of scope:** making any newly authorable content *do* anything. A `JobDefinition` this - unit lets an author write and the validator accept is still resolved by `stubResolver` - until W53. This is the authoring and loading surface, not behaviour. -- **Status:** Done — [PR #227](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/227). - -### [x] W53 — Simulation Kind: Employment and Income {#w53} - -**Delivers:** The player can look for work, apply for a job, negotiate its terms and then -actually work it — and be paid for it at the end of the week. -- **Spec:** [10 §7.2](10-simulation-kind.md#72-jobs), - [§7.9](10-simulation-kind.md#79-supporting-definitions) (employers), - [§6.8](10-simulation-kind.md#68-career), [§3](10-simulation-kind.md#3-the-turn-is-a-week) - (`employment`, `finance_income`), [§5.1](10-simulation-kind.md#51-resolver-dispatch)–[§5.3](10-simulation-kind.md#53-per-action-outcome), - [§10](10-simulation-kind.md#10-reason-codes). -- **Touches:** `src/engine/src/kinds/simulation/` — `resolvers.ts`, `endOfWeek.ts`, - `reasons.ts`; `src/engine/src/campaigns/stable-life.ts`; - `src/engine/fixtures/replay/`. -- **Depends on:** [W51](#w51), [W52](#w52). -- **Status:** Not started. -- **Done when:** - - W53.1 `work`, `work_overtime`, `search_for_work`, `apply_for_job` and - `negotiate_job_terms` each have a real `ActionResolver` — `canExecute`, `calculate`, - `apply` — and none of the five is `stubResolver`. - - W53.2 Time and money cost are computed by `calculate` from state and content; a plan - exceeding the week's time units is rejected with `insufficient_time`, leaving state - byte-identical and the action log unadvanced. - - W53.3 `apply_for_job` against a job whose requirements are unmet is rejected with - `requirement_unmet`; an employment action whose type is not in the current location's - `actionTypes` is rejected with `wrong_location`. - - W53.4 The `employment` system advances `Employment.performance` and any `PromotionPath` it - satisfies; `finance_income` pays wages **before** `housing` runs, proven by a fixture - whose rent is payable only out of that same week's wages. - - W53.5 Every random draw comes from `ctx.rng`; adding a draw to one resolver does not shift - another's results, proven by a substream test rather than asserted from §13. - - W53.6 A replay fixture covering a full search → apply → hire → first paycheque arc is - committed and passes the oracle. -- **Out of scope:** education, housing costs, debt and possessions — W54 to W56. A job's - training requirement may name a `CourseDefinition` id the validator resolves, but taking - the course is W54's work. -- **Status:** Done — [PR #228](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/228). - -### [x] W54 — Simulation Kind: Education and Skills {#w54} - -**Delivers:** The player can enrol on a course, attend it, study for it, and come out with a -skill that changes which jobs they can hold — or withdraw and lose the fees. -- **Spec:** [10 §7.3](10-simulation-kind.md#73-courses), - [§7.9](10-simulation-kind.md#79-supporting-definitions) (skills), - [§6.7](10-simulation-kind.md#67-education), - [§3](10-simulation-kind.md#3-the-turn-is-a-week) (`education`), - [§8.1](10-simulation-kind.md#81-requirements). -- **Touches:** `src/engine/src/kinds/simulation/` — `resolvers.ts`, `endOfWeek.ts`, - `startOfWeek.ts`; `src/engine/src/campaigns/stable-life.ts`; - `src/engine/fixtures/replay/`. -- **Depends on:** [W53](#w53). -- **Status:** Not started. -- **Done when:** - - W54.1 `enroll_course`, `attend_class`, `study` and `withdraw_course` each have a real - resolver, and none is `stubResolver`. - - W54.2 Enrolling adds a course commitment that start-of-week `time_commit` includes in - committed time, and withdrawing removes it the same week — exercised by a fixture in - which one course ends the week another begins. - - W54.3 The `education` system advances progress only for courses attended that week, and - awards the course's skill exactly once on completion however many weeks follow. - - W54.4 A skill awarded here satisfies a `JobDefinition` requirement W53 built, proven by a - fixture where `apply_for_job` is rejected before the course and accepted after it. - - W54.5 Enrolling without the fee is rejected with `insufficient_funds`; enrolling twice on - the same course is rejected and leaves the existing enrolment untouched. - - W54.6 A replay fixture covering enrol → attend → complete → qualify is committed and - passes the oracle. -- **Out of scope:** careers beyond what W53 built, and rival actors studying — agents are - unbuilt for the reason W50's *Out of scope* records. -- **Status:** Done — [PR #230](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/230). - -### [x] W55 — Simulation Kind: Housing, Debt, and Reconciliation {#w55} - -**Delivers:** Rent, bills, borrowing and eviction — the half of the week that happens whether -the player plans for it or not. - -This is where the contract's most carefully argued ordering rule lands. -[§3](10-simulation-kind.md#3-the-turn-is-a-week) splits finance into `finance_income` before -`housing` and `finance_reconcile` after it, and states exactly what each single-pass -alternative breaks: rent charged before wages arrive produces false overdrafts for a solvent -player, while reconciling before housing lags eviction escalation by a full week. W53 built -the first of the three; this unit builds the other two, and is the first that can prove the -split was necessary rather than merely argued. -- **Spec:** [10 §7.4](10-simulation-kind.md#74-housing), - [§6.4](10-simulation-kind.md#64-finances), [§6.9](10-simulation-kind.md#69-housing), - [§3](10-simulation-kind.md#3-the-turn-is-a-week) (`housing`, `finance_reconcile`), - [§10](10-simulation-kind.md#10-reason-codes). -- **Touches:** `src/engine/src/kinds/simulation/` — `resolvers.ts`, `endOfWeek.ts`; - `src/engine/src/campaigns/stable-life.ts`; `src/engine/fixtures/replay/`. -- **Depends on:** [W53](#w53). -- **Status:** Not started. -- **Done when:** - - W55.1 `move_housing`, `pay_bills`, `borrow_money`, `repay_debt`, `deposit_savings` and - `invest` each have a real resolver, and none is `stubResolver`. - - W55.2 `housing` levies `HousingDefinition.weeklyCostCents` after `finance_income` has paid - wages: a fixture whose only income arrives that week pays rent successfully, and the - same inputs with the two systems' order swapped overdraw — the ordering is proven by - outcome, not by reading the list. - - W55.3 `finance_reconcile` applies late fees and advances eviction only against balances - `housing` charged this week; a first missed rent advances eviction by exactly one step. - - W55.4 Every money value is integer `Cents`; no floating-point money value appears in - `serialize()` output, checked over the canonical string rather than by inspection. - - W55.5 `move_housing` to a home the player cannot afford is rejected with - `insufficient_funds` and leaves the current housing untouched. - - W55.6 Two replay fixtures are committed and pass: one reaching eviction, one avoiding it - by a single week's wages. -- **Out of scope:** items and their upkeep (W56), and any economy-wide drift in prices or - wages — `EconomyState` moves only where a system this unit builds moves it. -- **Status:** Done — [PR #232](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/232). - -### [x] W56 — Simulation Kind: Possessions, Places, and People {#w56} - -**Delivers:** Shopping, keeping what you own working, getting around town, and having a social -life — the actions that make a week feel like a life rather than a spreadsheet. -- **Spec:** [10 §7.5](10-simulation-kind.md#75-items), - [§7.7](10-simulation-kind.md#77-npcs--definition-and-runtime-state), - [§7.9](10-simulation-kind.md#79-supporting-definitions) (locations), - [§6.5](10-simulation-kind.md#65-needs), [§6.10](10-simulation-kind.md#610-inventory), - [§6.11](10-simulation-kind.md#611-relationships), - [§3](10-simulation-kind.md#3-the-turn-is-a-week) (`inventory`), - [§10](10-simulation-kind.md#10-reason-codes) (`wrong_location`). -- **Touches:** `src/engine/src/kinds/simulation/` — `resolvers.ts`, `endOfWeek.ts`; - `src/engine/src/campaigns/stable-life.ts`; `src/engine/fixtures/replay/`. -- **Depends on:** [W55](#w55). -- **Status:** Done — [PR #236](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/236). -- **Done when:** - - W56.1 `shop`, `maintain_item`, `repair_item`, `sell_item`, `travel`, `socialize` and - `exercise` each have a real resolver, and none is `stubResolver`. - - W56.2 Both halves of `wrong_location` are covered: `travel` to a location absent from the - current location's `connections` is rejected with it, and so is any action whose type - is absent from the current location's `actionTypes`. - - W56.3 The `inventory` system applies per-item condition decay, and an item at zero - condition stops contributing its modifiers rather than being removed from inventory. - - W56.4 `socialize` moves the named `RelationshipState` and is rejected when that NPC is not - present at the current location. The `relationships` end-of-week system stays an - explicit, documented stub — no weekly relationship rule exists in the contract to - implement (see *Known Open Items Carried In*). - - W56.5 `exercise` moves the needs §6.5 names, clamped to `0–100` the same way W39's - `eat`/`rest` are, emitting one `StateChange` per touched need. - - W56.6 A replay fixture covering buy → use → decay → repair → sell is committed and passes. -- **Out of scope:** events that fire *because* of a relationship or a possession — that is - W57's `events` system. Also out of scope: writing the missing weekly-relationship rule, - which is `/contract`'s, not a slice's. - -### [x] W57 — Simulation Kind: Events, Opportunities, Headlines, and Achievements {#w57} - -**Delivers:** The world starts acting on the player — random events arrive and demand a -response, opportunities appear and expire, the weekly headline reflects what actually -happened, and achievements unlock and persist across sessions. - -Closes the last of [§3](10-simulation-kind.md#3-the-turn-is-a-week)'s stubbed systems and -[§2.3](10-simulation-kind.md#23-effects-opportunities-and-scheduled-events)'s two lifecycles. -Ordering is load-bearing again and stated: `headline` runs after `events` so a week's headline -can reference the strangeness that week's own events moved; `achievements` runs second-to-last -so a condition can depend on a counter `goals`/`failure` just incremented; and §2.3's revoke -and expire both run before offer. - -**This unit is blocked on one contract decision and must not start without it.** -[§12](10-simulation-kind.md#12-terminal-identity)'s callout states that whether a week which -simultaneously exhausts `weekLimit` *and* resolves every goal reports `week_limit_reached` or -`goals_met` is genuinely unresolved — not undocumented here, but unresolved in the upstream -source — and §3 confirms `END_WEEK_SYSTEM_ORDER` has no slot for the check at all. -`week_limit_reached` is one of `outcome()`'s three non-null values and is currently -unreachable. Route that to `/contract` first; do not guess an ordering, and do not invent a -system position upstream never named. -- **Spec:** [10 §7.6](10-simulation-kind.md#76-events), - [§7.9](10-simulation-kind.md#79-supporting-definitions) (opportunities, achievements, - headlines), [§2.3](10-simulation-kind.md#23-effects-opportunities-and-scheduled-events), - [§3](10-simulation-kind.md#3-the-turn-is-a-week), - [§11](10-simulation-kind.md#11-events), [§12](10-simulation-kind.md#12-terminal-identity); - [04 §7.1](04-core.md#71-the-profile-store). -- **Touches:** `src/engine/src/kinds/simulation/` — `resolvers.ts`, `endOfWeek.ts`, - `startOfWeek.ts`, `outcome.ts`; `src/engine/src/campaigns/stable-life.ts`; - `src/engine/fixtures/replay/`. -- **Depends on:** [W56](#w56), **and** a contract decision on §12's `week_limit_reached` - precedence callout. -- **Status:** Done — [PR #265](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/265). -- **Done when:** - - W57.1 `respond_to_event`, `accept_opportunity` and `decline_opportunity` each have a real - resolver, and `"custom"` still reaches resolution nowhere — a `GameAction` typed - `"custom"` fails, with no route around the `ResolverTable`. - - W57.2 The `events` system fires scheduled and seeded random events in the order §2.3 - states, and a deferred `PendingEventResponse` is presented by the *next* week's - start-of-week `events` phase, never the same week it was deferred in. - - W57.3 The `opportunities` system runs revoke and expire before offer (§2.3); an - opportunity offered and expired within one week is never visible in `SimulationView`. - - W57.4 `headline` reads world strangeness after `events` has moved it, proven by a fixture - whose headline changes only because an event fired that week. - - W57.5 `achievements` evaluates `AchievementDefinition.condition` after `goals`/`failure`, - unlocks exactly once across repeated weeks, and upserts through `ProfileStore` so the - unlock survives a new session with the same `profileId`. - - W57.6 `week_limit_reached` is reachable and returned by `outcome()` under the precedence - the contract decision fixed, with its own committed replay fixture — the third terminal - path, previously unreachable by construction. - - W57.7 No end-of-week system in §3's list remains a stub except `relationships` and - `history`, each still documented at its own definition site with its reason. -- **Out of scope:** rival agents. - [§7.10](10-simulation-kind.md#710-agents--engine-owned-strategy-definition-and-runtime-state) - states plainly that how a scenario configures rivals is an open gap — no - `ScenarioDefinition` field names them, and upstream declares none either — so agents stay - unbuilt until that is decided. - -### Breadth: Content Packs, Experiments, and Locales - -Three units against contracts that are fully specified and entirely unbuilt. -[`11-content-packs.md`](11-content-packs.md) is the customization story -[`02-architecture.md`](02-architecture.md) §4a promised and 04 §10.1 cannot express; the locale -unit is the cheapest available test of a claim 04 §10.1 makes and nothing currently checks. - -### [x] W58 — Content Pack Resolution and Content Identity {#w58} - -**Delivers:** Lets a game be assembled from several content packs — a base campaign plus, say, -a culture pack that restyles its text — and makes a save record exactly which mix it was -played against, so replaying one player's game never silently runs it against another's -content. - -The fold is the easy half. **The load-bearing part is -[§6](11-content-packs.md#6-identity-and-why-determinism-needs-it)'s identity argument:** two -players on the same `campaignVersion` with different packs resolved are playing different -games, and the envelope had no way to say so. The resolution is that `resolvePacks` stamps -every campaign's `version` with a `ResolutionId` digest over the ordered `{id, version}` list — -which adds no envelope field and makes [07 §6](07-replay.md#6-the-runner-and-its-verdicts)'s -`unrunnable: campaign_version_missing` reachable for the reason it was written. - -`ContentRegistry` gains exactly one field. That is the whole change to 04 §10.1, and a second -one is a signal this unit has grown a design decision it should route rather than absorb. -- **Spec:** [11 §2](11-content-packs.md#2-what-a-pack-is), - [§3](11-content-packs.md#3-resolution), - [§4](11-content-packs.md#4-the-one-change-to-contentregistry), - [§5](11-content-packs.md#5-dependencies), - [§6](11-content-packs.md#6-identity-and-why-determinism-needs-it), - [§7](11-content-packs.md#7-validation); [04 §10.1](04-core.md#101-content-registry), - [§11](04-core.md#11-tiered-validation); [07 §6](07-replay.md#6-the-runner-and-its-verdicts). -- **Touches:** `src/engine/src/core/registry/` — `types.ts`, `build.ts`, and a new `packs.ts`; - `src/engine/src/core/validation/tiered.ts`. -- **Depends on:** nothing engine-side. -- **Status:** Done — [PR #238](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/238). -- **Done when:** - - W58.1 `resolvePacks` is pure and total — no file or network I/O, and it returns either a - registry or a complete list of conflicts, never a partial registry. - - W58.2 A later pack replaces a campaign wholesale by `campaign.id` and never field-merges - it; a later pack replaces a string per key. A two-pack campaign collision yields - exactly one of the two campaigns, asserted directly rather than by absence of a merge. - - W58.3 `dependsOn` is topologically sorted before the fold; a cycle fails, and two packs - requiring different versions of a third fails rather than picking one. - - W58.4 The same ordered `{id, version}` list produces the same `ResolutionId` across - processes, and the same list in a different order produces a different one. - - W58.5 Every campaign `resolvePacks` produces carries the `ResolutionId` as its - `Campaign.version`, and `GameState` gains no field. - - W58.6 Loading a replay fixture whose `campaignVersion` no longer resolves returns - `unrunnable` with `campaign_version_missing` — not `diverged`. - - W58.7 Each of §7's three checks fires: Tier 1 for a `kindId` mismatch, an unresolvable - `dependsOn` and a cycle; Tier 1 for a campaign id colliding *within* one pack; Tier 2 - for a pack overriding a campaign or string no earlier pack supplied. - - W58.8 Every existing single-campaign registry test passes unmodified, and the campaigns - W15–W40 committed still serialize byte-identically. -- **Out of scope:** experiment gates (W59); pack discovery and distribution, partial or lazy - loading, community trust, and per-locale pack splitting — - [§8](11-content-packs.md#8-what-is-deferred) defers all four by name. - -### [x] W59 — Experiment Gates and the `ExperimentSource` Port {#w59} - -**Delivers:** Turns A/B tests and feature flags into one mechanism rather than two — a pack is -simply in the set or not, decided before resolution — and gives a host the seam that decides -which variant a player is in. - -[§5a](11-content-packs.md#5a-experiment-gates)'s design is deliberately small: -`applyExperimentGates` filters the pack array *before* `resolvePacks` sees it, so nothing about -the fold's signature or purity changes and it never learns gates exist. `ExperimentSource` is -the last unbuilt port in [06 §5](06-extensibility.md#5-the-port-catalogue)'s catalogue — W1's -`src/engine/src/core/composition/types.ts` predates that design and declares `IdSource`, -`Clock`, `EngineHost` and a `SessionHost` with no `experiments` field. - -The safety property is worth restating because it is easy to lose in implementation: a gated -pack is included only when `assignments[gate.experimentId] === gate.variant`, which is never -true for `null` ("not enrolled") or a missing key — so "no `ExperimentSource` supplied" is safe -by construction rather than by luck of which default string someone picked. -- **Spec:** [11 §5a](11-content-packs.md#5a-experiment-gates); - [06 §4](06-extensibility.md#4-the-composition-root), - [§5.5](06-extensibility.md#experimentsource). -- **Touches:** `src/engine/src/core/composition/types.ts`; - `src/engine/src/core/registry/packs.ts`; `src/engine/src/core/session/store.ts`. -- **Depends on:** [W58](#w58). -- **Status:** Done — [PR #239](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/239). -- **Done when:** - - W59.1 `ExperimentSource` is declared in `composition/types.ts` and `SessionHost` gains an - optional `experiments`, with a working default meaning "no experiments running". - - W59.2 An ungated pack is always included; a gated pack is included only on an exact - variant match and excluded for `null`, for a missing key, and for a different variant - — each asserted as its own case. - - W59.3 With no `ExperimentSource` supplied, every gated pack is excluded and every ungated - pack included; no gated pack reaches a registry by default. - - W59.4 `resolvePacks`' signature, purity and W58 tests are unchanged, and a test asserts it - never receives a gated-out pack. - - W59.5 Assignments resolve once per session — one call per distinct `experimentId` across - the candidate packs, keyed by `profileId` where present and `seed` otherwise — proven - by a counting `ExperimentSource` rather than asserted. - - W59.6 Two sessions in different variants produce different `campaignVersion`s through - W58's existing digest, with no further mechanism added. -- **Out of scope:** bucketing algorithms, rollout percentages, sticky-session semantics beyond - `bucketKey`, and outcome measurement — §5a and §8 place all four outside this contract. - -### [x] W60 — A Second Locale, End to End {#w60} - -**Delivers:** Proves the platform is not accidentally English-only — the same campaign plays -through in a second language with the engine unchanged, and a missing translation is caught at -load rather than shown to a player as a raw key. - -[04 §10.1](04-core.md#101-content-registry) states the authoring→registry types already support -more than one locale and that the MVP ships English only, so this is string tables plus tooling -with no type change. **No test currently makes that claim**, and it is a small vertical slice -that would find out cheaply: the protected `core.reason.*` merge, `LocKey` resolution, the text -client's rendering and the MCP surface all sit on the path. -- **Spec:** [04 §10.1](04-core.md#101-content-registry), - [§12](04-core.md#12-reason-codes-state-changes-messages), - [§17](04-core.md#17-identifier-conventions); - [09 §5](09-clients.md#5-reason-codes-and-messages). -- **Touches:** `src/engine/src/core/localization/resolve.ts`; - `src/engine/src/core/registry/build.ts` and `strings.ts`; - `src/engine/src/campaigns/`; `src/engine/src/clients/text/render.ts`. -- **Depends on:** nothing engine-side. -- **Status:** Done — [PR #240](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/240). -- **Done when:** - - W60.1 One shipped campaign has a complete second-locale string table and the registry - builds for both locales with no change to any type in `core/registry/types.ts`. - - W60.2 A key present in English and absent in the second locale fails Tier 1 with the key's - path — never a silent fallback to English, never a raw key rendered at play. - - W60.3 `core.reason.*` messages resolve in the second locale, and a campaign attempting to - override one is still rejected. - - W60.4 The same seed and the same choices under either locale produce byte-identical - `serialize()` output — locale is presentation and reaches no persisted state. - - W60.5 The text client and the MCP surface both render the second locale from the registry, - and neither contains a translated string in its own source. -- **Out of scope:** string-table extraction, translation workflow and coverage reporting — - those belong to the *Content Tooling* workstream below. Per-locale content packs are - [11 §8](11-content-packs.md#8-what-is-deferred)'s deferral, not this unit's. - -### Breadth: The First Culture Pack - -Both preconditions have landed. [W58](#w58) built the pack fold and the `ResolutionId`; -[W50](#w50)–[W57](#w57) made the simulation kind playable, so there is now a game worth -reskinning. Sliced as **W71–W72**. - -**This is the riskiest unproven claim left in the design**, which is why it leads. -[`02-architecture.md`](02-architecture.md) §4a calls culture packs "the volume play: one kind, -many settings," and [§11](02-architecture.md#11-what-is-settled-what-is-next) says the Bulgaria -pack "needs no new deliverable — it is a content pack over the existing simulation kind." -Neither statement has -ever been exercised: W58 proved the fold against synthetic packs, and no real culture pack -exists. W71 is the first one, and its job is to find out whether "no engine change" is true. - -### [x] W71 — The Bulgaria Culture Pack: Mechanism and Voice {#w71} - -**Delivers:** Proves the platform's central customization claim — that one game's mechanics can -host a completely different world without touching the engine. Life in the Fast Lane's "Stable -Life" is assembled from two content packs instead of one: the base game, plus a Bulgarian -culture pack that swaps the narrator's voice and the world it describes. Two players who both -say they are playing "Stable Life" are recorded as having played different games when one of -them resolved the Bulgarian pack, which is the whole point of content identity. - -The pack replaces the campaign **wholesale by id** and its strings **per key** ([11 §3](11-content-packs.md#3-resolution)) — so the Bulgarian world arrives under the same `stable-life` id, and the `ResolutionId` is the only thing that distinguishes the two ([11 §6](11-content-packs.md#6-identity-and-why-determinism-needs-it)). Every surface this needs is already an exported contract surface (`resolvePacks`, `computeResolutionId`, `ContentPack`, `buildValidatedContentRegistry`, `TextClient`); the unit adds no signature. -- **Spec:** [`02-architecture.md`](02-architecture.md) §4a; [11 §2](11-content-packs.md#2-what-a-pack-is), - [§3](11-content-packs.md#3-resolution), [§6](11-content-packs.md#6-identity-and-why-determinism-needs-it); - [04 §10.1](04-core.md#101-content-registry); [10 §7](10-simulation-kind.md#7-content-definition-types). -- **Touches:** `src/engine/src/campaigns/` — a base-pack wrapper around the existing - `stable-life` source and a new Bulgarian culture pack, plus a test alongside. - `src/engine/src/core/registry/packs.ts` is **read, not modified**. -- **Depends on:** [W58](#w58), [W59](#w59), [W50](#w50)–[W57](#w57) — all done. -- **Status:** Done — [PR #314](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/314). -- **Done when:** - - W71.1 Resolving `[base]` and `[base, bulgaria]` both produce a registry that builds and - passes validation; the two differ in rendered text at every key the pack overrides and - at no other key, asserted key-by-key rather than by a whole-table comparison. - - W71.2 No file under `src/engine/src/core/` or `src/engine/src/kinds/` changes. §4a's "no - engine change" is the claim under test, so a diff touching either directory falsifies - it — **stop and report rather than absorbing the change**. - - W71.3 The two resolutions produce different `ResolutionId`s, and each resolved campaign - carries its own as `Campaign.version`, so a save records which mix it was played - against. - - W71.4 A game created against the Bulgarian resolution plays a full week through the text - client — `plan.add`, `end_week`, `view` — and renders the pack's text, never - `stable-life`'s. - - W71.5 A replay fixture captured under one resolution returns `unrunnable` with - `campaign_version_missing` against the other, not `diverged` — the reachability - [W58.6](#w58) built and nothing has yet exercised with real packs. - - W71.6 The same seed and the same actions under the Bulgarian resolution replay - byte-identically through `serialize()`. -- **Out of scope:** the full Bulgarian setting ([W72](#w72)); a Bulgarian *locale* string table - — [W60](#w60) owns translation, and this is voice-and-world within one locale, a - different axis; pack discovery, distribution, and loading from disk, which - [11 §8](11-content-packs.md#8-what-is-deferred) defers by name; a second culture pack. - -### [x] W72 — The Bulgaria Culture Pack: The Full Setting {#w72} - -**Delivers:** Fills the Bulgarian world out from a proof into something worth playing — its -jobs, places, events, housing, possessions and prices — so a player can take the Bulgarian -"Stable Life" all the way to a win and to a loss, rather than only to a correctly rendered -first week. - -Split from [W71](#w71) on size, not on principle: `stable-life`'s content is roughly a thousand -lines across five files, and a unit that authors a Bulgarian equivalent *and* proves the -mechanism would not finish in one session. W71 proves the claim; this one does the volume. -- **Spec:** [10 §7](10-simulation-kind.md#7-content-definition-types); [`02-architecture.md`](02-architecture.md) §4a; - [11 §7](11-content-packs.md#7-validation). -- **Touches:** `src/engine/src/campaigns/` — the pack's content files, mirroring the split - `stable-life-events.ts` / `-housing.ts` / `-possessions.ts` / `-effects.ts` already uses; - `fixtures/replay/`; `src/engine/scripts/demo-cli.ts`. -- **Depends on:** [W71](#w71). -- **Status:** Done — [PR #317](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/317). -- **Done when:** - - W72.1 The pack supplies Bulgarian content for every collection `stable-life` populates — - jobs, places, events, housing, possessions, effects — with none left silently - inheriting the base pack's content by omission. - - W72.2 Two committed replay fixtures under the Bulgarian resolution reach a terminal state, - one `goals_met` and one a failure — the same both-paths bar W40 set for `stable-life`. - - W72.3 Tier 1 and Tier 2 validation pass on the Bulgarian resolution, producing no warning - class `stable-life` does not also produce. - - W72.4 Every player-facing string the pack adds is a `LocKey` with an entry in the pack's - string table; no literal player-facing text reaches a client from the pack's source. - - W72.5 `npm run demo` lists and plays the Bulgarian resolution alongside the base one. -- **Out of scope:** balancing the Bulgarian numbers — this pack inherits the simulation kind's - provisional-numbers debt ([issue #267](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/issues/267)) - rather than resolving it; a Bulgarian locale translation; Jones-in-Corporate or any - second culture pack, which is volume this unit does not need to prove. - -### Breadth: The Platform - -- [x] **Interactive CLI play-test harness — built.** `src/engine/scripts/demo-cli.ts`, run with - `npm run demo` from `src/engine/`: a stdin/stdout loop over the existing `TextClient` and - `SessionStore` that lists the committed campaigns, takes a campaign id and an optional - seed, and accepts ` [key=value ...]`, `view`, `save`, `load ` and `quit`. - Verified playing `stable-life` — the simulation kind — end to end, so it covers both - shipped kinds and not just the story-graph ones. It lives outside `src/`, so neither the - determinism guard nor the client-contract import rule applies to it, and it is covered by - `npm run lint` and `npm run typecheck` (`eslint src scripts`, `tsconfig.scripts.json`). - Manual play-testing tooling, not a shipped client surface — distinct from "more clients" - below; no projection, store, or contract change. - -### [x] W61 — Public Playable Web Demo {#w61} - -**Delivers:** Turns the completed Bureaucracy MVP into a public `/play/` route a visitor can -finish without cloning the repository. The engine runs locally in the browser behind a real -client over `SessionStore`; the page renders scenes, shown choices, disabled reasons, visible -state and achievements, offers non-committing previews and same-page checkpoints, and reaches -the existing ending with no React-owned game rule. - -This is deliberately one campaign and one route. The five story campaigns, Stable Life, and -the world-graph MVP already prove useful engine breadth, but putting all of them in a picker -would turn the first browser boundary into three rendering problems and still leave the -load-bearing question unanswered: can the package, session store, save envelope, registry and -client contract run together in a production browser bundle? [`13-playable-web-demo.md`](13-playable-web-demo.md) -fixes that product and architecture boundary. - -The browser currently exposes three real portability defects hidden by the Node.js CLI: -`version.ts` reads `node:fs`, `envelope.ts` uses `node:crypto`, and `emitter.ts` reads an -unguarded `process.env`. Close those in the shared runtime, not with a reduced browser fork. -The checksum stays SHA-256 over the same canonical bytes, and the pure engine remains -synchronous; only the already-async store boundary may await platform crypto. - -- **Spec:** [`13-playable-web-demo.md`](13-playable-web-demo.md); - [09 §1](09-clients.md#1-the-rule-made-testable), - [§2](09-clients.md#2-the-only-surface), - [§4](09-clients.md#4-the-api-coverage-checklist), - [§6](09-clients.md#6-projection-is-not-optional); - [04 §7](04-core.md#7-the-session-store-and-the-platform-api), - [§9](04-core.md#9-projection), [§10.2](04-core.md#102-save-envelope-and-migration). -- **Touches:** `src/engine/src/version.ts`, `core/persistence/envelope.ts`, - `core/observability/emitter.ts`, the package root and browser-bundle smoke test; - `site/` — a new play entry, composition root, browser adapter, React page, shared - navigation, styles and tests; the static-build/merge verification; factual status copy - in `README.md`, `src/engine/README.md`, and the landing page. -- **Depends on:** [W19](#x-w19--mvp-acceptance), [W31](#x-w31--save-migration), and W41. - Implement against whichever - landing build/merge mechanism is on `main` when the unit starts; issue #179's package - migration is not a semantic dependency and must not be reimplemented here. -- **Status:** Done — implemented via #183 and #184. -- **Done when:** - - W61.1 The supported engine entry graph used by the site produces a real browser bundle - with no `node:` import, unguarded Node.js global, runtime filesystem read, or second - browser-only engine path; Node.js typecheck, lint, tests and package build remain green. - - W61.2 `ENGINE_VERSION` still has package metadata as its single owner, and save/load under - Node.js and a browser produce the same lowercase SHA-256 checksum for the same - `{ state, replayCompatible }` canonical bytes without changing any committed replay or - serialization fixture. - - W61.3 The package root exports the committed Bureaucracy builder needed by the site - composition root; React and the browser adapter import only `SessionStore` types and - call no engine, kind, registry, validation, projection, or persistence helper. - - W61.4 A direct static request to `/play/` succeeds; the production artifact contains - `/`, `/roadmap/`, `/play/`, and `/docs/`, and the protected merge proves the docs - subtree byte-identical before and after overlay. - - W61.5 A visitor can start Bureaucracy, traverse the `office_visits >= 3` loop, see the - gated choice with its reason, exercise the seeded transition, reach the existing - ending, see `it_builds_character`, and start again. No raw `LocKey` appears in any - ready, playing, rejected, preview, or ended state. - - W61.6 Previewing an enabled choice shows the labelled prospective scene without changing - the committed scene, view, action sequence or checkpoint; committing it afterwards - reaches the same result as choosing it without a preview. - - W61.7 Save/load is presented honestly as a same-page checkpoint. Restoring it loses no - state; refreshing starts a new demo and the UI says so. No component writes raw state - or a save envelope to browser storage. - - W61.8 [09 §4](09-clients.md#4-the-api-coverage-checklist)'s browser column is checked - against ten named adapter tests. The full Bureaucracy path through the browser adapter - and text client, under the same seed and counting `IdSource`, produces identical - `Scene`/`PlayerView` steps and byte-identical final `serialize()` output. - - W61.9 The page is keyboard-complete and usable at 320 px, 390 px, 768 px and 1280 px: - native action controls, adjacent disabled reasons, visible focus, announced committed - scene changes, no colour-only state, no horizontal overflow, and complete reduced-motion - behavior. - - W61.10 The public header and landing page expose `Play`; stale claims that nothing is - playable are corrected without calling the demo a finished game. Site checks, - documentation checks, engine gates, `git diff --check`, and the exact-merge deployment - verification all pass before the route is announced. -- **Out of scope:** additional campaigns or kinds; durable browser storage; profiles across - reloads; accounts, cloud sync or any backend; new gameplay; art, audio, analytics, - session capture, service workers, a PWA, or a generic reusable web-client package. - -### [x] W62 — Platform Static Host Image {#w62} - -**Delivers:** Adds a product-owned ASP.NET Core host under `src/host/`, composed with -`SubZeroDev.Platform.Hosting`, and packages W61's verified combined static artifact into a -stateless container. Pull requests build, run, and smoke the image. Merges to `main` publish a -new immutable GHCR image when relevant hosting inputs change, but do not deploy it; GitHub Pages -remains the public host. - -This is the first Platform consumer and deliberately the smaller half of hosting. The engine -continues to execute in the browser. A later hosted-engine-edge slice owns the `.NET Platform -edge → Node engine workload`, JSON/HTTP boundary, MCP projection, and remote session semantics. - -- **Spec:** [`15-platform-static-host.md`](15-platform-static-host.md); - [`13-playable-web-demo.md`](13-playable-web-demo.md) §6; - the Platform repository's `platform-identity.md`, `engine-hosting-contract.md`, ADR-002, - ADR-005, and D3 implementation plan. -- **Touches:** new `src/host/` web project and tests; the multi-stage container definition and - build context; static artifact/route smoke scripts; CI and GHCR publication workflows; - package-source configuration without credentials; hosting documentation. -- **Depends on:** [W61](#w61) and SubZeroDev.Platform S9 package publication. A temporary sibling - `ProjectReference` may unblock local development, but W62 cannot merge until the project - uses one exact released `SubZeroDev.Platform.Hosting` package version and a clean CI clone - restores without `../SubZeroDev.Platform`. -- **Status:** Done — [PR #264](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/264). -- **Done when:** - - W62.1 `src/host/` is the product composition root. It calls `AddPlatformWebHost()` and maps - Platform probes; Platform gains no GameEngine dependency, and the host adds no worker, - persistence, migration, outbox, account, or session service. - - W62.2 The committed project pins an exact released `SubZeroDev.Platform.Hosting` NuGet - version. CI restores it with a short-lived secret that is absent from repository files, - Docker arguments, environment layers, runtime image history, and build output. - - W62.3 One multi-stage build constructs the site and documentation from the same commit, runs - the protected merge, proves the docs subtree byte-identical, publishes the host, and - copies only the verified combined artifact into `wwwroot` in the runtime stage. - - W62.4 Direct container requests to `/`, `/roadmap/`, `/play/`, and `/docs/` return the - expected documents; Platform liveness and readiness succeed; a named unknown route - returns `404` with no SPA fallback. - - W62.5 The browser demo remains W61's local `SessionStore` client: the container exposes no - engine API, game action, or runtime content endpoint, and a production browser smoke - observes no such network request. - - W62.6 The runtime image contains no Node.js, package-manager cache, source tree, build tools, - or registry credential; it runs non-root, supports a read-only root filesystem, writes no - product data, performs no normal outbound request, and stops gracefully on `SIGTERM`. - - W62.7 PR CI builds and starts the exact image, runs positive route/probe/browser checks, and - contains a deliberate missing-or-corrupt-artifact case that proves the gate fails red. - - W62.8 A path-filtered `main` workflow publishes a new GHCR image only when host, site, - documentation-build, merge, container, or locked dependency inputs change. It records an - immutable full-commit tag and digest, creates no `latest` tag, and performs no deployment. - - W62.9 The existing GitHub Pages exact-merge workflow and public routes remain unchanged and - green; an image publication failure cannot alter the live site or an earlier image. -- **Out of scope:** public deployment, DNS/TLS/custom domain, traffic cutover or rollback; - hosted Node engine/API/MCP/session behavior (a later hosted-engine-edge slice); persistence, - auth, accounts, databases, worker processes; gameplay, campaign, browser-client, or - serialization changes; a generic static-site facility in Platform. - -### [x] W63 — Absurd Game Interface {#w63} - -**Delivers:** Rebuilds the public story shelf and story-graph play surface so it reads as a -game rather than a styled form. The interface becomes an original absurd adventure cabinet: -a theatrical scene viewport, tactile action deck, satirical status console, dossier-like story -shelf, and brief mechanical transitions. Its inspiration is the graphic-adventure staging of -*Indiana Jones and the Fate of Atlantis* and the busy life-board energy of *Jones in the Fast -Lane*; it copies neither game's assets, layout, characters, logos, fonts, sounds, or trade dress. - -The slice is presentation-only. Existing `BrowserClient` DTOs and `SessionStore` remain the -only game-facing boundary, and the redesign may not add rules, rewrite campaign text, infer -hidden state, reorder actions, or change serialized outcomes. “Absurd” is a controlled visual -language, not maximum noise: one hero joke and at most two minor jokes per visible state. - -- **Spec:** [`14-game-interface.md`](14-game-interface.md); - [`13-playable-web-demo.md`](13-playable-web-demo.md) §§1–3, §7–§9; - [09 §1](09-clients.md#1-the-rule-made-testable), - [§2](09-clients.md#2-the-only-surface), - [§6](09-clients.md#6-projection-is-not-optional). -- **Touches:** `site/src/play/` — shelf, game cabinet, action deck, status console, transitions, - responsive states, original local assets, component/browser/visual/accessibility tests; - static-build verification for asset budgets and the direct `/play/` route. No engine, - campaign, replay-fixture, or contract-type change. -- **Depends on:** [W61](#w61) and the multi-campaign story shelf already present on `main`. -- **Status:** Done — implemented via #188 and #190. W63.7 (visual snapshots) and W63.8 - (automated accessibility, forced-colours, 200% zoom, and long-text checks) were verified - by manual review at 320/390/768/1280 px rather than a dedicated automated suite; see - [OPEN-QUESTIONS.md §3](OPEN-QUESTIONS.md#3-judgement-calls-to-revisit-settled-for-the-mvp). -- **Done when:** - - W63.1 Ready, playing, busy, unavailable, rejected, persistence-warning, and ended states - all use the cabinet visual grammar and remain distinguishable without colour or motion. - - W63.2 The story shelf is a keyboard-navigable dossier/archive composition; selecting a - story opens a labelled briefing, content notices remain plain and accessible, and - returning from play restores the selected dossier and shelf position. - - W63.3 The scene viewport renders authored text unchanged; the action deck preserves action - order and full labels; the status console renders only `PlayerView`. No raw node id, - `LocKey`, seed, action log, hidden variable, or opaque kind state appears. - - W63.4 Every visible-stat control prints its value in addition to any gauge treatment. A - campaign with no visible stats receives an honest empty-state prop, not a fabricated - score or progress measure. - - W63.5 All art is original local PNG/JPG or CSS-native decoration. A missing decorative - asset leaves a complete readable cabinet; no reference-game asset, font, logo, sound, - character, screenshot, traced composition, or trade dress ships. - - W63.6 One full Bureaucracy run and both Lucifer roles are playable through the redesigned - UI. Existing browser/text-client parity still produces byte-identical serialized - outcomes, proving presentation did not become game logic. - - W63.7 Visual snapshots cover ready, playing, unavailable-choice, persistence-warning, and - ended states at 320 px, 390 px, 768 px, and 1280 px, with no clipped authored text, - horizontal overflow, or action below an inaccessible internal scroll region. - - W63.8 Keyboard-only, automated accessibility, forced-colours, 200% zoom, long-text, and - missing-asset checks pass. Focus moves after committed scenes, restores after a - briefing/dialog closes, and never moves merely because decoration animates. - - W63.9 Motion is limited to brief state punctuation; reduced motion removes transforms, - parallax, wipes, flicker, and staged delay. No action waits for animation and no - permanent timer runs while the page is idle. - - W63.10 Initial decorative payload is at most 1.5 MB compressed, no single decorative asset - exceeds 500 KB, phone layouts do not fetch desktop backdrops, `/play/` remains a direct - static route, and the page performs no runtime request for engine or campaign content. -- **Out of scope:** new campaigns, mechanics, projections, engine APIs, a visual language for - simulation/world-graph, copied reference material, canvas/WebGL, point-and-click movement, - a verb parser, mandatory audio, voice acting, cut-scenes, or procedural art. - -### Depth: Story Campaigns Become Adventures - -### [x] W64 — Replayable Story Campaign Expansion {#w64} - -**Delivers:** Reauthors the six story-graph campaigns already on the public shelf as -replayable narrative adventures and presents them as a playable choose-your-own-adventure -casebook. Return, Bureaucracy, Driving, Inheritance, Enterprise, and Lucifer Chronicles gain -routes that stay apart long enough to feel different, optional scenes, delayed consequences, -hidden discoveries, seeded flavour events, stat-dependent choices, and multiple endings. A -first playthrough must leave authored content undiscovered, while the interface makes the -causal link between the choice just made and the scene that followed unmistakable. - -This is a content slice over the existing `story-graph` contract, not a new narrative runtime. -Money, documents, relationships, attention, and character memory are typed campaign variables; -secrets use `showWhen`; visible obstacles use `requirements`; delayed consequences are earlier -effects read by later conditions; and random events are seeded `random` nodes. A random draw may -change flavour or open an optional detour, but it may not make a planned route unwinnable. - -For this unit, a **visible scene** is a reachable `choice` or `ending` node. `auto` and `random` -nodes settle before projection and therefore do not count toward a campaign's scene target. A -**material route** reaches a different ending or traverses at least two consecutive visible -scenes that another route skips before the routes rejoin. A **delayed consequence** is a choice -effect consulted by a `showWhen`, `requirements`, achievement, or later route at least three -player submissions after it was applied. These definitions keep node inflation and immediate -reconvergence from masquerading as depth. - -The expansion keeps each campaign standalone. Recurring clerks, mechanics, police, relatives, -coffee, flies, and locations make the catalog feel like one universe, but no campaign reads -another campaign's save, profile achievements, or variables. Existing campaign, node, ending, -choice, and achievement ids are published identifiers: retain them where the corresponding -content remains, and cover every necessary rename through campaign migration rather than -silently stranding a v1 save. - -The W63 cabinet remains the outer game shell; its scene viewport becomes an open **adventure -casebook** rather than another disconnected slide. The current scene is the current page, the -action deck is a numbered set of “turn to your choice” passages, visible stats read as a compact -character sheet, achievements arrive as stamps/bookmarks, and an ending closes the volume with -its authored title. CSS-native paper, ink, marginalia, tabs, page edges, and brief page-turn -punctuation may strengthen the game metaphor without copying a published book or hiding the -text inside a decorative texture. - -Every committed transition produces a visible **arrival receipt**: “You chose …” followed by -“which brought you here.” A persistent, read-only **Journey so far** display lists only pages -and choices this player has already seen, links the previous entry to the current page, and -offers “Where I came from” without exposing node ids, hidden choices, conditions, seed, or the -engine action log. The journal is presentation memory assembled from successful projected -scenes and resolved action labels. Previewed and rejected actions never enter it; browsing an -old entry never rewinds or resubmits the game. - -- **Campaign shape:** - - **Return:** at least 20 visible scenes across Returning Home, Reality, and Settling; the - airport/customs arrival, first bureaucracy, neighbours and old connections, and the - rent/village/apartment/family/hotel decision form different routes. At least optimistic, - sceptical, and exhausted endings are reachable. - - **Bureaucracy:** 20–30 visible scenes spanning municipality, cadastral, tax, civil-registry, - archives, notary, and translation-office routes. Helpful/angry clerk and supervisor - memory pays off later; document obtained, gave up, lawyer solved, miracle, and system - failure endings are all reachable. - - **Driving:** at least 25 visible scenes covering inspection, mechanic trust, insurance and - tax, fuel/LPG, road and weather trouble, police, parking, parts, marketplace, towing, and - the insurer. Reliable car, endless repairs, sold car, collector item, and abandoned - project endings are all reachable. - - **Inheritance:** at least 25 visible scenes across news, documents, village, neighbours, - family, police, lawyers, court, settlement, and aftermath. Evidence, support, cost, - tension, and property condition expose old-document, helpful-neighbour, lost-deed, - police-report, and secret-agreement paths. Court, settlement, abandonment, buyout, - family peace, and family war endings are all reachable. - - **Enterprise:** at least 30 visible scenes across registration, first client, tax and - invoicing, cashflow, hiring, competition, government, growth, and failure. Seeded late - payment, audit, tax letter, lucky client, bad review, server outage, and opportunity - events alter pressure without erasing deliberate preparation. Consultant, agency, - successful company, platform company, bankruptcy, and sale endings are all reachable. - - **Lucifer Chronicles:** Ben and Lucifer remain genuinely different perspectives, not a - shared route with renamed prose. Every act decision scene offers four to six authored - choices, earlier interactions alter later availability or dialogue, and every recurring - character with more than one appearance remembers at least one prior interaction. - Generic numbered conclusions are replaced by authored philosophical endings, including - The Bureaucrat, The Observer, The Escapist, The Builder, The Stoic, The Entertainer, - Customer Support Manager of Hell, The One Who Asked One Question, and The Guy Who Just - Wanted to Fix a House. -- **Spec:** [03 §§2–7](03-story-graph-kind.md) for variables, nodes, gates, effects, - conditions and achievements; [§8.2](03-story-graph-kind.md) for settle semantics; - [§10](03-story-graph-kind.md) for determinism, save and versioning; - [04 §10.2](04-core.md#102-save-envelope-and-migration) for campaign migration; - [09 §1](09-clients.md#1-the-rule-made-testable) for client parity. -- **Touches:** the six `src/engine/src/campaigns/` sources and their focused tests, - determinism snapshots and replay fixtures; campaign exports only if composition needs - them; `/play/` catalog metadata, duration/content notices, casebook/journey presentation, - styles, and browser tests. No core, kind, projection, session, or client contract changes. -- **Depends on:** [W31](#x-w31--save-migration) for the v1 → v2 campaign boundary and - [W61](#w61) for public browser proof. The story-graph mechanics themselves are already - complete in W9–W14. -- **Status:** Done — implemented via #189 and #191. -- **Done when:** - - W64.1 Each campaign has at least three material routes, three optional visible scenes, - three delayed consequences, three `showWhen` discoveries driven by different state, - two seeded random-event points whose outcomes are both exercised, and two later gates - driven by stats or remembered interactions. Static graph assertions and named - playthroughs prove the counts; source-line or raw-node counts do not. - - W64.2 All six campaign-shape targets above are met using reachable visible scenes. No - promised consequence is authored only as pass-through text that settle prevents a - client from seeing, and no route pads its length with repeated “continue” decisions. - - W64.3 Two playthroughs of each campaign diverge for at least two consecutive visible - scenes before any reconvergence, and no single valid playthrough visits more than 70% - of that campaign's reachable visible scenes. At least one tested ending per campaign - requires an earlier choice made three or more submissions before the ending path opens. - - W64.4 Every named ending above is reachable by an intentional route, with an authored - label and ending text rather than a numbered placeholder. Exploration achievements - unlock exactly once for optional content and do not become resolution inputs across - sessions. - - W64.5 Every campaign publishes version `2.0.0`. A v1 active save, a v1 ended save, and a - v1 save with achievements migrate or fail with a deliberately tested published-id - decision; no save resumes on a missing node, and migrated saves are marked - `replayCompatible: false` as W31 requires. - - W64.6 Each source builds with no Tier 1 findings and no unexplained Tier 2 warning. A graph - test detects unreachable scenes, exitless settle cycles, duplicate ids, endings that - cannot be reached, hidden choices that can never appear, and random branches that can - remove every route to a planned objective. - - W64.7 At least three committed route fixtures per campaign cover materially different - paths and endings; separate seeds cover every authored random transition. Replaying a - fixture twice is byte-identical, sink-independent, and stable across save/load at a - delayed-consequence checkpoint. - - W64.8 One representative alternate route through each campaign produces the same ordered - scenes, available actions, visible view, and final serialized state through the text - client and browser adapter. A hidden choice remains absent and returns - `unknown_action` if probed; the browser never gains campaign-specific rules. - - W64.9 Recurring-universe references are prose and stable authored ids only. A test registry - containing all six campaigns builds without string-key conflicts, and running any - campaign with a fresh profile produces the same initial state regardless of which - other campaigns were previously played. - - W64.10 `/play/` exposes all six v2 campaigns with truthful duration and content notices; - one full route in each is keyboard-complete at the W63 breakpoints, renders every - visible stat as text, and shows no raw `LocKey`, internal node id, or hidden state. - - W64.11 The initial page says “Your story begins here.” After every successful choice, the - next page names the resolved label under “You chose” and visually connects it to the - new scene under “which brought you here.” Preview, unavailable, rejected, save, and - load operations cannot create a false transition receipt; rapid double submission - cannot duplicate one. - - W64.12 “Journey so far” records the ordered projected scene excerpt and committed choice - label for the current live route, highlights the current page, and lets the player - inspect a prior entry read-only before returning focus to the current choice. It stores - no `Scene.id`, action id, condition, raw `PlayerView`, seed, or serialized state. A - checkpoint may carry a separate presentation-only journal beside the save handle; if - that journal is missing or invalid, load succeeds and displays “Journey resumed at this - checkpoint” rather than inventing earlier steps. Journal presence or absence produces - byte-identical engine serialization. - - W64.13 The casebook is recognisably playable before decoration loads: current page, - consequence link, numbered choices, character sheet, journey control, and ending action - remain in that reading order. Page-turn motion lasts no longer than state punctuation, - never delays an action, and becomes an instant page replacement under reduced motion. - At 320 px the book becomes one page with the same semantic order; it never requires a - two-page spread, hover, drag, sound, or a page-flip gesture. -- **Out of scope:** a new node kind, conditional-transition operator, inventory or relationship - subsystem, cross-campaign saves or unlocks, procedural/AI-authored prose, new campaigns, - simulation/world-graph content, localization, voice/audio, canvas/WebGL, undo/backtracking, - a fabricated completion percentage, and any journal field that participates in game - resolution. - -### [x] W65 — Browser Test Harness for the Site {#w65} - -**Delivers:** Gives `site/` the ability to prove anything about how a page actually renders. -Its tests run in jsdom today, which performs no layout: `getBoundingClientRect` returns zeros -and stylesheet CSS is never cascaded, so no computed size, hit area, overflow, or contrast can -be asserted. That is why [W63](#w63) was accepted on manual inspection at four widths, recorded -as known-and-retained in [`OPEN-QUESTIONS.md`](OPEN-QUESTIONS.md), whose stated revisit trigger is -exactly this: extend `site/` with real-browser tooling, and extend it to `/play/` first. - -W65 adds a real-browser runner, an accessibility scanner, and visual snapshots, then **captures -the currently shipped rendering as the baseline**. Ordering matters: [W66](#w66) recomposes the -play surface and promises the desktop compositions survive untouched, and that promise is only -provable against a baseline taken before the CSS moves. A harness stood up alongside the -redesign would baseline the changed rendering and prove nothing. - -The harness is test infrastructure. It ships no product behaviour, no page change, and no -engine change. Where an existing jsdom test is adequate it stays put; this is not a migration -of the whole site suite. - -- **Spec:** [`14-game-interface.md`](14-game-interface.md) §10 for the proof list this must be - able to execute, and §8 for the widths and states it must reach; - [13 §7–§8](13-playable-web-demo.md#7-client-proof-and-tests). -- **Touches:** `site/package.json`, `site/vite.config.ts`, a browser-test setup file, new - specs under `site/src/play/` and `site/src/`, committed baseline snapshots, and the CI - workflow that runs the site's check script. No `src/engine/`, `design/` contract, or - product-code change beyond a test id where a control is otherwise unaddressable. -- **Depends on:** [W63](#w63) and [W64](#w64) being on `main`, since the baseline is of what - they shipped. No engine dependency. -- **Done when:** - - W65.1 `site/` runs specs in a real browser engine, driven by the package's existing check - script and by CI, with a documented single command. A deliberately failing computed-style - assertion fails that command; a jsdom-only run cannot silently satisfy it. - - W65.2 The runner can set viewport width and height, so a spec can assert at 320, 360, 390, - 414, 768, and 1280 px in portrait and at one landscape phone size, and can emulate - `prefers-reduced-motion` and forced colours. - - W65.3 A spec can read a **computed** style and a real hit area from a rendered control, and - can assert the document does not scroll horizontally. Each of those three capabilities - has a self-test proving it fails when the condition is violated. - - W65.4 An automated accessibility scan runs against the shelf, briefing, content notice, - playing, unavailable-choice, rejected, and ended states, and fails the build on a - violation at the agreed severity. Existing violations, if any, are recorded explicitly - rather than silenced by lowering the threshold. - - W65.5 Visual snapshots of the shipped `/play/` rendering are captured and committed for - playing, unavailable-choice, persistence-warning, and ended states at 320, 390, 768, and - 1280 px. Snapshot review and update are documented, and a snapshot diff fails the build. - - W65.6 The harness is deterministic enough to run in CI without flake: fonts, animation, and - any time-dependent rendering are pinned or disabled for capture, and a repeated run on an - unchanged tree produces no diff. - - W65.7 Engine gates, documentation checks, the existing jsdom suite, the production site - build, and `git diff --check` all still pass, and the added tooling does not enter the - shipped `/play/` bundle. - - W65.8 The known-and-retained W63.7/W63.8 entry in [`OPEN-QUESTIONS.md`](OPEN-QUESTIONS.md) is closed - or narrowed to whatever genuinely remains, rather than left standing beside a harness - that resolves it. -- **Out of scope:** any `/play/` visual, layout, type, or markup change — that is [W66](#w66); - migrating the existing jsdom suite wholesale; a harness for `docs/`; performance - budgets, Lighthouse scoring, or cross-browser matrices beyond the one engine needed to - make the assertions real; and any engine or campaign change. - -### [x] W66 — The Play Surface on a Phone {#w66} - -**Delivers:** Recomposes `/play/` for the device most visitors actually hold. The W63 cabinet -and the W64 casebook were both measured on a desktop and then allowed to shrink: authored -prose renders at 16 px and choice labels at about 13 px on a phone, cabinet controls stand -roughly 34 px tall against a 44 px comfortable touch target, the cabinet's 8 px offset shadow -sits outside a nearly full-width element at 320 px, panels are sized in `vh` under a collapsing -mobile toolbar, no edge respects a device safe area, and every turn requires scrolling past the -scene to reach the choices — after which committing one scrolls the player back to the top. - -W66 makes the phone the composed case rather than the degraded one. Below 768 px a turn becomes -**two scroll-snapped pages** in one ordinary scrolling column: a scene page that fills the -viewport and names how many choices wait below it, then a choice page of full-width cards under -a pinned one-line echo of the scene. Type and hit-area floors from -[14 §8.1](14-game-interface.md#81-type-and-target-floors) raise every width, including desktop, -because a 0.68–0.82 rem control scale was never comfortable there either. - -**The retro look is a fixed input, not a variable.** The palette, the terminal type family, the -scan lines, stamped uppercase labels, offset shadows, double borders, and campaign accents all -survive unchanged. This slice moves size, spacing, safe areas, and reading order. A submission -that reads as a modern mobile app has failed even if every measurement passes. - -The slice stays presentation-only under the boundary -[13 §3](13-playable-web-demo.md#3-composition-and-dependency-direction) and -[14 §1](14-game-interface.md#1-outcome-and-boundary) already set. No engine, kind, campaign, -projection, DTO, session, persistence, or client-parity change; no new gesture, route, bundle, -component tree, or user-agent branch. - -One correction rides along because it is a phone problem specifically: the authored scene body -is currently marked up as an `h2`, which makes the screen-reader heading rotor — the primary -navigation mechanism on a phone — return a wall of story instead of a landmark. The scene -becomes a labelled region with a short real heading, and the post-commit focus target moves -with it. Rendered authored text is unchanged. - -- **Spec:** [`14-game-interface.md`](14-game-interface.md) §1 and §8 (Revision 2), with §§2–7 - unchanged; [`13-playable-web-demo.md`](13-playable-web-demo.md) §3, §7–§9; - [09 §1](09-clients.md#1-the-rule-made-testable), - [§2](09-clients.md#2-the-only-surface), - [§6](09-clients.md#6-projection-is-not-optional). -- **Touches:** `site/src/play/play.css` (type scale, hit areas, snap pages, safe areas, - full-bleed trim, breakpoints), `site/src/play/PlayApp.tsx` (scene region and heading, - choice-count cue, post-commit scroll target), `site/src/play/PlayApp.test.tsx` and the - site's browser/visual checks, and `site/play/index.html` only if a viewport or - `theme-color` correction is needed. No `src/engine/` change of any kind. -- **Depends on:** [W65](#w65) — hard, not preferential. Every measured criterion below needs a - real browser, and the promise that the desktop compositions survive is only checkable - against a baseline W65 captures before this slice moves any CSS. Also [W63](#w63) for the - cabinet grammar and [W64](#w64) for the casebook, journey log, and arrival receipt that - must survive the recomposition. No engine dependency. -- **Done when:** - - W66.1 Every §8.1 role meets its floor as a **computed** style at 320 px — authored prose - 1.125 rem at line-height 1.6 or more, choice labels 1.0625 rem, cabinet controls and - dossier titles 1 rem, stat labels and values 0.9375 rem, reason/receipt/journey/save - text 0.875 rem. Assertions read computed values from a rendered tree; matching the - stylesheet's source text does not count. Only stamped marquee, eyebrow, and disk labels - sit below that, and each is decorative or duplicated by larger text nearby. - - W66.2 Every interactive control — choice card, cabinet button, dossier, notice button, - journey control, scene-echo cue — presents at least a 44 × 44 px hit area at 320 px and - at 1280 px, produced by padding rather than a transparent overlay, with at least 8 px of - non-actionable space between adjacent choice controls. - - W66.3 Below 768 px a turn renders as two scroll-snap stops: a scene page that fills the - viewport and names the shown-choice count, then a choice page of full-width cards under - a pinned single-line scene echo that returns to the scene page when activated. At - 768 px and above no snapping applies, and the 1280 px composition differs from the - W65 baseline only in the type and spacing W66.1 and W66.2 require — every other - difference is either justified in the pull request or reverted. - - W66.4 With scroll-snap unsupported, with smooth scrolling unavailable, and with the cue's - script path disabled, the scene, every choice, and the status console all remain - reachable by ordinary vertical scrolling in that order. Both pages are in the DOM at - all times; no choice is conditionally unmounted, gesture-gated, or revealed only by - animation. - - W66.5 Committing an action lands the player on the new turn's scene page with focus on the - scene, never on a stale choice page and never mid-transition. A rejected action leaves - the player where they were with the scene still authoritative, and cannot produce a - false arrival receipt or journey entry — W64.11 and W64.12 still hold verbatim. - - W66.6 No gesture is introduced: no swipe, horizontal paging, carousel, drag, long-press, - edge gesture, or pull-to-refresh interception. Pinch zoom is not disabled and the - viewport is not pinned to a fixed width. - - W66.7 Full-height panels use dynamic viewport units, every inset-facing edge adds - `env(safe-area-inset-*)` padding, and below 768 px the cabinet is full-bleed with its - offset shadow and double border collapsed to a single edge. The document does not - scroll horizontally at 320, 360, 390, 414, or 768 px in portrait, in landscape, or at - 200% zoom; at 200% zoom on a 390 px viewport the narrow composition is retained. - - W66.8 The authored scene renders inside a labelled region with a short real heading; the - prose itself is not a heading. The page keeps one H1, a coherent heading order, and its - existing live-region announcements. Automated accessibility checks pass on shelf, - briefing, notice, playing, unavailable-choice, rejected, and ended states, and a - keyboard-only pass completes a full turn without a pointer. - - W66.9 The retro identity is preserved and shown to be: the palette custom properties, type - family, scan-line overlay, stamped uppercase labels, offset shadows, double borders, - and the six campaign accent themes are all still present and applied. No colour token - changes value. Uppercase transformation appears on stamped labels only — never on - authored prose, choice labels, reasons, or error text. - - W66.10 Reduced motion makes the cue jump and the post-commit return instant, removes - smooth scrolling, and leaves snapping and every authored transition already governed by - W63.9 intact. No action waits on an animation and no permanent timer runs while idle. - - W66.11 One full route through Bureaucracy and one through each Lucifer role complete at - 320 px, 390 px, 768 px portrait and one landscape phone, with no clipped authored text, - no truncated or ellipsised choice label, and no action stranded below an inaccessible - internal scroll region. Visual snapshots cover playing, unavailable-choice, - persistence-warning, and ended states at each of those sizes. - - W66.12 Browser/text-client parity still produces byte-identical serialized outcomes, the - engine package is untouched, `/play/` remains a direct static route making no runtime - request, and the decorative payload does not grow. Site checks, documentation checks, - engine gates, and `git diff --check` all pass. -- **Out of scope:** any engine, kind, campaign, projection, DTO, reason-code, session, or - persistence change; new campaigns, scenes, endings, or mechanics; a palette, type-family, - or voice refresh; a separate mobile route, bundle, component tree, or user-agent branch; - a native shell, PWA, service worker, install prompt, or offline mode; gesture navigation, - a bottom-sheet choices modal, or a carousel; durable storage, accounts, analytics, audio, - new art beyond CSS-native trim adjustments, and a mobile visual language for the - simulation or world-graph kinds. - -### Correctness Debt Found by Reconciliation - -### [x] W67 — Restore the Story-Graph Regression Evidence {#w67} - -**Delivers:** The replay corpus, determinism goldens, and observability acceptance test that -the [W64](#w64) campaign rewrite removed for the flagship kind. Story-graph is currently the -only shipped kind with no cross-version oracle, and nothing reports that: the suite is green, -because the tests that would have failed were deleted along with the campaigns they covered. - -Three separate losses, one cause. `bulgaria-bureaucracy.replay.test.ts` was the only reader of -`fixtures/replay/bureaucracy-*.{fixture,outcome}.json`; those six files are still on disk, -still pinned at `campaignVersion: "1.0.0"`, and now orphaned. Five -`__snapshots__/*.determinism.test.ts.snap` golden files went with their tests. And -`bulgaria-bureaucracy.observability.test.ts` — the executable form of `MVP.md` §5's -*Observable* box, proving a human reading the `jsonl` stream can diagnose the gate's visit -counts and the random transition's pick — has no replacement; `nullEmitter` now appears only -in two unit suites, against no real campaign. - -The corpus files could not have survived the rewrite unchanged, since the campaigns now publish -`2.0.0`. That is the reason to regenerate them deliberately, not the reason to leave them. - -**A regenerated `.outcome.json` is a statement that the game changed** -([07 §4](07-replay.md#4-the-corpus)) and is reviewed as one. This unit produces new outcome -files against v2 routes; each is read on its merits, not accepted because the runner emitted it. - -- **Spec:** [`07-replay.md`](07-replay.md) §4, §6, §7; [04 §14](04-core.md#14-determinism-harness); - [`05-observability.md`](05-observability.md) §12; `MVP.md` §5. -- **Touches:** `src/engine/fixtures/replay/bureaucracy-*.{fixture,outcome}.json`, - a restored replay suite and determinism suite beside the campaigns, and a restored - `jsonl` observability suite. No `design/` change — this unit implements what the - documents already require. -- **Depends on:** [W64](#w64) being on `main`, since the fixtures are regenerated against its - v2 campaign graphs. -- **Status:** Done — [PR #261](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/261). -- **Done when:** - - W67.1 Three `bureaucracy-*` fixture/outcome pairs exist against `campaignVersion: "2.0.0"`, - covering materially different routes and matching [07 §4](07-replay.md#4-the-corpus)'s - own priority order: a Definition-of-Done arc, a gated choice, and one deliberate edge - case. Each outcome file is reviewed as content, not regenerated in bulk. - - W67.2 A replay suite enumerates the corpus by prefix from `fixtures/replay/`, so a new - fixture pair needs no test-file edit, and honours `REPLAY_BASELINE_DIR` the same way the - `stable-life` and `world-graph-mvp` suites do — the W23 release-tag job must cover - story-graph again. - - W67.3 **The corpus asserts its own membership.** A test fails when an expected fixture - prefix is absent and when a `.fixture.json` has no matching `.outcome.json`, or vice - versa. Directory enumeration alone is what made the original deletion invisible; this - criterion is the whole lesson of that loss and is not optional. - - W67.4 A story-graph determinism suite commits a golden `serialize()` output for at least - one campaign and replays it under `nullEmitter` and `recordingEmitter` with - byte-identical output — [04 §14](04-core.md#14-determinism-harness)'s golden-file and - sink-independence rows, against a real campaign rather than a synthetic state. - - W67.5 Stream reproducibility holds: the same fixture under `recordingEmitter` twice yields - the identical event sequence, with `gameId` normalized out - ([05 §12](05-observability.md#12-validation-and-tests)). - - W67.6 A `jsonl` observability suite restores `MVP.md` §5's *Observable* claim against a real - campaign: the stream is unfiltered, and both a gate's visit counts and a random - transition's pick are readable from it. - - W67.7 Engine gates, documentation checks, and `git diff --check` all pass, and no orphaned - fixture file remains in `fixtures/replay/`. -- **Out of scope:** campaign content changes; a new corpus for `simulation` or `world-graph`, - both of which still have theirs; the cross-repository replay corpus; any change to the - runner's verdict vocabulary or to `Outcome`'s shape. - -### [~] W68 — Make the Browser Save Adapter Actually Restore — cancelled {#w68} - -**Cancelled.** Every file this unit touches is in `site/src/play/`, the route -[`13-playable-web-demo.md`](13-playable-web-demo.md) now records as superseded by -[SubZeroDev.Adventures](https://github.com/The-Running-Dev/SubZeroDev.Adventures). -Implementing a durable checkpoint into a surface being deleted buys nothing, and Adventures -already has the durable version this unit was reaching for — server-side saves in Postgres, -not a `localStorage` adapter. - -Two of the criteria were about the engine rather than the route, and outlive the -cancellation. Both are in [`OPEN-QUESTIONS.md`](OPEN-QUESTIONS.md)'s open register: -**W68.5** — `SaveRecordStore.delete` has no caller in the engine or in either shipped host, -so the port requires a method nobody invokes. **W68.2**'s resume affordance rests on a -per-player save query the `SessionStore` contract has no operation for, which two independent -hosts have now separately invented. - -The original statement of the unit follows, unchanged, as the record of what was planned. - -**Delivers:** The working half of the durable local checkpoint that -[13 §5](13-playable-web-demo.md#5-checkpoints-and-lifetime) Revision 2 now specifies. The -`SessionPersistence` port and a `localStorage` adapter shipped with [W61](#w61); the adapter -writes under `record.campaignId` and reads by `saveId`, so every write succeeds, every read -misses, and no gate notices. Nothing on the page ever attempts a restore, and the session half -of the adapter is an in-memory `Map`, so nothing survives a reload today regardless. - -The design was written after the code here, which is the defect and is recorded as such in -`design/90-decisions.md`. This unit brings the code up to the specification that -now exists. - -- **Spec:** [`13-playable-web-demo.md`](13-playable-web-demo.md) §5 (Revision 2); - [04 §7.2](04-core.md#72-host-persistence--the-record-store-beneath-the-session-store); - [06 §5.2](06-extensibility.md#52-sessionpersistence-and-profilestore); - [09 §3](09-clients.md#3-what-a-client-may-and-may-not-do). -- **Touches:** `site/src/play/composition.ts` (the adapter), `site/src/play/PlayApp.tsx` - (resume offer and honest save state), `site/src/play/browser-client.test.ts` and - `PlayApp.test.tsx`. `src/engine/` only if `SaveRecordStore.delete` is resolved by removing - it. -- **Depends on:** [W61](#w61). Independent of [W65](#w65)/[W66](#w66) — it changes behaviour, - not layout — but should land before them so the resume path is in the visual baseline. -- **Done when:** - - W68.1 The adapter addresses a save by `saveId` for both `get` and `put`, and a test writes - a save, reconstructs the store from a fresh adapter over the same backing storage, and - loads it back — the round trip the current implementation cannot perform. - - W68.2 `/play/` offers to resume a stored checkpoint on load, per campaign, and declining it - starts a new run without destroying the stored one until the player commits to that. - - W68.3 Storage failure is honest and non-fatal: a quota error, disabled storage, or private - browsing surfaces `storage_failure` rendered through the string table — never a raw - English message — and the run continues in memory. A run with `SessionPersistence` - omitted entirely still satisfies all ten rows of [09 §4](09-clients.md#4-the-api-coverage-checklist). - - W68.4 The save state the page shows is truthful: "saved" is claimed only after a write the - adapter confirmed, and the existing warning copy no longer implies a durable write that - did not happen. - - W68.5 `SaveRecordStore.delete` is resolved — either the store calls it on a path that needs - it, or it is removed from the port. A required method with no caller obliges every host - to implement nothing. - - W68.6 The client still persists nothing: React holds a `SessionStore` and never sees a blob, - an envelope, or a storage key. The adapter lives in the site composition root. - - W68.7 Browser/text-client parity still produces byte-identical serialized outcomes, and - engine gates, site checks, documentation checks, and `git diff --check` all pass. -- **Out of scope:** accounts, cloud sync, cross-device resume, multiple named save slots per - campaign, a server-held session, or any storage-format migration mechanism — the format is - the existing `SaveEnvelope` and §10.2 already owns its versioning. - -### [x] W69 — Consume the Reusable Landing-Page Package {#w69} - -**Delivers:** Keeps the Engine's existing landing page and roadmap exactly as visitors see -them, while replacing its repository-owned Vite build and PowerShell merge machinery with the -reusable landing-page package Platform already publishes. The Engine continues to own its -React pages, visual identity, metadata and tests; the package owns route builds and the -protected documentation merge. - -The site is already an exceptional custom frontend, not a README-driven generic page. It -therefore consumes the package's published custom-adapter seam: two Engine-owned entry modules, -two independently declared metadata sets, the existing static assets, and no copied Platform -component or stylesheet. This is a toolchain extraction, not a landing-page redesign. -- **Spec:** none — site toolchain only. The consumed contract is - `subzerodev-platform-ui-landing-page@0.2.0`'s published `defineLandingPage` adapter and - protected `merge` command. -- **Touches:** `site/landing.config.ts` (new); `site/package.json`, `site/package-lock.json`, - `site/vitest.config.ts` (new), `site/README.md`, `site/scripts/verify-build.mjs`, and a - merge-verification script under `site/scripts/`; the obsolete `site/vite.config.ts`, - `site/index.html` and `site/roadmap/index.html`; `build/Merge-LandingPage.ps1`; - `.github/workflows/docs-ci.yml` and `.github/workflows/docs-deploy.yml`. -- **Depends on:** nothing Engine-side. The immutable external prerequisite is - `subzerodev-platform-ui-landing-page@0.2.0`, whose adapter preserves route-specific - canonical, Open Graph, X/Twitter, icon, theme-colour and no-script metadata. -- **Status:** Done — [PR #272](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/272), - with a follow-up in the same branch fixing the host-image build and the `/play/` host gap - the extraction left behind. - - **W69.4 is not fully met, deliberately, and that is recorded rather than waved through.** - The component tests pass unchanged, but `/play/` was dropped from the build during the - slice, which removed the `cta-play` link from `App.tsx` and its rule from `landing.css` — - a real rendered-content change, authorized mid-slice rather than in this unit's stated - scope. It is consistent with `/play/` being superseded by - [SubZeroDev.Adventures](https://github.com/The-Running-Dev/SubZeroDev.Adventures) - ([`13-playable-web-demo.md`](13-playable-web-demo.md), *Succeeded by - SubZeroDev.Adventures*), which is why it was accepted; the deviation from W69.4 as - written is the part worth naming. The removed `/play/` browser smoke it also cost is - tracked as - [issue #273](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/issues/273). -- **Done when:** - - W69.1 `site/package.json` pins `subzerodev-platform-ui-landing-page` exactly at `0.2.0`, - the lockfile resolves that version, and the site no longer declares Vite or the React - Vite plugin directly; Vitest retains its existing jsdom setup through a dedicated - test configuration. - - W69.2 `site/landing.config.ts` declares exactly `/` and `/roadmap/`, each pointing at its - existing Engine-owned entry module and carrying the complete metadata currently in - that route's HTML: title, description, canonical URL, Open Graph fields, X/Twitter - card, icons, theme colour and no-script text. - - W69.3 `npm --prefix site run build` emits `site/dist/index.html` and - `site/dist/roadmap/index.html`, copies every referenced public asset, and leaves no - development-only `/src/` reference in either built document. - - W69.4 The existing landing and roadmap component tests pass unchanged, and no Engine - component, page copy, stylesheet or public asset changes; adopting the package causes - no rendered-content or visual redesign. - - W69.5 The site scripts use the package CLI for `dev`, `build` and `merge`; the handwritten - Vite route inputs and `build/Merge-LandingPage.ps1` have no remaining caller or copy in - the repository. - - W69.6 A merge test starts from a real landing build and a fixture documentation output, - then proves the result contains `/`, `/roadmap/` and `/docs/` while every file under - the protected `docs/` subtree remains byte-identical. A landing fixture containing a - top-level `docs/` path is rejected, proving the guard's negative path. - - W69.7 Both documentation workflows install the pinned site dependencies, run the full site - check, and invoke the package-backed merge. Their triggers, Pages permissions, - concurrency and deployment environment remain caller-owned and unchanged. - - W69.8 `npm --prefix site run check`, `build/Test-Documentation.ps1` and - `git diff --check` pass; the production documentation build remains a separately - reported Docker-dependent gate. -- **Out of scope:** changing landing-page or roadmap content, visual design, routes, roadmap - delivery status, documentation information architecture, GitHub Pages policy, or the - reusable package itself; adopting the package's generic README renderer; copying any - Platform page component, style or token into Engine. - -### [~] W70 — Gate `/play/`'s Startup Request Surface — cancelled {#w70} - -**Cancelled**, for the same reason as [W68](#w68): it gates the emitted `/play/` bundle, and -that route is superseded by -[SubZeroDev.Adventures](https://github.com/The-Running-Dev/SubZeroDev.Adventures) -([`13-playable-web-demo.md`](13-playable-web-demo.md), *Succeeded by SubZeroDev.Adventures*). - -**The property is not cancelled — only this unit's location for it.** A browser build that -silently acquires an off-origin request is exactly the regression -[`13`](13-playable-web-demo.md) §4 refuses to leave unasserted, and Adventures ships a browser -build. The gate belongs beside the bundle it guards, in the repository that emits it, which is -no longer this one. Adventures already carries §4's Node-only-import half of the same check in -its own `scripts/verify-build.mjs`; the startup-request half is the piece still missing there. - -The original statement of the unit follows, unchanged. - -**Delivers:** An assertion that the emitted `/play/` bundle issues no startup request beyond -the same-origin `campaigns/` files the deployment already contains. - -`13-playable-web-demo.md` §6 used to claim the deployment made no runtime network request at -all, and campaigns are now runtime-loaded JSON, so the claim was false and nothing noticed. -Reconciliation restated §6 against what ships; this unit supplies the gate, because a restated -claim with no check is the same unasserted property §4 already refused to accept for `node:` -specifiers. `site/scripts/verify-build.mjs` is the existing home: it already scans the emitted -bundle rather than trusting the build to have failed. - -The check is a boundary, not an inventory. It must fail on a *new* request destination — a CDN -font, an analytics beacon, a third-party host, an engine API — without failing every time a -campaign is added, since the campaign set is enumerated by `manifest.json` and changes by -design. -- **Spec:** [`13-playable-web-demo.md`](13-playable-web-demo.md) §4 (the gate is an assertion - over the emitted bundle, not the build succeeding), §6 (what the startup path may - request), §9 (a fetch failure is a start-up failure the error boundary owns). -- **Touches:** `site/scripts/verify-build.mjs`; a browser test under `site/src/play/browser/`. -- **Depends on:** nothing. [W62](#w62)'s container smoke consumes the same property but does - not block this. -- **Status:** Not started. -- **Done when:** - - W70.1 A build-verification assertion scans the emitted `/play/` bundle for request - destinations and fails on any absolute URL, protocol-relative URL, or non-`campaigns/` - same-origin path reachable from the startup path. - - W70.2 The assertion has a proven negative: a fixture bundle containing one off-origin - request makes the check go red, committed alongside it. A gate never seen to fail is - not evidence. - - W70.3 Adding or removing a campaign JSON changes no assertion and needs no update to the - gate — the campaign set is data, enumerated by `manifest.json`. - - W70.4 A browser test asserts the observed network requests during a real `/play/` load are - exactly `campaigns/manifest.json` plus the files that manifest lists, and nothing else. - - W70.5 A failed campaign fetch renders §9's single error boundary with a restart path, and - never a raw exception or a partial shelf. - - W70.6 `npm --prefix site run check`, the engine gates, `build/Test-Documentation.ps1` and - `git diff --check` all pass. -- **Out of scope:** moving campaigns back into the bundle, changing the portable format, - caching or a service worker (a named non-goal, [`13`](13-playable-web-demo.md) §10), and - W62's container-side smoke, which asserts the same property from outside the page. - -- [ ] More clients (Discord; the first web client is [W61](#w61)). -- [ ] **Additional locales — sliced as [W60](#w60).** The MVP ships English only; the - authoring→registry types already support more - ([04 §10.1](04-core.md#101-content-registry)), so this is string tables plus tooling, - no type change. -- [ ] AI-assisted authoring (content only; engine validates). -- [ ] **W63 proposed — Hosted engine edge.** Follow W62 with the real - `.NET Platform edge → Node engine workload` process boundary: generated JSON/HTTP service - contract first, MCP as a projection, one in-memory remote session before persistence, - accounts, catalogue, or metering. Slice it only after W62 and the Platform package gate are - proven ([`neaas-platform-vision.md`](https://github.com/The-Running-Dev/SubZeroDev.Platform)). -- [ ] Content packs — **sliced as [W58](#w58) and [W59](#w59)** — per - [`11-content-packs.md`](11-content-packs.md): `resolvePacks` as a - pure ordered fold; campaigns replace wholesale, strings per key; exact-version - dependencies with no range solving; `campaignVersion` stamped with the `ResolutionId` - so a game records the content it actually ran against; experiment gates (§5a) as the - one mechanism for both A/B testing and feature flags, filtered before the fold via - `ExperimentSource` ([06 §5.5](06-extensibility.md#experimentsource)). Before mods, not before MVP - ([`neaas-platform-vision.md`](https://github.com/The-Running-Dev/SubZeroDev.Platform) → Known deferred gaps). - `src/engine/src/core/composition/types.ts` now declares `ExperimentSource`, and the gate - machinery ships and is exported. [06 §4](06-extensibility.md#4-the-composition-root) now - settles the remaining seam: `SessionHost.experiments` carries the already-resolved, - non-null assignment map for event attribution; candidate packs and `ExperimentSource` - remain above the session layer. The declaration and record-stamping implementation still - need to be brought into line with that resolved contract. - -### Content Tooling — A First-Class Workstream, Not an Afterthought - -> Peer review's sharpest point: as campaigns grow, the runtime stabilizes while -> **tooling becomes the larger effort.** Named here so it is planned, not discovered. - -- [ ] Content validator / linter (the Tier 1/2 checks, as an author-facing tool). -- [ ] Graph visualization + a visual node editor. -- [ ] Content diff and balancing tools. -- [ ] Localization tooling (string-table extraction, coverage, translation). -- [ ] Authoring assistants (AI-drafted content → the same validation, §9). - -**Tier 3 is the one of these that is fully specified and entirely unbuilt**, so it is sliced -first, as **W73**. [04 §11](04-core.md#11-tiered-validation) names it — "unwinnable campaigns, -dead-end states — found by running, not reading" — and [03 §11](03-story-graph-kind.md#11-validation-story-graph-specific) -gives its two story-graph cases, but nothing under `src/engine/src/core/validation/` -implements it. - -### [x] W73 — Tier 3 Validation as an Author-Facing Check {#w73} - -**Delivers:** Tells a campaign author the thing that reading the campaign cannot — which -endings no sequence of choices can actually reach, and which choices no reachable state can -ever satisfy. Today a campaign passes every load-time check and still ships with an ending -nobody can get to; the only way to find that out is to play it and notice. - -**Scoped deliberately narrow, because the contract disagrees with itself about what Tier 3 is.** -[12 §15](12-world-graph-kind.md#15-validation) rejects "Tier 3 simulation findings" outright — -dominant strategies, infinite-money loops, unavoidable bankruptcy — on the grounds that they -are content-balance findings from a long-running search, and putting one inside registry -construction would break [04 §10.1](04-core.md#101-content-registry)'s purity requirement. That -objection is correct and this unit does not touch it. What is left, and what 03 §11 actually -asks for, is **reachability over a finite authored graph** — decidable, bounded, and run -offline as tooling, never at load. If the two statements turn out not to reconcile this way, -that is `/contract`'s call, not this unit's. -- **Spec:** [04 §11](04-core.md#11-tiered-validation) (Tier 3 is not part of load), - [§10.1](04-core.md#101-content-registry) (registry construction is pure and total); - [03 §11](03-story-graph-kind.md#11-validation-story-graph-specific) (the two story-graph - Tier 3 cases); [12 §15](12-world-graph-kind.md#15-validation) (what Tier 3 is *not*). -- **Touches:** a new `src/engine/scripts/validate-campaign.ts` — tooling outside `src/`, the - same placement and rationale `demo-cli.ts` already uses; `src/engine/package.json` - scripts; a broken fixture under `src/engine/src/campaigns/`. -- **Depends on:** nothing. -- **Status:** Done — [PR #319](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/319). -- **Done when:** - - W73.1 Running the checker over a committed story-graph campaign reports, per ending, - whether some sequence of choices from `startNodeId` reaches it, and exits non-zero when - any ending is unreachable. - - W73.2 A committed fixture with exactly one unreachable ending makes the checker name that - ending and fail — and the same fixture passes Tier 1 and Tier 2, which is what proves - the check finds something load-time validation cannot. - - W73.3 A choice whose `requirements` no reachable variable state can satisfy is reported - with its node id and choice id — 03 §11's second Tier 3 case. - - W73.4 No registry path invokes the checker. `buildValidatedContentRegistry` is asserted - unchanged in behaviour and timing, so registry construction stays pure and total. - - W73.5 The search is bounded and honest about it: it terminates on every committed campaign, - and distinguishes **proven unreachable** from **not proven reachable within the bound** - rather than reporting the second as the first. - - W73.6 `npm run typecheck`, `npm run lint` and `npm test` pass from `src/engine/`. -- **Out of scope:** balance findings of every kind — dominant strategies, unwinnable economics, - loops — which 12 §15 assigns to a game-side balance harness by name; Tier 3 for the - `simulation` and `world-graph` kinds, whose state spaces are not finite the way an - authored story graph's is; wiring the checker into CI as a required gate, which is a - policy change and wants its own decision; and the Tier 1/2 author-facing linter in the - bullet list above, which is a different tool over checks that already exist. - ---- - -### [x] W74 — Campaign Content Ownership: The Authoring Seam {#w74} - -**Delivers:** Someone writing campaigns in the content repository can install the engine and get -exactly the tools authoring needs — the builders, the portable serializer, the digests — without -the engine's own published stories arriving alongside them. That separation exists in the source -tree today but nothing checks it, so the first time it breaks, it breaks in someone else's build -rather than in this one. - -**Resized 2026-08-19, from five criteria spanning two repositories and a breaking release into -four units that each fit a session.** `W74.2` and `W74.3` assert facts about -[SubZeroDev.Adventures.Content](https://github.com/The-Running-Dev/SubZeroDev.Adventures.Content): -a unit of work in this repository can neither make them true nor honestly tick them, so they move -to [W74c](#w74c) as verified preconditions of the removal they were always guarding. `W74.4` -becomes [W74a](#w74a), and `W74.5` splits into [W74b](#w74b) and [W74c](#w74c) — it was never one -session's work. **`W74.2`–`W74.5` are retired and never reused**; the gap in this unit's numbering -is the record of the split. Two canonical statements still cite `W74.5` by name — -[20 §19](04-core.md#19-published-narrative-authoring) and -[10 §13](02-architecture.md#13-published-narrative-content-ownership) — and W74c owns re-pointing -both. - -- **Spec:** [20 §19](04-core.md#19-published-narrative-authoring) (the subpath is the author-time - contract, the root is the runtime one, and a runtime host must not import authored campaign - source merely to play published portable JSON). -- **Touches:** `src/engine/src/authoring.ts`; `consumer-smoke/smoke.ts` and - `consumer-smoke/package.json`; `.github/workflows/ci.yml`'s consumer-smoke step, if - resolving a second entry point needs one. -- **Depends on:** [W67](#w67). The original line also named "portable format graduation", which - is not a unit id and resolves to nothing in this ledger; `src/engine/src/portable/format.ts` - is shipped and gates nothing here. -- **Status:** Done — closed by - [#301](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/issues/301), all four - criteria ticked. -- **Done when:** - - W74.1 `@the-running-dev/game-engine/authoring` is packed, installable, and exposes only - the documented author-time surface. - - W74.6 `consumer-smoke` resolves `@the-running-dev/game-engine/authoring` **from the packed - tarball**, not from a source link, and imports every value and type the subpath exports. - Deleting one export from `src/engine/src/authoring.ts` fails the smoke build — verified - by deleting one and confirming the failure. - - W74.7 The subpath is closed against published content: a check enumerates its exported names - against a committed sorted list and fails both when a name is missing and when one is - added, and asserts that no published campaign builder or campaign id — any - `bulgaria-*`, `lucifer-*`, `saki-*` or `what-would-lucifer-do*` — is reachable through - it. The shared `buildAdventureCampaign` is not a published campaign and stays. - - W74.8 `npm run typecheck`, `npm run lint` and `npm test` pass from `src/engine/`, and - `install:engine`, `build` and `smoke` pass from `consumer-smoke/`. -- **Out of scope:** removing anything at all — no campaign source, root export, exporter or - `/play/` artifact is deleted here, and `src/engine/package.json`'s version is not bumped; - that is [W74b](#w74b) and [W74c](#w74c). Adventures.Content's own build, which this unit - neither reads nor changes. - ---- - -### [x] W74a — The Bureaucracy Fixture, Frozen Byte-for-Byte {#w74a} - -**Delivers:** The engine keeps one story campaign purely as regression evidence, and it becomes -impossible to change it by accident. Today the same file is both the oracle and a shipped -publication, so an edit made for the story's sake silently moves the baseline everything else is -measured against — and the move only shows up much later, as a replay failure nobody can date. - -- **Spec:** [10 §13](02-architecture.md#13-published-narrative-content-ownership) (a frozen - Bureaucracy campaign may remain inside the engine as story-graph regression evidence only; - it is not a publication source); [20 §19](04-core.md#19-published-narrative-authoring) - (such a fixture is not published and not listed in a manifest); - [W67](#w67) (the corpus and the evidence suites this unit pins). -- **Touches:** `src/engine/src/campaigns/bulgaria-bureaucracy.ts` and - `bulgaria-bureaucracy.bg.ts`; a new freeze assertion beside them; the four - `bulgaria-bureaucracy.*.test.ts` evidence suites; `src/engine/fixtures/replay/`, read only. -- **Depends on:** none. -- **Status:** Done — [PR #333](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/333). -- **Done when:** - - W74a.1 A committed golden pins the built campaign byte-for-byte — the canonical - serialization of `buildBulgariaBureaucracyCampaign()` and its `digestPortableCampaign` — - and any edit to either source file that changes either value fails a named test. - Verified by making a one-character edit and confirming the failure. - - W74a.2 The freeze names itself. The failure message says the campaign is frozen regression - evidence and that changing it invalidates the replay corpus, rather than reporting a bare - digest mismatch that reads like a bug. - - W74a.3 All four evidence suites — replay, determinism, localization, observability — still - run against the frozen campaign, and a test asserts all four files exist, so deleting one - fails rather than silently shrinking the evidence. - - W74a.4 The three `bureaucracy-*` fixture/outcome pairs are still enumerated by prefix from - `fixtures/replay/`, and `.github/workflows/ci.yml`'s release-tag cross-version job still - names `src/campaigns/bulgaria-bureaucracy.replay.test.ts`. - - W74a.5 `npm run typecheck`, `npm run lint` and `npm test` pass from `src/engine/`. -- **Out of scope:** removing Bureaucracy from `scripts/export-campaigns.ts` or from - `site/public/campaigns/` — that is [W74b](#w74b), and while `/play/`'s artifacts are still - in the tree the exporter's output must stay consistent with them. Freezing any other - campaign: the other eight belong to Content and are deleted outright in [W74c](#w74c), - not frozen. - ---- - -### [x] W74b — Retire the Engine's Own Play Surface {#w74b} - -**Delivers:** The engine repository stops carrying a browser game nobody can reach. The route was -removed from the build by [W69](#w69) and Adventures has been the only play surface since, but the -source, the screenshot baselines and the campaign files it used to fetch are all still here — -still type-checked, still tested, still telling anyone who opens the folder that this is where you -go to play. - -- **Spec:** [10 §13](02-architecture.md#13-published-narrative-content-ownership) (the former - in-repository `/play/` route and its campaign artifact directory are superseded, and - Adventures consumes the deployed Content feed rather than Engine-generated campaign files); - and [`13-playable-web-demo.md`](13-playable-web-demo.md), *Succeeded by - SubZeroDev.Adventures*. -- **Touches:** `site/src/play/` in full, including `browser/__screenshots__/`; - `site/public/campaigns/`; `site/vitest.config.ts` and `site/vitest.browser.config.ts`, if - either names a removed path; `src/engine/scripts/export-campaigns.ts` and the - `export:campaigns` script in `src/engine/package.json`. -- **Depends on:** [W74a](#w74a). The freeze must exist before Bureaucracy's exporter entry is - deleted, so the deletion is proven not to have moved the fixture. -- **Status:** Done — [PR #335](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/335). -- **Done when:** - - W74b.1 `site/src/play/` is gone in full, and `npm --prefix site run check` passes — format, - lint, typecheck, unit tests, the real-browser suite, the build and the merge — with no - suite skipped, filtered or renamed to stand in for a deleted one. - - W74b.2 `site/public/campaigns/` is gone, and `site/scripts/verify-build.mjs` and - `site/scripts/verify-merge.mjs` both still pass, which is what proves the built landing - page never referenced it. - - W74b.3 `scripts/export-campaigns.ts` and the `export:campaigns` script are gone, and - `npm run typecheck` from `src/engine/` still covers the remaining scripts through - `tsconfig.scripts.json`. - - W74b.4 Nothing in the repository fetches `campaigns/` or `manifest.json` from a relative - site path any longer; a search across `site/` for both returns nothing. - - W74b.5 The engine's campaign sources and root exports are untouched: `src/index.ts` still - exports all nine builders and `src/engine/package.json`'s version is unchanged. This unit - deletes publication and play artifacts only, so it is not a breaking release. - - W74b.6 `npm run typecheck`, `npm run lint` and `npm test` pass from `src/engine/`; - `npm --prefix site run check` passes; `./build/Test-Documentation.ps1` passes. -- **Out of scope:** deleting campaign sources or root exports and bumping the version — both are - [W74c](#w74c), and both are breaking where this unit is not. Rewriting - [`13-playable-web-demo.md`](13-playable-web-demo.md) or - [`14-game-interface.md`](14-game-interface.md), which already record `/play/` - as superseded; describing the retirement a third time is churn, not reconciliation. - ---- - -### [x] W74c — The Breaking Ownership Release {#w74c} - -**Delivers:** The engine package stops shipping the stories. Someone installing -`@the-running-dev/game-engine` gets an engine — kinds, validation, portable hydration, authoring -primitives — and fetches content from the content feed, which is what every host has actually -been doing since Adventures shipped. The version number finally says so out loud, so a host -knows which upgrade is the one that moves the content. - -- **Spec:** [20 §19](04-core.md#19-published-narrative-authoring) (existing frozen campaigns stay - package-root exports through 0.8.0; the breaking 0.9.0 release removes them from the root, - and the peg is a name to check against `src/engine/package.json` before a bump, not after); - [10 §13](02-architecture.md#13-published-narrative-content-ownership) (published campaign - builders are not package-root API). -- **Touches:** the eight published campaign sources under `src/engine/src/campaigns/`, with their - tests and snapshots; `src/engine/src/index.ts`; `src/engine/src/portable/format.test.ts`; - `src/engine/src/campaigns/story-campaign-expansion.test.ts`; - `src/engine/scripts/demo-cli.ts`; `src/engine/scripts/validate-campaign.ts`; - `src/engine/package.json`; `consumer-smoke/smoke.ts`, if it names a removed export; - `design/20-contract.md` §19 and `design/10-design.md` §13, for the retired `W74.5` citation - only. -- **Depends on:** [W74](#w74), [W74a](#w74a), [W74b](#w74b). W74b is a hard ordering rather than a - preference: `site/src/play/browser-client.test.ts` imports six campaign builders from the - package root, so removing them before that file is deleted breaks `site`'s suite. -- **Status:** Done — [PR #336](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/336). -- **Done when:** - - W74c.1 **The preconditions are verified before anything is deleted, and recorded in the pull - request as commit shas rather than as an assertion.** Adventures.Content publishes all - nine campaigns and its `manifest.json` from its own `scripts/export-content.ts`, invoking - no engine exporter, and its Deploy workflow has succeeded on its current tip. - - W74c.2 At the sha W74c.1 names, the five expanded Bulgaria publications in Content carry - `2.0.0` and 75 endings between them, and Content's `manifest.json` carries a resolution - digest. All three values are quoted in the pull request, and this unit changes none of - them — it is a GameEngine-side deletion, so any movement in them is a signal to stop. - - W74c.3 The eight published campaign sources, their tests and their snapshots are removed, and - their root exports are gone from `src/index.ts`. `bulgaria-bureaucracy.*` stays, exported - from nowhere, and [W74a](#w74a)'s freeze still passes untouched — which is what proves - the removal did not disturb the regression oracle. - - W74c.4 `scripts/validate-campaign.ts` and `scripts/demo-cli.ts` both still run over the - campaigns that remain, and each reports which ones by name. Neither loses a campaign to a - dangling import, and neither is left with an empty set. - - W74c.5 `src/engine/package.json` reads `0.9.0`, and `consumer-smoke` resolves the package - root with no removed name. - - W74c.6 Neither §19 nor §13 cites the retired `W74.5`; both name [W74c](#w74c) instead. That - citation is the only change made to either section — restating §19's version peg now that - it is satisfied belongs to `/reconcile`, not to this unit. - - W74c.7 `npm run typecheck`, `npm run lint` and `npm test` pass from `src/engine/`; - `npm --prefix site run check` passes; `install:engine`, `build` and `smoke` pass from - `consumer-smoke/`; `./build/Test-Documentation.ps1` passes. -- **Out of scope:** publishing or tagging 0.9.0 — the release workflow is the user's to trigger, - and `design/90-decisions.md` records how the last peg was spent by an unrelated - bump. Bumping the engine submodule pin in Adventures or Adventures.Content, which are - separate repositories with their own gates. Removing `fromPortable` or the `Portable*` - types, which Adventures depends on and which are runtime-root surface, not publication. - ---- - -### [x] W75 — Classified Persistence Conflicts {#w75} - -**Delivers:** the one classified adapter failure -[20 §7.2](04-core.md#7-the-session-store-and-the-platform-api) now carves out — a host-branded -lost update surfacing as `concurrent_modification` instead of `storage_failure` — together with -the cache invariant that makes the new message honest. - -**Six of the seven criteria are now satisfied, and the seventh is the validation gate.** This -was written expecting the contract to land first and the code after, which is the order -`design/90-decisions.md` (2026-08-13) argues for. That is not what happened: the branch -carrying the amendment was cut from `d44afde` — the `slice/S1` draft's own commit, opened as -PR #306 — rather than from `main`, so merging the amendment (PR #307) carried the mapping and -its four tests along with it. PR #306 is closed; its commit is on `main` regardless. PR #311 -then closed the two criteria that were left: the cache-restore behaviour on a `submitAction` -conflict (`W75.4`) and the version bump for the added root exports (`W75.6`). - -So this unit **completed** rather than implemented from scratch. `W75.1`–`W75.6` were -satisfied by what landed in `d44afde` and PR #311; `W75.7` was verified 2026-08-19 on `main` -at `7a66da3` — typecheck, lint, 1116 tests across 79 files, `git diff --check`, and -`./build/Test-Documentation.ps1` all clean. Criterion ids are retained as written — they are -never renumbered — so the record of which arrived how stays readable. - -- **Spec:** [20 §7.2](04-core.md#7-the-session-store-and-the-platform-api), - [§12](04-core.md#12-reason-codes-state-changes-messages), - [06 §5.2](06-extensibility.md#52-sessionpersistence-and-profilestore). -- **Depends on:** nothing beyond the shipped session store. -- **Status:** Done — closed by [#308](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/issues/308), all seven criteria ticked. -- **Done when:** - - W75.1 A session write whose adapter throws an exception branded with - `SESSION_PERSISTENCE_CONFLICT` raises `SessionStoreError` with code - `concurrent_modification`; an unbranded throw and a differently branded throw both - still raise `storage_failure`. - - W75.2 `concurrent_modification` is in `BASE_REASON_CODES` with a shipped - `core.reason.*` message, and `20-contract.md` §12's list and growth tally agree with - `reasons.ts`. - - W75.3 The brand is matched on `name`, not by `instanceof`, and a test proves it by - raising a plain object carrying only that `name`. - - W75.4 **A conflict on `submitAction` leaves the store's cache no further ahead than - persistence.** The path mutates a cached record in place before persisting, so the - unit restores or evicts it when the write throws, and a test asserts a subsequent - read returns the pre-conflict state rather than the refused mutation. This is the - invariant 20 §7.2's blockquote states, and it is what makes "refresh and try again" - a promise the store can keep. - - W75.5 The asymmetry is asserted, not assumed: save writes, save reads and session reads - continue to raise `storage_failure` for a branded exception, with - [#226](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/issues/226) named as - the owner of the save-side race. - - W75.6 `SESSION_PERSISTENCE_CONFLICT` and `SessionPersistenceConflict` are root exports, - the package version is bumped for the added surface, and `consumer-smoke` resolves - both. - - W75.7 `npm run typecheck`, `npm run lint` and `npm test` pass from `src/engine/`, and - `./build/Test-Documentation.ps1` passes. - ---- - -### [x] W76 — A Folded Registry Keeps Its Resolution {#w76} - -**Delivers:** A game assembled from several content packs can still say which mix of content it -was played against — on the one route a host is actually able to use. Today that route quietly -loses the record, so every host has to remember to put it back by hand, and a host that forgets -loses it with no symptom at all: the game plays perfectly and simply stops being able to say -what it was. - -**Backfilled from [#315](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/issues/315), -which was authored straight onto the tracker.** The issue carried the full slice — narrative, -ten criteria, out-of-scope list — but no `W76` section ever existed in this ledger, so its pin -cited a section that could not be read. Recorded here so the pin resolves and the ledger stays -the origin; the criteria are transcribed as written and their ids are retained. - -- **Spec:** [11 §3](11-content-packs.md#3-resolution), - [§4](11-content-packs.md#4-the-one-change-to-contentregistry), - [§7](11-content-packs.md#7-validation); - [04 §10.1](04-core.md#101-content-registry), - [§11](04-core.md#11-tiered-validation), - [§12](04-core.md#12-reason-codes-state-changes-messages). -- **Touches:** `src/engine/src/core/registry/packs.ts` and `types.ts`; - `src/engine/src/core/validation/tiered.ts`; `src/engine/src/index.ts`; - `src/engine/src/campaigns/stable-life-packs.ts` and `stable-life-packs.test.ts`; - `src/engine/package.json`. -- **Depends on:** nothing beyond the shipped pack resolution. -- **Status:** Done — [PR #337](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/337), - closing [#315](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/issues/315). All ten - criteria verified against `main` and ticked 2026-08-19. -- **Done when:** - - W76.1 One call takes an ordered pack set and returns a validated, frozen registry whose - `resolution` equals `computeResolutionId` over that same set, with no caller reattaching - anything. Asserted for both a one-pack set and a two-pack set, and the two values differ. - - W76.2 That same call still merges each used kind's own `.reason.*` messages into the - frozen table: a rejection raised through the resulting registry renders the kind's - message, not a bare key. - - W76.3 The new path fails at both stages and reports which: a pack set violating an 11 §7 - Tier 1 rule and a pack set that resolves but whose campaign fails 04 §11 Tier 1 each - return `ok: false` carrying that stage's errors and no registry. One committed fixture - per stage, with the error counts stated. - - W76.4 Tier 2 warnings from both stages reach the caller in one result: a - `pack_override_unexpected` warning and a kind-supplied warning appear together, so - folding swallows neither. - - W76.5 The no-pack route is unchanged. `buildContentRegistry` and the existing - `buildValidatedContentRegistry` signature still produce a registry with - `resolution === undefined`, and every existing call site compiles untouched. - - W76.6 Which string table each campaign is validated against on the folded path is pinned by - a test, not left implicit: a campaign whose `titleKey` resolves nowhere in the resolved - set fails with `missing_string_key`, and a campaign whose `titleKey` resolves only - through a later pack's contribution has its outcome asserted either way. If that outcome - contradicts 04 §11's per-campaign scoping, stop and report it rather than picking a - reading — that is `/contract`'s call. - - W76.7 `stable-life-packs.ts`'s `resolveStableLifeRegistry` — which already consolidated the - fold-then-validate-then-reattach sequence out of its three former callers — is rewritten - to call the new shared call instead of reimplementing that sequence itself, and the - suite passes through the new path with no local helper reconstructing what the call now - returns. - - W76.8 Proven by reverting: with the `resolution` assignment removed from the new path, a - named test goes red. - - W76.9 Whatever shape is chosen — an optional parameter or a pack-aware sibling — the surface - a host must call is reachable from the package root, the package version is bumped for - the added or widened surface, and `consumer-smoke` resolves it through the packed tarball - rather than a source link. - - W76.10 `npm run typecheck`, `npm run lint` and `npm test` pass from `src/engine/`, and - `./build/Test-Documentation.ps1` passes. -- **Out of scope:** making `resolution` required on `ContentRegistry`; changing `resolvePacks`' - fold rules or `computeResolutionId`'s digest; the engine reading `resolution`; pack - discovery, distribution, and loading from disk; W72's Bulgarian content volume; the seven - Adventures findings, which route to `/contract`. - -### Content Tooling — Slicing the Remaining Bullets - -**W73 took the one bullet that was fully specified.** The five above are still open, and they -are now the largest named body of unbuilt work in this repository: the runtime has three kinds, -nine story-graph campaigns, a second locale and a culture pack, and an author has exactly one -command to point at any of it. Four units below take four of the five bullets — two of them -only in half, because the other half of each is blocked on something a slice may not invent. -What is deliberately *not* sliced, and why, is stated in that unit's **Out of scope** rather -than left as an absence. - -### [x] W77 — Tier 1 and Tier 2 as an Author-Facing Check {#w77} - -**Delivers:** Someone writing a campaign can ask what is wrong with it before anyone plays it, -and get back a list they can act on — naming the node, key or id at fault. Today those checks -already run on every campaign, but the only way to see what they found is to be a program that -caught a registry build failing; there is no way for the person who wrote the content to just -ask. - -**The premise under test is [`02-architecture.md`](02-architecture.md) §9's N10** — "the engine -validates AI-authored content" is a safety property only if the validation is reachable by -whoever authored it. It currently is not. The second thing this unit finds out is whether -`ValidationError` as shipped is *legible*: `path` is optional and `details` is sparse, so if a -real finding cannot be located from what the type carries, that is a contract finding to report -rather than paper over. -- **Spec:** [04 §11](04-core.md#11-tiered-validation), - [§12](04-core.md#12-reason-codes-state-changes-messages), - [§17](04-core.md#17-identifier-conventions); - [03 §11](03-story-graph-kind.md#11-validation-story-graph-specific); - [`02-architecture.md`](02-architecture.md) §9, §9.2. -- **Touches:** a new checker under `src/engine/scripts/`, alongside `validate-campaign.ts` — - the placement and rationale [`02-architecture.md`](02-architecture.md) §9.2 already - states; `src/engine/package.json` scripts. `src/engine/src/core/validation/tiered.ts` is - **read, not modified**. -- **Depends on:** nothing. -- **Status:** Done — [PR #338](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/338), all seven criteria ticked. -- **Done when:** - - W77.1 Running the check over a named committed campaign reports every Tier 1 error and - Tier 2 warning `buildValidatedContentRegistry` produces for it, each rendered with its - `code`, its message resolved against the string table — never a bare `messageKey` — and - its `path` when one is set. - - W77.2 The exit code carries the tier split: a campaign with any Tier 1 error exits - non-zero, a campaign with only Tier 2 warnings exits zero and still prints them. - - W77.3 Both committed broken fixtures — `bulgaria-bureaucracy.broken.ts` and - `stable-life.broken.ts` — produce a named, non-empty finding list, with the exact error - and warning counts stated in the test rather than asserted as "more than zero." - - W77.4 Coverage is not maintained by remembering. A test enumerates - `src/engine/src/campaigns/*.ts` and requires every module exporting a campaign builder - to be either in the checker's catalogue or in an explicit exclusion list carrying a - stated reason — a module in neither fails the test. So a campaign added without - registering it turns a suite red instead of going silently unchecked, and the - deliberately-broken and locale fixtures stay excluded on the record rather than by - omission. Three scripts (`demo-cli.ts`, `export-campaigns.ts`, `validate-campaign.ts`) - each hand-maintain their own list today; this unit introduces the shared catalogue - rather than a fourth list. - - W77.5 No registry path invokes the checker and no new rule is added. - `buildValidatedContentRegistry` is asserted unchanged in behaviour, and every finding - the checker prints is one the registry path already produces. - - W77.6 Every finding the two broken fixtures produce carries a location — a `path`, or a - `details` entry naming one — asserted finding-by-finding rather than in aggregate. Any - that does not is reported as a contract gap with the campaign and check named, and is - **not** worked around by having the checker re-derive the location itself, which would - put a second validation implementation in the tree. - - W77.7 `npm run typecheck`, `npm run lint` and `npm test` pass from `src/engine/`. -- **Out of scope:** Tier 3 reachability, which is [W73](#w73)'s `validate-campaign` and stays a - separate command; wiring either checker into CI as a required gate, which W73 already - named as a policy change wanting its own decision; migrating `demo-cli.ts` and - `export-campaigns.ts` onto the shared catalogue; adding, tightening or relaxing any - validation rule — this unit surfaces the checks that exist and authors none. - -### [x] W78 — Localization Coverage and String Extraction {#w78} - -**Delivers:** A translator can be handed the exact list of lines a campaign needs, and a -maintainer can see which campaigns are translated, which are half-translated and which are not -translated at all. Today the only way to discover a translation is incomplete is to try to -build it and read the failure — one missing line at a time, with no way to ask how many are -left. - -[W60](#w60) put string-table extraction, translation workflow and coverage reporting out of its -own scope by name and routed them here. [04 §10.1](04-core.md#101-content-registry) says -additional locales are "string tables plus tooling, no type change"; W60 proved the string-table -half for one campaign, and this is the tooling half, across a shelf where exactly one campaign -of nine has a second locale. -- **Spec:** [04 §10.1](04-core.md#101-content-registry), - [§11](04-core.md#11-tiered-validation) (`missing_string_key`), - [§17](04-core.md#17-identifier-conventions); [09 §5](09-clients.md#5-reason-codes-and-messages). -- **Touches:** a new script under `src/engine/scripts/`; `src/engine/package.json` scripts; - a second-locale fixture under `src/engine/src/campaigns/`. - `src/engine/src/core/localization/` and `src/engine/src/core/registry/` are **read, not - modified**. -- **Depends on:** [W77](#w77), for the shared campaign catalogue. -- **Status:** Done — [issue #326](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/issues/326). -- **Done when:** - - W78.1 For a named campaign the tool emits the complete, sorted set of `LocKey`s its built - campaign requires — the keys `BuiltCampaign.strings` carries — in a form a translator - can fill in. Running it twice produces byte-identical output. - - W78.2 For a campaign with a second-locale build the tool reports, per locale: keys present - in the reference locale and absent here, keys present here and in no reference locale, - and the resulting covered/total count. Asserted against `bulgaria-bureaucracy` and its - `.bg` build, which is complete, and against a fixture with exactly one key removed, - whose report names that key. - - W78.3 An **untranslated** key — present in both tables with byte-identical text — is - counted and reported separately from a missing one. This is the case Tier 1 cannot - catch at all, since the key resolves; stated as a count and asserted on a fixture - carrying exactly one. - - W78.4 Across the whole catalogue the tool reports how many campaigns have a second locale - and which, so the shelf-wide gap is a number rather than an absence nobody counted. - - W78.5 The tool writes only files it was explicitly asked to write: a run against the - committed shelf leaves `git status --short` clean. - - W78.6 `npm run typecheck`, `npm run lint` and `npm test` pass from `src/engine/`. -- **Out of scope:** translating anything; a translation-memory, vendor or review workflow; - per-locale content packs, which [11 §8](11-content-packs.md#8-what-is-deferred) defers by - name; locale-aware pluralization and formatting, which - [09 §9](09-clients.md#9-deferred) leaves to a client; making coverage a CI gate, which is - the same policy decision W77 declines to take unilaterally. - -### [x] W79 — What Changed Between Two Resolutions {#w79} - -**Delivers:** When two saves say they were played against different content, someone can find -out what was actually different. The platform can already prove two content resolutions are not -the same — that is what the resolution id is for — but it cannot say how, so every question -about a divergence ends at "the digests differ," which is exactly where the interesting part -starts. - -[W58](#w58) built `computeResolutionId` and [W71](#w71) proved two real resolutions of -`stable-life` produce different ids. Neither can answer what moved between them, and -[11 §6](11-content-packs.md#6-identity-and-why-determinism-needs-it) is deliberately a digest — -it is *meant* to be opaque, which is precisely why the readable view has to be tooling beside -it rather than a widening of it. -- **Spec:** [11 §3](11-content-packs.md#3-resolution), - [§4](11-content-packs.md#4-the-one-change-to-contentregistry), - [§6](11-content-packs.md#6-identity-and-why-determinism-needs-it); - [04 §10.1](04-core.md#101-content-registry). -- **Touches:** a new script under `src/engine/scripts/`; `src/engine/package.json` scripts. - `src/engine/src/core/registry/packs.ts` and `src/engine/src/campaigns/stable-life-packs.ts` - are **read, not modified**. -- **Depends on:** [W58](#w58), [W71](#w71) — both done. -- **Status:** Done — [issue #327](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/issues/327). -- **Done when:** - - W79.1 Given two ordered pack sets the tool reports both `ResolutionId`s and, when they - differ, an itemized difference following [11 §3](11-content-packs.md#3-resolution)'s two - replacement rules: campaigns replaced wholesale by id, and string keys added, removed or - changed. Asserted on `[base]` versus `[base, bulgaria]` with the expected item counts - stated in the test. - - W79.2 A key whose *value* changed is reported as changed, distinct from added and removed. - This is the case a set difference alone misses entirely, and it is what a culture pack - is almost wholly made of — W71's pack overrides text at existing keys. - - W79.3 Two identical pack sets produce equal ids and an empty difference, and the tool says - so explicitly rather than printing nothing. - - W79.4 Output is deterministic and independent of object key order: the same two sets produce - byte-identical output across runs, and reordering a pack's fields in source does not - change it. - - W79.5 `resolvePacks` and `computeResolutionId` are unchanged and the tool only reads them — - asserted by no file under `src/engine/src/core/` changing. - - W79.6 `npm run typecheck`, `npm run lint` and `npm test` pass from `src/engine/`. -- **Out of scope:** **balancing tools — the other half of this workstream's third bullet.** They - need the simulation harness the provisional-numbers debt - ([issue #267](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/issues/267)) is - itself waiting on, and neither the harness's shape nor what a balance finding *is* is - specified anywhere; slicing it now would mean inventing both. Also out: diffing saves or - runtime state; diffing portable campaign JSON as shipped to hosts, which belongs to - Adventures.Content ([W74](#w74)); pack discovery and loading from disk - ([11 §8](11-content-packs.md#8-what-is-deferred)). - -### [x] W80 — Seeing a Story Graph {#w80} - -**Delivers:** Anyone looking at an authored adventure can see its shape — where it starts, where -it branches, which endings hang off which route — instead of reconstructing it from several -hundred lines of node ids. The shelf is nine campaigns and, across the five expanded Bulgaria -publications alone, seventy-five endings ([W74.3](#w74)); there is currently no way to look at -any of it. - -- **Spec:** [03 §3](03-story-graph-kind.md#3-nodes--the-single-content-type-n7), - [§4](03-story-graph-kind.md#4-choices-and-transitions), - [§8.5](03-story-graph-kind.md#85-terminal-identity); - [`02-architecture.md`](02-architecture.md) §9.1, §9.2 — the three tests that make this - tooling rather than a fourth layer apply here unchanged. -- **Touches:** a new script under `src/engine/scripts/`; `src/engine/package.json` scripts. -- **Depends on:** [W77](#w77), for the shared campaign catalogue and for the Tier 2 warnings - W80.4 cross-checks against. -- **Status:** Done — [issue #328](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/issues/328). -- **Done when:** - - W80.1 For a named story-graph campaign the tool emits a text graph — Mermaid, which needs no - dependency to write — with one vertex per node, edges labelled by choice id, and - ending nodes visually distinguished from `choice`, `auto` and `random` nodes. Every node - and every choice in the campaign appears exactly once, asserted by count against the - built campaign. - - W80.2 Output is deterministic: the same campaign produces byte-identical output across runs, - with vertices and edges in a stated canonical order rather than object-iteration order. - - W80.3 The largest committed campaign renders without truncation, and the tool prints its - node, choice and ending counts beside the graph so a reader can check the picture - against the numbers. - - W80.4 A node no edge reaches is marked as such, and the set it marks equals the set of nodes - [W77](#w77) reports a Tier 2 unreachable warning for on the same campaign — the same - finding in two forms, never two different answers. If they disagree, that is the - finding: report it rather than reconciling it in the renderer. - - W80.5 The tool reads the built campaign only. No file under `src/engine/src/` changes, and - nothing in the registry path imports it. - - W80.6 `npm run typecheck`, `npm run lint` and `npm test` pass from `src/engine/`. -- **Out of scope:** **the visual node editor — the other half of this workstream's second - bullet.** It writes content rather than reading it, so it needs decisions this repository - has not taken: where it lives, what it emits, and whether authored source or built - campaign is its file format. [`02-architecture.md`](02-architecture.md) §9.1's "a campaign - is free not to use one" test is about builders, and an editor that owns the file is a - different question. Route to `/design` before it is sliced. Also out: graphing the - `simulation` and `world-graph` kinds, whose state is not an authored graph; rendering to - an image format; embedding the graph in the documentation site. - -> **The fifth bullet — authoring assistants — is not sliced, and the reason is a decision, not a -> size.** [`02-architecture.md`](02-architecture.md) §9 settles the *boundary* (AI authors -> campaigns, never kinds; its output is data and is validated identically), which is the part -> that needed settling. What it does not settle is anything an implementation needs: which model -> or service, which is a new dependency and so needs a decision-log entry naming what was -> rejected; whether the assistant lives in this repository at all now that -> [SubZeroDev.Adventures.Content](https://github.com/The-Running-Dev/SubZeroDev.Adventures.Content) -> owns published narrative content ([W74](#w74)); and what it would be measured against. W77 is -> its real precondition regardless — an assistant whose output is checked by a validator no -> human can run is the same gap one level up. - -### Correctness Debt — The Tick Pipeline Runs Twenty Systems and Implements Fifteen - -**The world-graph kind's tick pipeline is the largest thing in this repository that is specified -in full and built in part.** [12 §4](12-world-graph-kind.md#4-the-turn-is-a-tick-batch) fixes -twenty tick systems in a normative order, and `src/engine/src/kinds/world-graph/tick/pipeline.ts` -registers and runs all twenty — the ordering, the `processingTick` guard and the finalize-once -assertion are genuine and tested. **Five of the twenty do not do what the contract describes**, -recorded as a known-and-retained gap in `design/90-decisions.md` since 2026-08-05 — named -individually there rather than in this repository's public register, with the stated revisit -condition *each system's own build unit lands*. W81–W85 are those build units, one per system. - -**Re-read against the source while slicing, the gap is seven systems rather than five**, and the -two extra ones decide how these units are cut. `task-generate` (system 9) pushes only `clean` and -`service` candidates, and `staff-work` (system 11) branches on only those two kinds — so `build` -and `restock` sit in the task-kind comparator and in `StaffTaskType`, reachable by nothing. -Systems 12 and 13 therefore cannot be built alone: each needs its own half of systems 9 and 11, -which is why W81 and W82 name three systems apiece. **The register's list is not wrong about the -five; it is silent about the two**, and correcting it is folded into W81 rather than left to be -rediscovered. - -**No unit here introduces a signature.** Every runtime and content type these systems need already -ships — `ConstructionSite`, `Building.wear` and `.cleanliness`, `Incident`, `Alert`, -`ServiceProduct.capacity`/`initialUnits`/`restockTaskPriority`, -`BuildingDefinition.constructionWork`/`constructionTaskPriority`, `IncidentDefinition`'s roll -scope, chance, weight and cooldown, and the world-graph `AchievementDefinition`. What is missing -is the code between them, and the content to exercise it: `world-graph-mvp.ts` declares -`constructionWork: 0`, `capacity: null` and `rollChanceBasisPoints: 0` throughout and carries no -achievements or policies — deliberately, so W49's fixture stayed small. Each unit below therefore -extends that fixture as its content half, which is also what keeps it a vertical slice rather than -a system rewrite. - -**Ordered so the determinism risk is exercised earliest.** The kind rests on batch invariance -([12 §5](12-world-graph-kind.md#5-batch-invariance--and-the-two-seam-changes-it-forced)) — that -`advance_ticks n` reaches the same state as any split of it — and on the draw discipline in -[§9](12-world-graph-kind.md#9-determinism-beyond-the-seed). W81 goes first because construction is -the first system to carry work *across* ticks from a player action as its origin, which is where -an invariance defect would first show. W84 sits where it does because it is the only one of the -five that opens a new RNG draw site, and it needs W83's meters to have something to act on. - -**The shared `SystemPipeline` ([issue #270](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/issues/270)) -is deliberately not sliced here.** Its recorded trigger — a second tick-driven kind — has fired, -but extracting a shared runner from a pipeline where a quarter of the systems are stubs would -extract the shape of the stubs. It becomes sliceable once W85 lands, and it is better for the -wait: five more real systems is five more constraints on what the abstraction has to carry. - -### [x] W81 — Construction Finishes What `build` Starts {#w81} - -**Delivers:** Someone laying out a resort can put up a structure that takes time to build, watch -their staff work on it, and then use it. Today starting one is a dead end — the site appears, no -worker is ever sent to it, and it never becomes a building, so a resort is limited forever to -whatever the scenario placed at the start plus whatever happens to cost nothing to construct. - -The `build` action already opens the site and reserves the building and queue ids it will complete -into, and [12 §13](12-world-graph-kind.md#13-statechange-at-batch-grain) already declares both -audit rows it may write. Everything after the action is missing: the `build` task kind is never -generated, never given effort, and never applied. -- **Spec:** [12 §4](12-world-graph-kind.md#4-the-turn-is-a-tick-batch) §4.11, §4.13, §4.14; - [§5](12-world-graph-kind.md#5-batch-invariance--and-the-two-seam-changes-it-forced); - [§9](12-world-graph-kind.md#9-determinism-beyond-the-seed); - [§12](12-world-graph-kind.md#12-events); - [§13](12-world-graph-kind.md#13-statechange-at-batch-grain); - [04 §12](04-core.md#12-reason-codes-state-changes-messages). -- **Touches:** `src/engine/src/kinds/world-graph/tick/pipeline.ts`; - `src/engine/src/kinds/world-graph/kind.ts` for the two event names; - `src/engine/src/kinds/world-graph/reasons.ts` if an audit reason is added; - `src/engine/src/campaigns/world-graph-mvp.ts`; a new fixture pair under - `src/engine/fixtures/replay/`. -- **Depends on:** [W45](#w45), [W46](#w46), [W47](#w47) — all done; they shipped the reducer, the - construction-site state and the comparator registry. -- **Status:** Done — [PR #345](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/345). -- **Done when:** - - W81.1 A building definition declaring construction work, placed through `build`, opens a site - whose remaining work falls by the assigned staff member's `build` effort per tick on every - tick that member is at the site, clamped once at zero. The test states the exact tick on - which it reaches zero, computed from the declared work and rate, not a bound. - - W81.2 On completion the building and its queue carry the ids the site reserved, asserted in a - test where another entity is allocated between the `build` action and the completion tick - — so completion timing cannot renumber a later entity. - - W81.3 Completion removes the site, materializes the definition's defaults (status, wear, - cleanliness, prices, and the declared initial units), increments the completed-buildings - counter, and increments the map revision the path caches are keyed on. - - W81.4 System 9 generates one `build` candidate per site, with effort equal to the site's - remaining work and priority taken from the building definition's construction task - priority and from nowhere else. A staff role whose supported task kinds omit `build` is - never assigned one, asserted with a cleaner-only roster that leaves the site untouched. - - W81.5 `advance_ticks n` across the whole construction span serializes byte-identically to any - split of that batch, asserted for at least one split falling strictly inside the - construction rather than only at its ends. - - W81.6 `construction.progressed` and `construction.completed` are emitted at the sites - [12 §12](12-world-graph-kind.md#12-events) names, declared in `Kind.eventNames`, and both - rows' status moves to `delivered` in the same change. Dropping every event still leaves - `serialize()` identical ([05 §2](05-observability.md#2-the-determinism-rule)). - - W81.7 The `advance_ticks` result carries the batch-grain existence row for the completed - building, with a reason registered in the kind's vocabulary and a message resolvable - through the string table. If the contract names no reason for completion, that is a - finding reported in the pull request, not a code invented quietly. - - W81.8 The two committed world-graph replay outcomes are unchanged byte-for-byte — the MVP - campaign's only building declares zero construction work, so nothing existing enters this - path — and the new behaviour is proved by a new committed fixture. Any outcome that does - move is recorded as an intended change per - [07 §7](07-replay.md#7-intended-change-versus-regression), naming the fields, rather than - re-recorded silently. - - W81.9 The known-and-retained tick-system entry in `design/90-decisions.md` is edited in the - same pull request: system 12 leaves the list, and the two partial systems this unit found - (9 and 11) are named in it. The standing obligation that entry already carries has been - walked past twice before; this criterion is what makes it a gate rather than a sentence. - - W81.10 `npm run typecheck`, `npm run lint` and `npm test` pass from `src/engine/`. -- **Out of scope:** demolishing or refunding a site that is still under construction — `demolish` - targets a building, and whether a site is a legal target is a contract question rather than - a slice's; system 13's restock and operational status, which is [W82](#w82); scenery, which - has no construction path at all; the shared `SystemPipeline` - ([issue #270](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/issues/270)). - -### [x] W82 — A Kiosk That Ran Out Can Be Refilled {#w82} - -**Delivers:** A stall that sells a limited number of things can be restocked by staff, so running -out is a setback rather than the permanent end of that building's usefulness. Today the running-out -half already works — a guest is refused and stops queueing when the shelf is empty — and there is -no way to fill it again, which makes finite stock a trap no author would use on purpose. - -The consuming side landed with W47: service refuses at zero units and an empty building stops being -a candidate at all. What never runs is the refilling side, which is systems 9, 11 and 13 together — -and system 13 also owns the non-wear operational status changes that no other system may make. -- **Spec:** [12 §4](12-world-graph-kind.md#4-the-turn-is-a-tick-batch) §4.11, §4.13, §4.15; - [§10](12-world-graph-kind.md#10-projection); - [§12](12-world-graph-kind.md#12-events); - [§13](12-world-graph-kind.md#13-statechange-at-batch-grain). -- **Touches:** `src/engine/src/kinds/world-graph/tick/pipeline.ts`; - `src/engine/src/kinds/world-graph/kind.ts`; `src/engine/src/campaigns/world-graph-mvp.ts`; - a new fixture pair under `src/engine/fixtures/replay/`. -- **Depends on:** [W81](#w81), for the systems 9 and 11 task-kind branches it establishes. -- **Status:** Done — [PR #347](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/347). -- **Done when:** - - W82.1 A service product declaring initial units and a capacity is served down to zero, a - restock candidate is generated with effort equal to the missing units and the priority the - product declares, a staff member with the `restock` kind carries it out, and the building - serves again — asserted as one continuous tick sequence with the serving, empty and - refilled ticks each named. - - W82.2 Restock never exceeds capacity, and a product whose units are `null` is infinite: it - generates no candidate and its inventory is never written. - - W82.3 A product's unit cost is recognized exactly once, at service, and system 13 does not - recognize it again — asserted by cash and expense totals across a full stock-out and - refill cycle, stated as exact figures. - - W82.4 A building definition with no restock source, and a decorative one, are honest no-ops: - neither generates a candidate nor writes state, asserted rather than assumed. - - W82.5 System 13 emits `building.status.changed` and the batch-grain status row only when a - public status actually changes, and never applies cleanliness or wear — that is - [W83](#w83)'s system, and a test asserts system 13 leaves both meters untouched. - - W82.6 `advance_ticks n` serializes byte-identically to any split of that batch across a - stock-out and refill. - - W82.7 The two committed world-graph replay outcomes are unchanged byte-for-byte — the MVP - campaign declares no capacity — and the new behaviour is proved by a new committed - fixture; any outcome that does move follows - [07 §7](07-replay.md#7-intended-change-versus-regression). - - W82.8 `npm run typecheck`, `npm run lint` and `npm test` pass from `src/engine/`. -- **Out of scope:** production operations other than service and restock, which - [12 §4](12-world-graph-kind.md#4-the-turn-is-a-tick-batch) §4.15 marks post-MVP no-ops by - name; supply chains, warehouses or delivery; pricing behaviour, which `set_price` already - owns; cleanliness and wear ([W83](#w83)). - -### [x] W83 — Buildings Get Dirty, Wear Out, and Break {#w83} - -**Delivers:** A resort that is left unattended degrades, and one that is looked after does not. -Buildings get dirty from use and from litter, wear down over time, and eventually break and stop -serving — which is the whole reason a player hires cleaners and the reason quality affects where -guests choose to go. Today cleanliness moves only from unresolved litter, and wear never moves at -all, so a resort is exactly as good on its thousandth tick as on its first. - -This is the system the register singles out as the one most likely to be mistaken for finished: -it looks implemented, applies one of its five delta sources, and never touches the meter the -system is half named after. -- **Spec:** [12 §4](12-world-graph-kind.md#4-the-turn-is-a-tick-batch) §4.16, and §4.2 for the - typed scratch deltas systems 1, 4 and 11 hand it; - [§10](12-world-graph-kind.md#10-projection); - [§12](12-world-graph-kind.md#12-events); - [§13](12-world-graph-kind.md#13-statechange-at-batch-grain). -- **Touches:** `src/engine/src/kinds/world-graph/tick/pipeline.ts`; - `src/engine/src/kinds/world-graph/tick/scratch.ts`; - `src/engine/src/kinds/world-graph/kind.ts`; `src/engine/src/campaigns/world-graph-mvp.ts`; - a new fixture pair under `src/engine/fixtures/replay/`. -- **Depends on:** nothing — it shares no code path with [W81](#w81) or [W82](#w82) and may be - taken before either. It is placed here because [W84](#w84) depends on it. -- **Status:** Done — [PR #348](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/348). -- **Done when:** - - W83.1 For one building receiving deltas from every source in the same tick, the five sources - are applied in the contract's stated order — service, litter, incident, staff, policy — - summed, then clamped once to 0..100. Asserted with a case where applying the same set in a - different order, or clamping between sources, would give a different result, so the test - distinguishes the rule from an equivalent-looking one. - - W83.2 Wear moves, and wear reaching zero changes an open or closed building to broken; a - broken building serves nobody and is not a candidate. Cleanliness reaching zero never - closes a building on its own — asserted as a negative case, since that is the rule most - likely to be added by intuition. - - W83.3 Cleaning increments the cleaned-litter counter by the amount actually removed, - independently of any definition effect, and an incident amount reaching zero resolves the - occurrence with its transition effects run exactly once — asserted by a repeated tick that - must not run them twice. - - W83.4 `building.meter.changed` is emitted and declared; the batch-grain audit carries status - transitions and does not carry per-tick meter steps, asserted by the returned row count - over a long batch. - - W83.5 `advance_ticks n` serializes byte-identically to any split of that batch across a - degradation-to-broken sequence. - - W83.6 The two committed world-graph replay outcomes either stay byte-identical or the change - is recorded as intended per [07 §7](07-replay.md#7-intended-change-versus-regression) with - the differing fields named in the pull request. Unlike [W81](#w81) and [W82](#w82), this - one is not predicted either way: the MVP campaign already has a litter incident, so this - unit may legitimately move an existing outcome. - - W83.7 `npm run typecheck`, `npm run lint` and `npm test` pass from `src/engine/`. -- **Out of scope:** repair as a player action or a staff task kind — the contract's task kinds are - service, clean, restock and build, and adding a fifth is a contract amendment; balance values - for the drift rates, which are content and belong with Sun Trap; guest opinion changes driven - by quality, which the utility model already reads. - -### [x] W84 — Incidents That Happen On Their Own {#w84} - -**Delivers:** Things go wrong in the resort without the player causing them — a breakdown, a -spill, a security problem — and the player has to deal with them. Today the only incident that can -ever occur is litter dropped by a guest being served: everything an author writes about weather, -fires, breakdowns or security is content the game will never use. - -This is the one unit of the five that adds a new random draw, so it is where the determinism rules -get their real test: one handle per system per tick, a draw for every declared scope whether or not -it succeeds, and a rejected candidate consuming nothing. -- **Spec:** [12 §4](12-world-graph-kind.md#4-the-turn-is-a-tick-batch) §4.18; - [§9](12-world-graph-kind.md#9-determinism-beyond-the-seed); - [§12](12-world-graph-kind.md#12-events); - [§13](12-world-graph-kind.md#13-statechange-at-batch-grain); - [04 §8](04-core.md#8-randomness). -- **Touches:** `src/engine/src/kinds/world-graph/tick/pipeline.ts`; - `src/engine/src/kinds/world-graph/tick/random.ts`; - `src/engine/src/kinds/world-graph/kind.ts`; `src/engine/src/campaigns/world-graph-mvp.ts`; - a new fixture pair under `src/engine/fixtures/replay/`. -- **Depends on:** [W83](#w83), so a rolled incident has meters to act on and a resolution path that - is not litter-only. -- **Status:** Done — [PR #360](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/360). -- **Done when:** - - W84.1 An eligible definition rolls against its declared chance from the tick's incidents - handle, and a successful roll allocates an occurrence with its start effects applied - before system 17 runs — asserted with a seed where the roll succeeds and a seed where it - does not, both stated. - - W84.2 Scopes are visited world, then zone id, then building id, and every declared scope - consumes its draw whether or not it produces an occurrence — asserted by a case where a - scope with no eligible definition changes nothing and a *later* scope's outcome is - nonetheless identical to a run where that scope was eligible and failed. That is the - property a "skip empty scopes" implementation silently breaks. - - W84.3 An active occurrence blocks its definition in the same scope, and so does a retained - one still inside its cooldown; the block lifts on the exact tick the cooldown ends, stated - as a tick number. - - W84.4 An occurrence resolves when its resolution condition becomes true as well as when its - expiry passes, and its resolved tick is written before its resolve effects run, so it is - retained but not active for the rest of that list. - - W84.5 An incident started by an effect in an earlier system is not rolled again in the same - tick, asserted against the existing guest-litter path. - - W84.6 `incident.raised` is emitted and declared, alongside the already-delivered - `incident.resolved`. - - W84.7 `advance_ticks n` serializes byte-identically to any split of that batch across a batch - containing at least one successful and one failed roll — the case where a per-call handle, - rather than a per-tick one, would diverge. - - W84.8 The two committed world-graph replay outcomes are unchanged byte-for-byte — the MVP - campaign's only incident declares a zero roll chance and zero weight — and the new - behaviour is proved by a new committed fixture; any outcome that does move follows - [07 §7](07-replay.md#7-intended-change-versus-regression). - - W84.9 `npm run typecheck`, `npm run lint` and `npm test` pass from `src/engine/`. -- **Out of scope:** authored incident chains or incidents that spawn incidents; guest-visible - narrative around an incident, which is a client concern; alerts raised for an active - incident, which is [W85](#w85); balance values for chances and cooldowns, which are content. - -### [x] W85 — The Resort Tells You What Needs Attention {#w85} - -**Delivers:** A player is told when something needs them — a building has broken, an incident is -running, the scenario has resolved — instead of having to notice it in the numbers; and the -achievements a campaign author writes actually unlock. Today the alert list is a feature the -player can dismiss from and that nothing ever puts anything into, and a world-graph campaign's -achievements can never be earned no matter what the player does. - -This is the last of the five stub systems, and the only one that reaches outside the tick: an -unlocked achievement mirrors to the player's profile after the whole action succeeds, which is -core behaviour this kind has never exercised. -- **Spec:** [12 §4](12-world-graph-kind.md#4-the-turn-is-a-tick-batch) §4.21; - [§8](12-world-graph-kind.md#8-status-win-loss-and-terminal-identity); - [§10](12-world-graph-kind.md#10-projection); - [§12](12-world-graph-kind.md#12-events); - [§13](12-world-graph-kind.md#13-statechange-at-batch-grain); - [04 §7.1](04-core.md#71-the-profile-store); - [§12](04-core.md#12-reason-codes-state-changes-messages). -- **Touches:** `src/engine/src/kinds/world-graph/tick/pipeline.ts`; - `src/engine/src/kinds/world-graph/kind.ts`; - `src/engine/src/kinds/world-graph/reasons.ts` for the kind-owned alert strings; - `src/engine/src/campaigns/world-graph-mvp.ts`; a new fixture pair under - `src/engine/fixtures/replay/`. -- **Depends on:** [W83](#w83) — the broken-building alert family has no source at all until wear - can reach zero. [W84](#w84) is not required, since guest litter already raises incidents, - but is sequenced ahead so the incident family is proved against an incident the player did - not cause. -- **Status:** Done — [PR #361](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/361). -- **Done when:** - - W85.1 Still-locked achievements are evaluated by definition id against post-resolution state, - an unlock writes the achievement's existence row with the core `achievement_unlocked` - reason, and the profile mirror happens only after the whole action succeeds — asserted by - a run whose action is refused afterwards leaving the profile untouched. - - W85.2 Exactly three alert families exist — active incident, broken building, and scenario - resolved — each keyed on published ids only, with no player-facing or authored text in the - key. A test asserts a key built from a campaign's own strings would fail, so the rule is - checked rather than described. - - W85.3 A newly active source raises one alert; a source that is no longer active is marked - cleared; and a second tick with the same source still active raises no duplicate, - asserted across three consecutive ticks. - - W85.4 The two kind-owned alert families resolve their title and message through - `world-graph.alert..title|message` in the kind's built-in strings, validated with - the kind's own content, and an incident alert reuses its definition's name and description - keys instead. No alert resolves to a bare key. - - W85.5 No alert or achievement feeds another system: a test that drops the whole alert list - before system 20 leaves the rest of the tick's state identical. - - W85.6 `achievement.unlocked`, `alert.raised` and `alert.cleared` are emitted and declared, - with alert creation and removal audits hidden as the contract requires. - - W85.7 `advance_ticks n` serializes byte-identically to any split of that batch across a batch - that raises and clears an alert. - - W85.8 The two committed world-graph replay outcomes are unchanged byte-for-byte — the MVP - campaign declares no achievements — and the new behaviour is proved by a new committed - fixture that unlocks one; any outcome that does move follows - [07 §7](07-replay.md#7-intended-change-versus-regression). - - W85.9 The known-and-retained tick-system entry in `design/90-decisions.md` is closed out in - the same pull request, since this unit removes the last of the five it names. - - W85.10 `npm run typecheck`, `npm run lint` and `npm test` pass from `src/engine/`. -- **Out of scope:** alert presentation, ordering or grouping in a client; alert severity policy - beyond what the contract fixes; a fourth alert family, which would widen a closed key set - and is a contract amendment; cross-kind achievement listing, which - [issue #282](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/issues/282) already - holds as contract work. - -### Rigour: Two Event Streams Specified and Never Emitted - -> [05 §2](05-observability.md#2-the-determinism-rule)'s guarantee — that dropping every event -> changes nothing — is what makes a declared-but-unemitted event safe to leave unbuilt, and also -> what makes it easy to leave unbuilt indefinitely. Two of the three kinds have event tables whose -> status column is mostly *specified, not yet delivered*: six of ten rows in -> [03 §8.4](03-story-graph-kind.md#84-events), and, after W81–W85 deliver the rows their own -> systems own, the remainder of [12 §12](12-world-graph-kind.md#12-events). `simulation` is the -> counter-example, and the reason this reads as a gap rather than a policy: all nine of its -> declared events are emitted today. +**Status:** Delivery ledger. Nothing is in flight. All 125 W-numbered units have landed or been +cancelled, and each is retired to one row under [Landed](#landed). The next slice is planned as +`## S121`, above *Landed*, in the agent kit's slice format. It reads `Status: todo` until the pull +request that builds it sets `Status: done`. + +> **Where a retired unit's full text is.** Every W unit's body — what it delivers, its numbered +> acceptance criteria, its scope, and the programme prose that grouped units together — is in this +> ledger as it stood at +> [`7081402`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/blob/7081402c6dccc277b23250d44a22b022a16fd683/design/30-slices.md). +> Most units also have an execution record under +> [`plans/`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/tree/main/plans). Each row's +> anchor is the unit's id in lower case, so a link ending in `#w90` still lands on W90's row. > -> Both tables say in their own prose that nothing needs redeciding first — the names are fixed, -> the emit sites are named, and adding one is declaring it and emitting it. These two units are -> that, and nothing else. - -### [x] W86 — The Story-Graph Kind Says Why {#w86} - -**Delivers:** A campaign author can find out why a choice was refused or greyed out, and a -developer can find out why a replay diverged, without adding logging to the engine by hand. Today -the kind reports that a requirement was unmet and stops there, which is correct for the player and -useless for the person who wrote the requirement. - -[03 §8.4](03-story-graph-kind.md#84-events) fixes ten event names and the shipped kind declares -four. Its own prose singles out two of the missing six as carrying most of the value, for the two -audiences the channel exists to serve. -- **Spec:** [03 §8.4](03-story-graph-kind.md#84-events), and §8.2 for the step each event is - emitted at; [05 §9](05-observability.md#9-kind-events); - [§2](05-observability.md#2-the-determinism-rule); - [04 §3](04-core.md#3-the-kind-interface--the-seam). -- **Touches:** `src/engine/src/kinds/story-graph/advance.ts`, - `src/engine/src/kinds/story-graph/achievements.ts` and - `src/engine/src/kinds/story-graph/kind.ts`. - `src/engine/src/core/observability/` is **read, not modified**. -- **Depends on:** nothing. -- **Status:** Done — [PR #362](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/362). -- **Done when:** - - W86.1 All six remaining rows are emitted at the step the table names, with exactly the `data` - fields it lists and no others, and all ten are declared in `Kind.eventNames`. A test - enumerates the declared names against the emitted ones so a future row cannot be declared - without an emit site or emitted without being declared. - - W86.2 `requirement.evaluated` fires once per requirement rather than once per choice, - asserted on a choice carrying more than one requirement with the exact event count stated. - **The row's stated purpose does not survive its own `data` column, and that is a finding - to report rather than resolve here:** [03 §8.4](03-story-graph-kind.md#84-events) says a - compound condition "reports which clause failed", but the row declares `choiceId` and - `satisfied` only, which cannot name a clause. Emit what the table declares and state the - discrepancy in the pull request; widening the `data` column is a contract amendment. - - W86.3 Playing the committed Bureaucracy arc to its ending with a recording sink produces the - full expected sequence, asserted as an ordered list of names rather than a set, since - ordering is what makes the stream readable. - - W86.4 Dropping every event changes nothing: the same arc replayed with a null sink and with a - recording sink produces byte-identical `serialize()` output and identical `StateChange` - arrays. - - W86.5 Every committed story-graph replay outcome is unchanged byte-for-byte. - - W86.6 The status column in [03 §8.4](03-story-graph-kind.md#84-events) has no remaining - *specified, not yet delivered* row, and the paragraph explaining what that status means is - rewritten rather than left describing a state that no longer exists. - - W86.7 `npm run typecheck`, `npm run lint` and `npm test` pass from `src/engine/`. -- **Out of scope:** an author-facing presentation of these events, which - [05 §13](05-observability.md#13-what-is-deferred) routes to content tooling by name; new - event names; changing any severity the table fixes; the sink implementations, which already - ship. - -### [x] W87 — The World-Graph Kind Says What Its Ticks Did {#w87} - -**Delivers:** Someone diagnosing a resort that behaves oddly can watch what happened inside a tick -— which guests could not reach a building, which tasks were assigned and cancelled, what each -charge was for, how an objective moved — instead of inferring it from the state before and after. -Today the pipeline runs twenty systems and reports on six of them. - -The row this unit exists for is stated in the contract itself: a resort where guests silently -cannot reach a building looks identical to one where they simply do not want to, and the -difference is invisible in the projection and obvious in the stream. -- **Spec:** [12 §12](12-world-graph-kind.md#12-events), and - [§4](12-world-graph-kind.md#4-the-turn-is-a-tick-batch) for each named emit site; - [05 §9](05-observability.md#9-kind-events); - [§7](05-observability.md#7-severity-and-volume); - [§2](05-observability.md#2-the-determinism-rule). -- **Touches:** `src/engine/src/kinds/world-graph/tick/pipeline.ts`; - `src/engine/src/kinds/world-graph/kind.ts`. -- **Depends on:** [W81](#w81)–[W85](#w85), which each deliver the rows their own system owns. This - unit takes what is left rather than duplicating them. -- **Status:** Done — [PR #363](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/363). -- **Done when:** - - W87.1 Every row in [12 §12](12-world-graph-kind.md#12-events)'s table is emitted at the system - it names and declared in `Kind.eventNames`; the status column has no remaining *specified, - not yet delivered* row, and the paragraph explaining that status is rewritten rather than - left describing a state that no longer exists. - - W87.2 A test enumerates declared names against emitted ones in both directions, so a declared - name with no emit site and an emitted name that was never declared each fail. This is the - gate the *emitted → registered* gap in the contract's own reason-code section says is - missing everywhere it recurs. - - W87.3 Events emit in system order and then in the owning comparator order, asserted over a - multi-tick batch as an ordered list — the property that makes the stream a readable causal - trace rather than an unordered log. - - W87.4 Dropping every event changes nothing: the same batch run with a null sink and with a - recording sink produces byte-identical `serialize()` output and identical `StateChange` - arrays. - - W87.5 Severity assignment matches the table exactly, asserted per row, since severity is the - only thing standing between a host and the six-figure trace volume a large batch produces. - - W87.6 Every committed world-graph replay outcome is unchanged byte-for-byte. - - W87.7 `npm run typecheck`, `npm run lint` and `npm test` pass from `src/engine/`. -- **Out of scope:** new event names or severities; the optional candidate-generation diagnostic's - shape beyond what the table fixes; any sink, exporter or trace-context work, which - [05 §13](05-observability.md#13-what-is-deferred) defers by name; presenting the stream - anywhere. - ---- - -## Known Open Items Carried In - -> Full register of unknowns, gaps, and deferred decisions: +> The contracts the units were built against are unchanged: +> [`04-core.md`](04-core.md), [`03-story-graph-kind.md`](03-story-graph-kind.md), +> [`10-simulation-kind.md`](10-simulation-kind.md), [`12-world-graph-kind.md`](12-world-graph-kind.md) +> and [`05-observability.md`](05-observability.md). The register of unknowns is > [`OPEN-QUESTIONS.md`](OPEN-QUESTIONS.md). -> **The eight findings from the first downstream host are contract work, not slices.** -> [`OPEN-QUESTIONS.md`](OPEN-QUESTIONS.md) §2, *Found by the first downstream host*, indexes what -> SubZeroDev.Adventures turned up by implementing the ports outside a browser tab -> ([#266](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/issues/266), -> [#276](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/issues/276)–[#282](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/issues/282)). -> They are deliberately **not** sliced here. Every one of them either asks for a signature the -> contract does not have — a player-keyed save query, a fork operation, an async -> `listCampaigns`, a `min`/`max` on `VisibleStat`, a cross-kind-legible `Kind.outcome` — or asks -> the contract to *state* a decision it currently leaves implicit, which is not something a -> vertical slice can deliver at all. Slicing one would mean inventing the signature first, and -> that is exactly the failure the "no slice may introduce a signature absent from the contract" -> rule exists to prevent. **Route to `/contract`**; they become sliceable the moment the seam is -> settled, and several are small once it is. - - -- [x] **`SessionHost`/`createSessionLayer` ([06 §4](06-extensibility.md#4-the-composition-root)) don't reconcile as written — closed.** - Resolved by drawing the seam one level down at `SessionPersistence` - ([04 §7.2](04-core.md#72-host-persistence--the-record-store-beneath-the-session-store)); - `createSessionLayer` now ships and is exported. `OPEN-QUESTIONS.md` §2 records the - closure and the reason its original "revisit when" was the wrong trigger. The replay - oracle still drives `Engine` directly, but for its own stated reason — it needs the raw - `GameState` no `SessionStore` returns ([07 §3.2](07-replay.md)) — not for want of a root. -- [x] **`wisdom` has a consumer — closed.** [W100](#w100)'s 2026-08-31 decision made the - generic attribute `Requirement` path its consumer, and W100.5 proved two otherwise - identical actors receive different available behaviour. Confirmed by `/align` on #275. -- [x] **Four simulation `ActionType`s have no content-definition type to resolve against — closed.** - [§7.12](10-simulation-kind.md#712-projects-and-businesses-w101) declares `ProjectDefinition` and - `BusinessDefinition`, and [W101](#w101) built the four resolvers against them. As first found: - `start_project`, `work_on_project`, `start_business` and `operate_business` are members - of [10 §4.2](10-simulation-kind.md#42-action-types)'s closed union, so `ResolverTable`'s - completeness check compels a resolver for each — but - [§7](10-simulation-kind.md#7-content-definition-types) declares no `ProjectDefinition` or - `BusinessDefinition`, and nothing in §7.2–§7.10 covers them. They are the only four of - the thirty that no unit W53–W57 can implement. **Revisit when** a scenario needs one: - either §7 gains the definitions or the union loses the members, and deciding that is - `/contract`'s work, not a slice's. Found while slicing W53–W57. -- [x] **The simulation `relationships` end-of-week system has no rule to implement — closed.** - [§7.11](10-simulation-kind.md#711-the-campaign-envelope-and-weekly-tuning)'s `relationshipDrift` is the rule, - and [W100](#w100).3 built the system against it. As first found: - [10 §3](10-simulation-kind.md#3-the-turn-is-a-week) names it in `END_WEEK_SYSTEM_ORDER` - and [§6.11](10-simulation-kind.md#611-relationships) gives `RelationshipState` its full - shape, but no weekly movement rule for it is stated anywhere in the contract — the stub's - own comment in `src/engine/src/kinds/simulation/endOfWeek.ts` has said so since W37. - W56 builds `socialize` against the shape and leaves the system a documented stub for - exactly this reason. **Revisit when** the rule is written, which is `/contract`'s work. -- [ ] Provisional numbers across the simulation kind (drift rates, scenario economics, - `demandBand` thresholds, housing-quality formula) are placeholder balance. **Balancing is - game-side**; the engine owes the levers, not the numbers (`90-decisions.md`, 2026-09-27, - `/align` on #267). The levers are [§7.14](10-simulation-kind.md#714-remaining-campaign-physics-524), - sliced as [W117](#w117)–[W119](#w119). Travel costs are already content-owned - (`LocationDefinition.travelTimeUnits`) and were never a lever to add. -- [ ] **Three `docs-template` hardening findings, to raise upstream — after this PR - merges, not before.** Surfaced by automated review on - [PR #3](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/3); all three - sit in files installed verbatim from `ghcr.io/the-running-dev/docs-template` (not - authored in this repo), and this repo's own W0 decision is to never hand-edit - installer-owned files — so the fix belongs in the `docs-template` project, filed as - a separate PR there once this one is settled: - - `docs-ci.yml` / `docs-deploy.yml` pin `ghcr.io/the-running-dev/docs-template:latest`, - a mutable tag — non-reproducible, silent behaviour drift possible on future runs. - Checked whether the installer's own `-BaseImage` avoids this without an upstream - change: it doesn't — repinning an already-installed file needs `-Overwrite`, which - would also replace this repo's five preserved local files (`docusaurus.config.ts`, - `sidebar.ts`, `Dockerfile`, `.dockerignore`, `docs.ps1`). A fix needs a pin mechanism - scoped to just the docs workflows, independent of `-Overwrite`. - - `build/Test-Documentation.ps1`'s link validator resolves relative targets with - `Join-Path` + `GetFullPath` and only checks `Test-Path`, without constraining the - result to stay under `$Root` — a `../../` link can resolve outside the repository and - still "pass." Not currently exploitable here (no `../`-style links exist in this - repo's docs today); a validator-correctness gap, not a live defect. - - Same script's file enumeration (`Get-DocumentationFile`) recurses every directory - before applying `ExcludedSegments`, so excluded trees (`.git`, `node_modules`) are - still walked. Performance only, tagged "Optional" by the reviewer. - - Full findings, the verification behind declining each in this repo, and the reply - text posted on each review thread: PR #3, review comments - [1](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/3#discussion_r3660515997), - [2](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/3#discussion_r3660516002), - [3](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/3#discussion_r3660516006). - ---- - -## Outstanding - -**One question: what stands between the `simulation` kind as it runs today and Life in the -Fast Lane being playable end to end.** The answer is not what the earlier programme headers -would suggest. W36–W40 built the kind, W50–W57 built the behaviour behind it, and the -mechanics are very nearly whole — twenty-seven of thirty-one -[§4.2](10-simulation-kind.md#42-action-types) `ActionType`s have a real resolver, fourteen of -fifteen [§3](10-simulation-kind.md#3-the-turn-is-a-week) end-of-week systems have real bodies, -[§9](10-simulation-kind.md#9-projection)'s projection is built, and the kind is proven through -the replay oracle, the text client and MCP. What is missing sits on either side of the -mechanics: **nothing outside the package can author or type a simulation campaign, and -nothing has ever run one for longer than three weeks.** - -Two units below close those two. Everything else Life in the Fast Lane still needs is a -contract decision, listed first so it is not mistaken for work a slice can pick up. - -### Contract Prerequisites — Not Sliceable, Route to `/contract` - -Named here in the shape *Known Open Items Carried In* uses for the eight downstream-host -findings, and for the same reason: each either asks for a type the contract does not declare -or asks it to state a rule it currently leaves unwritten, and slicing one would mean inventing -the answer first. Several are already carried above and are repeated only because they sit on -this path; the rest are new to this pass. - -**Status, 2026-10-03.** Every prerequisite below has since been contracted. P1 and P4 -were settled by [W101](#w101) (§7.12's project and business definitions; §7.8's `RivalConfig`). -P2, P5 and P6's `attendanceRatio` were settled by [W100](#w100) (§7.11's `relationshipDrift`, -`emptyPlanPolicy` and `attendanceTracking`), and P6's `wisdom` by W100's generic attribute -`Requirement` path (#275). P7 was settled by [W102](#w102) (§16; the portable simulation arm -carries `migration`). P3 is contracted as §7.14 and sliced as [W117](#w117)–[W119](#w119). P8 -is contracted as three rule-family codes in §7.14 and §10: `invalid_tuning_value`, -`unknown_tuning_key` and `invalid_eviction_stages`. §10 now also lists -`invalid_attendance_window` (`90-decisions.md`, 2026-10-03). Where W117–W119 say "P8's code", -read the §7.14 code for the rule being tested. The bullets below are kept as found. - -- **P1 — Four `ActionType`s have no content-definition type.** Carried above. - `start_project`, `work_on_project`, `start_business` and `operate_business` are members of - [§4.2](10-simulation-kind.md#42-action-types)'s closed union and - [§7](10-simulation-kind.md#7-content-definition-types) declares nothing for them to resolve - against. **What has not been recorded is the live consequence**, and it is worse than an - unbuilt feature: `ResolverTable`'s completeness check forced a resolver for each, so all - four are `stubResolver` — always valid, always a neutral no-op — and `view.ts` derives - `SimulationView.plan.availableActionTypes` from the whole union minus `"custom"`. **Every - client is therefore offering four actions that accept the plan, consume no time, cost no - money and change nothing.** A life sim in which starting a business is a progression path - cannot ship that way. The decision is whether - [§7](10-simulation-kind.md#7-content-definition-types) gains `ProjectDefinition` and - `BusinessDefinition` or §4.2's union loses the four members; which way it goes decides - whether the follow-on unit builds two content types or narrows a projection field, so - neither side can be sliced first. -- **P2 — The `relationships` end-of-week system has no rule to implement.** Carried above, - unchanged since W37: [§6.11](10-simulation-kind.md#611-relationships) gives - `RelationshipState` its full shape and - [§7.7](10-simulation-kind.md#77-npcs--definition-and-runtime-state) the NPC, and nothing - states what a week does to either. It is the last stub left in §3's order. -- **P3 — A campaign has no way to set its own numbers.** `SimulationCampaign` - (`src/engine/src/kinds/simulation/campaign.ts`) declares seventeen content collections, a - scenario id, a precedence flag, optional starting effects and three label keys — and **not - one tuning value**. The weekly time budget, every need's drift rate, the late-fee rate, the - eviction ladder, performance drift and its work bonus, the strangeness step, and every - action's own time cost and restore amount are `const`s in `initial.ts`, `endOfWeek.ts` and - `resolvers.ts`. A campaign cannot move any of them, so Life in the Fast Lane would be played - at *Stable Life's* physics — a fixture's provisional balance, not a game's. This is the - deeper reading of the *provisional numbers* item carried above: the problem is not that the - numbers are unbalanced, it is that they sit where no campaign can reach them. A tuning block - on `SimulationCampaign` is a new public interface and needs the amendment rather than a - slice. **Note what this is not:** [12 §15](12-world-graph-kind.md#15-validation) assigns - balance *findings* to a game-side harness, and that stands — this is about where the levers - live, not about who turns them. -- **P3 contract gate (#524, 2026-09-28):** §7.14 fixes the optional flat campaign fields, - per-key defaults, validation, source-builder omission behavior and a six-stage default - eviction ladder. Implementation remains open. The smallest first slice wires - `needDriftPerWeek` through source, validator and weekly system and proves two otherwise - identical campaigns diverge with `satiety: -4` versus `-5` under the same seed and action - log, including repeat/save-load determinism; omitted tuning remains byte-identical to - committed replay and golden oracles. Subsequent slices wire the remaining §7.14 fields. -- **P4 — Rival agents.** - [§7.10](10-simulation-kind.md#710-agents--engine-owned-strategy-definition-and-runtime-state) - states outright that how a scenario configures rivals is an open gap; no `ScenarioDefinition` - field names them. `PublicWorldState` was declared by [W50](#w50) so `AgentStrategy` could - typecheck and is exercised by nothing. [W57](#w57) put agents out of scope for exactly this - reason and nothing has changed since. -- **P5 — `plan_empty` has no field to condition on.** - [§10](10-simulation-kind.md#10-reason-codes) specifies it for "`end_week` with nothing - planned, where the campaign forbids it", and no campaign can express forbidding it — so - `available.ts` documents the gate as deliberately unwired rather than writing dead code. One - of the two §10 codes still undispatched. -- **P6 — `Employment.attendanceRatio` and `wisdom` are declared and read by nothing.** Both - carried above; they are the same shape and want the same kind of answer. - `attendanceRatio` ([§6.8](10-simulation-kind.md#68-career)) is set to `100` at hire and never - moved, so anything built on it reads a perfect record forever; `wisdom` - ([§6.6](10-simulation-kind.md#66-attributes)) is resolved through the derived-value layer and - consulted by no resolver or system. Each needs a mechanism decision — what a missed work week - *is*, and what wisdom changes — before it is a field to fill in. -- **P7 — A published simulation campaign cannot carry a save migration.** `PortableMigration` - is `nodeMap`/`endingMap` and sits on the story-graph arm of `PortableCampaignBody` by - construction, which `src/engine/src/portable/format.ts` documents as a deliberate structural - fact rather than an omission. Correct for the format as it stands, and a real ceiling for a - long-running life sim: once the game is published as portable JSON, no content revision can - migrate an existing save. **This does not block first play** — it blocks revising a live - game — so it is last here rather than first; it is also the one prerequisite carrying a - wire-format cost. -- **P8 — §7.14's Tier 1 rejection has no reason code.** - [§7.14](10-simulation-kind.md#714-remaining-campaign-physics-524) says Tier 1 rejects an invalid - physics value or stage sequence "with a path identifying the field/key", but - [§10](10-simulation-kind.md#10-reason-codes)'s Tier 1 table names no code for it. The one - precedent does not settle it either: W100 shipped `invalid_attendance_window` for §7.11's - window, and §10 does not list that code. One shared code, one per field, or one per rule - family is a choice of public, cross-version-stable vocabulary, so a slice cannot pick it. - Found while slicing [W117](#w117); it blocks W117–W119 and nothing else. The same pass should - either list `invalid_attendance_window` in §10 or record why it stays unlisted. - -### Depth: Life in the Fast Lane, End to End - -Two units. **W88 first, because nobody can start without it**: the kind's authoring surface is -not exported, so the content repository that would own the campaign cannot build one, and a -host that fetched it could not type what it renders. **W89 second, because nothing has been -tested at the scale the game is played at**: every simulation fixture in the corpus is two or -three weeks long, and a life sim is measured in years. - -### [x] W88 — The Simulation Kind Nobody Outside the Package Can Use {#w88} - -**Delivers:** Lets a content repository actually write Life in the Fast Lane, and lets a host -render it. Today the simulation kind is one you can register and cannot author: the package -exports `simulationKind` and nothing else about it — not the campaign builder, not the source -types it takes, not the campaign, state, view or outcome types a host must compile against. -`world-graph` has that whole set at the package root and `story-graph` has its builder and -source types on the `/authoring` subpath; `simulation` has neither, and nothing records the -absence as a decision. - -**The gap is exports, not code.** `buildSimulationCampaign` and `SimulationCampaignSource` have -existed in `src/engine/src/kinds/simulation/source.ts` since [W52](#w52), and `SimulationView`, -`PublicWorldState` and `SimulationOutcome` since [W50](#w50); every campaign under -`src/engine/src/campaigns/` reaches them by relative import, which is exactly why nothing -caught it. The packed tarball is the only place the omission is visible, and `consumer-smoke/` -has never asked for a simulation type. - -**This unit amends [20 §19](04-core.md#19-published-narrative-authoring)'s export list in the -same change that implements it**, the precedent [W50](#w50) took from [W48](#w48). That is -deliberate and narrow: §19's *rule* is settled — the root is the runtime contract, the subpath -is the author-time contract for repositories that own campaign source — and what is missing is -that its list names the story-graph builder and not the simulation one. If drafting turns out -to need a decision rather than a list entry, that is `/contract`'s call, not this unit's. - -**The placement question has a defensible answer and this unit states it rather than absorbs -it.** The two existing kinds disagree: `WorldGraphKindState`, `WorldGraphView`, -`WorldGraphOutcome`, `WorldGraphCampaign` *and* `buildWorldGraphCampaign` are all root exports, -while `StoryGraphCampaign` and `StoryGraphKindState` are `/authoring`-only. §19 resolves it by -kind of *thing* rather than by kind: **the builder and its source types are author-time and go -to `/authoring`; the campaign, state, view and outcome types are what a runtime host compiles -against and go to the root.** That splits the difference the two precedents straddle without -disturbing either — `buildWorldGraphCampaign`'s root placement predates the subpath and is -noted here rather than moved. -- **Spec:** [20 §19](04-core.md#19-published-narrative-authoring) (the root is the runtime - contract, the subpath the author-time one); - [10 §13](02-architecture.md#13-published-narrative-content-ownership) (published campaign - builders are not package-root API — a *builder* is not a published campaign); - [10 §9](10-simulation-kind.md#9-projection) (`SimulationView`, what a host renders); - [§7](10-simulation-kind.md#7-content-definition-types) (what the source types cover); - [§12](10-simulation-kind.md#12-terminal-identity) (`SimulationOutcome`). -- **Touches:** `src/engine/src/authoring.ts` and its committed sorted export list - ([W74.7](#w74)); `src/engine/src/index.ts`; `consumer-smoke/smoke.ts`; - `design/20-contract.md` (the `engine/04-core.md` block, §19). -- **Depends on:** [W57](#w57), the behaviour these types describe, and [W74c](#w74c), which - fixed what the root surface is for. Not on [W89](#w89). -- **Status:** Complete — [PR #367](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/367), merged at `8115478`. The root/runtime and `/authoring` export split is covered by the packed-tarball consumer smoke. -- **Done when:** - - W88.1 `buildSimulationCampaign` and every `*Source` type - `src/engine/src/kinds/simulation/source.ts` declares are exported from - `@the-running-dev/game-engine/authoring`; `SimulationCampaign`, `SimulationKindState`, - `SimulationView`, `PublicWorldState`, `SimulationOutcome`, `ActionType`, `GameAction` - and `WeeklyActionPlan` are exported from the package root. - - W88.2 `consumer-smoke` builds a simulation campaign from source through the subpath and - types a `SimulationView` through the root, **resolving both from the packed tarball** - rather than a source link — the bar [W74.6](#w74) already set. Deleting one of the new - exports fails the smoke build, verified by deleting one. - - W88.3 The subpath stays closed against published content: W74.7's enumeration check passes - with the new names in its committed list, and no `stable-life*` or - `bulgaria-stable-life` builder or campaign id becomes reachable through either surface. - - W88.4 §19's export sentence names the simulation builder and source types and states the - author-time/runtime split this unit applies, so the next kind has a rule to follow - rather than two precedents to choose between. - - W88.5 A test asserts the root exports no simulation *authoring* value — the direction - W74c's boundary check does not cover, since it enumerates the subpath and not the root. - - W88.6 `npm run typecheck`, `npm run lint` and `npm test` pass from `src/engine/`; - `install:engine`, `build` and `smoke` pass from `consumer-smoke/`; - `./build/Test-Documentation.ps1` passes. -- **Out of scope:** moving `buildWorldGraphCampaign` or the story-graph types to match — a - breaking change to two settled surfaces for consistency alone, which is [W74c](#w74c)'s - kind of unit and wants its own decision; exporting any campaign, which §19 forbids and - W74c removed; and every prerequisite above — this unit exports what already exists and - adds no behaviour. - ---- - -### [x] W89 — A Game-Length Life {#w89} - -**Delivers:** The first evidence that this kind survives being played as a game rather than as -a test. **The longest simulation run committed anywhere in this repository is three weeks** — -`stable-life-win` is three `end_week` calls, and all twelve simulation fixtures under -`src/engine/fixtures/replay/` are two or three. Each was built to prove one system, which is -what W53–W57 needed and is not what Life in the Fast Lane is: a scenario measured in years, -where the same fifteen systems run two hundred times over accumulating state. - -**What a long run exercises that a short one cannot** is why this is a unit rather than a -bigger fixture appended to an existing one. Three weeks never reaches the end of the eviction -ladder; never completes a course, let alone a promotion chain whose `minimumWeeksInRole` gates -re-measure from each promotion; never accumulates enough expired `activeEffects`, resolved -`PendingEventResponse`s or achievement counters to say anything about growth in the serialized -state; never exhausts an event or opportunity pool and finds out what the systems do when one -is empty; and never draws enough from the weekly RNG substreams to make substream independence -([§13](10-simulation-kind.md#13-determinism)) an observed property rather than an argued one. -Each of those is a way a game-length run can diverge or wedge while every current fixture stays -green. - -**The fixture is engine-owned and unpublished, which -[20 §19](04-core.md#19-published-narrative-authoring) permits explicitly** — "GameEngine may -retain a frozen campaign solely as a regression fixture; such a fixture is not published and -not listed in a manifest." It is regression evidence in the shape [W74a](#w74a) froze -Bureaucracy into, not a second game, and it must not become one: it needs enough content to -sustain a long run and no more. It is emphatically not Life in the Fast Lane, whose content -lives in -[SubZeroDev.GameOfLife](https://github.com/The-Running-Dev/SubZeroDev.GameOfLife). - -**Sized on the argument that the action log is generated, not authored.** A scenario with a -long horizon plus a scripted weekly policy driving the plan is one session's work; -hand-writing two hundred weeks of `plan.add` calls is not, and a unit reaching for the second -has been mis-sized rather than under-resourced. -- **Spec:** [`07-replay.md`](07-replay.md) §4, §6, §7 (the corpus and the runner's verdicts); - [04 §14](04-core.md#14-determinism-harness) (a build against itself); - [10 §3](10-simulation-kind.md#3-the-turn-is-a-week) (the two orderings a long run re-runs - two hundred times), [§13](10-simulation-kind.md#13-determinism) (substreams), - [§12](10-simulation-kind.md#12-terminal-identity); - [20 §19](04-core.md#19-published-narrative-authoring) (a retained fixture is not a - publication). -- **Touches:** a new long-horizon fixture campaign under `src/engine/src/campaigns/`; new - fixture and outcome pairs under `src/engine/fixtures/replay/`; - `src/engine/src/campaigns/replay-corpus.ts`. -- **Depends on:** [W57](#w57). Independent of [W88](#w88) — the two may land in either order. -- **Status:** Complete — [PR #376](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/376), merged at `71e7364`. The paired 150+ week replay fixtures prove deterministic win/loss paths and expose the retained pending-response and relationship-rule follow-up work. -- **Done when:** - - W89.1 A committed fixture plays at least one hundred and fifty weeks to a terminal - `outcome()` and is in the replay corpus, its outcome frozen the way every other corpus - entry is. - - W89.2 Both a win and a loss at that length are committed, and the loss reaches the end of - the eviction ladder rather than ending on a goal-failure condition — the terminal path - no short fixture has ever walked. - - W89.3 Every one of the twenty-seven dispatched `ActionType`s resolves at least once across - the committed long runs, asserted by counting resolved action types against - `RESOLVER_TABLE`'s non-stub entries, so the count moves on its own when P1 is answered. - - W89.4 Each of the fifteen [§3](10-simulation-kind.md#3-the-turn-is-a-week) end-of-week - systems is observed doing something at least once over the run — a `system.ran` event - alone does not count, since a stub emits one too. - - W89.5 The run is byte-identical when replayed, and byte-identical when the same weeks are - played as one session and as a save/restore split partway through, which is what proves - nothing accumulated outside the serialized state. - - W89.6 What the run's final state costs is asserted, not printed: serialized size and the - count of each unbounded collection at week one against the final week, committed with a - stated ceiling. A collection that grows every week is a defect this unit is the first - thing able to see. - - W89.7 Every existing committed replay outcome is unchanged byte-for-byte; this unit adds - fixtures and changes no behaviour. - - W89.8 `npm run typecheck`, `npm run lint` and `npm test` pass from `src/engine/`. -- **Out of scope:** fixing anything the run exposes — a defect found here is its own unit, and - the whole value of this one is being the first thing capable of finding one; any balance - judgement about the run's numbers, which - [12 §15](12-world-graph-kind.md#15-validation) assigns to a game-side harness and which - P3 shows the engine cannot host anyway; Tier 3 validation for this kind, which - [W73](#w73) put out of scope by name and which needs its state space argued before it is - built; and wiring a long run into CI as a required gate, a policy change with a runtime - cost that wants its own decision. ---- - -## Stabilization Programme - -The W88/W89 reconciliation closes the existing defined programme. The following units are -the additive `0.11.0` programme; W90–W92 make `0.10.0` honest and releasable first. - -**`0.10.0` was tagged and never published.** Its release run failed in 11 seconds on a shell -syntax error inside the tag/version guard, before any publish step, so the registry still holds -`0.4.0`, `0.5.0` and `0.8.0` only. W90–W92 made the package reproducible, which is not the same -as shipped — and the failure is why [W108](#w108)'s W108.5 requires those guards to be -executable without pushing a tag. - -### [x] W90 — Canonical and Public Truth {#w90} - -**Delivers:** A canonical ledger, generated documentation, README, landing page, and roadmap -that describe the three shipped kinds, completed W88/W89 work, the retired in-repository play -surface, and the ecosystem ownership boundary without rewriting historical evidence. - -**Status:** Complete — [PR #377](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/377). - -### [x] W91 — Reproducible Package and Release Gate {#w91} - -**Delivers:** A clean build/pack path, tarball-content assertion, and tag/version-checked -release workflow that runs the engine and packed-consumer checks before publishing `0.10.0`. - -**Status:** Complete — the release workflow packs from a clean build, rejects excluded archive -content, and installs that same archive into a lockfile-backed consumer smoke before publish. - -### [x] W92 — Local and CI Gate Health {#w92} - -**Delivers:** A single-pass documentation drift check, repaired Pester bootstrap, cached Alpine -Chromium, and a responsive shared header covered at phone and desktop widths. - -**Status:** Complete — documentation validation retains its full generated-file, link, anchor, -and terminology coverage in a 46-second local run; the remaining CI/bootstrap/cache/header -work shipped in [PR #377](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/377). - -### [x] W93 — World-Graph Spatial and Audit Correctness {#w93} - -**Delivers:** World-graph players can demolish, place scenery, and reassign staff without the -engine choosing the wrong route, accepting invalid geometry, or reporting changes that did not happen. - -- **Spec:** [12 §3.3](12-world-graph-kind.md#33-structural-answers-and-what-remains-the-games), - [§6](12-world-graph-kind.md#6-actions--one-model-spatial-verbs), - [§9.3](12-world-graph-kind.md#93-canonical-shortest-path), - [§13](12-world-graph-kind.md#13-statechange-at-batch-grain), and - [§15](12-world-graph-kind.md#15-validation). -- **Touches:** `src/engine/src/kinds/world-graph/` — `actions/build.ts`, - `actions/staff.ts`, `initial.ts`, `spatial.ts`, `validate.ts`, `content.ts`, - `source.ts`, `tick/effects.ts`, and focused tests beside those seams. -- **Depends on:** nothing beyond the shipped world-graph kind. -- **Status:** Complete — [PR #398](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/398), merged at `f0735fc`. -- **Done when:** - - W93.1 Demolishing a building sends each displaced guest to the reachable exit with the - lowest canonical path cost from that guest's position; equal-cost exits use the - row-major tie-break from §9.3, and a test proves a nearer exit other than - `map.exits[0]` wins. - - W93.2 Scenario scenery placements are rejected at Tier 1 when their rotated footprint is - out of bounds or overlaps another placed footprint, using the same rotation and - occupancy rules as runtime building placement; the finding names the exact - `sceneryPlacements[n]` path. - - W93.3 `assign_staff` returns a `StateChange` only for an assignment field whose value - changed. Changing only the building emits no unchanged zone row, changing only the - zone emits no unchanged building row, and submitting the current pair emits neither. - - W93.4 Direct tests cover the public behaviour of `validate.ts`, `content.ts`, `source.ts`, - and deferred/non-deferred effect application in `tick/effects.ts`; deleting any one - of those suites makes the engine test gate fail. - - W93.5 Every added spatial fixture is deterministic across two runs and across a - serialize/deserialize cut immediately before the corrected operation. - - W93.6 `npm run typecheck`, `npm run lint` and `npm test` pass from `src/engine/` with every - previously committed replay outcome unchanged. -- **Out of scope:** changing canonical-path cost or tie-breaking, adding new placement rules, - adding scenery build/demolish actions, or broadening `StateChange` beyond the scalar-path - contract. - -### [x] W94 — Simulation Resolution Correctness {#w94} - -**Delivers:** Simulation players must answer mandatory events, can buy every shop-reachable item, -and compete for finite jobs without long campaigns accumulating unstable state. - -- **Spec:** [10 §2.3](10-simulation-kind.md#23-effects-opportunities-and-scheduled-events), - [§3](10-simulation-kind.md#3-the-turn-is-a-week), - [§7.2](10-simulation-kind.md#72-jobs), - [§7.5](10-simulation-kind.md#75-items), and - [§14](10-simulation-kind.md#14-validation). -- **Touches:** `src/engine/src/kinds/simulation/` — `advance.ts`, `availableActions.ts`, - `resolvers.ts`, `endOfWeek.ts`, `validate.ts`, and focused tests; the Stable Life - long-horizon campaign and replay fixtures. -- **Depends on:** [W89](#w89), whose long run supplies the bounded-state baseline. The - job-opening scarcity criterion lands with or after [W101](#w101)'s rival contract so - the player and rivals cannot acquire different meanings for the same finite count. -- **Status:** Complete — [PR #400](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/400), merged at `e103de6`. -- **Done when:** - - W94.1 While `pendingResponses` contains a mandatory event response, the projection offers - only the response choices that event permits; `end_week` and every unrelated action - are unavailable and reject without changing serialized state. - - W94.2 Resolving the pending event removes exactly that response, applies its selected - outcome once, and permits ordinary planning again only when no other mandatory - response remains; save/load between presentation and response produces the same result. - - W94.3 `validateUnreachableItems` treats an item as reachable when at least one reachable - location permits `shop`; a shop-only item produces no Tier-2 warning, while an item - reachable through neither starting inventory nor a shop still does. - - W94.4 Filling one position in a finite job opening decrements - `positionsAvailable` from two to one and keeps the opening available; filling its last - position retires it. The same transition is used regardless of whether the successful - applicant is the player or a scripted rival. - - W94.5 The W89 win and loss paths run for at least 150 weeks without bypassing a pending - response, with the final serialized-size and unbounded-collection ceilings asserted - rather than logged. - - W94.6 Each long run is byte-identical on repeat and when split by a save/restore boundary, - and every existing replay outcome remains byte-identical. - - W94.7 `npm run typecheck`, `npm run lint` and `npm test` pass from `src/engine/`. -- **Out of scope:** balance-tuning the long run, defining rival strategies, changing event - content, or adding a new item-acquisition action beyond the shipped `shop` path. - -### [x] W95 — Effect and Audit Semantics {#w95} - -**Delivers:** Players and observers see status effects, world-meter changes, and audit reasons -that describe exactly what the engine applied—once, and only when state really changed. - -- **Spec:** [10 §2.3](10-simulation-kind.md#23-effects-opportunities-and-scheduled-events), - [§6.1](10-simulation-kind.md#61-base-and-derived-values), - [§10](10-simulation-kind.md#10-reason-codes); - [12 §9.2](12-world-graph-kind.md#92-curves-multiplication-and-rounding), - [§11](12-world-graph-kind.md#11-reason-codes), and - [§12](12-world-graph-kind.md#12-events). -- **Touches:** simulation effect insertion and modifier tests; world-graph - `tick/effects.ts`, `tick/pipeline.ts`, and tests; the canonical audit-code tables in - `design/20-contract.md` and their generated human documentation. -- **Depends on:** a real status-effect insertion path from [W101](#w101) if none exists when - this unit starts; the deferred-meter and audit-table work is otherwise independent. -- **Status:** Complete — [PR #403](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/403) - (W95.1–3, 5–7) and [PR #404](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/404) - (W95.4), merged. -- **Done when:** - - W95.1 Every simulation code path that inserts a `StatusEffect` goes through one invariant: - a same-source `refresh` replaces the prior layer and resets expiry, a same-source - `stack` retains independent layers, and different sources always coexist. - - W95.2 Focused tests prove same-source refresh, same-source stack, different-source stack, - and save/load immediately after insertion; modifier totals and expiry weeks match the - persisted layers in every case. - - W95.3 A deferred world-graph building-meter effect is reported as applied only when the - final system-14 composition and clamp changes the target meter. Equal and opposing - same-tick deltas that net to zero emit no `scenario.effect.applied` for that effect. - - W95.4 A non-zero deferred result still emits the applied event once, and the result is - identical across every partition of the same tick batch. - - W95.5 The world-graph audit-code table contains all eleven shipped visible reasons, - including `building_broken`, with its exact producer and transport; the stated count - equals the table and the runtime registered set. - - W95.6 Canonical documentation is regenerated and `./build/Test-Documentation.ps1` passes. - - W95.7 `npm run typecheck`, `npm run lint` and `npm test` pass from `src/engine/`, with - existing replay outcomes unchanged. -- **Out of scope:** inventing a new effect family, changing meter aggregation or clamp order, - changing event severity, or solving the general emitted-to-registered gate delivered by - [W96](#w96). - -### [x] W96 — Mechanical Regression Boundaries {#w96} - -**Delivers:** Engine maintainers get required checks that catch determinism, ownership, -reason-registration, event-severity, or per-kind regression drift before it can ship. - -- **Spec:** [04 §3](04-core.md#3-the-kind-interface--the-seam), - [§12](04-core.md#12-reason-codes-state-changes-messages), - [§14](04-core.md#14-determinism-harness); - [05 §7](05-observability.md#7-severity-and-volume); - [12 §5](12-world-graph-kind.md#5-batch-invariance--and-the-two-seam-changes-it-forced) and - [§15.3](12-world-graph-kind.md#153-resolution-verification-matrix-for-w46w47). -- **Touches:** core determinism/validation tests and guard scripts; each kind's event and - reason declarations and focused test corpus; CI/package scripts that make the guards - required. -- **Depends on:** [W93](#w93)–[W95](#w95), so the new gates record the corrected baseline - instead of freezing known defects. -- **Status:** Complete — [PR #408](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/408), merged at `bb62ffe`. -- **Done when:** - - W96.1 World-graph batch invariance compares complete canonical kind state for unsplit runs - against `[1,9]`, `[5,5]`, `[2,3,5]`, and ten one-tick calls, across at least two seeds - and across service, construction, incident, day-reset, departure, and terminal - boundaries. - - W96.2 A required manifest or equivalent check names regression evidence for - `story-graph`, `simulation`, and `world-graph`; deleting any named per-kind suite or - fixture fails the same gate CI runs. - - W96.3 A mechanical ownership check fails when a kind state, campaign, or projection repeats - a field owned by `GameState`, `Campaign`, `ContentRegistry`, `Scene`, or `PlayerView`, - and passes all three shipped kinds without a handwritten exception for a current field. - - W96.4 A mechanical reason check proves every visible rejection or audit reason a kind can - emit is registered and localized, including values carried indirectly through - `EffectContext` and batch-change recorders; a seeded unregistered indirect reason - makes the test fail. - - W96.5 Each kind owns one name-to-severity table. The declared names, runtime emit sites, - and canonical contract table are compared mechanically, and two severities for one - event name or a missing/extra name fail the gate. - - W96.6 All guards run from existing package/CI entry points, fail with the kind and value - that drifted, and do not depend on network access. - - W96.7 `npm run typecheck`, `npm run lint` and `npm test` pass from `src/engine/`; all - committed replays remain byte-identical. -- **Out of scope:** adding kind names, event names, severities, reason vocabulary, or envelope - fields; testing balance; and making per-call audit arrays batch-invariant when the - contract requires only final canonical state. - -### [x] W97 — Immutable Projections and Shared Pipelines {#w97} - -**Delivers:** Engine consumers can inspect and manipulate returned views without mutating a game, -while maintainers can evolve the two tick-driven kinds on one behaviour-preserving pipeline. - -- **Spec:** [04 §3](04-core.md#3-the-kind-interface--the-seam), - [§9](04-core.md#9-projection), - [§14](04-core.md#14-determinism-harness); - [10 §3](10-simulation-kind.md#3-the-turn-is-a-week), and - [12 §4](12-world-graph-kind.md#4-the-turn-is-a-tick-batch). -- **Touches:** the `Kind.project`/`Engine.view` seam and all three kind projections; simulation - end-of-week and world-graph tick pipeline composition; an engine-internal pipeline module - and focused equivalence tests. -- **Depends on:** a fresh `/contract` decision that assigns defensive-copy ownership at the - `Kind.project` seam and a `/design` or `/contract` decision that fixes the shared - pipeline's ordering/error semantics. No implementation begins while either shape is - unresolved. -- **Status:** Complete — [PR #409](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/409), merged at `b9ecdc2`. -- **Done when:** - - W97.1 The canonical contract states whether a kind or the kernel owns defensive copying, - including nested arrays/records and the treatment of immutable primitives; the source - and all three built-in kinds implement that one rule. - - W97.2 Mutating every nested object/array reachable from a returned story-graph, - simulation, or world-graph view leaves the originating `GameState`, a fresh second - view, `serialize()`, and the action log unchanged. - - W97.3 The shared pipeline accepts an explicit ordered system list, threads the existing - frame/context, stops or continues only under the contract's stated rule, and is not - exported from the package root or `/authoring`. - - W97.4 Simulation's ordered end-of-week systems and world-graph's twenty tick systems both - execute through that substrate without changing their kind-specific system ids, - comparators, event order, or durable handoffs. - - W97.5 Before/after golden runs for both kinds produce byte-identical serialization, - outcomes, visible changes, and event streams with recording emitters; null emitters - produce the same state. - - W97.6 `npm run typecheck`, `npm run lint` and `npm test` pass from `src/engine/`, and packed - consumer smoke proves no new public export was introduced. -- **Out of scope:** merging the two kinds' state or mechanics, making pipelines campaign data, - changing any system order, or general-purpose workflow/middleware infrastructure. - -### [x] W98 — Catalog and Projection Completeness {#w98} - -**Delivers:** Clients can render campaign choices, stat ranges, ending progress, and terminal -results through the public store and projection surfaces without reading engine-owned content or state. - -- **Spec:** [04 §3](04-core.md#3-the-kind-interface--the-seam), - [§7](04-core.md#7-the-session-store-and-the-platform-api), - [§9](04-core.md#9-projection); - [03 §9](03-story-graph-kind.md#9-projection--what-a-client-sees), and - [09 §4](09-clients.md#4-the-api-coverage-checklist). -- **Touches:** `CampaignSummary`, `SessionStore.listCampaigns`, built-in outcome/projection - types and implementations; text client, MCP server, browser composition, service-contract - mirrors, and packed-consumer tests. -- **Depends on:** a fresh `/contract` pass that declares the asynchronous catalog result, - session-free title resolution, range/progress projection, and cross-kind terminal - identity. Those public shapes do not exist in the current contract and are not to be - inferred from this slice. -- **Status:** Complete — [PR #414](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/414), merged at `d7ca8eb`. -- **Done when:** - - W98.1 The canonical contract declares an asynchronous campaign-list operation and a result - that a client can render as a human-readable campaign selector using only - `SessionStore`; a fetch-backed test implementation does not preload a registry. - - W98.2 The text client and MCP `list_campaigns` render the same resolved titles without - starting a session or reading `ContentRegistry` directly, and the API coverage checklist - matches the chosen operation count. Browser-shelf parity with these two surfaces is the - client contract's requirement, not this repository's — `/play/` was retired in favor of - `SubZeroDev.Adventures` (`10-design.md`, *Succeeded by SubZeroDev.Adventures*), and its - own reconciliation ticks that box, the same treatment `10-design.md` §4 already gives the - Browser client column for W99's rows 11–13. Amended 2026-09-13 (`90-decisions.md`) after - `/play/`'s retirement moved the browser surface out of this repository. - - W98.3 Every projected visible story stat carries its declared lower and upper bounds, so a - client renders the current value and range without reading campaign content. - - W98.4 Story-graph ending progress exposes the contract-selected discovered and total facts - without exposing hidden ending ids; a fresh profile and a profile with one unlocked - ending have distinct, exact projections. - - W98.5 A host can read the contract-selected common terminal identity from each built-in - kind without switching on private kind-state shapes, while kind-specific published ids - remain available and balance-sensitive facts remain excluded. - - W98.6 Existing in-memory callers have an explicit migration path to the asynchronous - listing, and type-level packed-consumer fixtures prove both the new path and the - intended rejection of the old signature. - - W98.7 `npm run typecheck`, `npm run lint` and `npm test` pass from `src/engine/`; site and - host checks pass with committed replay outcomes unchanged. -- **Out of scope:** adding locales, exposing hidden variables/endings, returning campaign - content through `SessionStore`, making outcomes a balance report, or changing session - creation semantics. - -### [x] W99 — Session Lifecycle Operations {#w99} - -**Delivers:** Hosts can list, branch, and delete a player's saves through the session store and -reproduce stored sessions exactly, while retaining responsibility for authorization. - -- **Spec:** [04 §7](04-core.md#7-the-session-store-and-the-platform-api), - [§10](04-core.md#10-content-saves-migration); - [06 §2](06-extensibility.md#2-the-rule), - [§5.2](06-extensibility.md#52-sessionpersistence-and-profilestore), and - [07 §6](07-replay.md#6-the-runner-and-its-verdicts). -- **Touches:** session/store and persistence contracts and implementations; `IdSource`, text - and MCP/client coverage, service-contract mirrors, host adapters, and replay tests. -- **Depends on:** a fresh `/contract` pass that chooses the store signatures, result/error - shapes, canonical save-list order, and concurrency semantics. Authorization remains a - documented host boundary unless that pass explicitly reopens it. -- **Status:** Complete — [PR #416](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/416), merged at `51ad81a`. -- **Done when:** - - W99.1 A player-keyed save-list operation exists at the contract-selected store boundary, - returns only that player's saves in a specified deterministic order, and lets both - existing hosts delete their private shadow indexes. - - W99.2 Branching a session at a valid action sequence is one store operation: it allocates a - new id through `IdSource`, leaves the source session/save untouched, and the branch - serializes and replays byte-identically through the fork point. - - W99.3 Branching an unknown session or invalid sequence returns the contract-selected typed - failure and performs no persistence write. - - W99.4 Deleting a save is reachable through the chosen public lifecycle surface; success - removes exactly the addressed record, and the specified missing/wrong-player result - leaves every record untouched. - - W99.5 A delete authorized against an older record cannot erase a concurrent replacement - written under the same `saveId`; the race is reproduced deterministically against the - persistence conformance suite. - - W99.6 The replay/extensibility contract states that reproducing a stored session blob from - its log requires `IdSource.newGameId` pinned to the original `gameId`, and a test proves - pinned reproduction is exact while an unrelated id is observably different. - - W99.7 The contract states that caller identity and authorization are host-owned and - `SessionStore` remains caller-agnostic, with the deferred hosting/NEaaS trigger that - would reopen the decision. - - W99.8 Text, MCP/service-contract, host, and packed-consumer coverage include every new - operation; `npm run typecheck`, `npm run lint` and `npm test` pass from `src/engine/`. -- **Out of scope:** accounts, authentication/authorization policy, cloud synchronization, - sharing saves between players, editing historical actions, or changing the save envelope. - -### [x] W100 — Campaign-Tunable Weekly Rules {#w100} - -**Delivers:** Campaign authors can tune weekly rules, empty plans, relationships, attendance, and -wisdom-driven choices while existing campaigns keep their current behaviour by default. - -- **Spec:** [10 §2](10-simulation-kind.md#2-kindstate--what-belongs-here), - [§3](10-simulation-kind.md#3-the-turn-is-a-week), - [§6](10-simulation-kind.md#6-player-state), - [§7](10-simulation-kind.md#7-content-definition-types), - [§9](10-simulation-kind.md#9-projection), and - [§10](10-simulation-kind.md#10-reason-codes). -- **Touches:** simulation campaign/source types, builder defaults and validation; planning and - end-of-week systems for relationships/employment; projection, conditions/effects, - fixtures, and replay tests. -- **Depends on:** a fresh `/contract` pass that fixes the tuning fields and defaults, the - `plan_empty` policy, relationship transition, attendance formula, and the specific - wisdom consumer. The current contract explicitly leaves those rules absent or inert. -- **Status:** Complete — [PR #419](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/419), merged at `3d496b3`. -- **Done when:** - - W100.1 The source/runtime campaign contract contains one closed, validated home for every - weekly tuning value this unit changes, with explicit defaults; omitted fields on every - 0.10 campaign build to the exact former runtime values. - - W100.2 The empty-plan policy is campaign-selectable. A forbidding campaign rejects - `end_week` with `plan_empty` and unchanged state, while a permitting campaign advances - through the ordinary ordered pipeline; the default preserves 0.10 behaviour. - - W100.3 The `relationships` system applies the contract's ordered drift once per week to the - correct actor/NPC records, clamps at declared bounds, emits exact visible changes, and - resumes identically across save/load. - - W100.4 Employment attendance is updated from the contract's declared planned/worked inputs, - uses the specified rounding and rolling window, affects the existing attendance - requirements, and cannot be advanced twice for one week. - - W100.5 `wisdom` has the contract-selected visible projection and at least one typed content - read with a fixture proving two otherwise identical actors receive different available - behaviour; no client reads raw campaign or kind state to obtain it. - - W100.6 Builder and validator tests cover every default, invalid bound, and explicit override; - the Stable Life campaign builds with no new unexplained finding. - - W100.7 Every 0.10 campaign, replay, and save fixture is byte-identical when all new fields - are omitted; `npm run typecheck`, `npm run lint` and `npm test` pass from `src/engine/`. -- **Out of scope:** tuning concrete campaign balance, adding projects/businesses/rivals, profile - progression, or changing the ordered list of end-of-week systems. - -### [x] W101 — Projects, Businesses, and Rival Scarcity {#w101} - -**Delivers:** Campaign authors can offer persistent projects and businesses, and players can face -scripted rivals who compete reproducibly for the same finite opportunities. - -- **Spec:** [10 §2.2](10-simulation-kind.md#22-world-state), - [§4.2](10-simulation-kind.md#42-action-types), - [§6.2](10-simulation-kind.md#62-the-shared-actor-shape), - [§7](10-simulation-kind.md#7-content-definition-types), and - [§7.10](10-simulation-kind.md#710-agents--engine-owned-strategy-definition-and-runtime-state). -- **Touches:** simulation content/source and runtime state; project/business resolvers and - end-of-week systems; scenario rival configuration, strategy registry, opportunity/job - scarcity, validation, projections, events, and replay fixtures. -- **Depends on:** [W100](#w100) for the defaulted campaign-tuning boundary, and a fresh - `/contract` pass that declares project/business definitions, their durable progress, - scenario rival selection, and deterministic competition. The four action names and - engine-owned strategy seam alone are not sufficient public contracts. -- **Status:** Complete — [PR #421](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/421), merged at `b595fe2`. -- **Done when:** - - W101.1 Project definitions are campaign content with closed typed requirements, costs, - progress, completion and rewards; resolver selection uses the action/type discriminator - declared by the contract and never a tag or id naming convention. - - W101.2 Starting and advancing a project consumes the exact planned time/resources, persists - partial progress, completes once, and produces the same state and published changes - when save/load cuts occur before start, mid-progress, and immediately before completion. - - W101.3 Business definitions and runtime records drive start-up cost, weekly revenue, - expenses and closure under contract-stated integer/rounding rules; cashflow posts once - in the ordered weekly pipeline and is invariant across replay and save boundaries. - - W101.4 A scenario declares zero or more rivals by the contract-selected strategy ids and - initial conditions. Zero rivals preserves current behaviour; an unknown strategy or - invalid advantage is a precise Tier-1 finding. - - W101.5 Player and rivals use the same `ActorState` mechanics and the same action resolvers. - A fixture that gives both actors identical state and the same chosen action produces - identical actor-local results before contested allocation is applied. - - W101.6 Each rival derives choices only from its allowed public-world view and its stable RNG - stream. Reordering registry construction or enabling a recording emitter does not - change choices, allocation, state, or outcome. - - W101.7 Two actors contesting a finite opportunity or job opening are resolved by the - contract's complete comparator; one filled position decrements the count, the last - retires the opening, and the losing actor receives the declared unavailable/revoked - result without duplicated rewards. - - W101.8 Committed project, business, and rival-scarcity replays are byte-identical on repeat - and across a save/restore cut; `npm run typecheck`, `npm run lint` and `npm test` pass - from `src/engine/`. -- **Out of scope:** adaptive/remote AI, hidden-information cheating, multiplayer, arbitrary - user-authored code, economic balance of a concrete campaign, or changing `ActorState` - into separate player and rival shapes. - -### [x] W102 — Profile Chains and Simulation Save Migration {#w102} - -**Delivers:** Simulation players can carry profile-scoped chains into later sessions and resume -older portable saves through deterministic, declared migrations. - -- **Spec:** [04 §7.1](04-core.md#71-the-profile-store), - [§10.2](04-core.md#102-save-envelope-and-migration); - [10 §2.2](10-simulation-kind.md#22-world-state), and - [§15](10-simulation-kind.md#15-what-was-ported-and-what-was-found-along-the-way). -- **Touches:** `PlayerProfile`, profile persistence and kind integration; simulation chain - content/state; campaign/kind migration declarations and dispatch; save/replay fixtures, - host adapters, and packed-consumer tests. -- **Depends on:** [W101](#w101), so the profile and migration contracts are tested against real - simulation content rather than a mechanism with no consumer; and a fresh `/contract` - pass defining both public shapes before implementation. -- **Status:** Complete — [PR #423](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/423), merged at `6e4a7bb`. -- **Done when:** - - W102.1 `PlayerProfile` has a versioned, JSON-serializable, kind-owned data boundary whose - ownership, unknown-kind behaviour, size/version validation, and migration order are - stated in the canonical contract without importing a simulation type into core. - - W102.2 A simulation chain declared with profile scope writes through `ProfileStore`, is - visible to a new session for the same profile, remains absent for another profile, and - never changes terminal resolution or deterministic state for a session that does not - read that chain. - - W102.3 Reapplying an already-recorded profile-chain transition is idempotent: it creates no - duplicate entry, event, achievement, or reward, including after save/load and process - restart against the persistence conformance adapter. - - W102.4 The simulation campaign contract declares portable migration steps for published-id - remaps, required defaults, removals, and validation. Steps are data or engine-owned - declarations, never host callbacks, and their order is deterministic. - - W102.5 Loading an older simulation save applies kind-shape migration before campaign-data - migration, stamps current versions, sets `replayCompatible: false` sticky-forward, and - yields the same canonical result on repeated migration. - - W102.6 Missing remaps/defaults and invalid post-migration state return the existing typed - migration failures without partially writing a session or save; focused fixtures cover - success on both axes and failure on each axis. - - W102.7 Every 0.10 profile and save with no profile-chain data loads under the declared - default with unchanged projection and outcome; packed host consumers compile against - the amended types. - - W102.8 `npm run typecheck`, `npm run lint` and `npm test` pass from `src/engine/`; all - unaffected committed replay outcomes remain byte-identical. -- **Out of scope:** cloud profile synchronization, merging concurrent profiles, arbitrary - executable migration scripts supplied by a host, migrating unpublished development - ids, or making profile state part of terminal resolution. - -### [x] W103 — Companion Contracts and Ownership Reconciliation {#w103} - -**Delivers:** Ecosystem maintainers get one consistent account of simulation lifecycles, hosted -operations, fixture ownership, package visibility, and the proof that replaces the retired browser. - -- **Spec:** [10 §2.3](10-simulation-kind.md#23-effects-opportunities-and-scheduled-events) - and [§15](10-simulation-kind.md#15-what-was-ported-and-what-was-found-along-the-way); - [09 §4](09-clients.md#4-the-api-coverage-checklist); - [13 §6](13-playable-web-demo.md#6-route-visual-system-and-delivery), and - [15 §6](15-platform-static-host.md#6-ci-publication-and-deployment-boundary). -- **Touches:** canonical companion-mirror sections and generated docs; Platform, - `@subzerodev/service-contract`, and Adventures integration evidence; campaign fixture - notices; package-visibility and retired-browser-proof decisions; agent-kit metadata and - repository-specific companions where the sync is part of the reconciliation. -- **Depends on:** GameOfLife S6/S7 and its zero-concept-finding gate; [W98](#w98) and - [W99](#w99) for the final operation shapes mirrored to Platform/service consumers. -- **Status:** Landed — all eight criteria confirmed met; closed by issue #391. -- **Done when:** - - [x] W103.1 GameOfLife S7 is landed and its `Test-SpecSet.ps1` reports zero `concept` findings; - every lifecycle it added is mirrored here with both creation and retirement paths. A - contradiction with shipped simulation code is reported and stops the mirror rather - than being silently reconciled. Re-verified by PR #438: `SubZeroDev.GameOfLife` S6/S7 - are landed (its issues #13, #14); `design/20-contract.md` §15.1 maps all twelve of S7's - `lifecycle-` concepts onto this kind's own shape. No contradiction with shipped - simulation code surfaced. - - [x] W103.2 Platform's hosted MCP contract includes `preview_action` and every W98/W99 - operation with the same arguments/results and one-operation/one-tool mapping. The row - set moved out of Platform entirely (its S2.11) into - [`SubZeroDev.ServiceContract`'s `mcp-tool-contract.md`](https://github.com/The-Running-Dev/SubZeroDev.ServiceContract/blob/5d3fb8e6ebeb8fb6200e36ebc61df107ad355ce7/mcp-tool-contract.md) - (`5d3fb8e`, package `0.6.0`), which names `preview_action` as the tenth of thirteen - operations, including W98's revised `list_campaigns` and W99's `list_saves`/ - `branch_session`/`delete_save`. Platform's - [`workloads/game-service/src/mcp-surface.ts`](https://github.com/The-Running-Dev/SubZeroDev.Platform/blob/665103a3cdd182b1bc248c5aa08d2202fb6ac041/workloads/game-service/src/mcp-surface.ts) - (`665103a`) builds `listTools()`/`callTool()` directly from that package's - `contract.operations`, so the mapping is generated, not hand-mirrored. Closes #269 (see - `90-decisions.md`). - - [x] W103.3 The service-contract package's `src/generate.ts` (`SubZeroDev.ServiceContract` - @ `5d3fb8e`) enforces arity both ways — every engine `SessionStore` method must have a - row and every row must name a real method — failing the build on a missing or extra - operation. Adventures vendors that same `0.6.0` artifact - (`server/package.json`: `file:./vendor/subzerodev-service-contract-0.6.0.tgz`, - `SubZeroDev.Adventures` @ - [`aeef4b4`](https://github.com/The-Running-Dev/SubZeroDev.Adventures/blob/aeef4b4089a50d2d0b55e5c421066f0fd245e9db/server/package.json)) - and runs its own missing/extra-operation gate, - [`server/src/contract.test.ts`](https://github.com/The-Running-Dev/SubZeroDev.Adventures/blob/aeef4b4089a50d2d0b55e5c421066f0fd245e9db/server/src/contract.test.ts), - comparing the routes it serves against `loadPublishedContract().operations`. The browser - client, - [`src/play/remote-store.ts`](https://github.com/The-Running-Dev/SubZeroDev.Adventures/blob/aeef4b4089a50d2d0b55e5c421066f0fd245e9db/src/play/remote-store.ts), - implements all thirteen operations — catalog, save, load, branch, and delete included — - as `fetch` calls against the server's HTTP routes, never importing `SessionStore` or - touching persistence directly. - - [x] W103.4 Every campaign source and exported JSON snapshot retained in this repository says - within its owning file that it is a test fixture, names the external Content repository - as the publication authority, and leaves fixture behaviour/tests unchanged. - - [x] W103.5 The package-visibility decision is recorded once and `package.json`, plans 39/40, - release documentation, and consumer access all agree on public or private; no document - retains the rejected answer. See `90-decisions.md` §2, closing issue #302. - - [x] W103.6 The no-engine-API browser proof is either assigned to a named replacement owner with - an executable check or retired by a recorded decision that updates both design 13 and - 15; the deleted `/play/` surface is not recreated to satisfy it. PR #436 retires the - proof rather than reassigning it: Adventures is deliberately a hosted API with - persistence, so the property has no surviving host to assert over. Recorded in - `90-decisions.md` §2, closing issue #273. - - [x] W103.7 The current agent kit is reconciled without losing repository-specific `-Skip:` - guards or command companions; the Pester run reports both pass and intentional skip - counts, and any third recurrence is raised upstream as the existing issue requires. - Closed by PR #442: `/kit-sync` fast-forwarded `~/.agent-kit` (`5095a55` → `36f0a7a`, - 44 commits) and reconciled `tools/*.ps1` and the two divergent command cores via - `tools/Sync-Kit.ps1`. `tools/` Pester reports 324 passed, 0 failed, 35 intentional - skips. - - [x] W103.8 Canonical docs regenerated via `build/ConvertTo-HumanDocumentation.ps1`; - `build/Test-Documentation.ps1` passed (18 generated engine pages, compatibility - pointers, guide, and 189 Markdown files); `src/engine`'s `typecheck`, `lint`, and - `test` (94 files, 1554 tests) all passed. Every companion dependency cited above for - W103.2/W103.3 names an immutable commit SHA in a public repository, not an - unversioned branch. -- **Out of scope:** authoring GameOfLife lifecycles in this repository, implementing - Presentation's scene layer, republishing external packages, moving fixture content back - from Content, or adopting a companion's design-state corpus wholesale. - -### [x] W104 — Release 0.11 Compatibility Sweep {#w104} - -**Delivers:** Existing campaign, save, replay, host, and package consumers can move from 0.10 to -the additive candidate without silently changing behaviour. - -- **Spec:** [04 §10](04-core.md#10-content-saves-migration), - [07 §6](07-replay.md#6-the-runner-and-its-verdicts), and the compatibility/default rules - fixed by [W98](#w98)–[W102](#w102). -- **Touches:** a committed 0.10 compatibility corpus; campaign builders, save loader, replay - runner, host/service adapters, package archive assertions, and consumer smoke projects. -- **Depends on:** [W93](#w93)–[W103](#w103) complete. This is the first unit allowed to call - their combined result a release candidate baseline. -- **Status:** Landed — all seven criteria confirmed met; closed by issue #392. -- **Done when:** - - [x] W104.1 A committed manifest names every 0.10 built-in campaign/version, representative - active and ended save for each kind, every replay fixture, both hosts, and the packed - public consumer surface; the sweep fails if an entry disappears without an explicit - replacement/evidence note. Closed by PR #448: `scripts/w104-manifest.test.ts` names every - 0.10 built-in campaign/version and the save/compat-baseline fixture sets, on top of the - hosts/packed-consumer-surface entries PR #447 added to `regressionManifest.test.ts`. - - [x] W104.2 Every 0.10 campaign source builds with all 0.11 additive fields omitted and produces - the same canonical runtime content, initial projection, available actions, and Tier-1/2 - findings as its recorded 0.10 baseline unless a separately approved correction is named. - Closed by PR #448: `fixtures/compat/v0.10.0/*.json` freezes the canonical initial `Scene` - and Tier-1/2 findings per built-in campaign, captured at the `v0.10.0` tag; a HEAD rerun - is byte-identical across all five entries. - - [x] W104.3 Every 0.10 save loads or migrates according to its recorded version boundary, with - unchanged projection/outcome for defaulted additions and no partial write on failure. - Closed by PR #448: `fixtures/saves/*.json` (one active/ended pair per kind) load through - a real `SessionStore.loadGame` and match an independent re-run; a tampered-checksum case - proves no partial write. - - [x] W104.4 Every pre-existing replay receives the same verdict and byte-identical outcome; any - intentionally migrated fixture is separately identified and proves - `replayCompatible: false` rather than silently replacing its golden file. Closed by - PR #447: 29 fixtures replayed against `v0.10.0`'s own committed corpus, byte-identical. - - [x] W104.5 The text client, MCP server, static host and Adventures/service-contract integration - pass against the same engine archive and use only the declared SessionStore surface. - Closed by PR #452, after PR #449 covered the in-repository text client and MCP server via - the client-boundary ESLint rule and their own API-coverage suites: the engine packed at - `e52e7a1` was re-vendored into `SubZeroDev.Platform`, `SubZeroDev.Adventures`, and - `SubZeroDev.ServiceContract`, each passing its full suite with no reach past - `SessionStore`/`Portable*`/`fromPortable`. Recorded as a point-in-time proof, not a - standing cross-repo CI gate, since none of the three repos live inside this one. - - [x] W104.6 A clean pack contains the asserted files and no private source/plans, installs into - the lockfile-backed consumer smoke, and compiles/runs all public and `/authoring` - examples without workspace resolution. Closed by PR #447: clean tarball, no workspace - resolution, `/authoring` exports exercised in the consumer smoke. - - [x] W104.7 The compatibility sweep runs from one documented command/CI job and exits non-zero - with the exact artifact that drifted; its green result and baseline commit are recorded. - Closed by PR #447: `build/Test-CompatibilitySweep.ps1` composes typecheck/lint/test, the - replay oracle, and the packed-tarball consumer smoke into one documented command. -- **Out of scope:** preserving undocumented implementation details, treating an approved bug - fix as a compatibility failure without its evidence, publishing a package, or accepting - new feature work after the baseline is cut. - -### [x] W105 — Documentation and Landing Publication Review {#w105} - -**Delivers:** Prospective users can read and navigate an accurate public account of the -stabilization release on both documentation and landing surfaces. - -- **Spec:** the canonical sections changed by W93–W104; the documentation source/generation - boundary in `CLAUDE.md`; and the public claims in the README, guide, roadmap, and landing - configuration. -- **Touches:** canonical design text, generated `docs/docs/engine/` pages, guide/navigation, - README/roadmap/release material, landing configuration and rendered browser snapshots. -- **Depends on:** [W104](#w104), so the public review describes the verified compatibility - candidate rather than work still moving underneath it. -- **Status:** Complete. Closed via #393; all criteria (W105.1–W105.7) verified. -- **Done when:** - - W105.1 Canonical-to-human generation produces no diff on a second run, and the generated - pages contain every amended contract/slice section with no hand-edited generated copy. - - W105.2 Documentation validation passes generated-file freshness, internal/external links, - anchors, terminology, navigation, and guide-stamp checks from a clean checkout. - - W105.3 README, public guide, roadmap and landing page agree that the engine has three kinds, - the in-repository play surface is retired, published content is externally owned, and - `0.11.0` is a candidate rather than an already published release. - - W105.4 Every public SessionStore, projection, outcome, profile and migration example compiles - against the packed candidate; no example uses a removed synchronous signature or reads - registry/kind state across a client boundary. - - W105.5 The landing and documentation entry pages are rendered at the repository's committed - phone and desktop widths with no horizontal overflow, clipped navigation, unreachable - primary link, or overlap; reviewed screenshots are committed or attached as immutable - CI evidence. - - W105.6 The customer/alternative/monetization thesis is either stated in the public brief or - explicitly deferred with reasoning and an owner; it is no longer absent from design. - - W105.7 `npm --prefix site run check`, `npm --prefix site run build`, and - `./build/Test-Documentation.ps1` pass using the same generated artifact. -- **Out of scope:** redesigning the site, adding a playable client, changing product strategy - beyond making its current status explicit, deploying, or publishing the npm package. - -### [x] W106 — Tracker Evidence Closure {#w106} - -**Delivers:** Maintainers can trust that every resolved engine issue links to immutable evidence, -while genuinely unavailable Presentation work remains visibly blocked instead of falsely closed. - -- **Spec:** this ledger's stable criterion-id rules, `CLAUDE.md`'s tracking conventions, and - the issue/mirror evidence produced by W93–W105. -- **Touches:** GitHub issue/project state and labels; `design/state/work/` mirrors and index; - canonical decisions/slices only where the recorded outcome requires correction. -- **Depends on:** [W93](#w93)–[W105](#w105). It closes evidence; it does not substitute issue - closure for unfinished implementation. -- **Status:** Complete. Closed via #394; all criteria (W106.1–W106.7) verified. -- **Done when:** - - W106.1 A frozen inventory records every open engine issue number, title, current state, - owning W criterion or explicit non-programme disposition, and immutable implementation, - decision, supersession, or external-blocker evidence. - - W106.2 An issue is closed as implemented only when each of its acceptance checkboxes is - satisfied by a merged commit/PR and passing verification; stale and dissolved issues - cite the exact later contract/commit that removed their premise. - - W106.3 Decision-only issues cite the canonical decision that answers them and no longer - retain contradictory open-register text; externally owned issues cite the companion - issue/commit and remain open when that work is not actually complete. - - W106.4 The Presentation spike remains open with its unavailable/blocked state and transfer - target unless the Presentation repository and authority are genuinely available; no - engine issue or slice claims to implement it. - - W106.5 `/track` reports no missing issue for W93–W108, no criterion-id drift, no stale issue - body, and no implemented unit whose heading remains unchecked. Historical pre-tracker - units retain their recorded exemption rather than receiving fabricated retroactive work. - - W106.6 Work mirrors reproduce the live tracker state at one named commit/time, preserve - immutable evidence links, and a second mirror run is a no-op. - - W106.7 The repository's design-state checks either pass fully or every remaining - tool/content incompatibility has one owning issue and an explicit unavailable result; - `ContractListUnreadable`, `ProjectorFailed`, and false `TrackerUnavailable` are not - reported as successful checks. -- **Out of scope:** closing blocked work to make counts green, rewriting historical issue - narratives, implementing any unresolved issue inside the tracking pass, or transferring - work to an unavailable repository. - -### [x] W107 — 0.11 Release Candidate Verification {#w107} - -**Delivers:** Release maintainers can judge one additive candidate from a complete, reproducible -verification record covering the engine and every supported delivery surface. - -- **Spec:** [W104](#w104)'s compatibility manifest, [W105](#w105)'s publication artifact, and - the repository release/check matrices referenced by `CLAUDE.md`. -- **Touches:** verification commands and CI workflows only as needed to run the existing - engine, host, site, docs, container, consumer and companion gates against one candidate. -- **Depends on:** [W106](#w106), so verification starts with tracker/design truth reconciled. -- **Status:** Complete. Closed via #395; all criteria (W107.1–W107.7) verified. Rerun against the - `0.11.0` candidate `2b525d3`. W107.1, W107.2, W107.3, W107.4, W107.6 and W107.7 all hold; - the record is - `.claude/release-candidate-w108.json`. W107.3's production Docusaurus build and W107.4's - container smoke could not run locally — no Docker daemon — and were established instead as - required checks on PR #471, green at branch head `14f4323`; re-packing there reproduced the - candidate archive's digest bit-for-bit, so nothing in the archive moved between the two. - **W107.5 now holds.** Its GameOfLife half was resolved first: that companion did not - compile against the candidate because `stable-life.ts` lacked the `projects` and - `businesses` that [W101](#w101) made required, and, once that cleared, the event-chain - declarations [W102](#w102) requires. Both were **pre-existing, not caused by `0.11.0`**, and - both are fixed in the companion by SubZeroDev.GameOfLife#120 (`8ea9441`). GameOfLife now - compiles against engine `6910bf5`, whose `src/engine` is byte-identical to the candidate's. - SubZeroDev.Platform's durable/Postgres profile was the other half — unavailable at report - time for want of a Docker daemon, and green (197 of 198, 1 pre-existing skip) once Docker - became available in this environment and the same committed `docker-compose.yml` service - was brought up. W107.4's own text was corrected by - PR #461 before this rerun, so the rerun exercised the corrected requirement and added no - engine API to the static host; the superseded failure in - `.claude/release-candidate-w107.json` is history, not an open finding. -- **Done when:** - - W107.1 A candidate commit and one clean packed archive digest are recorded; every consumer - and host test uses that archive rather than a workspace import or a separately packed - copy. - - W107.2 A clean `src/engine/` install passes typecheck, determinism lint, unit/integration - tests, build, archive-content assertion, replay corpus, and the W104 compatibility sweep. - - W107.3 A clean site install passes lint/typecheck/unit/browser checks and production build; - documentation conversion and validation run from canonical sources and are no-op on a - second generation. - - W107.4 The static host builds and its container smoke proves `200` for `/`, `/roadmap/`, - `/docs/`, liveness and readiness, `404` for a named unknown route, and a negative - fixture that fails to start on a missing required artifact, with no source tree - mounted at runtime. Per `15-platform-static-host.md` §8, the host receives no action, - owns no session, and exposes no engine API — session creation, action - preview/submission, save/resume, and the W98/W99 operations are contract-gated on the - engine's own API, not this host's, and this criterion must not require them. - - W107.5 Packed text/MCP consumers and the lockfile-backed consumer smoke compile and run; - Platform, service-contract, Adventures and GameOfLife companion checks name exact - compatible versions or commits and have no unresolved contract mismatch. - - W107.6 Required CI workflows are green at the candidate commit, including Pester with its - pass/skip counts and documentation/deploy Chromium setup; a skipped or unavailable - check is listed as such and never rendered as a pass. - - W107.7 A release-candidate report records command, exit code, duration, artifact digest and - evidence URL for every row, with zero unexplained failure or dirty-tree substitution. -- **Out of scope:** fixing a failed gate inside the verification unit, changing candidate - scope, tagging, creating a GitHub release, deploying, or publishing to npm. - -### [x] W108 — Publish 0.11 Readiness {#w108} - -**Delivers:** The release owner gets a public roadmap and exact checklist authorizing `0.11.0` -as the next publication, while the actual publish remains a separate explicit action. - -- **Spec:** the repository's version/tag/release contract, [W107](#w107)'s candidate report, - and the public roadmap/release material reconciled by [W105](#w105). -- **Touches:** canonical roadmap/release checklist, README/landing release status, package and - changelog metadata required for a future user-triggered publication; no remote release - mutation. -- **Depends on:** [W107](#w107) green and every blocker in [W106](#w106) explicitly resolved or - accepted by the user. -- **Status:** Complete. Closed via #396; all criteria (W108.1–W108.6) verified. `0.11.0` is set - in the package and lockfile, and the - roadmap, README, generated docs and landing page all name it as the next authorized - publication (W108.1). The checklist is `RELEASE.md`, naming the candidate - `2b525d38d6876829b35e9f5ff5fcf22af421f082`, the archive - `the-running-dev-game-engine-0.11.0.tgz` and its SHA-256, the required `v0.11.0` tag, the - registry and its read-only availability result, the release-note source, and the - non-publishing commands. W108.4 holds — the archive ships exactly three kinds, two entry - points, no campaign content the engine does not own, and no play surface. W108.5 holds: the - guards moved to `src/engine/scripts/verify-release.mjs`, run in a non-publishing validation - path, and each has rejected a mismatched tag, a dirty tree and a substituted archive. - W108.6 holds — nothing was published, tagged, released or deployed. **W108.2 and W108.3 now - hold too**, both resolved 2026-09-13 by the user in `90-decisions.md`: W108.2 accepts - `RELEASE.md` §7's consumer-side rollback as the procedure outright rather than holding out - for a tested GitHub Packages deprecation, and W108.3 is satisfied against W98.2 as amended - that day, scoping browser-shelf parity to the client contract governing - `SubZeroDev.Adventures` instead of this repository. Every other blocker the rerun found has - closed: W107.3, W107.4 and W107.6 on PR #471's own CI, W107.5's GameOfLife half with - SubZeroDev.GameOfLife#120, and W107.5's Platform half once Docker became available. - **Readiness is no longer blocked; publication remains a separate explicit action.** -- **Done when:** - - W108.1 The canonical roadmap, README, generated public docs and landing page all identify - `0.11.0` as the next authorized publication and describe the same additive scope and - compatibility claim. - - W108.2 The release checklist names the exact candidate commit/archive digest, required tag - and package versions, changelog/release-note source, registry access, dry-run commands, - post-publish verification, and rollback/deprecation procedure. - - W108.3 Every W93–W107 unit is complete with immutable evidence, or the checklist names the - remaining blocker and does not claim readiness; no unchecked criterion is hidden by a - summary status. - - W108.4 Package metadata, archive contents and public documentation agree on version, - visibility, supported entry points, three shipped kinds, external content ownership, - and the retired play surface. - - W108.5 The release workflow's tag/version and clean-build guards are exercised in a - non-publishing validation path against the candidate and reject a mismatched tag or - dirty/repacked artifact. - - W108.6 No npm publish, Git tag, GitHub release, deployment, DNS change, or external package - release occurs in this unit; the final checklist states that publication requires a - separate explicit user action. -- **Out of scope:** performing the publication, choosing credentials, deploying the site, - announcing the release, or adding any feature/fix after the verified candidate. - -### Depth: The Five GameOfLife Engine Blockers - -Five contract amendments dated 2026-09-07 in `90-decisions.md` each close a gap a *Life in the -Fast Lane* issue named, and **every one is contract-only**: the types and rules are stated, and -nothing in `src/engine` reads them. They are **0.12 scope** — [W108](#w108)'s own scope forbids -adding a feature after the verified 0.11 candidate — and they stay **1:1 with their upstream -issues**, so one engine unit answers one game issue rather than one unit answering two. - -Ordered by blast radius, not by issue number: the two that change no state and move no fixture -first, the two that move cash trajectories next, and the one that bumps `kindVersion` last and -alone. `plans/50-gameoflife-engine-blockers.md` carries the sizing this ordering came from, and -the four user decisions behind it. **[W111](#w111) closes the already-open engine issue #418**; -the other four have no engine issue yet. - -### [x] W109 — A Uniform That Makes Its Wearer More Employable {#w109} - -**Delivers:** Lets a campaign author write an item or trait whose effect is on how the world -regards the player — a work uniform that makes its wearer more employable, a scandal that fades -when it expires — instead of only on their needs, attributes and skills. Reputation is something -the game already stores and already reads; until now nothing an author could write was able to -move it. - -- **Spec:** [§6.1](10-simulation-kind.md#61-base-and-derived-values)'s `DerivedPath` union and - its writable/formula-only partition, [§6.2](10-simulation-kind.md#62-the-shared-actor-shape)'s - stored `reputation` record, [§7.1](10-simulation-kind.md#7-content-definition-types)'s - writable-target table, and `90-decisions.md`'s 2026-09-07 `W105.1` entry. -- **Touches:** the kind's derived-value layer and its Tier 1 modifier-target check, plus their - tests. No state shape change, no campaign change, no fixture regeneration. -- **Depends on:** [W108](#w108), for scheduling only — this is the first unit of 0.12 and must - not land against the verified 0.11 candidate. -- **Status:** Done — [PR #478](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/478). -- **Done when:** - - W109.1 A campaign whose `Modifier.target` is `player.reputation.` passes Tier 1 - validation, and the same campaign is rejected before this unit; the writable prefix set - grows by exactly one entry and the `calendar.committedTimeUnits` exception is unchanged. - - W109.2 Resolving `player.reputation.` returns the stored base with every active - modifier layered over it in §6.1's fixed `add`/`subtract` → `multiply` → `set` order, - clamped to `0–100` — the same treatment `player.skills.*` already gets. - - W109.3 When the effect expires, the same read returns the stored base again, unchanged; - nothing was written back to state at any point. - - W109.4 Resolving a reputation key the actor does not store returns no value rather than - `0`, matching the `player.skills.*` precedent line for line. - - W109.5 A `Modifier` targeting any of the four formula-only paths still fails Tier 1 - `read_only_field`; state both counts — targets newly accepted, and targets still - rejected. - - W109.6 The replay corpus is byte-identical: no committed fixture moves, because no existing - campaign targets reputation. -- **Out of scope:** travel-time effects — issue #108's bicycle is deliberately left unamended - and stays unexpressible after this unit; consuming reputation through - `CheckModifier.source`/`PerformanceFactor.source`, neither of which is dispatched by any - resolver today; authoring a campaign that uses the new target. - -### [x] W110 — An NPC Who Already Remembers You {#w110} - -**Delivers:** Lets a scenario begin with an NPC who already has history with the player — a -landlord who already distrusts them, a rival carrying an old grudge — instead of every -acquaintance starting blank and accumulating a past only through play. - -- **Spec:** [§7.7](10-simulation-kind.md#77-npcs--definition-and-runtime-state)'s - `NPCDefinition.startingMemories` and the `NPCMemory` shape it carries, §14's - every-`LocKey`-resolves rule, and `90-decisions.md`'s 2026-09-07 `W105.4` entry. -- **Touches:** the kind's NPC content type and whichever reducer first materialises an NPC's - runtime state, plus their tests. -- **Depends on:** none. -- **Status:** Done — [PR #479](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/479). -- **Done when:** - - W110.1 A campaign may author starting memories on an NPC definition; the resulting NPC's - memory list equals the authored list in authored order, with the author-supplied ids - preserved and no id minted from an `IdSource`. - - W110.2 A definition declaring no starting memories, or an empty list, produces an empty - memory list — today's behaviour — and every committed simulation fixture replays - byte-identically. - - W110.3 Memories are seeded once, at NPC creation: a memory removed or expired during play - does not reappear in a later week. - - W110.4 A starting memory whose `descriptionKey` resolves to nothing in the string table - fails Tier 1 validation; state both counts — campaigns accepted, and campaigns rejected - by this check. -- **Out of scope:** what a week does to memories or relationships — the `relationships` - end-of-week system is contract prerequisite **P2** and has no rule to implement; memory - expiry mechanics; authoring starting memories into the *Stable Life* scenario. - -### [x] W111 — Conditions That Can Ask "Do You Own One?" {#w111} - -**Delivers:** Lets an author write a goal or event that asks whether something exists in the -player's world at all, and how many there are — a pending job application, an owned car, a -course they are enrolled in. Every condition until now could only compare one value against -another, which is why four planned *Stable Life* events could not be written. - -- **Spec:** [§8.2](10-simulation-kind.md#82-collections-for-existscount-w1055)'s closed - seven-path table, `04 §18`'s already-frozen `ExistsCondition`/`CountCondition` and - `ConditionResolver.collection` seam, §14's Tier 1 list, and `90-decisions.md`'s 2026-09-07 - `W105.5` entry. Closes engine issue #418. -- **Touches:** the kind's condition resolver, which throws unconditionally on any collection - today, and its Tier 1 validator, plus their tests. -- **Depends on:** none. -- **Status:** Done — [PR #481](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/481). -- **Done when:** - - W111.1 Each of §8.2's seven declared collection paths resolves to its state array and - supports both `exists` and `count`; state the count of paths accepted. - - W111.2 A `where` clause reads fields relative to a single array element: an `exists` over - the player's inventory testing `definitionId` `in` a list of ids matches when one of - those items is owned and does not match when none is. - - W111.3 Naming any other path — a scalar path, an unlisted array, a typo — fails Tier 1 - `unknown_collection` at load time, never at first evaluation; state both counts, cases - accepted and cases rejected, and prove the load-time-not-runtime claim with a condition - placed on a branch the test never evaluates. - - W111.4 A `count` condition compares the match total against a number and is correct at - zero, at the comparison boundary, and above it. - - W111.5 A `where` naming a field the array element does not carry — an item's `category`, - which lives on the definition and never reaches the resolver — does not match, and a - test pins that outcome rather than leaving it to be discovered by an author. - - W111.6 No new core condition operator is introduced; `04 §18`'s `Condition` type is - unchanged by this unit. -- **Out of scope:** joining a collection member against its content definition, which would need - a core-level `ConditionResolver` widening and is recorded as an open item; adding a new - core operator; authoring the four *Stable Life* events themselves. - -### [x] W112 — A Car That Costs Money to Run {#w112} - -**Delivers:** Makes owning something cost money week after week, not only at the moment of -purchase. A vehicle, a subscription, anything with a declared running cost now drains cash every -week it is owned and working — which is what turns a cheap car with expensive upkeep into a real -decision instead of a free asset. - -- **Spec:** [§7.5](10-simulation-kind.md#75-items)'s `ItemDefinition.weeklyCostCents`, - [§3](10-simulation-kind.md#3-the-turn-is-a-week)'s fixed end-of-week system order, and - `90-decisions.md`'s 2026-09-07 `W105.2` entry. -- **Touches:** the `inventory` end-of-week system, its tests, and the replay fixtures whose cash - trajectories move. -- **Depends on:** none mechanically. It is the first of the two units that move committed - fixtures, so it is scheduled after [W111](#w111) to keep the fixture churn off the three - units that cause none. -- **Status:** Done — [PR #490](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/490). -- **Done when:** - - W112.1 The weekly cost of every owned item is summed and charged against cash in the same - pass that already decays condition, running after income and before housing — §3's - existing order, unchanged. - - W112.2 The charge is per owned instance, not per definition: a player holding three - instances of one definition is charged three times. - - W112.3 An item declaring no weekly cost contributes zero, and an item at zero condition - contributes zero — the same broken-item rule that already stops its effects applying. - - W112.4 The charge is unconditional and may take cash negative; no missed amount is recorded, - no arrears field is added, and nothing is repossessed or disabled for non-payment. - - W112.5 The regression test is the charge itself, verified by reverting the charge and - confirming the test fails. - - W112.6 Every regenerated replay fixture is named in the pull request with the cash delta - that explains it; no fixture is regenerated without an explanation. -- **Out of scope:** any arrears, repossession or collections mechanism for unpaid running costs; - charging conditional on the item having been used that week; branching charge semantics on - an item's free-text `category`; housing's own utilities and transport, which are - [W113](#w113). - -### [x] W113 — Utilities and Transport on the Weekly Bill {#w113} - -**Delivers:** Makes the weekly cost of a home read like an actual bill — rent, utilities and -transport as separate lines — and gives owning a vehicle a payoff, because the transport line is -waived for anyone who has one. A player weighing a cheap flat with a long commute against a -dearer one nearby finally has the numbers in front of them. - -- **Spec:** [§7.4](10-simulation-kind.md#74-housing)'s `utilitiesCents`/`transportCents` and the - reserved `"vehicle"` tag, [§6.9](10-simulation-kind.md#69-housing)'s `HousingState`, - [§3](10-simulation-kind.md#3-the-turn-is-a-week)'s `housing` and `finance_reconcile` - systems, `04 §10.2`'s `kindVersion`/`Kind.migrateState` axis, and `90-decisions.md`'s - 2026-09-07 `W105.3` entry including its same-day revision. -- **Touches:** the kind's housing content and runtime types, the `housing` end-of-week system - and the item definitions threaded into it, the kind's version and migration hook, migration - tests, and the replay fixtures whose cash trajectories move. -- **Depends on:** [W112](#w112). **Do this one last and alone** — it is the only one of the five - that changes a persisted state shape, so batching it means either two version bumps or a - unit that does not fit one session. -- **Status:** Done — [PR #491](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/491). -- **Done when:** - - W113.1 A housing definition may declare utilities and transport costs; both absent means - zero, and a campaign declaring neither charges exactly what it charges today. - - W113.2 Both are stamped onto the player's housing state at move-in, exactly as rent already - is; editing the definition afterwards does not change an existing tenancy's charge. - - W113.3 The weekly charge is rent plus utilities plus effective transport as one combined - levy against cash, in the pass rent already used, and cash may go negative — the same - "wages before costs" ordering rent alone already proves. - - W113.4 A shortfall against the combined total advances the same - overdue/missed-payments/eviction ladder rent alone drove; no separate utilities or - transport arrears state exists, and no second reconciliation rule is added. - - W113.5 Transport is charged as zero for a week in which the player holds at least one - inventory item above zero condition whose definition tags include the literal - `"vehicle"`; a broken vehicle does not waive it, and utilities are never waived. - - W113.6 The kind version is bumped and a migration is attached; a fixture holding a save - written at the previous version round-trips through load with both new fields defaulted - to zero. - - W113.7 Every regenerated replay fixture is named in the pull request with the cash delta - that explains it. -- **Out of scope:** separate arrears tracking or a bill-paying action for utilities and - transport; promoting `"vehicle"` from a reserved literal to a type; scaling utilities by - housing tier in engine code — the game's own baseline scales it per definition, as - authored numbers; the per-actor travel-time mechanism still deferred by [W109](#w109). - -### [x] W114 — The NPCs Are Actually There {#w114} - -**Delivers:** Puts every NPC a campaign declares into the running game from week one, carrying -the history an author gave them. Until now [W110](#w110)'s starting memories were validated and -then never used, because nothing created an NPC's runtime state at all. - -- **Spec:** [§7.7](10-simulation-kind.md#77-npcs--definition-and-runtime-state)'s `NPCState` - and its seeding rule, [§8.2](10-simulation-kind.md#82-collections-for-existscount-w1055)'s - `world.npcs` collection, and `90-decisions.md`'s 2026-09-15 entry routing it here. Closes - the NPC half of engine issue #425. -- **Touches:** the kind's `initialState`, its tests, and the golden snapshots of campaigns that - declare NPCs. -- **Depends on:** [W110](#w110). -- **Status:** Landed in [#495](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/495). -- **Done when:** - - W114.1 A new game holds one NPC state per NPC definition, in content order, keyed by the - definition's id, with its role, availability and starting memories taken from that - definition. - - W114.2 A campaign declaring no NPCs starts with an empty collection, as today. - - W114.3 Seeded state shares no array with the authored definition: changing an NPC's memories - or availability during play cannot reach back into content. - - W114.4 An NPC's own state never carries its definition's initial relationship. - - W114.5 Every moved golden snapshot or replay fixture is named in the pull request, and each - moves in `world.npcs` alone. -- **Out of scope:** seeding `world.locations`, which is blocked on campaigns whose starting - location has no definition and stays with #425's locations half; any end-of-week system - that reads or changes NPC state; projecting NPCs into the view. - -### Depth: The World-Graph Client Read Model - -One unit. `90-decisions.md`'s 2026-09-15 `/reconcile` found that -[12 §10](12-world-graph-kind.md#10-projection)'s Revision 5 view is contracted and marked -unbuilt, and routed its implementation here. It is the only item that entry routed to -`/slices` still unimplemented; its sibling, NPC seeding, landed as [W114](#w114). The third -entry of that date — a Tier 1 check for `where` fields a collection's item type does not -declare — went to `/contract` first, was specified on 2026-09-24, and is a separate unit, not -part of this one. - -### [x] W115 — A Resort a Client Can Actually Draw {#w115} - -**Delivers:** Lets anyone building a screen for a world-graph game — a resort map, an inspector -panel, a build menu — draw everything the player can see and offer every action the player can -take, from what the engine hands them alone. Today the engine hands over counts and ids, so a -client cannot place a building on a map, show a guest's mood, or explain why a building is -locked without reading the game's internals. - -- **Spec:** [12 §10](12-world-graph-kind.md#10-projection) — the Revision 5 `WorldGraphView`, - its *Derivation and ownership*, *Completeness invariants* and *Deliberate exclusions* - paragraphs, and the ordering rules; [§11](12-world-graph-kind.md#11-reason-codes) for the - `blockedBy` vocabulary; `90-decisions.md`'s 2026-09-15 entry routing it here. -- **Touches:** the kind's projector (`src/engine/src/kinds/world-graph/view.ts`) and the - `WorldGraphView` type in its state module, their tests, and any golden snapshot that - records a world-graph view. -- **Depends on:** none. -- **Status:** Not started. -- **Done when:** - - W115.1 The projected view carries every field §10's `WorldGraphView` declares, with the - declared types: `finances` gains `revenueTotalCents`/`expensesTotalCents`; the view gains - `scenario`, `definitions`, `staffOptions`, `constructionSites`, `guests` and `incidents`; - `map` carries `id`, name/description keys, `terrain`, `paths` and placed `scenery`, and - its `zones` are full `Zone` records rather than ids; each building carries position, - size, rotation, full queue, prices and inventory; each staff member carries position, - task and `tasksCompleted`; each alert carries `entityId`. `npm run typecheck` passes with - the §10 declaration in place of the old one. - - W115.2 The view contains none of §10's *Deliberate exclusions*: a test serializes the view - of the MVP campaign after at least one incident has occurred and asserts that none of - the state or campaign field names backing those exclusions — RNG state, - `nextEntityOrdinal`, incident weights and triggers, scheduled changes, failure and - objective conditions, placement rules, adjacency effects, guest preference profiles, - utility curves and scores, entity path caches, hidden achievements, unlock bookkeeping - — appears as a key at any depth. The pull request lists the exact key names checked. - - W115.3 For the MVP campaign, every build definition id the reducer accepts for `build`, - every role id it accepts for `hire_staff` and every product id it accepts for - `set_price` appears in `buildOptions`, `staffOptions` and `definitions.products` - respectively. Locked entries are present with `canBuild`/`canHire` false. - - W115.4 For every definition and role, `canBuild`/`canHire` is false exactly when submitting - it at a placement-valid position (or to a valid assignment) is rejected, and `blockedBy` - equals the set of placement-independent §11 codes the reducer returns — asserted for - at least `building_locked`, `building_limit_reached`, `staff_limit_reached` and - `insufficient_funds`, each driven by a state constructed to trigger it. - - W115.5 Every building, staff member and alert id that `demolish`, `fire_staff`, - `assign_staff`, `open_building`, `close_building`, `set_price` or `dismiss_alert` would - accept in a given state appears in the corresponding view collection, and no id the - reducer would reject with `unknown_entity` does. - - W115.6 `alerts` omits every alert with a non-null `dismissedAtTick` or `clearedAtTick`, and - `incidents` omits every incident with a non-null `resolvedAtTick`: a test dismisses one - alert and resolves one incident and asserts both disappear from the next projection. - - W115.7 `definitions` is the smallest safe catalog §10 names and no larger: `guestArchetypes` - equals the archetypes reachable from the current scenario's pool, `products` equals the - products referenced by `buildOptions`, and `incidents` equals the definitions of - currently unresolved incidents — each asserted by exact id list against the MVP - campaign. - - W115.8 Ordering follows §10: every definition and entity collection is sorted by id; - terrain cells, zone cells, spawn points and exits are row-major (`y`, then `x`); path - edges are sorted by `from` then `to`; rotations are numeric ascending; per-product and - per-meter value lists are sorted by definition id; staff requirements by role id; and - `queue.guestIds` equals the state's FIFO order unchanged. A test builds state whose - arrays are deliberately out of order and asserts each sorted result. - - W115.9 Projection is pure and non-aliasing: projecting the same state and campaign twice - yields structurally equal views, and mutating any array of a returned view leaves both - the state and a fresh projection unchanged. - - W115.10 No save or campaign schema moves: `kindVersion`, every committed save fixture and - every replay fixture are byte-identical before and after. Every moved golden snapshot is - named in the pull request and moves only inside a world-graph view. - - W115.11 §10's *Specified, not yet implemented* status paragraph is deleted from - `design/20-contract.md` in the same pull request, and the human docs are regenerated. -- **Out of scope:** exporting the helper types (`WorldGraphViewText`, `WorldGraphBuildOption`, - `WorldGraphStaffOption`, `WorldGraphViewMeterDefinition`, `WorldGraphViewValue`) from the - package root, which the contract does not list; per-building sales history or guest - thought history, which §10 says need a state-and-save change first; placement-dependent - build previews, which stay `previewAction`'s; any change to a client, including - SubZeroDev.Adventures; the simulation kind's `where`-field Tier 1 check. - -### Depth: The Simulation Campaign's Own Rules - -Four units. Two `/contract` passes routed work here, and neither has a unit yet. The 2026-09-24 -entry specified the Tier 1 check for `where` fields -([§8.2](10-simulation-kind.md#82-collections-for-existscount-w1055)), which [W115](#w115) named -as separate. The 2026-09-28 entry specified #524's campaign physics -([§7.14](10-simulation-kind.md#714-remaining-campaign-physics-524)): the numbers a campaign -cannot yet set. - -Ordered by risk. [W116](#w116) is fully contracted, depends on nothing, and closes a gap where a -campaign that loads can still throw during play. [W117](#w117) wires one field so it can test -the assumption all of §7.14 rests on: that a per-key default resolved at use reproduces today's -output byte for byte. If it cannot, #524's stop condition applies, and W118 and W119 should not -start. W118 covers the week's own rules and W119 the per-action prices. Neither W118 nor W119 -depends on the other. All three physics units wait on P8 (under *Contract Prerequisites* -above), the reason code §7.14's validation never named. - -### [x] W116 — A Campaign Cannot Ask About a Field Its Collection Lacks {#w116} - -**Delivers:** As a campaign author, a condition that asks about a property its collection's -items do not have is rejected when the campaign loads, and so is a comparison on an optional -property that would crash mid-game. The rejection says which of the two went wrong. Today the -first quietly matches the wrong things and the second fails during play. - -- **Spec:** [10 §8.2](10-simulation-kind.md#82-collections-for-existscount-w1055), - [§10](10-simulation-kind.md#10-reason-codes) and - [§14](10-simulation-kind.md#14-validation); `90-decisions.md`, 2026-09-15 and 2026-09-24. -- **Touches:** the simulation validator and reason-code registry, and their tests; the §8.2 - status paragraph and §10 markers in `design/20-contract.md`; the regenerated human docs. -- **Depends on:** none. -- **Status:** Not started. -- **Done when:** - - W116.1 For each of §8.2's eight collections, a test shows a `where` `field` naming a scalar - property of the collection's item type passing Tier 1. The same test shows a field the - item type does not declare failing with `unknown_collection_field`. - - W116.2 Each of the following fails with `unknown_collection_field`: - - a dotted path against `world.npcs` into `memories`, `availability` or `flags` (for - example `flags.met`); - - a one-segment field naming a property of array or object type. - - W116.3 The check reaches every `field` leaf at any depth under `all`, `any` and `not`. A - `where` nested two or more levels deep is rejected for its innermost bad field. - Inside a `where`, a nested `exists` or `count` is checked against its own collection's - item type, not the outer one: a field legal only on the outer type is rejected there. - - W116.4 An optional property passes under `equals`, `not_equals`, `in` and `not_in`. It fails - with `optional_field_operator` under each other operator, with one test case per - operator. - - W116.5 The engine's table of legal fields is typed against each item type's properties, so - leaving a property out is a compile error. A type-level test proves this, for example - with `@ts-expect-error`. - - W116.6 Both codes are registered in `reasons.ts` with messages. - - W116.7 Every committed simulation campaign, including Stable Life and every test campaign, - loads with no new finding. The pull request states how many rejecting cases and how many - passing cases the new tests contain. - - W116.8 In the same pull request: - - §8.2's *Status: contracted, not built* paragraph is deleted; - - both "contracted, not built" markers in §10's Tier 1 table are deleted; - - the human docs are regenerated. - - `./build/Test-Documentation.ps1` passes, and `npm run typecheck`, `npm run lint` and - `npm test` pass from `src/engine/`. -- **Out of scope:** - - An operator that does not fit a *present* field's type, for example `has_tag` against a - number. §8.2 leaves this out explicitly. - - Any change to the runtime condition evaluator, which stays non-throwing. - - Collection-name validation, which is [W111](#w111)'s. - - Condition validation in other kinds. - -### [x] W117 — Two Campaigns, One Lever, Different Weeks {#w117} - -**Delivers:** As a game author, I can set how fast each need drains per week in my own campaign. -Two campaigns that differ only in that setting play out differently. Every campaign that does not -set it plays exactly as it does today. - -- **Spec:** [10 §7.14](10-simulation-kind.md#714-remaining-campaign-physics-524) and - [§14](10-simulation-kind.md#14-validation); `90-decisions.md`, 2026-09-28; - [#524](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/issues/524). -- **Touches:** the simulation campaign and source types, the source builder, the validator, and - the `needs` end-of-week system; their tests and synthetic fixtures. -- **Depends on:** P8 under *Contract Prerequisites* above, the reason code for an invalid - physics value. -- **Status:** Not started. -- **Done when:** - - W117.1 `needDriftPerWeek` is declared on `SimulationCampaign` and `SimulationCampaignSource` - exactly as §7.14 states. The builder copies it only when it is supplied: - - an omitted field stays absent from the built campaign; - - a partial record keeps only the keys supplied. - - W117.2 The `needs` system resolves each need key at the point of use: - - a supplied key takes the campaign's value; - - an omitted key, or an omitted field, takes §7.14's default (health −1, energy −3, - happiness −2, satiety −4, stress +2); - - an explicit `0` for a key leaves that need undrifted. - - W117.3 Take two synthetic campaigns that differ only in `needDriftPerWeek: { satiety: -5 }` - versus omission. Give them the same seed, the same `end_week` log, and a starting satiety - away from either clamp. After one `end_week`: - - base satiety differs by exactly 1; - - the `need_drift` change for satiety differs by exactly 1; - - every other need and every other change is identical. - - W117.4 The W117.3 divergence is identical on a repeat run, and across a save/load cut taken - mid-log: `serialize()` output matches the uncut run. - - W117.5 A campaign that omits the field is byte-identical to the committed replay and golden - oracles, and no committed fixture file changes. - - W117.6 Tier 1 rejects a non-finite delta, a non-integer delta and an unknown need key. Each - rejection uses P8's code and a path naming the key. The pull request states how many - rejecting cases and how many passing cases the tests contain. - - W117.7 `npm run typecheck`, `npm run lint` and `npm test` pass from `src/engine/`. -- **Out of scope:** - - Every other §7.14 field. - - Any change to a default number, or to Stable Life's content. - - Balancing, which is game-side (`90-decisions.md`, 2026-09-27). - -### [x] W118 — The Week's Own Rules Belong to the Campaign {#w118} - -**Delivers:** As a game author, I can set my campaign's weekly time budget, late-fee rate and -eviction ladder. I can also set how job performance drifts and how much each event shifts the -world's strangeness. A save made under one ladder still loads and progresses under another. - -- **Spec:** [10 §7.14](10-simulation-kind.md#714-remaining-campaign-physics-524) and - [§14](10-simulation-kind.md#14-validation); `90-decisions.md`, 2026-09-28. -- **Touches:** the simulation campaign and source types, the source builder and the validator; - the initial state and start-of-week time budget; the `finance_reconcile`, `employment` and - `events` end-of-week systems; their tests and synthetic fixtures. -- **Depends on:** P8, and [W117](#w117), whose omission oracle this extends. -- **Status:** Not started. -- **Done when:** - - W118.1 These six fields are declared on the campaign and source types as §7.14 states: - `weeklyTimeUnits`, `lateFeeBasisPoints`, `evictionStages`, `performanceDriftRate`, - `performanceWorkBonus` and `strangenessPerEvent`. The builder copies each only when it - is supplied. - - W118.2 Each field has a fixture pair that differs in that field alone. Each pair diverges - only on the path that field governs: - - the week's time budget; - - the late fee in cents; - - the eviction stage; - - employment performance; - - `strangenessBase`. - - W118.3 Under a shortened ladder `none, penalty, evicted`: - - a missed week advances one listed stage; - - a loaded state at the omitted `warning` stage advances to `penalty`; - - `evicted` stays terminal; - - `pay_bills` still resets the stage to `none`. - - W118.4 With `performanceDriftRate: 0`, a week without work leaves performance unchanged. With - `1`, such a week moves it to the job's `weeklyDriftToward`. Rounding happens once, as - §7.14 states. - - W118.5 Tier 1 rejects each of the following, using P8's code and a path: - - a `weeklyTimeUnits` that is not a positive integer; - - a negative or non-integer `lateFeeBasisPoints`; - - a `performanceDriftRate` that is non-finite or outside [0, 1]; - - a non-integer bonus or strangeness step; - - a stage sequence that does not start at `none`, does not end at `evicted`, repeats a - stage, breaks canonical order, or names an unknown stage. - - The pull request states how many rejecting cases and how many passing cases the tests - contain. - - W118.6 A campaign that omits all six fields is byte-identical to the committed replay and - golden oracles. Default paths keep their `StateChange` order and shape, and no committed - fixture file changes. - - W118.7 `npm run typecheck`, `npm run lint` and `npm test` pass from `src/engine/`. -- **Out of scope:** - - Per-action time costs and need deltas, which are [W119](#w119)'s. - - Any change to the persisted `EvictionStage` union. - - Any change to a default number, and any balancing. - -### [x] W119 — Every Fixed Action Price Is the Campaign's {#w119} - -**Delivers:** As a game author, I can price every fixed-cost action in time, including actions -that are free today. I can also set what eating, resting and exercising do to needs, how much -socializing moves a relationship, and how fast applications, raises, study and projects -progress. Every action I leave alone costs exactly what it costs today. - -- **Spec:** [10 §7.14](10-simulation-kind.md#714-remaining-campaign-physics-524) and - [§14](10-simulation-kind.md#14-validation); `90-decisions.md`, 2026-09-28. -- **Touches:** the simulation campaign and source types, the source builder and the validator; - the action resolvers named by §7.14's `actionTimeCosts` keys and per-action fields; their - tests and synthetic fixtures. -- **Depends on:** P8, and [W117](#w117). -- **Status:** Not started. -- **Done when:** - - W119.1 These fields are declared on the campaign and source types as §7.14 states: - `actionTimeCosts`, `eatNeedDeltas`, `restNeedDeltas`, `exerciseNeedDeltas`, - `socializeAffinityGain`, `socializeTrustGain`, `applicationResolveWeeks`, - `negotiateRaiseBasisPoints`, `studyUnitsPerSession` and `projectProgressPerSession`. The - builder copies each only when it is supplied, and keeps partial records partial. - - W119.2 A table-driven test covers all twenty-six `actionTimeCosts` keys. For each, an omitted - key reproduces today's calculated time cost and audit shape exactly. - - W119.3 When a zero-default action such as `eat` is given a positive price: - - it checks available time, and fails with `insufficient_time` when time is short; - - it emits the spent-time change; - - `apply` consumes exactly the calculated value. - - An explicit `0` keeps today's shape. `eat` and `rest` still omit `calculatedTimeCost` - and the spent-time change; the other zero-default actions still report - `calculatedTimeCost: 0`. - - W119.4 Need-delta records resolve per key: supplying one `exerciseNeedDeltas` key changes - only that need. `restNeedDeltas.stress` is a signed delta, and its default of −5 lowers - stress. - - W119.5 Each of the six scalar fields has a fixture pair that differs in that field alone, and - each pair diverges only on the path that field governs. These action costs are unchanged - by every new field: - - `work`; - - `travel`; - - `maintain_item`; - - `work_on_project`; - - `respond_to_event`. - - W119.6 Tier 1 rejects each of the following, using P8's code and a path: - - a negative or non-integer action cost; - - an unknown action key or an unknown need key; - - a non-integer need delta or gain; - - a resolve-week, study or project value that is not a positive integer; - - a negative or non-integer raise in basis points. - - The pull request states how many rejecting cases and how many passing cases the tests - contain. - - W119.7 A campaign that omits every field is byte-identical to the committed replay and golden - oracles. Default paths keep their `StateChange` order and shape, and no committed fixture - file changes. `npm run typecheck`, `npm run lint` and `npm test` pass from `src/engine/`. -- **Out of scope:** - - The identity constants §7.14 lists as non-levers: `NO_MONEY_COST`, `NEW_ITEM_CONDITION`, - `NEW_ITEM_QUANTITY` and `INVESTMENT_INTEREST_RATE`. - - The repair formula. - - Content-owned costs. - - Any change to a default number, and any balancing. - -### Depth: Campaign Composition - -One unit. The 2026-10-07 entry for #293 and #292 decided that content crosses a campaign -boundary by composition at registry build. Its follow-up, #561, contracted the result. That -contract covers the core envelope ([04 §10.4](04-core.md#104-campaign-composition)), the -story-graph merge ([03 §1.1](03-story-graph-kind.md#11-modules-includes-and-attachments)) and -pack attachments ([11 §3a](11-content-packs.md#3a-attachments--reaching-into-a-campaign-a-pack-does-not-own)). -It is one unit because no part of it can be played without the others. An include with no merge -composes nothing, and a merge with no core envelope has nothing to call it. - -### [x] W120 — One Campaign Can Include Another {#w120} - -**Delivers:** As a campaign author, I can include another story-graph campaign as a module. -I name its exits and bind its inputs and outputs. As a pack author, I can attach a module to a -choice node in a campaign my pack does not own. Either way the player plays one campaign, in one -session, and the save format does not change. - -- **Spec:** [04 §3](04-core.md#3-the-kind-interface--the-seam), - [§10.4](04-core.md#104-campaign-composition), [§11](04-core.md#11-tiered-validation), - [§12](04-core.md#12-reason-codes-state-changes-messages), - [§17](04-core.md#17-identifier-conventions) and C17; - [03 §1.1](03-story-graph-kind.md#11-modules-includes-and-attachments), - [§5](03-story-graph-kind.md#5-consequences--typed-effects), - [§8.3](03-story-graph-kind.md#83-reason-codes) and - [§11](03-story-graph-kind.md#11-validation-story-graph-specific); - [11 §3a](11-content-packs.md#3a-attachments--reaching-into-a-campaign-a-pack-does-not-own), - [§6](11-content-packs.md#6-identity-and-why-determinism-needs-it) and - [§7](11-content-packs.md#7-validation); `90-decisions.md`, 2026-10-07 (both entries); - [#561](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/issues/561). -- **Touches:** the `Kind` interface and the core reason-code registry; the campaign and pack - types, `resolvePacks` and the pack digest; both validated registry entry points; the - story-graph kind's campaign and source types, source builder, consequences, interpolation, - validator and reason codes; their tests and synthetic fixtures; the regenerated human docs. -- **Depends on:** none. -- **Status:** Done — [PR #568](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/568). -- **Done when:** - - W120.1 These types are declared as 04 §10.4, 11 §3a and 03 §1.1 state: - - `CampaignRef`, `CampaignInclude`, `CampaignAttachment`, `ComposedModule` and - `ComposedAttachment`; - - `Campaign.includes`, `ContentPack.attachments` and `ResolvedRegistry`; - - `Kind.composeContent`; - - `ModuleInterface`, `StoryGraphIncludeBinding` and `StoryGraphAttachment`. - - `resolvePacks` returns the authored version of each campaign beside the stamped one. - - W120.2 Both `buildValidatedContentRegistry` and `buildValidatedPackRegistry` compose before - they validate, in the order §10.4 fixes. Composition is depth-first, and each campaign is - composed once. A three-level chain composes to ids of the form `a::b::x`. Two hosts that - include the same module get the same composed module. - - W120.3 Each of the seven core codes is rejected by at least one fixture, with a path: - `include_missing`, `include_version_mismatch`, `include_kind_mismatch`, `include_cycle` - (one direct cycle and one transitive cycle), `include_alias_collision`, - `attachment_host_missing` and `compose_unsupported`. In the packed path, a pin that names - the authored version passes. A pin that names the stamped resolution id fails. - - W120.4 Each of the six story-graph codes is rejected by at least one fixture, with a path: - `include_not_module` (once with no `module` block, once for a module that reads - `ending`), `exit_unmapped`, `binding_undeclared`, `binding_type_mismatch`, - `attachment_node_not_choice` and `attachment_choice_collision`. - - W120.5 An authored id that contains `:` is still rejected with `invalid_identifier`. This - holds for a node, a variable, an achievement and an alias. A composed id is accepted. - - W120.6 The `copy` consequence writes one declared variable from another, reading `from` as - earlier consequences in the same list left it, as 03 §5 states. Tier 1 rejects an - undeclared `from` with `undeclared_variable`. It rejects a `from` that cannot be assigned - to `var` with `invalid_consequence_value`, and an `enum` whose values are not all among - the target's is one such case. - - W120.7 Play a host that includes a module through its entry, through the module, out of - one exit and on to a host ending. The run is deterministic on a repeat, and also across a - save/load cut taken inside the module: `serialize()` output matches the uncut run. Each - of the following is shown: - - the entry and the exit each cost one `turn`; - - `visited.` counts entries; - - a bound input is copied in on every entry; - - a bound output is copied out on exit; - - interpolation in module text reads the module's own variable; - - a module achievement unlocks into the host's `unlockedAchievements`. - - W120.8 A pack attachment appends one choice to a host choice node, after every authored - choice and in the order §10.4 fixes. The attached choice reaches the module. An - attachment on a module reaches every host that includes it. - - W120.9 A pack with no includes and no attachments keeps its committed `campaignVersion` - digest. Every committed campaign, fixture, replay oracle and golden file is unchanged. - A pack that adds an attachment gets a different digest. - - W120.10 Every new code is registered with a message, in `reasons.ts` and in the - story-graph kind's reason table. The pull request states how many rejecting cases and - how many passing cases the new tests contain. `npm run typecheck`, `npm run lint` and - `npm test` pass from `src/engine/`. -- **Out of scope:** - - Carrying `Campaign.includes` in the portable format, which is its own decision. - - Runtime call/return frames, and any change to `kindState`, the envelope or the save format. - - An attachment that does anything other than append a choice. - - `composeContent` for the simulation and world-graph kinds. +## Carried Forward + +Open items the retired bodies carried that were never sliced. They are moved here so that none is +lost. None is sliceable as written: each needs a contract or design decision first. + +- **Session capture** ([`08-session-capture.md`](08-session-capture.md)). The hosting gate is met: + [SubZeroDev.Adventures](https://github.com/The-Running-Dev/SubZeroDev.Adventures) is a hosted + deployment. What blocks it is a missing contract surface. [08 §3.2](08-session-capture.md#3-what-is-refused) + keeps only the parameters a kind declares, and no kind declares its parameters anywhere the core + can read. Either the `Kind` interface gains a declared-parameter surface, or §3.2 is restated + against something a host can enforce. +- **Hosted engine edge.** The `.NET Platform edge → Node engine workload` process boundary, proposed + to follow W62: a generated JSON/HTTP service contract first, MCP as a projection, and one + in-memory remote session before persistence, accounts, catalogue or metering. +- **More clients**, such as Discord. +- **Content tooling not yet built.** The visual node editor needs a design decision on where it + lives, what it emits, and whether authored source or the built campaign is its file format. + Content balancing tools are unbuilt. AI authoring assistants need a dependency decision, and + W77's author-facing validator is their precondition. +- **Experiment attribution.** [06 §4](06-extensibility.md#4-the-composition-root) settles + `SessionHost.experiments` as the already-resolved, non-null assignment map. The declaration and + record-stamping implementation were last recorded as not yet brought into line with it. +- **Simulation balance.** Drift rates, scenario economics, `demandBand` thresholds and the + housing-quality formula are placeholder numbers. Balancing is game-side; the engine owes the + levers, which [§7.14](10-simulation-kind.md#714-remaining-campaign-physics-524) contracts and + W117–W119 shipped. +- **Three `docs-template` hardening findings, to raise upstream**, from the automated review of + [PR #3](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/3): the docs workflows pin + the mutable `:latest` tag + ([comment](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/3#discussion_r3660515997)); + the link validator does not constrain resolved targets to the repository root + ([comment](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/3#discussion_r3660516002)); + and file enumeration walks excluded trees before filtering them + ([comment](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/3#discussion_r3660516006)). +- **The world-graph programme gate.** An immutable published package version must carry the + replay-guarded third kind, so Sun Trap can install it without a sibling checkout. `v0.10.0` is + tagged; publication is an external release action this ledger does not evidence. +- **The games' own Definitions of Done** live with the games: + `games/life-in-the-fast-lane.md` and `games/bulgaria-adventure.md` in + [SubZeroDev.GameOfLife](https://github.com/The-Running-Dev/SubZeroDev.GameOfLife). ## Landed -A slice's full body lives under `## Outstanding` only until its issue closes; once closed, the -body retires here to a single row — name, closing issue, criteria range, and the commit the body -was last complete at. Nothing here yet. +Ordered by id. *Criteria* is the range of numbered acceptance +criteria; units sliced before criteria were numbered show a dash. *Merge* is the merge commit of +the last pull request listed. -| Slice | Name | Issue | Criteria | Commit | +| Slice | Name | Pull requests | Criteria | Merge | |---|---|---|---|---| +| W0 | CI and Documentation Gates | [#3](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/3), [#6](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/6) | — | [`98ccee9`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/98ccee99813d61b514f1048b87cce45dadb91776) | +| W1 | Core Contract Types and Module Skeleton | [#17](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/17) | — | [`f7d8f59`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/f7d8f598ef7e6591aa2184285bf5bc25a4525345) | +| W2 | RNG Handle and Stream Derivation | [#22](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/22) | — | [`9f90eae`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/9f90eae1d8a00b95286da3ef99a64d128e7007dc) | +| W3 | Pure Engine Kernel | [#33](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/33) | — | [`7a46e57`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/7a46e5720c45d9b1c57d1958f875302ee6499ac9) | +| W3a | Observability: Emitter, Events, and Sinks | [#34](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/34) | — | [`4567e43`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/4567e432a8eae80d2b0fb30f11427759875d1240) | +| W4 | Registry, Authoring Builder, Localization | [#35](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/35) | — | [`a7e9dc3`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/a7e9dc3e1d60ad6ab792011c0ea424fbadc1e1ab) | +| W5 | Tiered Validation | [#36](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/36) | — | [`71ff9e8`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/71ff9e8858acc5de41aa099793e043021985ac30) | +| W6 | Projection | [#37](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/37) | — | [`b4d2af9`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/b4d2af9575694a3bd1246b88c784ce48849af9e8) | +| W7 | Session Store | [#38](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/38) | — | [`10b4ce9`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/10b4ce9f96836741651399c0b332023fc1530317) | +| W8 | Profile Store | [#39](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/39) | — | [`d9af5d8`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/d9af5d8dbd0bc88829c8d43571aee92b874b2536) | +| W9 | Variables and Consequences | [#41](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/41) | — | [`9359a12`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/9359a12d86014b8d6cbd9648ebbf07dfdfabba01) | +| W10 | Conditions and Requirements | [#43](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/43) | — | [`388f2dd`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/388f2dd4a2515cc414d30d2bb419c214acdfbbe4) | +| W11 | Nodes, Turn, and Settle | [#44](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/44) | — | [`6d85917`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/6d8591728194881e34c1c6b53147b42cc20dbe0f) | +| W12 | Scene, Actions, Projection, Reason Codes | [#47](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/47) | — | [`9eb400a`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/9eb400ac9da2083fec4f17d699aad7cb74bff07f) | +| W13 | Endings and Achievements | [#51](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/51) | — | [`0fb7cff`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/0fb7cffe499b914c29250b8b817e5e348f2b0595) | +| W14 | Story-Graph Validation | [#55](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/55) | — | [`231683f`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/231683ffc50ef31e190c48612dfb7007f7e8f0dd) | +| W15 | The Bureaucracy Campaign and Broken Fixtures | [#60](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/60) | — | [`75fc64a`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/75fc64a4887a026c6b5e46641540013f8f94aa58) | +| W16 | Text Client | [#63](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/63) | — | [`5d6e418`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/5d6e418ba435482262adf3fddecfbaf1cb0e44b3) | +| W17 | MCP Server | [#66](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/66) | — | [`74d81b7`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/74d81b7912b574c83ed0bc9ac3716a1b1dcd53ed) | +| W18 | Determinism Harness | [#70](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/70) | — | [`bf106d6`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/bf106d657295c4cc3c3d958c420f3be08a1dfd61) | +| W19 | MVP Acceptance | [#71](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/71) | — | [`dcb7803`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/dcb78038cbe8bb8e002f0651c9551b47ff874bb5) | +| W20 | Engine Versioning and Release Tags | [#73](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/73) | — | [`e26fa9d`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/e26fa9dbc9e1a2814443bfff952a15562b608dc8) | +| W21 | Replay Oracle: Outcome and the Runner | [#73](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/73) | — | [`e26fa9d`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/e26fa9dbc9e1a2814443bfff952a15562b608dc8) | +| W22 | Replay Oracle: The Corpus | [#73](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/73) | — | [`e26fa9d`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/e26fa9dbc9e1a2814443bfff952a15562b608dc8) | +| W23 | Replay Oracle: CI Wiring | [#73](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/73) | — | [`e26fa9d`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/e26fa9dbc9e1a2814443bfff952a15562b608dc8) | +| W24 | Core Spec Reconciliation | [#77](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/77) | — | [`f203325`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/f20332518331a6bd1714dcbd44156db7ad592d65) | +| W25 | Simulation Kind Seam Reconciliation | [#80](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/80) | — | [`399391b`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/399391b8e12a393fe11ec2aa4f67252168507b29) | +| W26 | Toolchain Upgrade | [#82](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/82) | — | [`0d3cbb1`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/0d3cbb1f693113ab65dcf360fbf7d4f95d277206) | +| W27 | Bulgaria Adventure: The Driving Arc | [#86](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/86) | — | [`25a9411`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/25a9411e3afea417e3235d439dc5b7853605e98a) | +| W28 | Bulgaria Adventure: The Return Arc | [#87](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/87) | — | [`c186f4d`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/c186f4d02b3ccfe3fa5a77d650890b058b5973db) | +| W29 | Bulgaria Adventure: The Inheritance Arc | [#88](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/88) | — | [`265a1ca`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/265a1ca95ba0766405341bca20d04aad86d9c049) | +| W30 | Bulgaria Adventure: The Enterprise Arc | [#89](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/89) | — | [`d8d8821`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/d8d8821e6d8ae8965bedf9cea45d65036dbd2c66) | +| W31 | Save Migration | [#92](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/92) | — | [`588567d`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/588567d2d4ff84f7b38a904b62c586b315122f07) | +| W32 | Simulation Kind: State Types | [#94](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/94) | — | [`fc1b29b`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/fc1b29b06a034ece06eab2735cdeb263cf9d5368) | +| W33 | Simulation Kind: Actor State | [#95](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/95) | — | [`2f0b7ea`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/2f0b7ea3a3079d7ba38b5c97f5a7a1713ae59cc0) | +| W34 | Simulation Kind: Content Definition Types | [#96](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/96) | — | [`9f157cb`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/9f157cbdb768863601e4e462b7ce291b3d4db5de) | +| W35 | Simulation Kind: Resolution and Systems | [#97](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/97) | — | [`f1d7a21`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/f1d7a21cc2cd1bc29d777c9ebf11086cdd722fb6) | +| W36 | Simulation Kind: State, Variables, and the Plan | [#98](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/98) | — | [`4c80b05`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/4c80b054e344576504af40affe3d27d17c957ff3) | +| W37 | Simulation Kind: The Week | [#99](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/99) | — | [`0815702`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/08157023d778a792fab225f3a827b0d3d63922e7) | +| W38 | Simulation Kind: Content-Definition Types | [#100](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/100) | — | [`bdd07d6`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/bdd07d6ba14a08cc10ed01847c384c144641468c) | +| W39 | Simulation Kind: Wiring the "Stable Life" Vertical Slice | [#101](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/101) | — | [`1206daf`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/1206dafcbaf1556bba56e4b12ab27a58c3b45d0b) | +| W40 | Simulation Kind: The "Stable Life" Scenario, Validation, and Corpus | [#102](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/102) | — | [`9fdf77c`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/9fdf77c63773ea0bc0ffc288fbba0995ee04c3ff) | +| W41 | Engine Consumer Boundary | [#108](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/108) | — | [`db9c62a`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/db9c62aec509ed083179a73ae2ec49b1b53d3d26) | +| W42 | World-Graph Runtime State Contract | [#116](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/116) | — | [`5f5f8f5`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/5f5f8f542f619e70b2a15d8952998e48d12766d1) | +| W43 | World-Graph Content Definition Contract | [#119](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/119), [#121](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/121) | — | [`4ab8b0a`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/4ab8b0a73928a3320491c79afbd58837ca9e647a) | +| W44 | World-Graph Resolution Contract | [#120](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/120) | — | [`5eca57b`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/5eca57b9dc50b939fe55d7ef7a06edac9f38c346) | +| W45 | World-Graph Kind Skeleton and Immediate Actions | [#125](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/125) | — | [`c6662bb`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/c6662bb6b638e70348a133f8c028b2e18f26963c) | +| W46 | World-Graph Deterministic Tick Pipeline | [#128](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/128) | — | [`6301a49`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/6301a497cc956c4e47df584c7b0b9d3c0455b71c) | +| W47 | World-Graph MVP Vertical Slice | [#131](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/131) | — | [`2390750`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/23907505b64042b9dbc744ba4edac0c53122c673) | +| W48 | Preview/Client Parity | [#133](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/133) | — | [`c81227d`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/c81227dca661e028cd8b50ab7fbcffe9be09c2f6) | +| W49 | World-Graph Validation, Scenario and Replay Guard | [#134](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/134), [#138](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/138) | — | [`6e3d38e`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/6e3d38e77d0e34686dc4e0956b2ff01da9b8af3d) | +| W50 | Simulation Kind: Projection and Client Parity | [#166](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/166) | W50.1–W50.8 | [`bb97138`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/bb971389a7fa888d1937d8390b2a24872dbbf0d9) | +| W51 | Simulation Kind: Derived Values, Modifiers, and Effects | [#194](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/194), [#204](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/204) | W51.1–W51.6 | [`6b573d3`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/6b573d314bde4ce5c8466da49457a44f7b63bca5) | +| W52 | Simulation Kind: The Scenario Campaign and Full Validation | [#227](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/227) | W52.1–W52.6 | [`a9b7307`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/a9b73078a0db1e69f9351a3c305fc9a25a2bdf96) | +| W53 | Simulation Kind: Employment and Income | [#228](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/228) | W53.1–W53.6 | [`4034606`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/403460677a6f3f4c4e5644e69f8a7040c07a1e92) | +| W54 | Simulation Kind: Education and Skills | [#230](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/230) | W54.1–W54.6 | [`bd163f9`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/bd163f91c58412013084c3829306a63d0d346f5d) | +| W55 | Simulation Kind: Housing, Debt, and Reconciliation | [#232](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/232) | W55.1–W55.6 | [`1e63ce5`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/1e63ce57e924724399f6077ced701fbef876ccd2) | +| W56 | Simulation Kind: Possessions, Places, and People | [#236](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/236) | W56.1–W56.6 | [`e20cfdb`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/e20cfdbd5318ed08ea333695ba927de7cee26136) | +| W57 | Simulation Kind: Events, Opportunities, Headlines, and Achievements | [#265](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/265) | W57.1–W57.7 | [`9d69704`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/9d69704dc45197fab7632aebc6654d16050d221d) | +| W58 | Content Pack Resolution and Content Identity | [#238](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/238) | W58.1–W58.8 | [`1dda794`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/1dda794480d671c4ae59f177e1304a6d719dd952) | +| W59 | Experiment Gates and the `ExperimentSource` Port | [#239](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/239) | W59.1–W59.6 | [`fffe42e`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/fffe42e79641b1ad9994eb7ca2bad9a8d77e2606) | +| W60 | A Second Locale, End to End | [#240](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/240) | W60.1–W60.5 | [`214a6cc`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/214a6cc7f51b19893299654060c8f53c1df081a3) | +| W61 | Public Playable Web Demo | [#183](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/183), [#184](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/184) | W61.1–W61.10 | [`cd75c37`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/cd75c3783bd8afd75cacae6a699e6ae4906f2488) | +| W62 | Platform Static Host Image | [#264](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/264) | W62.1–W62.9 | [`6013c69`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/6013c69fe0174374bd1182aa66c46cc3a70a224b) | +| W63 | Absurd Game Interface | [#188](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/188), [#190](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/190) | W63.1–W63.10 | [`a94da4b`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/a94da4bf5eb5bdbd108c4e9b64abb6f1322592a3) | +| W64 | Replayable Story Campaign Expansion | [#189](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/189), [#191](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/191) | W64.1–W64.13 | [`c1db016`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/c1db0160acb047a70fb93612d7951256e2d58fe8) | +| W65 | Browser Test Harness for the Site | [#201](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/201) | W65.1–W65.8 | [`8e9378d`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/8e9378d82c52f692b3ce6acae798137f289f6bab) | +| W66 | The Play Surface on a Phone | [#205](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/205) | W66.1–W66.12 | [`a115098`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/a115098a131b3d5a511048bfc27391325d937c9b) | +| W67 | Restore the Story-Graph Regression Evidence | [#261](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/261) | W67.1–W67.7 | [`ce90041`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/ce900416b948ed268b236f67584066878f778acc) | +| W68 | Make the Browser Save Adapter Actually Restore | Cancelled — [#271](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/271) | W68.1–W68.7 | — | +| W69 | Consume the Reusable Landing-Page Package | [#272](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/272) | W69.1–W69.8 | [`38b7c34`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/38b7c34eaddf2c35d8fbbaa7c5d8e3da30451e6d) | +| W70 | Gate `/play/`'s Startup Request Surface | Cancelled — [#271](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/271) | W70.1–W70.6 | — | +| W71 | The Bulgaria Culture Pack: Mechanism and Voice | [#314](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/314) | W71.1–W71.6 | [`f0d1fee`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/f0d1fee3ef28b4d4eab3bd9033795cf7de8cd6d9) | +| W72 | The Bulgaria Culture Pack: The Full Setting | [#317](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/317) | W72.1–W72.5 | [`66075df`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/66075df095bc61d3cb9528ce0caaabdf11cdd547) | +| W73 | Tier 3 Validation as an Author-Facing Check | [#319](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/319) | W73.1–W73.6 | [`1bdb5b6`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/1bdb5b6a40e56fe70ba8faebd3f1e7b33b23ddf5) | +| W74 | Campaign Content Ownership: The Authoring Seam | [#298](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/298), [#299](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/299), [#332](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/332) | W74.1–W74.8 | [`cbbe0cd`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/cbbe0cde444788050e0a9def90b7fd9bf450b607) | +| W74a | The Bureaucracy Fixture, Frozen Byte-for-Byte | [#333](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/333) | W74a.1–W74a.5 | [`7cb09a0`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/7cb09a07a51514deef178b4273481d8a34dd8e74) | +| W74b | Retire the Engine's Own Play Surface | [#335](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/335) | W74b.1–W74b.6 | [`5c26ab0`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/5c26ab0530b4746b69938735c8c80710feebb7ff) | +| W74c | The Breaking Ownership Release | [#336](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/336) | W74c.1–W74c.7 | [`3fee866`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/3fee866fbaac51b90b9f7a297f320f1062a71456) | +| W75 | Classified Persistence Conflicts | [#311](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/311) | W75.1–W75.7 | [`6f5adb4`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/6f5adb47531fb1929d0e03c98b573cf3abb7fe0c) | +| W76 | A Folded Registry Keeps Its Resolution | [#337](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/337) | W76.1–W76.10 | [`b1cd9be`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/b1cd9bed82fb32daf31707d144d84f8e2164066a) | +| W77 | Tier 1 and Tier 2 as an Author-Facing Check | [#338](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/338) | W77.1–W77.7 | [`6dd7b4f`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/6dd7b4f4453b5856a3f9e43e5a52931921b63dd6) | +| W78 | Localization Coverage and String Extraction | [#340](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/340) | W78.1–W78.6 | [`77e35c7`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/77e35c7761864a2b05585dd6b282d2b4c89b5b65) | +| W79 | What Changed Between Two Resolutions | [#342](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/342) | W79.1–W79.6 | [`b6f8bb2`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/b6f8bb288452b804022f070026f84d13f6b4a98f) | +| W80 | Seeing a Story Graph | [#343](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/343) | W80.1–W80.6 | [`1ba588e`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/1ba588e30eb0140a02754f804bf85125fd6c16d9) | +| W81 | Construction Finishes What `build` Starts | [#345](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/345) | W81.1–W81.10 | [`b589b6c`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/b589b6c58f5c10b28dddd5579686b63c8ed7d79b) | +| W82 | A Kiosk That Ran Out Can Be Refilled | [#347](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/347) | W82.1–W82.8 | [`aa56f77`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/aa56f7799d3a8d311d9b8f2378c85d61a3414cf0) | +| W83 | Buildings Get Dirty, Wear Out, and Break | [#348](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/348) | W83.1–W83.7 | [`04c1baf`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/04c1baf19f93f740e01b54a38925e7edbabe3dcd) | +| W84 | Incidents That Happen On Their Own | [#360](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/360) | W84.1–W84.9 | [`8fdded3`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/8fdded3afdacf133fbdf8d23dd32d07e17082125) | +| W85 | The Resort Tells You What Needs Attention | [#361](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/361) | W85.1–W85.10 | [`3ea878e`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/3ea878e0d318f498646620a28f6b59f268606f10) | +| W86 | The Story-Graph Kind Says Why | [#362](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/362) | W86.1–W86.7 | [`450935c`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/450935cc1b0a24b09a98b5d9476890e36192a1e4) | +| W87 | The World-Graph Kind Says What Its Ticks Did | [#363](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/363) | W87.1–W87.7 | [`24641e0`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/24641e0e625b3d96d41c5b73640d8be9a35dd9b0) | +| W88 | The Simulation Kind Nobody Outside the Package Can Use | [#367](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/367) | W88.1–W88.6 | [`8115478`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/8115478aa9caefa5d53596c32f3348e6167601c0) | +| W89 | A Game-Length Life | [#376](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/376) | W89.1–W89.8 | [`71e7364`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/71e7364c85cb68e0115a95b9c960263dbb0ed9bb) | +| W90 | Canonical and Public Truth | [#377](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/377) | — | [`274fa6e`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/274fa6e492defd5634a6c35972b47a199802366d) | +| W91 | Reproducible Package and Release Gate | [#379](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/379) | — | [`63a0c12`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/63a0c1231f582f2b05632a11a37188e3b8fd1537) | +| W92 | Local and CI Gate Health | [#378](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/378) | — | [`fab72b1`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/fab72b1cd3fed40d71d80fd7144863543b2d5fcb) | +| W93 | World-Graph Spatial and Audit Correctness | [#398](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/398) | W93.1–W93.6 | [`f0735fc`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/f0735fc59fcfd879911771fefdeb1f41cc90f252) | +| W94 | Simulation Resolution Correctness | [#400](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/400) | W94.1–W94.7 | [`e103de6`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/e103de63996c6005aaae3f251e8c5267010ac72e) | +| W95 | Effect and Audit Semantics | [#403](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/403), [#404](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/404) | W95.1–W95.7 | [`6c1fb4f`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/6c1fb4f6c5043e912cf72e8660cb0ccef653098a) | +| W96 | Mechanical Regression Boundaries | [#408](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/408) | W96.1–W96.7 | [`bb62ffe`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/bb62ffe83ac920ceeb8db7ce16e6d4516f8336e7) | +| W97 | Immutable Projections and Shared Pipelines | [#409](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/409) | W97.1–W97.6 | [`b9ecdc2`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/b9ecdc274c0bf093dc235b4e08776bd666f6a6fe) | +| W98 | Catalog and Projection Completeness | [#414](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/414) | W98.1–W98.7 | [`d7ca8eb`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/d7ca8eb9886cd9cfe0b38e7780d73d18d9730c5f) | +| W99 | Session Lifecycle Operations | [#416](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/416) | W99.1–W99.8 | [`51ad81a`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/51ad81af88a16d84b24ce8b6e77b7e80ac575e14) | +| W100 | Campaign-Tunable Weekly Rules | [#419](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/419) | W100.1–W100.7 | [`3d496b3`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/3d496b3fdb8e705f31632980890f9bf42884bf77) | +| W101 | Projects, Businesses, and Rival Scarcity | [#421](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/421) | W101.1–W101.8 | [`b595fe2`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/b595fe2e7e7e92e9c3e62715998b2a7fe89bfa22) | +| W102 | Profile Chains and Simulation Save Migration | [#423](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/423) | W102.1–W102.8 | [`6e4a7bb`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/6e4a7bb855ac3559953594104d71cc7f4f304390) | +| W103 | Companion Contracts and Ownership Reconciliation | [#424](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/424), [#427](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/427), [#451](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/451) | W103.1–W103.8 | [`e52e7a1`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/e52e7a104311c3afa431d412f3ef8f778d402ba1) | +| W104 | Release 0.11 Compatibility Sweep | [#447](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/447), [#448](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/448) | W104.1–W104.7 | [`9ece598`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/9ece598028240b7541ad6fc16a1041a2fb669f48) | +| W105 | Documentation and Landing Publication Review | [#455](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/455) | W105.1–W105.7 | [`9439b99`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/9439b99a2c9f2d2263a0c2ce03c0b73a3b883180) | +| W106 | Tracker Evidence Closure | [#456](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/456) | W106.1–W106.7 | [`865f827`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/865f827b050eb92ca1894a8e89e743ae5112e41b) | +| W107 | 0.11 Release Candidate Verification | [#460](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/460), [#461](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/461) | W107.1–W107.7 | [`a84d9c9`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/a84d9c99824b7cc9ca4f7735961a3a4fdf7b814d) | +| W108 | Publish 0.11 Readiness | [#471](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/471), [#474](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/474), [#475](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/475) | W108.1–W108.6 | [`651a2a4`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/651a2a4ea87a3d8fb0dab70d0a7337d7e8b8270c) | +| W109 | A Uniform That Makes Its Wearer More Employable | [#478](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/478) | W109.1–W109.6 | [`a2850f4`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/a2850f40d24f516243a71055375b5937c746bbd4) | +| W110 | An NPC Who Already Remembers You | [#479](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/479) | W110.1–W110.4 | [`85f59c5`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/85f59c55f19c35e1841469bf607a9e150bb2b824) | +| W111 | Conditions That Can Ask "Do You Own One?" | [#481](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/481) | W111.1–W111.6 | [`ef8abff`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/ef8abffdf7aff049a26f20cf36cfeaa58a95c217) | +| W112 | A Car That Costs Money to Run | [#490](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/490) | W112.1–W112.6 | [`a431ed2`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/a431ed2c29e3a3d8f71b838b35b4a5dba8b4b9ab) | +| W113 | Utilities and Transport on the Weekly Bill | [#491](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/491) | W113.1–W113.7 | [`aadb482`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/aadb482a493a0e2e75b3c402370f83846243096d) | +| W114 | The NPCs Are Actually There | [#495](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/495) | W114.1–W114.5 | [`7d8850d`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/7d8850df21d99d6b224d38fea6b9cd720e3d3e38) | +| W115 | A Resort a Client Can Actually Draw | [#518](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/518) | W115.1–W115.11 | [`8e13420`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/8e13420fb16c3ee765ac209c66d2939c91b484a8) | +| W116 | A Campaign Cannot Ask About a Field Its Collection Lacks | [#538](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/538) | W116.1–W116.8 | [`7fc2237`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/7fc2237aa0d80df87c284409a182ff41b3eff93d) | +| W117 | Two Campaigns, One Lever, Different Weeks | [#542](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/542) | W117.1–W117.7 | [`19442c4`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/19442c4ae46d5842011a629a900eb3449a61e344) | +| W118 | The Week's Own Rules Belong to the Campaign | [#544](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/544) | W118.1–W118.7 | [`c187ee6`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/c187ee6fc655c6b2f22634a745518d48218031a2) | +| W119 | Every Fixed Action Price Is the Campaign's | [#548](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/548) | W119.1–W119.7 | [`5435b74`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/5435b742b52f9bcaf69455a6da7fe1e93d8ff2ac) | +| W120 | One Campaign Can Include Another | [#568](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/568) | W120.1–W120.10 | [`7081402`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/7081402c6dccc277b23250d44a22b022a16fd683) | diff --git a/design/90-decisions.md b/design/90-decisions.md index bbb81f9d..dc75d93a 100644 --- a/design/90-decisions.md +++ b/design/90-decisions.md @@ -2305,3 +2305,9 @@ Rejected: - **Check bindings against the content merged so far.** That makes a binding's validity depend on the order of includes. - **Raise merge collisions in Tier 1.** The path would name the merged node, not the include that caused it. Reversibility: cheap. Each call is internal to the build, and no published campaign composes yet. + +### 2026-10-07 — `30-slices.md` moves to the kit's current format: every W unit retires to *Landed* +Context: the ledger predated the kit's settled slice format. Its 125 W units were `### [x] W` headings with prose status lines inside topical programme sections, about 5,400 lines. The kit's `get-next-slice.ts` reads only `## S` sections and the `## Landed` table, so it saw no slice at all and reported the plan finished whatever the ledger said. Twelve status lines had also gone stale: W50–W55 and W115–W119 still read "Not started" after merging, and W70 after being cancelled. With W120 merged, nothing was in flight. +Chosen: retire every W unit to one Landed row: id, name, the merged pull requests that delivered it, its criteria range, and the merge commit. W68 and W70 are marked cancelled with the pull request that cancelled them (#271). Each row carries an `` anchor, so `30-slices.md#w114` here and the roadmap's `engine/todo#w90`, `#w93` and `#w109` still resolve. `build/Test-Documentation.ps1` learns to collect inline `` anchors, because it only read headings. Nine open items that the bodies carried unsliced move to a *Carried Forward* section, so none is lost. New slices are `## S` from S121, so no S id can be mistaken for a W id. The full bodies stay readable at `7081402` and in `plans/`. The site roadmap's unit count, which matched `### [x] W` headings, now counts Landed rows that are not cancelled plus `Status: done` S slices. `CLAUDE.md`'s W-id clause is amended to match. This supersedes the 2026-09-05 entry that rejected the *Outstanding*/*Landed* split as expensive and hard to reverse. That entry's cost was real, but it assumed live units whose programme sections still explained them. With every unit landed, the sections are history, and the anchors are kept per row. +Rejected: **Fixing the twelve status lines only.** That leaves the kit's reader blind to the ledger, and the next `/agentkit:next` reports "finished" for the wrong reason. **Keeping the bodies and adding S-format headings beside them.** Two formats in one file would leave each unit's state recorded in two places. +Reversibility: one pull request. Reverting it restores the bodies, the old count and the old gate behaviour; no source, test or contract changes. diff --git a/design/TODO.md b/design/TODO.md index ceee6a5c..7370b6ee 100644 --- a/design/TODO.md +++ b/design/TODO.md @@ -4,316 +4,6 @@ # TODO -## Done — Specification and Scaffold - -# The MVP — Units of Work - -## Core - -### [x] W0 — CI and Documentation Gates - -### [x] W1 — Core Contract Types and Module Skeleton - -### [x] W2 — RNG Handle and Stream Derivation - -### [x] W3 — Pure Engine Kernel - -### [x] W3a — Observability: Emitter, Events, and Sinks - -### [x] W4 — Registry, Authoring Builder, Localization - -### [x] W5 — Tiered Validation - -### [x] W6 — Projection - -### [x] W7 — Session Store - -### [x] W8 — Profile Store - -## The Story-Graph Kind - -### [x] W9 — Variables and Consequences - -### [x] W10 — Conditions and Requirements - -### [x] W11 — Nodes, Turn, and Settle - -### [x] W12 — Scene, Actions, Projection, Reason Codes - -### [x] W13 — Endings and Achievements - -### [x] W14 — Story-Graph Validation - -## Content, Clients, Proof - -### [x] W15 — The Bureaucracy Campaign and Broken Fixtures - -### [x] W16 — Text Client - -### [x] W17 — MCP Server - -### [x] W18 — Determinism Harness - -### [x] W19 — MVP Acceptance - -## Post-MVP — Depth - -### Rigour: The Replay Regression Oracle - -### [x] W20 — Engine Versioning and Release Tags - -### [x] W21 — Replay Oracle: Outcome and the Runner - -### [x] W22 — Replay Oracle: The Corpus - -### [x] W23 — Replay Oracle: CI Wiring - -### Rigour: Spec and Toolchain Debt - -### [x] W24 — Core Spec Reconciliation - -### [x] W25 — Simulation Kind Seam Reconciliation - -### [x] W26 — Toolchain Upgrade - -### Rigour: Session Capture - -### Depth: Life in the Fast Lane (The `simulation` Kind) - -### Depth: Sun Trap (The `world-graph` Kind) - -### [x] W41 — Engine Consumer Boundary {#w41} - -### [x] W42 — World-Graph Runtime State Contract {#w42} - -### [x] W43 — World-Graph Content Definition Contract {#w43} - -### [x] W44 — World-Graph Resolution Contract {#w44} - -### [x] W45 — World-Graph Kind Skeleton and Immediate Actions {#w45} - -### [x] W46 — World-Graph Deterministic Tick Pipeline {#w46} - -### [x] W47 — World-Graph MVP Vertical Slice {#w47} - -### [x] W48 — Preview/Client Parity {#w48} - -### [x] W49 — World-Graph Validation, Scenario and Replay Guard {#w49} - -### Depth: Finish the Bulgaria Adventure - -### [x] W27 — Bulgaria Adventure: The Driving Arc - -### [x] W28 — Bulgaria Adventure: The Return Arc - -### [x] W29 — Bulgaria Adventure: The Inheritance Arc - -### [x] W30 — Bulgaria Adventure: The Enterprise Arc - -### [x] W31 — Save Migration - -### [x] W32 — Simulation Kind: State Types - -### [x] W33 — Simulation Kind: Actor State - -### [x] W34 — Simulation Kind: Content Definition Types - -### [x] W35 — Simulation Kind: Resolution and Systems - -### [x] W36 — Simulation Kind: State, Variables, and the Plan - -### [x] W37 — Simulation Kind: The Week - -### [x] W38 — Simulation Kind: Content-Definition Types - -### [x] W39 — Simulation Kind: Wiring the "Stable Life" Vertical Slice {#w39} - -### [x] W40 — Simulation Kind: The "Stable Life" Scenario, Validation, and Corpus - -### Depth: Life in the Fast Lane Becomes Playable - -### [x] W50 — Simulation Kind: Projection and Client Parity {#w50} - -### [x] W51 — Simulation Kind: Derived Values, Modifiers, and Effects {#w51} - -### [x] W52 — Simulation Kind: The Scenario Campaign and Full Validation {#w52} - -### [x] W53 — Simulation Kind: Employment and Income {#w53} - -### [x] W54 — Simulation Kind: Education and Skills {#w54} - -### [x] W55 — Simulation Kind: Housing, Debt, and Reconciliation {#w55} - -### [x] W56 — Simulation Kind: Possessions, Places, and People {#w56} - -### [x] W57 — Simulation Kind: Events, Opportunities, Headlines, and Achievements {#w57} - -### Breadth: Content Packs, Experiments, and Locales - -### [x] W58 — Content Pack Resolution and Content Identity {#w58} - -### [x] W59 — Experiment Gates and the `ExperimentSource` Port {#w59} - -### [x] W60 — A Second Locale, End to End {#w60} - -### Breadth: The First Culture Pack - -### [x] W71 — The Bulgaria Culture Pack: Mechanism and Voice {#w71} - -### [x] W72 — The Bulgaria Culture Pack: The Full Setting {#w72} - -### Breadth: The Platform - -### [x] W61 — Public Playable Web Demo {#w61} - -### [x] W62 — Platform Static Host Image {#w62} - -### [x] W63 — Absurd Game Interface {#w63} - -### Depth: Story Campaigns Become Adventures - -### [x] W64 — Replayable Story Campaign Expansion {#w64} - -### [x] W65 — Browser Test Harness for the Site {#w65} - -### [x] W66 — The Play Surface on a Phone {#w66} - -### Correctness Debt Found by Reconciliation - -### [x] W67 — Restore the Story-Graph Regression Evidence {#w67} - -### [~] W68 — Make the Browser Save Adapter Actually Restore — cancelled {#w68} - -### [x] W69 — Consume the Reusable Landing-Page Package {#w69} - -### [~] W70 — Gate `/play/`'s Startup Request Surface — cancelled {#w70} - -### Content Tooling — A First-Class Workstream, Not an Afterthought - -### [x] W73 — Tier 3 Validation as an Author-Facing Check {#w73} - -### [x] W74 — Campaign Content Ownership: The Authoring Seam {#w74} - -### [x] W74a — The Bureaucracy Fixture, Frozen Byte-for-Byte {#w74a} - -### [x] W74b — Retire the Engine's Own Play Surface {#w74b} - -### [x] W74c — The Breaking Ownership Release {#w74c} - -### [x] W75 — Classified Persistence Conflicts {#w75} - -### [x] W76 — A Folded Registry Keeps Its Resolution {#w76} - -### Content Tooling — Slicing the Remaining Bullets - -### [x] W77 — Tier 1 and Tier 2 as an Author-Facing Check {#w77} - -### [x] W78 — Localization Coverage and String Extraction {#w78} - -### [x] W79 — What Changed Between Two Resolutions {#w79} - -### [x] W80 — Seeing a Story Graph {#w80} - -### Correctness Debt — The Tick Pipeline Runs Twenty Systems and Implements Fifteen - -### [x] W81 — Construction Finishes What `build` Starts {#w81} - -### [x] W82 — A Kiosk That Ran Out Can Be Refilled {#w82} - -### [x] W83 — Buildings Get Dirty, Wear Out, and Break {#w83} - -### [x] W84 — Incidents That Happen On Their Own {#w84} - -### [x] W85 — The Resort Tells You What Needs Attention {#w85} - -### Rigour: Two Event Streams Specified and Never Emitted - -### [x] W86 — The Story-Graph Kind Says Why {#w86} - -### [x] W87 — The World-Graph Kind Says What Its Ticks Did {#w87} - -## Known Open Items Carried In - -## Outstanding - -### Contract Prerequisites — Not Sliceable, Route to `/contract` - -### Depth: Life in the Fast Lane, End to End - -### [x] W88 — The Simulation Kind Nobody Outside the Package Can Use {#w88} - -### [x] W89 — A Game-Length Life {#w89} - -## Stabilization Programme - -### [x] W90 — Canonical and Public Truth {#w90} - -### [x] W91 — Reproducible Package and Release Gate {#w91} - -### [x] W92 — Local and CI Gate Health {#w92} - -### [x] W93 — World-Graph Spatial and Audit Correctness {#w93} - -### [x] W94 — Simulation Resolution Correctness {#w94} - -### [x] W95 — Effect and Audit Semantics {#w95} - -### [x] W96 — Mechanical Regression Boundaries {#w96} - -### [x] W97 — Immutable Projections and Shared Pipelines {#w97} - -### [x] W98 — Catalog and Projection Completeness {#w98} - -### [x] W99 — Session Lifecycle Operations {#w99} - -### [x] W100 — Campaign-Tunable Weekly Rules {#w100} - -### [x] W101 — Projects, Businesses, and Rival Scarcity {#w101} - -### [x] W102 — Profile Chains and Simulation Save Migration {#w102} - -### [x] W103 — Companion Contracts and Ownership Reconciliation {#w103} - -### [x] W104 — Release 0.11 Compatibility Sweep {#w104} - -### [x] W105 — Documentation and Landing Publication Review {#w105} - -### [x] W106 — Tracker Evidence Closure {#w106} - -### [x] W107 — 0.11 Release Candidate Verification {#w107} - -### [x] W108 — Publish 0.11 Readiness {#w108} - -### Depth: The Five GameOfLife Engine Blockers - -### [x] W109 — A Uniform That Makes Its Wearer More Employable {#w109} - -### [x] W110 — An NPC Who Already Remembers You {#w110} - -### [x] W111 — Conditions That Can Ask "Do You Own One?" {#w111} - -### [x] W112 — A Car That Costs Money to Run {#w112} - -### [x] W113 — Utilities and Transport on the Weekly Bill {#w113} - -### [x] W114 — The NPCs Are Actually There {#w114} - -### Depth: The World-Graph Client Read Model - -### [x] W115 — A Resort a Client Can Actually Draw {#w115} - -### Depth: The Simulation Campaign's Own Rules - -### [x] W116 — A Campaign Cannot Ask About a Field Its Collection Lacks {#w116} - -### [x] W117 — Two Campaigns, One Lever, Different Weeks {#w117} - -### [x] W118 — The Week's Own Rules Belong to the Campaign {#w118} - -### [x] W119 — Every Fixed Action Price Is the Campaign's {#w119} - -### Depth: Campaign Composition - -### [x] W120 — One Campaign Can Include Another {#w120} +## Carried Forward ## Landed diff --git a/docs/docs/engine/TODO.md b/docs/docs/engine/TODO.md index 1b7065cf..09987430 100644 --- a/docs/docs/engine/TODO.md +++ b/docs/docs/engine/TODO.md @@ -6,5372 +6,196 @@ slug: todo # TODO -**Status:** Living task list, ordered. The MVP is broken into **units of work** — each one -a single responsibility with its own contract references, dependencies, and done-criteria, -sized to be picked up in a fresh session. The MVP boundary is marked; everything below it -is post-MVP. - -> The MVP's Definition of Done is [`MVP.md`](MVP.md) §5 — every unit below rolls up to it. -> The contracts are [`04-core.md`](04-core.md), -> [`03-story-graph-kind.md`](03-story-graph-kind.md) and -> [`05-observability.md`](05-observability.md). Nothing unsettled remains for the -> MVP: [`OPEN-QUESTIONS.md`](OPEN-QUESTIONS.md) §1 is now a decision log. -> -> **Unit numbering is positional, like the doc numbering.** A unit inserted between two -> existing ones takes a letter suffix — `W3a` — rather than renumbering everything after -> it and invalidating every reference in `plans/`. Same convention as architecture §4a. - -Legend: `[ ]` not started · `[~]` in progress · `[x]` done - ---- - -## Done — Specification and Scaffold - -- [x] **[`03-story-graph-kind.md`](03-story-graph-kind.md)** — `Node`, `Choice`, - `Requirement` (reuses the `Condition` tree verbatim), `Consequence`, `Ending`, - `VariableSchema`, `AchievementDefinition`, seeded random-transition node, turn/settle - semantics, projection, worked Bureaucracy-arc example. -- [x] **[`04-core.md`](04-core.md)** — the Kind seam and the platform types, so the build - runs against contracts rather than decisions. Forced the `03` state reconciliation - (envelope vs kind-state). -- [x] **`executeAction` removed.** No client called it; the plan flow covers execution - (`games/05-text-client.md` §6). A method with no caller is a hypothesis. -- [x] **MVP contracts finalized** — campaign/content identity split ([04 §10.1](04-core.md#101-content-registry)), `visited` - semantics + start-of-game RNG stream ([03 §8.2](03-story-graph-kind.md), [04 §4](04-core.md#4-registration-and-the-pure-engine)/[§8](04-core.md#8-randomness)), `AdvanceResult` tightened, - Definition of Done agreed ([`MVP.md`](MVP.md) §5). -- [x] **All eight MVP-blocking gaps decided** — profile store, base reason strings, - authoring→registry builder, zero-choice campaigns, `InitialStateResult`, `params` to - `advance`, story-graph reason codes, the two format versions - ([`OPEN-QUESTIONS.md`](OPEN-QUESTIONS.md) §1). -- [x] Project scaffold: `src/engine/` package ([Engine Package](/docs/guide/engine-package)), - TypeScript strict, vitest, eslint with the - determinism guard (bans `Math.random`, `Math.pow/exp/log/sin/cos/tan`, `Date.now`). -- [x] Version control: this repo (engine **source + specs**). Companions: the games - [SubZeroDev.GameOfLife](https://github.com/The-Running-Dev/SubZeroDev.GameOfLife) and - [SubZeroDev.SunTrap](https://github.com/The-Running-Dev/SubZeroDev.SunTrap), and - the hosting layer [SubZeroDev.Platform](https://github.com/The-Running-Dev/SubZeroDev.Platform). - This repository and Sun Trap are **public** — verified 2026-08-02, when the engine - package's own visibility raised the question. This line previously said "All private." -- [x] **Seeded PRNG (PCG32)** + `deriveStream` substreams, serializable state. - `src/engine/src/core/determinism/pcg32.ts` — verified bit-identical to the reference vectors. -- [x] **Canonical serialization** (sorted keys, rejects non-finite). - `src/engine/src/core/persistence/canonical.ts`. -- [x] **Toolchain runs green** — `npm install && npm test && npm run lint && npm run - typecheck`; 15 tests across `pcg32` and `canonical`. - ---- - -# The MVP — Units of Work - -> **MVP DONE.** W1–W19 are all checked. [`MVP.md`](MVP.md) §5's Definition of Done is -> checked box-by-box against a named test for each one — the platform is proven. What -> follows below (Post-MVP) is depth and breadth building on it, not the MVP itself. - -Ordered by dependency. **W1–W8** are core (shared by every kind), **W9–W14** the -story-graph kind, **W15–W19** content, clients, and proof. A unit is done when its -done-criteria are demonstrated by a test, not by inspection. - -## Core - -### [x] W0 — CI and Documentation Gates -**Author** `.github/workflows/ci.yml` with one `engine` job (install / typecheck / lint / -test), and **install** the documentation system from the published container image, which -brings `docs-ci.yml` (link-and-terminology gate + production build) and `docs-deploy.yml` -(build + GitHub Pages) ready-made. Every unit below is then guarded from the first commit -rather than the last. The docs half is not optional garnish — `docs/Dockerfile` runs a dev -server, and Docusaurus enforces `onBrokenLinks` only during a production build, so without -CI that pass never runs at all. Both Docusaurus link checks are `'warn'` by design; the hard -gate is `build/Test-Documentation.ps1`, which fails on every relative link and heading -anchor. Also pins the Node floor (`engines`) so CI and local cannot drift, generates the -site homepage from `README.md`, and publishes the site. -- **Depends on:** nothing. -- **Status:** Done — [PR #3](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/3) - (workflow + docs system; landed across a few follow-up PRs — see the evidence below). -- **Done when:** `engine` plus the gate and the docs build all run green on a push; a - newer run for the same repository branch cancels its superseded push/PR run; Pages is - enabled and a push to `main` has deployed to the real published URL; the three - pull-request checks are required on the default branch (deploy runs only on `main`, so - requiring it would leave every PR pending); `engines.node` establishes Node 24 as the - floor while CI runs Node 24; and three deliberate failures — a failing test, a broken - README link, a broken spec link — have each turned their own check red, with run URLs - recorded. - - [x] Workflow authored, docs system installed, README converted, Node 24 aligned — all - green remotely on [PR #3](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/3). - - [x] Required checks configured on the `main` ruleset (`engine`, - *Documentation links and terminology*, *Verify Documentation Build*; deploy - excluded). - - [x] Red-path proof captured and reverted to green, with run URLs — full evidence in - [`plans/04-w0-phase-1-implementation.md`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/blob/main/plans/04-w0-phase-1-implementation.md). - - [x] First deploy to `main` — green twice: `47342b3` - ([run](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/actions/runs/30303045991), - PR #3) and `4e3effc` - ([run](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/actions/runs/30316383318), - PR #5). `https://game-engine.subzerodev.com/docs/` serves the generated homepage. - - [x] HTTPS enforcement — enabled in *Settings* → *Pages* by the repository owner. Note - for future reference: the domain is Cloudflare-fronted, so the `http` → `https` - redirect observable from outside is Cloudflare's "Always Use HTTPS" and is not by - itself evidence of this setting — the checkbox state is. - - **Known, deliberate end state:** `routeBasePath` stays `'docs'`, so the specs serve - from `https://game-engine.subzerodev.com/docs/` and the bare - `https://game-engine.subzerodev.com/` serves the README, generated into - `docs/src/pages/index.md` by the docs installer. That is a real route, so both - broken-link checks are `'throw'`. They were briefly `'warn'` while a static file held - the root instead — a static file serves the request but never satisfies a route - checker, so the navbar brand's link to `/` failed the build under `'throw'`. The - `/docs/` landing page lists the specs in reading order and adds one top-level sidebar - entry above the `engine` category; ordering inside `engine/` is unaffected. -- **Plan:** [`plans/02-w0-ci-workflow.md`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/blob/main/plans/02-w0-ci-workflow.md), [`plans/04-w0-phase-1-implementation.md`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/blob/main/plans/04-w0-phase-1-implementation.md) - -### [x] W1 — Core Contract Types and Module Skeleton -Create the module tree of 04 §1.1 (`kernel`, `session`, `persistence`, `projection`, -`validation`, `registry`, `localization`, `determinism`, `observability`, `composition`) and -put each declared type in the module that owns it. Types only — no behaviour. -- **Spec:** [04 §§1.1–3, 5–12, §17](04-core.md); [05 §§3–4](05-observability.md) for the - `observability` types; [`06-extensibility.md`](06-extensibility.md) §4–§5 for - `composition` — the two host roots and the `IdSource` and `Clock` port interfaces. -- **Depends on:** nothing. -- **Status:** Done — [PR #17](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/17). -- **Done when:** `npm run typecheck` passes with `exactOptionalPropertyTypes`; a dependency - scan shows no core module importing `kinds/`, `clients/`, or `mcp/`; `kindState` is - `unknown`, not a union; `GameState` carries no clock, profile, or kind state; - `EngineEvent` carries no timestamp and no trace id — both are added at the boundary - ([05 §6](05-observability.md#6-the-boundary--stamping-and-tracing)); every port is an interface with a working default, supplied only through - `EngineHost` or `SessionHost`, and no core module reads a clock or generates an id - itself ([06 §4](06-extensibility.md#4-the-composition-root)). - -### [x] W2 — RNG Handle and Stream Derivation -Wrap the built `Pcg32` behind `RngHandle`, and implement the normative `StreamId` → string -encoding. No generator state is persisted anywhere. -- **Spec:** [04 §8](04-core.md#8-randomness). -- **Depends on:** [W1](#x-w1--core-contract-types-and-module-skeleton). -- **Status:** Done — [PR #22](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/22). -- **Done when:** all four encoding forms round-trip exactly as specified; the same - `(seed, streamId)` yields identical draws across runs; different stream ids are - independent; `GameState` contains no RNG field. - -### [x] W3 — Pure Engine Kernel -`createEngine`, `createGame` (consuming `InitialStateResult`), `submitAction` (passing -`params`, returning the new state in `value`), `scene`, `availableActions`, `serialize`, -and a **validating** `deserialize` returning `CommandResult`. -- **Spec:** [04 §§2–5, §12](04-core.md). -- **Depends on:** [W1](#x-w1--core-contract-types-and-module-skeleton), - [W2](#x-w2--rng-handle-and-stream-derivation). -- **Status:** Done — [PR #33](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/33). -- **Done when:** a successful action appends exactly one monotonic `LoggedAction`; a - rejected action leaves serialized state byte-identical and does not advance the log; - every operation returns a new envelope and leaves its input untouched; `deserialize` - rejects a malformed envelope instead of casting; unknown kind, unknown campaign, - ended session, and unknown action each have a test. - -### [x] W3a — Observability: Emitter, Events, and Sinks -The **core half** of the operational event channel. `Emitter`, the per-resolution -`ResolutionEmitter` handle on `KindContext`, the `GameEvent`/`SystemEvent` split, the core -event set ([05 §8](05-observability.md#8-core-events)), and two sinks — `nullEmitter` and `recordingEmitter`. Numbered `3a` -rather than inserted, so no existing unit renumbers — the same convention architecture §4a -uses. - -**The boundary half is W7's**, because it cannot exist before the session store does: -stamping, spans, `attempt`, and `jsonlEmitter` all belong to the layer that owns a clock. -- **Spec:** [`05-observability.md`](05-observability.md) §§1–5, §7–§10, §12; - [04 §3.1](04-core.md#31-kindcontext), [§4](04-core.md#4-registration-and-the-pure-engine), - [§14](04-core.md#14-determinism-harness). -- **Depends on:** [W1](#x-w1--core-contract-types-and-module-skeleton), - [W3](#x-w3--pure-engine-kernel). (Kind events come with the kind units — - [W11](#x-w11--nodes-turn-and-settle) and [W12](#x-w12--scene-actions-projection-reason-codes).) -- **Status:** Done — [PR #34](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/34). -- **Done when:** `emit` returns `void` and no core code path reads anything back from a - sink; the core isolates every `emit`, so a sink that throws on every call does not - fail a game; a fixture replays byte-identically under `nullEmitter` and - `recordingEmitter`; the same fixture twice under `recordingEmitter` yields the - identical event sequence including ordinals, comparing modulo `gameId`; ordinals - restart at 0 each resolution, so a stream does not depend on how many games ran - before; no `EngineEvent` field is populated from a clock or an RNG draw; - `core.validation.completed` and `core.deserialize.rejected` are `scope: "system"` and - carry no `gameId`; a rejected unknown action id is absent from the emitted `data`; a - name outside `core.*` emitted by the core, or outside `kind..*` by a kind, - fails. - -### [x] W4 — Registry, Authoring Builder, Localization -The frozen in-memory `ContentRegistry`; `AuthoredText` → `BuiltCampaign` pure builder; the -protected `core.reason.*` string merge; `LocKey` resolution. Parsing and file I/O stay in -an outer adapter. -- **Spec:** [04 §10.1](04-core.md#101-content-registry), - [§12](04-core.md#12-reason-codes-state-changes-messages), - [§17](04-core.md#17-identifier-conventions). -- **Depends on:** [W1](#x-w1--core-contract-types-and-module-skeleton). -- **Status:** Done — [PR #35](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/35). -- **Done when:** identical key/text pairs deduplicate and conflicting ones fail; a write - into `core.reason.*` is rejected; a registered reason code with no message fails - construction; the engine package performs no filesystem or network I/O. - -### [x] W5 — Tiered Validation -The Tier 1 / Tier 2 framework, identifier and `LocKey` rules, delegating kind checks to -`validateCampaign`. -- **Spec:** [04 §11](04-core.md#11-tiered-validation), [§17](04-core.md#17-identifier-conventions). -- **Depends on:** [W4](#x-w4--registry-authoring-builder-localization). -- **Status:** Done — [PR #36](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/36). -- **Done when:** a Tier 1 error fails registry construction with a path; a Tier 2 warning - loads and is reported; duplicate and malformed identifiers fail; an unvalidated - registry can never be frozen. - -### [x] W6 — Projection -`Engine.view`, the `player` / `ai` audiences, and the `kind.project` seam. -- **Spec:** [04 §9](04-core.md#9-projection). -- **Depends on:** [W3](#x-w3--pure-engine-kernel). -- **Status:** Done — [PR #37](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/37). -- **Done when:** `seed`, `actionLog`, and raw `kindState` cannot reach a client by any - path; the `ai` audience is not wider than `player` by default. - -### [x] W7 — Session Store -The in-memory store: `listCampaigns`, `getScene`, `getView`, `createSession`, -`resumeSession`, `submitAction`, `saveGame`, `loadGame`. Persist canonical blobs, not live -objects. **Owns the observability boundary** ([05 §6](05-observability.md#6-the-boundary--stamping-and-tracing)) — the half W3a deliberately leaves -out, because stamping needs the layer that has a clock. -- **Spec:** [04 §7](04-core.md#7-the-session-store-and-the-platform-api), - [§10.2](04-core.md#102-save-envelope-and-migration); - [05 §6](05-observability.md#6-the-boundary--stamping-and-tracing), - [§6.1](05-observability.md#61-how-per-command-context-reaches-an-event), - [§11](05-observability.md#11-incident-forensics--a-bug-report-is-a-fixture). -- **Depends on:** [W3a](#x-w3a--observability-emitter-events-and-sinks), - [W3](#x-w3--pure-engine-kernel), [W6](#x-w6--projection). -- **Status:** Done — [PR #38](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/38). -- **Done when:** save mid-session → load → continue loses no state; two sessions cannot - mutate each other; `savedAt`, owner ids, and other host metadata never appear in a - serialized `GameState`; every command wraps the base emitter per call via - `withEmitter` and stamps `emittedAt`, `traceId`, `spanId`, `attempt` and `sessionId`; - two concurrent commands never cross-attribute an event, verified with interleaved - sessions rather than asserted; `attempt` increments on **rejected** submissions too, - so repeated invalid actions are distinguishable where `seq` repeats ([05 §5](05-observability.md#5-correlation-without-a-clock)); - `jsonlEmitter` writes one stamped record per line. - -### [x] W8 — Profile Store -`PlayerProfile`, `ProfileStore`, `profileId` on `CreateSessionConfig`, and the post-action -idempotent upsert. -- **Spec:** [04 §7.1](04-core.md#71-the-profile-store). -- **Depends on:** [W7](#x-w7--session-store). -- **Status:** Done — [PR #39](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/39). -- **Done when:** an unlock survives a new session with the same `profileId`; no `profileId` - means no read and no write; missing and corrupt both load an empty profile with the - right warning; a write failure warns without rolling back the game action; a profile - read can be shown never to affect resolution. - -## The Story-Graph Kind - -### [x] W9 — Variables and Consequences -`VariableSchema`, typed `set` / `increment` / `decrement`, clamp-after-all-effects, sorted -iteration of state-affecting records. -- **Spec:** [03 §2](03-story-graph-kind.md#2-variable-schema--fully-typed-n6), - [§5](03-story-graph-kind.md#5-consequences--typed-effects), [§8.1](03-story-graph-kind.md#81-state). -- **Depends on:** [W1](#x-w1--core-contract-types-and-module-skeleton). -- **Status:** Done — [PR #41](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/41). -- **Done when:** undeclared and mistyped writes are rejected; `+5` then `-5` on a clamped - int nets zero rather than clipping; a save/load round trip cannot reorder a `Record`. - -### [x] W10 — Conditions and Requirements -The frozen `Condition` evaluator plus this kind's field namespace (`var.*`, `turn`, -`visited.*`, `achieved.*`, `ending`). -- **Spec:** [03 §6](03-story-graph-kind.md#6-requirements-and-conditions); - [04 §18](04-core.md#18-frozen-primitives). -- **Depends on:** [W9](#x-w9--variables-and-consequences). -- **Status:** Done — [PR #43](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/43). -- **Done when:** only the frozen operator set evaluates; every `field` path is checked at - load against the schema and node set; an unknown path is a Tier 1 error. - -### [x] W11 — Nodes, Turn, and Settle -The four node kinds, `enter(nodeId)`, the settle loop, the `SETTLE_STEPS` guard, and -`initialState` returning `InitialStateResult`. -- **Spec:** [03 §3](03-story-graph-kind.md#3-nodes--the-single-content-type-n7), - [§8.1](03-story-graph-kind.md#81-state), [§8.2](03-story-graph-kind.md), - [§8.4](03-story-graph-kind.md#84-events). -- **Depends on:** [W2](#x-w2--rng-handle-and-stream-derivation), - [W3a](#x-w3a--observability-emitter-events-and-sinks), - [W9](#x-w9--variables-and-consequences), [W10](#x-w10--conditions-and-requirements). -- **Status:** Done — [PR #44](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/44). -- **Done when:** an auto/random chain settles to a choice or ending; every entry increments - its visit count, including the start node and pass-throughs; a 64-step - non-terminating chain fails with `settle_guard_tripped`; a start that settles onto an - ending reports `status: "ended"`; random transitions reproduce from seed + action log; - the settle loop emits `settle.step`, `node.entered` (with `visitCount`) and - `random.picked`, and a stream diff localizes a seeded divergence to one transition. - -### [x] W12 — Scene, Actions, Projection, Reason Codes -`availableActions` (omit on `showWhen`, disable with a reason on `requirements`), `scene`, -the slim `StoryGraphView`, and the kind's reason codes. -- **Spec:** [03 §4](03-story-graph-kind.md#4-choices-and-transitions), - [§8.3](03-story-graph-kind.md#83-reason-codes), [§8.4](03-story-graph-kind.md#84-events), - [§9](03-story-graph-kind.md#9-projection--what-a-client-sees); [04 §6](04-core.md#6-scenes-and-actions-generic). -- **Depends on:** [W6](#x-w6--projection), [W11](#x-w11--nodes-turn-and-settle). -- **Status:** Done — [PR #47](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/47). -- **Done when:** a `showWhen`-hidden choice is absent from the view **and** returns - `unknown_action` when submitted — indistinguishable from a nonexistent id; a gated - choice renders disabled with its `requirementFailKey`; hidden variables and visit - counts never appear in a projection; `StoryGraphView` repeats nothing the generic - `Scene` already carries. - -### [x] W13 — Endings and Achievements -Ending resolution, achievement evaluation after every turn, unlock-once into `kindState` -plus an `achievement_unlocked` `StateChange`. -- **Spec:** [03 §7](03-story-graph-kind.md#7-achievements), [§8.2](03-story-graph-kind.md). -- **Depends on:** [W8](#x-w8--profile-store), [W11](#x-w11--nodes-turn-and-settle). -- **Status:** Done — [PR #51](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/51). -- **Done when:** an achievement fires exactly once across repeated turns; the unlock is - readable as `achieved.` in a later condition; `advance` performs no I/O. - -### [x] W14 — Story-Graph Validation -The kind's Tier 1 and Tier 2 checks via `validateCampaign`. -- **Spec:** [03 §11](03-story-graph-kind.md#11-validation-story-graph-specific). -- **Depends on:** [W5](#x-w5--tiered-validation), [W11](#x-w11--nodes-turn-and-settle). -- **Status:** Done — [PR #55](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/55). -- **Done when:** dangling `goto`, undeclared variable, duplicate id, missing `LocKey`, - non-visible variable in text, and a non-positive-integer `weight` each fail Tier 1 - with a path; unreachable nodes, exitless cycles, and `no_reachable_choice` warn at - Tier 2 without blocking the load. - -## Content, Clients, Proof - -### [x] W15 — The Bureaucracy Campaign and Broken Fixtures -Author 03 §12 in the W4 source form with all its strings, plus four deliberately broken -copies: dangling node, undeclared variable, unreachable node, settlement cycle. -- **Spec:** [03 §12](03-story-graph-kind.md#12-worked-example--the-mvp-bureaucracy-arc); - `games/bulgaria.md`; [MVP §3](MVP.md#3-in-scope). -- **Depends on:** [W4](#x-w4--registry-authoring-builder-localization), - [W14](#x-w14--story-graph-validation). -- **Status:** Done — [PR #60](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/60). -- **Done when:** the valid campaign loads with no Tier 1 errors; the loop reaches its - `office_visits >= 3` gate; the seeded clerk transition reproduces; each broken fixture - produces its expected tier and path; every authored string resolves through the - registry. - -### [x] W16 — Text Client -The plain proving instrument, over `SessionStore` only. -- **Spec:** [04 §§6–7](04-core.md); [`09-clients.md`](09-clients.md) — the contract, and - [§4](09-clients.md#4-the-api-coverage-checklist) the checklist; - [MVP §5](MVP.md#5-definition-of-done--the-mvp) "Honest." -- **Depends on:** [W7](#x-w7--session-store), [W12](#x-w12--scene-actions-projection-reason-codes). -- **Status:** Done — [PR #63](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/63). -- **Done when:** the **API coverage checklist** ([09 §4](09-clients.md#4-the-api-coverage-checklist)) is complete for the text-client - column — all nine operations that existed when W16 shipped exercised by automated - tests, not by inspection (W48 later adds and proves the tenth); it - imports nothing from `kinds/` and never reads a persisted `GameState`; requirement - failures render from reason codes, never matched English; an unknown reason code - renders rather than crashing ([09 §5](09-clients.md#5-reason-codes-and-messages)). - -### [x] W17 — MCP Server -The same operations as tools — a sibling adapter, no AI-specific path. -- **Spec:** [04 §13](04-core.md#13-the-mcp-surface); [09 §7](09-clients.md#7-mcp-is-a-sibling-not-a-special-case) - — MCP is a sibling, not a special case; the tool table itself lives in - [SubZeroDev.ServiceContract's `mcp-tool-contract.md`](https://github.com/The-Running-Dev/SubZeroDev.ServiceContract/blob/main/mcp-tool-contract.md). -- **Depends on:** [W7](#x-w7--session-store), [W12](#x-w12--scene-actions-projection-reason-codes). -- **Status:** Done — [PR #66](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/66). -- **Done when:** every tool matches its documented args and results and maps - one-to-one onto a store operation, with no tool that is not one ([09 §4](09-clients.md#4-the-api-coverage-checklist)); the MCP column of - the coverage checklist is complete; an agent completes the arc; the same seed and - choices, under the same counting `IdSource`, produce **byte-identical** `serialize()` - output to W16's run — the client contract's proof ([09 §1](09-clients.md#1-the-rule-made-testable)); an agent sees no more than a human client does, including - getting `unknown_action` for a hidden choice. - -### [x] W18 — Determinism Harness -The `PlaythroughFixture` runner, committed golden files, property tests, and the -sink-independence pass. -- **Spec:** [04 §14](04-core.md#14-determinism-harness); [05 §12](05-observability.md#12-validation-and-tests). -- **Depends on:** [W3a](#x-w3a--observability-emitter-events-and-sinks), - [W15](#x-w15--the-bureaucracy-campaign-and-broken-fixtures). -- **Status:** Done — [PR #70](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/70). -- **Done when:** the same seed + action log serializes byte-identically; a one-byte golden - edit fails the suite; N random seeds run twice match; `deserialize(serialize(state))` - round-trips; every fixture replays byte-identically under `nullEmitter` and - `recordingEmitter`; the event stream is golden-filed and a stream diff fails the suite - on an unintended behavioural change; the suite passes in Node with no DOM, network, or - AI adapter installed. - -### [x] W19 — MVP Acceptance -Walk [`MVP.md`](MVP.md) §5 and attach test evidence to each box. -- **Depends on:** every unit above. -- **Status:** Done — [PR #71](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/71). -- **Done when:** every box is checked with a named test. **MVP DONE.** - ---- - -## Post-MVP — Depth - -### Rigour: The Replay Regression Oracle - -Replaying committed fixtures across **engine versions**, per -[`07-replay.md`](07-replay.md). Distinct from W18, which compares a build against itself. - -**Not MVP.** It compares versions, and before W19 there is only one. Sequenced here so the -contract is settled while the reasoning is fresh, which is the same call observability took. -Broken into four units — see `plans/27-replay-oracle-programme.md` for the reasoning behind -the split and the decisions each one resolved. - -### [x] W20 — Engine Versioning and Release Tags -Set a real version on the engine package and define the tagging scheme the oracle's -cross-version comparison depends on. -- **Spec:** [07 §2](07-replay.md#2-fixtures-are-inputs-not-state), [§8](07-replay.md#8-where-this-runs). -- **Depends on:** nothing. -- **Status:** Done — [PR #73](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/73). -- **Done when:** `src/engine/package.json` carries a real semver; a documented tag scheme - exists; the version is readable from code without a runtime dependency; the first tag - is cut at the current MVP-done commit. - -### [x] W21 — Replay Oracle: Outcome and the Runner -The `Outcome`/`Decision` projection and the three-verdict runner (07 §3, §6), driven by a -counting `IdSource` ([06 §5.1](06-extensibility.md)) so `createGame` itself replays. Core-owned -and kind-agnostic, the same split `core/determinism/harness.ts` (W18) used, proved first -against a synthetic kind. Composed directly against `Engine` and `ProfileStore`, not -`createInMemorySessionStore` — its client-facing `SessionStore` surface never returns the raw -`GameState` `finalStatus`/`terminal` need (07 §3.2, revised from this unit's original plan -during implementation). -- **Spec:** [07 §2–§3](07-replay.md#2-fixtures-are-inputs-not-state), [§5](07-replay.md#prerequisite-a-controllable-idsource), [§6](07-replay.md#6-the-runner-and-its-verdicts). -- **Depends on:** [W20](#x-w20--engine-versioning-and-release-tags). -- **Status:** Done — [PR #73](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/73). -- **Done when:** all three verdicts (`match`/`diverged`/`unrunnable`) are reachable and - tested; `at` is a `Decision.index`, never a `seq`; a rejected submission records - `seq: null` and does not stop the replay; achievements are read from an in-memory - `ProfileStore` after the last submission. - -### [x] W22 — Replay Oracle: The Corpus -The committed `fixtures/replay/*.{fixture,outcome}.json` set (07 §4) against the real -Bureaucracy campaign: every MVP §5 playable box, plus a deliberate edge case. Also promotes -`createCountingIds` to `core/determinism/counting-ids.ts` and extracts the single real -`story-graph` kind assembly (`kinds/story-graph/kind.ts`), de-duplicating five byte-identical -copies test files accumulated across W16–W19 that a sixth (this unit's own corpus test) would -otherwise have joined. -- **Spec:** [07 §4](07-replay.md#4-the-corpus), [§5](07-replay.md#prerequisite-a-controllable-idsource). -- **Depends on:** [W21](#x-w21--replay-oracle-outcome-and-the-runner). -- **Status:** Done — [PR #73](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/73). -- **Done when:** every MVP §5 *Playable* box has a fixture (the arc, the gated choice, the - seeded transition, the achievement, the loop gate); at least one deliberate edge-case - fixture exists (a rejection, an unknown action); `kinds/story-graph/kind.ts` is the - single kind assembly and the five duplicates are gone; a hand-edited `.outcome.json` - produces `diverged` with the right `at`. - -### [x] W23 — Replay Oracle: CI Wiring -No `paths` filter was actually kept on `pull_request` — one was tried and reverted (`ci.yml`'s -own comment: a path-filtered required check that never starts leaves a PR waiting on a report -that never arrives). The equivalent skip lives *inside* the `engine` job instead ("Determine -whether the engine package changed"), so it always reports while skipping the expensive steps -on a documentation-only PR ([07 §8](07-replay.md#8-where-this-runs)). `push` to `main` stays -unfiltered regardless, since path filters and tag pushes don't reliably combine. A new -`release-tag-replay` job runs only on `v*` tags, extracts the previous tag's committed -`.outcome.json` files via `git show`, and runs the corpus test against them via -`REPLAY_BASELINE_DIR` — the actual cross-version comparison. -Regenerating a committed `.outcome.json` is a deliberate, reviewed, single-fixture step — never -an automatic sweep ([07 §7](07-replay.md#7-intended-change-versus-regression)). -- **Spec:** [07 §7–§8](07-replay.md#7-intended-change-versus-regression). -- **Depends on:** [W20](#x-w20--engine-versioning-and-release-tags), [W22](#x-w22--replay-oracle-the-corpus). -- **Status:** Done — [PR #73](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/73). -- **Done when:** the suite runs on engine-package changes and on release tags; it does not - run on documentation-only changes (verified live: a `plans/`-only PR reported `engine` - green in 22s rather than not running at all). Regenerating an outcome file is a - documented, deliberate per-fixture command, never a sweep. **The release-tag job's - comparison branch is now verified live**, not just by local shell-logic testing: cutting - `v0.2.0` (the first commit with a replay corpus) found the job's checkout never fetched - sibling tags (`actions/checkout`'s `fetch-tags` defaults to `false`), so the comparison - always fell through to "nothing to compare yet" regardless of what tags existed — fixed - in `fetch-tags: true`, then proven end to end with a disposable tag: `4 passed | 5 - skipped` against `v0.2.0`'s real fixtures (PR #76). Milestone M3 - (`plans/27-replay-oracle-programme.md`) needed restating too — it said the *second* tag - would prove this, which undercounts by one: `v0.1.0` predates the corpus entirely, so a - second tag would have hit the same fixture-free skip on correct code. `v0.2.0` is the - actual milestone M3 needed (`plans/35-w26-toolchain-upgrade.md`, Decision 4). - -### Rigour: Spec and Toolchain Debt - -Not new capability — the documentation and tooling debt six units (W8–W13) each correctly -deferred as "not blocking," none since picked up because no unit owned them. Named and -sequenced in [`plans/33-post-mvp-programme.md`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/blob/main/plans/33-post-mvp-programme.md), Tranche A. - -### [x] W24 — Core Spec Reconciliation -Codifies in `04-core.md` §12 and §18 the conventions the code has implemented and depended on -since W8–W13 (two `StateChange` shapes, the `.reason.*` messageKey namespace, the -`Condition` shape itself), fixes one intra-document contradiction in `03-story-graph-kind.md` -§9, closes the doc-tree numbering item, ticks all eighteen boxes of `09-clients.md` §4's API -coverage checklist with a real test cited per box, and corrects six entries `TODO.md` itself -had gotten wrong. -- **Spec:** [04 §12](04-core.md#12-reason-codes-state-changes-messages), [§18](04-core.md#18-frozen-primitives); [03 §9](03-story-graph-kind.md#9-projection--what-a-client-sees); [09 §4](09-clients.md#4-the-api-coverage-checklist). -- **Depends on:** nothing. -- **Status:** Done — [PR #77](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/77). -- **Done when:** every convention is stated where the interface it belongs to is defined, not - only where it is used; `09-clients.md`'s checklist reflects real, run, passing tests - rather than an unticked table beside a closed MVP claim; every corrected `TODO.md` entry - cites the code or plan that proves the correction; no file under `src/engine/` changes. -- **Plan:** [`plans/34-w24-core-spec-reconciliation.md`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/blob/main/plans/34-w24-core-spec-reconciliation.md) - -### [x] W25 — Simulation Kind Seam Reconciliation -Brings `10-simulation-kind.md` up to date against the conventions W24 codified and closes gaps -against the `Kind` interface it never addressed: folds `initialState`/`InitialStateResult` into -§3, adds a deferred §14 Validation (matching the `history` deferral's own idiom), narrows the -`GameStatus` mapping in §2 to match `12-world-graph-kind.md` §8's identical resolution, and -replaces `outcome()`'s shape — dropping an `endingId` this kind never had a concept for, -widening `resolution` to three values, and adding `goalsFailed`. Completes §15 ("What Remains -Upstream"), which accounted for one of `SimulationKindState`'s ten fields before this. -- **Spec:** [10 §2](10-simulation-kind.md#2-kindstate--what-belongs-here), [§3](10-simulation-kind.md#3-the-turn-is-a-week), - [§12](10-simulation-kind.md#12-terminal-identity), [§14](10-simulation-kind.md#14-validation), - [§15](10-simulation-kind.md#15-what-was-ported-and-what-was-found-along-the-way). -- **Depends on:** [W24](#x-w24--core-spec-reconciliation). -- **Status:** Done — [PR #80](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/80). -- **Done when:** every field `SimulationKindState` names has a row in §15; `outcome()`'s shape - is justified against the upstream source rather than assumed (confirmed by full-text - search that this kind has no ending concept); a review finding that the - `week_limit_reached`/goal precedence is unresolved is flagged explicitly rather than - guessed at; no file under `src/engine/` changes. -- **Plan:** [`plans/32-w25-simulation-kind-seam-reconciliation.md`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/blob/main/plans/32-w25-simulation-kind-seam-reconciliation.md) - -### [x] W26 — Toolchain Upgrade -`vitest` 2.1.9 → 4.1.10, `eslint` 9.39.5 → 10.8.0. `typescript-eslint` did **not** need to -move — 8.65.0 already declares `eslint: "^8.57.0 || ^9.0.0 || ^10.0.0"` as a valid peer, so the -plan's anticipated three-package bump was actually two. `npm audit` went from 6 vulnerabilities -(3 moderate, 2 high, 1 critical) to **0** — better than the "partial clear is fine" outcome the -plan allowed for. The determinism guard's specific rules (`no-restricted-properties`, -`no-restricted-globals`, `no-restricted-imports`) are unchanged in eslint 10's migration guide; -Node 24 already satisfies its new floor (`>=20.19`/`>=22.13`/`>=24`). -- **Spec:** none — tooling only. -- **Depends on:** [W18](#x-w18--determinism-harness), [W22](#x-w22--replay-oracle-the-corpus) — - both done, the entry's own stated precondition, met twice over once W20–W23 added the - replay oracle as a second instrument. -- **Status:** Done — [PR #82](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/82). -- **Done when:** 39 files / 445 tests pass unchanged; the determinism guard is proven still - live by a deliberate red-path test (a `Math.random()` and a banned core→kind import each - independently fail lint, both reverted); the replay corpus and the W18 event-stream golden - are verified byte-unmodified by the upgrade, not merely unregenerated; `v0.2.0` — tagged - ahead of this unit specifically so a real predecessor corpus would exist — precedes the - tag this unit cuts. -- **Plan:** [`plans/35-w26-toolchain-upgrade.md`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/blob/main/plans/35-w26-toolchain-upgrade.md) - -### Rigour: Session Capture - -Turning a played session into a fixture, per -[`08-session-capture.md`](08-session-capture.md). **Gated on the hosting layer**, which -MVP §4 defers — there is nothing to capture from a local client the developer drives -themselves. - -- [ ] Capture emits a `ReplayFixture` and no new format ([08 §2](08-session-capture.md#2-what-is-captured)). -- [ ] The refusal rules hold under test: no identity, only kind-declared params, no timing - ([08 §3](08-session-capture.md#3-what-is-refused)). A fixture built from a submission carrying undeclared keys drops them. -- [ ] Capture triggers only on an `error`-severity event or an explicit report — never as - background collection ([08 §5](08-session-capture.md#5-when-a-capture-may-be-taken)). -- [ ] Promotion into the replay corpus is a reviewed human step, never automatic ([08 §7](08-session-capture.md#7-promotion-is-a-one-way-door)). - -> **Not sliced, and the reason changed.** The stated gate above — "there is nothing to capture -> from a local client the developer drives themselves" — has quietly stopped being true: -> [SubZeroDev.Adventures](https://github.com/The-Running-Dev/SubZeroDev.Adventures) is a real -> hosted deployment with an API, Postgres persistence and accounts. The hosting precondition is -> met. -> -> **What blocks it now is a missing contract surface, which is `/contract`'s work and not a -> slice's.** [08 §3.2](08-session-capture.md#3-what-is-refused) requires capture to keep "only -> the parameters the kind declares," dropping every other key — but no kind declares its -> parameters anywhere the core can read. [04 §6](04-core.md#6-scenes-and-actions-generic) states -> the opposite outright: `AvailableAction` "describes a verb, not its parameter space," and -> pushes the parameter domain into the kind's projection, which the core holds as `unknown` by -> design. So §3.2 is unimplementable as written, and a slice for it would have to invent a -> `Kind` signature the contract does not have, which no slice may do. The same gap makes -> [08 §9](08-session-capture.md#9-deferred)'s first deferral stale for a second reason: it -> defers simulation-kind capture because "the kind does not exist yet," and W32–W57 built it. -> -> **Route to `/contract`:** either the `Kind` interface gains a declared-parameter surface, or -> §3.2 is restated against something a host can actually enforce. Everything else in this -> section is buildable once that is settled. - -### Depth: Life in the Fast Lane (The `simulation` Kind) - -- [x] **Specify the kind first, in this repository.** Done across W25 (seam reconciliation) - and the four-unit programme W32–W35 (field-level port). The simulation equivalent of - [`03-story-graph-kind.md`](03-story-graph-kind.md) now exists in - [`10-simulation-kind.md`](10-simulation-kind.md), reconciled with `04` §3, the - `GameState` envelope, `Kind.outcome`, and the `kind.simulation.*` event namespace - ([05 §9](05-observability.md#9-kind-events)) — see - [`plans/36-simulation-kind-programme.md`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/blob/main/plans/36-simulation-kind-programme.md) - for the full split and findings. -- [x] **Build the engine-owned simulation kind.** Done across W36–W40: state and variables, - the weekly pipeline, content definitions, the vertical slice, validation, and the real - `Kind` assembly all run through the core engine seam. -- [x] **Engine-owned "Stable Life" fixture reaches a win and a loss through the replay - oracle.** W40 commits both paths and records the remaining honest gap: simulation - projection is not implemented, so neither path is playable through the text client or - MCP yet. That is separate from the engine/replay milestone and remains part of the full - game Definition of Done below. -- [ ] **From a proven loop to a played game — W50–W57 below.** W36–W40 proved the kind - through the engine seam and the replay oracle; it is still not playable by a person, - and eleven of §3's fifteen end-of-week systems, twenty-eight of §4.2's thirty - `ActionType`s, §6.1's derived-value layer and §9's projection are all specified and - unbuilt. Those eight units are that gap, sliced. -- [ ] Its Definition of Done: `games/life-in-the-fast-lane.md`. - -### Depth: Sun Trap (The `world-graph` Kind) - -The third kind, and the first spatial one. **Specified —** -[`12-world-graph-kind.md`](12-world-graph-kind.md) fixes the seam; the game it serves lives -in [SubZeroDev.SunTrap](https://github.com/The-Running-Dev/SubZeroDev.SunTrap) ([12 §17](12-world-graph-kind.md#17-what-remains-in-the-game-repository)). - -**Now has its own programme doc**, the same milestone `simulation` reached with `plans/36`: -[`plans/39-world-graph-kind-programme.md`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/blob/main/plans/39-world-graph-kind-programme.md), -proposing **W41–W49** (one consumer-boundary unit, three contract units, five build units), -cut once Sun Trap's own -design docs (`content-and-systems.md`, `game-design.md`, `mvp.md`, `client-specification.md`) -existed to size it against — the precondition `plans/33`'s own Decision 3 named as missing -when it declined a programme doc for this kind. - -- [x] **`KindContext.derive` and the `tick` stream — already built, since W1/W2.** Not a gap - this kind needs to close: `KindContext.derive` (04 §3.1) and all four `StreamId` - variants, including `tick` and `agent`, exist in `core/kernel/types.ts`, - `core/kernel/engine.ts`, `core/determinism/types.ts` and `core/determinism/rng.ts` - (the encoder, exhaustiveness-guarded). `simulation`'s NPC draws and this kind's tick - draws already have a reachable home — 04 §3.1's own callout box already documents - `derive` closing exactly the reachability gap this checkbox describes as still open. - - -### [x] W41 — Engine Consumer Boundary {#w41} - -[PR #108](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/108) landed the -whole boundary: the package renamed to `@the-running-dev/game-engine`, one root export -(`src/engine/src/index.ts`, explicit named re-exports only), `exports` with `types` and -`import` targets, `files: ["dist"]` plus a `tsconfig.build.json` that excludes tests and -`campaigns/` from the emit, a `consumer-smoke/` project that installs the **packed -tarball** rather than linking the source, three new required-CI steps (pack, tarball -inspection, consumer smoke — all three verified running green on `main` at `db9c62a`), -and `release-engine-package.yml` publishing on a `v*` tag with `packages: write` and no -stored credential. Review also corrected a drift this unit made live: `package.json` -had never tracked the release tags (`v0.1.0` shipped `0.0.0`; `v0.2.0` and `v0.3.0` both -shipped `0.1.0`), harmless while private and unpublished, fatal once `npm publish` ships -what the manifest says. Set to `0.3.0`; tag and manifest move together from here. -**Now complete.** `@the-running-dev/game-engine@0.4.0` published on the `v0.4.0` tag -(2026-08-02), verified against the packages API rather than inferred from the workflow's -exit status; the coordinate is recorded in `plans/40`'s Done-When, and milestone **T0** -is reached. One deviation, recorded rather than waved through: it published **public** -where `plans/39` and `plans/40` specify private. That also surfaced a stale claim in -this file's own introduction, which described every companion repository as private when -this one and Sun Trap are public — corrected in the same change -([`OPEN-QUESTIONS.md`](OPEN-QUESTIONS.md) §2). No read-access grant was needed as a -result. - -- **Plan:** [`plans/40-w41-engine-consumer-boundary.md`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/blob/main/plans/40-w41-engine-consumer-boundary.md) - -### [x] W42 — World-Graph Runtime State Contract {#w42} - -Merged in [PR #116](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/116). -Authoritative map, guest, building, queue, staff, construction and finance shapes now live in -[`12-world-graph-kind.md`](12-world-graph-kind.md) §3. The plan remains the evidence trail: -[`plans/42-w42-world-graph-state-contract.md`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/blob/main/plans/42-w42-world-graph-state-contract.md), -including the correction to `plans/39`'s original sizing: six types in -`WorldGraphKindState`'s closure -(`Incident`, `ObjectiveProgress`, `Alert`, `TerrainCell`, `PathCell`, `Zone`) are -drafted in neither repository, so this is design work, not a port. - -### [x] W43 — World-Graph Content Definition Contract {#w43} - -Merged in [PR #119](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/119). -Maps, archetypes, buildings, products, terrain, incidents, scenarios, objectives, policies, -and achievements now have their complete source/runtime schema, W42 reconciliation, -validation tiers, and worked fixtures in `design/20-contract.md`'s -`engine/12-world-graph-kind.md` block and its generated reader copy. The execution -record and evidence checklist live in -[`plans/43-w43-world-graph-content-contract.md`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/blob/main/plans/43-w43-world-graph-content-contract.md), -which is historical rather than a second contract authority. - -### [x] W44 — World-Graph Resolution Contract {#w44} - -Merged in [PR #120](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/120). -The 20-system pipeline, utility scoring, canonical pathfinding, queue/service and staff-task -semantics, simultaneous terminal precedence, and deep batch invariance are in -`design/20-contract.md`'s `engine/12-world-graph-kind.md` block and its generated -reader copy. Its execution record is -[`plans/44-w44-world-graph-resolution-contract.md`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/blob/main/plans/44-w44-world-graph-resolution-contract.md), -which is historical rather than a second contract authority. - -### [x] W45 — World-Graph Kind Skeleton and Immediate Actions {#w45} - -Merged in [PR #125](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/125). -The source/runtime builder, total validation, shared spatial substrate, deterministic initial state, read surfaces, -production assembly, package exports, and nine no-time-passes reducers are reconciled -in [PR #125](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/125), with -[PR #124](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/124) retained as -the original implementation review. The execution record is -[`plans/45-w45-world-graph-kind-skeleton.md`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/blob/main/plans/45-w45-world-graph-kind-skeleton.md), -which is historical rather than a second contract authority. - -### [x] W46 — World-Graph Deterministic Tick Pipeline {#w46} - -Fixed-order systems, bounded `advance_ticks`, derived streams and batch invariance are delivered in -[PR #128](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/128), merged to -`main` at `6301a49`. Its implementation plan is -[`plans/46-w46-world-graph-tick-pipeline.md`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/blob/main/plans/46-w46-world-graph-tick-pipeline.md); -it follows the canonical world-graph contract and does not supersede it. - -### [x] W47 — World-Graph MVP Vertical Slice {#w47} - -The synthetic guest journey — spawn → walk → queue → buy → litter → clean → objective → win/lose — is delivered in -[PR #131](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/131), merged -to `main` at `2390750`. It follows the canonical world-graph contract and does not -supersede it. - -### [x] W48 — Preview/Client Parity {#w48} - -`previewAction` across Engine, session, text and MCP surfaces, with 09 §4 and `MVP.md` §5 amended in the same unit, is delivered in -[PR #133](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/133). - -### [x] W49 — World-Graph Validation, Scenario and Replay Guard {#w49} - -The canonical engine-owned MVP fixture and its Tier 1/Tier 2 validation landed in -[PR #134](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/134); deterministic -winning and losing replay pairs and release-corpus coverage landed in -[PR #136](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/136). -Session-parity replay cases, a clean-build serialization proof, and a consumer-smoke -rerun landed in [PR #138](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/138). -W49's engineering scope is complete. Package version selection and publication remain -external release actions, not evidence this unit claims. - -- [ ] **T4 programme gate:** batch invariance and determinism beyond the seed pass; an - immutable package version must carry the replay-guarded third kind so Sun Trap can - install it without a sibling checkout. - -- **Plan:** [`plans/39-world-graph-kind-programme.md`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/blob/main/plans/39-world-graph-kind-programme.md) - -### Depth: Finish the Bulgaria Adventure - -### [x] W27 — Bulgaria Adventure: The Driving Arc -The second real arc: `src/engine/src/campaigns/bulgaria-driving.ts`, authored from -`games/bulgaria.md`'s "Driving" and "BMW Ownership" scenes, mirroring -`bulgaria-bureaucracy.ts`'s established pattern. Picked over the other three remaining arcs -because reading the actual built Bureaucracy campaign against the design doc surfaced two real -discrepancies affecting Enterprise and Return specifically — both recorded in -[`OPEN-QUESTIONS.md`](OPEN-QUESTIONS.md) §2, neither resolved here. Demonstrates a branching -ending (two endings, gated by an earlier choice via `showWhen`) that Bureaucracy's single-ending -design never exercised. -- **Spec:** [03 §4](03-story-graph-kind.md#4-choices-and-transitions) (`showWhen`); - `games/bulgaria.md`, `games/bulgaria-adventure.md`. -- **Depends on:** nothing engine-side — `story-graph` is fully built; this is content only. -- **Status:** Done — [PR #86](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/86). -- **Done when:** both endings are reachable and the wrong-branch choice is absent, not merely - disabled, verified by test; `serialize()` output for each path is golden-filed and - round-trips; the determinism harness's sink-independence and replay-byte-identity checks - pass; test count grows from 39 files/445 tests. Replay-corpus fixtures were found not to - be cheap for a second campaign — the existing harness assumes one campaign per corpus - directory (`OPEN-QUESTIONS.md` §2) — so this unit has none; not blocking. -- **Plan:** [`plans/37-w27-bulgaria-driving-arc.md`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/blob/main/plans/37-w27-bulgaria-driving-arc.md) - -### [x] W28 — Bulgaria Adventure: The Return Arc -The third real arc: `src/engine/src/campaigns/bulgaria-return.ts`, authored from -`games/bulgaria.md`'s single "Expat Returns" scene. No separate plan file — the pattern was -proven twice by W27, and this arc's own design had no open question to resolve: unlike Driving, -`games/bulgaria-adventure.md` names no mechanic for Return beyond "seeds variables the other -arcs read," already found (`OPEN-QUESTIONS.md` §2, W27) not to be mechanically achievable. The -correct minimal design is therefore a single `choice` node whose four options (matching the -source scene's own four reactions) all converge on one shared `ending` — no invented flag or -branch. Deliberately the smallest arc built so far: one node, no variables, no achievement. -- **Spec:** `games/bulgaria.md`, `games/bulgaria-adventure.md`. -- **Depends on:** nothing engine-side — content only, same as W27. -- **Status:** Done — [PR #87](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/87). -- **Done when:** all four choices reach the one ending; `serialize()` output is golden-filed and - round-trips; the determinism harness's sink-independence and replay-byte-identity checks - pass; test count grows from 41 files/478 tests. - -### [x] W29 — Bulgaria Adventure: The Inheritance Arc -The fourth real arc: `src/engine/src/campaigns/bulgaria-inheritance.ts`, authored from -`games/bulgaria.md`'s three scenes ("Property Inheritance", "Village Life", "Family Meeting"). -Larger than the two before it — `games/bulgaria-adventure.md` names this arc's own exercise as -"branching on prior choices, relationship variables, an ending," which needed real design, not -just transcription. Two variables carry it: `family_tension` (int, visible) accumulates the way -Bureaucracy's own counters do, without gating anything; `has_documentation` (bool, set only by -`request_records` or `consult_lawyer` in the first scene) gates the one choice at the climax -that actually resolves the plot (`bring_out_documents`, via `showWhen`) — that option does not -exist at all for a player who never sought documentation, the clearest possible instance of -"branching on prior choices." A second, different kind of branch: `pretend_never_inherited` in -the second scene skips the climax entirely via an immediate `goto`, rather than a gate. Three -endings result: an early opt-out, an ungated "nothing resolved," and the one gated "documents -settle it" (`outcome: "win"`). -- **Spec:** `games/bulgaria.md`, `games/bulgaria-adventure.md`; - [03 §4](03-story-graph-kind.md#4-choices-and-transitions) (`showWhen`). -- **Depends on:** nothing engine-side — content only, same as W27/W28. -- **Status:** Done — [PR #88](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/88). -- **Done when:** the gated choice is confirmed absent (not disabled) without prior - documentation, and present and leading to the win ending with it; the early-opt-out branch - is confirmed to skip the climax node entirely; `family_tension` accumulates and clamps at - its floor; `serialize()` output for each of the four fixtures is golden-filed and - round-trips; the determinism harness's sink-independence and replay-byte-identity checks - pass; test count grows from 43 files/497 tests. - -### [x] W30 — Bulgaria Adventure: The Enterprise Arc -The fifth and final real arc: `src/engine/src/campaigns/bulgaria-enterprise.ts`, authored from -`games/bulgaria.md`'s "Starting a Business" and "Entrepreneur" scenes only — the arc's own third -scene, "Ultimate Reward," and its "It Builds Character" achievement, are already spent by -Bureaucracy (`OPEN-QUESTIONS.md` §2, found during W27). Design proposed for sign-off before -implementation, since inventing closing content is a bigger step than transcribing existing -scenes: `debt_cents` (int, visible) carries what remains of the named exercise -("accumulating debt"), as a running stat in the same idiom as Bureaucracy's own counters, not a -gate; no achievement, since the game's Definition of Done needs only "at least one" across the -whole game; one shared ending rather than a branch, since `games/bulgaria-adventure.md` names -"an ending" for this arc singular, same as the others, and with the achievement gone nothing -remaining calls for more structure. The ending's own prose is new, not adapted from any -`bulgaria.md` scene, since the one this arc was assigned is unavailable. - -**All five Bulgaria Adventure arcs are now built** — Bureaucracy (MVP), Driving (W27), Return -(W28), Inheritance (W29), Enterprise (W30). -- **Spec:** `games/bulgaria.md`, `games/bulgaria-adventure.md`. -- **Depends on:** nothing engine-side — content only, same as W27–W29. -- **Status:** Done — [PR #89](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/89). -- **Done when:** all four `starting_a_business` choices proceed to `entrepreneur`; each - `entrepreneur` choice's `debt_cents` effect is confirmed independently (0, 5000, or - 20000) and all four reach the one ending; `serialize()` output for the high-debt and - no-debt paths is golden-filed and round-trips; the determinism harness's - sink-independence and replay-byte-identity checks pass; test count grows from 45 - files/537 tests. - -- [ ] Its full Definition of Done beyond content: `games/bulgaria-adventure.md` lists MCP - parity, gated choices, seeded random, achievement persistence, save/load, validation, and - byte-identical replay as separate checkboxes, but all seven are platform capabilities - already proven generically (W7, W14, W16–W18) against Bureaucracy and re-exercised by - every arc's own test suite since — not separate work per arc. - -### [x] W31 — Save Migration -Builds the real save-migration mechanism `04-core.md` §10.2 specifies, closing the gap this -list carried since W3. `Kind` gains `version` and an optional `migrateState`; `Campaign` gains -an optional `migrateState` for content-id renames — neither is a new port -([06 §6](06-extensibility.md#6-adding-a-port)'s own rule rules that out, since a migration -function's whole purpose is to change what `serialize()` produces. `SessionStore.saveGame` -now stamps a real `SaveEnvelope` (`core/persistence/envelope.ts`) instead of a bare blob; -`loadGame` verifies its checksum and all five stamped fields, dispatching to `Kind.migrateState` -then `Campaign.migrateState` on a version mismatch, and failing loudly -(`save_requires_migration` / `migration_failed`) when no migration path resolves one. Proven -against a synthetic kind/campaign fixture, not a real Bulgaria campaign republish — every -shipped campaign is still at `1.0.0`. -- **Spec:** [04 §3](04-core.md#3-the-kind-interface--the-seam), [§10.2](04-core.md#102-save-envelope-and-migration); [06 §6](06-extensibility.md#6-adding-a-port). -- **Depends on:** nothing engine-side. -- **Status:** Done — [PR #92](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/92). -- **Done when:** every base-case save/load test still passes unmodified (the envelope is - transparent to `SessionStore`'s public surface); a kind-version mismatch with a - registered migration succeeds and flips `replayCompatible: false`; the same with no - migration registered fails loudly; a campaign-version mismatch does the same; both axes - moving at once run kind migration before campaign migration, proven by an ordering - guard, not just an assertion; a `saveFormatVersion`/`serializationVersion` mismatch fails - loudly (neither has ever moved); an `engineVersion` mismatch never gates a load; `npm run - typecheck && npm run lint && npm test` all pass. -- **Plan:** [`plans/38-save-migration-programme.md`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/blob/main/plans/38-save-migration-programme.md) - -### [x] W32 — Simulation Kind: State Types -The first contract unit of the simulation-kind programme -([`plans/36-simulation-kind-programme.md`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/blob/main/plans/36-simulation-kind-programme.md), -proposed there as W27). Doc-only — ports upstream §5.1, §5.3–§5.6 and §9.1 into -`10-simulation-kind.md` as new §2.1–§2.5 and §4.1: `CalendarState`, `WorldState` (with its -world-strangeness and chain-scope subsections), `StatusEffect`/`Opportunity`/`ScheduledEvent`/ -`PendingEventResponse` (with both lifecycle subsections), `GoalState`, `EconomyState`, and -`WeeklyActionPlan`'s own shape. Introduces this kind's `Cents`/`BasisPoints` primitives and its -sorted-`Record`-iteration rule, both reused by later units. Eight of `SimulationKindState`'s ten -fields are now specified in this repository; only `PlayerState` (W28, next) and the `GameAction` -schema `WeeklyActionPlan.actions` holds (W30) remain. -- **Spec:** [10 §2](10-simulation-kind.md#2-kindstate--what-belongs-here), [§4.1](10-simulation-kind.md#41-the-weekly-action-plan), - [§15](10-simulation-kind.md#15-what-was-ported-and-what-was-found-along-the-way). -- **Depends on:** [W25](#x-w25--simulation-kind-seam-reconciliation) (the seam must be complete - before field detail hangs off it, per `plans/36`). -- **Status:** Done — [PR #94](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/94). -- **Done when:** every field named in `SimulationKindState` (§2) except `PlayerState` has a - full type restated in this repository, reconciled against envelope-duplication and - forward-referencing not-yet-ported types (`NPCState`, `AgentState`, `GameAction`, - `Modifier`, `OpportunityDefinition`) by name rather than inventing placeholder shapes; - §15's table drops the rows this unit closes and gains no new ones; a genuine new open - item found during the port (`ChainScope`'s `"profile"` value has nowhere to persist) is - recorded in `OPEN-QUESTIONS.md`, not silently absorbed or dropped; no file under - `src/engine/` changes; `build/Test-Documentation.ps1` passes. -- **Plan:** [`plans/36-simulation-kind-programme.md`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/blob/main/plans/36-simulation-kind-programme.md) - -### [x] W33 — Simulation Kind: Actor State -The second contract unit of the simulation-kind programme -([`plans/36-simulation-kind-programme.md`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/blob/main/plans/36-simulation-kind-programme.md), -proposed there as W28). Doc-only — ports upstream §7 and §8.1–§8.9 into `10-simulation-kind.md` -as new §6.1–§6.11: the base/derived-value layer (`DerivedPath`, application order, stacking, -expiry), the shared `ActorState`/`PlayerState` shape, and its nine areas (identity, finances, -needs, attributes, education, career, housing, inventory, relationships). `ActorState` comes -over whole, shared verbatim by the player and every rival (`plans/36` Finding 1) — not ported -"for the player" with rival support deferred. Every field `SimulationKindState` (§2) names now -has a full shape in this repository. -- **Spec:** [10 §6](10-simulation-kind.md#6-player-state), [§15](10-simulation-kind.md#15-what-was-ported-and-what-was-found-along-the-way). -- **Depends on:** [W32](#x-w32--simulation-kind-state-types). -- **Status:** Done — [PR #95](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/95). -- **Done when:** `ActorState`'s open-keyed `Record` fields (`skills`, `reputation`, `flags`, - `counters`) are explicitly reconciled against `02-architecture.md` N6 ("the loose bag is - banned") rather than left to look like an unexamined exception; not-yet-ported types - (`AgentState`, `NPCState`, `JobDefinition`, `CourseDefinition`, `HousingDefinition`, - `ItemDefinition`, `BackgroundDefinition`) are forward-referenced by name; the integer - `0–100` range rule for needs/skills/attributes/reputation is stated somewhere, not - silently dropped; §15's table drops every row this unit closes; no file under - `src/engine/` changes; `build/Test-Documentation.ps1` passes. -- **Plan:** [`plans/36-simulation-kind-programme.md`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/blob/main/plans/36-simulation-kind-programme.md) - -### [x] W34 — Simulation Kind: Content Definition Types -The third and largest contract unit of the simulation-kind programme -([`plans/36-simulation-kind-programme.md`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/blob/main/plans/36-simulation-kind-programme.md), -proposed there as W29). Doc-only — ports upstream §13.3–§13.4 and §14.1–§14.9 into -`10-simulation-kind.md` as new §7.1–§7.10: `Modifier`/`Reward`, then jobs, courses, housing, -items, events, NPCs, goals/scenarios/difficulty, supporting definitions (opportunities, -achievements, headlines, employers, locations, backgrounds, traits, skills), and agents. -Corrects a factual error in `plans/32`'s Finding H and `plans/36`'s own Finding 2 — both claimed -upstream specifies no rounding rule for `Modifier.operation: "multiply"` against integer-cents -money; checked directly against the primary source while drafting §7.1, and it does ("rounded -half-away-from-zero after the full chain"). Both plan documents corrected in place. Every -upstream section this contract needs except the last unit's own (`ActionType`/`GameAction`, -`Requirement`, end-of-week ordering) is now specified in this repository. -- **Spec:** [10 §7](10-simulation-kind.md#7-content-definition-types), [§8](10-simulation-kind.md#8-conditions-and-requirements), - [§15](10-simulation-kind.md#15-what-was-ported-and-what-was-found-along-the-way). -- **Depends on:** [W33](#x-w33--simulation-kind-actor-state). -- **Status:** Done — [PR #96](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/96). -- **Done when:** every content type upstream §14.1–§14.9 names has a full shape in this - repository, with identity fields (`id`/`version`/`titleKey`) excluded per the - envelope-duplication rule; `Modifier`'s `multiply` semantics are stated precisely - (basis-points-shaped `value`, round-half-away-from-zero once after the full chain), not - merely flagged as unresolved; the `plans/32`/`plans/36` correction is recorded in both - plan documents, not only in the new spec content; not-yet-ported types (`Requirement`, - `GameAction`, `ActionOutcome`) are forward-referenced by name; §15's table drops every - row this unit closes and states that exactly one contract unit remains; no file under - `src/engine/` changes; `build/Test-Documentation.ps1` passes. -- **Plan:** [`plans/36-simulation-kind-programme.md`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/blob/main/plans/36-simulation-kind-programme.md) - -### [x] W35 — Simulation Kind: Resolution and Systems -The fourth and final contract unit of the simulation-kind programme -([`plans/36-simulation-kind-programme.md`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/blob/main/plans/36-simulation-kind-programme.md), -proposed there as W30). Doc-only — ports upstream §9, §10, §12.2–§12.3 and §13.2 into -`10-simulation-kind.md`: `ActionType`/`GameAction` (§4.2), the resolver dispatch mechanism and -per-action outcome (§5.1–§5.3, reconciled against `04-core.md`'s already-adopted -`StateChange`/`ValidationError`/`ValidationWarning` rather than restating upstream's own -divergent pre-adoption shapes), end-of-week system order and goal/failure precedence (§3), and -`Requirement`/`RequirementType` (§8.1). Closes **S1** of the programme's milestones — every -type `SimulationKindState` names, every content definition type, and the mechanics that -dispatch on both are now specified in this repository; `10-simulation-kind.md` stops being -"the seam only." - -Three reconciliation findings, not plain transcription: this kind's own runtime-validation -result needed a name (`ActionValidation`) distinct from `04-core.md`'s load-time -`ValidationResult` — upstream never had to disambiguate the two, having no campaign-validation -concept of its own to collide with. `ResolutionDebugInfo` (upstream §3.3) is superseded, not -ported — this platform's `trace`-severity observability channel already serves its purpose, and -the `metadata.transparency` field it would gate on lives outside `SimulationKindState` entirely. -And `wrong_location` — load-bearing in `LocationDefinition`'s own prose since W34 — was missing -from §10's reason-code table, a real gap caught while finishing this unit, not a pre-existing -one reported from elsewhere. -- **Spec:** [10 §3](10-simulation-kind.md#3-the-turn-is-a-week), [§4.2](10-simulation-kind.md#42-action-types), - [§5.1](10-simulation-kind.md#51-resolver-dispatch), [§8.1](10-simulation-kind.md#81-requirements), - [§15](10-simulation-kind.md#15-what-was-ported-and-what-was-found-along-the-way). -- **Depends on:** [W34](#x-w34--simulation-kind-content-definition-types). -- **Status:** Done — [PR #97](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/97). -- **Done when:** `ActionType`/`GameAction` are specified with no client-supplied cost fields; - the resolver dispatch mechanism (`ActionResolver`, `ResolverTable`) is reconciled against - `KindContext` (04 §3.1) rather than upstream's own bespoke `ResolutionContext`; per-action - outcome types reuse `04-core.md`'s `StateChange`/`OutcomeMessage`/`ValidationError`/ - `ValidationWarning` verbatim rather than restating upstream's divergent versions; - end-of-week ordering states plainly that `weekLimit` has no scheduled check anywhere in - it, reinforcing rather than merely repeating §12's existing open callout; every stale - "§7/§9, once ported" forward reference written across W32–W34 (16+ instances) is fixed to - point at a real section; §15 is rewritten from "what remains" to a historical record with - no open rows; no file under `src/engine/` changes; `build/Test-Documentation.ps1` passes. -- **Plan:** [`plans/36-simulation-kind-programme.md`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/blob/main/plans/36-simulation-kind-programme.md) - -### [x] W36 — Simulation Kind: State, Variables, and the Plan -First **build** unit of the simulation-kind programme (`plans/36-simulation-kind-programme.md`, -proposed there as W31, mirroring story-graph's W9). `SimulationKindState` and every nested -runtime-state type (`src/engine/src/kinds/simulation/state.ts`, `actor.ts`) as real TypeScript, -plus `ActionType`/`GameAction`/`WeeklyActionPlan` and pure `plan.add`/`remove`/`clear` reducers -(`plan.ts`) — standalone functions, not wired into `Kind.advance` yet, the same precedent W9 -set for `applyConsequences`. `removeAction` rejects an out-of-range index as a genuine runtime -rejection (`action_not_planned`), not a throw — a stale client index is ordinary play, unlike -W9's undeclared-variable case. -- **Spec:** [10 §2](10-simulation-kind.md#2-kindstate--what-belongs-here), [§4.1](10-simulation-kind.md#41-the-weekly-action-plan), - [§4.2](10-simulation-kind.md#42-action-types), [§6](10-simulation-kind.md#6-player-state). -- **Depends on:** [W35](#x-w35--simulation-kind-resolution-and-systems). -- **Status:** Done — [PR #98](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/98). -- **Done when:** every field `SimulationKindState`/`ActorState` names in the contract has a - real TypeScript type; `addAction`/`clearPlan` never mutate their input plan; - `removeAction` rejects a negative index, an index equal to the plan's length, and a - non-integer index, all with `action_not_planned`; no `Kind.advance`/`kernel/engine.ts` - wiring exists yet; `npm run typecheck && npm run lint && npm test` all pass. -- **Plan:** [`plans/36-simulation-kind-programme.md`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/blob/main/plans/36-simulation-kind-programme.md) - -### [x] W37 — Simulation Kind: The Week -Second build unit of the simulation-kind programme (`plans/36-simulation-kind-programme.md`, -proposed there as W32, mirroring story-graph's W11+W12 combined). `Kind.initialState`, -`Kind.advance` (`plan.add`/`remove`/`clear`/`end_week`), the resolver dispatch table, and -the full 4-step start-of-week + 14-step end-of-week system pipeline, all real and wired -through `createEngine`/`submitAction` for the first time. - -Most end-of-week systems (`employment`, `education`, `housing`, `finance_*`, `opportunities`' -offer/revoke, `events`, `headline`, `goals`, `failure`, `achievements`) are explicit, -individually-documented stubs — each needs a content type (`JobDefinition`, -`CourseDefinition`, …) that doesn't exist until the content-definition-types build unit, a -genuine dependency this unit's own research surfaced rather than one story-graph's W11/W12 -ever had to solve (its "content" — the node graph — already existed by then). `needs` drift -and opportunity expiry are real logic; every stub is documented at its own definition site, -not silently doing nothing. The `ResolverTable` (`resolvers.ts`) uses one shared stub -resolver for all 30 `ActionType`s, built as a real object literal (not `Object.fromEntries` + -a cast) specifically so TypeScript's own exhaustiveness check has teeth — verified directly -by temporarily deleting an entry and confirming the compiler catches it. -- **Spec:** [10 §3](10-simulation-kind.md#3-the-turn-is-a-week), [§5](10-simulation-kind.md#5-resolution-and-statechange), - [§5.1](10-simulation-kind.md#51-resolver-dispatch). -- **Depends on:** [W36](#x-w36--simulation-kind-state-variables-and-the-plan). -- **Status:** Done — [PR #99](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/99). -- **Done when:** `initialState` builds week-one state from a synthetic `SimulationCampaign`; - start-of-week increments the week, resets spent time, and expires effects correctly; - end-of-week runs all fourteen named systems in the documented order (verified via - `kind.simulation.system.ran`'s own emitted order, since most systems are stubs and - can't be distinguished by their state effects alone); needs drift clamps to `0–100` - and emits one `StateChange` per touched need; a real `createEngine`/`submitAction` - round trip runs `plan.add` then `end_week` and lands on the next week with a fresh, - empty plan; `npm run typecheck && npm run lint && npm test` all pass (641 tests, was - 607). -- **Plan:** [`plans/36-simulation-kind-programme.md`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/blob/main/plans/36-simulation-kind-programme.md) - -### [x] W38 — Simulation Kind: Content-Definition Types -Third build unit of the simulation-kind programme — but no longer the single unit -`plans/36-simulation-kind-programme.md` originally proposed as W33. That unit assumed -story-graph's own precedent — *author content against already-coded types* — held here; it -doesn't, since this kind's content-definition types (contract §7) were deferred to the -doc-only contract phase (W34) rather than built as code the way story-graph's were across -W9–W13. `plans/36` now splits W33 into W38–W40; see that document's own callout under *Build* -for the reasoning. This unit is the first: port contract §7 to -`src/engine/src/kinds/simulation/content.ts` — `Reward`, `JobDefinition` (+6 nested types), `CourseDefinition`, -`HousingDefinition`, `ItemDefinition` (+1 nested), `EventDefinition` (+5 nested), -`NPCDefinition`, `GoalDefinition`/`ScenarioDefinition`/`DifficultyDefinition`, -`OpportunityDefinition`/`AchievementDefinition`/`HeadlineDefinition`/`EmployerDefinition`/ -`LocationDefinition`/`BackgroundDefinition`/`TraitDefinition`/`SkillDefinition`, and -`AgentStrategy`. `NPCState`/`AgentState`/`NPCMemory`/`NPCRelationship`/`AvailabilityRule`/ -`Modifier` already exist (`state.ts`, W36) as runtime state — this unit ports only their -content-side counterparts, and everything else §7 names. No system/resolver wiring here — -that's W39's job, against a settled type surface, the same "contract before code" discipline -the four contract units (W32–W35) already used one level up. -- **Spec:** [10 §7](10-simulation-kind.md#7-content-definition-types), [§7.1](10-simulation-kind.md#71-modifiers-and-rewards)–[§7.10](10-simulation-kind.md#710-agents--engine-owned-strategy-definition-and-runtime-state). -- **Depends on:** [W37](#x-w37--simulation-kind-the-week). -- **Status:** Done — [PR #100](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/100). -- **Done when:** every type §7 names that isn't already runtime state in `state.ts`/`actor.ts` - exists in `content.ts`; `AvailabilityRule.condition` — `unknown` since W36, deferred per - its own comment there — is narrowed to the real core `Condition` type, this unit's - natural import point for it; `npm run typecheck && npm run lint && npm test` all pass - with no new runtime logic — this unit is types only. -- **Plan:** [`plans/36-simulation-kind-programme.md`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/blob/main/plans/36-simulation-kind-programme.md) - -### [x] W39 — Simulation Kind: Wiring the "Stable Life" Vertical Slice {#w39} -Fourth build unit — the second half of the W33 split. Wires real logic into exactly the -systems and `RESOLVER_TABLE` entries a goal-driven win/loss loop needs, against W38's -content types — not all twelve stubbed end-of-week systems or all thirty `ActionType`s. -Real now: `goals` and `failure` (evaluate `GoalDefinition.conditions`/`failureConditions` -via a new `conditions.ts`, tracking `GoalState`'s persistent-goal fields per §2.4 — -`consecutiveWeeksSatisfied` resets to zero on any unsatisfied week, `status` becomes -`"completed"` once it reaches `requiredDurationWeeks`, default 1); `eat`/`rest` (the two -resolvers give the player any way to counter `needs` drift at all — without them no -needs-based goal could ever be won); `Kind.outcome` (`outcome.ts`, §12's terminal-identity -shape); and `advance.ts`'s `end_week` now reports `status: "ended"` once `outcome()` -resolves non-null. Everything else (`employment`, `education`, `housing`, `finance_*`, -`inventory`, `relationships`, `opportunities`' offer/revoke, `events`, `headline`, -`achievements`) stays an honest, documented stub — the same discipline W37 established. - -**This is deliberately a smaller loop than the real game's own "Stable Life," not a first -pass at the whole thing.** The real scenario (`games/03-game-design.md` §16.3 in the -companion `SubZeroDev.GameOfLife` repo) needs six completion criteria across employment, -education, housing and finance together — wiring all of that is its own multi-unit depth -effort, not this unit's job. This mirrors story-graph's own W15 Bureaucracy-campaign -precedent: the engine repo proves the mechanism with a synthetic fixture; the real, -full-depth flagship content is a companion-repo concern layered on afterward. - -Two acknowledged, documented gaps carried from `outcome.ts`: `week_limit_reached` is never -returned (`state` alone carries no `weekLimit` to compare against, and §12 itself calls the -precedence question upstream-unresolved), and a mixed multi-goal outcome (some completed, -some failed) resolves conservatively to `"failed"` — verified only against this unit's own -single-goal tests, not a settled rule. - -`SimulationCampaign` (`campaign.ts`) gained exactly two fields for this: `goals: readonly -GoalDefinition[]` and `goalFailurePrecedence` — still not the real authoring surface -(`ScenarioDefinition` integration is W40's job), just what this unit's own wiring needs. -- **Spec:** [10 §5](10-simulation-kind.md#5-resolution-and-statechange), [§3](10-simulation-kind.md#3-the-turn-is-a-week), - [§12](10-simulation-kind.md#12-terminal-identity). -- **Depends on:** [W38](#x-w38--simulation-kind-content-definition-types). -- **Status:** Done — [PR #101](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/101). -- **Done when:** a goal with no `requiredDurationWeeks` completes the first week its - condition is met; a persistent goal requires that many consecutive satisfied weeks, - resetting on any miss; `goalFailurePrecedence` resolves a goal whose completion and - failure conditions trip the same week (`"goals_win"`, the default, completes it - anyway; `"failure_wins"` fails it instead); `eat`/`rest` restore the needs they target, - clamped; `advance`'s `end_week` reports `status: "ended"` once `outcome()` resolves; - every stub is still individually documented; `npm run typecheck && npm run lint && - npm test` all pass (669 tests, was 644). -- **Plan:** [`plans/36-simulation-kind-programme.md`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/blob/main/plans/36-simulation-kind-programme.md) - -### [x] W40 — Simulation Kind: The "Stable Life" Scenario, Validation, and Corpus -Fifth build unit — the third and last piece of the W33 split, mirroring story-graph's -W14+W15+W22 combined (the original mirror `plans/36` gave W33 as a whole), now that W38/W39 -give it real types and real logic to author and validate against. Authors the "Stable Life" -fixture `SimulationCampaign` (`campaigns/stable-life.ts` — one goal, "Well Rested": maintain -`player.needs.energy` at or above 70 for two consecutive weeks, failing outright below 40), -Tier 1/2 `validateCampaign` for the simulation kind (`kinds/simulation/validate.ts`), the -real `Kind` assembly (`kinds/simulation/kind.ts`, mirroring -`kinds/story-graph/kind.ts`'s own role), and commits replay-corpus fixtures -(`fixtures/replay/stable-life-{win,loss}.*.json`) for both paths — folded together per -`plans/36`'s own original reasoning: the scenario *is* the test subject. Reaches this -programme's **S3**/**S4** milestones — with one criterion honestly short, below. - -**Validation is scoped to what `SimulationCampaign` actually carries** (`goals`, -`goalFailurePrecedence`) — not §14's full list across every content-definition type, most -of which this campaign shape has no field for yet. `kinds/simulation/validate.ts`'s own -header names exactly what's checked (goal id uniqueness, `LocKey` resolution) and defers -the rest to whichever future unit adds the collection each check needs. - -**Two committed fixtures, both captured by running the real engine once, not hand-typed:** -`stable-life-win` (three weeks of `rest`, completing the goal — two consecutive satisfied -weeks, not one, is the actual proof of `endOfWeek.ts`'s persistence tracking) and -`stable-life-loss` (four weeks of nothing, tripping `failureConditions`). Fixing these -fixtures also surfaced a real, previously-latent bug: `bulgaria-bureaucracy.replay.test.ts` -(W22) enumerated *every* `*.fixture.json` in the shared `fixtures/replay/` directory, -having never anticipated a second kind landing fixtures beside its own — it then tried to -replay `stable-life-*` through the story-graph-only registry and failed with -`campaign_withdrawn`. Fixed by prefix-filtering both suites (`bureaucracy-`/`stable-life-`) -and wiring `stable-life.replay.test.ts` into `ci.yml`'s release-tag-replay job alongside -bureaucracy's, with the same `skipIf`-style guard for a baseline tag that predates this -corpus entirely. - -**Honest scope gap: win/loss are not reachable through the text client or MCP.** The -original "Done when" below asked for that; `scene`/`availableActions`/`project` -(`kinds/simulation/kind.ts`) are placeholders, because §9 (Projection) is still prose-only -in the contract — no `SimulationView`/`PublicWorldState` shape exists to implement against. -This unit's actual consumer, the replay oracle, never calls those three methods (only -`createGame`/`submitAction`), so it doesn't need them — but a text-client/MCP playthrough -does, and that's a real, separate future unit (story-graph's own equivalent, W16/W17, came -*after* its turn loop already worked, not bundled into W14/W15). Recorded here rather than -quietly dropped from the criteria. -- **Spec:** [10 §14](10-simulation-kind.md#14-validation), [07 §4](07-replay.md#4-the-corpus). -- **Depends on:** [W39](#w39). -- **Status:** Done — [PR #102](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/102). -- **Done when:** Tier 1/2 validation rejects a duplicate goal id and an unresolved `LocKey` - for this kind's own content; one win fixture and one loss fixture are committed under - `fixtures/replay/` and pass the replay oracle, including the release-tag-replay CI job; - `npm run typecheck && npm run lint && npm test` all pass (677 tests, was 669). Win/loss - through the text client and MCP is **not** met — see the gap noted above. -- **Plan:** [`plans/36-simulation-kind-programme.md`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/blob/main/plans/36-simulation-kind-programme.md) - -### Depth: Life in the Fast Lane Becomes Playable - -Eight units taking the `simulation` kind from *proven through the replay oracle* to *played by -a person*. W36–W40 built the state, the week pipeline, the content types and a minimal -goal-driven loop; everything below is contract that exists and code that does not. - -**Ordered so the riskiest assumption goes first.** The platform's central claim is that a -client is a projection of the session store (09 §1) — and one of three kinds currently cannot -be projected at all, which W40 recorded honestly rather than dropping. W50 tests that claim. -W51 comes second because the derived-value layer is what every unit after it reads through, -and its rounding rule is the most replay-sensitive line in the kind. - -### [x] W50 — Simulation Kind: Projection and Client Parity {#w50} - -**Delivers:** Makes Life in the Fast Lane something a person can actually play. Today the -simulation kind runs only inside the replay harness — a game starts, a week is planned and -ended, and nothing can show you the result, because `scene`, `availableActions` and `project` -are placeholders returning empty values. This unit fixes what a client is allowed to see and -builds it, so the "Stable Life" scenario can be won and lost through the text client and -through MCP. - -W40 named this gap in its own done-criteria rather than quietly meeting a weaker bar. -[10 §9](10-simulation-kind.md#9-projection) is prose-only: it states the rule — `SimulationView` -carries **only what the generic surface does not**, the rule -[03 §9](03-story-graph-kind.md#9-projection--what-a-client-sees) already follows — but declares -no shape. `PublicWorldState`, which -[§7.10](10-simulation-kind.md#710-agents--engine-owned-strategy-definition-and-runtime-state)'s -`AgentStrategy.selectActions` takes as its first parameter, is referenced in the contract and -declared nowhere at all. - -**This unit amends §9 in the same change that implements it**, the precedent W48 set when it -amended [09 §4](09-clients.md#4-the-api-coverage-checklist) and -`MVP.md` §5 alongside `previewAction`. That is deliberate and narrow: §9's *rule* is settled, -and what is missing is the field list it implies — not a design decision. If drafting the shape -turns out to require one, that is `/contract`'s call, not this unit's. - -**The envelope-duplication ledger is the live risk, and it has bitten the view side before** — -entry 3, `StoryGraphView` duplicating scene and status fields. `SimulationView` has ten -`SimulationKindState` fields to choose from, next to a `GameState` envelope and a generic -`Scene`/`PlayerView` that already carry identity, status and turn. -- **Spec:** [10 §9](10-simulation-kind.md#9-projection), - [§2](10-simulation-kind.md#2-kindstate--what-belongs-here), - [§4](10-simulation-kind.md#4-actions--one-model-richer-verbs), - [§10](10-simulation-kind.md#10-reason-codes), [§11](10-simulation-kind.md#11-events); - [03 §9](03-story-graph-kind.md#9-projection--what-a-client-sees) (the rule to follow); - [04 §6](04-core.md#6-scenes-and-actions-generic), [§9](04-core.md#9-projection); - [09 §4](09-clients.md#4-the-api-coverage-checklist), - [§6](09-clients.md#6-projection-is-not-optional). -- **Touches:** `design/20-contract.md` (the `engine/10-simulation-kind.md` block, §9 and §11); - `src/engine/src/kinds/simulation/` — new `view.ts`, `scene.ts`, `available.ts`, and - `kind.ts`'s three placeholders; `src/engine/src/clients/text/render.ts`; - `src/engine/src/mcp/server.ts`; `design/10-design.md` (09 §4's checklist). -- **Depends on:** [W40](#x-w40--simulation-kind-the-stable-life-scenario-validation-and-corpus). -- **Status:** Not started. -- **Done when:** - - W50.1 Contract §9 declares `SimulationView` and `PublicWorldState` as complete TypeScript - interfaces with every field named, and `AgentStrategy.selectActions` (§7.10) resolves - against the declared `PublicWorldState` rather than an undeclared name. - - W50.2 No `SimulationView` field repeats one the `GameState` envelope, the generic `Scene`, - or `PlayerView` already carries — asserted by a test that names the envelope fields and - checks each is absent, not by review. - - W50.3 `project` never emits `seed`, `actionLog`, raw `kindState`, `AgentState.strategy`, - `RelationshipState.resentment`, or an unrevealed `Opportunity`, for either audience; - each is asserted by name, and the `ai` audience is not wider than `player`. - - W50.4 `availableActions` returns `plan.add`, `plan.remove`, `plan.clear` and `end_week` - with the params §4 declares for each; where the campaign forbids an empty plan, - `end_week` renders disabled with `plan_empty` rather than being hidden. - - W50.5 `scene` renders from registry strings only; a `LocKey` it references but the registry - does not resolve fails registry construction rather than rendering a raw key at play. - - W50.6 Playing `stable-life` to its committed win through the text client and through MCP, - under the same seed and the same counting `IdSource`, produces **byte-identical** - `serialize()` output — 09 §1's proof, now exercised for the first kind whose actions - carry declared `params`. - - W50.7 [09 §4](09-clients.md#4-the-api-coverage-checklist)'s coverage checklist has a - simulation column complete for all ten operations, each ticked against a named passing - test rather than an assertion of intent. - - W50.8 All eight events [§11](10-simulation-kind.md#11-events) declares are emitted at the - points that section names and listed in `Kind.eventNames`; a golden event stream for a - full `stable-life` win covers their order, and a name outside `kind.simulation.*` fails. -- **Out of scope:** wiring any additional resolver or end-of-week system — the projection shows - what the kind already computes, and W51–W57 add behaviour behind it. Also out of scope: - a rival agent actually running. `PublicWorldState` is declared here because - `AgentStrategy` cannot typecheck without it, not because this unit builds agents — how a - scenario configures rivals is an open gap §7.10 states outright. -- **Status:** Done — [PR #166](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/166). - -### [x] W51 — Simulation Kind: Derived Values, Modifiers, and Effects {#w51} - -**Delivers:** Makes a status effect or a trait actually change what the player can do — a -"reduced hours" effect really lowers the time available that week, a bonus really shifts a -cost — instead of every number in the game being the raw stored one. - -[§6.1](10-simulation-kind.md#61-base-and-derived-values)'s base/derived layer is the substrate -every resolver and every end-of-week system in W52–W57 reads through, and none of it exists: -`DerivedPath`, `DerivedValueResolver`, application order, stacking and expiry are absent from -`src/engine/src/kinds/simulation/` entirely. -[§7.1](10-simulation-kind.md#71-modifiers-and-rewards)'s `Modifier`/`Reward` application is -likewise unbuilt — including the `multiply` rounding rule W34 checked against the primary -source and corrected two plan documents over (basis-points `value`, round half-away-from-zero -**once** after the full chain), which is the single most replay-sensitive line in this kind. - -A substrate on its own is not a vertical slice, so this one is paired with the place it is -already observable without any new content: start-of-week `time_commit` -([§3](10-simulation-kind.md#3-the-turn-is-a-week)), whose two-phase split exists precisely so -an expiring effect changes committed time correctly. That makes the unit provable end to end -rather than a layer added on faith. -- **Spec:** [10 §6.1](10-simulation-kind.md#61-base-and-derived-values), - [§7.1](10-simulation-kind.md#71-modifiers-and-rewards), - [§2.3](10-simulation-kind.md#23-effects-opportunities-and-scheduled-events), - [§3](10-simulation-kind.md#3-the-turn-is-a-week), - [§13](10-simulation-kind.md#13-determinism). -- **Touches:** `src/engine/src/kinds/simulation/` — new `derived.ts` and `modifiers.ts`, plus - `startOfWeek.ts`, `endOfWeek.ts`, `state.ts`; `src/engine/fixtures/replay/`. -- **Depends on:** [W50](#w50). -- **Status:** Not started. -- **Done when:** - - W51.1 Every `DerivedPath` §6.1 names resolves through `DerivedValueResolver`; a path that - is not a declared derived path fails at load with its path, not at read. - - W51.2 Modifier application follows §6.1's stated order and stacking rule: two modifiers - touching the same path produce the identical result in either registration order, - proven by a test that applies them both ways round. - - W51.3 `Modifier.operation: "multiply"` against integer `Cents` rounds half-away-from-zero - exactly once after the full chain — a test shows a three-multiply chain differing from - three separately-rounded multiplies, so the rule is proven rather than assumed. - - W51.4 An `activeEffect` reducing committed time changes the budget in `time_commit` and - not in `time_advance`; a fixture where that effect expires the same week shows the - un-reduced budget, proving the two phases have not been collapsed. - - W51.5 A derived value is visible through `SimulationView` (W50) and never persisted — - `serialize()` output contains base values only, asserted over the canonical string. - - W51.6 A replay fixture covering an effect applying and expiring is committed and passes - the oracle; the determinism harness's sink-independence and byte-identity checks pass. -- **Out of scope:** the content that grants effects. Jobs, courses and items arrive with their - own units; this one proves the mechanism against a hand-authored effect on the existing - "Stable Life" fixture, the same way W39 proved goals against a single synthetic goal. -- **Status:** Done — [PR #204](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/204), - superseding [PR #194](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/194). - -### [x] W52 — Simulation Kind: The Scenario Campaign and Full Validation {#w52} - -**Delivers:** Replaces the hand-assembled test fixture with a real authored scenario — a -starting background, home, job, location, inventory and week cap that a content author writes -— and makes the loader reject a broken one with a path to the mistake instead of failing at -play. - -`SimulationCampaign` carries five starting-state blobs plus `goals` and -`goalFailurePrecedence`, and its own header says it is "deliberately minimal, not the real -authoring surface." Every [§7](10-simulation-kind.md#7-content-definition-types) content -collection exists as a type and has nowhere to live. There is no source/runtime split for this -kind at all — `story-graph` and `world-graph` each have one — so -[04 §10.1](04-core.md#101-content-registry)'s authoring boundary is asserted for two kinds and -proven for two of three. And [§14](10-simulation-kind.md#14-validation)'s Tier 1 list names -seventeen content types whose ids must be unique and whose cross-references must resolve; -what is implemented is goal-id uniqueness and one `LocKey`. -- **Spec:** [10 §7.8](10-simulation-kind.md#78-goals-scenarios-and-difficulty), - [§7.9](10-simulation-kind.md#79-supporting-definitions), - [§14](10-simulation-kind.md#14-validation); - [04 §10.1](04-core.md#101-content-registry), [§11](04-core.md#11-tiered-validation), - [§17](04-core.md#17-identifier-conventions). -- **Touches:** `src/engine/src/kinds/simulation/` — `campaign.ts`, `validate.ts`, `initial.ts`, - and a new `source.ts`; `src/engine/src/campaigns/stable-life.ts` and new broken fixtures - beside it. -- **Depends on:** [W50](#w50). -- **Status:** Not started. -- **Done when:** - - W52.1 `SimulationCampaignSource` → `SimulationCampaign` is a pure builder performing no - file or network I/O, and every §7 collection §14 validates has a field on the runtime - root. - - W52.2 `initialState` builds week one from a `ScenarioDefinition` — starting backgrounds, - housing, location, inventory and week cap — rather than from five literal state blobs. - - W52.3 Each of §14's Tier 1 checks fails with a path: a duplicate id within every one of - the seventeen content types independently, and each named cross-reference - (`PromotionPath.toJobId`, `ScenarioDefinition.startingBackgroundIds`, - `startingHousingId`, `startingLocationId`, `goalIds`, - `startingInventory[].definitionId`) pointing at an id that does not exist. - - W52.4 Each of §14's Tier 2 checks warns and still loads, reported rather than swallowed. - - W52.5 A deliberately broken campaign fixture exists per Tier 1 family — the same - instrument W15 built for `story-graph` — and each produces its expected tier and path. - - W52.6 The re-authored "Stable Life" campaign reaches its committed win and loss fixtures - byte-identically; if a fixture legitimately changes, it is regenerated by the - deliberate per-fixture command ([07 §7](07-replay.md#7-intended-change-versus-regression)) - and named in the PR — never an automatic sweep. -- **Out of scope:** making any newly authorable content *do* anything. A `JobDefinition` this - unit lets an author write and the validator accept is still resolved by `stubResolver` - until W53. This is the authoring and loading surface, not behaviour. -- **Status:** Done — [PR #227](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/227). - -### [x] W53 — Simulation Kind: Employment and Income {#w53} - -**Delivers:** The player can look for work, apply for a job, negotiate its terms and then -actually work it — and be paid for it at the end of the week. -- **Spec:** [10 §7.2](10-simulation-kind.md#72-jobs), - [§7.9](10-simulation-kind.md#79-supporting-definitions) (employers), - [§6.8](10-simulation-kind.md#68-career), [§3](10-simulation-kind.md#3-the-turn-is-a-week) - (`employment`, `finance_income`), [§5.1](10-simulation-kind.md#51-resolver-dispatch)–[§5.3](10-simulation-kind.md#53-per-action-outcome), - [§10](10-simulation-kind.md#10-reason-codes). -- **Touches:** `src/engine/src/kinds/simulation/` — `resolvers.ts`, `endOfWeek.ts`, - `reasons.ts`; `src/engine/src/campaigns/stable-life.ts`; - `src/engine/fixtures/replay/`. -- **Depends on:** [W51](#w51), [W52](#w52). -- **Status:** Not started. -- **Done when:** - - W53.1 `work`, `work_overtime`, `search_for_work`, `apply_for_job` and - `negotiate_job_terms` each have a real `ActionResolver` — `canExecute`, `calculate`, - `apply` — and none of the five is `stubResolver`. - - W53.2 Time and money cost are computed by `calculate` from state and content; a plan - exceeding the week's time units is rejected with `insufficient_time`, leaving state - byte-identical and the action log unadvanced. - - W53.3 `apply_for_job` against a job whose requirements are unmet is rejected with - `requirement_unmet`; an employment action whose type is not in the current location's - `actionTypes` is rejected with `wrong_location`. - - W53.4 The `employment` system advances `Employment.performance` and any `PromotionPath` it - satisfies; `finance_income` pays wages **before** `housing` runs, proven by a fixture - whose rent is payable only out of that same week's wages. - - W53.5 Every random draw comes from `ctx.rng`; adding a draw to one resolver does not shift - another's results, proven by a substream test rather than asserted from §13. - - W53.6 A replay fixture covering a full search → apply → hire → first paycheque arc is - committed and passes the oracle. -- **Out of scope:** education, housing costs, debt and possessions — W54 to W56. A job's - training requirement may name a `CourseDefinition` id the validator resolves, but taking - the course is W54's work. -- **Status:** Done — [PR #228](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/228). - -### [x] W54 — Simulation Kind: Education and Skills {#w54} - -**Delivers:** The player can enrol on a course, attend it, study for it, and come out with a -skill that changes which jobs they can hold — or withdraw and lose the fees. -- **Spec:** [10 §7.3](10-simulation-kind.md#73-courses), - [§7.9](10-simulation-kind.md#79-supporting-definitions) (skills), - [§6.7](10-simulation-kind.md#67-education), - [§3](10-simulation-kind.md#3-the-turn-is-a-week) (`education`), - [§8.1](10-simulation-kind.md#81-requirements). -- **Touches:** `src/engine/src/kinds/simulation/` — `resolvers.ts`, `endOfWeek.ts`, - `startOfWeek.ts`; `src/engine/src/campaigns/stable-life.ts`; - `src/engine/fixtures/replay/`. -- **Depends on:** [W53](#w53). -- **Status:** Not started. -- **Done when:** - - W54.1 `enroll_course`, `attend_class`, `study` and `withdraw_course` each have a real - resolver, and none is `stubResolver`. - - W54.2 Enrolling adds a course commitment that start-of-week `time_commit` includes in - committed time, and withdrawing removes it the same week — exercised by a fixture in - which one course ends the week another begins. - - W54.3 The `education` system advances progress only for courses attended that week, and - awards the course's skill exactly once on completion however many weeks follow. - - W54.4 A skill awarded here satisfies a `JobDefinition` requirement W53 built, proven by a - fixture where `apply_for_job` is rejected before the course and accepted after it. - - W54.5 Enrolling without the fee is rejected with `insufficient_funds`; enrolling twice on - the same course is rejected and leaves the existing enrolment untouched. - - W54.6 A replay fixture covering enrol → attend → complete → qualify is committed and - passes the oracle. -- **Out of scope:** careers beyond what W53 built, and rival actors studying — agents are - unbuilt for the reason W50's *Out of scope* records. -- **Status:** Done — [PR #230](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/230). - -### [x] W55 — Simulation Kind: Housing, Debt, and Reconciliation {#w55} - -**Delivers:** Rent, bills, borrowing and eviction — the half of the week that happens whether -the player plans for it or not. - -This is where the contract's most carefully argued ordering rule lands. -[§3](10-simulation-kind.md#3-the-turn-is-a-week) splits finance into `finance_income` before -`housing` and `finance_reconcile` after it, and states exactly what each single-pass -alternative breaks: rent charged before wages arrive produces false overdrafts for a solvent -player, while reconciling before housing lags eviction escalation by a full week. W53 built -the first of the three; this unit builds the other two, and is the first that can prove the -split was necessary rather than merely argued. -- **Spec:** [10 §7.4](10-simulation-kind.md#74-housing), - [§6.4](10-simulation-kind.md#64-finances), [§6.9](10-simulation-kind.md#69-housing), - [§3](10-simulation-kind.md#3-the-turn-is-a-week) (`housing`, `finance_reconcile`), - [§10](10-simulation-kind.md#10-reason-codes). -- **Touches:** `src/engine/src/kinds/simulation/` — `resolvers.ts`, `endOfWeek.ts`; - `src/engine/src/campaigns/stable-life.ts`; `src/engine/fixtures/replay/`. -- **Depends on:** [W53](#w53). -- **Status:** Not started. -- **Done when:** - - W55.1 `move_housing`, `pay_bills`, `borrow_money`, `repay_debt`, `deposit_savings` and - `invest` each have a real resolver, and none is `stubResolver`. - - W55.2 `housing` levies `HousingDefinition.weeklyCostCents` after `finance_income` has paid - wages: a fixture whose only income arrives that week pays rent successfully, and the - same inputs with the two systems' order swapped overdraw — the ordering is proven by - outcome, not by reading the list. - - W55.3 `finance_reconcile` applies late fees and advances eviction only against balances - `housing` charged this week; a first missed rent advances eviction by exactly one step. - - W55.4 Every money value is integer `Cents`; no floating-point money value appears in - `serialize()` output, checked over the canonical string rather than by inspection. - - W55.5 `move_housing` to a home the player cannot afford is rejected with - `insufficient_funds` and leaves the current housing untouched. - - W55.6 Two replay fixtures are committed and pass: one reaching eviction, one avoiding it - by a single week's wages. -- **Out of scope:** items and their upkeep (W56), and any economy-wide drift in prices or - wages — `EconomyState` moves only where a system this unit builds moves it. -- **Status:** Done — [PR #232](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/232). - -### [x] W56 — Simulation Kind: Possessions, Places, and People {#w56} - -**Delivers:** Shopping, keeping what you own working, getting around town, and having a social -life — the actions that make a week feel like a life rather than a spreadsheet. -- **Spec:** [10 §7.5](10-simulation-kind.md#75-items), - [§7.7](10-simulation-kind.md#77-npcs--definition-and-runtime-state), - [§7.9](10-simulation-kind.md#79-supporting-definitions) (locations), - [§6.5](10-simulation-kind.md#65-needs), [§6.10](10-simulation-kind.md#610-inventory), - [§6.11](10-simulation-kind.md#611-relationships), - [§3](10-simulation-kind.md#3-the-turn-is-a-week) (`inventory`), - [§10](10-simulation-kind.md#10-reason-codes) (`wrong_location`). -- **Touches:** `src/engine/src/kinds/simulation/` — `resolvers.ts`, `endOfWeek.ts`; - `src/engine/src/campaigns/stable-life.ts`; `src/engine/fixtures/replay/`. -- **Depends on:** [W55](#w55). -- **Status:** Done — [PR #236](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/236). -- **Done when:** - - W56.1 `shop`, `maintain_item`, `repair_item`, `sell_item`, `travel`, `socialize` and - `exercise` each have a real resolver, and none is `stubResolver`. - - W56.2 Both halves of `wrong_location` are covered: `travel` to a location absent from the - current location's `connections` is rejected with it, and so is any action whose type - is absent from the current location's `actionTypes`. - - W56.3 The `inventory` system applies per-item condition decay, and an item at zero - condition stops contributing its modifiers rather than being removed from inventory. - - W56.4 `socialize` moves the named `RelationshipState` and is rejected when that NPC is not - present at the current location. The `relationships` end-of-week system stays an - explicit, documented stub — no weekly relationship rule exists in the contract to - implement (see *Known Open Items Carried In*). - - W56.5 `exercise` moves the needs §6.5 names, clamped to `0–100` the same way W39's - `eat`/`rest` are, emitting one `StateChange` per touched need. - - W56.6 A replay fixture covering buy → use → decay → repair → sell is committed and passes. -- **Out of scope:** events that fire *because* of a relationship or a possession — that is - W57's `events` system. Also out of scope: writing the missing weekly-relationship rule, - which is `/contract`'s, not a slice's. - -### [x] W57 — Simulation Kind: Events, Opportunities, Headlines, and Achievements {#w57} - -**Delivers:** The world starts acting on the player — random events arrive and demand a -response, opportunities appear and expire, the weekly headline reflects what actually -happened, and achievements unlock and persist across sessions. - -Closes the last of [§3](10-simulation-kind.md#3-the-turn-is-a-week)'s stubbed systems and -[§2.3](10-simulation-kind.md#23-effects-opportunities-and-scheduled-events)'s two lifecycles. -Ordering is load-bearing again and stated: `headline` runs after `events` so a week's headline -can reference the strangeness that week's own events moved; `achievements` runs second-to-last -so a condition can depend on a counter `goals`/`failure` just incremented; and §2.3's revoke -and expire both run before offer. - -**This unit is blocked on one contract decision and must not start without it.** -[§12](10-simulation-kind.md#12-terminal-identity)'s callout states that whether a week which -simultaneously exhausts `weekLimit` *and* resolves every goal reports `week_limit_reached` or -`goals_met` is genuinely unresolved — not undocumented here, but unresolved in the upstream -source — and §3 confirms `END_WEEK_SYSTEM_ORDER` has no slot for the check at all. -`week_limit_reached` is one of `outcome()`'s three non-null values and is currently -unreachable. Route that to `/contract` first; do not guess an ordering, and do not invent a -system position upstream never named. -- **Spec:** [10 §7.6](10-simulation-kind.md#76-events), - [§7.9](10-simulation-kind.md#79-supporting-definitions) (opportunities, achievements, - headlines), [§2.3](10-simulation-kind.md#23-effects-opportunities-and-scheduled-events), - [§3](10-simulation-kind.md#3-the-turn-is-a-week), - [§11](10-simulation-kind.md#11-events), [§12](10-simulation-kind.md#12-terminal-identity); - [04 §7.1](04-core.md#71-the-profile-store). -- **Touches:** `src/engine/src/kinds/simulation/` — `resolvers.ts`, `endOfWeek.ts`, - `startOfWeek.ts`, `outcome.ts`; `src/engine/src/campaigns/stable-life.ts`; - `src/engine/fixtures/replay/`. -- **Depends on:** [W56](#w56), **and** a contract decision on §12's `week_limit_reached` - precedence callout. -- **Status:** Done — [PR #265](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/265). -- **Done when:** - - W57.1 `respond_to_event`, `accept_opportunity` and `decline_opportunity` each have a real - resolver, and `"custom"` still reaches resolution nowhere — a `GameAction` typed - `"custom"` fails, with no route around the `ResolverTable`. - - W57.2 The `events` system fires scheduled and seeded random events in the order §2.3 - states, and a deferred `PendingEventResponse` is presented by the *next* week's - start-of-week `events` phase, never the same week it was deferred in. - - W57.3 The `opportunities` system runs revoke and expire before offer (§2.3); an - opportunity offered and expired within one week is never visible in `SimulationView`. - - W57.4 `headline` reads world strangeness after `events` has moved it, proven by a fixture - whose headline changes only because an event fired that week. - - W57.5 `achievements` evaluates `AchievementDefinition.condition` after `goals`/`failure`, - unlocks exactly once across repeated weeks, and upserts through `ProfileStore` so the - unlock survives a new session with the same `profileId`. - - W57.6 `week_limit_reached` is reachable and returned by `outcome()` under the precedence - the contract decision fixed, with its own committed replay fixture — the third terminal - path, previously unreachable by construction. - - W57.7 No end-of-week system in §3's list remains a stub except `relationships` and - `history`, each still documented at its own definition site with its reason. -- **Out of scope:** rival agents. - [§7.10](10-simulation-kind.md#710-agents--engine-owned-strategy-definition-and-runtime-state) - states plainly that how a scenario configures rivals is an open gap — no - `ScenarioDefinition` field names them, and upstream declares none either — so agents stay - unbuilt until that is decided. - -### Breadth: Content Packs, Experiments, and Locales - -Three units against contracts that are fully specified and entirely unbuilt. -[`11-content-packs.md`](11-content-packs.md) is the customization story -[`02-architecture.md`](02-architecture.md) §4a promised and 04 §10.1 cannot express; the locale -unit is the cheapest available test of a claim 04 §10.1 makes and nothing currently checks. - -### [x] W58 — Content Pack Resolution and Content Identity {#w58} - -**Delivers:** Lets a game be assembled from several content packs — a base campaign plus, say, -a culture pack that restyles its text — and makes a save record exactly which mix it was -played against, so replaying one player's game never silently runs it against another's -content. - -The fold is the easy half. **The load-bearing part is -[§6](11-content-packs.md#6-identity-and-why-determinism-needs-it)'s identity argument:** two -players on the same `campaignVersion` with different packs resolved are playing different -games, and the envelope had no way to say so. The resolution is that `resolvePacks` stamps -every campaign's `version` with a `ResolutionId` digest over the ordered `{id, version}` list — -which adds no envelope field and makes [07 §6](07-replay.md#6-the-runner-and-its-verdicts)'s -`unrunnable: campaign_version_missing` reachable for the reason it was written. - -`ContentRegistry` gains exactly one field. That is the whole change to 04 §10.1, and a second -one is a signal this unit has grown a design decision it should route rather than absorb. -- **Spec:** [11 §2](11-content-packs.md#2-what-a-pack-is), - [§3](11-content-packs.md#3-resolution), - [§4](11-content-packs.md#4-the-one-change-to-contentregistry), - [§5](11-content-packs.md#5-dependencies), - [§6](11-content-packs.md#6-identity-and-why-determinism-needs-it), - [§7](11-content-packs.md#7-validation); [04 §10.1](04-core.md#101-content-registry), - [§11](04-core.md#11-tiered-validation); [07 §6](07-replay.md#6-the-runner-and-its-verdicts). -- **Touches:** `src/engine/src/core/registry/` — `types.ts`, `build.ts`, and a new `packs.ts`; - `src/engine/src/core/validation/tiered.ts`. -- **Depends on:** nothing engine-side. -- **Status:** Done — [PR #238](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/238). -- **Done when:** - - W58.1 `resolvePacks` is pure and total — no file or network I/O, and it returns either a - registry or a complete list of conflicts, never a partial registry. - - W58.2 A later pack replaces a campaign wholesale by `campaign.id` and never field-merges - it; a later pack replaces a string per key. A two-pack campaign collision yields - exactly one of the two campaigns, asserted directly rather than by absence of a merge. - - W58.3 `dependsOn` is topologically sorted before the fold; a cycle fails, and two packs - requiring different versions of a third fails rather than picking one. - - W58.4 The same ordered `{id, version}` list produces the same `ResolutionId` across - processes, and the same list in a different order produces a different one. - - W58.5 Every campaign `resolvePacks` produces carries the `ResolutionId` as its - `Campaign.version`, and `GameState` gains no field. - - W58.6 Loading a replay fixture whose `campaignVersion` no longer resolves returns - `unrunnable` with `campaign_version_missing` — not `diverged`. - - W58.7 Each of §7's three checks fires: Tier 1 for a `kindId` mismatch, an unresolvable - `dependsOn` and a cycle; Tier 1 for a campaign id colliding *within* one pack; Tier 2 - for a pack overriding a campaign or string no earlier pack supplied. - - W58.8 Every existing single-campaign registry test passes unmodified, and the campaigns - W15–W40 committed still serialize byte-identically. -- **Out of scope:** experiment gates (W59); pack discovery and distribution, partial or lazy - loading, community trust, and per-locale pack splitting — - [§8](11-content-packs.md#8-what-is-deferred) defers all four by name. - -### [x] W59 — Experiment Gates and the `ExperimentSource` Port {#w59} - -**Delivers:** Turns A/B tests and feature flags into one mechanism rather than two — a pack is -simply in the set or not, decided before resolution — and gives a host the seam that decides -which variant a player is in. - -[§5a](11-content-packs.md#5a-experiment-gates)'s design is deliberately small: -`applyExperimentGates` filters the pack array *before* `resolvePacks` sees it, so nothing about -the fold's signature or purity changes and it never learns gates exist. `ExperimentSource` is -the last unbuilt port in [06 §5](06-extensibility.md#5-the-port-catalogue)'s catalogue — W1's -`src/engine/src/core/composition/types.ts` predates that design and declares `IdSource`, -`Clock`, `EngineHost` and a `SessionHost` with no `experiments` field. - -The safety property is worth restating because it is easy to lose in implementation: a gated -pack is included only when `assignments[gate.experimentId] === gate.variant`, which is never -true for `null` ("not enrolled") or a missing key — so "no `ExperimentSource` supplied" is safe -by construction rather than by luck of which default string someone picked. -- **Spec:** [11 §5a](11-content-packs.md#5a-experiment-gates); - [06 §4](06-extensibility.md#4-the-composition-root), - [§5.5](06-extensibility.md#experimentsource). -- **Touches:** `src/engine/src/core/composition/types.ts`; - `src/engine/src/core/registry/packs.ts`; `src/engine/src/core/session/store.ts`. -- **Depends on:** [W58](#w58). -- **Status:** Done — [PR #239](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/239). -- **Done when:** - - W59.1 `ExperimentSource` is declared in `composition/types.ts` and `SessionHost` gains an - optional `experiments`, with a working default meaning "no experiments running". - - W59.2 An ungated pack is always included; a gated pack is included only on an exact - variant match and excluded for `null`, for a missing key, and for a different variant - — each asserted as its own case. - - W59.3 With no `ExperimentSource` supplied, every gated pack is excluded and every ungated - pack included; no gated pack reaches a registry by default. - - W59.4 `resolvePacks`' signature, purity and W58 tests are unchanged, and a test asserts it - never receives a gated-out pack. - - W59.5 Assignments resolve once per session — one call per distinct `experimentId` across - the candidate packs, keyed by `profileId` where present and `seed` otherwise — proven - by a counting `ExperimentSource` rather than asserted. - - W59.6 Two sessions in different variants produce different `campaignVersion`s through - W58's existing digest, with no further mechanism added. -- **Out of scope:** bucketing algorithms, rollout percentages, sticky-session semantics beyond - `bucketKey`, and outcome measurement — §5a and §8 place all four outside this contract. - -### [x] W60 — A Second Locale, End to End {#w60} - -**Delivers:** Proves the platform is not accidentally English-only — the same campaign plays -through in a second language with the engine unchanged, and a missing translation is caught at -load rather than shown to a player as a raw key. - -[04 §10.1](04-core.md#101-content-registry) states the authoring→registry types already support -more than one locale and that the MVP ships English only, so this is string tables plus tooling -with no type change. **No test currently makes that claim**, and it is a small vertical slice -that would find out cheaply: the protected `core.reason.*` merge, `LocKey` resolution, the text -client's rendering and the MCP surface all sit on the path. -- **Spec:** [04 §10.1](04-core.md#101-content-registry), - [§12](04-core.md#12-reason-codes-state-changes-messages), - [§17](04-core.md#17-identifier-conventions); - [09 §5](09-clients.md#5-reason-codes-and-messages). -- **Touches:** `src/engine/src/core/localization/resolve.ts`; - `src/engine/src/core/registry/build.ts` and `strings.ts`; - `src/engine/src/campaigns/`; `src/engine/src/clients/text/render.ts`. -- **Depends on:** nothing engine-side. -- **Status:** Done — [PR #240](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/240). -- **Done when:** - - W60.1 One shipped campaign has a complete second-locale string table and the registry - builds for both locales with no change to any type in `core/registry/types.ts`. - - W60.2 A key present in English and absent in the second locale fails Tier 1 with the key's - path — never a silent fallback to English, never a raw key rendered at play. - - W60.3 `core.reason.*` messages resolve in the second locale, and a campaign attempting to - override one is still rejected. - - W60.4 The same seed and the same choices under either locale produce byte-identical - `serialize()` output — locale is presentation and reaches no persisted state. - - W60.5 The text client and the MCP surface both render the second locale from the registry, - and neither contains a translated string in its own source. -- **Out of scope:** string-table extraction, translation workflow and coverage reporting — - those belong to the *Content Tooling* workstream below. Per-locale content packs are - [11 §8](11-content-packs.md#8-what-is-deferred)'s deferral, not this unit's. - -### Breadth: The First Culture Pack - -Both preconditions have landed. [W58](#w58) built the pack fold and the `ResolutionId`; -[W50](#w50)–[W57](#w57) made the simulation kind playable, so there is now a game worth -reskinning. Sliced as **W71–W72**. - -**This is the riskiest unproven claim left in the design**, which is why it leads. -[`02-architecture.md`](02-architecture.md) §4a calls culture packs "the volume play: one kind, -many settings," and [§11](02-architecture.md#11-what-is-settled-what-is-next) says the Bulgaria -pack "needs no new deliverable — it is a content pack over the existing simulation kind." -Neither statement has -ever been exercised: W58 proved the fold against synthetic packs, and no real culture pack -exists. W71 is the first one, and its job is to find out whether "no engine change" is true. - -### [x] W71 — The Bulgaria Culture Pack: Mechanism and Voice {#w71} - -**Delivers:** Proves the platform's central customization claim — that one game's mechanics can -host a completely different world without touching the engine. Life in the Fast Lane's "Stable -Life" is assembled from two content packs instead of one: the base game, plus a Bulgarian -culture pack that swaps the narrator's voice and the world it describes. Two players who both -say they are playing "Stable Life" are recorded as having played different games when one of -them resolved the Bulgarian pack, which is the whole point of content identity. - -The pack replaces the campaign **wholesale by id** and its strings **per key** ([11 §3](11-content-packs.md#3-resolution)) — so the Bulgarian world arrives under the same `stable-life` id, and the `ResolutionId` is the only thing that distinguishes the two ([11 §6](11-content-packs.md#6-identity-and-why-determinism-needs-it)). Every surface this needs is already an exported contract surface (`resolvePacks`, `computeResolutionId`, `ContentPack`, `buildValidatedContentRegistry`, `TextClient`); the unit adds no signature. -- **Spec:** [`02-architecture.md`](02-architecture.md) §4a; [11 §2](11-content-packs.md#2-what-a-pack-is), - [§3](11-content-packs.md#3-resolution), [§6](11-content-packs.md#6-identity-and-why-determinism-needs-it); - [04 §10.1](04-core.md#101-content-registry); [10 §7](10-simulation-kind.md#7-content-definition-types). -- **Touches:** `src/engine/src/campaigns/` — a base-pack wrapper around the existing - `stable-life` source and a new Bulgarian culture pack, plus a test alongside. - `src/engine/src/core/registry/packs.ts` is **read, not modified**. -- **Depends on:** [W58](#w58), [W59](#w59), [W50](#w50)–[W57](#w57) — all done. -- **Status:** Done — [PR #314](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/314). -- **Done when:** - - W71.1 Resolving `[base]` and `[base, bulgaria]` both produce a registry that builds and - passes validation; the two differ in rendered text at every key the pack overrides and - at no other key, asserted key-by-key rather than by a whole-table comparison. - - W71.2 No file under `src/engine/src/core/` or `src/engine/src/kinds/` changes. §4a's "no - engine change" is the claim under test, so a diff touching either directory falsifies - it — **stop and report rather than absorbing the change**. - - W71.3 The two resolutions produce different `ResolutionId`s, and each resolved campaign - carries its own as `Campaign.version`, so a save records which mix it was played - against. - - W71.4 A game created against the Bulgarian resolution plays a full week through the text - client — `plan.add`, `end_week`, `view` — and renders the pack's text, never - `stable-life`'s. - - W71.5 A replay fixture captured under one resolution returns `unrunnable` with - `campaign_version_missing` against the other, not `diverged` — the reachability - [W58.6](#w58) built and nothing has yet exercised with real packs. - - W71.6 The same seed and the same actions under the Bulgarian resolution replay - byte-identically through `serialize()`. -- **Out of scope:** the full Bulgarian setting ([W72](#w72)); a Bulgarian *locale* string table - — [W60](#w60) owns translation, and this is voice-and-world within one locale, a - different axis; pack discovery, distribution, and loading from disk, which - [11 §8](11-content-packs.md#8-what-is-deferred) defers by name; a second culture pack. - -### [x] W72 — The Bulgaria Culture Pack: The Full Setting {#w72} - -**Delivers:** Fills the Bulgarian world out from a proof into something worth playing — its -jobs, places, events, housing, possessions and prices — so a player can take the Bulgarian -"Stable Life" all the way to a win and to a loss, rather than only to a correctly rendered -first week. - -Split from [W71](#w71) on size, not on principle: `stable-life`'s content is roughly a thousand -lines across five files, and a unit that authors a Bulgarian equivalent *and* proves the -mechanism would not finish in one session. W71 proves the claim; this one does the volume. -- **Spec:** [10 §7](10-simulation-kind.md#7-content-definition-types); [`02-architecture.md`](02-architecture.md) §4a; - [11 §7](11-content-packs.md#7-validation). -- **Touches:** `src/engine/src/campaigns/` — the pack's content files, mirroring the split - `stable-life-events.ts` / `-housing.ts` / `-possessions.ts` / `-effects.ts` already uses; - `fixtures/replay/`; `src/engine/scripts/demo-cli.ts`. -- **Depends on:** [W71](#w71). -- **Status:** Done — [PR #317](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/317). -- **Done when:** - - W72.1 The pack supplies Bulgarian content for every collection `stable-life` populates — - jobs, places, events, housing, possessions, effects — with none left silently - inheriting the base pack's content by omission. - - W72.2 Two committed replay fixtures under the Bulgarian resolution reach a terminal state, - one `goals_met` and one a failure — the same both-paths bar W40 set for `stable-life`. - - W72.3 Tier 1 and Tier 2 validation pass on the Bulgarian resolution, producing no warning - class `stable-life` does not also produce. - - W72.4 Every player-facing string the pack adds is a `LocKey` with an entry in the pack's - string table; no literal player-facing text reaches a client from the pack's source. - - W72.5 `npm run demo` lists and plays the Bulgarian resolution alongside the base one. -- **Out of scope:** balancing the Bulgarian numbers — this pack inherits the simulation kind's - provisional-numbers debt ([issue #267](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/issues/267)) - rather than resolving it; a Bulgarian locale translation; Jones-in-Corporate or any - second culture pack, which is volume this unit does not need to prove. - -### Breadth: The Platform - -- [x] **Interactive CLI play-test harness — built.** `src/engine/scripts/demo-cli.ts`, run with - `npm run demo` from `src/engine/`: a stdin/stdout loop over the existing `TextClient` and - `SessionStore` that lists the committed campaigns, takes a campaign id and an optional - seed, and accepts ` [key=value ...]`, `view`, `save`, `load ` and `quit`. - Verified playing `stable-life` — the simulation kind — end to end, so it covers both - shipped kinds and not just the story-graph ones. It lives outside `src/`, so neither the - determinism guard nor the client-contract import rule applies to it, and it is covered by - `npm run lint` and `npm run typecheck` (`eslint src scripts`, `tsconfig.scripts.json`). - Manual play-testing tooling, not a shipped client surface — distinct from "more clients" - below; no projection, store, or contract change. - -### [x] W61 — Public Playable Web Demo {#w61} - -**Delivers:** Turns the completed Bureaucracy MVP into a public `/play/` route a visitor can -finish without cloning the repository. The engine runs locally in the browser behind a real -client over `SessionStore`; the page renders scenes, shown choices, disabled reasons, visible -state and achievements, offers non-committing previews and same-page checkpoints, and reaches -the existing ending with no React-owned game rule. - -This is deliberately one campaign and one route. The five story campaigns, Stable Life, and -the world-graph MVP already prove useful engine breadth, but putting all of them in a picker -would turn the first browser boundary into three rendering problems and still leave the -load-bearing question unanswered: can the package, session store, save envelope, registry and -client contract run together in a production browser bundle? [`13-playable-web-demo.md`](13-playable-web-demo.md) -fixes that product and architecture boundary. - -The browser currently exposes three real portability defects hidden by the Node.js CLI: -`version.ts` reads `node:fs`, `envelope.ts` uses `node:crypto`, and `emitter.ts` reads an -unguarded `process.env`. Close those in the shared runtime, not with a reduced browser fork. -The checksum stays SHA-256 over the same canonical bytes, and the pure engine remains -synchronous; only the already-async store boundary may await platform crypto. - -- **Spec:** [`13-playable-web-demo.md`](13-playable-web-demo.md); - [09 §1](09-clients.md#1-the-rule-made-testable), - [§2](09-clients.md#2-the-only-surface), - [§4](09-clients.md#4-the-api-coverage-checklist), - [§6](09-clients.md#6-projection-is-not-optional); - [04 §7](04-core.md#7-the-session-store-and-the-platform-api), - [§9](04-core.md#9-projection), [§10.2](04-core.md#102-save-envelope-and-migration). -- **Touches:** `src/engine/src/version.ts`, `core/persistence/envelope.ts`, - `core/observability/emitter.ts`, the package root and browser-bundle smoke test; - `site/` — a new play entry, composition root, browser adapter, React page, shared - navigation, styles and tests; the static-build/merge verification; factual status copy - in `README.md`, `src/engine/README.md`, and the landing page. -- **Depends on:** [W19](#x-w19--mvp-acceptance), [W31](#x-w31--save-migration), and W41. - Implement against whichever - landing build/merge mechanism is on `main` when the unit starts; issue #179's package - migration is not a semantic dependency and must not be reimplemented here. -- **Status:** Done — implemented via #183 and #184. -- **Done when:** - - W61.1 The supported engine entry graph used by the site produces a real browser bundle - with no `node:` import, unguarded Node.js global, runtime filesystem read, or second - browser-only engine path; Node.js typecheck, lint, tests and package build remain green. - - W61.2 `ENGINE_VERSION` still has package metadata as its single owner, and save/load under - Node.js and a browser produce the same lowercase SHA-256 checksum for the same - `{ state, replayCompatible }` canonical bytes without changing any committed replay or - serialization fixture. - - W61.3 The package root exports the committed Bureaucracy builder needed by the site - composition root; React and the browser adapter import only `SessionStore` types and - call no engine, kind, registry, validation, projection, or persistence helper. - - W61.4 A direct static request to `/play/` succeeds; the production artifact contains - `/`, `/roadmap/`, `/play/`, and `/docs/`, and the protected merge proves the docs - subtree byte-identical before and after overlay. - - W61.5 A visitor can start Bureaucracy, traverse the `office_visits >= 3` loop, see the - gated choice with its reason, exercise the seeded transition, reach the existing - ending, see `it_builds_character`, and start again. No raw `LocKey` appears in any - ready, playing, rejected, preview, or ended state. - - W61.6 Previewing an enabled choice shows the labelled prospective scene without changing - the committed scene, view, action sequence or checkpoint; committing it afterwards - reaches the same result as choosing it without a preview. - - W61.7 Save/load is presented honestly as a same-page checkpoint. Restoring it loses no - state; refreshing starts a new demo and the UI says so. No component writes raw state - or a save envelope to browser storage. - - W61.8 [09 §4](09-clients.md#4-the-api-coverage-checklist)'s browser column is checked - against ten named adapter tests. The full Bureaucracy path through the browser adapter - and text client, under the same seed and counting `IdSource`, produces identical - `Scene`/`PlayerView` steps and byte-identical final `serialize()` output. - - W61.9 The page is keyboard-complete and usable at 320 px, 390 px, 768 px and 1280 px: - native action controls, adjacent disabled reasons, visible focus, announced committed - scene changes, no colour-only state, no horizontal overflow, and complete reduced-motion - behavior. - - W61.10 The public header and landing page expose `Play`; stale claims that nothing is - playable are corrected without calling the demo a finished game. Site checks, - documentation checks, engine gates, `git diff --check`, and the exact-merge deployment - verification all pass before the route is announced. -- **Out of scope:** additional campaigns or kinds; durable browser storage; profiles across - reloads; accounts, cloud sync or any backend; new gameplay; art, audio, analytics, - session capture, service workers, a PWA, or a generic reusable web-client package. - -### [x] W62 — Platform Static Host Image {#w62} - -**Delivers:** Adds a product-owned ASP.NET Core host under `src/host/`, composed with -`SubZeroDev.Platform.Hosting`, and packages W61's verified combined static artifact into a -stateless container. Pull requests build, run, and smoke the image. Merges to `main` publish a -new immutable GHCR image when relevant hosting inputs change, but do not deploy it; GitHub Pages -remains the public host. - -This is the first Platform consumer and deliberately the smaller half of hosting. The engine -continues to execute in the browser. A later hosted-engine-edge slice owns the `.NET Platform -edge → Node engine workload`, JSON/HTTP boundary, MCP projection, and remote session semantics. - -- **Spec:** [`15-platform-static-host.md`](15-platform-static-host.md); - [`13-playable-web-demo.md`](13-playable-web-demo.md) §6; - the Platform repository's `platform-identity.md`, `engine-hosting-contract.md`, ADR-002, - ADR-005, and D3 implementation plan. -- **Touches:** new `src/host/` web project and tests; the multi-stage container definition and - build context; static artifact/route smoke scripts; CI and GHCR publication workflows; - package-source configuration without credentials; hosting documentation. -- **Depends on:** [W61](#w61) and SubZeroDev.Platform S9 package publication. A temporary sibling - `ProjectReference` may unblock local development, but W62 cannot merge until the project - uses one exact released `SubZeroDev.Platform.Hosting` package version and a clean CI clone - restores without `../SubZeroDev.Platform`. -- **Status:** Done — [PR #264](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/264). -- **Done when:** - - W62.1 `src/host/` is the product composition root. It calls `AddPlatformWebHost()` and maps - Platform probes; Platform gains no GameEngine dependency, and the host adds no worker, - persistence, migration, outbox, account, or session service. - - W62.2 The committed project pins an exact released `SubZeroDev.Platform.Hosting` NuGet - version. CI restores it with a short-lived secret that is absent from repository files, - Docker arguments, environment layers, runtime image history, and build output. - - W62.3 One multi-stage build constructs the site and documentation from the same commit, runs - the protected merge, proves the docs subtree byte-identical, publishes the host, and - copies only the verified combined artifact into `wwwroot` in the runtime stage. - - W62.4 Direct container requests to `/`, `/roadmap/`, `/play/`, and `/docs/` return the - expected documents; Platform liveness and readiness succeed; a named unknown route - returns `404` with no SPA fallback. - - W62.5 The browser demo remains W61's local `SessionStore` client: the container exposes no - engine API, game action, or runtime content endpoint, and a production browser smoke - observes no such network request. - - W62.6 The runtime image contains no Node.js, package-manager cache, source tree, build tools, - or registry credential; it runs non-root, supports a read-only root filesystem, writes no - product data, performs no normal outbound request, and stops gracefully on `SIGTERM`. - - W62.7 PR CI builds and starts the exact image, runs positive route/probe/browser checks, and - contains a deliberate missing-or-corrupt-artifact case that proves the gate fails red. - - W62.8 A path-filtered `main` workflow publishes a new GHCR image only when host, site, - documentation-build, merge, container, or locked dependency inputs change. It records an - immutable full-commit tag and digest, creates no `latest` tag, and performs no deployment. - - W62.9 The existing GitHub Pages exact-merge workflow and public routes remain unchanged and - green; an image publication failure cannot alter the live site or an earlier image. -- **Out of scope:** public deployment, DNS/TLS/custom domain, traffic cutover or rollback; - hosted Node engine/API/MCP/session behavior (a later hosted-engine-edge slice); persistence, - auth, accounts, databases, worker processes; gameplay, campaign, browser-client, or - serialization changes; a generic static-site facility in Platform. - -### [x] W63 — Absurd Game Interface {#w63} - -**Delivers:** Rebuilds the public story shelf and story-graph play surface so it reads as a -game rather than a styled form. The interface becomes an original absurd adventure cabinet: -a theatrical scene viewport, tactile action deck, satirical status console, dossier-like story -shelf, and brief mechanical transitions. Its inspiration is the graphic-adventure staging of -*Indiana Jones and the Fate of Atlantis* and the busy life-board energy of *Jones in the Fast -Lane*; it copies neither game's assets, layout, characters, logos, fonts, sounds, or trade dress. - -The slice is presentation-only. Existing `BrowserClient` DTOs and `SessionStore` remain the -only game-facing boundary, and the redesign may not add rules, rewrite campaign text, infer -hidden state, reorder actions, or change serialized outcomes. “Absurd” is a controlled visual -language, not maximum noise: one hero joke and at most two minor jokes per visible state. - -- **Spec:** [`14-game-interface.md`](14-game-interface.md); - [`13-playable-web-demo.md`](13-playable-web-demo.md) §§1–3, §7–§9; - [09 §1](09-clients.md#1-the-rule-made-testable), - [§2](09-clients.md#2-the-only-surface), - [§6](09-clients.md#6-projection-is-not-optional). -- **Touches:** `site/src/play/` — shelf, game cabinet, action deck, status console, transitions, - responsive states, original local assets, component/browser/visual/accessibility tests; - static-build verification for asset budgets and the direct `/play/` route. No engine, - campaign, replay-fixture, or contract-type change. -- **Depends on:** [W61](#w61) and the multi-campaign story shelf already present on `main`. -- **Status:** Done — implemented via #188 and #190. W63.7 (visual snapshots) and W63.8 - (automated accessibility, forced-colours, 200% zoom, and long-text checks) were verified - by manual review at 320/390/768/1280 px rather than a dedicated automated suite; see - [OPEN-QUESTIONS.md §3](OPEN-QUESTIONS.md#3-judgement-calls-to-revisit-settled-for-the-mvp). -- **Done when:** - - W63.1 Ready, playing, busy, unavailable, rejected, persistence-warning, and ended states - all use the cabinet visual grammar and remain distinguishable without colour or motion. - - W63.2 The story shelf is a keyboard-navigable dossier/archive composition; selecting a - story opens a labelled briefing, content notices remain plain and accessible, and - returning from play restores the selected dossier and shelf position. - - W63.3 The scene viewport renders authored text unchanged; the action deck preserves action - order and full labels; the status console renders only `PlayerView`. No raw node id, - `LocKey`, seed, action log, hidden variable, or opaque kind state appears. - - W63.4 Every visible-stat control prints its value in addition to any gauge treatment. A - campaign with no visible stats receives an honest empty-state prop, not a fabricated - score or progress measure. - - W63.5 All art is original local PNG/JPG or CSS-native decoration. A missing decorative - asset leaves a complete readable cabinet; no reference-game asset, font, logo, sound, - character, screenshot, traced composition, or trade dress ships. - - W63.6 One full Bureaucracy run and both Lucifer roles are playable through the redesigned - UI. Existing browser/text-client parity still produces byte-identical serialized - outcomes, proving presentation did not become game logic. - - W63.7 Visual snapshots cover ready, playing, unavailable-choice, persistence-warning, and - ended states at 320 px, 390 px, 768 px, and 1280 px, with no clipped authored text, - horizontal overflow, or action below an inaccessible internal scroll region. - - W63.8 Keyboard-only, automated accessibility, forced-colours, 200% zoom, long-text, and - missing-asset checks pass. Focus moves after committed scenes, restores after a - briefing/dialog closes, and never moves merely because decoration animates. - - W63.9 Motion is limited to brief state punctuation; reduced motion removes transforms, - parallax, wipes, flicker, and staged delay. No action waits for animation and no - permanent timer runs while the page is idle. - - W63.10 Initial decorative payload is at most 1.5 MB compressed, no single decorative asset - exceeds 500 KB, phone layouts do not fetch desktop backdrops, `/play/` remains a direct - static route, and the page performs no runtime request for engine or campaign content. -- **Out of scope:** new campaigns, mechanics, projections, engine APIs, a visual language for - simulation/world-graph, copied reference material, canvas/WebGL, point-and-click movement, - a verb parser, mandatory audio, voice acting, cut-scenes, or procedural art. - -### Depth: Story Campaigns Become Adventures - -### [x] W64 — Replayable Story Campaign Expansion {#w64} - -**Delivers:** Reauthors the six story-graph campaigns already on the public shelf as -replayable narrative adventures and presents them as a playable choose-your-own-adventure -casebook. Return, Bureaucracy, Driving, Inheritance, Enterprise, and Lucifer Chronicles gain -routes that stay apart long enough to feel different, optional scenes, delayed consequences, -hidden discoveries, seeded flavour events, stat-dependent choices, and multiple endings. A -first playthrough must leave authored content undiscovered, while the interface makes the -causal link between the choice just made and the scene that followed unmistakable. - -This is a content slice over the existing `story-graph` contract, not a new narrative runtime. -Money, documents, relationships, attention, and character memory are typed campaign variables; -secrets use `showWhen`; visible obstacles use `requirements`; delayed consequences are earlier -effects read by later conditions; and random events are seeded `random` nodes. A random draw may -change flavour or open an optional detour, but it may not make a planned route unwinnable. - -For this unit, a **visible scene** is a reachable `choice` or `ending` node. `auto` and `random` -nodes settle before projection and therefore do not count toward a campaign's scene target. A -**material route** reaches a different ending or traverses at least two consecutive visible -scenes that another route skips before the routes rejoin. A **delayed consequence** is a choice -effect consulted by a `showWhen`, `requirements`, achievement, or later route at least three -player submissions after it was applied. These definitions keep node inflation and immediate -reconvergence from masquerading as depth. - -The expansion keeps each campaign standalone. Recurring clerks, mechanics, police, relatives, -coffee, flies, and locations make the catalog feel like one universe, but no campaign reads -another campaign's save, profile achievements, or variables. Existing campaign, node, ending, -choice, and achievement ids are published identifiers: retain them where the corresponding -content remains, and cover every necessary rename through campaign migration rather than -silently stranding a v1 save. - -The W63 cabinet remains the outer game shell; its scene viewport becomes an open **adventure -casebook** rather than another disconnected slide. The current scene is the current page, the -action deck is a numbered set of “turn to your choice” passages, visible stats read as a compact -character sheet, achievements arrive as stamps/bookmarks, and an ending closes the volume with -its authored title. CSS-native paper, ink, marginalia, tabs, page edges, and brief page-turn -punctuation may strengthen the game metaphor without copying a published book or hiding the -text inside a decorative texture. - -Every committed transition produces a visible **arrival receipt**: “You chose …” followed by -“which brought you here.” A persistent, read-only **Journey so far** display lists only pages -and choices this player has already seen, links the previous entry to the current page, and -offers “Where I came from” without exposing node ids, hidden choices, conditions, seed, or the -engine action log. The journal is presentation memory assembled from successful projected -scenes and resolved action labels. Previewed and rejected actions never enter it; browsing an -old entry never rewinds or resubmits the game. - -- **Campaign shape:** - - **Return:** at least 20 visible scenes across Returning Home, Reality, and Settling; the - airport/customs arrival, first bureaucracy, neighbours and old connections, and the - rent/village/apartment/family/hotel decision form different routes. At least optimistic, - sceptical, and exhausted endings are reachable. - - **Bureaucracy:** 20–30 visible scenes spanning municipality, cadastral, tax, civil-registry, - archives, notary, and translation-office routes. Helpful/angry clerk and supervisor - memory pays off later; document obtained, gave up, lawyer solved, miracle, and system - failure endings are all reachable. - - **Driving:** at least 25 visible scenes covering inspection, mechanic trust, insurance and - tax, fuel/LPG, road and weather trouble, police, parking, parts, marketplace, towing, and - the insurer. Reliable car, endless repairs, sold car, collector item, and abandoned - project endings are all reachable. - - **Inheritance:** at least 25 visible scenes across news, documents, village, neighbours, - family, police, lawyers, court, settlement, and aftermath. Evidence, support, cost, - tension, and property condition expose old-document, helpful-neighbour, lost-deed, - police-report, and secret-agreement paths. Court, settlement, abandonment, buyout, - family peace, and family war endings are all reachable. - - **Enterprise:** at least 30 visible scenes across registration, first client, tax and - invoicing, cashflow, hiring, competition, government, growth, and failure. Seeded late - payment, audit, tax letter, lucky client, bad review, server outage, and opportunity - events alter pressure without erasing deliberate preparation. Consultant, agency, - successful company, platform company, bankruptcy, and sale endings are all reachable. - - **Lucifer Chronicles:** Ben and Lucifer remain genuinely different perspectives, not a - shared route with renamed prose. Every act decision scene offers four to six authored - choices, earlier interactions alter later availability or dialogue, and every recurring - character with more than one appearance remembers at least one prior interaction. - Generic numbered conclusions are replaced by authored philosophical endings, including - The Bureaucrat, The Observer, The Escapist, The Builder, The Stoic, The Entertainer, - Customer Support Manager of Hell, The One Who Asked One Question, and The Guy Who Just - Wanted to Fix a House. -- **Spec:** [03 §§2–7](03-story-graph-kind.md) for variables, nodes, gates, effects, - conditions and achievements; [§8.2](03-story-graph-kind.md) for settle semantics; - [§10](03-story-graph-kind.md) for determinism, save and versioning; - [04 §10.2](04-core.md#102-save-envelope-and-migration) for campaign migration; - [09 §1](09-clients.md#1-the-rule-made-testable) for client parity. -- **Touches:** the six `src/engine/src/campaigns/` sources and their focused tests, - determinism snapshots and replay fixtures; campaign exports only if composition needs - them; `/play/` catalog metadata, duration/content notices, casebook/journey presentation, - styles, and browser tests. No core, kind, projection, session, or client contract changes. -- **Depends on:** [W31](#x-w31--save-migration) for the v1 → v2 campaign boundary and - [W61](#w61) for public browser proof. The story-graph mechanics themselves are already - complete in W9–W14. -- **Status:** Done — implemented via #189 and #191. -- **Done when:** - - W64.1 Each campaign has at least three material routes, three optional visible scenes, - three delayed consequences, three `showWhen` discoveries driven by different state, - two seeded random-event points whose outcomes are both exercised, and two later gates - driven by stats or remembered interactions. Static graph assertions and named - playthroughs prove the counts; source-line or raw-node counts do not. - - W64.2 All six campaign-shape targets above are met using reachable visible scenes. No - promised consequence is authored only as pass-through text that settle prevents a - client from seeing, and no route pads its length with repeated “continue” decisions. - - W64.3 Two playthroughs of each campaign diverge for at least two consecutive visible - scenes before any reconvergence, and no single valid playthrough visits more than 70% - of that campaign's reachable visible scenes. At least one tested ending per campaign - requires an earlier choice made three or more submissions before the ending path opens. - - W64.4 Every named ending above is reachable by an intentional route, with an authored - label and ending text rather than a numbered placeholder. Exploration achievements - unlock exactly once for optional content and do not become resolution inputs across - sessions. - - W64.5 Every campaign publishes version `2.0.0`. A v1 active save, a v1 ended save, and a - v1 save with achievements migrate or fail with a deliberately tested published-id - decision; no save resumes on a missing node, and migrated saves are marked - `replayCompatible: false` as W31 requires. - - W64.6 Each source builds with no Tier 1 findings and no unexplained Tier 2 warning. A graph - test detects unreachable scenes, exitless settle cycles, duplicate ids, endings that - cannot be reached, hidden choices that can never appear, and random branches that can - remove every route to a planned objective. - - W64.7 At least three committed route fixtures per campaign cover materially different - paths and endings; separate seeds cover every authored random transition. Replaying a - fixture twice is byte-identical, sink-independent, and stable across save/load at a - delayed-consequence checkpoint. - - W64.8 One representative alternate route through each campaign produces the same ordered - scenes, available actions, visible view, and final serialized state through the text - client and browser adapter. A hidden choice remains absent and returns - `unknown_action` if probed; the browser never gains campaign-specific rules. - - W64.9 Recurring-universe references are prose and stable authored ids only. A test registry - containing all six campaigns builds without string-key conflicts, and running any - campaign with a fresh profile produces the same initial state regardless of which - other campaigns were previously played. - - W64.10 `/play/` exposes all six v2 campaigns with truthful duration and content notices; - one full route in each is keyboard-complete at the W63 breakpoints, renders every - visible stat as text, and shows no raw `LocKey`, internal node id, or hidden state. - - W64.11 The initial page says “Your story begins here.” After every successful choice, the - next page names the resolved label under “You chose” and visually connects it to the - new scene under “which brought you here.” Preview, unavailable, rejected, save, and - load operations cannot create a false transition receipt; rapid double submission - cannot duplicate one. - - W64.12 “Journey so far” records the ordered projected scene excerpt and committed choice - label for the current live route, highlights the current page, and lets the player - inspect a prior entry read-only before returning focus to the current choice. It stores - no `Scene.id`, action id, condition, raw `PlayerView`, seed, or serialized state. A - checkpoint may carry a separate presentation-only journal beside the save handle; if - that journal is missing or invalid, load succeeds and displays “Journey resumed at this - checkpoint” rather than inventing earlier steps. Journal presence or absence produces - byte-identical engine serialization. - - W64.13 The casebook is recognisably playable before decoration loads: current page, - consequence link, numbered choices, character sheet, journey control, and ending action - remain in that reading order. Page-turn motion lasts no longer than state punctuation, - never delays an action, and becomes an instant page replacement under reduced motion. - At 320 px the book becomes one page with the same semantic order; it never requires a - two-page spread, hover, drag, sound, or a page-flip gesture. -- **Out of scope:** a new node kind, conditional-transition operator, inventory or relationship - subsystem, cross-campaign saves or unlocks, procedural/AI-authored prose, new campaigns, - simulation/world-graph content, localization, voice/audio, canvas/WebGL, undo/backtracking, - a fabricated completion percentage, and any journal field that participates in game - resolution. - -### [x] W65 — Browser Test Harness for the Site {#w65} - -**Delivers:** Gives `site/` the ability to prove anything about how a page actually renders. -Its tests run in jsdom today, which performs no layout: `getBoundingClientRect` returns zeros -and stylesheet CSS is never cascaded, so no computed size, hit area, overflow, or contrast can -be asserted. That is why [W63](#w63) was accepted on manual inspection at four widths, recorded -as known-and-retained in [`OPEN-QUESTIONS.md`](OPEN-QUESTIONS.md), whose stated revisit trigger is -exactly this: extend `site/` with real-browser tooling, and extend it to `/play/` first. - -W65 adds a real-browser runner, an accessibility scanner, and visual snapshots, then **captures -the currently shipped rendering as the baseline**. Ordering matters: [W66](#w66) recomposes the -play surface and promises the desktop compositions survive untouched, and that promise is only -provable against a baseline taken before the CSS moves. A harness stood up alongside the -redesign would baseline the changed rendering and prove nothing. - -The harness is test infrastructure. It ships no product behaviour, no page change, and no -engine change. Where an existing jsdom test is adequate it stays put; this is not a migration -of the whole site suite. - -- **Spec:** [`14-game-interface.md`](14-game-interface.md) §10 for the proof list this must be - able to execute, and §8 for the widths and states it must reach; - [13 §7–§8](13-playable-web-demo.md#7-client-proof-and-tests). -- **Touches:** `site/package.json`, `site/vite.config.ts`, a browser-test setup file, new - specs under `site/src/play/` and `site/src/`, committed baseline snapshots, and the CI - workflow that runs the site's check script. No `src/engine/`, `design/` contract, or - product-code change beyond a test id where a control is otherwise unaddressable. -- **Depends on:** [W63](#w63) and [W64](#w64) being on `main`, since the baseline is of what - they shipped. No engine dependency. -- **Done when:** - - W65.1 `site/` runs specs in a real browser engine, driven by the package's existing check - script and by CI, with a documented single command. A deliberately failing computed-style - assertion fails that command; a jsdom-only run cannot silently satisfy it. - - W65.2 The runner can set viewport width and height, so a spec can assert at 320, 360, 390, - 414, 768, and 1280 px in portrait and at one landscape phone size, and can emulate - `prefers-reduced-motion` and forced colours. - - W65.3 A spec can read a **computed** style and a real hit area from a rendered control, and - can assert the document does not scroll horizontally. Each of those three capabilities - has a self-test proving it fails when the condition is violated. - - W65.4 An automated accessibility scan runs against the shelf, briefing, content notice, - playing, unavailable-choice, rejected, and ended states, and fails the build on a - violation at the agreed severity. Existing violations, if any, are recorded explicitly - rather than silenced by lowering the threshold. - - W65.5 Visual snapshots of the shipped `/play/` rendering are captured and committed for - playing, unavailable-choice, persistence-warning, and ended states at 320, 390, 768, and - 1280 px. Snapshot review and update are documented, and a snapshot diff fails the build. - - W65.6 The harness is deterministic enough to run in CI without flake: fonts, animation, and - any time-dependent rendering are pinned or disabled for capture, and a repeated run on an - unchanged tree produces no diff. - - W65.7 Engine gates, documentation checks, the existing jsdom suite, the production site - build, and `git diff --check` all still pass, and the added tooling does not enter the - shipped `/play/` bundle. - - W65.8 The known-and-retained W63.7/W63.8 entry in [`OPEN-QUESTIONS.md`](OPEN-QUESTIONS.md) is closed - or narrowed to whatever genuinely remains, rather than left standing beside a harness - that resolves it. -- **Out of scope:** any `/play/` visual, layout, type, or markup change — that is [W66](#w66); - migrating the existing jsdom suite wholesale; a harness for `docs/`; performance - budgets, Lighthouse scoring, or cross-browser matrices beyond the one engine needed to - make the assertions real; and any engine or campaign change. - -### [x] W66 — The Play Surface on a Phone {#w66} - -**Delivers:** Recomposes `/play/` for the device most visitors actually hold. The W63 cabinet -and the W64 casebook were both measured on a desktop and then allowed to shrink: authored -prose renders at 16 px and choice labels at about 13 px on a phone, cabinet controls stand -roughly 34 px tall against a 44 px comfortable touch target, the cabinet's 8 px offset shadow -sits outside a nearly full-width element at 320 px, panels are sized in `vh` under a collapsing -mobile toolbar, no edge respects a device safe area, and every turn requires scrolling past the -scene to reach the choices — after which committing one scrolls the player back to the top. - -W66 makes the phone the composed case rather than the degraded one. Below 768 px a turn becomes -**two scroll-snapped pages** in one ordinary scrolling column: a scene page that fills the -viewport and names how many choices wait below it, then a choice page of full-width cards under -a pinned one-line echo of the scene. Type and hit-area floors from -[14 §8.1](14-game-interface.md#81-type-and-target-floors) raise every width, including desktop, -because a 0.68–0.82 rem control scale was never comfortable there either. - -**The retro look is a fixed input, not a variable.** The palette, the terminal type family, the -scan lines, stamped uppercase labels, offset shadows, double borders, and campaign accents all -survive unchanged. This slice moves size, spacing, safe areas, and reading order. A submission -that reads as a modern mobile app has failed even if every measurement passes. - -The slice stays presentation-only under the boundary -[13 §3](13-playable-web-demo.md#3-composition-and-dependency-direction) and -[14 §1](14-game-interface.md#1-outcome-and-boundary) already set. No engine, kind, campaign, -projection, DTO, session, persistence, or client-parity change; no new gesture, route, bundle, -component tree, or user-agent branch. - -One correction rides along because it is a phone problem specifically: the authored scene body -is currently marked up as an `h2`, which makes the screen-reader heading rotor — the primary -navigation mechanism on a phone — return a wall of story instead of a landmark. The scene -becomes a labelled region with a short real heading, and the post-commit focus target moves -with it. Rendered authored text is unchanged. - -- **Spec:** [`14-game-interface.md`](14-game-interface.md) §1 and §8 (Revision 2), with §§2–7 - unchanged; [`13-playable-web-demo.md`](13-playable-web-demo.md) §3, §7–§9; - [09 §1](09-clients.md#1-the-rule-made-testable), - [§2](09-clients.md#2-the-only-surface), - [§6](09-clients.md#6-projection-is-not-optional). -- **Touches:** `site/src/play/play.css` (type scale, hit areas, snap pages, safe areas, - full-bleed trim, breakpoints), `site/src/play/PlayApp.tsx` (scene region and heading, - choice-count cue, post-commit scroll target), `site/src/play/PlayApp.test.tsx` and the - site's browser/visual checks, and `site/play/index.html` only if a viewport or - `theme-color` correction is needed. No `src/engine/` change of any kind. -- **Depends on:** [W65](#w65) — hard, not preferential. Every measured criterion below needs a - real browser, and the promise that the desktop compositions survive is only checkable - against a baseline W65 captures before this slice moves any CSS. Also [W63](#w63) for the - cabinet grammar and [W64](#w64) for the casebook, journey log, and arrival receipt that - must survive the recomposition. No engine dependency. -- **Done when:** - - W66.1 Every §8.1 role meets its floor as a **computed** style at 320 px — authored prose - 1.125 rem at line-height 1.6 or more, choice labels 1.0625 rem, cabinet controls and - dossier titles 1 rem, stat labels and values 0.9375 rem, reason/receipt/journey/save - text 0.875 rem. Assertions read computed values from a rendered tree; matching the - stylesheet's source text does not count. Only stamped marquee, eyebrow, and disk labels - sit below that, and each is decorative or duplicated by larger text nearby. - - W66.2 Every interactive control — choice card, cabinet button, dossier, notice button, - journey control, scene-echo cue — presents at least a 44 × 44 px hit area at 320 px and - at 1280 px, produced by padding rather than a transparent overlay, with at least 8 px of - non-actionable space between adjacent choice controls. - - W66.3 Below 768 px a turn renders as two scroll-snap stops: a scene page that fills the - viewport and names the shown-choice count, then a choice page of full-width cards under - a pinned single-line scene echo that returns to the scene page when activated. At - 768 px and above no snapping applies, and the 1280 px composition differs from the - W65 baseline only in the type and spacing W66.1 and W66.2 require — every other - difference is either justified in the pull request or reverted. - - W66.4 With scroll-snap unsupported, with smooth scrolling unavailable, and with the cue's - script path disabled, the scene, every choice, and the status console all remain - reachable by ordinary vertical scrolling in that order. Both pages are in the DOM at - all times; no choice is conditionally unmounted, gesture-gated, or revealed only by - animation. - - W66.5 Committing an action lands the player on the new turn's scene page with focus on the - scene, never on a stale choice page and never mid-transition. A rejected action leaves - the player where they were with the scene still authoritative, and cannot produce a - false arrival receipt or journey entry — W64.11 and W64.12 still hold verbatim. - - W66.6 No gesture is introduced: no swipe, horizontal paging, carousel, drag, long-press, - edge gesture, or pull-to-refresh interception. Pinch zoom is not disabled and the - viewport is not pinned to a fixed width. - - W66.7 Full-height panels use dynamic viewport units, every inset-facing edge adds - `env(safe-area-inset-*)` padding, and below 768 px the cabinet is full-bleed with its - offset shadow and double border collapsed to a single edge. The document does not - scroll horizontally at 320, 360, 390, 414, or 768 px in portrait, in landscape, or at - 200% zoom; at 200% zoom on a 390 px viewport the narrow composition is retained. - - W66.8 The authored scene renders inside a labelled region with a short real heading; the - prose itself is not a heading. The page keeps one H1, a coherent heading order, and its - existing live-region announcements. Automated accessibility checks pass on shelf, - briefing, notice, playing, unavailable-choice, rejected, and ended states, and a - keyboard-only pass completes a full turn without a pointer. - - W66.9 The retro identity is preserved and shown to be: the palette custom properties, type - family, scan-line overlay, stamped uppercase labels, offset shadows, double borders, - and the six campaign accent themes are all still present and applied. No colour token - changes value. Uppercase transformation appears on stamped labels only — never on - authored prose, choice labels, reasons, or error text. - - W66.10 Reduced motion makes the cue jump and the post-commit return instant, removes - smooth scrolling, and leaves snapping and every authored transition already governed by - W63.9 intact. No action waits on an animation and no permanent timer runs while idle. - - W66.11 One full route through Bureaucracy and one through each Lucifer role complete at - 320 px, 390 px, 768 px portrait and one landscape phone, with no clipped authored text, - no truncated or ellipsised choice label, and no action stranded below an inaccessible - internal scroll region. Visual snapshots cover playing, unavailable-choice, - persistence-warning, and ended states at each of those sizes. - - W66.12 Browser/text-client parity still produces byte-identical serialized outcomes, the - engine package is untouched, `/play/` remains a direct static route making no runtime - request, and the decorative payload does not grow. Site checks, documentation checks, - engine gates, and `git diff --check` all pass. -- **Out of scope:** any engine, kind, campaign, projection, DTO, reason-code, session, or - persistence change; new campaigns, scenes, endings, or mechanics; a palette, type-family, - or voice refresh; a separate mobile route, bundle, component tree, or user-agent branch; - a native shell, PWA, service worker, install prompt, or offline mode; gesture navigation, - a bottom-sheet choices modal, or a carousel; durable storage, accounts, analytics, audio, - new art beyond CSS-native trim adjustments, and a mobile visual language for the - simulation or world-graph kinds. - -### Correctness Debt Found by Reconciliation - -### [x] W67 — Restore the Story-Graph Regression Evidence {#w67} - -**Delivers:** The replay corpus, determinism goldens, and observability acceptance test that -the [W64](#w64) campaign rewrite removed for the flagship kind. Story-graph is currently the -only shipped kind with no cross-version oracle, and nothing reports that: the suite is green, -because the tests that would have failed were deleted along with the campaigns they covered. - -Three separate losses, one cause. `bulgaria-bureaucracy.replay.test.ts` was the only reader of -`fixtures/replay/bureaucracy-*.{fixture,outcome}.json`; those six files are still on disk, -still pinned at `campaignVersion: "1.0.0"`, and now orphaned. Five -`__snapshots__/*.determinism.test.ts.snap` golden files went with their tests. And -`bulgaria-bureaucracy.observability.test.ts` — the executable form of `MVP.md` §5's -*Observable* box, proving a human reading the `jsonl` stream can diagnose the gate's visit -counts and the random transition's pick — has no replacement; `nullEmitter` now appears only -in two unit suites, against no real campaign. - -The corpus files could not have survived the rewrite unchanged, since the campaigns now publish -`2.0.0`. That is the reason to regenerate them deliberately, not the reason to leave them. - -**A regenerated `.outcome.json` is a statement that the game changed** -([07 §4](07-replay.md#4-the-corpus)) and is reviewed as one. This unit produces new outcome -files against v2 routes; each is read on its merits, not accepted because the runner emitted it. - -- **Spec:** [`07-replay.md`](07-replay.md) §4, §6, §7; [04 §14](04-core.md#14-determinism-harness); - [`05-observability.md`](05-observability.md) §12; `MVP.md` §5. -- **Touches:** `src/engine/fixtures/replay/bureaucracy-*.{fixture,outcome}.json`, - a restored replay suite and determinism suite beside the campaigns, and a restored - `jsonl` observability suite. No `design/` change — this unit implements what the - documents already require. -- **Depends on:** [W64](#w64) being on `main`, since the fixtures are regenerated against its - v2 campaign graphs. -- **Status:** Done — [PR #261](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/261). -- **Done when:** - - W67.1 Three `bureaucracy-*` fixture/outcome pairs exist against `campaignVersion: "2.0.0"`, - covering materially different routes and matching [07 §4](07-replay.md#4-the-corpus)'s - own priority order: a Definition-of-Done arc, a gated choice, and one deliberate edge - case. Each outcome file is reviewed as content, not regenerated in bulk. - - W67.2 A replay suite enumerates the corpus by prefix from `fixtures/replay/`, so a new - fixture pair needs no test-file edit, and honours `REPLAY_BASELINE_DIR` the same way the - `stable-life` and `world-graph-mvp` suites do — the W23 release-tag job must cover - story-graph again. - - W67.3 **The corpus asserts its own membership.** A test fails when an expected fixture - prefix is absent and when a `.fixture.json` has no matching `.outcome.json`, or vice - versa. Directory enumeration alone is what made the original deletion invisible; this - criterion is the whole lesson of that loss and is not optional. - - W67.4 A story-graph determinism suite commits a golden `serialize()` output for at least - one campaign and replays it under `nullEmitter` and `recordingEmitter` with - byte-identical output — [04 §14](04-core.md#14-determinism-harness)'s golden-file and - sink-independence rows, against a real campaign rather than a synthetic state. - - W67.5 Stream reproducibility holds: the same fixture under `recordingEmitter` twice yields - the identical event sequence, with `gameId` normalized out - ([05 §12](05-observability.md#12-validation-and-tests)). - - W67.6 A `jsonl` observability suite restores `MVP.md` §5's *Observable* claim against a real - campaign: the stream is unfiltered, and both a gate's visit counts and a random - transition's pick are readable from it. - - W67.7 Engine gates, documentation checks, and `git diff --check` all pass, and no orphaned - fixture file remains in `fixtures/replay/`. -- **Out of scope:** campaign content changes; a new corpus for `simulation` or `world-graph`, - both of which still have theirs; the cross-repository replay corpus; any change to the - runner's verdict vocabulary or to `Outcome`'s shape. - -### [~] W68 — Make the Browser Save Adapter Actually Restore — cancelled {#w68} - -**Cancelled.** Every file this unit touches is in `site/src/play/`, the route -[`13-playable-web-demo.md`](13-playable-web-demo.md) now records as superseded by -[SubZeroDev.Adventures](https://github.com/The-Running-Dev/SubZeroDev.Adventures). -Implementing a durable checkpoint into a surface being deleted buys nothing, and Adventures -already has the durable version this unit was reaching for — server-side saves in Postgres, -not a `localStorage` adapter. - -Two of the criteria were about the engine rather than the route, and outlive the -cancellation. Both are in [`OPEN-QUESTIONS.md`](OPEN-QUESTIONS.md)'s open register: -**W68.5** — `SaveRecordStore.delete` has no caller in the engine or in either shipped host, -so the port requires a method nobody invokes. **W68.2**'s resume affordance rests on a -per-player save query the `SessionStore` contract has no operation for, which two independent -hosts have now separately invented. - -The original statement of the unit follows, unchanged, as the record of what was planned. - -**Delivers:** The working half of the durable local checkpoint that -[13 §5](13-playable-web-demo.md#5-checkpoints-and-lifetime) Revision 2 now specifies. The -`SessionPersistence` port and a `localStorage` adapter shipped with [W61](#w61); the adapter -writes under `record.campaignId` and reads by `saveId`, so every write succeeds, every read -misses, and no gate notices. Nothing on the page ever attempts a restore, and the session half -of the adapter is an in-memory `Map`, so nothing survives a reload today regardless. - -The design was written after the code here, which is the defect and is recorded as such in -`design/90-decisions.md`. This unit brings the code up to the specification that -now exists. - -- **Spec:** [`13-playable-web-demo.md`](13-playable-web-demo.md) §5 (Revision 2); - [04 §7.2](04-core.md#72-host-persistence--the-record-store-beneath-the-session-store); - [06 §5.2](06-extensibility.md#52-sessionpersistence-and-profilestore); - [09 §3](09-clients.md#3-what-a-client-may-and-may-not-do). -- **Touches:** `site/src/play/composition.ts` (the adapter), `site/src/play/PlayApp.tsx` - (resume offer and honest save state), `site/src/play/browser-client.test.ts` and - `PlayApp.test.tsx`. `src/engine/` only if `SaveRecordStore.delete` is resolved by removing - it. -- **Depends on:** [W61](#w61). Independent of [W65](#w65)/[W66](#w66) — it changes behaviour, - not layout — but should land before them so the resume path is in the visual baseline. -- **Done when:** - - W68.1 The adapter addresses a save by `saveId` for both `get` and `put`, and a test writes - a save, reconstructs the store from a fresh adapter over the same backing storage, and - loads it back — the round trip the current implementation cannot perform. - - W68.2 `/play/` offers to resume a stored checkpoint on load, per campaign, and declining it - starts a new run without destroying the stored one until the player commits to that. - - W68.3 Storage failure is honest and non-fatal: a quota error, disabled storage, or private - browsing surfaces `storage_failure` rendered through the string table — never a raw - English message — and the run continues in memory. A run with `SessionPersistence` - omitted entirely still satisfies all ten rows of [09 §4](09-clients.md#4-the-api-coverage-checklist). - - W68.4 The save state the page shows is truthful: "saved" is claimed only after a write the - adapter confirmed, and the existing warning copy no longer implies a durable write that - did not happen. - - W68.5 `SaveRecordStore.delete` is resolved — either the store calls it on a path that needs - it, or it is removed from the port. A required method with no caller obliges every host - to implement nothing. - - W68.6 The client still persists nothing: React holds a `SessionStore` and never sees a blob, - an envelope, or a storage key. The adapter lives in the site composition root. - - W68.7 Browser/text-client parity still produces byte-identical serialized outcomes, and - engine gates, site checks, documentation checks, and `git diff --check` all pass. -- **Out of scope:** accounts, cloud sync, cross-device resume, multiple named save slots per - campaign, a server-held session, or any storage-format migration mechanism — the format is - the existing `SaveEnvelope` and §10.2 already owns its versioning. - -### [x] W69 — Consume the Reusable Landing-Page Package {#w69} - -**Delivers:** Keeps the Engine's existing landing page and roadmap exactly as visitors see -them, while replacing its repository-owned Vite build and PowerShell merge machinery with the -reusable landing-page package Platform already publishes. The Engine continues to own its -React pages, visual identity, metadata and tests; the package owns route builds and the -protected documentation merge. - -The site is already an exceptional custom frontend, not a README-driven generic page. It -therefore consumes the package's published custom-adapter seam: two Engine-owned entry modules, -two independently declared metadata sets, the existing static assets, and no copied Platform -component or stylesheet. This is a toolchain extraction, not a landing-page redesign. -- **Spec:** none — site toolchain only. The consumed contract is - `subzerodev-platform-ui-landing-page@0.2.0`'s published `defineLandingPage` adapter and - protected `merge` command. -- **Touches:** `site/landing.config.ts` (new); `site/package.json`, `site/package-lock.json`, - `site/vitest.config.ts` (new), `site/README.md`, `site/scripts/verify-build.mjs`, and a - merge-verification script under `site/scripts/`; the obsolete `site/vite.config.ts`, - `site/index.html` and `site/roadmap/index.html`; `build/Merge-LandingPage.ps1`; - `.github/workflows/docs-ci.yml` and `.github/workflows/docs-deploy.yml`. -- **Depends on:** nothing Engine-side. The immutable external prerequisite is - `subzerodev-platform-ui-landing-page@0.2.0`, whose adapter preserves route-specific - canonical, Open Graph, X/Twitter, icon, theme-colour and no-script metadata. -- **Status:** Done — [PR #272](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/272), - with a follow-up in the same branch fixing the host-image build and the `/play/` host gap - the extraction left behind. - - **W69.4 is not fully met, deliberately, and that is recorded rather than waved through.** - The component tests pass unchanged, but `/play/` was dropped from the build during the - slice, which removed the `cta-play` link from `App.tsx` and its rule from `landing.css` — - a real rendered-content change, authorized mid-slice rather than in this unit's stated - scope. It is consistent with `/play/` being superseded by - [SubZeroDev.Adventures](https://github.com/The-Running-Dev/SubZeroDev.Adventures) - ([`13-playable-web-demo.md`](13-playable-web-demo.md), *Succeeded by - SubZeroDev.Adventures*), which is why it was accepted; the deviation from W69.4 as - written is the part worth naming. The removed `/play/` browser smoke it also cost is - tracked as - [issue #273](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/issues/273). -- **Done when:** - - W69.1 `site/package.json` pins `subzerodev-platform-ui-landing-page` exactly at `0.2.0`, - the lockfile resolves that version, and the site no longer declares Vite or the React - Vite plugin directly; Vitest retains its existing jsdom setup through a dedicated - test configuration. - - W69.2 `site/landing.config.ts` declares exactly `/` and `/roadmap/`, each pointing at its - existing Engine-owned entry module and carrying the complete metadata currently in - that route's HTML: title, description, canonical URL, Open Graph fields, X/Twitter - card, icons, theme colour and no-script text. - - W69.3 `npm --prefix site run build` emits `site/dist/index.html` and - `site/dist/roadmap/index.html`, copies every referenced public asset, and leaves no - development-only `/src/` reference in either built document. - - W69.4 The existing landing and roadmap component tests pass unchanged, and no Engine - component, page copy, stylesheet or public asset changes; adopting the package causes - no rendered-content or visual redesign. - - W69.5 The site scripts use the package CLI for `dev`, `build` and `merge`; the handwritten - Vite route inputs and `build/Merge-LandingPage.ps1` have no remaining caller or copy in - the repository. - - W69.6 A merge test starts from a real landing build and a fixture documentation output, - then proves the result contains `/`, `/roadmap/` and `/docs/` while every file under - the protected `docs/` subtree remains byte-identical. A landing fixture containing a - top-level `docs/` path is rejected, proving the guard's negative path. - - W69.7 Both documentation workflows install the pinned site dependencies, run the full site - check, and invoke the package-backed merge. Their triggers, Pages permissions, - concurrency and deployment environment remain caller-owned and unchanged. - - W69.8 `npm --prefix site run check`, `build/Test-Documentation.ps1` and - `git diff --check` pass; the production documentation build remains a separately - reported Docker-dependent gate. -- **Out of scope:** changing landing-page or roadmap content, visual design, routes, roadmap - delivery status, documentation information architecture, GitHub Pages policy, or the - reusable package itself; adopting the package's generic README renderer; copying any - Platform page component, style or token into Engine. - -### [~] W70 — Gate `/play/`'s Startup Request Surface — cancelled {#w70} - -**Cancelled**, for the same reason as [W68](#w68): it gates the emitted `/play/` bundle, and -that route is superseded by -[SubZeroDev.Adventures](https://github.com/The-Running-Dev/SubZeroDev.Adventures) -([`13-playable-web-demo.md`](13-playable-web-demo.md), *Succeeded by SubZeroDev.Adventures*). - -**The property is not cancelled — only this unit's location for it.** A browser build that -silently acquires an off-origin request is exactly the regression -[`13`](13-playable-web-demo.md) §4 refuses to leave unasserted, and Adventures ships a browser -build. The gate belongs beside the bundle it guards, in the repository that emits it, which is -no longer this one. Adventures already carries §4's Node-only-import half of the same check in -its own `scripts/verify-build.mjs`; the startup-request half is the piece still missing there. - -The original statement of the unit follows, unchanged. - -**Delivers:** An assertion that the emitted `/play/` bundle issues no startup request beyond -the same-origin `campaigns/` files the deployment already contains. - -`13-playable-web-demo.md` §6 used to claim the deployment made no runtime network request at -all, and campaigns are now runtime-loaded JSON, so the claim was false and nothing noticed. -Reconciliation restated §6 against what ships; this unit supplies the gate, because a restated -claim with no check is the same unasserted property §4 already refused to accept for `node:` -specifiers. `site/scripts/verify-build.mjs` is the existing home: it already scans the emitted -bundle rather than trusting the build to have failed. - -The check is a boundary, not an inventory. It must fail on a *new* request destination — a CDN -font, an analytics beacon, a third-party host, an engine API — without failing every time a -campaign is added, since the campaign set is enumerated by `manifest.json` and changes by -design. -- **Spec:** [`13-playable-web-demo.md`](13-playable-web-demo.md) §4 (the gate is an assertion - over the emitted bundle, not the build succeeding), §6 (what the startup path may - request), §9 (a fetch failure is a start-up failure the error boundary owns). -- **Touches:** `site/scripts/verify-build.mjs`; a browser test under `site/src/play/browser/`. -- **Depends on:** nothing. [W62](#w62)'s container smoke consumes the same property but does - not block this. -- **Status:** Not started. -- **Done when:** - - W70.1 A build-verification assertion scans the emitted `/play/` bundle for request - destinations and fails on any absolute URL, protocol-relative URL, or non-`campaigns/` - same-origin path reachable from the startup path. - - W70.2 The assertion has a proven negative: a fixture bundle containing one off-origin - request makes the check go red, committed alongside it. A gate never seen to fail is - not evidence. - - W70.3 Adding or removing a campaign JSON changes no assertion and needs no update to the - gate — the campaign set is data, enumerated by `manifest.json`. - - W70.4 A browser test asserts the observed network requests during a real `/play/` load are - exactly `campaigns/manifest.json` plus the files that manifest lists, and nothing else. - - W70.5 A failed campaign fetch renders §9's single error boundary with a restart path, and - never a raw exception or a partial shelf. - - W70.6 `npm --prefix site run check`, the engine gates, `build/Test-Documentation.ps1` and - `git diff --check` all pass. -- **Out of scope:** moving campaigns back into the bundle, changing the portable format, - caching or a service worker (a named non-goal, [`13`](13-playable-web-demo.md) §10), and - W62's container-side smoke, which asserts the same property from outside the page. - -- [ ] More clients (Discord; the first web client is [W61](#w61)). -- [ ] **Additional locales — sliced as [W60](#w60).** The MVP ships English only; the - authoring→registry types already support more - ([04 §10.1](04-core.md#101-content-registry)), so this is string tables plus tooling, - no type change. -- [ ] AI-assisted authoring (content only; engine validates). -- [ ] **W63 proposed — Hosted engine edge.** Follow W62 with the real - `.NET Platform edge → Node engine workload` process boundary: generated JSON/HTTP service - contract first, MCP as a projection, one in-memory remote session before persistence, - accounts, catalogue, or metering. Slice it only after W62 and the Platform package gate are - proven ([`neaas-platform-vision.md`](https://github.com/The-Running-Dev/SubZeroDev.Platform)). -- [ ] Content packs — **sliced as [W58](#w58) and [W59](#w59)** — per - [`11-content-packs.md`](11-content-packs.md): `resolvePacks` as a - pure ordered fold; campaigns replace wholesale, strings per key; exact-version - dependencies with no range solving; `campaignVersion` stamped with the `ResolutionId` - so a game records the content it actually ran against; experiment gates (§5a) as the - one mechanism for both A/B testing and feature flags, filtered before the fold via - `ExperimentSource` ([06 §5.5](06-extensibility.md#experimentsource)). Before mods, not before MVP - ([`neaas-platform-vision.md`](https://github.com/The-Running-Dev/SubZeroDev.Platform) → Known deferred gaps). - `src/engine/src/core/composition/types.ts` now declares `ExperimentSource`, and the gate - machinery ships and is exported. [06 §4](06-extensibility.md#4-the-composition-root) now - settles the remaining seam: `SessionHost.experiments` carries the already-resolved, - non-null assignment map for event attribution; candidate packs and `ExperimentSource` - remain above the session layer. The declaration and record-stamping implementation still - need to be brought into line with that resolved contract. - -### Content Tooling — A First-Class Workstream, Not an Afterthought - -> Peer review's sharpest point: as campaigns grow, the runtime stabilizes while -> **tooling becomes the larger effort.** Named here so it is planned, not discovered. - -- [ ] Content validator / linter (the Tier 1/2 checks, as an author-facing tool). -- [ ] Graph visualization + a visual node editor. -- [ ] Content diff and balancing tools. -- [ ] Localization tooling (string-table extraction, coverage, translation). -- [ ] Authoring assistants (AI-drafted content → the same validation, §9). - -**Tier 3 is the one of these that is fully specified and entirely unbuilt**, so it is sliced -first, as **W73**. [04 §11](04-core.md#11-tiered-validation) names it — "unwinnable campaigns, -dead-end states — found by running, not reading" — and [03 §11](03-story-graph-kind.md#11-validation-story-graph-specific) -gives its two story-graph cases, but nothing under `src/engine/src/core/validation/` -implements it. - -### [x] W73 — Tier 3 Validation as an Author-Facing Check {#w73} - -**Delivers:** Tells a campaign author the thing that reading the campaign cannot — which -endings no sequence of choices can actually reach, and which choices no reachable state can -ever satisfy. Today a campaign passes every load-time check and still ships with an ending -nobody can get to; the only way to find that out is to play it and notice. - -**Scoped deliberately narrow, because the contract disagrees with itself about what Tier 3 is.** -[12 §15](12-world-graph-kind.md#15-validation) rejects "Tier 3 simulation findings" outright — -dominant strategies, infinite-money loops, unavoidable bankruptcy — on the grounds that they -are content-balance findings from a long-running search, and putting one inside registry -construction would break [04 §10.1](04-core.md#101-content-registry)'s purity requirement. That -objection is correct and this unit does not touch it. What is left, and what 03 §11 actually -asks for, is **reachability over a finite authored graph** — decidable, bounded, and run -offline as tooling, never at load. If the two statements turn out not to reconcile this way, -that is `/contract`'s call, not this unit's. -- **Spec:** [04 §11](04-core.md#11-tiered-validation) (Tier 3 is not part of load), - [§10.1](04-core.md#101-content-registry) (registry construction is pure and total); - [03 §11](03-story-graph-kind.md#11-validation-story-graph-specific) (the two story-graph - Tier 3 cases); [12 §15](12-world-graph-kind.md#15-validation) (what Tier 3 is *not*). -- **Touches:** a new `src/engine/scripts/validate-campaign.ts` — tooling outside `src/`, the - same placement and rationale `demo-cli.ts` already uses; `src/engine/package.json` - scripts; a broken fixture under `src/engine/src/campaigns/`. -- **Depends on:** nothing. -- **Status:** Done — [PR #319](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/319). -- **Done when:** - - W73.1 Running the checker over a committed story-graph campaign reports, per ending, - whether some sequence of choices from `startNodeId` reaches it, and exits non-zero when - any ending is unreachable. - - W73.2 A committed fixture with exactly one unreachable ending makes the checker name that - ending and fail — and the same fixture passes Tier 1 and Tier 2, which is what proves - the check finds something load-time validation cannot. - - W73.3 A choice whose `requirements` no reachable variable state can satisfy is reported - with its node id and choice id — 03 §11's second Tier 3 case. - - W73.4 No registry path invokes the checker. `buildValidatedContentRegistry` is asserted - unchanged in behaviour and timing, so registry construction stays pure and total. - - W73.5 The search is bounded and honest about it: it terminates on every committed campaign, - and distinguishes **proven unreachable** from **not proven reachable within the bound** - rather than reporting the second as the first. - - W73.6 `npm run typecheck`, `npm run lint` and `npm test` pass from `src/engine/`. -- **Out of scope:** balance findings of every kind — dominant strategies, unwinnable economics, - loops — which 12 §15 assigns to a game-side balance harness by name; Tier 3 for the - `simulation` and `world-graph` kinds, whose state spaces are not finite the way an - authored story graph's is; wiring the checker into CI as a required gate, which is a - policy change and wants its own decision; and the Tier 1/2 author-facing linter in the - bullet list above, which is a different tool over checks that already exist. - ---- - -### [x] W74 — Campaign Content Ownership: The Authoring Seam {#w74} - -**Delivers:** Someone writing campaigns in the content repository can install the engine and get -exactly the tools authoring needs — the builders, the portable serializer, the digests — without -the engine's own published stories arriving alongside them. That separation exists in the source -tree today but nothing checks it, so the first time it breaks, it breaks in someone else's build -rather than in this one. - -**Resized 2026-08-19, from five criteria spanning two repositories and a breaking release into -four units that each fit a session.** `W74.2` and `W74.3` assert facts about -[SubZeroDev.Adventures.Content](https://github.com/The-Running-Dev/SubZeroDev.Adventures.Content): -a unit of work in this repository can neither make them true nor honestly tick them, so they move -to [W74c](#w74c) as verified preconditions of the removal they were always guarding. `W74.4` -becomes [W74a](#w74a), and `W74.5` splits into [W74b](#w74b) and [W74c](#w74c) — it was never one -session's work. **`W74.2`–`W74.5` are retired and never reused**; the gap in this unit's numbering -is the record of the split. Two canonical statements still cite `W74.5` by name — -[20 §19](04-core.md#19-published-narrative-authoring) and -[10 §13](02-architecture.md#13-published-narrative-content-ownership) — and W74c owns re-pointing -both. - -- **Spec:** [20 §19](04-core.md#19-published-narrative-authoring) (the subpath is the author-time - contract, the root is the runtime one, and a runtime host must not import authored campaign - source merely to play published portable JSON). -- **Touches:** `src/engine/src/authoring.ts`; `consumer-smoke/smoke.ts` and - `consumer-smoke/package.json`; `.github/workflows/ci.yml`'s consumer-smoke step, if - resolving a second entry point needs one. -- **Depends on:** [W67](#w67). The original line also named "portable format graduation", which - is not a unit id and resolves to nothing in this ledger; `src/engine/src/portable/format.ts` - is shipped and gates nothing here. -- **Status:** Done — closed by - [#301](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/issues/301), all four - criteria ticked. -- **Done when:** - - W74.1 `@the-running-dev/game-engine/authoring` is packed, installable, and exposes only - the documented author-time surface. - - W74.6 `consumer-smoke` resolves `@the-running-dev/game-engine/authoring` **from the packed - tarball**, not from a source link, and imports every value and type the subpath exports. - Deleting one export from `src/engine/src/authoring.ts` fails the smoke build — verified - by deleting one and confirming the failure. - - W74.7 The subpath is closed against published content: a check enumerates its exported names - against a committed sorted list and fails both when a name is missing and when one is - added, and asserts that no published campaign builder or campaign id — any - `bulgaria-*`, `lucifer-*`, `saki-*` or `what-would-lucifer-do*` — is reachable through - it. The shared `buildAdventureCampaign` is not a published campaign and stays. - - W74.8 `npm run typecheck`, `npm run lint` and `npm test` pass from `src/engine/`, and - `install:engine`, `build` and `smoke` pass from `consumer-smoke/`. -- **Out of scope:** removing anything at all — no campaign source, root export, exporter or - `/play/` artifact is deleted here, and `src/engine/package.json`'s version is not bumped; - that is [W74b](#w74b) and [W74c](#w74c). Adventures.Content's own build, which this unit - neither reads nor changes. - ---- - -### [x] W74a — The Bureaucracy Fixture, Frozen Byte-for-Byte {#w74a} - -**Delivers:** The engine keeps one story campaign purely as regression evidence, and it becomes -impossible to change it by accident. Today the same file is both the oracle and a shipped -publication, so an edit made for the story's sake silently moves the baseline everything else is -measured against — and the move only shows up much later, as a replay failure nobody can date. - -- **Spec:** [10 §13](02-architecture.md#13-published-narrative-content-ownership) (a frozen - Bureaucracy campaign may remain inside the engine as story-graph regression evidence only; - it is not a publication source); [20 §19](04-core.md#19-published-narrative-authoring) - (such a fixture is not published and not listed in a manifest); - [W67](#w67) (the corpus and the evidence suites this unit pins). -- **Touches:** `src/engine/src/campaigns/bulgaria-bureaucracy.ts` and - `bulgaria-bureaucracy.bg.ts`; a new freeze assertion beside them; the four - `bulgaria-bureaucracy.*.test.ts` evidence suites; `src/engine/fixtures/replay/`, read only. -- **Depends on:** none. -- **Status:** Done — [PR #333](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/333). -- **Done when:** - - W74a.1 A committed golden pins the built campaign byte-for-byte — the canonical - serialization of `buildBulgariaBureaucracyCampaign()` and its `digestPortableCampaign` — - and any edit to either source file that changes either value fails a named test. - Verified by making a one-character edit and confirming the failure. - - W74a.2 The freeze names itself. The failure message says the campaign is frozen regression - evidence and that changing it invalidates the replay corpus, rather than reporting a bare - digest mismatch that reads like a bug. - - W74a.3 All four evidence suites — replay, determinism, localization, observability — still - run against the frozen campaign, and a test asserts all four files exist, so deleting one - fails rather than silently shrinking the evidence. - - W74a.4 The three `bureaucracy-*` fixture/outcome pairs are still enumerated by prefix from - `fixtures/replay/`, and `.github/workflows/ci.yml`'s release-tag cross-version job still - names `src/campaigns/bulgaria-bureaucracy.replay.test.ts`. - - W74a.5 `npm run typecheck`, `npm run lint` and `npm test` pass from `src/engine/`. -- **Out of scope:** removing Bureaucracy from `scripts/export-campaigns.ts` or from - `site/public/campaigns/` — that is [W74b](#w74b), and while `/play/`'s artifacts are still - in the tree the exporter's output must stay consistent with them. Freezing any other - campaign: the other eight belong to Content and are deleted outright in [W74c](#w74c), - not frozen. - ---- - -### [x] W74b — Retire the Engine's Own Play Surface {#w74b} - -**Delivers:** The engine repository stops carrying a browser game nobody can reach. The route was -removed from the build by [W69](#w69) and Adventures has been the only play surface since, but the -source, the screenshot baselines and the campaign files it used to fetch are all still here — -still type-checked, still tested, still telling anyone who opens the folder that this is where you -go to play. - -- **Spec:** [10 §13](02-architecture.md#13-published-narrative-content-ownership) (the former - in-repository `/play/` route and its campaign artifact directory are superseded, and - Adventures consumes the deployed Content feed rather than Engine-generated campaign files); - and [`13-playable-web-demo.md`](13-playable-web-demo.md), *Succeeded by - SubZeroDev.Adventures*. -- **Touches:** `site/src/play/` in full, including `browser/__screenshots__/`; - `site/public/campaigns/`; `site/vitest.config.ts` and `site/vitest.browser.config.ts`, if - either names a removed path; `src/engine/scripts/export-campaigns.ts` and the - `export:campaigns` script in `src/engine/package.json`. -- **Depends on:** [W74a](#w74a). The freeze must exist before Bureaucracy's exporter entry is - deleted, so the deletion is proven not to have moved the fixture. -- **Status:** Done — [PR #335](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/335). -- **Done when:** - - W74b.1 `site/src/play/` is gone in full, and `npm --prefix site run check` passes — format, - lint, typecheck, unit tests, the real-browser suite, the build and the merge — with no - suite skipped, filtered or renamed to stand in for a deleted one. - - W74b.2 `site/public/campaigns/` is gone, and `site/scripts/verify-build.mjs` and - `site/scripts/verify-merge.mjs` both still pass, which is what proves the built landing - page never referenced it. - - W74b.3 `scripts/export-campaigns.ts` and the `export:campaigns` script are gone, and - `npm run typecheck` from `src/engine/` still covers the remaining scripts through - `tsconfig.scripts.json`. - - W74b.4 Nothing in the repository fetches `campaigns/` or `manifest.json` from a relative - site path any longer; a search across `site/` for both returns nothing. - - W74b.5 The engine's campaign sources and root exports are untouched: `src/index.ts` still - exports all nine builders and `src/engine/package.json`'s version is unchanged. This unit - deletes publication and play artifacts only, so it is not a breaking release. - - W74b.6 `npm run typecheck`, `npm run lint` and `npm test` pass from `src/engine/`; - `npm --prefix site run check` passes; `./build/Test-Documentation.ps1` passes. -- **Out of scope:** deleting campaign sources or root exports and bumping the version — both are - [W74c](#w74c), and both are breaking where this unit is not. Rewriting - [`13-playable-web-demo.md`](13-playable-web-demo.md) or - [`14-game-interface.md`](14-game-interface.md), which already record `/play/` - as superseded; describing the retirement a third time is churn, not reconciliation. - ---- - -### [x] W74c — The Breaking Ownership Release {#w74c} - -**Delivers:** The engine package stops shipping the stories. Someone installing -`@the-running-dev/game-engine` gets an engine — kinds, validation, portable hydration, authoring -primitives — and fetches content from the content feed, which is what every host has actually -been doing since Adventures shipped. The version number finally says so out loud, so a host -knows which upgrade is the one that moves the content. - -- **Spec:** [20 §19](04-core.md#19-published-narrative-authoring) (existing frozen campaigns stay - package-root exports through 0.8.0; the breaking 0.9.0 release removes them from the root, - and the peg is a name to check against `src/engine/package.json` before a bump, not after); - [10 §13](02-architecture.md#13-published-narrative-content-ownership) (published campaign - builders are not package-root API). -- **Touches:** the eight published campaign sources under `src/engine/src/campaigns/`, with their - tests and snapshots; `src/engine/src/index.ts`; `src/engine/src/portable/format.test.ts`; - `src/engine/src/campaigns/story-campaign-expansion.test.ts`; - `src/engine/scripts/demo-cli.ts`; `src/engine/scripts/validate-campaign.ts`; - `src/engine/package.json`; `consumer-smoke/smoke.ts`, if it names a removed export; - `design/20-contract.md` §19 and `design/10-design.md` §13, for the retired `W74.5` citation - only. -- **Depends on:** [W74](#w74), [W74a](#w74a), [W74b](#w74b). W74b is a hard ordering rather than a - preference: `site/src/play/browser-client.test.ts` imports six campaign builders from the - package root, so removing them before that file is deleted breaks `site`'s suite. -- **Status:** Done — [PR #336](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/336). -- **Done when:** - - W74c.1 **The preconditions are verified before anything is deleted, and recorded in the pull - request as commit shas rather than as an assertion.** Adventures.Content publishes all - nine campaigns and its `manifest.json` from its own `scripts/export-content.ts`, invoking - no engine exporter, and its Deploy workflow has succeeded on its current tip. - - W74c.2 At the sha W74c.1 names, the five expanded Bulgaria publications in Content carry - `2.0.0` and 75 endings between them, and Content's `manifest.json` carries a resolution - digest. All three values are quoted in the pull request, and this unit changes none of - them — it is a GameEngine-side deletion, so any movement in them is a signal to stop. - - W74c.3 The eight published campaign sources, their tests and their snapshots are removed, and - their root exports are gone from `src/index.ts`. `bulgaria-bureaucracy.*` stays, exported - from nowhere, and [W74a](#w74a)'s freeze still passes untouched — which is what proves - the removal did not disturb the regression oracle. - - W74c.4 `scripts/validate-campaign.ts` and `scripts/demo-cli.ts` both still run over the - campaigns that remain, and each reports which ones by name. Neither loses a campaign to a - dangling import, and neither is left with an empty set. - - W74c.5 `src/engine/package.json` reads `0.9.0`, and `consumer-smoke` resolves the package - root with no removed name. - - W74c.6 Neither §19 nor §13 cites the retired `W74.5`; both name [W74c](#w74c) instead. That - citation is the only change made to either section — restating §19's version peg now that - it is satisfied belongs to `/reconcile`, not to this unit. - - W74c.7 `npm run typecheck`, `npm run lint` and `npm test` pass from `src/engine/`; - `npm --prefix site run check` passes; `install:engine`, `build` and `smoke` pass from - `consumer-smoke/`; `./build/Test-Documentation.ps1` passes. -- **Out of scope:** publishing or tagging 0.9.0 — the release workflow is the user's to trigger, - and `design/90-decisions.md` records how the last peg was spent by an unrelated - bump. Bumping the engine submodule pin in Adventures or Adventures.Content, which are - separate repositories with their own gates. Removing `fromPortable` or the `Portable*` - types, which Adventures depends on and which are runtime-root surface, not publication. - ---- - -### [x] W75 — Classified Persistence Conflicts {#w75} - -**Delivers:** the one classified adapter failure -[20 §7.2](04-core.md#7-the-session-store-and-the-platform-api) now carves out — a host-branded -lost update surfacing as `concurrent_modification` instead of `storage_failure` — together with -the cache invariant that makes the new message honest. - -**Six of the seven criteria are now satisfied, and the seventh is the validation gate.** This -was written expecting the contract to land first and the code after, which is the order -`design/90-decisions.md` (2026-08-13) argues for. That is not what happened: the branch -carrying the amendment was cut from `d44afde` — the `slice/S1` draft's own commit, opened as -PR #306 — rather than from `main`, so merging the amendment (PR #307) carried the mapping and -its four tests along with it. PR #306 is closed; its commit is on `main` regardless. PR #311 -then closed the two criteria that were left: the cache-restore behaviour on a `submitAction` -conflict (`W75.4`) and the version bump for the added root exports (`W75.6`). - -So this unit **completed** rather than implemented from scratch. `W75.1`–`W75.6` were -satisfied by what landed in `d44afde` and PR #311; `W75.7` was verified 2026-08-19 on `main` -at `7a66da3` — typecheck, lint, 1116 tests across 79 files, `git diff --check`, and -`./build/Test-Documentation.ps1` all clean. Criterion ids are retained as written — they are -never renumbered — so the record of which arrived how stays readable. - -- **Spec:** [20 §7.2](04-core.md#7-the-session-store-and-the-platform-api), - [§12](04-core.md#12-reason-codes-state-changes-messages), - [06 §5.2](06-extensibility.md#52-sessionpersistence-and-profilestore). -- **Depends on:** nothing beyond the shipped session store. -- **Status:** Done — closed by [#308](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/issues/308), all seven criteria ticked. -- **Done when:** - - W75.1 A session write whose adapter throws an exception branded with - `SESSION_PERSISTENCE_CONFLICT` raises `SessionStoreError` with code - `concurrent_modification`; an unbranded throw and a differently branded throw both - still raise `storage_failure`. - - W75.2 `concurrent_modification` is in `BASE_REASON_CODES` with a shipped - `core.reason.*` message, and `20-contract.md` §12's list and growth tally agree with - `reasons.ts`. - - W75.3 The brand is matched on `name`, not by `instanceof`, and a test proves it by - raising a plain object carrying only that `name`. - - W75.4 **A conflict on `submitAction` leaves the store's cache no further ahead than - persistence.** The path mutates a cached record in place before persisting, so the - unit restores or evicts it when the write throws, and a test asserts a subsequent - read returns the pre-conflict state rather than the refused mutation. This is the - invariant 20 §7.2's blockquote states, and it is what makes "refresh and try again" - a promise the store can keep. - - W75.5 The asymmetry is asserted, not assumed: save writes, save reads and session reads - continue to raise `storage_failure` for a branded exception, with - [#226](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/issues/226) named as - the owner of the save-side race. - - W75.6 `SESSION_PERSISTENCE_CONFLICT` and `SessionPersistenceConflict` are root exports, - the package version is bumped for the added surface, and `consumer-smoke` resolves - both. - - W75.7 `npm run typecheck`, `npm run lint` and `npm test` pass from `src/engine/`, and - `./build/Test-Documentation.ps1` passes. - ---- - -### [x] W76 — A Folded Registry Keeps Its Resolution {#w76} - -**Delivers:** A game assembled from several content packs can still say which mix of content it -was played against — on the one route a host is actually able to use. Today that route quietly -loses the record, so every host has to remember to put it back by hand, and a host that forgets -loses it with no symptom at all: the game plays perfectly and simply stops being able to say -what it was. - -**Backfilled from [#315](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/issues/315), -which was authored straight onto the tracker.** The issue carried the full slice — narrative, -ten criteria, out-of-scope list — but no `W76` section ever existed in this ledger, so its pin -cited a section that could not be read. Recorded here so the pin resolves and the ledger stays -the origin; the criteria are transcribed as written and their ids are retained. - -- **Spec:** [11 §3](11-content-packs.md#3-resolution), - [§4](11-content-packs.md#4-the-one-change-to-contentregistry), - [§7](11-content-packs.md#7-validation); - [04 §10.1](04-core.md#101-content-registry), - [§11](04-core.md#11-tiered-validation), - [§12](04-core.md#12-reason-codes-state-changes-messages). -- **Touches:** `src/engine/src/core/registry/packs.ts` and `types.ts`; - `src/engine/src/core/validation/tiered.ts`; `src/engine/src/index.ts`; - `src/engine/src/campaigns/stable-life-packs.ts` and `stable-life-packs.test.ts`; - `src/engine/package.json`. -- **Depends on:** nothing beyond the shipped pack resolution. -- **Status:** Done — [PR #337](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/337), - closing [#315](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/issues/315). All ten - criteria verified against `main` and ticked 2026-08-19. -- **Done when:** - - W76.1 One call takes an ordered pack set and returns a validated, frozen registry whose - `resolution` equals `computeResolutionId` over that same set, with no caller reattaching - anything. Asserted for both a one-pack set and a two-pack set, and the two values differ. - - W76.2 That same call still merges each used kind's own `.reason.*` messages into the - frozen table: a rejection raised through the resulting registry renders the kind's - message, not a bare key. - - W76.3 The new path fails at both stages and reports which: a pack set violating an 11 §7 - Tier 1 rule and a pack set that resolves but whose campaign fails 04 §11 Tier 1 each - return `ok: false` carrying that stage's errors and no registry. One committed fixture - per stage, with the error counts stated. - - W76.4 Tier 2 warnings from both stages reach the caller in one result: a - `pack_override_unexpected` warning and a kind-supplied warning appear together, so - folding swallows neither. - - W76.5 The no-pack route is unchanged. `buildContentRegistry` and the existing - `buildValidatedContentRegistry` signature still produce a registry with - `resolution === undefined`, and every existing call site compiles untouched. - - W76.6 Which string table each campaign is validated against on the folded path is pinned by - a test, not left implicit: a campaign whose `titleKey` resolves nowhere in the resolved - set fails with `missing_string_key`, and a campaign whose `titleKey` resolves only - through a later pack's contribution has its outcome asserted either way. If that outcome - contradicts 04 §11's per-campaign scoping, stop and report it rather than picking a - reading — that is `/contract`'s call. - - W76.7 `stable-life-packs.ts`'s `resolveStableLifeRegistry` — which already consolidated the - fold-then-validate-then-reattach sequence out of its three former callers — is rewritten - to call the new shared call instead of reimplementing that sequence itself, and the - suite passes through the new path with no local helper reconstructing what the call now - returns. - - W76.8 Proven by reverting: with the `resolution` assignment removed from the new path, a - named test goes red. - - W76.9 Whatever shape is chosen — an optional parameter or a pack-aware sibling — the surface - a host must call is reachable from the package root, the package version is bumped for - the added or widened surface, and `consumer-smoke` resolves it through the packed tarball - rather than a source link. - - W76.10 `npm run typecheck`, `npm run lint` and `npm test` pass from `src/engine/`, and - `./build/Test-Documentation.ps1` passes. -- **Out of scope:** making `resolution` required on `ContentRegistry`; changing `resolvePacks`' - fold rules or `computeResolutionId`'s digest; the engine reading `resolution`; pack - discovery, distribution, and loading from disk; W72's Bulgarian content volume; the seven - Adventures findings, which route to `/contract`. - -### Content Tooling — Slicing the Remaining Bullets - -**W73 took the one bullet that was fully specified.** The five above are still open, and they -are now the largest named body of unbuilt work in this repository: the runtime has three kinds, -nine story-graph campaigns, a second locale and a culture pack, and an author has exactly one -command to point at any of it. Four units below take four of the five bullets — two of them -only in half, because the other half of each is blocked on something a slice may not invent. -What is deliberately *not* sliced, and why, is stated in that unit's **Out of scope** rather -than left as an absence. - -### [x] W77 — Tier 1 and Tier 2 as an Author-Facing Check {#w77} - -**Delivers:** Someone writing a campaign can ask what is wrong with it before anyone plays it, -and get back a list they can act on — naming the node, key or id at fault. Today those checks -already run on every campaign, but the only way to see what they found is to be a program that -caught a registry build failing; there is no way for the person who wrote the content to just -ask. - -**The premise under test is [`02-architecture.md`](02-architecture.md) §9's N10** — "the engine -validates AI-authored content" is a safety property only if the validation is reachable by -whoever authored it. It currently is not. The second thing this unit finds out is whether -`ValidationError` as shipped is *legible*: `path` is optional and `details` is sparse, so if a -real finding cannot be located from what the type carries, that is a contract finding to report -rather than paper over. -- **Spec:** [04 §11](04-core.md#11-tiered-validation), - [§12](04-core.md#12-reason-codes-state-changes-messages), - [§17](04-core.md#17-identifier-conventions); - [03 §11](03-story-graph-kind.md#11-validation-story-graph-specific); - [`02-architecture.md`](02-architecture.md) §9, §9.2. -- **Touches:** a new checker under `src/engine/scripts/`, alongside `validate-campaign.ts` — - the placement and rationale [`02-architecture.md`](02-architecture.md) §9.2 already - states; `src/engine/package.json` scripts. `src/engine/src/core/validation/tiered.ts` is - **read, not modified**. -- **Depends on:** nothing. -- **Status:** Done — [PR #338](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/338), all seven criteria ticked. -- **Done when:** - - W77.1 Running the check over a named committed campaign reports every Tier 1 error and - Tier 2 warning `buildValidatedContentRegistry` produces for it, each rendered with its - `code`, its message resolved against the string table — never a bare `messageKey` — and - its `path` when one is set. - - W77.2 The exit code carries the tier split: a campaign with any Tier 1 error exits - non-zero, a campaign with only Tier 2 warnings exits zero and still prints them. - - W77.3 Both committed broken fixtures — `bulgaria-bureaucracy.broken.ts` and - `stable-life.broken.ts` — produce a named, non-empty finding list, with the exact error - and warning counts stated in the test rather than asserted as "more than zero." - - W77.4 Coverage is not maintained by remembering. A test enumerates - `src/engine/src/campaigns/*.ts` and requires every module exporting a campaign builder - to be either in the checker's catalogue or in an explicit exclusion list carrying a - stated reason — a module in neither fails the test. So a campaign added without - registering it turns a suite red instead of going silently unchecked, and the - deliberately-broken and locale fixtures stay excluded on the record rather than by - omission. Three scripts (`demo-cli.ts`, `export-campaigns.ts`, `validate-campaign.ts`) - each hand-maintain their own list today; this unit introduces the shared catalogue - rather than a fourth list. - - W77.5 No registry path invokes the checker and no new rule is added. - `buildValidatedContentRegistry` is asserted unchanged in behaviour, and every finding - the checker prints is one the registry path already produces. - - W77.6 Every finding the two broken fixtures produce carries a location — a `path`, or a - `details` entry naming one — asserted finding-by-finding rather than in aggregate. Any - that does not is reported as a contract gap with the campaign and check named, and is - **not** worked around by having the checker re-derive the location itself, which would - put a second validation implementation in the tree. - - W77.7 `npm run typecheck`, `npm run lint` and `npm test` pass from `src/engine/`. -- **Out of scope:** Tier 3 reachability, which is [W73](#w73)'s `validate-campaign` and stays a - separate command; wiring either checker into CI as a required gate, which W73 already - named as a policy change wanting its own decision; migrating `demo-cli.ts` and - `export-campaigns.ts` onto the shared catalogue; adding, tightening or relaxing any - validation rule — this unit surfaces the checks that exist and authors none. - -### [x] W78 — Localization Coverage and String Extraction {#w78} - -**Delivers:** A translator can be handed the exact list of lines a campaign needs, and a -maintainer can see which campaigns are translated, which are half-translated and which are not -translated at all. Today the only way to discover a translation is incomplete is to try to -build it and read the failure — one missing line at a time, with no way to ask how many are -left. - -[W60](#w60) put string-table extraction, translation workflow and coverage reporting out of its -own scope by name and routed them here. [04 §10.1](04-core.md#101-content-registry) says -additional locales are "string tables plus tooling, no type change"; W60 proved the string-table -half for one campaign, and this is the tooling half, across a shelf where exactly one campaign -of nine has a second locale. -- **Spec:** [04 §10.1](04-core.md#101-content-registry), - [§11](04-core.md#11-tiered-validation) (`missing_string_key`), - [§17](04-core.md#17-identifier-conventions); [09 §5](09-clients.md#5-reason-codes-and-messages). -- **Touches:** a new script under `src/engine/scripts/`; `src/engine/package.json` scripts; - a second-locale fixture under `src/engine/src/campaigns/`. - `src/engine/src/core/localization/` and `src/engine/src/core/registry/` are **read, not - modified**. -- **Depends on:** [W77](#w77), for the shared campaign catalogue. -- **Status:** Done — [issue #326](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/issues/326). -- **Done when:** - - W78.1 For a named campaign the tool emits the complete, sorted set of `LocKey`s its built - campaign requires — the keys `BuiltCampaign.strings` carries — in a form a translator - can fill in. Running it twice produces byte-identical output. - - W78.2 For a campaign with a second-locale build the tool reports, per locale: keys present - in the reference locale and absent here, keys present here and in no reference locale, - and the resulting covered/total count. Asserted against `bulgaria-bureaucracy` and its - `.bg` build, which is complete, and against a fixture with exactly one key removed, - whose report names that key. - - W78.3 An **untranslated** key — present in both tables with byte-identical text — is - counted and reported separately from a missing one. This is the case Tier 1 cannot - catch at all, since the key resolves; stated as a count and asserted on a fixture - carrying exactly one. - - W78.4 Across the whole catalogue the tool reports how many campaigns have a second locale - and which, so the shelf-wide gap is a number rather than an absence nobody counted. - - W78.5 The tool writes only files it was explicitly asked to write: a run against the - committed shelf leaves `git status --short` clean. - - W78.6 `npm run typecheck`, `npm run lint` and `npm test` pass from `src/engine/`. -- **Out of scope:** translating anything; a translation-memory, vendor or review workflow; - per-locale content packs, which [11 §8](11-content-packs.md#8-what-is-deferred) defers by - name; locale-aware pluralization and formatting, which - [09 §9](09-clients.md#9-deferred) leaves to a client; making coverage a CI gate, which is - the same policy decision W77 declines to take unilaterally. - -### [x] W79 — What Changed Between Two Resolutions {#w79} - -**Delivers:** When two saves say they were played against different content, someone can find -out what was actually different. The platform can already prove two content resolutions are not -the same — that is what the resolution id is for — but it cannot say how, so every question -about a divergence ends at "the digests differ," which is exactly where the interesting part -starts. - -[W58](#w58) built `computeResolutionId` and [W71](#w71) proved two real resolutions of -`stable-life` produce different ids. Neither can answer what moved between them, and -[11 §6](11-content-packs.md#6-identity-and-why-determinism-needs-it) is deliberately a digest — -it is *meant* to be opaque, which is precisely why the readable view has to be tooling beside -it rather than a widening of it. -- **Spec:** [11 §3](11-content-packs.md#3-resolution), - [§4](11-content-packs.md#4-the-one-change-to-contentregistry), - [§6](11-content-packs.md#6-identity-and-why-determinism-needs-it); - [04 §10.1](04-core.md#101-content-registry). -- **Touches:** a new script under `src/engine/scripts/`; `src/engine/package.json` scripts. - `src/engine/src/core/registry/packs.ts` and `src/engine/src/campaigns/stable-life-packs.ts` - are **read, not modified**. -- **Depends on:** [W58](#w58), [W71](#w71) — both done. -- **Status:** Done — [issue #327](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/issues/327). -- **Done when:** - - W79.1 Given two ordered pack sets the tool reports both `ResolutionId`s and, when they - differ, an itemized difference following [11 §3](11-content-packs.md#3-resolution)'s two - replacement rules: campaigns replaced wholesale by id, and string keys added, removed or - changed. Asserted on `[base]` versus `[base, bulgaria]` with the expected item counts - stated in the test. - - W79.2 A key whose *value* changed is reported as changed, distinct from added and removed. - This is the case a set difference alone misses entirely, and it is what a culture pack - is almost wholly made of — W71's pack overrides text at existing keys. - - W79.3 Two identical pack sets produce equal ids and an empty difference, and the tool says - so explicitly rather than printing nothing. - - W79.4 Output is deterministic and independent of object key order: the same two sets produce - byte-identical output across runs, and reordering a pack's fields in source does not - change it. - - W79.5 `resolvePacks` and `computeResolutionId` are unchanged and the tool only reads them — - asserted by no file under `src/engine/src/core/` changing. - - W79.6 `npm run typecheck`, `npm run lint` and `npm test` pass from `src/engine/`. -- **Out of scope:** **balancing tools — the other half of this workstream's third bullet.** They - need the simulation harness the provisional-numbers debt - ([issue #267](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/issues/267)) is - itself waiting on, and neither the harness's shape nor what a balance finding *is* is - specified anywhere; slicing it now would mean inventing both. Also out: diffing saves or - runtime state; diffing portable campaign JSON as shipped to hosts, which belongs to - Adventures.Content ([W74](#w74)); pack discovery and loading from disk - ([11 §8](11-content-packs.md#8-what-is-deferred)). - -### [x] W80 — Seeing a Story Graph {#w80} - -**Delivers:** Anyone looking at an authored adventure can see its shape — where it starts, where -it branches, which endings hang off which route — instead of reconstructing it from several -hundred lines of node ids. The shelf is nine campaigns and, across the five expanded Bulgaria -publications alone, seventy-five endings ([W74.3](#w74)); there is currently no way to look at -any of it. - -- **Spec:** [03 §3](03-story-graph-kind.md#3-nodes--the-single-content-type-n7), - [§4](03-story-graph-kind.md#4-choices-and-transitions), - [§8.5](03-story-graph-kind.md#85-terminal-identity); - [`02-architecture.md`](02-architecture.md) §9.1, §9.2 — the three tests that make this - tooling rather than a fourth layer apply here unchanged. -- **Touches:** a new script under `src/engine/scripts/`; `src/engine/package.json` scripts. -- **Depends on:** [W77](#w77), for the shared campaign catalogue and for the Tier 2 warnings - W80.4 cross-checks against. -- **Status:** Done — [issue #328](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/issues/328). -- **Done when:** - - W80.1 For a named story-graph campaign the tool emits a text graph — Mermaid, which needs no - dependency to write — with one vertex per node, edges labelled by choice id, and - ending nodes visually distinguished from `choice`, `auto` and `random` nodes. Every node - and every choice in the campaign appears exactly once, asserted by count against the - built campaign. - - W80.2 Output is deterministic: the same campaign produces byte-identical output across runs, - with vertices and edges in a stated canonical order rather than object-iteration order. - - W80.3 The largest committed campaign renders without truncation, and the tool prints its - node, choice and ending counts beside the graph so a reader can check the picture - against the numbers. - - W80.4 A node no edge reaches is marked as such, and the set it marks equals the set of nodes - [W77](#w77) reports a Tier 2 unreachable warning for on the same campaign — the same - finding in two forms, never two different answers. If they disagree, that is the - finding: report it rather than reconciling it in the renderer. - - W80.5 The tool reads the built campaign only. No file under `src/engine/src/` changes, and - nothing in the registry path imports it. - - W80.6 `npm run typecheck`, `npm run lint` and `npm test` pass from `src/engine/`. -- **Out of scope:** **the visual node editor — the other half of this workstream's second - bullet.** It writes content rather than reading it, so it needs decisions this repository - has not taken: where it lives, what it emits, and whether authored source or built - campaign is its file format. [`02-architecture.md`](02-architecture.md) §9.1's "a campaign - is free not to use one" test is about builders, and an editor that owns the file is a - different question. Route to `/design` before it is sliced. Also out: graphing the - `simulation` and `world-graph` kinds, whose state is not an authored graph; rendering to - an image format; embedding the graph in the documentation site. - -> **The fifth bullet — authoring assistants — is not sliced, and the reason is a decision, not a -> size.** [`02-architecture.md`](02-architecture.md) §9 settles the *boundary* (AI authors -> campaigns, never kinds; its output is data and is validated identically), which is the part -> that needed settling. What it does not settle is anything an implementation needs: which model -> or service, which is a new dependency and so needs a decision-log entry naming what was -> rejected; whether the assistant lives in this repository at all now that -> [SubZeroDev.Adventures.Content](https://github.com/The-Running-Dev/SubZeroDev.Adventures.Content) -> owns published narrative content ([W74](#w74)); and what it would be measured against. W77 is -> its real precondition regardless — an assistant whose output is checked by a validator no -> human can run is the same gap one level up. - -### Correctness Debt — The Tick Pipeline Runs Twenty Systems and Implements Fifteen - -**The world-graph kind's tick pipeline is the largest thing in this repository that is specified -in full and built in part.** [12 §4](12-world-graph-kind.md#4-the-turn-is-a-tick-batch) fixes -twenty tick systems in a normative order, and `src/engine/src/kinds/world-graph/tick/pipeline.ts` -registers and runs all twenty — the ordering, the `processingTick` guard and the finalize-once -assertion are genuine and tested. **Five of the twenty do not do what the contract describes**, -recorded as a known-and-retained gap in `design/90-decisions.md` since 2026-08-05 — named -individually there rather than in this repository's public register, with the stated revisit -condition *each system's own build unit lands*. W81–W85 are those build units, one per system. - -**Re-read against the source while slicing, the gap is seven systems rather than five**, and the -two extra ones decide how these units are cut. `task-generate` (system 9) pushes only `clean` and -`service` candidates, and `staff-work` (system 11) branches on only those two kinds — so `build` -and `restock` sit in the task-kind comparator and in `StaffTaskType`, reachable by nothing. -Systems 12 and 13 therefore cannot be built alone: each needs its own half of systems 9 and 11, -which is why W81 and W82 name three systems apiece. **The register's list is not wrong about the -five; it is silent about the two**, and correcting it is folded into W81 rather than left to be -rediscovered. - -**No unit here introduces a signature.** Every runtime and content type these systems need already -ships — `ConstructionSite`, `Building.wear` and `.cleanliness`, `Incident`, `Alert`, -`ServiceProduct.capacity`/`initialUnits`/`restockTaskPriority`, -`BuildingDefinition.constructionWork`/`constructionTaskPriority`, `IncidentDefinition`'s roll -scope, chance, weight and cooldown, and the world-graph `AchievementDefinition`. What is missing -is the code between them, and the content to exercise it: `world-graph-mvp.ts` declares -`constructionWork: 0`, `capacity: null` and `rollChanceBasisPoints: 0` throughout and carries no -achievements or policies — deliberately, so W49's fixture stayed small. Each unit below therefore -extends that fixture as its content half, which is also what keeps it a vertical slice rather than -a system rewrite. - -**Ordered so the determinism risk is exercised earliest.** The kind rests on batch invariance -([12 §5](12-world-graph-kind.md#5-batch-invariance--and-the-two-seam-changes-it-forced)) — that -`advance_ticks n` reaches the same state as any split of it — and on the draw discipline in -[§9](12-world-graph-kind.md#9-determinism-beyond-the-seed). W81 goes first because construction is -the first system to carry work *across* ticks from a player action as its origin, which is where -an invariance defect would first show. W84 sits where it does because it is the only one of the -five that opens a new RNG draw site, and it needs W83's meters to have something to act on. - -**The shared `SystemPipeline` ([issue #270](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/issues/270)) -is deliberately not sliced here.** Its recorded trigger — a second tick-driven kind — has fired, -but extracting a shared runner from a pipeline where a quarter of the systems are stubs would -extract the shape of the stubs. It becomes sliceable once W85 lands, and it is better for the -wait: five more real systems is five more constraints on what the abstraction has to carry. - -### [x] W81 — Construction Finishes What `build` Starts {#w81} - -**Delivers:** Someone laying out a resort can put up a structure that takes time to build, watch -their staff work on it, and then use it. Today starting one is a dead end — the site appears, no -worker is ever sent to it, and it never becomes a building, so a resort is limited forever to -whatever the scenario placed at the start plus whatever happens to cost nothing to construct. - -The `build` action already opens the site and reserves the building and queue ids it will complete -into, and [12 §13](12-world-graph-kind.md#13-statechange-at-batch-grain) already declares both -audit rows it may write. Everything after the action is missing: the `build` task kind is never -generated, never given effort, and never applied. -- **Spec:** [12 §4](12-world-graph-kind.md#4-the-turn-is-a-tick-batch) §4.11, §4.13, §4.14; - [§5](12-world-graph-kind.md#5-batch-invariance--and-the-two-seam-changes-it-forced); - [§9](12-world-graph-kind.md#9-determinism-beyond-the-seed); - [§12](12-world-graph-kind.md#12-events); - [§13](12-world-graph-kind.md#13-statechange-at-batch-grain); - [04 §12](04-core.md#12-reason-codes-state-changes-messages). -- **Touches:** `src/engine/src/kinds/world-graph/tick/pipeline.ts`; - `src/engine/src/kinds/world-graph/kind.ts` for the two event names; - `src/engine/src/kinds/world-graph/reasons.ts` if an audit reason is added; - `src/engine/src/campaigns/world-graph-mvp.ts`; a new fixture pair under - `src/engine/fixtures/replay/`. -- **Depends on:** [W45](#w45), [W46](#w46), [W47](#w47) — all done; they shipped the reducer, the - construction-site state and the comparator registry. -- **Status:** Done — [PR #345](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/345). -- **Done when:** - - W81.1 A building definition declaring construction work, placed through `build`, opens a site - whose remaining work falls by the assigned staff member's `build` effort per tick on every - tick that member is at the site, clamped once at zero. The test states the exact tick on - which it reaches zero, computed from the declared work and rate, not a bound. - - W81.2 On completion the building and its queue carry the ids the site reserved, asserted in a - test where another entity is allocated between the `build` action and the completion tick - — so completion timing cannot renumber a later entity. - - W81.3 Completion removes the site, materializes the definition's defaults (status, wear, - cleanliness, prices, and the declared initial units), increments the completed-buildings - counter, and increments the map revision the path caches are keyed on. - - W81.4 System 9 generates one `build` candidate per site, with effort equal to the site's - remaining work and priority taken from the building definition's construction task - priority and from nowhere else. A staff role whose supported task kinds omit `build` is - never assigned one, asserted with a cleaner-only roster that leaves the site untouched. - - W81.5 `advance_ticks n` across the whole construction span serializes byte-identically to any - split of that batch, asserted for at least one split falling strictly inside the - construction rather than only at its ends. - - W81.6 `construction.progressed` and `construction.completed` are emitted at the sites - [12 §12](12-world-graph-kind.md#12-events) names, declared in `Kind.eventNames`, and both - rows' status moves to `delivered` in the same change. Dropping every event still leaves - `serialize()` identical ([05 §2](05-observability.md#2-the-determinism-rule)). - - W81.7 The `advance_ticks` result carries the batch-grain existence row for the completed - building, with a reason registered in the kind's vocabulary and a message resolvable - through the string table. If the contract names no reason for completion, that is a - finding reported in the pull request, not a code invented quietly. - - W81.8 The two committed world-graph replay outcomes are unchanged byte-for-byte — the MVP - campaign's only building declares zero construction work, so nothing existing enters this - path — and the new behaviour is proved by a new committed fixture. Any outcome that does - move is recorded as an intended change per - [07 §7](07-replay.md#7-intended-change-versus-regression), naming the fields, rather than - re-recorded silently. - - W81.9 The known-and-retained tick-system entry in `design/90-decisions.md` is edited in the - same pull request: system 12 leaves the list, and the two partial systems this unit found - (9 and 11) are named in it. The standing obligation that entry already carries has been - walked past twice before; this criterion is what makes it a gate rather than a sentence. - - W81.10 `npm run typecheck`, `npm run lint` and `npm test` pass from `src/engine/`. -- **Out of scope:** demolishing or refunding a site that is still under construction — `demolish` - targets a building, and whether a site is a legal target is a contract question rather than - a slice's; system 13's restock and operational status, which is [W82](#w82); scenery, which - has no construction path at all; the shared `SystemPipeline` - ([issue #270](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/issues/270)). - -### [x] W82 — A Kiosk That Ran Out Can Be Refilled {#w82} - -**Delivers:** A stall that sells a limited number of things can be restocked by staff, so running -out is a setback rather than the permanent end of that building's usefulness. Today the running-out -half already works — a guest is refused and stops queueing when the shelf is empty — and there is -no way to fill it again, which makes finite stock a trap no author would use on purpose. - -The consuming side landed with W47: service refuses at zero units and an empty building stops being -a candidate at all. What never runs is the refilling side, which is systems 9, 11 and 13 together — -and system 13 also owns the non-wear operational status changes that no other system may make. -- **Spec:** [12 §4](12-world-graph-kind.md#4-the-turn-is-a-tick-batch) §4.11, §4.13, §4.15; - [§10](12-world-graph-kind.md#10-projection); - [§12](12-world-graph-kind.md#12-events); - [§13](12-world-graph-kind.md#13-statechange-at-batch-grain). -- **Touches:** `src/engine/src/kinds/world-graph/tick/pipeline.ts`; - `src/engine/src/kinds/world-graph/kind.ts`; `src/engine/src/campaigns/world-graph-mvp.ts`; - a new fixture pair under `src/engine/fixtures/replay/`. -- **Depends on:** [W81](#w81), for the systems 9 and 11 task-kind branches it establishes. -- **Status:** Done — [PR #347](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/347). -- **Done when:** - - W82.1 A service product declaring initial units and a capacity is served down to zero, a - restock candidate is generated with effort equal to the missing units and the priority the - product declares, a staff member with the `restock` kind carries it out, and the building - serves again — asserted as one continuous tick sequence with the serving, empty and - refilled ticks each named. - - W82.2 Restock never exceeds capacity, and a product whose units are `null` is infinite: it - generates no candidate and its inventory is never written. - - W82.3 A product's unit cost is recognized exactly once, at service, and system 13 does not - recognize it again — asserted by cash and expense totals across a full stock-out and - refill cycle, stated as exact figures. - - W82.4 A building definition with no restock source, and a decorative one, are honest no-ops: - neither generates a candidate nor writes state, asserted rather than assumed. - - W82.5 System 13 emits `building.status.changed` and the batch-grain status row only when a - public status actually changes, and never applies cleanliness or wear — that is - [W83](#w83)'s system, and a test asserts system 13 leaves both meters untouched. - - W82.6 `advance_ticks n` serializes byte-identically to any split of that batch across a - stock-out and refill. - - W82.7 The two committed world-graph replay outcomes are unchanged byte-for-byte — the MVP - campaign declares no capacity — and the new behaviour is proved by a new committed - fixture; any outcome that does move follows - [07 §7](07-replay.md#7-intended-change-versus-regression). - - W82.8 `npm run typecheck`, `npm run lint` and `npm test` pass from `src/engine/`. -- **Out of scope:** production operations other than service and restock, which - [12 §4](12-world-graph-kind.md#4-the-turn-is-a-tick-batch) §4.15 marks post-MVP no-ops by - name; supply chains, warehouses or delivery; pricing behaviour, which `set_price` already - owns; cleanliness and wear ([W83](#w83)). - -### [x] W83 — Buildings Get Dirty, Wear Out, and Break {#w83} - -**Delivers:** A resort that is left unattended degrades, and one that is looked after does not. -Buildings get dirty from use and from litter, wear down over time, and eventually break and stop -serving — which is the whole reason a player hires cleaners and the reason quality affects where -guests choose to go. Today cleanliness moves only from unresolved litter, and wear never moves at -all, so a resort is exactly as good on its thousandth tick as on its first. - -This is the system the register singles out as the one most likely to be mistaken for finished: -it looks implemented, applies one of its five delta sources, and never touches the meter the -system is half named after. -- **Spec:** [12 §4](12-world-graph-kind.md#4-the-turn-is-a-tick-batch) §4.16, and §4.2 for the - typed scratch deltas systems 1, 4 and 11 hand it; - [§10](12-world-graph-kind.md#10-projection); - [§12](12-world-graph-kind.md#12-events); - [§13](12-world-graph-kind.md#13-statechange-at-batch-grain). -- **Touches:** `src/engine/src/kinds/world-graph/tick/pipeline.ts`; - `src/engine/src/kinds/world-graph/tick/scratch.ts`; - `src/engine/src/kinds/world-graph/kind.ts`; `src/engine/src/campaigns/world-graph-mvp.ts`; - a new fixture pair under `src/engine/fixtures/replay/`. -- **Depends on:** nothing — it shares no code path with [W81](#w81) or [W82](#w82) and may be - taken before either. It is placed here because [W84](#w84) depends on it. -- **Status:** Done — [PR #348](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/348). -- **Done when:** - - W83.1 For one building receiving deltas from every source in the same tick, the five sources - are applied in the contract's stated order — service, litter, incident, staff, policy — - summed, then clamped once to 0..100. Asserted with a case where applying the same set in a - different order, or clamping between sources, would give a different result, so the test - distinguishes the rule from an equivalent-looking one. - - W83.2 Wear moves, and wear reaching zero changes an open or closed building to broken; a - broken building serves nobody and is not a candidate. Cleanliness reaching zero never - closes a building on its own — asserted as a negative case, since that is the rule most - likely to be added by intuition. - - W83.3 Cleaning increments the cleaned-litter counter by the amount actually removed, - independently of any definition effect, and an incident amount reaching zero resolves the - occurrence with its transition effects run exactly once — asserted by a repeated tick that - must not run them twice. - - W83.4 `building.meter.changed` is emitted and declared; the batch-grain audit carries status - transitions and does not carry per-tick meter steps, asserted by the returned row count - over a long batch. - - W83.5 `advance_ticks n` serializes byte-identically to any split of that batch across a - degradation-to-broken sequence. - - W83.6 The two committed world-graph replay outcomes either stay byte-identical or the change - is recorded as intended per [07 §7](07-replay.md#7-intended-change-versus-regression) with - the differing fields named in the pull request. Unlike [W81](#w81) and [W82](#w82), this - one is not predicted either way: the MVP campaign already has a litter incident, so this - unit may legitimately move an existing outcome. - - W83.7 `npm run typecheck`, `npm run lint` and `npm test` pass from `src/engine/`. -- **Out of scope:** repair as a player action or a staff task kind — the contract's task kinds are - service, clean, restock and build, and adding a fifth is a contract amendment; balance values - for the drift rates, which are content and belong with Sun Trap; guest opinion changes driven - by quality, which the utility model already reads. - -### [x] W84 — Incidents That Happen On Their Own {#w84} - -**Delivers:** Things go wrong in the resort without the player causing them — a breakdown, a -spill, a security problem — and the player has to deal with them. Today the only incident that can -ever occur is litter dropped by a guest being served: everything an author writes about weather, -fires, breakdowns or security is content the game will never use. - -This is the one unit of the five that adds a new random draw, so it is where the determinism rules -get their real test: one handle per system per tick, a draw for every declared scope whether or not -it succeeds, and a rejected candidate consuming nothing. -- **Spec:** [12 §4](12-world-graph-kind.md#4-the-turn-is-a-tick-batch) §4.18; - [§9](12-world-graph-kind.md#9-determinism-beyond-the-seed); - [§12](12-world-graph-kind.md#12-events); - [§13](12-world-graph-kind.md#13-statechange-at-batch-grain); - [04 §8](04-core.md#8-randomness). -- **Touches:** `src/engine/src/kinds/world-graph/tick/pipeline.ts`; - `src/engine/src/kinds/world-graph/tick/random.ts`; - `src/engine/src/kinds/world-graph/kind.ts`; `src/engine/src/campaigns/world-graph-mvp.ts`; - a new fixture pair under `src/engine/fixtures/replay/`. -- **Depends on:** [W83](#w83), so a rolled incident has meters to act on and a resolution path that - is not litter-only. -- **Status:** Done — [PR #360](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/360). -- **Done when:** - - W84.1 An eligible definition rolls against its declared chance from the tick's incidents - handle, and a successful roll allocates an occurrence with its start effects applied - before system 17 runs — asserted with a seed where the roll succeeds and a seed where it - does not, both stated. - - W84.2 Scopes are visited world, then zone id, then building id, and every declared scope - consumes its draw whether or not it produces an occurrence — asserted by a case where a - scope with no eligible definition changes nothing and a *later* scope's outcome is - nonetheless identical to a run where that scope was eligible and failed. That is the - property a "skip empty scopes" implementation silently breaks. - - W84.3 An active occurrence blocks its definition in the same scope, and so does a retained - one still inside its cooldown; the block lifts on the exact tick the cooldown ends, stated - as a tick number. - - W84.4 An occurrence resolves when its resolution condition becomes true as well as when its - expiry passes, and its resolved tick is written before its resolve effects run, so it is - retained but not active for the rest of that list. - - W84.5 An incident started by an effect in an earlier system is not rolled again in the same - tick, asserted against the existing guest-litter path. - - W84.6 `incident.raised` is emitted and declared, alongside the already-delivered - `incident.resolved`. - - W84.7 `advance_ticks n` serializes byte-identically to any split of that batch across a batch - containing at least one successful and one failed roll — the case where a per-call handle, - rather than a per-tick one, would diverge. - - W84.8 The two committed world-graph replay outcomes are unchanged byte-for-byte — the MVP - campaign's only incident declares a zero roll chance and zero weight — and the new - behaviour is proved by a new committed fixture; any outcome that does move follows - [07 §7](07-replay.md#7-intended-change-versus-regression). - - W84.9 `npm run typecheck`, `npm run lint` and `npm test` pass from `src/engine/`. -- **Out of scope:** authored incident chains or incidents that spawn incidents; guest-visible - narrative around an incident, which is a client concern; alerts raised for an active - incident, which is [W85](#w85); balance values for chances and cooldowns, which are content. - -### [x] W85 — The Resort Tells You What Needs Attention {#w85} - -**Delivers:** A player is told when something needs them — a building has broken, an incident is -running, the scenario has resolved — instead of having to notice it in the numbers; and the -achievements a campaign author writes actually unlock. Today the alert list is a feature the -player can dismiss from and that nothing ever puts anything into, and a world-graph campaign's -achievements can never be earned no matter what the player does. - -This is the last of the five stub systems, and the only one that reaches outside the tick: an -unlocked achievement mirrors to the player's profile after the whole action succeeds, which is -core behaviour this kind has never exercised. -- **Spec:** [12 §4](12-world-graph-kind.md#4-the-turn-is-a-tick-batch) §4.21; - [§8](12-world-graph-kind.md#8-status-win-loss-and-terminal-identity); - [§10](12-world-graph-kind.md#10-projection); - [§12](12-world-graph-kind.md#12-events); - [§13](12-world-graph-kind.md#13-statechange-at-batch-grain); - [04 §7.1](04-core.md#71-the-profile-store); - [§12](04-core.md#12-reason-codes-state-changes-messages). -- **Touches:** `src/engine/src/kinds/world-graph/tick/pipeline.ts`; - `src/engine/src/kinds/world-graph/kind.ts`; - `src/engine/src/kinds/world-graph/reasons.ts` for the kind-owned alert strings; - `src/engine/src/campaigns/world-graph-mvp.ts`; a new fixture pair under - `src/engine/fixtures/replay/`. -- **Depends on:** [W83](#w83) — the broken-building alert family has no source at all until wear - can reach zero. [W84](#w84) is not required, since guest litter already raises incidents, - but is sequenced ahead so the incident family is proved against an incident the player did - not cause. -- **Status:** Done — [PR #361](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/361). -- **Done when:** - - W85.1 Still-locked achievements are evaluated by definition id against post-resolution state, - an unlock writes the achievement's existence row with the core `achievement_unlocked` - reason, and the profile mirror happens only after the whole action succeeds — asserted by - a run whose action is refused afterwards leaving the profile untouched. - - W85.2 Exactly three alert families exist — active incident, broken building, and scenario - resolved — each keyed on published ids only, with no player-facing or authored text in the - key. A test asserts a key built from a campaign's own strings would fail, so the rule is - checked rather than described. - - W85.3 A newly active source raises one alert; a source that is no longer active is marked - cleared; and a second tick with the same source still active raises no duplicate, - asserted across three consecutive ticks. - - W85.4 The two kind-owned alert families resolve their title and message through - `world-graph.alert..title|message` in the kind's built-in strings, validated with - the kind's own content, and an incident alert reuses its definition's name and description - keys instead. No alert resolves to a bare key. - - W85.5 No alert or achievement feeds another system: a test that drops the whole alert list - before system 20 leaves the rest of the tick's state identical. - - W85.6 `achievement.unlocked`, `alert.raised` and `alert.cleared` are emitted and declared, - with alert creation and removal audits hidden as the contract requires. - - W85.7 `advance_ticks n` serializes byte-identically to any split of that batch across a batch - that raises and clears an alert. - - W85.8 The two committed world-graph replay outcomes are unchanged byte-for-byte — the MVP - campaign declares no achievements — and the new behaviour is proved by a new committed - fixture that unlocks one; any outcome that does move follows - [07 §7](07-replay.md#7-intended-change-versus-regression). - - W85.9 The known-and-retained tick-system entry in `design/90-decisions.md` is closed out in - the same pull request, since this unit removes the last of the five it names. - - W85.10 `npm run typecheck`, `npm run lint` and `npm test` pass from `src/engine/`. -- **Out of scope:** alert presentation, ordering or grouping in a client; alert severity policy - beyond what the contract fixes; a fourth alert family, which would widen a closed key set - and is a contract amendment; cross-kind achievement listing, which - [issue #282](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/issues/282) already - holds as contract work. - -### Rigour: Two Event Streams Specified and Never Emitted - -> [05 §2](05-observability.md#2-the-determinism-rule)'s guarantee — that dropping every event -> changes nothing — is what makes a declared-but-unemitted event safe to leave unbuilt, and also -> what makes it easy to leave unbuilt indefinitely. Two of the three kinds have event tables whose -> status column is mostly *specified, not yet delivered*: six of ten rows in -> [03 §8.4](03-story-graph-kind.md#84-events), and, after W81–W85 deliver the rows their own -> systems own, the remainder of [12 §12](12-world-graph-kind.md#12-events). `simulation` is the -> counter-example, and the reason this reads as a gap rather than a policy: all nine of its -> declared events are emitted today. +**Status:** Delivery ledger. Nothing is in flight. All 125 W-numbered units have landed or been +cancelled, and each is retired to one row under [Landed](#landed). The next slice is planned as +`## S121`, above *Landed*, in the agent kit's slice format. It reads `Status: todo` until the pull +request that builds it sets `Status: done`. + +> **Where a retired unit's full text is.** Every W unit's body — what it delivers, its numbered +> acceptance criteria, its scope, and the programme prose that grouped units together — is in this +> ledger as it stood at +> [`7081402`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/blob/7081402c6dccc277b23250d44a22b022a16fd683/design/30-slices.md). +> Most units also have an execution record under +> [`plans/`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/tree/main/plans). Each row's +> anchor is the unit's id in lower case, so a link ending in `#w90` still lands on W90's row. > -> Both tables say in their own prose that nothing needs redeciding first — the names are fixed, -> the emit sites are named, and adding one is declaring it and emitting it. These two units are -> that, and nothing else. - -### [x] W86 — The Story-Graph Kind Says Why {#w86} - -**Delivers:** A campaign author can find out why a choice was refused or greyed out, and a -developer can find out why a replay diverged, without adding logging to the engine by hand. Today -the kind reports that a requirement was unmet and stops there, which is correct for the player and -useless for the person who wrote the requirement. - -[03 §8.4](03-story-graph-kind.md#84-events) fixes ten event names and the shipped kind declares -four. Its own prose singles out two of the missing six as carrying most of the value, for the two -audiences the channel exists to serve. -- **Spec:** [03 §8.4](03-story-graph-kind.md#84-events), and §8.2 for the step each event is - emitted at; [05 §9](05-observability.md#9-kind-events); - [§2](05-observability.md#2-the-determinism-rule); - [04 §3](04-core.md#3-the-kind-interface--the-seam). -- **Touches:** `src/engine/src/kinds/story-graph/advance.ts`, - `src/engine/src/kinds/story-graph/achievements.ts` and - `src/engine/src/kinds/story-graph/kind.ts`. - `src/engine/src/core/observability/` is **read, not modified**. -- **Depends on:** nothing. -- **Status:** Done — [PR #362](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/362). -- **Done when:** - - W86.1 All six remaining rows are emitted at the step the table names, with exactly the `data` - fields it lists and no others, and all ten are declared in `Kind.eventNames`. A test - enumerates the declared names against the emitted ones so a future row cannot be declared - without an emit site or emitted without being declared. - - W86.2 `requirement.evaluated` fires once per requirement rather than once per choice, - asserted on a choice carrying more than one requirement with the exact event count stated. - **The row's stated purpose does not survive its own `data` column, and that is a finding - to report rather than resolve here:** [03 §8.4](03-story-graph-kind.md#84-events) says a - compound condition "reports which clause failed", but the row declares `choiceId` and - `satisfied` only, which cannot name a clause. Emit what the table declares and state the - discrepancy in the pull request; widening the `data` column is a contract amendment. - - W86.3 Playing the committed Bureaucracy arc to its ending with a recording sink produces the - full expected sequence, asserted as an ordered list of names rather than a set, since - ordering is what makes the stream readable. - - W86.4 Dropping every event changes nothing: the same arc replayed with a null sink and with a - recording sink produces byte-identical `serialize()` output and identical `StateChange` - arrays. - - W86.5 Every committed story-graph replay outcome is unchanged byte-for-byte. - - W86.6 The status column in [03 §8.4](03-story-graph-kind.md#84-events) has no remaining - *specified, not yet delivered* row, and the paragraph explaining what that status means is - rewritten rather than left describing a state that no longer exists. - - W86.7 `npm run typecheck`, `npm run lint` and `npm test` pass from `src/engine/`. -- **Out of scope:** an author-facing presentation of these events, which - [05 §13](05-observability.md#13-what-is-deferred) routes to content tooling by name; new - event names; changing any severity the table fixes; the sink implementations, which already - ship. - -### [x] W87 — The World-Graph Kind Says What Its Ticks Did {#w87} - -**Delivers:** Someone diagnosing a resort that behaves oddly can watch what happened inside a tick -— which guests could not reach a building, which tasks were assigned and cancelled, what each -charge was for, how an objective moved — instead of inferring it from the state before and after. -Today the pipeline runs twenty systems and reports on six of them. - -The row this unit exists for is stated in the contract itself: a resort where guests silently -cannot reach a building looks identical to one where they simply do not want to, and the -difference is invisible in the projection and obvious in the stream. -- **Spec:** [12 §12](12-world-graph-kind.md#12-events), and - [§4](12-world-graph-kind.md#4-the-turn-is-a-tick-batch) for each named emit site; - [05 §9](05-observability.md#9-kind-events); - [§7](05-observability.md#7-severity-and-volume); - [§2](05-observability.md#2-the-determinism-rule). -- **Touches:** `src/engine/src/kinds/world-graph/tick/pipeline.ts`; - `src/engine/src/kinds/world-graph/kind.ts`. -- **Depends on:** [W81](#w81)–[W85](#w85), which each deliver the rows their own system owns. This - unit takes what is left rather than duplicating them. -- **Status:** Done — [PR #363](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/363). -- **Done when:** - - W87.1 Every row in [12 §12](12-world-graph-kind.md#12-events)'s table is emitted at the system - it names and declared in `Kind.eventNames`; the status column has no remaining *specified, - not yet delivered* row, and the paragraph explaining that status is rewritten rather than - left describing a state that no longer exists. - - W87.2 A test enumerates declared names against emitted ones in both directions, so a declared - name with no emit site and an emitted name that was never declared each fail. This is the - gate the *emitted → registered* gap in the contract's own reason-code section says is - missing everywhere it recurs. - - W87.3 Events emit in system order and then in the owning comparator order, asserted over a - multi-tick batch as an ordered list — the property that makes the stream a readable causal - trace rather than an unordered log. - - W87.4 Dropping every event changes nothing: the same batch run with a null sink and with a - recording sink produces byte-identical `serialize()` output and identical `StateChange` - arrays. - - W87.5 Severity assignment matches the table exactly, asserted per row, since severity is the - only thing standing between a host and the six-figure trace volume a large batch produces. - - W87.6 Every committed world-graph replay outcome is unchanged byte-for-byte. - - W87.7 `npm run typecheck`, `npm run lint` and `npm test` pass from `src/engine/`. -- **Out of scope:** new event names or severities; the optional candidate-generation diagnostic's - shape beyond what the table fixes; any sink, exporter or trace-context work, which - [05 §13](05-observability.md#13-what-is-deferred) defers by name; presenting the stream - anywhere. - ---- - -## Known Open Items Carried In - -> Full register of unknowns, gaps, and deferred decisions: +> The contracts the units were built against are unchanged: +> [`04-core.md`](04-core.md), [`03-story-graph-kind.md`](03-story-graph-kind.md), +> [`10-simulation-kind.md`](10-simulation-kind.md), [`12-world-graph-kind.md`](12-world-graph-kind.md) +> and [`05-observability.md`](05-observability.md). The register of unknowns is > [`OPEN-QUESTIONS.md`](OPEN-QUESTIONS.md). -> **The eight findings from the first downstream host are contract work, not slices.** -> [`OPEN-QUESTIONS.md`](OPEN-QUESTIONS.md) §2, *Found by the first downstream host*, indexes what -> SubZeroDev.Adventures turned up by implementing the ports outside a browser tab -> ([#266](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/issues/266), -> [#276](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/issues/276)–[#282](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/issues/282)). -> They are deliberately **not** sliced here. Every one of them either asks for a signature the -> contract does not have — a player-keyed save query, a fork operation, an async -> `listCampaigns`, a `min`/`max` on `VisibleStat`, a cross-kind-legible `Kind.outcome` — or asks -> the contract to *state* a decision it currently leaves implicit, which is not something a -> vertical slice can deliver at all. Slicing one would mean inventing the signature first, and -> that is exactly the failure the "no slice may introduce a signature absent from the contract" -> rule exists to prevent. **Route to `/contract`**; they become sliceable the moment the seam is -> settled, and several are small once it is. - - -- [x] **`SessionHost`/`createSessionLayer` ([06 §4](06-extensibility.md#4-the-composition-root)) don't reconcile as written — closed.** - Resolved by drawing the seam one level down at `SessionPersistence` - ([04 §7.2](04-core.md#72-host-persistence--the-record-store-beneath-the-session-store)); - `createSessionLayer` now ships and is exported. `OPEN-QUESTIONS.md` §2 records the - closure and the reason its original "revisit when" was the wrong trigger. The replay - oracle still drives `Engine` directly, but for its own stated reason — it needs the raw - `GameState` no `SessionStore` returns ([07 §3.2](07-replay.md)) — not for want of a root. -- [x] **`wisdom` has a consumer — closed.** [W100](#w100)'s 2026-08-31 decision made the - generic attribute `Requirement` path its consumer, and W100.5 proved two otherwise - identical actors receive different available behaviour. Confirmed by `/align` on #275. -- [x] **Four simulation `ActionType`s have no content-definition type to resolve against — closed.** - [§7.12](10-simulation-kind.md#712-projects-and-businesses-w101) declares `ProjectDefinition` and - `BusinessDefinition`, and [W101](#w101) built the four resolvers against them. As first found: - `start_project`, `work_on_project`, `start_business` and `operate_business` are members - of [10 §4.2](10-simulation-kind.md#42-action-types)'s closed union, so `ResolverTable`'s - completeness check compels a resolver for each — but - [§7](10-simulation-kind.md#7-content-definition-types) declares no `ProjectDefinition` or - `BusinessDefinition`, and nothing in §7.2–§7.10 covers them. They are the only four of - the thirty that no unit W53–W57 can implement. **Revisit when** a scenario needs one: - either §7 gains the definitions or the union loses the members, and deciding that is - `/contract`'s work, not a slice's. Found while slicing W53–W57. -- [x] **The simulation `relationships` end-of-week system has no rule to implement — closed.** - [§7.11](10-simulation-kind.md#711-the-campaign-envelope-and-weekly-tuning)'s `relationshipDrift` is the rule, - and [W100](#w100).3 built the system against it. As first found: - [10 §3](10-simulation-kind.md#3-the-turn-is-a-week) names it in `END_WEEK_SYSTEM_ORDER` - and [§6.11](10-simulation-kind.md#611-relationships) gives `RelationshipState` its full - shape, but no weekly movement rule for it is stated anywhere in the contract — the stub's - own comment in `src/engine/src/kinds/simulation/endOfWeek.ts` has said so since W37. - W56 builds `socialize` against the shape and leaves the system a documented stub for - exactly this reason. **Revisit when** the rule is written, which is `/contract`'s work. -- [ ] Provisional numbers across the simulation kind (drift rates, scenario economics, - `demandBand` thresholds, housing-quality formula) are placeholder balance. **Balancing is - game-side**; the engine owes the levers, not the numbers (`90-decisions.md`, 2026-09-27, - `/align` on #267). The levers are [§7.14](10-simulation-kind.md#714-remaining-campaign-physics-524), - sliced as [W117](#w117)–[W119](#w119). Travel costs are already content-owned - (`LocationDefinition.travelTimeUnits`) and were never a lever to add. -- [ ] **Three `docs-template` hardening findings, to raise upstream — after this PR - merges, not before.** Surfaced by automated review on - [PR #3](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/3); all three - sit in files installed verbatim from `ghcr.io/the-running-dev/docs-template` (not - authored in this repo), and this repo's own W0 decision is to never hand-edit - installer-owned files — so the fix belongs in the `docs-template` project, filed as - a separate PR there once this one is settled: - - `docs-ci.yml` / `docs-deploy.yml` pin `ghcr.io/the-running-dev/docs-template:latest`, - a mutable tag — non-reproducible, silent behaviour drift possible on future runs. - Checked whether the installer's own `-BaseImage` avoids this without an upstream - change: it doesn't — repinning an already-installed file needs `-Overwrite`, which - would also replace this repo's five preserved local files (`docusaurus.config.ts`, - `sidebar.ts`, `Dockerfile`, `.dockerignore`, `docs.ps1`). A fix needs a pin mechanism - scoped to just the docs workflows, independent of `-Overwrite`. - - `build/Test-Documentation.ps1`'s link validator resolves relative targets with - `Join-Path` + `GetFullPath` and only checks `Test-Path`, without constraining the - result to stay under `$Root` — a `../../` link can resolve outside the repository and - still "pass." Not currently exploitable here (no `../`-style links exist in this - repo's docs today); a validator-correctness gap, not a live defect. - - Same script's file enumeration (`Get-DocumentationFile`) recurses every directory - before applying `ExcludedSegments`, so excluded trees (`.git`, `node_modules`) are - still walked. Performance only, tagged "Optional" by the reviewer. - - Full findings, the verification behind declining each in this repo, and the reply - text posted on each review thread: PR #3, review comments - [1](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/3#discussion_r3660515997), - [2](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/3#discussion_r3660516002), - [3](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/3#discussion_r3660516006). - ---- - -## Outstanding - -**One question: what stands between the `simulation` kind as it runs today and Life in the -Fast Lane being playable end to end.** The answer is not what the earlier programme headers -would suggest. W36–W40 built the kind, W50–W57 built the behaviour behind it, and the -mechanics are very nearly whole — twenty-seven of thirty-one -[§4.2](10-simulation-kind.md#42-action-types) `ActionType`s have a real resolver, fourteen of -fifteen [§3](10-simulation-kind.md#3-the-turn-is-a-week) end-of-week systems have real bodies, -[§9](10-simulation-kind.md#9-projection)'s projection is built, and the kind is proven through -the replay oracle, the text client and MCP. What is missing sits on either side of the -mechanics: **nothing outside the package can author or type a simulation campaign, and -nothing has ever run one for longer than three weeks.** - -Two units below close those two. Everything else Life in the Fast Lane still needs is a -contract decision, listed first so it is not mistaken for work a slice can pick up. - -### Contract Prerequisites — Not Sliceable, Route to `/contract` - -Named here in the shape *Known Open Items Carried In* uses for the eight downstream-host -findings, and for the same reason: each either asks for a type the contract does not declare -or asks it to state a rule it currently leaves unwritten, and slicing one would mean inventing -the answer first. Several are already carried above and are repeated only because they sit on -this path; the rest are new to this pass. - -**Status, 2026-10-03.** Every prerequisite below has since been contracted. P1 and P4 -were settled by [W101](#w101) (§7.12's project and business definitions; §7.8's `RivalConfig`). -P2, P5 and P6's `attendanceRatio` were settled by [W100](#w100) (§7.11's `relationshipDrift`, -`emptyPlanPolicy` and `attendanceTracking`), and P6's `wisdom` by W100's generic attribute -`Requirement` path (#275). P7 was settled by [W102](#w102) (§16; the portable simulation arm -carries `migration`). P3 is contracted as §7.14 and sliced as [W117](#w117)–[W119](#w119). P8 -is contracted as three rule-family codes in §7.14 and §10: `invalid_tuning_value`, -`unknown_tuning_key` and `invalid_eviction_stages`. §10 now also lists -`invalid_attendance_window` (`90-decisions.md`, 2026-10-03). Where W117–W119 say "P8's code", -read the §7.14 code for the rule being tested. The bullets below are kept as found. - -- **P1 — Four `ActionType`s have no content-definition type.** Carried above. - `start_project`, `work_on_project`, `start_business` and `operate_business` are members of - [§4.2](10-simulation-kind.md#42-action-types)'s closed union and - [§7](10-simulation-kind.md#7-content-definition-types) declares nothing for them to resolve - against. **What has not been recorded is the live consequence**, and it is worse than an - unbuilt feature: `ResolverTable`'s completeness check forced a resolver for each, so all - four are `stubResolver` — always valid, always a neutral no-op — and `view.ts` derives - `SimulationView.plan.availableActionTypes` from the whole union minus `"custom"`. **Every - client is therefore offering four actions that accept the plan, consume no time, cost no - money and change nothing.** A life sim in which starting a business is a progression path - cannot ship that way. The decision is whether - [§7](10-simulation-kind.md#7-content-definition-types) gains `ProjectDefinition` and - `BusinessDefinition` or §4.2's union loses the four members; which way it goes decides - whether the follow-on unit builds two content types or narrows a projection field, so - neither side can be sliced first. -- **P2 — The `relationships` end-of-week system has no rule to implement.** Carried above, - unchanged since W37: [§6.11](10-simulation-kind.md#611-relationships) gives - `RelationshipState` its full shape and - [§7.7](10-simulation-kind.md#77-npcs--definition-and-runtime-state) the NPC, and nothing - states what a week does to either. It is the last stub left in §3's order. -- **P3 — A campaign has no way to set its own numbers.** `SimulationCampaign` - (`src/engine/src/kinds/simulation/campaign.ts`) declares seventeen content collections, a - scenario id, a precedence flag, optional starting effects and three label keys — and **not - one tuning value**. The weekly time budget, every need's drift rate, the late-fee rate, the - eviction ladder, performance drift and its work bonus, the strangeness step, and every - action's own time cost and restore amount are `const`s in `initial.ts`, `endOfWeek.ts` and - `resolvers.ts`. A campaign cannot move any of them, so Life in the Fast Lane would be played - at *Stable Life's* physics — a fixture's provisional balance, not a game's. This is the - deeper reading of the *provisional numbers* item carried above: the problem is not that the - numbers are unbalanced, it is that they sit where no campaign can reach them. A tuning block - on `SimulationCampaign` is a new public interface and needs the amendment rather than a - slice. **Note what this is not:** [12 §15](12-world-graph-kind.md#15-validation) assigns - balance *findings* to a game-side harness, and that stands — this is about where the levers - live, not about who turns them. -- **P3 contract gate (#524, 2026-09-28):** §7.14 fixes the optional flat campaign fields, - per-key defaults, validation, source-builder omission behavior and a six-stage default - eviction ladder. Implementation remains open. The smallest first slice wires - `needDriftPerWeek` through source, validator and weekly system and proves two otherwise - identical campaigns diverge with `satiety: -4` versus `-5` under the same seed and action - log, including repeat/save-load determinism; omitted tuning remains byte-identical to - committed replay and golden oracles. Subsequent slices wire the remaining §7.14 fields. -- **P4 — Rival agents.** - [§7.10](10-simulation-kind.md#710-agents--engine-owned-strategy-definition-and-runtime-state) - states outright that how a scenario configures rivals is an open gap; no `ScenarioDefinition` - field names them. `PublicWorldState` was declared by [W50](#w50) so `AgentStrategy` could - typecheck and is exercised by nothing. [W57](#w57) put agents out of scope for exactly this - reason and nothing has changed since. -- **P5 — `plan_empty` has no field to condition on.** - [§10](10-simulation-kind.md#10-reason-codes) specifies it for "`end_week` with nothing - planned, where the campaign forbids it", and no campaign can express forbidding it — so - `available.ts` documents the gate as deliberately unwired rather than writing dead code. One - of the two §10 codes still undispatched. -- **P6 — `Employment.attendanceRatio` and `wisdom` are declared and read by nothing.** Both - carried above; they are the same shape and want the same kind of answer. - `attendanceRatio` ([§6.8](10-simulation-kind.md#68-career)) is set to `100` at hire and never - moved, so anything built on it reads a perfect record forever; `wisdom` - ([§6.6](10-simulation-kind.md#66-attributes)) is resolved through the derived-value layer and - consulted by no resolver or system. Each needs a mechanism decision — what a missed work week - *is*, and what wisdom changes — before it is a field to fill in. -- **P7 — A published simulation campaign cannot carry a save migration.** `PortableMigration` - is `nodeMap`/`endingMap` and sits on the story-graph arm of `PortableCampaignBody` by - construction, which `src/engine/src/portable/format.ts` documents as a deliberate structural - fact rather than an omission. Correct for the format as it stands, and a real ceiling for a - long-running life sim: once the game is published as portable JSON, no content revision can - migrate an existing save. **This does not block first play** — it blocks revising a live - game — so it is last here rather than first; it is also the one prerequisite carrying a - wire-format cost. -- **P8 — §7.14's Tier 1 rejection has no reason code.** - [§7.14](10-simulation-kind.md#714-remaining-campaign-physics-524) says Tier 1 rejects an invalid - physics value or stage sequence "with a path identifying the field/key", but - [§10](10-simulation-kind.md#10-reason-codes)'s Tier 1 table names no code for it. The one - precedent does not settle it either: W100 shipped `invalid_attendance_window` for §7.11's - window, and §10 does not list that code. One shared code, one per field, or one per rule - family is a choice of public, cross-version-stable vocabulary, so a slice cannot pick it. - Found while slicing [W117](#w117); it blocks W117–W119 and nothing else. The same pass should - either list `invalid_attendance_window` in §10 or record why it stays unlisted. - -### Depth: Life in the Fast Lane, End to End - -Two units. **W88 first, because nobody can start without it**: the kind's authoring surface is -not exported, so the content repository that would own the campaign cannot build one, and a -host that fetched it could not type what it renders. **W89 second, because nothing has been -tested at the scale the game is played at**: every simulation fixture in the corpus is two or -three weeks long, and a life sim is measured in years. - -### [x] W88 — The Simulation Kind Nobody Outside the Package Can Use {#w88} - -**Delivers:** Lets a content repository actually write Life in the Fast Lane, and lets a host -render it. Today the simulation kind is one you can register and cannot author: the package -exports `simulationKind` and nothing else about it — not the campaign builder, not the source -types it takes, not the campaign, state, view or outcome types a host must compile against. -`world-graph` has that whole set at the package root and `story-graph` has its builder and -source types on the `/authoring` subpath; `simulation` has neither, and nothing records the -absence as a decision. - -**The gap is exports, not code.** `buildSimulationCampaign` and `SimulationCampaignSource` have -existed in `src/engine/src/kinds/simulation/source.ts` since [W52](#w52), and `SimulationView`, -`PublicWorldState` and `SimulationOutcome` since [W50](#w50); every campaign under -`src/engine/src/campaigns/` reaches them by relative import, which is exactly why nothing -caught it. The packed tarball is the only place the omission is visible, and `consumer-smoke/` -has never asked for a simulation type. - -**This unit amends [20 §19](04-core.md#19-published-narrative-authoring)'s export list in the -same change that implements it**, the precedent [W50](#w50) took from [W48](#w48). That is -deliberate and narrow: §19's *rule* is settled — the root is the runtime contract, the subpath -is the author-time contract for repositories that own campaign source — and what is missing is -that its list names the story-graph builder and not the simulation one. If drafting turns out -to need a decision rather than a list entry, that is `/contract`'s call, not this unit's. - -**The placement question has a defensible answer and this unit states it rather than absorbs -it.** The two existing kinds disagree: `WorldGraphKindState`, `WorldGraphView`, -`WorldGraphOutcome`, `WorldGraphCampaign` *and* `buildWorldGraphCampaign` are all root exports, -while `StoryGraphCampaign` and `StoryGraphKindState` are `/authoring`-only. §19 resolves it by -kind of *thing* rather than by kind: **the builder and its source types are author-time and go -to `/authoring`; the campaign, state, view and outcome types are what a runtime host compiles -against and go to the root.** That splits the difference the two precedents straddle without -disturbing either — `buildWorldGraphCampaign`'s root placement predates the subpath and is -noted here rather than moved. -- **Spec:** [20 §19](04-core.md#19-published-narrative-authoring) (the root is the runtime - contract, the subpath the author-time one); - [10 §13](02-architecture.md#13-published-narrative-content-ownership) (published campaign - builders are not package-root API — a *builder* is not a published campaign); - [10 §9](10-simulation-kind.md#9-projection) (`SimulationView`, what a host renders); - [§7](10-simulation-kind.md#7-content-definition-types) (what the source types cover); - [§12](10-simulation-kind.md#12-terminal-identity) (`SimulationOutcome`). -- **Touches:** `src/engine/src/authoring.ts` and its committed sorted export list - ([W74.7](#w74)); `src/engine/src/index.ts`; `consumer-smoke/smoke.ts`; - `design/20-contract.md` (the `engine/04-core.md` block, §19). -- **Depends on:** [W57](#w57), the behaviour these types describe, and [W74c](#w74c), which - fixed what the root surface is for. Not on [W89](#w89). -- **Status:** Complete — [PR #367](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/367), merged at `8115478`. The root/runtime and `/authoring` export split is covered by the packed-tarball consumer smoke. -- **Done when:** - - W88.1 `buildSimulationCampaign` and every `*Source` type - `src/engine/src/kinds/simulation/source.ts` declares are exported from - `@the-running-dev/game-engine/authoring`; `SimulationCampaign`, `SimulationKindState`, - `SimulationView`, `PublicWorldState`, `SimulationOutcome`, `ActionType`, `GameAction` - and `WeeklyActionPlan` are exported from the package root. - - W88.2 `consumer-smoke` builds a simulation campaign from source through the subpath and - types a `SimulationView` through the root, **resolving both from the packed tarball** - rather than a source link — the bar [W74.6](#w74) already set. Deleting one of the new - exports fails the smoke build, verified by deleting one. - - W88.3 The subpath stays closed against published content: W74.7's enumeration check passes - with the new names in its committed list, and no `stable-life*` or - `bulgaria-stable-life` builder or campaign id becomes reachable through either surface. - - W88.4 §19's export sentence names the simulation builder and source types and states the - author-time/runtime split this unit applies, so the next kind has a rule to follow - rather than two precedents to choose between. - - W88.5 A test asserts the root exports no simulation *authoring* value — the direction - W74c's boundary check does not cover, since it enumerates the subpath and not the root. - - W88.6 `npm run typecheck`, `npm run lint` and `npm test` pass from `src/engine/`; - `install:engine`, `build` and `smoke` pass from `consumer-smoke/`; - `./build/Test-Documentation.ps1` passes. -- **Out of scope:** moving `buildWorldGraphCampaign` or the story-graph types to match — a - breaking change to two settled surfaces for consistency alone, which is [W74c](#w74c)'s - kind of unit and wants its own decision; exporting any campaign, which §19 forbids and - W74c removed; and every prerequisite above — this unit exports what already exists and - adds no behaviour. - ---- - -### [x] W89 — A Game-Length Life {#w89} - -**Delivers:** The first evidence that this kind survives being played as a game rather than as -a test. **The longest simulation run committed anywhere in this repository is three weeks** — -`stable-life-win` is three `end_week` calls, and all twelve simulation fixtures under -`src/engine/fixtures/replay/` are two or three. Each was built to prove one system, which is -what W53–W57 needed and is not what Life in the Fast Lane is: a scenario measured in years, -where the same fifteen systems run two hundred times over accumulating state. - -**What a long run exercises that a short one cannot** is why this is a unit rather than a -bigger fixture appended to an existing one. Three weeks never reaches the end of the eviction -ladder; never completes a course, let alone a promotion chain whose `minimumWeeksInRole` gates -re-measure from each promotion; never accumulates enough expired `activeEffects`, resolved -`PendingEventResponse`s or achievement counters to say anything about growth in the serialized -state; never exhausts an event or opportunity pool and finds out what the systems do when one -is empty; and never draws enough from the weekly RNG substreams to make substream independence -([§13](10-simulation-kind.md#13-determinism)) an observed property rather than an argued one. -Each of those is a way a game-length run can diverge or wedge while every current fixture stays -green. - -**The fixture is engine-owned and unpublished, which -[20 §19](04-core.md#19-published-narrative-authoring) permits explicitly** — "GameEngine may -retain a frozen campaign solely as a regression fixture; such a fixture is not published and -not listed in a manifest." It is regression evidence in the shape [W74a](#w74a) froze -Bureaucracy into, not a second game, and it must not become one: it needs enough content to -sustain a long run and no more. It is emphatically not Life in the Fast Lane, whose content -lives in -[SubZeroDev.GameOfLife](https://github.com/The-Running-Dev/SubZeroDev.GameOfLife). - -**Sized on the argument that the action log is generated, not authored.** A scenario with a -long horizon plus a scripted weekly policy driving the plan is one session's work; -hand-writing two hundred weeks of `plan.add` calls is not, and a unit reaching for the second -has been mis-sized rather than under-resourced. -- **Spec:** [`07-replay.md`](07-replay.md) §4, §6, §7 (the corpus and the runner's verdicts); - [04 §14](04-core.md#14-determinism-harness) (a build against itself); - [10 §3](10-simulation-kind.md#3-the-turn-is-a-week) (the two orderings a long run re-runs - two hundred times), [§13](10-simulation-kind.md#13-determinism) (substreams), - [§12](10-simulation-kind.md#12-terminal-identity); - [20 §19](04-core.md#19-published-narrative-authoring) (a retained fixture is not a - publication). -- **Touches:** a new long-horizon fixture campaign under `src/engine/src/campaigns/`; new - fixture and outcome pairs under `src/engine/fixtures/replay/`; - `src/engine/src/campaigns/replay-corpus.ts`. -- **Depends on:** [W57](#w57). Independent of [W88](#w88) — the two may land in either order. -- **Status:** Complete — [PR #376](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/376), merged at `71e7364`. The paired 150+ week replay fixtures prove deterministic win/loss paths and expose the retained pending-response and relationship-rule follow-up work. -- **Done when:** - - W89.1 A committed fixture plays at least one hundred and fifty weeks to a terminal - `outcome()` and is in the replay corpus, its outcome frozen the way every other corpus - entry is. - - W89.2 Both a win and a loss at that length are committed, and the loss reaches the end of - the eviction ladder rather than ending on a goal-failure condition — the terminal path - no short fixture has ever walked. - - W89.3 Every one of the twenty-seven dispatched `ActionType`s resolves at least once across - the committed long runs, asserted by counting resolved action types against - `RESOLVER_TABLE`'s non-stub entries, so the count moves on its own when P1 is answered. - - W89.4 Each of the fifteen [§3](10-simulation-kind.md#3-the-turn-is-a-week) end-of-week - systems is observed doing something at least once over the run — a `system.ran` event - alone does not count, since a stub emits one too. - - W89.5 The run is byte-identical when replayed, and byte-identical when the same weeks are - played as one session and as a save/restore split partway through, which is what proves - nothing accumulated outside the serialized state. - - W89.6 What the run's final state costs is asserted, not printed: serialized size and the - count of each unbounded collection at week one against the final week, committed with a - stated ceiling. A collection that grows every week is a defect this unit is the first - thing able to see. - - W89.7 Every existing committed replay outcome is unchanged byte-for-byte; this unit adds - fixtures and changes no behaviour. - - W89.8 `npm run typecheck`, `npm run lint` and `npm test` pass from `src/engine/`. -- **Out of scope:** fixing anything the run exposes — a defect found here is its own unit, and - the whole value of this one is being the first thing capable of finding one; any balance - judgement about the run's numbers, which - [12 §15](12-world-graph-kind.md#15-validation) assigns to a game-side harness and which - P3 shows the engine cannot host anyway; Tier 3 validation for this kind, which - [W73](#w73) put out of scope by name and which needs its state space argued before it is - built; and wiring a long run into CI as a required gate, a policy change with a runtime - cost that wants its own decision. ---- - -## Stabilization Programme - -The W88/W89 reconciliation closes the existing defined programme. The following units are -the additive `0.11.0` programme; W90–W92 make `0.10.0` honest and releasable first. - -**`0.10.0` was tagged and never published.** Its release run failed in 11 seconds on a shell -syntax error inside the tag/version guard, before any publish step, so the registry still holds -`0.4.0`, `0.5.0` and `0.8.0` only. W90–W92 made the package reproducible, which is not the same -as shipped — and the failure is why [W108](#w108)'s W108.5 requires those guards to be -executable without pushing a tag. - -### [x] W90 — Canonical and Public Truth {#w90} - -**Delivers:** A canonical ledger, generated documentation, README, landing page, and roadmap -that describe the three shipped kinds, completed W88/W89 work, the retired in-repository play -surface, and the ecosystem ownership boundary without rewriting historical evidence. - -**Status:** Complete — [PR #377](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/377). - -### [x] W91 — Reproducible Package and Release Gate {#w91} - -**Delivers:** A clean build/pack path, tarball-content assertion, and tag/version-checked -release workflow that runs the engine and packed-consumer checks before publishing `0.10.0`. - -**Status:** Complete — the release workflow packs from a clean build, rejects excluded archive -content, and installs that same archive into a lockfile-backed consumer smoke before publish. - -### [x] W92 — Local and CI Gate Health {#w92} - -**Delivers:** A single-pass documentation drift check, repaired Pester bootstrap, cached Alpine -Chromium, and a responsive shared header covered at phone and desktop widths. - -**Status:** Complete — documentation validation retains its full generated-file, link, anchor, -and terminology coverage in a 46-second local run; the remaining CI/bootstrap/cache/header -work shipped in [PR #377](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/377). - -### [x] W93 — World-Graph Spatial and Audit Correctness {#w93} - -**Delivers:** World-graph players can demolish, place scenery, and reassign staff without the -engine choosing the wrong route, accepting invalid geometry, or reporting changes that did not happen. - -- **Spec:** [12 §3.3](12-world-graph-kind.md#33-structural-answers-and-what-remains-the-games), - [§6](12-world-graph-kind.md#6-actions--one-model-spatial-verbs), - [§9.3](12-world-graph-kind.md#93-canonical-shortest-path), - [§13](12-world-graph-kind.md#13-statechange-at-batch-grain), and - [§15](12-world-graph-kind.md#15-validation). -- **Touches:** `src/engine/src/kinds/world-graph/` — `actions/build.ts`, - `actions/staff.ts`, `initial.ts`, `spatial.ts`, `validate.ts`, `content.ts`, - `source.ts`, `tick/effects.ts`, and focused tests beside those seams. -- **Depends on:** nothing beyond the shipped world-graph kind. -- **Status:** Complete — [PR #398](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/398), merged at `f0735fc`. -- **Done when:** - - W93.1 Demolishing a building sends each displaced guest to the reachable exit with the - lowest canonical path cost from that guest's position; equal-cost exits use the - row-major tie-break from §9.3, and a test proves a nearer exit other than - `map.exits[0]` wins. - - W93.2 Scenario scenery placements are rejected at Tier 1 when their rotated footprint is - out of bounds or overlaps another placed footprint, using the same rotation and - occupancy rules as runtime building placement; the finding names the exact - `sceneryPlacements[n]` path. - - W93.3 `assign_staff` returns a `StateChange` only for an assignment field whose value - changed. Changing only the building emits no unchanged zone row, changing only the - zone emits no unchanged building row, and submitting the current pair emits neither. - - W93.4 Direct tests cover the public behaviour of `validate.ts`, `content.ts`, `source.ts`, - and deferred/non-deferred effect application in `tick/effects.ts`; deleting any one - of those suites makes the engine test gate fail. - - W93.5 Every added spatial fixture is deterministic across two runs and across a - serialize/deserialize cut immediately before the corrected operation. - - W93.6 `npm run typecheck`, `npm run lint` and `npm test` pass from `src/engine/` with every - previously committed replay outcome unchanged. -- **Out of scope:** changing canonical-path cost or tie-breaking, adding new placement rules, - adding scenery build/demolish actions, or broadening `StateChange` beyond the scalar-path - contract. - -### [x] W94 — Simulation Resolution Correctness {#w94} - -**Delivers:** Simulation players must answer mandatory events, can buy every shop-reachable item, -and compete for finite jobs without long campaigns accumulating unstable state. - -- **Spec:** [10 §2.3](10-simulation-kind.md#23-effects-opportunities-and-scheduled-events), - [§3](10-simulation-kind.md#3-the-turn-is-a-week), - [§7.2](10-simulation-kind.md#72-jobs), - [§7.5](10-simulation-kind.md#75-items), and - [§14](10-simulation-kind.md#14-validation). -- **Touches:** `src/engine/src/kinds/simulation/` — `advance.ts`, `availableActions.ts`, - `resolvers.ts`, `endOfWeek.ts`, `validate.ts`, and focused tests; the Stable Life - long-horizon campaign and replay fixtures. -- **Depends on:** [W89](#w89), whose long run supplies the bounded-state baseline. The - job-opening scarcity criterion lands with or after [W101](#w101)'s rival contract so - the player and rivals cannot acquire different meanings for the same finite count. -- **Status:** Complete — [PR #400](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/400), merged at `e103de6`. -- **Done when:** - - W94.1 While `pendingResponses` contains a mandatory event response, the projection offers - only the response choices that event permits; `end_week` and every unrelated action - are unavailable and reject without changing serialized state. - - W94.2 Resolving the pending event removes exactly that response, applies its selected - outcome once, and permits ordinary planning again only when no other mandatory - response remains; save/load between presentation and response produces the same result. - - W94.3 `validateUnreachableItems` treats an item as reachable when at least one reachable - location permits `shop`; a shop-only item produces no Tier-2 warning, while an item - reachable through neither starting inventory nor a shop still does. - - W94.4 Filling one position in a finite job opening decrements - `positionsAvailable` from two to one and keeps the opening available; filling its last - position retires it. The same transition is used regardless of whether the successful - applicant is the player or a scripted rival. - - W94.5 The W89 win and loss paths run for at least 150 weeks without bypassing a pending - response, with the final serialized-size and unbounded-collection ceilings asserted - rather than logged. - - W94.6 Each long run is byte-identical on repeat and when split by a save/restore boundary, - and every existing replay outcome remains byte-identical. - - W94.7 `npm run typecheck`, `npm run lint` and `npm test` pass from `src/engine/`. -- **Out of scope:** balance-tuning the long run, defining rival strategies, changing event - content, or adding a new item-acquisition action beyond the shipped `shop` path. - -### [x] W95 — Effect and Audit Semantics {#w95} - -**Delivers:** Players and observers see status effects, world-meter changes, and audit reasons -that describe exactly what the engine applied—once, and only when state really changed. - -- **Spec:** [10 §2.3](10-simulation-kind.md#23-effects-opportunities-and-scheduled-events), - [§6.1](10-simulation-kind.md#61-base-and-derived-values), - [§10](10-simulation-kind.md#10-reason-codes); - [12 §9.2](12-world-graph-kind.md#92-curves-multiplication-and-rounding), - [§11](12-world-graph-kind.md#11-reason-codes), and - [§12](12-world-graph-kind.md#12-events). -- **Touches:** simulation effect insertion and modifier tests; world-graph - `tick/effects.ts`, `tick/pipeline.ts`, and tests; the canonical audit-code tables in - `design/20-contract.md` and their generated human documentation. -- **Depends on:** a real status-effect insertion path from [W101](#w101) if none exists when - this unit starts; the deferred-meter and audit-table work is otherwise independent. -- **Status:** Complete — [PR #403](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/403) - (W95.1–3, 5–7) and [PR #404](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/404) - (W95.4), merged. -- **Done when:** - - W95.1 Every simulation code path that inserts a `StatusEffect` goes through one invariant: - a same-source `refresh` replaces the prior layer and resets expiry, a same-source - `stack` retains independent layers, and different sources always coexist. - - W95.2 Focused tests prove same-source refresh, same-source stack, different-source stack, - and save/load immediately after insertion; modifier totals and expiry weeks match the - persisted layers in every case. - - W95.3 A deferred world-graph building-meter effect is reported as applied only when the - final system-14 composition and clamp changes the target meter. Equal and opposing - same-tick deltas that net to zero emit no `scenario.effect.applied` for that effect. - - W95.4 A non-zero deferred result still emits the applied event once, and the result is - identical across every partition of the same tick batch. - - W95.5 The world-graph audit-code table contains all eleven shipped visible reasons, - including `building_broken`, with its exact producer and transport; the stated count - equals the table and the runtime registered set. - - W95.6 Canonical documentation is regenerated and `./build/Test-Documentation.ps1` passes. - - W95.7 `npm run typecheck`, `npm run lint` and `npm test` pass from `src/engine/`, with - existing replay outcomes unchanged. -- **Out of scope:** inventing a new effect family, changing meter aggregation or clamp order, - changing event severity, or solving the general emitted-to-registered gate delivered by - [W96](#w96). - -### [x] W96 — Mechanical Regression Boundaries {#w96} - -**Delivers:** Engine maintainers get required checks that catch determinism, ownership, -reason-registration, event-severity, or per-kind regression drift before it can ship. - -- **Spec:** [04 §3](04-core.md#3-the-kind-interface--the-seam), - [§12](04-core.md#12-reason-codes-state-changes-messages), - [§14](04-core.md#14-determinism-harness); - [05 §7](05-observability.md#7-severity-and-volume); - [12 §5](12-world-graph-kind.md#5-batch-invariance--and-the-two-seam-changes-it-forced) and - [§15.3](12-world-graph-kind.md#153-resolution-verification-matrix-for-w46w47). -- **Touches:** core determinism/validation tests and guard scripts; each kind's event and - reason declarations and focused test corpus; CI/package scripts that make the guards - required. -- **Depends on:** [W93](#w93)–[W95](#w95), so the new gates record the corrected baseline - instead of freezing known defects. -- **Status:** Complete — [PR #408](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/408), merged at `bb62ffe`. -- **Done when:** - - W96.1 World-graph batch invariance compares complete canonical kind state for unsplit runs - against `[1,9]`, `[5,5]`, `[2,3,5]`, and ten one-tick calls, across at least two seeds - and across service, construction, incident, day-reset, departure, and terminal - boundaries. - - W96.2 A required manifest or equivalent check names regression evidence for - `story-graph`, `simulation`, and `world-graph`; deleting any named per-kind suite or - fixture fails the same gate CI runs. - - W96.3 A mechanical ownership check fails when a kind state, campaign, or projection repeats - a field owned by `GameState`, `Campaign`, `ContentRegistry`, `Scene`, or `PlayerView`, - and passes all three shipped kinds without a handwritten exception for a current field. - - W96.4 A mechanical reason check proves every visible rejection or audit reason a kind can - emit is registered and localized, including values carried indirectly through - `EffectContext` and batch-change recorders; a seeded unregistered indirect reason - makes the test fail. - - W96.5 Each kind owns one name-to-severity table. The declared names, runtime emit sites, - and canonical contract table are compared mechanically, and two severities for one - event name or a missing/extra name fail the gate. - - W96.6 All guards run from existing package/CI entry points, fail with the kind and value - that drifted, and do not depend on network access. - - W96.7 `npm run typecheck`, `npm run lint` and `npm test` pass from `src/engine/`; all - committed replays remain byte-identical. -- **Out of scope:** adding kind names, event names, severities, reason vocabulary, or envelope - fields; testing balance; and making per-call audit arrays batch-invariant when the - contract requires only final canonical state. - -### [x] W97 — Immutable Projections and Shared Pipelines {#w97} - -**Delivers:** Engine consumers can inspect and manipulate returned views without mutating a game, -while maintainers can evolve the two tick-driven kinds on one behaviour-preserving pipeline. - -- **Spec:** [04 §3](04-core.md#3-the-kind-interface--the-seam), - [§9](04-core.md#9-projection), - [§14](04-core.md#14-determinism-harness); - [10 §3](10-simulation-kind.md#3-the-turn-is-a-week), and - [12 §4](12-world-graph-kind.md#4-the-turn-is-a-tick-batch). -- **Touches:** the `Kind.project`/`Engine.view` seam and all three kind projections; simulation - end-of-week and world-graph tick pipeline composition; an engine-internal pipeline module - and focused equivalence tests. -- **Depends on:** a fresh `/contract` decision that assigns defensive-copy ownership at the - `Kind.project` seam and a `/design` or `/contract` decision that fixes the shared - pipeline's ordering/error semantics. No implementation begins while either shape is - unresolved. -- **Status:** Complete — [PR #409](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/409), merged at `b9ecdc2`. -- **Done when:** - - W97.1 The canonical contract states whether a kind or the kernel owns defensive copying, - including nested arrays/records and the treatment of immutable primitives; the source - and all three built-in kinds implement that one rule. - - W97.2 Mutating every nested object/array reachable from a returned story-graph, - simulation, or world-graph view leaves the originating `GameState`, a fresh second - view, `serialize()`, and the action log unchanged. - - W97.3 The shared pipeline accepts an explicit ordered system list, threads the existing - frame/context, stops or continues only under the contract's stated rule, and is not - exported from the package root or `/authoring`. - - W97.4 Simulation's ordered end-of-week systems and world-graph's twenty tick systems both - execute through that substrate without changing their kind-specific system ids, - comparators, event order, or durable handoffs. - - W97.5 Before/after golden runs for both kinds produce byte-identical serialization, - outcomes, visible changes, and event streams with recording emitters; null emitters - produce the same state. - - W97.6 `npm run typecheck`, `npm run lint` and `npm test` pass from `src/engine/`, and packed - consumer smoke proves no new public export was introduced. -- **Out of scope:** merging the two kinds' state or mechanics, making pipelines campaign data, - changing any system order, or general-purpose workflow/middleware infrastructure. - -### [x] W98 — Catalog and Projection Completeness {#w98} - -**Delivers:** Clients can render campaign choices, stat ranges, ending progress, and terminal -results through the public store and projection surfaces without reading engine-owned content or state. - -- **Spec:** [04 §3](04-core.md#3-the-kind-interface--the-seam), - [§7](04-core.md#7-the-session-store-and-the-platform-api), - [§9](04-core.md#9-projection); - [03 §9](03-story-graph-kind.md#9-projection--what-a-client-sees), and - [09 §4](09-clients.md#4-the-api-coverage-checklist). -- **Touches:** `CampaignSummary`, `SessionStore.listCampaigns`, built-in outcome/projection - types and implementations; text client, MCP server, browser composition, service-contract - mirrors, and packed-consumer tests. -- **Depends on:** a fresh `/contract` pass that declares the asynchronous catalog result, - session-free title resolution, range/progress projection, and cross-kind terminal - identity. Those public shapes do not exist in the current contract and are not to be - inferred from this slice. -- **Status:** Complete — [PR #414](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/414), merged at `d7ca8eb`. -- **Done when:** - - W98.1 The canonical contract declares an asynchronous campaign-list operation and a result - that a client can render as a human-readable campaign selector using only - `SessionStore`; a fetch-backed test implementation does not preload a registry. - - W98.2 The text client and MCP `list_campaigns` render the same resolved titles without - starting a session or reading `ContentRegistry` directly, and the API coverage checklist - matches the chosen operation count. Browser-shelf parity with these two surfaces is the - client contract's requirement, not this repository's — `/play/` was retired in favor of - `SubZeroDev.Adventures` (`10-design.md`, *Succeeded by SubZeroDev.Adventures*), and its - own reconciliation ticks that box, the same treatment `10-design.md` §4 already gives the - Browser client column for W99's rows 11–13. Amended 2026-09-13 (`90-decisions.md`) after - `/play/`'s retirement moved the browser surface out of this repository. - - W98.3 Every projected visible story stat carries its declared lower and upper bounds, so a - client renders the current value and range without reading campaign content. - - W98.4 Story-graph ending progress exposes the contract-selected discovered and total facts - without exposing hidden ending ids; a fresh profile and a profile with one unlocked - ending have distinct, exact projections. - - W98.5 A host can read the contract-selected common terminal identity from each built-in - kind without switching on private kind-state shapes, while kind-specific published ids - remain available and balance-sensitive facts remain excluded. - - W98.6 Existing in-memory callers have an explicit migration path to the asynchronous - listing, and type-level packed-consumer fixtures prove both the new path and the - intended rejection of the old signature. - - W98.7 `npm run typecheck`, `npm run lint` and `npm test` pass from `src/engine/`; site and - host checks pass with committed replay outcomes unchanged. -- **Out of scope:** adding locales, exposing hidden variables/endings, returning campaign - content through `SessionStore`, making outcomes a balance report, or changing session - creation semantics. - -### [x] W99 — Session Lifecycle Operations {#w99} - -**Delivers:** Hosts can list, branch, and delete a player's saves through the session store and -reproduce stored sessions exactly, while retaining responsibility for authorization. - -- **Spec:** [04 §7](04-core.md#7-the-session-store-and-the-platform-api), - [§10](04-core.md#10-content-saves-migration); - [06 §2](06-extensibility.md#2-the-rule), - [§5.2](06-extensibility.md#52-sessionpersistence-and-profilestore), and - [07 §6](07-replay.md#6-the-runner-and-its-verdicts). -- **Touches:** session/store and persistence contracts and implementations; `IdSource`, text - and MCP/client coverage, service-contract mirrors, host adapters, and replay tests. -- **Depends on:** a fresh `/contract` pass that chooses the store signatures, result/error - shapes, canonical save-list order, and concurrency semantics. Authorization remains a - documented host boundary unless that pass explicitly reopens it. -- **Status:** Complete — [PR #416](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/416), merged at `51ad81a`. -- **Done when:** - - W99.1 A player-keyed save-list operation exists at the contract-selected store boundary, - returns only that player's saves in a specified deterministic order, and lets both - existing hosts delete their private shadow indexes. - - W99.2 Branching a session at a valid action sequence is one store operation: it allocates a - new id through `IdSource`, leaves the source session/save untouched, and the branch - serializes and replays byte-identically through the fork point. - - W99.3 Branching an unknown session or invalid sequence returns the contract-selected typed - failure and performs no persistence write. - - W99.4 Deleting a save is reachable through the chosen public lifecycle surface; success - removes exactly the addressed record, and the specified missing/wrong-player result - leaves every record untouched. - - W99.5 A delete authorized against an older record cannot erase a concurrent replacement - written under the same `saveId`; the race is reproduced deterministically against the - persistence conformance suite. - - W99.6 The replay/extensibility contract states that reproducing a stored session blob from - its log requires `IdSource.newGameId` pinned to the original `gameId`, and a test proves - pinned reproduction is exact while an unrelated id is observably different. - - W99.7 The contract states that caller identity and authorization are host-owned and - `SessionStore` remains caller-agnostic, with the deferred hosting/NEaaS trigger that - would reopen the decision. - - W99.8 Text, MCP/service-contract, host, and packed-consumer coverage include every new - operation; `npm run typecheck`, `npm run lint` and `npm test` pass from `src/engine/`. -- **Out of scope:** accounts, authentication/authorization policy, cloud synchronization, - sharing saves between players, editing historical actions, or changing the save envelope. - -### [x] W100 — Campaign-Tunable Weekly Rules {#w100} - -**Delivers:** Campaign authors can tune weekly rules, empty plans, relationships, attendance, and -wisdom-driven choices while existing campaigns keep their current behaviour by default. - -- **Spec:** [10 §2](10-simulation-kind.md#2-kindstate--what-belongs-here), - [§3](10-simulation-kind.md#3-the-turn-is-a-week), - [§6](10-simulation-kind.md#6-player-state), - [§7](10-simulation-kind.md#7-content-definition-types), - [§9](10-simulation-kind.md#9-projection), and - [§10](10-simulation-kind.md#10-reason-codes). -- **Touches:** simulation campaign/source types, builder defaults and validation; planning and - end-of-week systems for relationships/employment; projection, conditions/effects, - fixtures, and replay tests. -- **Depends on:** a fresh `/contract` pass that fixes the tuning fields and defaults, the - `plan_empty` policy, relationship transition, attendance formula, and the specific - wisdom consumer. The current contract explicitly leaves those rules absent or inert. -- **Status:** Complete — [PR #419](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/419), merged at `3d496b3`. -- **Done when:** - - W100.1 The source/runtime campaign contract contains one closed, validated home for every - weekly tuning value this unit changes, with explicit defaults; omitted fields on every - 0.10 campaign build to the exact former runtime values. - - W100.2 The empty-plan policy is campaign-selectable. A forbidding campaign rejects - `end_week` with `plan_empty` and unchanged state, while a permitting campaign advances - through the ordinary ordered pipeline; the default preserves 0.10 behaviour. - - W100.3 The `relationships` system applies the contract's ordered drift once per week to the - correct actor/NPC records, clamps at declared bounds, emits exact visible changes, and - resumes identically across save/load. - - W100.4 Employment attendance is updated from the contract's declared planned/worked inputs, - uses the specified rounding and rolling window, affects the existing attendance - requirements, and cannot be advanced twice for one week. - - W100.5 `wisdom` has the contract-selected visible projection and at least one typed content - read with a fixture proving two otherwise identical actors receive different available - behaviour; no client reads raw campaign or kind state to obtain it. - - W100.6 Builder and validator tests cover every default, invalid bound, and explicit override; - the Stable Life campaign builds with no new unexplained finding. - - W100.7 Every 0.10 campaign, replay, and save fixture is byte-identical when all new fields - are omitted; `npm run typecheck`, `npm run lint` and `npm test` pass from `src/engine/`. -- **Out of scope:** tuning concrete campaign balance, adding projects/businesses/rivals, profile - progression, or changing the ordered list of end-of-week systems. - -### [x] W101 — Projects, Businesses, and Rival Scarcity {#w101} - -**Delivers:** Campaign authors can offer persistent projects and businesses, and players can face -scripted rivals who compete reproducibly for the same finite opportunities. - -- **Spec:** [10 §2.2](10-simulation-kind.md#22-world-state), - [§4.2](10-simulation-kind.md#42-action-types), - [§6.2](10-simulation-kind.md#62-the-shared-actor-shape), - [§7](10-simulation-kind.md#7-content-definition-types), and - [§7.10](10-simulation-kind.md#710-agents--engine-owned-strategy-definition-and-runtime-state). -- **Touches:** simulation content/source and runtime state; project/business resolvers and - end-of-week systems; scenario rival configuration, strategy registry, opportunity/job - scarcity, validation, projections, events, and replay fixtures. -- **Depends on:** [W100](#w100) for the defaulted campaign-tuning boundary, and a fresh - `/contract` pass that declares project/business definitions, their durable progress, - scenario rival selection, and deterministic competition. The four action names and - engine-owned strategy seam alone are not sufficient public contracts. -- **Status:** Complete — [PR #421](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/421), merged at `b595fe2`. -- **Done when:** - - W101.1 Project definitions are campaign content with closed typed requirements, costs, - progress, completion and rewards; resolver selection uses the action/type discriminator - declared by the contract and never a tag or id naming convention. - - W101.2 Starting and advancing a project consumes the exact planned time/resources, persists - partial progress, completes once, and produces the same state and published changes - when save/load cuts occur before start, mid-progress, and immediately before completion. - - W101.3 Business definitions and runtime records drive start-up cost, weekly revenue, - expenses and closure under contract-stated integer/rounding rules; cashflow posts once - in the ordered weekly pipeline and is invariant across replay and save boundaries. - - W101.4 A scenario declares zero or more rivals by the contract-selected strategy ids and - initial conditions. Zero rivals preserves current behaviour; an unknown strategy or - invalid advantage is a precise Tier-1 finding. - - W101.5 Player and rivals use the same `ActorState` mechanics and the same action resolvers. - A fixture that gives both actors identical state and the same chosen action produces - identical actor-local results before contested allocation is applied. - - W101.6 Each rival derives choices only from its allowed public-world view and its stable RNG - stream. Reordering registry construction or enabling a recording emitter does not - change choices, allocation, state, or outcome. - - W101.7 Two actors contesting a finite opportunity or job opening are resolved by the - contract's complete comparator; one filled position decrements the count, the last - retires the opening, and the losing actor receives the declared unavailable/revoked - result without duplicated rewards. - - W101.8 Committed project, business, and rival-scarcity replays are byte-identical on repeat - and across a save/restore cut; `npm run typecheck`, `npm run lint` and `npm test` pass - from `src/engine/`. -- **Out of scope:** adaptive/remote AI, hidden-information cheating, multiplayer, arbitrary - user-authored code, economic balance of a concrete campaign, or changing `ActorState` - into separate player and rival shapes. - -### [x] W102 — Profile Chains and Simulation Save Migration {#w102} - -**Delivers:** Simulation players can carry profile-scoped chains into later sessions and resume -older portable saves through deterministic, declared migrations. - -- **Spec:** [04 §7.1](04-core.md#71-the-profile-store), - [§10.2](04-core.md#102-save-envelope-and-migration); - [10 §2.2](10-simulation-kind.md#22-world-state), and - [§15](10-simulation-kind.md#15-what-was-ported-and-what-was-found-along-the-way). -- **Touches:** `PlayerProfile`, profile persistence and kind integration; simulation chain - content/state; campaign/kind migration declarations and dispatch; save/replay fixtures, - host adapters, and packed-consumer tests. -- **Depends on:** [W101](#w101), so the profile and migration contracts are tested against real - simulation content rather than a mechanism with no consumer; and a fresh `/contract` - pass defining both public shapes before implementation. -- **Status:** Complete — [PR #423](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/423), merged at `6e4a7bb`. -- **Done when:** - - W102.1 `PlayerProfile` has a versioned, JSON-serializable, kind-owned data boundary whose - ownership, unknown-kind behaviour, size/version validation, and migration order are - stated in the canonical contract without importing a simulation type into core. - - W102.2 A simulation chain declared with profile scope writes through `ProfileStore`, is - visible to a new session for the same profile, remains absent for another profile, and - never changes terminal resolution or deterministic state for a session that does not - read that chain. - - W102.3 Reapplying an already-recorded profile-chain transition is idempotent: it creates no - duplicate entry, event, achievement, or reward, including after save/load and process - restart against the persistence conformance adapter. - - W102.4 The simulation campaign contract declares portable migration steps for published-id - remaps, required defaults, removals, and validation. Steps are data or engine-owned - declarations, never host callbacks, and their order is deterministic. - - W102.5 Loading an older simulation save applies kind-shape migration before campaign-data - migration, stamps current versions, sets `replayCompatible: false` sticky-forward, and - yields the same canonical result on repeated migration. - - W102.6 Missing remaps/defaults and invalid post-migration state return the existing typed - migration failures without partially writing a session or save; focused fixtures cover - success on both axes and failure on each axis. - - W102.7 Every 0.10 profile and save with no profile-chain data loads under the declared - default with unchanged projection and outcome; packed host consumers compile against - the amended types. - - W102.8 `npm run typecheck`, `npm run lint` and `npm test` pass from `src/engine/`; all - unaffected committed replay outcomes remain byte-identical. -- **Out of scope:** cloud profile synchronization, merging concurrent profiles, arbitrary - executable migration scripts supplied by a host, migrating unpublished development - ids, or making profile state part of terminal resolution. - -### [x] W103 — Companion Contracts and Ownership Reconciliation {#w103} - -**Delivers:** Ecosystem maintainers get one consistent account of simulation lifecycles, hosted -operations, fixture ownership, package visibility, and the proof that replaces the retired browser. - -- **Spec:** [10 §2.3](10-simulation-kind.md#23-effects-opportunities-and-scheduled-events) - and [§15](10-simulation-kind.md#15-what-was-ported-and-what-was-found-along-the-way); - [09 §4](09-clients.md#4-the-api-coverage-checklist); - [13 §6](13-playable-web-demo.md#6-route-visual-system-and-delivery), and - [15 §6](15-platform-static-host.md#6-ci-publication-and-deployment-boundary). -- **Touches:** canonical companion-mirror sections and generated docs; Platform, - `@subzerodev/service-contract`, and Adventures integration evidence; campaign fixture - notices; package-visibility and retired-browser-proof decisions; agent-kit metadata and - repository-specific companions where the sync is part of the reconciliation. -- **Depends on:** GameOfLife S6/S7 and its zero-concept-finding gate; [W98](#w98) and - [W99](#w99) for the final operation shapes mirrored to Platform/service consumers. -- **Status:** Landed — all eight criteria confirmed met; closed by issue #391. -- **Done when:** - - [x] W103.1 GameOfLife S7 is landed and its `Test-SpecSet.ps1` reports zero `concept` findings; - every lifecycle it added is mirrored here with both creation and retirement paths. A - contradiction with shipped simulation code is reported and stops the mirror rather - than being silently reconciled. Re-verified by PR #438: `SubZeroDev.GameOfLife` S6/S7 - are landed (its issues #13, #14); `design/20-contract.md` §15.1 maps all twelve of S7's - `lifecycle-` concepts onto this kind's own shape. No contradiction with shipped - simulation code surfaced. - - [x] W103.2 Platform's hosted MCP contract includes `preview_action` and every W98/W99 - operation with the same arguments/results and one-operation/one-tool mapping. The row - set moved out of Platform entirely (its S2.11) into - [`SubZeroDev.ServiceContract`'s `mcp-tool-contract.md`](https://github.com/The-Running-Dev/SubZeroDev.ServiceContract/blob/5d3fb8e6ebeb8fb6200e36ebc61df107ad355ce7/mcp-tool-contract.md) - (`5d3fb8e`, package `0.6.0`), which names `preview_action` as the tenth of thirteen - operations, including W98's revised `list_campaigns` and W99's `list_saves`/ - `branch_session`/`delete_save`. Platform's - [`workloads/game-service/src/mcp-surface.ts`](https://github.com/The-Running-Dev/SubZeroDev.Platform/blob/665103a3cdd182b1bc248c5aa08d2202fb6ac041/workloads/game-service/src/mcp-surface.ts) - (`665103a`) builds `listTools()`/`callTool()` directly from that package's - `contract.operations`, so the mapping is generated, not hand-mirrored. Closes #269 (see - `90-decisions.md`). - - [x] W103.3 The service-contract package's `src/generate.ts` (`SubZeroDev.ServiceContract` - @ `5d3fb8e`) enforces arity both ways — every engine `SessionStore` method must have a - row and every row must name a real method — failing the build on a missing or extra - operation. Adventures vendors that same `0.6.0` artifact - (`server/package.json`: `file:./vendor/subzerodev-service-contract-0.6.0.tgz`, - `SubZeroDev.Adventures` @ - [`aeef4b4`](https://github.com/The-Running-Dev/SubZeroDev.Adventures/blob/aeef4b4089a50d2d0b55e5c421066f0fd245e9db/server/package.json)) - and runs its own missing/extra-operation gate, - [`server/src/contract.test.ts`](https://github.com/The-Running-Dev/SubZeroDev.Adventures/blob/aeef4b4089a50d2d0b55e5c421066f0fd245e9db/server/src/contract.test.ts), - comparing the routes it serves against `loadPublishedContract().operations`. The browser - client, - [`src/play/remote-store.ts`](https://github.com/The-Running-Dev/SubZeroDev.Adventures/blob/aeef4b4089a50d2d0b55e5c421066f0fd245e9db/src/play/remote-store.ts), - implements all thirteen operations — catalog, save, load, branch, and delete included — - as `fetch` calls against the server's HTTP routes, never importing `SessionStore` or - touching persistence directly. - - [x] W103.4 Every campaign source and exported JSON snapshot retained in this repository says - within its owning file that it is a test fixture, names the external Content repository - as the publication authority, and leaves fixture behaviour/tests unchanged. - - [x] W103.5 The package-visibility decision is recorded once and `package.json`, plans 39/40, - release documentation, and consumer access all agree on public or private; no document - retains the rejected answer. See `90-decisions.md` §2, closing issue #302. - - [x] W103.6 The no-engine-API browser proof is either assigned to a named replacement owner with - an executable check or retired by a recorded decision that updates both design 13 and - 15; the deleted `/play/` surface is not recreated to satisfy it. PR #436 retires the - proof rather than reassigning it: Adventures is deliberately a hosted API with - persistence, so the property has no surviving host to assert over. Recorded in - `90-decisions.md` §2, closing issue #273. - - [x] W103.7 The current agent kit is reconciled without losing repository-specific `-Skip:` - guards or command companions; the Pester run reports both pass and intentional skip - counts, and any third recurrence is raised upstream as the existing issue requires. - Closed by PR #442: `/kit-sync` fast-forwarded `~/.agent-kit` (`5095a55` → `36f0a7a`, - 44 commits) and reconciled `tools/*.ps1` and the two divergent command cores via - `tools/Sync-Kit.ps1`. `tools/` Pester reports 324 passed, 0 failed, 35 intentional - skips. - - [x] W103.8 Canonical docs regenerated via `build/ConvertTo-HumanDocumentation.ps1`; - `build/Test-Documentation.ps1` passed (18 generated engine pages, compatibility - pointers, guide, and 189 Markdown files); `src/engine`'s `typecheck`, `lint`, and - `test` (94 files, 1554 tests) all passed. Every companion dependency cited above for - W103.2/W103.3 names an immutable commit SHA in a public repository, not an - unversioned branch. -- **Out of scope:** authoring GameOfLife lifecycles in this repository, implementing - Presentation's scene layer, republishing external packages, moving fixture content back - from Content, or adopting a companion's design-state corpus wholesale. - -### [x] W104 — Release 0.11 Compatibility Sweep {#w104} - -**Delivers:** Existing campaign, save, replay, host, and package consumers can move from 0.10 to -the additive candidate without silently changing behaviour. - -- **Spec:** [04 §10](04-core.md#10-content-saves-migration), - [07 §6](07-replay.md#6-the-runner-and-its-verdicts), and the compatibility/default rules - fixed by [W98](#w98)–[W102](#w102). -- **Touches:** a committed 0.10 compatibility corpus; campaign builders, save loader, replay - runner, host/service adapters, package archive assertions, and consumer smoke projects. -- **Depends on:** [W93](#w93)–[W103](#w103) complete. This is the first unit allowed to call - their combined result a release candidate baseline. -- **Status:** Landed — all seven criteria confirmed met; closed by issue #392. -- **Done when:** - - [x] W104.1 A committed manifest names every 0.10 built-in campaign/version, representative - active and ended save for each kind, every replay fixture, both hosts, and the packed - public consumer surface; the sweep fails if an entry disappears without an explicit - replacement/evidence note. Closed by PR #448: `scripts/w104-manifest.test.ts` names every - 0.10 built-in campaign/version and the save/compat-baseline fixture sets, on top of the - hosts/packed-consumer-surface entries PR #447 added to `regressionManifest.test.ts`. - - [x] W104.2 Every 0.10 campaign source builds with all 0.11 additive fields omitted and produces - the same canonical runtime content, initial projection, available actions, and Tier-1/2 - findings as its recorded 0.10 baseline unless a separately approved correction is named. - Closed by PR #448: `fixtures/compat/v0.10.0/*.json` freezes the canonical initial `Scene` - and Tier-1/2 findings per built-in campaign, captured at the `v0.10.0` tag; a HEAD rerun - is byte-identical across all five entries. - - [x] W104.3 Every 0.10 save loads or migrates according to its recorded version boundary, with - unchanged projection/outcome for defaulted additions and no partial write on failure. - Closed by PR #448: `fixtures/saves/*.json` (one active/ended pair per kind) load through - a real `SessionStore.loadGame` and match an independent re-run; a tampered-checksum case - proves no partial write. - - [x] W104.4 Every pre-existing replay receives the same verdict and byte-identical outcome; any - intentionally migrated fixture is separately identified and proves - `replayCompatible: false` rather than silently replacing its golden file. Closed by - PR #447: 29 fixtures replayed against `v0.10.0`'s own committed corpus, byte-identical. - - [x] W104.5 The text client, MCP server, static host and Adventures/service-contract integration - pass against the same engine archive and use only the declared SessionStore surface. - Closed by PR #452, after PR #449 covered the in-repository text client and MCP server via - the client-boundary ESLint rule and their own API-coverage suites: the engine packed at - `e52e7a1` was re-vendored into `SubZeroDev.Platform`, `SubZeroDev.Adventures`, and - `SubZeroDev.ServiceContract`, each passing its full suite with no reach past - `SessionStore`/`Portable*`/`fromPortable`. Recorded as a point-in-time proof, not a - standing cross-repo CI gate, since none of the three repos live inside this one. - - [x] W104.6 A clean pack contains the asserted files and no private source/plans, installs into - the lockfile-backed consumer smoke, and compiles/runs all public and `/authoring` - examples without workspace resolution. Closed by PR #447: clean tarball, no workspace - resolution, `/authoring` exports exercised in the consumer smoke. - - [x] W104.7 The compatibility sweep runs from one documented command/CI job and exits non-zero - with the exact artifact that drifted; its green result and baseline commit are recorded. - Closed by PR #447: `build/Test-CompatibilitySweep.ps1` composes typecheck/lint/test, the - replay oracle, and the packed-tarball consumer smoke into one documented command. -- **Out of scope:** preserving undocumented implementation details, treating an approved bug - fix as a compatibility failure without its evidence, publishing a package, or accepting - new feature work after the baseline is cut. - -### [x] W105 — Documentation and Landing Publication Review {#w105} - -**Delivers:** Prospective users can read and navigate an accurate public account of the -stabilization release on both documentation and landing surfaces. - -- **Spec:** the canonical sections changed by W93–W104; the documentation source/generation - boundary in `CLAUDE.md`; and the public claims in the README, guide, roadmap, and landing - configuration. -- **Touches:** canonical design text, generated `docs/docs/engine/` pages, guide/navigation, - README/roadmap/release material, landing configuration and rendered browser snapshots. -- **Depends on:** [W104](#w104), so the public review describes the verified compatibility - candidate rather than work still moving underneath it. -- **Status:** Complete. Closed via #393; all criteria (W105.1–W105.7) verified. -- **Done when:** - - W105.1 Canonical-to-human generation produces no diff on a second run, and the generated - pages contain every amended contract/slice section with no hand-edited generated copy. - - W105.2 Documentation validation passes generated-file freshness, internal/external links, - anchors, terminology, navigation, and guide-stamp checks from a clean checkout. - - W105.3 README, public guide, roadmap and landing page agree that the engine has three kinds, - the in-repository play surface is retired, published content is externally owned, and - `0.11.0` is a candidate rather than an already published release. - - W105.4 Every public SessionStore, projection, outcome, profile and migration example compiles - against the packed candidate; no example uses a removed synchronous signature or reads - registry/kind state across a client boundary. - - W105.5 The landing and documentation entry pages are rendered at the repository's committed - phone and desktop widths with no horizontal overflow, clipped navigation, unreachable - primary link, or overlap; reviewed screenshots are committed or attached as immutable - CI evidence. - - W105.6 The customer/alternative/monetization thesis is either stated in the public brief or - explicitly deferred with reasoning and an owner; it is no longer absent from design. - - W105.7 `npm --prefix site run check`, `npm --prefix site run build`, and - `./build/Test-Documentation.ps1` pass using the same generated artifact. -- **Out of scope:** redesigning the site, adding a playable client, changing product strategy - beyond making its current status explicit, deploying, or publishing the npm package. - -### [x] W106 — Tracker Evidence Closure {#w106} - -**Delivers:** Maintainers can trust that every resolved engine issue links to immutable evidence, -while genuinely unavailable Presentation work remains visibly blocked instead of falsely closed. - -- **Spec:** this ledger's stable criterion-id rules, `CLAUDE.md`'s tracking conventions, and - the issue/mirror evidence produced by W93–W105. -- **Touches:** GitHub issue/project state and labels; `design/state/work/` mirrors and index; - canonical decisions/slices only where the recorded outcome requires correction. -- **Depends on:** [W93](#w93)–[W105](#w105). It closes evidence; it does not substitute issue - closure for unfinished implementation. -- **Status:** Complete. Closed via #394; all criteria (W106.1–W106.7) verified. -- **Done when:** - - W106.1 A frozen inventory records every open engine issue number, title, current state, - owning W criterion or explicit non-programme disposition, and immutable implementation, - decision, supersession, or external-blocker evidence. - - W106.2 An issue is closed as implemented only when each of its acceptance checkboxes is - satisfied by a merged commit/PR and passing verification; stale and dissolved issues - cite the exact later contract/commit that removed their premise. - - W106.3 Decision-only issues cite the canonical decision that answers them and no longer - retain contradictory open-register text; externally owned issues cite the companion - issue/commit and remain open when that work is not actually complete. - - W106.4 The Presentation spike remains open with its unavailable/blocked state and transfer - target unless the Presentation repository and authority are genuinely available; no - engine issue or slice claims to implement it. - - W106.5 `/track` reports no missing issue for W93–W108, no criterion-id drift, no stale issue - body, and no implemented unit whose heading remains unchecked. Historical pre-tracker - units retain their recorded exemption rather than receiving fabricated retroactive work. - - W106.6 Work mirrors reproduce the live tracker state at one named commit/time, preserve - immutable evidence links, and a second mirror run is a no-op. - - W106.7 The repository's design-state checks either pass fully or every remaining - tool/content incompatibility has one owning issue and an explicit unavailable result; - `ContractListUnreadable`, `ProjectorFailed`, and false `TrackerUnavailable` are not - reported as successful checks. -- **Out of scope:** closing blocked work to make counts green, rewriting historical issue - narratives, implementing any unresolved issue inside the tracking pass, or transferring - work to an unavailable repository. - -### [x] W107 — 0.11 Release Candidate Verification {#w107} - -**Delivers:** Release maintainers can judge one additive candidate from a complete, reproducible -verification record covering the engine and every supported delivery surface. - -- **Spec:** [W104](#w104)'s compatibility manifest, [W105](#w105)'s publication artifact, and - the repository release/check matrices referenced by `CLAUDE.md`. -- **Touches:** verification commands and CI workflows only as needed to run the existing - engine, host, site, docs, container, consumer and companion gates against one candidate. -- **Depends on:** [W106](#w106), so verification starts with tracker/design truth reconciled. -- **Status:** Complete. Closed via #395; all criteria (W107.1–W107.7) verified. Rerun against the - `0.11.0` candidate `2b525d3`. W107.1, W107.2, W107.3, W107.4, W107.6 and W107.7 all hold; - the record is - `.claude/release-candidate-w108.json`. W107.3's production Docusaurus build and W107.4's - container smoke could not run locally — no Docker daemon — and were established instead as - required checks on PR #471, green at branch head `14f4323`; re-packing there reproduced the - candidate archive's digest bit-for-bit, so nothing in the archive moved between the two. - **W107.5 now holds.** Its GameOfLife half was resolved first: that companion did not - compile against the candidate because `stable-life.ts` lacked the `projects` and - `businesses` that [W101](#w101) made required, and, once that cleared, the event-chain - declarations [W102](#w102) requires. Both were **pre-existing, not caused by `0.11.0`**, and - both are fixed in the companion by SubZeroDev.GameOfLife#120 (`8ea9441`). GameOfLife now - compiles against engine `6910bf5`, whose `src/engine` is byte-identical to the candidate's. - SubZeroDev.Platform's durable/Postgres profile was the other half — unavailable at report - time for want of a Docker daemon, and green (197 of 198, 1 pre-existing skip) once Docker - became available in this environment and the same committed `docker-compose.yml` service - was brought up. W107.4's own text was corrected by - PR #461 before this rerun, so the rerun exercised the corrected requirement and added no - engine API to the static host; the superseded failure in - `.claude/release-candidate-w107.json` is history, not an open finding. -- **Done when:** - - W107.1 A candidate commit and one clean packed archive digest are recorded; every consumer - and host test uses that archive rather than a workspace import or a separately packed - copy. - - W107.2 A clean `src/engine/` install passes typecheck, determinism lint, unit/integration - tests, build, archive-content assertion, replay corpus, and the W104 compatibility sweep. - - W107.3 A clean site install passes lint/typecheck/unit/browser checks and production build; - documentation conversion and validation run from canonical sources and are no-op on a - second generation. - - W107.4 The static host builds and its container smoke proves `200` for `/`, `/roadmap/`, - `/docs/`, liveness and readiness, `404` for a named unknown route, and a negative - fixture that fails to start on a missing required artifact, with no source tree - mounted at runtime. Per `15-platform-static-host.md` §8, the host receives no action, - owns no session, and exposes no engine API — session creation, action - preview/submission, save/resume, and the W98/W99 operations are contract-gated on the - engine's own API, not this host's, and this criterion must not require them. - - W107.5 Packed text/MCP consumers and the lockfile-backed consumer smoke compile and run; - Platform, service-contract, Adventures and GameOfLife companion checks name exact - compatible versions or commits and have no unresolved contract mismatch. - - W107.6 Required CI workflows are green at the candidate commit, including Pester with its - pass/skip counts and documentation/deploy Chromium setup; a skipped or unavailable - check is listed as such and never rendered as a pass. - - W107.7 A release-candidate report records command, exit code, duration, artifact digest and - evidence URL for every row, with zero unexplained failure or dirty-tree substitution. -- **Out of scope:** fixing a failed gate inside the verification unit, changing candidate - scope, tagging, creating a GitHub release, deploying, or publishing to npm. - -### [x] W108 — Publish 0.11 Readiness {#w108} - -**Delivers:** The release owner gets a public roadmap and exact checklist authorizing `0.11.0` -as the next publication, while the actual publish remains a separate explicit action. - -- **Spec:** the repository's version/tag/release contract, [W107](#w107)'s candidate report, - and the public roadmap/release material reconciled by [W105](#w105). -- **Touches:** canonical roadmap/release checklist, README/landing release status, package and - changelog metadata required for a future user-triggered publication; no remote release - mutation. -- **Depends on:** [W107](#w107) green and every blocker in [W106](#w106) explicitly resolved or - accepted by the user. -- **Status:** Complete. Closed via #396; all criteria (W108.1–W108.6) verified. `0.11.0` is set - in the package and lockfile, and the - roadmap, README, generated docs and landing page all name it as the next authorized - publication (W108.1). The checklist is `RELEASE.md`, naming the candidate - `2b525d38d6876829b35e9f5ff5fcf22af421f082`, the archive - `the-running-dev-game-engine-0.11.0.tgz` and its SHA-256, the required `v0.11.0` tag, the - registry and its read-only availability result, the release-note source, and the - non-publishing commands. W108.4 holds — the archive ships exactly three kinds, two entry - points, no campaign content the engine does not own, and no play surface. W108.5 holds: the - guards moved to `src/engine/scripts/verify-release.mjs`, run in a non-publishing validation - path, and each has rejected a mismatched tag, a dirty tree and a substituted archive. - W108.6 holds — nothing was published, tagged, released or deployed. **W108.2 and W108.3 now - hold too**, both resolved 2026-09-13 by the user in `90-decisions.md`: W108.2 accepts - `RELEASE.md` §7's consumer-side rollback as the procedure outright rather than holding out - for a tested GitHub Packages deprecation, and W108.3 is satisfied against W98.2 as amended - that day, scoping browser-shelf parity to the client contract governing - `SubZeroDev.Adventures` instead of this repository. Every other blocker the rerun found has - closed: W107.3, W107.4 and W107.6 on PR #471's own CI, W107.5's GameOfLife half with - SubZeroDev.GameOfLife#120, and W107.5's Platform half once Docker became available. - **Readiness is no longer blocked; publication remains a separate explicit action.** -- **Done when:** - - W108.1 The canonical roadmap, README, generated public docs and landing page all identify - `0.11.0` as the next authorized publication and describe the same additive scope and - compatibility claim. - - W108.2 The release checklist names the exact candidate commit/archive digest, required tag - and package versions, changelog/release-note source, registry access, dry-run commands, - post-publish verification, and rollback/deprecation procedure. - - W108.3 Every W93–W107 unit is complete with immutable evidence, or the checklist names the - remaining blocker and does not claim readiness; no unchecked criterion is hidden by a - summary status. - - W108.4 Package metadata, archive contents and public documentation agree on version, - visibility, supported entry points, three shipped kinds, external content ownership, - and the retired play surface. - - W108.5 The release workflow's tag/version and clean-build guards are exercised in a - non-publishing validation path against the candidate and reject a mismatched tag or - dirty/repacked artifact. - - W108.6 No npm publish, Git tag, GitHub release, deployment, DNS change, or external package - release occurs in this unit; the final checklist states that publication requires a - separate explicit user action. -- **Out of scope:** performing the publication, choosing credentials, deploying the site, - announcing the release, or adding any feature/fix after the verified candidate. - -### Depth: The Five GameOfLife Engine Blockers - -Five contract amendments dated 2026-09-07 in `90-decisions.md` each close a gap a *Life in the -Fast Lane* issue named, and **every one is contract-only**: the types and rules are stated, and -nothing in `src/engine` reads them. They are **0.12 scope** — [W108](#w108)'s own scope forbids -adding a feature after the verified 0.11 candidate — and they stay **1:1 with their upstream -issues**, so one engine unit answers one game issue rather than one unit answering two. - -Ordered by blast radius, not by issue number: the two that change no state and move no fixture -first, the two that move cash trajectories next, and the one that bumps `kindVersion` last and -alone. `plans/50-gameoflife-engine-blockers.md` carries the sizing this ordering came from, and -the four user decisions behind it. **[W111](#w111) closes the already-open engine issue #418**; -the other four have no engine issue yet. - -### [x] W109 — A Uniform That Makes Its Wearer More Employable {#w109} - -**Delivers:** Lets a campaign author write an item or trait whose effect is on how the world -regards the player — a work uniform that makes its wearer more employable, a scandal that fades -when it expires — instead of only on their needs, attributes and skills. Reputation is something -the game already stores and already reads; until now nothing an author could write was able to -move it. - -- **Spec:** [§6.1](10-simulation-kind.md#61-base-and-derived-values)'s `DerivedPath` union and - its writable/formula-only partition, [§6.2](10-simulation-kind.md#62-the-shared-actor-shape)'s - stored `reputation` record, [§7.1](10-simulation-kind.md#7-content-definition-types)'s - writable-target table, and `90-decisions.md`'s 2026-09-07 `W105.1` entry. -- **Touches:** the kind's derived-value layer and its Tier 1 modifier-target check, plus their - tests. No state shape change, no campaign change, no fixture regeneration. -- **Depends on:** [W108](#w108), for scheduling only — this is the first unit of 0.12 and must - not land against the verified 0.11 candidate. -- **Status:** Done — [PR #478](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/478). -- **Done when:** - - W109.1 A campaign whose `Modifier.target` is `player.reputation.` passes Tier 1 - validation, and the same campaign is rejected before this unit; the writable prefix set - grows by exactly one entry and the `calendar.committedTimeUnits` exception is unchanged. - - W109.2 Resolving `player.reputation.` returns the stored base with every active - modifier layered over it in §6.1's fixed `add`/`subtract` → `multiply` → `set` order, - clamped to `0–100` — the same treatment `player.skills.*` already gets. - - W109.3 When the effect expires, the same read returns the stored base again, unchanged; - nothing was written back to state at any point. - - W109.4 Resolving a reputation key the actor does not store returns no value rather than - `0`, matching the `player.skills.*` precedent line for line. - - W109.5 A `Modifier` targeting any of the four formula-only paths still fails Tier 1 - `read_only_field`; state both counts — targets newly accepted, and targets still - rejected. - - W109.6 The replay corpus is byte-identical: no committed fixture moves, because no existing - campaign targets reputation. -- **Out of scope:** travel-time effects — issue #108's bicycle is deliberately left unamended - and stays unexpressible after this unit; consuming reputation through - `CheckModifier.source`/`PerformanceFactor.source`, neither of which is dispatched by any - resolver today; authoring a campaign that uses the new target. - -### [x] W110 — An NPC Who Already Remembers You {#w110} - -**Delivers:** Lets a scenario begin with an NPC who already has history with the player — a -landlord who already distrusts them, a rival carrying an old grudge — instead of every -acquaintance starting blank and accumulating a past only through play. - -- **Spec:** [§7.7](10-simulation-kind.md#77-npcs--definition-and-runtime-state)'s - `NPCDefinition.startingMemories` and the `NPCMemory` shape it carries, §14's - every-`LocKey`-resolves rule, and `90-decisions.md`'s 2026-09-07 `W105.4` entry. -- **Touches:** the kind's NPC content type and whichever reducer first materialises an NPC's - runtime state, plus their tests. -- **Depends on:** none. -- **Status:** Done — [PR #479](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/479). -- **Done when:** - - W110.1 A campaign may author starting memories on an NPC definition; the resulting NPC's - memory list equals the authored list in authored order, with the author-supplied ids - preserved and no id minted from an `IdSource`. - - W110.2 A definition declaring no starting memories, or an empty list, produces an empty - memory list — today's behaviour — and every committed simulation fixture replays - byte-identically. - - W110.3 Memories are seeded once, at NPC creation: a memory removed or expired during play - does not reappear in a later week. - - W110.4 A starting memory whose `descriptionKey` resolves to nothing in the string table - fails Tier 1 validation; state both counts — campaigns accepted, and campaigns rejected - by this check. -- **Out of scope:** what a week does to memories or relationships — the `relationships` - end-of-week system is contract prerequisite **P2** and has no rule to implement; memory - expiry mechanics; authoring starting memories into the *Stable Life* scenario. - -### [x] W111 — Conditions That Can Ask "Do You Own One?" {#w111} - -**Delivers:** Lets an author write a goal or event that asks whether something exists in the -player's world at all, and how many there are — a pending job application, an owned car, a -course they are enrolled in. Every condition until now could only compare one value against -another, which is why four planned *Stable Life* events could not be written. - -- **Spec:** [§8.2](10-simulation-kind.md#82-collections-for-existscount-w1055)'s closed - seven-path table, `04 §18`'s already-frozen `ExistsCondition`/`CountCondition` and - `ConditionResolver.collection` seam, §14's Tier 1 list, and `90-decisions.md`'s 2026-09-07 - `W105.5` entry. Closes engine issue #418. -- **Touches:** the kind's condition resolver, which throws unconditionally on any collection - today, and its Tier 1 validator, plus their tests. -- **Depends on:** none. -- **Status:** Done — [PR #481](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/481). -- **Done when:** - - W111.1 Each of §8.2's seven declared collection paths resolves to its state array and - supports both `exists` and `count`; state the count of paths accepted. - - W111.2 A `where` clause reads fields relative to a single array element: an `exists` over - the player's inventory testing `definitionId` `in` a list of ids matches when one of - those items is owned and does not match when none is. - - W111.3 Naming any other path — a scalar path, an unlisted array, a typo — fails Tier 1 - `unknown_collection` at load time, never at first evaluation; state both counts, cases - accepted and cases rejected, and prove the load-time-not-runtime claim with a condition - placed on a branch the test never evaluates. - - W111.4 A `count` condition compares the match total against a number and is correct at - zero, at the comparison boundary, and above it. - - W111.5 A `where` naming a field the array element does not carry — an item's `category`, - which lives on the definition and never reaches the resolver — does not match, and a - test pins that outcome rather than leaving it to be discovered by an author. - - W111.6 No new core condition operator is introduced; `04 §18`'s `Condition` type is - unchanged by this unit. -- **Out of scope:** joining a collection member against its content definition, which would need - a core-level `ConditionResolver` widening and is recorded as an open item; adding a new - core operator; authoring the four *Stable Life* events themselves. - -### [x] W112 — A Car That Costs Money to Run {#w112} - -**Delivers:** Makes owning something cost money week after week, not only at the moment of -purchase. A vehicle, a subscription, anything with a declared running cost now drains cash every -week it is owned and working — which is what turns a cheap car with expensive upkeep into a real -decision instead of a free asset. - -- **Spec:** [§7.5](10-simulation-kind.md#75-items)'s `ItemDefinition.weeklyCostCents`, - [§3](10-simulation-kind.md#3-the-turn-is-a-week)'s fixed end-of-week system order, and - `90-decisions.md`'s 2026-09-07 `W105.2` entry. -- **Touches:** the `inventory` end-of-week system, its tests, and the replay fixtures whose cash - trajectories move. -- **Depends on:** none mechanically. It is the first of the two units that move committed - fixtures, so it is scheduled after [W111](#w111) to keep the fixture churn off the three - units that cause none. -- **Status:** Done — [PR #490](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/490). -- **Done when:** - - W112.1 The weekly cost of every owned item is summed and charged against cash in the same - pass that already decays condition, running after income and before housing — §3's - existing order, unchanged. - - W112.2 The charge is per owned instance, not per definition: a player holding three - instances of one definition is charged three times. - - W112.3 An item declaring no weekly cost contributes zero, and an item at zero condition - contributes zero — the same broken-item rule that already stops its effects applying. - - W112.4 The charge is unconditional and may take cash negative; no missed amount is recorded, - no arrears field is added, and nothing is repossessed or disabled for non-payment. - - W112.5 The regression test is the charge itself, verified by reverting the charge and - confirming the test fails. - - W112.6 Every regenerated replay fixture is named in the pull request with the cash delta - that explains it; no fixture is regenerated without an explanation. -- **Out of scope:** any arrears, repossession or collections mechanism for unpaid running costs; - charging conditional on the item having been used that week; branching charge semantics on - an item's free-text `category`; housing's own utilities and transport, which are - [W113](#w113). - -### [x] W113 — Utilities and Transport on the Weekly Bill {#w113} - -**Delivers:** Makes the weekly cost of a home read like an actual bill — rent, utilities and -transport as separate lines — and gives owning a vehicle a payoff, because the transport line is -waived for anyone who has one. A player weighing a cheap flat with a long commute against a -dearer one nearby finally has the numbers in front of them. - -- **Spec:** [§7.4](10-simulation-kind.md#74-housing)'s `utilitiesCents`/`transportCents` and the - reserved `"vehicle"` tag, [§6.9](10-simulation-kind.md#69-housing)'s `HousingState`, - [§3](10-simulation-kind.md#3-the-turn-is-a-week)'s `housing` and `finance_reconcile` - systems, `04 §10.2`'s `kindVersion`/`Kind.migrateState` axis, and `90-decisions.md`'s - 2026-09-07 `W105.3` entry including its same-day revision. -- **Touches:** the kind's housing content and runtime types, the `housing` end-of-week system - and the item definitions threaded into it, the kind's version and migration hook, migration - tests, and the replay fixtures whose cash trajectories move. -- **Depends on:** [W112](#w112). **Do this one last and alone** — it is the only one of the five - that changes a persisted state shape, so batching it means either two version bumps or a - unit that does not fit one session. -- **Status:** Done — [PR #491](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/491). -- **Done when:** - - W113.1 A housing definition may declare utilities and transport costs; both absent means - zero, and a campaign declaring neither charges exactly what it charges today. - - W113.2 Both are stamped onto the player's housing state at move-in, exactly as rent already - is; editing the definition afterwards does not change an existing tenancy's charge. - - W113.3 The weekly charge is rent plus utilities plus effective transport as one combined - levy against cash, in the pass rent already used, and cash may go negative — the same - "wages before costs" ordering rent alone already proves. - - W113.4 A shortfall against the combined total advances the same - overdue/missed-payments/eviction ladder rent alone drove; no separate utilities or - transport arrears state exists, and no second reconciliation rule is added. - - W113.5 Transport is charged as zero for a week in which the player holds at least one - inventory item above zero condition whose definition tags include the literal - `"vehicle"`; a broken vehicle does not waive it, and utilities are never waived. - - W113.6 The kind version is bumped and a migration is attached; a fixture holding a save - written at the previous version round-trips through load with both new fields defaulted - to zero. - - W113.7 Every regenerated replay fixture is named in the pull request with the cash delta - that explains it. -- **Out of scope:** separate arrears tracking or a bill-paying action for utilities and - transport; promoting `"vehicle"` from a reserved literal to a type; scaling utilities by - housing tier in engine code — the game's own baseline scales it per definition, as - authored numbers; the per-actor travel-time mechanism still deferred by [W109](#w109). - -### [x] W114 — The NPCs Are Actually There {#w114} - -**Delivers:** Puts every NPC a campaign declares into the running game from week one, carrying -the history an author gave them. Until now [W110](#w110)'s starting memories were validated and -then never used, because nothing created an NPC's runtime state at all. - -- **Spec:** [§7.7](10-simulation-kind.md#77-npcs--definition-and-runtime-state)'s `NPCState` - and its seeding rule, [§8.2](10-simulation-kind.md#82-collections-for-existscount-w1055)'s - `world.npcs` collection, and `90-decisions.md`'s 2026-09-15 entry routing it here. Closes - the NPC half of engine issue #425. -- **Touches:** the kind's `initialState`, its tests, and the golden snapshots of campaigns that - declare NPCs. -- **Depends on:** [W110](#w110). -- **Status:** Landed in [#495](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/495). -- **Done when:** - - W114.1 A new game holds one NPC state per NPC definition, in content order, keyed by the - definition's id, with its role, availability and starting memories taken from that - definition. - - W114.2 A campaign declaring no NPCs starts with an empty collection, as today. - - W114.3 Seeded state shares no array with the authored definition: changing an NPC's memories - or availability during play cannot reach back into content. - - W114.4 An NPC's own state never carries its definition's initial relationship. - - W114.5 Every moved golden snapshot or replay fixture is named in the pull request, and each - moves in `world.npcs` alone. -- **Out of scope:** seeding `world.locations`, which is blocked on campaigns whose starting - location has no definition and stays with #425's locations half; any end-of-week system - that reads or changes NPC state; projecting NPCs into the view. - -### Depth: The World-Graph Client Read Model - -One unit. `90-decisions.md`'s 2026-09-15 `/reconcile` found that -[12 §10](12-world-graph-kind.md#10-projection)'s Revision 5 view is contracted and marked -unbuilt, and routed its implementation here. It is the only item that entry routed to -`/slices` still unimplemented; its sibling, NPC seeding, landed as [W114](#w114). The third -entry of that date — a Tier 1 check for `where` fields a collection's item type does not -declare — went to `/contract` first, was specified on 2026-09-24, and is a separate unit, not -part of this one. - -### [x] W115 — A Resort a Client Can Actually Draw {#w115} - -**Delivers:** Lets anyone building a screen for a world-graph game — a resort map, an inspector -panel, a build menu — draw everything the player can see and offer every action the player can -take, from what the engine hands them alone. Today the engine hands over counts and ids, so a -client cannot place a building on a map, show a guest's mood, or explain why a building is -locked without reading the game's internals. - -- **Spec:** [12 §10](12-world-graph-kind.md#10-projection) — the Revision 5 `WorldGraphView`, - its *Derivation and ownership*, *Completeness invariants* and *Deliberate exclusions* - paragraphs, and the ordering rules; [§11](12-world-graph-kind.md#11-reason-codes) for the - `blockedBy` vocabulary; `90-decisions.md`'s 2026-09-15 entry routing it here. -- **Touches:** the kind's projector (`src/engine/src/kinds/world-graph/view.ts`) and the - `WorldGraphView` type in its state module, their tests, and any golden snapshot that - records a world-graph view. -- **Depends on:** none. -- **Status:** Not started. -- **Done when:** - - W115.1 The projected view carries every field §10's `WorldGraphView` declares, with the - declared types: `finances` gains `revenueTotalCents`/`expensesTotalCents`; the view gains - `scenario`, `definitions`, `staffOptions`, `constructionSites`, `guests` and `incidents`; - `map` carries `id`, name/description keys, `terrain`, `paths` and placed `scenery`, and - its `zones` are full `Zone` records rather than ids; each building carries position, - size, rotation, full queue, prices and inventory; each staff member carries position, - task and `tasksCompleted`; each alert carries `entityId`. `npm run typecheck` passes with - the §10 declaration in place of the old one. - - W115.2 The view contains none of §10's *Deliberate exclusions*: a test serializes the view - of the MVP campaign after at least one incident has occurred and asserts that none of - the state or campaign field names backing those exclusions — RNG state, - `nextEntityOrdinal`, incident weights and triggers, scheduled changes, failure and - objective conditions, placement rules, adjacency effects, guest preference profiles, - utility curves and scores, entity path caches, hidden achievements, unlock bookkeeping - — appears as a key at any depth. The pull request lists the exact key names checked. - - W115.3 For the MVP campaign, every build definition id the reducer accepts for `build`, - every role id it accepts for `hire_staff` and every product id it accepts for - `set_price` appears in `buildOptions`, `staffOptions` and `definitions.products` - respectively. Locked entries are present with `canBuild`/`canHire` false. - - W115.4 For every definition and role, `canBuild`/`canHire` is false exactly when submitting - it at a placement-valid position (or to a valid assignment) is rejected, and `blockedBy` - equals the set of placement-independent §11 codes the reducer returns — asserted for - at least `building_locked`, `building_limit_reached`, `staff_limit_reached` and - `insufficient_funds`, each driven by a state constructed to trigger it. - - W115.5 Every building, staff member and alert id that `demolish`, `fire_staff`, - `assign_staff`, `open_building`, `close_building`, `set_price` or `dismiss_alert` would - accept in a given state appears in the corresponding view collection, and no id the - reducer would reject with `unknown_entity` does. - - W115.6 `alerts` omits every alert with a non-null `dismissedAtTick` or `clearedAtTick`, and - `incidents` omits every incident with a non-null `resolvedAtTick`: a test dismisses one - alert and resolves one incident and asserts both disappear from the next projection. - - W115.7 `definitions` is the smallest safe catalog §10 names and no larger: `guestArchetypes` - equals the archetypes reachable from the current scenario's pool, `products` equals the - products referenced by `buildOptions`, and `incidents` equals the definitions of - currently unresolved incidents — each asserted by exact id list against the MVP - campaign. - - W115.8 Ordering follows §10: every definition and entity collection is sorted by id; - terrain cells, zone cells, spawn points and exits are row-major (`y`, then `x`); path - edges are sorted by `from` then `to`; rotations are numeric ascending; per-product and - per-meter value lists are sorted by definition id; staff requirements by role id; and - `queue.guestIds` equals the state's FIFO order unchanged. A test builds state whose - arrays are deliberately out of order and asserts each sorted result. - - W115.9 Projection is pure and non-aliasing: projecting the same state and campaign twice - yields structurally equal views, and mutating any array of a returned view leaves both - the state and a fresh projection unchanged. - - W115.10 No save or campaign schema moves: `kindVersion`, every committed save fixture and - every replay fixture are byte-identical before and after. Every moved golden snapshot is - named in the pull request and moves only inside a world-graph view. - - W115.11 §10's *Specified, not yet implemented* status paragraph is deleted from - `design/20-contract.md` in the same pull request, and the human docs are regenerated. -- **Out of scope:** exporting the helper types (`WorldGraphViewText`, `WorldGraphBuildOption`, - `WorldGraphStaffOption`, `WorldGraphViewMeterDefinition`, `WorldGraphViewValue`) from the - package root, which the contract does not list; per-building sales history or guest - thought history, which §10 says need a state-and-save change first; placement-dependent - build previews, which stay `previewAction`'s; any change to a client, including - SubZeroDev.Adventures; the simulation kind's `where`-field Tier 1 check. - -### Depth: The Simulation Campaign's Own Rules - -Four units. Two `/contract` passes routed work here, and neither has a unit yet. The 2026-09-24 -entry specified the Tier 1 check for `where` fields -([§8.2](10-simulation-kind.md#82-collections-for-existscount-w1055)), which [W115](#w115) named -as separate. The 2026-09-28 entry specified #524's campaign physics -([§7.14](10-simulation-kind.md#714-remaining-campaign-physics-524)): the numbers a campaign -cannot yet set. - -Ordered by risk. [W116](#w116) is fully contracted, depends on nothing, and closes a gap where a -campaign that loads can still throw during play. [W117](#w117) wires one field so it can test -the assumption all of §7.14 rests on: that a per-key default resolved at use reproduces today's -output byte for byte. If it cannot, #524's stop condition applies, and W118 and W119 should not -start. W118 covers the week's own rules and W119 the per-action prices. Neither W118 nor W119 -depends on the other. All three physics units wait on P8 (under *Contract Prerequisites* -above), the reason code §7.14's validation never named. - -### [x] W116 — A Campaign Cannot Ask About a Field Its Collection Lacks {#w116} - -**Delivers:** As a campaign author, a condition that asks about a property its collection's -items do not have is rejected when the campaign loads, and so is a comparison on an optional -property that would crash mid-game. The rejection says which of the two went wrong. Today the -first quietly matches the wrong things and the second fails during play. - -- **Spec:** [10 §8.2](10-simulation-kind.md#82-collections-for-existscount-w1055), - [§10](10-simulation-kind.md#10-reason-codes) and - [§14](10-simulation-kind.md#14-validation); `90-decisions.md`, 2026-09-15 and 2026-09-24. -- **Touches:** the simulation validator and reason-code registry, and their tests; the §8.2 - status paragraph and §10 markers in `design/20-contract.md`; the regenerated human docs. -- **Depends on:** none. -- **Status:** Not started. -- **Done when:** - - W116.1 For each of §8.2's eight collections, a test shows a `where` `field` naming a scalar - property of the collection's item type passing Tier 1. The same test shows a field the - item type does not declare failing with `unknown_collection_field`. - - W116.2 Each of the following fails with `unknown_collection_field`: - - a dotted path against `world.npcs` into `memories`, `availability` or `flags` (for - example `flags.met`); - - a one-segment field naming a property of array or object type. - - W116.3 The check reaches every `field` leaf at any depth under `all`, `any` and `not`. A - `where` nested two or more levels deep is rejected for its innermost bad field. - Inside a `where`, a nested `exists` or `count` is checked against its own collection's - item type, not the outer one: a field legal only on the outer type is rejected there. - - W116.4 An optional property passes under `equals`, `not_equals`, `in` and `not_in`. It fails - with `optional_field_operator` under each other operator, with one test case per - operator. - - W116.5 The engine's table of legal fields is typed against each item type's properties, so - leaving a property out is a compile error. A type-level test proves this, for example - with `@ts-expect-error`. - - W116.6 Both codes are registered in `reasons.ts` with messages. - - W116.7 Every committed simulation campaign, including Stable Life and every test campaign, - loads with no new finding. The pull request states how many rejecting cases and how many - passing cases the new tests contain. - - W116.8 In the same pull request: - - §8.2's *Status: contracted, not built* paragraph is deleted; - - both "contracted, not built" markers in §10's Tier 1 table are deleted; - - the human docs are regenerated. - - `./build/Test-Documentation.ps1` passes, and `npm run typecheck`, `npm run lint` and - `npm test` pass from `src/engine/`. -- **Out of scope:** - - An operator that does not fit a *present* field's type, for example `has_tag` against a - number. §8.2 leaves this out explicitly. - - Any change to the runtime condition evaluator, which stays non-throwing. - - Collection-name validation, which is [W111](#w111)'s. - - Condition validation in other kinds. - -### [x] W117 — Two Campaigns, One Lever, Different Weeks {#w117} - -**Delivers:** As a game author, I can set how fast each need drains per week in my own campaign. -Two campaigns that differ only in that setting play out differently. Every campaign that does not -set it plays exactly as it does today. - -- **Spec:** [10 §7.14](10-simulation-kind.md#714-remaining-campaign-physics-524) and - [§14](10-simulation-kind.md#14-validation); `90-decisions.md`, 2026-09-28; - [#524](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/issues/524). -- **Touches:** the simulation campaign and source types, the source builder, the validator, and - the `needs` end-of-week system; their tests and synthetic fixtures. -- **Depends on:** P8 under *Contract Prerequisites* above, the reason code for an invalid - physics value. -- **Status:** Not started. -- **Done when:** - - W117.1 `needDriftPerWeek` is declared on `SimulationCampaign` and `SimulationCampaignSource` - exactly as §7.14 states. The builder copies it only when it is supplied: - - an omitted field stays absent from the built campaign; - - a partial record keeps only the keys supplied. - - W117.2 The `needs` system resolves each need key at the point of use: - - a supplied key takes the campaign's value; - - an omitted key, or an omitted field, takes §7.14's default (health −1, energy −3, - happiness −2, satiety −4, stress +2); - - an explicit `0` for a key leaves that need undrifted. - - W117.3 Take two synthetic campaigns that differ only in `needDriftPerWeek: { satiety: -5 }` - versus omission. Give them the same seed, the same `end_week` log, and a starting satiety - away from either clamp. After one `end_week`: - - base satiety differs by exactly 1; - - the `need_drift` change for satiety differs by exactly 1; - - every other need and every other change is identical. - - W117.4 The W117.3 divergence is identical on a repeat run, and across a save/load cut taken - mid-log: `serialize()` output matches the uncut run. - - W117.5 A campaign that omits the field is byte-identical to the committed replay and golden - oracles, and no committed fixture file changes. - - W117.6 Tier 1 rejects a non-finite delta, a non-integer delta and an unknown need key. Each - rejection uses P8's code and a path naming the key. The pull request states how many - rejecting cases and how many passing cases the tests contain. - - W117.7 `npm run typecheck`, `npm run lint` and `npm test` pass from `src/engine/`. -- **Out of scope:** - - Every other §7.14 field. - - Any change to a default number, or to Stable Life's content. - - Balancing, which is game-side (`90-decisions.md`, 2026-09-27). - -### [x] W118 — The Week's Own Rules Belong to the Campaign {#w118} - -**Delivers:** As a game author, I can set my campaign's weekly time budget, late-fee rate and -eviction ladder. I can also set how job performance drifts and how much each event shifts the -world's strangeness. A save made under one ladder still loads and progresses under another. - -- **Spec:** [10 §7.14](10-simulation-kind.md#714-remaining-campaign-physics-524) and - [§14](10-simulation-kind.md#14-validation); `90-decisions.md`, 2026-09-28. -- **Touches:** the simulation campaign and source types, the source builder and the validator; - the initial state and start-of-week time budget; the `finance_reconcile`, `employment` and - `events` end-of-week systems; their tests and synthetic fixtures. -- **Depends on:** P8, and [W117](#w117), whose omission oracle this extends. -- **Status:** Not started. -- **Done when:** - - W118.1 These six fields are declared on the campaign and source types as §7.14 states: - `weeklyTimeUnits`, `lateFeeBasisPoints`, `evictionStages`, `performanceDriftRate`, - `performanceWorkBonus` and `strangenessPerEvent`. The builder copies each only when it - is supplied. - - W118.2 Each field has a fixture pair that differs in that field alone. Each pair diverges - only on the path that field governs: - - the week's time budget; - - the late fee in cents; - - the eviction stage; - - employment performance; - - `strangenessBase`. - - W118.3 Under a shortened ladder `none, penalty, evicted`: - - a missed week advances one listed stage; - - a loaded state at the omitted `warning` stage advances to `penalty`; - - `evicted` stays terminal; - - `pay_bills` still resets the stage to `none`. - - W118.4 With `performanceDriftRate: 0`, a week without work leaves performance unchanged. With - `1`, such a week moves it to the job's `weeklyDriftToward`. Rounding happens once, as - §7.14 states. - - W118.5 Tier 1 rejects each of the following, using P8's code and a path: - - a `weeklyTimeUnits` that is not a positive integer; - - a negative or non-integer `lateFeeBasisPoints`; - - a `performanceDriftRate` that is non-finite or outside [0, 1]; - - a non-integer bonus or strangeness step; - - a stage sequence that does not start at `none`, does not end at `evicted`, repeats a - stage, breaks canonical order, or names an unknown stage. - - The pull request states how many rejecting cases and how many passing cases the tests - contain. - - W118.6 A campaign that omits all six fields is byte-identical to the committed replay and - golden oracles. Default paths keep their `StateChange` order and shape, and no committed - fixture file changes. - - W118.7 `npm run typecheck`, `npm run lint` and `npm test` pass from `src/engine/`. -- **Out of scope:** - - Per-action time costs and need deltas, which are [W119](#w119)'s. - - Any change to the persisted `EvictionStage` union. - - Any change to a default number, and any balancing. - -### [x] W119 — Every Fixed Action Price Is the Campaign's {#w119} - -**Delivers:** As a game author, I can price every fixed-cost action in time, including actions -that are free today. I can also set what eating, resting and exercising do to needs, how much -socializing moves a relationship, and how fast applications, raises, study and projects -progress. Every action I leave alone costs exactly what it costs today. - -- **Spec:** [10 §7.14](10-simulation-kind.md#714-remaining-campaign-physics-524) and - [§14](10-simulation-kind.md#14-validation); `90-decisions.md`, 2026-09-28. -- **Touches:** the simulation campaign and source types, the source builder and the validator; - the action resolvers named by §7.14's `actionTimeCosts` keys and per-action fields; their - tests and synthetic fixtures. -- **Depends on:** P8, and [W117](#w117). -- **Status:** Not started. -- **Done when:** - - W119.1 These fields are declared on the campaign and source types as §7.14 states: - `actionTimeCosts`, `eatNeedDeltas`, `restNeedDeltas`, `exerciseNeedDeltas`, - `socializeAffinityGain`, `socializeTrustGain`, `applicationResolveWeeks`, - `negotiateRaiseBasisPoints`, `studyUnitsPerSession` and `projectProgressPerSession`. The - builder copies each only when it is supplied, and keeps partial records partial. - - W119.2 A table-driven test covers all twenty-six `actionTimeCosts` keys. For each, an omitted - key reproduces today's calculated time cost and audit shape exactly. - - W119.3 When a zero-default action such as `eat` is given a positive price: - - it checks available time, and fails with `insufficient_time` when time is short; - - it emits the spent-time change; - - `apply` consumes exactly the calculated value. - - An explicit `0` keeps today's shape. `eat` and `rest` still omit `calculatedTimeCost` - and the spent-time change; the other zero-default actions still report - `calculatedTimeCost: 0`. - - W119.4 Need-delta records resolve per key: supplying one `exerciseNeedDeltas` key changes - only that need. `restNeedDeltas.stress` is a signed delta, and its default of −5 lowers - stress. - - W119.5 Each of the six scalar fields has a fixture pair that differs in that field alone, and - each pair diverges only on the path that field governs. These action costs are unchanged - by every new field: - - `work`; - - `travel`; - - `maintain_item`; - - `work_on_project`; - - `respond_to_event`. - - W119.6 Tier 1 rejects each of the following, using P8's code and a path: - - a negative or non-integer action cost; - - an unknown action key or an unknown need key; - - a non-integer need delta or gain; - - a resolve-week, study or project value that is not a positive integer; - - a negative or non-integer raise in basis points. - - The pull request states how many rejecting cases and how many passing cases the tests - contain. - - W119.7 A campaign that omits every field is byte-identical to the committed replay and golden - oracles. Default paths keep their `StateChange` order and shape, and no committed fixture - file changes. `npm run typecheck`, `npm run lint` and `npm test` pass from `src/engine/`. -- **Out of scope:** - - The identity constants §7.14 lists as non-levers: `NO_MONEY_COST`, `NEW_ITEM_CONDITION`, - `NEW_ITEM_QUANTITY` and `INVESTMENT_INTEREST_RATE`. - - The repair formula. - - Content-owned costs. - - Any change to a default number, and any balancing. - -### Depth: Campaign Composition - -One unit. The 2026-10-07 entry for #293 and #292 decided that content crosses a campaign -boundary by composition at registry build. Its follow-up, #561, contracted the result. That -contract covers the core envelope ([04 §10.4](04-core.md#104-campaign-composition)), the -story-graph merge ([03 §1.1](03-story-graph-kind.md#11-modules-includes-and-attachments)) and -pack attachments ([11 §3a](11-content-packs.md#3a-attachments--reaching-into-a-campaign-a-pack-does-not-own)). -It is one unit because no part of it can be played without the others. An include with no merge -composes nothing, and a merge with no core envelope has nothing to call it. - -### [x] W120 — One Campaign Can Include Another {#w120} - -**Delivers:** As a campaign author, I can include another story-graph campaign as a module. -I name its exits and bind its inputs and outputs. As a pack author, I can attach a module to a -choice node in a campaign my pack does not own. Either way the player plays one campaign, in one -session, and the save format does not change. - -- **Spec:** [04 §3](04-core.md#3-the-kind-interface--the-seam), - [§10.4](04-core.md#104-campaign-composition), [§11](04-core.md#11-tiered-validation), - [§12](04-core.md#12-reason-codes-state-changes-messages), - [§17](04-core.md#17-identifier-conventions) and C17; - [03 §1.1](03-story-graph-kind.md#11-modules-includes-and-attachments), - [§5](03-story-graph-kind.md#5-consequences--typed-effects), - [§8.3](03-story-graph-kind.md#83-reason-codes) and - [§11](03-story-graph-kind.md#11-validation-story-graph-specific); - [11 §3a](11-content-packs.md#3a-attachments--reaching-into-a-campaign-a-pack-does-not-own), - [§6](11-content-packs.md#6-identity-and-why-determinism-needs-it) and - [§7](11-content-packs.md#7-validation); `90-decisions.md`, 2026-10-07 (both entries); - [#561](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/issues/561). -- **Touches:** the `Kind` interface and the core reason-code registry; the campaign and pack - types, `resolvePacks` and the pack digest; both validated registry entry points; the - story-graph kind's campaign and source types, source builder, consequences, interpolation, - validator and reason codes; their tests and synthetic fixtures; the regenerated human docs. -- **Depends on:** none. -- **Status:** Done — [PR #568](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/568). -- **Done when:** - - W120.1 These types are declared as 04 §10.4, 11 §3a and 03 §1.1 state: - - `CampaignRef`, `CampaignInclude`, `CampaignAttachment`, `ComposedModule` and - `ComposedAttachment`; - - `Campaign.includes`, `ContentPack.attachments` and `ResolvedRegistry`; - - `Kind.composeContent`; - - `ModuleInterface`, `StoryGraphIncludeBinding` and `StoryGraphAttachment`. - - `resolvePacks` returns the authored version of each campaign beside the stamped one. - - W120.2 Both `buildValidatedContentRegistry` and `buildValidatedPackRegistry` compose before - they validate, in the order §10.4 fixes. Composition is depth-first, and each campaign is - composed once. A three-level chain composes to ids of the form `a::b::x`. Two hosts that - include the same module get the same composed module. - - W120.3 Each of the seven core codes is rejected by at least one fixture, with a path: - `include_missing`, `include_version_mismatch`, `include_kind_mismatch`, `include_cycle` - (one direct cycle and one transitive cycle), `include_alias_collision`, - `attachment_host_missing` and `compose_unsupported`. In the packed path, a pin that names - the authored version passes. A pin that names the stamped resolution id fails. - - W120.4 Each of the six story-graph codes is rejected by at least one fixture, with a path: - `include_not_module` (once with no `module` block, once for a module that reads - `ending`), `exit_unmapped`, `binding_undeclared`, `binding_type_mismatch`, - `attachment_node_not_choice` and `attachment_choice_collision`. - - W120.5 An authored id that contains `:` is still rejected with `invalid_identifier`. This - holds for a node, a variable, an achievement and an alias. A composed id is accepted. - - W120.6 The `copy` consequence writes one declared variable from another, reading `from` as - earlier consequences in the same list left it, as 03 §5 states. Tier 1 rejects an - undeclared `from` with `undeclared_variable`. It rejects a `from` that cannot be assigned - to `var` with `invalid_consequence_value`, and an `enum` whose values are not all among - the target's is one such case. - - W120.7 Play a host that includes a module through its entry, through the module, out of - one exit and on to a host ending. The run is deterministic on a repeat, and also across a - save/load cut taken inside the module: `serialize()` output matches the uncut run. Each - of the following is shown: - - the entry and the exit each cost one `turn`; - - `visited.` counts entries; - - a bound input is copied in on every entry; - - a bound output is copied out on exit; - - interpolation in module text reads the module's own variable; - - a module achievement unlocks into the host's `unlockedAchievements`. - - W120.8 A pack attachment appends one choice to a host choice node, after every authored - choice and in the order §10.4 fixes. The attached choice reaches the module. An - attachment on a module reaches every host that includes it. - - W120.9 A pack with no includes and no attachments keeps its committed `campaignVersion` - digest. Every committed campaign, fixture, replay oracle and golden file is unchanged. - A pack that adds an attachment gets a different digest. - - W120.10 Every new code is registered with a message, in `reasons.ts` and in the - story-graph kind's reason table. The pull request states how many rejecting cases and - how many passing cases the new tests contain. `npm run typecheck`, `npm run lint` and - `npm test` pass from `src/engine/`. -- **Out of scope:** - - Carrying `Campaign.includes` in the portable format, which is its own decision. - - Runtime call/return frames, and any change to `kindState`, the envelope or the save format. - - An attachment that does anything other than append a choice. - - `composeContent` for the simulation and world-graph kinds. +## Carried Forward + +Open items the retired bodies carried that were never sliced. They are moved here so that none is +lost. None is sliceable as written: each needs a contract or design decision first. + +- **Session capture** ([`08-session-capture.md`](08-session-capture.md)). The hosting gate is met: + [SubZeroDev.Adventures](https://github.com/The-Running-Dev/SubZeroDev.Adventures) is a hosted + deployment. What blocks it is a missing contract surface. [08 §3.2](08-session-capture.md#3-what-is-refused) + keeps only the parameters a kind declares, and no kind declares its parameters anywhere the core + can read. Either the `Kind` interface gains a declared-parameter surface, or §3.2 is restated + against something a host can enforce. +- **Hosted engine edge.** The `.NET Platform edge → Node engine workload` process boundary, proposed + to follow W62: a generated JSON/HTTP service contract first, MCP as a projection, and one + in-memory remote session before persistence, accounts, catalogue or metering. +- **More clients**, such as Discord. +- **Content tooling not yet built.** The visual node editor needs a design decision on where it + lives, what it emits, and whether authored source or the built campaign is its file format. + Content balancing tools are unbuilt. AI authoring assistants need a dependency decision, and + W77's author-facing validator is their precondition. +- **Experiment attribution.** [06 §4](06-extensibility.md#4-the-composition-root) settles + `SessionHost.experiments` as the already-resolved, non-null assignment map. The declaration and + record-stamping implementation were last recorded as not yet brought into line with it. +- **Simulation balance.** Drift rates, scenario economics, `demandBand` thresholds and the + housing-quality formula are placeholder numbers. Balancing is game-side; the engine owes the + levers, which [§7.14](10-simulation-kind.md#714-remaining-campaign-physics-524) contracts and + W117–W119 shipped. +- **Three `docs-template` hardening findings, to raise upstream**, from the automated review of + [PR #3](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/3): the docs workflows pin + the mutable `:latest` tag + ([comment](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/3#discussion_r3660515997)); + the link validator does not constrain resolved targets to the repository root + ([comment](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/3#discussion_r3660516002)); + and file enumeration walks excluded trees before filtering them + ([comment](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/3#discussion_r3660516006)). +- **The world-graph programme gate.** An immutable published package version must carry the + replay-guarded third kind, so Sun Trap can install it without a sibling checkout. `v0.10.0` is + tagged; publication is an external release action this ledger does not evidence. +- **The games' own Definitions of Done** live with the games: + `games/life-in-the-fast-lane.md` and `games/bulgaria-adventure.md` in + [SubZeroDev.GameOfLife](https://github.com/The-Running-Dev/SubZeroDev.GameOfLife). ## Landed -A slice's full body lives under `## Outstanding` only until its issue closes; once closed, the -body retires here to a single row — name, closing issue, criteria range, and the commit the body -was last complete at. Nothing here yet. +Ordered by id. *Criteria* is the range of numbered acceptance +criteria; units sliced before criteria were numbered show a dash. *Merge* is the merge commit of +the last pull request listed. -| Slice | Name | Issue | Criteria | Commit | +| Slice | Name | Pull requests | Criteria | Merge | |---|---|---|---|---| +| W0 | CI and Documentation Gates | [#3](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/3), [#6](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/6) | — | [`98ccee9`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/98ccee99813d61b514f1048b87cce45dadb91776) | +| W1 | Core Contract Types and Module Skeleton | [#17](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/17) | — | [`f7d8f59`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/f7d8f598ef7e6591aa2184285bf5bc25a4525345) | +| W2 | RNG Handle and Stream Derivation | [#22](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/22) | — | [`9f90eae`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/9f90eae1d8a00b95286da3ef99a64d128e7007dc) | +| W3 | Pure Engine Kernel | [#33](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/33) | — | [`7a46e57`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/7a46e5720c45d9b1c57d1958f875302ee6499ac9) | +| W3a | Observability: Emitter, Events, and Sinks | [#34](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/34) | — | [`4567e43`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/4567e432a8eae80d2b0fb30f11427759875d1240) | +| W4 | Registry, Authoring Builder, Localization | [#35](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/35) | — | [`a7e9dc3`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/a7e9dc3e1d60ad6ab792011c0ea424fbadc1e1ab) | +| W5 | Tiered Validation | [#36](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/36) | — | [`71ff9e8`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/71ff9e8858acc5de41aa099793e043021985ac30) | +| W6 | Projection | [#37](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/37) | — | [`b4d2af9`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/b4d2af9575694a3bd1246b88c784ce48849af9e8) | +| W7 | Session Store | [#38](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/38) | — | [`10b4ce9`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/10b4ce9f96836741651399c0b332023fc1530317) | +| W8 | Profile Store | [#39](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/39) | — | [`d9af5d8`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/d9af5d8dbd0bc88829c8d43571aee92b874b2536) | +| W9 | Variables and Consequences | [#41](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/41) | — | [`9359a12`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/9359a12d86014b8d6cbd9648ebbf07dfdfabba01) | +| W10 | Conditions and Requirements | [#43](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/43) | — | [`388f2dd`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/388f2dd4a2515cc414d30d2bb419c214acdfbbe4) | +| W11 | Nodes, Turn, and Settle | [#44](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/44) | — | [`6d85917`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/6d8591728194881e34c1c6b53147b42cc20dbe0f) | +| W12 | Scene, Actions, Projection, Reason Codes | [#47](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/47) | — | [`9eb400a`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/9eb400ac9da2083fec4f17d699aad7cb74bff07f) | +| W13 | Endings and Achievements | [#51](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/51) | — | [`0fb7cff`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/0fb7cffe499b914c29250b8b817e5e348f2b0595) | +| W14 | Story-Graph Validation | [#55](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/55) | — | [`231683f`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/231683ffc50ef31e190c48612dfb7007f7e8f0dd) | +| W15 | The Bureaucracy Campaign and Broken Fixtures | [#60](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/60) | — | [`75fc64a`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/75fc64a4887a026c6b5e46641540013f8f94aa58) | +| W16 | Text Client | [#63](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/63) | — | [`5d6e418`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/5d6e418ba435482262adf3fddecfbaf1cb0e44b3) | +| W17 | MCP Server | [#66](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/66) | — | [`74d81b7`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/74d81b7912b574c83ed0bc9ac3716a1b1dcd53ed) | +| W18 | Determinism Harness | [#70](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/70) | — | [`bf106d6`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/bf106d657295c4cc3c3d958c420f3be08a1dfd61) | +| W19 | MVP Acceptance | [#71](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/71) | — | [`dcb7803`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/dcb78038cbe8bb8e002f0651c9551b47ff874bb5) | +| W20 | Engine Versioning and Release Tags | [#73](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/73) | — | [`e26fa9d`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/e26fa9dbc9e1a2814443bfff952a15562b608dc8) | +| W21 | Replay Oracle: Outcome and the Runner | [#73](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/73) | — | [`e26fa9d`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/e26fa9dbc9e1a2814443bfff952a15562b608dc8) | +| W22 | Replay Oracle: The Corpus | [#73](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/73) | — | [`e26fa9d`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/e26fa9dbc9e1a2814443bfff952a15562b608dc8) | +| W23 | Replay Oracle: CI Wiring | [#73](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/73) | — | [`e26fa9d`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/e26fa9dbc9e1a2814443bfff952a15562b608dc8) | +| W24 | Core Spec Reconciliation | [#77](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/77) | — | [`f203325`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/f20332518331a6bd1714dcbd44156db7ad592d65) | +| W25 | Simulation Kind Seam Reconciliation | [#80](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/80) | — | [`399391b`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/399391b8e12a393fe11ec2aa4f67252168507b29) | +| W26 | Toolchain Upgrade | [#82](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/82) | — | [`0d3cbb1`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/0d3cbb1f693113ab65dcf360fbf7d4f95d277206) | +| W27 | Bulgaria Adventure: The Driving Arc | [#86](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/86) | — | [`25a9411`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/25a9411e3afea417e3235d439dc5b7853605e98a) | +| W28 | Bulgaria Adventure: The Return Arc | [#87](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/87) | — | [`c186f4d`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/c186f4d02b3ccfe3fa5a77d650890b058b5973db) | +| W29 | Bulgaria Adventure: The Inheritance Arc | [#88](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/88) | — | [`265a1ca`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/265a1ca95ba0766405341bca20d04aad86d9c049) | +| W30 | Bulgaria Adventure: The Enterprise Arc | [#89](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/89) | — | [`d8d8821`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/d8d8821e6d8ae8965bedf9cea45d65036dbd2c66) | +| W31 | Save Migration | [#92](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/92) | — | [`588567d`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/588567d2d4ff84f7b38a904b62c586b315122f07) | +| W32 | Simulation Kind: State Types | [#94](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/94) | — | [`fc1b29b`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/fc1b29b06a034ece06eab2735cdeb263cf9d5368) | +| W33 | Simulation Kind: Actor State | [#95](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/95) | — | [`2f0b7ea`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/2f0b7ea3a3079d7ba38b5c97f5a7a1713ae59cc0) | +| W34 | Simulation Kind: Content Definition Types | [#96](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/96) | — | [`9f157cb`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/9f157cbdb768863601e4e462b7ce291b3d4db5de) | +| W35 | Simulation Kind: Resolution and Systems | [#97](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/97) | — | [`f1d7a21`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/f1d7a21cc2cd1bc29d777c9ebf11086cdd722fb6) | +| W36 | Simulation Kind: State, Variables, and the Plan | [#98](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/98) | — | [`4c80b05`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/4c80b054e344576504af40affe3d27d17c957ff3) | +| W37 | Simulation Kind: The Week | [#99](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/99) | — | [`0815702`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/08157023d778a792fab225f3a827b0d3d63922e7) | +| W38 | Simulation Kind: Content-Definition Types | [#100](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/100) | — | [`bdd07d6`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/bdd07d6ba14a08cc10ed01847c384c144641468c) | +| W39 | Simulation Kind: Wiring the "Stable Life" Vertical Slice | [#101](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/101) | — | [`1206daf`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/1206dafcbaf1556bba56e4b12ab27a58c3b45d0b) | +| W40 | Simulation Kind: The "Stable Life" Scenario, Validation, and Corpus | [#102](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/102) | — | [`9fdf77c`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/9fdf77c63773ea0bc0ffc288fbba0995ee04c3ff) | +| W41 | Engine Consumer Boundary | [#108](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/108) | — | [`db9c62a`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/db9c62aec509ed083179a73ae2ec49b1b53d3d26) | +| W42 | World-Graph Runtime State Contract | [#116](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/116) | — | [`5f5f8f5`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/5f5f8f542f619e70b2a15d8952998e48d12766d1) | +| W43 | World-Graph Content Definition Contract | [#119](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/119), [#121](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/121) | — | [`4ab8b0a`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/4ab8b0a73928a3320491c79afbd58837ca9e647a) | +| W44 | World-Graph Resolution Contract | [#120](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/120) | — | [`5eca57b`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/5eca57b9dc50b939fe55d7ef7a06edac9f38c346) | +| W45 | World-Graph Kind Skeleton and Immediate Actions | [#125](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/125) | — | [`c6662bb`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/c6662bb6b638e70348a133f8c028b2e18f26963c) | +| W46 | World-Graph Deterministic Tick Pipeline | [#128](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/128) | — | [`6301a49`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/6301a497cc956c4e47df584c7b0b9d3c0455b71c) | +| W47 | World-Graph MVP Vertical Slice | [#131](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/131) | — | [`2390750`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/23907505b64042b9dbc744ba4edac0c53122c673) | +| W48 | Preview/Client Parity | [#133](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/133) | — | [`c81227d`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/c81227dca661e028cd8b50ab7fbcffe9be09c2f6) | +| W49 | World-Graph Validation, Scenario and Replay Guard | [#134](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/134), [#138](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/138) | — | [`6e3d38e`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/6e3d38e77d0e34686dc4e0956b2ff01da9b8af3d) | +| W50 | Simulation Kind: Projection and Client Parity | [#166](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/166) | W50.1–W50.8 | [`bb97138`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/bb971389a7fa888d1937d8390b2a24872dbbf0d9) | +| W51 | Simulation Kind: Derived Values, Modifiers, and Effects | [#194](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/194), [#204](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/204) | W51.1–W51.6 | [`6b573d3`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/6b573d314bde4ce5c8466da49457a44f7b63bca5) | +| W52 | Simulation Kind: The Scenario Campaign and Full Validation | [#227](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/227) | W52.1–W52.6 | [`a9b7307`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/a9b73078a0db1e69f9351a3c305fc9a25a2bdf96) | +| W53 | Simulation Kind: Employment and Income | [#228](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/228) | W53.1–W53.6 | [`4034606`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/403460677a6f3f4c4e5644e69f8a7040c07a1e92) | +| W54 | Simulation Kind: Education and Skills | [#230](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/230) | W54.1–W54.6 | [`bd163f9`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/bd163f91c58412013084c3829306a63d0d346f5d) | +| W55 | Simulation Kind: Housing, Debt, and Reconciliation | [#232](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/232) | W55.1–W55.6 | [`1e63ce5`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/1e63ce57e924724399f6077ced701fbef876ccd2) | +| W56 | Simulation Kind: Possessions, Places, and People | [#236](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/236) | W56.1–W56.6 | [`e20cfdb`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/e20cfdbd5318ed08ea333695ba927de7cee26136) | +| W57 | Simulation Kind: Events, Opportunities, Headlines, and Achievements | [#265](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/265) | W57.1–W57.7 | [`9d69704`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/9d69704dc45197fab7632aebc6654d16050d221d) | +| W58 | Content Pack Resolution and Content Identity | [#238](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/238) | W58.1–W58.8 | [`1dda794`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/1dda794480d671c4ae59f177e1304a6d719dd952) | +| W59 | Experiment Gates and the `ExperimentSource` Port | [#239](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/239) | W59.1–W59.6 | [`fffe42e`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/fffe42e79641b1ad9994eb7ca2bad9a8d77e2606) | +| W60 | A Second Locale, End to End | [#240](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/240) | W60.1–W60.5 | [`214a6cc`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/214a6cc7f51b19893299654060c8f53c1df081a3) | +| W61 | Public Playable Web Demo | [#183](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/183), [#184](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/184) | W61.1–W61.10 | [`cd75c37`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/cd75c3783bd8afd75cacae6a699e6ae4906f2488) | +| W62 | Platform Static Host Image | [#264](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/264) | W62.1–W62.9 | [`6013c69`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/6013c69fe0174374bd1182aa66c46cc3a70a224b) | +| W63 | Absurd Game Interface | [#188](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/188), [#190](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/190) | W63.1–W63.10 | [`a94da4b`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/a94da4bf5eb5bdbd108c4e9b64abb6f1322592a3) | +| W64 | Replayable Story Campaign Expansion | [#189](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/189), [#191](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/191) | W64.1–W64.13 | [`c1db016`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/c1db0160acb047a70fb93612d7951256e2d58fe8) | +| W65 | Browser Test Harness for the Site | [#201](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/201) | W65.1–W65.8 | [`8e9378d`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/8e9378d82c52f692b3ce6acae798137f289f6bab) | +| W66 | The Play Surface on a Phone | [#205](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/205) | W66.1–W66.12 | [`a115098`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/a115098a131b3d5a511048bfc27391325d937c9b) | +| W67 | Restore the Story-Graph Regression Evidence | [#261](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/261) | W67.1–W67.7 | [`ce90041`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/ce900416b948ed268b236f67584066878f778acc) | +| W68 | Make the Browser Save Adapter Actually Restore | Cancelled — [#271](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/271) | W68.1–W68.7 | — | +| W69 | Consume the Reusable Landing-Page Package | [#272](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/272) | W69.1–W69.8 | [`38b7c34`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/38b7c34eaddf2c35d8fbbaa7c5d8e3da30451e6d) | +| W70 | Gate `/play/`'s Startup Request Surface | Cancelled — [#271](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/271) | W70.1–W70.6 | — | +| W71 | The Bulgaria Culture Pack: Mechanism and Voice | [#314](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/314) | W71.1–W71.6 | [`f0d1fee`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/f0d1fee3ef28b4d4eab3bd9033795cf7de8cd6d9) | +| W72 | The Bulgaria Culture Pack: The Full Setting | [#317](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/317) | W72.1–W72.5 | [`66075df`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/66075df095bc61d3cb9528ce0caaabdf11cdd547) | +| W73 | Tier 3 Validation as an Author-Facing Check | [#319](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/319) | W73.1–W73.6 | [`1bdb5b6`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/1bdb5b6a40e56fe70ba8faebd3f1e7b33b23ddf5) | +| W74 | Campaign Content Ownership: The Authoring Seam | [#298](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/298), [#299](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/299), [#332](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/332) | W74.1–W74.8 | [`cbbe0cd`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/cbbe0cde444788050e0a9def90b7fd9bf450b607) | +| W74a | The Bureaucracy Fixture, Frozen Byte-for-Byte | [#333](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/333) | W74a.1–W74a.5 | [`7cb09a0`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/7cb09a07a51514deef178b4273481d8a34dd8e74) | +| W74b | Retire the Engine's Own Play Surface | [#335](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/335) | W74b.1–W74b.6 | [`5c26ab0`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/5c26ab0530b4746b69938735c8c80710feebb7ff) | +| W74c | The Breaking Ownership Release | [#336](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/336) | W74c.1–W74c.7 | [`3fee866`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/3fee866fbaac51b90b9f7a297f320f1062a71456) | +| W75 | Classified Persistence Conflicts | [#311](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/311) | W75.1–W75.7 | [`6f5adb4`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/6f5adb47531fb1929d0e03c98b573cf3abb7fe0c) | +| W76 | A Folded Registry Keeps Its Resolution | [#337](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/337) | W76.1–W76.10 | [`b1cd9be`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/b1cd9bed82fb32daf31707d144d84f8e2164066a) | +| W77 | Tier 1 and Tier 2 as an Author-Facing Check | [#338](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/338) | W77.1–W77.7 | [`6dd7b4f`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/6dd7b4f4453b5856a3f9e43e5a52931921b63dd6) | +| W78 | Localization Coverage and String Extraction | [#340](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/340) | W78.1–W78.6 | [`77e35c7`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/77e35c7761864a2b05585dd6b282d2b4c89b5b65) | +| W79 | What Changed Between Two Resolutions | [#342](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/342) | W79.1–W79.6 | [`b6f8bb2`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/b6f8bb288452b804022f070026f84d13f6b4a98f) | +| W80 | Seeing a Story Graph | [#343](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/343) | W80.1–W80.6 | [`1ba588e`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/1ba588e30eb0140a02754f804bf85125fd6c16d9) | +| W81 | Construction Finishes What `build` Starts | [#345](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/345) | W81.1–W81.10 | [`b589b6c`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/b589b6c58f5c10b28dddd5579686b63c8ed7d79b) | +| W82 | A Kiosk That Ran Out Can Be Refilled | [#347](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/347) | W82.1–W82.8 | [`aa56f77`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/aa56f7799d3a8d311d9b8f2378c85d61a3414cf0) | +| W83 | Buildings Get Dirty, Wear Out, and Break | [#348](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/348) | W83.1–W83.7 | [`04c1baf`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/04c1baf19f93f740e01b54a38925e7edbabe3dcd) | +| W84 | Incidents That Happen On Their Own | [#360](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/360) | W84.1–W84.9 | [`8fdded3`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/8fdded3afdacf133fbdf8d23dd32d07e17082125) | +| W85 | The Resort Tells You What Needs Attention | [#361](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/361) | W85.1–W85.10 | [`3ea878e`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/3ea878e0d318f498646620a28f6b59f268606f10) | +| W86 | The Story-Graph Kind Says Why | [#362](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/362) | W86.1–W86.7 | [`450935c`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/450935cc1b0a24b09a98b5d9476890e36192a1e4) | +| W87 | The World-Graph Kind Says What Its Ticks Did | [#363](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/363) | W87.1–W87.7 | [`24641e0`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/24641e0e625b3d96d41c5b73640d8be9a35dd9b0) | +| W88 | The Simulation Kind Nobody Outside the Package Can Use | [#367](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/367) | W88.1–W88.6 | [`8115478`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/8115478aa9caefa5d53596c32f3348e6167601c0) | +| W89 | A Game-Length Life | [#376](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/376) | W89.1–W89.8 | [`71e7364`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/71e7364c85cb68e0115a95b9c960263dbb0ed9bb) | +| W90 | Canonical and Public Truth | [#377](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/377) | — | [`274fa6e`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/274fa6e492defd5634a6c35972b47a199802366d) | +| W91 | Reproducible Package and Release Gate | [#379](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/379) | — | [`63a0c12`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/63a0c1231f582f2b05632a11a37188e3b8fd1537) | +| W92 | Local and CI Gate Health | [#378](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/378) | — | [`fab72b1`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/fab72b1cd3fed40d71d80fd7144863543b2d5fcb) | +| W93 | World-Graph Spatial and Audit Correctness | [#398](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/398) | W93.1–W93.6 | [`f0735fc`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/f0735fc59fcfd879911771fefdeb1f41cc90f252) | +| W94 | Simulation Resolution Correctness | [#400](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/400) | W94.1–W94.7 | [`e103de6`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/e103de63996c6005aaae3f251e8c5267010ac72e) | +| W95 | Effect and Audit Semantics | [#403](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/403), [#404](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/404) | W95.1–W95.7 | [`6c1fb4f`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/6c1fb4f6c5043e912cf72e8660cb0ccef653098a) | +| W96 | Mechanical Regression Boundaries | [#408](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/408) | W96.1–W96.7 | [`bb62ffe`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/bb62ffe83ac920ceeb8db7ce16e6d4516f8336e7) | +| W97 | Immutable Projections and Shared Pipelines | [#409](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/409) | W97.1–W97.6 | [`b9ecdc2`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/b9ecdc274c0bf093dc235b4e08776bd666f6a6fe) | +| W98 | Catalog and Projection Completeness | [#414](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/414) | W98.1–W98.7 | [`d7ca8eb`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/d7ca8eb9886cd9cfe0b38e7780d73d18d9730c5f) | +| W99 | Session Lifecycle Operations | [#416](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/416) | W99.1–W99.8 | [`51ad81a`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/51ad81af88a16d84b24ce8b6e77b7e80ac575e14) | +| W100 | Campaign-Tunable Weekly Rules | [#419](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/419) | W100.1–W100.7 | [`3d496b3`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/3d496b3fdb8e705f31632980890f9bf42884bf77) | +| W101 | Projects, Businesses, and Rival Scarcity | [#421](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/421) | W101.1–W101.8 | [`b595fe2`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/b595fe2e7e7e92e9c3e62715998b2a7fe89bfa22) | +| W102 | Profile Chains and Simulation Save Migration | [#423](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/423) | W102.1–W102.8 | [`6e4a7bb`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/6e4a7bb855ac3559953594104d71cc7f4f304390) | +| W103 | Companion Contracts and Ownership Reconciliation | [#424](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/424), [#427](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/427), [#451](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/451) | W103.1–W103.8 | [`e52e7a1`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/e52e7a104311c3afa431d412f3ef8f778d402ba1) | +| W104 | Release 0.11 Compatibility Sweep | [#447](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/447), [#448](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/448) | W104.1–W104.7 | [`9ece598`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/9ece598028240b7541ad6fc16a1041a2fb669f48) | +| W105 | Documentation and Landing Publication Review | [#455](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/455) | W105.1–W105.7 | [`9439b99`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/9439b99a2c9f2d2263a0c2ce03c0b73a3b883180) | +| W106 | Tracker Evidence Closure | [#456](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/456) | W106.1–W106.7 | [`865f827`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/865f827b050eb92ca1894a8e89e743ae5112e41b) | +| W107 | 0.11 Release Candidate Verification | [#460](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/460), [#461](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/461) | W107.1–W107.7 | [`a84d9c9`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/a84d9c99824b7cc9ca4f7735961a3a4fdf7b814d) | +| W108 | Publish 0.11 Readiness | [#471](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/471), [#474](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/474), [#475](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/475) | W108.1–W108.6 | [`651a2a4`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/651a2a4ea87a3d8fb0dab70d0a7337d7e8b8270c) | +| W109 | A Uniform That Makes Its Wearer More Employable | [#478](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/478) | W109.1–W109.6 | [`a2850f4`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/a2850f40d24f516243a71055375b5937c746bbd4) | +| W110 | An NPC Who Already Remembers You | [#479](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/479) | W110.1–W110.4 | [`85f59c5`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/85f59c55f19c35e1841469bf607a9e150bb2b824) | +| W111 | Conditions That Can Ask "Do You Own One?" | [#481](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/481) | W111.1–W111.6 | [`ef8abff`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/ef8abffdf7aff049a26f20cf36cfeaa58a95c217) | +| W112 | A Car That Costs Money to Run | [#490](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/490) | W112.1–W112.6 | [`a431ed2`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/a431ed2c29e3a3d8f71b838b35b4a5dba8b4b9ab) | +| W113 | Utilities and Transport on the Weekly Bill | [#491](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/491) | W113.1–W113.7 | [`aadb482`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/aadb482a493a0e2e75b3c402370f83846243096d) | +| W114 | The NPCs Are Actually There | [#495](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/495) | W114.1–W114.5 | [`7d8850d`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/7d8850df21d99d6b224d38fea6b9cd720e3d3e38) | +| W115 | A Resort a Client Can Actually Draw | [#518](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/518) | W115.1–W115.11 | [`8e13420`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/8e13420fb16c3ee765ac209c66d2939c91b484a8) | +| W116 | A Campaign Cannot Ask About a Field Its Collection Lacks | [#538](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/538) | W116.1–W116.8 | [`7fc2237`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/7fc2237aa0d80df87c284409a182ff41b3eff93d) | +| W117 | Two Campaigns, One Lever, Different Weeks | [#542](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/542) | W117.1–W117.7 | [`19442c4`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/19442c4ae46d5842011a629a900eb3449a61e344) | +| W118 | The Week's Own Rules Belong to the Campaign | [#544](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/544) | W118.1–W118.7 | [`c187ee6`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/c187ee6fc655c6b2f22634a745518d48218031a2) | +| W119 | Every Fixed Action Price Is the Campaign's | [#548](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/548) | W119.1–W119.7 | [`5435b74`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/5435b742b52f9bcaf69455a6da7fe1e93d8ff2ac) | +| W120 | One Campaign Can Include Another | [#568](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/pull/568) | W120.1–W120.10 | [`7081402`](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/commit/7081402c6dccc277b23250d44a22b022a16fd683) | diff --git a/docs/docs/index.md b/docs/docs/index.md index d57d0943..92e61b15 100644 --- a/docs/docs/index.md +++ b/docs/docs/index.md @@ -53,7 +53,7 @@ What ships first, in what order, and what is still undecided. | Document | Holds | |---|---| | [MVP](engine/MVP.md) | The smallest slice that proves the platform, and its Definition of Done | -| [TODO](engine/TODO.md) | The complete W-numbered delivery ledger, with contract references and done-criteria | +| [TODO](engine/TODO.md) | The delivery ledger: open items carried forward, and every landed unit with its pull requests | | [Open Questions](engine/OPEN-QUESTIONS.md) | Living register of unknowns and deferred decisions; §1 is a decision log | ## Working on It diff --git a/site/src/roadmap/roadmapData.ts b/site/src/roadmap/roadmapData.ts index b7aaa9f6..7678ad39 100644 --- a/site/src/roadmap/roadmapData.ts +++ b/site/src/roadmap/roadmapData.ts @@ -20,13 +20,20 @@ export type RoadmapChapter = { const repo = "https://github.com/The-Running-Dev/SubZeroDev.GameEngine"; const commit = (hash: string) => `${repo}/commit/${hash}`; -// TODO.md's `### [x] W…` headings are now the complete machine-readable record — W41–W49 -// caught back up once the ledger format resumed carrying completed headings, so the exception -// list that used to patch this count by hand (`completedBeyondTodo`, W41–W49) is redundant and -// has been retired. If a future gap reopens, reintroduce the same pattern rather than silently -// undercounting. -export const completedWorkUnitCount = (todo.match(/^### \[x\] W[\w]+/gm) ?? []) - .length; +// TODO.md is the machine-readable record. Every W unit is retired to one Landed row carrying an +// `` anchor, and a cancelled one says so in its row; a slice planned after the +// retirement is an `## S…` section that counts once it reads `Status: done`. If a future gap +// reopens, reintroduce an explicit exception list rather than silently undercounting. +const landedWorkUnits = ( + todo.match(/^\| <\/a>W\w+ \|.*$/gm) ?? [] +).filter((row) => !row.includes("Cancelled")).length; +const doneSlices = todo + .split(/^(?=## )/m) + .filter( + (section) => + /^## S\d+\b/.test(section) && /^Status:[ \t]*done\b/m.test(section), + ).length; +export const completedWorkUnitCount = landedWorkUnits + doneSlices; export const shippedChapters: readonly RoadmapChapter[] = [ {