Files
msd-core/tests/adr-612-bracket-heading-selection.test.cjs
BeeHiggs 0967358b8b enhance(#3638): render bracket phase IDs on progress, stats, manager and statusline surfaces (epic #612 PR-5) (#4111)
* enhance(#3638): render bracket IDs on display surfaces

Gate progress, stats, manager, and statusline projections on the bracket convention; validate phase_id_convention and single-source the convention card.

Forward note: the uat.cts bracket co-change remains deliberately deferred to its owning slice.

* chore(#3638): point the changeset at PR #4111

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(#3638): close bracket display review gaps

* docs(#3638): register phase display modules

* chore(#3638): re-trigger CI after macOS shard SIGTERM

`full test (macos-latest, 24, shard 3/3)` failed on 20ce98cd1 in
`tests/lint-compiled-artifact-sync.test.cjs` — the spawned
`scripts/lint-compiled-artifact-sync.cjs` was killed at 60024ms
(`exited null (signal SIGTERM)`, stdout and stderr both empty), 24ms past
the test's own `TSC_COMPILE_TIMEOUT_MS`. That is the failure mode the
constant's comment already documents ("under CI shard load that compile
can exceed the budget, dying to a SIGTERM with empty piped stdout").

No content change; this empty commit exists only to re-run the matrix,
since re-running a job needs write access on the upstream repository.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
2026-09-15 04:47:47 -04:00

728 lines
37 KiB
JavaScript

'use strict';
/**
* PR-2 (#2761 / epic #612) — convention-GATED heading-intro selection.
*
* The design this file pins, and why it replaced the previous one:
*
* PR-2 first widened every heading reader unconditionally, resting on the claim
* that the newly-admitted shape — a `[CODE.MM]` bracket followed by a digit —
* "cannot occur in a legacy ROADMAP". That claim is false. `### [RFC.2119] 5:`,
* `### [v1.0] 2024:`, `### [ADR.612] 3:`, `### [SPEC.1] 3:` and
* `### [ISO.8601] 2026:` are all ordinary headings a project that never heard of
* this convention can contain, and every one of them was claimed as a phase:
* `phase_count` and `total_phases` moved, and `validate health` grew W006s, on
* repos that never opted in.
*
* No amount of narrowing rescues an ungated widening, because the argument it
* needs — "no legacy document contains this" — is unprovable about documents we
* do not control. So the widening is now SELECTED, not argued: a repo whose
* resolved `phase_id_convention` is not 'bracket' compiles the same source
* string it compiled before, and the question of what that string does or does
* not match never arises.
*
* THE STRUCTURAL TEST is the load-bearing assertion here. It carries its own
* transcription of each call site's base spelling — copied from
* `git show d04592de:src/<file>.cts` — and asserts byte-equality against what
* the selector returns. It deliberately does NOT compare the selector against a
* constant the selector itself is built from: that would restate the
* implementation and pass no matter what either side said. Byte-equality with an
* independently transcribed literal is the whole proof, and it needs no corpus.
*/
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const core = require('../gsd-core/bin/lib/phase-id.cjs');
const B = core.PHASE_HEADING_BASELINE;
// ─── Base spellings, transcribed by hand from d04592de ─────────────────────
// One entry per call site PR-2 converts. `src` is what that site's regex
// contains on the base commit, character for character. If a rebase moves a
// site's base spelling, this table is what fails.
const BASE_SITES = [
// --- baseline: the site already tolerates `[anything] Phase N`
{ file: 'roadmap.cts', site: 'searchPhaseInContent headingPattern',
baseline: B.ANY_BRACKET, src: '(?:\\[[^\\]]{1,200}\\]\\s*)?Phase\\s+' },
{ file: 'roadmap.cts', site: 'cmdRoadmapAnalyze phasePattern',
baseline: B.ANY_BRACKET, src: '(?:\\[[^\\]]{1,200}\\]\\s*)?Phase\\s+' },
{ file: 'roadmap.cts', site: 'cmdRoadmapAnalyze nextHeader',
baseline: B.ANY_BRACKET, src: '(?:\\[[^\\]]{1,200}\\]\\s*)?Phase\\s+' },
{ file: 'validate.cts', site: 'buildRoadmapPhaseVariants phasePattern',
baseline: B.ANY_BRACKET, src: '(?:\\[[^\\]]{1,200}\\]\\s*)?Phase\\s+' },
{ file: 'roadmap-parser.cts', site: 'getMilestonePhaseFilter phaseHeadingPattern',
baseline: B.ANY_BRACKET, src: '(?:\\[[^\\]]{1,200}\\]\\s*)?Phase\\s+' },
{ file: 'commands.cts', site: 'cmdStats headingPattern',
baseline: B.ANY_BRACKET, src: '(?:\\[[^\\]]{1,200}\\]\\s*)?Phase\\s+' },
// #2761 B2: BRACKET_PHASE_TAIL_RE (isBracketMilestoneBoundary's phase-tail
// discriminator) always passes the literal 'bracket' convention — it is not
// itself convention-gated (the CALLER, isBracketMilestoneBoundary, is only
// ever consulted when bracketBoundaryActive is already true) — but it still
// shares the SAME ANY_BRACKET baseline and produces the identical BASE
// source on a non-bracket convention as every other ANY_BRACKET site, so it
// is pinned here for count-exactness rather than left as an unpinned hole.
//
// #2761 round-3 Minor 1: `runtimeGated: true` below is the load-bearing
// fact this row's STRUCTURAL IDENTITY assertion (the loop just below)
// cannot see — that assertion is a property of `phaseHeadingPrefixSrcFor`
// (the FUNCTION: "called with a non-bracket convention, it returns the
// base source"), not of THIS site, and it would pass unchanged even if
// this call were deleted entirely. Safety at this specific call site rests
// ENTIRELY on a runtime gate the guard cannot see: `isBracketMilestoneBoundary`
// has exactly two callers, both guarded — `computeSectionEnd`'s
// `bracketBoundaryActive && isBracketMilestoneBoundary(...)` and the
// preambleCutoff scan's own call, itself nested inside an
// `if (bracketBoundaryActive) { … }` block (`src/roadmap-parser.cts`,
// grep `isBracketMilestoneBoundary(` for current line numbers — both
// round-3 fixes shifted them since this note was first written) — and
// `BRACKET_PHASE_TAIL_RE` has TWO consumers as of #2761 round-4 (its own
// use inside `isBracketMilestoneBoundary`, plus `bracketHeadingHasMatchingChild`'s
// same-id-PHASE-child conjunct added by 65d257ce) — both still gated on
// the same `bracketBoundaryActive` flag, but NOT both through the same
// mechanism: `bracketHeadingHasMatchingChild` is reached only via its one
// caller (`:593`, inside the `if (bracketBoundaryActive) { … }` block this
// note names above), while `isBracketMilestoneBoundary` — see this row's
// own "exactly two callers" paragraph above — is reached via TWO different
// mechanisms, an inline `bracketBoundaryActive &&` conjunct at one call
// site and that same block at the other. (round-5 Nit 1 first added this
// sentence to note the consumer count was stale by one commit; round-6
// Nit 2 corrected the mechanism claim that sentence introduced — the
// CONCLUSION is unaffected either time: every consumer is still
// runtime-gated on the same flag.)
// `BRACKET_HEADING_INTRO_RE` has THREE consumers as of #2761 round-4
// (`isBracketMilestoneBoundary` itself, plus two uses inside
// `bracketHeadingHasMatchingChild` — its own id and its same-id-PHASE-child
// scan) — all still nested inside the same `bracketBoundaryActive` runtime
// gate this note is about (round-4 Nit 1: this sentence was stale by one
// commit, claiming zero other consumers when 2e06aef5 had already added
// two; the CONCLUSION is unaffected — every consumer is still
// runtime-gated, and `BRACKET_HEADING_INTRO_RE` is built from
// `BRACKET_ID_SRC`, not `phaseHeadingPrefixSrcFor`, so it was never a
// selector site to begin with). The marker exists so a future reader does
// not mistake this row for "selector-covered like the other 14."
{ file: 'roadmap-parser.cts', site: 'isBracketMilestoneBoundary BRACKET_PHASE_TAIL_RE',
baseline: B.ANY_BRACKET, src: '(?:\\[[^\\]]{1,200}\\]\\s*)?Phase\\s+', runtimeGated: true },
// --- baseline: the site spells a BARE `Phase ` with no bracket tolerance
{ file: 'roadmap.cts', site: 'searchPhaseInContent checklistPattern',
baseline: B.LABEL_ONLY, src: 'Phase\\s+' },
{ file: 'roadmap.cts', site: 'cmdRoadmapAnalyze checkboxPattern',
baseline: B.LABEL_ONLY, src: 'Phase\\s+' },
{ file: 'roadmap.cts', site: 'cmdRoadmapAnalyze checklistPattern',
baseline: B.LABEL_ONLY, src: 'Phase\\s+' },
{ file: 'validate.cts', site: 'buildRoadmapPhaseVariants checklistPattern',
baseline: B.LABEL_ONLY, src: 'Phase\\s+' },
{ file: 'validate.cts', site: 'buildNotStartedPhaseVariants uncheckedPattern',
baseline: B.LABEL_ONLY, src: 'Phase\\s+' },
{ file: 'state.cts', site: 'buildStateFrontmatter roadmapPhaseCount',
baseline: B.LABEL_ONLY, src: 'Phase\\s+' },
{ file: 'state.cts', site: 'cmdStateSync roadmapPhaseCount', baseline: B.LABEL_ONLY, src: 'Phase\\s+' },
{ file: 'state.cts', site: 'extractRetiredPhaseNumbers phaseRef',
baseline: B.LABEL_ONLY, src: 'Phase\\s+' },
// #3309/#3310 moved the health reads out of verify.cts and into the parsed
// planning snapshot consumed by the diagnostic rule table. Pin the same two
// ROADMAP reads at their new owner so neither can silently narrow.
{ file: 'planning-snapshot.cts', site: 'buildCurrentMilestoneRoadmapPhaseIdsField (W026, ex-verify.cts B6)',
baseline: B.LABEL_ONLY, src: 'Phase\\s+' },
{ file: 'planning-snapshot.cts', site: 'buildRoadmapPhaseCheckboxesField (W011/W006 not-started)',
baseline: B.LABEL_ONLY, src: 'Phase\\s+' },
{ file: 'init.cts', site: 'cmdInitManager phaseHeadingPrefix',
baseline: B.LABEL_ONLY, src: 'Phase\\s+' },
{ file: 'init.cts', site: 'cmdInitManager phaseHeadingPrefixNoCapture',
baseline: B.LABEL_ONLY, src: 'Phase\\s+' },
];
// Every convention value that is NOT the bracket convention. A repo carrying any
// of these must read exactly as it did at base.
const NON_BRACKET = [undefined, null, '', 'milestone-prefixed', 'Bracket', 'BRACKET', 'brackets', 'bracket-ish'];
describe('#612 PR-2 STRUCTURAL IDENTITY: a non-bracket repo compiles the BASE pattern', () => {
for (const { file, site, baseline, src, runtimeGated } of BASE_SITES) {
// #2761 round-3 Minor 1: the title makes the gating mechanism visible in
// test output, not just in a source comment — a row with no
// `[runtime-gated]` suffix IS selector-covered by this test; one WITH it
// is safe only because of a call-site guard this test cannot see.
test(`${file} — ${site}${runtimeGated ? ' [runtime-gated, not selector-covered]' : ''}`, () => {
for (const convention of NON_BRACKET) {
assert.strictEqual(
core.phaseHeadingPrefixSrcFor(baseline, convention),
src,
`convention ${JSON.stringify(convention)} must compile the base source byte-for-byte`,
);
assert.strictEqual(
core.phaseHeadingPrefixSrcFor(baseline, convention, true),
src,
'the capturing variant adds no group when there is no bracket alternative',
);
}
});
}
test('only the exact string "bracket" selects the widened form', () => {
for (const convention of NON_BRACKET) {
assert.ok(
!core.phaseHeadingPrefixSrcFor(B.LABEL_ONLY, convention).includes('['),
`${JSON.stringify(convention)} must not admit any bracket alternative`,
);
}
assert.ok(core.phaseHeadingPrefixSrcFor(B.LABEL_ONLY, 'bracket').includes('['));
});
test('an unknown baseline is treated as label-only, never as widened', () => {
assert.strictEqual(
core.phaseHeadingPrefixSrcFor('nonsense', null), core.BASE_PHASE_LABEL_PREFIX_SRC,
);
});
});
// ─── What the legacy counterexamples do under each convention ──────────────
const scan = (prefixSrc, doc) => {
const re = new RegExp(`#{2,4}\\s*${prefixSrc}([\\w][\\w.-]*)(?:\\s*\\([^)\\n]{0,200}\\))?\\s*:`, 'gi');
const out = [];
let m;
while ((m = re.exec(doc)) !== null) out.push(m[1]);
return out;
};
describe('#612 PR-2: the reviewer counterexamples, under each convention', () => {
// Every one of these was claimed as a phase by the ungated widening.
const COUNTEREXAMPLES = [
'### [RFC.2119] 5: Keyword definitions',
'### [v1.0] 2024: Retrospective',
'### [v1.0] 2026-01-15: Shipped release notes',
'### [ADR.612] 3: Decisions to ratify',
'### [SPEC.1] 3: Scope',
'### [ISO.8601] 2026: Dates',
'### [rev.2] 9: Revision nine notes',
'### [Fig.3] 2: Diagram',
];
test('a legacy repo claims NONE of them (this is the fix)', () => {
for (const heading of COUNTEREXAMPLES) {
for (const convention of NON_BRACKET) {
assert.deepEqual(
scan(core.phaseHeadingPrefixSrcFor(B.ANY_BRACKET, convention), heading), [],
`${JSON.stringify(heading)} under ${JSON.stringify(convention)}`,
);
}
}
});
test('a legacy repo still reads its own real headings', () => {
const doc = '### Phase 5: Real\n### [GSD] Phase 2-01: Legacy\n#### Phase Details:';
assert.deepEqual(scan(core.phaseHeadingPrefixSrcFor(B.ANY_BRACKET, null), doc), ['5', '2-01', 'Details']);
});
test('DISCLOSED: on a BRACKET repo some of them are still claimed', () => {
// Gating removes the legacy blast radius; it does not make `[RFC.2119] 5:`
// unambiguous. On a repo that HAS opted in, a citation-shaped bracket whose
// milestone matches the emit width still reads as a phase. Pinned rather
// than hidden — the coherence check surfaces it as a milestone mismatch.
const src = core.phaseHeadingPrefixSrcFor(B.ANY_BRACKET, 'bracket');
assert.deepEqual(scan(src, '### [RFC.2119] 5: Keyword definitions'), ['5']);
// The emit-width rule does exclude the 1-digit-milestone family outright.
assert.deepEqual(scan(src, '### [SPEC.1] 3: Scope'), []);
assert.deepEqual(scan(src, '### [Fig.3] 2: Diagram'), []);
});
});
// ─── The label-only sites keep their narrowness on bracket repos too ───────
describe('#612 PR-2: a label-only site never gains any-bracket tolerance', () => {
const bullets = (prefixSrc, doc) => {
const re = new RegExp(`-\\s*\\[[ xX]\\]\\s*\\*{0,2}${prefixSrc}([\\w][\\w.-]*)\\s*:`, 'gi');
const out = [];
let m;
while ((m = re.exec(doc)) !== null) out.push(m[1]);
return out;
};
test('`[GSD] Phase 2-01` stays unmatched under BOTH conventions', () => {
const doc = '- [x] **[GSD] Phase 2-01: Legacy**';
assert.deepEqual(bullets(core.phaseHeadingPrefixSrcFor(B.LABEL_ONLY, null), doc), []);
assert.deepEqual(bullets(core.phaseHeadingPrefixSrcFor(B.LABEL_ONLY, 'bracket'), doc), [],
'the bracket convention widens to bracket IDs, not to arbitrary bracket text');
});
test('`[v1.2] Phase 3` — the retro-grant counterexample — stays unmatched on legacy', () => {
const doc = '- [ ] **[v1.2] Phase 3: Something legacy**';
assert.deepEqual(bullets(core.phaseHeadingPrefixSrcFor(B.LABEL_ONLY, null), doc), []);
});
test('a bracket repo does admit the bracket-ID form at a label-only site', () => {
const doc = '- [ ] **[GSD.02] 05: Real**';
assert.deepEqual(bullets(core.phaseHeadingPrefixSrcFor(B.LABEL_ONLY, null), doc), []);
assert.deepEqual(bullets(core.phaseHeadingPrefixSrcFor(B.LABEL_ONLY, 'bracket'), doc), ['05']);
});
});
// ─── The one identity grammar (F3) ─────────────────────────────────────────
describe('#612 PR-2: one bracket identity grammar, one width rule', () => {
test('the three former spellings now agree on the 1-digit-milestone family', () => {
// `GSD.2-05-feature` used to have a qualified key and a token but not be a
// phase directory, depending on which private spelling a caller reached.
assert.equal(core.bracketQualifiedKey('GSD.2-05-feature', 'bracket'), null);
assert.equal(core.extractPhaseToken('GSD.2-05-feature', 'bracket'), 'GSD.2-05-feature');
assert.ok(!new RegExp(`^${core.BRACKET_DIR_PREFIX_SRC}`, 'i').test('GSD.2-05-feature'));
});
test('the canonical padded forms resolve identically everywhere', () => {
assert.equal(core.bracketQualifiedKey('GSD.02-05-feature', 'bracket'), 'GSD.2-5');
assert.equal(core.extractPhaseToken('GSD.02-05-feature', 'bracket'), '05');
assert.ok(new RegExp(`^${core.BRACKET_DIR_PREFIX_SRC}`, 'i').test('GSD.02-05-feature'));
});
test('case folding: a lowercase bracket id passes every identity test', () => {
assert.equal(core.isSentinelPhaseId('gsd.999-01', 'bracket'), true, 'lowercase icebox is a sentinel');
assert.equal(core.isSentinelPhaseId('GSD.999-01', 'bracket'), true);
assert.equal(core.isSentinelPhaseId('gsd.00-01', 'bracket'), true);
assert.equal(core.isSentinelPhaseId('gsd.02-05', 'bracket'), false);
assert.equal(core.getMilestoneFromPhaseId('gsd.02-05', 'bracket'), 'v2.0');
assert.equal(core.bracketQualifiedKey('ck.03-02', 'bracket'), 'CK.3-2');
});
test('sentinel milestones and their neighbours', () => {
for (const mm of ['00', '999']) {
assert.equal(core.isSentinelPhaseId(`GSD.${mm}-01`, 'bracket'), true, mm);
}
for (const mm of ['01', '99', '100', '998', '1000']) {
assert.equal(core.isSentinelPhaseId(`GSD.${mm}-01`, 'bracket'), false, mm);
}
// Widths toDir cannot emit are not bracket ids at all — pad2 never produces
// a bare `0`, and the emit validator rejects a leading-zero 3+ run.
for (const mm of ['0', '000', '0999', '002', '2']) {
assert.equal(core.isSentinelPhaseId(`GSD.${mm}-01`, 'bracket'), false, `${mm} is malformed`);
}
});
test('an out-of-range milestone integer is refused, not collapsed to Infinity', () => {
const huge = 'A.' + '9'.repeat(400) + '-1';
assert.equal(core.bracketQualifiedKey(huge, 'bracket'), null,
'two 400-digit milestones must not share one key');
});
test('G3: the capturing variant captures the id in BOTH bracket forms', () => {
// `### [GSD.999] Phase 07:` used to fall through to the base alternative,
// which captures nothing — so the reader saw no bracket, applied the legacy
// token rule, and counted a labeled icebox heading while excluding the
// label-less one beside it.
for (const baseline of [B.ANY_BRACKET, B.LABEL_ONLY]) {
const re = new RegExp(
`^${core.phaseHeadingPrefixSrcFor(baseline, 'bracket', true)}([\\w][\\w.-]*)\\s*:`, 'i');
for (const heading of ['[GSD.999] Phase 07: Icebox', '[GSD.999] 07: Icebox']) {
const m = heading.match(re);
assert.ok(m, `${baseline}: ${heading}`);
assert.equal(m[1], 'GSD.999', `${baseline}: ${heading} — bracket id must be captured`);
assert.equal(m[2], '07');
}
}
});
test('G7: the qualified key shares the dir token boundary and width', () => {
// A qualified hit returns UNCONDITIONALLY from phaseTokenMatches, so a key
// that matches a directory isPhaseDirName rejects is a final wrong answer.
assert.equal(core.bracketQualifiedKey('GSD.02-12A-hotfix', 'bracket'), null);
assert.equal(core.bracketQualifiedKey('GSD.02-05.03.07-x', 'bracket'), null);
assert.equal(core.bracketQualifiedKey('GSD.2-05', 'bracket'), null, 'unpadded is malformed');
assert.equal(core.bracketQualifiedKey('GSD.02-05-slug', 'bracket'), 'GSD.2-5');
assert.equal(core.bracketQualifiedKey('GSD.02-05.03', 'bracket'), 'GSD.2-5.3');
});
test('G7: the qualified branch resolves its own milestone and refuses malformed dirs', () => {
// Kills the dead-branch mutant: deleting the qualified branch must fail here.
assert.equal(core.phaseTokenMatches('CK.03-02-shell', 'CK.03-02', 'bracket'), true);
assert.equal(core.phaseTokenMatches('CK.02-02-other', 'CK.03-02', 'bracket'), false,
'must not resolve to another milestone same-numbered dir');
assert.equal(core.phaseTokenMatches('GSD.02-12A-hotfix', 'GSD.02-12', 'bracket'), false,
'a directory isPhaseDirName rejects must not satisfy a qualified query');
});
test('the bracket path stays OFF without an explicit signal', () => {
assert.equal(core.bracketQualifiedKey('CK.03-02'), null);
assert.equal(core.bracketQualifiedKey('CK.03-02', 'milestone-prefixed'), null);
assert.equal(core.extractPhaseToken('GSD.02-05.03-01'), 'GSD.02-05.03-01');
// The #2043 numeric-tail family keeps its convention-less reading.
assert.equal(core.phaseTokenMatches('P0.03-02-tenant', 'P0.3-2'), false);
});
});
// ─── `\s*` must not span newlines (NIT 12) ─────────────────────────────────
describe('#612 PR-2: the bracket alternative does not span lines', () => {
test('prose on the line after a bracket-terminated heading is not a phase', () => {
const doc = '### [GSD.02]\n\n05: Orphan digits\n';
assert.deepEqual(scan(core.phaseHeadingPrefixSrcFor(B.ANY_BRACKET, 'bracket'), doc), [],
'a heading that ends at the bracket claims nothing on later lines');
});
test('a tab between the bracket and the token is still one heading', () => {
assert.deepEqual(
scan(core.phaseHeadingPrefixSrcFor(B.ANY_BRACKET, 'bracket'), '### [GSD.02]\t05: Tabbed'),
['05'],
);
});
});
// ─── The federated convention resolver ─────────────────────────────────────
describe('#612 PR-2: phase_id_convention resolves workstream -> root', () => {
const fs = require('fs');
const path = require('path');
const os = require('os');
const { resolvePhaseIdConvention } = require('../gsd-core/bin/lib/planning-workspace.cjs');
const { cleanup } = require('./helpers.cjs');
let dir;
const setup = ({ root, ws }) => {
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'adr-612-fed-'));
fs.mkdirSync(path.join(dir, '.planning', 'workstreams', 'ws1'), { recursive: true });
if (root !== undefined) {
fs.writeFileSync(path.join(dir, '.planning', 'config.json'),
JSON.stringify({ phase_id_convention: root }));
}
if (ws !== undefined) {
fs.writeFileSync(path.join(dir, '.planning', 'workstreams', 'ws1', 'config.json'),
JSON.stringify({ phase_id_convention: ws }));
}
return dir;
};
const withWs = (name, fn) => {
const prev = process.env.GSD_WORKSTREAM;
if (name === null) delete process.env.GSD_WORKSTREAM;
else process.env.GSD_WORKSTREAM = name;
try { return fn(); } finally {
if (prev === undefined) delete process.env.GSD_WORKSTREAM;
else process.env.GSD_WORKSTREAM = prev;
}
};
const cleanupDir = () => cleanup(dir);
test('config at ROOT only, no workstream active', () => {
const d = setup({ root: 'bracket' });
try { withWs(null, () => assert.equal(resolvePhaseIdConvention(d), 'bracket')); } finally { cleanupDir(); }
});
test('config at ROOT only, workstream active — falls back to root', () => {
// This is the split that made a workstream repo report every bracket phase
// missing from disk: the ROADMAP read resolved from one base, the directory
// read from the other.
const d = setup({ root: 'bracket' });
try { withWs('ws1', () => assert.equal(resolvePhaseIdConvention(d), 'bracket')); } finally { cleanupDir(); }
});
test('config at WORKSTREAM only, workstream active', () => {
const d = setup({ ws: 'bracket' });
try { withWs('ws1', () => assert.equal(resolvePhaseIdConvention(d), 'bracket')); } finally { cleanupDir(); }
});
test('config at BOTH — the workstream wins', () => {
// Governs the #612 bracket-selection reads ONLY. The shipped
// milestone-prefixed W021 gate keeps its own root-only read: re-basing a
// legacy convention's gate onto this resolver moved its answer in both
// directions on workstream repos, and that is pinned at the CLI in
// tests/adr-612-bracket-coherence.test.cjs.
const d = setup({ root: 'milestone-prefixed', ws: 'bracket' });
try {
withWs('ws1', () => assert.equal(resolvePhaseIdConvention(d), 'bracket'));
} finally { cleanupDir(); }
});
test('a PROJECT-scoped config stands alone — no root fallback', () => {
// config-loader falls back to the root config only under `if (ws)`, so a
// project-only split must not inherit the root's value.
const fsx = require('fs');
const d = setup({ root: 'bracket' });
try {
fsx.mkdirSync(path.join(d, '.planning', 'proj1'), { recursive: true });
const prev = process.env.GSD_PROJECT;
process.env.GSD_PROJECT = 'proj1';
try {
assert.equal(resolvePhaseIdConvention(d), null, 'root must not leak into a project scope');
fsx.writeFileSync(path.join(d, '.planning', 'proj1', 'config.json'),
JSON.stringify({ phase_id_convention: 'bracket' }));
assert.equal(resolvePhaseIdConvention(d), 'bracket');
} finally {
if (prev === undefined) delete process.env.GSD_PROJECT; else process.env.GSD_PROJECT = prev;
}
} finally { cleanupDir(); }
});
test('config at WORKSTREAM only, no workstream active — not visible', () => {
const d = setup({ ws: 'bracket' });
try { withWs(null, () => assert.equal(resolvePhaseIdConvention(d), null)); } finally { cleanupDir(); }
});
test('absent, empty, and unparseable configs all resolve to null', () => {
const d = setup({});
try {
withWs(null, () => assert.equal(resolvePhaseIdConvention(d), null, 'absent'));
fs.writeFileSync(path.join(d, '.planning', 'config.json'), '{}');
withWs(null, () => assert.equal(resolvePhaseIdConvention(d), null, 'empty object'));
fs.writeFileSync(path.join(d, '.planning', 'config.json'), '{ not json');
withWs(null, () => assert.equal(resolvePhaseIdConvention(d), null, 'unparseable'));
fs.writeFileSync(path.join(d, '.planning', 'config.json'), JSON.stringify({ phase_id_convention: '' }));
withWs(null, () => assert.equal(resolvePhaseIdConvention(d), null, 'empty string'));
fs.writeFileSync(path.join(d, '.planning', 'config.json'), JSON.stringify({ phase_id_convention: true }));
withWs(null, () => assert.equal(resolvePhaseIdConvention(d), null, 'non-string'));
} finally { cleanupDir(); }
});
// ─── #2761 B1: the workstream is an ARGUMENT, not only an env var ────────
test('the ws ARGUMENT selects the config, with no GSD_WORKSTREAM set', () => {
const d = setup({ root: 'milestone-prefixed', ws: 'bracket' });
try {
withWs(null, () => assert.equal(
resolvePhaseIdConvention(d, 'ws1'), 'bracket',
'a workstream passed by argument must resolve its OWN config, not the root',
));
} finally { cleanupDir(); }
});
test('arg and env resolve identically — `--workstream ws1` === `GSD_WORKSTREAM=ws1`', () => {
const d = setup({ root: 'milestone-prefixed', ws: 'bracket' });
try {
const viaArg = withWs(null, () => resolvePhaseIdConvention(d, 'ws1'));
const viaEnv = withWs('ws1', () => resolvePhaseIdConvention(d));
assert.equal(viaArg, viaEnv, 'arg-driven and env-driven callers must agree');
assert.equal(viaArg, 'bracket');
} finally { cleanupDir(); }
});
test('an explicit ws overrides GSD_WORKSTREAM rather than being ignored', () => {
const d = setup({ root: 'bracket' });
try {
fs.writeFileSync(path.join(d, '.planning', 'workstreams', 'ws1', 'config.json'),
JSON.stringify({ phase_id_convention: 'milestone-prefixed' }));
// env names ws1; the ARGUMENT names no workstream at all -> root scope.
withWs('ws1', () => assert.equal(resolvePhaseIdConvention(d, null), 'bracket'));
// env names nothing; the ARGUMENT names ws1 -> the workstream's own value.
withWs(null, () => assert.equal(resolvePhaseIdConvention(d, 'ws1'), 'milestone-prefixed'));
} finally { cleanupDir(); }
});
test('omitting ws keeps the env fallback — pre-#2761 call sites are unchanged', () => {
const d = setup({ root: 'milestone-prefixed', ws: 'bracket' });
try {
withWs('ws1', () => assert.equal(resolvePhaseIdConvention(d), 'bracket'));
withWs(null, () => assert.equal(resolvePhaseIdConvention(d), 'milestone-prefixed'));
} finally { cleanupDir(); }
});
});
// ─── #2761 B1: workstream isolation, end-to-end through the readers ─────────
describe('#2761 B1: a workstream\'s convention scopes ITS OWN roadmap read', () => {
const fs = require('fs');
const path = require('path');
const os = require('os');
const { extractCurrentMilestone, getMilestonePhaseFilter } =
require('../gsd-core/bin/lib/roadmap-parser.cjs');
const { cleanup } = require('./helpers.cjs');
// Two bracket milestones, NEITHER carrying a version string. Under the
// bracket convention STATE's `v2.0` selects `[GSD.02]` alone; under any other
// convention nothing matches and the window is the whole document. So "which
// convention did this read use" is directly observable in the window.
const ROADMAP = [
'# Roadmap', '',
'## [GSD.01] Foundation', '',
'### [GSD.01] 01: Alpha',
'### [GSD.01] 02: Alpha2', '',
'## [GSD.02] Second', '',
'### [GSD.02] 01: Beta',
'### [GSD.02] 02: Beta2',
'### [GSD.02] 03: Beta3', '',
].join('\n');
let dir;
const setup = ({ root, ws }) => {
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'adr-612-b1-'));
const wsDir = path.join(dir, '.planning', 'workstreams', 'foo');
fs.mkdirSync(path.join(wsDir, 'phases'), { recursive: true });
if (root !== undefined) {
fs.writeFileSync(path.join(dir, '.planning', 'config.json'),
JSON.stringify({ phase_id_convention: root }));
}
if (ws !== undefined) {
fs.writeFileSync(path.join(wsDir, 'config.json'),
JSON.stringify({ phase_id_convention: ws }));
}
fs.writeFileSync(path.join(wsDir, 'ROADMAP.md'), ROADMAP);
fs.writeFileSync(path.join(wsDir, 'STATE.md'), '---\nmilestone: v2.0\n---\n');
return dir;
};
const withWs = (name, fn) => {
const prev = process.env.GSD_WORKSTREAM;
if (name === null) delete process.env.GSD_WORKSTREAM;
else process.env.GSD_WORKSTREAM = name;
try { return fn(); } finally {
if (prev === undefined) delete process.env.GSD_WORKSTREAM;
else process.env.GSD_WORKSTREAM = prev;
}
};
const cleanupDir = () => cleanup(dir);
const scopedToGsd02 = (window) =>
window.includes('[GSD.02] 01: Beta') && !window.includes('[GSD.01] 01: Alpha');
// Repro A (trek-e). The workstream declares milestone-prefixed. Flipping ONLY
// the ROOT config used to change which milestone this workstream extracted,
// because the convention was resolved from `planningDir(cwd)` — the root,
// since no GSD_WORKSTREAM was set — while the DOCUMENT came from the
// workstream. The workstream's own declaration was never consulted at all.
test('repro A: flipping the ROOT config cannot move a workstream that owns its convention', () => {
const windows = [];
for (const root of ['bracket', 'milestone-prefixed']) {
const d = setup({ root, ws: 'milestone-prefixed' });
try {
const content = fs.readFileSync(
path.join(d, '.planning', 'workstreams', 'foo', 'ROADMAP.md'), 'utf-8');
windows.push(withWs(null, () => extractCurrentMilestone(content, d, 'foo')));
} finally { cleanupDir(); }
}
assert.equal(windows[0], windows[1],
'root=bracket and root=milestone-prefixed must produce the SAME window for a ' +
'workstream whose own config says milestone-prefixed');
assert.ok(!scopedToGsd02(windows[0]),
'a milestone-prefixed workstream must not take the bracket scoping path');
});
// The federation itself is preserved: a workstream that declares NOTHING
// still inherits the root, exactly as config-loader does. That is inheritance,
// not the leak — pinned so the B1 fix cannot be over-applied into isolation.
test('a workstream that declares no convention still inherits the root', () => {
const d = setup({ root: 'bracket', ws: undefined });
try {
const content = fs.readFileSync(
path.join(d, '.planning', 'workstreams', 'foo', 'ROADMAP.md'), 'utf-8');
assert.ok(scopedToGsd02(withWs(null, () => extractCurrentMilestone(content, d, 'foo'))));
} finally { cleanupDir(); }
});
// Repro B (trek-e): `--workstream foo` vs `GSD_WORKSTREAM=foo`.
test('repro B: the ws ARGUMENT and GSD_WORKSTREAM produce the same window', () => {
const d = setup({ root: 'milestone-prefixed', ws: 'bracket' });
try {
const content = fs.readFileSync(
path.join(d, '.planning', 'workstreams', 'foo', 'ROADMAP.md'), 'utf-8');
const viaArg = withWs(null, () => extractCurrentMilestone(content, d, 'foo'));
const viaEnv = withWs('foo', () => extractCurrentMilestone(content, d, undefined));
assert.equal(viaArg, viaEnv, 'arg-driven and env-driven reads must agree');
assert.ok(scopedToGsd02(viaArg), 'both must take the workstream\'s own bracket scoping');
} finally { cleanupDir(); }
});
test('repro B: arg/env parity holds through getMilestonePhaseFilter too', () => {
const d = setup({ root: 'milestone-prefixed', ws: 'bracket' });
try {
const viaArg = withWs(null, () => getMilestonePhaseFilter(d, 'v2.0', undefined, 'foo'));
const viaEnv = withWs('foo', () => getMilestonePhaseFilter(d, 'v2.0', undefined, undefined));
assert.equal(viaArg.phaseCount, viaEnv.phaseCount);
assert.equal(viaArg.phaseCount, 3, 'v2.0 declares exactly 3 bracket phases');
} finally { cleanupDir(); }
});
// The workstream-inventory delta: those two sites passed a literal `null`
// ("resolved, and it is not bracket") where they meant `undefined` ("resolve
// it"). On a bracket workstream `null` collapsed the heading set to empty.
test('undefined resolves the workstream convention where null pinned legacy', () => {
const d = setup({ root: undefined, ws: 'bracket' });
try {
withWs(null, () => {
assert.equal(getMilestonePhaseFilter(d, 'v2.0', undefined, 'foo').phaseCount, 3,
'undefined must resolve foo\'s bracket convention');
assert.equal(getMilestonePhaseFilter(d, 'v2.0', null, 'foo').phaseCount, 0,
'null still means "explicitly not bracket" — the discriminator is intact');
});
} finally { cleanupDir(); }
});
test('a NON-bracket workstream counts identically under null and undefined', () => {
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'adr-612-b1-legacy-'));
const wsDir = path.join(dir, '.planning', 'workstreams', 'bar');
fs.mkdirSync(path.join(wsDir, 'phases'), { recursive: true });
fs.writeFileSync(path.join(wsDir, 'ROADMAP.md'), [
'# Roadmap', '', '## v2.0 — Second', '',
'### Phase 01: Beta', '### Phase 02: Beta2', '',
].join('\n'));
fs.writeFileSync(path.join(wsDir, 'STATE.md'), '---\nmilestone: v2.0\n---\n');
try {
withWs(null, () => {
const asNull = getMilestonePhaseFilter(dir, 'v2.0', null, 'bar');
const asUndef = getMilestonePhaseFilter(dir, 'v2.0', undefined, 'bar');
assert.equal(asNull.phaseCount, asUndef.phaseCount);
assert.equal(asUndef.phaseCount, 2);
});
} finally { cleanupDir(); }
});
});
// ─── G8: the pin reads LIVE source, not a transcription ────────────────────
describe('#612 PR-2: every selector call site declares the right baseline (live src)', () => {
// The structural table above pins transcription <-> selector. It cannot see a
// call site whose BASELINE ARGUMENT is wrong: flipping the planning
// snapshot's milestone-complete site from LABEL_ONLY to ANY_BRACKET grants a
// fires-on-every-repo check `[anything] Phase N` tolerance it has never had,
// and every behavioural test still passed. So the mode at each site is pinned
// count-exact against the shipped sources.
//
// #2761 M4 (trek-e review): the source READING is no longer done here. This
// block claimed the no-source-grep escape with a source-text-is-the-product
// reason, but that escape (CONTEXT.md: RULESET.TESTS.no-source-grep.exemption)
// is reserved for tests whose subject is a runtime CONTRACT FILE — STATE.md,
// config.toml, hooks.json, agent .md — and `src/*.cts` is none of those.
// Worse, the escape is FILE-level (eslint-rules/no-source-grep.cjs matches the
// marker in any comment), so one block's claim disarmed the rule for all ~700
// lines of this suite. Rather than widen the documented scope to fit the test,
// the scan moved to the seam's own guard script
// (`scripts/lint-phase-id-drift.cjs`, where source scanning is sanctioned and
// already happens for the grammar rules) and is consumed here as STRUCTURED
// DATA. No file text reaches this file and no marker remains, so the rule is
// live again across the whole suite — the escape is gone, not relocated.
const { scanSelectorBaselines } = require('../scripts/lint-phase-id-drift.cjs');
const CENSUS = scanSelectorBaselines(require('path').join(__dirname, '..'));
// file -> [ANY_BRACKET count, LABEL_ONLY count]
const EXPECTED = {
'commands.cts': [1, 0],
'init.cts': [0, 2],
'roadmap.cts': [3, 3],
'validate.cts': [1, 2],
'state.cts': [0, 3],
'planning-snapshot.cts': [0, 2],
'roadmap-parser.cts': [2, 0],
};
for (const [file, [anyBracket, labelOnly]] of Object.entries(EXPECTED)) {
test(`${file}: ${anyBracket} any-bracket + ${labelOnly} label-only, and nothing else`, () => {
const c = CENSUS[file];
assert.ok(c, `${file} no longer consumes the selector at all — update EXPECTED`);
assert.equal(c.ANY_BRACKET, anyBracket, `${file} any-bracket call count`);
assert.equal(c.LABEL_ONLY, labelOnly, `${file} label-only call count`);
assert.equal(
c.total, anyBracket + labelOnly,
`${file} has a phaseHeadingPrefixSrcFor call that does not name a PHASE_HEADING_BASELINE mode`,
);
});
}
test('no OTHER src file consumes the selector unpinned', () => {
const unpinned = Object.keys(CENSUS).filter(f => !(f in EXPECTED)).sort();
assert.deepEqual(unpinned, [], 'a new selector consumer must be added to EXPECTED');
});
test('the census is live — it found the consumers, not an empty scan', () => {
// A census that silently returned {} would make every count assertion above
// fail loudly, but the unpinned check would pass vacuously. Pin the floor.
assert.deepEqual(Object.keys(CENSUS).sort(), Object.keys(EXPECTED).sort());
});
test('the transcription table covers exactly the live call sites', () => {
const live = Object.values(EXPECTED).reduce((n, [a, l]) => n + a + l, 0);
assert.equal(BASE_SITES.length, live, 'BASE_SITES row count must equal live call-site count');
});
});