Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions design/90-decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -2229,3 +2229,16 @@ Context: The kit's `v2026.10.07` removed `/track`, `Test-DesignState.ps1` and `U
Chosen: Delete `design/state/` and `design/state-index.md`; remove the *Writing a design-state record* section from `AGENTS.md` and the orphaned-mirror sentence from `CLAUDE.md`. Earlier decision-log entries and `30-slices.md` text that name them are history and stay.
Rejected: Keep them frozen — a stale mirror of issue state that can only mislead.
Reversibility: cheap. Restoring is a checkout of the parent commit's `design/state*`.

### 2026-10-07 — Content crosses a campaign boundary by composition at load, never by traversal (#293, #292)
Context: #293 asked what a session becomes when a choice leads into another campaign's content. `GameState.campaignId` is singular (`src/engine/src/core/kernel/types.ts:44`), the save envelope rejects a `campaignId` mismatch as `invalid_state` (`src/engine/src/core/persistence/envelope.ts:165`), `StoryGraphKindState` is one flat record with no frames, and every `adventure-builder.ts` campaign declares the same `route`/`preparation`/`connections`/`pressure` variables, so carrying one campaign's state into another's nodes would reinterpret silently rather than fail. #292 asked for node-granular content deltas, which `resolvePacks` cannot express because the core cannot interpret `Campaign.content`.
Chosen: **Composition at registry build.** A host campaign *includes* another campaign as a module, pinned by `{id, version}`. A kind-owned, optional, pure `Kind` operation composes the included content into the host before the existing tiered validation runs, so at runtime there is one campaign and one session, and `GameState`, the save envelope and every host are unchanged. The boundary is specified:
- **Variables.** A module's variables, and its node ids, live under the include's alias prefix (`<alias>::<name>`). The prefix is what makes the shared-name collision impossible; existing campaigns are not renamed. A module declares its inputs and outputs, and the include binds them to host variables on the entry and exit edges as ordinary consequences.
- **`visitedCounts`.** Keyed by the prefixed node id, so a module's counts are its own by construction.
- **Endings.** A module's endings become exits back into the host; an include maps each exit to a host node, or to a host ending. A module never terminates the host run on its own.
- **Achievements.** A module's achievements are prefixed and credited to the host campaign, the campaign the session is in. They do not credit the module's standalone campaign.
- **Identity.** The include pins the module's `{id, version}` and composition refuses a mismatch, so the host's published version determines the composed content. Under content packs the resolution digest already covers it; `computeResolutionId` is unchanged.
- **Packs.** A pack can add an include to a campaign it does not own; that is the third-party side-quest case, and it is the structural delta #292 asked for. Text deltas stay the per-key string fold.
Contract work, in `design/` before any code: the `Kind` composition operation, the story-graph include and module declarations, and the reason codes composition raises.
Rejected: **Not supported** (author side quests as nodes inside one campaign) — overruled by the owner; it leaves #292's third-party case without an answer. **Jump** — leaves the first campaign's variables and `visitedCounts` in a session now claiming to be the second. **Call/return frames** — a `StoryGraphKindState` shape change and a save migration; kept as the extension if a target must ever be chosen at runtime, and a module's entry/exit/inputs/outputs are exactly the interface a frame would call. **Linked sessions** — every progress, standings and achievement surface would have to learn session parents. **Profile carry-over** — fits sequels, not a side quest inside a run; it needs no decision to use.
Reversibility: cheap now — no code or contract exists yet. Expensive once composed campaigns are published, because prefixed ids become published ids (C18).
Loading