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 are04-core.md,03-story-graph-kind.mdand05-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 inplans/. Same convention as architecture §4a.
Legend: [ ] not started · [~] in progress · [x] done
Done — Specification and Scaffold
-
03-story-graph-kind.md—Node,Choice,Requirement(reuses theConditiontree 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 the03state reconciliation (envelope vs kind-state). -
executeActionremoved. 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),
visitedsemantics + start-of-game RNG stream (03 §8.2, 04 §4/§8),AdvanceResulttightened, 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,paramstoadvance, 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 (bansMath.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) +
deriveStreamsubstreams, 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 green —
npm install && npm test && npm run lint && npm run typecheck; 15 tests acrosspcg32andcanonical.
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:
engineplus 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 tomainhas deployed to the real published URL; the three pull-request checks are required on the default branch (deploy runs only onmain, so requiring it would leave every PR pending);engines.nodeestablishes 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
mainruleset (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) and4e3effc(run, PR #5).https://game-engine.subzerodev.com/docs/serves the generated homepage. - HTTPS enforcement — enabled in Settings → Pages by the repository owner. Note
for future reference: the domain is Cloudflare-fronted, so the
http→httpsredirect 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:
routeBasePathstays'docs', so the specs serve fromhttps://game-engine.subzerodev.com/docs/and the barehttps://game-engine.subzerodev.com/serves the README, generated intodocs/src/pages/index.mdby 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 theenginecategory; ordering insideengine/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
observabilitytypes;06-extensibility.md§4–§5 forcomposition— the two host roots and theIdSourceandClockport interfaces. - Depends on: nothing.
- Status: Done — PR #17.
- Done when:
npm run typecheckpasses withexactOptionalPropertyTypes; a dependency scan shows no core module importingkinds/,clients/, ormcp/;kindStateisunknown, not a union;GameStatecarries no clock, profile, or kind state;EngineEventcarries 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 throughEngineHostorSessionHost, 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;GameStatecontains 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;deserializerejects 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:
emitreturnsvoidand no core code path reads anything back from a sink; the core isolates everyemit, so a sink that throws on every call does not fail a game; a fixture replays byte-identically undernullEmitterandrecordingEmitter; the same fixture twice underrecordingEmitteryields the identical event sequence including ordinals, comparing modulogameId; ordinals restart at 0 each resolution, so a stream does not depend on how many games ran before; noEngineEventfield is populated from a clock or an RNG draw;core.validation.completedandcore.deserialize.rejectedarescope: "system"and carry nogameId; a rejected unknown action id is absent from the emitteddata; a name outsidecore.*emitted by the core, or outsidekind.<kindId>.*by a kind, fails.
[x] W4 — Registry, Authoring Builder, Localization
The frozen in-memory ContentRegistry; AuthoredText → BuiltCampaign pure builder; the
protected core.reason.* string merge; LocKey resolution. Parsing and file I/O stay in
an outer adapter.
- Spec: 04 §10.1, §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 rawkindStatecannot reach a client by any path; theaiaudience is not wider thanplayerby 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 serializedGameState; every command wraps the base emitter per call viawithEmitterand stampsemittedAt,traceId,spanId,attemptandsessionId; two concurrent commands never cross-attribute an event, verified with interleaved sessions rather than asserted;attemptincrements on rejected submissions too, so repeated invalid actions are distinguishable whereseqrepeats (05 §5);jsonlEmitterwrites 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; noprofileIdmeans 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;
+5then-5on a clamped int nets zero rather than clipping; a save/load round trip cannot reorder aRecord.
[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
fieldpath 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 reportsstatus: "ended"; random transitions reproduce from seed + action log; the settle loop emitssettle.step,node.entered(withvisitCount) andrandom.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 returnsunknown_actionwhen submitted — indistinguishable from a nonexistent id; a gated choice renders disabled with itsrequirementFailKey; hidden variables and visit counts never appear in a projection;StoryGraphViewrepeats nothing the genericScenealready 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;advanceperforms 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, missingLocKey, non-visible variable in text, and a non-positive-integerweighteach fail Tier 1 with a path; unreachable nodes, exitless cycles, andno_reachable_choicewarn 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 >= 3gate; 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 persistedGameState; 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-identicalserialize()output to W16's run — the client contract's proof (09 §1); an agent sees no more than a human client does, including gettingunknown_actionfor 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 undernullEmitterandrecordingEmitter; 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.jsoncarries 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;atis aDecision.index, never aseq; a rejected submission recordsseq: nulland does not stop the replay; achievements are read from an in-memoryProfileStoreafter 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.tsis the single kind assembly and the five duplicates are gone; a hand-edited.outcome.jsonproducesdivergedwith the rightat.
[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 reportedenginegreen 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: cuttingv0.2.0(the first commit with a replay corpus) found the job's checkout never fetched sibling tags (actions/checkout'sfetch-tagsdefaults tofalse), so the comparison always fell through to "nothing to compare yet" regardless of what tags existed — fixed infetch-tags: true, then proven end to end with a disposable tag:4 passed | 5 skippedagainstv0.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.0predates the corpus entirely, so a second tag would have hit the same fixture-free skip on correct code.v0.2.0is 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 correctedTODO.mdentry cites the code or plan that proves the correction; no file undersrc/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
SimulationKindStatenames 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 theweek_limit_reached/goal precedence is unresolved is flagged explicitly rather than guessed at; no file undersrc/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
ReplayFixtureand 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 asunknownby design. So §3.2 is unimplementable as written, and a slice for it would have to invent aKindsignature 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 theKindinterface 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.mdnow exists in10-simulation-kind.md, reconciled with04§3, theGameStateenvelope,Kind.outcome, and thekind.simulation.*event namespace (05 §9) — seeplans/36-simulation-kind-programme.mdfor 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.deriveand thetickstream — already built, since W1/W2. Not a gap this kind needs to close:KindContext.derive(04 §3.1) and all fourStreamIdvariants, includingtickandagent, exist incore/kernel/types.ts,core/kernel/engine.ts,core/determinism/types.tsandcore/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 documentsderiveclosing 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.
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-graphis 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_tensionaccumulates 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_businesschoices proceed toentrepreneur; eachentrepreneurchoice'sdebt_centseffect 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.mdlists 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 flipsreplayCompatible: 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; asaveFormatVersion/serializationVersionmismatch fails loudly (neither has ever moved); anengineVersionmismatch never gates a load;npm run typecheck && npm run lint && npm testall 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) exceptPlayerStatehas 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 inOPEN-QUESTIONS.md, not silently absorbed or dropped; no file undersrc/engine/changes;build/Test-Documentation.ps1passes. - 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-keyedRecordfields (skills,reputation,flags,counters) are explicitly reconciled against02-architecture.mdN6 ("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 integer0–100range rule for needs/skills/attributes/reputation is stated somewhere, not silently dropped; §15's table drops every row this unit closes; no file undersrc/engine/changes;build/Test-Documentation.ps1passes. - 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'smultiplysemantics are stated precisely (basis-points-shapedvalue, round-half-away-from-zero once after the full chain), not merely flagged as unresolved; theplans/32/plans/36correction 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 undersrc/engine/changes;build/Test-Documentation.ps1passes. - 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/GameActionare specified with no client-supplied cost fields; the resolver dispatch mechanism (ActionResolver,ResolverTable) is reconciled againstKindContext(04 §3.1) rather than upstream's own bespokeResolutionContext; per-action outcome types reuse04-core.md'sStateChange/OutcomeMessage/ValidationError/ValidationWarningverbatim rather than restating upstream's divergent versions; end-of-week ordering states plainly thatweekLimithas 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 undersrc/engine/changes;build/Test-Documentation.ps1passes. - 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/ActorStatenames in the contract has a real TypeScript type;addAction/clearPlannever mutate their input plan;removeActionrejects a negative index, an index equal to the plan's length, and a non-integer index, all withaction_not_planned; noKind.advance/kernel/engine.tswiring exists yet;npm run typecheck && npm run lint && npm testall 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:
initialStatebuilds week-one state from a syntheticSimulationCampaign; 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 viakind.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 to0–100and emits oneStateChangeper touched need; a realcreateEngine/submitActionround trip runsplan.addthenend_weekand lands on the next week with a fresh, empty plan;npm run typecheck && npm run lint && npm testall 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.ts — Reward, JobDefinition (+6 nested types), CourseDefinition,
HousingDefinition, ItemDefinition (+1 nested), EventDefinition (+5 nested),
NPCDefinition, GoalDefinition/ScenarioDefinition/DifficultyDefinition,
OpportunityDefinition/AchievementDefinition/HeadlineDefinition/EmployerDefinition/
LocationDefinition/BackgroundDefinition/TraitDefinition/SkillDefinition, and
AgentStrategy. NPCState/AgentState/NPCMemory/NPCRelationship/AvailabilityRule/
Modifier already exist (state.ts, W36) as runtime state — this unit ports only their
content-side counterparts, and everything else §7 names. No system/resolver wiring here —
that's W39's job, against a settled type surface, the same "contract before code" discipline
the four contract units (W32–W35) already used one level up.
- Spec: 10 §7, §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.tsexists incontent.ts;AvailabilityRule.condition—unknownsince W36, deferred per its own comment there — is narrowed to the real coreConditiontype, this unit's natural import point for it;npm run typecheck && npm run lint && npm testall 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
requiredDurationWeekscompletes the first week its condition is met; a persistent goal requires that many consecutive satisfied weeks, resetting on any miss;goalFailurePrecedenceresolves a goal whose completion and failure conditions trip the same week ("goals_win", the default, completes it anyway;"failure_wins"fails it instead);eat/restrestore the needs they target, clamped;advance'send_weekreportsstatus: "ended"onceoutcome()resolves; every stub is still individually documented;npm run typecheck && npm run lint && npm testall 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
LocKeyfor this kind's own content; one win fixture and one loss fixture are committed underfixtures/replay/and pass the replay oracle, including the release-tag-replay CI job;npm run typecheck && npm run lint && npm testall 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(theengine/10-simulation-kind.mdblock, §9 and §11);src/engine/src/kinds/simulation/— newview.ts,scene.ts,available.ts, andkind.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
SimulationViewandPublicWorldStateas complete TypeScript interfaces with every field named, andAgentStrategy.selectActions(§7.10) resolves against the declaredPublicWorldStaterather than an undeclared name. - W50.2 No
SimulationViewfield repeats one theGameStateenvelope, the genericScene, orPlayerViewalready carries — asserted by a test that names the envelope fields and checks each is absent, not by review. - W50.3
projectnever emitsseed,actionLog, rawkindState,AgentState.strategy,RelationshipState.resentment, or an unrevealedOpportunity, for either audience; each is asserted by name, and theaiaudience is not wider thanplayer. - W50.4
availableActionsreturnsplan.add,plan.remove,plan.clearandend_weekwith the params §4 declares for each; where the campaign forbids an empty plan,end_weekrenders disabled withplan_emptyrather than being hidden. - W50.5
scenerenders from registry strings only; aLocKeyit references but the registry does not resolve fails registry construction rather than rendering a raw key at play. - W50.6 Playing
stable-lifeto its committed win through the text client and through MCP, under the same seed and the same countingIdSource, produces byte-identicalserialize()output — 09 §1's proof, now exercised for the first kind whose actions carry declaredparams. - 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 fullstable-lifewin covers their order, and a name outsidekind.simulation.*fails.
- W50.1 Contract §9 declares
- 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.
PublicWorldStateis declared here becauseAgentStrategycannot 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/— newderived.tsandmodifiers.ts, plusstartOfWeek.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 throughDerivedValueResolver; 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 integerCentsrounds 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
activeEffectreducing committed time changes the budget intime_commitand not intime_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.
- W51.1 Every
- 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 newsource.ts;src/engine/src/campaigns/stable-life.tsand new broken fixtures beside it. - Depends on: W50.
- Status: Not started.
- Done when:
- W52.1
SimulationCampaignSource→SimulationCampaignis 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
initialStatebuilds week one from aScenarioDefinition— 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.
- W52.1
- Out of scope: making any newly authorable content do anything. A
JobDefinitionthis unit lets an author write and the validator accept is still resolved bystubResolveruntil 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_jobandnegotiate_job_termseach have a realActionResolver—canExecute,calculate,apply— and none of the five isstubResolver. - W53.2 Time and money cost are computed by
calculatefrom state and content; a plan exceeding the week's time units is rejected withinsufficient_time, leaving state byte-identical and the action log unadvanced. - W53.3
apply_for_jobagainst a job whose requirements are unmet is rejected withrequirement_unmet; an employment action whose type is not in the current location'sactionTypesis rejected withwrong_location. - W53.4 The
employmentsystem advancesEmployment.performanceand anyPromotionPathit satisfies;finance_incomepays wages beforehousingruns, 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.
- W53.1
- Out of scope: education, housing costs, debt and possessions — W54 to W56. A job's
training requirement may name a
CourseDefinitionid 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,studyandwithdraw_courseeach have a real resolver, and none isstubResolver. - W54.2 Enrolling adds a course commitment that start-of-week
time_commitincludes 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
educationsystem 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
JobDefinitionrequirement W53 built, proven by a fixture whereapply_for_jobis 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.
- W54.1
- 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_savingsandinvesteach have a real resolver, and none isstubResolver. - W55.2
housingleviesHousingDefinition.weeklyCostCentsafterfinance_incomehas 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_reconcileapplies late fees and advances eviction only against balanceshousingcharged 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 inserialize()output, checked over the canonical string rather than by inspection. - W55.5
move_housingto a home the player cannot afford is rejected withinsufficient_fundsand 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.
- W55.1
- Out of scope: items and their upkeep (W56), and any economy-wide drift in prices or
wages —
EconomyStatemoves 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,socializeandexerciseeach have a real resolver, and none isstubResolver. - W56.2 Both halves of
wrong_locationare covered:travelto a location absent from the current location'sconnectionsis rejected with it, and so is any action whose type is absent from the current location'sactionTypes. - W56.3 The
inventorysystem applies per-item condition decay, and an item at zero condition stops contributing its modifiers rather than being removed from inventory. - W56.4
socializemoves the namedRelationshipStateand is rejected when that NPC is not present at the current location. Therelationshipsend-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
exercisemoves the needs §6.5 names, clamped to0–100the same way W39'seat/restare, emitting oneStateChangeper touched need. - W56.6 A replay fixture covering buy → use → decay → repair → sell is committed and passes.
- W56.1
- Out of scope: events that fire because of a relationship or a possession — that is
W57's
eventssystem. 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_reachedprecedence callout. - Status: Done — PR #265.
- Done when:
- W57.1
respond_to_event,accept_opportunityanddecline_opportunityeach have a real resolver, and"custom"still reaches resolution nowhere — aGameActiontyped"custom"fails, with no route around theResolverTable. - W57.2 The
eventssystem fires scheduled and seeded random events in the order §2.3 states, and a deferredPendingEventResponseis presented by the next week's start-of-weekeventsphase, never the same week it was deferred in. - W57.3 The
opportunitiessystem runs revoke and expire before offer (§2.3); an opportunity offered and expired within one week is never visible inSimulationView. - W57.4
headlinereads world strangeness aftereventshas moved it, proven by a fixture whose headline changes only because an event fired that week. - W57.5
achievementsevaluatesAchievementDefinition.conditionaftergoals/failure, unlocks exactly once across repeated weeks, and upserts throughProfileStoreso the unlock survives a new session with the sameprofileId. - W57.6
week_limit_reachedis reachable and returned byoutcome()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
relationshipsandhistory, each still documented at its own definition site with its reason.
- W57.1
- Out of scope: rival agents.
§7.10
states plainly that how a scenario configures rivals is an open gap — no
ScenarioDefinitionfield 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 newpacks.ts;src/engine/src/core/validation/tiered.ts. - Depends on: nothing engine-side.
- Status: Done — PR #238.
- Done when:
- W58.1
resolvePacksis 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.idand 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
dependsOnis 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 sameResolutionIdacross processes, and the same list in a different order produces a different one. - W58.5 Every campaign
resolvePacksproduces carries theResolutionIdas itsCampaign.version, andGameStategains no field. - W58.6 Loading a replay fixture whose
campaignVersionno longer resolves returnsunrunnablewithcampaign_version_missing— notdiverged. - W58.7 Each of §7's three checks fires: Tier 1 for a
kindIdmismatch, an unresolvabledependsOnand 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.
- W58.1
- 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
ExperimentSourceis declared incomposition/types.tsandSessionHostgains an optionalexperiments, 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
ExperimentSourcesupplied, 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
experimentIdacross the candidate packs, keyed byprofileIdwhere present andseedotherwise — proven by a countingExperimentSourcerather than asserted. - W59.6 Two sessions in different variants produce different
campaignVersions through W58's existing digest, with no further mechanism added.
- W59.1
- 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.tsandstrings.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.
- 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
- 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;
W50–W57 made the simulation kind playable, so there is now a game worth
reskinning. Sliced as W71–W72.
This is the riskiest unproven claim left in the design, which is why it leads.
02-architecture.md §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 existingstable-lifesource and a new Bulgarian culture pack, plus a test alongside.src/engine/src/core/registry/packs.tsis read, not modified. - Depends on: W58, W59, W50–W57 — 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/orsrc/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 asCampaign.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, neverstable-life's. - W71.5 A replay fixture captured under one resolution returns
unrunnablewithcampaign_version_missingagainst the other, notdiverged— 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().
- W71.1 Resolving
- 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 splitstable-life-events.ts/-housing.ts/-possessions.ts/-effects.tsalready 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-lifepopulates — 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_metand one a failure — the same both-paths bar W40 set forstable-life. - W72.3 Tier 1 and Tier 2 validation pass on the Bulgarian resolution, producing no warning
class
stable-lifedoes not also produce. - W72.4 Every player-facing string the pack adds is a
LocKeywith 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 demolists and plays the Bulgarian resolution alongside the base one.
- W72.1 The pack supplies Bulgarian content for every collection
- 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 withnpm run demofromsrc/engine/: a stdin/stdout loop over the existingTextClientandSessionStorethat lists the committed campaigns, takes a campaign id and an optional seed, and accepts<actionId> [key=value ...],view,save,load <id>andquit. Verified playingstable-life— the simulation kind — end to end, so it covers both shipped kinds and not just the story-graph ones. It lives outsidesrc/, so neither the determinism guard nor the client-contract import rule applies to it, and it is covered bynpm run lintandnpm 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 inREADME.md,src/engine/README.md, and the landing page. - Depends on: W19, W31, and W41.
Implement against whichever
landing build/merge mechanism is on
mainwhen 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_VERSIONstill 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
SessionStoretypes 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 >= 3loop, see the gated choice with its reason, exercise the seeded transition, reach the existing ending, seeit_builds_character, and start again. No rawLocKeyappears 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 identicalScene/PlayerViewsteps and byte-identical finalserialize()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.
- W61.1 The supported engine entry graph used by the site produces a real browser bundle
with no
- 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'splatform-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
ProjectReferencemay unblock local development, but W62 cannot merge until the project uses one exact releasedSubZeroDev.Platform.Hostingpackage 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 callsAddPlatformWebHost()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.HostingNuGet 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
wwwrootin 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 returns404with no SPA fallback. - W62.5 The browser demo remains W61's local
SessionStoreclient: 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
mainworkflow 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 nolatesttag, 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.
- W62.1
- 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
showWhendiscoveries 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 markedreplayCompatible: falseas 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_actionif 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 rawLocKey, 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, rawPlayerView, 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.
- W64.1 Each campaign has at least three material routes, three optional visible scenes,
three delayed consequences, three
- 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 undersite/src/play/andsite/src/, committed baseline snapshots, and the CI workflow that runs the site's check script. Nosrc/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-motionand 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 --checkall 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.mdis closed or narrowed to whatever genuinely remains, rather than left standing beside a harness that resolves it.
- W65.1
- Out of scope: any
/play/visual, layout, type, or markup change — that is W66; migrating the existing jsdom suite wholesale; a harness fordocs/; 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.tsxand the site's browser/visual checks, andsite/play/index.htmlonly if a viewport ortheme-colorcorrection is needed. Nosrc/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, andgit diff --checkall 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 restoredjsonlobservability suite. Nodesign/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 againstcampaignVersion: "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 honoursREPLAY_BASELINE_DIRthe same way thestable-lifeandworld-graph-mvpsuites 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.jsonhas 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 undernullEmitterandrecordingEmitterwith 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
recordingEmittertwice yields the identical event sequence, withgameIdnormalized out (05 §12). - W67.6 A
jsonlobservability suite restoresMVP.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 --checkall pass, and no orphaned fixture file remains infixtures/replay/.
- W67.1 Three
- Out of scope: campaign content changes; a new corpus for
simulationorworld-graph, both of which still have theirs; the cross-repository replay corpus; any change to the runner's verdict vocabulary or toOutcome'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.5 — SaveRecordStore.delete has no caller in the engine or in either shipped host,
so the port requires a method nobody invokes. W68.2's resume affordance rests on a
per-player save query the SessionStore contract has no operation for, which two independent
hosts have now separately invented.
The original statement of the unit follows, unchanged, as the record of what was planned.
Delivers: The working half of the durable local checkpoint that
13 §5 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.tsandPlayApp.test.tsx.src/engine/only ifSaveRecordStore.deleteis 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
saveIdfor bothgetandput, 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_failurerendered through the string table — never a raw English message — and the run continues in memory. A run withSessionPersistenceomitted 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.deleteis 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
SessionStoreand 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 --checkall pass.
- W68.1 The adapter addresses a save by
- 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
SaveEnvelopeand §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 publisheddefineLandingPageadapter and protectedmergecommand. -
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 undersite/scripts/; the obsoletesite/vite.config.ts,site/index.htmlandsite/roadmap/index.html;build/Merge-LandingPage.ps1;.github/workflows/docs-ci.ymland.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 thecta-playlink fromApp.tsxand its rule fromlanding.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.jsonpinssubzerodev-platform-ui-landing-pageexactly at0.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.tsdeclares 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 buildemitssite/dist/index.htmlandsite/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,buildandmerge; the handwritten Vite route inputs andbuild/Merge-LandingPage.ps1have 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 protecteddocs/subtree remains byte-identical. A landing fixture containing a top-leveldocs/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.ps1andgit diff --checkpass; the production documentation build remains a separately reported Docker-dependent gate.
- W69.1
-
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 undersite/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 exactlycampaigns/manifest.jsonplus 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.ps1andgit diff --checkall pass.
- W70.1 A build-verification assertion scans the emitted
-
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 workloadprocess 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:resolvePacksas a pure ordered fold; campaigns replace wholesale, strings per key; exact-version dependencies with no range solving;campaignVersionstamped with theResolutionIdso 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 viaExperimentSource(06 §5.5). Before mods, not before MVP (neaas-platform-vision.md→ Known deferred gaps).src/engine/src/core/composition/types.tsnow declaresExperimentSource, and the gate machinery ships and is exported. 06 §4 now settles the remaining seam:SessionHost.experimentscarries the already-resolved, non-null assignment map for event attribution; candidate packs andExperimentSourceremain 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 outsidesrc/, the same placement and rationaledemo-cli.tsalready uses;src/engine/package.jsonscripts; a broken fixture undersrc/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
startNodeIdreaches 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
requirementsno 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.
buildValidatedContentRegistryis 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 lintandnpm testpass fromsrc/engine/.
- W73.1 Running the checker over a committed story-graph campaign reports, per ending,
whether some sequence of choices from
- 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
simulationandworld-graphkinds, 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.2–W74.5 are retired and never reused; the gap in this unit's numbering
is the record of the split. Two canonical statements still cite W74.5 by name —
20 §19 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.tsandconsumer-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.tsis shipped and gates nothing here. - Status: Done — closed by #301, all four criteria ticked.
- Done when:
- W74.1
@the-running-dev/game-engine/authoringis packed, installable, and exposes only the documented author-time surface. - W74.6
consumer-smokeresolves@the-running-dev/game-engine/authoringfrom the packed tarball, not from a source link, and imports every value and type the subpath exports. Deleting one export fromsrc/engine/src/authoring.tsfails 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-*orwhat-would-lucifer-do*— is reachable through it. The sharedbuildAdventureCampaignis not a published campaign and stays. - W74.8
npm run typecheck,npm run lintandnpm testpass fromsrc/engine/, andinstall:engine,buildandsmokepass fromconsumer-smoke/.
- W74.1
- Out of scope: removing anything at all — no campaign source, root export, exporter or
/play/artifact is deleted here, andsrc/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.tsandbulgaria-bureaucracy.bg.ts; a new freeze assertion beside them; the fourbulgaria-bureaucracy.*.test.tsevidence 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 itsdigestPortableCampaign— 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 fromfixtures/replay/, and.github/workflows/ci.yml's release-tag cross-version job still namessrc/campaigns/bulgaria-bureaucracy.replay.test.ts. - W74a.5
npm run typecheck,npm run lintandnpm testpass fromsrc/engine/.
- W74a.1 A committed golden pins the built campaign byte-for-byte — the canonical
serialization of
- Out of scope: removing Bureaucracy from
scripts/export-campaigns.tsor fromsite/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); and13-playable-web-demo.md, Succeeded by SubZeroDev.Adventures. - Touches:
site/src/play/in full, includingbrowser/__screenshots__/;site/public/campaigns/;site/vitest.config.tsandsite/vitest.browser.config.ts, if either names a removed path;src/engine/scripts/export-campaigns.tsand theexport:campaignsscript insrc/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, andnpm --prefix site run checkpasses — 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, andsite/scripts/verify-build.mjsandsite/scripts/verify-merge.mjsboth still pass, which is what proves the built landing page never referenced it. - W74b.3
scripts/export-campaigns.tsand theexport:campaignsscript are gone, andnpm run typecheckfromsrc/engine/still covers the remaining scripts throughtsconfig.scripts.json. - W74b.4 Nothing in the repository fetches
campaigns/ormanifest.jsonfrom a relative site path any longer; a search acrosssite/for both returns nothing. - W74b.5 The engine's campaign sources and root exports are untouched:
src/index.tsstill exports all nine builders andsrc/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 lintandnpm testpass fromsrc/engine/;npm --prefix site run checkpasses;./build/Test-Documentation.ps1passes.
- W74b.1
- 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.mdor14-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.jsonbefore 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 anddesign/10-design.md§13, for the retiredW74.5citation only. - Depends on: W74, W74a, W74b. W74b is a hard ordering rather than a
preference:
site/src/play/browser-client.test.tsimports six campaign builders from the package root, so removing them before that file is deleted breakssite'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.jsonfrom its ownscripts/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.0and 75 endings between them, and Content'smanifest.jsoncarries 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.tsandscripts/demo-cli.tsboth 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.jsonreads0.9.0, andconsumer-smokeresolves 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 lintandnpm testpass fromsrc/engine/;npm --prefix site run checkpasses;install:engine,buildandsmokepass fromconsumer-smoke/;./build/Test-Documentation.ps1passes.
- 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
- Out of scope: publishing or tagging 0.9.0 — the release workflow is the user's to trigger,
and
design/90-decisions.mdrecords 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. RemovingfromPortableor thePortable*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.1–W75.6 were
satisfied by what landed in d44afde and PR #311; W75.7 was verified 2026-08-19 on main
at 7a66da3 — typecheck, lint, 1116 tests across 79 files, git diff --check, and
./build/Test-Documentation.ps1 all clean. Criterion ids are retained as written — they are
never renumbered — so the record of which arrived how stays readable.
- Spec: 20 §7.2, §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_CONFLICTraisesSessionStoreErrorwith codeconcurrent_modification; an unbranded throw and a differently branded throw both still raisestorage_failure. - W75.2
concurrent_modificationis inBASE_REASON_CODESwith a shippedcore.reason.*message, and20-contract.md§12's list and growth tally agree withreasons.ts. - W75.3 The brand is matched on
name, not byinstanceof, and a test proves it by raising a plain object carrying only thatname. - W75.4 A conflict on
submitActionleaves 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_failurefor a branded exception, with #226 named as the owner of the save-side race. - W75.6
SESSION_PERSISTENCE_CONFLICTandSessionPersistenceConflictare root exports, the package version is bumped for the added surface, andconsumer-smokeresolves both. - W75.7
npm run typecheck,npm run lintandnpm testpass fromsrc/engine/, and./build/Test-Documentation.ps1passes.
- W75.1 A session write whose adapter throws an exception branded with
[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.tsandtypes.ts;src/engine/src/core/validation/tiered.ts;src/engine/src/index.ts;src/engine/src/campaigns/stable-life-packs.tsandstable-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
mainand ticked 2026-08-19. - Done when:
- W76.1 One call takes an ordered pack set and returns a validated, frozen registry whose
resolutionequalscomputeResolutionIdover 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: falsecarrying 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_unexpectedwarning and a kind-supplied warning appear together, so folding swallows neither. - W76.5 The no-pack route is unchanged.
buildContentRegistryand the existingbuildValidatedContentRegistrysignature still produce a registry withresolution === 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
titleKeyresolves nowhere in the resolved set fails withmissing_string_key, and a campaign whosetitleKeyresolves 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'sresolveStableLifeRegistry— 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
resolutionassignment 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-smokeresolves it through the packed tarball rather than a source link. - W76.10
npm run typecheck,npm run lintandnpm testpass fromsrc/engine/, and./build/Test-Documentation.ps1passes.
- W76.1 One call takes an ordered pack set and returns a validated, frozen registry whose
- Out of scope: making
resolutionrequired onContentRegistry; changingresolvePacks' fold rules orcomputeResolutionId's digest; the engine readingresolution; 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/, alongsidevalidate-campaign.ts— the placement and rationale02-architecture.md§9.2 already states;src/engine/package.jsonscripts.src/engine/src/core/validation/tiered.tsis 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
buildValidatedContentRegistryproduces for it, each rendered with itscode, its message resolved against the string table — never a baremessageKey— and itspathwhen 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.tsandstable-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/*.tsand 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.
buildValidatedContentRegistryis 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 adetailsentry 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 lintandnpm testpass fromsrc/engine/.
- W77.1 Running the check over a named committed campaign reports every Tier 1 error and
Tier 2 warning
- Out of scope: Tier 3 reachability, which is W73's
validate-campaignand 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; migratingdemo-cli.tsandexport-campaigns.tsonto 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.jsonscripts; a second-locale fixture undersrc/engine/src/campaigns/.src/engine/src/core/localization/andsrc/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 keysBuiltCampaign.stringscarries — 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-bureaucracyand its.bgbuild, 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 --shortclean. - W78.6
npm run typecheck,npm run lintandnpm testpass fromsrc/engine/.
- W78.1 For a named campaign the tool emits the complete, sorted set of
- 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.jsonscripts.src/engine/src/core/registry/packs.tsandsrc/engine/src/campaigns/stable-life-packs.tsare 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
resolvePacksandcomputeResolutionIdare unchanged and the tool only reads them — asserted by no file undersrc/engine/src/core/changing. - W79.6
npm run typecheck,npm run lintandnpm testpass fromsrc/engine/.
- W79.1 Given two ordered pack sets the tool reports both
- 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.jsonscripts. - 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,autoandrandomnodes. 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 lintandnpm testpass fromsrc/engine/.
- 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
- 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/designbefore it is sliced. Also out: graphing thesimulationandworld-graphkinds, 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.tsfor the two event names;src/engine/src/kinds/world-graph/reasons.tsif an audit reason is added;src/engine/src/campaigns/world-graph-mvp.ts; a new fixture pair undersrc/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'sbuildeffort 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
buildaction 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
buildcandidate 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 omitbuildis never assigned one, asserted with a cleaner-only roster that leaves the site untouched. - W81.5
advance_ticks nacross 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.progressedandconstruction.completedare emitted at the sites 12 §12 names, declared inKind.eventNames, and both rows' status moves todeliveredin the same change. Dropping every event still leavesserialize()identical (05 §2). - W81.7 The
advance_ticksresult 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.mdis 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 lintandnpm testpass fromsrc/engine/.
- W81.1 A building definition declaring construction work, placed through
- Out of scope: demolishing or refunding a site that is still under construction —
demolishtargets 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 sharedSystemPipeline(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 undersrc/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
restockkind 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
nullis 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.changedand 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 nserializes 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 lintandnpm testpass fromsrc/engine/.
- 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
- 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_pricealready 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 undersrc/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.changedis 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 nserializes 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 lintandnpm testpass fromsrc/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 undersrc/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.raisedis emitted and declared, alongside the already-deliveredincident.resolved. - W84.7
advance_ticks nserializes 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 lintandnpm testpass fromsrc/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.tsfor the kind-owned alert strings;src/engine/src/campaigns/world-graph-mvp.ts; a new fixture pair undersrc/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_unlockedreason, 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|messagein 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.raisedandalert.clearedare emitted and declared, with alert creation and removal audits hidden as the contract requires. - W85.7
advance_ticks nserializes 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.mdis 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 lintandnpm testpass fromsrc/engine/.
- 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
- 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.
simulationis 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.tsandsrc/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
datafields it lists and no others, and all ten are declared inKind.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.evaluatedfires 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 owndatacolumn, 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 declareschoiceIdandsatisfiedonly, which cannot name a clause. Emit what the table declares and state the discrepancy in the pull request; widening thedatacolumn 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 identicalStateChangearrays. - 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 lintandnpm testpass fromsrc/engine/.
- W86.1 All six remaining rows are emitted at the step the table names, with exactly the
- 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: W81–W85, 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 identicalStateChangearrays. - 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 lintandnpm testpass fromsrc/engine/.
- W87.1 Every row in 12 §12's table is emitted at the system
it names and declared in
- 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 asynclistCampaigns, amin/maxonVisibleStat, a cross-kind-legibleKind.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 atSessionPersistence(04 §7.2);createSessionLayernow 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 drivesEnginedirectly, but for its own stated reason — it needs the rawGameStatenoSessionStorereturns (07 §3.2) — not for want of a root. -
wisdomattribute 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_businessandoperate_businessare members of 10 §4.2's closed union, soResolverTable's completeness check compels a resolver for each — but §7 declares noProjectDefinitionorBusinessDefinition, 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
relationshipsend-of-week system has no rule to implement. 10 §3 names it inEND_WEEK_SYSTEM_ORDERand §6.11 givesRelationshipStateits full shape, but no weekly movement rule for it is stated anywhere in the contract — the stub's own comment insrc/engine/src/kinds/simulation/endOfWeek.tshas said so since W37. W56 buildssocializeagainst 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,
demandBandthresholds, housing-quality formula, travel costs) need a balancing pass once the sim harness runs. - Three
docs-templatehardening findings, to raise upstream — after this PR merges, not before. Surfaced by automated review on PR #3; all three sit in files installed verbatim fromghcr.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 thedocs-templateproject, filed as a separate PR there once this one is settled:docs-ci.yml/docs-deploy.ymlpinghcr.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-BaseImageavoids 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 withJoin-Path+GetFullPathand only checksTest-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 applyingExcludedSegments, 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_businessandoperate_businessare 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 arestubResolver— always valid, always a neutral no-op — andview.tsderivesSimulationView.plan.availableActionTypesfrom 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 gainsProjectDefinitionandBusinessDefinitionor §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
relationshipsend-of-week system has no rule to implement. Carried above, unchanged since W37: §6.11 givesRelationshipStateits 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 areconsts ininitial.ts,endOfWeek.tsandresolvers.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 onSimulationCampaignis 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
ScenarioDefinitionfield names them.PublicWorldStatewas declared by W50 soAgentStrategycould typecheck and is exercised by nothing. W57 put agents out of scope for exactly this reason and nothing has changed since. - P5 —
plan_emptyhas no field to condition on. §10 specifies it for "end_weekwith nothing planned, where the campaign forbids it", and no campaign can express forbidding it — soavailable.tsdocuments the gate as deliberately unwired rather than writing dead code. One of the two §10 codes still undispatched. - P6 —
Employment.attendanceRatioandwisdomare 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 to100at 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.
PortableMigrationisnodeMap/endingMapand sits on the story-graph arm ofPortableCampaignBodyby construction, whichsrc/engine/src/portable/format.tsdocuments 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.tsand its committed sorted export list (W74.7);src/engine/src/index.ts;consumer-smoke/smoke.ts;design/20-contract.md(theengine/04-core.mdblock, §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/authoringexport split is covered by the packed-tarball consumer smoke. - Done when:
- W88.1
buildSimulationCampaignand every*Sourcetypesrc/engine/src/kinds/simulation/source.tsdeclares are exported from@the-running-dev/game-engine/authoring;SimulationCampaign,SimulationKindState,SimulationView,PublicWorldState,SimulationOutcome,ActionType,GameActionandWeeklyActionPlanare exported from the package root. - W88.2
consumer-smokebuilds a simulation campaign from source through the subpath and types aSimulationViewthrough 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*orbulgaria-stable-lifebuilder 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 lintandnpm testpass fromsrc/engine/;install:engine,buildandsmokepass fromconsumer-smoke/;./build/Test-Documentation.ps1passes.
- W88.1
- Out of scope: moving
buildWorldGraphCampaignor 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 weeks —
stable-life-win is three end_week calls, and all twelve simulation fixtures under
src/engine/fixtures/replay/ are two or three. Each was built to prove one system, which is
what W53–W57 needed and is not what Life in the Fast Lane is: a scenario measured in years,
where the same fifteen systems run two hundred times over accumulating state.
What a long run exercises that a short one cannot is why this is a unit rather than a
bigger fixture appended to an existing one. Three weeks never reaches the end of the eviction
ladder; never completes a course, let alone a promotion chain whose minimumWeeksInRole gates
re-measure from each promotion; never accumulates enough expired activeEffects, resolved
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 undersrc/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 againstRESOLVER_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.ranevent 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 lintandnpm testpass fromsrc/engine/.
- W89.1 A committed fixture plays at least one hundred and fifty weeks to a terminal
- 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_staffreturns aStateChangeonly 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 intick/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 lintandnpm testpass fromsrc/engine/with every previously committed replay outcome unchanged.
- 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
- Out of scope: changing canonical-path cost or tie-breaking, adding new placement rules,
adding scenery build/demolish actions, or broadening
StateChangebeyond 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
pendingResponsescontains a mandatory event response, the projection offers only the response choices that event permits;end_weekand 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
validateUnreachableItemstreats an item as reachable when at least one reachable location permitsshop; 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
positionsAvailablefrom 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 lintandnpm testpass fromsrc/engine/.
- W94.1 While
- Out of scope: balance-tuning the long run, defining rival strategies, changing event
content, or adding a new item-acquisition action beyond the shipped
shoppath.
[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 indesign/20-contract.mdand 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
StatusEffectgoes through one invariant: a same-sourcerefreshreplaces the prior layer and resets expiry, a same-sourcestackretains 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.appliedfor 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.ps1passes. - W95.7
npm run typecheck,npm run lintandnpm testpass fromsrc/engine/, with existing replay outcomes unchanged.
- W95.1 Every simulation code path that inserts a
- 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: W93–W95, 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, andworld-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, orPlayerView, 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
EffectContextand 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 lintandnpm testpass fromsrc/engine/; all committed replays remain byte-identical.
- W96.1 World-graph batch invariance compares complete canonical kind state for unsplit runs
against
- 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.viewseam 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
/contractdecision that assigns defensive-copy ownership at theKind.projectseam and a/designor/contractdecision 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 lintandnpm testpass fromsrc/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
/contractpass 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_campaignsrender the same resolved titles without starting a session or readingContentRegistrydirectly, 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 ofSubZeroDev.Adventures(10-design.md, Succeeded by SubZeroDev.Adventures), and its own reconciliation ticks that box, the same treatment10-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 lintandnpm testpass fromsrc/engine/; site and host checks pass with committed replay outcomes unchanged.
- 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
- 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
/contractpass 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.newGameIdpinned to the originalgameId, 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
SessionStoreremains 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 lintandnpm testpass fromsrc/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
/contractpass that fixes the tuning fields and defaults, theplan_emptypolicy, 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_weekwithplan_emptyand unchanged state, while a permitting campaign advances through the ordinary ordered pipeline; the default preserves 0.10 behaviour. - W100.3 The
relationshipssystem 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
wisdomhas 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 lintandnpm testpass fromsrc/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
/contractpass 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
ActorStatemechanics 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 lintandnpm testpass fromsrc/engine/.
- Out of scope: adaptive/remote AI, hidden-information cheating, multiplayer, arbitrary
user-authored code, economic balance of a concrete campaign, or changing
ActorStateinto 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
/contractpass defining both public shapes before implementation. - Status: Complete — PR #423, merged at
6e4a7bb. - Done when:
- W102.1
PlayerProfilehas 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: falsesticky-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 lintandnpm testpass fromsrc/engine/; all unaffected committed replay outcomes remain byte-identical.
- W102.1
- 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.ps1reports zeroconceptfindings; 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.GameOfLifeS6/S7 are landed (its issues #13, #14);design/20-contract.md§15.1 maps all twelve of S7'slifecycle-concepts onto this kind's own shape. No contradiction with shipped simulation code surfaced. - W103.2 Platform's hosted MCP contract includes
preview_actionand 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) intoSubZeroDev.ServiceContract'smcp-tool-contract.md(5d3fb8e, package0.6.0), which namespreview_actionas the tenth of thirteen operations, including W98's revisedlist_campaignsand W99'slist_saves/branch_session/delete_save. Platform'sworkloads/game-service/src/mcp-surface.ts(665103a) buildslistTools()/callTool()directly from that package'scontract.operations, so the mapping is generated, not hand-mirrored. Closes #269 (see90-decisions.md). - W103.3 The service-contract package's
src/generate.ts(SubZeroDev.ServiceContract@5d3fb8e) enforces arity both ways — every engineSessionStoremethod must have a row and every row must name a real method — failing the build on a missing or extra operation. Adventures vendors that same0.6.0artifact (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 againstloadPublishedContract().operations. The browser client,src/play/remote-store.ts, implements all thirteen operations — catalog, save, load, branch, and delete included — asfetchcalls against the server's HTTP routes, never importingSessionStoreor 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. See90-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 in90-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-syncfast-forwarded~/.agent-kit(5095a55→36f0a7a, 44 commits) and reconciledtools/*.ps1and the two divergent command cores viatools/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.ps1passed (18 generated engine pages, compatibility pointers, guide, and 189 Markdown files);src/engine'stypecheck,lint, andtest(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.
- W103.1 GameOfLife S7 is landed and its
- 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 W98–W102.
- Touches: a committed 0.10 compatibility corpus; campaign builders, save loader, replay runner, host/service adapters, package archive assertions, and consumer smoke projects.
- Depends on: W93–W103 complete. This is the first unit allowed to call their combined result a release candidate baseline.
- Status: Landed — all seven criteria confirmed met; closed by issue #392.
- Done when:
- 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.tsnames 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 toregressionManifest.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/*.jsonfreezes the canonical initialSceneand Tier-1/2 findings per built-in campaign, captured at thev0.10.0tag; 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 realSessionStore.loadGameand 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: falserather than silently replacing its golden file. Closed by PR #447: 29 fixtures replayed againstv0.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
e52e7a1was re-vendored intoSubZeroDev.Platform,SubZeroDev.Adventures, andSubZeroDev.ServiceContract, each passing its full suite with no reach pastSessionStore/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
/authoringexamples without workspace resolution. Closed by PR #447: clean tarball, no workspace resolution,/authoringexports 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.ps1composes typecheck/lint/test, the replay oracle, and the packed-tarball consumer smoke into one documented command.
- 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:
- 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.0is 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.ps1pass 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: W93–W105. It closes evidence; it does not substitute issue closure for unfinished implementation.
- Status: Complete. Closed via #394; all criteria (W106.1–W106.7) verified.
- Done when:
- W106.1 A frozen inventory records every open engine issue number, title, current state, owning W criterion or explicit non-programme disposition, and immutable implementation, decision, supersession, or external-blocker evidence.
- W106.2 An issue is closed as implemented only when each of its acceptance checkboxes is satisfied by a merged commit/PR and passing verification; stale and dissolved issues cite the exact later contract/commit that removed their premise.
- W106.3 Decision-only issues cite the canonical decision that answers them and no longer retain contradictory open-register text; externally owned issues cite the companion issue/commit and remain open when that work is not actually complete.
- W106.4 The Presentation spike remains open with its unavailable/blocked state and transfer target unless the Presentation repository and authority are genuinely available; no engine issue or slice claims to implement it.
- W106.5
/trackreports 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 falseTrackerUnavailableare 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.0candidate2b525d3. 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 head14f4323; 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 becausestable-life.tslacked theprojectsandbusinessesthat W101 made required, and, once that cleared, the event-chain declarations W102 requires. Both were pre-existing, not caused by0.11.0, and both are fixed in the companion by SubZeroDev.GameOfLife#120 (8ea9441). GameOfLife now compiles against engine6910bf5, whosesrc/engineis 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 committeddocker-compose.ymlservice 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.jsonis 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
200for/,/roadmap/,/docs/, liveness and readiness,404for a named unknown route, and a negative fixture that fails to start on a missing required artifact, with no source tree mounted at runtime. Per15-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.0is 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 isRELEASE.md, naming the candidate2b525d38d6876829b35e9f5ff5fcf22af421f082, the archivethe-running-dev-game-engine-0.11.0.tgzand its SHA-256, the requiredv0.11.0tag, 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 tosrc/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 in90-decisions.md: W108.2 acceptsRELEASE.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 governingSubZeroDev.Adventuresinstead 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.0as 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.
- W108.1 The canonical roadmap, README, generated public docs and landing page all identify
- Out of scope: performing the publication, choosing credentials, deploying the site, announcing the release, or adding any feature/fix after the verified candidate.
Depth: The Five GameOfLife Engine Blockers
Five contract amendments dated 2026-09-07 in 90-decisions.md each close a gap a Life in the
Fast Lane issue named, and every one is contract-only: the types and rules are stated, and
nothing in src/engine reads them. They are 0.12 scope — W108'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
DerivedPathunion and its writable/formula-only partition, §6.2's storedreputationrecord, §7.1's writable-target table, and90-decisions.md's 2026-09-07W105.1entry. - 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.targetisplayer.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 thecalendar.committedTimeUnitsexception is unchanged. - W109.2 Resolving
player.reputation.<key>returns the stored base with every active modifier layered over it in §6.1's fixedadd/subtract→multiply→setorder, clamped to0–100— the same treatmentplayer.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 theplayer.skills.*precedent line for line. - W109.5 A
Modifiertargeting any of the four formula-only paths still fails Tier 1read_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.
- W109.1 A campaign whose
- 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.startingMemoriesand theNPCMemoryshape it carries, §14's every-LocKey-resolves rule, and90-decisions.md's 2026-09-07W105.4entry. - 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
descriptionKeyresolves to nothing in the string table fails Tier 1 validation; state both counts — campaigns accepted, and campaigns rejected by this check.
- 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
- Out of scope: what a week does to memories or relationships — the
relationshipsend-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-frozenExistsCondition/CountConditionandConditionResolver.collectionseam, §14's Tier 1 list, and90-decisions.md's 2026-09-07W105.5entry. 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
existsandcount; state the count of paths accepted. - W111.2 A
whereclause reads fields relative to a single array element: anexistsover the player's inventory testingdefinitionIdina 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_collectionat 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
countcondition compares the match total against a number and is correct at zero, at the comparison boundary, and above it. - W111.5 A
wherenaming a field the array element does not carry — an item'scategory, 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'sConditiontype is unchanged by this unit.
- W111.1 Each of §8.2's seven declared collection paths resolves to its state array and
supports both
- Out of scope: joining a collection member against its content definition, which would need
a core-level
ConditionResolverwidening 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, and90-decisions.md's 2026-09-07W105.2entry. - Touches: the
inventoryend-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/transportCentsand the reserved"vehicle"tag, §6.9'sHousingState, §3'shousingandfinance_reconcilesystems,04 §10.2'skindVersion/Kind.migrateStateaxis, and90-decisions.md's 2026-09-07W105.3entry including its same-day revision. - Touches: the kind's housing content and runtime types, the
housingend-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.