feat(phase-plans): extract shared scanPhasePlans helper (k014) (#3308)
* test(phase-plans): red — shared scanPhasePlans contract + parity across call sites (#3262) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(phase-plans): extract shared scanPhasePlans helper (k014) (#3262) Eliminates four divergent copies of the plan-scan algorithm: - roadmap.cjs:countPhasePlansAndSummaries (root call site) - state.cjs:buildStateFrontmatter (1 of 3) - state.cjs:cmdStateValidate (2 of 3) - state.cjs:cmdStateSync (3 of 3) - init.cjs:listPhasePlanFiles / listPhaseSummaryFiles New bin/lib/plan-scan.cjs exports scanPhasePlans(phaseDir) → { planCount, summaryCount, completed, hasNestedPlans, planFiles, summaryFiles } Divergences resolved: - roadmap.cjs used a broad isPlanFile (any .md containing PLAN in name, matching the extended layout 5-PLAN-01-setup.md); canonical helper adopts this wider pattern as the reference implementation. - state.cjs used a strict endsWith(-PLAN.md) filter, missing extended- layout root files; now unified with roadmap.cjs semantics. - init.cjs listPhasePlanFiles used ^PLAN-\d+ for nested, missing the -PLAN-\d+ variant state.cjs also matched; helper includes both. - pre-bounce exclusion broadened to /.pre-bounce.md$/i (any pre-bounce file), not just -PLAN.*\.pre-bounce\.md (roadmap form) or flat .pre-bounce.md (state form). - OUTLINE exclusion broadened to /-OUTLINE\.md$/i to catch both flat (-PLAN-OUTLINE.md) and nested (PLAN-01-OUTLINE.md) forms. Sibling audit: no 5th call site found. phase.cjs:looksLikePlanFile is a diagnostic probe for non-canonical naming (not a counter) — left in place per its distinct purpose. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore(changelog): add entry for #3262 scanPhasePlans extraction Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore(changeset): add changeset fragment for #3262 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(inventory): add plan-scan.cjs row to INVENTORY.md CLI Modules table (#3262) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(3262): update bug-3128 test + INVENTORY counts for plan-scan.cjs - Update tests/bug-3128-roadmap-plan-count-slug-layout.test.cjs to verify that roadmap.cjs delegates to plan-scan.cjs (require check) and that the extended filter lives in plan-scan.cjs as isRootPlanFile with /PLAN/i - Bump docs/INVENTORY.md CLI Modules headline from 46 to 47 (plan-scan.cjs) - Regenerate docs/INVENTORY-MANIFEST.json to include cli_modules/plan-scan.cjs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(3262): migrate two missed call sites to scanPhasePlans (k014) - init.cjs cmdInitExecutePhase: replace inline /-PLAN\.md$/i filter with listPhasePlanFiles(path) to honour nested, extended-layout, OUTLINE and pre-bounce exclusions (CR finding) - state.cjs cmdStateUpdateProgress: replace dual /-PLAN\.md$/i and /-SUMMARY\.md$/i filters with scanPhasePlans() so the progress-bar body field uses the same counts as buildStateFrontmatter frontmatter Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(3262): correct INVENTORY-MANIFEST.json to tracked files only Remove 3 untracked local entries from cli_modules so the manifest matches what CI sees (47 tracked .cjs files, not 50 local). Previous regeneration ran against the local filesystem which included cjs-command-router-adapter.cjs, state-document.cjs, and workstream-inventory.cjs (all untracked on this branch). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ const { execSync } = require('child_process');
|
||||
const { loadConfig, resolveModelInternal, findPhaseInternal, getRoadmapPhaseInternal, pathExistsInternal, generateSlugInternal, getMilestoneInfo, getMilestonePhaseFilter, stripShippedMilestones, extractCurrentMilestone, normalizePhaseName, toPosixPath, output, error, checkAgentsInstalled, phaseTokenMatches } = require('./core.cjs');
|
||||
const { planningPaths, planningDir, planningRoot } = require('./planning-workspace.cjs');
|
||||
const { maskIfSecret } = require('./secrets.cjs');
|
||||
const scanPhasePlans = require('./plan-scan.cjs');
|
||||
|
||||
// Accept all bold/colon variants of the Requirements header (#2769):
|
||||
// **Requirements:** / **Requirements**: / **Requirements** : render the
|
||||
@@ -15,27 +16,11 @@ const { maskIfSecret } = require('./secrets.cjs');
|
||||
const REQUIREMENTS_HEADER_RE = /^\*\*Requirements:?\*\*[^\S\n]*:?[^\S\n]*([^\n]*)$/m;
|
||||
|
||||
function listPhaseSummaryFiles(phaseDir) {
|
||||
const phaseFiles = fs.readdirSync(phaseDir);
|
||||
const rootSummaries = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
|
||||
const plansDir = path.join(phaseDir, 'plans');
|
||||
let nestedSummaries = [];
|
||||
if (fs.existsSync(plansDir)) {
|
||||
const files = fs.readdirSync(plansDir);
|
||||
nestedSummaries = files.filter(f => /^SUMMARY-\d+.*\.md$/i.test(f));
|
||||
}
|
||||
return rootSummaries.concat(nestedSummaries);
|
||||
return scanPhasePlans(phaseDir).summaryFiles;
|
||||
}
|
||||
|
||||
function listPhasePlanFiles(phaseDir) {
|
||||
const phaseFiles = fs.readdirSync(phaseDir);
|
||||
const rootPlans = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md');
|
||||
const plansDir = path.join(phaseDir, 'plans');
|
||||
let nestedPlans = [];
|
||||
if (fs.existsSync(plansDir)) {
|
||||
const files = fs.readdirSync(plansDir);
|
||||
nestedPlans = files.filter(f => /^PLAN-\d+.*\.md$/i.test(f));
|
||||
}
|
||||
return rootPlans.concat(nestedPlans);
|
||||
return scanPhasePlans(phaseDir).planFiles;
|
||||
}
|
||||
|
||||
function getLatestCompletedMilestone(cwd) {
|
||||
@@ -212,8 +197,7 @@ function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
|
||||
const warnings = [];
|
||||
const phasesPath = planningPaths(cwd).phases;
|
||||
if (phaseInfo && phaseInfo.directory && fs.existsSync(path.join(cwd, phaseInfo.directory))) {
|
||||
const files = fs.readdirSync(path.join(cwd, phaseInfo.directory));
|
||||
const diskPlans = files.filter(f => f.match(/-PLAN\.md$/i)).length;
|
||||
const diskPlans = listPhasePlanFiles(path.join(cwd, phaseInfo.directory)).length;
|
||||
const totalPlansRaw = stateExtractField(stateContent, 'Total Plans in Phase');
|
||||
const totalPlansInPhase = totalPlansRaw ? parseInt(totalPlansRaw, 10) : null;
|
||||
if (totalPlansInPhase !== null && diskPlans !== totalPlansInPhase) {
|
||||
|
||||
138
get-shit-done/bin/lib/plan-scan.cjs
Normal file
138
get-shit-done/bin/lib/plan-scan.cjs
Normal file
@@ -0,0 +1,138 @@
|
||||
'use strict';
|
||||
/**
|
||||
* plan-scan — canonical phase-plan scanner (k014)
|
||||
*
|
||||
* Single source of truth for detecting plan and summary files in a phase
|
||||
* directory, replacing four divergent copies in state.cjs, roadmap.cjs,
|
||||
* init.cjs, and phase.cjs (#3262).
|
||||
*
|
||||
* Layout support:
|
||||
* Flat (pre-#3139): phases/<N>/*-PLAN.md, *-SUMMARY.md
|
||||
* Nested (post-#3139): phases/<N>/plans/PLAN-<NN>-*.md, SUMMARY-<NN>-*.md
|
||||
*
|
||||
* @module plan-scan
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Excluded derivative files — present alongside real plans but must not be
|
||||
// counted. OUTLINE exclusion catches both flat (-PLAN-OUTLINE.md) and nested
|
||||
// (PLAN-NN-OUTLINE.md) forms via a broad -OUTLINE.md$ pattern. The
|
||||
// pre-bounce pattern is intentionally broad (matches any *.pre-bounce.md) so
|
||||
// stale bounce files never inflate plan counts (#3257 regression root cause).
|
||||
const PLAN_OUTLINE_RE = /-OUTLINE\.md$/i;
|
||||
const PLAN_PRE_BOUNCE_RE = /\.pre-bounce\.md$/i;
|
||||
|
||||
/**
|
||||
* Determine whether a filename from the flat phase root is a plan file.
|
||||
*
|
||||
* Accepts:
|
||||
* - Bare PLAN.md
|
||||
* - Canonical padded 01-01-PLAN.md
|
||||
* - Extended layout 5-PLAN-01-setup.md (the format gsd-plan-phase writes;
|
||||
* looksLikePlanFile in phase.cjs / isPlanFile in roadmap.cjs)
|
||||
*
|
||||
* Rejects: -PLAN-OUTLINE.md, *.pre-bounce.md
|
||||
*/
|
||||
function isRootPlanFile(f) {
|
||||
if (PLAN_OUTLINE_RE.test(f)) return false;
|
||||
if (PLAN_PRE_BOUNCE_RE.test(f)) return false;
|
||||
// Canonical suffix or bare name
|
||||
if (f.endsWith('-PLAN.md') || f === 'PLAN.md') return true;
|
||||
// Extended layout: any .md that contains PLAN (case-insensitive) in the name
|
||||
return /\.md$/i.test(f) && /PLAN/i.test(f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether a filename from the nested plans/ subdir is a plan file.
|
||||
*
|
||||
* Nested layout names: PLAN-NN-slug.md or N-PLAN-NN-slug.md.
|
||||
* Excludes OUTLINE and pre-bounce suffixes.
|
||||
*/
|
||||
function isNestedPlanFile(f) {
|
||||
if (PLAN_OUTLINE_RE.test(f)) return false;
|
||||
if (PLAN_PRE_BOUNCE_RE.test(f)) return false;
|
||||
return /^PLAN-\d+.*\.md$/i.test(f) || /-PLAN-\d+.*\.md$/i.test(f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether a filename from the flat phase root is a summary file.
|
||||
*/
|
||||
function isRootSummaryFile(f) {
|
||||
return f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md';
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether a filename from the nested plans/ subdir is a summary.
|
||||
*/
|
||||
function isNestedSummaryFile(f) {
|
||||
return /^SUMMARY-\d+.*\.md$/i.test(f) || /-SUMMARY-\d+.*\.md$/i.test(f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan a single phase directory for plan and summary files.
|
||||
*
|
||||
* @param {string} phaseDir — absolute path to the phase directory
|
||||
* @returns {{
|
||||
* planCount: number,
|
||||
* summaryCount: number,
|
||||
* completed: boolean,
|
||||
* hasNestedPlans: boolean,
|
||||
* planFiles: string[],
|
||||
* summaryFiles: string[],
|
||||
* }}
|
||||
*/
|
||||
function scanPhasePlans(phaseDir) {
|
||||
let rootFiles;
|
||||
try {
|
||||
rootFiles = fs.readdirSync(phaseDir);
|
||||
} catch {
|
||||
return {
|
||||
planCount: 0,
|
||||
summaryCount: 0,
|
||||
completed: false,
|
||||
hasNestedPlans: false,
|
||||
planFiles: [],
|
||||
summaryFiles: [],
|
||||
};
|
||||
}
|
||||
|
||||
const rootPlanFiles = rootFiles.filter(isRootPlanFile);
|
||||
const rootSummaryFiles = rootFiles.filter(isRootSummaryFile);
|
||||
|
||||
let nestedPlanFiles = [];
|
||||
let nestedSummaryFiles = [];
|
||||
let hasNestedPlans = false;
|
||||
|
||||
const nestedDir = path.join(phaseDir, 'plans');
|
||||
if (fs.existsSync(nestedDir)) {
|
||||
try {
|
||||
const nested = fs.readdirSync(nestedDir);
|
||||
nestedPlanFiles = nested.filter(isNestedPlanFile);
|
||||
nestedSummaryFiles = nested.filter(isNestedSummaryFile);
|
||||
hasNestedPlans = nestedPlanFiles.length > 0;
|
||||
} catch { /* ignore if plans/ is not a readable directory */ }
|
||||
}
|
||||
|
||||
const planFiles = rootPlanFiles.concat(nestedPlanFiles);
|
||||
const summaryFiles = rootSummaryFiles.concat(nestedSummaryFiles);
|
||||
const planCount = planFiles.length;
|
||||
const summaryCount = summaryFiles.length;
|
||||
|
||||
return {
|
||||
planCount,
|
||||
summaryCount,
|
||||
completed: planCount > 0 && summaryCount >= planCount,
|
||||
hasNestedPlans,
|
||||
planFiles,
|
||||
summaryFiles,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = scanPhasePlans;
|
||||
module.exports.scanPhasePlans = scanPhasePlans;
|
||||
module.exports.isRootPlanFile = isRootPlanFile;
|
||||
module.exports.isNestedPlanFile = isNestedPlanFile;
|
||||
module.exports.isRootSummaryFile = isRootSummaryFile;
|
||||
module.exports.isNestedSummaryFile = isNestedSummaryFile;
|
||||
@@ -6,6 +6,7 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { escapeRegex, normalizePhaseName, output, error, findPhaseInternal, stripShippedMilestones, extractCurrentMilestone, replaceInCurrentMilestone, phaseTokenMatches, atomicWriteFileSync } = require('./core.cjs');
|
||||
const { planningPaths, withPlanningLock } = require('./planning-workspace.cjs');
|
||||
const scanPhasePlans = require('./plan-scan.cjs');
|
||||
|
||||
/**
|
||||
* Coerce an arbitrary YAML scalar/object into a string for cross-cutting
|
||||
@@ -37,31 +38,14 @@ function coerceTruthToString(t) {
|
||||
}
|
||||
|
||||
function countPhasePlansAndSummaries(phaseDir) {
|
||||
const phaseFiles = fs.readdirSync(phaseDir);
|
||||
// Canonical form: *-PLAN.md or PLAN.md.
|
||||
// Extended form: {N}-PLAN-{NN}-{slug}.md — the layout gsd-plan-phase
|
||||
// actually writes (e.g. 5-PLAN-01-setup.md). Mirrors the looksLikePlanFile
|
||||
// logic in phase.cjs (#2893 / #3128).
|
||||
const PLAN_OUTLINE_RE = /-PLAN-OUTLINE\.md$/i;
|
||||
const PLAN_PRE_BOUNCE_RE = /-PLAN.*\.pre-bounce\.md$/i;
|
||||
const isPlanFile = (f) =>
|
||||
(f.endsWith('-PLAN.md') || f === 'PLAN.md') ||
|
||||
(/\.md$/i.test(f) && /PLAN/i.test(f) && !PLAN_OUTLINE_RE.test(f) && !PLAN_PRE_BOUNCE_RE.test(f));
|
||||
const rootPlans = phaseFiles.filter(isPlanFile);
|
||||
const rootSummaries = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
|
||||
|
||||
let nestedPlans = [];
|
||||
let nestedSummaries = [];
|
||||
const plansDir = path.join(phaseDir, 'plans');
|
||||
if (fs.existsSync(plansDir)) {
|
||||
const planFiles = fs.readdirSync(plansDir);
|
||||
nestedPlans = planFiles.filter(f => /^PLAN-\d+.*\.md$/i.test(f));
|
||||
nestedSummaries = planFiles.filter(f => /^SUMMARY-\d+.*\.md$/i.test(f));
|
||||
}
|
||||
|
||||
const { planCount, summaryCount } = scanPhasePlans(phaseDir);
|
||||
// hasContext and hasResearch are not plan-scan concerns — read the directory
|
||||
// once for the non-plan metadata that cmdRoadmapAnalyze needs.
|
||||
let phaseFiles = [];
|
||||
try { phaseFiles = fs.readdirSync(phaseDir); } catch { /* empty */ }
|
||||
return {
|
||||
planCount: rootPlans.length + nestedPlans.length,
|
||||
summaryCount: rootSummaries.length + nestedSummaries.length,
|
||||
planCount,
|
||||
summaryCount,
|
||||
hasContext: phaseFiles.some(f => f.endsWith('-CONTEXT.md') || f === 'CONTEXT.md'),
|
||||
hasResearch: phaseFiles.some(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md'),
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ const path = require('path');
|
||||
const { escapeRegex, loadConfig, getMilestoneInfo, getMilestonePhaseFilter, normalizeMd, output, error, atomicWriteFileSync } = require('./core.cjs');
|
||||
const { planningDir, planningPaths } = require('./planning-workspace.cjs');
|
||||
const { extractFrontmatter, reconstructFrontmatter } = require('./frontmatter.cjs');
|
||||
const scanPhasePlans = require('./plan-scan.cjs');
|
||||
|
||||
// Cache disk scan results from buildStateFrontmatter per cwd per process (#1967).
|
||||
// Avoids re-reading N+1 directories on every state write when the phase structure
|
||||
@@ -458,9 +459,9 @@ function cmdStateUpdateProgress(cwd, raw) {
|
||||
.filter(e => e.isDirectory()).map(e => e.name)
|
||||
.filter(isDirInMilestone);
|
||||
for (const dir of phaseDirs) {
|
||||
const files = fs.readdirSync(path.join(phasesDir, dir));
|
||||
totalPlans += files.filter(f => f.match(/-PLAN\.md$/i)).length;
|
||||
totalSummaries += files.filter(f => f.match(/-SUMMARY\.md$/i)).length;
|
||||
const { planCount, summaryCount } = scanPhasePlans(path.join(phasesDir, dir));
|
||||
totalPlans += planCount;
|
||||
totalSummaries += summaryCount;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -872,39 +873,12 @@ function buildStateFrontmatter(bodyContent, cwd) {
|
||||
let diskTotalSummaries = 0;
|
||||
let diskCompletedPhases = 0;
|
||||
|
||||
// Regex constants mirror roadmap.cjs:countPhasePlansAndSummaries (#3257).
|
||||
const PLAN_OUTLINE_RE = /-PLAN-OUTLINE\.md$/i;
|
||||
const PLAN_PRE_BOUNCE_RE = /\.pre-bounce\.md$/i;
|
||||
for (const dir of phaseDirs) {
|
||||
const phaseDir = path.join(phasesDir, dir);
|
||||
const topFiles = fs.readdirSync(phaseDir);
|
||||
// Canonical flat-layout plan files in phase root.
|
||||
let plans = topFiles.filter(f =>
|
||||
(f.endsWith('-PLAN.md') || f === 'PLAN.md') &&
|
||||
!PLAN_OUTLINE_RE.test(f) && !PLAN_PRE_BOUNCE_RE.test(f)
|
||||
).length;
|
||||
let summaries = topFiles.filter(f =>
|
||||
f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md'
|
||||
).length;
|
||||
// Nested layout (post-#3139): phases/<N>/plans/<N>-PLAN-<NN>-<slug>.md
|
||||
// Mirrors roadmap.cjs:countPhasePlansAndSummaries — do NOT extract here
|
||||
// (shared helper is tracked as follow-on for k014).
|
||||
const nestedPlansDir = path.join(phaseDir, 'plans');
|
||||
if (fs.existsSync(nestedPlansDir)) {
|
||||
try {
|
||||
const nested = fs.readdirSync(nestedPlansDir);
|
||||
plans += nested.filter(f =>
|
||||
(/^PLAN-\d+.*\.md$/i.test(f) || /-PLAN-\d+.*\.md$/i.test(f)) &&
|
||||
!PLAN_OUTLINE_RE.test(f) && !PLAN_PRE_BOUNCE_RE.test(f)
|
||||
).length;
|
||||
summaries += nested.filter(f =>
|
||||
/^SUMMARY-\d+.*\.md$/i.test(f) || /-SUMMARY-\d+.*\.md$/i.test(f)
|
||||
).length;
|
||||
} catch { /* ignore if plans/ is not a readable directory */ }
|
||||
}
|
||||
diskTotalPlans += plans;
|
||||
diskTotalSummaries += summaries;
|
||||
if (plans > 0 && summaries >= plans) diskCompletedPhases++;
|
||||
const { planCount, summaryCount, completed } = scanPhasePlans(phaseDir);
|
||||
diskTotalPlans += planCount;
|
||||
diskTotalSummaries += summaryCount;
|
||||
if (completed) diskCompletedPhases++;
|
||||
}
|
||||
cached = {
|
||||
totalPhases: isDirInMilestone.phaseCount > 0
|
||||
@@ -1515,33 +1489,7 @@ function cmdStateValidate(cwd, raw) {
|
||||
const phaseDir = entries.find(e => e.isDirectory() && e.name.startsWith(normalized.replace(/^0+/, '').padStart(2, '0')));
|
||||
if (phaseDir) {
|
||||
const phaseDirPath = path.join(phasesDir, phaseDir.name);
|
||||
const files = fs.readdirSync(phaseDirPath);
|
||||
// Regex constants mirror roadmap.cjs:countPhasePlansAndSummaries (#3257).
|
||||
const PLAN_OUTLINE_RE_V = /-PLAN-OUTLINE\.md$/i;
|
||||
const PLAN_PRE_BOUNCE_RE_V = /\.pre-bounce\.md$/i;
|
||||
let diskPlans = files.filter(f =>
|
||||
(f.endsWith('-PLAN.md') || f === 'PLAN.md') &&
|
||||
!PLAN_OUTLINE_RE_V.test(f) && !PLAN_PRE_BOUNCE_RE_V.test(f)
|
||||
).length;
|
||||
let diskSummaries = files.filter(f =>
|
||||
f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md'
|
||||
).length;
|
||||
// Nested layout (post-#3139): phases/<N>/plans/<N>-PLAN-<NN>-<slug>.md
|
||||
// Mirrors roadmap.cjs:countPhasePlansAndSummaries — do NOT extract here
|
||||
// (shared helper is tracked as follow-on for k014).
|
||||
const nestedPlansDirV = path.join(phaseDirPath, 'plans');
|
||||
if (fs.existsSync(nestedPlansDirV)) {
|
||||
try {
|
||||
const nested = fs.readdirSync(nestedPlansDirV);
|
||||
diskPlans += nested.filter(f =>
|
||||
(/^PLAN-\d+.*\.md$/i.test(f) || /-PLAN-\d+.*\.md$/i.test(f)) &&
|
||||
!PLAN_OUTLINE_RE_V.test(f) && !PLAN_PRE_BOUNCE_RE_V.test(f)
|
||||
).length;
|
||||
diskSummaries += nested.filter(f =>
|
||||
/^SUMMARY-\d+.*\.md$/i.test(f) || /-SUMMARY-\d+.*\.md$/i.test(f)
|
||||
).length;
|
||||
} catch { /* ignore if plans/ is not a readable directory */ }
|
||||
}
|
||||
const { planCount: diskPlans, summaryCount: diskSummaries } = scanPhasePlans(phaseDirPath);
|
||||
|
||||
// Check plan count mismatch
|
||||
if (totalPlansInPhase !== null && diskPlans !== totalPlansInPhase) {
|
||||
@@ -1550,6 +1498,7 @@ function cmdStateValidate(cwd, raw) {
|
||||
}
|
||||
|
||||
// Check for VERIFICATION.md
|
||||
const files = fs.readdirSync(phaseDirPath);
|
||||
const verificationFiles = files.filter(f => f.includes('VERIFICATION') && f.endsWith('.md'));
|
||||
for (const vf of verificationFiles) {
|
||||
try {
|
||||
@@ -1620,40 +1569,12 @@ function cmdStateSync(cwd, options, raw) {
|
||||
let highestIncompletePhaseplanCount = 0;
|
||||
let highestIncompletePhaseSummaryCount = 0;
|
||||
|
||||
// Regex constants mirror roadmap.cjs:countPhasePlansAndSummaries (#3257).
|
||||
const PLAN_OUTLINE_RE_S = /-PLAN-OUTLINE\.md$/i;
|
||||
const PLAN_PRE_BOUNCE_RE_S = /\.pre-bounce\.md$/i;
|
||||
|
||||
for (const dir of entries) {
|
||||
const dirPath = path.join(phasesDir, dir);
|
||||
const files = fs.readdirSync(dirPath);
|
||||
// Canonical flat-layout plan files in phase root.
|
||||
let plans = files.filter(f =>
|
||||
(f.endsWith('-PLAN.md') || f === 'PLAN.md') &&
|
||||
!PLAN_OUTLINE_RE_S.test(f) && !PLAN_PRE_BOUNCE_RE_S.test(f)
|
||||
).length;
|
||||
let summaries = files.filter(f =>
|
||||
f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md'
|
||||
).length;
|
||||
// Nested layout (post-#3139): phases/<N>/plans/<N>-PLAN-<NN>-<slug>.md
|
||||
// Mirrors roadmap.cjs:countPhasePlansAndSummaries — do NOT extract here
|
||||
// (shared helper is tracked as follow-on for k014).
|
||||
const nestedPlansDirS = path.join(dirPath, 'plans');
|
||||
if (fs.existsSync(nestedPlansDirS)) {
|
||||
try {
|
||||
const nested = fs.readdirSync(nestedPlansDirS);
|
||||
plans += nested.filter(f =>
|
||||
(/^PLAN-\d+.*\.md$/i.test(f) || /-PLAN-\d+.*\.md$/i.test(f)) &&
|
||||
!PLAN_OUTLINE_RE_S.test(f) && !PLAN_PRE_BOUNCE_RE_S.test(f)
|
||||
).length;
|
||||
summaries += nested.filter(f =>
|
||||
/^SUMMARY-\d+.*\.md$/i.test(f) || /-SUMMARY-\d+.*\.md$/i.test(f)
|
||||
).length;
|
||||
} catch { /* ignore if plans/ is not a readable directory */ }
|
||||
}
|
||||
const { planCount: plans, summaryCount: summaries, completed } = scanPhasePlans(dirPath);
|
||||
totalDiskPlans += plans;
|
||||
totalDiskSummaries += summaries;
|
||||
if (plans > 0 && summaries >= plans) diskCompletedPhases++;
|
||||
if (completed) diskCompletedPhases++;
|
||||
|
||||
// Track the highest phase with incomplete plans (or any plans)
|
||||
const phaseMatch = dir.match(/^(\d+[A-Z]?(?:\.\d+)*)/i);
|
||||
|
||||
Reference in New Issue
Block a user