Skip to main content

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 §5 — every unit below rolls up to it. The contracts are 04-core.md, 03-story-graph-kind.md and 05-observability.md. Nothing unsettled remains for the MVP: 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

  • 03-story-graph-kind.mdNode, Choice, Requirement (reuses the Condition tree verbatim), Consequence, Ending, VariableSchema, AchievementDefinition, seeded random-transition node, turn/settle semantics, projection, worked Bureaucracy-arc example.
  • 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).
  • 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.
  • MVP contracts finalized — campaign/content identity split (04 §10.1), visited semantics + start-of-game RNG stream (03 §8.2, 04 §4/§8), AdvanceResult tightened, Definition of Done agreed (MVP.md §5).
  • 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 §1).
  • Project scaffold: src/engine/ package (Engine Package), TypeScript strict, vitest, eslint with the determinism guard (bans Math.random, Math.pow/exp/log/sin/cos/tan, Date.now).
  • Version control: this repo (engine source + specs). Companions: the games SubZeroDev.GameOfLife and SubZeroDev.SunTrap, and the hosting layer 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."
  • Seeded PRNG (PCG32) + deriveStream substreams, serializable state. src/engine/src/core/determinism/pcg32.ts — verified bit-identical to the reference vectors.
  • Canonical serialization (sorted keys, rejects non-finite). src/engine/src/core/persistence/canonical.ts.
  • Toolchain runs greennpm 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 §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 (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.
    • Workflow authored, docs system installed, README converted, Node 24 aligned — all green remotely on PR #3.
    • Required checks configured on the main ruleset (engine, Documentation links and terminology, Verify Documentation Build; deploy excluded).
    • Red-path proof captured and reverted to green, with run URLs — full evidence in plans/04-w0-phase-1-implementation.md.
    • First deploy to main — green twice: 47342b3 (run, PR #3) and 4e3effc (run, PR #5). https://game-engine.subzerodev.com/docs/ serves the generated homepage.
    • HTTPS enforcement — enabled in SettingsPages by the repository owner. Note for future reference: the domain is Cloudflare-fronted, so the httphttps 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, 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; 05 §§3–4 for the observability types; 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.
  • 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); 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).

[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.
  • Depends on: W1.
  • Status: Done — PR #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<GameState>.

  • Spec: 04 §§2–5, §12.
  • Depends on: W1, W2.
  • Status: Done — PR #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), 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 §§1–5, §7–§10, §12; 04 §3.1, §4, §14.
  • Depends on: W1, W3. (Kind events come with the kind units — W11 and W12.)
  • Status: Done — PR #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.<kindId>.* by a kind, fails.

[x] W4 — Registry, Authoring Builder, Localization

The frozen in-memory ContentRegistry; AuthoredTextBuiltCampaign pure builder; the protected core.reason.* string merge; LocKey resolution. Parsing and file I/O stay in an outer adapter.

  • Spec: 04 §10.1, §12, §17.
  • Depends on: W1.
  • Status: Done — PR #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, §17.
  • Depends on: W4.
  • Status: Done — PR #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.
  • Depends on: W3.
  • Status: Done — PR #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) — the half W3a deliberately leaves out, because stamping needs the layer that has a clock.

  • Spec: 04 §7, §10.2; 05 §6, §6.1, §11.
  • Depends on: W3a, W3, W6.
  • Status: Done — PR #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); 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.
  • Depends on: W7.
  • Status: Done — PR #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, §5, §8.1.
  • Depends on: W1.
  • Status: Done — PR #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; 04 §18.
  • Depends on: W9.
  • Status: Done — PR #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, §8.1, §8.2, §8.4.
  • Depends on: W2, W3a, W9, W10.
  • Status: Done — PR #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, §8.3, §8.4, §9; 04 §6.
  • Depends on: W6, W11.
  • Status: Done — PR #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, §8.2.
  • Depends on: W8, W11.
  • Status: Done — PR #51.
  • Done when: an achievement fires exactly once across repeated turns; the unlock is readable as achieved.<id> 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.
  • Depends on: W5, W11.
  • Status: Done — PR #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; games/bulgaria.md; MVP §3.
  • Depends on: W4, W14.
  • Status: Done — PR #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; 09-clients.md — the contract, and §4 the checklist; MVP §5 "Honest."
  • Depends on: W7, W12.
  • Status: Done — PR #63.
  • Done when: the API coverage checklist (09 §4) 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).

[x] W17 — MCP Server

The same operations as tools — a sibling adapter, no AI-specific path.

  • Spec: 04 §13; 09 §7 — MCP is a sibling, not a special case; the tool table itself lives in SubZeroDev.ServiceContract's mcp-tool-contract.md.
  • Depends on: W7, W12.
  • Status: Done — PR #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); 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); 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; 05 §12.
  • Depends on: W3a, W15.
  • Status: Done — PR #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 §5 and attach test evidence to each box.

  • Depends on: every unit above.
  • Status: Done — PR #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. 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, §8.
  • Depends on: nothing.
  • Status: Done — PR #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) 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, §5, §6.
  • Depends on: W20.
  • Status: Done — PR #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, §5.
  • Depends on: W21.
  • Status: Done — PR #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). 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).

  • Spec: 07 §7–§8.
  • Depends on: W20, W22.
  • Status: Done — PR #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, 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 <kindId>.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, §18; 03 §9; 09 §4.
  • Depends on: nothing.
  • Status: Done — PR #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

[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, §3, §12, §14, §15.
  • Depends on: W24.
  • Status: Done — PR #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

[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, W22 — 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.
  • 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

Rigour: Session Capture

Turning a played session into a fixture, per 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).
  • The refusal rules hold under test: no identity, only kind-declared params, no timing (08 §3). 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).
  • Promotion into the replay corpus is a reviewed human step, never automatic (08 §7).

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 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 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 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'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)

  • 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 now exists in 10-simulation-kind.md, reconciled with 04 §3, the GameState envelope, Kind.outcome, and the kind.simulation.* event namespace (05 §9) — see plans/36-simulation-kind-programme.md for the full split and findings.
  • 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<SimulationKindState> assembly all run through the core engine seam.
  • 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 ActionTypes, §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 fixes the seam; the game it serves lives in SubZeroDev.SunTrap (12 §17).

Now has its own programme doc, the same milestone simulation reached with plans/36: 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.

  • 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

PR #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/[email protected] 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 §2). No read-access grant was needed as a result.

[x] W42 — World-Graph Runtime State Contract

Merged in PR #116. Authoritative map, guest, building, queue, staff, construction and finance shapes now live in 12-world-graph-kind.md §3. The plan remains the evidence trail: 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

Merged in PR #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, which is historical rather than a second contract authority.

[x] W44 — World-Graph Resolution Contract

Merged in PR #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, which is historical rather than a second contract authority.

[x] W45 — World-Graph Kind Skeleton and Immediate Actions

Merged in PR #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, with PR #124 retained as the original implementation review. The execution record is plans/45-w45-world-graph-kind-skeleton.md, which is historical rather than a second contract authority.

[x] W46 — World-Graph Deterministic Tick Pipeline

Fixed-order systems, bounded advance_ticks, derived streams and batch invariance are delivered in PR #128, merged to main at 6301a49. Its implementation plan is 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

The synthetic guest journey — spawn → walk → queue → buy → litter → clean → objective → win/lose — is delivered in PR #131, merged to main at 2390750. It follows the canonical world-graph contract and does not supersede it.

[x] W48 — Preview/Client Parity

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.

[x] W49 — World-Graph Validation, Scenario and Replay Guard

The canonical engine-owned MVP fixture and its Tier 1/Tier 2 validation landed in PR #134; deterministic winning and losing replay pairs and release-corpus coverage landed in PR #136. Session-parity replay cases, a clean-build serialization proof, and a consumer-smoke rerun landed in PR #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

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 §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 (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.
  • 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

[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.
  • 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 (showWhen).
  • Depends on: nothing engine-side — content only, same as W27/W28.
  • Status: Done — PR #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.

  • 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'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, §10.2; 06 §6.
  • Depends on: nothing engine-side.
  • Status: Done — PR #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

[x] W32 — Simulation Kind: State Types

The first contract unit of the simulation-kind programme (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, §4.1, §15.
  • Depends on: W25 (the seam must be complete before field detail hangs off it, per plans/36).
  • Status: Done — PR #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

[x] W33 — Simulation Kind: Actor State

The second contract unit of the simulation-kind programme (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, §15.
  • Depends on: W32.
  • Status: Done — PR #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

[x] W34 — Simulation Kind: Content Definition Types

The third and largest contract unit of the simulation-kind programme (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, §8, §15.
  • Depends on: W33.
  • Status: Done — PR #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

[x] W35 — Simulation Kind: Resolution and Systems

The fourth and final contract unit of the simulation-kind programme (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, §4.2, §5.1, §8.1, §15.
  • Depends on: W34.
  • Status: Done — PR #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

[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, §4.1, §4.2, §6.
  • Depends on: W35.
  • Status: Done — PR #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

[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 ActionTypes, 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, §5, §5.1.
  • Depends on: W36.
  • Status: Done — PR #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

[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.tsReward, 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, §7.1§7.10.
  • Depends on: W37.
  • Status: Done — PR #100.
  • Done when: every type §7 names that isn't already runtime state in state.ts/actor.ts exists in content.ts; AvailabilityRule.conditionunknown 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

[x] W39 — Simulation Kind: Wiring the "Stable Life" Vertical Slice

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 ActionTypes. 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, §3, §12.
  • Depends on: W38.
  • Status: Done — PR #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

[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<SimulationKindState> 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, 07 §4.
  • Depends on: W39.
  • Status: Done — PR #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

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

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 is prose-only: it states the rule — SimulationView carries only what the generic surface does not, the rule 03 §9 already follows — but declares no shape. PublicWorldState, which §7.10'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 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, §2, §4, §10, §11; 03 §9 (the rule to follow); 04 §6, §9; 09 §4, §6.
  • 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.
  • 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'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 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.

[x] W51 — Simulation Kind: Derived Values, Modifiers, and Effects

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'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'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), 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, §7.1, §2.3, §3, §13.
  • 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.
  • 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, superseding PR #194.

[x] W52 — Simulation Kind: The Scenario Campaign and Full Validation

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 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's authoring boundary is asserted for two kinds and proven for two of three. And §14'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, §7.9, §14; 04 §10.1, §11, §17.
  • 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.
  • Status: Not started.
  • Done when:
    • W52.1 SimulationCampaignSourceSimulationCampaign 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) 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.

[x] W53 — Simulation Kind: Employment and Income

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, §7.9 (employers), §6.8, §3 (employment, finance_income), §5.1§5.3, §10.
  • 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, 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 ActionResolvercanExecute, 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.

[x] W54 — Simulation Kind: Education and Skills

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, §7.9 (skills), §6.7, §3 (education), §8.1.
  • 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.
  • 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.

[x] W55 — Simulation Kind: Housing, Debt, and Reconciliation

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 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, §6.4, §6.9, §3 (housing, finance_reconcile), §10.
  • Touches: src/engine/src/kinds/simulation/resolvers.ts, endOfWeek.ts; src/engine/src/campaigns/stable-life.ts; src/engine/fixtures/replay/.
  • Depends on: 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.

[x] W56 — Simulation Kind: Possessions, Places, and People

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, §7.7, §7.9 (locations), §6.5, §6.10, §6.11, §3 (inventory), §10 (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.
  • Status: Done — PR #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

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's stubbed systems and §2.3'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'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, §7.9 (opportunities, achievements, headlines), §2.3, §3, §11, §12; 04 §7.1.
  • 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, and a contract decision on §12's week_limit_reached precedence callout.
  • Status: Done — PR #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 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 is the customization story 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

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'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'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, §3, §4, §5, §6, §7; 04 §10.1, §11; 07 §6.
  • 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.
  • 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 defers all four by name.

[x] W59 — Experiment Gates and the ExperimentSource Port

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'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'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; 06 §4, §5.5.
  • 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.
  • Status: Done — PR #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 campaignVersions 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

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 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, §12, §17; 09 §5.
  • 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.
  • 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's deferral, not this unit's.

Breadth: The First Culture Pack

Both preconditions have landed. W58 built the pack fold and the ResolutionId; W50W57 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 §4a calls culture packs "the volume play: one kind, many settings," and §11 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

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) — so the Bulgarian world arrives under the same stable-life id, and the ResolutionId is the only thing that distinguishes the two (11 §6). Every surface this needs is already an exported contract surface (resolvePacks, computeResolutionId, ContentPack, buildValidatedContentRegistry, TextClient); the unit adds no signature.

  • Spec: 02-architecture.md §4a; 11 §2, §3, §6; 04 §10.1; 10 §7.
  • 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, W59, W50W57 — all done.
  • Status: Done — PR #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 ResolutionIds, 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 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); a Bulgarian locale string table — 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 defers by name; a second culture pack.

[x] W72 — The Bulgaria Culture Pack: The Full Setting

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 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; 02-architecture.md §4a; 11 §7.
  • 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.
  • Status: Done — PR #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) 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

  • 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 <actionId> [key=value ...], view, save, load <id> 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

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 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; 09 §1, §2, §4, §6; 04 §7, §9, §10.2.
  • 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, W31, 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'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

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; 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 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.
  • 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

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; 13-playable-web-demo.md §§1–3, §7–§9; 09 §1, §2, §6.
  • 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 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.
  • 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

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 for variables, nodes, gates, effects, conditions and achievements; §8.2 for settle semantics; §10 for determinism, save and versioning; 04 §10.2 for campaign migration; 09 §1 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 for the v1 → v2 campaign boundary and 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

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 was accepted on manual inspection at four widths, recorded as known-and-retained in 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 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 §10 for the proof list this must be able to execute, and §8 for the widths and states it must reach; 13 §7–§8.
  • 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 and 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 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; 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

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 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 and 14 §1 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 §1 and §8 (Revision 2), with §§2–7 unchanged; 13-playable-web-demo.md §3, §7–§9; 09 §1, §2, §6.
  • 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 — 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 for the cabinet grammar and 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

Delivers: The replay corpus, determinism goldens, and observability acceptance test that the 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) 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 §4, §6, §7; 04 §14; 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 being on main, since the fixtures are regenerated against its v2 campaign graphs.
  • Status: Done — PR #261.
  • Done when:
    • W67.1 Three bureaucracy-* fixture/outcome pairs exist against campaignVersion: "2.0.0", covering materially different routes and matching 07 §4'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'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).
    • 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

Cancelled. Every file this unit touches is in site/src/play/, the route 13-playable-web-demo.md now records as superseded by 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's open register: W68.5SaveRecordStore.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 Revision 2 now specifies. The SessionPersistence port and a localStorage adapter shipped with 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 §5 (Revision 2); 04 §7.2; 06 §5.2; 09 §3.
  • 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. Independent of W65/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.
    • 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

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 [email protected]'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 [email protected], whose adapter preserves route-specific canonical, Open Graph, X/Twitter, icon, theme-colour and no-script metadata.

  • Status: Done — PR #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 (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.

  • 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

Cancelled, for the same reason as W68: it gates the emitted /play/ bundle, and that route is superseded by SubZeroDev.Adventures (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 §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 §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'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 §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).

  • Additional locales — sliced as W60. The MVP ships English only; the authoring→registry types already support more (04 §10.1), 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).

  • Content packs — sliced as W58 and W59 — per 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). Before mods, not before MVP (neaas-platform-vision.md → Known deferred gaps). src/engine/src/core/composition/types.ts now declares ExperimentSource, and the gate machinery ships and is exported. 06 §4 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 names it — "unwinnable campaigns, dead-end states — found by running, not reading" — and 03 §11 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

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 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'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 (Tier 3 is not part of load), §10.1 (registry construction is pure and total); 03 §11 (the two story-graph Tier 3 cases); 12 §15 (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.
  • 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

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: a unit of work in this repository can neither make them true nor honestly tick them, so they move to W74c as verified preconditions of the removal they were always guarding. W74.4 becomes W74a, and W74.5 splits into W74b and W74c — it was never one session's work. W74.2W74.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 and 10 §13 — and W74c owns re-pointing both.

  • Spec: 20 §19 (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. 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, 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 and W74c. Adventures.Content's own build, which this unit neither reads nor changes.

[x] W74a — The Bureaucracy Fixture, Frozen Byte-for-Byte

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 (a frozen Bureaucracy campaign may remain inside the engine as story-graph regression evidence only; it is not a publication source); 20 §19 (such a fixture is not published and not listed in a manifest); 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.
  • 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, 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, not frozen.

[x] W74b — Retire the Engine's Own Play Surface

Delivers: The engine repository stops carrying a browser game nobody can reach. The route was removed from the build by 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 (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, 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. 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.
  • 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, and both are breaking where this unit is not. Rewriting 13-playable-web-demo.md or 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

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 (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 (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, W74a, 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.
  • 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'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 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

Delivers: the one classified adapter failure 20 §7.2 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.1W75.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, §12, 06 §5.2.
  • Depends on: nothing beyond the shipped session store.
  • Status: Done — closed by #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 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

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, 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, §4, §7; 04 §10.1, §11, §12.
  • 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, closing #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 <kindId>.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

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 §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, §12, §17; 03 §11; 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 §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, 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'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

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 put string-table extraction, translation workflow and coverage reporting out of its own scope by name and routed them here. 04 §10.1 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, §11 (missing_string_key), §17; 09 §5.
  • 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, for the shared campaign catalogue.
  • Status: Done — issue #326.
  • Done when:
    • W78.1 For a named campaign the tool emits the complete, sorted set of LocKeys 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 defers by name; locale-aware pluralization and formatting, which 09 §9 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

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 built computeResolutionId and W71 proved two real resolutions of stable-life produce different ids. Neither can answer what moved between them, and 11 §6 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, §4, §6; 04 §10.1.
  • 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, W71 — both done.
  • Status: Done — issue #327.
  • Done when:
    • W79.1 Given two ordered pack sets the tool reports both ResolutionIds and, when they differ, an itemized difference following 11 §3'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) 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); pack discovery and loading from disk (11 §8).

[x] W80 — Seeing a Story Graph

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); there is currently no way to look at any of it.

  • Spec: 03 §3, §4, §8.5; 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, for the shared campaign catalogue and for the Tier 2 warnings W80.4 cross-checks against.
  • Status: Done — issue #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 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 §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 §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 owns published narrative content (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 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) — that advance_ticks n reaches the same state as any split of it — and on the draw discipline in §9. 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) 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

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 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 §4.11, §4.13, §4.14; §5; §9; §12; §13; 04 §12.
  • 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, W46, W47 — all done; they shipped the reducer, the construction-site state and the comparator registry.
  • Status: Done — PR #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 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).
    • 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, 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; scenery, which has no construction path at all; the shared SystemPipeline (issue #270).

[x] W82 — A Kiosk That Ran Out Can Be Refilled

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 §4.11, §4.13, §4.15; §10; §12; §13.
  • 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, for the systems 9 and 11 task-kind branches it establishes.
  • Status: Done — PR #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'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.
    • 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 §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).

[x] W83 — Buildings Get Dirty, Wear Out, and Break

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 §4.16, and §4.2 for the typed scratch deltas systems 1, 4 and 11 hand it; §10; §12; §13.
  • 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 or W82 and may be taken before either. It is placed here because W84 depends on it.
  • Status: Done — PR #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 with the differing fields named in the pull request. Unlike W81 and 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

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 §4.18; §9; §12; §13; 04 §8.
  • 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, so a rolled incident has meters to act on and a resolution path that is not litter-only.
  • Status: Done — PR #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.
    • 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; balance values for chances and cooldowns, which are content.

[x] W85 — The Resort Tells You What Needs Attention

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 §4.21; §8; §10; §12; §13; 04 §7.1; §12.
  • 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 — the broken-building alert family has no source at all until wear can reach zero. 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.
  • 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.<type>.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.
    • 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 already holds as contract work.

Rigour: Two Event Streams Specified and Never Emitted

05 §2'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, and, after W81–W85 deliver the rows their own systems own, the remainder of 12 §12. 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.

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

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 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, and §8.2 for the step each event is emitted at; 05 §9; §2; 04 §3.
  • 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.
  • 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 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 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 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

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, and §4 for each named emit site; 05 §9; §7; §2.
  • Touches: src/engine/src/kinds/world-graph/tick/pipeline.ts; src/engine/src/kinds/world-graph/kind.ts.
  • Depends on: W81W85, which each deliver the rows their own system owns. This unit takes what is left rather than duplicating them.
  • Status: Done — PR #363.
  • Done when:
    • W87.1 Every row in 12 §12'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 defers by name; presenting the stream anywhere.

Known Open Items Carried In

Full register of unknowns, gaps, and deferred decisions: OPEN-QUESTIONS.md.

The eight findings from the first downstream host are contract work, not slices. 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, #276#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.

  • SessionHost/createSessionLayer (06 §4) don't reconcile as written — closed. Resolved by drawing the seam one level down at SessionPersistence (04 §7.2); 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) — not for want of a root.
  • wisdom attribute has no consumer in the simulation kind — needs one to earn its place (games/04-engine-specification.md §8.4).
  • Four simulation ActionTypes have no content-definition type to resolve against. start_project, work_on_project, start_business and operate_business are members of 10 §4.2's closed union, so ResolverTable's completeness check compels a resolver for each — but §7 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.
  • The simulation relationships end-of-week system has no rule to implement. 10 §3 names it in END_WEEK_SYSTEM_ORDER and §6.11 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, travel costs) need a balancing pass once the sim harness runs.
  • Three docs-template hardening findings, to raise upstream — after this PR merges, not before. Surfaced by automated review on PR #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, 2, 3.

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 ActionTypes have a real resolver, fourteen of fifteen §3 end-of-week systems have real bodies, §9'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.

  • P1 — Four ActionTypes have no content-definition type. Carried above. start_project, work_on_project, start_business and operate_business are members of §4.2's closed union and §7 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 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 gives RelationshipState its full shape and §7.7 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 consts 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 assigns balance findings to a game-side harness, and that stands — this is about where the levers live, not about who turns them.
  • P4 — Rival agents. §7.10 states outright that how a scenario configures rivals is an open gap; no ScenarioDefinition field names them. PublicWorldState was declared by W50 so AgentStrategy could typecheck and is exercised by nothing. 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 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) is set to 100 at hire and never moved, so anything built on it reads a perfect record forever; wisdom (§6.6) 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.

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

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, and SimulationView, PublicWorldState and SimulationOutcome since 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's export list in the same change that implements it, the precedent W50 took from 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 (the root is the runtime contract, the subpath the author-time one); 10 §13 (published campaign builders are not package-root API — a builder is not a published campaign); 10 §9 (SimulationView, what a host renders); §7 (what the source types cover); §12 (SimulationOutcome).
  • Touches: src/engine/src/authoring.ts and its committed sorted export list (W74.7); src/engine/src/index.ts; consumer-smoke/smoke.ts; design/20-contract.md (the engine/04-core.md block, §19).
  • Depends on: W57, the behaviour these types describe, and W74c, which fixed what the root surface is for. Not on W89.
  • Status: Complete — PR #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 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'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

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 weeksstable-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 PendingEventResponses 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) 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 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 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.

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 §4, §6, §7 (the corpus and the runner's verdicts); 04 §14 (a build against itself); 10 §3 (the two orderings a long run re-runs two hundred times), §13 (substreams), §12; 20 §19 (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. Independent of W88 — the two may land in either order.
  • Status: Complete — PR #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 ActionTypes 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 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 assigns to a game-side harness and which P3 shows the engine cannot host anyway; Tier 3 validation for this kind, which 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's W108.5 requires those guards to be executable without pushing a tag.

[x] W90 — Canonical and Public Truth

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.

[x] W91 — Reproducible Package and Release Gate

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

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.

[x] W93 — World-Graph Spatial and Audit Correctness

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, §6, §9.3, §13, and §15.
  • 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, 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

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, §3, §7.2, §7.5, and §14.
  • 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, whose long run supplies the bounded-state baseline. The job-opening scarcity criterion lands with or after W101's rival contract so the player and rivals cannot acquire different meanings for the same finite count.
  • Status: Complete — PR #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

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, §6.1, §10; 12 §9.2, §11, and §12.
  • 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 if none exists when this unit starts; the deferred-meter and audit-table work is otherwise independent.
  • Status: Complete — PR #403 (W95.1–3, 5–7) and PR #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.

[x] W96 — Mechanical Regression Boundaries

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, §12, §14; 05 §7; 12 §5 and §15.3.
  • 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: W93W95, so the new gates record the corrected baseline instead of freezing known defects.
  • Status: Complete — PR #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

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, §9, §14; 10 §3, and 12 §4.
  • 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, 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

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, §7, §9; 03 §9, and 09 §4.
  • 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, 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

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, §10; 06 §2, §5.2, and 07 §6.
  • 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, 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

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, §3, §6, §7, §9, and §10.
  • 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, 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

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, §4.2, §6.2, §7, and §7.10.
  • 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 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, 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

Delivers: Simulation players can carry profile-scoped chains into later sessions and resume older portable saves through deterministic, declared migrations.

  • Spec: 04 §7.1, §10.2; 10 §2.2, and §15.
  • 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, 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, 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

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 and §15; 09 §4; 13 §6, and 15 §6.
  • 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 and W99 for the final operation shapes mirrored to Platform/service consumers.
  • Status: Landed — all eight criteria confirmed met; closed by issue #391.
  • Done when:
    • 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.
    • 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 (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 (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).
    • 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) and runs its own missing/extra-operation gate, server/src/contract.test.ts, comparing the routes it serves against loadPublishedContract().operations. The browser client, 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.
    • 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.
    • 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.
    • 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.
    • 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 (5095a5536f0a7a, 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.
    • 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

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, 07 §6, and the compatibility/default rules fixed by W98W102.
  • 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: W93W103 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:
    • 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.
    • 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.
    • 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.
    • 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.
    • 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.
    • 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.
    • 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

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, 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

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: W93W105. 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

Delivers: Release maintainers can judge one additive candidate from a complete, reproducible verification record covering the engine and every supported delivery surface.

  • Spec: W104's compatibility manifest, 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, 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 made required, and, once that cleared, the event-chain declarations 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

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's candidate report, and the public roadmap/release material reconciled by 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 green and every blocker in 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 scopeW108'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 closes the already-open engine issue #418; the other four have no engine issue yet.

[ ] W109 — A Uniform That Makes Its Wearer More Employable

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's DerivedPath union and its writable/formula-only partition, §6.2's stored reputation record, §7.1'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, for scheduling only — this is the first unit of 0.12 and must not land against the verified 0.11 candidate.
  • Status: Not started.
  • Done when:
    • W109.1 A campaign whose Modifier.target is player.reputation.<key> 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.<key> returns the stored base with every active modifier layered over it in §6.1's fixed add/subtractmultiplyset 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.

[ ] W110 — An NPC Who Already Remembers You

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'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: Not started.
  • 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.

[ ] W111 — Conditions That Can Ask "Do You Own One?"

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'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: Not started.
  • 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.

[ ] W112 — A Car That Costs Money to Run

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's ItemDefinition.weeklyCostCents, §3'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 to keep the fixture churn off the three units that cause none.
  • Status: Not started.
  • 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 — Utilities and Transport on the Weekly Bill

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's utilitiesCents/transportCents and the reserved "vehicle" tag, §6.9's HousingState, §3'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. 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: Not started.
  • 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.