gswd 1.0.0 → 1.1.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 (86) hide show
  1. package/bin/gswd-tools.cjs +228 -0
  2. package/bin/install.js +8 -0
  3. package/commands/gswd/imagine.md +7 -1
  4. package/commands/gswd/start.md +507 -32
  5. package/dist/lib/audit.d.ts +205 -0
  6. package/dist/lib/audit.js +805 -0
  7. package/dist/lib/bootstrap.d.ts +103 -0
  8. package/dist/lib/bootstrap.js +563 -0
  9. package/dist/lib/compile.d.ts +239 -0
  10. package/dist/lib/compile.js +1152 -0
  11. package/dist/lib/config.d.ts +49 -0
  12. package/dist/lib/config.js +150 -0
  13. package/dist/lib/imagine-agents.d.ts +54 -0
  14. package/dist/lib/imagine-agents.js +185 -0
  15. package/dist/lib/imagine-gate.d.ts +47 -0
  16. package/dist/lib/imagine-gate.js +131 -0
  17. package/dist/lib/imagine-input.d.ts +46 -0
  18. package/dist/lib/imagine-input.js +233 -0
  19. package/dist/lib/imagine-synthesis.d.ts +90 -0
  20. package/dist/lib/imagine-synthesis.js +453 -0
  21. package/dist/lib/imagine.d.ts +56 -0
  22. package/dist/lib/imagine.js +413 -0
  23. package/dist/lib/intake.d.ts +27 -0
  24. package/dist/lib/intake.js +82 -0
  25. package/dist/lib/parse.d.ts +59 -0
  26. package/dist/lib/parse.js +171 -0
  27. package/dist/lib/render.d.ts +309 -0
  28. package/dist/lib/render.js +624 -0
  29. package/dist/lib/specify-agents.d.ts +120 -0
  30. package/dist/lib/specify-agents.js +269 -0
  31. package/dist/lib/specify-journeys.d.ts +124 -0
  32. package/dist/lib/specify-journeys.js +279 -0
  33. package/dist/lib/specify-nfr.d.ts +45 -0
  34. package/dist/lib/specify-nfr.js +159 -0
  35. package/dist/lib/specify-roles.d.ts +46 -0
  36. package/dist/lib/specify-roles.js +88 -0
  37. package/dist/lib/specify.d.ts +70 -0
  38. package/dist/lib/specify.js +676 -0
  39. package/dist/lib/state.d.ts +140 -0
  40. package/dist/lib/state.js +340 -0
  41. package/dist/tests/audit.test.d.ts +4 -0
  42. package/dist/tests/audit.test.js +1579 -0
  43. package/dist/tests/bootstrap.test.d.ts +5 -0
  44. package/dist/tests/bootstrap.test.js +611 -0
  45. package/dist/tests/compile.test.d.ts +4 -0
  46. package/dist/tests/compile.test.js +862 -0
  47. package/dist/tests/config.test.d.ts +4 -0
  48. package/dist/tests/config.test.js +191 -0
  49. package/dist/tests/imagine-agents.test.d.ts +6 -0
  50. package/dist/tests/imagine-agents.test.js +179 -0
  51. package/dist/tests/imagine-gate.test.d.ts +6 -0
  52. package/dist/tests/imagine-gate.test.js +264 -0
  53. package/dist/tests/imagine-input.test.d.ts +6 -0
  54. package/dist/tests/imagine-input.test.js +283 -0
  55. package/dist/tests/imagine-synthesis.test.d.ts +7 -0
  56. package/dist/tests/imagine-synthesis.test.js +380 -0
  57. package/dist/tests/imagine.test.d.ts +8 -0
  58. package/dist/tests/imagine.test.js +406 -0
  59. package/dist/tests/parse.test.d.ts +4 -0
  60. package/dist/tests/parse.test.js +285 -0
  61. package/dist/tests/render.test.d.ts +4 -0
  62. package/dist/tests/render.test.js +236 -0
  63. package/dist/tests/specify-agents.test.d.ts +4 -0
  64. package/dist/tests/specify-agents.test.js +352 -0
  65. package/dist/tests/specify-journeys.test.d.ts +5 -0
  66. package/dist/tests/specify-journeys.test.js +440 -0
  67. package/dist/tests/specify-nfr.test.d.ts +4 -0
  68. package/dist/tests/specify-nfr.test.js +205 -0
  69. package/dist/tests/specify-roles.test.d.ts +4 -0
  70. package/dist/tests/specify-roles.test.js +136 -0
  71. package/dist/tests/specify.test.d.ts +9 -0
  72. package/dist/tests/specify.test.js +544 -0
  73. package/dist/tests/state.test.d.ts +4 -0
  74. package/dist/tests/state.test.js +316 -0
  75. package/lib/bootstrap.ts +37 -11
  76. package/lib/compile.ts +426 -4
  77. package/lib/imagine-agents.ts +53 -7
  78. package/lib/imagine-synthesis.ts +170 -6
  79. package/lib/imagine.ts +59 -5
  80. package/lib/intake.ts +60 -0
  81. package/lib/parse.ts +2 -1
  82. package/lib/render.ts +566 -5
  83. package/lib/specify-agents.ts +25 -3
  84. package/lib/state.ts +115 -0
  85. package/package.json +4 -2
  86. package/templates/gswd/DECISIONS.template.md +3 -0
@@ -0,0 +1,862 @@
1
+ "use strict";
2
+ /**
3
+ * Compile module tests — spec bundle parsing, sort helpers, and 4 document generators
4
+ */
5
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
6
+ if (k2 === undefined) k2 = k;
7
+ var desc = Object.getOwnPropertyDescriptor(m, k);
8
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
9
+ desc = { enumerable: true, get: function() { return m[k]; } };
10
+ }
11
+ Object.defineProperty(o, k2, desc);
12
+ }) : (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ o[k2] = m[k];
15
+ }));
16
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
17
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
18
+ }) : function(o, v) {
19
+ o["default"] = v;
20
+ });
21
+ var __importStar = (this && this.__importStar) || (function () {
22
+ var ownKeys = function(o) {
23
+ ownKeys = Object.getOwnPropertyNames || function (o) {
24
+ var ar = [];
25
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
26
+ return ar;
27
+ };
28
+ return ownKeys(o);
29
+ };
30
+ return function (mod) {
31
+ if (mod && mod.__esModule) return mod;
32
+ var result = {};
33
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
34
+ __setModuleDefault(result, mod);
35
+ return result;
36
+ };
37
+ })();
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ const node_test_1 = require("node:test");
40
+ const assert = __importStar(require("node:assert"));
41
+ const fs = __importStar(require("node:fs"));
42
+ const path = __importStar(require("node:path"));
43
+ const os = __importStar(require("node:os"));
44
+ const compile_js_1 = require("../lib/compile.js");
45
+ // ─── Test Fixture ────────────────────────────────────────────────────────────
46
+ /**
47
+ * Build a minimal SpecBundle for testing.
48
+ *
49
+ * - 2 journey types: onboarding J-001 (Phase 1) and error_states J-002 (Phase 3)
50
+ * - J-001 has linkedIntegrations: ['I-001'] to exercise integration_sanity path
51
+ * - 3 FRs with different scope/priority: FR-001 (v1/P0), FR-002 (v1/P1), FR-003 (v2/P1)
52
+ * - 2 NFRs in different categories: NFR-001 (security), NFR-002 (performance)
53
+ * - 1 integration (I-001 approved)
54
+ * - Basic vision/targetUser/productDirection/wedge strings
55
+ * - 2 frozen decisions, 1 metric, 1 out-of-scope, 1 risk, 1 open question
56
+ */
57
+ function makeTestBundle() {
58
+ const frs = [
59
+ { id: 'FR-001', description: 'User can register', scope: 'v1', priority: 'P0', sourceJourneys: ['J-001'] },
60
+ { id: 'FR-002', description: 'User can set profile', scope: 'v1', priority: 'P1', sourceJourneys: ['J-001'] },
61
+ { id: 'FR-003', description: 'Advanced reporting', scope: 'v2', priority: 'P1', sourceJourneys: [] },
62
+ ];
63
+ const nfrs = [
64
+ { id: 'NFR-001', description: 'Data encrypted at rest', category: 'security', threshold: 'AES-256' },
65
+ { id: 'NFR-002', description: 'API response under 200ms', category: 'performance', threshold: 'p95 < 200ms' },
66
+ ];
67
+ const journeys = [
68
+ { id: 'J-001', name: 'User Onboarding', type: 'onboarding', linkedFRs: ['FR-001', 'FR-002'], linkedIntegrations: ['I-001'] },
69
+ { id: 'J-002', name: 'Error Handling', type: 'error_states', linkedFRs: ['FR-001'], linkedIntegrations: [] },
70
+ ];
71
+ const integrations = [
72
+ { id: 'I-001', name: 'Email Provider', status: 'approved', fallback: 'Manual email' },
73
+ ];
74
+ return {
75
+ vision: 'A tool that helps developers ship faster',
76
+ targetUser: 'Solo developers and small teams',
77
+ productDirection: 'Eliminate boilerplate and reduce setup friction',
78
+ wedge: 'One-click project scaffolding',
79
+ frozenDecisions: [
80
+ 'Use TypeScript throughout',
81
+ 'Prefer zero external dependencies',
82
+ ],
83
+ successMetrics: ['10 projects scaffolded in first month'],
84
+ outOfScope: ['Multi-tenant support in v1'],
85
+ risks: ['Dependency on external CLI tools'],
86
+ openQuestions: ['Should we support Windows?'],
87
+ approvals: {
88
+ auth_model: 'passwordless_email',
89
+ data_store: 'sqlite',
90
+ },
91
+ frs,
92
+ roles: 'Admin role only',
93
+ nfrs,
94
+ journeys,
95
+ integrations,
96
+ projectSlug: 'test-project',
97
+ };
98
+ }
99
+ // ─── sortFRs ─────────────────────────────────────────────────────────────────
100
+ (0, node_test_1.describe)('sortFRs', () => {
101
+ (0, node_test_1.it)('sorts v1 before v2 before out', () => {
102
+ const frs = [
103
+ { id: 'FR-003', description: 'Out of scope thing', scope: 'out', priority: 'P0', sourceJourneys: [] },
104
+ { id: 'FR-002', description: 'Future feature', scope: 'v2', priority: 'P0', sourceJourneys: [] },
105
+ { id: 'FR-001', description: 'Core feature', scope: 'v1', priority: 'P0', sourceJourneys: [] },
106
+ ];
107
+ const sorted = (0, compile_js_1.sortFRs)(frs);
108
+ assert.strictEqual(sorted[0].scope, 'v1');
109
+ assert.strictEqual(sorted[1].scope, 'v2');
110
+ assert.strictEqual(sorted[2].scope, 'out');
111
+ });
112
+ (0, node_test_1.it)('within same scope, sorts P0 before P1 before P2', () => {
113
+ const frs = [
114
+ { id: 'FR-003', description: 'Low priority', scope: 'v1', priority: 'P2', sourceJourneys: [] },
115
+ { id: 'FR-002', description: 'Medium priority', scope: 'v1', priority: 'P1', sourceJourneys: [] },
116
+ { id: 'FR-001', description: 'High priority', scope: 'v1', priority: 'P0', sourceJourneys: [] },
117
+ ];
118
+ const sorted = (0, compile_js_1.sortFRs)(frs);
119
+ assert.strictEqual(sorted[0].priority, 'P0');
120
+ assert.strictEqual(sorted[1].priority, 'P1');
121
+ assert.strictEqual(sorted[2].priority, 'P2');
122
+ });
123
+ (0, node_test_1.it)('within same scope+priority, sorts by numeric ID ascending (FR-001 before FR-010)', () => {
124
+ const frs = [
125
+ { id: 'FR-010', description: 'Later FR', scope: 'v1', priority: 'P0', sourceJourneys: [] },
126
+ { id: 'FR-001', description: 'First FR', scope: 'v1', priority: 'P0', sourceJourneys: [] },
127
+ { id: 'FR-005', description: 'Middle FR', scope: 'v1', priority: 'P0', sourceJourneys: [] },
128
+ ];
129
+ const sorted = (0, compile_js_1.sortFRs)(frs);
130
+ assert.strictEqual(sorted[0].id, 'FR-001');
131
+ assert.strictEqual(sorted[1].id, 'FR-005');
132
+ assert.strictEqual(sorted[2].id, 'FR-010');
133
+ });
134
+ });
135
+ // ─── parseSpecBundle ─────────────────────────────────────────────────────────
136
+ (0, node_test_1.describe)('parseSpecBundle', () => {
137
+ const MINIMAL_IMAGINE = `## Vision
138
+ A tool that helps developers ship faster
139
+
140
+ ## Target User
141
+ Solo developers and small teams
142
+
143
+ ## Product Direction
144
+ Eliminate boilerplate and reduce setup friction
145
+
146
+ ## Wedge
147
+ One-click project scaffolding
148
+ `;
149
+ const MINIMAL_DECISIONS = `## Frozen Decisions
150
+
151
+ 1. Use TypeScript throughout
152
+ 2. Prefer zero external dependencies
153
+
154
+ ## Success Metrics
155
+
156
+ - 10 projects scaffolded in first month
157
+
158
+ ## Out of Scope
159
+
160
+ - Multi-tenant support in v1
161
+
162
+ ## Risks & Mitigations
163
+
164
+ - Dependency on external CLI tools
165
+
166
+ ## Open Questions
167
+
168
+ - Should we support Windows?
169
+ `;
170
+ const MINIMAL_SPEC = `## Roles & Permissions
171
+ Admin role only.
172
+
173
+ ## Functional Requirements
174
+
175
+ ### FR-001: User can register
176
+ **Scope:** v1
177
+ **Priority:** P0
178
+
179
+ ### FR-002: User can set profile
180
+ **Scope:** v1
181
+ **Priority:** P1
182
+
183
+ ### FR-003: Advanced reporting
184
+ **Scope:** v2
185
+ **Priority:** P1
186
+
187
+ ## Acceptance Criteria
188
+ TBD
189
+ `;
190
+ const MINIMAL_NFR = `## Non-Functional Requirements
191
+
192
+ ### NFR-001: Data encrypted at rest
193
+ **Category:** security
194
+ **Threshold:** AES-256
195
+
196
+ ### NFR-002: API response under 200ms
197
+ **Category:** performance
198
+ **Threshold:** p95 < 200ms
199
+ `;
200
+ const MINIMAL_JOURNEYS = `## Journeys
201
+
202
+ ### J-001: User Onboarding
203
+ **Type:** onboarding
204
+ **Linked FRs:** FR-001, FR-002
205
+ **Linked Integrations:** I-001
206
+
207
+ #### Acceptance Tests
208
+ - User completes onboarding
209
+
210
+ ### J-002: Error Handling
211
+ **Type:** error_states
212
+ **Linked FRs:** FR-001
213
+ **Linked Integrations:** none
214
+ `;
215
+ const MINIMAL_INTEGRATIONS = `## Integrations
216
+
217
+ ### I-001: Email Provider
218
+ **Status:** approved
219
+ **Fallback:** Manual email
220
+ `;
221
+ (0, node_test_1.it)('parses minimal valid spec bundle (all 6 files present) into SpecBundle with correct field population', () => {
222
+ const bundle = (0, compile_js_1.parseSpecBundle)({
223
+ imagine: MINIMAL_IMAGINE,
224
+ decisions: MINIMAL_DECISIONS,
225
+ spec: MINIMAL_SPEC,
226
+ nfr: MINIMAL_NFR,
227
+ journeys: MINIMAL_JOURNEYS,
228
+ integrations: MINIMAL_INTEGRATIONS,
229
+ });
230
+ assert.ok(bundle.vision.length > 0, 'vision should be populated');
231
+ assert.ok(bundle.targetUser.length > 0, 'targetUser should be populated');
232
+ assert.ok(bundle.frozenDecisions.length >= 1, 'frozenDecisions should have entries');
233
+ assert.ok(bundle.frs.length === 3, 'should parse 3 FRs');
234
+ assert.ok(bundle.nfrs.length === 2, 'should parse 2 NFRs');
235
+ assert.ok(bundle.journeys.length === 2, 'should parse 2 journeys');
236
+ assert.ok(bundle.integrations.length === 1, 'should parse 1 integration');
237
+ // Verify FR fields
238
+ const fr001 = bundle.frs.find(fr => fr.id === 'FR-001');
239
+ assert.ok(fr001, 'FR-001 should be parsed');
240
+ assert.strictEqual(fr001.scope, 'v1');
241
+ assert.strictEqual(fr001.priority, 'P0');
242
+ });
243
+ (0, node_test_1.it)('handles missing optional fields gracefully (empty arrays, empty strings)', () => {
244
+ const bundle = (0, compile_js_1.parseSpecBundle)({
245
+ imagine: '',
246
+ decisions: '',
247
+ spec: '',
248
+ nfr: '',
249
+ journeys: '',
250
+ integrations: '',
251
+ });
252
+ assert.strictEqual(bundle.frs.length, 0, 'empty spec -> no FRs');
253
+ assert.strictEqual(bundle.nfrs.length, 0, 'empty nfr -> no NFRs');
254
+ assert.strictEqual(bundle.journeys.length, 0, 'empty journeys -> no journeys');
255
+ assert.strictEqual(bundle.integrations.length, 0, 'empty integrations -> no integrations');
256
+ assert.strictEqual(bundle.frozenDecisions.length, 0, 'empty decisions -> no frozen decisions');
257
+ assert.strictEqual(bundle.vision, '', 'empty imagine -> empty vision');
258
+ });
259
+ });
260
+ // ─── generateProjectDoc ───────────────────────────────────────────────────────
261
+ (0, node_test_1.describe)('generateProjectDoc', () => {
262
+ (0, node_test_1.it)('produces markdown containing all required headings', () => {
263
+ const bundle = makeTestBundle();
264
+ const doc = (0, compile_js_1.generateProjectDoc)(bundle);
265
+ assert.ok(doc.includes('## What This Is'), 'should have What This Is section');
266
+ assert.ok(doc.includes('## Target User'), 'should have Target User section');
267
+ assert.ok(doc.includes('## Problem Statement'), 'should have Problem Statement section');
268
+ assert.ok(doc.includes('## Wedge / MVP Boundary'), 'should have Wedge section');
269
+ assert.ok(doc.includes('## Success Metrics'), 'should have Success Metrics section');
270
+ assert.ok(doc.includes('## Out of Scope'), 'should have Out of Scope section');
271
+ });
272
+ (0, node_test_1.it)('content from bundle appears in correct sections', () => {
273
+ const bundle = makeTestBundle();
274
+ const doc = (0, compile_js_1.generateProjectDoc)(bundle);
275
+ assert.ok(doc.includes(bundle.vision), 'vision should appear in doc');
276
+ assert.ok(doc.includes(bundle.targetUser), 'targetUser should appear in doc');
277
+ assert.ok(doc.includes(bundle.productDirection), 'productDirection should appear in doc');
278
+ assert.ok(doc.includes(bundle.wedge), 'wedge should appear in doc');
279
+ assert.ok(doc.includes(bundle.successMetrics[0]), 'success metric should appear in doc');
280
+ assert.ok(doc.includes(bundle.outOfScope[0]), 'out-of-scope item should appear in doc');
281
+ });
282
+ });
283
+ // ─── generateRequirementsDoc ──────────────────────────────────────────────────
284
+ (0, node_test_1.describe)('generateRequirementsDoc', () => {
285
+ (0, node_test_1.it)('FRs are grouped by scope (v1 section before v2 section), sorted by priority within scope', () => {
286
+ const bundle = makeTestBundle();
287
+ const doc = (0, compile_js_1.generateRequirementsDoc)(bundle);
288
+ // v1 section should appear before v2 section
289
+ const v1Pos = doc.indexOf('### v1 (In Scope)');
290
+ const v2Pos = doc.indexOf('### v2 (Future)');
291
+ assert.ok(v1Pos !== -1, 'v1 section should be present');
292
+ assert.ok(v2Pos !== -1, 'v2 section should be present');
293
+ assert.ok(v1Pos < v2Pos, 'v1 section should appear before v2 section');
294
+ // Within v1, P0 (FR-001) should appear before P1 (FR-002)
295
+ const fr001Pos = doc.indexOf('FR-001');
296
+ const fr002Pos = doc.indexOf('FR-002');
297
+ assert.ok(fr001Pos < fr002Pos, 'FR-001 (P0) should appear before FR-002 (P1)');
298
+ });
299
+ (0, node_test_1.it)('NFRs grouped by category in fixed order (security, privacy, performance, observability)', () => {
300
+ const bundle = makeTestBundle();
301
+ const doc = (0, compile_js_1.generateRequirementsDoc)(bundle);
302
+ // Security should appear before performance (no privacy or observability in test bundle)
303
+ const secPos = doc.indexOf('### Security');
304
+ const perfPos = doc.indexOf('### Performance');
305
+ assert.ok(secPos !== -1, 'Security section should be present');
306
+ assert.ok(perfPos !== -1, 'Performance section should be present');
307
+ assert.ok(secPos < perfPos, 'Security should appear before Performance');
308
+ // NFR content should be present
309
+ assert.ok(doc.includes('NFR-001'), 'NFR-001 should be in doc');
310
+ assert.ok(doc.includes('NFR-002'), 'NFR-002 should be in doc');
311
+ });
312
+ (0, node_test_1.it)('traceability table present with FR-to-journey mapping', () => {
313
+ const bundle = makeTestBundle();
314
+ const doc = (0, compile_js_1.generateRequirementsDoc)(bundle);
315
+ assert.ok(doc.includes('## Traceability'), 'should have Traceability section');
316
+ assert.ok(doc.includes('| FR |'), 'should have table header with FR column');
317
+ assert.ok(doc.includes('| Description |'), 'should have table header with Description column');
318
+ assert.ok(doc.includes('| Journeys |'), 'should have table header with Journeys column');
319
+ // FR-001 (v1) should appear in traceability table with journey reference
320
+ const tracePos = doc.indexOf('## Traceability');
321
+ const fr001TracePos = doc.indexOf('FR-001', tracePos);
322
+ assert.ok(fr001TracePos !== -1, 'FR-001 should be in traceability table');
323
+ });
324
+ });
325
+ // ─── generateRoadmapDoc ───────────────────────────────────────────────────────
326
+ (0, node_test_1.describe)('generateRoadmapDoc', () => {
327
+ (0, node_test_1.it)('all 4 phases always present in output', () => {
328
+ const bundle = makeTestBundle();
329
+ const doc = (0, compile_js_1.generateRoadmapDoc)(bundle);
330
+ assert.ok(doc.includes('### Phase 1'), 'Phase 1 should always be present');
331
+ assert.ok(doc.includes('### Phase 2'), 'Phase 2 should always be present');
332
+ assert.ok(doc.includes('### Phase 3'), 'Phase 3 should always be present');
333
+ assert.ok(doc.includes('### Phase 4'), 'Phase 4 should always be present');
334
+ });
335
+ (0, node_test_1.it)('journeys assigned to correct phases by type (onboarding->Phase1, error_states->Phase3)', () => {
336
+ const bundle = makeTestBundle();
337
+ const doc = (0, compile_js_1.generateRoadmapDoc)(bundle);
338
+ // J-001 (onboarding) -> Phase 1
339
+ const phase1Start = doc.indexOf('### Phase 1');
340
+ const phase2Start = doc.indexOf('### Phase 2');
341
+ const j001InPhase1 = doc.indexOf('J-001', phase1Start) < phase2Start;
342
+ assert.ok(j001InPhase1, 'J-001 (onboarding) should be in Phase 1');
343
+ // J-002 (error_states) -> Phase 3
344
+ const phase3Start = doc.indexOf('### Phase 3');
345
+ const phase4Start = doc.indexOf('### Phase 4');
346
+ const j002InPhase3 = doc.indexOf('J-002', phase3Start) < phase4Start;
347
+ assert.ok(j002InPhase3, 'J-002 (error_states) should be in Phase 3');
348
+ });
349
+ (0, node_test_1.it)('Phase 4 contains observability+security NFRs, not journeys', () => {
350
+ const bundle = makeTestBundle();
351
+ const doc = (0, compile_js_1.generateRoadmapDoc)(bundle);
352
+ const phase4Start = doc.indexOf('### Phase 4');
353
+ const phase4Content = doc.slice(phase4Start);
354
+ // NFR-001 (security) should be in Phase 4
355
+ assert.ok(phase4Content.includes('NFR-001'), 'NFR-001 (security) should be in Phase 4');
356
+ // Phase 4 should NOT contain journey IDs (J-001, J-002)
357
+ assert.ok(!phase4Content.includes('J-001'), 'journeys should not appear in Phase 4');
358
+ assert.ok(!phase4Content.includes('J-002'), 'journeys should not appear in Phase 4');
359
+ });
360
+ });
361
+ // ─── generateStateDoc ─────────────────────────────────────────────────────────
362
+ (0, node_test_1.describe)('generateStateDoc', () => {
363
+ (0, node_test_1.it)('contains frozen decisions, approvals, open questions, risks sections', () => {
364
+ const bundle = makeTestBundle();
365
+ const doc = (0, compile_js_1.generateStateDoc)(bundle);
366
+ assert.ok(doc.includes('## Frozen Decisions'), 'should have Frozen Decisions section');
367
+ assert.ok(doc.includes('## Approvals'), 'should have Approvals section');
368
+ assert.ok(doc.includes('## Open Questions'), 'should have Open Questions section');
369
+ assert.ok(doc.includes('## Risks'), 'should have Risks section');
370
+ });
371
+ (0, node_test_1.it)('all required headings present and content populated from bundle', () => {
372
+ const bundle = makeTestBundle();
373
+ const doc = (0, compile_js_1.generateStateDoc)(bundle);
374
+ // Frozen decisions content
375
+ assert.ok(doc.includes(bundle.frozenDecisions[0]), 'first frozen decision should appear in doc');
376
+ assert.ok(doc.includes(bundle.frozenDecisions[1]), 'second frozen decision should appear in doc');
377
+ // Open questions content
378
+ assert.ok(doc.includes(bundle.openQuestions[0]), 'open question should appear in doc');
379
+ // Risks content
380
+ assert.ok(doc.includes(bundle.risks[0]), 'risk should appear in doc');
381
+ });
382
+ });
383
+ // ─── Determinism ─────────────────────────────────────────────────────────────
384
+ (0, node_test_1.describe)('determinism', () => {
385
+ (0, node_test_1.it)('running all 4 generators twice with same bundle produces byte-identical strings', () => {
386
+ const bundle1 = makeTestBundle();
387
+ const bundle2 = makeTestBundle();
388
+ // Run generators twice
389
+ const project1 = (0, compile_js_1.generateProjectDoc)(bundle1);
390
+ const project2 = (0, compile_js_1.generateProjectDoc)(bundle2);
391
+ const requirements1 = (0, compile_js_1.generateRequirementsDoc)(bundle1);
392
+ const requirements2 = (0, compile_js_1.generateRequirementsDoc)(bundle2);
393
+ const roadmap1 = (0, compile_js_1.generateRoadmapDoc)(bundle1);
394
+ const roadmap2 = (0, compile_js_1.generateRoadmapDoc)(bundle2);
395
+ const state1 = (0, compile_js_1.generateStateDoc)(bundle1);
396
+ const state2 = (0, compile_js_1.generateStateDoc)(bundle2);
397
+ assert.strictEqual(project1, project2, 'generateProjectDoc must be deterministic');
398
+ assert.strictEqual(requirements1, requirements2, 'generateRequirementsDoc must be deterministic');
399
+ assert.strictEqual(roadmap1, roadmap2, 'generateRoadmapDoc must be deterministic');
400
+ assert.strictEqual(state1, state2, 'generateStateDoc must be deterministic');
401
+ });
402
+ });
403
+ // ─── validateContracts ────────────────────────────────────────────────────────
404
+ /**
405
+ * Generate all 4 docs from the test bundle for use in validator tests.
406
+ */
407
+ function makeTestDocs(bundle) {
408
+ return {
409
+ project: (0, compile_js_1.generateProjectDoc)(bundle),
410
+ requirements: (0, compile_js_1.generateRequirementsDoc)(bundle),
411
+ roadmap: (0, compile_js_1.generateRoadmapDoc)(bundle),
412
+ state: (0, compile_js_1.generateStateDoc)(bundle),
413
+ };
414
+ }
415
+ (0, node_test_1.describe)('validateContracts() — v1 coverage check', () => {
416
+ (0, node_test_1.it)('PASS when all v1 FRs appear in generated roadmap content', () => {
417
+ const bundle = makeTestBundle();
418
+ const docs = makeTestDocs(bundle);
419
+ const result = (0, compile_js_1.validateContracts)(docs, bundle);
420
+ // No v1_coverage findings (FR-001 and FR-002 should appear in the roadmap via journeys)
421
+ const coverageFindings = result.findings.filter(f => f.check === 'v1_coverage');
422
+ assert.strictEqual(coverageFindings.length, 0, 'should have no v1_coverage findings when all v1 FRs are covered');
423
+ });
424
+ (0, node_test_1.it)('FAIL with finding when a v1 FR is missing from roadmap content', () => {
425
+ const bundle = makeTestBundle();
426
+ // Add a v1 FR with a unique ID that will not appear in the generated roadmap
427
+ const missingFR = {
428
+ id: 'FR-099',
429
+ description: 'A feature not covered by any journey',
430
+ scope: 'v1',
431
+ priority: 'P1',
432
+ sourceJourneys: ['J-001'], // Has a journey so orphan check passes
433
+ };
434
+ const bundleWithMissingFR = { ...bundle, frs: [...bundle.frs, missingFR] };
435
+ const docs = makeTestDocs(bundle); // Roadmap generated without FR-099
436
+ const result = (0, compile_js_1.validateContracts)(docs, bundleWithMissingFR);
437
+ const coverageFindings = result.findings.filter(f => f.check === 'v1_coverage');
438
+ assert.ok(coverageFindings.length > 0, 'should have v1_coverage finding when FR is missing from roadmap');
439
+ assert.ok(coverageFindings.some(f => f.id === 'FR-099'), 'finding should reference the missing FR-099');
440
+ });
441
+ });
442
+ (0, node_test_1.describe)('validateContracts() — orphan requirements check', () => {
443
+ (0, node_test_1.it)('PASS when all v1 FRs are referenced by at least 1 journey', () => {
444
+ const bundle = makeTestBundle();
445
+ // In makeTestBundle: FR-001 -> J-001, FR-002 -> J-001
446
+ const docs = makeTestDocs(bundle);
447
+ const result = (0, compile_js_1.validateContracts)(docs, bundle);
448
+ const orphanFindings = result.findings.filter(f => f.check === 'orphan_requirement');
449
+ assert.strictEqual(orphanFindings.length, 0, 'should have no orphan_requirement findings when all v1 FRs have journey refs');
450
+ });
451
+ (0, node_test_1.it)('FAIL with finding for v1 FR not referenced by any journey', () => {
452
+ const bundle = makeTestBundle();
453
+ // Add an orphan v1 FR with no sourceJourneys
454
+ const orphanFR = { id: 'FR-004', description: 'Orphan FR', scope: 'v1', priority: 'P1', sourceJourneys: [] };
455
+ const modifiedBundle = { ...bundle, frs: [...bundle.frs, orphanFR] };
456
+ const docs = makeTestDocs(modifiedBundle);
457
+ // Also inject FR-004 into roadmap so v1_coverage check passes for it
458
+ const modifiedDocs = { ...docs, roadmap: docs.roadmap + '\nFR-004\n' };
459
+ const result = (0, compile_js_1.validateContracts)(modifiedDocs, modifiedBundle);
460
+ const orphanFindings = result.findings.filter(f => f.check === 'orphan_requirement');
461
+ assert.ok(orphanFindings.length > 0, 'should have orphan_requirement finding for FR-004');
462
+ assert.ok(orphanFindings.some(f => f.id === 'FR-004'), 'finding should reference the orphan FR-004');
463
+ });
464
+ });
465
+ (0, node_test_1.describe)('validateContracts() — integration sanity check', () => {
466
+ (0, node_test_1.it)('PASS when all integrations referenced by journeys are approved or deferred-with-fallback', () => {
467
+ const bundle = makeTestBundle();
468
+ // In makeTestBundle: I-001 is 'approved' and referenced by J-001
469
+ const docs = makeTestDocs(bundle);
470
+ const result = (0, compile_js_1.validateContracts)(docs, bundle);
471
+ const integrationFindings = result.findings.filter(f => f.check === 'integration_sanity');
472
+ assert.strictEqual(integrationFindings.length, 0, 'should have no integration_sanity findings for approved integrations');
473
+ });
474
+ (0, node_test_1.it)('FAIL when a journey linkedIntegrations references an integration with unapproved status', () => {
475
+ const bundle = makeTestBundle();
476
+ // Change I-001 to an unapproved status without fallback
477
+ const badIntegration = { id: 'I-001', name: 'Email Provider', status: 'rejected', fallback: '' };
478
+ const modifiedBundle = { ...bundle, integrations: [badIntegration] };
479
+ const docs = makeTestDocs(bundle); // Use original docs
480
+ const result = (0, compile_js_1.validateContracts)(docs, modifiedBundle);
481
+ const integrationFindings = result.findings.filter(f => f.check === 'integration_sanity');
482
+ assert.ok(integrationFindings.length > 0, 'should have integration_sanity finding for rejected integration');
483
+ assert.ok(integrationFindings.some(f => f.id === 'I-001'), 'finding should reference the unapproved I-001');
484
+ });
485
+ });
486
+ (0, node_test_1.describe)('validateContracts() — required headings check', () => {
487
+ (0, node_test_1.it)('PASS when all 4 generated docs contain their required headings', () => {
488
+ const bundle = makeTestBundle();
489
+ const docs = makeTestDocs(bundle);
490
+ const result = (0, compile_js_1.validateContracts)(docs, bundle);
491
+ const headingFindings = result.findings.filter(f => f.check === 'required_headings');
492
+ assert.strictEqual(headingFindings.length, 0, 'should have no required_headings findings for complete generated docs');
493
+ });
494
+ (0, node_test_1.it)('FAIL when a generated doc is missing a required heading', () => {
495
+ const bundle = makeTestBundle();
496
+ const docs = makeTestDocs(bundle);
497
+ // Remove a required heading from the project doc
498
+ const modifiedDocs = { ...docs, project: docs.project.replace('## What This Is', '## REMOVED') };
499
+ const result = (0, compile_js_1.validateContracts)(modifiedDocs, bundle);
500
+ const headingFindings = result.findings.filter(f => f.check === 'required_headings');
501
+ assert.ok(headingFindings.length > 0, 'should have required_headings finding when heading is missing');
502
+ assert.ok(headingFindings.some(f => f.id === 'PROJECT.md'), 'finding should reference the file with missing heading');
503
+ });
504
+ });
505
+ (0, node_test_1.describe)('validateContracts() — combined', () => {
506
+ (0, node_test_1.it)('returns { passed: true, findings: [] } when all 4 checks pass', () => {
507
+ const bundle = makeTestBundle();
508
+ const docs = makeTestDocs(bundle);
509
+ const result = (0, compile_js_1.validateContracts)(docs, bundle);
510
+ assert.strictEqual(result.passed, true, 'should pass when all checks pass');
511
+ assert.strictEqual(result.findings.length, 0, 'should have no findings when all checks pass');
512
+ });
513
+ (0, node_test_1.it)('returns { passed: false, findings: [...] } with multiple findings when multiple checks fail', () => {
514
+ const bundle = makeTestBundle();
515
+ // Add a v1 FR with unique ID not in roadmap (v1_coverage failure)
516
+ const missingFR = {
517
+ id: 'FR-099',
518
+ description: 'A feature not covered by any journey',
519
+ scope: 'v1',
520
+ priority: 'P1',
521
+ sourceJourneys: ['J-001'], // Has a journey so orphan check passes
522
+ };
523
+ const bundleWithMissingFR = { ...bundle, frs: [...bundle.frs, missingFR] };
524
+ const baseDocs = makeTestDocs(bundle); // Roadmap generated without FR-099
525
+ // Also remove a required heading from project doc (required_headings failure)
526
+ const modifiedDocs = {
527
+ ...baseDocs,
528
+ project: baseDocs.project.replace('## What This Is', '## REMOVED'),
529
+ };
530
+ const result = (0, compile_js_1.validateContracts)(modifiedDocs, bundleWithMissingFR);
531
+ assert.strictEqual(result.passed, false, 'should fail when any check fails');
532
+ assert.ok(result.findings.length >= 2, 'should have multiple findings when multiple checks fail');
533
+ const checkTypes = new Set(result.findings.map(f => f.check));
534
+ assert.ok(checkTypes.has('v1_coverage'), 'should have v1_coverage finding');
535
+ assert.ok(checkTypes.has('required_headings'), 'should have required_headings finding');
536
+ });
537
+ });
538
+ // ─── Workflow Orchestrator ────────────────────────────────────────────────────
539
+ // Minimal spec artifact content strings for workflow tests
540
+ const WORKFLOW_IMAGINE = `## Vision
541
+ A tool that helps developers ship faster
542
+
543
+ ## Target User
544
+ Solo developers and small teams
545
+
546
+ ## Product Direction
547
+ Eliminate boilerplate and reduce setup friction
548
+
549
+ ## Wedge
550
+ One-click project scaffolding
551
+ `;
552
+ const WORKFLOW_DECISIONS = `## Frozen Decisions
553
+
554
+ 1. Use TypeScript throughout
555
+ 2. Prefer zero external dependencies
556
+
557
+ ## Success Metrics
558
+
559
+ - 10 projects scaffolded in first month
560
+
561
+ ## Out of Scope
562
+
563
+ - Multi-tenant support in v1
564
+
565
+ ## Risks & Mitigations
566
+
567
+ - Dependency on external CLI tools
568
+
569
+ ## Open Questions
570
+
571
+ - Should we support Windows?
572
+ `;
573
+ const WORKFLOW_SPEC = `## Roles & Permissions
574
+ Admin role only.
575
+
576
+ ## Functional Requirements
577
+
578
+ ### FR-001: User can register
579
+ **Scope:** v1
580
+ **Priority:** P0
581
+
582
+ ### FR-002: User can set profile
583
+ **Scope:** v1
584
+ **Priority:** P1
585
+
586
+ ## Acceptance Criteria
587
+ TBD
588
+ `;
589
+ const WORKFLOW_NFR = `## Non-Functional Requirements
590
+
591
+ ### NFR-001: Data encrypted at rest
592
+ **Category:** security
593
+ **Threshold:** AES-256
594
+ `;
595
+ const WORKFLOW_JOURNEYS = `## Journeys
596
+
597
+ ### J-001: User Onboarding
598
+ **Type:** onboarding
599
+ **Linked FRs:** FR-001, FR-002
600
+ **Linked Integrations:** none
601
+
602
+ #### Acceptance Tests
603
+ - User completes onboarding
604
+ `;
605
+ const WORKFLOW_INTEGRATIONS = `## Integrations
606
+
607
+ `;
608
+ // Helper to create a temp directory with spec artifact files
609
+ function makeTempPlanningDir(options = {}) {
610
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gswd-test-'));
611
+ // Create STATE.json
612
+ const state = {
613
+ version: 1,
614
+ project_slug: 'test-project',
615
+ created_at: '2026-01-01T00:00:00.000Z',
616
+ updated_at: '2026-01-01T00:00:00.000Z',
617
+ stage: 'audit',
618
+ stage_status: {
619
+ imagine: 'done',
620
+ specify: 'done',
621
+ audit: options.includeAuditPass ? 'pass' : 'fail',
622
+ compile: 'not_started',
623
+ },
624
+ last_checkpoint: null,
625
+ auto: { enabled: false, policy: 'balanced', interrupt_reasons: [] },
626
+ approvals: {
627
+ auth_model: 'passwordless_email',
628
+ data_store: 'sqlite',
629
+ paid_integrations: [],
630
+ },
631
+ };
632
+ const statePath = path.join(tmpDir, 'STATE.json');
633
+ fs.writeFileSync(statePath, JSON.stringify(state, null, 2) + '\n', 'utf-8');
634
+ // Write spec artifact files
635
+ fs.writeFileSync(path.join(tmpDir, 'IMAGINE.md'), WORKFLOW_IMAGINE, 'utf-8');
636
+ fs.writeFileSync(path.join(tmpDir, 'DECISIONS.md'), WORKFLOW_DECISIONS, 'utf-8');
637
+ fs.writeFileSync(path.join(tmpDir, 'SPEC.md'), options.customSpec ?? WORKFLOW_SPEC, 'utf-8');
638
+ fs.writeFileSync(path.join(tmpDir, 'NFR.md'), WORKFLOW_NFR, 'utf-8');
639
+ fs.writeFileSync(path.join(tmpDir, 'JOURNEYS.md'), options.customJourneys ?? WORKFLOW_JOURNEYS, 'utf-8');
640
+ fs.writeFileSync(path.join(tmpDir, 'INTEGRATIONS.md'), WORKFLOW_INTEGRATIONS, 'utf-8');
641
+ return {
642
+ planningDir: tmpDir,
643
+ statePath,
644
+ cleanup: () => {
645
+ try {
646
+ fs.rmSync(tmpDir, { recursive: true, force: true });
647
+ }
648
+ catch { /* ignore */ }
649
+ },
650
+ };
651
+ }
652
+ (0, node_test_1.describe)('readSpecArtifacts()', () => {
653
+ (0, node_test_1.it)('reads all 6 artifact files from a temp planning directory', () => {
654
+ const { planningDir, cleanup } = makeTempPlanningDir({ includeAuditPass: true });
655
+ try {
656
+ const artifacts = (0, compile_js_1.readSpecArtifacts)(planningDir);
657
+ assert.ok(typeof artifacts['imagine'] === 'string', 'should have imagine key');
658
+ assert.ok(typeof artifacts['decisions'] === 'string', 'should have decisions key');
659
+ assert.ok(typeof artifacts['spec'] === 'string', 'should have spec key');
660
+ assert.ok(typeof artifacts['nfr'] === 'string', 'should have nfr key');
661
+ assert.ok(typeof artifacts['journeys'] === 'string', 'should have journeys key');
662
+ assert.ok(typeof artifacts['integrations'] === 'string', 'should have integrations key');
663
+ assert.ok(artifacts['imagine'].length > 0, 'imagine content should be non-empty');
664
+ assert.ok(artifacts['spec'].length > 0, 'spec content should be non-empty');
665
+ }
666
+ finally {
667
+ cleanup();
668
+ }
669
+ });
670
+ (0, node_test_1.it)('returns empty strings for missing files (does not throw)', () => {
671
+ const emptyDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gswd-empty-'));
672
+ try {
673
+ const artifacts = (0, compile_js_1.readSpecArtifacts)(emptyDir);
674
+ assert.strictEqual(artifacts['imagine'], '', 'missing IMAGINE.md -> empty string');
675
+ assert.strictEqual(artifacts['spec'], '', 'missing SPEC.md -> empty string');
676
+ assert.strictEqual(artifacts['journeys'], '', 'missing JOURNEYS.md -> empty string');
677
+ }
678
+ finally {
679
+ try {
680
+ fs.rmSync(emptyDir, { recursive: true, force: true });
681
+ }
682
+ catch { /* ignore */ }
683
+ }
684
+ });
685
+ });
686
+ (0, node_test_1.describe)('runCompileWorkflow() — audit gate', () => {
687
+ (0, node_test_1.it)('returns { passed: false, error: "Audit gate not passed" } when audit status is not pass', () => {
688
+ const { planningDir, statePath, cleanup } = makeTempPlanningDir({ includeAuditPass: false });
689
+ try {
690
+ const result = (0, compile_js_1.runCompileWorkflow)({ planningDir, statePath });
691
+ assert.strictEqual(result.passed, false, 'should fail when audit gate not passed');
692
+ assert.ok(result.error?.includes('Audit gate not passed'), 'error should mention audit gate');
693
+ assert.deepStrictEqual(result.filesWritten, [], 'no files should be written');
694
+ }
695
+ finally {
696
+ cleanup();
697
+ }
698
+ });
699
+ });
700
+ (0, node_test_1.describe)('runCompileWorkflow() — happy path', () => {
701
+ (0, node_test_1.it)('given valid spec artifacts and audit gate passing, produces all 4 contract files and updates state', () => {
702
+ const { planningDir, statePath, cleanup } = makeTempPlanningDir({ includeAuditPass: true });
703
+ try {
704
+ const result = (0, compile_js_1.runCompileWorkflow)({ planningDir, statePath });
705
+ assert.strictEqual(result.passed, true, 'workflow should pass');
706
+ assert.ok(!result.error, 'no error should be set');
707
+ // 4 files should be written
708
+ assert.ok(result.filesWritten.length === 4, 'should write exactly 4 contract files');
709
+ // All 4 contract files should exist on disk
710
+ const expectedFiles = ['PROJECT.md', 'REQUIREMENTS.md', 'ROADMAP.md', 'STATE.md'];
711
+ for (const filename of expectedFiles) {
712
+ const filepath = path.join(planningDir, filename);
713
+ assert.ok(fs.existsSync(filepath), `${filename} should exist on disk`);
714
+ }
715
+ // STATE.json should be updated: stage_status.compile = 'done', stage = 'compile'
716
+ const updatedState = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
717
+ assert.strictEqual(updatedState.stage_status.compile, 'done', 'compile status should be done');
718
+ assert.strictEqual(updatedState.stage, 'compile', 'stage should be compile');
719
+ // validator should have passed
720
+ assert.ok(result.validatorResult?.passed, 'validator should pass');
721
+ }
722
+ finally {
723
+ cleanup();
724
+ }
725
+ });
726
+ });
727
+ (0, node_test_1.describe)('runCompileWorkflow() — validator FAIL', () => {
728
+ (0, node_test_1.it)('when validator fails, no contract files written and state updated to fail', () => {
729
+ // Create a spec where FR-001 is v1 but no journey references it (orphan)
730
+ // Additionally, no journey at all means v1 FRs won't appear in roadmap
731
+ const orphanSpec = `## Roles & Permissions
732
+ Admin role only.
733
+
734
+ ## Functional Requirements
735
+
736
+ ### FR-001: User can register
737
+ **Scope:** v1
738
+ **Priority:** P0
739
+
740
+ ## Acceptance Criteria
741
+ TBD
742
+ `;
743
+ // No journeys means FR-001 won't appear in roadmap -> v1_coverage FAIL
744
+ // Also FR-001 has no sourceJourneys -> orphan_requirement FAIL
745
+ const emptyJourneys = `## Journeys
746
+
747
+ `;
748
+ const { planningDir, statePath, cleanup } = makeTempPlanningDir({
749
+ includeAuditPass: true,
750
+ customSpec: orphanSpec,
751
+ customJourneys: emptyJourneys,
752
+ });
753
+ try {
754
+ const result = (0, compile_js_1.runCompileWorkflow)({ planningDir, statePath });
755
+ assert.strictEqual(result.passed, false, 'workflow should fail when validator fails');
756
+ assert.ok(!result.error, 'error field should not be set for validator failure (use validatorResult instead)');
757
+ // No contract files should be on disk
758
+ const contractFiles = ['PROJECT.md', 'REQUIREMENTS.md', 'ROADMAP.md', 'STATE.md'];
759
+ for (const filename of contractFiles) {
760
+ const filepath = path.join(planningDir, filename);
761
+ assert.ok(!fs.existsSync(filepath), `${filename} should NOT exist on disk when validator fails`);
762
+ }
763
+ // STATE.json compile status should be 'fail'
764
+ const updatedState = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
765
+ assert.strictEqual(updatedState.stage_status.compile, 'fail', 'compile status should be fail');
766
+ // filesWritten should be empty
767
+ assert.deepStrictEqual(result.filesWritten, [], 'no files should be written when validator fails');
768
+ }
769
+ finally {
770
+ cleanup();
771
+ }
772
+ });
773
+ });
774
+ // ─── Golden-File Tests (TEST-04) ──────────────────────────────────────────────
775
+ const FIXTURE_SPEC_BUNDLE_DIR = path.join(__dirname, 'fixtures/compile/spec_bundle');
776
+ const FIXTURE_EXPECTED_CONTRACT_DIR = path.join(__dirname, 'fixtures/compile/expected_contract');
777
+ /**
778
+ * Read fixture spec bundle files and parse into SpecBundle with projectSlug = 'taskflow'.
779
+ * Used by golden-file tests to ensure consistent input parsing.
780
+ */
781
+ function loadFixtureBundle() {
782
+ const files = {
783
+ imagine: fs.readFileSync(path.join(FIXTURE_SPEC_BUNDLE_DIR, 'IMAGINE.md'), 'utf-8'),
784
+ decisions: fs.readFileSync(path.join(FIXTURE_SPEC_BUNDLE_DIR, 'DECISIONS.md'), 'utf-8'),
785
+ spec: fs.readFileSync(path.join(FIXTURE_SPEC_BUNDLE_DIR, 'SPEC.md'), 'utf-8'),
786
+ nfr: fs.readFileSync(path.join(FIXTURE_SPEC_BUNDLE_DIR, 'NFR.md'), 'utf-8'),
787
+ journeys: fs.readFileSync(path.join(FIXTURE_SPEC_BUNDLE_DIR, 'JOURNEYS.md'), 'utf-8'),
788
+ integrations: fs.readFileSync(path.join(FIXTURE_SPEC_BUNDLE_DIR, 'INTEGRATIONS.md'), 'utf-8'),
789
+ };
790
+ // No statePath — projectSlug defaults to ''. Explicitly set to 'taskflow' per plan spec.
791
+ const bundle = (0, compile_js_1.parseSpecBundle)(files);
792
+ bundle.projectSlug = 'taskflow';
793
+ return bundle;
794
+ }
795
+ (0, node_test_1.describe)('golden-file tests (TEST-04)', () => {
796
+ (0, node_test_1.describe)('compile(spec_bundle) matches expected_contract', () => {
797
+ (0, node_test_1.it)('generateProjectDoc output matches expected_contract/PROJECT.md byte-for-byte', () => {
798
+ const bundle = loadFixtureBundle();
799
+ const generated = (0, compile_js_1.generateProjectDoc)(bundle);
800
+ const expected = fs.readFileSync(path.join(FIXTURE_EXPECTED_CONTRACT_DIR, 'PROJECT.md'), 'utf-8');
801
+ assert.strictEqual(generated, expected, 'generateProjectDoc must be byte-identical to golden PROJECT.md');
802
+ });
803
+ (0, node_test_1.it)('generateRequirementsDoc output matches expected_contract/REQUIREMENTS.md byte-for-byte', () => {
804
+ const bundle = loadFixtureBundle();
805
+ const generated = (0, compile_js_1.generateRequirementsDoc)(bundle);
806
+ const expected = fs.readFileSync(path.join(FIXTURE_EXPECTED_CONTRACT_DIR, 'REQUIREMENTS.md'), 'utf-8');
807
+ assert.strictEqual(generated, expected, 'generateRequirementsDoc must be byte-identical to golden REQUIREMENTS.md');
808
+ });
809
+ (0, node_test_1.it)('generateRoadmapDoc output matches expected_contract/ROADMAP.md byte-for-byte', () => {
810
+ const bundle = loadFixtureBundle();
811
+ const generated = (0, compile_js_1.generateRoadmapDoc)(bundle);
812
+ const expected = fs.readFileSync(path.join(FIXTURE_EXPECTED_CONTRACT_DIR, 'ROADMAP.md'), 'utf-8');
813
+ assert.strictEqual(generated, expected, 'generateRoadmapDoc must be byte-identical to golden ROADMAP.md');
814
+ });
815
+ (0, node_test_1.it)('generateStateDoc output matches expected_contract/STATE.md byte-for-byte', () => {
816
+ const bundle = loadFixtureBundle();
817
+ const generated = (0, compile_js_1.generateStateDoc)(bundle);
818
+ const expected = fs.readFileSync(path.join(FIXTURE_EXPECTED_CONTRACT_DIR, 'STATE.md'), 'utf-8');
819
+ assert.strictEqual(generated, expected, 'generateStateDoc must be byte-identical to golden STATE.md');
820
+ });
821
+ });
822
+ (0, node_test_1.describe)('compile is deterministic across runs (CMPL-07)', () => {
823
+ (0, node_test_1.it)('running all 4 generators twice on same fixture produces byte-identical output', () => {
824
+ // Parse fixture bundle twice independently
825
+ const bundle1 = loadFixtureBundle();
826
+ const bundle2 = loadFixtureBundle();
827
+ // Generate all 4 docs from each bundle
828
+ const result1 = {
829
+ project: (0, compile_js_1.generateProjectDoc)(bundle1),
830
+ requirements: (0, compile_js_1.generateRequirementsDoc)(bundle1),
831
+ roadmap: (0, compile_js_1.generateRoadmapDoc)(bundle1),
832
+ state: (0, compile_js_1.generateStateDoc)(bundle1),
833
+ };
834
+ const result2 = {
835
+ project: (0, compile_js_1.generateProjectDoc)(bundle2),
836
+ requirements: (0, compile_js_1.generateRequirementsDoc)(bundle2),
837
+ roadmap: (0, compile_js_1.generateRoadmapDoc)(bundle2),
838
+ state: (0, compile_js_1.generateStateDoc)(bundle2),
839
+ };
840
+ assert.strictEqual(result1.project, result2.project, 'generateProjectDoc must be deterministic across runs');
841
+ assert.strictEqual(result1.requirements, result2.requirements, 'generateRequirementsDoc must be deterministic across runs');
842
+ assert.strictEqual(result1.roadmap, result2.roadmap, 'generateRoadmapDoc must be deterministic across runs');
843
+ assert.strictEqual(result1.state, result2.state, 'generateStateDoc must be deterministic across runs');
844
+ });
845
+ });
846
+ (0, node_test_1.describe)('golden files have correct structure', () => {
847
+ (0, node_test_1.it)('all 4 golden files pass validateHeadings() for their file types', () => {
848
+ const { validateHeadings } = require('../lib/parse.js');
849
+ const goldenFiles = [
850
+ ['PROJECT.md', path.join(FIXTURE_EXPECTED_CONTRACT_DIR, 'PROJECT.md')],
851
+ ['REQUIREMENTS.md', path.join(FIXTURE_EXPECTED_CONTRACT_DIR, 'REQUIREMENTS.md')],
852
+ ['ROADMAP.md', path.join(FIXTURE_EXPECTED_CONTRACT_DIR, 'ROADMAP.md')],
853
+ ['STATE.md', path.join(FIXTURE_EXPECTED_CONTRACT_DIR, 'STATE.md')],
854
+ ];
855
+ for (const [fileType, filePath] of goldenFiles) {
856
+ const content = fs.readFileSync(filePath, 'utf-8');
857
+ const validation = validateHeadings(content, fileType);
858
+ assert.ok(validation.valid, `Golden file ${fileType} failed heading validation. Missing: ${validation.missing.join(', ')}`);
859
+ }
860
+ });
861
+ });
862
+ });