diff --git a/.planning/season-7-end-state.md b/.planning/season-7-end-state.md new file mode 100644 index 0000000..9a6fed7 --- /dev/null +++ b/.planning/season-7-end-state.md @@ -0,0 +1,112 @@ +# Season 7 End-State Design (Principle-Level) + +*Phase 1 deliverable per PIPE-05 + CONTEXT D-08. Principle-level only — treatment text is authored in Phase 7.* + +This document answers the question that ends ROADMAP.md Phase 7's success criterion #4: + +> *"the finite Roothold ceiling from Phase 4 has held the line, and the game has ended +> the way A Dark Room and Universal Paperclips ended."* + +Per .planning/research/PITFALLS.md #1, "the story ends but the idle loop doesn't" +is the single most dangerous structural pitfall for this project. This document +is the canonical answer the project has *before* any economy code lands in Phase 2. + +Per CONTEXT D-08: this is **principle-level**, not treatment-level. It defines the +contract Phase 7's authoring obeys, not the text of any final scene. + +## What does *rest state* mean? + +The rest state is the post-credits configuration the player can return to indefinitely +without grinding. Concretely: + +- **No new fragments are added to the pool.** All authored content has been delivered. + Harvests after the final binary choice yield re-readable previously-collected + fragments — nothing new. +- **No new currency tiers unlock.** Roothold has reached its finite ceiling (see below) + and stays there. There is no "Season 8" hidden behind a number. +- **The garden continues to render and respond to clicks.** Plants can still be + planted. Seasons (now in Return register) continue to crossfade. The world is + not frozen — it is *finished*. +- **The Pale has receded.** The Heartsoil expands beyond the garden walls. Lura's + arc has resolved. The Archivist's question has been answered (in the player's + Season 7 binary choice — STRY-08). +- **The cello and ambient layers continue.** The audio is *quiet*, *finite*, + *understood* — never crescendos again, never hard-cuts. + +This is not "endgame content." It is **rest**. Lineage: *A Dark Room* fades to its +ending screen and the player returns to it for the same reason they return to a +finished album — not because there is more, but because there was *enough*. + +## What is the finite Roothold ceiling tied to? + +Roothold's ceiling is anchored in the **count of authored fragments and the count +of Seasons** — not in an arbitrary number, not in a designer's intuition. + +The principle: + +> *One cannot accumulate more Roothold than the player has actually understood, +> and what the player can understand is bounded by what the writer has actually written.* + +Concrete tie: + +- Roothold gain per Season is gated to a hard cap proportional to the fragment + count of that Season + a small contribution from Roothold-relevant story beats + (Lura conversations, the Nameless Man's arc, the Archivist's question, etc.). +- Total Roothold ceiling = Σ(per-Season caps). +- **Phase 4 enforces this cap** when it implements `migrate_v1_to_v2` and the + prestige state machine (SEAS-04). Phase 7 verifies the ceiling holds through + full play. +- When Roothold reaches the ceiling, the UI displays "Roothold (full)" — never + a hidden multiplier or "go again to overflow." + +Implication for designers: when adding fragments in Phase 5+, the Roothold ceiling +*moves* — adding 5 new Season-3 fragments adds proportional headroom. This is +intentional. Roothold is bounded by content; content is bounded by the writer. + +## What tonal register does the coda live in? + +- **Warm**, not pyrrhic. The garden persists *because* you tended it; this is + earned redemption, not survival. Lineage: the closing minutes of *Spiritfarer*, + not the closing minutes of *A Dark Room* (which earned its bitterness; we earn + our warmth). +- **Quiet**, not climactic. The cello does not crescendo at the binary choice. + It rests. The chosen ending paragraph displays softly; "The garden persists." + lands without underscore. +- **Specific**, not abstract. The final visible state is a *real* garden — the + one this player built, with their actual planted ecosystems, their actual + Roothold value, their actual collected fragments — viewed in soft dawn-silver + light per AEST-06's Season-7 palette anchor. +- **Final**, not infinite. There is no Season 8. There is no New Game+. The Pale + receded **here**, in **this** garden. Future patches may add cosmetic items or + additional fragments per CONT-01 (post-launch additive content), but they slot + *between* authored beats; they never extend the arc. + +## What this document is NOT + +This document defines principles. It does **not** define: + +- The text of the Season 7 binary-choice scene — *authored Phase 7*. +- The text of either ending paragraph (`"They help us remember"` / `"They help us grow"`) — *authored Phase 7*. +- The exact line "The garden persists." appears in both endings, but its surrounding + paragraph and Lura's final line are *authored Phase 7*, not Phase 1. +- The credits / coda screen visual treatment — *designed Phase 7*. +- The exact tonal register or shape of individual final-Season fragments — *authored Phase 7*. +- The numeric value of the Roothold ceiling — *computed Phase 4* from the + content count at that point + ROADMAP-locked principle. + +This document is **the principle the economy obeys, the writer obeys, and the +Phase 7 designer obeys** — not the implementation of any of those. + +## Source Documents + +This doctrine consolidates constraints already locked in: + +- **PROJECT.md** § "Core Value" — "every idle mechanic must function as a metaphor"; "what survives is what you understood" +- **REQUIREMENTS.md** SEAS-04 (finite Roothold ceiling), SEAS-09 (Season 7 late-game shape), SEAS-10 (rest state, not infinite prestige tiers), STRY-08 (binary choice + "The garden persists.") +- **ROADMAP.md** § "Phase 7: Season 7 (Return) & Final Choice" — the 4 success criteria +- **.planning/research/PITFALLS.md** § "Pitfall 1: The Story Ends but the Idle Loop Doesn't" — the rationale this document directly addresses + +--- + +*Authored: Phase 1 deliverable. Phase 4 enforces the Roothold ceiling. Phase 7 authors +the treatment-level final scenes against the principles above.* diff --git a/scripts/doctrine.test.ts b/scripts/doctrine.test.ts new file mode 100644 index 0000000..2569fbe --- /dev/null +++ b/scripts/doctrine.test.ts @@ -0,0 +1,79 @@ +import { describe, it, expect } from 'vitest'; +import { readFileSync, existsSync } from 'node:fs'; + +// PIPE-05 doctrine doc-lint test. +// +// Per RESEARCH § "Validation Architecture" PIPE-05 row, this is the only +// automated enforcement of the Phase-1 doctrine documents. CONTEXT D-07 +// explicitly forbids a lint rule on UX strings, so this structural test +// asserts (a) both docs exist on disk, (b) each contains its required H2 +// sections, (c) each cites its required source documents. +// +// If a future plan moves either doc, update PATH constants below. + +describe('PIPE-05: doctrine documents exist with required H2 sections', () => { + describe('.planning/anti-fomo-doctrine.md', () => { + const PATH = '.planning/anti-fomo-doctrine.md'; + + it('exists', () => { + expect(existsSync(PATH)).toBe(true); + }); + + it('contains all 4 required H2 sections', () => { + const md = readFileSync(PATH, 'utf8'); + expect(md).toMatch(/^## Banned Mechanics$/m); + expect(md).toMatch(/^## Allowed Engagement$/m); + expect(md).toMatch(/^## Review Checklist$/m); + expect(md).toMatch(/^## Source Documents$/m); + }); + + it('cites all 4 source documents (PROJECT, REQUIREMENTS, CLAUDE, PITFALLS)', () => { + const md = readFileSync(PATH, 'utf8'); + expect(md).toMatch(/PROJECT\.md/); + expect(md).toMatch(/REQUIREMENTS\.md/); + expect(md).toMatch(/CLAUDE\.md/); + expect(md).toMatch(/PITFALLS\.md/); + }); + + it('does NOT propose a lint rule on UX strings (CONTEXT D-07 explicit rejection)', () => { + const md = readFileSync(PATH, 'utf8'); + // The doc may *mention* that lint rules were rejected, but it must not + // propose adding one. Allow "no lint rule" but reject "add a lint rule". + expect(md).not.toMatch(/\b(add|implement|propose).{0,40}lint rule/i); + }); + }); + + describe('.planning/season-7-end-state.md', () => { + const PATH = '.planning/season-7-end-state.md'; + + it('exists', () => { + expect(existsSync(PATH)).toBe(true); + }); + + it('contains all 5 required H2 sections (CONTEXT D-08)', () => { + const md = readFileSync(PATH, 'utf8'); + expect(md).toMatch(/^## What does \*rest state\* mean\?$/m); + expect(md).toMatch(/^## What is the finite Roothold ceiling tied to\?$/m); + expect(md).toMatch(/^## What tonal register does the coda live in\?$/m); + expect(md).toMatch(/^## What this document is NOT$/m); + expect(md).toMatch(/^## Source Documents$/m); + }); + + it('cites SEAS-04, SEAS-09, SEAS-10, STRY-08', () => { + const md = readFileSync(PATH, 'utf8'); + expect(md).toMatch(/SEAS-04/); + expect(md).toMatch(/SEAS-09/); + expect(md).toMatch(/SEAS-10/); + expect(md).toMatch(/STRY-08/); + }); + + it('does NOT include treatment-level details forbidden by CONTEXT D-08', () => { + const md = readFileSync(PATH, 'utf8'); + // Check the "What this document is NOT" section is present — this is the + // structural guarantee against treatment-level scope creep. + expect(md).toMatch(/## What this document is NOT/); + // The doc must explicitly disclaim authoring the ending paragraphs. + expect(md).toMatch(/authored Phase 7/); + }); + }); +}); diff --git a/tsconfig.node.json b/tsconfig.node.json index 001be2e..48b1567 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -17,5 +17,5 @@ "noFallthroughCasesInSwitch": true, "noUncheckedSideEffectImports": true }, - "include": ["vite.config.ts", "vitest.config.ts", "playwright.config.ts", "scripts/**/*.mjs"] + "include": ["vite.config.ts", "vitest.config.ts", "playwright.config.ts", "scripts/**/*.mjs", "scripts/**/*.ts"] } diff --git a/vitest.config.ts b/vitest.config.ts index d0bff50..22b35af 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -11,7 +11,7 @@ import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { environment: 'happy-dom', - include: ['src/**/*.test.ts', 'src/**/*.test.tsx', 'scripts/**/*.test.mjs'], + include: ['src/**/*.test.ts', 'src/**/*.test.tsx', 'scripts/**/*.test.mjs', 'scripts/**/*.test.ts'], passWithNoTests: false, globals: false, },