* refactor(#1294): T-final — delete the core.cjs re-export spine (epic #1267 complete) After T0–T6 nothing imports core, so retire the spine and its scaffolding: - delete src/core.cts (and the gitignored gsd-core/bin/lib/core.cjs artifact; remove its .gitignore + eslint-ignore entries) - delete scripts/lint-core-spine-imports.cjs + its allowlist; drop it from the package.json lint:ci chain - regenerate docs/INVENTORY-MANIFEST.json (drops the core.cjs surface) - sweep stale references: CONTEXT.md glossary back-compat clauses (spine retired, callers import the leaf directly), planning-config.md CONFIG_DEFAULTS owner, and false present-tense core.cjs claims in leaf-module docstrings The ADR-857 decomposition is complete: the former Core god-module is fully dissolved into its leaf modules; no re-export spine remains. No behaviour change. Closes #1294 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#1294): migrate the computed-path core.cjs importers the literal grep missed bin/install.js used require(path.join(_gsdLibDir, 'core.cjs')) (a computed path, and bin/install.js was never in the convergence lint's scan roots), and ~8 test files referenced core.cjs via path.join/readFileSync/existsSync/FILE_ARG forms the literal-string migration grep missed. Route install.js's symbols to their leaves (RUNTIME_PROFILE_MAP->model-catalog, resolveTierEntry/EFFORT_SET-> model-resolver) and repoint/adjust the test references to the leaves. Recovers the 161 'Cannot find module core.cjs' failures from the spine deletion. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
181 lines
6.5 KiB
TypeScript
181 lines
6.5 KiB
TypeScript
/**
|
|
* Phase Locator — Phase-directory search and location
|
|
*
|
|
* ADR-857 rollout phase 2d: extracted from core.cts (issue #881).
|
|
* Owns active-phase discovery against the `.planning/phases/` tree
|
|
* (`searchPhaseInDir`, `findPhaseInternal`) and archived-phase-dir
|
|
* enumeration (`getArchivedPhaseDirs`), matching phase ids/tokens against
|
|
* the filesystem. Behaviour is preserved byte-for-behaviour from the prior
|
|
* location; only the module boundary moved. The core.cjs re-export spine
|
|
* was retired in epic #1267; callers import phase-locator helpers directly.
|
|
*
|
|
* Dependencies (leaf modules only — no loadConfig):
|
|
* - node:fs / node:path (stdlib)
|
|
* - ./phase-id.cjs (normalizePhaseName, phaseTokenMatches, extractPhaseToken)
|
|
* - ./core-utils.cjs (readSubdirectories, getPhaseFileStats, extractCanonicalPlanId, toPosixPath)
|
|
* - ./planning-workspace.cjs (planningDir)
|
|
*/
|
|
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
import phaseIdModule = require('./phase-id.cjs');
|
|
const { normalizePhaseName, phaseTokenMatches, extractPhaseToken } = phaseIdModule;
|
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
import coreUtilsModule = require('./core-utils.cjs');
|
|
const { readSubdirectories, getPhaseFileStats, extractCanonicalPlanId, toPosixPath } = coreUtilsModule;
|
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
import planningWorkspace = require('./planning-workspace.cjs');
|
|
const { planningDir } = planningWorkspace;
|
|
|
|
// ─── Phase search types ───────────────────────────────────────────────────────
|
|
|
|
interface PhaseSearchResult {
|
|
found: boolean;
|
|
directory: string;
|
|
phase_number: string;
|
|
phase_name: string | null;
|
|
phase_slug: string | null;
|
|
plans: string[];
|
|
summaries: string[];
|
|
incomplete_plans: string[];
|
|
has_research: boolean;
|
|
has_context: boolean;
|
|
has_verification: boolean;
|
|
has_reviews: boolean;
|
|
archived?: string;
|
|
}
|
|
|
|
interface ArchivedPhaseDir {
|
|
name: string;
|
|
milestone: string;
|
|
basePath: string;
|
|
fullPath: string;
|
|
}
|
|
|
|
// ─── Phase search helpers ─────────────────────────────────────────────────────
|
|
|
|
function searchPhaseInDir(baseDir: string, relBase: string, normalized: string): PhaseSearchResult | null {
|
|
try {
|
|
const dirs = readSubdirectories(baseDir, true);
|
|
const match = dirs.find(d => phaseTokenMatches(d, normalized));
|
|
if (!match) return null;
|
|
|
|
const phaseToken = extractPhaseToken(match);
|
|
const phaseNumber = phaseToken || normalized;
|
|
const afterToken = match.slice(phaseToken ? phaseToken.length : 0).replace(/^-/, '');
|
|
const phaseName = afterToken || null;
|
|
const phaseDir = path.join(baseDir, match);
|
|
const { plans: unsortedPlans, summaries: unsortedSummaries, hasResearch, hasContext, hasVerification, hasReviews } = getPhaseFileStats(phaseDir);
|
|
const plans = unsortedPlans.sort();
|
|
const summaries = unsortedSummaries.sort();
|
|
|
|
const completedPlanIds = new Set(
|
|
summaries.flatMap(s => {
|
|
const exact = s.replace('-SUMMARY.md', '').replace('SUMMARY.md', '');
|
|
const canonical = extractCanonicalPlanId(s);
|
|
return canonical === exact ? [exact] : [exact, canonical];
|
|
})
|
|
);
|
|
const incompletePlans = plans.filter(p => {
|
|
const planId = p.replace('-PLAN.md', '').replace('PLAN.md', '');
|
|
const canonical = extractCanonicalPlanId(p);
|
|
return !completedPlanIds.has(planId) && !completedPlanIds.has(canonical);
|
|
});
|
|
|
|
return {
|
|
found: true,
|
|
directory: toPosixPath(path.join(relBase, match)),
|
|
phase_number: phaseNumber,
|
|
phase_name: phaseName,
|
|
phase_slug: phaseName ? phaseName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') : null,
|
|
plans,
|
|
summaries,
|
|
incomplete_plans: incompletePlans,
|
|
has_research: hasResearch,
|
|
has_context: hasContext,
|
|
has_verification: hasVerification,
|
|
has_reviews: hasReviews,
|
|
};
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function findPhaseInternal(cwd: string, phase: unknown): 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);
|
|
if (current) return current;
|
|
|
|
const milestonesDir = path.join(cwd, '.planning', 'milestones');
|
|
if (!fs.existsSync(milestonesDir)) return null;
|
|
|
|
try {
|
|
const milestoneEntries = fs.readdirSync(milestonesDir, { withFileTypes: true });
|
|
const archiveDirs = milestoneEntries
|
|
.filter(e => e.isDirectory() && /^v[\d.]+-phases$/.test(e.name))
|
|
.map(e => e.name)
|
|
.sort()
|
|
.reverse();
|
|
|
|
for (const archiveName of archiveDirs) {
|
|
const versionMatch = archiveName.match(/^(v[\d.]+)-phases$/);
|
|
const version = versionMatch![1];
|
|
const archivePath = path.join(milestonesDir, archiveName);
|
|
const relBase = '.planning/milestones/' + archiveName;
|
|
const result = searchPhaseInDir(archivePath, relBase, normalized);
|
|
if (result) {
|
|
result.archived = version;
|
|
return result;
|
|
}
|
|
}
|
|
} catch { /* intentionally empty */ }
|
|
|
|
return null;
|
|
}
|
|
|
|
function getArchivedPhaseDirs(cwd: string): ArchivedPhaseDir[] {
|
|
const milestonesDir = path.join(cwd, '.planning', 'milestones');
|
|
const results: ArchivedPhaseDir[] = [];
|
|
|
|
if (!fs.existsSync(milestonesDir)) return results;
|
|
|
|
try {
|
|
const milestoneEntries = fs.readdirSync(milestonesDir, { withFileTypes: true });
|
|
const phaseDirs = milestoneEntries
|
|
.filter(e => e.isDirectory() && /^v[\d.]+-phases$/.test(e.name))
|
|
.map(e => e.name)
|
|
.sort()
|
|
.reverse();
|
|
|
|
for (const archiveName of phaseDirs) {
|
|
const versionMatch = archiveName.match(/^(v[\d.]+)-phases$/);
|
|
const version = versionMatch![1];
|
|
const archivePath = path.join(milestonesDir, archiveName);
|
|
const dirs = readSubdirectories(archivePath, true);
|
|
|
|
for (const dir of dirs) {
|
|
results.push({
|
|
name: dir,
|
|
milestone: version,
|
|
basePath: path.join('.planning', 'milestones', archiveName),
|
|
fullPath: path.join(archivePath, dir),
|
|
});
|
|
}
|
|
}
|
|
} catch { /* intentionally empty */ }
|
|
|
|
return results;
|
|
}
|
|
|
|
export = {
|
|
searchPhaseInDir,
|
|
findPhaseInternal,
|
|
getArchivedPhaseDirs,
|
|
};
|