fix(5): decision-coverage gate parses <action> XML tag bodies for D-NN citations (#155)

* test(5): add failing test for decision IDs inside <objective>/<tasks>/<task>/<action> XML bodies

Regression test for issue #5 — the translation gate (check.decision-coverage-plan)
is blind to D-NN citations placed inside XML tag bodies by gsd-planner.

Maintainer acceptance criteria (verbatim, issue #5):
  "Gate parses <action> tag bodies for decision ID citations; regression test
   with XML-tag plan body covers all decision IDs."

Five new test cases added to the 'XML tag body citation parsing (issue #5)' suite:
  1. RED: five decisions cited only in <objective>/<action> bodies → gate fails (before fix)
  2. Non-canonical tag <comment> → must NOT count (negative control, passes)
  3. Plain prose under undesignated heading → must NOT count (negative control, passes)
  4. Self-closing <action/> → no crash, D-NN not covered (passes)
  5. D-NN in <objective> body → should count (also fails before fix, GREEN after)

Tests 1 and 5 are the load-bearing RED cases. All others are negative controls.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(5): extend extractPlanSections to scan <objective>/<tasks>/<task>/<action> for D-NN citations

Closes #5.

Root cause: extractPlanSections() in check-decision-coverage.ts collected only
front-matter (must_haves/truths/objective) and body lines under designated
markdown headings. The gsd-planner spec (agents/gsd-planner.md line 66) says
'Task actions reference the decision ID they implement (e.g., "per D-03")' and
emits citations inside <action> tag bodies — a location the gate could not see.

Fix: add extractXmlTagBodies() helper that matches the four canonical planner
XML tags (<objective>, <tasks>, <task>, <action>) via a deliberately narrow
regex (no XML parser library — D2 design decision). The helper output is
appended to the designated string inside extractPlanSections(), making any
D-NN citation inside those tag bodies count toward coverage.

Maintainer acceptance criteria (verbatim, issue #5):
  "Gate parses <action> tag bodies for decision ID citations; regression test
   with XML-tag plan body covers all decision IDs."

Self-closing tags (<action/>) are safely ignored — the capturing group does
not match. Non-canonical tags (<comment>, <note>, etc.) are not in the
alternation and are ignored by design.

The CJS surface for check-decision-coverage.ts does NOT have a generator
(gen-decisions.mjs covers decisions.ts, not this gate). No CJS artifact
exists for this module. Per the generator framework established in PR #154
(ADR-3524), a CJS migration is a follow-up; this PR focuses on the TS fix.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs(5): clarify in gsd-planner.md that decision-coverage gate reads XML tag bodies

Adds a parenthetical note to the existing self-check bullet (line 66) explaining
which locations the gate scans so the planner's own guidance and the gate's
behavior are explicitly aligned.

Refs #5. No behavior change — documentation truthing only.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore(5): add changeset fragment for decision-coverage XML body fix

Refs #5.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs(5): extract Interface Context for Executors into reference file to pass planner-decomposition gate

gsd-planner.md was 49446 chars after the XML-tag clarification added in
this PR, exceeding the 48K threshold enforced by
tests/planner-decomposition.test.cjs. Extracted the "Interface Context
for Executors" section (~2137 chars) into
get-shit-done/references/planner-interface-context.md, leaving a one-line
pointer in gsd-planner.md. New normalized size: 47310 chars (1842 chars
under threshold).

Refs #5

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(5): register planner-interface-context.md in INVENTORY.md and manifest

- Bump References headline from 61 to 62 to match filesystem count
- Add planner-interface-context.md row in Modular Planner Decomposition table
- Update footnote from 61 to 62 top-level references
- Regenerate docs/INVENTORY-MANIFEST.json via gen-inventory-manifest.cjs --write

Fixes inventory-counts and inventory-manifest-sync CI failures caused by the
extraction commit (32e8950f) adding a new reference file without updating the
inventory artefacts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-05-23 16:22:26 -04:00
committed by GitHub
parent 7f02f9090b
commit 41210f014e
7 changed files with 303 additions and 63 deletions

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 5
---
**`check.decision-coverage-plan` gate now recognises D-NN citations inside `<objective>`, `<tasks>`, `<task>`, and `<action>` XML tag bodies** — `extractPlanSections()` previously searched only front-matter (`must_haves`, `truths`, `objective`) and body lines under designated markdown headings. The `gsd-planner` spec directs agents to cite decision IDs inside `<action>` bodies; those citations were invisible to the gate, causing plans that correctly followed the spec to report `passed: false` with all decisions uncovered. `extractXmlTagBodies()` now extracts inner text from the four canonical planner XML tags via a narrow regex (no parser library), and that text is appended to the `designated` search string. Self-closing tags and non-canonical tags are safely ignored. (#5)

View File

@@ -64,6 +64,7 @@ The orchestrator provides user decisions in `<user_decisions>` tags from `/gsd:d
**Self-check before returning:** For each plan, verify:
- [ ] Every locked decision (D-01, D-02, etc.) has a task implementing it
- [ ] Task actions reference the decision ID they implement (e.g., "per D-03")
(The decision-coverage gate `check.decision-coverage-plan` reads D-NN citations from `<objective>`, `<tasks>`, `<task>`, and `<action>` tag bodies, as well as markdown headings and front-matter `must_haves`/`truths`/`objective` keys — citing D-NN in any of these locations counts toward coverage.)
- [ ] No task implements a deferred idea
- [ ] Discretion areas are handled reasonably
@@ -519,66 +520,7 @@ Wave numbers are pre-computed during planning. Execute-phase reads `wave` direct
## Interface Context for Executors
**Key insight:** "The difference between handing a contractor blueprints versus telling them 'build me a house.'"
When creating plans that depend on existing code or create new interfaces consumed by other plans:
### For plans that USE existing code:
After determining `files_modified`, extract the key interfaces/types/exports from the codebase that executors will need:
```bash
# Extract type definitions, interfaces, and exports from relevant files
grep -n "export\\|interface\\|type\\|class\\|function" {relevant_source_files} 2>/dev/null | head -50
```
Embed these in the plan's `<context>` section as an `<interfaces>` block:
```xml
<interfaces>
<!-- Key types and contracts the executor needs. Extracted from codebase. -->
<!-- Executor should use these directly — no codebase exploration needed. -->
From src/types/user.ts:
```typescript
export interface User {
id: string;
email: string;
name: string;
createdAt: Date;
}
```
From src/api/auth.ts:
```typescript
export function validateToken(token: string): Promise<User | null>;
export function createSession(user: User): Promise<SessionToken>;
```
</interfaces>
```
### For plans that CREATE new interfaces:
If this plan creates types/interfaces that later plans depend on, include a "Wave 0" skeleton step:
```xml
<task type="auto">
<name>Task 0: Write interface contracts</name>
<files>src/types/newFeature.ts</files>
<action>Create type definitions that downstream plans will implement against. These are the contracts — implementation comes in later tasks.</action>
<verify>File exists with exported types, no implementation</verify>
<done>Interface file committed, types exported</done>
</task>
```
### When to include interfaces:
- Plan touches files that import from other modules → extract those module's exports
- Plan creates a new API endpoint → extract the request/response types
- Plan modifies a component → extract its props interface
- Plan depends on a previous plan's output → extract the types from that plan's files_modified
### When to skip:
- Plan is self-contained (creates everything from scratch, no imports)
- Plan is pure configuration (no code interfaces involved)
- Level 0 discovery (all patterns already established)
See `get-shit-done/references/planner-interface-context.md` for the full interface extraction guide.
## Context Section Rules

View File

@@ -226,6 +226,7 @@
"planner-gap-closure.md",
"planner-graphify-auto-update.md",
"planner-human-verify-mode.md",
"planner-interface-context.md",
"planner-mvp-mode.md",
"planner-reviews.md",
"planner-revision.md",

View File

@@ -262,7 +262,7 @@ Full roster at `get-shit-done/workflows/*.md`. Workflows are thin orchestrators
---
## References (61 shipped)
## References (62 shipped)
Full roster at `get-shit-done/references/*.md`. References are shared knowledge documents that workflows and agents `@-reference`. The groupings below match [`docs/ARCHITECTURE.md`](ARCHITECTURE.md#references-get-shit-donereferencesmd) — core, workflow, thinking-model clusters, and the modular planner decomposition.
@@ -353,11 +353,12 @@ The `gsd-planner` agent is decomposed into a core agent plus reference modules t
| `planner-mvp-mode.md` | Vertical-slice planning rules for MVP mode. |
| `planner-human-verify-mode.md` | Rules for `workflow.human_verify_mode = end-of-phase`: suppress `checkpoint:human-verify` task emission and route deferred items via `<verify><human-check>`. |
| `planner-graphify-auto-update.md` | How `load_graph_context` surfaces `.last-build-status.json` auto-update state (running / failed / stale head) alongside the existing staleness annotation. Opt-in via `graphify.auto_update` (#3347). |
| `planner-interface-context.md` | Interface context rules for executors — how to extract key interfaces/types/exports from existing code and document new interfaces that downstream plans will consume. |
| `skeleton-template.md` | SKELETON.md template emitted for new-project Walking Skeleton (Phase 1 + `--mvp`). |
| `user-story-template.md` | User story format for MVP planning — "As a / I want to / So that" structured fields. |
| `spidr-splitting.md` | SPIDR splitting decomposition rules for handling large user stories in MVP mode. |
> **Subdirectory:** `get-shit-done/references/few-shot-examples/` contains additional few-shot examples (`plan-checker.md`, `verifier.md`) that are referenced from specific agents. These are not counted in the 61 top-level references.
> **Subdirectory:** `get-shit-done/references/few-shot-examples/` contains additional few-shot examples (`plan-checker.md`, `verifier.md`) that are referenced from specific agents. These are not counted in the 62 top-level references.
---

View File

@@ -0,0 +1,62 @@
# Interface Context for Executors
**Key insight:** "The difference between handing a contractor blueprints versus telling them 'build me a house.'"
When creating plans that depend on existing code or create new interfaces consumed by other plans:
## For plans that USE existing code:
After determining `files_modified`, extract the key interfaces/types/exports from the codebase that executors will need:
```bash
# Extract type definitions, interfaces, and exports from relevant files
grep -n "export\\|interface\\|type\\|class\\|function" {relevant_source_files} 2>/dev/null | head -50
```
Embed these in the plan's `<context>` section as an `<interfaces>` block:
```xml
<interfaces>
<!-- Key types and contracts the executor needs. Extracted from codebase. -->
<!-- Executor should use these directly — no codebase exploration needed. -->
From src/types/user.ts:
```typescript
export interface User {
id: string;
email: string;
name: string;
createdAt: Date;
}
```
From src/api/auth.ts:
```typescript
export function validateToken(token: string): Promise<User | null>;
export function createSession(user: User): Promise<SessionToken>;
```
</interfaces>
```
## For plans that CREATE new interfaces:
If this plan creates types/interfaces that later plans depend on, include a "Wave 0" skeleton step:
```xml
<task type="auto">
<name>Task 0: Write interface contracts</name>
<files>src/types/newFeature.ts</files>
<action>Create type definitions that downstream plans will implement against. These are the contracts — implementation comes in later tasks.</action>
<verify>File exists with exported types, no implementation</verify>
<done>Interface file committed, types exported</done>
</task>
```
## When to include interfaces:
- Plan touches files that import from other modules → extract those module's exports
- Plan creates a new API endpoint → extract the request/response types
- Plan modifies a component → extract its props interface
- Plan depends on a previous plan's output → extract the types from that plan's files_modified
## When to skip:
- Plan is self-contained (creates everything from scratch, no imports)
- Plan is pure configuration (no code interfaces involved)
- Level 0 discovery (all patterns already established)

View File

@@ -517,3 +517,197 @@ describe('config-type validation (review F16)', () => {
expect(warnings.some((w) => /context_coverage_gate.*invalid type/.test(w))).toBe(true);
});
});
// ─── XML tag body parsing (issue #5) ──────────────────────────────────────
/**
* Regression tests for issue #5: the translation gate must parse decision IDs
* from <objective>, <tasks>, <task>, and <action> tag bodies.
*
* The gsd-planner spec (agents/gsd-planner.md line 66) says:
* "Task actions reference the decision ID they implement (e.g., 'per D-03')"
* Plans produced by that spec put D-NN citations inside <action> bodies,
* which the gate was blind to before this fix.
*
* Maintainer acceptance criteria (verbatim from issue #5):
* "Gate parses <action> tag bodies for decision ID citations; regression test
* with XML-tag plan body covers all decision IDs."
*/
describe('XML tag body citation parsing (issue #5)', () => {
/** Five locked decisions, D-01..D-05. */
const FIVE_DECISIONS = `<decisions>
### Architecture
- **D-01:** Use strict TypeScript everywhere
- **D-02:** Prefer functional composition over class inheritance
- **D-03:** All public APIs must have JSDoc
- **D-04:** Errors are typed, never thrown as plain strings
- **D-05:** Async functions always return explicit Promises
</decisions>`;
/**
* Plan whose D-NN citations are ONLY inside XML tag bodies —
* no front-matter must_haves/truths/objective entries.
*/
const XML_PLAN = `---
phase: 17
plan: 1
type: implementation
wave: 1
depends_on: []
files_modified: []
autonomous: true
must_haves:
- thing-a
objective: |
Build the foo.
---
<objective>Implement foo per D-01 and D-02 design</objective>
<tasks>
<task>
<action>Apply D-03 mitigation to the bar module</action>
</task>
<task>
<action>Refactor per D-04 — ensures D-05 invariant</action>
</task>
</tasks>
`;
it('RED: gate is blind to D-NN citations inside XML tag bodies (before fix)', async () => {
// This test is expected to FAIL on origin/main (gate returns covered: 0).
// After fix it should pass (covered: 5).
await setupPhase(FIVE_DECISIONS, { '17-01-PLAN.md': XML_PLAN });
const result = await checkDecisionCoveragePlan([phaseDir, contextPath], tmp);
// GREEN assertion (what we want AFTER the fix):
expect(result.data.passed).toBe(true);
expect(result.data.covered).toBe(5);
expect(result.data.uncovered).toEqual([]);
expect(result.data.total).toBe(5);
});
it('does NOT count D-NN inside a non-canonical XML tag like <comment>', async () => {
// <comment> is not one of the four canonical tags; must not count.
const planWithComment = `---
phase: 17
plan: 1
type: implementation
wave: 1
depends_on: []
files_modified: []
autonomous: true
must_haves:
- thing-a
---
<comment>D-01 not implemented yet</comment>
`;
await setupPhase(
`<decisions>
### Cat
- **D-01:** A trackable decision six words or more
</decisions>`,
{ '17-01-PLAN.md': planWithComment },
);
const result = await checkDecisionCoveragePlan([phaseDir, contextPath], tmp);
expect(result.data.passed).toBe(false);
expect(result.data.uncovered.map((u: { id: string }) => u.id)).toContain('D-01');
});
it('does NOT count D-NN mentioned in regular prose outside any XML tag', async () => {
// Prose outside designated headings and XML tags must not count.
const planWithProse = `---
phase: 17
plan: 1
type: implementation
wave: 1
depends_on: []
files_modified: []
autonomous: true
must_haves:
- thing-a
---
This section mentions D-01 in plain prose under an undesignated heading.
## Design Notes
D-01 appears here too but this heading is not designated.
`;
await setupPhase(
`<decisions>
### Cat
- **D-01:** A trackable decision six words or more
</decisions>`,
{ '17-01-PLAN.md': planWithProse },
);
const result = await checkDecisionCoveragePlan([phaseDir, contextPath], tmp);
expect(result.data.passed).toBe(false);
expect(result.data.uncovered.map((u: { id: string }) => u.id)).toContain('D-01');
});
it('does not crash on self-closing <action/> tags', async () => {
const planWithSelfClosing = `---
phase: 17
plan: 1
type: implementation
wave: 1
depends_on: []
files_modified: []
autonomous: true
must_haves:
- thing-a
---
<tasks>
<task>
<action/>
</task>
</tasks>
`;
await setupPhase(
`<decisions>
### Cat
- **D-01:** A trackable decision six words or more
</decisions>`,
{ '17-01-PLAN.md': planWithSelfClosing },
);
// Should not throw; D-01 is uncovered (no citation in self-closing tag)
const result = await checkDecisionCoveragePlan([phaseDir, contextPath], tmp);
expect(result.data.passed).toBe(false);
expect(result.data.uncovered.map((u: { id: string }) => u.id)).toContain('D-01');
});
it('counts D-NN in <objective> tag body', async () => {
const planWithObjective = `---
phase: 17
plan: 1
type: implementation
wave: 1
depends_on: []
files_modified: []
autonomous: true
must_haves:
- thing-a
---
<objective>Implement D-01 as the core type system</objective>
`;
await setupPhase(
`<decisions>
### Cat
- **D-01:** A trackable decision six words or more
</decisions>`,
{ '17-01-PLAN.md': planWithObjective },
);
const result = await checkDecisionCoveragePlan([phaseDir, contextPath], tmp);
expect(result.data.passed).toBe(true);
expect(result.data.covered).toBe(1);
});
});

View File

@@ -157,6 +157,21 @@ interface PlanSections {
const DESIGNATED_HEADINGS_RE = /^#{1,6}\s+(?:must[_ ]haves?|truths?|tasks?|objective)\b/i;
/**
* Extracts text from the four canonical XML tags that gsd-planner emits for
* decision citations: <objective>, <tasks>, <task>, <action>.
*
* Uses a deliberately narrow regex (D2) — no XML parser library — because the
* planner's XML convention is a project-internal convention, not formal XML.
* Self-closing tags (e.g., <action/>) are harmlessly skipped (no capture group
* match). Non-canonical tags (e.g., <comment>) are NOT matched.
*
* Reference: agents/gsd-planner.md line 66:
* "Task actions reference the decision ID they implement (e.g., 'per D-03')"
*/
const XML_DECISION_TAGS_RE =
/<(?:objective|tasks?|action)(?:\s[^>]*)?>([\s\S]*?)<\/(?:objective|tasks?|action)>/gi;
/** Strip HTML comments AND fenced code blocks from `text`. */
function stripCommentsAndFences(text: string): string {
return text
@@ -165,6 +180,19 @@ function stripCommentsAndFences(text: string): string {
.replace(/~~~[\s\S]*?~~~/g, ' ');
}
/**
* Extract the inner text of all canonical XML tag bodies from `text`.
* Returns a concatenated string of all matched inner bodies, or '' if none.
* Called on the cleaned (comments+fences stripped) plan body.
*/
function extractXmlTagBodies(text: string): string {
const parts: string[] = [];
for (const m of text.matchAll(XML_DECISION_TAGS_RE)) {
if (m[1]) parts.push(m[1]);
}
return parts.join('\n');
}
/** Extract a YAML block scalar (key followed by indented continuation lines). */
function extractYamlBlock(frontmatter: string, key: string): string {
const re = new RegExp(`^${key}\\s*:(.*)$`, 'm');
@@ -214,7 +242,14 @@ function extractPlanSections(planContent: string): PlanSections {
if (inDesignated) bodyParts.push(line);
}
return { designated: [...fmParts, bodyParts.join('\n')].join('\n\n') };
// Also include the inner text of canonical XML tag bodies (<objective>, <tasks>,
// <task>, <action>). The planner spec (agents/gsd-planner.md line 66) directs
// agents to cite D-NN inside <action> bodies; the gate must honour those citations.
// extractXmlTagBodies is called on the full cleaned content (not just the body
// portion) so that <objective> blocks at the top of the document are also caught.
const xmlParts = extractXmlTagBodies(cleaned);
return { designated: [...fmParts, bodyParts.join('\n'), xmlParts].join('\n\n') };
}
async function loadPlanSections(phaseDir: string): Promise<PlanSections[]> {