refactor(#3309): add allPhaseDirNames field to planning-snapshot
W007 (orphan disk dir with no ROADMAP entry) cannot be sourced from phaseDirs, which is windowed to ROADMAP-declared phases only — an orphan dir can never appear in an already-ROADMAP-filtered set. Adds an unwindowed allPhaseDirNames field so the rule can actually fire.
This commit is contained in:
@@ -135,6 +135,27 @@ interface PlanningSnapshot {
|
||||
scope: Scope;
|
||||
};
|
||||
planningRootFiles: { value: string[]; scope: Scope };
|
||||
// W006/W007 (ROADMAP/disk consistency group) fidelity fix, found while
|
||||
// implementing `src/health-diagnostic-rules/roadmap-disk-consistency.cts`:
|
||||
// `phaseDirs` (Phase 10) is deliberately WINDOWED to the phases
|
||||
// `listMilestonePhaseDirs`'s `inWindow` filter (`getMilestonePhaseFilter`,
|
||||
// `src/roadmap-parser.cts:1220`) resolves as belonging to the CURRENT
|
||||
// milestone window — a directory whose phase id is NOT declared anywhere
|
||||
// in ROADMAP.md is EXCLUDED from `phaseDirs.value` by construction
|
||||
// (`isDirInMilestone` membership test). That is exactly the directory
|
||||
// W007 exists to find ("an on-disk phase dir has no matching ROADMAP
|
||||
// entry"), so sourcing W007 from `phaseDirs.value` would make it
|
||||
// structurally unable to fire on the very case it names: an orphan
|
||||
// directory can never be a member of the set that is itself defined as
|
||||
// "directories the roadmap already declares." `allPhaseDirNames` is the
|
||||
// un-windowed twin — every directory actually present under the active
|
||||
// `phases/` root, unfiltered by roadmap declaration (sentinel-id
|
||||
// exclusion is left to the RULE, mirroring `verify.cts:2091`'s own
|
||||
// per-entry `isSentinelPhaseId` guard rather than baking it into the
|
||||
// field). Archived-milestone directories are out of scope here exactly as
|
||||
// they already are for `phaseDirs` (see this batch's own disclosed
|
||||
// fidelity reduction for that).
|
||||
allPhaseDirNames: { value: string[]; scope: Scope };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -607,6 +628,29 @@ function buildPlanningRootFilesField(cwd: string): { value: string[]; scope: Sco
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve `allPhaseDirNames` — every directory name directly under the
|
||||
* active `phases/` root, UNFILTERED by `listMilestonePhaseDirs`'s
|
||||
* current-milestone-window membership test (unlike `phaseDirs`). Backs
|
||||
* W007 (see the field's own doc comment on `PlanningSnapshot` for why
|
||||
* `phaseDirs` cannot). An absent `phases/` root is a real empty, not a
|
||||
* failure (mirrors `listMilestonePhaseDirs`'s own treatment); a present but
|
||||
* unreadable root degrades to `UNREADABLE` with an empty list.
|
||||
*/
|
||||
function buildAllPhaseDirNamesField(phasesDir: string): { value: string[]; scope: Scope } {
|
||||
if (!fs.existsSync(phasesDir)) return { value: [], scope: SCOPE.COMPLETE };
|
||||
try {
|
||||
const value = fs
|
||||
.readdirSync(phasesDir, { withFileTypes: true })
|
||||
.filter((e) => e.isDirectory())
|
||||
.map((e) => e.name)
|
||||
.sort();
|
||||
return { value, scope: SCOPE.COMPLETE };
|
||||
} catch {
|
||||
return { value: [], scope: SCOPE.UNREADABLE };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the full `.planning/` projection for `cwd`. Composes the six §7
|
||||
* owners named in the design doc's "Owners consumed" table, plus (Phase 11,
|
||||
@@ -642,6 +686,7 @@ function buildPlanningSnapshot(cwd: string): PlanningSnapshot {
|
||||
researchValidationStatus: buildResearchValidationStatusField(paths.phases, phaseDirs.value, phaseDirs.scope),
|
||||
milestoneArchiveStatus: buildMilestoneArchiveStatusField(cwd),
|
||||
planningRootFiles: buildPlanningRootFilesField(cwd),
|
||||
allPhaseDirNames: buildAllPhaseDirNamesField(paths.phases),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1048,3 +1048,45 @@ describe('planningRootFiles field (Phase 11, #3309)', () => {
|
||||
assert.deepStrictEqual(snap.planningRootFiles, { value: [], scope: SCOPE.UNREADABLE });
|
||||
});
|
||||
});
|
||||
|
||||
describe('allPhaseDirNames field (Phase 11, #3309 — health-diagnostic-rules/roadmap-disk-consistency batch)', () => {
|
||||
// Found while implementing W007 (`src/health-diagnostic-rules/
|
||||
// roadmap-disk-consistency.cts`): `phaseDirs` is windowed to directories
|
||||
// the ROADMAP already declares, so it can never expose a genuine orphan
|
||||
// directory. `allPhaseDirNames` is the unwindowed twin.
|
||||
|
||||
test('happy: lists every directory under phases/, including one NOT declared anywhere in ROADMAP.md', (t) => {
|
||||
const cwd = createTempDir('gsd-3309-apdn1-');
|
||||
t.after(() => cleanup(cwd));
|
||||
writeRoadmap(cwd, ['## v1.0 Current 🚧', '', '### Phase 1: Foo'].join('\n'));
|
||||
fs.mkdirSync(path.join(planningDirOf(cwd), 'phases', '01-foo'), { recursive: true });
|
||||
fs.mkdirSync(path.join(planningDirOf(cwd), 'phases', '04-extra'), { recursive: true }); // undeclared
|
||||
|
||||
const snap = buildPlanningSnapshot(cwd);
|
||||
assert.deepStrictEqual(snap.allPhaseDirNames.value.slice().sort(), ['01-foo', '04-extra']);
|
||||
assert.strictEqual(snap.allPhaseDirNames.scope, SCOPE.COMPLETE);
|
||||
// Sanity: `phaseDirs` (windowed) must NOT include the undeclared dir —
|
||||
// this is the exact gap `allPhaseDirNames` exists to close.
|
||||
assert.ok(!snap.phaseDirs.value.includes('04-extra'));
|
||||
});
|
||||
|
||||
test('absence: no phases/ directory at all is a real empty, not a failure', (t) => {
|
||||
const cwd = createTempDir('gsd-3309-apdn2-');
|
||||
t.after(() => cleanup(cwd));
|
||||
writeRoadmap(cwd, ['## v1.0 Current 🚧', ''].join('\n'));
|
||||
|
||||
const snap = buildPlanningSnapshot(cwd);
|
||||
assert.deepStrictEqual(snap.allPhaseDirNames, { value: [], scope: SCOPE.COMPLETE });
|
||||
});
|
||||
|
||||
test('hostile: an unreadable phases/ directory degrades to an empty list, scope UNREADABLE, without throwing', (t) => {
|
||||
const cwd = createTempDir('gsd-3309-apdn3-');
|
||||
t.after(() => cleanup(cwd));
|
||||
const phasesDir = path.join(planningDirOf(cwd), 'phases');
|
||||
fs.mkdirSync(phasesDir, { recursive: true });
|
||||
injectPhaseDirFault(t, phasesDir);
|
||||
|
||||
const snap = buildPlanningSnapshot(cwd);
|
||||
assert.deepStrictEqual(snap.allPhaseDirNames, { value: [], scope: SCOPE.UNREADABLE });
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user