Files
msd-core/tests/adr-612-bracket-phase-counting.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

3419 lines
138 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
// docs-guard-exempt: this file performs no filesystem read of any docs/ path.
// Its only docs/ occurrence is the ADR-612 Decision 1 citation inside a line
// comment further down; every read call here targets a tmpdir .planning
// fixture. It trips Detector 3 because an odd prose backtick in the comment
// block above that citation opens a span the template-literal heuristic reads
// as a literal containing the citation. Registering it in the docs-guard lane
// would run this suite on docs/ changes whose content it never reads, so the
// marker is the correct side of that trade per this lint's own guidance.
/**
* PR-2 (#2761 / epic #612) — total_phases counts bracket phase headings.
*
* `total_phases` is derived TWICE: buildStateFrontmatter feeds `state json`, and
* cmdStateSync feeds `state sync`. The second carries the comment "Mirrors the
* logic in buildStateFrontmatter so both report consistent percents (#3242 Bug
* B)". Teaching one and not the other ships that divergence.
*
* TWO ORACLE HAZARDS this file is shaped around:
*
* 1. #1446 removed total_phases from the ratchet, so it corrects DOWNWARD
* silently. Every assertion here is an EXACT number; "no error" or ">= n"
* passes straight through the bug.
*
* 2. Reading `state json` after `state sync` measures the READ derivation
* twice — sync leaves STATE.md untouched when its computed total already
* matches, so the write-path guard is never observed and a mutation to it
* survives. The sync assertions below pre-write a WRONG total into the
* frontmatter, run sync, and then read THE FILE, so the number asserted is
* the one sync actually wrote.
*/
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 phaseIdHelpers = require('../msd-core/bin/lib/phase-id.cjs');
let tmpDir;
const stateMd = () => [
'---',
'msd_state_version: 1.0',
'milestone: v2.0',
'milestone_name: Expansion',
'status: executing',
'---',
'',
'# Project State',
'',
'**Phase:** 05',
'',
// The body Progress line is what makes sync derive and WRITE the progress
// block; without it sync has nothing to update and STATE.md is left untouched.
'**Progress:** [░░░░░░░░░░] 0%',
'',
].join('\n');
function writeProject(roadmap, convention, dirs = ['MSD.02-01-setup']) {
const planning = path.join(tmpDir, '.planning');
fs.writeFileSync(path.join(planning, 'ROADMAP.md'), roadmap, 'utf-8');
fs.writeFileSync(path.join(planning, 'STATE.md'), stateMd(), 'utf-8');
fs.writeFileSync(
path.join(planning, 'config.json'),
JSON.stringify(convention === undefined ? {} : { phase_id_convention: convention }), 'utf-8',
);
// `state sync` short-circuits with no phase directories, leaving STATE.md
// byte-unchanged — which is exactly how a mutation to the write-path counter
// survives a test that reads `state json` afterwards. Give it real work.
// #3186 made completion disk-strict: a PLAN and SUMMARY no longer imply a
// completed phase without a passing verification verdict. Keep these fixtures
// modeling the complete phases they modeled before that upstream change.
// A dir spec is either `'name'` (a PLAN, SUMMARY, and passing verification)
// or `['name', false]` (a PLAN alone — incomplete). The mixed fixtures need
// both shapes to distinguish completed_phases from total_phases.
// Write verification last so it cannot be stale relative to the summary.
for (const spec of dirs) {
const [d, complete = true] = Array.isArray(spec) ? spec : [spec, true];
const dir = path.join(planning, 'phases', d);
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(path.join(dir, '01-01-x-PLAN.md'), '# plan\n', 'utf-8');
if (complete) {
fs.writeFileSync(path.join(dir, '01-01-x-SUMMARY.md'), '# summary\n', 'utf-8');
fs.writeFileSync(
path.join(dir, verificationNameFor(d)),
'---\nstatus: passed\n---\n# Verification\n',
'utf-8',
);
}
}
}
/**
* The verification filename cmdScaffold writes into `dir` when the phase is
* named by its unqualified token — `${normalizePhaseName(phase)}-VERIFICATION.md`
* (`src/commands.cts`): the PHASE'S OWN padded token.
*
* UPDATED at the origin/next merge (#3511): every complete dir used to get a
* hardcoded `01-VERIFICATION.md`, which was harmless while nothing scoped a
* phase directory's listing. Upstream #3511 made membership real — in any dir
* that is not phase 01 that hardcoded name is now (correctly, on BOTH
* conventions once #612 threads `convention` into the seam) a cross-phase
* stray, excluded from completion. These fixtures mean "a COMPLETE phase",
* not "a phase holding another phase's report", so they must write what the
* scaffolder writes. The stray shape is pinned deliberately, as its own
* regression block — see "#612 PR-2: a cross-phase stray in a bracket dir"
* below — so this rename cannot silently stand in for the seam fix.
*
* The padding comes from the production `normalizePhaseName` itself (single
* owner, no fixture drift). What stays fixture-local is the bracket-prefix
* strip below — deliberately: production does NOT strip `{CODE}.` prefixes
* (`normalizePhaseName('MSD.02-01')` returns it unchanged via the custom-id
* arm; scaffold echoes whatever `--phase` form it is given), so a repo's
* artifact layout is unqualified or qualified depending on how phases were
* scaffolded. This file's fixtures model the UNQUALIFIED layout; the
* QUALIFIED layout is modeled by tests/adr-612-bracket-read-tolerance.test.cjs
* and by the qualified-stem assertions in the stray-regression block below.
*/
function verificationNameFor(dir) {
// Fixture-local unqualified-layout modeling (see docstring) — not a claim
// about scaffold, and not a second grammar owner: the seam's own tests
// assert the production regex.
const rest = dir.replace(/^[A-Z][A-Z0-9_]*\.\d+-/i, '');
const token = [];
for (const seg of rest.split('-')) {
if (/^\d+(?:\.\d+)*$/.test(seg)) token.push(seg);
else break;
}
assert.ok(token.length > 0, `fixture dir ${dir} carries no phase token to name its verification`);
return `${phaseIdHelpers.normalizePhaseName(token.join('-'))}-VERIFICATION.md`;
}
/** total_phases as the READ path derives it. */
function readTotal() {
const r = runMsdTools(['state', 'json'], tmpDir);
assert.ok(r.success, `state json failed: ${r.error}`);
return JSON.parse(r.output).progress?.total_phases ?? null;
}
/**
* total_phases as the WRITE path derives it — read back out of STATE.md, not out
* of `state json`. This is the assertion that observes cmdStateSync at all.
*/
function syncedTotal() {
const r = runMsdTools(['state', 'sync'], tmpDir);
assert.ok(r.success, `state sync failed: ${r.error}`);
const raw = fs.readFileSync(path.join(tmpDir, '.planning', 'STATE.md'), 'utf-8');
const m = raw.match(/^\s*total_phases:\s*(\d+)\s*$/m);
assert.ok(m, 'state sync must have written a total_phases into STATE.md');
return parseInt(m[1], 10);
}
/**
* The PERCENT `state sync` wrote into the STATE.md body.
*
* This is the only observable of cmdStateSync's own counter. Its
* `syncTotalPhases` never reaches the frontmatter `total_phases` field — that
* one is written by the read derivation — it reaches `computeProgressPercent`
* and nothing else. Asserting the frontmatter number after a sync therefore
* measures the READ path twice and lets a mutation to the write-path counter
* survive, which is exactly how this guard shipped untested the first time.
* Percent is completed/total, so the denominator is visible here.
*/
function syncedPercent() {
const r = runMsdTools(['state', 'sync'], tmpDir);
assert.ok(r.success, `state sync failed: ${r.error}`);
const raw = fs.readFileSync(path.join(tmpDir, '.planning', 'STATE.md'), 'utf-8');
// eslint-disable-next-line local/no-unbounded-quantifier -- parses the STATE.md this test just wrote, fixed-size fixture output, not adversarial input
const m = raw.match(/^\*\*Progress:\*\*[^\r\n]*?(\d+)%/m);
assert.ok(m, `state sync must have written a Progress percent; got:\n${raw}`);
return parseInt(m[1], 10);
}
/** The explicit reason `state sync` withheld progress for an unsafe scope. */
function syncSkipReason() {
const r = runMsdTools(['state', 'sync'], tmpDir);
assert.ok(r.success, `state sync failed: ${r.error}`);
const changes = JSON.parse(r.output).changes || [];
const skipped = changes.find((change) => /^Progress: skipped/.test(change));
assert.ok(skipped, `state sync must record why Progress was skipped; got: ${JSON.stringify(changes)}`);
const raw = fs.readFileSync(path.join(tmpDir, '.planning', 'STATE.md'), 'utf-8');
assert.match(raw, /^\*\*Progress:\*\* \[░{10}\] 0%$/m,
'no percent may be rendered when sync says it skipped');
return skipped;
}
const BRACKET_ROADMAP = `# Roadmap
## [MSD.02] v2.0
### [MSD.02] 01: Setup
**Goal:** a
### [MSD.02] 05: Real work
**Goal:** b
### [MSD.02] 06: Follow-up
**Goal:** c
`;
describe('#612 PR-2: bracket phase headings enter total_phases', () => {
beforeEach(() => { tmpDir = createTempProject('adr-612-count-'); });
afterEach(() => { cleanup(tmpDir); });
test('the read path counts all three (exact)', () => {
writeProject(BRACKET_ROADMAP, 'bracket');
assert.equal(readTotal(), 3);
});
test('#3242: the WRITE path writes the same number, observed in STATE.md', () => {
// Stale total forces sync to do real work, so the number in the file is the
// one cmdStateSync computed rather than the one that was already there.
writeProject(BRACKET_ROADMAP, 'bracket');
assert.equal(syncedTotal(), 3, 'state sync must WRITE 3');
assert.equal(readTotal(), 3, 'and the read path must agree');
});
test('a NON-bracket repo counts neither form of the same roadmap', () => {
// One phase directory on disk, three bracket headings the reader cannot see:
// the total falls back to the directory count, exactly as it did before.
writeProject(BRACKET_ROADMAP, undefined);
assert.equal(readTotal(), 1, 'bracket headings are invisible without the convention');
assert.equal(syncedTotal(), 1);
});
test('a mixed legacy + bracket roadmap counts both on a bracket repo', () => {
writeProject(`# Roadmap
## v2.0
### Phase 1: Legacy one
**Goal:** a
### [MSD.02] 05: Bracket one
**Goal:** b
### Phase Overview:
`, 'bracket',
// #2761 Major 1: the default single dir ('MSD.02-01-setup') names a phase
// number ("01") this roadmap never declares — only "1" (legacy-labelled)
// and "05" (bracket) are real — so post-Major-1 the disk-side milestone
// filter correctly excludes it and `state sync` becomes a no-op (nothing
// to write). Naming the ACTUAL bracket-declared phase here keeps this
// test about what it says it's about (mixed heading-style counting), not
// an accidental side effect of the disk-scan gating fixed elsewhere.
['MSD.02-05-bracket-one']);
assert.equal(readTotal(), 2, '`Phase Overview:` still excluded');
assert.equal(syncedTotal(), 2);
});
});
describe('#612 PR-2: bracket sentinels stay OUT of both derivations', () => {
beforeEach(() => { tmpDir = createTempProject('adr-612-count-sent-'); });
afterEach(() => { cleanup(tmpDir); });
const SENTINEL_ROADMAP = `# Roadmap
## [MSD.02] v2.0
### [MSD.999] 01: Icebox item
**Goal:** a
### [MSD.00] 02: Pre-milestone
**Goal:** b
### [MSD.02] 05: Real work
**Goal:** c
### [MSD.02] 06: Follow-up
**Goal:** d
`;
test('999.x and 0.x are excluded from the READ derivation (exact)', () => {
writeProject(SENTINEL_ROADMAP, 'bracket');
assert.equal(readTotal(), 2);
});
test('999.x and 0.x are excluded from the WRITE derivation too (observed in STATE.md)', () => {
// The assertion that actually exercises cmdStateSync's sentinel guard —
// reading `state json` after sync would measure the read path a second time
// and let a mutation to the write path survive.
// One completed phase directory — named for the REAL "05" phase this
// roadmap declares (#2761 Major 1: the default dir names phase "01",
// which this roadmap never declares, and the disk-side milestone filter
// now correctly excludes an undeclared phase number from the WRITE-path
// scan too, making `state sync` a no-op on the default dir). With the
// sentinel guard the denominator is 2 (05, 06) so the percent is 50;
// without it the two sentinel headings inflate it to 4 and the percent
// drops to 25.
writeProject(SENTINEL_ROADMAP, 'bracket', ['MSD.02-05-real-work']);
assert.equal(syncedPercent(), 50, 'sentinel headings must not inflate the sync denominator');
});
test('a LOWERCASE sentinel bracket is excluded from both', () => {
const doc = SENTINEL_ROADMAP.replace(/MSD\./g, 'msd.');
writeProject(doc, 'bracket', ['msd.02-05-real-work']);
assert.equal(readTotal(), 2);
assert.equal(syncedTotal(), 2);
});
test('G5: a 999 token under a real milestone is still a backlog sentinel', () => {
writeProject(`# Roadmap
## [MSD.02] v2.0
### [MSD.02] 999: Late work
**Goal:** a
### [MSD.02] 05: Real
**Goal:** b
`, 'bracket', ['MSD.02-05-real']);
// The composed rule: bracket-sentinel OR legacy 999 token.
assert.equal(readTotal(), 1);
assert.equal(syncedTotal(), 1);
});
});
describe('#612 PR-2: #1514 retired bracket phases leave the denominator', () => {
beforeEach(() => { tmpDir = createTempProject('adr-612-retired-'); });
afterEach(() => { cleanup(tmpDir); });
// The canonical #1514 gesture, verbatim from the shipped legacy tests: strike
// the checklist BULLET and leave the detail heading intact. A bracket-form
// retirement went undetected, so the phase stayed in the denominator forever
// and a shipped bracket milestone could never reach 100%.
const retiredRoadmap = (bullet, heading) => `# Roadmap
## [MSD.02] v2.0
- [x] ${bullet} — folded into 05; number retired
- [ ] **[MSD.02] 05: Real work**
- [ ] **[MSD.02] 06: Follow-up**
${heading}
**Goal:** folded
### [MSD.02] 05: Real work
**Goal:** b
### [MSD.02] 06: Follow-up
**Goal:** c
`;
test('bullet-only strike (the canonical gesture) excludes the phase', () => {
writeProject(
retiredRoadmap('~~**[MSD.02] 04: Delta**~~', '### [MSD.02] 04: Delta'), 'bracket',
// #2761 Major 1: name one of the two REAL (non-retired) phases so the
// WRITE-path milestone filter has something to admit — the retired "04"
// itself is deliberately never used here (that's a separate dedicated
// test below).
['MSD.02-05-real-work']);
assert.equal(readTotal(), 2, 'the retired bracket phase must leave the denominator');
assert.equal(syncedTotal(), 2);
});
test('unbolded bullet strike also excludes', () => {
writeProject(
retiredRoadmap('~~[MSD.02] 04: Delta~~', '### [MSD.02] 04: Delta'), 'bracket');
assert.equal(readTotal(), 2);
});
test('a struck HEADING is excluded as well (it simply stops matching)', () => {
writeProject(
retiredRoadmap('~~**[MSD.02] 04: Delta**~~', '#### ~~**[MSD.02] 04: Delta**~~'), 'bracket');
assert.equal(readTotal(), 2);
});
test('the legacy retirement gesture is unchanged', () => {
writeProject(`# Roadmap
## v2.0
- [x] ~~**Phase 04: Delta**~~ — folded into Phase 05; number retired
- [ ] **Phase 05: Real**
### Phase 04: Delta
**Goal:** folded
### Phase 05: Real
**Goal:** b
### Phase 06: Other
**Goal:** c
`, undefined);
assert.equal(readTotal(), 2, 'legacy 04 retired, 05 and 06 remain');
});
test('a retired bracket phase DIRECTORY is skipped too', () => {
// The other half of the same comparison: the retired key has to match the
// directory's key, and phaseKeyFromDir needs the convention to produce one.
writeProject(
retiredRoadmap('~~**[MSD.02] 04: Delta**~~', '### [MSD.02] 04: Delta'), 'bracket',
['MSD.02-04-delta', 'MSD.02-05-real-work', 'MSD.02-06-follow-up']);
// Three directories on disk, one of them retired. If phaseKeyFromDir cannot
// key a bracket directory the retired one is counted anyway and the total is
// 3 — the denominator a shipped bracket milestone could never work off.
assert.equal(readTotal(), 2, 'the retired phase must not be re-added by its directory');
});
});
describe('#612 PR-2: legacy counting is byte-identical', () => {
beforeEach(() => { tmpDir = createTempProject('adr-612-count-legacy-'); });
afterEach(() => { cleanup(tmpDir); });
test('#549: pure-word section headings still excluded (exact)', () => {
writeProject(`# Roadmap
## v2.0
## Phase Overview:
### Phase 1: One
**Goal:** a
### Phase 2.1: Two point one
**Goal:** b
### Phase 12A: Letter suffix
**Goal:** c
#### Phase Details:
`, undefined);
assert.equal(readTotal(), 3);
assert.equal(syncedTotal(), 3);
});
test('#1445: a legacy 999.x heading is still excluded from the read path', () => {
writeProject(`# Roadmap
## v2.0
### Phase 999.1: Icebox
**Goal:** a
### Phase 5: Real
**Goal:** b
`, undefined);
assert.equal(readTotal(), 1);
});
test('a project-code phase id still counts (exact)', () => {
writeProject(`# Roadmap
## v2.0
### Phase PROJ-42: Coded
**Goal:** a
### Phase 5: Real
**Goal:** b
`, undefined);
assert.equal(readTotal(), 2);
assert.equal(syncedTotal(), 2);
});
});
describe('#612 PR-2: the ADR-canonical milestone heading scopes the milestone', () => {
beforeEach(() => { tmpDir = createTempProject('adr-612-scope-'); });
afterEach(() => { cleanup(tmpDir); });
// ADR-612 Decision 1 pins the bracket milestone heading as `## [MSD.02] Foundation`
// — a NAME, with no version. Milestone scoping matches STATE's `milestone: v2.0`
// STRING against a heading, so the canonical form matched nothing, scoping was
// lost, and total_phases fell back to the on-disk directory count. Every earlier
// fixture in this file embeds `v2.0` in the heading and so never ran the form
// the ADR actually specifies.
const roadmap = (heading) => `# Roadmap
${heading}
### [MSD.02] 05: Real work
**Goal:** a
### [MSD.02] 06: Follow-up
**Goal:** b
### [MSD.02] 07: Third
**Goal:** c
`;
test('name-only heading: total_phases comes from the ROADMAP, not the dir count', () => {
writeProject(roadmap('## [MSD.02] Foundation'), 'bracket', ['MSD.02-05-real-work']);
assert.equal(readTotal(), 3, 'three headings in scope, not one directory');
});
test('the dir count no longer drives the answer', () => {
// The tell for the fallback: without scoping the total tracks the number of
// directories instead of staying at the ROADMAP's phase count.
writeProject(roadmap('## [MSD.02] Foundation'), 'bracket',
['MSD.02-05-real-work', 'MSD.02-06-follow-up']);
assert.equal(readTotal(), 3);
});
test('the version-embedded heading still works', () => {
writeProject(roadmap('## [MSD.02] v2.0 — Foundation'), 'bracket', ['MSD.02-05-real-work']);
assert.equal(readTotal(), 3);
});
test('an unpadded bracket milestone scopes NOTHING (emit-grammar strict)', () => {
// Post-unification an unpadded `[MSD.2]` is malformed: it is not a phase id,
// so it must not bound or scope a milestone either. The tell is that the
// bracket reading equals the null-convention reading — if either behaviour
// flips, these two numbers diverge.
// The tell is that the unpadded heading SCOPES NOTHING: with it, the reading
// must equal the reading of a roadmap that has no milestone heading at all.
// If `[MSD.2]` ever starts scoping again, these two diverge.
const dirs = ['MSD.02-05-real-work'];
writeProject(roadmap('## [MSD.2] Foundation'), 'bracket', dirs);
const unpadded = readTotal();
writeProject(roadmap('## Some heading with no milestone'), 'bracket', dirs);
const unscoped = readTotal();
assert.equal(unpadded, unscoped,
'an unpadded bracket milestone must bound nothing, exactly like no milestone heading');
// And the canonical spelling DOES scope, so the pair is not trivially equal.
writeProject(roadmap('## [MSD.02] Foundation'), 'bracket', dirs);
assert.equal(readTotal(), 3, 'the padded spelling scopes');
});
test('a milestone that does NOT match STATE is not scoped in', () => {
// Use two genuine milestone sections, neither matching STATE's v2.0. This
// removes the old fixture's accidental dependence on the word "milestone"
// appearing in a bracket phase title. Upstream #3354/#3480, strengthened by
// #3642 for the single-section sibling, classifies this as milestoned but
// unbounded: neither the whole-document count nor the on-disk directory
// count is authoritative. With no stored total, `state json` reports null
// rather than guessing.
writeProject(`# Roadmap
## [MSD.03] Later milestone
### [MSD.03] 09: Not scoped here
**Goal:** a
## [MSD.04] Even later milestone
### [MSD.04] 10: Also not scoped
**Goal:** b
`, 'bracket', ['MSD.02-05-real-work']);
assert.equal(readTotal(), null, 'no phases for the asserted milestone — withheld, not computed');
});
test('a NON-bracket repo does not gain bracket scoping', () => {
writeProject(roadmap('## [MSD.02] Foundation'), undefined, ['MSD.02-05-real-work']);
assert.equal(readTotal(), 1, 'no scoping, no counting — invisible as designed');
});
});
describe('#612 PR-2: labeled sentinels, composed sentinels, and the disk-side filter', () => {
beforeEach(() => { tmpDir = createTempProject('adr-612-g3g4g5-'); });
afterEach(() => { cleanup(tmpDir); });
const analyze = () => {
const r = runMsdTools(['roadmap', 'analyze'], tmpDir);
assert.ok(r.success, `roadmap analyze failed: ${r.error}`);
return JSON.parse(r.output);
};
test('G3: a LABELED bracket sentinel is excluded from every counter', () => {
// `### [MSD.999] Phase 07:` fell through to the base alternative, which
// captures nothing — so analyze applied the legacy token rule and counted it
// while state json excluded it. Two derivations of one ROADMAP disagreed.
writeProject(`# Roadmap
## [MSD.02] v2.0
### [MSD.999] Phase 07: Icebox labeled
**Goal:** a
### [MSD.999] 08: Icebox bare
**Goal:** b
### [MSD.02] 01: Real
**Goal:** c
`, 'bracket');
const out = analyze();
assert.deepEqual(out.phases.map(p => p.number), ['01'], 'labeled and bare both excluded');
assert.equal(readTotal(), 1);
assert.equal(out.phase_count, readTotal(), 'analyze and state must agree');
});
test('G5: the legacy 999/0 token rule still applies to a bracketed heading', () => {
// READING-B ADDS a rule; it does not replace one. A mid-migration ROADMAP
// carrying a legacy backlog block under bracket headings must not gain
// denominator entries.
writeProject(`# Roadmap
## [MSD.02] v2.0
### [MSD.02] 01: One
**Goal:** a
### [MSD.02] 999: Backlog
**Goal:** b
### [MSD.02] 0: Zero
**Goal:** c
`, 'bracket');
const out = analyze();
assert.deepEqual(out.phases.map(p => p.number), ['01'],
'analyze excludes both the 999 and the 0 token, as it does for legacy headings');
// DISCLOSED, pre-existing: the state counter's legacy token rule is 999-only
// — it has never excluded a bare `0` — so `[MSD.02] 0:` still reaches the
// denominator there. Adding a 0 filter would move legacy totals, which is out
// of scope; what this pin asserts is that the 999 rule was not DROPPED for
// bracketed headings.
// Under bracket the token rule composes as the full {0, 999} set, so this
// counter now agrees with roadmap analyze. The LEGACY path keeps its
// pre-existing 999-only rule — pinned separately.
assert.equal(readTotal(), 1, 'both 999 and 0 excluded under bracket');
});
test('G4: the disk-side milestone filter does not count another milestone dirs', () => {
// getMilestonePhaseFilter's heading scan collected nothing on a bracket
// ROADMAP, so it degraded to pass-all and Math.max(dirs, roadmap) counted
// the previous milestone's directories — making bracket strictly worse than
// the M-NN convention it supersedes.
writeProject(`# Roadmap
## [MSD.02] v2.0
### [MSD.02] 01: One
**Goal:** a
### [MSD.02] 02: Two
**Goal:** b
### [MSD.02] 03: Three
**Goal:** c
`, 'bracket', ['MSD.01-01-prev', 'MSD.01-02-prev2', 'MSD.02-01-one', 'MSD.02-02-two', 'MSD.02-03-three']);
assert.equal(readTotal(), 3, 'scoped to this milestone, not the whole disk');
});
});
// ─────────────────────────────────────────────────────────────────────────────
// The four numbers `total_phases` was hiding.
//
// Every bracket counting assertion above reads `total_phases` and nothing else,
// and `total_phases` is the ONE number the disk-side milestone filter cannot
// move: `Math.max(phaseDirs.length, roadmapPhaseCount)` (state.cts) floors it at
// the ROADMAP count no matter how many directories the filter rejects. So a
// filter that rejects EVERY bracket directory leaves that number right and
// zeroes `completed_phases`, `total_plans`, `completed_plans` and `percent` —
// green suite, `state json` reporting 0% on a repo `state sync` calls 67% in the
// same second. These pin all five.
//
// THE ORACLE IS THE LEGACY TWIN, and it is computed in the same run rather than
// quoted: each test builds the identical repo in the flat legacy spelling and
// asserts the bracket reading equals it, number for number. Exact literals are
// asserted too — an equality alone would pass with both sides broken.
//
// WHY FLAT LEGACY AND NOT M-NN. The M-NN spelling of these shapes cannot serve
// as the oracle: buildStateFrontmatter's #2445 de-dup key is
// `dir.match(/^0*(\d+[A-Za-z]?(?:\.\d+)*)/)`, which captures only the LEADING
// integer, so `02-01-one`, `02-02-two` and `02-03-three` all key to `2` and two
// of the three directories are dropped before they are ever counted. Measured
// on the true base build (d04592de), the M-NN twin of the first shape below
// reads `[3,0,1,0,0]` where flat legacy reads `[3,2,3,2,67]`; the divergence is
// present identically at base and is untouched by this PR. It is a legacy defect
// in a key space bracket directories cannot enter — `MSD.02-01-one` does not
// match that regex at all, so each bracket dir keys to its own name. The
// source's own `phase-id-owner:` sanction at that line records the divergence.
// ─────────────────────────────────────────────────────────────────────────────
/** The whole progress block as the READ path derives it. */
function readProgress() {
const r = runMsdTools(['state', 'json'], tmpDir);
assert.ok(r.success, `state json failed: ${r.error}`);
const p = JSON.parse(r.output).progress || {};
return [p.total_phases, p.completed_phases, p.total_plans, p.completed_plans, p.percent];
}
/**
* The progress block `state sync` WROTE into STATE.md.
*
* Labelled precisely: this is the READ derivation observed a second time (sync
* rebuilds the frontmatter through buildStateFrontmatter). It is asserted
* because a write that disagrees with `state json` is the #3242 Bug B artifact
* this file exists to prevent — but it is NOT coverage of cmdStateSync's own
* counter. That counter reaches `computeProgressPercent` and nothing else, so
* `syncedPercent()` above is its only observable.
*
* `state json` echoes a `progress:` frontmatter block verbatim when one exists
* and only derives when there is none, so this must be read out of the FILE and
* `stateMd()` must stay block-free. (Measured: same repo, block-free `state
* json` → 3/2/3/2/67; with a `progress: 99…` block → 99/99/99/99/99.)
*/
function syncedProgress() {
const r = runMsdTools(['state', 'sync'], tmpDir);
assert.ok(r.success, `state sync failed: ${r.error}`);
const raw = fs.readFileSync(path.join(tmpDir, '.planning', 'STATE.md'), 'utf-8');
const m = raw.match(
// eslint-disable-next-line local/no-unbounded-quantifier -- parses the STATE.md this test just wrote, fixed-size fixture output, not adversarial input
/total_phases:\s*(\d+)[\s\S]*?completed_phases:\s*(\d+)[\s\S]*?total_plans:\s*(\d+)[\s\S]*?completed_plans:\s*(\d+)[\s\S]*?percent:\s*(-?\d+)/);
assert.ok(m, `state sync must have written a full progress block; got:\n${raw}`);
return m.slice(1).map(Number);
}
describe('#612 PR-2: the disk-side filter scopes bracket dirs — all five numbers', () => {
beforeEach(() => { tmpDir = createTempProject('adr-612-fivenum-'); });
afterEach(() => { cleanup(tmpDir); });
// ── SHAPE 1: one milestone, three phases, three dirs, the first two complete ──
const ONE_MILESTONE_BRACKET = `# Roadmap
## [MSD.02] v2.0: Current
### [MSD.02] 01: One
**Goal:** a
### [MSD.02] 02: Two
**Goal:** b
### [MSD.02] 03: Three
**Goal:** c
`;
const ONE_MILESTONE_LEGACY = `# Roadmap
## v2.0: Current
### Phase 01: One
**Goal:** a
### Phase 02: Two
**Goal:** b
### Phase 03: Three
**Goal:** c
`;
// The M-NN spelling of the same shape — pinned as a CHARACTERIZATION at the end
// of this block, not used as an oracle. See the comment there.
const ONE_MILESTONE_MNN = `# Roadmap
## v2.0: Current
### Phase 2-01: One
**Goal:** a
### Phase 2-02: Two
**Goal:** b
### Phase 2-03: Three
**Goal:** c
`;
const ONE_BRACKET_DIRS = ['MSD.02-01-one', 'MSD.02-02-two', ['MSD.02-03-three', false]];
const ONE_LEGACY_DIRS = ['01-one', '02-two', ['03-three', false]];
const ONE_MNN_DIRS = ['02-01-one', '02-02-two', ['02-03-three', false]];
test('shape 1 READ: 3 phases, 2 complete, 3 plans, 2 done, 67% — not four zeros', () => {
writeProject(ONE_MILESTONE_BRACKET, 'bracket', ONE_BRACKET_DIRS);
assert.deepEqual(readProgress(), [3, 2, 3, 2, 67],
'every bracket directory must satisfy the milestone filter');
});
test('shape 1 READ equals its flat-legacy twin exactly', () => {
writeProject(ONE_MILESTONE_BRACKET, 'bracket', ONE_BRACKET_DIRS);
const bracket = readProgress();
writeProject(ONE_MILESTONE_LEGACY, undefined, ONE_LEGACY_DIRS);
const legacy = readProgress();
assert.deepEqual(bracket, legacy, 'bracket must read exactly what the legacy twin reads');
assert.deepEqual(legacy, [3, 2, 3, 2, 67], 'and the twin is the right answer, not a shared wrong one');
});
test('shape 1 WRITE: sync writes 67%, and its frontmatter agrees with `state json`', () => {
writeProject(ONE_MILESTONE_BRACKET, 'bracket', ONE_BRACKET_DIRS);
// The write derivation's own observable.
assert.equal(syncedPercent(), 67, 'state sync must write 67% into the body');
// …and the block it wrote must not contradict it (#3242 Bug B).
assert.deepEqual(syncedProgress(), [3, 2, 3, 2, 67]);
assert.deepEqual(readProgress(), syncedProgress(), 'the two derivations must not disagree');
});
test('shape 1 WRITE percent equals its flat-legacy twin', () => {
writeProject(ONE_MILESTONE_BRACKET, 'bracket', ONE_BRACKET_DIRS);
const bracket = syncedPercent();
writeProject(ONE_MILESTONE_LEGACY, undefined, ONE_LEGACY_DIRS);
assert.equal(bracket, syncedPercent());
assert.equal(bracket, 67);
});
// ── SHAPE 2: two milestones, scoped to v2.0, two stale prior-milestone dirs ──
const TWO_MILESTONE_BRACKET = `# Roadmap
## [MSD.01] v1.0: Prior
### [MSD.01] 01: Old one
**Goal:** a
### [MSD.01] 02: Old two
**Goal:** b
## [MSD.02] v2.0: Current
### [MSD.02] 01: One
**Goal:** c
### [MSD.02] 02: Two
**Goal:** d
### [MSD.02] 03: Three
**Goal:** e
`;
const TWO_MILESTONE_LEGACY = `# Roadmap
## v1.0: Prior
### Phase 01: Old one
**Goal:** a
### Phase 02: Old two
**Goal:** b
## v2.0: Current
### Phase 03: One
**Goal:** c
### Phase 04: Two
**Goal:** d
### Phase 05: Three
**Goal:** e
`;
const TWO_BRACKET_DIRS = ['MSD.01-01-old-one', 'MSD.01-02-old-two', 'MSD.02-01-one',
['MSD.02-02-two', false], ['MSD.02-03-three', false]];
const TWO_LEGACY_DIRS = ['01-old-one', '02-old-two', '03-one', ['04-two', false], ['05-three', false]];
test('shape 2 READ: the prior milestone dirs are excluded — 3/1/3/1/33', () => {
// Both milestones number their phases 01/02/…, so the bare token cannot tell
// `MSD.01-01-old-one` from `MSD.02-01-one`. Only the milestone-qualified key
// separates them; matching on the token would read 5/3/5/3/60 instead.
writeProject(TWO_MILESTONE_BRACKET, 'bracket', TWO_BRACKET_DIRS);
assert.deepEqual(readProgress(), [3, 1, 3, 1, 33]);
});
test('shape 2 READ equals its flat-legacy twin exactly', () => {
writeProject(TWO_MILESTONE_BRACKET, 'bracket', TWO_BRACKET_DIRS);
const bracket = readProgress();
writeProject(TWO_MILESTONE_LEGACY, undefined, TWO_LEGACY_DIRS);
const legacy = readProgress();
assert.deepEqual(bracket, legacy);
assert.deepEqual(legacy, [3, 1, 3, 1, 33]);
});
test('shape 2 WRITE (#2761 Major 1 fix): bracket closes to 33% (matches read); legacy stays at the disclosed 60%', () => {
// UPDATED by #2761 Major 1 — this test used to pin the BUG this fix
// closes, titled "the DISCLOSED legacy gap, mirrored — not closed":
// cmdStateSync did its own `fs.readdirSync` and never called the
// milestone filter, so its denominator was the whole disk (3 summaries
// over 5 plans = 60%) against the read path's scoped 33% — for BOTH
// bracket and legacy alike, since the divergence was engine-wide, not
// bracket-specific.
//
// Major 1 gates cmdStateSync's disk scan by getMilestonePhaseFilter under
// `phase_id_convention === 'bracket'` ONLY — deliberately NOT
// unconditionally: an unconditional filter would ALSO move every legacy
// repo's persisted percent, which is out of this fix's scope (the
// binding constraint is "legacy stays byte-identical"). So: bracket now
// closes to 33% (agrees with the read path — repro3's fix, verified
// here on a SECOND fixture with real prior-milestone noise dirs);
// legacy stays at the pre-existing 60% (unchanged, deliberately).
writeProject(TWO_MILESTONE_BRACKET, 'bracket', TWO_BRACKET_DIRS);
assert.equal(syncedPercent(), 33, 'bracket: the gap is now closed, agrees with the read path');
writeProject(TWO_MILESTONE_LEGACY, undefined, TWO_LEGACY_DIRS);
assert.equal(syncedPercent(), 60, 'legacy: the gap remains — moving it is out of this fix\'s scope');
});
test('shape 2 WRITE frontmatter agrees with `state json` on both spellings', () => {
writeProject(TWO_MILESTONE_BRACKET, 'bracket', TWO_BRACKET_DIRS);
assert.deepEqual(syncedProgress(), [3, 1, 3, 1, 33]);
assert.deepEqual(readProgress(), syncedProgress());
writeProject(TWO_MILESTONE_LEGACY, undefined, TWO_LEGACY_DIRS);
assert.deepEqual(syncedProgress(), [3, 1, 3, 1, 33]);
assert.deepEqual(readProgress(), syncedProgress());
});
test('a bracket repo carrying LEGACY-shaped dirs is unaffected (the branch is additive)', () => {
// The bracket branch tries the qualified key first and FALLS THROUGH on a
// miss, so the three legacy dir checks still run on a bracket project. An
// early `return false` there would silently drop this repo to zero.
writeProject(ONE_MILESTONE_LEGACY, 'bracket', ONE_LEGACY_DIRS);
assert.deepEqual(readProgress(), [3, 2, 3, 2, 67]);
});
test('CHARACTERIZATION: the M-NN twin of shape 1 no longer collapses — three plans, not one', () => {
// Holds the oracle substitution honest. The two "equals its flat-legacy
// twin" tests above compare bracket against FLAT legacy; nothing else in the
// suite pins the M-NN spelling, so the changeset's claim that the M-NN
// divergence is pre-existing and untouched would go stale silently the first
// time a sibling slice widens the de-dup key.
//
// Upstream #3185 replaced the leading-integer de-dup key with canonical
// phaseKeyFromDir ownership. The M-NN directories now key as 02-01,
// 02-02, and 02-03, so all three distinct phases and plans survive.
writeProject(ONE_MILESTONE_MNN, undefined, ONE_MNN_DIRS);
const mnn = readProgress();
assert.equal(mnn[0], 3, 'M-NN: total_phases still tracks the ROADMAP');
assert.equal(mnn[2], 3, 'M-NN: all three dirs survive the de-dup after #3185');
// Build the flat-legacy twin in a fresh project. Reusing this project would
// stack both directory spellings now that the M-NN entries no longer
// collapse under the old leading-integer key.
cleanup(tmpDir);
tmpDir = createTempProject('adr-612-fivenum-mnn-twin-');
writeProject(ONE_MILESTONE_LEGACY, undefined, ONE_LEGACY_DIRS);
assert.deepEqual(readProgress(), [3, 2, 3, 2, 67]);
});
});
// ─────────────────────────────────────────────────────────────────────────────
// R4-m3 — the milestone-qualified key is a string SPLICE, so a heading whose
// token carries its own hyphen mis-parses into the wrong directory.
//
// `### [MSD.02] Phase 02-01:` spliced to `MSD.02-02-01`, which the qualified-key
// grammar reads as milestone 02 / phase 02 — the trailing `-01` truncated, both
// such headings collapsing to one key, and the heading claiming
// `MSD.02-02-two` (the directory it does NOT name) while rejecting
// `MSD.02-01-one` (the one it does).
//
// The oracle is the SAME ROADMAP read under `milestone-prefixed`, which is
// base-identical on this shape — so the bracket acceptance vector must equal it.
// Scope note: only the ACCEPTANCE VECTOR is claimed base-equivalent.
// `total_phases` on this fixture does move 1 -> 2, because the bracket heading
// count is the feature this PR ships; measured, that move is identical with and
// without this guard, and identical to what the canonical `### [MSD.02] 01:`
// spelling does (both read 2 with zero directories on disk, where base reads 0).
// ─────────────────────────────────────────────────────────────────────────────
describe('#612 PR-2: a hyphenated heading token forms no qualified key', () => {
beforeEach(() => { tmpDir = createTempProject('adr-612-hyphen-tok-'); });
afterEach(() => { cleanup(tmpDir); });
const MIXED = `# Roadmap
## [MSD.02] v2.0: Current
### [MSD.02] Phase 02-01: One
**Goal:** a
### [MSD.02] Phase 02-02: Two
**Goal:** b
`;
const DIRS = ['MSD.02-02-two', 'MSD.02-01-one', '02-01-mnn', '02-2026-photos', '46-6-rs-thing', '2-01-x'];
/** The disk-side filter's own acceptance vector, read straight off the module. */
const acceptance = () => {
const rp = require('../msd-core/bin/lib/roadmap-parser.cjs');
const f = rp.getMilestonePhaseFilter(tmpDir);
return Object.fromEntries(DIRS.map(d => [d, !!f(d)]));
};
test('the heading does not claim the directory it does not name', () => {
writeProject(MIXED, 'bracket', DIRS);
const a = acceptance();
assert.equal(a['MSD.02-02-two'], false,
'`[MSD.02] Phase 02-01` must not claim MSD.02-02-two by a truncated key');
assert.equal(a['MSD.02-01-one'], false,
'and it does not resolve its own dir either — unqualified, exactly as at base');
assert.equal(a['02-01-mnn'], true, 'the legacy fall-through is untouched');
});
test('the acceptance vector equals the milestone-prefixed control on the same ROADMAP', () => {
writeProject(MIXED, 'bracket', DIRS);
const bracket = acceptance();
cleanup(tmpDir);
tmpDir = createTempProject('adr-612-hyphen-tok-ctl-');
writeProject(MIXED, 'milestone-prefixed', DIRS);
const control = acceptance();
assert.deepEqual(bracket, control,
'a hyphenated token must read the disk identically under both conventions');
});
test('a CANONICAL bracket heading still forms its qualified key', () => {
// Guards the guard: `!token.includes('-')` must not disable qualified
// matching for the spelling the convention actually specifies.
writeProject(`# Roadmap
## [MSD.02] v2.0: Current
### [MSD.02] 01: One
**Goal:** a
`, 'bracket', ['MSD.02-01-one', 'MSD.01-01-old-one']);
const rp = require('../msd-core/bin/lib/roadmap-parser.cjs');
const f = rp.getMilestonePhaseFilter(tmpDir);
assert.equal(f('MSD.02-01-one'), true, 'the canonical qualified key still resolves');
assert.equal(f('MSD.01-01-old-one'), false, 'and still scopes out the foreign milestone');
});
});
// ─────────────────────────────────────────────────────────────────────────────
// R4-m2 — extractCurrentMilestone must not throw.
//
// Its bracket-scoping fallback calls resolvePhaseIdConvention, which reaches
// planningDir, which throws a plain Error for a MSD_PROJECT/MSD_WORKSTREAM
// segment containing `/`, `\` or `..`. At base the only planningDir call in this
// function sits inside the STATE-read try, so the function returned normally;
// an unguarded call broke the never-throws invariant that getRoadmapPhaseInternal
// and getMilestoneInfo carry #2245 / ADR-227 notes about.
//
// Module level on purpose: the CLI rejects a bad MSD_WORKSTREAM up front, so
// this contract is only observable to an in-process embedder — which is exactly
// who the invariant protects.
// ─────────────────────────────────────────────────────────────────────────────
describe('#612 PR-2: extractCurrentMilestone never throws on a poisoned env', () => {
const ROADMAP = `# Roadmap
## Milestones
- 🚧 **v1.0 Alpha** — in progress
## Alpha
### Phase 01: Setup
`;
test('a traversal segment in MSD_WORKSTREAM degrades instead of escaping', () => {
const dir = createTempProject('adr-612-envguard-');
fs.writeFileSync(path.join(dir, '.planning', 'ROADMAP.md'), ROADMAP, 'utf-8');
fs.writeFileSync(path.join(dir, '.planning', 'config.json'), '{}', 'utf-8');
const prior = process.env.MSD_WORKSTREAM;
try {
process.env.MSD_WORKSTREAM = '../evil';
const rp = require('../msd-core/bin/lib/roadmap-parser.cjs');
const out = rp.extractCurrentMilestone(ROADMAP, dir);
assert.equal(typeof out, 'string', 'must return content, not throw');
assert.ok(out.length > 0);
} finally {
// Restore before anything else in this chunk runs — a leaked
// MSD_WORKSTREAM would poison every later test in the same process.
if (prior === undefined) delete process.env.MSD_WORKSTREAM;
else process.env.MSD_WORKSTREAM = prior;
cleanup(dir);
}
});
});
// ─── REGRESSION: the version-less bracket milestone heading now scopes ──────
/**
* FIXED (#2761 B1, reviewer trek-e blocker). Every other bracket fixture in
* this repo writes its milestone heading as `## [MSD.02] v2.0: Current` — with a
* version. ADR-612's canonical form is a NAME and no version
* (`## [MSD.02] Foundation`), which `isMilestoneBounded`'s own doc comment in
* state.cts calls out as canonical. Until this fix, that form left the
* disk-side filter unscoped: directories from BOTH the prior and the later
* milestone were admitted into the current one.
*
* Mechanism, in `extractCurrentMilestone` (roadmap-parser.cts):
* - the bracket scope branch selects the right `currentSection`, but
* - `preambleCutoff` was driven only by `anyMilestonePattern`, which requires
* `v\d+\.\d+` or a status emoji. A version-less roadmap matches none, so the
* cutoff fell back to the CURRENT milestone's own offset and every PRIOR
* milestone landed in the preamble — whose phase-stripping regex only strips
* `Phase N:`-labelled headings, so bracket phase headings survived it; and
* - `computeSectionEnd` accepted a boundary only if the heading carried a
* version or emoji, so with none present the section ran to EOF and every
* LATER milestone was swept in too.
*
* The leak was bidirectional and had two sites. The fix: under the bracket
* scope branch (`bracketScopeConvention === 'bracket'`), both
* `computeSectionEnd` and the `preambleCutoff` scan ALSO accept a
* `#{1,2}\s+\[CODE.MM\]` heading as a milestone boundary, built from
* phase-id.cts's `BRACKET_ID_SRC` (the single owner of the bracket-id grammar)
* rather than a re-typed literal. `#{1,2}` is the discriminator because bracket
* PHASE headings are `###` and carry the same `[CODE.MM]` prefix (a `#{1,3}`
* pattern would match both, which is why the scope branch's own matcher returns
* the phase headings as well). Reachable ONLY when the bracket scope branch has
* fired, so version-bearing/emoji repos and non-bracket conventions take the
* exact pre-existing code path.
*
* These tests used to assert the pre-fix reading; they are inverted here as the
* fix's regression proof, not deleted.
*/
describe('#612 PR-2 REGRESSION: a version-less bracket milestone scopes correctly', () => {
beforeEach(() => { tmpDir = createTempProject('adr-612-versionless-'); });
afterEach(() => { cleanup(tmpDir); });
const VERSIONLESS = `# Roadmap
## [MSD.01] Prior Milestone
### [MSD.01] 01: Old one
**Goal:** a
## [MSD.02] Current Milestone
### [MSD.02] 01: One
**Goal:** b
### [MSD.02] 02: Two
**Goal:** c
## [MSD.03] Later Milestone
### [MSD.03] 01: Later one
**Goal:** d
`;
// Same roadmap, milestone headings carrying their version — the shape every
// other fixture in this file uses, and the control that proves the difference
// is the VERSION STRING and nothing else.
const VERSIONED = VERSIONLESS
.replace('## [MSD.01] Prior Milestone', '## [MSD.01] v1.0: Prior Milestone')
.replace('## [MSD.02] Current Milestone', '## [MSD.02] v2.0: Current Milestone')
.replace('## [MSD.03] Later Milestone', '## [MSD.03] v3.0: Later Milestone');
const DIRS = ['MSD.01-01-old-one', 'MSD.02-01-one', 'MSD.02-02-two', 'MSD.03-01-later-one'];
const accepts = () => {
const rp = require('../msd-core/bin/lib/roadmap-parser.cjs');
const f = rp.getMilestonePhaseFilter(tmpDir);
return Object.fromEntries(DIRS.map(d => [d, !!f(d)]));
};
test('CONTROL: with a version in the heading, scoping works in both directions', () => {
writeProject(VERSIONED, 'bracket', DIRS);
assert.deepEqual(accepts(), {
'MSD.01-01-old-one': false,
'MSD.02-01-one': true,
'MSD.02-02-two': true,
'MSD.03-01-later-one': false,
});
});
test('without a version, scoping ALSO works in both directions (regression proof)', () => {
// Mirrors the CONTROL assertion exactly, on the version-less fixture — the
// fix makes the two shapes agree.
writeProject(VERSIONLESS, 'bracket', DIRS);
assert.deepEqual(accepts(), {
'MSD.01-01-old-one': false,
'MSD.02-01-one': true,
'MSD.02-02-two': true,
'MSD.03-01-later-one': false,
});
});
test('without a version, the PRIOR milestone no longer leaks in (preambleCutoff)', () => {
writeProject(VERSIONLESS, 'bracket', DIRS);
assert.equal(accepts()['MSD.01-01-old-one'], false,
'regression proof — pinned true before the #2761 B1 scoping fix');
});
test('without a version, the LATER milestone no longer leaks in (computeSectionEnd)', () => {
writeProject(VERSIONLESS, 'bracket', DIRS);
assert.equal(accepts()['MSD.03-01-later-one'], false,
'regression proof — pinned true before the #2761 B1 scoping fix');
});
test('total_phases counts only the asserted milestone, not the whole disk', () => {
writeProject(VERSIONLESS, 'bracket', DIRS);
// 4 directories on disk, 2 phases in the milestone STATE.md asserts.
assert.equal(readTotal(), 2, 'regression proof — pinned 4 before the #2761 B1 scoping fix');
});
test('the current milestone\'s own dirs are admitted either way (no under-count)', () => {
// Whatever else leaked before the fix, the milestone's real phases must
// always resolve — that property held before and still holds after.
writeProject(VERSIONLESS, 'bracket', DIRS);
const a = accepts();
assert.equal(a['MSD.02-01-one'], true);
assert.equal(a['MSD.02-02-two'], true);
});
});
// ─── #2761 B1 FOLLOW-UP: mixed heading shapes and boundary heading levels ───
//
// Two gaps flagged during self-review of the B1 fix above, closed here with
// deterministic fixtures:
//
// 1. The earliest-of-either comparison added to `preambleCutoff` (taking
// whichever of the version/emoji match or the bracket match sits first in
// the document) was only exercised where the two patterns happen to agree
// on the same heading (every milestone in the REGRESSION block above is
// uniformly version-bearing or uniformly version-less). A genuinely mixed
// roadmap — one milestone version-bearing, its sibling version-less — was
// untested.
//
// 2. `computeSectionEnd`'s `h.level <= 2` conjunct (added alongside the
// pre-existing `h.level > level` skip) is REDUNDANT whenever the selected
// milestone heading is level 2 — the ADR-canonical shape, and every
// existing fixture in this repo: `h.level > level` alone already implies
// `h.level <= 2` there, so a mutant deleting the conjunct would survive
// every test written before this one. It is NOT redundant when the
// selected heading is level 3 (or level 1) — see the fixtures below.
describe('#612 PR-2 B1 FOLLOW-UP: mixed heading shapes and boundary heading levels', () => {
beforeEach(() => { tmpDir = createTempProject('adr-612-mixed-'); });
afterEach(() => { cleanup(tmpDir); });
const acceptsFor = (dirs) => {
const rp = require('../msd-core/bin/lib/roadmap-parser.cjs');
const f = rp.getMilestonePhaseFilter(tmpDir);
return Object.fromEntries(dirs.map((d) => [d, !!f(d)]));
};
test('mixed shape: version-bearing PRIOR + version-less CURRENT — prior stays out of the preamble leak set', () => {
// The mid-migration shape: an already-versioned milestone sits before a
// newer one that has not yet had its version added. anyMilestonePattern
// alone already finds MSD.01 here — it's the first (and only) version-
// bearing heading in the document — so this fixture pins that the
// earliest-of-either comparison does not regress that pre-existing path
// when the two patterns agree on the same heading, while computeSectionEnd
// still needs the bracket-boundary fix to correctly exclude MSD.03 (which
// remains version-less).
const roadmap = `# Roadmap
## [MSD.01] v1.0: Prior Milestone
### [MSD.01] 01: Old one
**Goal:** a
## [MSD.02] Current Milestone
### [MSD.02] 01: One
**Goal:** b
### [MSD.02] 02: Two
**Goal:** c
## [MSD.03] Later Milestone
### [MSD.03] 01: Later one
**Goal:** d
`;
const dirs = ['MSD.01-01-old-one', 'MSD.02-01-one', 'MSD.02-02-two', 'MSD.03-01-later-one'];
writeProject(roadmap, 'bracket', dirs);
assert.deepEqual(acceptsFor(dirs), {
'MSD.01-01-old-one': false,
'MSD.02-01-one': true,
'MSD.02-02-two': true,
'MSD.03-01-later-one': false,
});
assert.equal(readTotal(), 2);
});
test('boundary heading level: a level-3 CURRENT milestone heading still scopes correctly (kills the h.level<=2 equivalent-mutant)', () => {
// The selected milestone heading is written with THREE hashes
// (`### [MSD.02] Foundation`) — unusual, but syntactically admitted by the
// same `#{1,3}` grammar every heading matcher in this function already
// compiles. With level=3, `h.level > level` alone no longer excludes a
// level-3 heading, so computeSectionEnd's own first phase heading
// (`### [MSD.02] 01: One`) — itself bracket-shaped — would ALSO satisfy the
// bracket-boundary test if the `h.level <= 2` conjunct were removed,
// truncating the section to nothing but the bare milestone heading and
// dropping BOTH of its own phases. A real PRIOR milestone precedes it so the
// preamble side-channel cannot independently rescue the truncated phases —
// confirmed by hand-mutating a throwaway build copy: without the guard this
// fixture's own phases vanish from the returned scope entirely, and
// getMilestonePhaseFilter's zero-token pass-all degrade then admits every
// directory on disk instead (the exact pre-#612 symptom).
const roadmap = `# Roadmap
## [MSD.01] v1.0: Prior Milestone
### [MSD.01] 01: Old
**Goal:** z
### [MSD.02] Foundation
### [MSD.02] 01: One
**Goal:** a
### [MSD.02] 02: Two
**Goal:** b
## [MSD.03] v3.0: Next Milestone
### [MSD.03] 01: Later
**Goal:** c
`;
const dirs = ['MSD.01-01-old', 'MSD.02-01-one', 'MSD.02-02-two', 'MSD.03-01-later'];
writeProject(roadmap, 'bracket', dirs);
assert.deepEqual(acceptsFor(dirs), {
'MSD.01-01-old': false,
'MSD.02-01-one': true,
'MSD.02-02-two': true,
'MSD.03-01-later': false,
});
assert.equal(readTotal(), 2);
});
test('boundary heading level: a level-1 CURRENT milestone heading also scopes correctly (#{1,2} tolerance, not just level 2)', () => {
// A level-1/level-1 pairing (consistent heading-level convention across
// sibling milestones) — distinct from the level-3 case above: this pins
// that the `#{1,2}` bracket-boundary source tolerates level 1, not only the
// ADR-canonical level 2.
const roadmap = `# [MSD.02] Foundation
### [MSD.02] 01: One
**Goal:** a
### [MSD.02] 02: Two
**Goal:** b
# [MSD.03] v3.0: Next Milestone
### [MSD.03] 01: Later
**Goal:** c
`;
const dirs = ['MSD.02-01-one', 'MSD.02-02-two', 'MSD.03-01-later'];
writeProject(roadmap, 'bracket', dirs);
assert.deepEqual(acceptsFor(dirs), {
'MSD.02-01-one': true,
'MSD.02-02-two': true,
'MSD.03-01-later': false,
});
assert.equal(readTotal(), 2);
});
});
// ─── #2761 B1 (round-2 adversarial review, Blocker 1): a SAME-MILESTONE ──────
// ─── continuation heading is not mistaken for a DIFFERENT milestone's ───────
// ─── boundary ────────────────────────────────────────────────────────────────
//
// The B1 fix above (08d5b0c4) taught computeSectionEnd/preambleCutoff to
// recognise ANY `#{1,2} [CODE.MM]` heading as a milestone boundary. It did not
// distinguish "a heading for a DIFFERENT milestone" (a real boundary) from "a
// heading that merely CONTINUES the CURRENT milestone's own section" (e.g. a
// version-less checklist/detail split: `## [MSD.02] Foundation (Phase
// Details)`, or an ad-hoc continuation heading `## [MSD.02] Foundation —
// continued`) — the latter is not a boundary at all. The `(Phase Details)`
// re-append at `detailsMatch` below only searches `allMatches` (the
// VERSION-STRING matches from the top of this function), so a version-less
// continuation heading was cut out by the boundary and never re-appended: the
// milestone's OWN later phases silently vanished from the returned scope,
// while `getMilestonePhaseFilter`'s phaseCount stayed non-zero (unlike the
// original #612 defect), so the pass-all degrade never caught it either — a
// confidently wrong, non-degraded phase count for a still-incomplete
// milestone.
//
// Fixed here by teaching isBracketMilestoneBoundary a same-milestone
// exclusion: a candidate heading whose OWN bracket id (case-folded) equals the
// SELECTED milestone's bracket id is never a boundary.
describe('#612 PR-2 B1 round-2: a same-milestone continuation heading is not a boundary', () => {
beforeEach(() => { tmpDir = createTempProject('adr-612-b1r2-'); });
afterEach(() => { cleanup(tmpDir); });
const D = [['MSD.02-01-one', true], ['MSD.02-02-two', false]];
/**
* Per the file header's oracle-hazard #2: pre-write a WRONG total_phases
* into STATE.md's frontmatter before calling `syncedTotal()`/
* `syncedPercent()`, so `state sync` is forced to do real work rather than
* silently no-op because its computed total already happens to match.
*/
function poisonTotalPhases() {
const statePath = path.join(tmpDir, '.planning', 'STATE.md');
const raw = fs.readFileSync(statePath, 'utf-8');
fs.writeFileSync(statePath, raw.replace(/^---\r?\n/, '---\ntotal_phases: 999\n'), 'utf-8');
}
test('RED (repro8 case 1): version-bearing head + version-less "(Phase Details)" continuation — 2/1/50, not 1/1/100', () => {
writeProject(`# Roadmap
## [MSD.02] v2.0: Foundation
### [MSD.02] 01: One
**Goal:** b
## [MSD.02] Foundation (Phase Details)
### [MSD.02] 02: Two
**Goal:** c
`, 'bracket', D);
assert.equal(readTotal(), 2, 'pinned 1 before this fix — the continuation heading truncated the section');
poisonTotalPhases();
assert.equal(syncedTotal(), 2, 'the WRITE path must agree with the READ path');
assert.equal(syncedPercent(), 50, 'pinned 100 before this fix');
});
test('RED (repro5): fully version-less milestone split across two headings, no siblings — 2/1/50, not 1/1/100', () => {
writeProject(`# Roadmap
## [MSD.02] Foundation
### [MSD.02] 01: One
**Goal:** b
## [MSD.02] Foundation — continued
### [MSD.02] 02: Two
**Goal:** c
`, 'bracket', D);
assert.equal(readTotal(), 2, 'pinned 1 before this fix');
poisonTotalPhases();
assert.equal(syncedTotal(), 2);
// The roadmap is version-less while STATE pins v2.0, so upstream #3217
// withholds the percentage rather than treating the scope as complete.
assert.match(syncSkipReason(), /scope is "unscoped", not COMPLETE \(#3217\)/);
});
test('PIN (repro8 case 3): a trailing DIFFERENT-id icebox section still terminates the primary section — unchanged at 2/1/50', () => {
writeProject(`# Roadmap
## [MSD.02] v2.0: Foundation
### [MSD.02] 01: One
**Goal:** b
### [MSD.02] 02: Two
**Goal:** c
## [MSD.999] Icebox
### [MSD.999] 07: Someday
**Goal:** z
`, 'bracket', D);
assert.equal(readTotal(), 2);
poisonTotalPhases();
assert.equal(syncedTotal(), 2);
assert.equal(syncedPercent(), 50);
});
test('PIN (repro10 A1): all-version-bearing + icebox + "(Phase Details)" — exact 2/1/50, no double-count', () => {
// The same-milestone exclusion must not make the PRIMARY section swallow
// the (Phase Details) section a second time on top of the pre-existing
// detailsMatch re-append — total_phases must read EXACTLY 2, not 4.
const dirs = [
['MSD.01-01-old', true],
['MSD.02-01-one', true],
['MSD.02-02-two', false],
['MSD.03-01-later', true],
];
writeProject(`# Roadmap
## [MSD.01] v1.0: Prior
### [MSD.01] 01: Old
**Goal:** a
## [MSD.02] v2.0: Current
### [MSD.02] 01: One
**Goal:** b
## [MSD.03] v3.0: Later
### [MSD.03] 01: Later
**Goal:** d
## [MSD.02] v2.0: Current (Phase Details)
### [MSD.02] 02: Two
**Goal:** c
## [MSD.999] Icebox
### [MSD.999] 07: Someday
**Goal:** z
`, 'bracket', dirs);
assert.equal(readTotal(), 2, 'exact — a regression here would double-count to 4');
// NOTE: no syncedTotal()/syncedPercent() assertions here — this fixture
// carries dirs OUTSIDE the current milestone (MSD.01-01-old,
// MSD.03-01-later), which is exactly the shape that exposes Major 1
// (cmdStateSync's body percent is computed from an unfiltered whole-disk
// scan). Asserting the synced percent here before Major 1's fix lands
// would fail on a DIFFERENT, not-yet-fixed defect. Covered once Major 1 is
// fixed (#2761 Major 1 commit), where this fixture's synced values are
// asserted directly.
});
});
// ─── #2761 B2 (round-2 adversarial review, Blocker 2): the heading ──────────
// ─── discriminator is CONTENT, not level ────────────────────────────────────
//
// Three sites disagreed about which heading levels are a bracket milestone:
// the selector (`^#{1,3}\s+\[CODE.MM\]`) and `isMilestoneBounded` (state.cts)
// both admit level 1-3, but the B1 boundary only admitted level 1-2
// (`h.level <= 2`). A `###`-level bracket milestone was therefore SELECTED and
// BOUNDED but never TERMINATED: computeSectionEnd ran with level=3, a level-3
// SIBLING milestone survived `h.level > level` (not deeper), failed the
// version/emoji test (version-less), then failed `h.level <= 2` — so the
// function fell through to `return content.length`, sweeping the sibling
// milestone's own phases into the current one. This reproduces trek-e's
// original #612 defect verbatim (a safe degrade became a confidently-wrong
// persisted number) on a heading level the selector and bounding predicate
// both already admit.
//
// ADR-612 Decision 1 (docs/adr/612-bracket-phase-id-convention.md:56)
// specifies the discriminator as CONTENT: "a phase heading is a bracket
// followed by a digit-then-colon; a milestone heading is a bracket followed
// by a name." Fixed by replacing the `level > 2` rejection with
// BRACKET_PHASE_TAIL_RE (built from phase-id.cts's single-owner
// phaseHeadingPrefixSrcFor, not a re-typed grammar), with the level check
// widened to a depth-sanity cap of 3 (mirroring the selector's own `#{1,3}`
// ceiling — NOT a phase/milestone discriminator itself).
describe('#612 PR-2 B2 round-2: the bracket boundary is a CONTENT discriminator, not a level cap', () => {
beforeEach(() => { tmpDir = createTempProject('adr-612-b2r2-'); });
afterEach(() => { cleanup(tmpDir); });
test('RED (repro2 case C): all milestones ###, all phases ####, version-less — total 2, completion scoped, percent withheld', () => {
// MSD.02 (asserted milestone) has 2 phases, both complete. isMilestoneBounded
// already returns true at #{1,3} (state.cts, unaffected by this fix), so the
// phase totals remain directly observable — pinned 4/3 (whole-disk
// fallback) before this fix. Upstream #3573 now threads the asserted v2.0
// milestone into truthful progress scoping. Because this deliberately
// version-less roadmap cannot resolve that versioned window, percentage
// is withheld instead of manufacturing 100 from an UNSCOPED input.
const dirs = [
['MSD.01-01-old', true],
['MSD.02-01-one', true],
['MSD.02-02-two', true],
['MSD.03-01-later', false],
];
writeProject(`# Roadmap
### [MSD.01] Prior Milestone
#### [MSD.01] 01: Old one
**Goal:** a
### [MSD.02] Current Milestone
#### [MSD.02] 01: One
**Goal:** b
#### [MSD.02] 02: Two
**Goal:** c
### [MSD.03] Later Milestone
#### [MSD.03] 01: Later one
**Goal:** d
`, 'bracket', dirs);
assert.equal(readTotal(), 2, 'pinned 4 before this fix (whole-doc fallback)');
const r = runMsdTools(['state', 'json'], tmpDir);
assert.ok(r.success, `state json failed: ${r.error}`);
const progress = JSON.parse(r.output).progress;
assert.equal(progress?.total_phases, 2, 'pinned 4 before this fix (whole-doc fallback)');
assert.equal(progress?.completed_phases, 2, 'both real MSD.02 phases are complete — scoping, not the whole disk, is what this fixture pins');
// Upstream #3573 threads STATE's stored `milestone: v2.0` into this read.
// A version-less ROADMAP cannot resolve that version, so the scope remains
// UNSCOPED and the percent is withheld even though the bracket-scoped
// totals above are correct.
assert.equal(progress?.percent, undefined, 'percent is withheld (upstream #3573 x version-less-bracket-document interaction) — disclosed, not a scoping regression');
});
test('mechanism (repro7): extractCurrentMilestone actually scopes a level-3 milestone, not the whole document', () => {
const dirs = ['MSD.01-01-old', 'MSD.02-01-one', 'MSD.02-02-two', 'MSD.03-01-later'];
const roadmap = `# Roadmap
### [MSD.01] Prior
#### [MSD.01] 01: Old
**Goal:** a
### [MSD.02] Current
#### [MSD.02] 01: One
**Goal:** b
#### [MSD.02] 02: Two
**Goal:** c
### [MSD.03] Later
#### [MSD.03] 01: Later
**Goal:** d
`;
writeProject(roadmap, 'bracket', dirs);
const rp = require('../msd-core/bin/lib/roadmap-parser.cjs');
const scope = rp.extractCurrentMilestone(
fs.readFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), 'utf-8'),
tmpDir,
);
assert.ok(scope.length < roadmap.length, 'pinned scope === full document before this fix (fell through to content.length)');
assert.ok(scope.includes('01: One') && scope.includes('02: Two'), 'the milestone\'s own phases must survive scoping');
assert.ok(!scope.includes('Old') && !scope.includes('Later'), 'sibling milestones must not leak in');
});
test('PIN: a dotted sub-phase heading ([MSD.02] 05.03:) is phase-tail-shaped, not a boundary', () => {
// The tail grammar must cover BOTH `05:` and the dotted `05.03:` forms —
// this is precisely where a regex slip in the tail discriminator would
// hide (a milestone-shaped false negative would truncate the section at
// what is actually a real sub-phase heading).
const dirs = ['MSD.02-01-one', 'MSD.02-05.03-sub', 'MSD.03-01-later'];
writeProject(`# Roadmap
## [MSD.02] Current
### [MSD.02] 01: One
**Goal:** a
### [MSD.02] 05.03: Name
**Goal:** b
## [MSD.03] Later
### [MSD.03] 01: Later
**Goal:** c
`, 'bracket', dirs);
const rp = require('../msd-core/bin/lib/roadmap-parser.cjs');
const f = rp.getMilestonePhaseFilter(tmpDir);
assert.equal(!!f('MSD.02-01-one'), true);
assert.equal(!!f('MSD.02-05.03-sub'), true, 'the dotted sub-phase heading must not be excluded as a boundary');
assert.equal(!!f('MSD.03-01-later'), false);
assert.equal(readTotal(), 2, 'both MSD.02 phases (01 and 05.03) must be counted');
});
test('PIN (repro8 case 3, content-discriminator mechanism): a trailing DIFFERENT-id icebox section still terminates — 2/1/50', () => {
// Re-pins the same shape as the B1 round-2 block above, now that the
// discriminator is CONTENT rather than level: [MSD.999] is bracket-shaped
// and NOT phase-tail-shaped ("Icebox" carries no digit-colon), and its id
// differs from the selected milestone's — a boundary either way.
const D = [['MSD.02-01-one', true], ['MSD.02-02-two', false]];
writeProject(`# Roadmap
## [MSD.02] v2.0: Foundation
### [MSD.02] 01: One
**Goal:** b
### [MSD.02] 02: Two
**Goal:** c
## [MSD.999] Icebox
### [MSD.999] 07: Someday
**Goal:** z
`, 'bracket', D);
assert.equal(readTotal(), 2);
});
});
// ─── #2761 Blocker 3 (round-2 adversarial review): the preamble-cutoff ──────
// ─── bracket scan is now fence-aware ─────────────────────────────────────────
//
// `preambleCutoff`'s bracket branch used a raw `content.match`/`matchAll` —
// blind to fenced code blocks — while its sibling `computeSectionEnd` (a few
// lines above it) already consumed `tokenizeHeadings(content)`, which strips
// fences. A fenced markdown example in the preamble containing a bracket
// heading (ADR-612's own docs do exactly this) was textually the earliest
// `#{1,3} [CODE.MM]` match: `preambleCutoff` landed INSIDE the fence,
// `preamble = content.slice(0, preambleCutoff)` ended with an unclosed
// opener, and the unbalanced fence then blinded
// `getMilestonePhaseFilter`'s own `tokenizeHeadings(scope)` call — EVERY
// heading in the returned scope vanished, `phaseCount` degraded to 0, and the
// pass-all filter admitted every directory on disk (repro11's mechanism).
// Regression vs round-1, which had no bracket pattern to blind and so fell
// back to the correct (non-fenced) heading.
//
// Fixed by scanning the SAME `currentMilestoneHeadings` token list
// computeSectionEnd consumes, instead of a raw regex — closing the asymmetry
// between the two halves of one boundary semantic.
describe('#612 PR-2 Blocker 3 round-2: preambleCutoff is fence-aware (bracket branch only)', () => {
beforeEach(() => { tmpDir = createTempProject('adr-612-b3fence-'); });
afterEach(() => { cleanup(tmpDir); });
const D = [
['MSD.01-01-stray', true],
['MSD.02-01-one', true],
['MSD.02-02-two', false],
['MSD.03-01-stray', true],
];
test('RED (repro12 bracket row): a fenced example bracket heading in the preamble no longer blinds the scan — 2/1/50', () => {
writeProject(`# Roadmap
Authoring guide:
\`\`\`markdown
## [MSD.00] Example milestone heading
\`\`\`
## [MSD.02] Current
### [MSD.02] 01: One
**Goal:** b
### [MSD.02] 02: Two
**Goal:** c
`, 'bracket', D);
assert.equal(readTotal(), 2, 'pinned 4 before this fix (fence-blind scan degraded phaseCount to 0)');
});
test('mechanism (repro11): the returned scope has an EVEN, balanced fence count and a non-degraded phaseCount', () => {
const roadmap = `# Roadmap
Docs for authors:
\`\`\`markdown
## [MSD.00] Example milestone heading
### [MSD.00] 01: Example phase
\`\`\`
## [MSD.02] Current
### [MSD.02] 01: One
**Goal:** b
### [MSD.02] 02: Two
**Goal:** c
## [MSD.03] Later
### [MSD.03] 01: Later
**Goal:** d
`;
const dirs = ['MSD.01-01-old', 'MSD.02-01-one', 'MSD.02-02-two', 'MSD.03-01-later'];
writeProject(roadmap, 'bracket', dirs);
const rp = require('../msd-core/bin/lib/roadmap-parser.cjs');
const ms = require('../msd-core/bin/lib/markdown-sectionizer.cjs');
const scope = rp.extractCurrentMilestone(
fs.readFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), 'utf-8'),
tmpDir,
);
const fenceCount = (scope.match(/^```/gm) || []).length;
assert.equal(fenceCount % 2, 0, 'pinned an ODD (unbalanced) fence count before this fix');
const headings = ms.tokenizeHeadings(scope).map((h) => h.text);
assert.ok(headings.some((t) => /MSD\.02.*01: One/.test(t)), 'real headings must survive tokenization, not collapse to just "Roadmap"');
const f = rp.getMilestonePhaseFilter(tmpDir);
assert.notEqual(f.phaseCount, 0, 'pinned 0 (pass-all degrade) before this fix');
assert.deepEqual(
Object.fromEntries(dirs.map((d) => [d, !!f(d)])),
{ 'MSD.01-01-old': false, 'MSD.02-01-one': true, 'MSD.02-02-two': true, 'MSD.03-01-later': false },
'pinned every directory admitted (pass-all) before this fix',
);
});
test('UPSTREAM COMPOSITION (repro12 LEGACY control): #3573 scopes the fenced-example fixture to total 2', () => {
// This bracket-only fix still does not widen the LEGACY
// `anyMilestonePattern` raw-match path. Upstream's newer state derivation,
// however, routes this read through fence-aware `sliceMilestoneWindow`
// using STATE's stored milestone. The fixture therefore no longer reaches
// the blind path and must pin the improved scoped result, not the old
// whole-document total of 4.
writeProject(`# Roadmap
Authoring guide:
\`\`\`markdown
## Milestone v9.0: Example
\`\`\`
## Milestone v2.0: Current
### Phase 01: One
**Goal:** b
### Phase 02: Two
**Goal:** c
`, undefined, [['03-stray', true], ['01-one', true], ['02-two', false], ['04-stray', true]]);
assert.equal(readTotal(), 2, 'upstream #3573 now scopes this read via sliceMilestoneWindow instead of the fence-blind anyMilestonePattern path');
});
test('PIN (repro10 A3): a fenced heading INSIDE the current section still must not terminate it', () => {
const dirs = ['MSD.01-01-old', 'MSD.02-01-one', 'MSD.02-02-two', 'MSD.03-01-later'];
writeProject(`# Roadmap
## [MSD.02] Current
### [MSD.02] 01: One
**Goal:** b
\`\`\`markdown
## [MSD.03] Not a real heading
\`\`\`
### [MSD.02] 02: Two
**Goal:** c
## [MSD.03] Later
### [MSD.03] 01: Later
**Goal:** d
`, 'bracket', dirs);
assert.equal(readTotal(), 2);
});
});
// ─── #2761 B3 (self-caught, round-2 verification): CURRENT version-bearing, ──
// ─── a sibling milestone is not ──────────────────────────────────────────────
//
// The B1 fix (commit 08d5b0c4) resolved `bracketScopeConvention` only inside
// the `if (headingMatches.length === 0)` gate that also drives SELECTION's own
// bracket fallback. That gate is correct for SELECTION (only try the bracket
// heading shape when the version-string match found nothing), but
// `bracketScopeConvention` also feeds `computeSectionEnd`'s and
// `preambleCutoff`'s boundary-detection — which accidentally inherited
// SELECTION's gate instead of having its own.
//
// Trigger shape: the CURRENT milestone heading is ITSELF version-bearing
// (`## [MSD.02] v2.0: Current Milestone`), so the primary version-string
// `sectionPattern` matches it directly — `headingMatches.length !== 0` from the
// very first check — and the entire bracket-resolution branch is skipped. A
// sibling milestone (PRIOR or LATER) that is version-less then gets NEITHER
// the version/emoji boundary rule (it has no version) NOR the bracket boundary
// rule (never resolved) — reproducing the exact #612 defect symptom
// (total_phases falls back to the whole-disk count) through a structural
// shape B1's own fixtures never exercised (every B1 fixture is uniformly
// version-bearing or uniformly version-less across all three milestones).
describe('#612 PR-2 B3: bracket boundaries engage even when CURRENT is version-bearing but a sibling is not', () => {
beforeEach(() => { tmpDir = createTempProject('adr-612-b3-'); });
afterEach(() => { cleanup(tmpDir); });
const ROADMAP = `# Roadmap
## [MSD.01] Prior Milestone
### [MSD.01] 01: Old one
**Goal:** a
## [MSD.02] v2.0: Current Milestone
### [MSD.02] 01: One
**Goal:** b
### [MSD.02] 02: Two
**Goal:** c
## [MSD.03] Later Milestone
### [MSD.03] 01: Later one
**Goal:** d
`;
const DIRS = ['MSD.01-01-old-one', 'MSD.02-01-one', 'MSD.02-02-two', 'MSD.03-01-later-one'];
const acceptsFor = (dirs) => {
const rp = require('../msd-core/bin/lib/roadmap-parser.cjs');
const f = rp.getMilestonePhaseFilter(tmpDir);
return Object.fromEntries(dirs.map((d) => [d, !!f(d)]));
};
test('CURRENT version-bearing + both siblings version-less: scoping works in both directions', () => {
writeProject(ROADMAP, 'bracket', DIRS);
assert.deepEqual(acceptsFor(DIRS), {
'MSD.01-01-old-one': false,
'MSD.02-01-one': true,
'MSD.02-02-two': true,
'MSD.03-01-later-one': false,
});
});
test('the PRIOR (version-less) milestone does not leak into the preamble (preambleCutoff twin)', () => {
writeProject(ROADMAP, 'bracket', DIRS);
assert.equal(acceptsFor(DIRS)['MSD.01-01-old-one'], false);
});
test('the LATER (version-less) milestone does not leak into scope (computeSectionEnd twin)', () => {
writeProject(ROADMAP, 'bracket', DIRS);
assert.equal(acceptsFor(DIRS)['MSD.03-01-later-one'], false);
});
test('total_phases counts only the asserted milestone, not the whole disk', () => {
writeProject(ROADMAP, 'bracket', DIRS);
// 4 directories on disk, 2 phases in the milestone STATE.md asserts.
assert.equal(readTotal(), 2);
});
});
// ─── #2761 Blocker 1 (round-3 adversarial re-verify): the preambleCutoff ────
// ─── scan drops current-milestone content whenever ANY bracket-shaped ──────
// ─── heading precedes the selected milestone heading ────────────────────────
//
// The round-2 fix (39c42a89) threaded `selectedBracketId` as the REAL value
// at computeSectionEnd but as bare `null` at the preambleCutoff scan — the
// deviation's own rationale ("the selected heading's own occurrence is
// always the correct earliest answer") was right, but `null` disables the
// same-milestone check for EVERY heading, not just the selected one. Any
// bracket-shaped heading earlier than the selected milestone — same id
// (cases A, B) or a DIFFERENT id with no children of its own (case D) — was
// wrongly accepted as the earliest boundary, and the region between it and
// the real `sectionStart` was silently dropped: a completed phase vanished
// and `state sync` persisted a confident 0% where base and round-1 both
// correctly wrote 50%.
//
// Fixed with two changes, both scoped to the preambleCutoff scan only
// (computeSectionEnd is untouched — it already threads the real
// `selectedBracketId`):
// (a) `h.offset === sectionStart` bypasses BOTH the same-milestone check
// and the same-id-child rule below — the selected heading's own
// position is definitionally correct, and this is the one case where
// neither discriminator should even run (rejecting it would be
// rejecting the heading against ITSELF).
// (b) every OTHER bracket-shaped, non-phase-tail-shaped candidate must
// additionally have a matching-id CHILD (the next strictly-deeper
// heading beneath it) to count as a boundary — `bracketHeadingHasMatchingChild`.
// This is what distinguishes a genuine sibling milestone (whose own
// phase children carry ITS bracket id) from an unrelated bracket-shaped
// PROSE heading like `## [ADR.612] Heading convention used by this
// roadmap` sitting above the current milestone's own content.
describe('#612 PR-2 Blocker 1 round-3: preambleCutoff identity is offset- and child-aware', () => {
beforeEach(() => { tmpDir = createTempProject('adr-612-b1r3-'); });
afterEach(() => { cleanup(tmpDir); });
const D = [['MSD.02-01-one', true], ['MSD.02-02-two', false]];
function poisonTotalPhases() {
const statePath = path.join(tmpDir, '.planning', 'STATE.md');
const raw = fs.readFileSync(statePath, 'utf-8');
fs.writeFileSync(statePath, raw.replace(/^---\r?\n/, '---\ntotal_phases: 999\n'), 'utf-8');
}
test('RED case A (rv-attack1): a same-milestone version-LESS heading earlier than the version-bearing selected heading — 2/1/50, not 1/0/0', () => {
writeProject(`# Roadmap
## [MSD.02] Foundation
- [ ] **[MSD.02] 01: One**
- [ ] **[MSD.02] 02: Two**
### [MSD.02] 01: One
**Goal:** a
## [MSD.02] v2.0: Foundation (Phase Details)
### [MSD.02] 02: Two
**Goal:** b
`, 'bracket', D);
assert.equal(readTotal(), 2, 'pinned 1 before this fix — the earlier checklist heading dropped everything before sectionStart');
poisonTotalPhases();
assert.equal(syncedTotal(), 2);
assert.equal(syncedPercent(), 50, 'pinned 0 before this fix — a confidently wrong persisted 0%');
});
test('RED case B (rv-attack1): a same-milestone OVERVIEW heading (no "(Phase Details)" spelling) earlier than the version-bearing selected heading — 2/1/50, not 1/0/0', () => {
writeProject(`# Roadmap
## [MSD.02] Foundation (overview)
### [MSD.02] 01: One
**Goal:** a
## [MSD.02] v2.0: Foundation
### [MSD.02] 02: Two
**Goal:** b
`, 'bracket', D);
assert.equal(readTotal(), 2, 'pinned 1 before this fix');
poisonTotalPhases();
assert.equal(syncedTotal(), 2);
assert.equal(syncedPercent(), 50, 'pinned 0 before this fix');
});
test('RED case D (rv-attack1b): a DIFFERENT-id bracket-shaped PROSE heading before the selected milestone — 2/1/50, not 1/0/0', () => {
writeProject(`# Roadmap
## [ADR.612] Heading convention used by this roadmap
Phases are listed under their milestone.
### [MSD.02] 01: One
**Goal:** a
## [MSD.02] v2.0: Foundation
### [MSD.02] 02: Two
**Goal:** b
`, 'bracket', D);
assert.equal(readTotal(), 2, 'pinned 1 before this fix — [ADR.612] read as a genuine boundary with no child-id check');
poisonTotalPhases();
assert.equal(syncedTotal(), 2);
assert.equal(syncedPercent(), 50, 'pinned 0 before this fix');
});
test('PIN: a genuine prior sibling milestone (real children sharing its own id) is still excluded from the preamble', () => {
const dirs = ['MSD.01-01-old-one', 'MSD.02-01-one', 'MSD.02-02-two'];
writeProject(`# Roadmap
## [MSD.01] Prior Milestone
### [MSD.01] 01: Old one
**Goal:** a
## [MSD.02] v2.0: Foundation
### [MSD.02] 01: One
**Goal:** b
### [MSD.02] 02: Two
**Goal:** c
`, 'bracket', dirs.map((d, i) => [d, i !== 2]));
const rp = require('../msd-core/bin/lib/roadmap-parser.cjs');
const f = rp.getMilestonePhaseFilter(tmpDir);
assert.equal(!!f('MSD.01-01-old-one'), false, 'the prior milestone must still be excluded — the child rule must not re-admit a genuine sibling');
assert.equal(readTotal(), 2);
});
test('PIN: a CHILDLESS prior sibling milestone (no phases of its own) degrades to NOT cutting the preamble — over-inclusive, safe', () => {
// "## [MSD.01] Empty Prior Milestone" has no deeper heading before the
// next heading at its own level — the child rule rejects it as a
// boundary, so its own heading TEXT stays in the preamble. That text is
// not phase-shaped (no digit-colon), so it contributes nothing to any
// count — over-inclusive, not under-inclusive, the safe direction.
writeProject(`# Roadmap
## [MSD.01] Empty Prior Milestone
## [MSD.02] v2.0: Current
### [MSD.02] 01: One
**Goal:** a
### [MSD.02] 02: Two
**Goal:** b
`, 'bracket', D);
assert.equal(readTotal(), 2, 'the current milestone\'s own two phases must still be counted, unpolluted by the childless sibling');
poisonTotalPhases();
assert.equal(syncedPercent(), 50);
});
test('Nit 2 PIN: a colon-less bracket heading ("[MSD.02] 05" with no trailing colon) does not spuriously terminate the preamble', () => {
// A colon-less bracket heading is bracket-shaped but NOT phase-tail-shaped
// (BRACKET_PHASE_TAIL_RE requires the trailing colon), so
// isBracketMilestoneBoundary alone would read it as a boundary. It is
// also — precisely because it is malformed/incomplete rather than a real
// milestone — childless (nothing deeper follows it before the next
// same-or-shallower heading), so the child rule neutralizes it here.
writeProject(`# Roadmap
### [MSD.02] 05
## [MSD.02] v2.0: Current
### [MSD.02] 01: One
**Goal:** a
### [MSD.02] 02: Two
**Goal:** b
`, 'bracket', D);
assert.equal(readTotal(), 2);
});
test('mechanism (rv-mech1): extractCurrentMilestone now scopes the FULL document for case A, phaseCount reflects both real phases', () => {
const roadmap = `# Roadmap
## [MSD.02] Foundation
- [ ] **[MSD.02] 01: One**
- [ ] **[MSD.02] 02: Two**
### [MSD.02] 01: One
**Goal:** a
## [MSD.02] v2.0: Foundation (Phase Details)
### [MSD.02] 02: Two
**Goal:** b
`;
writeProject(roadmap, 'bracket', D);
const rp = require('../msd-core/bin/lib/roadmap-parser.cjs');
const scope = rp.extractCurrentMilestone(
fs.readFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), 'utf-8'),
tmpDir,
);
assert.equal(scope, roadmap, 'pinned a truncated scope (bytes [11,124) dropped) before this fix');
const f = rp.getMilestonePhaseFilter(tmpDir);
assert.equal(f.phaseCount, 2, 'pinned phaseCount 1 before this fix');
assert.equal(!!f('MSD.02-01-one'), true, 'pinned false before this fix — a real dir of the CURRENT milestone');
assert.equal(!!f('MSD.02-02-two'), true);
});
});
// ─── #2761 Major 1 (round-3 adversarial re-verify): the version/emoji half ──
// ─── of preambleCutoff's min() is now fence-aware too, on the bracket ───────
// ─── branch only ─────────────────────────────────────────────────────────────
//
// ff6bf0a8 (round-2 Blocker 3) made the BRACKET half of preambleCutoff's
// "earliest milestone-shaped heading" search fence-aware, but left the
// VERSION/emoji half a raw `content.match` even on the bracket branch. A
// fenced VERSION-BEARING example heading in a bracket repo's preamble
// (ADR-612's own docs illustrate the LEGACY heading shape this way, inside a
// fenced authoring-guide block) was still textually the earliest match for
// that raw regex, winning the min() and formerly un-suppressing a wrong
// persisted 75%. On the rebased base, #3217 keeps the unsafe percentage
// suppressed explicitly when the version-less milestone cannot yield a
// COMPLETE version override.
describe('#612 PR-2 Major 1 round-3: preambleCutoff\'s version/emoji half is fence-aware on the bracket branch', () => {
beforeEach(() => { tmpDir = createTempProject('adr-612-m1r3-'); });
afterEach(() => { cleanup(tmpDir); });
const D = [
['MSD.02-01-one', true], ['MSD.02-02-two', false],
['MSD.03-01-x', true], ['MSD.03-02-y', true],
];
test('RED case C1 (rv-attack3c): a fenced VERSION-bearing example does not inflate the total; unsafe percent stays withheld', () => {
writeProject(`# Roadmap
Authoring guide — a milestone heading looks like:
\`\`\`markdown
## Milestone v9.0: Example
\`\`\`
## [MSD.02] Current
### [MSD.02] 01: One
### [MSD.02] 02: Two
## [MSD.03] Later
### [MSD.03] 01: X
### [MSD.03] 02: Y
`, 'bracket', D);
assert.equal(readTotal(), 2, 'pinned 4 before this fix — the fenced VERSION heading won the min() and swallowed everything before it into the preamble unstripped');
poisonTotalPhases();
assert.match(syncSkipReason(), /scope is "unscoped", not COMPLETE \(#3217\)/,
'upstream must keep the unsafe percentage withheld rather than resurface the old 75%');
});
test('PIN case C2: a fenced BRACKET-shaped heading in the same position (round-2\'s own fix target) stays unchanged at 2/1/50', () => {
writeProject(`# Roadmap
Authoring guide — a milestone heading looks like:
\`\`\`markdown
## [MSD.00] Example
\`\`\`
## [MSD.02] Current
### [MSD.02] 01: One
### [MSD.02] 02: Two
## [MSD.03] Later
### [MSD.03] 01: X
### [MSD.03] 02: Y
`, 'bracket', D);
assert.equal(readTotal(), 2);
});
function poisonTotalPhases() {
const statePath = path.join(tmpDir, '.planning', 'STATE.md');
const raw = fs.readFileSync(statePath, 'utf-8');
fs.writeFileSync(statePath, raw.replace(/^---\r?\n/, '---\ntotal_phases: 999\n'), 'utf-8');
}
});
// ─── #2761 round-3 hardening (team-lead review of f87bba0e): two edges in ──
// ─── the new preambleCutoff code ────────────────────────────────────────────
//
// AMENDMENT 1 — bracketHeadingHasMatchingChild originally checked only
// `headings[index + 1]` (the IMMEDIATE next heading), not the candidate's
// whole subtree. A genuine prior sibling milestone whose section opens with
// a non-bracket subsection before its first phase (`## [MSD.01] Setup` /
// `### Notes` / `### [MSD.01] 01: Old`) was therefore wrongly rejected as a
// boundary — its own real phase heading is TWO headings deep, not one — and
// its entire section leaked into the preamble unstripped.
//
// CONFIRMED RED at f87bba0e before this fix (per the team lead's request to
// check observability, not just theory): the leak is NOT merely inert —
// `MSD.01-01-old`'s directory was wrongly admitted into the CURRENT
// milestone's filter via the leaked heading's qualified key
// (`MSD.01-01`), reading **3/2/67%** where truth is **2/1/50%**. Scope
// membership DOES drive the disk-side filter on this shape. Fixed by
// scanning the candidate's full SUBTREE (continue past a non-matching
// deeper heading instead of returning false immediately; only a
// same-or-shallower heading actually closes the subtree).
//
// AMENDMENT 2 — the round-3 Major 1 fix (c483552a) ported the version/emoji
// half of preambleCutoff to the token-based scan with no level cap; the raw
// `content.match(anyMilestonePattern)` it replaced was anchored `^#{1,3}\s+`.
// A level-4+ version-bearing heading in the preamble (`#### v2.0 notes`)
// therefore won the scan on the bracket branch where the raw pattern (and
// the legacy path, unaffected) ignores it outright — a heading neither the
// selector nor `isMilestoneBounded` would ever treat as a milestone marker.
// Fixed with `if (h.level > 3) continue;`, mirroring the depth-sanity cap
// `isBracketMilestoneBoundary` already applies to the bracket half.
describe('#612 PR-2 round-3 hardening: subtree child scan + level cap on preambleCutoff', () => {
beforeEach(() => { tmpDir = createTempProject('adr-612-r3h-'); });
afterEach(() => { cleanup(tmpDir); });
test('RED (rv2-amend1): a genuine prior sibling with an intervening non-bracket subsection is still excluded — 2/1/50, not 3/2/67', () => {
const dirs = [['MSD.01-01-old', true], ['MSD.02-01-one', true], ['MSD.02-02-two', false]];
writeProject(`# Roadmap
## [MSD.01] Setup
### Notes
Some prose about the prior milestone.
### [MSD.01] 01: Old
## [MSD.02] v2.0: Current
### [MSD.02] 01: One
### [MSD.02] 02: Two
`, 'bracket', dirs);
assert.equal(readTotal(), 2, 'pinned 3 before this fix — the immediate-next-heading-only check rejected [MSD.01] Setup as a boundary because its FIRST child (### Notes) is not bracket-shaped, even though its SECOND child (### [MSD.01] 01: Old) is');
const rp = require('../msd-core/bin/lib/roadmap-parser.cjs');
const f = rp.getMilestonePhaseFilter(tmpDir);
assert.equal(!!f('MSD.01-01-old'), false, 'pinned true before this fix — the leaked heading\'s qualified key wrongly admitted the prior milestone\'s own directory');
assert.equal(!!f('MSD.02-01-one'), true);
assert.equal(!!f('MSD.02-02-two'), true);
});
test('PIN (rv2-amend2): a level-4 version-bearing preamble heading is NOT a cutoff on the bracket branch — the preamble text survives unstripped', () => {
const roadmap = `# Roadmap
#### v2.0 notes
Some prose that happens to mention v2.0 in a deep heading.
## [MSD.02] Current
### [MSD.02] 01: One
### [MSD.02] 02: Two
`;
writeProject(roadmap, 'bracket', [['MSD.02-01-one', true], ['MSD.02-02-two', false]]);
const rp = require('../msd-core/bin/lib/roadmap-parser.cjs');
const scope = rp.extractCurrentMilestone(
fs.readFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), 'utf-8'),
tmpDir,
);
assert.ok(scope.includes('v2.0 notes'), 'pinned dropped before this fix — the level-4 heading wrongly won the earliest-of-either scan and truncated the preamble at itself');
assert.equal(readTotal(), 2);
});
test('PIN: the LEGACY control for the level-4 preamble heading is unchanged (raw content.match path untouched)', () => {
writeProject(`# Roadmap
#### v2.0 notes
Some prose that happens to mention v2.0 in a deep heading.
## Milestone v2.0: Current
### Phase 01: One
### Phase 02: Two
`, undefined, [['01-one', true], ['02-two', false]]);
assert.equal(readTotal(), 2);
});
});
// ─── #2761 round-4 (team-lead re-verify of fbfd0fca): the child rule must ──
// ─── require a same-id PHASE, not merely a same-id heading ──────────────────
//
// bracketHeadingHasMatchingChild's subtree scan (fbfd0fca) proved SAME-ID-NESS
// but never asked whether the matching child was PHASE-shaped. Case F1
// reopens round-3's case D one heading later: `## [ADR.612] Heading
// convention` is followed by its OWN sub-heading `### [ADR.612] Examples` —
// same bracket id as the candidate, but MILESTONE-shaped (a name, no
// digit-then-colon), not a phase. Same-id-ness alone satisfied the subtree
// scan, re-cutting the preamble at exactly the shape the round-3 hardening
// was written to close.
function poisonTotalPhasesR4(dir) {
const statePath = path.join(dir, '.planning', 'STATE.md');
const raw = fs.readFileSync(statePath, 'utf-8');
fs.writeFileSync(statePath, raw.replace(/^---\r?\n/, '---\ntotal_phases: 999\n'), 'utf-8');
}
describe('#612 PR-2 round-4 Blocker 1: the same-id child must be PHASE-shaped', () => {
beforeEach(() => { tmpDir = createTempProject('adr-612-r4b1-'); });
afterEach(() => { cleanup(tmpDir); });
test('RED (F1): a same-id MILESTONE-shaped child on a prose heading no longer satisfies the child rule — 2/1/50, not 1/0/0', () => {
writeProject(`# Roadmap
## [ADR.612] Heading convention
### [ADR.612] Examples
Prose about the convention.
### [MSD.02] 01: One
**Goal:** a
## [MSD.02] v2.0: Foundation
### [MSD.02] 02: Two
**Goal:** b
`, 'bracket', [['MSD.02-01-one', true], ['MSD.02-02-two', false]]);
assert.equal(readTotal(), 2, 'pinned 1 before this fix — [ADR.612]\'s own MILESTONE-shaped sub-heading satisfied the same-id-only rule');
poisonTotalPhasesR4(tmpDir);
assert.equal(syncedTotal(), 2);
assert.equal(syncedPercent(), 50, 'pinned 0 before this fix — state sync reported "nothing to do" because its wrong 0% happened to equal the seed');
});
test('PIN (F11): a genuine prior sibling whose same-id child lacks the trailing colon is still correctly excluded (inert leak, not a boundary either way)', () => {
const dirs = [['MSD.01-01-old', true], ['MSD.02-01-one', true], ['MSD.02-02-two', false]];
writeProject(`# Roadmap
## [MSD.01] Setup
### [MSD.01] 01 Old
## [MSD.02] v2.0: Current
### [MSD.02] 01: One
**Goal:** a
### [MSD.02] 02: Two
**Goal:** b
`, 'bracket', dirs);
assert.equal(readTotal(), 2);
const rp = require('../msd-core/bin/lib/roadmap-parser.cjs');
const f = rp.getMilestonePhaseFilter(tmpDir);
assert.equal(!!f('MSD.01-01-old'), false, 'the colon-less heading forms no qualified key either, so the childless-degrade leak is inert');
});
test('PIN (F11b): a genuine prior sibling whose phases exist only as bullets is still correctly excluded', () => {
const dirs = [['MSD.01-01-old', true], ['MSD.02-01-one', true], ['MSD.02-02-two', false]];
writeProject(`# Roadmap
## [MSD.01] Setup
- [x] **[MSD.01] 01: Old**
### [MSD.01] Retrospective
## [MSD.02] v2.0: Current
### [MSD.02] 01: One
**Goal:** a
### [MSD.02] 02: Two
**Goal:** b
`, 'bracket', dirs);
assert.equal(readTotal(), 2);
const rp = require('../msd-core/bin/lib/roadmap-parser.cjs');
const f = rp.getMilestonePhaseFilter(tmpDir);
assert.equal(!!f('MSD.01-01-old'), false, 'a bracket bullet never forms a qualified key (BULLET_PHASE_LINE_PATTERN needs a literal **Phase )');
});
test('re-verify: the four existing child-rule pins (F2-F5 shapes) are unaffected by the phase-shape requirement', () => {
// F2: subtree closure — same-id child appears only after a same-level
// heading closes the subtree; must not count (over-inclusive degrade).
writeProject(`# Roadmap
## [MSD.01] Setup
Prose, no children.
## [DOC.99] Reference
### [MSD.01] 01: Old
### [MSD.02] 01: One
**Goal:** a
## [MSD.02] v2.0: Current
### [MSD.02] 02: Two
**Goal:** b
`, 'bracket', [['MSD.01-01-old', true], ['MSD.02-01-one', true], ['MSD.02-02-two', false]]);
assert.equal(readTotal(), 3, 'unchanged — the declared over-inclusive/safe degrade (Minor 1), not a regression');
// F3: deep wrong-id children then a same-id PHASE child inside the subtree — must count.
writeProject(`# Roadmap
## [MSD.01] Setup
### Notes
#### Deep note
### [DOC.99] Aside
### [MSD.01] 01: Old
## [MSD.02] v2.0: Current
### [MSD.02] 01: One
**Goal:** a
### [MSD.02] 02: Two
**Goal:** b
`, 'bracket', [['MSD.01-01-old', true], ['MSD.02-01-one', true], ['MSD.02-02-two', false]]);
assert.equal(readTotal(), 2);
// F4: the same-id PHASE child is itself level 4 — the child scan has no depth ceiling.
writeProject(`# Roadmap
## [MSD.01] Setup
### Notes
#### [MSD.01] 01: Old
## [MSD.02] v2.0: Current
### [MSD.02] 01: One
**Goal:** a
### [MSD.02] 02: Two
**Goal:** b
`, 'bracket', [['MSD.01-01-old', true], ['MSD.02-01-one', true], ['MSD.02-02-two', false]]);
assert.equal(readTotal(), 2);
// F5: trailing bracket sibling at document end (childless) — computeSectionEnd
// excludes it without needing the child rule at all.
writeProject(`# Roadmap
## [MSD.02] v2.0: Current
### [MSD.02] 01: One
**Goal:** a
### [MSD.02] 02: Two
**Goal:** b
## [MSD.03] Later
`, 'bracket', [['MSD.02-01-one', true], ['MSD.02-02-two', false], ['MSD.03-01-later', true]]);
assert.equal(readTotal(), 2);
});
});
// ─── #2761 round-4 Major 1: the four fence-blind sites known at round 4 ────
// ─── on the bracket path (a fifth — the retirement scan — was found at ────
// ─── round 5 and has its own block further below) ──────────────────────────
//
// The scope string extractCurrentMilestone returns is fence-BALANCED and
// fence-STRIPPED-by-tokenizeHeadings only when its CONSUMERS ask it that way.
// Four sites still read it (or the raw ROADMAP) with a plain regex `.exec`,
// blind to fences:
//
// (a) roadmapPhaseCount — TWO independent copies, buildStateFrontmatter
// (read path) and cmdStateSync (write path). A fenced EXAMPLE phase
// heading in the preamble inflated total_phases (F10); on a
// version-less roadmap the SAME fence-blindness compounds with (c)
// below (F9).
// (b) isMilestoneBounded — a raw `.test(roadmapRaw)`. A fenced-ONLY bracket
// heading (no real section for the asserted milestone at all) wrongly
// BOUNDED a milestone that isn't in the roadmap, un-suppressing a
// percent that should stay suppressed (F12).
// (c) the bracket-fallback SELECTOR (only reachable when the version-string
// selection found nothing) — a raw `content.matchAll`. A fenced example
// sharing the CURRENT project's own bracket id could be SELECTED as the
// current milestone, landing `sectionStart` inside a fence (F9).
//
// Fixed by extracting ONE shared counter (countRoadmapPhaseHeadings, in
// src/state.cts, immediately above buildStateFrontmatter) used by both (a)
// copies, and converting (b) and (c) to tokenizeHeadings-based scans. The
// PRODUCER (extractCurrentMilestone's returned scope string) is deliberately
// UNCHANGED — every other consumer of that string needs its full content
// fidelity, and legacy identity forbids touching the shared string. Legacy
// (non-bracket) behavior at all four sites is byte-identical; this is the
// ONLY commit in this arc that touches SELECTION.
describe('#612 PR-2 round-4 Major 1: the four fence-blind sites known at round 4 (a fifth was found at round 5, see its own block below)', () => {
beforeEach(() => { tmpDir = createTempProject('adr-612-r4m1-'); });
afterEach(() => { cleanup(tmpDir); });
const D2 = [['MSD.02-01-one', true], ['MSD.02-02-two', false]];
function poisonTotalPhasesR4M1() {
const statePath = path.join(tmpDir, '.planning', 'STATE.md');
const raw = fs.readFileSync(statePath, 'utf-8');
fs.writeFileSync(statePath, raw.replace(/^---\r?\n/, '---\ntotal_phases: 999\n'), 'utf-8');
}
test('RED (F10): a fenced SAME-id PHASE heading in the preamble no longer inflates roadmapPhaseCount — 2/1/50, not 3/1/33', () => {
writeProject(`# Roadmap
Authoring guide:
\`\`\`markdown
### [MSD.02] 05: Example phase
\`\`\`
## [MSD.02] v2.0: Foundation
### [MSD.02] 01: One
**Goal:** a
### [MSD.02] 02: Two
**Goal:** b
`, 'bracket', D2);
assert.equal(readTotal(), 2, 'pinned 3 before this fix — the fenced example phase heading was counted by the fence-blind raw .exec()');
poisonTotalPhasesR4M1();
assert.equal(syncedTotal(), 2);
assert.equal(syncedPercent(), 50, 'pinned 33 before this fix');
});
test('PIN (F10 LEGACY control): the same fenced-phase shape on a non-bracket repo is unchanged (correct on every build)', () => {
writeProject(`# Roadmap
Authoring guide:
\`\`\`markdown
### Phase 05: Example phase
\`\`\`
## Milestone v2.0: Foundation
### Phase 01: One
**Goal:** a
### Phase 02: Two
**Goal:** b
`, undefined, [['01-one', true], ['02-two', false]]);
assert.equal(readTotal(), 2);
});
test('PIN (F10c): same document, fenced line is NOT phase-shaped — unaffected either way', () => {
writeProject(`# Roadmap
Authoring guide:
\`\`\`markdown
### [MSD.02] Example section
\`\`\`
## [MSD.02] v2.0: Foundation
### [MSD.02] 01: One
**Goal:** a
### [MSD.02] 02: Two
**Goal:** b
`, 'bracket', D2);
assert.equal(readTotal(), 2);
});
test('RED (F9): version-LESS roadmap + a fenced example sharing the SAME milestone id — both the selector and the counter must be fence-aware — 2/1/50, not 3/1/33', () => {
writeProject(`# Roadmap
Authoring guide:
\`\`\`markdown
## [MSD.02] Example milestone heading
### [MSD.02] 05: Example phase
\`\`\`
## [MSD.02] Foundation
### [MSD.02] 01: One
**Goal:** a
### [MSD.02] 02: Two
**Goal:** b
`, 'bracket', D2);
assert.equal(readTotal(), 2, 'pinned 3 before this fix');
poisonTotalPhasesR4M1();
assert.equal(syncedTotal(), 2);
// The roadmap is version-less while STATE pins v2.0, so upstream #3217
// withholds the percentage rather than treating the scope as complete.
assert.match(syncSkipReason(), /scope is "unscoped", not COMPLETE \(#3217\)/);
});
test('RED (F12): a fenced-ONLY bracket heading no longer bounds a milestone absent from the roadmap — percent stays suppressed', () => {
const dirs = [['MSD.01-01-old', true], ['MSD.02-01-one', true], ['MSD.02-02-two', false]];
writeProject(`# Roadmap
Authoring guide:
\`\`\`markdown
## [MSD.02] Example milestone heading
\`\`\`
## [MSD.01] v1.0: Prior
### [MSD.01] 01: Old
**Goal:** a
`, 'bracket', dirs);
const r = runMsdTools(['state', 'json'], tmpDir);
assert.ok(r.success, `state json failed: ${r.error}`);
const progress = JSON.parse(r.output).progress;
assert.equal(progress?.percent, undefined, 'pinned 67 before this fix — a fenced-only [MSD.02] example wrongly bounded a milestone with no real section');
// state sync must not persist a percent either — the body stays at its seed.
const syncResult = runMsdTools(['state', 'sync'], tmpDir);
assert.ok(syncResult.success);
const raw = fs.readFileSync(path.join(tmpDir, '.planning', 'STATE.md'), 'utf-8');
assert.match(raw, /\*\*Progress:\*\*[^\r\n]*?0%/, 'pinned 67% persisted before this fix');
});
test('PIN: unfenced bracket-fallback selection is byte-identical — first real milestone-shaped heading still wins', () => {
// No version anywhere, no fences — the selector's plain first-match-wins
// behavior over REAL headings must be completely unaffected by routing it
// through tokenizeHeadings.
const dirs = ['MSD.01-01-old', 'MSD.02-01-one', 'MSD.02-02-two', 'MSD.03-01-later'];
writeProject(`# Roadmap
## [MSD.01] Prior Milestone
### [MSD.01] 01: Old
**Goal:** a
## [MSD.02] Current Milestone
### [MSD.02] 01: One
**Goal:** b
### [MSD.02] 02: Two
**Goal:** c
## [MSD.03] Later Milestone
### [MSD.03] 01: Later
**Goal:** d
`, 'bracket', dirs);
const rp = require('../msd-core/bin/lib/roadmap-parser.cjs');
const f = rp.getMilestonePhaseFilter(tmpDir);
assert.deepEqual(Object.fromEntries(dirs.map((d) => [d, !!f(d)])), {
'MSD.01-01-old': false,
'MSD.02-01-one': true,
'MSD.02-02-two': true,
'MSD.03-01-later': false,
});
assert.equal(readTotal(), 2);
});
});
// ─── #2761 round-5 Blocker 1: 3be5c412's merge dropped the `bracketId &&` ───
// ─── guard countRoadmapPhaseHeadings' bracket branch needs ──────────────────
//
// Every other isSentinelPhaseId call site in src/ (roadmap-parser.cts,
// roadmap.cts, validate.cts ×2, verify.cts) guards the call with
// `bracketId &&`. The shared counter's bracket branch omitted it: when the
// LEGACY alternative of the intro grammar matches (a `### Phase 00:` heading
// in a `phase_id_convention: "bracket"` repo — the mid-migration shape this
// PR exists for), `m[1]` (bracketId) is `undefined`, and the call becomes
// `isSentinelPhaseId("undefined-00", 'bracket')` — which is TRUE, silently
// dropping a real phase from the denominator. `getMilestonePhaseFilter`
// still counts it and admits its directory, so the filter and the counter
// disagree — a half-done milestone reads as 100% complete.
describe('#612 PR-2 round-5 Blocker 1: countRoadmapPhaseHeadings restores the bracketId guard', () => {
beforeEach(() => { tmpDir = createTempProject('adr-612-r5b1-'); });
afterEach(() => { cleanup(tmpDir); });
function poisonTotalPhasesR5(dir) {
const statePath = path.join(dir, '.planning', 'STATE.md');
const raw = fs.readFileSync(statePath, 'utf-8');
fs.writeFileSync(statePath, raw.replace(/^---\r?\n/, '---\ntotal_phases: 999\n'), 'utf-8');
}
test('RED (G3): a legacy `### Phase 00:` heading in a bracket repo is no longer dropped — 3/2/67, not 2/2/100', () => {
writeProject(`# Roadmap
## Milestone v2.0: Foundation
### Phase 00: Bootstrap
**Goal:** z
### Phase 01: One
**Goal:** a
### Phase 02: Two
**Goal:** b
`, 'bracket', [['00-bootstrap', true], ['01-one', true]]);
assert.equal(readTotal(), 3, 'pinned 2 before this fix — "undefined-00" read as a sentinel');
poisonTotalPhasesR5(tmpDir);
assert.equal(syncedTotal(), 3);
assert.equal(syncedPercent(), 67, 'pinned 100 before this fix — a half-done milestone read as shipped');
});
test('PIN (G3 LEGACY control): the identical document under the legacy convention is unaffected', () => {
writeProject(`# Roadmap
## Milestone v2.0: Foundation
### Phase 00: Bootstrap
**Goal:** z
### Phase 01: One
**Goal:** a
### Phase 02: Two
**Goal:** b
`, undefined, [['00-bootstrap', true], ['01-one', true]]);
// Upstream #3185 canonically treats legacy 00 as a sentinel; the bracket
// branch deliberately remains narrower, which is the distinction above.
assert.equal(readTotal(), 2);
});
test('RED (G3d, mixed mid-migration): bracket headings PLUS one legacy `### Phase 00:` — 3/2/67, not 2/2/100', () => {
writeProject(`# Roadmap
## [MSD.02] v2.0: Foundation
### Phase 00: Bootstrap
**Goal:** z
### [MSD.02] 01: One
**Goal:** a
### [MSD.02] 02: Two
**Goal:** b
`, 'bracket', [['00-bootstrap', true], ['MSD.02-01-one', true]]);
assert.equal(readTotal(), 3, 'pinned 2 before this fix');
poisonTotalPhasesR5(tmpDir);
assert.equal(syncedTotal(), 3);
assert.equal(syncedPercent(), 67, 'pinned 100 before this fix');
});
test('PIN (G3b, isolates the counter): a `### Phase 000:` heading with no directory — total still 3, no dir to admit', () => {
writeProject(`# Roadmap
## Milestone v2.0: Foundation
### Phase 000: Bootstrap
**Goal:** z
### Phase 01: One
**Goal:** a
### Phase 02: Two
**Goal:** b
`, 'bracket', [['01-one', true], ['02-two', false]]);
assert.equal(readTotal(), 3, 'pinned 2 before this fix');
});
test('PIN (G3c control): legacy `### Phase 01:`/`02:` only, no sentinel-shaped token — unaffected', () => {
writeProject(`# Roadmap
## Milestone v2.0: Foundation
### Phase 01: One
**Goal:** a
### Phase 02: Two
**Goal:** b
`, 'bracket', [['01-one', true], ['02-two', false]]);
assert.equal(readTotal(), 2);
});
});
// ─── #2761 round-5 Major 1: extractRetiredPhaseNumbers is a FIFTH ───────────
// ─── fence-blind site on the bracket path ───────────────────────────────────
//
// The retirement gesture's own line scan iterates `scope.split(/\r?\n/)`
// with no fence awareness. Once the bracket alternative is compiled into
// `introSrc` (this PR's own change), a FENCED authoring EXAMPLE showing the
// #1514 retirement gesture in bracket spelling is indistinguishable from a
// real one — it retires a genuine phase, shrinking the denominator and
// jumping the percent to a confident 100%.
describe('#612 PR-2 round-5 Major 1: extractRetiredPhaseNumbers is fence-aware on the bracket path', () => {
beforeEach(() => { tmpDir = createTempProject('adr-612-r5m1-'); });
afterEach(() => { cleanup(tmpDir); });
const D2 = [['MSD.02-01-one', true], ['MSD.02-02-two', false]];
test('RED (G2): a fenced retired-strikethrough bracket bullet in the preamble no longer retires a real phase — 2/1/50, not 1/1/100', () => {
writeProject(`# Roadmap
Retiring a phase looks like this:
\`\`\`markdown
- [x] ~~**[MSD.02] 02: Two**~~ — folded into 03; number retired
\`\`\`
## [MSD.02] v2.0: Foundation
### [MSD.02] 01: One
**Goal:** a
### [MSD.02] 02: Two
**Goal:** b
`, 'bracket', D2);
assert.equal(readTotal(), 2, 'pinned 1 before this fix — the fenced EXAMPLE retirement gesture wrongly retired phase 02');
});
test('RED (G2b): the same fenced retirement example placed INSIDE the milestone section — not a preamble-scoping artifact', () => {
writeProject(`# Roadmap
## [MSD.02] v2.0: Foundation
Retiring a phase looks like this:
\`\`\`markdown
- [x] ~~**[MSD.02] 02: Two**~~ — folded; number retired
\`\`\`
### [MSD.02] 01: One
**Goal:** a
### [MSD.02] 02: Two
**Goal:** b
`, 'bracket', D2);
assert.equal(readTotal(), 2, 'pinned 1 before this fix');
});
test('PIN (G2 LEGACY control): the same fenced-example shape on a non-bracket repo is unchanged (pre-existing, out of scope)', () => {
writeProject(`# Roadmap
Retiring a phase looks like this:
\`\`\`markdown
- [x] ~~**Phase 02: Two**~~ — folded into 03; number retired
\`\`\`
## Milestone v2.0: Foundation
### Phase 01: One
**Goal:** a
### Phase 02: Two
**Goal:** b
`, undefined, [['01-one', true], ['02-two', false]]);
assert.equal(readTotal(), 1, 'pre-existing legacy hazard — deliberately unchanged, base is wrong here too');
});
});
// ─── #2761 round-5 Major 2: state sync's own counter now excludes the ──────
// ─── bare `999` icebox token like the other two derivations ─────────────────
//
// Under bracket, READING-B puts the sentinel in the BRACKET
// (isSentinelPhaseId), so `/^999\b/` on the bare TOKEN is the only thing
// excluding a `### [MSD.02] 999:` icebox heading — and it ran on the read
// path (buildStateFrontmatter) and getMilestonePhaseFilter, but not on
// cmdStateSync's own counter. One `state sync` call could leave a single
// STATE.md with its frontmatter (percent 50, from the read-path re-sync) and
// its body (percent 33, from the write-path counter that still counted the
// icebox heading) disagreeing.
describe('#612 PR-2 round-5 Major 2: state sync excludes the bracket 999 icebox token like the read path', () => {
beforeEach(() => { tmpDir = createTempProject('adr-612-r5m2-'); });
afterEach(() => { cleanup(tmpDir); });
test('RED (G1): state sync\'s body percent now agrees with state json\'s percent on a bracket 999 icebox heading', () => {
writeProject(`# Roadmap
## [MSD.02] v2.0: Foundation
### [MSD.02] 01: One
**Goal:** a
### [MSD.02] 02: Two
**Goal:** b
### [MSD.02] 999: Backlog item
**Goal:** later
`, 'bracket', [['MSD.02-01-one', true], ['MSD.02-02-two', false]]);
const readPercent = (() => {
const r = runMsdTools(['state', 'json'], tmpDir);
assert.ok(r.success, `state json failed: ${r.error}`);
return JSON.parse(r.output).progress?.percent;
})();
assert.equal(readPercent, 50);
assert.equal(syncedPercent(), readPercent, 'pinned 33 (vs read-path 50) before this fix — one STATE.md, two disagreeing numbers');
});
test('PIN (G1 LEGACY control): the pre-existing legacy read/write divergence on the same shape is unchanged', () => {
writeProject(`# Roadmap
## Milestone v2.0: Foundation
### Phase 01: One
**Goal:** a
### Phase 02: Two
**Goal:** b
### Phase 999: Backlog item
**Goal:** later
`, undefined, [['01-one', true], ['02-two', false]]);
const r = runMsdTools(['state', 'json'], tmpDir);
assert.ok(r.success, `state json failed: ${r.error}`);
assert.equal(JSON.parse(r.output).progress?.percent, 50);
assert.equal(syncedPercent(), 33, 'the legacy asymmetry is genuinely pre-existing — deliberately unchanged');
});
});
// ─── #2761 round-5 Minor 1: the two tokenizeHeadings reconstructions ───────
// ─── (bracket-fallback selector, isMilestoneBounded) accept indented ───────
// ─── headings their raw line-start-anchored predecessors never did ─────────
//
// `HeadingToken.offset` is `tokenizeHeadings`' LINE-START offset. For a
// ≤3-space-indented heading that is NOT the `#` character's own offset, so
// a token the raw `^#{1,3}\s+\[...` regex never matched (indentation moves
// it off the line-start anchor) was still accepted by the reconstruction,
// which then mis-parses (selectedBracketId null, a sibling milestone's
// phases leaking into the scope).
describe('#612 PR-2 round-5 Minor 1: bracket-fallback selector skips indented headings (raw parity)', () => {
beforeEach(() => { tmpDir = createTempProject('adr-612-r5min1-'); });
afterEach(() => { cleanup(tmpDir); });
test('RED (G6): a 2-space-indented, version-less bracket milestone heading no longer leaks the NEXT milestone\'s phases in — total 2, not 3', () => {
writeProject(`# Roadmap
## [MSD.02] Foundation
### [MSD.02] 01: One
**Goal:** a
### [MSD.02] 02: Two
**Goal:** b
## [MSD.03] Later
### [MSD.03] 01: Later one
**Goal:** c
`, 'bracket', [['MSD.02-01-one', true], ['MSD.02-02-two', false], ['MSD.03-01-later-one', true]]);
assert.equal(readTotal(), 2, 'pinned 3 before this fix — MSD.03\'s phase leaked into scope');
assert.match(syncSkipReason(), /scope is "unscoped", not COMPLETE \(#3217\)/,
'upstream must withhold the unsafe version-less percentage');
});
test('PIN (G6c UNINDENTED control): the identical document with no leading indent is unaffected — total 2', () => {
writeProject(`# Roadmap
## [MSD.02] Foundation
### [MSD.02] 01: One
**Goal:** a
### [MSD.02] 02: Two
**Goal:** b
## [MSD.03] Later
### [MSD.03] 01: Later one
**Goal:** c
`, 'bracket', [['MSD.02-01-one', true], ['MSD.02-02-two', false], ['MSD.03-01-later-one', true]]);
assert.equal(readTotal(), 2);
assert.match(syncSkipReason(), /scope is "unscoped", not COMPLETE \(#3217\)/);
});
// G6 above happens to leave isMilestoneBounded's own verdict unchanged
// either way — the phase headings (`### [MSD.02] 01: One`, unindented)
// already satisfy its loose bracket-prefix regex, so it returns bounded=true
// both before and after this fix on that fixture. This second fixture
// isolates isMilestoneBounded specifically: the ONLY `[MSD.02]`-shaped
// heading anywhere in the document is indented, mirroring round-4's F12
// (fenced-ONLY) shape but with indentation as the parity gap instead of a
// fence.
test('RED (isMilestoneBounded site, indented-ONLY): an indented-only bracket heading no longer bounds a milestone absent from the roadmap — percent stays suppressed', () => {
const dirs = [['MSD.01-01-old', true], ['MSD.02-01-one', true], ['MSD.02-02-two', false]];
writeProject(`# Roadmap
Authoring guide:
## [MSD.02] Example milestone heading
## [MSD.01] v1.0: Prior
### [MSD.01] 01: Old
**Goal:** a
`, 'bracket', dirs);
const r = runMsdTools(['state', 'json'], tmpDir);
assert.ok(r.success, `state json failed: ${r.error}`);
const progress = JSON.parse(r.output).progress;
assert.equal(progress?.percent, undefined, 'pinned 100 before this fix — an indented-only [MSD.02] example wrongly bounded a milestone with no real section');
const syncResult = runMsdTools(['state', 'sync'], tmpDir);
assert.ok(syncResult.success);
const raw = fs.readFileSync(path.join(tmpDir, '.planning', 'STATE.md'), 'utf-8');
assert.match(raw, /\*\*Progress:\*\*[^\r\n]*?0%/, 'pinned 100% persisted before this fix; body must stay at its seed');
});
});
// ─── #2761 round-6 Blocker 1: countRoadmapPhaseHeadings' bracket-only ──────
// ─── `/^0\b/` sibling rule needs the SAME `bracketId &&` guard round-5's ───
// ─── Blocker 1 restored two lines above it ──────────────────────────────────
//
// Round-5's Blocker 1 was `isSentinelPhaseId("undefined-00")`. This is the
// identical failure one line down: when the phase-heading grammar's LEGACY
// alternative matches (`### Phase 0:` in a `phase_id_convention: "bracket"`
// repo — the mid-migration shape this PR exists for), `bracketId` is
// `undefined`, and the unguarded `/^0\b/` fires on the bare token anyway.
// Neither the LEGACY branch of this same function nor
// `getMilestonePhaseFilter` has a `/^0\b/` rule at all, so the filter counts
// the phase and admits its completed directory while the counter refuses to
// count its heading — a milestone with an unstarted phase 02 persists as a
// confident 100%. `/^0\b/` matches `0` and `0.5` (word boundary before the
// dot) but not `00` (no boundary between the two zeros) — which is exactly
// why round-5's G3/G3d fixtures (both spelled `Phase 00:`) never tripped
// this one.
describe('#612 PR-2 round-6 Blocker 1: countRoadmapPhaseHeadings\' bracket-only /^0\\b/ rule restores the bracketId guard', () => {
beforeEach(() => { tmpDir = createTempProject('adr-612-r6b1-'); });
afterEach(() => { cleanup(tmpDir); });
function poisonTotalPhasesR6(dir) {
const statePath = path.join(dir, '.planning', 'STATE.md');
const raw = fs.readFileSync(statePath, 'utf-8');
fs.writeFileSync(statePath, raw.replace(/^---\r?\n/, '---\ntotal_phases: 999\n'), 'utf-8');
}
test('RED (T0): a legacy `### Phase 0:` heading in a bracket repo is no longer dropped — 3/2/67, not 2/2/100', () => {
writeProject(`# Roadmap
## Milestone v2.0: Foundation
### Phase 0: Bootstrap
**Goal:** z
### Phase 01: One
**Goal:** a
### Phase 02: Two
**Goal:** b
`, 'bracket', [['0-bootstrap', true], ['01-one', true]]);
assert.equal(readTotal(), 3, 'pinned 2 before this fix — "0" read as a sentinel with no bracketId guard');
poisonTotalPhasesR6(tmpDir);
assert.equal(syncedTotal(), 3);
assert.equal(syncedPercent(), 67, 'pinned 100 before this fix — a milestone with an unstarted phase 02 read as shipped');
});
test('PIN (T0L LEGACY control): the identical document follows upstream legacy sentinel semantics', () => {
writeProject(`# Roadmap
## Milestone v2.0: Foundation
### Phase 0: Bootstrap
**Goal:** z
### Phase 01: One
**Goal:** a
### Phase 02: Two
**Goal:** b
`, undefined, [['0-bootstrap', true], ['01-one', true]]);
assert.equal(readTotal(), 2, 'upstream #3185 canonically excludes legacy phase 0');
});
test('RED (T05): a legacy `### Phase 0.5:` heading in a bracket repo — same defect, second spelling — 3/2/67, not 2/2/100', () => {
writeProject(`# Roadmap
## Milestone v2.0: Foundation
### Phase 0.5: Bootstrap
**Goal:** z
### Phase 01: One
**Goal:** a
### Phase 02: Two
**Goal:** b
`, 'bracket', [['0.5-bootstrap', true], ['01-one', true]]);
assert.equal(readTotal(), 3, 'pinned 2 before this fix');
poisonTotalPhasesR6(tmpDir);
assert.equal(syncedTotal(), 3);
assert.equal(syncedPercent(), 67, 'pinned 100 before this fix');
});
test('PIN (T05L LEGACY control): the identical document follows upstream legacy sentinel semantics', () => {
writeProject(`# Roadmap
## Milestone v2.0: Foundation
### Phase 0.5: Bootstrap
**Goal:** z
### Phase 01: One
**Goal:** a
### Phase 02: Two
**Goal:** b
`, undefined, [['0.5-bootstrap', true], ['01-one', true]]);
assert.equal(readTotal(), 2, 'upstream #3185 canonically excludes legacy phase 0.5');
});
test('PIN (B0, bracket-spelled `0` token — Minor 1, NOT fixed this round): counter/filter disagreement is base-parity, deliberately unchanged', () => {
// `### [MSD.02] 0: Bootstrap` — the SAME token shape, spelled in bracket
// form rather than legacy form. `isSentinelPhaseId('MSD.02-0','bracket')`
// is false (READING-B puts the sentinel in the bracket, not the token),
// so `/^0\b/` is the only thing that could exclude it — and per round-6's
// review this shape reads 2/2/100 on base AND HEAD (never closed by any
// build in this arc), so it is a pre-existing gap, not a regression.
// Disclosed in the changeset; pinned here as a base-parity characterization.
writeProject(`# Roadmap
## [MSD.02] v2.0: Foundation
### [MSD.02] 0: Bootstrap
**Goal:** z
### [MSD.02] 01: One
**Goal:** a
### [MSD.02] 02: Two
**Goal:** b
`, 'bracket', [['MSD.02-0-bootstrap', true], ['MSD.02-01-one', true]]);
assert.equal(readTotal(), 2, 'base-parity: this shape has never been closed by any build in this arc');
});
});
// ═════════════════════════════════════════════════════════════════════════
// #2761 round-11 BLOCKER (trek-e review): `listMilestonePhaseDirs`'s third
// param (`phaseIdConvention`) lost its `= null` default so a caller that
// omits it now gets "resolve from config" instead of "explicitly not
// bracket" — a deliberate flip, but the changeset claimed "the archival and
// milestone-completion paths are unchanged." False: `cmdMilestoneComplete`
// (src/milestone.cts) and `cmdStateUpdateProgress` (src/state.cts) both call
// the enumerator and neither file was in the PR-2 diff. Both now thread the
// convention explicitly (resolved once, ambiently, off `cwd`) instead of
// silently inheriting the lazy default. These two blocks PIN the enumerated
// set / write behavior on a bracket project so a future regression to a
// hardcoded (non-bracket) default — or to a different resolve-from-config
// answer — is a failing test, not a silent set change on a command that
// renames/archives directories or writes STATE.md.
// ═════════════════════════════════════════════════════════════════════════
describe('#2761 round-11 BLOCKER: cmdMilestoneComplete enumerates the bracket-scoped set', () => {
beforeEach(() => { tmpDir = createTempProject('adr-612-milestone-complete-'); });
afterEach(() => { cleanup(tmpDir); });
test('PIN: --dry-run would_archive.phases is exactly the 3 bracket-declared phase dirs, not the whole phases/ directory', () => {
// BRACKET_ROADMAP (above) declares milestone v2.0 with phases 01, 05, 06
// under the [MSD.02] bracket tag. One directory per declared phase, PLUS
// a decoy that belongs to NO declared phase. Correct (convention-threaded)
// scoping excludes the decoy. A regression back to the pre-#612 hardcoded
// "not bracket" default would fail to recognize the bracket HEADINGS at
// all, `milestonePhaseNums` would come back empty, and
// `getMilestonePhaseFilter` degrades to a pass-all predicate — which
// would sweep the decoy in too. Either failure mode shows up as a
// mismatch against the pinned set below.
writeProject(BRACKET_ROADMAP, 'bracket', [
'MSD.02-01-setup',
'MSD.02-05-real-work',
'MSD.02-06-follow-up',
// Incomplete: the decoy carries no phase token, so it cannot be named
// by verificationNameFor's production-derived scheme (see its
// docstring) — and completeness is irrelevant to what this test pins,
// which is exclusion from would_archive.phases regardless.
['not-a-declared-phase', false],
]);
const r = runMsdTools(['milestone', 'complete', 'v2.0', '--dry-run', '--raw'], tmpDir);
assert.ok(r.success, `milestone complete --dry-run failed: ${r.error}`);
const parsed = JSON.parse(r.output);
assert.deepStrictEqual(
[...parsed.would_archive.phases].sort(),
['MSD.02-01-setup', 'MSD.02-05-real-work', 'MSD.02-06-follow-up'],
'would_archive.phases must be exactly the 3 declared bracket phases — never the decoy, ' +
'and never empty (the pre-#612 hardcoded-legacy failure mode)',
);
assert.strictEqual(parsed.stats.phases, 3, 'the read-only stats loop shares the same single enumeration');
});
test('CONTROL: the identical shape under the legacy (non-bracket) convention is unaffected', () => {
// Same directory/decoy shape, legacy headings instead of bracket ones —
// this is the "byte-identical for null/milestone-prefixed projects"
// guarantee the changeset makes; pinned here at the cmdMilestoneComplete
// consumer, not just at the enumerator's own unit level.
writeProject(`# Roadmap
## v2.0
### Phase 01: Setup
**Goal:** a
### Phase 05: Real work
**Goal:** b
### Phase 06: Follow-up
**Goal:** c
`, undefined, ['01-setup', '05-real-work', '06-follow-up', ['not-a-declared-phase', false]]);
const r = runMsdTools(['milestone', 'complete', 'v2.0', '--dry-run', '--raw'], tmpDir);
assert.ok(r.success, `milestone complete --dry-run failed: ${r.error}`);
const parsed = JSON.parse(r.output);
assert.deepStrictEqual(
[...parsed.would_archive.phases].sort(),
['01-setup', '05-real-work', '06-follow-up'],
);
});
});
describe('#2761 round-11 BLOCKER: cmdStateUpdateProgress computes+writes a percent on bracket projects', () => {
beforeEach(() => { tmpDir = createTempProject('adr-612-update-progress-'); });
afterEach(() => { cleanup(tmpDir); });
test('PIN: `state update-progress` writes a real (non-withheld) percent for a bracket-scoped milestone', () => {
// One fully-verified phase (01) and one plan-only phase (05) under the
// [MSD.02] v2.0 bracket milestone — mirrors writeProject's own
// PLAN+SUMMARY+VERIFICATION shape for "complete" entries. Round-11 named
// this exact command: "cmdStateUpdateProgress ... now computes/writes a
// completion percent on bracket projects where it previously withheld
// one." Pinning `updated: true` plus the exact percent/completed/total
// triple is what makes a future regression to the withheld shape (or to
// silently mis-scoped counts) a failing assertion instead of an
// unobserved behavior change.
writeProject(BRACKET_ROADMAP, 'bracket', [
['MSD.02-01-setup', true],
['MSD.02-05-real-work', false],
]);
const r = runMsdTools(['state', 'update-progress'], tmpDir);
assert.ok(r.success, `state update-progress failed: ${r.error}`);
const parsed = JSON.parse(r.output);
assert.strictEqual(parsed.updated, true,
'a bracket project with a resolvable milestone window must not withhold — got: ' + JSON.stringify(parsed));
assert.strictEqual(parsed.completed, 1);
assert.strictEqual(parsed.total, 2);
const raw = fs.readFileSync(path.join(tmpDir, '.planning', 'STATE.md'), 'utf-8');
// eslint-disable-next-line local/no-unbounded-quantifier -- parses the STATE.md this test just wrote, fixed-size fixture output, not adversarial input
const m = raw.match(/^\*\*Progress:\*\*[^\r\n]*?(\d+)%/m);
assert.ok(m, `state update-progress must have rendered a Progress percent into STATE.md; got:\n${raw}`);
assert.strictEqual(parseInt(m[1], 10), parsed.percent,
'the percent update-progress reports and the one it writes into the body must agree');
});
test('#2761 round-12 GAP: the enumerator .value THIS call site feeds (not the separately-scoped ' +
'percent) gates the #3233 zero-plans no-op — a decoy outside the bracket window must not count', () => {
// Round-11's PIN test above (and its own inline comment at state.cts
// ~:965) established that `cmdStateUpdateProgress`'s reported/written
// percent is IMMUNE to this call site's convention threading — it comes
// from computeUpdateProgressPreview -> buildStateFrontmatter, a separate,
// independently-scoped derivation. Mutation testing on the round-11
// response confirmed that empirically: reverting this call site's thread
// left the PIN test above still green.
//
// What THIS call site's enumerated `.value` actually feeds is
// `totalPlans` (summed across the enumerated dirs, just below) and the
// #3233 zero-plans no-op gate built on it — the ONLY place that value has
// any observable effect on this command's output. This fixture pins
// exactly that gate: BRACKET_ROADMAP declares milestone v2.0's phases
// (01/05/06 under [MSD.02]) but none of their directories are scaffolded
// on disk — zero plans exist for this milestone's real window either way.
// A directory that plainly does not belong to it ('not-a-declared-phase':
// no bracket tag, no phase token at all) sits alongside it WITH a plan.
//
// Correctly bracket-scoped, the decoy is excluded (it matches no
// MSD.02-qualified id) and totalPlans stays 0 -> the #3233 no-op fires.
// Degraded to the pass-all legacy reading (BRACKET_ROADMAP's headings are
// invisible under a non-bracket read — proven above by "a NON-bracket
// repo counts neither form of the same roadmap" — so
// `milestonePhaseNums` comes back empty and the filter admits
// everything), the decoy is swept in and totalPlans flips nonzero -> the
// no-op never fires and the command proceeds past it.
writeProject(BRACKET_ROADMAP, 'bracket', [['not-a-declared-phase', false]]);
const r = runMsdTools(['state', 'update-progress'], tmpDir);
assert.ok(r.success, `state update-progress failed: ${r.error}`);
const parsed = JSON.parse(r.output);
assert.strictEqual(parsed.updated, false,
'a decoy directory outside the bracket-declared milestone window must never contribute to ' +
'totalPlans for that milestone — got: ' + JSON.stringify(parsed));
assert.match(String(parsed.reason ?? ''), /no plans found/,
'must be the #3233 zero-plans no-op specifically (proves the enumerator excluded the decoy), ' +
'not some other withhold reason — got: ' + JSON.stringify(parsed));
});
test('CONTROL: the identical shape under the legacy (non-bracket) convention behaves the same way', () => {
writeProject(`# Roadmap
## v2.0
### Phase 01: Setup
**Goal:** a
### Phase 05: Real work
**Goal:** b
`, undefined, [
['01-setup', true],
['05-real-work', false],
]);
const r = runMsdTools(['state', 'update-progress'], tmpDir);
assert.ok(r.success, `state update-progress failed: ${r.error}`);
const parsed = JSON.parse(r.output);
assert.strictEqual(parsed.updated, true);
assert.strictEqual(parsed.completed, 1);
assert.strictEqual(parsed.total, 2);
});
});
describe('#612 PR-2: a cross-phase stray in a bracket dir is excluded — the #3511 seam is convention-aware', () => {
// Pins the seam fix ITSELF, independently of the phase-matched fixture
// names verificationNameFor now writes. With phase-matched names
// everywhere, an inert (include-everything) bracket seam and a scoped one
// are indistinguishable — every earlier assertion in this file would pass
// under both. The genuine stray written here is the one shape that tells
// them apart, so the ALERT'd bracket-inertness (#3511's `isPhaseArtifact`
// fail-safes swallowing every bracket dir) cannot silently return.
const phaseIdMod = require('../msd-core/bin/lib/phase-id.cjs');
beforeEach(() => { tmpDir = createTempProject('adr-612-stray-'); });
afterEach(() => { cleanup(tmpDir); });
const STRAY_ROADMAP_BRACKET = `# Roadmap
## [MSD.02] v2.0: Current
### [MSD.02] 01: One
**Goal:** a
### [MSD.02] 02: Two
**Goal:** b
### [MSD.02] 03: Three
**Goal:** c
`;
const STRAY_ROADMAP_LEGACY = `# Roadmap
## v2.0: Current
### Phase 01: One
**Goal:** a
### Phase 02: Two
**Goal:** b
### Phase 03: Three
**Goal:** c
`;
function plantStray(dir) {
// Another phase's PASSING report, misfiled into the INCOMPLETE phase 03's
// directory — exactly the hardcoded shape the pre-merge fixture wrote by
// accident. An inert seam folds it into phase 03's completion
// (3/3 -> 100); the scoped seam excludes it (2/3 -> 67).
fs.writeFileSync(
path.join(tmpDir, '.planning', 'phases', dir, '01-VERIFICATION.md'),
'---\nstatus: passed\n---\n# Verification\n', 'utf-8');
}
test('READ: a stray 01-VERIFICATION.md cannot complete bracket phase 03', () => {
writeProject(STRAY_ROADMAP_BRACKET, 'bracket',
['MSD.02-01-one', 'MSD.02-02-two', ['MSD.02-03-three', false]]);
plantStray('MSD.02-03-three');
assert.deepEqual(readProgress(), [3, 2, 3, 2, 67],
'pinned [3,3,3,2,67] before #612 threaded convention into the seam — the stray '
+ 'counted as phase 03\'s completion; percent is plan-derived, so percent alone '
+ 'cannot catch this: the full vector is the assertion');
});
test('READ: the legacy twin of the stray shape reads identically', () => {
writeProject(STRAY_ROADMAP_BRACKET, 'bracket',
['MSD.02-01-one', 'MSD.02-02-two', ['MSD.02-03-three', false]]);
plantStray('MSD.02-03-three');
const bracket = readProgress();
writeProject(STRAY_ROADMAP_LEGACY, undefined,
['01-one', '02-two', ['03-three', false]]);
plantStray('03-three');
const legacy = readProgress();
assert.deepEqual(bracket, legacy,
'bracket must exclude the stray exactly as its legacy twin does');
assert.deepEqual(legacy, [3, 2, 3, 2, 67]);
});
test('WRITE: sync agrees — the stray moves neither derivation', () => {
writeProject(STRAY_ROADMAP_BRACKET, 'bracket',
['MSD.02-01-one', 'MSD.02-02-two', ['MSD.02-03-three', false]]);
plantStray('MSD.02-03-three');
assert.equal(syncedPercent(), 67);
assert.deepEqual(syncedProgress(), [3, 2, 3, 2, 67],
'pinned [3,3,3,2,67] before this fix (completed_phases moved; the plan-derived percent did not)');
assert.deepEqual(readProgress(), syncedProgress());
});
test('SEAM (the ALERT repro): scopeToPhase scopes a 3+-letter-code bracket dir under convention', () => {
// 3+-letter codes (`MSD.02-…`) fell into the ZERO-TOKEN fail-safe —
// `PROJECT_CODE_PREFIX_CAPTURE_RE_I` strips `{CODE}-`, not `{CODE}.` —
// NOT the `firstLetterPrefixed` branch the pre-#612 docblock named for
// the bracket family. Both families are pinned here so a fixer patching
// one branch cannot green this by half.
const files = ['01-01-x-PLAN.md', '01-01-x-SUMMARY.md', '01-VERIFICATION.md'];
assert.deepEqual(phaseIdMod.scopeToPhase(files, '02-two'), [],
'legacy control: the twin scopes these to nothing');
assert.deepEqual(phaseIdMod.scopeToPhase(files, 'MSD.02-02-two', 'bracket'), [],
'pinned all-three-kept (scoping a structural no-op) before this fix');
assert.deepEqual(phaseIdMod.scopeToPhase(files, 'A1.02-02-two', 'bracket'), [],
'short digit-bearing codes hit the firstLetterPrefixed fail-safe pre-fix; same pin');
});
test('SEAM: a bracket dir keeps its own artifacts — dash, dot continuation, and token-less containment', () => {
const own = ['02-VERIFICATION.md', '02-01-PLAN.md', '02.1-CONTEXT.md', 'VERIFICATION.md'];
assert.deepEqual(phaseIdMod.scopeToPhase(own, 'MSD.02-02-two', 'bracket'), own,
'the scoped bracket dir must not drop its own files (over-exclusion is the defect class #3511 exists to fix)');
// Sub-phase dir: own dotted token matches exactly, its .10 sibling does not
// (mirror of the legacy 35.1/35.10 pin in tests/phase-id.test.cjs).
assert.equal(phaseIdMod.isPhaseArtifact('02.1-VERIFICATION.md', 'MSD.02-02.1-fix', 'bracket'), true);
assert.equal(phaseIdMod.isPhaseArtifact('02.10-VERIFICATION.md', 'MSD.02-02.1-fix', 'bracket'), false);
// Recognition is case-insensitive, like every bracket reader.
assert.equal(phaseIdMod.isPhaseArtifact('02-VERIFICATION.md', 'msd.02-02-two', 'bracket'), true);
});
test('SEAM: exact legacy-twin equality across a spread of filenames', () => {
const spread = ['01-VERIFICATION.md', '02-VERIFICATION.md', '2-VERIFICATION.md',
'02-CORRECTION-VERIFICATION.md', '02.1-CONTEXT.md', '02_VERIFICATION.md',
'03-UAT.md', 'VERIFICATION.md', 'notes.md'];
for (const f of spread) {
assert.equal(
phaseIdMod.isPhaseArtifact(f, 'MSD.02-02-two', 'bracket'),
phaseIdMod.isPhaseArtifact(f, '02-two'),
`bracket and legacy twins must agree on ${f}`);
}
});
test('SEAM: everything outside the well-formed-bracket-plus-convention gate is byte-unchanged', () => {
// No convention signal -> the documented include-everything fail-safe
// stands, even for a perfectly-formed bracket dir.
assert.deepEqual(phaseIdMod.scopeToPhase(['01-VERIFICATION.md'], 'MSD.02-02-two'),
['01-VERIFICATION.md'], 'convention-less callers keep pre-#612 behavior verbatim');
// Bracket-MALFORMED (1-digit milestone — the shape W005 reports) fails
// BRACKET_DIR_TOKEN_RE and degrades to the same fail-safe.
assert.deepEqual(phaseIdMod.scopeToPhase(['01-VERIFICATION.md'], 'MSD.2-02-two', 'bracket'),
['01-VERIFICATION.md'], 'malformed bracket dirs keep the include-everything degrade');
// A legacy-shaped dir in a bracket repo scopes by the LEGACY rule — the
// additive-read guarantee (the "carrying LEGACY-shaped dirs" test above,
// at the seam level).
assert.equal(phaseIdMod.isPhaseArtifact('01-VERIFICATION.md', '02-two', 'bracket'), false);
assert.equal(phaseIdMod.isPhaseArtifact('02-VERIFICATION.md', '02-two', 'bracket'), true);
// The letter-prefixed-decimal ambiguity (`P0.3-2` could be a legacy dir
// in a mid-migration bracket repo; its milestone field is not canonical
// bracket spelling) keeps upstream's pinned include-everything trade even
// WITH the convention threaded.
assert.equal(phaseIdMod.isPhaseArtifact('99-VERIFICATION.md', 'P0.3-2-slug', 'bracket'), true);
});
test('SEAM: qualified-stem filenames compare on the qualified key; M-NN stems ratify the twin-equal exclusion', () => {
// Review Major 2: the convention gates the FILENAME reading too.
assert.equal(phaseIdMod.isPhaseArtifact('MSD.02-05-VERIFICATION.md', 'MSD.02-05-real', 'bracket'), true);
assert.equal(phaseIdMod.isPhaseArtifact('MSD.02-01-VERIFICATION.md', 'MSD.02-05-real', 'bracket'), false,
'pinned true (token-less containment admitted it) before this fix');
assert.equal(phaseIdMod.isPhaseArtifact('MSD.01-05-VERIFICATION.md', 'MSD.02-05-real', 'bracket'), false,
'same phase digits, different milestone — the qualified key separates what no bare token can');
// Round-2 verify blocker: the qualified-key comparison must keep the
// dash-OR-dot continuation rule — a qualified dotted SUB-PHASE artifact is
// the parent phase's own file (see the DOTTED SUB-PHASE CONTINUATION
// docblock), exactly as its legacy twin (05.1-… in 05-parent) is. A strict
// equality here silently excluded it: over-exclusion, the dangerous
// direction for these aggregate scans.
assert.equal(phaseIdMod.isPhaseArtifact('MSD.02-05.1-VERIFICATION.md', 'MSD.02-05-parent', 'bracket'), true,
'pinned false (strict key equality dropped the dot arm) before the round-2 fix');
assert.equal(phaseIdMod.isPhaseArtifact('05.1-VERIFICATION.md', '05-parent'), true,
'the legacy-twin control for the line above');
assert.equal(phaseIdMod.isPhaseArtifact('MSD.02-01.1-VERIFICATION.md', 'MSD.02-05-parent', 'bracket'), false,
'a DIFFERENT phase\'s dotted sub-phase artifact is still a stray');
assert.equal(phaseIdMod.isPhaseArtifact('MSD.02-01-VERIFICATION.md', 'MSD.02-05-real'), true,
'convention-less: the include-everything fail-safe stands, byte-identical to pre-#612');
// Review Major 1, RATIFIED as the twin-equal trade: an M-NN-stem report in a
// bracket dir is excluded exactly as its legacy twin excludes the same file;
// migration-window artifact renaming is the migrator slice's job (ADR-612).
assert.equal(phaseIdMod.isPhaseArtifact('02-01-VERIFICATION.md', 'MSD.02-01-one', 'bracket'), false,
'twin-parity: isPhaseArtifact("02-01-VERIFICATION.md","01-one") is false for the same reason');
assert.equal(phaseIdMod.isPhaseArtifact('02-01-VERIFICATION.md', '01-one'), false,
'the legacy-twin control for the line above');
});
});