fix(#2372): widen decision-coverage-plan to all planner-canonical tags, drop misleading "(or body)" (#2443)
* fix(#2372): widen decision-coverage scan to planner-canonical tags, fix message Bug: check.decision-coverage-plan's remediation message told the user to cite decisions "(or body)" but extractPlanDesignatedSections only scanned <objective>/<tasks>/<task>/<action>. A decision cited in <read_first>, <behavior>, <verify>, <acceptance_criteria>, or <done> was invisible to the gate — false BLOCKING coverage gap, plus the message's own fix-hint sent the user to "the body" where re-citing still failed. Two-part fix (must change together — that drift was the bug): 1. Widen XML_DECISION_TAGS_RE in src/check-command-router.cts to also match <read_first>, <behavior>, <verify>, <acceptance_criteria>, <done>. These are all planner-canonical tags the planner is told to use (plan-phase.md:830-862, plan-phase.md:772). The body negative- lookahead mirrors the opening-tag set so each tag's body is captured independently. 2. Correct buildPlanMessage to name ONLY the surfaces the extractor actually scans (front-matter must_haves/truths/objective, designated markdown headings, and the nine planner-canonical tag bodies). The misleading "(or body)" clause is gone. Also updates the planner's documented contract (agents/gsd-planner.md:69) and user-facing docs (docs/CONFIGURATION.md, docs/USER-GUIDE.md) to reflect the wider scan. Regression tests in tests/decisions.test.cjs cover each newly-scanned tag body, a control (no citation still uncovered), and a message/extractor parity assertion that names every scanned surface — so the two cannot drift apart again. Out of scope (per triage): cmdDecisionCoverageVerify/buildVerifyMessage is a separate command (decision-coverage-verify) checking shipped artifacts, not plan citations — untouched. * chore(#2372): regenerate agent-size-baseline + golden-install-parity fixtures gsd-planner.md grew 49172 → 49294 (+122 chars) from the widened decision- coverage contract (5 new scanned tag names + heading clarification). Growth is justified: the contract surface is itself the fix — the prior text under-described what the gate scans, which was the bug. Updates: - tests/agent-size-baseline.json (gsd-planner.md: 49172 → 49294) - 17 tests/fixtures/golden-install-parity/*.json (one hash per runtime) - tests/fixtures/install-tree/*.json (regenerated by gen:golden) * fix(#2372): per-tag matching — outer-tag citations survive inner-tag nesting Code review (subagent) flagged a Medium edge-case regression from the single-alternation regex: when a newly-scanned tag nests inside another scanned tag, the alternation's negative lookahead halts the outer tag's body at the inner tag — losing any D-NN citation in the outer tag's prefix prose. Concretely: <action>per D-05 <verify>npm test</verify></action> → 3-tag alternation (old): captured 'per D-05 <verify>npm test</verify>' as <action> body → D-05 caught → 9-tag alternation (bug): captured 'npm test' only (from <verify>); D-05 in <action> prefix LOST Switches extractXmlTagBodies to per-tag matching: each tag gets its own regex whose negative-lookahead tempers only against the SAME tag's reopening. So <verify> inside <action> is absorbed into <action>'s body (D-05 caught) AND <verify> is matched separately on its own pass. Per-tag preserves both: - the reporter's case (sibling tags inside <read_first>) - nested-tag citations in outer-tag prefix prose - ReDoS safety (each per-tag regex keeps the #2128 body tempering) Also adds the reviewer's other requested edge-case tests: - non-scanned tag (<name>) bearing D-NN must NOT count - self-closing form <read_first /> safely ignored - attribute form <verify type="...">D-NN</verify> (canonical planner shape) - CRLF newlines in tag body do not break capture * chore(changeset): backfill pr:2443 in .changeset/noble-elks-chatter.md
This commit is contained in:
5
.changeset/noble-elks-chatter.md
Normal file
5
.changeset/noble-elks-chatter.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 2443
|
||||
---
|
||||
**`check.decision-coverage-plan` no longer false-blocks on decisions cited in `<read_first>`/`<behavior>`/`<verify>`/`<acceptance_criteria>`/`<done>`** — the gate scanned only `<objective>`/`<tasks>`/`<task>`/`<action>` tag bodies while its remediation message claimed "(or body)". A decision faithfully cited in any of the five other planner-canonical tags (the natural place for "read this CONTEXT decision before editing" pointers, verification steps, acceptance criteria, etc.) was reported as uncovered with a misleading fix-hint that sent the fixer to "the body" — where a re-citation still failed. The scan now covers all nine planner-canonical tag bodies AND the message names the surfaces it actually scans, so message and behavior cannot drift apart again. (#2372)
|
||||
@@ -66,7 +66,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.)
|
||||
(The decision-coverage gate `check.decision-coverage-plan` reads D-NN citations from `<objective>`, `<tasks>`, `<task>`, `<action>`, `<read_first>`, `<behavior>`, `<verify>`, `<acceptance_criteria>`, and `<done>` tag bodies, as well as `## must_haves`/`truths`/`tasks`/`objective` 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
|
||||
|
||||
|
||||
@@ -927,7 +927,9 @@ plans and shipped code (issue #2492).
|
||||
existing requirements coverage gate, before plans are committed. For each
|
||||
trackable decision in `<decisions>`, it checks that the decision id
|
||||
(`D-NN`) or its text appears in at least one plan's `must_haves`,
|
||||
`truths`, or body. A miss surfaces the missing decision by id and refuses
|
||||
`truths`, or `objective` (front-matter), a `## must_haves`/`truths`/`tasks`/`objective`
|
||||
heading, or an `<objective>`/`<tasks>`/`<task>`/`<action>`/`<read_first>`/`<behavior>`/`<verify>`/`<acceptance_criteria>`/`<done>`
|
||||
tag body. A miss surfaces the missing decision by id and refuses
|
||||
to mark the phase planned.
|
||||
|
||||
**Verify-phase validation gate (NON-BLOCKING).** Runs alongside the other
|
||||
|
||||
@@ -231,7 +231,7 @@ See [docs/workflow-discuss-mode.md](workflow-discuss-mode.md) for the full discu
|
||||
|
||||
The discuss-phase captures implementation decisions in CONTEXT.md under a `<decisions>` block as numbered bullets (`- **D-01:** …`). Two gates ensure those decisions survive into plans and shipped code.
|
||||
|
||||
**Plan-phase translation gate (blocking).** After planning, GSD refuses to mark the phase planned until every trackable decision appears in at least one plan's `must_haves`, `truths`, or body.
|
||||
**Plan-phase translation gate (blocking).** After planning, GSD refuses to mark the phase planned until every trackable decision appears in at least one plan's scanned surfaces: front-matter `must_haves`/`truths`/`objective`, a `## must_haves`/`truths`/`tasks`/`objective` heading, or an `<objective>`/`<tasks>`/`<task>`/`<action>`/`<read_first>`/`<behavior>`/`<verify>`/`<acceptance_criteria>`/`<done>` tag body.
|
||||
|
||||
**Verify-phase validation gate (non-blocking).** During verification, GSD searches plans, SUMMARY.md, modified files, and recent commit messages for each trackable decision. Misses are logged to VERIFICATION.md as a warning section; verification status is unchanged.
|
||||
|
||||
|
||||
@@ -139,7 +139,30 @@ function loadPlanContents(phaseDir: string): string[] {
|
||||
}
|
||||
|
||||
const DESIGNATED_HEADINGS_RE = /^#{1,6}\s+(?:must[_ ]haves?|truths?|tasks?|objective)\b/i;
|
||||
const XML_DECISION_TAGS_RE = /<(?:objective|tasks?|action)(?:\s[^>]{0,1000})?>((?:(?!<(?:objective|tasks?|action)[\s>])[\s\S])*?)<\/(?:objective|tasks?|action)>/gi;
|
||||
// #2372: scanned-tag set must match the planner-canonical surfaces where a D-NN citation
|
||||
// is meaningful. `<objective>`/`<tasks>`/`<task>`/`<action>` are the historical core. The
|
||||
// planner is also explicitly told (plan-phase.md) to cite decisions in `<read_first>`,
|
||||
// `<behavior>`, `<verify>`, `<acceptance_criteria>`, and `<done>` — those are now scanned too,
|
||||
// so the gate no longer reports a false coverage gap when a decision is cited in any of them.
|
||||
//
|
||||
// Implementation: per-tag matching, NOT a single wide alternation. A single alternation
|
||||
// like `<(?:a|b|c)>...<\/(?:a|b|c)>` halts the outer tag's body capture at any inner tag
|
||||
// in the set, dropping any citation in the outer tag's prefix prose — e.g.
|
||||
// `<action>per D-05 <verify>npm test</verify></action>` would lose D-05 because `<verify>`
|
||||
// halts the `<action>` body before the citation. Per-tag matching avoids this: each tag's
|
||||
// body terminates only at its OWN closing tag, so `<verify>` inside `<action>` is absorbed
|
||||
// into `<action>`'s body (D-05 caught) AND `<verify>` is matched separately on its own pass.
|
||||
// Each per-tag regex keeps the ReDoS-safe negative-lookahead tempering (#2128).
|
||||
const XML_DECISION_TAG_NAMES = ['objective', 'tasks', 'task', 'action', 'read_first', 'behavior', 'verify', 'acceptance_criteria', 'done'] as const;
|
||||
|
||||
function buildXmlDecisionTagRegex(tagName: string): RegExp {
|
||||
// Per-tag: body tempering stops only at the SAME tag's reopening or closing — other
|
||||
// scanned tags pass through as text into this body. Non-greedy `*?` to first close.
|
||||
return new RegExp(
|
||||
`<${tagName}(?:\\s[^>]{0,1000})?>((?:(?!<${tagName}[\\s>])[\\s\\S])*?)<\\/${tagName}>`,
|
||||
'gi',
|
||||
);
|
||||
}
|
||||
|
||||
function stripCommentsAndFences(text: string): string {
|
||||
// HTML-comment stripping stays caller-side (the seam does not strip HTML comments).
|
||||
@@ -167,8 +190,11 @@ function extractYamlBlock(frontmatter: string, key: string): string {
|
||||
|
||||
function extractXmlTagBodies(text: string): string {
|
||||
const parts: string[] = [];
|
||||
for (const match of text.matchAll(XML_DECISION_TAGS_RE)) {
|
||||
if (match[1]) parts.push(match[1]);
|
||||
for (const tagName of XML_DECISION_TAG_NAMES) {
|
||||
const re = buildXmlDecisionTagRegex(tagName);
|
||||
for (const match of text.matchAll(re)) {
|
||||
if (match[1]) parts.push(match[1]);
|
||||
}
|
||||
}
|
||||
return parts.join('\n');
|
||||
}
|
||||
@@ -219,7 +245,10 @@ function buildPlanMessage(uncovered: UncoveredItem[]): string {
|
||||
'',
|
||||
...uncovered.map((item) => `- **${item.id}** (${item.category || 'uncategorized'}): ${item.text}`),
|
||||
'',
|
||||
'Resolve by citing `D-NN:` in a relevant plan\'s `must_haves`/`truths` (or body),',
|
||||
'Resolve by citing `D-NN:` in any of the scanned plan surfaces: front-matter',
|
||||
'`must_haves`/`truths`/`objective`, a `## must_haves`/`truths`/`tasks`/`objective`',
|
||||
'heading, or an `<objective>`/`<tasks>`/`<task>`/`<action>`/`<read_first>`/`<behavior>`/`<verify>`/`<acceptance_criteria>`/`<done>`',
|
||||
'tag body. Other locations (prose outside those headings, comments, other XML tags) are not scanned.',
|
||||
'OR move the decision to `### Claude\'s Discretion` / tag it `[informational]` if it should not be tracked.',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"gsd-pattern-mapper.md": 12487,
|
||||
"gsd-phase-researcher.md": 40866,
|
||||
"gsd-plan-checker.md": 44780,
|
||||
"gsd-planner.md": 49172,
|
||||
"gsd-planner.md": 49294,
|
||||
"gsd-project-researcher.md": 22242,
|
||||
"gsd-research-synthesizer.md": 13847,
|
||||
"gsd-roadmapper.md": 22273,
|
||||
|
||||
@@ -922,3 +922,205 @@ describe('parseDecisions — titled-colon bullet form (#1639)', () => {
|
||||
assert.equal(out.length, 13, 'all 13 titled-colon decisions must parse (not vacuously 0)');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── #2372: decision-coverage-plan must scan planner-canonical tag bodies ─────
|
||||
//
|
||||
// Bug: buildPlanMessage() told the user to cite decisions "(or body)" but
|
||||
// extractPlanDesignatedSections() only scanned <objective>/<tasks>/<task>/<action>.
|
||||
// A decision cited in <read_first>, <behavior>, <verify>, <acceptance_criteria>,
|
||||
// or <done> was invisible — false BLOCKING coverage gap, and the message sent the
|
||||
// fixer to "the body", where a re-citation still failed. Fix widens the scan to
|
||||
// the planner-canonical tag set AND corrects the message to name scanned surfaces.
|
||||
|
||||
describe('check.decision-coverage-plan — planner-canonical tag scanning (#2372)', () => {
|
||||
let tmpDir;
|
||||
let planningDir;
|
||||
let phaseDir;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = createTempProject('gsd-2372-');
|
||||
planningDir = path.join(tmpDir, '.planning');
|
||||
phaseDir = path.join(planningDir, 'phases', '01-init');
|
||||
fs.mkdirSync(phaseDir, { recursive: true });
|
||||
});
|
||||
|
||||
afterEach(() => cleanup(tmpDir));
|
||||
|
||||
// Common CONTEXT.md with a single trackable decision D-01.
|
||||
const CONTEXT_WITH_D01 = [
|
||||
'# Context',
|
||||
'',
|
||||
'<decisions>',
|
||||
'- **D-01:** Use OAuth 2.0 for authentication',
|
||||
'</decisions>',
|
||||
].join('\n');
|
||||
|
||||
// Helper: write CONTEXT + a PLAN whose body wraps `innerTagBody` in `tagName`.
|
||||
function writeContextAndPlan(tagName, innerTagBody) {
|
||||
writeContextFile(phaseDir, CONTEXT_WITH_D01);
|
||||
writePlanFile(phaseDir, '01', `# Plan\n\n<tasks>\n<task>\n <${tagName}>\n${innerTagBody}\n </${tagName}>\n</task>\n</tasks>\n`);
|
||||
}
|
||||
|
||||
const cases = [
|
||||
{ tag: 'read_first', citation: '- path/to/CONTEXT.md (D-01 — auth decision)' },
|
||||
{ tag: 'behavior', citation: 'Honor D-01: redirect unauthenticated users to OAuth flow.' },
|
||||
{ tag: 'verify', citation: 'Verify D-01: token exchange returns 200 with access_token.' },
|
||||
{ tag: 'acceptance_criteria', citation: 'D-01 honored: every protected route requires a valid OAuth token.' },
|
||||
{ tag: 'done', citation: 'D-01 implemented — OAuth 2.0 flow live.' },
|
||||
];
|
||||
|
||||
for (const { tag, citation } of cases) {
|
||||
test(`D-NN cited in <${tag}> body → covered (no false gap)`, () => {
|
||||
writeContextAndPlan(tag, citation);
|
||||
|
||||
const contextPath = path.join(phaseDir, 'CONTEXT.md');
|
||||
const result = runDecisionCoveragePlan(phaseDir, contextPath, tmpDir);
|
||||
const parsed = JSON.parse(result.output || '{}');
|
||||
|
||||
assert.strictEqual(parsed.total, 1, `expected total=1, got ${JSON.stringify(parsed)}`);
|
||||
assert.strictEqual(parsed.covered, 1, `D-01 cited in <${tag}> must count as covered. Got: ${JSON.stringify(parsed)}`);
|
||||
assert.strictEqual(parsed.passed, true, `gate must pass when D-01 is cited in <${tag}>. Got: ${JSON.stringify(parsed)}`);
|
||||
assert.strictEqual(parsed.uncovered.length, 0, `uncovered must be empty. Got: ${JSON.stringify(parsed.uncovered)}`);
|
||||
});
|
||||
}
|
||||
|
||||
test('control: D-NN cited nowhere → still uncovered (no false green from widening)', () => {
|
||||
writeContextFile(phaseDir, CONTEXT_WITH_D01);
|
||||
writePlanFile(phaseDir, '01', '# Plan\n\n<tasks>\n<task>\n <action>\n Implement the feature.\n </action>\n</task>\n</tasks>\n');
|
||||
|
||||
const contextPath = path.join(phaseDir, 'CONTEXT.md');
|
||||
const result = runDecisionCoveragePlan(phaseDir, contextPath, tmpDir);
|
||||
const parsed = JSON.parse(result.output || '{}');
|
||||
|
||||
assert.strictEqual(parsed.total, 1);
|
||||
assert.strictEqual(parsed.covered, 0);
|
||||
assert.strictEqual(parsed.passed, false);
|
||||
assert.strictEqual(parsed.uncovered.length, 1);
|
||||
assert.strictEqual(parsed.uncovered[0].id, 'D-01');
|
||||
});
|
||||
|
||||
// Message/extractor parity: the remediation text must name ONLY the surfaces the
|
||||
// extractor actually scans. Asserts no "(or body)" claim it doesn't back, AND
|
||||
// that every newly-scanned tag is named in the message — so the two cannot drift
|
||||
// apart again. The reporter's bug was exactly this drift.
|
||||
test('buildPlanMessage names every scanned surface (no "(or body)" drift, #2372)', () => {
|
||||
writeContextFile(phaseDir, CONTEXT_WITH_D01);
|
||||
writePlanFile(phaseDir, '01', '# Plan\nNo decision citation in any scanned surface.\n');
|
||||
|
||||
const contextPath = path.join(phaseDir, 'CONTEXT.md');
|
||||
const result = runDecisionCoveragePlan(phaseDir, contextPath, tmpDir);
|
||||
const parsed = JSON.parse(result.output || '{}');
|
||||
|
||||
assert.strictEqual(parsed.passed, false, 'fixture intentionally leaves D-01 uncovered');
|
||||
const msg = parsed.message || '';
|
||||
|
||||
// The misleading "(or body)" clause that promised a scope the extractor didn't implement is gone.
|
||||
assert.ok(!/\(or body\)/i.test(msg), `message must not claim "(or body)" — that was the bug. Got: "${msg}"`);
|
||||
|
||||
// Every surface the extractor now scans MUST appear by name in the message — if any is
|
||||
// missing, the message has drifted from the scan again.
|
||||
const requiredSurfaceNames = [
|
||||
'must_haves', 'truths', 'objective',
|
||||
'<objective>', '<tasks>', '<task>', '<action>',
|
||||
'<read_first>', '<behavior>', '<verify>', '<acceptance_criteria>', '<done>',
|
||||
];
|
||||
for (const surface of requiredSurfaceNames) {
|
||||
assert.ok(
|
||||
msg.includes(surface),
|
||||
`message must name scanned surface "${surface}" — otherwise the message/extractor drift apart. Got: "${msg}"`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Reviewer-driven edge cases (code review on the initial widening flagged these):
|
||||
//
|
||||
// 1. Nested scanned tag inside another scanned tag: a citation in the OUTER tag's
|
||||
// prefix prose must still count. Initial widening used a single alternation whose
|
||||
// negative lookahead halted the outer tag's body at any inner scanned tag — losing
|
||||
// the prefix citation. Switched to per-tag matching so each tag's body terminates
|
||||
// only at its own closing tag (other scanned tags pass through as text into this body).
|
||||
// 2. Non-scanned tag bearing a D-NN citation must NOT count toward coverage — guards
|
||||
// against future over-widening.
|
||||
// 3. Self-closing form `<read_first />` has no body and must not error or match.
|
||||
// 4. Attribute form `<verify type="automated">D-01</verify>` is the canonical planner
|
||||
// shape for <verify> and must match.
|
||||
// 5. CRLF newlines inside tag bodies must not break capture.
|
||||
|
||||
test('D-NN in outer scanned tag prefix is not lost when inner scanned tag follows (per-tag capture)', () => {
|
||||
writeContextFile(phaseDir, CONTEXT_WITH_D01);
|
||||
// The bug shape: <action>per D-01 <verify>...</verify></action> — D-01 lives in <action>'s prefix.
|
||||
writePlanFile(phaseDir, '01', [
|
||||
'# Plan',
|
||||
'',
|
||||
'<tasks>',
|
||||
'<task>',
|
||||
' <action>',
|
||||
' Implement per D-01 — OAuth 2.0 flow.',
|
||||
' <verify>token exchange returns 200</verify>',
|
||||
' </action>',
|
||||
'</task>',
|
||||
'</tasks>',
|
||||
].join('\n'));
|
||||
|
||||
const contextPath = path.join(phaseDir, 'CONTEXT.md');
|
||||
const result = runDecisionCoveragePlan(phaseDir, contextPath, tmpDir);
|
||||
const parsed = JSON.parse(result.output || '{}');
|
||||
|
||||
assert.strictEqual(parsed.covered, 1, `D-01 in <action> prefix must be caught (per-tag capture). Got: ${JSON.stringify(parsed)}`);
|
||||
assert.strictEqual(parsed.passed, true);
|
||||
});
|
||||
|
||||
test('D-NN inside a non-scanned tag body does NOT count toward coverage', () => {
|
||||
writeContextFile(phaseDir, CONTEXT_WITH_D01);
|
||||
// <name> is not in the scanned set — citation here must not be picked up.
|
||||
writePlanFile(phaseDir, '01', '# Plan\n\n<name>per D-01</name>\n');
|
||||
|
||||
const contextPath = path.join(phaseDir, 'CONTEXT.md');
|
||||
const result = runDecisionCoveragePlan(phaseDir, contextPath, tmpDir);
|
||||
const parsed = JSON.parse(result.output || '{}');
|
||||
|
||||
assert.strictEqual(parsed.total, 1);
|
||||
assert.strictEqual(parsed.covered, 0, 'non-scanned tag body must not count. Got: ' + JSON.stringify(parsed));
|
||||
assert.strictEqual(parsed.passed, false);
|
||||
assert.strictEqual(parsed.uncovered.length, 1);
|
||||
});
|
||||
|
||||
test('self-closing scanned tag form is safely ignored (no body to scan)', () => {
|
||||
writeContextFile(phaseDir, CONTEXT_WITH_D01);
|
||||
// Self-closing form has no body. The gate must not crash and must not match the (absent) body.
|
||||
writePlanFile(phaseDir, '01', '# Plan\n\n<tasks>\n<task>\n<read_first />\n<action>Implement feature.</action>\n</task>\n</tasks>\n');
|
||||
|
||||
const contextPath = path.join(phaseDir, 'CONTEXT.md');
|
||||
const result = runDecisionCoveragePlan(phaseDir, contextPath, tmpDir);
|
||||
const parsed = JSON.parse(result.output || '{}');
|
||||
|
||||
assert.strictEqual(parsed.total, 1);
|
||||
assert.strictEqual(parsed.covered, 0);
|
||||
assert.strictEqual(parsed.passed, false);
|
||||
});
|
||||
|
||||
test('attribute form `<verify type="...">D-NN</verify>` is scanned (canonical planner shape)', () => {
|
||||
writeContextFile(phaseDir, CONTEXT_WITH_D01);
|
||||
writePlanFile(phaseDir, '01', '# Plan\n\n<tasks>\n<task>\n<verify type="automated">Run npm test per D-01</verify>\n</task>\n</tasks>\n');
|
||||
|
||||
const contextPath = path.join(phaseDir, 'CONTEXT.md');
|
||||
const result = runDecisionCoveragePlan(phaseDir, contextPath, tmpDir);
|
||||
const parsed = JSON.parse(result.output || '{}');
|
||||
|
||||
assert.strictEqual(parsed.covered, 1, 'attribute form on scanned tag must match. Got: ' + JSON.stringify(parsed));
|
||||
assert.strictEqual(parsed.passed, true);
|
||||
});
|
||||
|
||||
test('CRLF newlines inside scanned tag body do not break capture', () => {
|
||||
writeContextFile(phaseDir, CONTEXT_WITH_D01);
|
||||
const planBody = ['# Plan', '', '<tasks>', '<task>', ' <action>', ' Implement per D-01.', ' </action>', '</task>', '</tasks>', ''].join('\r\n');
|
||||
fs.writeFileSync(path.join(phaseDir, '01-PLAN.md'), planBody);
|
||||
|
||||
const contextPath = path.join(phaseDir, 'CONTEXT.md');
|
||||
const result = runDecisionCoveragePlan(phaseDir, contextPath, tmpDir);
|
||||
const parsed = JSON.parse(result.output || '{}');
|
||||
|
||||
assert.strictEqual(parsed.covered, 1, 'CRLF body must not break capture. Got: ' + JSON.stringify(parsed));
|
||||
assert.strictEqual(parsed.passed, true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"agents/gsd-pattern-mapper.md": "e62ee90d39084802",
|
||||
"agents/gsd-phase-researcher.md": "cff1196c8e8bb4fa",
|
||||
"agents/gsd-plan-checker.md": "dd1e7cdc837d8f3e",
|
||||
"agents/gsd-planner.md": "6a442501f9d4caa8",
|
||||
"agents/gsd-planner.md": "e2ad6fa2952fe88f",
|
||||
"agents/gsd-project-researcher.md": "85de7f562872ee9b",
|
||||
"agents/gsd-research-synthesizer.md": "18a2e1b30ff7ae3a",
|
||||
"agents/gsd-roadmapper.md": "7a8465ac6d4dd29e",
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"agents/gsd-pattern-mapper.md": "43c6021cf7caabfa",
|
||||
"agents/gsd-phase-researcher.md": "f1f6fd6a3e67c7a8",
|
||||
"agents/gsd-plan-checker.md": "bf1a4e636f2390de",
|
||||
"agents/gsd-planner.md": "55cc953c6a52ae05",
|
||||
"agents/gsd-planner.md": "86f6cfd300125965",
|
||||
"agents/gsd-project-researcher.md": "4531b7cc8f5e5f7d",
|
||||
"agents/gsd-research-synthesizer.md": "4a4f68e6c75b133a",
|
||||
"agents/gsd-roadmapper.md": "bb2f57695dbab32c",
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"agents/gsd-pattern-mapper.md": "b45b5e106775bec1",
|
||||
"agents/gsd-phase-researcher.md": "4772d9eada32e8bd",
|
||||
"agents/gsd-plan-checker.md": "75851b147f35354a",
|
||||
"agents/gsd-planner.md": "4636f48b1b7ca1f6",
|
||||
"agents/gsd-planner.md": "44a04bdebe044114",
|
||||
"agents/gsd-project-researcher.md": "d7f355894519f9fe",
|
||||
"agents/gsd-research-synthesizer.md": "1c738df9932d325a",
|
||||
"agents/gsd-roadmapper.md": "453e9471ad27c7ea",
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"agents/gsd-pattern-mapper.md": "b45b5e106775bec1",
|
||||
"agents/gsd-phase-researcher.md": "85217c69c1ed2ac6",
|
||||
"agents/gsd-plan-checker.md": "c70134c61b969589",
|
||||
"agents/gsd-planner.md": "3098091aa1f0756d",
|
||||
"agents/gsd-planner.md": "0ec11dd0d673f828",
|
||||
"agents/gsd-project-researcher.md": "f468e96f8339d1e0",
|
||||
"agents/gsd-research-synthesizer.md": "7be02e47f4fd901b",
|
||||
"agents/gsd-roadmapper.md": "8a7f1f1256a6aed5",
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
"agents/gsd-pattern-mapper.md": "b526065fd2efa19c",
|
||||
"agents/gsd-phase-researcher.md": "c507db2ba66038f4",
|
||||
"agents/gsd-plan-checker.md": "a609245dbdc4ef2b",
|
||||
"agents/gsd-planner.md": "c2ab476257cd07cf",
|
||||
"agents/gsd-planner.md": "6f390cd0f07473a7",
|
||||
"agents/gsd-project-researcher.md": "049f816c6caa4316",
|
||||
"agents/gsd-research-synthesizer.md": "2f7dcbff50371d4c",
|
||||
"agents/gsd-roadmapper.md": "bbb23d3097911516",
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"agents/gsd-pattern-mapper.md": "92cfa2e6c2a06bf3",
|
||||
"agents/gsd-phase-researcher.md": "6338474da1a5d65e",
|
||||
"agents/gsd-plan-checker.md": "e704c083b02e8c35",
|
||||
"agents/gsd-planner.md": "8e3e314f962711cd",
|
||||
"agents/gsd-planner.md": "50f9d7d12d8268af",
|
||||
"agents/gsd-project-researcher.md": "e43c59f7f1f2f37a",
|
||||
"agents/gsd-research-synthesizer.md": "87955470c3c129b2",
|
||||
"agents/gsd-roadmapper.md": "20b69eff61a7a9fa",
|
||||
|
||||
@@ -120,8 +120,8 @@
|
||||
"agents/gsd-phase-researcher.toml": "44a3d510cd0ce3bd",
|
||||
"agents/gsd-plan-checker.md": "e7f02c10ea788aee",
|
||||
"agents/gsd-plan-checker.toml": "6f8ceb421d0ad721",
|
||||
"agents/gsd-planner.md": "cb3ff2ef2c58617c",
|
||||
"agents/gsd-planner.toml": "ac51638704036f71",
|
||||
"agents/gsd-planner.md": "19969475e4e38175",
|
||||
"agents/gsd-planner.toml": "f5f4f3876767d323",
|
||||
"agents/gsd-project-researcher.md": "959f2e57c3d69ed8",
|
||||
"agents/gsd-project-researcher.toml": "f395e8e8c4baf1ed",
|
||||
"agents/gsd-research-synthesizer.md": "497f85adf53259ef",
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"agents/gsd-pattern-mapper.agent.md": "b1f488b0fa6a2395",
|
||||
"agents/gsd-phase-researcher.agent.md": "03cfb510a766fe93",
|
||||
"agents/gsd-plan-checker.agent.md": "c50a5b008ddcbfad",
|
||||
"agents/gsd-planner.agent.md": "8f5f400efea5e446",
|
||||
"agents/gsd-planner.agent.md": "1721f5ace5c95434",
|
||||
"agents/gsd-project-researcher.agent.md": "d73bdbe986ffa8a6",
|
||||
"agents/gsd-research-synthesizer.agent.md": "f03eed4aa89e47c5",
|
||||
"agents/gsd-roadmapper.agent.md": "322048cf8ddcb4e5",
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"agents/gsd-pattern-mapper.md": "1229c215677f740d",
|
||||
"agents/gsd-phase-researcher.md": "982d59921bed463d",
|
||||
"agents/gsd-plan-checker.md": "ba51999876d40cf2",
|
||||
"agents/gsd-planner.md": "c788d7fb84cf5433",
|
||||
"agents/gsd-planner.md": "71c6e41403d6bc63",
|
||||
"agents/gsd-project-researcher.md": "beeac940d3a10e76",
|
||||
"agents/gsd-research-synthesizer.md": "6315f016d55176f4",
|
||||
"agents/gsd-roadmapper.md": "d28e7d4bac46dde2",
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"agents/gsd-pattern-mapper.md": "cea092600aeb3978",
|
||||
"agents/gsd-phase-researcher.md": "2bd0402f33d757ca",
|
||||
"agents/gsd-plan-checker.md": "4b4e2b475bf5b5c3",
|
||||
"agents/gsd-planner.md": "d5fa8236f975aacd",
|
||||
"agents/gsd-planner.md": "5f5fa906f62643b6",
|
||||
"agents/gsd-project-researcher.md": "425a7df7f37a5c06",
|
||||
"agents/gsd-research-synthesizer.md": "9d31c87fc2c87ffa",
|
||||
"agents/gsd-roadmapper.md": "64dce5d5f9fa5654",
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"agents/gsd-pattern-mapper.md": "6a5408fd11d70391",
|
||||
"agents/gsd-phase-researcher.md": "94818f28c498bb26",
|
||||
"agents/gsd-plan-checker.md": "56164206242c8caf",
|
||||
"agents/gsd-planner.md": "13c4b6db72b6bbb3",
|
||||
"agents/gsd-planner.md": "282aa34e2e8bc267",
|
||||
"agents/gsd-project-researcher.md": "60573a38d3dfd9fe",
|
||||
"agents/gsd-research-synthesizer.md": "1f7cd286c5783c86",
|
||||
"agents/gsd-roadmapper.md": "277e0a3252553ab7",
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
"agents/subagents/gsd-phase-researcher.yaml": "7633c8e82617e7cc",
|
||||
"agents/subagents/gsd-plan-checker.md": "bd302afc01ed40f0",
|
||||
"agents/subagents/gsd-plan-checker.yaml": "8295181071121db8",
|
||||
"agents/subagents/gsd-planner.md": "d29376ff9f57cc60",
|
||||
"agents/subagents/gsd-planner.md": "285c6a21227787f9",
|
||||
"agents/subagents/gsd-planner.yaml": "2e83ee194bcd7fbd",
|
||||
"agents/subagents/gsd-project-researcher.md": "39bc2ec5a8b18283",
|
||||
"agents/subagents/gsd-project-researcher.yaml": "ce12586b0347e2dc",
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"agents/gsd-pattern-mapper.md": "7c6d1d9817a9c1e7",
|
||||
"agents/gsd-phase-researcher.md": "9874110700b41f48",
|
||||
"agents/gsd-plan-checker.md": "28ca3dc43669894f",
|
||||
"agents/gsd-planner.md": "87d1aa910eb02ad5",
|
||||
"agents/gsd-planner.md": "5234ac4f1afce5dc",
|
||||
"agents/gsd-project-researcher.md": "dae210ae0b3c6e2b",
|
||||
"agents/gsd-research-synthesizer.md": "e02c6ad5d1b74171",
|
||||
"agents/gsd-roadmapper.md": "1658a40b20d8b575",
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"agents/gsd-pattern-mapper.md": "83c66c7722e8b165",
|
||||
"agents/gsd-phase-researcher.md": "284e55a86ae46d7f",
|
||||
"agents/gsd-plan-checker.md": "c8a8fcc8ed38eff0",
|
||||
"agents/gsd-planner.md": "f0a5bede90f23be4",
|
||||
"agents/gsd-planner.md": "c38e99d7be01a728",
|
||||
"agents/gsd-project-researcher.md": "b5baac64a15c85e2",
|
||||
"agents/gsd-research-synthesizer.md": "6cd9b501dc97bd50",
|
||||
"agents/gsd-roadmapper.md": "c357a77ab919e9e5",
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"agents/gsd-pattern-mapper.md": "b5d7a4abb1baecb9",
|
||||
"agents/gsd-phase-researcher.md": "2256f1f82212c757",
|
||||
"agents/gsd-plan-checker.md": "523119bd5d6599fe",
|
||||
"agents/gsd-planner.md": "fb330c599b9c1dac",
|
||||
"agents/gsd-planner.md": "ddfd810f17d39eb6",
|
||||
"agents/gsd-project-researcher.md": "ddf7794e81300032",
|
||||
"agents/gsd-research-synthesizer.md": "a124b00271748d07",
|
||||
"agents/gsd-roadmapper.md": "493ef92b42b12cf4",
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"agents/gsd-pattern-mapper.md": "ada0c169daa2f0ec",
|
||||
"agents/gsd-phase-researcher.md": "2a45ebde829555ec",
|
||||
"agents/gsd-plan-checker.md": "33fbf70b7b24eb1e",
|
||||
"agents/gsd-planner.md": "61ac0bd59b5b836a",
|
||||
"agents/gsd-planner.md": "f98cfbc92bfff334",
|
||||
"agents/gsd-project-researcher.md": "f6697b316b5995ba",
|
||||
"agents/gsd-research-synthesizer.md": "04036f38c1d373ea",
|
||||
"agents/gsd-roadmapper.md": "fb62e1e3de84b5f9",
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"agents/gsd-pattern-mapper.md": "68ecefd60811a669",
|
||||
"agents/gsd-phase-researcher.md": "2235f61764d8e969",
|
||||
"agents/gsd-plan-checker.md": "bb38f345d3d41edc",
|
||||
"agents/gsd-planner.md": "42f6e9832d5c2edc",
|
||||
"agents/gsd-planner.md": "1e904e38cccc5dc4",
|
||||
"agents/gsd-project-researcher.md": "f572892f138734ff",
|
||||
"agents/gsd-research-synthesizer.md": "29949bf3f049a8f1",
|
||||
"agents/gsd-roadmapper.md": "840ac933e3b094f9",
|
||||
|
||||
Reference in New Issue
Block a user