fix(#4257): harvest only prose phase references; W002 names its workstream scope (#4486)

* test(#4257): W002 harvest precision + workstream-scoped warning regression rows

Tests-only RED commit: A-rows pin the command-mention/code-span harvest
precision on statePhaseTokens, B-rows drive W002 under root and workstream
scope (scope clause asserted, root grammar byte-identical), C-rows pin the
additive workstream snapshot field. All fail on next; fix follows.

* fix(#4257): harvest only prose phase references; W002 names its workstream scope

Sub-defect (a): the statePhaseTokens harvest was the verbatim #3309
relocation of verify.cts's unanchored, markdown-blind scan
([Pp]hase\s+(TOKEN) over the raw file), so GSD's own command names
(/gsd-execute-phase 5, bare or quoted) and any token inside a code
span/fenced block were harvested as phase references and fired W002 on
ledger rows. Now strips fenced blocks then inline spans via the canonical
markdown-sectionizer seam (#2365 composition order) and matches with a
left word boundary (?<![-\w]) so hyphen- or word-suffixed carriers are
mentions, not references. Pinned tradeoff: a genuine reference written
in backticks stops counting (a quoted literal is not a reference).

Sub-defect (b): the valid set is workstream-scoped by construction
(planningPaths under GSD_WORKSTREAM; per-workstream numbering is
deliberate), but the message claimed 'only phases 1, 2 are declared'
unqualified. New additive PlanningSnapshot.workstream field, sourced
from planning-workspace's new resolveEnvWorkstream() — the ONE env
discriminator planningDir itself applies — so the clause cannot disagree
with the base the reads used. Root scope keeps the byte-identical
message; the checker's scope is unchanged.

* test(#4257): close the B2 quoted-literal code span (fixture typo)

The B2 fixture wrote a single opening backtick — an unterminated span is
literal text per CommonMark, so its content is prose and W002 correctly
fired on it. The test's name, the A3 snapshot-level twin, and the B2
matrix row all intend a closed span; pre-fix this was indistinguishable
because the unanchored harvest fired either way.

* chore(#4257): changeset fragment (pr number to backfill)

* chore(#4257): backfill PR number in changeset

---------

Co-authored-by: sim <sim@local>
This commit is contained in:
Tom Boucher
2026-09-07 14:31:35 -04:00
committed by GitHub
parent c3a18b5ba0
commit ac6ed6201d
6 changed files with 470 additions and 10 deletions

View File

@@ -152,7 +152,9 @@ describe('W002 — STATE.md references a phase not declared on disk or ROADMAP',
assert.equal(diagnostics.length, 1);
assert.equal(diagnostics[0].code, 'W002');
assert.equal(diagnostics[0].severity, SEVERITY.WARNING);
assert.match(diagnostics[0].message, /STATE\.md references phase 9, but only phases .* are declared/);
// #4257: root scope (no active workstream) keeps the BYTE-IDENTICAL
// message grammar — no scope clause is appended when none applies.
assert.match(diagnostics[0].message, /STATE\.md references phase 9, but only phases .* are declared$/);
assert.deepEqual(diagnostics[0].remedy, {
action: REMEDY_ACTION.ADVISE,
risk: REMEDY_RISK.NONE,
@@ -240,6 +242,137 @@ describe('W002 — STATE.md references a phase not declared on disk or ROADMAP',
const diagnostics = ruleFor('W002').check(snapshot);
assert.deepEqual(diagnostics, []);
});
// ─── #4257: command mentions are not phase references; the warning names ──
// ─── its workstream scope ─────────────────────────────────────────────────
//
// Fixture shape: the issue's own repro — active workstream `alpha` declares
// phases 1-2, sibling `beta` declares phase 5, and alpha's STATE.md carries
// a Queue/Ledger row mentioning the phase via a GSD command name or a quoted
// roadmap line. GSD_WORKSTREAM is set directly (save/restore per
// tests/health-diagnostic.test.cjs:598-602) — the same discriminator
// planningDir applies and the CLI bootstrap folds the stored pointer into.
function withWorkstreamEnv(t, name) {
const prev = process.env['GSD_WORKSTREAM'];
if (name === null) delete process.env['GSD_WORKSTREAM'];
else process.env['GSD_WORKSTREAM'] = name;
t.after(() => {
if (prev === undefined) delete process.env['GSD_WORKSTREAM'];
else process.env['GSD_WORKSTREAM'] = prev;
});
}
function makeTwoWorkstreamFixture(cwd, stateQueueLine) {
writeFile(cwd, '.planning/config.json', '{}');
writeFile(
cwd,
'.planning/workstreams/alpha/ROADMAP.md',
'## v1.0 Current 🚧\n\n### Phase 1: One\n\n### Phase 2: Two\n',
);
writeFile(
cwd,
'.planning/workstreams/beta/ROADMAP.md',
'## v1.0 Current 🚧\n\n### Phase 5: Five\n',
);
writeFile(
cwd,
'.planning/workstreams/alpha/STATE.md',
[
'---',
'status: planning',
'---',
'',
'## Current Position',
'',
'Phase: 1 of 2 (One)',
'Status: planning',
'',
'## Queue',
'',
`- ${stateQueueLine}`,
'',
].join('\n'),
);
}
test('#4257 row 1 (regression): `/gsd-execute-phase 5` in a Queue row is a command mention, not a phase reference — no W002 even though 5 is only declared in a sibling workstream', (t) => {
const cwd = createTempDir('gsd-4257-w002-1-');
t.after(() => cleanup(cwd));
makeTwoWorkstreamFixture(cwd, '`/gsd-execute-phase 5`');
withWorkstreamEnv(t, 'alpha');
const snapshot = buildPlanningSnapshot(cwd);
assert.deepEqual(ruleFor('W002').check(snapshot), []);
});
test('#4257: a quoted roadmap line `- [ ] **Phase 40:**` in a code span is a quoted literal, not a reference — no W002', (t) => {
const cwd = createTempDir('gsd-4257-w002-2-');
t.after(() => cleanup(cwd));
writeFile(cwd, '.planning/config.json', '{}');
writeFile(
cwd,
'.planning/workstreams/alpha/ROADMAP.md',
'## v1.0 Current 🚧\n\n### Phase 1: One\n\n### Phase 2: Two\n',
);
writeFile(
cwd,
'.planning/workstreams/beta/ROADMAP.md',
'## v1.0 Current 🚧\n\n### Phase 40: Forty\n',
);
writeFile(
cwd,
'.planning/workstreams/alpha/STATE.md',
[
'---',
'status: planning',
'---',
'',
'## Ledger',
'',
// Closed code span (matching the A3 snapshot-level twin and the
// 50-test-matrix.md B2 row). An UNTERMINATED backtick run is literal
// text per CommonMark, so its content is prose and W002 SHOULD fire
// on it — not the fixture this row pins.
'- `- [ ] **Phase 40:** quoted from the beta roadmap`',
'',
].join('\n'),
);
withWorkstreamEnv(t, 'alpha');
const snapshot = buildPlanningSnapshot(cwd);
assert.deepEqual(ruleFor('W002').check(snapshot), []);
});
test('#4257: a GENUINE undeclared prose reference still warns under a workstream, and the warning names its scope', (t) => {
const cwd = createTempDir('gsd-4257-w002-3-');
t.after(() => cleanup(cwd));
makeTwoWorkstreamFixture(cwd, 'Phase 5 wrap-up blocked on beta');
withWorkstreamEnv(t, 'alpha');
const snapshot = buildPlanningSnapshot(cwd);
const diagnostics = ruleFor('W002').check(snapshot);
assert.equal(diagnostics.length, 1);
assert.equal(diagnostics[0].code, 'W002');
// The valid set is workstream-scoped BY DESIGN (planningPaths under
// GSD_WORKSTREAM); the message must say so — phase 5 IS declared, in
// sibling `beta`, and the unqualified form read as a false project-wide
// claim (#4257 sub-defect 2).
assert.equal(
diagnostics[0].message,
'STATE.md references phase 5, but only phases 1, 2 are declared in workstream alpha',
);
});
test('#4257: a command mention naming a DECLARED phase stays silent (no false negative introduced either)', (t) => {
const cwd = createTempDir('gsd-4257-w002-4-');
t.after(() => cleanup(cwd));
makeTwoWorkstreamFixture(cwd, '`/gsd-execute-phase 2`');
withWorkstreamEnv(t, 'alpha');
const snapshot = buildPlanningSnapshot(cwd);
assert.deepEqual(ruleFor('W002').check(snapshot), []);
});
});
// ─── W011 — STATE current-phase status vs. ROADMAP checkbox disagree ───────

View File

@@ -813,6 +813,248 @@ describe('statePhaseTokens field (Phase 11, #3309)', () => {
assert.deepStrictEqual(snap.currentPhaseLabel, { value: null, scope: SCOPE.UNREADABLE });
assert.strictEqual(emitted, 1, 'the shared STATE.md read must not double-emit across fields');
});
// ─── #4257: harvest precision — a command MENTION is not a phase REFERENCE ──
//
// The pre-#4257 scan (`[Pp]hase\s+(TOKEN)`, unanchored, over the raw file)
// harvested the `-phase 5` tail of GSD's own command names and any token
// inside an inline code span / fenced block, so a ledger row quoting
// `/gsd-execute-phase 5` fired W002 as an undeclared-phase reference. The
// #4257 grammar: strip fenced blocks, then inline spans (the canonical
// markdown-sectionizer seam + composition order, #2365), THEN match with a
// left word boundary `(?<![-\w])` so a hyphen- or word-suffixed carrier
// (`execute-phase`, `myphase`) is not a reference while `Phase 5`,
// `**Phase 5:**`, `(Phase 5)` still are.
test('#4257 row 1 (regression): `/gsd-execute-phase 5` in a Queue ledger row inside an inline code span is NOT harvested', (t) => {
const cwd = createTempDir('gsd-4257-spt1-');
t.after(() => cleanup(cwd));
writeState(cwd, { milestone: 'v1.0' });
appendToState(cwd, [
'',
'## Current Position',
'',
'Phase: 1 of 2',
'',
'## Queue',
'',
'- `/gsd-execute-phase 5`',
'',
].join('\n'));
const snap = buildPlanningSnapshot(cwd);
assert.deepStrictEqual(snap.statePhaseTokens, { value: [], scope: SCOPE.COMPLETE });
});
test('#4257: a bare `/gsd-execute-phase 5` command mention (no backticks) is NOT harvested — left word boundary', (t) => {
const cwd = createTempDir('gsd-4257-spt2-');
t.after(() => cleanup(cwd));
writeState(cwd, { milestone: 'v1.0' });
appendToState(cwd, [
'',
'## Queue',
'',
'- Run /gsd-execute-phase 5 when the ledger clears',
'',
].join('\n'));
const snap = buildPlanningSnapshot(cwd);
assert.deepStrictEqual(snap.statePhaseTokens, { value: [], scope: SCOPE.COMPLETE });
});
test('#4257: a quoted roadmap line `- [ ] **Phase 40:**` inside an inline code span is NOT harvested', (t) => {
const cwd = createTempDir('gsd-4257-spt3-');
t.after(() => cleanup(cwd));
writeState(cwd, { milestone: 'v1.0' });
appendToState(cwd, [
'',
'## Ledger',
'',
'- `- [ ] **Phase 40:** quoted from a sibling workstream roadmap`',
'',
].join('\n'));
const snap = buildPlanningSnapshot(cwd);
assert.deepStrictEqual(snap.statePhaseTokens, { value: [], scope: SCOPE.COMPLETE });
});
test('#4257: a `Phase 9` mention inside a fenced code block is NOT harvested', (t) => {
const cwd = createTempDir('gsd-4257-spt4-');
t.after(() => cleanup(cwd));
writeState(cwd, { milestone: 'v1.0' });
appendToState(cwd, [
'',
'## Notes',
'',
'```',
'Phase 9 was quoted here verbatim',
'```',
'',
].join('\n'));
const snap = buildPlanningSnapshot(cwd);
assert.deepStrictEqual(snap.statePhaseTokens, { value: [], scope: SCOPE.COMPLETE });
});
test('#4257 tradeoff (pinned by the issue): a GENUINE reference written in backticks stops counting', (t) => {
const cwd = createTempDir('gsd-4257-spt5-');
t.after(() => cleanup(cwd));
writeState(cwd, { milestone: 'v1.0' });
appendToState(cwd, [
'',
'## Decisions',
'',
'- see `Phase 5` notes for the rationale',
'',
].join('\n'));
const snap = buildPlanningSnapshot(cwd);
assert.deepStrictEqual(snap.statePhaseTokens, { value: [], scope: SCOPE.COMPLETE });
});
test('#4257: word-suffixed carriers (myphase 5, alphaphase 3) are NOT harvested — boundary covers non-command suffixes too', (t) => {
const cwd = createTempDir('gsd-4257-spt6-');
t.after(() => cleanup(cwd));
writeState(cwd, { milestone: 'v1.0' });
appendToState(cwd, [
'',
'## Decisions',
'',
'- myphase 5 and alphaphase 3 are words, not references',
'',
].join('\n'));
const snap = buildPlanningSnapshot(cwd);
assert.deepStrictEqual(snap.statePhaseTokens, { value: [], scope: SCOPE.COMPLETE });
});
test('#4257 mixed form: prose reference survives while command mention and quoted literal are dropped from the same file', (t) => {
const cwd = createTempDir('gsd-4257-spt7-');
t.after(() => cleanup(cwd));
writeState(cwd, { milestone: 'v1.0' });
appendToState(cwd, [
'',
'## Current Position',
'',
'Phase: 1 of 2',
'',
'## Queue',
'',
'- `/gsd-execute-phase 9` once Phase 5 wraps (see `Phase 12` notes)',
'',
].join('\n'));
const snap = buildPlanningSnapshot(cwd);
assert.deepStrictEqual(snap.statePhaseTokens, { value: ['5'], scope: SCOPE.COMPLETE });
});
// KEEP rows — the #4257 boundary must NOT narrow legitimate references.
test('#4257 keep: bold `**Phase 5:**` and parenthesised `(Phase 5)` and heading `### Phase 5:` forms still harvest', (t) => {
const cwd = createTempDir('gsd-4257-spt8-');
t.after(() => cleanup(cwd));
writeState(cwd, { milestone: 'v1.0' });
appendToState(cwd, [
'',
'### Phase 5: Five',
'',
'- **Phase 5:** started',
'- (Phase 5) pending review',
'',
].join('\n'));
const snap = buildPlanningSnapshot(cwd);
assert.deepStrictEqual(snap.statePhaseTokens, { value: ['5', '5', '5'], scope: SCOPE.COMPLETE });
});
test('#4257 keep: silent forms stay silent (Phase5, flag form, non-English word)', (t) => {
const cwd = createTempDir('gsd-4257-spt9-');
t.after(() => cleanup(cwd));
writeState(cwd, { milestone: 'v1.0' });
appendToState(cwd, [
'',
'## Queue',
'',
'- `gsd-tools phase --insert 5`',
'- `Phase5` shorthand does not count',
'- фаза 5 is not the token either',
'',
].join('\n'));
const snap = buildPlanningSnapshot(cwd);
assert.deepStrictEqual(snap.statePhaseTokens, { value: [], scope: SCOPE.COMPLETE });
});
});
// ─── workstream field (#4257) ────────────────────────────────────────────────
describe('workstream field (#4257)', () => {
// The env save/restore pattern mirrors tests/health-diagnostic.test.cjs:598-602
// and tests/config-loader.test.cjs:499-509 — GSD_WORKSTREAM is the SAME
// discriminator planningDir applies (planning-workspace.cts:130), and the
// CLI bootstrap folds the stored active-workstream pointer into it
// (active-workstream-store.cts:488-494), so setting it directly is the
// faithful rule-level simulation of "workstream alpha is active".
function withWorkstreamEnv(t, name) {
const prev = process.env['GSD_WORKSTREAM'];
if (name === null) delete process.env['GSD_WORKSTREAM'];
else process.env['GSD_WORKSTREAM'] = name;
t.after(() => {
if (prev === undefined) delete process.env['GSD_WORKSTREAM'];
else process.env['GSD_WORKSTREAM'] = prev;
});
}
test('flat project (no GSD_WORKSTREAM): workstream is null', (t) => {
const cwd = createTempDir('gsd-4257-ws1-');
t.after(() => cleanup(cwd));
writeState(cwd, { milestone: 'v1.0' });
withWorkstreamEnv(t, null);
const snap = buildPlanningSnapshot(cwd);
assert.strictEqual(snap.workstream, null);
});
test('GSD_WORKSTREAM=alpha: workstream names the scope every workstream-aware read used', (t) => {
const cwd = createTempDir('gsd-4257-ws2-');
t.after(() => cleanup(cwd));
writeFile(cwd, '.planning/workstreams/alpha/STATE.md', [
'---',
'milestone: v1.0',
'---',
'',
'## Decisions',
'',
'- Phase 2 wrapped',
'',
].join('\n'));
withWorkstreamEnv(t, 'alpha');
const snap = buildPlanningSnapshot(cwd);
assert.strictEqual(snap.workstream, 'alpha');
});
test('non-divergence: the field names the base statePhaseTokens was actually read from', (t) => {
const cwd = createTempDir('gsd-4257-ws3-');
t.after(() => cleanup(cwd));
// Root STATE.md carries Phase 7; workstream STATE.md carries Phase 5. The
// tokens must come from the WORKSTREAM file (planningPaths scoped it) and
// the field must name that same workstream — one resolution point, two
// observable answers that cannot disagree.
writeState(cwd, { milestone: 'v1.0' });
appendToState(cwd, ['', '- Phase 7 lives at root scope', ''].join('\n'));
writeFile(cwd, '.planning/workstreams/alpha/STATE.md', [
'---',
'milestone: v1.0',
'---',
'',
'- Phase 5 lives in the workstream',
'',
].join('\n'));
withWorkstreamEnv(t, 'alpha');
const snap = buildPlanningSnapshot(cwd);
assert.deepStrictEqual(snap.statePhaseTokens, { value: ['5'], scope: SCOPE.COMPLETE });
assert.strictEqual(snap.workstream, 'alpha');
});
});
describe('stateStatus field (Phase 11, #3309)', () => {