* 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:
5
.changeset/witty-ravens-fly.md
Normal file
5
.changeset/witty-ravens-fly.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 4486
|
||||
---
|
||||
**W002 no longer fires on quoted commands in STATE.md** — the health check read GSD's own command names (like ``/gsd-execute-phase 5`` in a ledger row) and anything inside backticks as phase references, so healthy multi-workstream projects reported degraded with false warnings; under an active workstream the warning now also says its declared-phase list is workstream-scoped (`... are declared in workstream <name>`) instead of making an unqualified project-wide claim. (#4257)
|
||||
@@ -175,6 +175,17 @@ const RULE_W002: Rule = {
|
||||
);
|
||||
|
||||
const diagnostics: Diagnostic[] = [];
|
||||
// #4257: the valid set above is WORKSTREAM-scoped by construction (every
|
||||
// source field is read from `planningPaths(cwd)`'s base, which resolves
|
||||
// under `.planning/workstreams/<active-ws>/`), and per-workstream phase
|
||||
// numbering is deliberate — so under a workstream the message must NAME
|
||||
// that scope rather than make an unqualified project-wide claim (a
|
||||
// reference to a phase declared only in a SIBLING workstream reads as
|
||||
// "undeclared" against this list; that is the scope speaking, not drift).
|
||||
// Root scope (`workstream === null`, flat or root-planning projects)
|
||||
// keeps the byte-identical message — no clause is appended when none
|
||||
// applies.
|
||||
const scopeClause = snapshot.workstream ? ` in workstream ${snapshot.workstream}` : '';
|
||||
for (const ref of snapshot.statePhaseTokens.value) {
|
||||
const dotIdx = ref.indexOf('.');
|
||||
const head = dotIdx === -1 ? ref : ref.slice(0, dotIdx);
|
||||
@@ -184,7 +195,7 @@ const RULE_W002: Rule = {
|
||||
diagnostics.push({
|
||||
code: 'W002',
|
||||
severity: SEVERITY.WARNING,
|
||||
message: `STATE.md references phase ${ref}, but only phases ${sortedValid.join(', ')} are declared`,
|
||||
message: `STATE.md references phase ${ref}, but only phases ${sortedValid.join(', ')} are declared${scopeClause}`,
|
||||
remedy: adviseRemedy(
|
||||
'Review STATE.md manually before changing it; /gsd-health --repair will not overwrite an existing STATE.md for phase mismatches',
|
||||
),
|
||||
|
||||
@@ -38,7 +38,15 @@ import planningWorkspace = require('./planning-workspace.cjs');
|
||||
// `phase_id_convention` reader, from the same §7 owner module `planningPaths`
|
||||
// comes from. Resolved once in `buildPlanningSnapshot` — see the
|
||||
// `phaseIdConvention` field's comment for why one resolution point matters.
|
||||
const { planningPaths, planningRoot, resolvePhaseIdConvention } = planningWorkspace;
|
||||
// #4257: `resolveEnvWorkstream` is that same module's ONE owner of the env
|
||||
// workstream discriminator `planningDir` applies — the name W002's scope
|
||||
// clause prints comes from the same resolution point that scoped the reads.
|
||||
const { planningPaths, planningRoot, resolvePhaseIdConvention, resolveEnvWorkstream } = planningWorkspace;
|
||||
// #4257: canonical CommonMark code strippers (markdown-sectionizer is the
|
||||
// repo's T0 structural seam, adopted per the #2365 composition order — fenced
|
||||
// blocks first, then inline spans) so the `statePhaseTokens` harvest sees
|
||||
// PROSE, not quoted literals.
|
||||
import { stripFencedCode, stripInlineCode } from './markdown-sectionizer.cjs';
|
||||
import { platformReadSync, execGit } from './shell-command-projection.cjs';
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
import frontmatterMod = require('./frontmatter.cjs');
|
||||
@@ -278,6 +286,18 @@ interface PlanningSnapshot {
|
||||
// for it at all, so a check that can fail a repo runs only for the convention
|
||||
// that repo opted into.
|
||||
roadmapBracketIncoherences: { value: BracketIncoherence[]; scope: Scope };
|
||||
// ─── #4257 addition ────────────────────────────────────────────────────────
|
||||
// The name of the workstream every workstream-aware read in THIS snapshot
|
||||
// was scoped to, `null` on a flat/root-scope project. Derived from
|
||||
// `resolveEnvWorkstream()` — the ONE env discriminator `planningDir`
|
||||
// itself applies when `planningPaths(cwd)` resolves the base — so the
|
||||
// field cannot disagree with the base the fields were actually read from
|
||||
// (one resolution point; the #612 PR-2 two-readers-two-bases lesson).
|
||||
// Additive-only in the `archivedPhaseTokens` (#3652) shape; backs W002's
|
||||
// scope clause (`... are declared in workstream <name>`), which names the
|
||||
// scope because per-workstream phase numbering is deliberate and a
|
||||
// cross-workstream "declared" union would silence genuine drift.
|
||||
workstream: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -346,9 +366,11 @@ interface StateFields {
|
||||
* a whole-body fallback, together.
|
||||
* - `statePhaseTokens` scans the WHOLE document (`verify.cts`'s exact
|
||||
* `PHASE_NUMBER_TOKEN_SOURCE` regex, relocated verbatim from
|
||||
* `verify.cts:1731-1735`), not just the Current Position section, so it is
|
||||
* NOT degraded to `TRUNCATED` by a missing section header — it stays
|
||||
* `COMPLETE` whenever the file itself was read successfully.
|
||||
* `verify.cts:1731-1735`; #4257 adds the left word boundary and the
|
||||
* fenced-block/inline-span strip — see the harvest site's comment), not
|
||||
* just the Current Position section, so it is NOT degraded to `TRUNCATED`
|
||||
* by a missing section header — it stays `COMPLETE` whenever the file
|
||||
* itself was read successfully.
|
||||
*/
|
||||
function buildStateFields(statePath: string): StateFields {
|
||||
let content: string | null;
|
||||
@@ -411,9 +433,32 @@ function buildStateFields(statePath: string): StateFields {
|
||||
scope: currentPositionScope,
|
||||
});
|
||||
const statePhaseTokens = {
|
||||
value: [...content.matchAll(new RegExp(`[Pp]hase\\s+(${PHASE_NUMBER_TOKEN_SOURCE})`, 'g'))].map(
|
||||
(m) => m[1],
|
||||
),
|
||||
// #4257: harvest PROSE phase references, not every literal token match.
|
||||
// Two precisions over the pre-#4257 verbatim relocation of verify.cts's
|
||||
// scan (which was `[Pp]hase\s+(TOKEN)`, unanchored, over the raw file):
|
||||
//
|
||||
// 1. Strip fenced code blocks, then inline code spans (the #2365
|
||||
// composition order, via the canonical markdown-sectionizer seam) —
|
||||
// a token inside backticks is a QUOTED LITERAL (a ledger row quoting
|
||||
// `` `/gsd-execute-phase 5` `` or `` `- [ ] **Phase 40:` `` from a
|
||||
// sibling roadmap), not a reference. Pinned tradeoff: a GENUINE
|
||||
// reference written in backticks stops counting too — a quoted
|
||||
// literal and a reference are indistinguishable inside a code span.
|
||||
// 2. Left word boundary `(?<![-\w])` — the `-phase 5` tail of GSD's own
|
||||
// command names (`/gsd-execute-phase 5`, bare or in prose) is a
|
||||
// command mention, not a reference, and word-suffixed carriers
|
||||
// (`myphase 5`) never were references. `Phase 5` at line start,
|
||||
// `**Phase 5:**`, `(Phase 5)`, `[Phase 5]`, and `### Phase 5:` all
|
||||
// still harvest — the char before `Phase` is not in `[-\w]`.
|
||||
//
|
||||
// Still scans the WHOLE document (frontmatter included — the `Phase: 3`
|
||||
// field syntax never matched, `phase` is followed by a colon, not `\s`),
|
||||
// still `COMPLETE` whenever the file itself was read successfully.
|
||||
value: [
|
||||
...stripInlineCode(stripFencedCode(content).text).matchAll(
|
||||
new RegExp(`(?<![-\\w])[Pp]hase\\s+(${PHASE_NUMBER_TOKEN_SOURCE})`, 'g'),
|
||||
),
|
||||
].map((m) => m[1]),
|
||||
scope: SCOPE.COMPLETE,
|
||||
};
|
||||
|
||||
@@ -1207,6 +1252,11 @@ function buildPlanningSnapshot(cwd: string): PlanningSnapshot {
|
||||
// See the `phaseIdConvention` field's comment for why one resolution point is
|
||||
// load-bearing rather than a micro-optimisation.
|
||||
const phaseIdConvention = resolvePhaseIdConvention(cwd) ?? null;
|
||||
// #4257: the workstream `planningPaths(cwd)` just scoped every read to
|
||||
// (its `planningDir` call applies this exact discriminator when handed no
|
||||
// `ws`), resolved through the same owner so W002's scope clause names the
|
||||
// scope the valid set was ACTUALLY built from.
|
||||
const workstream = resolveEnvWorkstream();
|
||||
const milestone = getMilestoneInfo(cwd);
|
||||
// #612: deliberately LEFT to `listMilestonePhaseDirs`'s own lazy resolve —
|
||||
// this call is byte-identical to upstream's.
|
||||
@@ -1263,6 +1313,7 @@ function buildPlanningSnapshot(cwd: string): PlanningSnapshot {
|
||||
phaseIdConvention,
|
||||
roadmapSentinelPhaseTokens: roadmapDeclared.sentinelTokens,
|
||||
roadmapBracketIncoherences: buildRoadmapBracketIncoherencesField(paths.roadmap, phaseIdConvention),
|
||||
workstream,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -125,9 +125,26 @@ const PLANNING_LOCK_RETRY_ERRNOS = new Set([
|
||||
// compatible with the structural type the store expects.
|
||||
type WorkstreamAdapterOpts = Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* #4257: the ONE owner of the env workstream discriminator `planningDir`
|
||||
* itself applies when handed no `ws` argument. `planningPaths(cwd)` — and
|
||||
* therefore every workstream-scoped `PlanningSnapshot` read — resolves its
|
||||
* base through exactly this read, and the CLI bootstrap has already folded
|
||||
* the stored active-workstream pointer into the env by the time any
|
||||
* diagnostic runs (`resolveActiveWorkstream` → `applyResolvedWorkstreamEnv`,
|
||||
* `active-workstream-store.cjs`). Exposed so a consumer that needs to NAME
|
||||
* the scope those reads used (W002's warning message, via the snapshot's
|
||||
* `workstream` field) derives it from the same resolution point instead of
|
||||
* growing a second env read site that can drift (the #612 PR-2
|
||||
* two-readers-two-bases lesson).
|
||||
*/
|
||||
function resolveEnvWorkstream(): string | null {
|
||||
return process.env['GSD_WORKSTREAM'] ?? null;
|
||||
}
|
||||
|
||||
function planningDir(cwd: string, ws?: string | null, project?: string | null): string {
|
||||
if (project === undefined) project = process.env['GSD_PROJECT'] ?? null;
|
||||
if (ws === undefined) ws = process.env['GSD_WORKSTREAM'] ?? null;
|
||||
if (ws === undefined) ws = resolveEnvWorkstream();
|
||||
|
||||
// Reject path separators and traversal components in project/workstream names
|
||||
const BAD_SEGMENT = /[/\\]|\.\./;
|
||||
@@ -667,6 +684,7 @@ export = {
|
||||
createMemoryPointerAdapter,
|
||||
planningDir,
|
||||
planningRoot,
|
||||
resolveEnvWorkstream,
|
||||
resolvePhaseIdConvention,
|
||||
listAvailableWorkstreams,
|
||||
planningPaths,
|
||||
|
||||
@@ -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 ───────
|
||||
|
||||
@@ -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)', () => {
|
||||
|
||||
Reference in New Issue
Block a user