* test(#3641): failing-first bracket-window validate rows * fix(#3641): thread phase convention into hasphaseentries for v004/v005 * test(#3641): review rows - digit-anchor, decoy, probe parity, t.after * fix(#3641): digit-anchor bracket entry token; thread probe scope axis * fix(#3641): align frontmatter bound with probe; changeset * chore(#3641): backfill changeset pr number --------- Co-authored-by: sim <sim@local>
This commit is contained in:
@@ -26,7 +26,7 @@ import cliExitMod = require('./cli-exit.cjs');
|
||||
const { ExitError } = cliExitMod;
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
import roadmapParserMod = require('./roadmap-parser.cjs');
|
||||
const { extractCurrentMilestoneScoped } = roadmapParserMod;
|
||||
const { extractCurrentMilestoneScoped, hasPhaseEntries } = roadmapParserMod;
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
import planningScopeMod = require('./planning-scope.cjs');
|
||||
const { SCOPE } = planningScopeMod;
|
||||
@@ -188,11 +188,45 @@ function routeRoadmapCommand({ roadmap, args, cwd, raw, error }: RouteRoadmapCom
|
||||
}
|
||||
}
|
||||
|
||||
// No recognizable phase structure — at least one `### Phase N:` heading.
|
||||
// Mirrors the phase-heading pattern used across roadmap-parser.cts.
|
||||
const hasPhaseEntry = /^#{2,4}\s*Phase\s+\S/im.test(roadmapContent);
|
||||
// #3641: resolve phase_id_convention ONCE, ahead of every consumer in
|
||||
// this validate pass — V004's entry check and V005's scope classifier
|
||||
// below, and the W021 milestone-prefix check after them.
|
||||
// Authoritative source: .planning/config.json (set by the upgrade
|
||||
// command). Fallback: ROADMAP.md frontmatter (for projects that set
|
||||
// the field there directly).
|
||||
let convention: string | undefined | null;
|
||||
try {
|
||||
const cfg = loadConfig(cwd);
|
||||
convention = cfg['phase_id_convention'] as string | undefined | null;
|
||||
} catch {
|
||||
convention = undefined;
|
||||
}
|
||||
if (convention === undefined || convention === null) {
|
||||
// Fallback: read from ROADMAP.md frontmatter. Bounded to match
|
||||
// cmdRoadmapMilestoneScope's copy exactly (#3641 review NEW-1: an
|
||||
// unbounded capture here read past 4KB frontmatters the probe's
|
||||
// bounded copy could not, diverging validate from the probe).
|
||||
const fmMatch = roadmapContent.match(/^---\r?\n([\s\S]{0,4000}?)\r?\n---/);
|
||||
if (fmMatch) {
|
||||
const kvMatch = fmMatch[1].match(/^phase_id_convention:\s*(.*)$/m);
|
||||
if (kvMatch) {
|
||||
const val = kvMatch[1].trim();
|
||||
if (val !== 'null' && val !== '') {
|
||||
convention = val.replace(/^["']|["']$/g, '');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No recognizable phase structure. #3641: routed through the
|
||||
// roadmap-parser owner (`hasPhaseEntries` — headings, #2199 bullets,
|
||||
// #3577 table rows) instead of a private inline heading regex, so the
|
||||
// document-level check and the V005 scope axis below can never
|
||||
// disagree about what a phase entry is — and so a bracket-convention
|
||||
// project's `### [GSD.04] 01:` entries are entries here too.
|
||||
const hasPhaseEntry = hasPhaseEntries(roadmapContent, convention);
|
||||
if (!hasPhaseEntry && !warnings.some((w) => w.code === 'V002')) {
|
||||
warnings.push({ code: 'V004', message: 'ROADMAP.md contains no recognizable phase entries (no "### Phase N:" headings)' });
|
||||
warnings.push({ code: 'V004', message: 'ROADMAP.md contains no recognizable phase entries (no phase headings, bullet entries, or table rows)' });
|
||||
}
|
||||
|
||||
// #3263: a whole-document phase check (V004 above) is satisfied by a
|
||||
@@ -207,13 +241,15 @@ function routeRoadmapCommand({ roadmap, args, cwd, raw, error }: RouteRoadmapCom
|
||||
// an unscoped/unreadable window is a different failure mode with a
|
||||
// different remediation, deliberately not warned here.
|
||||
try {
|
||||
const scoped = extractCurrentMilestoneScoped(contentAfterBom, cwd);
|
||||
// #3641: thread the resolved convention so the scope axis's
|
||||
// hasPhaseEntries comparison recognizes bracket phase entries.
|
||||
const scoped = extractCurrentMilestoneScoped(contentAfterBom, cwd, undefined, convention);
|
||||
if (scoped.scope === SCOPE.TRUNCATED) {
|
||||
warnings.push({
|
||||
code: 'V005',
|
||||
message:
|
||||
'Active milestone window is truncated: phase entries exist in ROADMAP.md but are excluded from the ' +
|
||||
'active milestone\'s resolved window (check for a heading between the milestone heading and its "### Phase N:" sections)',
|
||||
'active milestone\'s resolved window (check for a heading between the milestone heading and its phase-entry sections)',
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
@@ -221,29 +257,8 @@ function routeRoadmapCommand({ roadmap, args, cwd, raw, error }: RouteRoadmapCom
|
||||
// structural warnings already collected above.
|
||||
}
|
||||
|
||||
// W021 only fires when phase_id_convention is explicitly 'milestone-prefixed'.
|
||||
// Authoritative source: .planning/config.json (set by the upgrade command).
|
||||
// Fallback: ROADMAP.md frontmatter (for projects that set the field there directly).
|
||||
let convention: string | undefined | null;
|
||||
try {
|
||||
const cfg = loadConfig(cwd);
|
||||
convention = cfg['phase_id_convention'] as string | undefined | null;
|
||||
} catch {
|
||||
convention = undefined;
|
||||
}
|
||||
if (convention === undefined || convention === null) {
|
||||
// Fallback: read from ROADMAP.md frontmatter
|
||||
const fmMatch = roadmapContent.match(/^---\r?\n([\s\S]+?)\r?\n---/);
|
||||
if (fmMatch) {
|
||||
const kvMatch = fmMatch[1].match(/^phase_id_convention:\s*(.*)$/m);
|
||||
if (kvMatch) {
|
||||
const val = kvMatch[1].trim();
|
||||
if (val !== 'null' && val !== '') {
|
||||
convention = val.replace(/^["']|["']$/g, '');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// W021 only fires when phase_id_convention is explicitly
|
||||
// 'milestone-prefixed' — the same hoisted resolution above (#3641).
|
||||
if (convention === 'milestone-prefixed') {
|
||||
warnings.push(...checkW021(roadmapContent));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user