feat(S06/T02): Strip all 16 lazy createRequire fallback paths from migr…

- src/resources/extensions/gsd/dispatch-guard.ts
- src/resources/extensions/gsd/auto-dispatch.ts
- src/resources/extensions/gsd/auto-verification.ts
- src/resources/extensions/gsd/parallel-eligibility.ts
- src/resources/extensions/gsd/doctor.ts
- src/resources/extensions/gsd/doctor-checks.ts
- src/resources/extensions/gsd/visualizer-data.ts
- src/resources/extensions/gsd/workspace-index.ts
This commit is contained in:
TÂCHES 2026-03-23 13:09:37 -06:00
parent 56efa72886
commit f76fe8ec1e
17 changed files with 67 additions and 501 deletions

View file

@ -85,7 +85,7 @@ node --import ./src/resources/extensions/gsd/tests/resolve-ts.mjs --experimental
- Verify: `node --import ./src/resources/extensions/gsd/tests/resolve-ts.mjs --experimental-strip-types --test src/resources/extensions/gsd/tests/parsers.test.ts src/resources/extensions/gsd/tests/roadmap-slices.test.ts src/resources/extensions/gsd/tests/planning-crossval.test.ts src/resources/extensions/gsd/tests/markdown-renderer.test.ts src/resources/extensions/gsd/tests/auto-recovery.test.ts src/resources/extensions/gsd/tests/migrate-writer.test.ts src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts src/resources/extensions/gsd/tests/complete-milestone.test.ts` — all pass
- Done when: `parseRoadmap` and `parsePlan` no longer exported from `files.ts`, all consumers import from `parsers-legacy.ts`, all parser/crossval/renderer tests pass
- [ ] **T02: Strip all 16 lazy createRequire fallback paths from migrated callers** `est:35m`
- [x] **T02: Strip all 16 lazy createRequire fallback paths from migrated callers** `est:35m`
- Why: With parsers relocated, the lazy fallback singletons in all 16 migrated callers are dead code — they imported from `files.ts` which no longer exports parsers. Strip them to complete the parser deprecation.
- Files: `src/resources/extensions/gsd/dispatch-guard.ts`, `src/resources/extensions/gsd/auto-dispatch.ts`, `src/resources/extensions/gsd/auto-verification.ts`, `src/resources/extensions/gsd/parallel-eligibility.ts`, `src/resources/extensions/gsd/doctor.ts`, `src/resources/extensions/gsd/doctor-checks.ts`, `src/resources/extensions/gsd/visualizer-data.ts`, `src/resources/extensions/gsd/workspace-index.ts`, `src/resources/extensions/gsd/dashboard-overlay.ts`, `src/resources/extensions/gsd/auto-dashboard.ts`, `src/resources/extensions/gsd/guided-flow.ts`, `src/resources/extensions/gsd/auto-prompts.ts`, `src/resources/extensions/gsd/auto-recovery.ts`, `src/resources/extensions/gsd/auto-direct-dispatch.ts`, `src/resources/extensions/gsd/auto-worktree.ts`, `src/resources/extensions/gsd/reactive-graph.ts`
- Do: For each of the 16 files: (1) remove `import { createRequire } from "node:module"`, (2) remove the lazy parser singleton declaration and function, (3) replace `if (isDbAvailable()) { ...DB path... } else { ...parser fallback... }` with just the DB path body — when DB unavailable, return early with empty/null/skip. Special cases: `workspace-index.ts` `titleFromRoadmapHeader` was parser-only with no DB equivalent — remove it or return null when DB unavailable. `auto-prompts.ts` has async `lazyParseRoadmap`/`lazyParsePlan` helpers wrapping 6 call sites — remove the helpers entirely and inline the DB-only path. `auto-recovery.ts` has `import { createRequire }` at top and 2 inline `createRequire` usages — remove all. Remove `import { createRequire }` from files that imported it only for parser fallback (check if any remaining non-parser `createRequire` usage exists before removing).

View file

@ -26,18 +26,6 @@ import { getActiveWorktreeName } from "./worktree-command.js";
import { loadEffectiveGSDPreferences, getGlobalGSDPreferencesPath } from "./preferences.js";
import { resolveServiceTierIcon, getEffectiveServiceTier } from "./service-tier.js";
// Lazy-loaded parsers — only resolved when DB is unavailable (fallback path)
import { createRequire } from "node:module";
let _lazyParsers: { parseRoadmap: (c: string) => { slices: Array<{ id: string; done: boolean; title: string }> }; parsePlan: (c: string) => { tasks: Array<{ id: string; done: boolean; title: string }> } } | null = null;
function getLazyParsers() {
if (!_lazyParsers) {
const req = createRequire(import.meta.url);
try { const mod = req("./files.ts"); _lazyParsers = { parseRoadmap: mod.parseRoadmap, parsePlan: mod.parsePlan }; }
catch { const mod = req("./files.js"); _lazyParsers = { parseRoadmap: mod.parseRoadmap, parsePlan: mod.parsePlan }; }
}
return _lazyParsers!;
}
// ─── UAT Slice Extraction ─────────────────────────────────────────────────────
/**
@ -266,10 +254,7 @@ export function updateSliceProgressCache(base: string, mid: string, activeSid?:
if (isDbAvailable()) {
normSlices = getMilestoneSlices(mid).map(s => ({ id: s.id, done: s.status === "complete", title: s.title }));
} else {
const roadmapFile = resolveMilestoneFile(base, mid, "ROADMAP");
if (!roadmapFile) return;
const content = readFileSync(roadmapFile, "utf-8");
normSlices = getLazyParsers().parseRoadmap(content).slices;
normSlices = [];
}
let activeSliceTasks: { done: number; total: number } | null = null;
@ -285,17 +270,6 @@ export function updateSliceProgressCache(base: string, mid: string, activeSid?:
};
taskDetails = dbTasks.map(t => ({ id: t.id, title: t.title, done: t.status === "complete" || t.status === "done" }));
}
} else {
const planFile = resolveSliceFile(base, mid, activeSid, "PLAN");
if (planFile && existsSync(planFile)) {
const planContent = readFileSync(planFile, "utf-8");
const plan = getLazyParsers().parsePlan(planContent);
activeSliceTasks = {
done: plan.tasks.filter(t => t.done).length,
total: plan.tasks.length,
};
taskDetails = plan.tasks.map(t => ({ id: t.id, title: t.title, done: t.done }));
}
}
} catch {
// Non-fatal — just omit task count

View file

@ -157,19 +157,8 @@ export async function dispatchDirectPhase(
if (isDbAvailable()) {
completedSliceIds = getMilestoneSlices(mid).filter(s => s.status === "complete").map(s => s.id);
} else {
const roadmapFile = resolveMilestoneFile(base, mid, "ROADMAP");
const roadmapContent = roadmapFile ? await loadFile(roadmapFile) : null;
if (!roadmapContent) {
ctx.ui.notify("Cannot dispatch reassess-roadmap: no roadmap found.", "warning");
return;
}
const { createRequire } = await import("node:module");
const _require = createRequire(import.meta.url);
let parseRoadmap: Function;
try { parseRoadmap = _require("./files.ts").parseRoadmap; }
catch { parseRoadmap = _require("./files.js").parseRoadmap; }
const roadmap = parseRoadmap(roadmapContent);
completedSliceIds = roadmap.slices.filter((s: { done: boolean }) => s.done).map((s: { id: string }) => s.id);
ctx.ui.notify("Cannot dispatch reassess-roadmap: DB unavailable.", "warning");
return;
}
if (completedSliceIds.length === 0) {
ctx.ui.notify("Cannot dispatch reassess-roadmap: no completed slices.", "warning");
@ -192,19 +181,8 @@ export async function dispatchDirectPhase(
if (isDbAvailable()) {
uatCompletedSliceIds = getMilestoneSlices(mid).filter(s => s.status === "complete").map(s => s.id);
} else {
const roadmapFile = resolveMilestoneFile(base, mid, "ROADMAP");
const roadmapContent = roadmapFile ? await loadFile(roadmapFile) : null;
if (!roadmapContent) {
ctx.ui.notify("Cannot dispatch run-uat: no roadmap found.", "warning");
return;
}
const { createRequire } = await import("node:module");
const _require = createRequire(import.meta.url);
let parseRoadmap: Function;
try { parseRoadmap = _require("./files.ts").parseRoadmap; }
catch { parseRoadmap = _require("./files.js").parseRoadmap; }
const roadmap = parseRoadmap(roadmapContent);
uatCompletedSliceIds = roadmap.slices.filter((s: { done: boolean }) => s.done).map((s: { id: string }) => s.id);
ctx.ui.notify("Cannot dispatch run-uat: DB unavailable.", "warning");
return;
}
if (uatCompletedSliceIds.length === 0) {
ctx.ui.notify("Cannot dispatch run-uat: no completed slices.", "warning");

View file

@ -14,21 +14,7 @@ import type { GSDPreferences } from "./preferences.js";
import type { UatType } from "./files.js";
import { loadFile, extractUatType, loadActiveOverrides } from "./files.js";
import { isDbAvailable, getMilestoneSlices } from "./gsd-db.js";
import { createRequire } from "node:module";
// Lazy-loaded parseRoadmap — only resolved when DB is unavailable (fallback path).
let _lazyParseRoadmap: ((content: string) => { slices: { id: string; done: boolean }[] }) | null = null;
function lazyParseRoadmap(content: string) {
if (!_lazyParseRoadmap) {
const req = createRequire(import.meta.url);
try {
_lazyParseRoadmap = req("./files.ts").parseRoadmap;
} catch {
_lazyParseRoadmap = req("./files.js").parseRoadmap;
}
}
return _lazyParseRoadmap!(content);
}
import {
resolveMilestoneFile,
resolveMilestonePath,
@ -194,11 +180,7 @@ export const DISPATCH_RULES: DispatchRule[] = [
.filter(s => s.status === "complete")
.map(s => s.id);
} else {
// Disk fallback
const roadmapContent = roadmapFile ? await loadFile(roadmapFile) : null;
if (!roadmapContent) return null;
const roadmap = lazyParseRoadmap(roadmapContent);
completedSliceIds = roadmap.slices.filter(s => s.done).map(s => s.id);
return null;
}
for (const sliceId of completedSliceIds) {
@ -532,14 +514,7 @@ export const DISPATCH_RULES: DispatchRule[] = [
if (isDbAvailable()) {
sliceIds = getMilestoneSlices(mid).map(s => s.id);
} else {
const roadmapFile = resolveMilestoneFile(basePath, mid, "ROADMAP");
const roadmapContent = roadmapFile ? await loadFile(roadmapFile) : null;
if (roadmapContent) {
const roadmap = lazyParseRoadmap(roadmapContent);
sliceIds = roadmap.slices.map(s => s.id);
} else {
sliceIds = [];
}
sliceIds = [];
}
if (sliceIds.length > 0) {
@ -600,14 +575,7 @@ export const DISPATCH_RULES: DispatchRule[] = [
if (isDbAvailable()) {
sliceIds = getMilestoneSlices(mid).map(s => s.id);
} else {
const roadmapFile = resolveMilestoneFile(basePath, mid, "ROADMAP");
const roadmapContent = roadmapFile ? await loadFile(roadmapFile) : null;
if (roadmapContent) {
const roadmap = lazyParseRoadmap(roadmapContent);
sliceIds = roadmap.slices.map(s => s.id);
} else {
sliceIds = [];
}
sliceIds = [];
}
if (sliceIds.length > 0) {

View file

@ -28,27 +28,6 @@ import { formatDecisionsCompact, formatRequirementsCompact } from "./structured-
const MAX_PREAMBLE_CHARS = 30_000;
// ─── Lazy parser helpers ──────────────────────────────────────────────────────
// Centralize createRequire fallback for callers that need parser as a last resort.
async function lazyParseRoadmap(content: string) {
const { createRequire } = await import("node:module");
const _require = createRequire(import.meta.url);
let parseRoadmap: Function;
try { parseRoadmap = _require("./files.ts").parseRoadmap; }
catch { parseRoadmap = _require("./files.js").parseRoadmap; }
return parseRoadmap(content) as { slices: { id: string; done: boolean; depends: string[] }[] };
}
async function lazyParsePlan(content: string) {
const { createRequire } = await import("node:module");
const _require = createRequire(import.meta.url);
let parsePlan: Function;
try { parsePlan = _require("./files.ts").parsePlan; }
catch { parsePlan = _require("./files.js").parsePlan; }
return parsePlan(content) as { tasks: { id: string; title: string; done: boolean; files: string[] }[]; filesLikelyTouched: string[] };
}
// ──────────────────────────────────────────────────────────────────────────────
function capPreamble(preamble: string): string {
if (preamble.length <= MAX_PREAMBLE_CHARS) return preamble;
return truncateAtSectionBoundary(preamble, MAX_PREAMBLE_CHARS).content;
@ -207,17 +186,11 @@ export async function inlineDependencySummaries(
if (!slice || slice.depends.length === 0) return "- (no dependencies)";
depends = slice.depends as string[];
}
} catch { /* fall through to parser */ }
} catch { /* fall through */ }
// Parser fallback — load roadmap and parse for depends
// If DB didn't provide depends, we can't determine them without parsers
if (!depends) {
const roadmapFile = resolveMilestoneFile(base, mid, "ROADMAP");
const roadmapContent = roadmapFile ? await loadFile(roadmapFile) : null;
if (!roadmapContent) return "- (no dependencies)";
const roadmap = await lazyParseRoadmap(roadmapContent);
const sliceEntry = roadmap.slices.find(s => s.id === sid);
if (!sliceEntry || sliceEntry.depends.length === 0) return "- (no dependencies)";
depends = sliceEntry.depends;
return "- (no dependencies)";
}
const sections: string[] = [];
@ -738,34 +711,10 @@ export async function checkNeedsReassessment(
if (!hasSummary) return null;
return { sliceId: lastCompleted };
}
} catch { /* fall through to parser */ }
} catch { /* fall through */ }
// Parser fallback
const roadmapFile = resolveMilestoneFile(base, mid, "ROADMAP");
const roadmapContent = roadmapFile ? await loadFile(roadmapFile) : null;
if (!roadmapContent) return null;
const roadmap = await lazyParseRoadmap(roadmapContent);
const completedSlices = roadmap.slices.filter(s => s.done);
const incompleteSlices = roadmap.slices.filter(s => !s.done);
// No completed slices or all slices done — skip
if (completedSlices.length === 0 || incompleteSlices.length === 0) return null;
// Check the last completed slice
const lastCompleted = completedSlices[completedSlices.length - 1];
const assessmentFile = resolveSliceFile(base, mid, lastCompleted.id, "ASSESSMENT");
const hasAssessment = !!(assessmentFile && await loadFile(assessmentFile));
if (hasAssessment) return null;
// Also need a summary to reassess against
const summaryFile = resolveSliceFile(base, mid, lastCompleted.id, "SUMMARY");
const hasSummary = !!(summaryFile && await loadFile(summaryFile));
if (!hasSummary) return null;
return { sliceId: lastCompleted.id };
// DB unavailable — cannot determine assessment needs
return null;
}
/**
@ -806,47 +755,10 @@ export async function checkNeedsRunUat(
const uatType = extractUatType(uatContent) ?? "artifact-driven";
return { sliceId: sid, uatType };
}
} catch { /* fall through to parser */ }
} catch { /* fall through */ }
// Parser fallback
const roadmapFile = resolveMilestoneFile(base, mid, "ROADMAP");
const roadmapContent = roadmapFile ? await loadFile(roadmapFile) : null;
if (!roadmapContent) return null;
const roadmap = await lazyParseRoadmap(roadmapContent);
const completedSlices = roadmap.slices.filter(s => s.done);
const incompleteSlices = roadmap.slices.filter(s => !s.done);
// No completed slices — nothing to UAT yet
if (completedSlices.length === 0) return null;
// All slices done — milestone complete path, skip (reassessment handles)
if (incompleteSlices.length === 0) return null;
// uat_dispatch must be opted in
if (!prefs?.uat_dispatch) return null;
// Take the last completed slice
const lastCompleted = completedSlices[completedSlices.length - 1];
const sid = lastCompleted.id;
// UAT file must exist
const uatFile = resolveSliceFile(base, mid, sid, "UAT");
if (!uatFile) return null;
const uatContent = await loadFile(uatFile);
if (!uatContent) return null;
// If UAT result already exists, skip (idempotent)
const uatResultFile = resolveSliceFile(base, mid, sid, "UAT-RESULT");
if (uatResultFile) {
const hasResult = !!(await loadFile(uatResultFile));
if (hasResult) return null;
}
// Classify UAT type; default to artifact-driven (LLM-executed UATs are always artifact-driven)
const uatType = extractUatType(uatContent) ?? "artifact-driven";
return { sliceId: sid, uatType };
// DB unavailable — cannot determine UAT needs
return null;
}
// ─── Prompt Builders ──────────────────────────────────────────────────────
@ -1307,13 +1219,7 @@ export async function buildCompleteMilestonePrompt(
sliceIds = getMilestoneSlices(mid).map(s => s.id);
}
} catch { /* fall through */ }
if (sliceIds.length === 0) {
const roadmapContent = roadmapPath ? await loadFile(roadmapPath) : null;
if (roadmapContent) {
const roadmap = await lazyParseRoadmap(roadmapContent);
sliceIds = roadmap.slices.map(s => s.id);
}
}
// If DB didn't provide slice IDs, sliceIds stays empty — no summaries to inline
const seenSlices = new Set<string>();
for (const sid of sliceIds) {
if (seenSlices.has(sid)) continue;
@ -1373,13 +1279,7 @@ export async function buildValidateMilestonePrompt(
valSliceIds = getMilestoneSlices(mid).map(s => s.id);
}
} catch { /* fall through */ }
if (valSliceIds.length === 0) {
const roadmapContent = roadmapPath ? await loadFile(roadmapPath) : null;
if (roadmapContent) {
const roadmap = await lazyParseRoadmap(roadmapContent);
valSliceIds = roadmap.slices.map(s => s.id);
}
}
// If DB didn't provide slice IDs, valSliceIds stays empty
const seenValSlices = new Set<string>();
for (const sid of valSliceIds) {
if (seenValSlices.has(sid)) continue;
@ -1714,12 +1614,8 @@ export async function buildRewriteDocsPrompt(
} catch { /* fall through */ }
if (!incompleteTasks) {
// Parser fallback
const planContent = await loadFile(slicePlanPath);
if (planContent) {
const plan = await lazyParsePlan(planContent);
incompleteTasks = plan.tasks.filter(t => !t.done).map(t => ({ id: t.id }));
}
// DB unavailable — no task data to inline
incompleteTasks = [];
}
if (incompleteTasks) {

View file

@ -10,9 +10,9 @@
import type { ExtensionContext } from "@gsd/pi-coding-agent";
import { parseUnitId } from "./unit-id.js";
import { atomicWriteSync } from "./atomic-write.js";
import { createRequire } from "node:module";
import { clearUnitRuntimeRecord } from "./unit-runtime.js";
import { clearParseCache } from "./files.js";
import { parseRoadmap as parseLegacyRoadmap, parsePlan as parseLegacyPlan } from "./parsers-legacy.js";
import { isDbAvailable, getTask, getSlice, getSliceTasks } from "./gsd-db.js";
import { isValidationTerminal } from "./state.js";
import {
@ -375,13 +375,9 @@ export function verifyExpectedArtifact(
}
if (!taskIds) {
// Parser fallback
// DB unavailable or no tasks in DB — parse plan file for task IDs
const planContent = readFileSync(absPath, "utf-8");
const _require = createRequire(import.meta.url);
let parsePlan: Function;
try { parsePlan = _require("./parsers-legacy.ts").parsePlan; }
catch { parsePlan = _require("./parsers-legacy.js").parsePlan; }
const plan = parsePlan(planContent);
const plan = parseLegacyPlan(planContent);
if (plan.tasks.length > 0) taskIds = plan.tasks.map((t: { id: string }) => t.id);
}
@ -418,16 +414,12 @@ export function verifyExpectedArtifact(
// DB available — trust it
if (dbSlice.status !== "complete") return false;
} else if (!isDbAvailable()) {
// DB unavailable — fall back to roadmap checkbox check
// DB unavailable — fall back to roadmap checkbox check via parsers-legacy
const roadmapFile = resolveMilestoneFile(base, mid, "ROADMAP");
if (roadmapFile && existsSync(roadmapFile)) {
try {
const roadmapContent = readFileSync(roadmapFile, "utf-8");
const _require = createRequire(import.meta.url);
let parseRoadmap: Function;
try { parseRoadmap = _require("./parsers-legacy.ts").parseRoadmap; }
catch { parseRoadmap = _require("./parsers-legacy.js").parseRoadmap; }
const roadmap = parseRoadmap(roadmapContent);
const roadmap = parseLegacyRoadmap(roadmapContent);
const slice = roadmap.slices.find((s) => s.id === sid);
if (slice && !slice.done) return false;
} catch {

View file

@ -13,7 +13,6 @@
import type { ExtensionContext, ExtensionAPI } from "@gsd/pi-coding-agent";
import { resolveSliceFile, resolveSlicePath } from "./paths.js";
import { isDbAvailable, getTask } from "./gsd-db.js";
import { createRequire } from "node:module";
import { loadEffectiveGSDPreferences } from "./preferences.js";
import {
runVerificationGate,
@ -67,25 +66,8 @@ export async function runPostUnitVerification(
const [mid, sid, tid] = parts;
if (isDbAvailable()) {
taskPlanVerify = getTask(mid, sid, tid)?.verify;
} else {
// Disk fallback: lazy-load parsePlan + loadFile
const planFile = resolveSliceFile(s.basePath, mid, sid, "PLAN");
if (planFile) {
const req = createRequire(import.meta.url);
let filesModule: { loadFile: (p: string) => Promise<string | null>; parsePlan: (c: string) => { tasks?: { id: string; verify?: string }[] } };
try {
filesModule = req("./files.ts");
} catch {
filesModule = req("./files.js");
}
const planContent = await filesModule.loadFile(planFile);
if (planContent) {
const slicePlan = filesModule.parsePlan(planContent);
const taskEntry = slicePlan?.tasks?.find((t) => t.id === tid);
taskPlanVerify = taskEntry?.verify;
}
}
}
// When DB unavailable, taskPlanVerify stays undefined — gate runs without task-specific checks
}
const result = runVerificationGate({

View file

@ -18,7 +18,6 @@ import {
lstatSync as lstatSyncFn,
} from "node:fs";
import { isAbsolute, join } from "node:path";
import { createRequire } from "node:module";
import { GSDError, GSD_IO_ERROR, GSD_GIT_ERROR } from "./errors.js";
import {
reconcileWorktreeDb,
@ -1005,14 +1004,8 @@ export function mergeMilestoneToMain(
completedSlices = getMilestoneSlices(milestoneId)
.filter(s => s.status === "complete")
.map(s => ({ id: s.id, title: s.title }));
} else {
const _require = createRequire(import.meta.url);
let parseRoadmap: Function;
try { parseRoadmap = _require("./files.ts").parseRoadmap; }
catch { parseRoadmap = _require("./files.js").parseRoadmap; }
const roadmap = parseRoadmap(roadmapContent);
completedSlices = roadmap.slices.filter((s: { done: boolean }) => s.done).map((s: { id: string; title: string }) => ({ id: s.id, title: s.title }));
}
// When DB unavailable, completedSlices stays empty — commit message will omit slice details
// 3. chdir to original base
const previousCwd = process.cwd();

View file

@ -27,18 +27,6 @@ import { estimateTimeRemaining } from "./auto-dashboard.js";
import { computeProgressScore, formatProgressLine } from "./progress-score.js";
import { runEnvironmentChecks, type EnvironmentCheckResult } from "./doctor-environment.js";
// Lazy-loaded parsers — only resolved when DB is unavailable (fallback path)
import { createRequire } from "node:module";
let _lazyParsers: { parseRoadmap: (c: string) => { slices: Array<{ id: string; done: boolean; title: string; risk: string }> }; parsePlan: (c: string) => { tasks: Array<{ id: string; done: boolean; title: string }> } } | null = null;
function getLazyParsers() {
if (!_lazyParsers) {
const req = createRequire(import.meta.url);
try { const mod = req("./files.ts"); _lazyParsers = { parseRoadmap: mod.parseRoadmap, parsePlan: mod.parsePlan }; }
catch { const mod = req("./files.js"); _lazyParsers = { parseRoadmap: mod.parseRoadmap, parsePlan: mod.parsePlan }; }
}
return _lazyParsers!;
}
function unitLabel(type: string): string {
switch (type) {
case "research-milestone": return "Research";
@ -172,13 +160,11 @@ export class GSDDashboardOverlay {
const roadmapFile = resolveMilestoneFile(base, mid, "ROADMAP");
const roadmapContent = roadmapFile ? await loadFile(roadmapFile) : null;
// Normalize slices: prefer DB, fall back to parser
// Normalize slices from DB
type NormSlice = { id: string; done: boolean; title: string; risk: string };
let normSlices: NormSlice[] = [];
if (isDbAvailable()) {
normSlices = getMilestoneSlices(mid).map(s => ({ id: s.id, done: s.status === "complete", title: s.title, risk: s.risk || "medium" }));
} else if (roadmapContent) {
normSlices = getLazyParsers().parseRoadmap(roadmapContent).slices;
}
for (const s of normSlices) {
@ -192,7 +178,7 @@ export class GSDDashboardOverlay {
};
if (sliceView.active) {
// Normalize tasks: prefer DB, fall back to parser
// Normalize tasks from DB
if (isDbAvailable()) {
const dbTasks = getSliceTasks(mid, s.id);
sliceView.taskProgress = {
@ -207,24 +193,6 @@ export class GSDDashboardOverlay {
active: state.activeTask?.id === t.id,
});
}
} else {
const planFile = resolveSliceFile(base, mid, s.id, "PLAN");
const planContent = planFile ? await loadFile(planFile) : null;
if (planContent) {
const plan = getLazyParsers().parsePlan(planContent);
sliceView.taskProgress = {
done: plan.tasks.filter(t => t.done).length,
total: plan.tasks.length,
};
for (const t of plan.tasks) {
sliceView.tasks.push({
id: t.id,
title: t.title,
done: t.done,
active: state.activeTask?.id === t.id,
});
}
}
}
}

View file

@ -1,27 +1,9 @@
// GSD Dispatch Guard — prevents out-of-order slice dispatch
import { readFileSync } from "node:fs";
import { createRequire } from "node:module";
import { resolveMilestoneFile } from "./paths.js";
import { findMilestoneIds } from "./guided-flow.js";
import { isDbAvailable, getMilestoneSlices } from "./gsd-db.js";
// Lazy-loaded parser — only resolved when DB is unavailable (fallback path).
// Uses createRequire so the function stays synchronous. Tries .ts first (strip-types dev)
// then .js (compiled production).
let _lazyParser: ((content: string) => { id: string; done: boolean; depends: string[] }[]) | null = null;
function lazyParseRoadmapSlices(content: string) {
if (!_lazyParser) {
const req = createRequire(import.meta.url);
try {
_lazyParser = req("./roadmap-slices.ts").parseRoadmapSlices;
} catch {
_lazyParser = req("./roadmap-slices.js").parseRoadmapSlices;
}
}
return _lazyParser!(content);
}
const SLICE_DISPATCH_TYPES = new Set([
"research-slice",
"plan-slice",
@ -30,28 +12,6 @@ const SLICE_DISPATCH_TYPES = new Set([
"complete-slice",
]);
/**
* Read a roadmap file from disk (working tree) rather than from a git branch.
*
* Prior implementation used `git show <branch>:<path>` which read committed
* state on a specific branch. This caused false-positive blockers when work
* was committed on a milestone/worktree branch but the integration branch
* (main) hadn't been updated yet the guard would see prior slices as
* incomplete on main even though they were done in the working tree (#530).
*
* Reading from disk always reflects the latest state, regardless of which
* branch is checked out or whether changes have been committed.
*/
function readRoadmapFromDisk(base: string, milestoneId: string): string | null {
try {
const absPath = resolveMilestoneFile(base, milestoneId, "ROADMAP");
if (!absPath) return null;
return readFileSync(absPath, "utf-8").trim();
} catch {
return null;
}
}
export function getPriorSliceCompletionBlocker(
base: string,
_mainBranch: string,
@ -74,24 +34,18 @@ export function getPriorSliceCompletionBlocker(
if (resolveMilestoneFile(base, mid, "PARKED")) continue;
if (resolveMilestoneFile(base, mid, "SUMMARY")) continue;
// Normalised slice list: prefer DB, fall back to disk parsing
// Normalised slice list from DB
type NormSlice = { id: string; done: boolean; depends: string[] };
let slices: NormSlice[];
if (isDbAvailable()) {
const rows = getMilestoneSlices(mid);
if (rows.length === 0) continue;
slices = rows.map((r) => ({
id: r.id,
done: r.status === "complete",
depends: r.depends ?? [],
}));
} else {
// Fallback: disk parsing when DB is not yet initialised
const roadmapContent = readRoadmapFromDisk(base, mid);
if (!roadmapContent) continue;
slices = lazyParseRoadmapSlices(roadmapContent);
}
if (!isDbAvailable()) continue;
const rows = getMilestoneSlices(mid);
if (rows.length === 0) continue;
const slices: NormSlice[] = rows.map((r) => ({
id: r.id,
done: r.status === "complete",
depends: r.depends ?? [],
}));
if (mid !== targetMid) {
const incomplete = slices.find((slice) => !slice.done);

View file

@ -4,6 +4,7 @@ import { basename, dirname, join, sep } from "node:path";
import type { DoctorIssue, DoctorIssueCode } from "./doctor-types.js";
import { readRepoMeta, externalProjectsRoot } from "./repo-identity.js";
import { loadFile } from "./files.js";
import { parseRoadmap as parseLegacyRoadmap } from "./parsers-legacy.js";
import { isDbAvailable, getMilestoneSlices } from "./gsd-db.js";
import { resolveMilestoneFile, milestonesDir, gsdRoot, resolveGsdRootFile, relGsdRootFile } from "./paths.js";
import { deriveState, isMilestoneComplete } from "./state.js";
@ -19,17 +20,6 @@ import { readAllSessionStatuses, isSessionStale, removeSessionStatus } from "./s
import { recoverFailedMigration } from "./migrate-external.js";
import { loadEffectiveGSDPreferences } from "./preferences.js";
// Lazy-loaded parser — only resolved when DB is unavailable (fallback path)
import { createRequire } from "node:module";
let _lazyParseRoadmap: ((c: string) => { title: string; slices: Array<{ id: string; done: boolean; title: string; risk: string; depends: string[]; demo: string }> }) | null = null;
function lazyParseRoadmap(content: string) {
if (!_lazyParseRoadmap) {
const req = createRequire(import.meta.url);
try { _lazyParseRoadmap = req("./files.ts").parseRoadmap; }
catch { _lazyParseRoadmap = req("./files.js").parseRoadmap; }
}
return _lazyParseRoadmap!(content);
}
export async function checkGitHealth(
basePath: string,
issues: DoctorIssue[],
@ -70,10 +60,11 @@ export async function checkGitHealth(
const roadmapPath = resolveMilestoneFile(basePath, milestoneId, "ROADMAP");
const roadmapContent = roadmapPath ? await loadFile(roadmapPath) : null;
if (roadmapContent) {
const roadmap = lazyParseRoadmap(roadmapContent);
const roadmap = parseLegacyRoadmap(roadmapContent);
isComplete = isMilestoneComplete(roadmap);
}
}
// When DB unavailable and no roadmap, isComplete stays false
}
if (isComplete) {
@ -122,7 +113,7 @@ export async function checkGitHealth(
} else {
const roadmapContent = roadmapPath ? await loadFile(roadmapPath) : null;
if (!roadmapContent) continue;
const roadmap = lazyParseRoadmap(roadmapContent);
const roadmap = parseLegacyRoadmap(roadmapContent);
branchMilestoneComplete = isMilestoneComplete(roadmap);
}
if (branchMilestoneComplete) {

View file

@ -2,6 +2,7 @@ import { existsSync, mkdirSync, lstatSync, readdirSync, readFileSync } from "nod
import { join } from "node:path";
import { loadFile, parseSummary, saveFile, parseTaskPlanMustHaves, countMustHavesMentionedInSummary } from "./files.js";
import { parseRoadmap as parseLegacyRoadmap, parsePlan as parseLegacyPlan } from "./parsers-legacy.js";
import { isDbAvailable, getMilestoneSlices, getSliceTasks } from "./gsd-db.js";
import { resolveMilestoneFile, resolveMilestonePath, resolveSliceFile, resolveSlicePath, resolveTaskFile, resolveTasksDir, milestonesDir, gsdRoot, relMilestoneFile, relSliceFile, relTaskFile, relSlicePath, relGsdRootFile, resolveGsdRootFile, relMilestonePath } from "./paths.js";
import { deriveState, isMilestoneComplete } from "./state.js";
@ -15,23 +16,6 @@ import { checkGitHealth, checkRuntimeHealth, checkGlobalHealth } from "./doctor-
import { checkEnvironmentHealth } from "./doctor-environment.js";
import { runProviderChecks } from "./doctor-providers.js";
// ── Lazy-loaded parsers — only resolved when DB is unavailable (fallback path) ──
import { createRequire } from "node:module";
let _lazyParsers: { parseRoadmap: (c: string) => { title: string; slices: RoadmapSliceEntry[] }; parsePlan: (c: string) => { title: string; goal: string; tasks: Array<{ id: string; done: boolean; title: string; estimate?: string; files?: string[]; verify?: string }> } } | null = null;
function getLazyParsers() {
if (!_lazyParsers) {
const req = createRequire(import.meta.url);
try {
const mod = req("./files.ts");
_lazyParsers = { parseRoadmap: mod.parseRoadmap, parsePlan: mod.parsePlan };
} catch {
const mod = req("./files.js");
_lazyParsers = { parseRoadmap: mod.parseRoadmap, parsePlan: mod.parsePlan };
}
}
return _lazyParsers!;
}
// ── Re-exports ─────────────────────────────────────────────────────────────
// All public types and functions from extracted modules are re-exported here
// so that existing imports from "./doctor.js" continue to work unchanged.
@ -231,13 +215,12 @@ export async function selectDoctorScope(basePath: string, requestedScope?: strin
const roadmapPath = resolveMilestoneFile(basePath, milestone.id, "ROADMAP");
const roadmapContent = roadmapPath ? await loadFile(roadmapPath) : null;
if (!roadmapContent) continue;
// DB primary path — check slice statuses directly from DB
if (isDbAvailable()) {
const dbSlices = getMilestoneSlices(milestone.id);
const allDone = dbSlices.length > 0 && dbSlices.every(s => s.status === "complete");
if (!allDone) return milestone.id;
} else {
const roadmap = getLazyParsers().parseRoadmap(roadmapContent);
const roadmap = parseLegacyRoadmap(roadmapContent);
if (!isMilestoneComplete(roadmap)) return milestone.id;
}
}
@ -500,7 +483,7 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
demo: s.demo,
}));
} else {
slices = getLazyParsers().parseRoadmap(roadmapContent).slices;
slices = parseLegacyRoadmap(roadmapContent).slices;
}
// Wrap in Roadmap-compatible shape for detectCircularDependencies
const roadmap = { slices };
@ -622,7 +605,7 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
const planPath = resolveSliceFile(basePath, milestoneId, slice.id, "PLAN");
const planContent = planPath ? await loadFile(planPath) : null;
// Normalize plan tasks: prefer DB, fall back to parser
// Normalize plan tasks: prefer DB, fall back to parsers-legacy
let plan: { tasks: Array<{ id: string; done: boolean; title: string; estimate?: string }> } | null = null;
if (isDbAvailable()) {
const dbTasks = getSliceTasks(milestoneId, slice.id);
@ -631,7 +614,7 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
}
}
if (!plan && planContent) {
plan = getLazyParsers().parsePlan(planContent);
plan = parseLegacyPlan(planContent);
}
if (!plan) {
if (!slice.done) {

View file

@ -39,18 +39,6 @@ import { findMilestoneIds, nextMilestoneId, reserveMilestoneId, getReservedMiles
import { parkMilestone, discardMilestone } from "./milestone-actions.js";
import { resolveModelWithFallbacksForUnit } from "./preferences-models.js";
// Lazy-loaded parseRoadmap — only resolved when DB is unavailable (fallback path)
import { createRequire } from "node:module";
let _lazyParseRoadmap: ((c: string) => { slices: Array<{ id: string; done: boolean; title: string; risk: string; depends: string[]; demo: string }> }) | null = null;
function lazyParseRoadmap(content: string) {
if (!_lazyParseRoadmap) {
const req = createRequire(import.meta.url);
try { _lazyParseRoadmap = req("./files.ts").parseRoadmap; }
catch { _lazyParseRoadmap = req("./files.js").parseRoadmap; }
}
return _lazyParseRoadmap!(content);
}
// ─── Re-exports (preserve public API for existing importers) ────────────────
export {
MILESTONE_ID_RE, generateMilestoneSuffix, nextMilestoneId,
@ -464,8 +452,6 @@ async function buildDiscussSlicePrompt(
let normSlices: NormSlice[] = [];
if (isDbAvailable()) {
normSlices = getMilestoneSlices(mid).map(s => ({ id: s.id, done: s.status === "complete" }));
} else if (roadmapContent) {
normSlices = lazyParseRoadmap(roadmapContent).slices;
}
for (const s of normSlices) {
if (!s.done || s.id === sid) continue;
@ -608,7 +594,7 @@ export async function showDiscuss(
if (isDbAvailable()) {
normSlices = getMilestoneSlices(mid).map(s => ({ id: s.id, done: s.status === "complete", title: s.title }));
} else {
normSlices = lazyParseRoadmap(roadmapContent!).slices;
normSlices = [];
}
const pendingSlices = normSlices.filter(s => !s.done);

View file

@ -9,7 +9,6 @@ import { deriveState } from "./state.js";
import { resolveMilestoneFile, resolveSliceFile } from "./paths.js";
import { findMilestoneIds } from "./guided-flow.js";
import { isDbAvailable, getMilestoneSlices, getSliceTasks } from "./gsd-db.js";
import { createRequire } from "node:module";
import type { MilestoneRegistryEntry } from "./types.js";
// ─── Types ───────────────────────────────────────────────────────────────────
@ -52,41 +51,8 @@ async function collectTouchedFiles(
}
}
}
} else {
// Disk fallback: lazy-load parsers
const req = createRequire(import.meta.url);
let filesModule: {
loadFile: (p: string) => Promise<string | null>;
parseRoadmap: (c: string) => { slices: { id: string }[] };
parsePlan: (c: string) => { filesLikelyTouched: string[] };
};
try {
filesModule = req("./files.ts");
} catch {
filesModule = req("./files.js");
}
const roadmapPath = resolveMilestoneFile(basePath, milestoneId, "ROADMAP");
if (!roadmapPath) return [];
const roadmapContent = await filesModule.loadFile(roadmapPath);
if (!roadmapContent) return [];
const roadmap = filesModule.parseRoadmap(roadmapContent);
for (const slice of roadmap.slices) {
const planPath = resolveSliceFile(basePath, milestoneId, slice.id, "PLAN");
if (!planPath) continue;
const planContent = await filesModule.loadFile(planPath);
if (!planContent) continue;
const plan = filesModule.parsePlan(planContent);
for (const f of plan.filesLikelyTouched) {
files.add(f);
}
}
}
// When DB unavailable, return empty file set — parallel eligibility cannot be determined
return [...files];
}

View file

@ -205,16 +205,8 @@ export async function loadSliceTaskIO(
} catch { /* fall through */ }
if (!taskEntries) {
// Parser fallback
if (!planContent) return [];
const { createRequire } = await import("node:module");
const _require = createRequire(import.meta.url);
let parsePlan: Function;
try { parsePlan = _require("./files.ts").parsePlan; }
catch { parsePlan = _require("./files.js").parsePlan; }
const plan = parsePlan(planContent);
taskEntries = plan.tasks;
if (!taskEntries || taskEntries.length === 0) return [];
// DB unavailable — cannot determine task graph
return [];
}
const tDir = resolveTasksDir(basePath, mid, sid);

View file

@ -37,18 +37,6 @@ import type {
UnitMetrics,
} from './metrics.js';
// Lazy-loaded parsers — only resolved when DB is unavailable (fallback path)
import { createRequire } from 'node:module';
let _lazyParsers: { parseRoadmap: (c: string) => { slices: Array<{ id: string; done: boolean; title: string; risk: string; depends: string[]; demo: string }> }; parsePlan: (c: string) => { tasks: Array<{ id: string; done: boolean; title: string; estimate?: string }> } } | null = null;
function getLazyParsers() {
if (!_lazyParsers) {
const req = createRequire(import.meta.url);
try { const mod = req('./files.ts'); _lazyParsers = { parseRoadmap: mod.parseRoadmap, parsePlan: mod.parsePlan }; }
catch { const mod = req('./files.js'); _lazyParsers = { parseRoadmap: mod.parseRoadmap, parsePlan: mod.parsePlan }; }
}
return _lazyParsers!;
}
// ─── Visualizer Types ─────────────────────────────────────────────────────────
export interface VisualizerMilestone {
@ -810,13 +798,13 @@ export async function loadVisualizerData(basePath: string): Promise<VisualizerDa
const roadmapContent = roadmapFile ? readFileCached(roadmapFile) : null;
if (roadmapContent || isDbAvailable()) {
// Normalize slices: prefer DB, fall back to parser
// Normalize slices from DB
type NormSlice = { id: string; done: boolean; title: string; risk: string; depends: string[]; demo: string };
let normSlices: NormSlice[];
if (isDbAvailable()) {
normSlices = getMilestoneSlices(mid).map(s => ({ id: s.id, done: s.status === 'complete', title: s.title, risk: s.risk || 'medium', depends: s.depends, demo: s.demo }));
} else {
normSlices = getLazyParsers().parseRoadmap(roadmapContent!).slices;
normSlices = [];
}
for (const s of normSlices) {
@ -827,7 +815,7 @@ export async function loadVisualizerData(basePath: string): Promise<VisualizerDa
const tasks: VisualizerTask[] = [];
if (isActiveSlice) {
// Normalize tasks: prefer DB, fall back to parser
// Normalize tasks from DB
if (isDbAvailable()) {
for (const t of getSliceTasks(mid, s.id)) {
tasks.push({
@ -838,21 +826,6 @@ export async function loadVisualizerData(basePath: string): Promise<VisualizerDa
estimate: t.estimate || undefined,
});
}
} else {
const planFile = resolveSliceFile(basePath, mid, s.id, 'PLAN');
const planContent = planFile ? readFileCached(planFile) : null;
if (planContent) {
const plan = getLazyParsers().parsePlan(planContent);
for (const t of plan.tasks) {
tasks.push({
id: t.id,
title: t.title,
done: t.done,
active: state.activeTask?.id === t.id,
estimate: t.estimate || undefined,
});
}
}
}
}

View file

@ -15,18 +15,6 @@ import type { RiskLevel } from "./types.js";
import { type ValidationIssue, validateCompleteBoundary, validatePlanBoundary } from "./observability-validator.js";
import { getSliceBranchName, detectWorktreeName } from "./worktree.js";
// Lazy-loaded parsers — only resolved when DB is unavailable (fallback path)
import { createRequire } from "node:module";
let _lazyParsers: { parseRoadmap: (c: string) => { title: string; slices: Array<{ id: string; done: boolean; title: string; risk: string; depends: string[]; demo: string }> }; parsePlan: (c: string) => { title: string; tasks: Array<{ id: string; done: boolean; title: string; estimate?: string }> } } | null = null;
function getLazyParsers() {
if (!_lazyParsers) {
const req = createRequire(import.meta.url);
try { const mod = req("./files.ts"); _lazyParsers = { parseRoadmap: mod.parseRoadmap, parsePlan: mod.parsePlan }; }
catch { const mod = req("./files.js"); _lazyParsers = { parseRoadmap: mod.parseRoadmap, parsePlan: mod.parsePlan }; }
}
return _lazyParsers!;
}
export interface WorkspaceTaskTarget {
id: string;
title: string;
@ -75,10 +63,12 @@ export interface GSDWorkspaceIndex {
validationIssues: ValidationIssue[];
}
// Extract milestone title from roadmap header without using parsers.
// Falls back to the milestone ID if no title line found.
function titleFromRoadmapHeader(content: string, fallbackId: string): string {
const roadmap = getLazyParsers().parseRoadmap(content);
return roadmap.title.replace(/^M\d+(?:-[a-z0-9]{6})?[^:]*:\s*/, "") || fallbackId;
// Parse the "# M001: Title" header directly
const match = content.match(/^#\s+M\d+(?:-[a-z0-9]{6})?[^:]*:\s*(.+)/m);
return match?.[1]?.trim() || fallbackId;
}
async function indexSlice(basePath: string, milestoneId: string, sliceId: string, fallbackTitle: string, done: boolean, roadmapMeta?: { risk?: RiskLevel; depends?: string[]; demo?: string }): Promise<WorkspaceSliceTarget> {
@ -90,7 +80,7 @@ async function indexSlice(basePath: string, milestoneId: string, sliceId: string
const tasks: WorkspaceTaskTarget[] = [];
let title = fallbackTitle;
// Prefer DB for task data, fall back to parser
// Prefer DB for task data
if (isDbAvailable()) {
const dbTasks = getSliceTasks(milestoneId, sliceId);
for (const task of dbTasks) {
@ -103,22 +93,8 @@ async function indexSlice(basePath: string, milestoneId: string, sliceId: string
summaryPath: resolveTaskFile(basePath, milestoneId, sliceId, task.id, "SUMMARY") ?? undefined,
});
}
} else if (planPath) {
const content = await loadFile(planPath);
if (content) {
const plan = getLazyParsers().parsePlan(content);
title = plan.title || fallbackTitle;
for (const task of plan.tasks) {
tasks.push({
id: task.id,
title: task.title,
done: task.done,
planPath: resolveTaskFile(basePath, milestoneId, sliceId, task.id, "PLAN") ?? undefined,
summaryPath: resolveTaskFile(basePath, milestoneId, sliceId, task.id, "SUMMARY") ?? undefined,
});
}
}
}
// When DB unavailable, tasks stays empty
return {
id: sliceId,
@ -158,24 +134,18 @@ export async function indexWorkspace(basePath: string, opts: IndexWorkspaceOptio
const slices: WorkspaceSliceTarget[] = [];
if (roadmapPath || isDbAvailable()) {
// Normalize slices: prefer DB, fall back to parser
// Normalize slices from DB
type NormSlice = { id: string; done: boolean; title: string; risk: string; depends: string[]; demo: string };
let normSlices: NormSlice[];
if (isDbAvailable()) {
normSlices = getMilestoneSlices(milestoneId).map(s => ({ id: s.id, done: s.status === "complete", title: s.title, risk: s.risk || "medium", depends: s.depends, demo: s.demo }));
// Get title from DB milestone or roadmap header
// Get title from roadmap header
if (roadmapPath) {
const roadmapContent = await loadFile(roadmapPath);
if (roadmapContent) title = titleFromRoadmapHeader(roadmapContent, milestoneId);
}
} else {
const roadmapContent = await loadFile(roadmapPath!);
if (roadmapContent) {
normSlices = getLazyParsers().parseRoadmap(roadmapContent).slices;
title = titleFromRoadmapHeader(roadmapContent, milestoneId);
} else {
normSlices = [];
}
normSlices = [];
}
if (normSlices!.length > 0) {