diff --git a/.changeset/wise-rams-swim.md b/.changeset/wise-rams-swim.md new file mode 100644 index 000000000..fc7828787 --- /dev/null +++ b/.changeset/wise-rams-swim.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 3014 +--- +**`/gsd-spike` no longer blends unrelated ideas' requirements together** — `.planning/spikes/MANIFEST.md` now scopes each idea's paragraph and Requirements under its own idea key, and `/gsd-spike --wrap-up` only emits a feature area's owning idea's requirements instead of the whole file. (#1700) diff --git a/gsd-core/references/artifact-types.md b/gsd-core/references/artifact-types.md index bc4045e8d..b2cbc0183 100644 --- a/gsd-core/references/artifact-types.md +++ b/gsd-core/references/artifact-types.md @@ -72,11 +72,11 @@ reads is inert — the consumption mechanism is what gives an artifact meaning. - **Location**: `.planning/spikes/SPIKE-NNN/` - **Consumed by**: Planner when spike is referenced; `pause-work` for spike context handoff -### Spike README.md / MANIFEST.md (per-spike, via /gsd:spike) -- **Shape**: YAML frontmatter (spike, name, validates, verdict, related, tags) + run instructions + results +### Spike README.md (per-spike) / MANIFEST.md (per-project index, via /gsd:spike) +- **Shape**: README — YAML frontmatter (spike, idea, name, validates, verdict, related, tags) + run instructions + results. MANIFEST.md — one `### {idea-key}` section per idea under `## Ideas` (idea paragraph + its own scoped Requirements), plus one durable `## Spikes` table (with an Idea column) indexing every spike across every idea. - **Lifecycle**: Created by `/gsd:spike` → Verified → Wrapped up by `/gsd-spike-wrap-up` - **Location**: `.planning/spikes/NNN-name/README.md`, `.planning/spikes/MANIFEST.md` -- **Consumed by**: `/gsd-spike-wrap-up` for curation; `pause-work` for spike context handoff +- **Consumed by**: `/gsd-spike-wrap-up` for curation (Requirements pulled only from the idea key(s) it wraps); `pause-work` for spike context handoff ### Sketch README.md / MANIFEST.md / index.html (per-sketch) - **Shape**: YAML frontmatter (sketch, name, question, winner, tags) + variants as tabbed HTML diff --git a/gsd-core/workflows/sketch.md b/gsd-core/workflows/sketch.md index 6c8cd937b..82623aab7 100644 --- a/gsd-core/workflows/sketch.md +++ b/gsd-core/workflows/sketch.md @@ -134,7 +134,7 @@ If spikes exist for this project, read them to ground the sketches in reality. M **a.** Glob for `./.claude/skills/spike-findings-*/SKILL.md` and read any that exist, plus their `references/*.md`. These contain validated patterns and requirements. -**b.** Read `.planning/spikes/MANIFEST.md` if it exists — check the Requirements section for non-negotiable design constraints (e.g., "must support streaming", "must render markdown"). These requirements should be visible in the mockup even though the mockup doesn't implement them for real. +**b.** Read `.planning/spikes/MANIFEST.md` if it exists — it may hold separate `### {idea-key}` sections for several unrelated ideas. Check the Requirements list of the idea key relevant to this sketch's design direction (or all of them if none clearly matches) for non-negotiable design constraints (e.g., "must support streaming", "must render markdown"). These requirements should be visible in the mockup even though the mockup doesn't implement them for real. **c.** Read `.planning/spikes/CONVENTIONS.md` if it exists — the established stack informs what's buildable and what interaction patterns are idiomatic. diff --git a/gsd-core/workflows/spike-wrap-up.md b/gsd-core/workflows/spike-wrap-up.md index 8cfef2fb6..bdccecaf7 100644 --- a/gsd-core/workflows/spike-wrap-up.md +++ b/gsd-core/workflows/spike-wrap-up.md @@ -22,8 +22,12 @@ Read all files referenced by the invoking prompt's execution_context before star ## Gather Spike Inventory -1. Read `.planning/spikes/MANIFEST.md` for the overall idea context and requirements -2. Glob `.planning/spikes/*/README.md` and parse YAML frontmatter from each +1. Read `.planning/spikes/MANIFEST.md` for the `## Ideas` sections (each idea's paragraph and + its own scoped Requirements list) and the `## Spikes` table (its Idea column tells you which + idea key each spike row belongs to). +2. Glob `.planning/spikes/*/README.md` and parse YAML frontmatter from each — each spike's + `idea:` field is the idea key that owns it. If a README predates #1700 and has no `idea:` + field, resolve its idea key from the matching `## Spikes` table row's Idea column instead. 3. Check if `./.claude/skills/spike-findings-*/SKILL.md` exists for this project - If yes: read its `processed_spikes` list from the metadata section and filter those out - If no: all spikes are candidates @@ -101,7 +105,12 @@ For each feature-area group, write a reference file at `references/[feature-area ## Requirements -[Non-negotiable design decisions from MANIFEST.md Requirements section that apply to this feature area. These MUST be honored in the real build. E.g., "Must use streaming JSON output", "Must support reconnection".] +[Non-negotiable design decisions pulled ONLY from the Requirements list of the idea key(s) that +own the spikes in this feature-area group — match each spike's `idea:` frontmatter (or Idea +column) to its `### {idea-key}` Requirements list in MANIFEST.md. These MUST be honored in the +real build. E.g., "Must use streaming JSON output", "Must support reconnection". + +Never include a requirement from an idea key that has no spike in this group.] ## How to Build It @@ -136,7 +145,8 @@ description: Implementation blueprint from spike experiments. Requirements, prov ## Project: [project-dir-name] -[One paragraph from MANIFEST.md describing the overall idea] +[One paragraph per idea key represented among the wrapped spikes, taken from that idea's +`### {idea-key}` section in MANIFEST.md — not the whole MANIFEST.md if it holds unrelated ideas.] Spike sessions wrapped: [date(s)] @@ -144,7 +154,11 @@ Spike sessions wrapped: [date(s)] ## Requirements -[Copied directly from MANIFEST.md Requirements section. These are non-negotiable design decisions that emerged from the user's choices during spiking. Every feature area reference must honor these.] +[Union of the Requirements lists for every idea key represented among the spikes being wrapped +in this session — never the whole MANIFEST.md. These are non-negotiable design decisions that +emerged from the user's choices while spiking those specific idea(s). Every feature area +reference must honor these. If this wrap-up spans more than one idea key, group the list by +idea key so a future reader can tell which requirement belongs to which idea.] - [requirement 1] - [requirement 2] @@ -299,6 +313,7 @@ After the summary, present next-step options: - [ ] Spikes grouped by feature area - [ ] Spike-findings skill exists at `./.claude/skills/` with SKILL.md (including requirements), references/, sources/ - [ ] Reference files are implementation blueprints with Requirements, How to Build It, What to Avoid, Constraints +- [ ] Requirements in each reference file and in SKILL.md are scoped to the idea key(s) actually represented among the wrapped spikes — never blended with an unrelated idea's requirements - [ ] `.planning/spikes/CONVENTIONS.md` created or updated with recurring stack/structure/pattern choices - [ ] `.planning/spikes/WRAP-UP-SUMMARY.md` written for project history - [ ] Project CLAUDE.md has auto-load routing line diff --git a/gsd-core/workflows/spike.md b/gsd-core/workflows/spike.md index 2a6e578d6..db1def70a 100644 --- a/gsd-core/workflows/spike.md +++ b/gsd-core/workflows/spike.md @@ -53,7 +53,8 @@ If no `.planning/spikes/` directory exists, tell the user there's nothing to ana Otherwise, load in this order: -**a. MANIFEST.md** — the overall idea, requirements, and spike table with verdicts. +**a. MANIFEST.md** — every idea section under `## Ideas` (each idea's paragraph and its own +scoped Requirements) and the `## Spikes` table with verdicts (each row tagged by idea). **b. Findings skills** — glob `./.claude/skills/spike-findings-*/SKILL.md` and read any that exist, plus their `references/*.md`. These contain curated knowledge from prior wrap-ups. @@ -74,7 +75,8 @@ If integration risks exist, present them as concrete proposed spikes with names ### Analyze for Frontier Spikes -Think laterally about the overall idea from MANIFEST.md and what's been proven so far. Consider: +Think laterally about every idea section from MANIFEST.md and what's been proven so far for +each. Consider: - **Gaps in the vision:** Capabilities assumed but unproven. - **Discovered dependencies:** Findings that reveal new questions. @@ -86,7 +88,7 @@ Present frontier spikes as concrete proposals numbered from the highest existing ### Get Alignment and Execute -Present all integration and frontier candidates, then ask which to run. When the user picks spikes, write definitions into `.planning/spikes/MANIFEST.md` (appending to existing table) and proceed directly to building them starting at `research`. +Present all integration and frontier candidates, then ask which to run. When the user picks spikes, write definitions into `.planning/spikes/MANIFEST.md` (appending to the existing table, with each row's Idea column set to the idea key(s) it extends or validates) and proceed directly to building them starting at `research`. @@ -212,33 +214,63 @@ If 2+ credible approaches exist, plan to build quick variants within the spike a -Create or update `.planning/spikes/MANIFEST.md`: +Create or update `.planning/spikes/MANIFEST.md`. + +**Assign an idea key.** Derive a short, stable, kebab-case slug (2-4 words) summarizing the +idea being spiked right now, e.g. `realtime-llm-streaming`. Reuse the exact same idea key for +every spike in this session and any later session that continues the same idea. Only mint a +new idea key when the current idea is not a continuation of one already indexed in +MANIFEST.md — never reuse an existing idea key for an unrelated idea, and never merge two +different ideas under one key. + +If `.planning/spikes/MANIFEST.md` doesn't exist, create it: ```markdown # Spike Manifest -## Idea -[One paragraph describing the overall idea being explored] +## Ideas -## Requirements -[Design decisions that emerged from the user's choices during spiking. Non-negotiable for the real build. Updated as spikes progress.] +### {idea-key} +[One paragraph describing this idea] + +**Requirements:** +[Design decisions that emerged from the user's choices while spiking THIS idea. Non-negotiable +for the real build of this idea. Updated as spikes progress. Never copy or merge requirements +from a different idea key into this list.] - [e.g., "Must use streaming JSON output, not single-response"] - [e.g., "Must support reconnection on network failure"] ## Spikes -| # | Name | Type | Validates | Verdict | Tags | -|---|------|------|-----------|---------|------| +| # | Idea | Name | Type | Validates | Verdict | Tags | +|---|------|------|------|-----------|---------|------| ``` -**Track requirements as they emerge.** When the user expresses a preference during spiking, add it to the Requirements section immediately. +If `.planning/spikes/MANIFEST.md` already exists: + +- **Same idea key already has a `### {idea-key}` section under `## Ideas`:** append to that + section's Requirements list as new requirements emerge. Never overwrite or rewrite its + `## Idea` paragraph. +- **New idea key, not yet present:** append a new `### {idea-key}` subsection under `## Ideas`, + after any existing idea sections. Never touch, merge into, or delete another idea's section. +- **A pre-#1700 MANIFEST.md with the old flat shape** (a single top-level `## Idea` / `## + Requirements` pair, no `## Ideas` heading): treat its existing content as one implicit idea. + Derive an idea key from its `## Idea` paragraph, migrate it in place to `## Ideas` > + `### {idea-key}` — preserving the paragraph and every existing Requirements bullet and + Spikes row verbatim — then continue as above. Do this migration once; do not repeat it once + `## Ideas` exists. + +Every row appended to `## Spikes` carries an **Idea** column set to the idea key it belongs to. + +**Track requirements as they emerge.** When the user expresses a preference during spiking, add +it to the current idea's Requirements list immediately — never to a different idea's list. ## Re-Ground Before Each Spike -Before starting each spike (not just the first), re-read `.planning/spikes/MANIFEST.md` and `.planning/spikes/CONVENTIONS.md` to prevent drift within long sessions. Check the Requirements section — make sure the spike doesn't contradict any established requirements. +Before starting each spike (not just the first), re-read `.planning/spikes/MANIFEST.md` and `.planning/spikes/CONVENTIONS.md` to prevent drift within long sessions. Check the current idea's `### {idea-key}` Requirements list — make sure the spike doesn't contradict any established requirement for this idea. Do not apply another idea's requirements. @@ -287,6 +319,7 @@ Multiple files per spike are expected for complex questions (e.g., `test-basic.j ```markdown --- spike: NNN +idea: {idea-key} name: descriptive-name type: standard validates: "Given [precondition], when [action], then [expected outcome]" @@ -337,7 +370,8 @@ tags: [tag1, tag2] → Does this match what you expected? Describe what you see. ────────────────────────────────────────────────────────────── -**h.** Update `.planning/spikes/MANIFEST.md` with the spike's row. +**h.** Update `.planning/spikes/MANIFEST.md` with the spike's row, setting the Idea column to +this spike's idea key. **i.** Commit (if `COMMIT_DOCS` is true): ```bash @@ -450,10 +484,10 @@ gsd_run query commit "docs(spikes): update conventions" --files .planning/spikes - [ ] Depth over speed — edge cases tested, surprising findings followed, investigation trail documented - [ ] Comparison spikes built back-to-back with head-to-head verdict - [ ] Spikes needing human interaction have forensic log layer -- [ ] Requirements tracked in MANIFEST.md as they emerge from user choices +- [ ] Requirements tracked in MANIFEST.md, scoped to the idea key that produced them, as they emerge from user choices - [ ] CONVENTIONS.md created or updated with patterns that emerged -- [ ] Each spike README has complete frontmatter, Investigation Trail, and Results -- [ ] MANIFEST.md is current (with Type column and Requirements section) +- [ ] Each spike README has complete frontmatter (including its idea key), Investigation Trail, and Results +- [ ] MANIFEST.md is current (with Idea and Type columns, and each idea's own scoped Requirements section) - [ ] Commits use `docs(spike-NNN): [VERDICT]` format - [ ] Consolidated report presented with next-step routing diff --git a/tests/emitted-drift-acks/1700-spike-manifest-idea-scoping.json b/tests/emitted-drift-acks/1700-spike-manifest-idea-scoping.json new file mode 100644 index 000000000..3f6481ffc --- /dev/null +++ b/tests/emitted-drift-acks/1700-spike-manifest-idea-scoping.json @@ -0,0 +1,8 @@ +{ + "version": 1, + "paths": { + "spike.md": "#1700: create_manifest step now derives an explicit idea key and restructures the .planning/spikes/MANIFEST.md template from one flat top-level `## Idea` / `## Requirements` pair (single-idea only) to `## Ideas` > `### {idea-key}` subsections, each carrying its own scoped Requirements list — the fix for the leak where one idea's requirements bled into another idea's build via spike-wrap-up.md. Growth is: the idea-key derivation instructions and the expanded MANIFEST.md template (new/existing-idea/pre-#1700-flat-shape-migration branches) in create_manifest; an Idea column added to the `## Spikes` table header everywhere it's referenced; an `idea:` field added to the build_spikes README frontmatter template; the reground step scoping its established-requirements check to the current idea only; the frontier_mode load step and 'Get Alignment and Execute' step describing per-idea sections instead of one flat idea; and two success_criteria bullets updated to call out idea-key scoping. This is the write side of the fix, not incidental prose.", + "spike-wrap-up.md": "#1700: gather/synthesize/write_skill steps now resolve each spike's idea key (from its README `idea:` frontmatter, falling back to the `## Spikes` table's Idea column for pre-#1700 spikes) and scope which Requirements they read and re-emit, so a feature-area reference or the generated spike-findings SKILL.md never pulls in an unrelated idea's requirements — the read/re-emit side of the #1700 fix. Growth is: the idea-key resolution instructions added to the gather step; the synthesize step's Requirements block rewritten to pull ONLY from the owning idea key(s) with an explicit 'never include a requirement from an idea key that has no spike in this group' rule; the write_skill step's and blocks scoped to the wrapped idea key(s) instead of 'the whole MANIFEST.md'; and one new success_criteria bullet forbidding blended requirements across ideas.", + "sketch.md": "#1700: sketch.md's build_sketches step (b) reads spikes/MANIFEST.md's Requirements for non-negotiable design constraints; once spike.md restructures that file from one flat Requirements section to per-idea `### {idea-key}` sections (same PR), sketch.md's single-flat-section instruction would silently go stale — collateral of the seam this fix touches, required so sketch.md keeps reading the file correctly rather than a defect introduced by this change. Growth is the one clause distinguishing per-idea sections and instructing the reader to check the Requirements list of the relevant idea key (or all of them if none clearly matches)." + } +} diff --git a/tests/fix-1700-spike-manifest-idea-scoping.test.cjs b/tests/fix-1700-spike-manifest-idea-scoping.test.cjs new file mode 100644 index 000000000..a96d874b3 --- /dev/null +++ b/tests/fix-1700-spike-manifest-idea-scoping.test.cjs @@ -0,0 +1,221 @@ +// allow-test-rule: source-text-is-the-product (#1700) +// spike.md / spike-wrap-up.md / artifact-types.md / sketch.md are workflow and reference +// markdown — their text IS the contract loaded by the agent at runtime. Testing text content +// tests the deployed contract; there is no build step, CLI entrypoint, or generated code to +// exercise for this class of file (confirmed via mcp__memtrace__find_code: only .md files and +// unrelated `.planning/phases/*/REQUIREMENTS.md` symbols in gap-checker.cts/milestone.cts +// reference this seam — a different artifact family, not this one). +// +// Regression test for #1700: `.planning/spikes/MANIFEST.md` held a single un-keyed `## Idea` / +// `## Requirements` pair while its own read path (`spike.md:138`, frontier mode) assumed the +// tree holds several unrelated ideas across campaigns. `spike-wrap-up.md` then copied that flat +// Requirements list verbatim into every generated feature-area reference and the generated +// spike-findings skill, so one idea's requirements leaked into another idea's build as binding +// constraints. The fix scopes both the write side (spike.md's create_manifest) and the read/ +// re-emit side (spike-wrap-up.md's gather/synthesize/write_skill) by an explicit idea key, +// carried through spike README frontmatter and a new Idea column in the `## Spikes` table. +// +// QA-matrix note: none of CONTRIBUTING.md's CLI / parser / filesystem-write / security matrices +// apply — this is a workflow-prompt template edit with no runtime code path, parser, or write +// seam of its own (the `.planning/spikes/` tree is written by an LLM agent following the +// workflow prose, not by GSD's own code). No numeric limit is introduced, so the limit-1/limit/ +// limit+1 boundary-coverage rule and the fast-check property-test requirement do not apply +// either — there is no parser, budget, or bijective contract in this change. See +// `.gsd/bug/fix-1700-spike-manifest-idea-scoping/50-test-matrix.md` for the full row table. + +'use strict'; + +const { test, describe } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); + +const WORKFLOWS_DIR = path.join(__dirname, '..', 'gsd-core', 'workflows'); +const REFERENCES_DIR = path.join(__dirname, '..', 'gsd-core', 'references'); + +const spikeMd = () => fs.readFileSync(path.join(WORKFLOWS_DIR, 'spike.md'), 'utf-8'); +const spikeWrapUpMd = () => fs.readFileSync(path.join(WORKFLOWS_DIR, 'spike-wrap-up.md'), 'utf-8'); +const sketchMd = () => fs.readFileSync(path.join(WORKFLOWS_DIR, 'sketch.md'), 'utf-8'); +const artifactTypesMd = () => fs.readFileSync(path.join(REFERENCES_DIR, 'artifact-types.md'), 'utf-8'); + +describe('fix-1700: spike MANIFEST.md scopes Idea/Requirements per idea key', () => { + // Row 1 — the failing-first regression test (50-test-matrix.md row 1). + test('spike.md create_manifest step defines an explicit idea key and a scoped ## Ideas structure', () => { + const content = spikeMd(); + assert.match( + content, + /idea key/i, + 'create_manifest step must introduce an explicit "idea key" concept — without one, ' + + 'the write side has no way to tell a continuation of the current idea from an unrelated new idea (#1700).' + ); + assert.match( + content, + /##\s*Ideas\b/, + 'MANIFEST.md template must have a `## Ideas` section (plural) that can hold more than one ' + + 'idea, replacing the old single flat `## Idea` paragraph.' + ); + assert.match( + content, + /###\s*\{idea-key\}/, + "MANIFEST.md template must scope each idea under its own `### {idea-key}` subsection so " + + "idea A's paragraph is never overwritten by idea B (#1700 acceptance criterion 1)." + ); + }); + + test('create_manifest step instructs appending a new idea section instead of overwriting an existing one', () => { + const content = spikeMd(); + assert.match( + content, + /new idea key[\s\S]{0,200}?append/i, + 'When the current idea is new (not a continuation), the workflow must append a new idea ' + + 'section rather than overwrite/merge into an existing one.' + ); + assert.match( + content, + /[Nn]ever touch[\s\S]{0,80}?(?:another|other) idea/i, + "The workflow must explicitly forbid touching another idea's section when adding to the current one." + ); + }); + + test("create_manifest step migrates a pre-#1700 flat-shape MANIFEST.md instead of discarding it (Hyrum's Law)", () => { + const content = spikeMd(); + assert.match( + content, + /old flat[\s\S]{0,40}?shape/i, + 'The workflow must handle an existing MANIFEST.md written before this fix (flat `## Idea` / ' + + '`## Requirements`, no `## Ideas` heading) by migrating it in place, not discarding user data.' + ); + }); + + test('## Spikes table gains an Idea column so every spike is attributable to an idea (acceptance criterion 3)', () => { + const content = spikeMd(); + assert.match( + content, + /\|\s*#\s*\|\s*Idea\s*\|\s*Name\s*\|\s*Type\s*\|\s*Validates\s*\|\s*Verdict\s*\|\s*Tags\s*\|/, + 'The `## Spikes` table header must include an Idea column between # and Name, so the ' + + 'durable index still covers every spike across every idea while attributing each row.' + ); + }); + + test('spike README frontmatter template carries an idea key field', () => { + const content = spikeMd(); + assert.match( + content, + /spike:\s*NNN\r?\nidea:\s*\{idea-key\}/, + 'The build_spikes README frontmatter template must record the owning idea key directly ' + + 'on the spike, so spike-wrap-up can attribute requirements without re-deriving it.' + ); + }); + + test("reground step checks the CURRENT idea's requirements, not all requirements", () => { + const content = spikeMd(); + assert.match( + content, + /current idea's[\s\S]{0,80}?Requirements/i, + 'The reground step must scope its established-requirements check to the idea being spiked, ' + + 'not the whole MANIFEST.md, or it would falsely flag idea B as contradicting idea A.' + ); + }); + + test("frontier_mode load step reads every idea's scoped requirements, not one flat section", () => { + const content = spikeMd(); + assert.match( + content, + /every idea section/i, + "Frontier mode's MANIFEST.md load step must describe reading per-idea sections, matching " + + 'the new ## Ideas structure it depends on.' + ); + }); + + test("spike-wrap-up.md gather step resolves each spike's idea key before reading requirements", () => { + const content = spikeWrapUpMd(); + assert.match( + content, + /idea key/i, + "spike-wrap-up.md must resolve an idea key per spike (from frontmatter or the Spikes table) " + + 'before it can scope which Requirements apply — otherwise it still reads the whole ' + + 'MANIFEST.md Requirements section flat (#1700 root cause on the consumer side).' + ); + }); + + test("spike-wrap-up.md synthesize step scopes Requirements to the feature area's owning idea key(s)", () => { + const content = spikeWrapUpMd(); + assert.match( + content, + /ONLY from the Requirements list of the idea key/, + "The synthesize step's Requirements block must pull only from the idea key(s) that own the " + + 'spikes in that feature-area group — never the whole MANIFEST.md (#1700 acceptance criterion 2).' + ); + assert.match( + content, + /[Nn]ever include a requirement from an idea key that has no spike in this group/, + "The synthesize step must explicitly forbid leaking an unrelated idea's requirement into a " + + 'feature-area reference it has no spike in.' + ); + }); + + test("spike-wrap-up.md write_skill step unions only the wrapped idea keys' requirements", () => { + const content = spikeWrapUpMd(); + assert.match( + content, + /never the whole MANIFEST\.md/, + "The generated SKILL.md block must be built from only the idea key(s) " + + 'represented among the spikes actually being wrapped in this session.' + ); + }); + + test('artifact-types.md no longer labels the project-level MANIFEST.md "per-spike" (acceptance criterion 4)', () => { + const content = artifactTypesMd(); + assert.doesNotMatch( + content, + /MANIFEST\.md \(per-spike/, + 'artifact-types.md must not describe the project-level, durable-index MANIFEST.md as ' + + '"per-spike" — that mislabeling is the third independent statement of the #1700 ambiguity ' + + "the maintainer's triage flagged." + ); + assert.match( + content, + /MANIFEST\.md \(per-project index/, + 'artifact-types.md must instead label MANIFEST.md as the per-project durable index.' + ); + }); + + test('artifact-types.md keeps the legacy /gsd:spike colon form untouched (out of scope, #2903)', () => { + const content = artifactTypesMd(); + assert.match( + content, + /via \/gsd:spike/, + 'The colon-form command reference belongs to #2903 and must not be touched by this fix.' + ); + }); + + test('sketch.md no longer assumes spikes/MANIFEST.md has a single flat Requirements section', () => { + const content = sketchMd(); + assert.match( + content, + /separate `### \{idea-key\}` sections/, + "sketch.md reads spikes/MANIFEST.md for design constraints; once MANIFEST.md nests " + + "Requirements under per-idea sections, sketch.md's single-section instruction goes stale " + + 'unless updated in the same PR (collateral of this fix\'s seam, not a separate concern).' + ); + }); + + test('success criteria in spike.md reflect idea-scoped requirement tracking', () => { + const content = spikeMd(); + assert.match( + content, + /scoped to the idea key/, + 'spike.md success_criteria must call out that Requirements are tracked per idea key, not globally.' + ); + }); + + test('success criteria in spike-wrap-up.md forbid blending requirements across ideas', () => { + const content = spikeWrapUpMd(); + assert.match( + content, + /never blended with an unrelated idea/, + 'spike-wrap-up.md success_criteria must explicitly call out that requirements must never ' + + 'blend across idea keys.' + ); + }); +});