fix(#3641): make v005/v004 see bracket-convention phase entries (#3723)

* 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:
Tom Boucher
2026-08-20 17:48:50 -04:00
committed by GitHub
parent 03a3f779ce
commit 072b97d276
5 changed files with 436 additions and 42 deletions

View File

@@ -34,6 +34,11 @@ import { platformWriteSync } from './shell-command-projection.cjs';
// eslint-disable-next-line @typescript-eslint/no-require-imports
import planningWorkspace = require('./planning-workspace.cjs');
const { planningPaths, withPlanningLock, findContextMdIn } = planningWorkspace;
// #3641: milestone-scope's convention resolution reads the project config
// (no cycle — config-loader does not import this module).
// eslint-disable-next-line @typescript-eslint/no-require-imports
import configLoaderForScope = require('./config-loader.cjs');
const { loadConfig: loadConfigForScope } = configLoaderForScope;
// eslint-disable-next-line @typescript-eslint/no-require-imports
import scanPhasePlans = require('./plan-scan.cjs');
// eslint-disable-next-line @typescript-eslint/no-require-imports
@@ -707,7 +712,35 @@ function cmdRoadmapMilestoneScope(cwd: string, raw: boolean): void {
}
const rawContent = fs.readFileSync(roadmapPath, 'utf-8');
const { value: window, scope } = extractCurrentMilestoneScoped(rawContent, cwd);
// #3641: resolve phase_id_convention and thread it into the scope axis, so
// this probe and `roadmap validate`'s V005 answer the SAME question the
// SAME way for a bracket-convention project — a window the classifier
// calls TRUNCATED in validate must never read COMPLETE here (the #3262
// capture/compare guard consumes this scope). Resolution mirrors the
// validate router's: .planning/config.json first, ROADMAP.md frontmatter
// as fallback.
let phaseIdConvention: string | undefined | null;
try {
const cfg = loadConfigForScope(cwd);
phaseIdConvention = cfg['phase_id_convention'] as string | undefined | null;
} catch {
phaseIdConvention = undefined;
}
if (phaseIdConvention === undefined || phaseIdConvention === null) {
// Bounded per local/no-unbounded-quantifier (#2128): frontmatter is a
// short header block — 4KB is orders of magnitude beyond any real one.
const fmMatch = rawContent.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 !== '') {
phaseIdConvention = val.replace(/^["']|["']$/g, '');
}
}
}
}
const { value: window, scope } = extractCurrentMilestoneScoped(rawContent, cwd, undefined, phaseIdConvention);
// Document order (Set insertion order) — deterministic for a given document.
const phases = [...scanMilestonePhaseIds(window)];
output({ scope, phases, phase_count: phases.length }, raw, undefined);