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:
parent
56efa72886
commit
f76fe8ec1e
17 changed files with 67 additions and 501 deletions
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue