gsd-pi 0.2.9 → 0.3.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/README.md +23 -0
- package/dist/cli.js +47 -5
- package/dist/wizard.js +2 -1
- package/package.json +1 -1
- package/src/resources/extensions/gsd/commands.ts +9 -3
- package/src/resources/extensions/gsd/dashboard-overlay.ts +6 -1
- package/src/resources/extensions/gsd/files.ts +7 -7
- package/src/resources/extensions/gsd/gitignore.ts +1 -0
- package/src/resources/extensions/gsd/index.ts +36 -1
- package/src/resources/extensions/gsd/migrate/command.ts +215 -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 +53 -0
- package/src/resources/extensions/gsd/migrate/writer.ts +539 -0
- package/src/resources/extensions/gsd/prompts/review-migration.md +66 -0
- package/src/resources/extensions/gsd/prompts/worktree-merge.md +89 -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/worktree-manager.test.ts +160 -0
- package/src/resources/extensions/gsd/worktree-command.ts +527 -0
- package/src/resources/extensions/gsd/worktree-manager.ts +302 -0
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
// Migration transformer — converts parsed PlanningProject into GSDProject.
|
|
2
|
+
// Pure function: no I/O, no side effects, no imports outside migrate/.
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
PlanningProject,
|
|
6
|
+
PlanningPhase,
|
|
7
|
+
PlanningPlan,
|
|
8
|
+
PlanningSummary,
|
|
9
|
+
PlanningRoadmapEntry,
|
|
10
|
+
PlanningRoadmapMilestone,
|
|
11
|
+
PlanningResearch,
|
|
12
|
+
PlanningRequirement,
|
|
13
|
+
GSDProject,
|
|
14
|
+
GSDMilestone,
|
|
15
|
+
GSDSlice,
|
|
16
|
+
GSDTask,
|
|
17
|
+
GSDRequirement,
|
|
18
|
+
GSDSliceSummaryData,
|
|
19
|
+
GSDTaskSummaryData,
|
|
20
|
+
GSDBoundaryEntry,
|
|
21
|
+
} from './types.ts';
|
|
22
|
+
|
|
23
|
+
// ─── Helpers ───────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
function padId(prefix: string, n: number, width = 2): string {
|
|
26
|
+
return `${prefix}${String(n).padStart(width, '0')}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function milestoneId(n: number): string {
|
|
30
|
+
return padId('M', n, 3);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function kebabToTitle(slug: string): string {
|
|
34
|
+
return slug
|
|
35
|
+
.split('-')
|
|
36
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
37
|
+
.join(' ');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function firstSentence(text: string): string {
|
|
41
|
+
const trimmed = text.trim();
|
|
42
|
+
const match = trimmed.match(/^[^.!?]*[.!?]/);
|
|
43
|
+
return match ? match[0].trim() : trimmed;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Preferred research ordering for consolidation. */
|
|
47
|
+
const RESEARCH_ORDER = ['SUMMARY.md', 'ARCHITECTURE.md', 'STACK.md', 'FEATURES.md', 'PITFALLS.md'];
|
|
48
|
+
|
|
49
|
+
function sortResearch(files: PlanningResearch[]): PlanningResearch[] {
|
|
50
|
+
return [...files].sort((a, b) => {
|
|
51
|
+
const ai = RESEARCH_ORDER.indexOf(a.fileName);
|
|
52
|
+
const bi = RESEARCH_ORDER.indexOf(b.fileName);
|
|
53
|
+
const aw = ai === -1 ? RESEARCH_ORDER.length : ai;
|
|
54
|
+
const bw = bi === -1 ? RESEARCH_ORDER.length : bi;
|
|
55
|
+
if (aw !== bw) return aw - bw;
|
|
56
|
+
return a.fileName.localeCompare(b.fileName);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function consolidateResearch(files: PlanningResearch[]): string | null {
|
|
61
|
+
if (files.length === 0) return null;
|
|
62
|
+
return sortResearch(files)
|
|
63
|
+
.map((f) => f.content.trim())
|
|
64
|
+
.join('\n\n');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ─── Task Mapping ──────────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
function buildTaskSummary(summary: PlanningSummary): GSDTaskSummaryData {
|
|
70
|
+
return {
|
|
71
|
+
completedAt: summary.frontmatter.completed ?? '',
|
|
72
|
+
provides: summary.frontmatter.provides ?? [],
|
|
73
|
+
keyFiles: summary.frontmatter['key-files'] ?? [],
|
|
74
|
+
duration: summary.frontmatter.duration ?? '',
|
|
75
|
+
whatHappened: summary.body?.trim() ?? '',
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function mapTask(plan: PlanningPlan, index: number, summaries: Record<string, PlanningSummary>): GSDTask {
|
|
80
|
+
const summary = summaries[plan.planNumber];
|
|
81
|
+
const done = summary !== undefined;
|
|
82
|
+
return {
|
|
83
|
+
id: padId('T', index + 1),
|
|
84
|
+
title: buildTaskTitle(plan),
|
|
85
|
+
description: plan.objective ?? '',
|
|
86
|
+
done,
|
|
87
|
+
estimate: done ? (summary.frontmatter.duration ?? '') : '',
|
|
88
|
+
files: plan.frontmatter.files_modified ?? [],
|
|
89
|
+
mustHaves: plan.frontmatter.must_haves?.truths ?? [],
|
|
90
|
+
summary: done ? buildTaskSummary(summary) : null,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function buildTaskTitle(plan: PlanningPlan): string {
|
|
95
|
+
const fm = plan.frontmatter;
|
|
96
|
+
if (fm.phase && fm.plan) {
|
|
97
|
+
return `${fm.phase} ${fm.plan}`;
|
|
98
|
+
}
|
|
99
|
+
return `Plan ${plan.planNumber}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ─── Slice Mapping ─────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
function buildSliceSummary(phase: PlanningPhase): GSDSliceSummaryData | null {
|
|
105
|
+
// Aggregate from all summaries in the phase
|
|
106
|
+
const summaryEntries = Object.values(phase.summaries);
|
|
107
|
+
if (summaryEntries.length === 0) return null;
|
|
108
|
+
|
|
109
|
+
const provides: string[] = [];
|
|
110
|
+
const keyFiles: string[] = [];
|
|
111
|
+
const keyDecisions: string[] = [];
|
|
112
|
+
const patternsEstablished: string[] = [];
|
|
113
|
+
let lastCompleted = '';
|
|
114
|
+
let totalDuration = '';
|
|
115
|
+
const bodies: string[] = [];
|
|
116
|
+
|
|
117
|
+
for (const s of summaryEntries) {
|
|
118
|
+
provides.push(...(s.frontmatter.provides ?? []));
|
|
119
|
+
keyFiles.push(...(s.frontmatter['key-files'] ?? []));
|
|
120
|
+
keyDecisions.push(...(s.frontmatter['key-decisions'] ?? []));
|
|
121
|
+
patternsEstablished.push(...(s.frontmatter['patterns-established'] ?? []));
|
|
122
|
+
if (s.frontmatter.completed) lastCompleted = s.frontmatter.completed;
|
|
123
|
+
if (s.frontmatter.duration) totalDuration = s.frontmatter.duration;
|
|
124
|
+
if (s.body?.trim()) bodies.push(s.body.trim());
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
completedAt: lastCompleted,
|
|
129
|
+
provides,
|
|
130
|
+
keyFiles,
|
|
131
|
+
keyDecisions,
|
|
132
|
+
patternsEstablished,
|
|
133
|
+
duration: totalDuration,
|
|
134
|
+
whatHappened: bodies.join('\n\n'),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function deriveDemo(phase: PlanningPhase, slug: string): string {
|
|
139
|
+
// First plan's objective, first sentence
|
|
140
|
+
const planNumbers = Object.keys(phase.plans).sort((a, b) => Number(a) - Number(b));
|
|
141
|
+
if (planNumbers.length > 0) {
|
|
142
|
+
const firstPlan = phase.plans[planNumbers[0]];
|
|
143
|
+
if (firstPlan?.objective) {
|
|
144
|
+
return firstSentence(firstPlan.objective);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return `unit tests prove ${slug} works`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function mapSlice(
|
|
151
|
+
phase: PlanningPhase | undefined,
|
|
152
|
+
entry: PlanningRoadmapEntry,
|
|
153
|
+
index: number,
|
|
154
|
+
prevSliceId: string | null,
|
|
155
|
+
): GSDSlice {
|
|
156
|
+
const sliceId = padId('S', index + 1);
|
|
157
|
+
const slug = phase?.slug ?? entry.title;
|
|
158
|
+
const demo = phase ? deriveDemo(phase, slug) : `unit tests prove ${entry.title} works`;
|
|
159
|
+
|
|
160
|
+
let tasks: GSDTask[] = [];
|
|
161
|
+
if (phase) {
|
|
162
|
+
const planNumbers = Object.keys(phase.plans).sort((a, b) => Number(a) - Number(b));
|
|
163
|
+
tasks = planNumbers.map((pn, i) => mapTask(phase.plans[pn], i, phase.summaries));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const done = entry.done;
|
|
167
|
+
const sliceSummary = done && phase ? buildSliceSummary(phase) : null;
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
id: sliceId,
|
|
171
|
+
title: kebabToTitle(slug),
|
|
172
|
+
risk: 'medium',
|
|
173
|
+
depends: prevSliceId ? [prevSliceId] : [],
|
|
174
|
+
done,
|
|
175
|
+
demo,
|
|
176
|
+
goal: demo,
|
|
177
|
+
tasks,
|
|
178
|
+
research: phase ? consolidateResearch(phase.research) : null,
|
|
179
|
+
summary: sliceSummary,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ─── Milestone Building ───────────────────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
function findPhase(phases: Record<string, PlanningPhase>, phaseNumber: number, entryTitle?: string): PlanningPhase | undefined {
|
|
186
|
+
const matches = Object.values(phases).filter((p) => p.number === phaseNumber);
|
|
187
|
+
if (matches.length <= 1) return matches[0];
|
|
188
|
+
// Multiple phases with the same number — try to match by title/slug similarity
|
|
189
|
+
if (entryTitle) {
|
|
190
|
+
const normalizedTitle = entryTitle.toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();
|
|
191
|
+
const best = matches.find((p) => {
|
|
192
|
+
const normalizedSlug = p.slug.replace(/-/g, ' ').toLowerCase();
|
|
193
|
+
return normalizedSlug === normalizedTitle || normalizedTitle.includes(normalizedSlug) || normalizedSlug.includes(normalizedTitle);
|
|
194
|
+
});
|
|
195
|
+
if (best) return best;
|
|
196
|
+
}
|
|
197
|
+
return matches[0];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function buildMilestoneFromEntries(
|
|
201
|
+
id: string,
|
|
202
|
+
title: string,
|
|
203
|
+
entries: PlanningRoadmapEntry[],
|
|
204
|
+
phases: Record<string, PlanningPhase>,
|
|
205
|
+
research: PlanningResearch[],
|
|
206
|
+
): GSDMilestone {
|
|
207
|
+
// Sort entries by phase number (float sort)
|
|
208
|
+
const sorted = [...entries].sort((a, b) => a.number - b.number);
|
|
209
|
+
|
|
210
|
+
const slices: GSDSlice[] = [];
|
|
211
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
212
|
+
const entry = sorted[i];
|
|
213
|
+
const phase = findPhase(phases, entry.number, entry.title);
|
|
214
|
+
const prevId = i > 0 ? slices[i - 1].id : null;
|
|
215
|
+
slices.push(mapSlice(phase, entry, i, prevId));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
id,
|
|
220
|
+
title,
|
|
221
|
+
vision: '',
|
|
222
|
+
successCriteria: [],
|
|
223
|
+
slices,
|
|
224
|
+
research: consolidateResearch(research),
|
|
225
|
+
boundaryMap: [],
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ─── Requirements Mapping ──────────────────────────────────────────────────
|
|
230
|
+
|
|
231
|
+
const VALID_STATUSES = new Set(['active', 'validated', 'deferred']);
|
|
232
|
+
const COMPLETE_ALIASES = new Set(['complete', 'completed', 'done', 'shipped']);
|
|
233
|
+
|
|
234
|
+
function normalizeStatus(status: string): 'active' | 'validated' | 'deferred' {
|
|
235
|
+
const lower = status.toLowerCase().trim();
|
|
236
|
+
if (VALID_STATUSES.has(lower)) return lower as 'active' | 'validated' | 'deferred';
|
|
237
|
+
if (COMPLETE_ALIASES.has(lower)) return 'validated';
|
|
238
|
+
return 'active';
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function mapRequirements(reqs: PlanningRequirement[]): GSDRequirement[] {
|
|
242
|
+
let autoId = 0;
|
|
243
|
+
return reqs.map((req) => {
|
|
244
|
+
autoId++;
|
|
245
|
+
return {
|
|
246
|
+
id: req.id && req.id.trim() !== '' ? req.id : padId('R', autoId, 3),
|
|
247
|
+
title: req.title,
|
|
248
|
+
class: 'core-capability',
|
|
249
|
+
status: normalizeStatus(req.status),
|
|
250
|
+
description: req.description,
|
|
251
|
+
source: 'inferred',
|
|
252
|
+
primarySlice: 'none yet',
|
|
253
|
+
};
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ─── Project-Level Derivation ──────────────────────────────────────────────
|
|
258
|
+
|
|
259
|
+
function deriveVision(parsed: PlanningProject): string {
|
|
260
|
+
// Try first non-heading line from PROJECT.md
|
|
261
|
+
if (parsed.project) {
|
|
262
|
+
const lines = parsed.project.split('\n');
|
|
263
|
+
for (const line of lines) {
|
|
264
|
+
const trimmed = line.trim();
|
|
265
|
+
if (trimmed && !trimmed.startsWith('#')) {
|
|
266
|
+
return firstSentence(trimmed);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
// Fallback: roadmap title
|
|
271
|
+
if (parsed.roadmap) {
|
|
272
|
+
if (parsed.roadmap.milestones.length > 0) {
|
|
273
|
+
return parsed.roadmap.milestones[0].title;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return 'Project migration from .planning format';
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function deriveDecisions(parsed: PlanningProject): string {
|
|
280
|
+
// Extract key decisions from phase summaries if available
|
|
281
|
+
const decisions: string[] = [];
|
|
282
|
+
for (const phase of Object.values(parsed.phases)) {
|
|
283
|
+
for (const summary of Object.values(phase.summaries)) {
|
|
284
|
+
const kd = summary.frontmatter['key-decisions'] ?? [];
|
|
285
|
+
decisions.push(...kd);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (decisions.length === 0) return '';
|
|
289
|
+
return decisions.map((d) => `- ${d}`).join('\n');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ─── Main Entry Point ──────────────────────────────────────────────────────
|
|
293
|
+
|
|
294
|
+
export function transformToGSD(parsed: PlanningProject): GSDProject {
|
|
295
|
+
const milestones: GSDMilestone[] = [];
|
|
296
|
+
|
|
297
|
+
const roadmap = parsed.roadmap;
|
|
298
|
+
const isMultiMilestone = roadmap !== null && roadmap.milestones.length > 0;
|
|
299
|
+
const hasFlatPhases = roadmap !== null && roadmap.phases.length > 0;
|
|
300
|
+
|
|
301
|
+
if (isMultiMilestone) {
|
|
302
|
+
// Multi-milestone mode: each roadmap milestone section → one GSDMilestone
|
|
303
|
+
for (let mi = 0; mi < roadmap!.milestones.length; mi++) {
|
|
304
|
+
const rm = roadmap!.milestones[mi];
|
|
305
|
+
milestones.push(
|
|
306
|
+
buildMilestoneFromEntries(
|
|
307
|
+
milestoneId(mi + 1),
|
|
308
|
+
rm.title,
|
|
309
|
+
rm.phases,
|
|
310
|
+
parsed.phases,
|
|
311
|
+
mi === 0 ? parsed.research : [],
|
|
312
|
+
),
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
} else if (hasFlatPhases) {
|
|
316
|
+
// Single-milestone mode from roadmap phases
|
|
317
|
+
milestones.push(
|
|
318
|
+
buildMilestoneFromEntries('M001', 'Migration', roadmap!.phases, parsed.phases, parsed.research),
|
|
319
|
+
);
|
|
320
|
+
} else {
|
|
321
|
+
// Null/empty roadmap fallback: use filesystem phases, all not-done
|
|
322
|
+
const fsPhases = Object.values(parsed.phases).sort((a, b) => a.number - b.number);
|
|
323
|
+
const entries: PlanningRoadmapEntry[] = fsPhases.map((p) => ({
|
|
324
|
+
number: p.number,
|
|
325
|
+
title: p.slug,
|
|
326
|
+
done: false,
|
|
327
|
+
raw: '',
|
|
328
|
+
}));
|
|
329
|
+
milestones.push(
|
|
330
|
+
buildMilestoneFromEntries('M001', 'Migration', entries, parsed.phases, parsed.research),
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Set vision on first milestone (or all if multi)
|
|
335
|
+
const vision = deriveVision(parsed);
|
|
336
|
+
for (const m of milestones) {
|
|
337
|
+
if (!m.vision) m.vision = vision;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return {
|
|
341
|
+
milestones,
|
|
342
|
+
projectContent: parsed.project ?? '',
|
|
343
|
+
requirements: mapRequirements(parsed.requirements),
|
|
344
|
+
decisionsContent: deriveDecisions(parsed),
|
|
345
|
+
};
|
|
346
|
+
}
|