Skip to main content

Implementation Plan

Document status: Design. The ordered plan; each stage names its own done-criteria.

Scope of this document

What to build, in what order, and how each stage is verified. It reconciles two schedules that were written independently: the ecosystem phase roadmap, and the Game Engine's own post-MVP programme.

"Phase N" always means the ecosystem roadmap's phase, and nothing else. This document does not maintain a second phase numbering — that rule exists because "Phase One" once meant three different things across the specification set.

D and G are stage labels, not phases. D0–D5 are this repository's design and build stages; G1–G4 are the Game Engine hosting stages. They are deliberately not numbered P0–P5, which is how they read at first and which caused a real misreading: D3 was taken for ecosystem Phase 3. It is not — D3 is ecosystem Phase 2. Each stage below names its phase explicitly where one applies, and where a stage has no ecosystem phase, it has none because Track B is this repository's own work.


1. Where Things Actually Stand​

Measured, not assumed.

State
Game EnginePast MVP, and now consumable. @the-running-dev/game-engine 0.4.0 is published to GitHub Packages with a 26-entry public surface. Two further kinds and four campaign arcs beyond the MVP
PlatformD3 shipped. Six packages — Abstractions, Core, Hosting, Observability, Persistence, Testing — build, pack and publish to a private GitHub Packages feed at an explicitly unstable 0.x, and the sample restores and runs on them (S1–S9, merged 2026-08-03 to 2026-08-07). The public identifiers are deliberately unspent. Built, but not yet proven by an outside product — see §4
AutomatorSpecified, unstarted
Plugin contractIts own repository, SubZeroDev.PluginContract, public, tagged v0.1.0. Schemas written and validated; not yet published at version-pathed URLs
Architecture specsVersion-controlled, SubZeroDev.Architecture, private
This repositoryIdentity settled (ADR-001); framework question settled (ADR-004)
D3 scope is settled; reuse remains mandatory

ADR-004 established that .NET already ships much of what the six packages describe, leaving narrow gaps — outbox, tenant filtering, module conventions — and requires those gaps to be checked against existing NuGet packages before anything is written. The D3 brief settles D3 as all six packages, which land together; the check against existing packages remains mandatory and is not a licence to hand-roll six.

The gate that used to block hosting work — "not before the engine MVP is done" — is satisfied. The engine's own order of operations puts the unified API and MCP surface as step 3 of 4, and that is the open step.


2. The Two Tracks​

Platform and Game Engine hosting are not one sequence, and treating them as one is the main scheduling error available here.

Track A — Platform D3 ✓ → D5 D4 is gated on evidence, not on its turn
Track B — GEaaS G1 ✓ → G2 ✓ → G3 → G4

Track A sits off the ecosystem critical path and can proceed in parallel with plugin and Automator work. Track B's old block on the engine's packaging is long gone — 0.4.0 is published, and G1 and G2 have both since shipped in full. What remains open on either track is D4, D5, G3 and G4.

D4 is not a position in a queue. Its own done-criterion is evidence — two named consumers, one of which actually runs on the package — so it fires when that evidence exists rather than when its turn comes. Writing it into the sequence between D3 and D5 implied a calendar gate it never had, and made any D5 work look like it was jumping the line. It is not.

That is deliberate, and it is the same reasoning the extraction guard applies: build the product first, extract when a second consumer proves the shape.


3. Completed by This Change​

D0 — Repository identity. Platform is the reusable framework and hosting layer; Game Engine as a Service is one hosted workload. The four ecosystem Platform specifications are brought in. The engine is renamed from "Narrative Engine" to "Game Engine" throughout. → Platform Identity

D1 — The engine hosting contract. Workload hosting rather than in-process ports; who owns what; two surfaces never merged; and the four questions a hosted deployment must answer that an in-process one never had to. → engine-hosting-contract.md

D2 — The technology decision, taken. .NET, with the boundary between Platform and a product it hosts stated as a process boundary rather than left as an accident. D1 was written to survive any answer and needed no revision when it landed. → ADR-002

Also settled: the repository-identity decision itself (ADR-001) and the package-scope conflict (ADR-003) — scopes are per-registry, and the engine's @the-running-dev coordinate is forced by GitHub Packages rather than drifted.


4. Track A — Platform​

D3 — The minimal package set (ecosystem Phase 2)​

Abstractions, Core, Hosting, Persistence, Observability, Testing. Boundaries and done-criteria are specified in minimal-platform-packages.md.

The count is settled. The D3 brief commits D3 to all six packages landing together. ADR-004 still requires the narrow gaps — outbox, tenant filtering, module conventions — to be evaluated against existing packages before anything is written; six packages is not a licence to hand-roll six.

Was blocked on the technology decision — the only stage that was, which is why it was taken early. ADR-002 settled it: .NET, so the package names above stand and the persistence baseline is EF Core.

D3 has shipped. All nine slices in d3/30-slices.md are marked shipped, merged between 2026-08-03 and 2026-08-07. The six packages build, pack and publish to a private GitHub Packages feed at an explicitly unstable 0.x, and the sample restores from that feed rather than from project references.

Done when every package's stated done-criteria are met, and — the one that matters most — a product runs on Platform with health, readiness, correlation ids, migrations and telemetry configured by nothing but the standard registration call. A framework whose first consumer needs bespoke wiring has not proven anything.

That last criterion is met by the sample, and only by the sample. S9.3 re-runs every S1–S8 assertion against the sample built on the restored packages, which proves pack, publish and authenticated restore. But the sample is Platform's own, and a framework proving itself against its own sample has not met the sentence above in the spirit it was written. The outside proof has two halves (#97, owner's ruling of 2026-10-17, superseding 2026-10-08's).

  • The package half. SubZeroDev.GameEngine's static host restores SubZeroDev.Platform.Hosting from the feed and calls AddPlatformWebHost() and nothing else. Its own tests assert the liveness and readiness probes. Correlation ids and telemetry come from the same call, but no test there asserts them. Migrations are not exercised, because the host has no persistence.
  • The hosting half. SubZeroDev.Adventures, a real product with real players, is served through Platform's edge as a workload. Its API is Node, so it consumes no package. It needs separate liveness and readiness, migrations on start under an advisory lock, and an honest answer from the edge when the backend is stopped (Adventures#9 stage 3). That work lives in the Adventures repository, and each gap it finds is filed here as D4 extraction evidence. Stage 3 shipped in Adventures#84.
    • Proven: its anonymous routes are served through the edge byte for byte, and the edge answers an honest 503 when the API or its database is down.
    • Waiting on G3: anything needing a signed-in player, because which of a caller's headers cross the edge is G3's question (#340).

Adventures' identity replacement stays evidence only (#100), and no Identity package is created from it. Until Adventures is served through the edge, and an outside consumer exercises migrations, D3 is built but not proven.

D4 — Extraction, on evidence (ecosystem Phase 5)​

Configuration, Events, Notifications, Storage, BackgroundJobs, Scheduling, Api — extracted from the Automator once a second consumer exists.

Done when each extracted package has two named consumers, not one and a plan, and at least one of them actually runs on the package before it reaches 1.0. The guard is only worth having if it is applied at the moment it is inconvenient.

This wording was amended, and the amendment is a loosening. It read "two named consumers in the repository", and ADR-007 removed in the repository so that a consumer living in its own repository counts — SkyNet HR, admitted as a second hosted workload and deliberately not brought in-tree. Those three words were doing real work: a consumer inside the tree is checkable by a build, and one outside it is asserted. The deploy condition above is the replacement, and it is weaker than what it replaces. ADR-007 states that cost rather than absorbing it.

D5 — Commercial (ecosystem Phase 8)​

Identity, Authorization, Organizations, Tenancy, Billing, Licensing, Audit, shared web UI and Platform.Mcp. The bounded scope and authoritative done-criteria are in the D5 brief.

Done when the assertion-driven sample proves all nine capabilities in both the operated and identity-free local compositions, including the brief's negative cases, database matrix, offline-local proof and dependency-direction gates; the public contracts are implemented in versioned 0.x CI package artifacts consumed by that sample; and consumer testing helpers and human-facing documentation cover the same boundaries. See the commercial guide for registration, composition, security defaults, failure semantics and the asserted scenarios.

The sample is the executable proof, not an external consumer. The retained consumer-evidence objection for Authorization, Licensing, Audit and shared web UI remains unresolved in the D5 decision log. Public registry publishing and onboarding any named consumer are separate work.

The tenant column does not wait for this stage. It ships with Persistence in D3.


5. Track B — Game Engine as a Service​

G1 — One session, over the wire​

A single service consuming the published engine package, composing the in-memory stores, exposing the tool surface over a real MCP transport. No database, no accounts, no billing, no tenancy.

Unblocked. This stage waited on the engine's consumer-boundary work; that is merged and @the-running-dev/game-engine 0.4.0 is published. Nothing else gates it.

One thing G1 had to decide that this stage did not anticipate. Platform is .NET (ADR-002) and the engine is Node, so a G1 written as a single Node service consumes the engine but not Platform. If G1 is also meant to be Platform's first consumer, it splits: a .NET edge on Platform in front of the Node engine workload. Decided — see §8.4: both, in sequence. The thin Node-only service first; the .NET edge in front of it as a fast follow, once the byte-identity proof exists.

Done when — and this is the criterion worth having, because the engine already knows how to write it:

The same arc, the same seed, the same counting IdSource and the same choices, played through the hosted transport, serialize byte-identically to the in-process run.

That is the engine's own client-contract invariant with a third client, and it proves the transport is a projection rather than a participant. Plus the API coverage checklist gains a third column — every store operation exercised through the hosted surface.

Why this is first. It is the cheapest thing that can fail informatively. If a hosted transport cannot reproduce a game byte-for-byte, everything above it is built on sand, and that is knowable in the smallest possible increment.

G2 — Durable sessions​

Real SessionStore and ProfileStore implementations against provisioned persistence, with compare-and-swap on the sequence number (engine-hosting-contract.md §6.1).

Done when a committed replay fixture round-trips through the durable store byte-identically to the in-memory one, and two concurrent actions against one session produce one success and one explicit rejection — never a silent overwrite.

This stage resolves an open engine question. The engine records that its session-layer composition root has two real call sites and zero real implementations of the abstraction it specifies, to be revisited "when a second SessionStore implementation is actually needed." This is that implementation.

Reading the Adventures POC against G2 (retrospective, #86)​

SubZeroDev.Adventures was a reference for G2, not a source (G1 decision, 2026-08-09). This is what G2 did with each idea. Reasons live in the G2 design; this table only points. "Deferred" means G2's binding non-goals sent it to G3 or G4, not that it was judged.

POC ideaVerdictWhere to see it
Postgres ports over plain pg and node-pg-migrate, inside the Node workloadAdopteddesign, "The store"; workloads/game-service/src/store.ts
Schema mapped 1:1 to the engine's recordsAdapted: tenant in every key, host-only columns, text idsdesign, "engine-owned values, host-owned columns"
Engine timestamps and blob as textAdopted, made a ruledesign, "Four things about this table are decisions"
Compare-and-swap on attempt_counterAdapted: same statement shape, store-owned versiondesign, opening adjudication; hosting contract §6.1
Lost race surfaces as 503Rejected: concurrent_modification at 409brief Scope 3; slices S1, S2
Long-lived session layer with a cacheRejected: composed per requestdesign, "Module boundaries"
Second store over the same DB as the durability testAdapted: three proofs, two real processesdesign, "Control flow 3"; tests/
One row per achievement, set unionAdopted, plus a header row and terminalsdesign, "profile and profile_achievement"
Unlocked saves, conditional delete, listByProfileAdopteddesign, "save"
Progress columns derived from the blobRejected (implicit): the store stays opaquedesign, "blob is text"
Once-per-deploy sweepAdapted: startup migration plus a timer sweep with a retention tombstonedesign, "Control flow 1", "Expiry"
Hand-written REST routes; replay endpointsRejectedG1 decisions, 2026-08-09; brief non-goals
Cookie identity, ownership decorator, per-player save listDeferred to G3; the storage seam is declaredcompose.ts StorageSeam; G2 decisions, 2026-08-20
Disk catalogue, admin publishingDeferred to G4—

For G3. Read principal.ts, store/ownedStore.ts and persistence.ts sessionOwner in the POC. Decide on purpose whether an unknown id and someone else's id look the same: the POC's decorator lets unknown ids through to a 404 and refuses owned ones with a 403. Adventures is a workload behind Platform's edge, not a package consumer (#97); its identity is evidence only (#100). Decide who owns a caller's credential at the edge, passed through to the workload or terminated there. That ruling sets which headers the edge forwards, and #340 lists the ones Adventures needs and the security notes either answer must keep.

For G4. Read the POC's campaigns/ and content-cell.ts, and its issues #27-#31: publishing without a rebuild, and content that must not be able to brick the server. This plan does not yet assume either.

G3 — Principals​

Authentication, ownership, and the authorization decorator (engine-hosting-contract.md §6.2, §6.3). The account surface appears here, separate from the game surface (§5).

Done when the decorated store produces byte-identical serialize() output to the undecorated one, a session id belonging to another principal returns a refusal rather than data, and no hosted endpoint returns engine state.

Adopts Platform's Identity if D5 has landed; otherwise implements it locally and becomes the second-consumer evidence that promotes it. Either order is fine — that is the guard working.

G4 — Catalogue, publishing, metering​

Content packs as the hosted artifact, resolution identity stamped into campaign versions, and an event-derived metering feed that billing consumes.

Blocked on: content-pack resolution existing in engine code. Specified, unbuilt.

Still deferred​

Multiplayer, white-label, AI-assisted authoring, and community modding — the last of which is a trust question this product owns, not a merge-semantics question the engine has since answered.


6. Ordering Constraints​

The things that genuinely cannot be reordered:

ConstraintWhyState
Engine packaging → G1Nothing can consume the engine until it packs and installsSatisfied — 0.4.0 published
Technology decision → D3Package identifiers and the module contract depend on itSatisfied — ADR-002, ADR-004
D3 → G1 S7The edge's entry point is composed by AddPlatformWebHost(), which lives in Platform.HostingSatisfied — Hosting shipped in D3 S1; G1 S7 shipped after it. Recorded late, per #96
G1 → G2The byte-identity proof must exist before persistence can be checked against itSatisfied — both shipped
Persistence → everything with a tenantThe column ships in the first schema or it is a migration on every tableSatisfied — the tenant column shipped in D3 S2
G2 → engine's composition-root questionThe second implementation is what makes the question answerableSatisfied — G2 shipped. The question is now answerable, and not yet answered

Everything else is genuinely parallel, including the whole of Track A against Track B.

Every constraint above is now satisfied, and neither track is externally gated. What gates them is §8 — and, for D3's proving criterion, an outside product rather than the sample.


7. What Would Make This Plan Wrong​

Recorded so the assumptions are visible rather than implied.

  • If the technology decision resolves to TypeScript, Track A's package names and the module contract change, and the transferred ecosystem specifications stop being verbatim. The boundaries in minimal-platform-packages.md survive; the layout does not.
  • If GEaaS is deprioritized indefinitely, the guard no longer loses everything with it — this risk was written when GEaaS was the only second consumer, and BarStrad is now a third. Identity, Tenancy and Mcp keep a second consumer and stay justified; Billing goes back to being speculative, since BarStrad's commercial model is unsettled and an unsettled consumer is not a consumer. Notifications and localized content are unaffected — BarStrad wants both independently of the engine. Read the row-by-row count in Platform Identity §4 before mothballing anything; the earlier wording would have retired four packages at once, and it is the kind of statement someone acts on rather than weighs.
  • If the Automator ships first and grows its own identity and tenancy, that is the guard working as designed, not a failure — extraction follows the second consumer, and D4 is where it lands.

8. Decisions and Remaining Questions​

Nothing external gates either track now. The first two decisions below are settled by the brief; the contract decision is settled by ADR-005. They remain here to make the planning consequences visible rather than quietly assumed.

8.1 How much of D3 is built — decided: six packages​

ADR-004 narrowed this twice: .NET already ships most of what the six packages describe, and the narrow gaps that remain — transactional outbox, tenant column and query filtering, module registration conventions — must each be checked against existing NuGet packages before anything is written.

The D3 brief settles D3 at six packages — Abstractions, Core, Hosting, Persistence, Observability and Testing — and requires them to land together. The evaluation against existing packages remains a prerequisite for each narrow gap; it informs whether the capability is taken or built, not whether D3 includes the package.

8.2 What proves Platform works — decided: a sample and the G1 edge​

A sample in samples/, run in CI, is the definition of done for D3; the G1 edge is its first genuine external validation. The brief requires both instruments: the sample now, with its requirements derived from the edge's needs, and the edge when it exists. The sample keeps D3 independent of Track B; the edge challenges the framework-authored proof without delaying D3.

It also keeps Track A genuinely parallel: no cross-repository coupling, and D3 does not wait on G1.

The known weakness, stated rather than hidden: a sample is written by the framework's authors, so it confirms rather than challenges. Deriving its requirements from the G1 edge and treating that edge as the first genuine validation mitigates rather than removes the weakness. Any API change the edge forces is expected and cheap while Platform is still 0.x.

The options as they were weighed, kept for the reasoning
InstrumentProves wellProves badly
A sample in samples/Persistence and Testing — a kill test mid-outbox is controllableWritten by the framework's authors, so it confirms rather than challenges
The G1 .NET edgeHosting, and a real distributed trace to a Node workloadPersistence and Testing — a thin proxy has little durable state
A widened edgeEverythingIt is G3 pulled into G1, losing G1's virtue as the cheapest informative failure

The rule the choice has to satisfy, whichever instrument is used: a package whose only proof is its own unit tests reaches a committed public API with no consumer, which is exactly what the extraction guard exists to prevent. The sample is the consumer until the edge is.

8.3 The contract between hosted things — decided: ADR-005​

Settled by ADR-005: boundary contracts are projected from their source of truth, never authored alongside it, and they live in SubZeroDev.ServiceContract — its own public repository, depended on by products, depending on nothing.

protobuf was evaluated and declined for now. It would create a second, hand-maintained definition of types the engine already owns, in a codebase whose recurring defect is two copies disagreeing. Revisited when a boundary needs streaming or payloads where JSON's size is measurable — and generated rather than authored when it is. JSON over HTTP with version-pathed schema URLs is the first wire. MCP stays a projection, per the ecosystem's own MCP decision.

Outstanding from that decision, each with a named cost rather than left implicit:

  • The repository does not exist yet. Creating it is the next concrete step.
  • mcp-tool-contract.md is its natural first content, and moving it means updating the engine's 09-clients.md, which links to it by URL. ADR-005 deliberately does not move it.
  • A generated schema needs a generator. Rule 2 is cheap to state and not cheap to honour; a hand-written schema "just for now" is how it gets lost.

8.2 and 8.3 no longer interact. Choosing the sample for 8.2 means the contract is not needed to prove Platform — so the repository can be created and populated on its own schedule, driven by the first real boundary rather than by D3's definition of done.

8.4 The shape of G1 — decided: Node-only first, the .NET edge as a fast follow​

The split named in §5's G1 note is resolved as a sequence rather than a choice. First, a thin Node-only service consuming the published engine package — in-memory stores, the ten-operation game surface as a JSON-over-HTTP wire with the MCP projection beside it, and G1's byte-identity done-criterion. It consumes the engine and not Platform, which keeps it the cheapest informative failure. Then, as a fast follow, the .NET edge in front of that workload: Platform's packages terminating transport, routing to the Node service, and carrying a distributed trace across the language boundary — the validation §8.2 assigns to the edge. The edge does not gate G2; the byte-identity proof exists after the first stage.

Where it runs, and what that costs. G1 is built in this repository, under workloads/game-service/, rather than in a repository of its own — decided 2026-08-08, with the reasoning and the rejected alternatives in design/g1/90-decisions.md. The named cost: §8.2 valued the edge as genuine external validation, and an edge living in Platform's own repository is nearer to framework-authored proof. The byte-identity criterion and the real distributed trace keep their value; the independence claim weakens, and this section says so rather than quietly dropping the word "external".