perf(roadmap): hoist readdirSync out of phase loop in analyze command (#1899)

cmdRoadmapAnalyze called fs.readdirSync(phasesDir) inside the
per-phase while loop, causing O(N^2) directory reads for N phases.
At 50 phases this produced 100 redundant syscalls; at 100 phases, 200.

Move the directory listing before the loop and build a lookup array
that is reused for each phase match. This reduces the pattern from
O(N^2) to O(N) directory reads.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Tibsfox
2026-04-07 14:24:58 -07:00
committed by GitHub
parent 9679e18ef4
commit 28517f7b6d

View File

@@ -129,6 +129,15 @@ function cmdRoadmapAnalyze(cwd, raw) {
const phases = [];
let match;
// Build phase directory lookup once (O(1) readdir instead of O(N) per phase)
const _phaseDirNames = (() => {
try {
return fs.readdirSync(phasesDir, { withFileTypes: true })
.filter(e => e.isDirectory())
.map(e => e.name);
} catch { return []; }
})();
while ((match = phasePattern.exec(content)) !== null) {
const phaseNum = match[1];
const phaseName = match[2].replace(/\(INSERTED\)/i, '').trim();
@@ -155,9 +164,7 @@ function cmdRoadmapAnalyze(cwd, raw) {
let hasResearch = false;
try {
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
const dirMatch = dirs.find(d => phaseTokenMatches(d, normalized));
const dirMatch = _phaseDirNames.find(d => phaseTokenMatches(d, normalized));
if (dirMatch) {
const phaseFiles = fs.readdirSync(path.join(phasesDir, dirMatch));