test(S06/T01): Extract parseRoadmap/parsePlan into parsers-legacy.ts, u…

- src/resources/extensions/gsd/parsers-legacy.ts
- src/resources/extensions/gsd/files.ts
- src/resources/extensions/gsd/state.ts
- src/resources/extensions/gsd/md-importer.ts
- src/resources/extensions/gsd/commands-maintenance.ts
- src/resources/extensions/gsd/markdown-renderer.ts
- src/resources/extensions/gsd/auto-recovery.ts
- src/resources/extensions/gsd/tests/parsers.test.ts
This commit is contained in:
TÂCHES 2026-03-23 12:53:49 -06:00
parent 3af95e601b
commit 56efa72886
16 changed files with 321 additions and 235 deletions

View file

@ -69,9 +69,16 @@ node --import ./src/resources/extensions/gsd/tests/resolve-ts.mjs --experimental
src/resources/extensions/gsd/tests/complete-milestone.test.ts
```
## Observability / Diagnostics
- **Failure visibility:** `doctor.test.ts` (and any test exercising the 16 migrated callers' fallback paths) will fail with `TypeError: getLazyParsers(...).parseRoadmap is not a function` after T01 completes — this is expected intermediate breakage that T02 resolves by stripping the fallback paths entirely.
- **Runtime signal:** `clearParseCache()` in `files.ts` invokes all registered cache-clear callbacks via `registerCacheClearCallback()`. If `parsers-legacy.ts` is not loaded (e.g., no consumer imported it), its cache won't be cleared — but this is correct: if nobody imported the parsers, there's nothing cached.
- **Inspection surface:** `grep -rn 'parseRoadmap\|parsePlan' src/resources/extensions/gsd/files.ts` must return exit code 1 (no matches) to confirm parser functions are fully extracted.
- **Diagnostic check:** After both tasks, `grep -rn 'createRequire' src/resources/extensions/gsd/{dispatch-guard,auto-dispatch,...}.ts` returns no matches — confirms all fallback paths removed.
## Tasks
- [ ] **T01: Create parsers-legacy.ts and relocate all parser functions from files.ts** `est:45m`
- [x] **T01: Create parsers-legacy.ts and relocate all parser functions from files.ts** `est:45m`
- Why: Parser functions must be extracted from `files.ts` into a dedicated legacy module before fallback paths can be stripped — otherwise removing exports from `files.ts` breaks the 4 legitimate consumers and 8 test files simultaneously
- Files: `src/resources/extensions/gsd/parsers-legacy.ts` (new), `src/resources/extensions/gsd/files.ts`, `src/resources/extensions/gsd/state.ts`, `src/resources/extensions/gsd/md-importer.ts`, `src/resources/extensions/gsd/commands-maintenance.ts`, `src/resources/extensions/gsd/markdown-renderer.ts`, `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/auto-recovery.test.ts`, `src/resources/extensions/gsd/tests/markdown-renderer.test.ts`, `src/resources/extensions/gsd/tests/complete-milestone.test.ts`, `src/resources/extensions/gsd/tests/migrate-writer.test.ts`, `src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts`
- Do: Create `parsers-legacy.ts` containing `parseRoadmap()`, `_parseRoadmapImpl()`, `parsePlan()`, `_parsePlanImpl()`, `cachedParse()`, and re-exporting `parseRoadmapSlices` from `roadmap-slices.js`. Import `extractSection`, `parseBullets`, `extractBoldField` from `./files.js`. Import `splitFrontmatter`, `parseFrontmatterMap` from `../shared/frontmatter.js`. Import `nativeParseRoadmap`, `nativeParsePlanFile` from `./native-parser-bridge.js`. Import `debugTime`, `debugCount` from `./debug-logger.js`. Keep `clearParseCache()` exported from `files.ts` (other callers depend on it) — have `parsers-legacy.ts` import it from `./files.js`. Remove `parseRoadmap`, `_parseRoadmapImpl`, `parsePlan`, `_parsePlanImpl` from `files.ts`. Remove `import { parseRoadmapSlices }` and `nativeParseRoadmap`/`nativeParsePlanFile` from `files.ts` imports (keep `nativeExtractSection`/`nativeParseSummaryFile`/`NATIVE_UNAVAILABLE` — used by non-parser functions). Update `state.ts` import to `./parsers-legacy.js`. Update `md-importer.ts` import to `./parsers-legacy.js`. Update `commands-maintenance.ts` dynamic import to `./parsers-legacy.js`. Update `markdown-renderer.ts` detectStaleRenders lazy import to `./parsers-legacy.ts`/`.js`. Update all 8 test files' imports.

View file

@ -379,8 +379,8 @@ export function verifyExpectedArtifact(
const planContent = readFileSync(absPath, "utf-8");
const _require = createRequire(import.meta.url);
let parsePlan: Function;
try { parsePlan = _require("./files.ts").parsePlan; }
catch { parsePlan = _require("./files.js").parsePlan; }
try { parsePlan = _require("./parsers-legacy.ts").parsePlan; }
catch { parsePlan = _require("./parsers-legacy.js").parsePlan; }
const plan = parsePlan(planContent);
if (plan.tasks.length > 0) taskIds = plan.tasks.map((t: { id: string }) => t.id);
}
@ -425,8 +425,8 @@ export function verifyExpectedArtifact(
const roadmapContent = readFileSync(roadmapFile, "utf-8");
const _require = createRequire(import.meta.url);
let parseRoadmap: Function;
try { parseRoadmap = _require("./files.ts").parseRoadmap; }
catch { parseRoadmap = _require("./files.js").parseRoadmap; }
try { parseRoadmap = _require("./parsers-legacy.ts").parseRoadmap; }
catch { parseRoadmap = _require("./parsers-legacy.js").parseRoadmap; }
const roadmap = parseRoadmap(roadmapContent);
const slice = roadmap.slices.find((s) => s.id === sid);
if (slice && !slice.done) return false;

View file

@ -44,7 +44,8 @@ export async function handleCleanupBranches(ctx: ExtensionCommandContext, basePa
try {
const { listWorktrees } = await import("./worktree-manager.js");
const { resolveMilestoneFile } = await import("./paths.js");
const { loadFile, parseRoadmap } = await import("./files.js");
const { loadFile } = await import("./files.js");
const { parseRoadmap } = await import("./parsers-legacy.js");
const { isMilestoneComplete } = await import("./state.js");
const attachedBranches = new Set(

View file

@ -10,8 +10,7 @@ import { resolveMilestoneFile, relMilestoneFile, resolveGsdRootFile } from './pa
import { milestoneIdSort, findMilestoneIds } from './milestone-ids.js';
import type {
Roadmap, BoundaryMapEntry,
SlicePlan, TaskPlanEntry, TaskPlanFile, TaskPlanFrontmatter,
TaskPlanFile, TaskPlanFrontmatter,
Summary, SummaryFrontmatter, SummaryRequires, FileModified,
Continue, ContinueFrontmatter, ContinueStatus,
RequirementCounts,
@ -21,9 +20,7 @@ import type {
} from './types.js';
import { checkExistingEnvKeys } from './env-utils.js';
import { parseRoadmapSlices } from './roadmap-slices.js';
import { nativeParseRoadmap, nativeExtractSection, nativeParsePlanFile, nativeParseSummaryFile, NATIVE_UNAVAILABLE } from './native-parser-bridge.js';
import { debugTime, debugCount } from './debug-logger.js';
import { nativeExtractSection, nativeParseSummaryFile, NATIVE_UNAVAILABLE } from './native-parser-bridge.js';
import { CACHE_MAX } from './constants.js';
import { splitFrontmatter, parseFrontmatterMap } from '../shared/frontmatter.js';
@ -55,9 +52,22 @@ function cachedParse<T>(content: string, tag: string, parseFn: (c: string) => T)
return result;
}
/** Clear the module-scoped parse cache. Call when files change on disk. */
// ─── Cross-module cache clear registry ────────────────────────────────────
// parsers-legacy.ts registers its cache-clear callback here at module init
// to avoid circular imports. clearParseCache() calls all registered callbacks.
const _cacheClearCallbacks: (() => void)[] = [];
/** Register a callback to be invoked when clearParseCache() is called.
* Used by parsers-legacy.ts to synchronously clear its own cache. */
export function registerCacheClearCallback(cb: () => void): void {
_cacheClearCallbacks.push(cb);
}
/** Clear the module-scoped parse cache. Call when files change on disk.
* Also clears any registered external caches (e.g. parsers-legacy.ts). */
export function clearParseCache(): void {
_parseCache.clear();
for (const cb of _cacheClearCallbacks) cb();
}
// ─── Helpers ───────────────────────────────────────────────────────────────
@ -117,95 +127,6 @@ export function extractBoldField(text: string, key: string): string | null {
return match ? match[1].trim() : null;
}
// ─── Roadmap Parser ────────────────────────────────────────────────────────
export function parseRoadmap(content: string): Roadmap {
return cachedParse(content, 'roadmap', _parseRoadmapImpl);
}
function _parseRoadmapImpl(content: string): Roadmap {
const stopTimer = debugTime("parse-roadmap");
// Try native parser first for better performance
const nativeResult = nativeParseRoadmap(content);
if (nativeResult) {
stopTimer({ native: true, slices: nativeResult.slices.length, boundaryEntries: nativeResult.boundaryMap.length });
debugCount("parseRoadmapCalls");
return nativeResult;
}
const lines = content.split('\n');
const h1 = lines.find(l => l.startsWith('# '));
const title = h1 ? h1.slice(2).trim() : '';
const vision = extractBoldField(content, 'Vision') || '';
const scSection = extractSection(content, 'Success Criteria', 2) ||
(() => {
const idx = content.indexOf('**Success Criteria:**');
if (idx === -1) return '';
const rest = content.slice(idx);
const nextSection = rest.indexOf('\n---');
const block = rest.slice(0, nextSection === -1 ? undefined : nextSection);
const firstNewline = block.indexOf('\n');
return firstNewline === -1 ? '' : block.slice(firstNewline + 1);
})();
const successCriteria = scSection ? parseBullets(scSection) : [];
// Slices
const slices = parseRoadmapSlices(content);
// Boundary map
const boundaryMap: BoundaryMapEntry[] = [];
const bmSection = extractSection(content, 'Boundary Map');
if (bmSection) {
const h3Sections = extractAllSections(bmSection, 3);
for (const [heading, sectionContent] of h3Sections) {
const arrowMatch = heading.match(/^(\S+)\s*→\s*(\S+)/);
if (!arrowMatch) continue;
const fromSlice = arrowMatch[1];
const toSlice = arrowMatch[2];
let produces = '';
let consumes = '';
// Use indexOf-based parsing instead of [\s\S]*? regex to avoid
// catastrophic backtracking on content with code fences (#468).
const prodIdx = sectionContent.search(/^Produces:\s*$/m);
if (prodIdx !== -1) {
const afterProd = sectionContent.indexOf('\n', prodIdx);
if (afterProd !== -1) {
const consIdx = sectionContent.search(/^Consumes/m);
const endIdx = consIdx !== -1 && consIdx > afterProd ? consIdx : sectionContent.length;
produces = sectionContent.slice(afterProd + 1, endIdx).trim();
}
}
const consLineMatch = sectionContent.match(/^Consumes[^:]*:\s*(.+)$/m);
if (consLineMatch) {
consumes = consLineMatch[1].trim();
}
if (!consumes) {
const consIdx = sectionContent.search(/^Consumes[^:]*:\s*$/m);
if (consIdx !== -1) {
const afterCons = sectionContent.indexOf('\n', consIdx);
if (afterCons !== -1) {
consumes = sectionContent.slice(afterCons + 1).trim();
}
}
}
boundaryMap.push({ fromSlice, toSlice, produces, consumes });
}
}
const result = { title, vision, successCriteria, slices, boundaryMap };
stopTimer({ native: false, slices: slices.length, boundaryEntries: boundaryMap.length });
debugCount("parseRoadmapCalls");
return result;
}
// ─── Secrets Manifest Parser ───────────────────────────────────────────────
const VALID_STATUSES = new Set<SecretsManifestEntryStatus>(['pending', 'collected', 'skipped']);
@ -314,131 +235,6 @@ export function parseTaskPlanFile(content: string): TaskPlanFile {
};
}
export function parsePlan(content: string): SlicePlan {
return cachedParse(content, 'plan', _parsePlanImpl);
}
function _parsePlanImpl(content: string): SlicePlan {
const stopTimer = debugTime("parse-plan");
const [, body] = splitFrontmatter(content);
// Try native parser first for better performance
const nativeResult = nativeParsePlanFile(body);
if (nativeResult) {
stopTimer({ native: true });
return {
id: nativeResult.id,
title: nativeResult.title,
goal: nativeResult.goal,
demo: nativeResult.demo,
mustHaves: nativeResult.mustHaves,
tasks: nativeResult.tasks.map(t => ({
id: t.id,
title: t.title,
description: t.description,
done: t.done,
estimate: t.estimate,
...(t.files.length > 0 ? { files: t.files } : {}),
...(t.verify ? { verify: t.verify } : {}),
})),
filesLikelyTouched: nativeResult.filesLikelyTouched,
};
}
const lines = body.split('\n');
const h1 = lines.find(l => l.startsWith('# '));
let id = '';
let title = '';
if (h1) {
const match = h1.match(/^#\s+(\w+):\s+(.+)/);
if (match) {
id = match[1];
title = match[2].trim();
} else {
title = h1.slice(2).trim();
}
}
const goal = extractBoldField(body, 'Goal') || '';
const demo = extractBoldField(body, 'Demo') || '';
const mhSection = extractSection(body, 'Must-Haves');
const mustHaves = mhSection ? parseBullets(mhSection) : [];
const tasksSection = extractSection(body, 'Tasks');
const tasks: TaskPlanEntry[] = [];
if (tasksSection) {
const taskLines = tasksSection.split('\n');
let currentTask: TaskPlanEntry | null = null;
for (const line of taskLines) {
const cbMatch = line.match(/^-\s+\[([ xX])\]\s+\*\*([\w.]+):\s+(.+?)\*\*\s*(.*)/);
// Heading-style: ### T01 -- Title, ### T01: Title, ### T01 — Title
const hdMatch = !cbMatch ? line.match(/^#{2,4}\s+([\w.]+)\s*(?:--|—|:)\s*(.+)/) : null;
if (cbMatch || hdMatch) {
if (currentTask) tasks.push(currentTask);
if (cbMatch) {
const rest = cbMatch[4] || '';
const estMatch = rest.match(/`est:([^`]+)`/);
const estimate = estMatch ? estMatch[1] : '';
currentTask = {
id: cbMatch[2],
title: cbMatch[3],
description: '',
done: cbMatch[1].toLowerCase() === 'x',
estimate,
};
} else {
const rest = hdMatch![2] || '';
const titleEstMatch = rest.match(/^(.+?)\s*`est:([^`]+)`\s*$/);
const title = titleEstMatch ? titleEstMatch[1].trim() : rest.trim();
const estimate = titleEstMatch ? titleEstMatch[2] : '';
currentTask = {
id: hdMatch![1],
title,
description: '',
done: false,
estimate,
};
}
} else if (currentTask && line.match(/^\s*-\s+Files:\s*(.*)/)) {
const filesMatch = line.match(/^\s*-\s+Files:\s*(.*)/);
if (filesMatch) {
currentTask.files = filesMatch[1]
.split(',')
.map(f => f.replace(/`/g, '').trim())
.filter(f => f.length > 0);
}
} else if (currentTask && line.match(/^\s*-\s+Verify:\s*(.*)/)) {
const verifyMatch = line.match(/^\s*-\s+Verify:\s*(.*)/);
if (verifyMatch) {
currentTask.verify = verifyMatch[1].trim();
}
} else if (currentTask && line.trim() && !line.startsWith('#')) {
const desc = line.trim();
if (desc) {
currentTask.description = currentTask.description
? currentTask.description + ' ' + desc
: desc;
}
}
}
if (currentTask) tasks.push(currentTask);
}
const filesSection = extractSection(body, 'Files Likely Touched');
const filesLikelyTouched = filesSection ? parseBullets(filesSection) : [];
const result = { id, title, goal, demo, mustHaves, tasks, filesLikelyTouched };
stopTimer({ tasks: tasks.length });
debugCount("parsePlanCalls");
return result;
}
// ─── Summary Parser ────────────────────────────────────────────────────────
export function parseSummary(content: string): Summary {

View file

@ -781,10 +781,10 @@ export function detectStaleRenders(basePath: string): StaleEntry[] {
const _require = createRequire(import.meta.url);
let parseRoadmap: Function, parsePlan: Function;
try {
const m = _require("./files.ts");
const m = _require("./parsers-legacy.ts");
parseRoadmap = m.parseRoadmap; parsePlan = m.parsePlan;
} catch {
const m = _require("./files.js");
const m = _require("./parsers-legacy.js");
parseRoadmap = m.parseRoadmap; parsePlan = m.parsePlan;
}

View file

@ -29,7 +29,8 @@ import {
resolveTaskFiles,
} from './paths.js';
import { findMilestoneIds } from './guided-flow.js';
import { parseRoadmap, parsePlan, parseContextDependsOn } from './files.js';
import { parseRoadmap, parsePlan } from './parsers-legacy.js';
import { parseContextDependsOn } from './files.js';
// ─── DECISIONS.md Parser ───────────────────────────────────────────────────

View file

@ -0,0 +1,271 @@
// GSD Extension - Legacy Parsers
// parseRoadmap() and parsePlan() extracted from files.ts.
// Used only by: md-importer.ts (migration), state.ts (pre-migration fallback),
// markdown-renderer.ts (detectStaleRenders disk-vs-DB comparison),
// commands-maintenance.ts (cold-path branch cleanup), and tests.
//
// NOT used in the dispatch loop or any hot-path runtime code.
import { extractSection, parseBullets, extractBoldField, extractAllSections, registerCacheClearCallback } from './files.js';
import { splitFrontmatter } from '../shared/frontmatter.js';
import { nativeParseRoadmap, nativeParsePlanFile } from './native-parser-bridge.js';
import { debugTime, debugCount } from './debug-logger.js';
import { CACHE_MAX } from './constants.js';
import type {
Roadmap, BoundaryMapEntry,
SlicePlan, TaskPlanEntry,
} from './types.js';
// Re-export parseRoadmapSlices so callers can import all legacy parsers from one module
import { parseRoadmapSlices } from './roadmap-slices.js';
export { parseRoadmapSlices };
// ─── Parse Cache (local to this module) ───────────────────────────────────
/** Fast composite key: length + first/mid/last 100 chars. The middle sample
* prevents collisions when only a few characters change in the interior of
* a file (e.g., a checkbox [ ] [x] that doesn't alter length or endpoints). */
function cacheKey(content: string): string {
const len = content.length;
const head = content.slice(0, 100);
const midStart = Math.max(0, Math.floor(len / 2) - 50);
const mid = len > 200 ? content.slice(midStart, midStart + 100) : '';
const tail = len > 100 ? content.slice(-100) : '';
return `${len}:${head}:${mid}:${tail}`;
}
const _parseCache = new Map<string, unknown>();
function cachedParse<T>(content: string, tag: string, parseFn: (c: string) => T): T {
const key = tag + '|' + cacheKey(content);
if (_parseCache.has(key)) return _parseCache.get(key) as T;
if (_parseCache.size >= CACHE_MAX) _parseCache.clear();
const result = parseFn(content);
_parseCache.set(key, result);
return result;
}
/** Clear the legacy parser cache. Called by clearParseCache() in files.ts. */
export function clearLegacyParseCache(): void {
_parseCache.clear();
}
// Register with files.ts so clearParseCache() also clears our cache
registerCacheClearCallback(clearLegacyParseCache);
// ─── Roadmap Parser ────────────────────────────────────────────────────────
export function parseRoadmap(content: string): Roadmap {
return cachedParse(content, 'roadmap', _parseRoadmapImpl);
}
function _parseRoadmapImpl(content: string): Roadmap {
const stopTimer = debugTime("parse-roadmap");
// Try native parser first for better performance
const nativeResult = nativeParseRoadmap(content);
if (nativeResult) {
stopTimer({ native: true, slices: nativeResult.slices.length, boundaryEntries: nativeResult.boundaryMap.length });
debugCount("parseRoadmapCalls");
return nativeResult;
}
const lines = content.split('\n');
const h1 = lines.find(l => l.startsWith('# '));
const title = h1 ? h1.slice(2).trim() : '';
const vision = extractBoldField(content, 'Vision') || '';
const scSection = extractSection(content, 'Success Criteria', 2) ||
(() => {
const idx = content.indexOf('**Success Criteria:**');
if (idx === -1) return '';
const rest = content.slice(idx);
const nextSection = rest.indexOf('\n---');
const block = rest.slice(0, nextSection === -1 ? undefined : nextSection);
const firstNewline = block.indexOf('\n');
return firstNewline === -1 ? '' : block.slice(firstNewline + 1);
})();
const successCriteria = scSection ? parseBullets(scSection) : [];
// Slices
const slices = parseRoadmapSlices(content);
// Boundary map
const boundaryMap: BoundaryMapEntry[] = [];
const bmSection = extractSection(content, 'Boundary Map');
if (bmSection) {
const h3Sections = extractAllSections(bmSection, 3);
for (const [heading, sectionContent] of h3Sections) {
const arrowMatch = heading.match(/^(\S+)\s*→\s*(\S+)/);
if (!arrowMatch) continue;
const fromSlice = arrowMatch[1];
const toSlice = arrowMatch[2];
let produces = '';
let consumes = '';
// Use indexOf-based parsing instead of [\s\S]*? regex to avoid
// catastrophic backtracking on content with code fences (#468).
const prodIdx = sectionContent.search(/^Produces:\s*$/m);
if (prodIdx !== -1) {
const afterProd = sectionContent.indexOf('\n', prodIdx);
if (afterProd !== -1) {
const consIdx = sectionContent.search(/^Consumes/m);
const endIdx = consIdx !== -1 && consIdx > afterProd ? consIdx : sectionContent.length;
produces = sectionContent.slice(afterProd + 1, endIdx).trim();
}
}
const consLineMatch = sectionContent.match(/^Consumes[^:]*:\s*(.+)$/m);
if (consLineMatch) {
consumes = consLineMatch[1].trim();
}
if (!consumes) {
const consIdx = sectionContent.search(/^Consumes[^:]*:\s*$/m);
if (consIdx !== -1) {
const afterCons = sectionContent.indexOf('\n', consIdx);
if (afterCons !== -1) {
consumes = sectionContent.slice(afterCons + 1).trim();
}
}
}
boundaryMap.push({ fromSlice, toSlice, produces, consumes });
}
}
const result = { title, vision, successCriteria, slices, boundaryMap };
stopTimer({ native: false, slices: slices.length, boundaryEntries: boundaryMap.length });
debugCount("parseRoadmapCalls");
return result;
}
// ─── Slice Plan Parser ─────────────────────────────────────────────────────
export function parsePlan(content: string): SlicePlan {
return cachedParse(content, 'plan', _parsePlanImpl);
}
function _parsePlanImpl(content: string): SlicePlan {
const stopTimer = debugTime("parse-plan");
const [, body] = splitFrontmatter(content);
// Try native parser first for better performance
const nativeResult = nativeParsePlanFile(body);
if (nativeResult) {
stopTimer({ native: true });
return {
id: nativeResult.id,
title: nativeResult.title,
goal: nativeResult.goal,
demo: nativeResult.demo,
mustHaves: nativeResult.mustHaves,
tasks: nativeResult.tasks.map(t => ({
id: t.id,
title: t.title,
description: t.description,
done: t.done,
estimate: t.estimate,
...(t.files.length > 0 ? { files: t.files } : {}),
...(t.verify ? { verify: t.verify } : {}),
})),
filesLikelyTouched: nativeResult.filesLikelyTouched,
};
}
const lines = body.split('\n');
const h1 = lines.find(l => l.startsWith('# '));
let id = '';
let title = '';
if (h1) {
const match = h1.match(/^#\s+(\w+):\s+(.+)/);
if (match) {
id = match[1];
title = match[2].trim();
} else {
title = h1.slice(2).trim();
}
}
const goal = extractBoldField(body, 'Goal') || '';
const demo = extractBoldField(body, 'Demo') || '';
const mhSection = extractSection(body, 'Must-Haves');
const mustHaves = mhSection ? parseBullets(mhSection) : [];
const tasksSection = extractSection(body, 'Tasks');
const tasks: TaskPlanEntry[] = [];
if (tasksSection) {
const taskLines = tasksSection.split('\n');
let currentTask: TaskPlanEntry | null = null;
for (const line of taskLines) {
const cbMatch = line.match(/^-\s+\[([ xX])\]\s+\*\*([\w.]+):\s+(.+?)\*\*\s*(.*)/);
// Heading-style: ### T01 -- Title, ### T01: Title, ### T01 — Title
const hdMatch = !cbMatch ? line.match(/^#{2,4}\s+([\w.]+)\s*(?:--|—|:)\s*(.+)/) : null;
if (cbMatch || hdMatch) {
if (currentTask) tasks.push(currentTask);
if (cbMatch) {
const rest = cbMatch[4] || '';
const estMatch = rest.match(/`est:([^`]+)`/);
const estimate = estMatch ? estMatch[1] : '';
currentTask = {
id: cbMatch[2],
title: cbMatch[3],
description: '',
done: cbMatch[1].toLowerCase() === 'x',
estimate,
};
} else {
const rest = hdMatch![2] || '';
const titleEstMatch = rest.match(/^(.+?)\s*`est:([^`]+)`\s*$/);
const title = titleEstMatch ? titleEstMatch[1].trim() : rest.trim();
const estimate = titleEstMatch ? titleEstMatch[2] : '';
currentTask = {
id: hdMatch![1],
title,
description: '',
done: false,
estimate,
};
}
} else if (currentTask && line.match(/^\s*-\s+Files:\s*(.*)/)) {
const filesMatch = line.match(/^\s*-\s+Files:\s*(.*)/);
if (filesMatch) {
currentTask.files = filesMatch[1]
.split(',')
.map(f => f.replace(/`/g, '').trim())
.filter(f => f.length > 0);
}
} else if (currentTask && line.match(/^\s*-\s+Verify:\s*(.*)/)) {
const verifyMatch = line.match(/^\s*-\s+Verify:\s*(.*)/);
if (verifyMatch) {
currentTask.verify = verifyMatch[1].trim();
}
} else if (currentTask && line.trim() && !line.startsWith('#')) {
const desc = line.trim();
if (desc) {
currentTask.description = currentTask.description
? currentTask.description + ' ' + desc
: desc;
}
}
}
if (currentTask) tasks.push(currentTask);
}
const filesSection = extractSection(body, 'Files Likely Touched');
const filesLikelyTouched = filesSection ? parseBullets(filesSection) : [];
const result = { id, title, goal, demo, mustHaves, tasks, filesLikelyTouched };
stopTimer({ tasks: tasks.length });
debugCount("parsePlanCalls");
return result;
}

View file

@ -14,6 +14,9 @@ import type {
import {
parseRoadmap,
parsePlan,
} from './parsers-legacy.js';
import {
parseSummary,
loadFile,
parseRequirementCounts,

View file

@ -13,7 +13,8 @@ import {
selfHealRuntimeRecords,
hasImplementationArtifacts,
} from "../auto-recovery.ts";
import { parseRoadmap, parsePlan, parseTaskPlanFile, clearParseCache } from "../files.ts";
import { parseRoadmap, parsePlan } from "../parsers-legacy.ts";
import { parseTaskPlanFile, clearParseCache } from "../files.ts";
import { invalidateAllCaches } from "../cache.ts";
import { deriveState, invalidateStateCache } from "../state.ts";
import {

View file

@ -158,7 +158,7 @@ async function main(): Promise<void> {
{
const { deriveState, isMilestoneComplete } = await import("../state.ts");
const { invalidateAllCaches: invalidateAllCachesDynamic } = await import("../cache.ts");
const { parseRoadmap } = await import("../files.ts");
const { parseRoadmap } = await import("../parsers-legacy.ts");
const base = createFixtureBase();
try {

View file

@ -30,6 +30,8 @@ import {
import {
parseRoadmap,
parsePlan,
} from '../parsers-legacy.ts';
import {
parseSummary,
parseTaskPlanFile,
clearParseCache,

View file

@ -9,7 +9,8 @@ import { tmpdir } from 'node:os';
import { writeGSDDirectory } from '../migrate/writer.ts';
import { generatePreview } from '../migrate/preview.ts';
import { parseRoadmap, parsePlan, parseSummary } from '../files.ts';
import { parseRoadmap, parsePlan } from '../parsers-legacy.ts';
import { parseSummary } from '../files.ts';
import { deriveState } from '../state.ts';
import { invalidateAllCaches } from '../cache.ts';
import type {

View file

@ -18,6 +18,8 @@ import {
import {
parseRoadmap,
parsePlan,
} from '../parsers-legacy.ts';
import {
parseSummary,
parseRequirementCounts,
} from '../files.ts';

View file

@ -1,4 +1,5 @@
import { parseRoadmap, parsePlan, parseTaskPlanFile, parseSummary, parseContinue, parseRequirementCounts, parseSecretsManifest, formatSecretsManifest } from '../files.ts';
import { parseRoadmap, parsePlan } from '../parsers-legacy.ts';
import { parseTaskPlanFile, parseSummary, parseContinue, parseRequirementCounts, parseSecretsManifest, formatSecretsManifest } from '../files.ts';
import { createTestContext } from './test-helpers.ts';
const { assertEq, assertTrue, report } = createTestContext();

View file

@ -21,7 +21,7 @@ import {
renderPlanFromDb,
} from '../markdown-renderer.ts';
import { parseRoadmapSlices } from '../roadmap-slices.ts';
import { parsePlan } from '../files.ts';
import { parsePlan } from '../parsers-legacy.ts';
import { createTestContext } from './test-helpers.ts';
const { assertEq, assertTrue, report } = createTestContext();

View file

@ -1,6 +1,6 @@
import test from "node:test";
import assert from "node:assert/strict";
import { parseRoadmap } from "../files.ts";
import { parseRoadmap } from "../parsers-legacy.ts";
import { parseRoadmapSlices, expandDependencies } from "../roadmap-slices.ts";
const content = `# M003: Current