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

@@ -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,