@tuan_son.dinh/gsd 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +453 -0
- package/dist/app-paths.d.ts +4 -0
- package/dist/app-paths.js +6 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +269 -0
- package/dist/loader.d.ts +2 -0
- package/dist/loader.js +70 -0
- package/dist/logo.d.ts +16 -0
- package/dist/logo.js +25 -0
- package/dist/onboarding.d.ts +43 -0
- package/dist/onboarding.js +418 -0
- package/dist/pi-migration.d.ts +14 -0
- package/dist/pi-migration.js +57 -0
- package/dist/resource-loader.d.ts +22 -0
- package/dist/resource-loader.js +60 -0
- package/dist/tool-bootstrap.d.ts +4 -0
- package/dist/tool-bootstrap.js +74 -0
- package/dist/wizard.d.ts +7 -0
- package/dist/wizard.js +25 -0
- package/package.json +60 -0
- package/patches/@mariozechner+pi-coding-agent+0.57.1.patch +108 -0
- package/patches/@mariozechner+pi-tui+0.57.1.patch +47 -0
- package/pkg/dist/modes/interactive/theme/dark.json +85 -0
- package/pkg/dist/modes/interactive/theme/light.json +84 -0
- package/pkg/dist/modes/interactive/theme/theme-schema.json +335 -0
- package/pkg/dist/modes/interactive/theme/theme.d.ts +78 -0
- package/pkg/dist/modes/interactive/theme/theme.d.ts.map +1 -0
- package/pkg/dist/modes/interactive/theme/theme.js +949 -0
- package/pkg/dist/modes/interactive/theme/theme.js.map +1 -0
- package/pkg/package.json +8 -0
- package/scripts/postinstall.js +127 -0
- package/src/resources/GSD-WORKFLOW.md +661 -0
- package/src/resources/agents/researcher.md +29 -0
- package/src/resources/agents/scout.md +56 -0
- package/src/resources/agents/worker.md +31 -0
- package/src/resources/extensions/ask-user-questions.ts +249 -0
- package/src/resources/extensions/bg-shell/index.ts +2808 -0
- package/src/resources/extensions/browser-tools/BROWSER-TOOLS-V2-PROPOSAL.md +1277 -0
- package/src/resources/extensions/browser-tools/core.js +1057 -0
- package/src/resources/extensions/browser-tools/index.ts +4989 -0
- package/src/resources/extensions/browser-tools/package.json +20 -0
- package/src/resources/extensions/context7/index.ts +428 -0
- package/src/resources/extensions/context7/package.json +11 -0
- package/src/resources/extensions/get-secrets-from-user.ts +352 -0
- package/src/resources/extensions/google-search/index.ts +323 -0
- package/src/resources/extensions/google-search/package.json +9 -0
- package/src/resources/extensions/gsd/activity-log.ts +69 -0
- package/src/resources/extensions/gsd/auto.ts +2744 -0
- package/src/resources/extensions/gsd/commands.ts +313 -0
- package/src/resources/extensions/gsd/crash-recovery.ts +85 -0
- package/src/resources/extensions/gsd/dashboard-overlay.ts +521 -0
- package/src/resources/extensions/gsd/docs/preferences-reference.md +176 -0
- package/src/resources/extensions/gsd/doctor.ts +690 -0
- package/src/resources/extensions/gsd/files.ts +732 -0
- package/src/resources/extensions/gsd/git-service.ts +597 -0
- package/src/resources/extensions/gsd/gitignore.ts +168 -0
- package/src/resources/extensions/gsd/guided-flow.ts +817 -0
- package/src/resources/extensions/gsd/index.ts +558 -0
- package/src/resources/extensions/gsd/metrics.ts +374 -0
- package/src/resources/extensions/gsd/migrate/command.ts +218 -0
- package/src/resources/extensions/gsd/migrate/index.ts +42 -0
- package/src/resources/extensions/gsd/migrate/parser.ts +323 -0
- package/src/resources/extensions/gsd/migrate/parsers.ts +624 -0
- package/src/resources/extensions/gsd/migrate/preview.ts +48 -0
- package/src/resources/extensions/gsd/migrate/transformer.ts +346 -0
- package/src/resources/extensions/gsd/migrate/types.ts +370 -0
- package/src/resources/extensions/gsd/migrate/validator.ts +55 -0
- package/src/resources/extensions/gsd/migrate/writer.ts +539 -0
- package/src/resources/extensions/gsd/observability-validator.ts +408 -0
- package/src/resources/extensions/gsd/package.json +11 -0
- package/src/resources/extensions/gsd/paths.ts +308 -0
- package/src/resources/extensions/gsd/preferences.ts +757 -0
- package/src/resources/extensions/gsd/prompt-loader.ts +50 -0
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +25 -0
- package/src/resources/extensions/gsd/prompts/complete-slice.md +29 -0
- package/src/resources/extensions/gsd/prompts/discuss.md +189 -0
- package/src/resources/extensions/gsd/prompts/doctor-heal.md +29 -0
- package/src/resources/extensions/gsd/prompts/execute-task.md +61 -0
- package/src/resources/extensions/gsd/prompts/guided-complete-slice.md +1 -0
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +3 -0
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +59 -0
- package/src/resources/extensions/gsd/prompts/guided-execute-task.md +1 -0
- package/src/resources/extensions/gsd/prompts/guided-plan-milestone.md +23 -0
- package/src/resources/extensions/gsd/prompts/guided-plan-slice.md +1 -0
- package/src/resources/extensions/gsd/prompts/guided-research-slice.md +11 -0
- package/src/resources/extensions/gsd/prompts/guided-resume-task.md +1 -0
- package/src/resources/extensions/gsd/prompts/plan-milestone.md +65 -0
- package/src/resources/extensions/gsd/prompts/plan-slice.md +51 -0
- package/src/resources/extensions/gsd/prompts/queue.md +85 -0
- package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +48 -0
- package/src/resources/extensions/gsd/prompts/replan-slice.md +39 -0
- package/src/resources/extensions/gsd/prompts/research-milestone.md +37 -0
- package/src/resources/extensions/gsd/prompts/research-slice.md +28 -0
- package/src/resources/extensions/gsd/prompts/review-migration.md +66 -0
- package/src/resources/extensions/gsd/prompts/run-uat.md +109 -0
- package/src/resources/extensions/gsd/prompts/system.md +187 -0
- package/src/resources/extensions/gsd/prompts/worktree-merge.md +123 -0
- package/src/resources/extensions/gsd/session-forensics.ts +487 -0
- package/src/resources/extensions/gsd/skill-discovery.ts +137 -0
- package/src/resources/extensions/gsd/state.ts +460 -0
- package/src/resources/extensions/gsd/templates/context.md +76 -0
- package/src/resources/extensions/gsd/templates/decisions.md +8 -0
- package/src/resources/extensions/gsd/templates/milestone-summary.md +73 -0
- package/src/resources/extensions/gsd/templates/plan.md +131 -0
- package/src/resources/extensions/gsd/templates/preferences.md +24 -0
- package/src/resources/extensions/gsd/templates/project.md +31 -0
- package/src/resources/extensions/gsd/templates/reassessment.md +28 -0
- package/src/resources/extensions/gsd/templates/requirements.md +81 -0
- package/src/resources/extensions/gsd/templates/research.md +46 -0
- package/src/resources/extensions/gsd/templates/roadmap.md +118 -0
- package/src/resources/extensions/gsd/templates/slice-context.md +58 -0
- package/src/resources/extensions/gsd/templates/slice-summary.md +99 -0
- package/src/resources/extensions/gsd/templates/state.md +19 -0
- package/src/resources/extensions/gsd/templates/task-plan.md +52 -0
- package/src/resources/extensions/gsd/templates/task-summary.md +57 -0
- package/src/resources/extensions/gsd/templates/uat.md +54 -0
- package/src/resources/extensions/gsd/tests/activity-log-prune.test.ts +327 -0
- package/src/resources/extensions/gsd/tests/auto-preflight.test.ts +56 -0
- package/src/resources/extensions/gsd/tests/auto-supervisor.test.mjs +53 -0
- package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +225 -0
- package/src/resources/extensions/gsd/tests/cost-projection.test.ts +160 -0
- package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +341 -0
- package/src/resources/extensions/gsd/tests/derive-state.test.ts +689 -0
- package/src/resources/extensions/gsd/tests/discuss-prompt.test.ts +38 -0
- package/src/resources/extensions/gsd/tests/doctor.test.ts +505 -0
- package/src/resources/extensions/gsd/tests/git-service.test.ts +1313 -0
- package/src/resources/extensions/gsd/tests/idle-recovery.test.ts +308 -0
- package/src/resources/extensions/gsd/tests/metrics-io.test.ts +201 -0
- package/src/resources/extensions/gsd/tests/metrics.test.ts +217 -0
- package/src/resources/extensions/gsd/tests/migrate-command.test.ts +390 -0
- package/src/resources/extensions/gsd/tests/migrate-parser.test.ts +786 -0
- package/src/resources/extensions/gsd/tests/migrate-transformer.test.ts +657 -0
- package/src/resources/extensions/gsd/tests/migrate-validator-parsers.test.ts +443 -0
- package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +318 -0
- package/src/resources/extensions/gsd/tests/migrate-writer.test.ts +420 -0
- package/src/resources/extensions/gsd/tests/must-have-parser.test.ts +309 -0
- package/src/resources/extensions/gsd/tests/parsers.test.ts +1351 -0
- package/src/resources/extensions/gsd/tests/plan-milestone.test.ts +163 -0
- package/src/resources/extensions/gsd/tests/plan-quality-validator.test.ts +386 -0
- package/src/resources/extensions/gsd/tests/reassess-prompt.test.ts +171 -0
- package/src/resources/extensions/gsd/tests/remote-questions.test.ts +155 -0
- package/src/resources/extensions/gsd/tests/remote-status.test.ts +99 -0
- package/src/resources/extensions/gsd/tests/replan-slice.test.ts +521 -0
- package/src/resources/extensions/gsd/tests/requirements.test.ts +125 -0
- package/src/resources/extensions/gsd/tests/resolve-ts-hooks.mjs +34 -0
- package/src/resources/extensions/gsd/tests/resolve-ts.mjs +11 -0
- package/src/resources/extensions/gsd/tests/run-uat.test.ts +348 -0
- package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +247 -0
- package/src/resources/extensions/gsd/tests/workflow-config.test.mjs +53 -0
- package/src/resources/extensions/gsd/tests/workspace-index.test.ts +94 -0
- package/src/resources/extensions/gsd/tests/worktree-integration.test.ts +253 -0
- package/src/resources/extensions/gsd/tests/worktree-manager.test.ts +160 -0
- package/src/resources/extensions/gsd/tests/worktree.test.ts +264 -0
- package/src/resources/extensions/gsd/types.ts +159 -0
- package/src/resources/extensions/gsd/unit-runtime.ts +184 -0
- package/src/resources/extensions/gsd/workspace-index.ts +203 -0
- package/src/resources/extensions/gsd/worktree-command.ts +845 -0
- package/src/resources/extensions/gsd/worktree-manager.ts +392 -0
- package/src/resources/extensions/gsd/worktree.ts +183 -0
- package/src/resources/extensions/mac-tools/index.ts +852 -0
- package/src/resources/extensions/mac-tools/swift-cli/Package.swift +22 -0
- package/src/resources/extensions/mac-tools/swift-cli/Sources/main.swift +1318 -0
- package/src/resources/extensions/mcporter/index.ts +429 -0
- package/src/resources/extensions/remote-questions/config.ts +81 -0
- package/src/resources/extensions/remote-questions/discord-adapter.ts +128 -0
- package/src/resources/extensions/remote-questions/format.ts +163 -0
- package/src/resources/extensions/remote-questions/manager.ts +192 -0
- package/src/resources/extensions/remote-questions/remote-command.ts +307 -0
- package/src/resources/extensions/remote-questions/slack-adapter.ts +92 -0
- package/src/resources/extensions/remote-questions/status.ts +31 -0
- package/src/resources/extensions/remote-questions/store.ts +77 -0
- package/src/resources/extensions/remote-questions/types.ts +75 -0
- package/src/resources/extensions/search-the-web/cache.ts +78 -0
- package/src/resources/extensions/search-the-web/command-search-provider.ts +95 -0
- package/src/resources/extensions/search-the-web/format.ts +258 -0
- package/src/resources/extensions/search-the-web/http.ts +238 -0
- package/src/resources/extensions/search-the-web/index.ts +65 -0
- package/src/resources/extensions/search-the-web/native-search.ts +157 -0
- package/src/resources/extensions/search-the-web/provider.ts +118 -0
- package/src/resources/extensions/search-the-web/tavily.ts +116 -0
- package/src/resources/extensions/search-the-web/tool-fetch-page.ts +519 -0
- package/src/resources/extensions/search-the-web/tool-llm-context.ts +561 -0
- package/src/resources/extensions/search-the-web/tool-search.ts +576 -0
- package/src/resources/extensions/search-the-web/url-utils.ts +91 -0
- package/src/resources/extensions/shared/confirm-ui.ts +126 -0
- package/src/resources/extensions/shared/interview-ui.ts +613 -0
- package/src/resources/extensions/shared/next-action-ui.ts +197 -0
- package/src/resources/extensions/shared/progress-widget.ts +282 -0
- package/src/resources/extensions/shared/terminal.ts +23 -0
- package/src/resources/extensions/shared/thinking-widget.ts +107 -0
- package/src/resources/extensions/shared/ui.ts +400 -0
- package/src/resources/extensions/shared/wizard-ui.ts +551 -0
- package/src/resources/extensions/slash-commands/audit.ts +88 -0
- package/src/resources/extensions/slash-commands/clear.ts +10 -0
- package/src/resources/extensions/slash-commands/create-extension.ts +297 -0
- package/src/resources/extensions/slash-commands/create-slash-command.ts +234 -0
- package/src/resources/extensions/slash-commands/index.ts +12 -0
- package/src/resources/extensions/subagent/agents.ts +126 -0
- package/src/resources/extensions/subagent/index.ts +1020 -0
- package/src/resources/extensions/voice/index.ts +195 -0
- package/src/resources/extensions/voice/speech-recognizer.swift +154 -0
- package/src/resources/skills/debug-like-expert/SKILL.md +231 -0
- package/src/resources/skills/debug-like-expert/references/debugging-mindset.md +253 -0
- package/src/resources/skills/debug-like-expert/references/hypothesis-testing.md +373 -0
- package/src/resources/skills/debug-like-expert/references/investigation-techniques.md +337 -0
- package/src/resources/skills/debug-like-expert/references/verification-patterns.md +425 -0
- package/src/resources/skills/debug-like-expert/references/when-to-research.md +361 -0
- package/src/resources/skills/frontend-design/SKILL.md +45 -0
- package/src/resources/skills/swiftui/SKILL.md +208 -0
- package/src/resources/skills/swiftui/references/animations.md +921 -0
- package/src/resources/skills/swiftui/references/architecture.md +1561 -0
- package/src/resources/skills/swiftui/references/layout-system.md +1186 -0
- package/src/resources/skills/swiftui/references/navigation.md +1492 -0
- package/src/resources/skills/swiftui/references/networking-async.md +214 -0
- package/src/resources/skills/swiftui/references/performance.md +1706 -0
- package/src/resources/skills/swiftui/references/platform-integration.md +204 -0
- package/src/resources/skills/swiftui/references/state-management.md +1443 -0
- package/src/resources/skills/swiftui/references/swiftdata.md +297 -0
- package/src/resources/skills/swiftui/references/testing-debugging.md +247 -0
- package/src/resources/skills/swiftui/references/uikit-appkit-interop.md +218 -0
- package/src/resources/skills/swiftui/workflows/add-feature.md +191 -0
- package/src/resources/skills/swiftui/workflows/build-new-app.md +311 -0
- package/src/resources/skills/swiftui/workflows/debug-swiftui.md +192 -0
- package/src/resources/skills/swiftui/workflows/optimize-performance.md +197 -0
- package/src/resources/skills/swiftui/workflows/ship-app.md +203 -0
- package/src/resources/skills/swiftui/workflows/write-tests.md +235 -0
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
// Old .planning format per-file parsers
|
|
2
|
+
// Pure functions that take file content (string) and return typed data.
|
|
3
|
+
// Zero Pi dependencies — uses only exported helpers from files.ts.
|
|
4
|
+
|
|
5
|
+
import { splitFrontmatter, parseFrontmatterMap, extractBoldField } from '../files.ts';
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
PlanningRoadmap,
|
|
9
|
+
PlanningRoadmapMilestone,
|
|
10
|
+
PlanningRoadmapEntry,
|
|
11
|
+
PlanningPlan,
|
|
12
|
+
PlanningPlanFrontmatter,
|
|
13
|
+
PlanningPlanMustHaves,
|
|
14
|
+
PlanningSummary,
|
|
15
|
+
PlanningSummaryFrontmatter,
|
|
16
|
+
PlanningSummaryRequires,
|
|
17
|
+
PlanningRequirement,
|
|
18
|
+
PlanningState,
|
|
19
|
+
PlanningConfig,
|
|
20
|
+
} from './types.ts';
|
|
21
|
+
|
|
22
|
+
// Re-export PlanningProjectMeta — not in types.ts yet, use string for project field
|
|
23
|
+
// Actually PlanningProjectMeta isn't in types.ts — project is stored as string | null.
|
|
24
|
+
// We'll keep parseOldProject returning a simple shape.
|
|
25
|
+
|
|
26
|
+
// ─── XML-in-Markdown Extraction ────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Extract content between XML-like tags in markdown.
|
|
30
|
+
* NOT a real XML parser — handles `<tag>content</tag>` with markdown inside.
|
|
31
|
+
*/
|
|
32
|
+
function extractXmlTag(content: string, tagName: string): string {
|
|
33
|
+
const regex = new RegExp(`<${tagName}>([\\s\\S]*?)<\\/${tagName}>`, 'i');
|
|
34
|
+
const match = regex.exec(content);
|
|
35
|
+
return match ? match[1].trim() : '';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Extract all nested `<task>` entries from within a `<tasks>` block.
|
|
40
|
+
*/
|
|
41
|
+
function extractTasks(content: string): string[] {
|
|
42
|
+
const tasksBlock = extractXmlTag(content, 'tasks');
|
|
43
|
+
if (!tasksBlock) return [];
|
|
44
|
+
|
|
45
|
+
const tasks: string[] = [];
|
|
46
|
+
const regex = /<task>([\s\S]*?)<\/task>/gi;
|
|
47
|
+
let match: RegExpExecArray | null;
|
|
48
|
+
while ((match = regex.exec(tasksBlock)) !== null) {
|
|
49
|
+
const trimmed = match[1].trim();
|
|
50
|
+
if (trimmed) tasks.push(trimmed);
|
|
51
|
+
}
|
|
52
|
+
return tasks;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ─── Roadmap Parser ────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
/** Parse a checkbox phase entry line: `- [x] 29 — Auth System` */
|
|
58
|
+
function parsePhaseEntry(line: string): PlanningRoadmapEntry | null {
|
|
59
|
+
// Strip bold markers (**) for uniform matching — old roadmaps often bold phase entries
|
|
60
|
+
const stripped = line.replace(/\*\*/g, '');
|
|
61
|
+
|
|
62
|
+
// Format 1: - [x] Phase 25: Title (N/N plans) -- completed ...
|
|
63
|
+
// Also handles: - [x] Phase 25: Title - Description (completed ...)
|
|
64
|
+
const fmtPhaseColon = stripped.match(/^-\s+\[([ xX])\]\s+(?:Phase\s+)?(\d+(?:\.\d+)?)\s*:\s*(.+)$/);
|
|
65
|
+
if (fmtPhaseColon) {
|
|
66
|
+
let title = fmtPhaseColon[3].trim();
|
|
67
|
+
// Strip trailing parentheticals, plan counts, and completion notes
|
|
68
|
+
title = title.replace(/\s*\(\d+\/\d+\s+plans?\)/, '')
|
|
69
|
+
.replace(/\s*--\s+.*$/, '')
|
|
70
|
+
.replace(/\s*-\s+.*$/, '') // strip "- description" suffix
|
|
71
|
+
.replace(/\s*\(completed.*\)$/i, '')
|
|
72
|
+
.replace(/\s*\(shipped.*\)$/i, '')
|
|
73
|
+
.trim();
|
|
74
|
+
return {
|
|
75
|
+
number: parseFloat(fmtPhaseColon[2]),
|
|
76
|
+
title,
|
|
77
|
+
done: fmtPhaseColon[1].toLowerCase() === 'x',
|
|
78
|
+
raw: line,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Format 2: - [x] 25 — Title (em-dash/en-dash only — NOT plain hyphen to avoid plan file refs)
|
|
83
|
+
const fmtDash = stripped.match(/^-\s+\[([ xX])\]\s+(?:Phase\s+)?(\d+(?:\.\d+)?)\s*[—–]\s*(.+)$/);
|
|
84
|
+
if (fmtDash) {
|
|
85
|
+
let title = fmtDash[3].trim();
|
|
86
|
+
title = title.replace(/\s*\(\d+\/\d+\s+plans?\)/, '')
|
|
87
|
+
.replace(/\s*--\s+.*$/, '')
|
|
88
|
+
.trim();
|
|
89
|
+
return {
|
|
90
|
+
number: parseFloat(fmtDash[2]),
|
|
91
|
+
title,
|
|
92
|
+
done: fmtDash[1].toLowerCase() === 'x',
|
|
93
|
+
raw: line,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Parse old-format ROADMAP.md.
|
|
102
|
+
* Handles two formats:
|
|
103
|
+
* 1. Flat phase lists — checkbox lines under a single Phases heading
|
|
104
|
+
* 2. Milestone-sectioned — `## v2.0 — Title` headings with optional `<details>` blocks
|
|
105
|
+
* 3. Details-sectioned — `<details><summary>v1.0 Title (Phases N-M)</summary>` blocks with phase checkboxes inside
|
|
106
|
+
*/
|
|
107
|
+
export function parseOldRoadmap(content: string): PlanningRoadmap {
|
|
108
|
+
const result: PlanningRoadmap = {
|
|
109
|
+
raw: content,
|
|
110
|
+
milestones: [],
|
|
111
|
+
phases: [],
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const lines = content.split('\n');
|
|
115
|
+
|
|
116
|
+
// ─── Strategy 1: Detect <details><summary>vN.N Title</summary> blocks ───
|
|
117
|
+
// This handles the format where milestones are <details> blocks containing phase checkboxes
|
|
118
|
+
const detailsMilestones = parseDetailsBlockMilestones(lines);
|
|
119
|
+
if (detailsMilestones.length > 0) {
|
|
120
|
+
result.milestones = detailsMilestones;
|
|
121
|
+
|
|
122
|
+
// Also check for non-collapsed milestone sections (### v3.0 Title)
|
|
123
|
+
// that follow the <details> blocks
|
|
124
|
+
for (let i = 0; i < lines.length; i++) {
|
|
125
|
+
const heading = lines[i].match(/^###\s+(v[\d.]+)\s+(.+?)(?:\s*\(.*\))?\s*$/);
|
|
126
|
+
if (heading) {
|
|
127
|
+
// Already captured as a details block?
|
|
128
|
+
const id = heading[1];
|
|
129
|
+
if (result.milestones.some(m => m.id === id)) continue;
|
|
130
|
+
|
|
131
|
+
// Collect phase entries until next ## or ### heading
|
|
132
|
+
const phases: PlanningRoadmapEntry[] = [];
|
|
133
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
134
|
+
if (/^##?\s/.test(lines[j]) || /^###\s/.test(lines[j])) break;
|
|
135
|
+
const entry = parsePhaseEntry(lines[j].trim());
|
|
136
|
+
if (entry) phases.push(entry);
|
|
137
|
+
}
|
|
138
|
+
result.milestones.push({
|
|
139
|
+
id,
|
|
140
|
+
title: heading[2].trim(),
|
|
141
|
+
collapsed: false,
|
|
142
|
+
phases,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ─── Strategy 2: Detect ## heading-sectioned milestones ───
|
|
150
|
+
const milestoneHeadingRegex = /^##\s+(.+)$/;
|
|
151
|
+
const milestoneHeadings: { index: number; id: string; title: string }[] = [];
|
|
152
|
+
|
|
153
|
+
for (let i = 0; i < lines.length; i++) {
|
|
154
|
+
const match = lines[i].match(milestoneHeadingRegex);
|
|
155
|
+
if (match) {
|
|
156
|
+
const heading = match[1].trim();
|
|
157
|
+
// Skip generic headings like "## Phases", "## Milestones", "## Phase Details", "## Progress"
|
|
158
|
+
if (/^(phases?|milestones?|phase\s+details?|progress)$/i.test(heading)) continue;
|
|
159
|
+
// Extract milestone ID (e.g. "v2.0" from "v2.0 — Foundation")
|
|
160
|
+
const idMatch = heading.match(/^(v[\d.]+|[\w.-]+)\s*[—–-]\s*(.+)$/);
|
|
161
|
+
if (idMatch) {
|
|
162
|
+
milestoneHeadings.push({ index: i, id: idMatch[1], title: idMatch[2].trim() });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (milestoneHeadings.length > 0) {
|
|
168
|
+
// Milestone-sectioned format
|
|
169
|
+
for (let m = 0; m < milestoneHeadings.length; m++) {
|
|
170
|
+
const startIdx = milestoneHeadings[m].index + 1;
|
|
171
|
+
const endIdx = m + 1 < milestoneHeadings.length ? milestoneHeadings[m + 1].index : lines.length;
|
|
172
|
+
const sectionLines = lines.slice(startIdx, endIdx);
|
|
173
|
+
|
|
174
|
+
const milestone: PlanningRoadmapMilestone = {
|
|
175
|
+
id: milestoneHeadings[m].id,
|
|
176
|
+
title: milestoneHeadings[m].title,
|
|
177
|
+
collapsed: false,
|
|
178
|
+
phases: [],
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// Check for <details> block
|
|
182
|
+
const sectionText = sectionLines.join('\n');
|
|
183
|
+
if (sectionText.includes('<details>')) {
|
|
184
|
+
milestone.collapsed = true;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Extract phase entries from the section (including inside <details>)
|
|
188
|
+
for (const line of sectionLines) {
|
|
189
|
+
const entry = parsePhaseEntry(line.trim());
|
|
190
|
+
if (entry) {
|
|
191
|
+
milestone.phases.push(entry);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
result.milestones.push(milestone);
|
|
196
|
+
}
|
|
197
|
+
} else {
|
|
198
|
+
// ─── Strategy 3: Flat format — just extract all phase checkbox lines ───
|
|
199
|
+
for (const line of lines) {
|
|
200
|
+
const entry = parsePhaseEntry(line.trim());
|
|
201
|
+
if (entry) {
|
|
202
|
+
result.phases.push(entry);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return result;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Parse <details><summary>vN.N Title (Phases N-M)</summary>...</details> blocks.
|
|
212
|
+
* Each block becomes a milestone with the phase entries inside it.
|
|
213
|
+
*/
|
|
214
|
+
function parseDetailsBlockMilestones(lines: string[]): PlanningRoadmapMilestone[] {
|
|
215
|
+
const milestones: PlanningRoadmapMilestone[] = [];
|
|
216
|
+
let inDetails = false;
|
|
217
|
+
let currentMilestone: PlanningRoadmapMilestone | null = null;
|
|
218
|
+
|
|
219
|
+
for (const line of lines) {
|
|
220
|
+
const trimmed = line.trim();
|
|
221
|
+
|
|
222
|
+
if (trimmed === '<details>') {
|
|
223
|
+
inDetails = true;
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (inDetails && !currentMilestone) {
|
|
228
|
+
// Look for <summary>vN.N Title (Phases N-M) -- STATUS</summary>
|
|
229
|
+
const summaryMatch = trimmed.match(/<summary>\s*(v[\d.]+)\s+(.+?)\s*(?:\(.*\))?\s*(?:--\s*.*)?\s*<\/summary>/);
|
|
230
|
+
if (summaryMatch) {
|
|
231
|
+
currentMilestone = {
|
|
232
|
+
id: summaryMatch[1],
|
|
233
|
+
title: summaryMatch[2].trim(),
|
|
234
|
+
collapsed: true,
|
|
235
|
+
phases: [],
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (trimmed === '</details>') {
|
|
242
|
+
if (currentMilestone) {
|
|
243
|
+
milestones.push(currentMilestone);
|
|
244
|
+
currentMilestone = null;
|
|
245
|
+
}
|
|
246
|
+
inDetails = false;
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (currentMilestone) {
|
|
251
|
+
const entry = parsePhaseEntry(trimmed);
|
|
252
|
+
if (entry) {
|
|
253
|
+
currentMilestone.phases.push(entry);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return milestones;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ─── Plan Parser (XML-in-Markdown) ─────────────────────────────────────────
|
|
262
|
+
|
|
263
|
+
/** Strip surrounding quotes from YAML string values */
|
|
264
|
+
function unquote(val: unknown): string {
|
|
265
|
+
const s = String(val ?? '');
|
|
266
|
+
if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
|
|
267
|
+
return s.slice(1, -1);
|
|
268
|
+
}
|
|
269
|
+
return s;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Parse the must_haves nested structure from frontmatter lines directly.
|
|
274
|
+
* parseFrontmatterMap doesn't handle 3-level nesting well, so we re-parse.
|
|
275
|
+
*/
|
|
276
|
+
function parseMustHavesFromLines(fmLines: string[]): PlanningPlanMustHaves | null {
|
|
277
|
+
const start = fmLines.findIndex(l => /^must_haves\s*:/.test(l));
|
|
278
|
+
if (start === -1) return null;
|
|
279
|
+
|
|
280
|
+
const truths: string[] = [];
|
|
281
|
+
const artifacts: string[] = [];
|
|
282
|
+
const keyLinks: string[] = [];
|
|
283
|
+
let currentList: string[] | null = null;
|
|
284
|
+
|
|
285
|
+
for (let i = start + 1; i < fmLines.length; i++) {
|
|
286
|
+
const line = fmLines[i];
|
|
287
|
+
// New top-level key — stop
|
|
288
|
+
if (/^\w/.test(line)) break;
|
|
289
|
+
// Sub-key at 2-space indent
|
|
290
|
+
const subKey = line.match(/^ (\w[\w_]*):/);
|
|
291
|
+
if (subKey) {
|
|
292
|
+
const key = subKey[1];
|
|
293
|
+
if (key === 'truths') currentList = truths;
|
|
294
|
+
else if (key === 'artifacts') currentList = artifacts;
|
|
295
|
+
else if (key === 'key_links') currentList = keyLinks;
|
|
296
|
+
else currentList = null;
|
|
297
|
+
// Check for inline empty array
|
|
298
|
+
if (/:\s*\[\]/.test(line)) currentList = null;
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
// Array item at 4-space indent
|
|
302
|
+
const item = line.match(/^ - (.+)$/);
|
|
303
|
+
if (item && currentList) {
|
|
304
|
+
currentList.push(item[1].trim());
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (truths.length === 0 && artifacts.length === 0 && keyLinks.length === 0) return null;
|
|
309
|
+
return { truths, artifacts, key_links: keyLinks };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function parsePlanFrontmatter(fm: Record<string, unknown>, fmLines: string[] | null): PlanningPlanFrontmatter {
|
|
313
|
+
const mustHaves = fmLines ? parseMustHavesFromLines(fmLines) : null;
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
phase: unquote(fm.phase),
|
|
317
|
+
plan: unquote(fm.plan),
|
|
318
|
+
type: unquote(fm.type),
|
|
319
|
+
wave: fm.wave !== undefined ? Number(fm.wave) : null,
|
|
320
|
+
depends_on: Array.isArray(fm.depends_on) ? fm.depends_on.map(s => unquote(s)) : [],
|
|
321
|
+
files_modified: Array.isArray(fm.files_modified) ? fm.files_modified.map(s => unquote(s)) : [],
|
|
322
|
+
autonomous: fm.autonomous === 'true' || fm.autonomous === true,
|
|
323
|
+
must_haves: mustHaves,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Parse old-format plan file with YAML frontmatter and XML-in-markdown sections.
|
|
329
|
+
* Falls back to plain markdown for quick-task plans that lack XML tags.
|
|
330
|
+
*/
|
|
331
|
+
export function parseOldPlan(content: string, fileName: string = '', planNumber: string = ''): PlanningPlan {
|
|
332
|
+
const [fmLines, body] = splitFrontmatter(content);
|
|
333
|
+
const fm = fmLines ? parseFrontmatterMap(fmLines) : {};
|
|
334
|
+
const frontmatter = parsePlanFrontmatter(fm, fmLines);
|
|
335
|
+
|
|
336
|
+
// Extract XML-in-markdown sections
|
|
337
|
+
const objective = extractXmlTag(content, 'objective');
|
|
338
|
+
const tasks = extractTasks(content);
|
|
339
|
+
const context = extractXmlTag(content, 'context');
|
|
340
|
+
const verification = extractXmlTag(content, 'verification');
|
|
341
|
+
const successCriteria = extractXmlTag(content, 'success_criteria');
|
|
342
|
+
|
|
343
|
+
return {
|
|
344
|
+
fileName,
|
|
345
|
+
planNumber: planNumber || String(fm.plan ?? ''),
|
|
346
|
+
frontmatter,
|
|
347
|
+
objective,
|
|
348
|
+
tasks,
|
|
349
|
+
context,
|
|
350
|
+
verification,
|
|
351
|
+
successCriteria,
|
|
352
|
+
raw: content,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// ─── Summary Parser (YAML Frontmatter) ─────────────────────────────────────
|
|
357
|
+
|
|
358
|
+
function parseRequiresArray(raw: unknown): PlanningSummaryRequires[] {
|
|
359
|
+
if (!Array.isArray(raw)) return [];
|
|
360
|
+
return raw.map(item => {
|
|
361
|
+
if (typeof item === 'object' && item !== null) {
|
|
362
|
+
const obj = item as Record<string, string>;
|
|
363
|
+
return { phase: obj.phase ?? '', provides: obj.provides ?? '' };
|
|
364
|
+
}
|
|
365
|
+
return { phase: '', provides: String(item) };
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function toStringArray(val: unknown): string[] {
|
|
370
|
+
if (Array.isArray(val)) return val.map(String);
|
|
371
|
+
return [];
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Parse YAML-like frontmatter lines into a flat key-value map.
|
|
376
|
+
* Like parseFrontmatterMap but supports hyphenated keys (e.g. `tech-stack:`).
|
|
377
|
+
*/
|
|
378
|
+
function parseFrontmatterMapHyphen(lines: string[]): Record<string, unknown> {
|
|
379
|
+
const result: Record<string, unknown> = {};
|
|
380
|
+
let currentKey: string | null = null;
|
|
381
|
+
let currentArray: unknown[] | null = null;
|
|
382
|
+
let currentObj: Record<string, string> | null = null;
|
|
383
|
+
|
|
384
|
+
for (const line of lines) {
|
|
385
|
+
// Nested object property (4-space indent with key: value)
|
|
386
|
+
const nestedMatch = line.match(/^ ([\w][\w_-]*)\s*:\s*(.*)$/);
|
|
387
|
+
if (nestedMatch && currentArray && currentObj) {
|
|
388
|
+
currentObj[nestedMatch[1]] = nestedMatch[2].trim();
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Array item (2-space indent)
|
|
393
|
+
const arrayMatch = line.match(/^ - (.*)$/);
|
|
394
|
+
if (arrayMatch && currentKey) {
|
|
395
|
+
if (currentObj && Object.keys(currentObj).length > 0) {
|
|
396
|
+
currentArray!.push(currentObj);
|
|
397
|
+
}
|
|
398
|
+
currentObj = null;
|
|
399
|
+
|
|
400
|
+
const val = arrayMatch[1].trim();
|
|
401
|
+
if (!currentArray) currentArray = [];
|
|
402
|
+
|
|
403
|
+
const nestedStart = val.match(/^([\w][\w_-]*)\s*:\s*(.*)$/);
|
|
404
|
+
if (nestedStart) {
|
|
405
|
+
currentObj = { [nestedStart[1]]: nestedStart[2].trim() };
|
|
406
|
+
} else {
|
|
407
|
+
currentArray.push(val);
|
|
408
|
+
}
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Flush previous key
|
|
413
|
+
if (currentKey) {
|
|
414
|
+
if (currentObj && Object.keys(currentObj).length > 0 && currentArray) {
|
|
415
|
+
currentArray.push(currentObj);
|
|
416
|
+
currentObj = null;
|
|
417
|
+
}
|
|
418
|
+
if (currentArray) {
|
|
419
|
+
result[currentKey] = currentArray;
|
|
420
|
+
}
|
|
421
|
+
currentArray = null;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Top-level key: value (supports hyphens in key names)
|
|
425
|
+
const kvMatch = line.match(/^([\w][\w_-]*)\s*:\s*(.*)$/);
|
|
426
|
+
if (kvMatch) {
|
|
427
|
+
currentKey = kvMatch[1];
|
|
428
|
+
const val = kvMatch[2].trim();
|
|
429
|
+
|
|
430
|
+
if (val === '' || val === '[]') {
|
|
431
|
+
currentArray = [];
|
|
432
|
+
} else if (val.startsWith('[') && val.endsWith(']')) {
|
|
433
|
+
const inner = val.slice(1, -1).trim();
|
|
434
|
+
result[currentKey] = inner ? inner.split(',').map(s => s.trim()) : [];
|
|
435
|
+
currentKey = null;
|
|
436
|
+
} else {
|
|
437
|
+
result[currentKey] = val;
|
|
438
|
+
currentKey = null;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Flush final key
|
|
444
|
+
if (currentKey) {
|
|
445
|
+
if (currentObj && Object.keys(currentObj).length > 0 && currentArray) {
|
|
446
|
+
currentArray.push(currentObj);
|
|
447
|
+
currentObj = null;
|
|
448
|
+
}
|
|
449
|
+
if (currentArray) {
|
|
450
|
+
result[currentKey] = currentArray;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return result;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function parseSummaryFrontmatter(fm: Record<string, unknown>): PlanningSummaryFrontmatter {
|
|
458
|
+
return {
|
|
459
|
+
phase: unquote(fm.phase),
|
|
460
|
+
plan: unquote(fm.plan),
|
|
461
|
+
subsystem: unquote(fm.subsystem),
|
|
462
|
+
tags: toStringArray(fm.tags),
|
|
463
|
+
requires: parseRequiresArray(fm.requires),
|
|
464
|
+
provides: toStringArray(fm.provides),
|
|
465
|
+
affects: toStringArray(fm.affects),
|
|
466
|
+
'tech-stack': toStringArray(fm['tech-stack']),
|
|
467
|
+
'key-files': toStringArray(fm['key-files']),
|
|
468
|
+
'key-decisions': toStringArray(fm['key-decisions']),
|
|
469
|
+
'patterns-established': toStringArray(fm['patterns-established']),
|
|
470
|
+
duration: unquote(fm.duration),
|
|
471
|
+
completed: unquote(fm.completed),
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Parse old-format summary file with YAML frontmatter.
|
|
477
|
+
*/
|
|
478
|
+
export function parseOldSummary(content: string, fileName: string = '', planNumber: string = ''): PlanningSummary {
|
|
479
|
+
const [fmLines, body] = splitFrontmatter(content);
|
|
480
|
+
const fm = fmLines ? parseFrontmatterMapHyphen(fmLines) : {};
|
|
481
|
+
|
|
482
|
+
return {
|
|
483
|
+
fileName,
|
|
484
|
+
planNumber: planNumber || String(fm.plan ?? ''),
|
|
485
|
+
frontmatter: parseSummaryFrontmatter(fm),
|
|
486
|
+
body,
|
|
487
|
+
raw: content,
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// ─── Requirements Parser ───────────────────────────────────────────────────
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Parse old-format REQUIREMENTS.md.
|
|
495
|
+
* Extracts requirement entries from markdown with status sections and requirement headings.
|
|
496
|
+
*/
|
|
497
|
+
export function parseOldRequirements(content: string): PlanningRequirement[] {
|
|
498
|
+
const requirements: PlanningRequirement[] = [];
|
|
499
|
+
const lines = content.split('\n');
|
|
500
|
+
|
|
501
|
+
let currentStatus = '';
|
|
502
|
+
let currentReq: Partial<PlanningRequirement> | null = null;
|
|
503
|
+
let currentRaw: string[] = [];
|
|
504
|
+
|
|
505
|
+
function flushReq() {
|
|
506
|
+
if (currentReq?.id && currentReq?.title) {
|
|
507
|
+
requirements.push({
|
|
508
|
+
id: currentReq.id,
|
|
509
|
+
title: currentReq.title,
|
|
510
|
+
status: currentReq.status || currentStatus || 'unknown',
|
|
511
|
+
description: currentReq.description || '',
|
|
512
|
+
raw: currentRaw.join('\n').trim(),
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
currentReq = null;
|
|
516
|
+
currentRaw = [];
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
for (const line of lines) {
|
|
520
|
+
// Status section heading (## Active, ## Validated, ## Deferred)
|
|
521
|
+
const statusMatch = line.match(/^##\s+(\w[\w\s&]*\w)\s*$/);
|
|
522
|
+
if (statusMatch) {
|
|
523
|
+
flushReq();
|
|
524
|
+
currentStatus = statusMatch[1].toLowerCase();
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Section heading (### Category Name) — use as context for bullet requirements
|
|
529
|
+
const sectionMatch = line.match(/^###\s+(.+)$/);
|
|
530
|
+
if (sectionMatch) {
|
|
531
|
+
// Check if this is a requirement heading (### R001 — Title)
|
|
532
|
+
const reqHeading = sectionMatch[1].match(/^(R\d+)\s*[—–-]\s*(.+)$/);
|
|
533
|
+
if (reqHeading) {
|
|
534
|
+
flushReq();
|
|
535
|
+
currentReq = { id: reqHeading[1], title: reqHeading[2].trim(), status: currentStatus, description: '' };
|
|
536
|
+
currentRaw.push(line);
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
// Otherwise just note the section — don't flush, could be a category for bullet reqs
|
|
540
|
+
flushReq();
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Bullet-format requirement: - [x] **ID**: Description
|
|
545
|
+
const bulletReqMatch = line.match(/^-\s+\[([ xX])\]\s+\*\*([^*]+)\*\*\s*:\s*(.+)$/);
|
|
546
|
+
if (bulletReqMatch) {
|
|
547
|
+
flushReq();
|
|
548
|
+
const done = bulletReqMatch[1].toLowerCase() === 'x';
|
|
549
|
+
const id = bulletReqMatch[2].trim();
|
|
550
|
+
const desc = bulletReqMatch[3].trim();
|
|
551
|
+
requirements.push({
|
|
552
|
+
id,
|
|
553
|
+
title: desc,
|
|
554
|
+
status: done ? 'complete' : (currentStatus || 'active'),
|
|
555
|
+
description: desc,
|
|
556
|
+
raw: line,
|
|
557
|
+
});
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Description or metadata within a requirement
|
|
562
|
+
if (currentReq) {
|
|
563
|
+
currentRaw.push(line);
|
|
564
|
+
const descMatch = line.match(/^-\s+Description:\s*(.+)$/);
|
|
565
|
+
if (descMatch) {
|
|
566
|
+
currentReq.description = descMatch[1].trim();
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
const statMatch = line.match(/^-\s+Status:\s*(.+)$/);
|
|
570
|
+
if (statMatch) {
|
|
571
|
+
currentReq.status = statMatch[1].trim();
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
flushReq();
|
|
577
|
+
return requirements;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// ─── Project Parser ────────────────────────────────────────────────────────
|
|
581
|
+
|
|
582
|
+
// PlanningProjectMeta isn't in types.ts — project field on PlanningProject is `string | null`.
|
|
583
|
+
// This parser returns the raw content as a string. The top-level parser stores it directly.
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Parse old-format PROJECT.md.
|
|
587
|
+
* Returns the raw content as a string (stored as project field on PlanningProject).
|
|
588
|
+
*/
|
|
589
|
+
export function parseOldProject(content: string): string {
|
|
590
|
+
return content;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// ─── State Parser ──────────────────────────────────────────────────────────
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Parse old-format STATE.md.
|
|
597
|
+
* Extracts current phase and status from bold-field patterns.
|
|
598
|
+
*/
|
|
599
|
+
export function parseOldState(content: string): PlanningState {
|
|
600
|
+
const currentPhase = extractBoldField(content, 'Current Phase');
|
|
601
|
+
const status = extractBoldField(content, 'Status');
|
|
602
|
+
|
|
603
|
+
return {
|
|
604
|
+
raw: content,
|
|
605
|
+
currentPhase,
|
|
606
|
+
status,
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// ─── Config Parser ─────────────────────────────────────────────────────────
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Parse old-format config.json.
|
|
614
|
+
* Returns null on invalid JSON (graceful error handling).
|
|
615
|
+
*/
|
|
616
|
+
export function parseOldConfig(content: string): PlanningConfig | null {
|
|
617
|
+
try {
|
|
618
|
+
const parsed = JSON.parse(content);
|
|
619
|
+
if (typeof parsed !== 'object' || parsed === null) return null;
|
|
620
|
+
return parsed as PlanningConfig;
|
|
621
|
+
} catch {
|
|
622
|
+
return null;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// GSD Migration Preview — Pre-write statistics
|
|
2
|
+
// Pure function, no I/O. Computes counts from a GSDProject.
|
|
3
|
+
|
|
4
|
+
import type { GSDProject } from './types.ts';
|
|
5
|
+
import type { MigrationPreview } from './writer.ts';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Compute pre-write statistics from a GSDProject without performing I/O.
|
|
9
|
+
* Used to show the user what a migration will produce before writing anything.
|
|
10
|
+
*/
|
|
11
|
+
export function generatePreview(project: GSDProject): MigrationPreview {
|
|
12
|
+
let totalSlices = 0;
|
|
13
|
+
let totalTasks = 0;
|
|
14
|
+
let doneSlices = 0;
|
|
15
|
+
let doneTasks = 0;
|
|
16
|
+
|
|
17
|
+
for (const milestone of project.milestones) {
|
|
18
|
+
for (const slice of milestone.slices) {
|
|
19
|
+
totalSlices++;
|
|
20
|
+
if (slice.done) doneSlices++;
|
|
21
|
+
for (const task of slice.tasks) {
|
|
22
|
+
totalTasks++;
|
|
23
|
+
if (task.done) doneTasks++;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const reqCounts = { active: 0, validated: 0, deferred: 0, outOfScope: 0, total: 0 };
|
|
29
|
+
for (const req of project.requirements) {
|
|
30
|
+
const status = req.status.toLowerCase();
|
|
31
|
+
if (status === 'active') reqCounts.active++;
|
|
32
|
+
else if (status === 'validated') reqCounts.validated++;
|
|
33
|
+
else if (status === 'deferred') reqCounts.deferred++;
|
|
34
|
+
else if (status === 'out-of-scope') reqCounts.outOfScope++;
|
|
35
|
+
reqCounts.total++;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
milestoneCount: project.milestones.length,
|
|
40
|
+
totalSlices,
|
|
41
|
+
totalTasks,
|
|
42
|
+
doneSlices,
|
|
43
|
+
doneTasks,
|
|
44
|
+
sliceCompletionPct: totalSlices > 0 ? Math.round((doneSlices / totalSlices) * 100) : 0,
|
|
45
|
+
taskCompletionPct: totalTasks > 0 ? Math.round((doneTasks / totalTasks) * 100) : 0,
|
|
46
|
+
requirements: reqCounts,
|
|
47
|
+
};
|
|
48
|
+
}
|