Open Questions & Known Concerns
Document status: Living register. Captures unknowns, gaps, and deferred decisions so they are planned, not rediscovered as bugs — the project's working convention.
Scope. A single place to see what is not settled. Full entries for concerns first surfaced here; pointers for items that already live in another doc — this register indexes, it does not duplicate (duplication is itself a drift surface).
- The finalized MVP contracts:
03-story-graph-kind.md·04-core.md- The task list:
TODO.md· the MVP target:MVP.md
1. MVP-Relevant Gaps — All Resolved
Every gap that blocked the story-graph MVP has been decided and written into the contracts. Kept here as a decision log: what the question was, what won, and where the answer now lives — so a later reader finds the reasoning without re-opening the argument.
| # | The gap | Resolved as | Lives in |
|---|---|---|---|
| 1.1 | PlayerProfile was defined only in the simulation kind, but the MVP DoD requires cross-session achievements | A ProfileStore beside the session store. profileId on CreateSessionConfig, never on NewGameConfig or GameState; records keyed campaignId + achievementId; the store upserts after a successful action; no profileId → anonymous, no read or write; missing/corrupt loads empty with a warning; a failed write never rolls back the game action | 04-core.md §7.1 · 03 §7 |
| 1.2 | Base reason codes had no player-facing strings | The core ships default-English messages under a reserved core.reason.* namespace. Registry merge rejects overrides — a campaign cannot restyle an engine-level error. Validation fails if any registered code lacks a message | 04-core.md §12 |
| 1.3 | The authoring → registry build step was prose, not a type | A typed source/runtime split: AuthoredText, per-kind …CampaignSource, a pure builder returning BuiltCampaign. Parsing and file I/O live in an outer adapter. One locale (English) for the MVP | 04-core.md §10.1 · 03 §1 |
| 1.4 | Could a campaign settle straight to an ending at turn 0? | Valid, and it plays. Validation emits a Tier 2 no_reachable_choice — warns the author without banning vignettes or single-scene fixtures | 04-core.md §11 · 03 §11 |
| 1.5 | Kind.initialState returned a bare KState, so a start that settled to an ending was recorded active | initialState returns InitialStateResult<KState> — AdvanceResult minus error, since a pre-validated campaign cannot fail to start. The core takes status from it and never inspects kindState | 04-core.md §3, §4 |
| 1.6 | params were written to the replay log but never handed to the kind | Kind.advance receives params. The story-graph kind declares none and rejects a non-empty object with unexpected_params — never silently ignored | 04-core.md §3 · 03 §8.2 |
| 1.7 | The story-graph kind declared no reason codes, and hidden-choice rejection was undefined | Three added codes (not_a_choice_node, unexpected_params, settle_guard_tripped) plus base reuse. A showWhen-hidden choice returns unknown_action — identical to a nonexistent id, so a probing client cannot confirm a secret path exists | 03 §8.3 |
| 1.8 | GameState.formatVersion and SaveEnvelope.saveFormatVersion both versioned "the format" | Both kept, distinction documented. Engine.serialize/deserialize round-trip a bare envelope with no wrapper — the golden files compare exactly that string — so the envelope needs its own stamp | 04-core.md §2, §10.2 |
Nothing MVP-blocking is currently open. When the next gap appears, add it here as a full entry, and move it into this table once decided.
2. Deferred by Decision — Post-MVP (Indexed; Live Elsewhere)
Settled as out of MVP scope. Listed so they resurface deliberately, not by accident.
- No customer, alternative, or monetization thesis — deferred, and now recorded as deferred rather than merely absent. The brief states what the platform is, who plays it, and what it refuses to do; it says nothing about who would buy it, what they would otherwise use, or how it would earn. That silence is deliberate, not an oversight. This repository ships a deterministic engine, its specifications, and its authoring tools; the commercial layer is a different repository — SubZeroDev.Platform, the deferred hosting / NEaaS layer. Stating a monetization thesis here would put product strategy in the document that owns engine scope, and every reader of the non-goals would then have to work out which of the two they bind. Owner: the repository owner (@The-Running-Dev); this is a product decision, and no specification pass settles it on their behalf. Revisit when Platform work actually starts — the first point at which a named customer, a named alternative, and a price have somewhere to live that is not this repository.
- Package visibility, decided: public.
src/engine/package.jsoncarries no"private"field and publishes tonpm.pkg.github.com; Engine Package states the choice is deliberate.plans/39-world-graph-kind-programme.mdandplans/40-w41-engine-consumer-boundary.mdboth specified private GitHub Packages at the time they were written and are annotated in place ("What shipped did not honour this") — left as historical record of the original plan rather than rewritten, since a plan document records intent at the time, not current fact. This is the final answer; issue #302 tracked deciding it and is closed by this entry. - The no-engine-API browser smoke — retired, not reassigned.
design/15-platform-static-host.md§6 and13-playable-web-demo.md§6 named a browser production smoke proving/play/made no engine API request and no request outside the same-origincampaigns/files; W69 removed the route and the smoke together, leaving open whether the property moved to a replacement owner (10-design.md§6, CI, Publication, and Deployment Boundary, mirroring §4's disposition for the Node-only-import property) or retired outright. It retires: §4's property survived because it is still true of the package SubZeroDev.Adventures bundles, and that repository'sscripts/verify-build.mjsasserts it there. This property is not still true anywhere — Adventures is deliberately a hosted Fastify API with Postgres persistence and accounts, the opposite of "no backend, no engine API, same-origin files only." There is no surviving host to assert it over, so no replacement check is created. Issue #273 tracked deciding it and is closed by this entry. - Provisional simulation numbers — drift rates, scenario economics,
demandBandthresholds, housing-quality formula, travel costs. Need a balancing pass once the sim harness runs. Tracked as issue #267. end_week'splan_emptygate is declared but not wired — W50.4. §10 namesplan_emptyfor "end_weekwith nothing planned, where the campaign forbids it," andavailableActions(src/engine/src/kinds/simulation/available.ts) always returnsend_weekwithavailable: true.SimulationCampaigndeclares no toggle to condition a disablement branch on, andstable-lifenever forbids an empty plan, so wiring the gate now would be dead code exercised by no scenario. Revisit when a campaign actually needs to forbid an empty-planend_week— the natural home is a newSimulationCampaign/ScenarioDefinitionfield, decided against that concrete need.packages/vssrc/engine/naming, and companion delivery — decided for W41. The simulation docs (games/05-text-client.mdheader,games/04§20) describe an aspirationalpackages/monorepo; the built package issrc/engine/(Engine Package). Keep the built layout. W41 inplans/39makes it a private GitHub Packages npm artefact named@the-running-dev/game-engine, with one root export, declarations, exact semver consumption and a packed-tarball consumer smoke test. A siblingfile:link is allowed only as local convenience, never CI or release evidence; Git dependencies are rejected because they expose repository layout and build side effects as the delivery contract. Revisit when a second independently versioned package actually exists and makes a monorepo/workspace layout useful rather than aspirational.packVersionis duplicated per pack file, not shared engine code. The 2026-08-18 W71 decision above makes a pack'sversiona content digest rather than a hand-written number, but the function that computes it (packVersioninsrc/engine/src/campaigns/stable-life-packs.ts) is local and unexported — each pack file must call its own copy, andContentPack.versionstays a plainstringthe type system does not check (10-design.md§6). The guarantee the decision was meant to make self-enforcing is currently enforced by convention again, one level down. W79'scampaignContentDigest(src/engine/scripts/diff-resolution.ts) is now a second, independent copy of the same idea — a canonical digest over a campaign's content fields excluding the ones stamped after the fact — with its own field list (it excludescampaign.version, whichpackVersionincludes, since W79 diffs already-resolved campaigns whoseversionis the resolution stamp itself). Revisit when a second pack is authored outsidestable-life-packs.ts— that is the concrete case for promotingpackVersionto an exported engine helper that both call, rather than doing it speculatively ahead of a second caller.historyin the simulation kind's state — the upstream model carrieshistory: HistoryEntry[], a narrative record of what happened. That overlapsStateChange[], whichadvancealready returns (04 §12), and the event stream (05-observability.md). Three records of the same events is the duplication rule10-simulation-kind.md§2 exists to prevent, sohistoryis not adopted until it is established what it holds thatStateChangedoes not — most likely player-facing narrative framing, which would make it a projection concern rather than state. Revisit when the simulation kind's field detail is ported (10 §15). The same question arises forworld-graph, which declineshistoryon identical grounds (12-world-graph-kind.md§3) — resolve both together or not at all.world-graph's three evaluated-but-unstored guest opinions. The game design has guests evaluate ten factors;GuestOpinions(12-world-graph-kind.md§3.2) stores seven. Staff behaviour, accessibility and noise are treated as evaluation inputs the utility model reads from world state at decision time (§3.3), not as impressions a guest carries between decisions — a guest can weigh noise without storing anoiseopinion. Revisit when W44's utility model names a system that writes one of the three between ticks. That is the condition that would make it state; until one exists, a field no system writes, no reason code reads and no projection carries is not state, and adding it toserialize()output is how therngandtotalTimeCostdefects happened. The same test retires the condition vocabulary (drunkenness, sunburn, confusion and the rest) to content: each is an evaluation input or a within-batch transient.ticksPerDay's value is Sun Trap's, and only its value. The "today" accumulator boundary isfloor(tick / ticksPerDay)(§3.3), a pure function oftickand campaign data, so the rule needed no answer from the game. Revisit when the companion confirms the number — which changes balance and no contract. Two other gates once filed here as blocking are settled in the contract itself: rotation declares all four values and Tier 1 narrows it, andBuilding.entrancesleft runtime state as a derived value, leaving only the authored offset shape open — and that is W43's, where aBuildingDefinitionexists to hold it.ChainScope's"profile"value has nowhere to persist — closed by W102's contract gate. A"profile"-scoped event chain (10 §2.2) must survive the game it started in, andPlayerProfilehad no field for kind-declared data. It has one now:PlayerProfile.kindData(04 §7.1), a core-opaque slice per kind, written through the same mirror the achievement and terminal upserts use and read back atcreateSessionthroughNewGameConfig.kindProfileData. Found while portingWorldState(10 §2.2, the field-detail portplans/36-simulation-kind-programme.mdproposed as W27 and cut as W32); tracked as issue #268, which the four W102 gate entries below close. What did not close is the next item.- Cumulative weeks played across every game under one profile. 10 §2.2 described a
"profile"-scoped chain as advancing on this, and the shape W102 gave it recordsfurthestSteponly. The aggregate has no formulation that is at once idempotent (whichKind.profileData.foldrequires), bounded (which §7.1's 65 536-byte slice cap requires), and correct under two live sessions sharing oneprofileId(which §7's second lock domain exists because hosts allow): a sum fails the first, per-game keying fails the second, and a settle-on-game-change counter fails the third. Revisit when a campaign actually wants a chain that paces on elapsed play rather than on progress — until one does, the decision about which of the three properties to give up has no evidence to be made on. Carried in20-contract.md's own## Unresolvedsection, which is the checkable copy. - The hosted MCP contract's W48 mirror — resolved, moved rather than edited in place.
Issue #269 tracked
SubZeroDev.Platform's
docs/docs/mcp-tool-contract.mdlagging the engine's tenth operation,preview_action. Platform's own S2.11 retired that copy rather than editing it: the table now lives inSubZeroDev.ServiceContract'smcp-tool-contract.md(5d3fb8e, package0.6.0), which documents all thirteenSessionStoreoperations withpreview_actionnamed as the tenth. Platform's hosted surface (workloads/game-service/src/mcp-surface.ts@665103a) builds its tool table fromcontract.operationsat runtime instead of a hand-maintained list, so there is no longer a Platform-side row set that can independently lag. Closed as resolved. - A shared simulation substrate for tick-driven kinds —
simulationandworld-graphare the same archetype: mutate pending configuration, then resolve a block of simulated time through an ordered system pipeline (12 §2). Both hand-roll that pipeline, and it is where determinism defects concentrate — the two-phase time ordering in 10 §3 is exactly the class of bug a shared, tested runner would stop recurring per kind. ASystemPipelinein the core (ordered registration, deterministic per-system stream keying, stable iteration, derived entity ids) would make kind N+1 cheaper. Not extracted whilesimulationwas the only tick-driven kind;world-graph(W41–W49) now makes it two. Tracked as issue #270. - Third-party kinds, and the sandbox they would require — architecture §1 N2
rejected downloadable code kinds as a security and reproducibility hazard, and
06-extensibility.md§7 leaves that standing. It is a rejected mechanism, not a closed question: a WASM host with a deterministic ABI — no clock, no ambient float nondeterminism, fuel-metered — could satisfy 06 §2's rule. Revisit when there is a concrete demand for kinds the engine team did not write; the conventions in 06 §8 are chosen so that revisiting costs no rework. - Observability beyond the event channel — the OpenTelemetry exporter, sampling, and
inbound trace-context propagation; metrics as a channel separate from events; per-kind
log-level configuration; author-facing presentation of
kind.story-graph.*events. The event contract itself is MVP scope and specified; these four are deliberately not (05-observability.md§13). The first belongs with the hosting layer, which is itself deferred (MVP §4). - Doc-tree numbering merge — closed. The engine specs and the game specs both start at
01-, which was a live problem only while they shared one tree. They no longer do (02-architecture.md§12: separate repositories, separate Docusaurus sites,games/…citations are prose provenance rather than links). Confirmed nothing depends on a merged numbering: the engine specs never link intogames/…as a route, and both ofdocs/'s link checks are'throw'(agent.md, Two link checks), so a real cross-repo link would already have failed the build if one existed. There is no merged numbering to collide, and none is coming — closing rather than leaving open. SessionHost/createSessionLayer— closed, resolved exactly as this entry predicted. The open question was that06-extensibility.md§4 specifiedcreateSessionLayer(host: SessionHost): SessionStoreover aSessionHostwhosesessionsfield was itself typedSessionStore— which only reconciled ifsessionsmeant a lower-level, storage-only port that the root wraps with stamping (05 §6.1) and profile-upsert (04 §7.1), a port04-core.mdnever named. That is now the built shape:SessionPersistence(04 §7.2) is the storage-only port,SessionHostcarries it alongsideregistry,clockandrecordSink, andcreateSessionLayercomposes the core-owned store around it. The deferral's trigger was wrong and worth remembering. This entry said revisit when a secondSessionStoreimplementation is needed; a second implementation was never wanted, and what forced the root was a host needing durable records under the same store — browserlocalStoragefor W61's checkpoints. "Wait for a second instance of X" fails when the real demand is for a seam one level below X.- Enterprise's climax scene was already spent — resolved by building it (W30).
games/bulgaria-adventure.md's arc table assignedgames/bulgaria.md's "Ultimate Bulgarian Reward" scene, and by extension its achievement, to Enterprise — butbulgaria-bureaucracy.tshad already consumed both verbatim as its own ending (endingId: "ultimate_reward", achievementit_builds_character), a real W15 authoring decision the design doc never caught up to. Decided rather than deferred further: Enterprise gets new climax content (adebt_centsrunning stat replacing the accumulation half of the named exercise, one shared ending rather than a branch) and no achievement — the game's own Definition of Done needs only "at least one" across the whole game, already satisfied by Bureaucracy's, so Enterprise doesn't need its own to close the game's content requirement. Design proposed for sign-off before implementation, given inventing narrative content the source material doesn't supply is a bigger step than transcribing existing scenes. Open remainder:games/bulgaria- adventure.mditself still assigns "Ultimate Reward" and the achievement to Enterprise — that document lives in the companionSubZeroDev.GameOfLiferepository, so correcting it is a follow-up there, same treatment as the Return finding below. - "Return seeds variables the other arcs read" isn't mechanically achievable — confirmed
by building it (W28).
games/bulgaria-adventure.mdsays this of the Return arc, but every arc is built as its own standaloneCampaign(confirmed by how Bureaucracy, Driving, and now Return are all wired: a self-containedid, its ownstartNodeId, no shared session).story-graph'sCampaignhas no mechanism for one campaign'skindStateto be read by another's — sessions are per-campaign (04 §7).bulgaria-return.tswas built standalone, with no variables at all, confirming the narrative-only reading rather than assuming it. Open remainder:games/bulgaria-adventure.mditself still claims the seeding property — that document lives in the companionSubZeroDev.GameOfLiferepository, so correcting it is a follow-up there, out of scope for this repo. Nothing here blocks on it: arc build order was already safe regardless, and that has now been exercised three times over. - The replay-corpus test harness assuming one campaign per corpus directory — closed,
resolved by prefix-filtering. Filed after W22 built
bulgaria-bureaucracy.replay.test.ts's genericreaddirSyncscan offixtures/replay/against only the Bureaucracy campaign's registry, before a second campaign existed to expose it. W40 hit exactly the predicted collision and fixed it by prefix-filtering both suites (bureaucracy-/stable-life-); W49 and W67 followed the same per-kind prefix-filtering pattern rather than reopening the shared-vs-per-campaign design question. Tracked and closednot_plannedas issue #303 — the friction this entry named is resolved in practice, not merely worked around.
Found by the first downstream host — SubZeroDev.Adventures
SubZeroDev.Adventures is the play
surface extracted from /play/ (13-playable-web-demo.md,
Succeeded by SubZeroDev.Adventures). It consumes this engine as a pinned submodule across a
repository boundary and adds a hosted API, Postgres persistence, and accounts. That makes it
the first host to implement the ports against something other than a browser tab, and eight
findings are what that exercise produced. They are recorded together because their shared
provenance is the evidence: each one is a place the contract held up in one host and bent in the
second.
None of these was found by review. They were found by building. That is worth stating, because the standing bar in this register — one built instance is not a pattern — cuts both ways: several of these clear it now, for the first time.
Seven of the eight are issues, not bullets here. This register indexes, it does not duplicate, and each issue carries the Done when that a bullet had nowhere to put — so the issue is the authority for those seven, not this section:
| Issue | Finding |
|---|---|
| #276 | SaveRecordStore.delete has no caller anywhere |
| #277 | No per-player save query, and two hosts have now invented one |
| #278 | VisibleStat omits the declared range, so clients read Campaign.content to get it |
| #279 | listCampaigns() is synchronous, so no remote store can implement it |
| #280 | Reproducing a stored session's blob requires pinning IdSource.newGameId |
| #281 | SessionStore has no concept of a caller, so authorization lives outside it |
| #282 | Kind.outcome has no shape a host can read generically |
The eighth is kept in full because it is the only one that arrived with a working implementation, and the caution attached to it is what a reader needs before copying that implementation:
- Session forking is built, and it bypasses the store. Adventures replays a stored action
log to an arbitrary
atSeqand writes a newStoredSessionRecordstraight to persistence, because no store operation covers it. This is issue #266 with a working reference implementation — and a caution, since writing through the persistence port rather than the store leaves the store's in-memory session cache unaware of the new session.
One further item was a standing cross-repository hazard rather than an engine defect, and is
now resolved: Adventures depended on fromPortable and the Portable* types while
src/engine/src/index.ts marked them // SPIKE: … not a contract export, so a submodule bump
could legitimately break the downstream host. 6991e37 (0.6.0) graduated the format: that same
line now reads "A real contract export", no SPIKE marker survives anywhere under
src/engine/src/, and §19 of 20-contract.md states the surface. The dependency is sanctioned,
so the hazard is gone — kept rather than deleted because the fact that it was once unsanctioned
and was deliberately regularised is the reasoning a later reader of
issue #285 will want. That
issue's premise no longer holds and it is /track's to close.
incidents[].onStart's building-meter rule — closed by W84, not W47. After W83, every
other effect list was accounted for: products[].effects and a building's operation.effects
defer as service, scheduledChanges and policies[].whileActive as policy, a
staff-resolved onResolve as staff, and a wear delta on objectives.onCompleted,
failures.onTriggered, or a duration-bearing onResolve is rejected (§9.2). onStart was
neither, and nothing misbehaved only because no system applied it yet — it was declared,
shape-validated, and dead. This entry named W47 as the unit that would make it live and own
the choice; the incidents family that actually did so is W84, and it resolved the
question this entry left open. tick/pipeline.ts calls applyWorldEffects on
incidentDefinition.onStart from exactly one site — system 16/17's roll, after system 14 has
already closed its broken-transition check for the tick — and validate.ts's
forbidUndeferrableWearDelta(entry.onStart, …) extends the §9.2 rejection to it, with the
comment recording why: "onStart only ever runs from system 16's roll, always after system 14
closed its broken-transition check for the tick, so a wear delta there can never be seen
(W84)." No start_incident call site in systems 1 or 4 applies onStart, so the
legitimate-deferral reading was not the one built. The contract's own MVP worked example
(§13's litter incident) puts a cleanliness delta in onStart, not a wear one, so the
wear-only rule does not contradict it.
Nothing checks emitted → registered for StateChange.reason, and it has now failed twice.
20-contract.md §13 says so in its own words — a reason threaded through EffectContext is not
visible at any call site that also names a visible flag, so the usual audit (scan for reason:
beside visible: true) finds the direct codes and none of the indirect ones. That gap let five
world-graph codes go unregistered through three units and one reconciliation pass. W83's
building_broken was the second occurrence: it shipped as an eleventh visible: true audit code
against a table stating there were ten, with all six required checks green, and was caught by code
review rather than by any gate. The fix is a test that fails when a reason recorded with
visible: true is missing from the contract's audit table; it was scoped out of W83's review pass
as its own unit, because parsing a markdown table from a test is a new kind of coupling and wants
deciding on its own. Until it exists, the tables are kept correct by hand and this is the note
saying that is a manual control, not an enforced one.
A deferred building-meter effect was marked applied before system 14 composed/clamped
it — closed by W95, not left as accepted. effects.ts's deferred branch still sets
applied[index] = true as soon as the local per-source delta is nonzero, not once the
final composed value actually differs from previous — unlike the non-deferred and
guestMeters branches, which wait for the clamped outcome — so .applied itself remains
technically premature for a deferred effect. Filed as
issue #349, which
recorded three ways out: leave it; stop marking deferred meters applied at all (trades
over-reporting for under-reporting, not obviously better); or move the event emission into
system 14 alongside the composition. W95 took the third, narrower than first scoped: rather
than touching the shared applyWorldEffects interpreter seam across all six call sites, it
moved only the one thing that actually read .applied for a building_meter_delta effect —
scenario (system 1) now skips emitting for that kind entirely, and cleanlinessWear
(system 14) emits kind.world-graph.scenario.effect.applied itself, once, only when a
policy-sourced contribution's building/meter pair actually changed after the single clamp.
effects.ts's applied[index] value for a deferred effect is therefore still not
meaningful on its own — it is only correct once combined with the caller no longer trusting
it for this kind. Revisit if a future caller reads .applied for a deferred
building_meter_delta directly, since it would reintroduce the same premature signal.
relationships (endOfWeek.ts) is the simulation kind's only remaining end-of-week
stub, and W89 is the first thing able to observe that it stays one. No weekly
relationship rule — decay, drift, or otherwise — is specified anywhere in
10-simulation-kind.md: §6.11 declares the state and §7.7 the
NPC, but nothing names what a week does to either, so the system can never emit anything
beyond system.ran (resolvers.ts's own socialize is the only thing that moves a
RelationshipState, and only on the player's own action). W89's coverage
assertion (long-horizon.replay.test.ts) therefore reaches fourteen of the fifteen —
relationships is excluded by name rather than silently passed. Thirteen of those
fourteen are asserted over the two long runs together; week_limit, which neither long run
can reach without contradicting W89.2's two terminal paths, is the fourteenth and has its
own isolated test in the same file. Revisit when a weekly
relationship rule is actually specified; writing one is /contract work, not a slice's
(W56's own Out of scope already made this call once).
long-horizon-loss's pendingEventResponses grows unbounded across its own 160-week
run — a real instance of the exact defect shape W89 exists to be the first thing able to
see, found and deliberately not fixed here (W89's own Out of scope). Nothing in
endOfWeek.ts's events system (or anywhere else) expires a PendingEventResponse that
respond_to_event never answers; long-horizon-loss's own weekly policy is deliberately
inactive (long-horizon.ts's header explains why — the eviction-ladder arithmetic has to
stay exact), so it never answers one, and the collection grows from 0 to 37 entries over
the run. long-horizon.replay.test.ts's own W89.6 assertion states this as an observed
ceiling for this fixture, not a claim of boundedness. Revisit by giving a
PendingEventResponse some expiry (an expiresAtWeek, mirroring Opportunity's own
field) or an explicit "declined by default" resolution once its presentWeek has passed
by some stated margin — a real content/contract decision, not a slice-sized fix.
3. Judgement Calls to Revisit (Settled for the MVP)
Decided deliberately, each with a documented "revisit when." Listed here only as a pointer so they are not forgotten.
-
Story-graph kind — dropped the
stringvariable type; nounlockconsequence;autovs a one-transitionrandom;SETTLE_STEPS= 64;visitedcounts every entry. See03-story-graph-kind.md§13. -
Core — the
Conditionoperator set is frozen; additions require a concrete campaign need. See04-core.md§18. -
Core — randomness is derived, never carried: streams are a pure function of
(seed, streamId), soGameStateholds no generator state and theStreamId→ string encoding is normative. Revisit only if a kind needs a generator that outlives one resolution. See04-core.md§8. -
Story-graph —
StoryGraphViewcarries only what the genericScene/PlayerViewdo not (turn, visible stats, achievements, ending); scene text and the choice list are the core's. Revisit if a client proves it needs a self-contained kind payload. See03-story-graph-kind.md§9. -
Story-graph and simulation —
campaign.content as <KindCampaign>remains unguarded.Campaign.contentisunknownby design (04 §2), and both older kinds read it via a bareascast with no runtime shape check —story-graph'svalidate.ts/advance.ts/scene.ts/settle.ts/view.tsandsimulation'sadvance.ts/initial.ts/validate.tsall do this identically. Malformed content (cross-version data, a hand- edited fixture) can throw during registry construction rather than surface as a structuredValidationResult, which is arguably not "total" pervalidation/types.ts's own header comment. Flagged during W40's review (PR #102) against one file (kinds/simulation/validate.ts); declined there specifically because fixing one cast in isolation would have been inconsistent before a programme-owned revisit point existed. W45 establishes the convention forworld-graph: its validator narrows the unknown root and malformed nested values into structured findings, then gameplay centralizes the validated-campaign assumption in one internal accessor. Revisit when story-graph or simulation next changes its content boundary; migrate that kind deliberately rather than turning W45 into an unrelated repo-wide rewrite. -
W63 (Absurd Game Interface) — harnessed by W65; W63.7/W63.8 narrowed, not closed. W63 was marked done on manual review at 320/390/768/1280 px because
site/had no visual-regression or axe-style accessibility scanner and its tests ran in jsdom, which performs no layout at all — no computed size, hit area, or overflow could be asserted there. W65 stood up a real Chromium harness (site/vitest.browser.config.ts, the playwright browser provider) with committed self-tests proving each capability fails when the condition it checks is violated (site/src/test/browser/assertions.browser.test.ts), an axe-core scan across shelf, briefing, notice, playing, unavailable-choice, rejected, and ended (site/src/play/browser/accessibility.browser.test.tsx), and committed baseline snapshots for playing, unavailable-choice, persistence-warning, and ended at 320/390/768/1280 px (site/src/play/browser/visual-baseline.browser.test.tsx) — the pre-W66 baseline W66 must diff against. That is real infrastructure, but it does not fully cover what W63.7/W63.8 actually ask for: the harness's own hit-area/gap/font/line-height assertions (assertMinFontSize/assertMinLineHeight/assertMinHitArea/assertMinGap) are self-tested against synthetic markup only, never applied to a real renderedPlayAppcontrol; there is no ready-state visual snapshot alongside playing/unavailable-choice/persistence-warning/ended; and W63.8's keyboard-only, 200%-zoom, long-text, and missing-asset checks, plus forced-colours application behaviour (as opposed to thematchMediasignal alone), are not exercised at all. Revisit when a future slice drives those assertions and scenarios against the actual rendered UI; this entry stays open, narrowed to that remaining gap, rather than closed beside a harness that does not yet resolve it.
Add to this register whenever a decision is deferred or an assumption is made — rather than leaving it in a commit message or a chat, where the next person will not find it.