@lipter7/blueprint 2.0.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.
Files changed (125) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +626 -0
  3. package/agents/bp-codebase-mapper.md +761 -0
  4. package/agents/bp-debugger.md +1198 -0
  5. package/agents/bp-executor.md +403 -0
  6. package/agents/bp-integration-checker.md +423 -0
  7. package/agents/bp-phase-researcher.md +469 -0
  8. package/agents/bp-plan-checker.md +622 -0
  9. package/agents/bp-planner.md +1157 -0
  10. package/agents/bp-project-researcher.md +618 -0
  11. package/agents/bp-research-synthesizer.md +236 -0
  12. package/agents/bp-roadmapper.md +605 -0
  13. package/agents/bp-verifier.md +523 -0
  14. package/bin/install.js +1754 -0
  15. package/blueprint/bin/blueprint-tools.js +4597 -0
  16. package/blueprint/bin/blueprint-tools.test.js +2033 -0
  17. package/blueprint/references/checkpoints.md +775 -0
  18. package/blueprint/references/continuation-format.md +249 -0
  19. package/blueprint/references/decimal-phase-calculation.md +65 -0
  20. package/blueprint/references/git-integration.md +248 -0
  21. package/blueprint/references/git-planning-commit.md +38 -0
  22. package/blueprint/references/model-profile-resolution.md +32 -0
  23. package/blueprint/references/model-profiles.md +73 -0
  24. package/blueprint/references/phase-argument-parsing.md +61 -0
  25. package/blueprint/references/planning-config.md +194 -0
  26. package/blueprint/references/questioning.md +141 -0
  27. package/blueprint/references/tdd.md +263 -0
  28. package/blueprint/references/ui-brand.md +160 -0
  29. package/blueprint/references/verification-patterns.md +612 -0
  30. package/blueprint/templates/DEBUG.md +159 -0
  31. package/blueprint/templates/UAT.md +247 -0
  32. package/blueprint/templates/codebase/architecture.md +255 -0
  33. package/blueprint/templates/codebase/concerns.md +310 -0
  34. package/blueprint/templates/codebase/conventions.md +307 -0
  35. package/blueprint/templates/codebase/integrations.md +280 -0
  36. package/blueprint/templates/codebase/stack.md +186 -0
  37. package/blueprint/templates/codebase/structure.md +285 -0
  38. package/blueprint/templates/codebase/testing.md +480 -0
  39. package/blueprint/templates/config.json +35 -0
  40. package/blueprint/templates/context.md +283 -0
  41. package/blueprint/templates/continue-here.md +78 -0
  42. package/blueprint/templates/debug-subagent-prompt.md +91 -0
  43. package/blueprint/templates/discovery.md +146 -0
  44. package/blueprint/templates/milestone-archive.md +123 -0
  45. package/blueprint/templates/milestone.md +115 -0
  46. package/blueprint/templates/phase-prompt.md +567 -0
  47. package/blueprint/templates/planner-subagent-prompt.md +117 -0
  48. package/blueprint/templates/project.md +184 -0
  49. package/blueprint/templates/requirements.md +231 -0
  50. package/blueprint/templates/research-project/ARCHITECTURE.md +204 -0
  51. package/blueprint/templates/research-project/FEATURES.md +147 -0
  52. package/blueprint/templates/research-project/PITFALLS.md +200 -0
  53. package/blueprint/templates/research-project/STACK.md +120 -0
  54. package/blueprint/templates/research-project/SUMMARY.md +170 -0
  55. package/blueprint/templates/research.md +552 -0
  56. package/blueprint/templates/roadmap.md +202 -0
  57. package/blueprint/templates/state.md +176 -0
  58. package/blueprint/templates/summary-complex.md +59 -0
  59. package/blueprint/templates/summary-minimal.md +41 -0
  60. package/blueprint/templates/summary-standard.md +48 -0
  61. package/blueprint/templates/summary.md +246 -0
  62. package/blueprint/templates/user-setup.md +311 -0
  63. package/blueprint/templates/verification-report.md +322 -0
  64. package/blueprint/workflows/add-phase.md +111 -0
  65. package/blueprint/workflows/add-todo.md +157 -0
  66. package/blueprint/workflows/audit-milestone.md +241 -0
  67. package/blueprint/workflows/check-todos.md +176 -0
  68. package/blueprint/workflows/complete-milestone.md +644 -0
  69. package/blueprint/workflows/diagnose-issues.md +219 -0
  70. package/blueprint/workflows/discovery-phase.md +289 -0
  71. package/blueprint/workflows/discuss-phase.md +408 -0
  72. package/blueprint/workflows/execute-phase.md +338 -0
  73. package/blueprint/workflows/execute-plan.md +437 -0
  74. package/blueprint/workflows/help.md +470 -0
  75. package/blueprint/workflows/insert-phase.md +129 -0
  76. package/blueprint/workflows/list-phase-assumptions.md +178 -0
  77. package/blueprint/workflows/map-codebase.md +327 -0
  78. package/blueprint/workflows/new-milestone.md +373 -0
  79. package/blueprint/workflows/new-project.md +958 -0
  80. package/blueprint/workflows/pause-work.md +122 -0
  81. package/blueprint/workflows/plan-milestone-gaps.md +256 -0
  82. package/blueprint/workflows/plan-phase.md +376 -0
  83. package/blueprint/workflows/progress.md +385 -0
  84. package/blueprint/workflows/quick.md +230 -0
  85. package/blueprint/workflows/remove-phase.md +154 -0
  86. package/blueprint/workflows/research-phase.md +74 -0
  87. package/blueprint/workflows/resume-project.md +306 -0
  88. package/blueprint/workflows/set-profile.md +80 -0
  89. package/blueprint/workflows/settings.md +145 -0
  90. package/blueprint/workflows/transition.md +493 -0
  91. package/blueprint/workflows/update.md +212 -0
  92. package/blueprint/workflows/verify-phase.md +226 -0
  93. package/blueprint/workflows/verify-work.md +570 -0
  94. package/commands/bp/add-phase.md +39 -0
  95. package/commands/bp/add-todo.md +42 -0
  96. package/commands/bp/audit-milestone.md +42 -0
  97. package/commands/bp/check-todos.md +41 -0
  98. package/commands/bp/complete-milestone.md +136 -0
  99. package/commands/bp/debug.md +162 -0
  100. package/commands/bp/discuss-phase.md +86 -0
  101. package/commands/bp/execute-phase.md +42 -0
  102. package/commands/bp/help.md +22 -0
  103. package/commands/bp/insert-phase.md +33 -0
  104. package/commands/bp/join-discord.md +18 -0
  105. package/commands/bp/list-phase-assumptions.md +50 -0
  106. package/commands/bp/map-codebase.md +71 -0
  107. package/commands/bp/new-milestone.md +51 -0
  108. package/commands/bp/new-project.md +42 -0
  109. package/commands/bp/pause-work.md +35 -0
  110. package/commands/bp/plan-milestone-gaps.md +40 -0
  111. package/commands/bp/plan-phase.md +44 -0
  112. package/commands/bp/progress.md +24 -0
  113. package/commands/bp/quick.md +38 -0
  114. package/commands/bp/reapply-patches.md +110 -0
  115. package/commands/bp/remove-phase.md +32 -0
  116. package/commands/bp/research-phase.md +187 -0
  117. package/commands/bp/resume-work.md +40 -0
  118. package/commands/bp/set-profile.md +34 -0
  119. package/commands/bp/settings.md +36 -0
  120. package/commands/bp/update.md +37 -0
  121. package/commands/bp/verify-work.md +39 -0
  122. package/hooks/dist/bp-check-update.js +62 -0
  123. package/hooks/dist/bp-statusline.js +91 -0
  124. package/package.json +48 -0
  125. package/scripts/build-hooks.js +42 -0
@@ -0,0 +1,2033 @@
1
+ /**
2
+ * Blueprint Tools Tests
3
+ */
4
+
5
+ const { test, describe, beforeEach, afterEach } = require('node:test');
6
+ const assert = require('node:assert');
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const { execSync } = require('child_process');
10
+
11
+ const TOOLS_PATH = path.join(__dirname, 'blueprint-tools.js');
12
+
13
+ // Helper to run blueprint-tools command
14
+ function runBlueprintTools(args, cwd = process.cwd()) {
15
+ try {
16
+ const result = execSync(`node "${TOOLS_PATH}" ${args}`, {
17
+ cwd,
18
+ encoding: 'utf-8',
19
+ stdio: ['pipe', 'pipe', 'pipe'],
20
+ });
21
+ return { success: true, output: result.trim() };
22
+ } catch (err) {
23
+ return {
24
+ success: false,
25
+ output: err.stdout?.toString().trim() || '',
26
+ error: err.stderr?.toString().trim() || err.message,
27
+ };
28
+ }
29
+ }
30
+
31
+ // Create temp directory structure
32
+ function createTempProject() {
33
+ const tmpDir = fs.mkdtempSync(path.join(require('os').tmpdir(), 'bp-test-'));
34
+ fs.mkdirSync(path.join(tmpDir, '.blueprint', 'phases'), { recursive: true });
35
+ return tmpDir;
36
+ }
37
+
38
+ function cleanup(tmpDir) {
39
+ fs.rmSync(tmpDir, { recursive: true, force: true });
40
+ }
41
+
42
+ describe('history-digest command', () => {
43
+ let tmpDir;
44
+
45
+ beforeEach(() => {
46
+ tmpDir = createTempProject();
47
+ });
48
+
49
+ afterEach(() => {
50
+ cleanup(tmpDir);
51
+ });
52
+
53
+ test('empty phases directory returns valid schema', () => {
54
+ const result = runBlueprintTools('history-digest', tmpDir);
55
+ assert.ok(result.success, `Command failed: ${result.error}`);
56
+
57
+ const digest = JSON.parse(result.output);
58
+
59
+ assert.deepStrictEqual(digest.phases, {}, 'phases should be empty object');
60
+ assert.deepStrictEqual(digest.decisions, [], 'decisions should be empty array');
61
+ assert.deepStrictEqual(digest.tech_stack, [], 'tech_stack should be empty array');
62
+ });
63
+
64
+ test('nested frontmatter fields extracted correctly', () => {
65
+ // Create phase directory with SUMMARY containing nested frontmatter
66
+ const phaseDir = path.join(tmpDir, '.blueprint', 'phases', '01-foundation');
67
+ fs.mkdirSync(phaseDir, { recursive: true });
68
+
69
+ const summaryContent = `---
70
+ phase: "01"
71
+ name: "Foundation Setup"
72
+ dependency-graph:
73
+ provides:
74
+ - "Database schema"
75
+ - "Auth system"
76
+ affects:
77
+ - "API layer"
78
+ tech-stack:
79
+ added:
80
+ - "prisma"
81
+ - "jose"
82
+ patterns-established:
83
+ - "Repository pattern"
84
+ - "JWT auth flow"
85
+ key-decisions:
86
+ - "Use Prisma over Drizzle"
87
+ - "JWT in httpOnly cookies"
88
+ ---
89
+
90
+ # Summary content here
91
+ `;
92
+
93
+ fs.writeFileSync(path.join(phaseDir, '01-01-SUMMARY.md'), summaryContent);
94
+
95
+ const result = runBlueprintTools('history-digest', tmpDir);
96
+ assert.ok(result.success, `Command failed: ${result.error}`);
97
+
98
+ const digest = JSON.parse(result.output);
99
+
100
+ // Check nested dependency-graph.provides
101
+ assert.ok(digest.phases['01'], 'Phase 01 should exist');
102
+ assert.deepStrictEqual(
103
+ digest.phases['01'].provides.sort(),
104
+ ['Auth system', 'Database schema'],
105
+ 'provides should contain nested values'
106
+ );
107
+
108
+ // Check nested dependency-graph.affects
109
+ assert.deepStrictEqual(
110
+ digest.phases['01'].affects,
111
+ ['API layer'],
112
+ 'affects should contain nested values'
113
+ );
114
+
115
+ // Check nested tech-stack.added
116
+ assert.deepStrictEqual(
117
+ digest.tech_stack.sort(),
118
+ ['jose', 'prisma'],
119
+ 'tech_stack should contain nested values'
120
+ );
121
+
122
+ // Check patterns-established (flat array)
123
+ assert.deepStrictEqual(
124
+ digest.phases['01'].patterns.sort(),
125
+ ['JWT auth flow', 'Repository pattern'],
126
+ 'patterns should be extracted'
127
+ );
128
+
129
+ // Check key-decisions
130
+ assert.strictEqual(digest.decisions.length, 2, 'Should have 2 decisions');
131
+ assert.ok(
132
+ digest.decisions.some(d => d.decision === 'Use Prisma over Drizzle'),
133
+ 'Should contain first decision'
134
+ );
135
+ });
136
+
137
+ test('multiple phases merged into single digest', () => {
138
+ // Create phase 01
139
+ const phase01Dir = path.join(tmpDir, '.blueprint', 'phases', '01-foundation');
140
+ fs.mkdirSync(phase01Dir, { recursive: true });
141
+ fs.writeFileSync(
142
+ path.join(phase01Dir, '01-01-SUMMARY.md'),
143
+ `---
144
+ phase: "01"
145
+ name: "Foundation"
146
+ provides:
147
+ - "Database"
148
+ patterns-established:
149
+ - "Pattern A"
150
+ key-decisions:
151
+ - "Decision 1"
152
+ ---
153
+ `
154
+ );
155
+
156
+ // Create phase 02
157
+ const phase02Dir = path.join(tmpDir, '.blueprint', 'phases', '02-api');
158
+ fs.mkdirSync(phase02Dir, { recursive: true });
159
+ fs.writeFileSync(
160
+ path.join(phase02Dir, '02-01-SUMMARY.md'),
161
+ `---
162
+ phase: "02"
163
+ name: "API"
164
+ provides:
165
+ - "REST endpoints"
166
+ patterns-established:
167
+ - "Pattern B"
168
+ key-decisions:
169
+ - "Decision 2"
170
+ tech-stack:
171
+ added:
172
+ - "zod"
173
+ ---
174
+ `
175
+ );
176
+
177
+ const result = runBlueprintTools('history-digest', tmpDir);
178
+ assert.ok(result.success, `Command failed: ${result.error}`);
179
+
180
+ const digest = JSON.parse(result.output);
181
+
182
+ // Both phases present
183
+ assert.ok(digest.phases['01'], 'Phase 01 should exist');
184
+ assert.ok(digest.phases['02'], 'Phase 02 should exist');
185
+
186
+ // Decisions merged
187
+ assert.strictEqual(digest.decisions.length, 2, 'Should have 2 decisions total');
188
+
189
+ // Tech stack merged
190
+ assert.deepStrictEqual(digest.tech_stack, ['zod'], 'tech_stack should have zod');
191
+ });
192
+
193
+ test('malformed SUMMARY.md skipped gracefully', () => {
194
+ const phaseDir = path.join(tmpDir, '.blueprint', 'phases', '01-test');
195
+ fs.mkdirSync(phaseDir, { recursive: true });
196
+
197
+ // Valid summary
198
+ fs.writeFileSync(
199
+ path.join(phaseDir, '01-01-SUMMARY.md'),
200
+ `---
201
+ phase: "01"
202
+ provides:
203
+ - "Valid feature"
204
+ ---
205
+ `
206
+ );
207
+
208
+ // Malformed summary (no frontmatter)
209
+ fs.writeFileSync(
210
+ path.join(phaseDir, '01-02-SUMMARY.md'),
211
+ `# Just a heading
212
+ No frontmatter here
213
+ `
214
+ );
215
+
216
+ // Another malformed summary (broken YAML)
217
+ fs.writeFileSync(
218
+ path.join(phaseDir, '01-03-SUMMARY.md'),
219
+ `---
220
+ broken: [unclosed
221
+ ---
222
+ `
223
+ );
224
+
225
+ const result = runBlueprintTools('history-digest', tmpDir);
226
+ assert.ok(result.success, `Command should succeed despite malformed files: ${result.error}`);
227
+
228
+ const digest = JSON.parse(result.output);
229
+ assert.ok(digest.phases['01'], 'Phase 01 should exist');
230
+ assert.ok(
231
+ digest.phases['01'].provides.includes('Valid feature'),
232
+ 'Valid feature should be extracted'
233
+ );
234
+ });
235
+
236
+ test('flat provides field still works (backward compatibility)', () => {
237
+ const phaseDir = path.join(tmpDir, '.blueprint', 'phases', '01-test');
238
+ fs.mkdirSync(phaseDir, { recursive: true });
239
+
240
+ fs.writeFileSync(
241
+ path.join(phaseDir, '01-01-SUMMARY.md'),
242
+ `---
243
+ phase: "01"
244
+ provides:
245
+ - "Direct provides"
246
+ ---
247
+ `
248
+ );
249
+
250
+ const result = runBlueprintTools('history-digest', tmpDir);
251
+ assert.ok(result.success, `Command failed: ${result.error}`);
252
+
253
+ const digest = JSON.parse(result.output);
254
+ assert.deepStrictEqual(
255
+ digest.phases['01'].provides,
256
+ ['Direct provides'],
257
+ 'Direct provides should work'
258
+ );
259
+ });
260
+
261
+ test('inline array syntax supported', () => {
262
+ const phaseDir = path.join(tmpDir, '.blueprint', 'phases', '01-test');
263
+ fs.mkdirSync(phaseDir, { recursive: true });
264
+
265
+ fs.writeFileSync(
266
+ path.join(phaseDir, '01-01-SUMMARY.md'),
267
+ `---
268
+ phase: "01"
269
+ provides: [Feature A, Feature B]
270
+ patterns-established: ["Pattern X", "Pattern Y"]
271
+ ---
272
+ `
273
+ );
274
+
275
+ const result = runBlueprintTools('history-digest', tmpDir);
276
+ assert.ok(result.success, `Command failed: ${result.error}`);
277
+
278
+ const digest = JSON.parse(result.output);
279
+ assert.deepStrictEqual(
280
+ digest.phases['01'].provides.sort(),
281
+ ['Feature A', 'Feature B'],
282
+ 'Inline array should work'
283
+ );
284
+ assert.deepStrictEqual(
285
+ digest.phases['01'].patterns.sort(),
286
+ ['Pattern X', 'Pattern Y'],
287
+ 'Inline quoted array should work'
288
+ );
289
+ });
290
+ });
291
+
292
+ // ─────────────────────────────────────────────────────────────────────────────
293
+ // phases list command
294
+ // ─────────────────────────────────────────────────────────────────────────────
295
+
296
+ describe('phases list command', () => {
297
+ let tmpDir;
298
+
299
+ beforeEach(() => {
300
+ tmpDir = createTempProject();
301
+ });
302
+
303
+ afterEach(() => {
304
+ cleanup(tmpDir);
305
+ });
306
+
307
+ test('empty phases directory returns empty array', () => {
308
+ const result = runBlueprintTools('phases list', tmpDir);
309
+ assert.ok(result.success, `Command failed: ${result.error}`);
310
+
311
+ const output = JSON.parse(result.output);
312
+ assert.deepStrictEqual(output.directories, [], 'directories should be empty');
313
+ assert.strictEqual(output.count, 0, 'count should be 0');
314
+ });
315
+
316
+ test('lists phase directories sorted numerically', () => {
317
+ // Create out-of-order directories
318
+ fs.mkdirSync(path.join(tmpDir, '.blueprint', 'phases', '10-final'), { recursive: true });
319
+ fs.mkdirSync(path.join(tmpDir, '.blueprint', 'phases', '02-api'), { recursive: true });
320
+ fs.mkdirSync(path.join(tmpDir, '.blueprint', 'phases', '01-foundation'), { recursive: true });
321
+
322
+ const result = runBlueprintTools('phases list', tmpDir);
323
+ assert.ok(result.success, `Command failed: ${result.error}`);
324
+
325
+ const output = JSON.parse(result.output);
326
+ assert.strictEqual(output.count, 3, 'should have 3 directories');
327
+ assert.deepStrictEqual(
328
+ output.directories,
329
+ ['01-foundation', '02-api', '10-final'],
330
+ 'should be sorted numerically'
331
+ );
332
+ });
333
+
334
+ test('handles decimal phases in sort order', () => {
335
+ fs.mkdirSync(path.join(tmpDir, '.blueprint', 'phases', '02-api'), { recursive: true });
336
+ fs.mkdirSync(path.join(tmpDir, '.blueprint', 'phases', '02.1-hotfix'), { recursive: true });
337
+ fs.mkdirSync(path.join(tmpDir, '.blueprint', 'phases', '02.2-patch'), { recursive: true });
338
+ fs.mkdirSync(path.join(tmpDir, '.blueprint', 'phases', '03-ui'), { recursive: true });
339
+
340
+ const result = runBlueprintTools('phases list', tmpDir);
341
+ assert.ok(result.success, `Command failed: ${result.error}`);
342
+
343
+ const output = JSON.parse(result.output);
344
+ assert.deepStrictEqual(
345
+ output.directories,
346
+ ['02-api', '02.1-hotfix', '02.2-patch', '03-ui'],
347
+ 'decimal phases should sort correctly between whole numbers'
348
+ );
349
+ });
350
+
351
+ test('--type plans lists only PLAN.md files', () => {
352
+ const phaseDir = path.join(tmpDir, '.blueprint', 'phases', '01-test');
353
+ fs.mkdirSync(phaseDir, { recursive: true });
354
+ fs.writeFileSync(path.join(phaseDir, '01-01-PLAN.md'), '# Plan 1');
355
+ fs.writeFileSync(path.join(phaseDir, '01-02-PLAN.md'), '# Plan 2');
356
+ fs.writeFileSync(path.join(phaseDir, '01-01-SUMMARY.md'), '# Summary');
357
+ fs.writeFileSync(path.join(phaseDir, 'RESEARCH.md'), '# Research');
358
+
359
+ const result = runBlueprintTools('phases list --type plans', tmpDir);
360
+ assert.ok(result.success, `Command failed: ${result.error}`);
361
+
362
+ const output = JSON.parse(result.output);
363
+ assert.deepStrictEqual(
364
+ output.files.sort(),
365
+ ['01-01-PLAN.md', '01-02-PLAN.md'],
366
+ 'should list only PLAN files'
367
+ );
368
+ });
369
+
370
+ test('--type summaries lists only SUMMARY.md files', () => {
371
+ const phaseDir = path.join(tmpDir, '.blueprint', 'phases', '01-test');
372
+ fs.mkdirSync(phaseDir, { recursive: true });
373
+ fs.writeFileSync(path.join(phaseDir, '01-01-PLAN.md'), '# Plan');
374
+ fs.writeFileSync(path.join(phaseDir, '01-01-SUMMARY.md'), '# Summary 1');
375
+ fs.writeFileSync(path.join(phaseDir, '01-02-SUMMARY.md'), '# Summary 2');
376
+
377
+ const result = runBlueprintTools('phases list --type summaries', tmpDir);
378
+ assert.ok(result.success, `Command failed: ${result.error}`);
379
+
380
+ const output = JSON.parse(result.output);
381
+ assert.deepStrictEqual(
382
+ output.files.sort(),
383
+ ['01-01-SUMMARY.md', '01-02-SUMMARY.md'],
384
+ 'should list only SUMMARY files'
385
+ );
386
+ });
387
+
388
+ test('--phase filters to specific phase directory', () => {
389
+ const phase01 = path.join(tmpDir, '.blueprint', 'phases', '01-foundation');
390
+ const phase02 = path.join(tmpDir, '.blueprint', 'phases', '02-api');
391
+ fs.mkdirSync(phase01, { recursive: true });
392
+ fs.mkdirSync(phase02, { recursive: true });
393
+ fs.writeFileSync(path.join(phase01, '01-01-PLAN.md'), '# Plan');
394
+ fs.writeFileSync(path.join(phase02, '02-01-PLAN.md'), '# Plan');
395
+
396
+ const result = runBlueprintTools('phases list --type plans --phase 01', tmpDir);
397
+ assert.ok(result.success, `Command failed: ${result.error}`);
398
+
399
+ const output = JSON.parse(result.output);
400
+ assert.deepStrictEqual(output.files, ['01-01-PLAN.md'], 'should only list phase 01 plans');
401
+ assert.strictEqual(output.phase_dir, 'foundation', 'should report phase name without number prefix');
402
+ });
403
+ });
404
+
405
+ // ─────────────────────────────────────────────────────────────────────────────
406
+ // roadmap get-phase command
407
+ // ─────────────────────────────────────────────────────────────────────────────
408
+
409
+ describe('roadmap get-phase command', () => {
410
+ let tmpDir;
411
+
412
+ beforeEach(() => {
413
+ tmpDir = createTempProject();
414
+ });
415
+
416
+ afterEach(() => {
417
+ cleanup(tmpDir);
418
+ });
419
+
420
+ test('extracts phase section from ROADMAP.md', () => {
421
+ fs.writeFileSync(
422
+ path.join(tmpDir, '.blueprint', 'ROADMAP.md'),
423
+ `# Roadmap v1.0
424
+
425
+ ## Phases
426
+
427
+ ### Phase 1: Foundation
428
+ **Goal:** Set up project infrastructure
429
+ **Plans:** 2 plans
430
+
431
+ Some description here.
432
+
433
+ ### Phase 2: API
434
+ **Goal:** Build REST API
435
+ **Plans:** 3 plans
436
+ `
437
+ );
438
+
439
+ const result = runBlueprintTools('roadmap get-phase 1', tmpDir);
440
+ assert.ok(result.success, `Command failed: ${result.error}`);
441
+
442
+ const output = JSON.parse(result.output);
443
+ assert.strictEqual(output.found, true, 'phase should be found');
444
+ assert.strictEqual(output.phase_number, '1', 'phase number correct');
445
+ assert.strictEqual(output.phase_name, 'Foundation', 'phase name extracted');
446
+ assert.strictEqual(output.goal, 'Set up project infrastructure', 'goal extracted');
447
+ });
448
+
449
+ test('returns not found for missing phase', () => {
450
+ fs.writeFileSync(
451
+ path.join(tmpDir, '.blueprint', 'ROADMAP.md'),
452
+ `# Roadmap v1.0
453
+
454
+ ### Phase 1: Foundation
455
+ **Goal:** Set up project
456
+ `
457
+ );
458
+
459
+ const result = runBlueprintTools('roadmap get-phase 5', tmpDir);
460
+ assert.ok(result.success, `Command failed: ${result.error}`);
461
+
462
+ const output = JSON.parse(result.output);
463
+ assert.strictEqual(output.found, false, 'phase should not be found');
464
+ });
465
+
466
+ test('handles decimal phase numbers', () => {
467
+ fs.writeFileSync(
468
+ path.join(tmpDir, '.blueprint', 'ROADMAP.md'),
469
+ `# Roadmap
470
+
471
+ ### Phase 2: Main
472
+ **Goal:** Main work
473
+
474
+ ### Phase 2.1: Hotfix
475
+ **Goal:** Emergency fix
476
+ `
477
+ );
478
+
479
+ const result = runBlueprintTools('roadmap get-phase 2.1', tmpDir);
480
+ assert.ok(result.success, `Command failed: ${result.error}`);
481
+
482
+ const output = JSON.parse(result.output);
483
+ assert.strictEqual(output.found, true, 'decimal phase should be found');
484
+ assert.strictEqual(output.phase_name, 'Hotfix', 'phase name correct');
485
+ assert.strictEqual(output.goal, 'Emergency fix', 'goal extracted');
486
+ });
487
+
488
+ test('extracts full section content', () => {
489
+ fs.writeFileSync(
490
+ path.join(tmpDir, '.blueprint', 'ROADMAP.md'),
491
+ `# Roadmap
492
+
493
+ ### Phase 1: Setup
494
+ **Goal:** Initialize everything
495
+
496
+ This phase covers:
497
+ - Database setup
498
+ - Auth configuration
499
+ - CI/CD pipeline
500
+
501
+ ### Phase 2: Build
502
+ **Goal:** Build features
503
+ `
504
+ );
505
+
506
+ const result = runBlueprintTools('roadmap get-phase 1', tmpDir);
507
+ assert.ok(result.success, `Command failed: ${result.error}`);
508
+
509
+ const output = JSON.parse(result.output);
510
+ assert.ok(output.section.includes('Database setup'), 'section includes description');
511
+ assert.ok(output.section.includes('CI/CD pipeline'), 'section includes all bullets');
512
+ assert.ok(!output.section.includes('Phase 2'), 'section does not include next phase');
513
+ });
514
+
515
+ test('handles missing ROADMAP.md gracefully', () => {
516
+ const result = runBlueprintTools('roadmap get-phase 1', tmpDir);
517
+ assert.ok(result.success, `Command failed: ${result.error}`);
518
+
519
+ const output = JSON.parse(result.output);
520
+ assert.strictEqual(output.found, false, 'should return not found');
521
+ assert.strictEqual(output.error, 'ROADMAP.md not found', 'should explain why');
522
+ });
523
+ });
524
+
525
+ // ─────────────────────────────────────────────────────────────────────────────
526
+ // phase next-decimal command
527
+ // ─────────────────────────────────────────────────────────────────────────────
528
+
529
+ describe('phase next-decimal command', () => {
530
+ let tmpDir;
531
+
532
+ beforeEach(() => {
533
+ tmpDir = createTempProject();
534
+ });
535
+
536
+ afterEach(() => {
537
+ cleanup(tmpDir);
538
+ });
539
+
540
+ test('returns X.1 when no decimal phases exist', () => {
541
+ fs.mkdirSync(path.join(tmpDir, '.blueprint', 'phases', '06-feature'), { recursive: true });
542
+ fs.mkdirSync(path.join(tmpDir, '.blueprint', 'phases', '07-next'), { recursive: true });
543
+
544
+ const result = runBlueprintTools('phase next-decimal 06', tmpDir);
545
+ assert.ok(result.success, `Command failed: ${result.error}`);
546
+
547
+ const output = JSON.parse(result.output);
548
+ assert.strictEqual(output.next, '06.1', 'should return 06.1');
549
+ assert.deepStrictEqual(output.existing, [], 'no existing decimals');
550
+ });
551
+
552
+ test('increments from existing decimal phases', () => {
553
+ fs.mkdirSync(path.join(tmpDir, '.blueprint', 'phases', '06-feature'), { recursive: true });
554
+ fs.mkdirSync(path.join(tmpDir, '.blueprint', 'phases', '06.1-hotfix'), { recursive: true });
555
+ fs.mkdirSync(path.join(tmpDir, '.blueprint', 'phases', '06.2-patch'), { recursive: true });
556
+
557
+ const result = runBlueprintTools('phase next-decimal 06', tmpDir);
558
+ assert.ok(result.success, `Command failed: ${result.error}`);
559
+
560
+ const output = JSON.parse(result.output);
561
+ assert.strictEqual(output.next, '06.3', 'should return 06.3');
562
+ assert.deepStrictEqual(output.existing, ['06.1', '06.2'], 'lists existing decimals');
563
+ });
564
+
565
+ test('handles gaps in decimal sequence', () => {
566
+ fs.mkdirSync(path.join(tmpDir, '.blueprint', 'phases', '06-feature'), { recursive: true });
567
+ fs.mkdirSync(path.join(tmpDir, '.blueprint', 'phases', '06.1-first'), { recursive: true });
568
+ fs.mkdirSync(path.join(tmpDir, '.blueprint', 'phases', '06.3-third'), { recursive: true });
569
+
570
+ const result = runBlueprintTools('phase next-decimal 06', tmpDir);
571
+ assert.ok(result.success, `Command failed: ${result.error}`);
572
+
573
+ const output = JSON.parse(result.output);
574
+ // Should take next after highest, not fill gap
575
+ assert.strictEqual(output.next, '06.4', 'should return 06.4, not fill gap at 06.2');
576
+ });
577
+
578
+ test('handles single-digit phase input', () => {
579
+ fs.mkdirSync(path.join(tmpDir, '.blueprint', 'phases', '06-feature'), { recursive: true });
580
+
581
+ const result = runBlueprintTools('phase next-decimal 6', tmpDir);
582
+ assert.ok(result.success, `Command failed: ${result.error}`);
583
+
584
+ const output = JSON.parse(result.output);
585
+ assert.strictEqual(output.next, '06.1', 'should normalize to 06.1');
586
+ assert.strictEqual(output.base_phase, '06', 'base phase should be padded');
587
+ });
588
+
589
+ test('returns error if base phase does not exist', () => {
590
+ fs.mkdirSync(path.join(tmpDir, '.blueprint', 'phases', '01-start'), { recursive: true });
591
+
592
+ const result = runBlueprintTools('phase next-decimal 06', tmpDir);
593
+ assert.ok(result.success, `Command should succeed: ${result.error}`);
594
+
595
+ const output = JSON.parse(result.output);
596
+ assert.strictEqual(output.found, false, 'base phase not found');
597
+ assert.strictEqual(output.next, '06.1', 'should still suggest 06.1');
598
+ });
599
+ });
600
+
601
+ // ─────────────────────────────────────────────────────────────────────────────
602
+ // phase-plan-index command
603
+ // ─────────────────────────────────────────────────────────────────────────────
604
+
605
+ describe('phase-plan-index command', () => {
606
+ let tmpDir;
607
+
608
+ beforeEach(() => {
609
+ tmpDir = createTempProject();
610
+ });
611
+
612
+ afterEach(() => {
613
+ cleanup(tmpDir);
614
+ });
615
+
616
+ test('empty phase directory returns empty plans array', () => {
617
+ fs.mkdirSync(path.join(tmpDir, '.blueprint', 'phases', '03-api'), { recursive: true });
618
+
619
+ const result = runBlueprintTools('phase-plan-index 03', tmpDir);
620
+ assert.ok(result.success, `Command failed: ${result.error}`);
621
+
622
+ const output = JSON.parse(result.output);
623
+ assert.strictEqual(output.phase, '03', 'phase number correct');
624
+ assert.deepStrictEqual(output.plans, [], 'plans should be empty');
625
+ assert.deepStrictEqual(output.waves, {}, 'waves should be empty');
626
+ assert.deepStrictEqual(output.incomplete, [], 'incomplete should be empty');
627
+ assert.strictEqual(output.has_checkpoints, false, 'no checkpoints');
628
+ });
629
+
630
+ test('extracts single plan with frontmatter', () => {
631
+ const phaseDir = path.join(tmpDir, '.blueprint', 'phases', '03-api');
632
+ fs.mkdirSync(phaseDir, { recursive: true });
633
+
634
+ fs.writeFileSync(
635
+ path.join(phaseDir, '03-01-PLAN.md'),
636
+ `---
637
+ wave: 1
638
+ autonomous: true
639
+ objective: Set up database schema
640
+ files-modified: [prisma/schema.prisma, src/lib/db.ts]
641
+ ---
642
+
643
+ ## Task 1: Create schema
644
+ ## Task 2: Generate client
645
+ `
646
+ );
647
+
648
+ const result = runBlueprintTools('phase-plan-index 03', tmpDir);
649
+ assert.ok(result.success, `Command failed: ${result.error}`);
650
+
651
+ const output = JSON.parse(result.output);
652
+ assert.strictEqual(output.plans.length, 1, 'should have 1 plan');
653
+ assert.strictEqual(output.plans[0].id, '03-01', 'plan id correct');
654
+ assert.strictEqual(output.plans[0].wave, 1, 'wave extracted');
655
+ assert.strictEqual(output.plans[0].autonomous, true, 'autonomous extracted');
656
+ assert.strictEqual(output.plans[0].objective, 'Set up database schema', 'objective extracted');
657
+ assert.deepStrictEqual(output.plans[0].files_modified, ['prisma/schema.prisma', 'src/lib/db.ts'], 'files extracted');
658
+ assert.strictEqual(output.plans[0].task_count, 2, 'task count correct');
659
+ assert.strictEqual(output.plans[0].has_summary, false, 'no summary yet');
660
+ });
661
+
662
+ test('groups multiple plans by wave', () => {
663
+ const phaseDir = path.join(tmpDir, '.blueprint', 'phases', '03-api');
664
+ fs.mkdirSync(phaseDir, { recursive: true });
665
+
666
+ fs.writeFileSync(
667
+ path.join(phaseDir, '03-01-PLAN.md'),
668
+ `---
669
+ wave: 1
670
+ autonomous: true
671
+ objective: Database setup
672
+ ---
673
+
674
+ ## Task 1: Schema
675
+ `
676
+ );
677
+
678
+ fs.writeFileSync(
679
+ path.join(phaseDir, '03-02-PLAN.md'),
680
+ `---
681
+ wave: 1
682
+ autonomous: true
683
+ objective: Auth setup
684
+ ---
685
+
686
+ ## Task 1: JWT
687
+ `
688
+ );
689
+
690
+ fs.writeFileSync(
691
+ path.join(phaseDir, '03-03-PLAN.md'),
692
+ `---
693
+ wave: 2
694
+ autonomous: false
695
+ objective: API routes
696
+ ---
697
+
698
+ ## Task 1: Routes
699
+ `
700
+ );
701
+
702
+ const result = runBlueprintTools('phase-plan-index 03', tmpDir);
703
+ assert.ok(result.success, `Command failed: ${result.error}`);
704
+
705
+ const output = JSON.parse(result.output);
706
+ assert.strictEqual(output.plans.length, 3, 'should have 3 plans');
707
+ assert.deepStrictEqual(output.waves['1'], ['03-01', '03-02'], 'wave 1 has 2 plans');
708
+ assert.deepStrictEqual(output.waves['2'], ['03-03'], 'wave 2 has 1 plan');
709
+ });
710
+
711
+ test('detects incomplete plans (no matching summary)', () => {
712
+ const phaseDir = path.join(tmpDir, '.blueprint', 'phases', '03-api');
713
+ fs.mkdirSync(phaseDir, { recursive: true });
714
+
715
+ // Plan with summary
716
+ fs.writeFileSync(path.join(phaseDir, '03-01-PLAN.md'), `---\nwave: 1\n---\n## Task 1`);
717
+ fs.writeFileSync(path.join(phaseDir, '03-01-SUMMARY.md'), `# Summary`);
718
+
719
+ // Plan without summary
720
+ fs.writeFileSync(path.join(phaseDir, '03-02-PLAN.md'), `---\nwave: 2\n---\n## Task 1`);
721
+
722
+ const result = runBlueprintTools('phase-plan-index 03', tmpDir);
723
+ assert.ok(result.success, `Command failed: ${result.error}`);
724
+
725
+ const output = JSON.parse(result.output);
726
+ assert.strictEqual(output.plans[0].has_summary, true, 'first plan has summary');
727
+ assert.strictEqual(output.plans[1].has_summary, false, 'second plan has no summary');
728
+ assert.deepStrictEqual(output.incomplete, ['03-02'], 'incomplete list correct');
729
+ });
730
+
731
+ test('detects checkpoints (autonomous: false)', () => {
732
+ const phaseDir = path.join(tmpDir, '.blueprint', 'phases', '03-api');
733
+ fs.mkdirSync(phaseDir, { recursive: true });
734
+
735
+ fs.writeFileSync(
736
+ path.join(phaseDir, '03-01-PLAN.md'),
737
+ `---
738
+ wave: 1
739
+ autonomous: false
740
+ objective: Manual review needed
741
+ ---
742
+
743
+ ## Task 1: Review
744
+ `
745
+ );
746
+
747
+ const result = runBlueprintTools('phase-plan-index 03', tmpDir);
748
+ assert.ok(result.success, `Command failed: ${result.error}`);
749
+
750
+ const output = JSON.parse(result.output);
751
+ assert.strictEqual(output.has_checkpoints, true, 'should detect checkpoint');
752
+ assert.strictEqual(output.plans[0].autonomous, false, 'plan marked non-autonomous');
753
+ });
754
+
755
+ test('phase not found returns error', () => {
756
+ const result = runBlueprintTools('phase-plan-index 99', tmpDir);
757
+ assert.ok(result.success, `Command should succeed: ${result.error}`);
758
+
759
+ const output = JSON.parse(result.output);
760
+ assert.strictEqual(output.error, 'Phase not found', 'should report phase not found');
761
+ });
762
+ });
763
+
764
+ // ─────────────────────────────────────────────────────────────────────────────
765
+ // state-snapshot command
766
+ // ─────────────────────────────────────────────────────────────────────────────
767
+
768
+ describe('state-snapshot command', () => {
769
+ let tmpDir;
770
+
771
+ beforeEach(() => {
772
+ tmpDir = createTempProject();
773
+ });
774
+
775
+ afterEach(() => {
776
+ cleanup(tmpDir);
777
+ });
778
+
779
+ test('missing STATE.md returns error', () => {
780
+ const result = runBlueprintTools('state-snapshot', tmpDir);
781
+ assert.ok(result.success, `Command should succeed: ${result.error}`);
782
+
783
+ const output = JSON.parse(result.output);
784
+ assert.strictEqual(output.error, 'STATE.md not found', 'should report missing file');
785
+ });
786
+
787
+ test('extracts basic fields from STATE.md', () => {
788
+ fs.writeFileSync(
789
+ path.join(tmpDir, '.blueprint', 'STATE.md'),
790
+ `# Project State
791
+
792
+ **Current Phase:** 03
793
+ **Current Phase Name:** API Layer
794
+ **Total Phases:** 6
795
+ **Current Plan:** 03-02
796
+ **Total Plans in Phase:** 3
797
+ **Status:** In progress
798
+ **Progress:** 45%
799
+ **Last Activity:** 2024-01-15
800
+ **Last Activity Description:** Completed 03-01-PLAN.md
801
+ `
802
+ );
803
+
804
+ const result = runBlueprintTools('state-snapshot', tmpDir);
805
+ assert.ok(result.success, `Command failed: ${result.error}`);
806
+
807
+ const output = JSON.parse(result.output);
808
+ assert.strictEqual(output.current_phase, '03', 'current phase extracted');
809
+ assert.strictEqual(output.current_phase_name, 'API Layer', 'phase name extracted');
810
+ assert.strictEqual(output.total_phases, 6, 'total phases extracted');
811
+ assert.strictEqual(output.current_plan, '03-02', 'current plan extracted');
812
+ assert.strictEqual(output.total_plans_in_phase, 3, 'total plans extracted');
813
+ assert.strictEqual(output.status, 'In progress', 'status extracted');
814
+ assert.strictEqual(output.progress_percent, 45, 'progress extracted');
815
+ assert.strictEqual(output.last_activity, '2024-01-15', 'last activity date extracted');
816
+ });
817
+
818
+ test('extracts decisions table', () => {
819
+ fs.writeFileSync(
820
+ path.join(tmpDir, '.blueprint', 'STATE.md'),
821
+ `# Project State
822
+
823
+ **Current Phase:** 01
824
+
825
+ ## Decisions Made
826
+
827
+ | Phase | Decision | Rationale |
828
+ |-------|----------|-----------|
829
+ | 01 | Use Prisma | Better DX than raw SQL |
830
+ | 02 | JWT auth | Stateless authentication |
831
+ `
832
+ );
833
+
834
+ const result = runBlueprintTools('state-snapshot', tmpDir);
835
+ assert.ok(result.success, `Command failed: ${result.error}`);
836
+
837
+ const output = JSON.parse(result.output);
838
+ assert.strictEqual(output.decisions.length, 2, 'should have 2 decisions');
839
+ assert.strictEqual(output.decisions[0].phase, '01', 'first decision phase');
840
+ assert.strictEqual(output.decisions[0].summary, 'Use Prisma', 'first decision summary');
841
+ assert.strictEqual(output.decisions[0].rationale, 'Better DX than raw SQL', 'first decision rationale');
842
+ });
843
+
844
+ test('extracts blockers list', () => {
845
+ fs.writeFileSync(
846
+ path.join(tmpDir, '.blueprint', 'STATE.md'),
847
+ `# Project State
848
+
849
+ **Current Phase:** 03
850
+
851
+ ## Blockers
852
+
853
+ - Waiting for API credentials
854
+ - Need design review for dashboard
855
+ `
856
+ );
857
+
858
+ const result = runBlueprintTools('state-snapshot', tmpDir);
859
+ assert.ok(result.success, `Command failed: ${result.error}`);
860
+
861
+ const output = JSON.parse(result.output);
862
+ assert.deepStrictEqual(output.blockers, [
863
+ 'Waiting for API credentials',
864
+ 'Need design review for dashboard',
865
+ ], 'blockers extracted');
866
+ });
867
+
868
+ test('extracts session continuity info', () => {
869
+ fs.writeFileSync(
870
+ path.join(tmpDir, '.blueprint', 'STATE.md'),
871
+ `# Project State
872
+
873
+ **Current Phase:** 03
874
+
875
+ ## Session
876
+
877
+ **Last Date:** 2024-01-15
878
+ **Stopped At:** Phase 3, Plan 2, Task 1
879
+ **Resume File:** .blueprint/phases/03-api/03-02-PLAN.md
880
+ `
881
+ );
882
+
883
+ const result = runBlueprintTools('state-snapshot', tmpDir);
884
+ assert.ok(result.success, `Command failed: ${result.error}`);
885
+
886
+ const output = JSON.parse(result.output);
887
+ assert.strictEqual(output.session.last_date, '2024-01-15', 'session date extracted');
888
+ assert.strictEqual(output.session.stopped_at, 'Phase 3, Plan 2, Task 1', 'stopped at extracted');
889
+ assert.strictEqual(output.session.resume_file, '.blueprint/phases/03-api/03-02-PLAN.md', 'resume file extracted');
890
+ });
891
+
892
+ test('handles paused_at field', () => {
893
+ fs.writeFileSync(
894
+ path.join(tmpDir, '.blueprint', 'STATE.md'),
895
+ `# Project State
896
+
897
+ **Current Phase:** 03
898
+ **Paused At:** Phase 3, Plan 1, Task 2 - mid-implementation
899
+ `
900
+ );
901
+
902
+ const result = runBlueprintTools('state-snapshot', tmpDir);
903
+ assert.ok(result.success, `Command failed: ${result.error}`);
904
+
905
+ const output = JSON.parse(result.output);
906
+ assert.strictEqual(output.paused_at, 'Phase 3, Plan 1, Task 2 - mid-implementation', 'paused_at extracted');
907
+ });
908
+ });
909
+
910
+ // ─────────────────────────────────────────────────────────────────────────────
911
+ // summary-extract command
912
+ // ─────────────────────────────────────────────────────────────────────────────
913
+
914
+ describe('summary-extract command', () => {
915
+ let tmpDir;
916
+
917
+ beforeEach(() => {
918
+ tmpDir = createTempProject();
919
+ });
920
+
921
+ afterEach(() => {
922
+ cleanup(tmpDir);
923
+ });
924
+
925
+ test('missing file returns error', () => {
926
+ const result = runBlueprintTools('summary-extract .blueprint/phases/01-test/01-01-SUMMARY.md', tmpDir);
927
+ assert.ok(result.success, `Command should succeed: ${result.error}`);
928
+
929
+ const output = JSON.parse(result.output);
930
+ assert.strictEqual(output.error, 'File not found', 'should report missing file');
931
+ });
932
+
933
+ test('extracts all fields from SUMMARY.md', () => {
934
+ const phaseDir = path.join(tmpDir, '.blueprint', 'phases', '01-foundation');
935
+ fs.mkdirSync(phaseDir, { recursive: true });
936
+
937
+ fs.writeFileSync(
938
+ path.join(phaseDir, '01-01-SUMMARY.md'),
939
+ `---
940
+ one-liner: Set up Prisma with User and Project models
941
+ key-files:
942
+ - prisma/schema.prisma
943
+ - src/lib/db.ts
944
+ tech-stack:
945
+ added:
946
+ - prisma
947
+ - zod
948
+ patterns-established:
949
+ - Repository pattern
950
+ - Dependency injection
951
+ key-decisions:
952
+ - Use Prisma over Drizzle: Better DX and ecosystem
953
+ - Single database: Start simple, shard later
954
+ ---
955
+
956
+ # Summary
957
+
958
+ Full summary content here.
959
+ `
960
+ );
961
+
962
+ const result = runBlueprintTools('summary-extract .blueprint/phases/01-foundation/01-01-SUMMARY.md', tmpDir);
963
+ assert.ok(result.success, `Command failed: ${result.error}`);
964
+
965
+ const output = JSON.parse(result.output);
966
+ assert.strictEqual(output.path, '.blueprint/phases/01-foundation/01-01-SUMMARY.md', 'path correct');
967
+ assert.strictEqual(output.one_liner, 'Set up Prisma with User and Project models', 'one-liner extracted');
968
+ assert.deepStrictEqual(output.key_files, ['prisma/schema.prisma', 'src/lib/db.ts'], 'key files extracted');
969
+ assert.deepStrictEqual(output.tech_added, ['prisma', 'zod'], 'tech added extracted');
970
+ assert.deepStrictEqual(output.patterns, ['Repository pattern', 'Dependency injection'], 'patterns extracted');
971
+ assert.strictEqual(output.decisions.length, 2, 'decisions extracted');
972
+ });
973
+
974
+ test('selective extraction with --fields', () => {
975
+ const phaseDir = path.join(tmpDir, '.blueprint', 'phases', '01-foundation');
976
+ fs.mkdirSync(phaseDir, { recursive: true });
977
+
978
+ fs.writeFileSync(
979
+ path.join(phaseDir, '01-01-SUMMARY.md'),
980
+ `---
981
+ one-liner: Set up database
982
+ key-files:
983
+ - prisma/schema.prisma
984
+ tech-stack:
985
+ added:
986
+ - prisma
987
+ patterns-established:
988
+ - Repository pattern
989
+ key-decisions:
990
+ - Use Prisma: Better DX
991
+ ---
992
+ `
993
+ );
994
+
995
+ const result = runBlueprintTools('summary-extract .blueprint/phases/01-foundation/01-01-SUMMARY.md --fields one_liner,key_files', tmpDir);
996
+ assert.ok(result.success, `Command failed: ${result.error}`);
997
+
998
+ const output = JSON.parse(result.output);
999
+ assert.strictEqual(output.one_liner, 'Set up database', 'one_liner included');
1000
+ assert.deepStrictEqual(output.key_files, ['prisma/schema.prisma'], 'key_files included');
1001
+ assert.strictEqual(output.tech_added, undefined, 'tech_added excluded');
1002
+ assert.strictEqual(output.patterns, undefined, 'patterns excluded');
1003
+ assert.strictEqual(output.decisions, undefined, 'decisions excluded');
1004
+ });
1005
+
1006
+ test('handles missing frontmatter fields gracefully', () => {
1007
+ const phaseDir = path.join(tmpDir, '.blueprint', 'phases', '01-foundation');
1008
+ fs.mkdirSync(phaseDir, { recursive: true });
1009
+
1010
+ fs.writeFileSync(
1011
+ path.join(phaseDir, '01-01-SUMMARY.md'),
1012
+ `---
1013
+ one-liner: Minimal summary
1014
+ ---
1015
+
1016
+ # Summary
1017
+ `
1018
+ );
1019
+
1020
+ const result = runBlueprintTools('summary-extract .blueprint/phases/01-foundation/01-01-SUMMARY.md', tmpDir);
1021
+ assert.ok(result.success, `Command failed: ${result.error}`);
1022
+
1023
+ const output = JSON.parse(result.output);
1024
+ assert.strictEqual(output.one_liner, 'Minimal summary', 'one-liner extracted');
1025
+ assert.deepStrictEqual(output.key_files, [], 'key_files defaults to empty');
1026
+ assert.deepStrictEqual(output.tech_added, [], 'tech_added defaults to empty');
1027
+ assert.deepStrictEqual(output.patterns, [], 'patterns defaults to empty');
1028
+ assert.deepStrictEqual(output.decisions, [], 'decisions defaults to empty');
1029
+ });
1030
+
1031
+ test('parses key-decisions with rationale', () => {
1032
+ const phaseDir = path.join(tmpDir, '.blueprint', 'phases', '01-foundation');
1033
+ fs.mkdirSync(phaseDir, { recursive: true });
1034
+
1035
+ fs.writeFileSync(
1036
+ path.join(phaseDir, '01-01-SUMMARY.md'),
1037
+ `---
1038
+ key-decisions:
1039
+ - Use Prisma: Better DX than alternatives
1040
+ - JWT tokens: Stateless auth for scalability
1041
+ ---
1042
+ `
1043
+ );
1044
+
1045
+ const result = runBlueprintTools('summary-extract .blueprint/phases/01-foundation/01-01-SUMMARY.md', tmpDir);
1046
+ assert.ok(result.success, `Command failed: ${result.error}`);
1047
+
1048
+ const output = JSON.parse(result.output);
1049
+ assert.strictEqual(output.decisions[0].summary, 'Use Prisma', 'decision summary parsed');
1050
+ assert.strictEqual(output.decisions[0].rationale, 'Better DX than alternatives', 'decision rationale parsed');
1051
+ assert.strictEqual(output.decisions[1].summary, 'JWT tokens', 'second decision summary');
1052
+ assert.strictEqual(output.decisions[1].rationale, 'Stateless auth for scalability', 'second decision rationale');
1053
+ });
1054
+ });
1055
+
1056
+ // ─────────────────────────────────────────────────────────────────────────────
1057
+ // init --include flag tests
1058
+ // ─────────────────────────────────────────────────────────────────────────────
1059
+
1060
+ describe('init commands with --include flag', () => {
1061
+ let tmpDir;
1062
+
1063
+ beforeEach(() => {
1064
+ tmpDir = createTempProject();
1065
+ });
1066
+
1067
+ afterEach(() => {
1068
+ cleanup(tmpDir);
1069
+ });
1070
+
1071
+ test('init execute-phase includes state and config content', () => {
1072
+ const phaseDir = path.join(tmpDir, '.blueprint', 'phases', '03-api');
1073
+ fs.mkdirSync(phaseDir, { recursive: true });
1074
+ fs.writeFileSync(path.join(phaseDir, '03-01-PLAN.md'), '# Plan');
1075
+ fs.writeFileSync(
1076
+ path.join(tmpDir, '.blueprint', 'STATE.md'),
1077
+ '# State\n\n**Current Phase:** 03\n**Status:** In progress'
1078
+ );
1079
+ fs.writeFileSync(
1080
+ path.join(tmpDir, '.blueprint', 'config.json'),
1081
+ JSON.stringify({ model_profile: 'balanced' })
1082
+ );
1083
+
1084
+ const result = runBlueprintTools('init execute-phase 03 --include state,config', tmpDir);
1085
+ assert.ok(result.success, `Command failed: ${result.error}`);
1086
+
1087
+ const output = JSON.parse(result.output);
1088
+ assert.ok(output.state_content, 'state_content should be included');
1089
+ assert.ok(output.state_content.includes('Current Phase'), 'state content correct');
1090
+ assert.ok(output.config_content, 'config_content should be included');
1091
+ assert.ok(output.config_content.includes('model_profile'), 'config content correct');
1092
+ });
1093
+
1094
+ test('init execute-phase without --include omits content', () => {
1095
+ const phaseDir = path.join(tmpDir, '.blueprint', 'phases', '03-api');
1096
+ fs.mkdirSync(phaseDir, { recursive: true });
1097
+ fs.writeFileSync(path.join(phaseDir, '03-01-PLAN.md'), '# Plan');
1098
+ fs.writeFileSync(path.join(tmpDir, '.blueprint', 'STATE.md'), '# State');
1099
+
1100
+ const result = runBlueprintTools('init execute-phase 03', tmpDir);
1101
+ assert.ok(result.success, `Command failed: ${result.error}`);
1102
+
1103
+ const output = JSON.parse(result.output);
1104
+ assert.strictEqual(output.state_content, undefined, 'state_content should be omitted');
1105
+ assert.strictEqual(output.config_content, undefined, 'config_content should be omitted');
1106
+ });
1107
+
1108
+ test('init plan-phase includes multiple file contents', () => {
1109
+ const phaseDir = path.join(tmpDir, '.blueprint', 'phases', '03-api');
1110
+ fs.mkdirSync(phaseDir, { recursive: true });
1111
+ fs.writeFileSync(path.join(tmpDir, '.blueprint', 'STATE.md'), '# Project State');
1112
+ fs.writeFileSync(path.join(tmpDir, '.blueprint', 'ROADMAP.md'), '# Roadmap v1.0');
1113
+ fs.writeFileSync(path.join(tmpDir, '.blueprint', 'REQUIREMENTS.md'), '# Requirements');
1114
+ fs.writeFileSync(path.join(phaseDir, '03-CONTEXT.md'), '# Phase Context');
1115
+ fs.writeFileSync(path.join(phaseDir, '03-RESEARCH.md'), '# Research Findings');
1116
+
1117
+ const result = runBlueprintTools('init plan-phase 03 --include state,roadmap,requirements,context,research', tmpDir);
1118
+ assert.ok(result.success, `Command failed: ${result.error}`);
1119
+
1120
+ const output = JSON.parse(result.output);
1121
+ assert.ok(output.state_content, 'state_content included');
1122
+ assert.ok(output.state_content.includes('Project State'), 'state content correct');
1123
+ assert.ok(output.roadmap_content, 'roadmap_content included');
1124
+ assert.ok(output.roadmap_content.includes('Roadmap v1.0'), 'roadmap content correct');
1125
+ assert.ok(output.requirements_content, 'requirements_content included');
1126
+ assert.ok(output.context_content, 'context_content included');
1127
+ assert.ok(output.research_content, 'research_content included');
1128
+ });
1129
+
1130
+ test('init plan-phase includes verification and uat content', () => {
1131
+ const phaseDir = path.join(tmpDir, '.blueprint', 'phases', '03-api');
1132
+ fs.mkdirSync(phaseDir, { recursive: true });
1133
+ fs.writeFileSync(path.join(phaseDir, '03-VERIFICATION.md'), '# Verification Results');
1134
+ fs.writeFileSync(path.join(phaseDir, '03-UAT.md'), '# UAT Findings');
1135
+
1136
+ const result = runBlueprintTools('init plan-phase 03 --include verification,uat', tmpDir);
1137
+ assert.ok(result.success, `Command failed: ${result.error}`);
1138
+
1139
+ const output = JSON.parse(result.output);
1140
+ assert.ok(output.verification_content, 'verification_content included');
1141
+ assert.ok(output.verification_content.includes('Verification Results'), 'verification content correct');
1142
+ assert.ok(output.uat_content, 'uat_content included');
1143
+ assert.ok(output.uat_content.includes('UAT Findings'), 'uat content correct');
1144
+ });
1145
+
1146
+ test('init progress includes state, roadmap, project, config', () => {
1147
+ fs.writeFileSync(path.join(tmpDir, '.blueprint', 'STATE.md'), '# State');
1148
+ fs.writeFileSync(path.join(tmpDir, '.blueprint', 'ROADMAP.md'), '# Roadmap');
1149
+ fs.writeFileSync(path.join(tmpDir, '.blueprint', 'PROJECT.md'), '# Project');
1150
+ fs.writeFileSync(
1151
+ path.join(tmpDir, '.blueprint', 'config.json'),
1152
+ JSON.stringify({ model_profile: 'quality' })
1153
+ );
1154
+
1155
+ const result = runBlueprintTools('init progress --include state,roadmap,project,config', tmpDir);
1156
+ assert.ok(result.success, `Command failed: ${result.error}`);
1157
+
1158
+ const output = JSON.parse(result.output);
1159
+ assert.ok(output.state_content, 'state_content included');
1160
+ assert.ok(output.roadmap_content, 'roadmap_content included');
1161
+ assert.ok(output.project_content, 'project_content included');
1162
+ assert.ok(output.config_content, 'config_content included');
1163
+ });
1164
+
1165
+ test('missing files return null in content fields', () => {
1166
+ const phaseDir = path.join(tmpDir, '.blueprint', 'phases', '03-api');
1167
+ fs.mkdirSync(phaseDir, { recursive: true });
1168
+ fs.writeFileSync(path.join(phaseDir, '03-01-PLAN.md'), '# Plan');
1169
+
1170
+ const result = runBlueprintTools('init execute-phase 03 --include state,config', tmpDir);
1171
+ assert.ok(result.success, `Command failed: ${result.error}`);
1172
+
1173
+ const output = JSON.parse(result.output);
1174
+ assert.strictEqual(output.state_content, null, 'missing state returns null');
1175
+ assert.strictEqual(output.config_content, null, 'missing config returns null');
1176
+ });
1177
+
1178
+ test('partial includes work correctly', () => {
1179
+ const phaseDir = path.join(tmpDir, '.blueprint', 'phases', '03-api');
1180
+ fs.mkdirSync(phaseDir, { recursive: true });
1181
+ fs.writeFileSync(path.join(phaseDir, '03-01-PLAN.md'), '# Plan');
1182
+ fs.writeFileSync(path.join(tmpDir, '.blueprint', 'STATE.md'), '# State');
1183
+ fs.writeFileSync(path.join(tmpDir, '.blueprint', 'ROADMAP.md'), '# Roadmap');
1184
+
1185
+ // Only request state, not roadmap
1186
+ const result = runBlueprintTools('init execute-phase 03 --include state', tmpDir);
1187
+ assert.ok(result.success, `Command failed: ${result.error}`);
1188
+
1189
+ const output = JSON.parse(result.output);
1190
+ assert.ok(output.state_content, 'state_content included');
1191
+ assert.strictEqual(output.roadmap_content, undefined, 'roadmap_content not requested, should be undefined');
1192
+ });
1193
+ });
1194
+
1195
+ // ─────────────────────────────────────────────────────────────────────────────
1196
+ // roadmap analyze command
1197
+ // ─────────────────────────────────────────────────────────────────────────────
1198
+
1199
+ describe('roadmap analyze command', () => {
1200
+ let tmpDir;
1201
+
1202
+ beforeEach(() => {
1203
+ tmpDir = createTempProject();
1204
+ });
1205
+
1206
+ afterEach(() => {
1207
+ cleanup(tmpDir);
1208
+ });
1209
+
1210
+ test('missing ROADMAP.md returns error', () => {
1211
+ const result = runBlueprintTools('roadmap analyze', tmpDir);
1212
+ assert.ok(result.success, `Command should succeed: ${result.error}`);
1213
+
1214
+ const output = JSON.parse(result.output);
1215
+ assert.strictEqual(output.error, 'ROADMAP.md not found');
1216
+ });
1217
+
1218
+ test('parses phases with goals and disk status', () => {
1219
+ fs.writeFileSync(
1220
+ path.join(tmpDir, '.blueprint', 'ROADMAP.md'),
1221
+ `# Roadmap v1.0
1222
+
1223
+ ### Phase 1: Foundation
1224
+ **Goal:** Set up infrastructure
1225
+
1226
+ ### Phase 2: Authentication
1227
+ **Goal:** Add user auth
1228
+
1229
+ ### Phase 3: Features
1230
+ **Goal:** Build core features
1231
+ `
1232
+ );
1233
+
1234
+ // Create phase dirs with varying completion
1235
+ const p1 = path.join(tmpDir, '.blueprint', 'phases', '01-foundation');
1236
+ fs.mkdirSync(p1, { recursive: true });
1237
+ fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan');
1238
+ fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Summary');
1239
+
1240
+ const p2 = path.join(tmpDir, '.blueprint', 'phases', '02-authentication');
1241
+ fs.mkdirSync(p2, { recursive: true });
1242
+ fs.writeFileSync(path.join(p2, '02-01-PLAN.md'), '# Plan');
1243
+
1244
+ const result = runBlueprintTools('roadmap analyze', tmpDir);
1245
+ assert.ok(result.success, `Command failed: ${result.error}`);
1246
+
1247
+ const output = JSON.parse(result.output);
1248
+ assert.strictEqual(output.phase_count, 3, 'should find 3 phases');
1249
+ assert.strictEqual(output.phases[0].disk_status, 'complete', 'phase 1 complete');
1250
+ assert.strictEqual(output.phases[1].disk_status, 'planned', 'phase 2 planned');
1251
+ assert.strictEqual(output.phases[2].disk_status, 'no_directory', 'phase 3 no directory');
1252
+ assert.strictEqual(output.completed_phases, 1, '1 phase complete');
1253
+ assert.strictEqual(output.total_plans, 2, '2 total plans');
1254
+ assert.strictEqual(output.total_summaries, 1, '1 total summary');
1255
+ assert.strictEqual(output.progress_percent, 50, '50% complete');
1256
+ assert.strictEqual(output.current_phase, '2', 'current phase is 2');
1257
+ });
1258
+
1259
+ test('extracts goals and dependencies', () => {
1260
+ fs.writeFileSync(
1261
+ path.join(tmpDir, '.blueprint', 'ROADMAP.md'),
1262
+ `# Roadmap
1263
+
1264
+ ### Phase 1: Setup
1265
+ **Goal:** Initialize project
1266
+ **Depends on:** Nothing
1267
+
1268
+ ### Phase 2: Build
1269
+ **Goal:** Build features
1270
+ **Depends on:** Phase 1
1271
+ `
1272
+ );
1273
+
1274
+ const result = runBlueprintTools('roadmap analyze', tmpDir);
1275
+ assert.ok(result.success, `Command failed: ${result.error}`);
1276
+
1277
+ const output = JSON.parse(result.output);
1278
+ assert.strictEqual(output.phases[0].goal, 'Initialize project');
1279
+ assert.strictEqual(output.phases[0].depends_on, 'Nothing');
1280
+ assert.strictEqual(output.phases[1].goal, 'Build features');
1281
+ assert.strictEqual(output.phases[1].depends_on, 'Phase 1');
1282
+ });
1283
+ });
1284
+
1285
+ // ─────────────────────────────────────────────────────────────────────────────
1286
+ // phase add command
1287
+ // ─────────────────────────────────────────────────────────────────────────────
1288
+
1289
+ describe('phase add command', () => {
1290
+ let tmpDir;
1291
+
1292
+ beforeEach(() => {
1293
+ tmpDir = createTempProject();
1294
+ });
1295
+
1296
+ afterEach(() => {
1297
+ cleanup(tmpDir);
1298
+ });
1299
+
1300
+ test('adds phase after highest existing', () => {
1301
+ fs.writeFileSync(
1302
+ path.join(tmpDir, '.blueprint', 'ROADMAP.md'),
1303
+ `# Roadmap v1.0
1304
+
1305
+ ### Phase 1: Foundation
1306
+ **Goal:** Setup
1307
+
1308
+ ### Phase 2: API
1309
+ **Goal:** Build API
1310
+
1311
+ ---
1312
+ `
1313
+ );
1314
+
1315
+ const result = runBlueprintTools('phase add User Dashboard', tmpDir);
1316
+ assert.ok(result.success, `Command failed: ${result.error}`);
1317
+
1318
+ const output = JSON.parse(result.output);
1319
+ assert.strictEqual(output.phase_number, 3, 'should be phase 3');
1320
+ assert.strictEqual(output.slug, 'user-dashboard');
1321
+
1322
+ // Verify directory created
1323
+ assert.ok(
1324
+ fs.existsSync(path.join(tmpDir, '.blueprint', 'phases', '03-user-dashboard')),
1325
+ 'directory should be created'
1326
+ );
1327
+
1328
+ // Verify ROADMAP updated
1329
+ const roadmap = fs.readFileSync(path.join(tmpDir, '.blueprint', 'ROADMAP.md'), 'utf-8');
1330
+ assert.ok(roadmap.includes('### Phase 3: User Dashboard'), 'roadmap should include new phase');
1331
+ assert.ok(roadmap.includes('**Depends on:** Phase 2'), 'should depend on previous');
1332
+ });
1333
+
1334
+ test('handles empty roadmap', () => {
1335
+ fs.writeFileSync(
1336
+ path.join(tmpDir, '.blueprint', 'ROADMAP.md'),
1337
+ `# Roadmap v1.0\n`
1338
+ );
1339
+
1340
+ const result = runBlueprintTools('phase add Initial Setup', tmpDir);
1341
+ assert.ok(result.success, `Command failed: ${result.error}`);
1342
+
1343
+ const output = JSON.parse(result.output);
1344
+ assert.strictEqual(output.phase_number, 1, 'should be phase 1');
1345
+ });
1346
+ });
1347
+
1348
+ // ─────────────────────────────────────────────────────────────────────────────
1349
+ // phase insert command
1350
+ // ─────────────────────────────────────────────────────────────────────────────
1351
+
1352
+ describe('phase insert command', () => {
1353
+ let tmpDir;
1354
+
1355
+ beforeEach(() => {
1356
+ tmpDir = createTempProject();
1357
+ });
1358
+
1359
+ afterEach(() => {
1360
+ cleanup(tmpDir);
1361
+ });
1362
+
1363
+ test('inserts decimal phase after target', () => {
1364
+ fs.writeFileSync(
1365
+ path.join(tmpDir, '.blueprint', 'ROADMAP.md'),
1366
+ `# Roadmap
1367
+
1368
+ ### Phase 1: Foundation
1369
+ **Goal:** Setup
1370
+
1371
+ ### Phase 2: API
1372
+ **Goal:** Build API
1373
+ `
1374
+ );
1375
+ fs.mkdirSync(path.join(tmpDir, '.blueprint', 'phases', '01-foundation'), { recursive: true });
1376
+
1377
+ const result = runBlueprintTools('phase insert 1 Fix Critical Bug', tmpDir);
1378
+ assert.ok(result.success, `Command failed: ${result.error}`);
1379
+
1380
+ const output = JSON.parse(result.output);
1381
+ assert.strictEqual(output.phase_number, '01.1', 'should be 01.1');
1382
+ assert.strictEqual(output.after_phase, '1');
1383
+
1384
+ // Verify directory
1385
+ assert.ok(
1386
+ fs.existsSync(path.join(tmpDir, '.blueprint', 'phases', '01.1-fix-critical-bug')),
1387
+ 'decimal phase directory should be created'
1388
+ );
1389
+
1390
+ // Verify ROADMAP
1391
+ const roadmap = fs.readFileSync(path.join(tmpDir, '.blueprint', 'ROADMAP.md'), 'utf-8');
1392
+ assert.ok(roadmap.includes('Phase 01.1: Fix Critical Bug (INSERTED)'), 'roadmap should include inserted phase');
1393
+ });
1394
+
1395
+ test('increments decimal when siblings exist', () => {
1396
+ fs.writeFileSync(
1397
+ path.join(tmpDir, '.blueprint', 'ROADMAP.md'),
1398
+ `# Roadmap
1399
+
1400
+ ### Phase 1: Foundation
1401
+ **Goal:** Setup
1402
+
1403
+ ### Phase 2: API
1404
+ **Goal:** Build API
1405
+ `
1406
+ );
1407
+ fs.mkdirSync(path.join(tmpDir, '.blueprint', 'phases', '01-foundation'), { recursive: true });
1408
+ fs.mkdirSync(path.join(tmpDir, '.blueprint', 'phases', '01.1-hotfix'), { recursive: true });
1409
+
1410
+ const result = runBlueprintTools('phase insert 1 Another Fix', tmpDir);
1411
+ assert.ok(result.success, `Command failed: ${result.error}`);
1412
+
1413
+ const output = JSON.parse(result.output);
1414
+ assert.strictEqual(output.phase_number, '01.2', 'should be 01.2');
1415
+ });
1416
+
1417
+ test('rejects missing phase', () => {
1418
+ fs.writeFileSync(
1419
+ path.join(tmpDir, '.blueprint', 'ROADMAP.md'),
1420
+ `# Roadmap\n### Phase 1: Test\n**Goal:** Test\n`
1421
+ );
1422
+
1423
+ const result = runBlueprintTools('phase insert 99 Fix Something', tmpDir);
1424
+ assert.ok(!result.success, 'should fail for missing phase');
1425
+ assert.ok(result.error.includes('not found'), 'error mentions not found');
1426
+ });
1427
+ });
1428
+
1429
+ // ─────────────────────────────────────────────────────────────────────────────
1430
+ // phase remove command
1431
+ // ─────────────────────────────────────────────────────────────────────────────
1432
+
1433
+ describe('phase remove command', () => {
1434
+ let tmpDir;
1435
+
1436
+ beforeEach(() => {
1437
+ tmpDir = createTempProject();
1438
+ });
1439
+
1440
+ afterEach(() => {
1441
+ cleanup(tmpDir);
1442
+ });
1443
+
1444
+ test('removes phase directory and renumbers subsequent', () => {
1445
+ // Setup 3 phases
1446
+ fs.writeFileSync(
1447
+ path.join(tmpDir, '.blueprint', 'ROADMAP.md'),
1448
+ `# Roadmap
1449
+
1450
+ ### Phase 1: Foundation
1451
+ **Goal:** Setup
1452
+ **Depends on:** Nothing
1453
+
1454
+ ### Phase 2: Auth
1455
+ **Goal:** Authentication
1456
+ **Depends on:** Phase 1
1457
+
1458
+ ### Phase 3: Features
1459
+ **Goal:** Core features
1460
+ **Depends on:** Phase 2
1461
+ `
1462
+ );
1463
+
1464
+ fs.mkdirSync(path.join(tmpDir, '.blueprint', 'phases', '01-foundation'), { recursive: true });
1465
+ const p2 = path.join(tmpDir, '.blueprint', 'phases', '02-auth');
1466
+ fs.mkdirSync(p2, { recursive: true });
1467
+ fs.writeFileSync(path.join(p2, '02-01-PLAN.md'), '# Plan');
1468
+ const p3 = path.join(tmpDir, '.blueprint', 'phases', '03-features');
1469
+ fs.mkdirSync(p3, { recursive: true });
1470
+ fs.writeFileSync(path.join(p3, '03-01-PLAN.md'), '# Plan');
1471
+ fs.writeFileSync(path.join(p3, '03-02-PLAN.md'), '# Plan 2');
1472
+
1473
+ // Remove phase 2
1474
+ const result = runBlueprintTools('phase remove 2', tmpDir);
1475
+ assert.ok(result.success, `Command failed: ${result.error}`);
1476
+
1477
+ const output = JSON.parse(result.output);
1478
+ assert.strictEqual(output.removed, '2');
1479
+ assert.strictEqual(output.directory_deleted, '02-auth');
1480
+
1481
+ // Phase 3 should be renumbered to 02
1482
+ assert.ok(
1483
+ fs.existsSync(path.join(tmpDir, '.blueprint', 'phases', '02-features')),
1484
+ 'phase 3 should be renumbered to 02-features'
1485
+ );
1486
+ assert.ok(
1487
+ !fs.existsSync(path.join(tmpDir, '.blueprint', 'phases', '03-features')),
1488
+ 'old 03-features should not exist'
1489
+ );
1490
+
1491
+ // Files inside should be renamed
1492
+ assert.ok(
1493
+ fs.existsSync(path.join(tmpDir, '.blueprint', 'phases', '02-features', '02-01-PLAN.md')),
1494
+ 'plan file should be renumbered to 02-01'
1495
+ );
1496
+ assert.ok(
1497
+ fs.existsSync(path.join(tmpDir, '.blueprint', 'phases', '02-features', '02-02-PLAN.md')),
1498
+ 'plan 2 should be renumbered to 02-02'
1499
+ );
1500
+
1501
+ // ROADMAP should be updated
1502
+ const roadmap = fs.readFileSync(path.join(tmpDir, '.blueprint', 'ROADMAP.md'), 'utf-8');
1503
+ assert.ok(!roadmap.includes('Phase 2: Auth'), 'removed phase should not be in roadmap');
1504
+ assert.ok(roadmap.includes('Phase 2: Features'), 'phase 3 should be renumbered to 2');
1505
+ });
1506
+
1507
+ test('rejects removal of phase with summaries unless --force', () => {
1508
+ const p1 = path.join(tmpDir, '.blueprint', 'phases', '01-test');
1509
+ fs.mkdirSync(p1, { recursive: true });
1510
+ fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan');
1511
+ fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Summary');
1512
+ fs.writeFileSync(
1513
+ path.join(tmpDir, '.blueprint', 'ROADMAP.md'),
1514
+ `# Roadmap\n### Phase 1: Test\n**Goal:** Test\n`
1515
+ );
1516
+
1517
+ // Should fail without --force
1518
+ const result = runBlueprintTools('phase remove 1', tmpDir);
1519
+ assert.ok(!result.success, 'should fail without --force');
1520
+ assert.ok(result.error.includes('executed plan'), 'error mentions executed plans');
1521
+
1522
+ // Should succeed with --force
1523
+ const forceResult = runBlueprintTools('phase remove 1 --force', tmpDir);
1524
+ assert.ok(forceResult.success, `Force remove failed: ${forceResult.error}`);
1525
+ });
1526
+
1527
+ test('removes decimal phase and renumbers siblings', () => {
1528
+ fs.writeFileSync(
1529
+ path.join(tmpDir, '.blueprint', 'ROADMAP.md'),
1530
+ `# Roadmap\n### Phase 6: Main\n**Goal:** Main\n### Phase 6.1: Fix A\n**Goal:** Fix A\n### Phase 6.2: Fix B\n**Goal:** Fix B\n### Phase 6.3: Fix C\n**Goal:** Fix C\n`
1531
+ );
1532
+
1533
+ fs.mkdirSync(path.join(tmpDir, '.blueprint', 'phases', '06-main'), { recursive: true });
1534
+ fs.mkdirSync(path.join(tmpDir, '.blueprint', 'phases', '06.1-fix-a'), { recursive: true });
1535
+ fs.mkdirSync(path.join(tmpDir, '.blueprint', 'phases', '06.2-fix-b'), { recursive: true });
1536
+ fs.mkdirSync(path.join(tmpDir, '.blueprint', 'phases', '06.3-fix-c'), { recursive: true });
1537
+
1538
+ const result = runBlueprintTools('phase remove 6.2', tmpDir);
1539
+ assert.ok(result.success, `Command failed: ${result.error}`);
1540
+
1541
+ // 06.3 should become 06.2
1542
+ assert.ok(
1543
+ fs.existsSync(path.join(tmpDir, '.blueprint', 'phases', '06.2-fix-c')),
1544
+ '06.3 should be renumbered to 06.2'
1545
+ );
1546
+ assert.ok(
1547
+ !fs.existsSync(path.join(tmpDir, '.blueprint', 'phases', '06.3-fix-c')),
1548
+ 'old 06.3 should not exist'
1549
+ );
1550
+ });
1551
+
1552
+ test('updates STATE.md phase count', () => {
1553
+ fs.writeFileSync(
1554
+ path.join(tmpDir, '.blueprint', 'ROADMAP.md'),
1555
+ `# Roadmap\n### Phase 1: A\n**Goal:** A\n### Phase 2: B\n**Goal:** B\n`
1556
+ );
1557
+ fs.writeFileSync(
1558
+ path.join(tmpDir, '.blueprint', 'STATE.md'),
1559
+ `# State\n\n**Current Phase:** 1\n**Total Phases:** 2\n`
1560
+ );
1561
+ fs.mkdirSync(path.join(tmpDir, '.blueprint', 'phases', '01-a'), { recursive: true });
1562
+ fs.mkdirSync(path.join(tmpDir, '.blueprint', 'phases', '02-b'), { recursive: true });
1563
+
1564
+ runBlueprintTools('phase remove 2', tmpDir);
1565
+
1566
+ const state = fs.readFileSync(path.join(tmpDir, '.blueprint', 'STATE.md'), 'utf-8');
1567
+ assert.ok(state.includes('**Total Phases:** 1'), 'total phases should be decremented');
1568
+ });
1569
+ });
1570
+
1571
+ // ─────────────────────────────────────────────────────────────────────────────
1572
+ // phase complete command
1573
+ // ─────────────────────────────────────────────────────────────────────────────
1574
+
1575
+ describe('phase complete command', () => {
1576
+ let tmpDir;
1577
+
1578
+ beforeEach(() => {
1579
+ tmpDir = createTempProject();
1580
+ });
1581
+
1582
+ afterEach(() => {
1583
+ cleanup(tmpDir);
1584
+ });
1585
+
1586
+ test('marks phase complete and transitions to next', () => {
1587
+ fs.writeFileSync(
1588
+ path.join(tmpDir, '.blueprint', 'ROADMAP.md'),
1589
+ `# Roadmap
1590
+
1591
+ - [ ] Phase 1: Foundation
1592
+ - [ ] Phase 2: API
1593
+
1594
+ ### Phase 1: Foundation
1595
+ **Goal:** Setup
1596
+ **Plans:** 1 plans
1597
+
1598
+ ### Phase 2: API
1599
+ **Goal:** Build API
1600
+ `
1601
+ );
1602
+ fs.writeFileSync(
1603
+ path.join(tmpDir, '.blueprint', 'STATE.md'),
1604
+ `# State\n\n**Current Phase:** 01\n**Current Phase Name:** Foundation\n**Status:** In progress\n**Current Plan:** 01-01\n**Last Activity:** 2025-01-01\n**Last Activity Description:** Working on phase 1\n`
1605
+ );
1606
+
1607
+ const p1 = path.join(tmpDir, '.blueprint', 'phases', '01-foundation');
1608
+ fs.mkdirSync(p1, { recursive: true });
1609
+ fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan');
1610
+ fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Summary');
1611
+ fs.mkdirSync(path.join(tmpDir, '.blueprint', 'phases', '02-api'), { recursive: true });
1612
+
1613
+ const result = runBlueprintTools('phase complete 1', tmpDir);
1614
+ assert.ok(result.success, `Command failed: ${result.error}`);
1615
+
1616
+ const output = JSON.parse(result.output);
1617
+ assert.strictEqual(output.completed_phase, '1');
1618
+ assert.strictEqual(output.plans_executed, '1/1');
1619
+ assert.strictEqual(output.next_phase, '02');
1620
+ assert.strictEqual(output.is_last_phase, false);
1621
+
1622
+ // Verify STATE.md updated
1623
+ const state = fs.readFileSync(path.join(tmpDir, '.blueprint', 'STATE.md'), 'utf-8');
1624
+ assert.ok(state.includes('**Current Phase:** 02'), 'should advance to phase 02');
1625
+ assert.ok(state.includes('**Status:** Ready to plan'), 'status should be ready to plan');
1626
+ assert.ok(state.includes('**Current Plan:** Not started'), 'plan should be reset');
1627
+
1628
+ // Verify ROADMAP checkbox
1629
+ const roadmap = fs.readFileSync(path.join(tmpDir, '.blueprint', 'ROADMAP.md'), 'utf-8');
1630
+ assert.ok(roadmap.includes('[x]'), 'phase should be checked off');
1631
+ assert.ok(roadmap.includes('completed'), 'completion date should be added');
1632
+ });
1633
+
1634
+ test('detects last phase in milestone', () => {
1635
+ fs.writeFileSync(
1636
+ path.join(tmpDir, '.blueprint', 'ROADMAP.md'),
1637
+ `# Roadmap\n### Phase 1: Only Phase\n**Goal:** Everything\n`
1638
+ );
1639
+ fs.writeFileSync(
1640
+ path.join(tmpDir, '.blueprint', 'STATE.md'),
1641
+ `# State\n\n**Current Phase:** 01\n**Status:** In progress\n**Current Plan:** 01-01\n**Last Activity:** 2025-01-01\n**Last Activity Description:** Working\n`
1642
+ );
1643
+
1644
+ const p1 = path.join(tmpDir, '.blueprint', 'phases', '01-only-phase');
1645
+ fs.mkdirSync(p1, { recursive: true });
1646
+ fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan');
1647
+ fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Summary');
1648
+
1649
+ const result = runBlueprintTools('phase complete 1', tmpDir);
1650
+ assert.ok(result.success, `Command failed: ${result.error}`);
1651
+
1652
+ const output = JSON.parse(result.output);
1653
+ assert.strictEqual(output.is_last_phase, true, 'should detect last phase');
1654
+ assert.strictEqual(output.next_phase, null, 'no next phase');
1655
+
1656
+ const state = fs.readFileSync(path.join(tmpDir, '.blueprint', 'STATE.md'), 'utf-8');
1657
+ assert.ok(state.includes('Milestone complete'), 'status should be milestone complete');
1658
+ });
1659
+ });
1660
+
1661
+ // ─────────────────────────────────────────────────────────────────────────────
1662
+ // milestone complete command
1663
+ // ─────────────────────────────────────────────────────────────────────────────
1664
+
1665
+ describe('milestone complete command', () => {
1666
+ let tmpDir;
1667
+
1668
+ beforeEach(() => {
1669
+ tmpDir = createTempProject();
1670
+ });
1671
+
1672
+ afterEach(() => {
1673
+ cleanup(tmpDir);
1674
+ });
1675
+
1676
+ test('archives roadmap, requirements, creates MILESTONES.md', () => {
1677
+ fs.writeFileSync(
1678
+ path.join(tmpDir, '.blueprint', 'ROADMAP.md'),
1679
+ `# Roadmap v1.0 MVP\n\n### Phase 1: Foundation\n**Goal:** Setup\n`
1680
+ );
1681
+ fs.writeFileSync(
1682
+ path.join(tmpDir, '.blueprint', 'REQUIREMENTS.md'),
1683
+ `# Requirements\n\n- [ ] User auth\n- [ ] Dashboard\n`
1684
+ );
1685
+ fs.writeFileSync(
1686
+ path.join(tmpDir, '.blueprint', 'STATE.md'),
1687
+ `# State\n\n**Status:** In progress\n**Last Activity:** 2025-01-01\n**Last Activity Description:** Working\n`
1688
+ );
1689
+
1690
+ const p1 = path.join(tmpDir, '.blueprint', 'phases', '01-foundation');
1691
+ fs.mkdirSync(p1, { recursive: true });
1692
+ fs.writeFileSync(
1693
+ path.join(p1, '01-01-SUMMARY.md'),
1694
+ `---\none-liner: Set up project infrastructure\n---\n# Summary\n`
1695
+ );
1696
+
1697
+ const result = runBlueprintTools('milestone complete v1.0 --name MVP Foundation', tmpDir);
1698
+ assert.ok(result.success, `Command failed: ${result.error}`);
1699
+
1700
+ const output = JSON.parse(result.output);
1701
+ assert.strictEqual(output.version, 'v1.0');
1702
+ assert.strictEqual(output.phases, 1);
1703
+ assert.ok(output.archived.roadmap, 'roadmap should be archived');
1704
+ assert.ok(output.archived.requirements, 'requirements should be archived');
1705
+
1706
+ // Verify archive files exist
1707
+ assert.ok(
1708
+ fs.existsSync(path.join(tmpDir, '.blueprint', 'milestones', 'v1.0-ROADMAP.md')),
1709
+ 'archived roadmap should exist'
1710
+ );
1711
+ assert.ok(
1712
+ fs.existsSync(path.join(tmpDir, '.blueprint', 'milestones', 'v1.0-REQUIREMENTS.md')),
1713
+ 'archived requirements should exist'
1714
+ );
1715
+
1716
+ // Verify MILESTONES.md created
1717
+ assert.ok(
1718
+ fs.existsSync(path.join(tmpDir, '.blueprint', 'MILESTONES.md')),
1719
+ 'MILESTONES.md should be created'
1720
+ );
1721
+ const milestones = fs.readFileSync(path.join(tmpDir, '.blueprint', 'MILESTONES.md'), 'utf-8');
1722
+ assert.ok(milestones.includes('v1.0 MVP Foundation'), 'milestone entry should contain name');
1723
+ assert.ok(milestones.includes('Set up project infrastructure'), 'accomplishments should be listed');
1724
+ });
1725
+
1726
+ test('appends to existing MILESTONES.md', () => {
1727
+ fs.writeFileSync(
1728
+ path.join(tmpDir, '.blueprint', 'MILESTONES.md'),
1729
+ `# Milestones\n\n## v0.9 Alpha (Shipped: 2025-01-01)\n\n---\n\n`
1730
+ );
1731
+ fs.writeFileSync(
1732
+ path.join(tmpDir, '.blueprint', 'ROADMAP.md'),
1733
+ `# Roadmap v1.0\n`
1734
+ );
1735
+ fs.writeFileSync(
1736
+ path.join(tmpDir, '.blueprint', 'STATE.md'),
1737
+ `# State\n\n**Status:** In progress\n**Last Activity:** 2025-01-01\n**Last Activity Description:** Working\n`
1738
+ );
1739
+
1740
+ const result = runBlueprintTools('milestone complete v1.0 --name Beta', tmpDir);
1741
+ assert.ok(result.success, `Command failed: ${result.error}`);
1742
+
1743
+ const milestones = fs.readFileSync(path.join(tmpDir, '.blueprint', 'MILESTONES.md'), 'utf-8');
1744
+ assert.ok(milestones.includes('v0.9 Alpha'), 'existing entry should be preserved');
1745
+ assert.ok(milestones.includes('v1.0 Beta'), 'new entry should be appended');
1746
+ });
1747
+ });
1748
+
1749
+ // ─────────────────────────────────────────────────────────────────────────────
1750
+ // validate consistency command
1751
+ // ─────────────────────────────────────────────────────────────────────────────
1752
+
1753
+ describe('validate consistency command', () => {
1754
+ let tmpDir;
1755
+
1756
+ beforeEach(() => {
1757
+ tmpDir = createTempProject();
1758
+ });
1759
+
1760
+ afterEach(() => {
1761
+ cleanup(tmpDir);
1762
+ });
1763
+
1764
+ test('passes for consistent project', () => {
1765
+ fs.writeFileSync(
1766
+ path.join(tmpDir, '.blueprint', 'ROADMAP.md'),
1767
+ `# Roadmap\n### Phase 1: A\n### Phase 2: B\n### Phase 3: C\n`
1768
+ );
1769
+ fs.mkdirSync(path.join(tmpDir, '.blueprint', 'phases', '01-a'), { recursive: true });
1770
+ fs.mkdirSync(path.join(tmpDir, '.blueprint', 'phases', '02-b'), { recursive: true });
1771
+ fs.mkdirSync(path.join(tmpDir, '.blueprint', 'phases', '03-c'), { recursive: true });
1772
+
1773
+ const result = runBlueprintTools('validate consistency', tmpDir);
1774
+ assert.ok(result.success, `Command failed: ${result.error}`);
1775
+
1776
+ const output = JSON.parse(result.output);
1777
+ assert.strictEqual(output.passed, true, 'should pass');
1778
+ assert.strictEqual(output.warning_count, 0, 'no warnings');
1779
+ });
1780
+
1781
+ test('warns about phase on disk but not in roadmap', () => {
1782
+ fs.writeFileSync(
1783
+ path.join(tmpDir, '.blueprint', 'ROADMAP.md'),
1784
+ `# Roadmap\n### Phase 1: A\n`
1785
+ );
1786
+ fs.mkdirSync(path.join(tmpDir, '.blueprint', 'phases', '01-a'), { recursive: true });
1787
+ fs.mkdirSync(path.join(tmpDir, '.blueprint', 'phases', '02-orphan'), { recursive: true });
1788
+
1789
+ const result = runBlueprintTools('validate consistency', tmpDir);
1790
+ assert.ok(result.success, `Command failed: ${result.error}`);
1791
+
1792
+ const output = JSON.parse(result.output);
1793
+ assert.ok(output.warning_count > 0, 'should have warnings');
1794
+ assert.ok(
1795
+ output.warnings.some(w => w.includes('disk but not in ROADMAP')),
1796
+ 'should warn about orphan directory'
1797
+ );
1798
+ });
1799
+
1800
+ test('warns about gaps in phase numbering', () => {
1801
+ fs.writeFileSync(
1802
+ path.join(tmpDir, '.blueprint', 'ROADMAP.md'),
1803
+ `# Roadmap\n### Phase 1: A\n### Phase 3: C\n`
1804
+ );
1805
+ fs.mkdirSync(path.join(tmpDir, '.blueprint', 'phases', '01-a'), { recursive: true });
1806
+ fs.mkdirSync(path.join(tmpDir, '.blueprint', 'phases', '03-c'), { recursive: true });
1807
+
1808
+ const result = runBlueprintTools('validate consistency', tmpDir);
1809
+ assert.ok(result.success, `Command failed: ${result.error}`);
1810
+
1811
+ const output = JSON.parse(result.output);
1812
+ assert.ok(
1813
+ output.warnings.some(w => w.includes('Gap in phase numbering')),
1814
+ 'should warn about gap'
1815
+ );
1816
+ });
1817
+ });
1818
+
1819
+ // ─────────────────────────────────────────────────────────────────────────────
1820
+ // progress command
1821
+ // ─────────────────────────────────────────────────────────────────────────────
1822
+
1823
+ describe('progress command', () => {
1824
+ let tmpDir;
1825
+
1826
+ beforeEach(() => {
1827
+ tmpDir = createTempProject();
1828
+ });
1829
+
1830
+ afterEach(() => {
1831
+ cleanup(tmpDir);
1832
+ });
1833
+
1834
+ test('renders JSON progress', () => {
1835
+ fs.writeFileSync(
1836
+ path.join(tmpDir, '.blueprint', 'ROADMAP.md'),
1837
+ `# Roadmap v1.0 MVP\n`
1838
+ );
1839
+ const p1 = path.join(tmpDir, '.blueprint', 'phases', '01-foundation');
1840
+ fs.mkdirSync(p1, { recursive: true });
1841
+ fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan');
1842
+ fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Done');
1843
+ fs.writeFileSync(path.join(p1, '01-02-PLAN.md'), '# Plan 2');
1844
+
1845
+ const result = runBlueprintTools('progress json', tmpDir);
1846
+ assert.ok(result.success, `Command failed: ${result.error}`);
1847
+
1848
+ const output = JSON.parse(result.output);
1849
+ assert.strictEqual(output.total_plans, 2, '2 total plans');
1850
+ assert.strictEqual(output.total_summaries, 1, '1 summary');
1851
+ assert.strictEqual(output.percent, 50, '50%');
1852
+ assert.strictEqual(output.phases.length, 1, '1 phase');
1853
+ assert.strictEqual(output.phases[0].status, 'In Progress', 'phase in progress');
1854
+ });
1855
+
1856
+ test('renders bar format', () => {
1857
+ fs.writeFileSync(
1858
+ path.join(tmpDir, '.blueprint', 'ROADMAP.md'),
1859
+ `# Roadmap v1.0\n`
1860
+ );
1861
+ const p1 = path.join(tmpDir, '.blueprint', 'phases', '01-test');
1862
+ fs.mkdirSync(p1, { recursive: true });
1863
+ fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan');
1864
+ fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Done');
1865
+
1866
+ const result = runBlueprintTools('progress bar --raw', tmpDir);
1867
+ assert.ok(result.success, `Command failed: ${result.error}`);
1868
+ assert.ok(result.output.includes('1/1'), 'should include count');
1869
+ assert.ok(result.output.includes('100%'), 'should include 100%');
1870
+ });
1871
+
1872
+ test('renders table format', () => {
1873
+ fs.writeFileSync(
1874
+ path.join(tmpDir, '.blueprint', 'ROADMAP.md'),
1875
+ `# Roadmap v1.0 MVP\n`
1876
+ );
1877
+ const p1 = path.join(tmpDir, '.blueprint', 'phases', '01-foundation');
1878
+ fs.mkdirSync(p1, { recursive: true });
1879
+ fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan');
1880
+
1881
+ const result = runBlueprintTools('progress table --raw', tmpDir);
1882
+ assert.ok(result.success, `Command failed: ${result.error}`);
1883
+ assert.ok(result.output.includes('Phase'), 'should have table header');
1884
+ assert.ok(result.output.includes('foundation'), 'should include phase name');
1885
+ });
1886
+ });
1887
+
1888
+ // ─────────────────────────────────────────────────────────────────────────────
1889
+ // todo complete command
1890
+ // ─────────────────────────────────────────────────────────────────────────────
1891
+
1892
+ describe('todo complete command', () => {
1893
+ let tmpDir;
1894
+
1895
+ beforeEach(() => {
1896
+ tmpDir = createTempProject();
1897
+ });
1898
+
1899
+ afterEach(() => {
1900
+ cleanup(tmpDir);
1901
+ });
1902
+
1903
+ test('moves todo from pending to completed', () => {
1904
+ const pendingDir = path.join(tmpDir, '.blueprint', 'todos', 'pending');
1905
+ fs.mkdirSync(pendingDir, { recursive: true });
1906
+ fs.writeFileSync(
1907
+ path.join(pendingDir, 'add-dark-mode.md'),
1908
+ `title: Add dark mode\narea: ui\ncreated: 2025-01-01\n`
1909
+ );
1910
+
1911
+ const result = runBlueprintTools('todo complete add-dark-mode.md', tmpDir);
1912
+ assert.ok(result.success, `Command failed: ${result.error}`);
1913
+
1914
+ const output = JSON.parse(result.output);
1915
+ assert.strictEqual(output.completed, true);
1916
+
1917
+ // Verify moved
1918
+ assert.ok(
1919
+ !fs.existsSync(path.join(tmpDir, '.blueprint', 'todos', 'pending', 'add-dark-mode.md')),
1920
+ 'should be removed from pending'
1921
+ );
1922
+ assert.ok(
1923
+ fs.existsSync(path.join(tmpDir, '.blueprint', 'todos', 'completed', 'add-dark-mode.md')),
1924
+ 'should be in completed'
1925
+ );
1926
+
1927
+ // Verify completion timestamp added
1928
+ const content = fs.readFileSync(
1929
+ path.join(tmpDir, '.blueprint', 'todos', 'completed', 'add-dark-mode.md'),
1930
+ 'utf-8'
1931
+ );
1932
+ assert.ok(content.startsWith('completed:'), 'should have completed timestamp');
1933
+ });
1934
+
1935
+ test('fails for nonexistent todo', () => {
1936
+ const result = runBlueprintTools('todo complete nonexistent.md', tmpDir);
1937
+ assert.ok(!result.success, 'should fail');
1938
+ assert.ok(result.error.includes('not found'), 'error mentions not found');
1939
+ });
1940
+ });
1941
+
1942
+ // ─────────────────────────────────────────────────────────────────────────────
1943
+ // scaffold command
1944
+ // ─────────────────────────────────────────────────────────────────────────────
1945
+
1946
+ describe('scaffold command', () => {
1947
+ let tmpDir;
1948
+
1949
+ beforeEach(() => {
1950
+ tmpDir = createTempProject();
1951
+ });
1952
+
1953
+ afterEach(() => {
1954
+ cleanup(tmpDir);
1955
+ });
1956
+
1957
+ test('scaffolds context file', () => {
1958
+ fs.mkdirSync(path.join(tmpDir, '.blueprint', 'phases', '03-api'), { recursive: true });
1959
+
1960
+ const result = runBlueprintTools('scaffold context --phase 3', tmpDir);
1961
+ assert.ok(result.success, `Command failed: ${result.error}`);
1962
+
1963
+ const output = JSON.parse(result.output);
1964
+ assert.strictEqual(output.created, true);
1965
+
1966
+ // Verify file content
1967
+ const content = fs.readFileSync(
1968
+ path.join(tmpDir, '.blueprint', 'phases', '03-api', '03-CONTEXT.md'),
1969
+ 'utf-8'
1970
+ );
1971
+ assert.ok(content.includes('Phase 3'), 'should reference phase number');
1972
+ assert.ok(content.includes('Decisions'), 'should have decisions section');
1973
+ assert.ok(content.includes('Discretion Areas'), 'should have discretion section');
1974
+ });
1975
+
1976
+ test('scaffolds UAT file', () => {
1977
+ fs.mkdirSync(path.join(tmpDir, '.blueprint', 'phases', '03-api'), { recursive: true });
1978
+
1979
+ const result = runBlueprintTools('scaffold uat --phase 3', tmpDir);
1980
+ assert.ok(result.success, `Command failed: ${result.error}`);
1981
+
1982
+ const output = JSON.parse(result.output);
1983
+ assert.strictEqual(output.created, true);
1984
+
1985
+ const content = fs.readFileSync(
1986
+ path.join(tmpDir, '.blueprint', 'phases', '03-api', '03-UAT.md'),
1987
+ 'utf-8'
1988
+ );
1989
+ assert.ok(content.includes('User Acceptance Testing'), 'should have UAT heading');
1990
+ assert.ok(content.includes('Test Results'), 'should have test results section');
1991
+ });
1992
+
1993
+ test('scaffolds verification file', () => {
1994
+ fs.mkdirSync(path.join(tmpDir, '.blueprint', 'phases', '03-api'), { recursive: true });
1995
+
1996
+ const result = runBlueprintTools('scaffold verification --phase 3', tmpDir);
1997
+ assert.ok(result.success, `Command failed: ${result.error}`);
1998
+
1999
+ const output = JSON.parse(result.output);
2000
+ assert.strictEqual(output.created, true);
2001
+
2002
+ const content = fs.readFileSync(
2003
+ path.join(tmpDir, '.blueprint', 'phases', '03-api', '03-VERIFICATION.md'),
2004
+ 'utf-8'
2005
+ );
2006
+ assert.ok(content.includes('Goal-Backward Verification'), 'should have verification heading');
2007
+ });
2008
+
2009
+ test('scaffolds phase directory', () => {
2010
+ const result = runBlueprintTools('scaffold phase-dir --phase 5 --name User Dashboard', tmpDir);
2011
+ assert.ok(result.success, `Command failed: ${result.error}`);
2012
+
2013
+ const output = JSON.parse(result.output);
2014
+ assert.strictEqual(output.created, true);
2015
+ assert.ok(
2016
+ fs.existsSync(path.join(tmpDir, '.blueprint', 'phases', '05-user-dashboard')),
2017
+ 'directory should be created'
2018
+ );
2019
+ });
2020
+
2021
+ test('does not overwrite existing files', () => {
2022
+ const phaseDir = path.join(tmpDir, '.blueprint', 'phases', '03-api');
2023
+ fs.mkdirSync(phaseDir, { recursive: true });
2024
+ fs.writeFileSync(path.join(phaseDir, '03-CONTEXT.md'), '# Existing content');
2025
+
2026
+ const result = runBlueprintTools('scaffold context --phase 3', tmpDir);
2027
+ assert.ok(result.success, `Command failed: ${result.error}`);
2028
+
2029
+ const output = JSON.parse(result.output);
2030
+ assert.strictEqual(output.created, false, 'should not overwrite');
2031
+ assert.strictEqual(output.reason, 'already_exists');
2032
+ });
2033
+ });