* test(#4801): failing-first — an archived phase directory must resolve and report complete in init.manager * fix(#4801): init.manager resolves archived phase directories through findPhaseInternal The private current-milestone-only scan (matchPhaseDirs over listMilestonePhaseDirs) counted archived phase directories as missing, so an archived phase with a passed verification reported no_directory/phase_complete:false. findPhaseInternal — already imported, already the shared primitive for five other init commands — searches the live directory first and falls back through the workstream-scoped archive (#2855); the private copy and its single-consumer entries list are retired. * chore(#4801): backfill changeset PR number (4893) --------- Co-authored-by: sim <sim@local>
This commit is contained in:
35
src/init.cts
35
src/init.cts
@@ -102,7 +102,6 @@ const { pathExistsInternal, generateSlugInternal, toPosixPath } = coreUtils;
|
||||
const {
|
||||
comparePhaseNum,
|
||||
normalizePhaseName,
|
||||
matchPhaseDirs,
|
||||
stripProjectCodePrefix,
|
||||
PHASE_NUMBER_TOKEN_SOURCE,
|
||||
PHASE_DEP_REF_SOURCE,
|
||||
@@ -2865,18 +2864,12 @@ function cmdInitManager(cwd: string, raw: boolean): void {
|
||||
}
|
||||
const rawContent = fs.readFileSync(paths.roadmap, 'utf-8');
|
||||
const content = extractCurrentMilestone(rawContent, cwd);
|
||||
const phasesDir = paths.phases;
|
||||
|
||||
// #3185 (ADR-3180 Decision 1): "which phase directories belong to the
|
||||
// CURRENT milestone" is the scoped question listMilestonePhaseDirs owns —
|
||||
// routed through it instead of a hand-rolled readdirSync + a separate
|
||||
// getMilestonePhaseFilter window check (which also never excluded
|
||||
// sentinels, unlike the owner).
|
||||
const _phaseDirEntries = listMilestonePhaseDirs(phasesDir, {
|
||||
cwd,
|
||||
phaseIdConvention,
|
||||
}).value;
|
||||
|
||||
const _checkboxStates = new Map<string, boolean>();
|
||||
const _cbPattern = new RegExp(
|
||||
`-\\s*\\[(x| )\\]\\s*.*${phaseHeadingPrefix}(${PHASE_NUMBER_TOKEN_SOURCE})[:\\s]`,
|
||||
@@ -2928,7 +2921,6 @@ function cmdInitManager(cwd: string, raw: boolean): void {
|
||||
const dependsMatch = section.match(/\*\*Depends on(?::\*\*|\*\*:)\s*([^\n]+)/i);
|
||||
const depends_on = dependsMatch ? dependsMatch[1].trim() : null;
|
||||
|
||||
const normalized = normalizePhaseName(phaseNum);
|
||||
let diskStatus = 'no_directory';
|
||||
let planCount = 0;
|
||||
let summaryCount = 0;
|
||||
@@ -2949,19 +2941,24 @@ function cmdInitManager(cwd: string, raw: boolean): void {
|
||||
);
|
||||
|
||||
try {
|
||||
// #3185 (ADR-3180 Decision 2) moved this lookup off the
|
||||
// milestone-scoped set and onto the physical one; that scope choice is
|
||||
// kept. Only the matcher is this PR's: matchPhaseDirs resolves
|
||||
// digit-leading directory names the token predicate cannot (#2528).
|
||||
const dirMatch = matchPhaseDirs(
|
||||
_phaseDirEntries,
|
||||
normalized,
|
||||
phaseIdConvention,
|
||||
).matches[0];
|
||||
// #4801: resolve through the canonical locator instead of a private
|
||||
// current-milestone-only scan. findPhaseInternal searches the live
|
||||
// .planning/phases directory FIRST (same set the retired
|
||||
// matchPhaseDirs/_phaseDirEntries pair scanned — the #3185 physical-dir
|
||||
// scope choice is inherited by the locator's live arm) and then falls
|
||||
// back through listArchiveVersionDirs (workstream-scoped, #2855), so an
|
||||
// ARCHIVED phase directory with a passing verification resolves instead
|
||||
// of reporting no_directory/phase_complete:false. Five other init
|
||||
// commands already route through this same primitive; this was the
|
||||
// holdout private copy (#4793-family declared-owner drift).
|
||||
const located = findPhaseInternal(cwd, phaseNum, phaseIdConvention) as unknown as Record<string, unknown> | null;
|
||||
const dirMatch = located && located['found']
|
||||
? path.posix.basename(String(located['directory']))
|
||||
: null;
|
||||
|
||||
if (dirMatch) {
|
||||
const fullDir = path.join(phasesDir, dirMatch);
|
||||
const phaseDirRel = toPosixPath(path.relative(cwd, fullDir));
|
||||
const fullDir = path.join(cwd, String(located!['directory']));
|
||||
const phaseDirRel = String(located!['directory']);
|
||||
// #4014 (epic #3473 B4-unreadable): this whole block used to swallow
|
||||
// ANY readdirSync failure below into the bare `catch { /* empty */ }`
|
||||
// at the bottom — an unreadable phase directory reported the exact
|
||||
|
||||
@@ -204,13 +204,15 @@ function listArchiveVersionDirs(cwd: string, wsOverride?: string | null): Archiv
|
||||
return out;
|
||||
}
|
||||
|
||||
function searchPhaseInDir(baseDir: string, relBase: string, normalized: string): PhaseSearchResult | null {
|
||||
function searchPhaseInDir(baseDir: string, relBase: string, normalized: string, convention?: string | null): PhaseSearchResult | null {
|
||||
try {
|
||||
const dirs = readSubdirectories(baseDir, true);
|
||||
// #2528: canonical two-pass selection (exact token match, then the
|
||||
// bare-integer leading-digit-run fallback) shared with the find-phase and
|
||||
// phase-plan-index scans — see phase-id.cts::matchPhaseDirs.
|
||||
const { matches, usedBareFallback } = matchPhaseDirs(dirs, normalized);
|
||||
// #4801: the convention is threaded (optional) so bracket-convention
|
||||
// consumers get exact token matching here too.
|
||||
const { matches, usedBareFallback } = matchPhaseDirs(dirs, normalized, convention);
|
||||
if (matches.length === 0) return null;
|
||||
|
||||
// #2237: fail loud when multiple directories match the same bare phase
|
||||
@@ -351,14 +353,15 @@ function searchPhaseInDir(baseDir: string, relBase: string, normalized: string):
|
||||
}
|
||||
}
|
||||
|
||||
function findPhaseInternal(cwd: string, phase: unknown): PhaseSearchResult | null {
|
||||
function findPhaseInternal(cwd: string, phase: unknown, convention?: string | null): PhaseSearchResult | null {
|
||||
if (!phase) return null;
|
||||
|
||||
const phasesDir = path.join(planningDir(cwd), 'phases');
|
||||
const normalized = normalizePhaseName(phase);
|
||||
|
||||
const relPhasesDir = toPosixPath(path.relative(cwd, phasesDir));
|
||||
const current = searchPhaseInDir(phasesDir, relPhasesDir, normalized);
|
||||
// #4801: convention threaded through to the matcher (see searchPhaseInDir).
|
||||
const current = searchPhaseInDir(phasesDir, relPhasesDir, normalized, convention);
|
||||
if (current) return current;
|
||||
|
||||
// #2855: scope the archived-milestone fallback to the SAME workstream as the
|
||||
@@ -372,7 +375,7 @@ function findPhaseInternal(cwd: string, phase: unknown): PhaseSearchResult | nul
|
||||
// getArchivedPhaseDirs via listArchiveVersionDirs (see its doc comment).
|
||||
for (const { version, archivePath } of listArchiveVersionDirs(cwd)) {
|
||||
const relBase = toPosixPath(path.relative(cwd, archivePath));
|
||||
const result = searchPhaseInDir(archivePath, relBase, normalized);
|
||||
const result = searchPhaseInDir(archivePath, relBase, normalized, convention);
|
||||
if (result) {
|
||||
result.archived = version;
|
||||
return result;
|
||||
|
||||
Reference in New Issue
Block a user