Files
msd-core/tests/adr-612-bracket-read-tolerance.test.cjs
Jakub Zych a9a7a328e6 refactor: hard-fork GSD -> MSD (Make Software Done)
Mechanical rename produced by scripts/msd-rename.cjs: gsd/Gsd/GSD -> msd/Msd/MSD
across contents and paths, upstream package/repo coordinates -> @golem15/msd-core
and golem15com/msd-core. Deep links into upstream history, sibling upstream
packages, the GSD-2 import feature, CHANGELOG.md and .changeset/ are kept as-is.

Hand edits on top: MSD block-letter banner and logos, LICENSE copyright line,
package/plugin identity, regenerated lockfile, install-tree fixtures, derived
registries and benchmark baseline; migration checksum baseline re-locked
(MSD keeps its own install state, so no install had applied the old sums);
sort-order and regex-escaped expectations in tests adjusted.
2026-10-06 01:47:40 +02:00

1579 lines
63 KiB
JavaScript

'use strict';
/**
* PR-2 (#2761 / epic #612) — the bracket-tolerant ROADMAP read path, at CLI level.
*
* Every reader here selects its heading grammar from the resolved
* `phase_id_convention`. The two things that buys, both pinned below:
*
* 1. A project that has not opted in reads exactly as it did before. The
* counterexample corpus is the one that falsified the earlier ungated
* design — `### [RFC.2119] 5:`, `### [v1.0] 2026-01-15:`, `### [ADR.612] 3:`,
* `### [rev.2] 9:`, `### [Cluster B] Phase 26:` — each of which was claimed
* as a phase and moved `phase_count` / `total_phases` / W006 on legacy
* repos. They are asserted against the CLI, not against a regex.
*
* 2. A project that HAS opted in gets its bracket headings read, including the
* sentinel exclusion that the widening would otherwise break: under
* READING-B the sentinel milestone lives in the bracket, so a filter that
* tests the phase token is blind to `### [MSD.999] 01:` and counts an
* icebox item as a real phase (#1445 / #1580) — quietly, since #1446 took
* total_phases out of the ratchet.
*
* Fixtures are raw markdown strings, never rendered through renderPhaseId/toDir.
*/
const { test, describe, beforeEach, afterEach } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const { runMsdTools, createTempProject, cleanup } = require('./helpers.cjs');
const { PATHOLOGICAL_INPUT_TEST_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
let tmpDir;
const write = (roadmap, convention) => {
const planning = path.join(tmpDir, '.planning');
fs.writeFileSync(path.join(planning, 'ROADMAP.md'), roadmap, 'utf-8');
fs.writeFileSync(
path.join(planning, 'config.json'),
JSON.stringify(convention === undefined ? {} : { phase_id_convention: convention }),
'utf-8',
);
};
const analyze = () => {
const r = runMsdTools(['roadmap', 'analyze'], tmpDir);
assert.ok(r.success, `roadmap analyze failed: ${r.error}`);
return JSON.parse(r.output);
};
// Upstream's shared diagnostic table returns coded IssueEntry objects; retain
// compatibility with the older string form so these assertions test messages,
// not the transport shape.
const consistencyMessages = (r) =>
(JSON.parse(r.output).warnings || []).map(w => (typeof w === 'string' ? w : w.message));
const BRACKET_ROADMAP = `# Roadmap
## [MSD.02] v2.0 — Foundation
- [x] **[MSD.02] 01: Setup**
- [ ] **[MSD.02] 05: Real work**
- [ ] **[MSD.02] 06: Follow-up**
### [MSD.02] 01: Setup
**Goal:** Lay the groundwork
### [MSD.02] 05: Real work
**Goal:** Build the thing
### [MSD.02] 06: Follow-up
**Goal:** Polish it
`;
// ─── The legacy no-op guarantee (the whole point of gating) ────────────────
describe('#612 PR-2: a non-bracket repo is untouched by the bracket read path', () => {
beforeEach(() => { tmpDir = createTempProject('adr-612-legacy-'); });
afterEach(() => { cleanup(tmpDir); });
// Each of these was claimed as a phase by the ungated design. `expect` is what
// the base build reports, so a regression here is a visible number change.
const COUNTEREXAMPLES = [
['### [RFC.2119] 5: Keyword definitions', 'RFC citation'],
['### [v1.0] 2026-01-15: Shipped release notes', 'version tag + date'],
['### [v1.0] 2024: Retrospective', 'version tag + year'],
['### [ADR.612] 3: Decisions to ratify', 'ADR citation'],
['### [rev.2] 9: Revision nine notes', 'lowercase tag'],
['### [ISO.8601] 2026: Dates', 'standard citation'],
['### [Cluster B] Phase 26: Clustered work', 'any-bracket + Phase label'],
['### [MSD] Phase 7: Bracketed legacy', 'project tag + Phase label'],
];
for (const [heading, label] of COUNTEREXAMPLES) {
for (const convention of [undefined, 'milestone-prefixed']) {
test(`${label} adds no phase (convention=${convention ?? 'unset'})`, () => {
write(`# Roadmap
## v1.0 — Foundation
### Phase 01: Setup
**Goal:** Groundwork
${heading}
**Goal:** Not a phase
`, convention);
const out = analyze();
// `[Cluster B] Phase 26` and `[MSD] Phase 7` DO match at base — the
// any-bracket tolerance is pre-existing — so they legitimately count.
const expected = /Phase \d/.test(heading) ? 2 : 1;
assert.equal(
out.phase_count, expected,
`phase_count moved; phases=${JSON.stringify(out.phases.map(p => p.number))}`,
);
});
}
}
test('a legacy roadmap reads its own headings, tags and checkboxes unchanged', () => {
write(`# Roadmap
## v1.0 — Foundation
## Phase Overview:
- [x] **Phase 1: Foundation**
- [ ] **Phase 2-01: API**
### Phase 1: Foundation
**Goal:** Set up
### Phase 2-01 (INSERTED): API
**Goal:** Build it
#### Phase Details:
`, undefined);
const out = analyze();
assert.deepEqual(out.phases.map(p => [p.number, p.name]), [['1', 'Foundation'], ['2-01', 'API']]);
assert.deepEqual(out.phases.map(p => p.roadmap_complete), [true, false]);
});
test('a bracket roadmap on a NON-bracket repo is invisible, not miscounted', () => {
// Mid-migration disclosure: headings written in the new form before the
// config is switched are not read. Silent invisibility is the deliberate
// trade — the alternative is claiming phases on repos that never opted in.
write(BRACKET_ROADMAP, undefined);
const out = analyze();
assert.equal(out.phase_count, 0, 'no phases, and no phantoms either');
});
});
// ─── The bracket repo actually reads ───────────────────────────────────────
describe('#612 PR-2: a bracket repo reads its bracket headings', () => {
beforeEach(() => { tmpDir = createTempProject('adr-612-read-'); });
afterEach(() => { cleanup(tmpDir); });
test('counts every phase and reads its NAME from the right capture group', () => {
write(BRACKET_ROADMAP, 'bracket');
const out = analyze();
assert.equal(out.phase_count, 3);
assert.deepEqual(
out.phases.map(p => [p.number, p.name]),
[['01', 'Setup'], ['05', 'Real work'], ['06', 'Follow-up']],
'number AND name — a group-offset error garbles the name first',
);
});
test('reads the Goal of each section (the heading is a section boundary)', () => {
write(BRACKET_ROADMAP, 'bracket');
assert.deepEqual(
analyze().phases.map(p => p.goal),
['Lay the groundwork', 'Build the thing', 'Polish it'],
);
});
test('reads summary-checkbox completion through a bracket bullet', () => {
write(BRACKET_ROADMAP, 'bracket');
assert.deepEqual(analyze().phases.map(p => p.roadmap_complete), [true, false, false]);
});
test('a legacy heading on a bracket repo still reads (migration window)', () => {
write(`# Roadmap
## [MSD.02] v2.0
### [MSD.02] 05: Bracket form
**Goal:** a
### Phase 6: Legacy form
**Goal:** b
`, 'bracket');
assert.deepEqual(analyze().phases.map(p => p.number), ['05', '6']);
});
test('get-phase resolves a bracket heading', () => {
write(BRACKET_ROADMAP, 'bracket');
const r = runMsdTools(['roadmap', 'get-phase', '05'], tmpDir);
assert.ok(r.success, r.error);
const out = JSON.parse(r.output);
assert.equal(out.found, true);
assert.equal(out.phase_name, 'Real work');
assert.equal(out.goal, 'Build the thing');
});
test('a checklist-only bracket phase reports malformed_roadmap', () => {
write(`# Roadmap
## [MSD.02] v2.0
- [ ] **[MSD.02] 09: Summary only**
### [MSD.02] 05: Real work
**Goal:** a
`, 'bracket');
const out = JSON.parse(runMsdTools(['roadmap', 'get-phase', '09'], tmpDir).output);
assert.equal(out.found, false);
assert.equal(out.error, 'malformed_roadmap');
assert.equal(out.phase_name, 'Summary only');
});
});
// ─── Sentinels (READING-B) ─────────────────────────────────────────────────
describe('#612 PR-2: bracket sentinel milestones never count', () => {
beforeEach(() => { tmpDir = createTempProject('adr-612-sentinel-'); });
afterEach(() => { cleanup(tmpDir); });
test('999 and 00 bracket milestones are excluded from phase_count', () => {
write(`# Roadmap
## [MSD.02] v2.0
### [MSD.999] 01: Icebox item
**Goal:** Someday
### [MSD.00] 02: Pre-milestone groundwork
**Goal:** Before v1
### [MSD.02] 05: Real work
**Goal:** Build it
`, 'bracket');
const out = analyze();
assert.deepEqual(out.phases.map(p => `${p.number}:${p.name}`), ['05:Real work']);
assert.equal(out.phase_count, 1);
});
test('a LOWERCASE sentinel bracket is excluded too', () => {
// Readers recognize `/i`; the identity helpers match `[A-Z]`. Without folding
// the captured id, `[msd.999]` failed every sentinel test and the icebox item
// counted as a real phase.
write(`# Roadmap
## [msd.02] v2.0
### [msd.999] 07: Icebox
**Goal:** Someday
### [msd.00] 08: Pre-milestone
**Goal:** Before
### [msd.02] 01: Real
**Goal:** Build
`, 'bracket');
const out = analyze();
assert.deepEqual(out.phases.map(p => p.number), ['01'], 'lowercase sentinels excluded');
});
test('a 999 bracket CHECKLIST entry is not a missing-detail phantom', () => {
write(`# Roadmap
## [MSD.02] v2.0
- [ ] **[MSD.999] 01: Icebox item**
- [ ] **[MSD.02] 07: Genuinely missing**
- [ ] **[MSD.02] 05: Real work**
### [MSD.02] 05: Real work
**Goal:** Build it
`, 'bracket');
assert.deepEqual(analyze().missing_phase_details, ['07']);
});
// ─── #2761 M1: sentinel classification is PER-OCCURRENCE ─────────────────
//
// The checklist scan keyed its bracket-id map by the bare TOKEN, first-wins,
// and the detail set was keyed by the bare token too. Under READING-B the
// sentinel lives in the BRACKET, so two checklist entries sharing a token
// across different brackets — the icebox shape the ADR itself documents —
// had ONE classification between them, decided by document order.
for (const [label, checklist] of [
['sentinel first', ['- [ ] **[MSD.999] 01: Icebox item**', '- [ ] **[MSD.02] 01: Genuinely missing**']],
['sentinel last', ['- [ ] **[MSD.02] 01: Genuinely missing**', '- [ ] **[MSD.999] 01: Icebox item**']],
]) {
test(`a real phase sharing an icebox token is reported missing (${label})`, () => {
write(`# Roadmap
## [MSD.02] v2.0
${checklist.join('\n')}
### [MSD.02] 05: Real work
**Goal:** Build it
`, 'bracket');
assert.deepEqual(
analyze().missing_phase_details, ['01'],
'[MSD.02] 01 has no detail heading and is not a sentinel — it is missing ' +
'in BOTH orders. Order-dependence here means the icebox entry\'s ' +
'classification was applied to the real phase.',
);
});
}
test('an icebox token is still suppressed when NO real phase shares it', () => {
// The other direction: per-occurrence classification must not turn the
// sentinel suppression itself into a false POSITIVE.
write(`# Roadmap
## [MSD.02] v2.0
- [ ] **[MSD.999] 01: Icebox item**
- [ ] **[MSD.00] 02: Pre-milestone**
### [MSD.02] 05: Real work
**Goal:** Build it
`, 'bracket');
assert.equal(analyze().missing_phase_details, null, 'both entries are sentinels');
});
test('a detail heading does not satisfy a SAME-TOKEN entry from another bracket', () => {
// The order-INDEPENDENT half of the same defect: the detail set was keyed
// by bare token, so `[MSD.02] 01`'s heading marked token `01` present and
// masked `[MSD.03] 01`, which has no heading at all. No version strings and
// no STATE milestone, so the window is the whole document — the shape that
// puts two real brackets in one scan.
write(`# Roadmap
## [MSD.02] Foundation
- [ ] **[MSD.02] 01: Has a heading**
### [MSD.02] 01: Has a heading
**Goal:** Build it
## [MSD.03] Second
- [ ] **[MSD.03] 01: Has no heading**
`, 'bracket');
assert.deepEqual(
analyze().missing_phase_details, ['01'],
'[MSD.03] 01 has no detail heading; [MSD.02] 01\'s heading must not cover it',
);
});
test('G5: a 999 token under a real milestone is STILL a backlog sentinel', () => {
write(`# Roadmap
## [MSD.02] v2.0
### [MSD.02] 999: Late work
**Goal:** Build it
`, 'bracket');
// READING-B adds the bracket rule; it does not repeal the engine-wide
// 0/999 backlog convention (#1445/#1580). A bracketed heading is a sentinel
// when EITHER its bracket milestone or its token is reserved.
assert.deepEqual(analyze().phases.map(p => p.number), []);
});
test('legacy sentinel filtering is unchanged on a legacy repo', () => {
write(`# Roadmap
## v2.0
### Phase 999.1: Icebox
**Goal:** a
### Phase 0: Pre-milestone
**Goal:** b
### Phase 5: Real
**Goal:** c
`, undefined);
assert.deepEqual(analyze().phases.map(p => p.number), ['5']);
});
});
// ─── validate.cts: the W006/W007 feeders and directory recognition ─────────
describe('#612 PR-2: validate.cts heading builders are convention-selected', () => {
const validate = require('../msd-core/bin/lib/validate.cjs');
// The letter-tolerant `[\w][\w.-]*` capture lives here, so this is where an
// ungated widening does the most damage: a phantom becomes a W007.
const PHANTOM_HEADINGS = [
'### [RFC.2119] 5: Keyword definitions',
'### [v1.0] 2024: Retrospective',
'### [ADR.612] 3: Decisions to ratify',
'### [ISO.8601] 2026: Dates',
];
test('a non-bracket repo admits none of the phantom headings', () => {
const doc = ['### Phase 5: Real work', ...PHANTOM_HEADINGS].join('\n');
for (const convention of [undefined, null, 'milestone-prefixed', 'Bracket']) {
const { roadmapPhases } = validate.buildRoadmapPhaseVariants(doc, convention);
assert.deepEqual([...roadmapPhases], ['5'], `convention=${convention}`);
}
});
test('a bracket repo reads bracket headings', () => {
const { roadmapPhases } = validate.buildRoadmapPhaseVariants(
'### [MSD.02] 05: Real work\n### [MSD.02] 06: Follow-up\n', 'bracket',
);
assert.deepEqual([...roadmapPhases].sort(), ['05', '06']);
});
test('legacy headings and bullets are byte-identical either way', () => {
const doc = [
'### Phase 1: Foundation', '### Phase 2-01 (INSERTED): API', '### Phase 12A: Hotfix',
'#### Phase Details:', '- [x] **Phase 3: Done**', '- [ ] **Phase 4: Todo**',
].join('\n');
const legacy = [...validate.buildRoadmapPhaseVariants(doc).roadmapPhases].sort();
assert.deepEqual(legacy, ['1', '12A', '2-01', '3', '4', 'Details'].sort());
assert.deepEqual([...validate.buildRoadmapPhaseVariants(doc, 'bracket').roadmapPhases].sort(), legacy);
});
test('a label-only bullet site never gains any-bracket tolerance', () => {
const doc = '- [x] **[MSD] Phase 2-01: Legacy**\n- [ ] **[MSD.02] 07: Bracket**\n';
assert.deepEqual([...validate.buildRoadmapPhaseVariants(doc).roadmapPhases], []);
assert.deepEqual([...validate.buildRoadmapPhaseVariants(doc, 'bracket').roadmapPhases], ['07']);
});
test('the unchecked-bullet site keeps its live W006 on a legacy repo', () => {
// `[v1.0] Phase 05` in an unchecked bullet used to SUPPRESS a W006 that
// fires at base — a vanishing warning, worse than an added one.
const doc = '- [ ] **[v1.0] Phase 05: Thing**\n';
assert.deepEqual([...validate.buildNotStartedPhaseVariants(doc)], [],
'the bullet must not register phase 05 as not-started on a legacy repo');
// `[v1.0]` is no longer a bracket id at all: the milestone width is now the
// emit grammar's, and pad2 never produces a bare `0`. The residual this
// previously disclosed is gone rather than merely gated.
assert.deepEqual([...validate.buildNotStartedPhaseVariants(doc, 'bracket')], []);
});
test('a bracket repo picks up unchecked bracket bullets', () => {
const notStarted = validate.buildNotStartedPhaseVariants(
'- [ ] **[MSD.02] 05: Real work**\n- [x] **[MSD.02] 01: Done**\n', 'bracket');
assert.ok(notStarted.has('05'));
assert.ok(!notStarted.has('01'));
});
});
describe('#612 PR-2: directory recognition is convention-gated', () => {
const validate = require('../msd-core/bin/lib/validate.cjs');
const core = require('../msd-core/bin/lib/phase-id.cjs');
const LEGACY_DIRS = [
'02-01-setup', '01-setup', 'MSD-02-01-setup', '999.1-backlog', '14-2026-photos',
'02-04-01-deep', '12A-hotfix', 'not-a-phase', 'P0.34-56-name', 'P0.12-34-name',
'P0.3-2-tenant', 'P0.16-gate',
];
test('legacy dirs answer identically to the untouched constants', () => {
for (const d of LEGACY_DIRS) {
assert.equal(validate.isPhaseDirName(d), validate.phaseDirNameRe.test(d), d);
const viaConst = d.match(validate.PHASE_TOKEN_FROM_DIR_RE);
assert.equal(validate.phaseTokenFromDir(d), viaConst ? viaConst[1] : null, d);
}
});
test('the default-off invariant, extended to the directory side', () => {
for (const d of ['P0.34-56-name', 'P0.12-34-name']) {
for (const convention of [undefined, null, 'milestone-prefixed', 'BRACKET']) {
assert.equal(validate.isPhaseDirName(d, convention), false, `${d} / ${convention}`);
assert.equal(validate.phaseTokenFromDir(d, convention), null);
}
assert.equal(validate.isPhaseDirName(d, 'bracket'), true, `${d} opted in`);
}
});
test('bracket dirs resolve under the bracket convention', () => {
for (const [dir, token] of [
['MSD.02-05-feature', '05'], ['MSD.02-05.03-feature', '05.03'],
['MSD.02-05', '05'], ['CK.01-12.04-feature', '12.04'], ['MSD_X2.100-05-feature', '05'],
]) {
assert.equal(validate.isPhaseDirName(dir, 'bracket'), true, dir);
assert.equal(validate.phaseTokenFromDir(dir, 'bracket'), token, dir);
assert.equal(validate.isPhaseDirName(dir), false, `${dir} without the signal`);
}
});
test('shapes outside the emit grammar are not bracket dirs', () => {
// Admitting these would make the recognizer disagree with the resolver.
for (const d of ['MSD.02-12A-hotfix', 'MSD.02-05.03.07-x', 'MSD.2-05-x', 'not-a-phase', 'MSD.02']) {
assert.equal(validate.isPhaseDirName(d, 'bracket'), false, d);
}
});
test('the two bracket dir readers agree on ACCEPTED and REJECTED input alike', () => {
const corpus = [
'MSD.02-05-feature', 'MSD.02-05.03-feature', 'MSD.02-05', 'CK.01-12.04-f',
'MSD_X2.100-05-f', 'MSD.999-01-icebox', 'MSD.02-12A-hotfix', 'MSD.02-05.03.07-x',
'MSD.2-05-x', '02-01-setup', 'MSD-02-01-setup', 'not-a-phase', 'P0.34-56-name',
];
for (const dir of corpus) {
const shaped = validate.BRACKET_PHASE_DIR_RE.test(dir);
const viaOwner = core.extractPhaseToken(dir, 'bracket');
if (shaped) {
assert.equal(validate.phaseTokenFromDir(dir, 'bracket'), viaOwner, `accepted: ${dir}`);
} else {
// Rejected by the recognizer — the owner must NOT resolve it through the
// bracket branch either, or W005 calls a directory malformed in the same
// run that the milestone-complete check treats it as a real phase dir.
const legacyToken = core.extractPhaseToken(dir);
assert.equal(
viaOwner, legacyToken,
`rejected by the recognizer but bracket-resolved by the owner: ${dir}`,
);
}
}
});
test('non-string input throws, matching the constants they wrap', () => {
for (const bad of [42, undefined, null, {}, [], true]) {
assert.throws(() => validate.isPhaseDirName(bad, 'bracket'), TypeError, String(bad));
assert.throws(() => validate.phaseTokenFromDir(bad, 'bracket'), TypeError, String(bad));
}
});
});
// ─── G6: validate consistency suppresses bracket sentinels ─────────────────
describe('#612 PR-2: bracket sentinels do not warn as missing directories', () => {
beforeEach(() => { tmpDir = createTempProject('adr-612-consist-'); });
afterEach(() => { cleanup(tmpDir); });
const consistencyWarnings = () => {
const r = runMsdTools(['validate', 'consistency'], tmpDir);
return consistencyMessages(r).filter(w => /no directory on disk/.test(w));
};
test('an icebox bracket phase is not reported as missing from disk', () => {
// validate health suppressed these via notStartedPhases while consistency
// did not, so the two verbs disagreed on the same repo.
write(`# Roadmap
## [MSD.02] v2.0
### [msd.999] 07: Icebox
**Goal:** a
### [MSD.999] 08: Icebox
**Goal:** b
### [MSD.02] 01: Real
**Goal:** c
`, 'bracket');
assert.deepEqual(
consistencyWarnings().filter(w => /\b0[78]\b/.test(w)), [],
'sentinel phases legitimately have no directory',
);
});
test('a real bracket phase with no directory still warns', () => {
write(`# Roadmap
## [MSD.02] v2.0
### [MSD.02] 09: Real but absent
**Goal:** a
`, 'bracket');
const w = consistencyWarnings();
assert.equal(w.length, 1, JSON.stringify(w));
assert.match(w[0], /Phase 09/);
});
// INVERTED by the merge of next @ 86101ee6. This case previously asserted the
// INHERITED WART — that a legacy `### Phase 999:` still warned here while
// `validate health` suppressed it — which this branch disclosed rather than
// fixed, because the legacy path was to stay byte-identical.
//
// ae7dc529 (#3225) fixed that wart upstream by adding the `isSentinelPhaseId`
// guard to this very loop, so the disagreement it pinned no longer exists and
// the old assertion inverted on the merge. The case is kept (not deleted) and
// flipped to assert the FIXED behaviour: it is the negative-space proof that
// this branch's `sentinelPhases` guard did not have to grow a legacy reading
// of its own, and it reds if a future resolution drops upstream's guard while
// keeping ours.
test('#3225 (merged): a legacy `### Phase 999:` no longer warns here', () => {
write(`# Roadmap
## v2.0
### Phase 999: Backlog
**Goal:** a
`, undefined);
assert.deepEqual(
consistencyWarnings(), [],
'the legacy leading-int sentinel rule suppresses this since #3225',
);
});
// Negative space for the case above: the #3225 guard is SENTINEL-scoped, not a
// blanket silencer. Without this, dropping the whole loop would also pass.
test('#3225 scope: a legacy NON-sentinel phase with no directory still warns', () => {
write(`# Roadmap
## v2.0
### Phase 09: Real but absent
**Goal:** a
`, undefined);
const w = consistencyWarnings();
assert.equal(w.length, 1, JSON.stringify(w));
assert.match(w[0], /Phase 09/);
});
});
// ─── #2761 B2: validate health and validate consistency must agree ─────────
//
// buildRoadmapPhaseVariants() surfaces `sentinelPhases` — tokens borne ONLY by
// a bracket-sentinel heading ([MSD.999] icebox / [MSD.00] pre-milestone) — so a
// backlog item's heading-only entry does not need a directory. cmdValidateConsistency
// consumes it (see the describe block above); cmdValidateHealth's W006 loop did
// not, so the SAME sentinel-only bracket ROADMAP produced a silent `validate
// consistency` and a false W006 from `validate health` — the two verbs
// contradicting each other about the same repo.
describe('#612 PR-2 B2: validate health and validate consistency agree on bracket sentinels', () => {
beforeEach(() => { tmpDir = createTempProject('adr-612-b2-agree-'); });
afterEach(() => { cleanup(tmpDir); });
const SENTINEL_ONLY = `# Roadmap
## [MSD.999] Icebox
### [MSD.999] 07: Someday
**Goal:** a
`;
const healthW006 = () => {
const r = runMsdTools(['validate', 'health'], tmpDir);
const out = JSON.parse(r.output);
return [...(out.errors || []), ...(out.warnings || [])]
.filter((i) => i.code === 'W006')
.map((i) => i.message);
};
const consistencyMissingDirWarnings = () => {
const r = runMsdTools(['validate', 'consistency'], tmpDir);
return consistencyMessages(r).filter((message) => /no directory on disk/.test(message));
};
test('a sentinel-only bracket roadmap: neither validator warns about the missing directory', () => {
write(SENTINEL_ONLY, 'bracket');
assert.deepEqual(
consistencyMissingDirWarnings(), [],
'validate consistency already excludes sentinel phases via sentinelPhases',
);
assert.deepEqual(
healthW006(), [],
'validate health must ALSO exclude sentinel phases — the two validators must agree on the same ROADMAP',
);
});
test('CONTROL: a real (non-sentinel) phase with no directory still warns on both validators', () => {
// The agreement above must not be achieved by suppressing W006 outright.
write(`# Roadmap
## [MSD.02] v2.0
### [MSD.02] 09: Real but absent
**Goal:** a
`, 'bracket');
const consistency = consistencyMissingDirWarnings();
const health = healthW006();
assert.equal(consistency.length, 1, JSON.stringify(consistency));
assert.equal(health.length, 1, JSON.stringify(health));
assert.match(consistency[0], /Phase 09/);
assert.match(health[0], /Phase 09/);
});
});
describe('#612 PR-2: sentinel suppression is occurrence-aware', () => {
beforeEach(() => { tmpDir = createTempProject('adr-612-occ-'); });
afterEach(() => { cleanup(tmpDir); });
const consistencyWarnings = () => {
const r = runMsdTools(['validate', 'consistency'], tmpDir);
return consistencyMessages(r).filter(w => /no directory on disk/.test(w));
};
test('a token borne ONLY by an icebox heading is suppressed', () => {
write(`# Roadmap
## [MSD.02] v2.0
### [MSD.999] 07: Icebox only
**Goal:** a
### [MSD.02] 02: Real two
**Goal:** b
`, 'bracket');
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', 'MSD.02-02-real-two'), { recursive: true });
assert.deepEqual(consistencyWarnings().filter(w => /\b07\b/.test(w)), []);
});
test('a token SHARED with a real heading still warns', () => {
// roadmapPhases is a token set, so `[MSD.999] 01` and `[MSD.02] 01` collapse
// to one entry. Keying suppression on the token alone let the icebox item
// silence a real phase that has no directory — a false negative worse than
// the warning it removed.
write(`# Roadmap
## [MSD.02] v2.0
### [MSD.999] 01: Icebox one
**Goal:** a
### [MSD.02] 01: REAL one, dir missing
**Goal:** b
### [MSD.02] 02: Real two
**Goal:** c
`, 'bracket');
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', 'MSD.02-02-real-two'), { recursive: true });
const w = consistencyWarnings();
assert.equal(w.length, 1, JSON.stringify(w));
assert.match(w[0], /Phase 01/);
});
});
// ─────────────────────────────────────────────────────────────────────────────
// R4-M1 — the DIRECTORY read inside `roadmap analyze`.
//
// `cmdRoadmapAnalyze` resolves the convention once and threads it into all four
// of its heading/checklist patterns, but the single `phaseTokenMatches` call
// that decides `disk_status` / `plan_count` / `summary_count` / `has_context` /
// `has_research` was left two-argument. Every canonical `{CODE}.{MM}-{PP}-slug`
// directory then read as `no_directory` with zero counts — while the SAME build
// resolved those same directories correctly in three other places on the same
// repo (W006/W007, `state json`, and the W021 milestone-complete read). Only the
// directory shape the convention exists to name failed; a mid-migration bracket
// repo carrying legacy `01-one` dirs resolved fine.
//
// Pinned the way the rest of this PR's gate is pinned: against the flat-legacy
// twin, computed in the same test run, PLUS exact literals so a shared wrong
// answer cannot pass. `grep disk_status tests/adr-612-*` was zero before this.
// ─────────────────────────────────────────────────────────────────────────────
describe('#612 PR-2: roadmap analyze resolves bracket phase DIRECTORIES', () => {
beforeEach(() => { tmpDir = createTempProject('adr-612-analyze-dir-'); });
afterEach(() => { cleanup(tmpDir); });
const BRK = `# Roadmap
## [MSD.02] v2.0: Current
### [MSD.02] 01: One
**Goal:** a
### [MSD.02] 02: Two
**Goal:** b
`;
const LEG = `# Roadmap
## v2.0: Current
### Phase 01: One
**Goal:** a
### Phase 02: Two
**Goal:** b
`;
/**
* Phase 01 complete (PLAN + SUMMARY + a passing `*-VERIFICATION.md`),
* phase 02 planned (PLAN only).
*
* #3186 (ADR-3180 §7.4) made completion disk-strict: a fully summarized
* phase is still partial until its verification verdict passes. The legacy
* twin uses the same convention-agnostic completion predicate.
*/
const mkDirs = (specs) => {
for (const [dir, stem, complete] of specs) {
const q = path.join(tmpDir, '.planning', 'phases', dir);
fs.mkdirSync(q, { recursive: true });
fs.writeFileSync(path.join(q, `${stem}-01-PLAN.md`), '# plan\n', 'utf-8');
if (complete) {
fs.writeFileSync(path.join(q, `${stem}-01-SUMMARY.md`), '# summary\n', 'utf-8');
fs.writeFileSync(
path.join(q, `${stem}-VERIFICATION.md`), '---\nstatus: passed\n---\n# Verification\n', 'utf-8');
}
}
};
const diskShape = () => analyze().phases.map(
p => [p.number, p.disk_status, p.plan_count, p.summary_count]);
const BRK_DIRS = [['MSD.02-01-one', 'MSD.02-01', true], ['MSD.02-02-two', 'MSD.02-02', false]];
const LEG_DIRS = [['01-one', '01', true], ['02-two', '02', false]];
test('bracket dirs report complete/planned with real counts — not no_directory/0/0', () => {
write(BRK, 'bracket');
mkDirs(BRK_DIRS);
assert.deepEqual(diskShape(), [['01', 'complete', 1, 1], ['02', 'planned', 1, 0]]);
});
test('and that shape equals its flat-legacy twin exactly', () => {
write(BRK, 'bracket');
mkDirs(BRK_DIRS);
const bracket = diskShape();
cleanup(tmpDir);
tmpDir = createTempProject('adr-612-analyze-dir-leg-');
write(LEG, undefined);
mkDirs(LEG_DIRS);
const legacy = diskShape();
assert.deepEqual(bracket, legacy, 'bracket must read the disk exactly as the legacy twin does');
assert.deepEqual(legacy, [['01', 'complete', 1, 1], ['02', 'planned', 1, 0]],
'and the twin is the right answer, not a shared wrong one');
});
test('a bracket repo carrying LEGACY-shaped dirs still resolves (the read is additive)', () => {
// This shape resolved even with the two-argument call, which is why the bug
// was invisible: it failed ONLY for the canonical bracket directory name.
write(BRK, 'bracket');
mkDirs(LEG_DIRS);
assert.deepEqual(diskShape(), [['01', 'complete', 1, 1], ['02', 'planned', 1, 0]]);
});
test('a NON-bracket repo is unaffected by the threaded convention', () => {
write(LEG, undefined);
mkDirs(LEG_DIRS);
assert.deepEqual(diskShape(), [['01', 'complete', 1, 1], ['02', 'planned', 1, 0]]);
});
});
// ─────────────────────────────────────────────────────────────────────────────
// R4-m1 — the CHECKLIST scan in buildRoadmapPhaseVariants.
//
// The heading scan is sentinel-aware; its checklist twin was compiled
// non-capturing and called every token REAL. The occurrence-aware un-suppression
// loop then deleted the icebox token the heading scan had correctly marked
// sentinel, and `validate consistency` warned that a bracket ICEBOX phase had no
// directory — in the HOUSE ROADMAP shape, where the icebox appears as both a
// bold bullet and a detail heading. `validate health` stayed silent on the same
// repo, so the two verbs disagreed.
//
// Both directions are pinned here: the icebox must be silent, AND a REAL phase
// carrying the same token must still warn. A fix that over-suppresses fails the
// second test.
// ─────────────────────────────────────────────────────────────────────────────
describe('#612 PR-2: a bracket sentinel in the CHECKLIST index is suppressed too', () => {
beforeEach(() => { tmpDir = createTempProject('adr-612-checklist-sent-'); });
afterEach(() => { cleanup(tmpDir); });
const warnings = () => {
const r = runMsdTools(['validate', 'consistency'], tmpDir);
return consistencyMessages(r).filter(w => /no directory on disk/.test(w));
};
const realTwoOnDisk = () => fs.mkdirSync(
path.join(tmpDir, '.planning', 'phases', 'MSD.02-02-real-two'), { recursive: true });
test('house shape: icebox as BOTH a bold bullet and a heading is silent', () => {
write(`# Roadmap
## [MSD.02] v2.0: Current
- [ ] **[MSD.999] 01: Icebox item**
- [ ] **[MSD.02] 02: Real two**
### [MSD.999] 01: Icebox item
**Goal:** z
### [MSD.02] 02: Real two
**Goal:** b
`, 'bracket');
realTwoOnDisk();
assert.deepEqual(warnings(), [], 'an icebox phase legitimately has no directory');
});
test('CONTROL: the same ROADMAP without the icebox lines is also silent', () => {
// Makes the assertion above non-vacuous: silence must come from suppression,
// not from the repo having nothing to say.
write(`# Roadmap
## [MSD.02] v2.0: Current
- [ ] **[MSD.02] 02: Real two**
### [MSD.02] 02: Real two
**Goal:** b
`, 'bracket');
realTwoOnDisk();
assert.deepEqual(warnings(), []);
});
test('a REAL phase sharing the sentinel token still warns (no over-suppression)', () => {
// The opposite direction. A checklist bullet for a REAL `[MSD.02] 01` must
// un-suppress the token that the `[MSD.999] 01` heading marked sentinel.
write(`# Roadmap
## [MSD.02] v2.0: Current
- [x] **[MSD.02] 01: Real one**
- [ ] **[MSD.02] 02: Real two**
### [MSD.999] 01: Icebox item
**Goal:** z
### [MSD.02] 02: Real two
**Goal:** b
`, 'bracket');
realTwoOnDisk();
const w = warnings();
assert.equal(w.length, 1, JSON.stringify(w));
assert.match(w[0], /Phase 01/);
});
});
// ─── Adversarial malformed bracket tokens ───────────────────────────────────
/**
* A tolerant reader's worst input is not a well-formed id it should reject —
* that is what the emit-grammar tests above cover — but a token that is
* STRUCTURALLY broken: the milestone is not a number, the bracket never closes,
* or a bracket is nested inside another. Each one is a plausible hand-edit or a
* half-finished migration, and each reaches every widened reader in this PR.
*
* The contract is the same for all three: no phantom phase, no throw, and the
* answer is IDENTICAL to what a non-opted-in repo gives, because none of these
* is in the emit grammar. A malformed bracket must not be "partly" read — a
* reader that recovers the `01` out of `[MSD.02] 01:` but not out of
* `[MSD.AB] 01:` is fine; one that recovers it from BOTH has invented a phase
* the ROADMAP does not declare.
*/
describe('#612 PR-2: malformed bracket tokens produce no phantom phase', () => {
const validate = require('../msd-core/bin/lib/validate.cjs');
beforeEach(() => { tmpDir = createTempProject('adr-612-malformed-'); });
afterEach(() => { cleanup(tmpDir); });
const MALFORMED = [
['non-numeric milestone', '### [MSD.AB] 01: Broken'],
['unclosed bracket', '### [MSD.02 01: Broken'],
['nested bracket', '### [MSD.[02]] 01: Broken'],
['empty bracket', '### [] 01: Broken'],
['bracket with no dot', '### [MSD02] 01: Broken'],
['dot but empty milestone', '### [MSD.] 01: Broken'],
['milestone is a float', '### [MSD.0.2] 01: Broken'],
['negative milestone', '### [MSD.-2] 01: Broken'],
['whitespace milestone', '### [MSD. ] 01: Broken'],
['double dot', '### [MSD..02] 01: Broken'],
];
for (const [label, heading] of MALFORMED) {
test(`${label} is read as a phase by NO convention`, () => {
const doc = `### Phase 5: Real work\n${heading}\n`;
const opted = [...validate.buildRoadmapPhaseVariants(doc, 'bracket').roadmapPhases].sort();
const legacy = [...validate.buildRoadmapPhaseVariants(doc).roadmapPhases].sort();
assert.deepEqual(opted, ['5'], `${label}: bracket repo invented a phase — ${JSON.stringify(opted)}`);
assert.deepEqual(opted, legacy, `${label}: opted-in and legacy repos must agree`);
});
}
test('the malformed corpus reaches roadmap analyze without inventing a phase', () => {
write(`# Roadmap
## [MSD.02] v2.0: Current
### [MSD.02] 01: Real one
**Goal:** a
${MALFORMED.map(([, h]) => `${h}\n**Goal:** x\n`).join('\n')}`, 'bracket');
assert.deepEqual(analyze().phases.map(p => p.number), ['01']);
});
test('a malformed bracket DIRECTORY is not recognized, and never throws', () => {
const dirs = [
'MSD.AB-01-broken', 'MSD.02-01-ok', 'MSD.[02]-01-broken', 'MSD.-01-broken',
'MSD..02-01-broken', '.02-01-broken', 'MSD.02--01-broken', 'MSD.0.2-01-broken',
];
for (const dir of dirs) {
for (const convention of [undefined, null, 'milestone-prefixed', 'bracket']) {
assert.doesNotThrow(() => validate.isPhaseDirName(dir, convention), `${dir} / ${convention}`);
assert.doesNotThrow(() => validate.phaseTokenFromDir(dir, convention), `${dir} / ${convention}`);
}
if (dir === 'MSD.02-01-ok') continue;
assert.equal(validate.isPhaseDirName(dir, 'bracket'), false, `${dir} must not be a bracket dir`);
}
assert.equal(validate.isPhaseDirName('MSD.02-01-ok', 'bracket'), true, 'control');
});
test('the not-started variant builder agrees with the roadmap builder on the corpus', () => {
// Two builders, one grammar. If only one widens, `validate consistency`
// reports a phase the health check does not — the #3242 Bug B shape.
for (const [label, heading] of MALFORMED) {
const doc = `- [ ] **Phase 5: Real**\n${heading.replace('### ', '- [ ] **')}**\n`;
const a = [...validate.buildNotStartedPhaseVariants(doc, 'bracket')].sort();
const b = [...validate.buildNotStartedPhaseVariants(doc)].sort();
assert.deepEqual(a, b, `${label}: the two conventions disagreed — ${JSON.stringify([a, b])}`);
}
});
// #2761 M2 (trek-e review): this measured `process.hrtime.bigint()` against a
// 1s ceiling — an assertion about the host machine, not the SUT, and a flake
// on a loaded CI runner (RULESET.TESTS.no-timing-assertion). The property it
// guarded is kept and stated ALGORITHMICALLY instead: every widened pattern
// is built from BRACKET_ID_SRC, whose milestone field is a bounded
// alternation, so the classic ReDoS shape (nested quantifiers over a long
// unclosed bracket) must be LINEAR in input length. Running each attack at 1x
// and 4x and requiring byte-identical results tests exactly that — a
// catastrophically backtracking matcher cannot complete the 4x leg under any
// ceiling, whereas a bounded one is indifferent to the scaling. The `timeout`
// option is a hang backstop, not an assertion.
// Each attack states the phases its reading must name, as a function of n.
// Four are malformed and must name none; the fifth is well-formed but
// oversized, and must name exactly its (n-digit) token — a reading that is
// linear in n BY CONSTRUCTION, which is the property under test.
const ATTACKS = [
['unclosed bracket code', (n) => `### [${'A'.repeat(n)} 01: x`, () => []],
['unclosed bracket numeric', (n) => `### [MSD.${'0'.repeat(n)} 01: x`, () => []],
['nested open brackets', (n) => `### ${'['.repeat(Math.floor(n / 2.5))}MSD.02] 01: x`, () => []],
['repeated bracket group', (n) => `### [${'A.02] ['.repeat(Math.floor(n / 5))}A.02] 01: x`, () => []],
['oversized milestone + token', (n) => `### [MSD.${'9'.repeat(n)}] ${'1'.repeat(n)}: x`, (n) => ['1'.repeat(n)]],
];
const readAll = (doc) => {
const r = validate.buildRoadmapPhaseVariants(doc, 'bracket');
return {
phases: [...r.roadmapPhases].sort(),
variantCount: r.roadmapPhaseVariants.size,
sentinels: [...r.sentinelPhases].sort(),
notStarted: [...validate.buildNotStartedPhaseVariants(doc, 'bracket')].sort(),
isDir: validate.isPhaseDirName(doc, 'bracket'),
token: validate.phaseTokenFromDir(doc, 'bracket'),
};
};
test('pathological bracket input reads correctly at 1x and 4x length', { timeout: PATHOLOGICAL_INPUT_TEST_TIMEOUT_MS }, () => {
for (const [label, doc, expectedPhases] of ATTACKS) {
const readings = [5000, 20000].map((n) => [n, readAll(doc(n))]);
for (const [n, r] of readings) {
assert.deepEqual(r.phases, expectedPhases(n), `${label} @${n}: wrong phase set`);
assert.deepEqual(r.sentinels, [], `${label} @${n}: named a sentinel`);
assert.deepEqual(r.notStarted, [], `${label} @${n}: named a not-started phase`);
assert.equal(r.isDir, false, `${label} @${n}: a heading is not a phase directory`);
assert.equal(r.token, null, `${label} @${n}: a heading yields no directory token`);
}
// Scale invariance of the STRUCTURE: quadrupling the input may lengthen
// an extracted token (linear), but must not change how many things the
// reading names. A backtracking regression never reaches this line.
assert.equal(
readings[0][1].variantCount, readings[1][1].variantCount,
`${label}: quadrupling the input changed the variant cardinality`,
);
}
});
test('control: the same readers DO extract a well-formed bracket heading', () => {
// Non-vacuity guard — "names no phase" above must mean "the input is
// malformed", not "these readers are inert".
assert.deepEqual(readAll('### [MSD.02] 01: Real work').phases, ['01']);
assert.equal(validate.isPhaseDirName('MSD.02-01-real-work', 'bracket'), true);
assert.equal(validate.phaseTokenFromDir('MSD.02-01-real-work', 'bracket'), '01');
});
});
// ─────────────────────────────────────────────────────────────────────────────
describe('#612 PR-2: state validate resolves bracket phase DIRECTORIES', () => {
beforeEach(() => { tmpDir = createTempProject('adr-612-validate-dir-'); });
afterEach(() => { cleanup(tmpDir); });
/**
* #3208 (merged to next as part of "resolve active state phase before drift
* scan") rewrote cmdStateValidate's directory lookup from a `startsWith`
* prefix test to the canonical `phaseKeyFromDir(...) === selectedPhaseKey`
* comparison. That is the correct surface — and it is exactly why the lookup
* now needs the resolved convention, which the rewrite does not pass.
*
* `phaseKeyFromDir` deliberately refuses to read a bracket directory without
* an explicit signal (ADR-2121: a bracket dir is string-indistinguishable
* from the legacy letter-prefixed-decimal family), so un-threaded it returns
* the whole dir name as the key — `MSD.02-05-real-work` -> `MSD.02-5-REAL-WORK`
* — while the STATE side is the bare `05` that `parsePhaseFromProse` yields.
* Both sides of one comparison derived under different conventions is #2562's
* defect class, and this file's other three `phaseKeyFromDir` call sites
* already thread against it.
*
* Observable symptom without the thread: a bracket repo whose phase directory
* plainly exists reports `valid: false` and "no phase directory matches phase
* 05" — wrong-and-confident on precisely the repos the convention supports,
* and drift detection (plan-count mismatch, verification status) never runs.
*
* The legacy twin below is the byte-identity control: threading a non-bracket
* convention must change nothing, since `extractPhaseToken` branches only on
* `=== 'bracket'`.
*/
const BRK = `# Roadmap
## [MSD.02] v2.0: Current
### [MSD.02] 05: Real work
**Goal:** a
`;
const LEG = `# Roadmap
## v2.0: Current
### Phase 05: Real work
**Goal:** a
`;
// STATE.md asserts 3 plans; disk carries 1. The drift warning is the PROOF
// the scan actually ran — a lookup that fails to find the directory returns
// before it, so "no plan_count drift reported" and "drift never ran" are
// distinguishable here rather than both reading as silence.
const STATE = `---
msd_state_version: '1.0'
milestone: v2.0
current_phase: '05'
status: executing
total_plans_in_phase: 3
---
# Project State
## Current Position
**Phase:** 05 — Real work
`;
const seed = (roadmap, convention, dir) => {
write(roadmap, convention);
fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), STATE, 'utf-8');
const q = path.join(tmpDir, '.planning', 'phases', dir);
fs.mkdirSync(q, { recursive: true });
fs.writeFileSync(path.join(q, '05-01-PLAN.md'), '# plan\n', 'utf-8');
};
// #3884 (e20744eac, "failure is a value") made argv strict: an unrecognized
// flag is now a hard `unknown flag ...; accepted: --strict` error on stderr
// with EMPTY stdout, where it was previously ignored. `state validate` never
// had a `--json` flag — JSON is its only output shape — so the token was a
// silent no-op that strict argv now rejects. Dropping it restores the same
// envelope this helper already parsed; the assertions below are unchanged.
const validateState = () => {
const r = runMsdTools(['state', 'validate'], tmpDir);
return JSON.parse(r.output);
};
// #3310 (ADR-3180 Phase 12) replaced `cmdStateValidate`'s `drift`
// object with coded `warnings: Diagnostic[]`: phase-directory misses are
// S004 and plan-count drift is S005. Keep these tests on their behavior-level
// subject while reading the current upstream envelope.
const codes = (out, code) =>
(out.warnings || []).filter((w) => w.code === code).map((w) => w.message);
const phaseDirWarnings = (out) =>
codes(out, 'S004').filter((m) => /no phase directory matches/.test(m));
const planCountWarnings = (out) => codes(out, 'S005');
test('a bracket phase directory is FOUND — no phantom "no phase directory matches"', () => {
seed(BRK, 'bracket', 'MSD.02-05-real-work');
const out = validateState();
assert.deepEqual(
phaseDirWarnings(out),
[],
`the directory exists and must resolve; an S004 phase-directory warning means the lookup missed it, got: ${JSON.stringify(out.warnings)}`,
);
});
test('and the drift scan actually RUNS — plan-count mismatch is reported', () => {
seed(BRK, 'bracket', 'MSD.02-05-real-work');
const out = validateState();
assert.deepEqual(
planCountWarnings(out),
['Plan count mismatch: STATE.md says 3 plans, disk has 1'],
'resolving the directory must let the plan-count drift check run',
);
});
test('and that answer equals its flat-legacy twin exactly', () => {
seed(BRK, 'bracket', 'MSD.02-05-real-work');
const bracket = validateState();
cleanup(tmpDir);
tmpDir = createTempProject('adr-612-validate-dir-leg-');
seed(LEG, undefined, '05-real-work');
const legacy = validateState();
assert.deepEqual(bracket.warnings, legacy.warnings,
'bracket must validate exactly as the legacy twin does');
assert.deepEqual(
planCountWarnings(legacy),
['Plan count mismatch: STATE.md says 3 plans, disk has 1'],
'and the twin is the right answer, not a shared wrong one',
);
});
test('a NON-bracket repo is unaffected by the threaded convention', () => {
seed(LEG, undefined, '05-real-work');
const out = validateState();
assert.deepEqual(phaseDirWarnings(out), []);
assert.deepEqual(
planCountWarnings(out),
['Plan count mismatch: STATE.md says 3 plans, disk has 1'],
);
});
test('a genuinely absent phase directory still reports not_found on bracket', () => {
// Non-vacuity guard: the thread must not make the lookup match ANYTHING.
seed(BRK, 'bracket', 'MSD.02-09-unrelated');
const out = validateState();
assert.equal(phaseDirWarnings(out).length, 1, JSON.stringify(out.warnings));
assert.equal(out.valid, false);
});
});
// ─── The #3309/#3310 re-homing: the reads that moved into the rule table ───
/**
* #3309/#3310 migrated `cmdValidateHealth` and `cmdValidateConsistency` onto the
* health-diagnostic rule table and deleted every helper this PR threaded —
* `collectDiskPhases`, `collectDiskPhaseEntries`, `collectArchivedPhaseDirNames`,
* `forEachArchivedPhaseToken`. Their reads now live in
* `src/planning-snapshot.cts` and the reads' CONSUMERS in
* `src/health-diagnostic-rules/*.cts`, so this PR's convention threading moved
* with them.
*
* Three of those re-homed sites turned out to be unfalsifiable by the rest of
* this suite once relocated: reverting the convention argument changed real CLI
* output and NOT ONE existing test went red. The pre-migration sites were
* covered indirectly, through helpers that no longer exist. Each case below
* pins one of them at the CLI, with its flat-legacy twin as the byte-identity
* control, so the threading is falsifiable in its new home rather than trusted.
*
* The fourth re-homed site — `buildValidPhaseSet`'s `extractPhaseToken(dir,
* convention)` in W002 (`state-consistency.cts`) — is deliberately NOT pinned
* here, because it is not observable: that rule unions the disk tokens with
* `roadmapDeclaredPhases` and `archivedPhaseTokens`, and any STATE.md reference
* a bracket disk token would rescue is already rescued by the ROADMAP half.
* It is threaded for provenance (it restores `collectDiskPhases(planBase,
* convention)`'s derivation exactly) and is disclosed as droppable rather than
* given a test that cannot fail for the right reason.
*/
describe('#612 PR-2: the re-homed W006/W007 reads still select by convention', () => {
beforeEach(() => { tmpDir = createTempProject('adr-612-rehome-'); });
afterEach(() => { cleanup(tmpDir); });
const healthCodes = (code) => {
const out = JSON.parse(runMsdTools(['validate', 'health'], tmpDir).output);
return [...(out.errors || []), ...(out.warnings || [])]
.filter(i => i.code === code)
.map(i => i.message);
};
const mkdir = (...parts) =>
fs.mkdirSync(path.join(tmpDir, '.planning', ...parts), { recursive: true });
// ── archivedPhaseTokens (planning-snapshot.cts) ──────────────────────────
// Pre-migration this was `forEachArchivedPhaseToken(planBase, cb, convention)`
// feeding W006's existence check. `PHASE_TOKEN_FROM_DIR_RE` rejects
// `MSD.02-05-shipped` outright, so un-threaded the archive is invisible and a
// phase whose only directory was archived draws a W006 it should not.
test('an ARCHIVED bracket phase directory satisfies W006', () => {
mkdir('milestones', 'v1.0-phases', 'MSD.02-05-shipped');
mkdir('phases');
write(`# Roadmap
## [MSD.02] v2.0
### [MSD.02] 05: Shipped work
**Goal:** a
`, 'bracket');
assert.deepEqual(healthCodes('W006'), [],
'the phase\'s directory lives in a milestone archive — that is not "no directory on disk"');
});
test('CONTROL: the flat-legacy twin of the same repo is equally silent', () => {
mkdir('milestones', 'v1.0-phases', '05-shipped');
mkdir('phases');
write(`# Roadmap
## v2.0
### Phase 05: Shipped work
**Goal:** a
`, undefined);
assert.deepEqual(healthCodes('W006'), []);
});
test('CONTROL: with the archive removed the same bracket repo DOES warn', () => {
// Non-vacuity: the silence above must come from the archive being read,
// not from W006 being unable to fire on this fixture at all.
mkdir('phases');
write(`# Roadmap
## [MSD.02] v2.0
### [MSD.02] 05: Shipped work
**Goal:** a
`, 'bracket');
const w = healthCodes('W006');
assert.equal(w.length, 1, JSON.stringify(w));
assert.match(w[0], /Phase 05/);
});
// ── roadmapPhaseCheckboxes (planning-snapshot.cts) ───────────────────────
// Pre-migration this exclusion came from `buildNotStartedPhaseVariants(
// roadmapContent, convention)`; the rule table replaced that call with the
// `roadmapPhaseCheckboxes` field. Un-threaded, a bracket repo's `- [ ]`
// bullets never parse, so every not-yet-started bracket phase draws a W006.
test('an UNCHECKED bracket checklist bullet excludes its phase from W006', () => {
mkdir('phases');
write(`# Roadmap
## [MSD.02] v2.0
- [ ] **[MSD.02] 09: Not started**
### [MSD.02] 09: Not started
**Goal:** a
`, 'bracket');
assert.deepEqual(healthCodes('W006'), [],
'an unstarted phase legitimately has no directory yet');
});
test('CONTROL: the flat-legacy twin of the same repo is equally silent', () => {
mkdir('phases');
write(`# Roadmap
## v2.0
- [ ] **Phase 09: Not started**
### Phase 09: Not started
**Goal:** a
`, undefined);
assert.deepEqual(healthCodes('W006'), []);
});
test('CONTROL: a CHECKED bracket bullet is not excluded', () => {
// Non-vacuity in the direction that matters: the exclusion must read the
// checkbox STATE, not merely the presence of a bracket bullet.
mkdir('phases');
write(`# Roadmap
## [MSD.02] v2.0
- [x] **[MSD.02] 09: Claimed complete**
### [MSD.02] 09: Claimed complete
**Goal:** a
`, 'bracket');
const w = healthCodes('W006');
assert.equal(w.length, 1, JSON.stringify(w));
assert.match(w[0], /Phase 09/);
});
// ── W007's token (roadmap-disk-consistency.cts) ──────────────────────────
// Pre-migration W007 iterated `collectDiskPhaseEntries`' keys, which were
// built by the convention-aware extractor; the migrated rule iterates
// directory NAMES and tokenizes per entry. Un-threaded it reports the whole
// directory name as the phase id.
test('an ORPHAN bracket directory is reported by its phase token, not its dir name', () => {
mkdir('phases', 'MSD.02-77-orphan');
write(`# Roadmap
## [MSD.02] v2.0
### [MSD.02] 05: Real
**Goal:** a
`, 'bracket');
const w = healthCodes('W007');
assert.equal(w.length, 1, JSON.stringify(w));
assert.match(w[0], /^Phase 77 exists on disk/,
'un-threaded this reads "Phase MSD.02-77-orphan exists on disk"');
});
test('CONTROL: the flat-legacy twin names its token the same way', () => {
mkdir('phases', '77-orphan');
write(`# Roadmap
## v2.0
### Phase 05: Real
**Goal:** a
`, undefined);
const w = healthCodes('W007');
assert.equal(w.length, 1, JSON.stringify(w));
assert.match(w[0], /^Phase 77 exists on disk/);
});
});
// ─────────────────────────────────────────────────────────────────────────────
// The #3165 TRUNCATED-WINDOW FALLBACK — `roadmap analyze`'s second reading pass.
//
// #3428 (merged in at next @ 483083ae) gave `cmdRoadmapAnalyze` a recovery path:
// when the scoped milestone window is suspect (non-COMPLETE scope), found zero
// phases, and phase directories exist on disk, it re-scans the
// shipped-milestone-stripped document through the SAME extracted enrichment
// (`collectAnalyzePhases`). That extraction turned every seam this PR threads
// into a TWO-call-site seam, and the second site was invisible to the suite:
// passing a null convention there — while the scoped site stayed threaded —
// was green across all 518 tests of the bracket, roadmap and milestone-window
// files. Every pre-existing bracket assertion reaches the enrichment through
// the SCOPED window, so none of them can see the fallback's reading at all.
//
// The shape below is the one that reaches it on a bracket repo: a MID-MIGRATION
// ROADMAP whose ACTIVE milestone is bracketed, whose phase-detail sections sit
// after an intervening CLOSED legacy milestone (so the window closes over prose
// only), and which still carries one legacy `### Phase N:` section of its own
// (so `hasPhaseEntries` — a convention-BLIND reader, see the disclosure below —
// sees the document as containing phases and the window as not, which is what
// classifies the window TRUNCATED).
//
// DISCLOSURE, measured and deliberately NOT fixed here: `hasPhaseEntries`
// (src/roadmap-parser.cts) is one of the convention-less readers this PR does
// not widen. On a PURE-bracket ROADMAP it finds no phase entries anywhere, so
// `classifyMilestoneWindow`'s row 8 cannot fire and the window classifies
// COMPLETE. The consequence is larger than a scope label: on the same layout
// as below but with no legacy section — bracket milestone, bracket phase
// headings after a closed milestone, canonical bracket directories on disk —
// `roadmap analyze` returns
// `{"scope":"complete","phase_count":0,"next_phase":null,"phases":[]}` where
// the flat-legacy twin of that document returns
// `{"scope":"truncated","phase_count":2,...}`. `scope: "complete"` with
// `phase_count: 0` is the "genuinely empty milestone" answer — precisely the
// indistinguishability #3184 introduced `scope` to remove — and
// `workflows/next.md` Route 0 iterates `.phases[]`, so the safety invariant
// #3165 exists to protect silently does not run, on the convention this epic
// is for.
//
// Not fixed in a merge round: widening `hasPhaseEntries` changes the value of
// the upstream-owned `scope` FIELD on bracket repos, which is a behaviour
// slice (the PR-2.5/PR-4 convention-less-readers question), not a conflict
// resolution. The mid-migration shape below is the reachable half — and it is
// the half a repo actually mid-migration has.
// ─────────────────────────────────────────────────────────────────────────────
describe('#612 PR-2: the #3165 truncated-window fallback reads bracket phases too', () => {
beforeEach(() => { tmpDir = createTempProject('adr-612-analyze-fallback-'); });
afterEach(() => { cleanup(tmpDir); });
const writeWithMilestone = (roadmap, convention) => {
write(roadmap, convention);
fs.writeFileSync(
path.join(tmpDir, '.planning', 'STATE.md'), '---\nmilestone: v2.0\n---\n', 'utf-8');
};
// The checklist bullets sit BESIDE the detail headings, past the closed
// milestone heading, deliberately: inside the window they would give the
// window phase entries of its own, `classifyMilestoneWindow` would call it
// COMPLETE, and the recovery path this block is about would never fire.
const BRK = `# Roadmap
## [MSD.02] v2.0 Current 🚧
Active milestone prose.
## v1.0 Old ✅ SHIPPED
### Phase 99: Legacy leftover
**Goal:** z
- [ ] **[MSD.02] 01: One**
- [ ] **[MSD.02] 02: Two**
### [MSD.02] 01: One
**Goal:** a
### [MSD.02] 02: Two
**Goal:** b
`;
const LEG = `# Roadmap
## v2.0 Current 🚧
Active milestone prose.
## v1.0 Old ✅ SHIPPED
### Phase 99: Legacy leftover
**Goal:** z
- [ ] **Phase 01: One**
- [ ] **Phase 02: Two**
### Phase 01: One
**Goal:** a
### Phase 02: Two
**Goal:** b
`;
const mkDirs = (specs) => {
for (const [dir, stem, complete] of specs) {
const q = path.join(tmpDir, '.planning', 'phases', dir);
fs.mkdirSync(q, { recursive: true });
fs.writeFileSync(path.join(q, `${stem}-01-PLAN.md`), '# plan\n', 'utf-8');
if (complete) {
fs.writeFileSync(path.join(q, `${stem}-01-SUMMARY.md`), '# summary\n', 'utf-8');
fs.writeFileSync(
path.join(q, `${stem}-VERIFICATION.md`), '---\nstatus: passed\n---\n# Verification\n', 'utf-8');
}
}
};
const BRK_DIRS = [['MSD.02-01-one', 'MSD.02-01', true], ['MSD.02-02-two', 'MSD.02-02', false]];
const LEG_DIRS = [['01-one', '01', true], ['02-two', '02', false]];
const diskShape = (a) => a.phases.map(p => [p.number, p.disk_status, p.plan_count, p.summary_count]);
const EXPECTED = [
['99', 'no_directory', 0, 0],
['01', 'complete', 1, 1],
['02', 'planned', 1, 0],
];
test('the fallback is what supplies the phases here — without dirs on disk it stays gated off', () => {
// Non-vacuity: the SCOPED window contributes nothing on this document. With
// the on-disk evidence removed, #3428's own precondition fails and the
// result is the empty one the recovery path exists to replace. Any later
// assertion about `disk_status` below is therefore an assertion about the
// FALLBACK's reading, not the scoped scan's.
writeWithMilestone(BRK, 'bracket');
const a = analyze();
assert.equal(a.scope, 'truncated', 'the window must be suspect or nothing recovers');
assert.equal(a.phase_count, 0, 'the scoped window genuinely finds no phases');
});
test('bracket phases recovered by the fallback resolve their DIRECTORIES', () => {
writeWithMilestone(BRK, 'bracket');
mkDirs(BRK_DIRS);
const a = analyze();
assert.equal(a.scope, 'truncated', 'still flagged best-effort, per #3165');
assert.deepEqual(diskShape(a), EXPECTED,
'un-threaded, the fallback does not match the bracket headings at all: phase_count 1');
});
test('and the recovered phases are not then reported as missing detail sections', () => {
// #2761 M1 x #3165: `detailKeys` — the occurrence index the checklist scan
// is filtered against — must be swapped for the FALLBACK scan's, in the
// same breath as `phases` and `effectiveContent`. Left holding the scoped
// scan's (empty) index while the checklist scan reads the fallback
// document, every phase the recovery just found is reported as a checklist
// entry with no detail section: `missing_phase_details: ["01","02"]`,
// wrong-and-confident on exactly the output #3165 exists to populate.
writeWithMilestone(BRK, 'bracket');
mkDirs(BRK_DIRS);
const a = analyze();
assert.equal(a.phase_count, 3, 'guard: the recovery fired, so this is the fallback index');
assert.equal(a.missing_phase_details, null,
'every checklist bullet has a detail heading in the same document');
});
test('CONTROL: a checklist entry with NO detail heading is still reported', () => {
// The companion direction: a fix that simply suppressed the report would
// pass the test above. A bullet with no heading anywhere must still surface.
writeWithMilestone(BRK.replace(
'- [ ] **[MSD.02] 02: Two**',
'- [ ] **[MSD.02] 02: Two**\n- [ ] **[MSD.02] 07: Never written**'), 'bracket');
mkDirs(BRK_DIRS);
assert.deepEqual(analyze().missing_phase_details, ['07']);
});
test('CONTROL: and that shape equals its flat-legacy twin exactly', () => {
writeWithMilestone(BRK, 'bracket');
mkDirs(BRK_DIRS);
const bracket = diskShape(analyze());
cleanup(tmpDir);
tmpDir = createTempProject('adr-612-analyze-fallback-leg-');
writeWithMilestone(LEG, undefined);
mkDirs(LEG_DIRS);
const legacy = diskShape(analyze());
assert.deepEqual(bracket, legacy,
'the fallback must read a bracket document exactly as it reads the legacy twin');
assert.deepEqual(legacy, EXPECTED, 'and the twin is the right answer, not a shared wrong one');
});
test('CONTROL: a NON-bracket repo takes the same path and is unaffected', () => {
writeWithMilestone(LEG, undefined);
mkDirs(LEG_DIRS);
assert.deepEqual(diskShape(analyze()), EXPECTED);
});
});