* refactor(query): extract phase lifecycle policy module
* refactor(query): extract phase fs and roadmap mutation adapters
* fix(sdk): propagate non-ENOENT readdir errors in phase-filesystem-adapter (CR finding 1)
Swallow only ENOENT in listDirectories; rethrow EACCES, EIO, and other
unexpected errors so callers surface real failures rather than silently
treating a permission-denied phases dir as empty.
Also adds regression test: EACCES from readdir now propagates as thrown
error instead of returning [].
* fix(sdk): propagate non-ENOENT readFile errors in phase-roadmap-mutation (CR finding 4)
readModifyWriteRoadmapMd now falls back to empty content only on ENOENT;
EACCES, EIO, and other errors are rethrown so a subsequent write cannot
clobber real roadmap content that is temporarily unreadable.
Regression tests: EACCES propagates; absent ROADMAP.md still starts empty.
* fix(sdk): omit Depends on: Phase 0 for first sequential phase; align prefix grammar (CR findings 2+3)
Finding 2: buildPhaseRoadmapEntry now omits the "Depends on" line when
phaseId == 1 (prevPhase would be 0, which is not a valid predecessor).
The guard is `prevPhase < 1` so future phase-0 configs are also safe.
Finding 3: collectDecimalSuffixesFromDirNames regex prefix pattern
updated from `[A-Z]{1,6}` to `[A-Z][A-Z0-9]*` (case-insensitive flag
added), matching the grammar used by scanSequentialMaxPhaseFromDirs.
Prevents k014 parity drift for alphanumeric project-code prefixes longer
than six characters or containing digits.
Regression tests for both fixes included.
36 lines
1.2 KiB
TypeScript
36 lines
1.2 KiB
TypeScript
import { existsSync } from 'node:fs';
|
|
import { mkdir, readdir, rename, writeFile } from 'node:fs/promises';
|
|
import { join } from 'node:path';
|
|
|
|
export async function listDirectories(dirPath: string): Promise<string[]> {
|
|
if (!existsSync(dirPath)) return [];
|
|
try {
|
|
const entries = await readdir(dirPath, { withFileTypes: true });
|
|
return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
} catch (err) {
|
|
if ((err as NodeJS.ErrnoException).code === 'ENOENT') return [];
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
export async function ensureDirectoryWithGitkeep(dirPath: string): Promise<void> {
|
|
await mkdir(dirPath, { recursive: true });
|
|
await writeFile(join(dirPath, '.gitkeep'), '', 'utf-8');
|
|
}
|
|
|
|
export async function archiveDirectories(
|
|
sourceDir: string,
|
|
archiveDir: string,
|
|
shouldArchive: (dirName: string) => boolean,
|
|
): Promise<number> {
|
|
await mkdir(archiveDir, { recursive: true });
|
|
const sourceDirs = await listDirectories(sourceDir);
|
|
let archivedCount = 0;
|
|
for (const dirName of sourceDirs) {
|
|
if (!shouldArchive(dirName)) continue;
|
|
await rename(join(sourceDir, dirName), join(archiveDir, dirName));
|
|
archivedCount++;
|
|
}
|
|
return archivedCount;
|
|
}
|