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.
- package/bin/gswd-tools.cjs +228 -0
- package/bin/install.js +8 -0
- package/commands/gswd/imagine.md +7 -1
- package/commands/gswd/start.md +507 -32
- package/dist/lib/audit.d.ts +205 -0
- package/dist/lib/audit.js +805 -0
- package/dist/lib/bootstrap.d.ts +103 -0
- package/dist/lib/bootstrap.js +563 -0
- package/dist/lib/compile.d.ts +239 -0
- package/dist/lib/compile.js +1152 -0
- package/dist/lib/config.d.ts +49 -0
- package/dist/lib/config.js +150 -0
- package/dist/lib/imagine-agents.d.ts +54 -0
- package/dist/lib/imagine-agents.js +185 -0
- package/dist/lib/imagine-gate.d.ts +47 -0
- package/dist/lib/imagine-gate.js +131 -0
- package/dist/lib/imagine-input.d.ts +46 -0
- package/dist/lib/imagine-input.js +233 -0
- package/dist/lib/imagine-synthesis.d.ts +90 -0
- package/dist/lib/imagine-synthesis.js +453 -0
- package/dist/lib/imagine.d.ts +56 -0
- package/dist/lib/imagine.js +413 -0
- package/dist/lib/intake.d.ts +27 -0
- package/dist/lib/intake.js +82 -0
- package/dist/lib/parse.d.ts +59 -0
- package/dist/lib/parse.js +171 -0
- package/dist/lib/render.d.ts +309 -0
- package/dist/lib/render.js +624 -0
- package/dist/lib/specify-agents.d.ts +120 -0
- package/dist/lib/specify-agents.js +269 -0
- package/dist/lib/specify-journeys.d.ts +124 -0
- package/dist/lib/specify-journeys.js +279 -0
- package/dist/lib/specify-nfr.d.ts +45 -0
- package/dist/lib/specify-nfr.js +159 -0
- package/dist/lib/specify-roles.d.ts +46 -0
- package/dist/lib/specify-roles.js +88 -0
- package/dist/lib/specify.d.ts +70 -0
- package/dist/lib/specify.js +676 -0
- package/dist/lib/state.d.ts +140 -0
- package/dist/lib/state.js +340 -0
- package/dist/tests/audit.test.d.ts +4 -0
- package/dist/tests/audit.test.js +1579 -0
- package/dist/tests/bootstrap.test.d.ts +5 -0
- package/dist/tests/bootstrap.test.js +611 -0
- package/dist/tests/compile.test.d.ts +4 -0
- package/dist/tests/compile.test.js +862 -0
- package/dist/tests/config.test.d.ts +4 -0
- package/dist/tests/config.test.js +191 -0
- package/dist/tests/imagine-agents.test.d.ts +6 -0
- package/dist/tests/imagine-agents.test.js +179 -0
- package/dist/tests/imagine-gate.test.d.ts +6 -0
- package/dist/tests/imagine-gate.test.js +264 -0
- package/dist/tests/imagine-input.test.d.ts +6 -0
- package/dist/tests/imagine-input.test.js +283 -0
- package/dist/tests/imagine-synthesis.test.d.ts +7 -0
- package/dist/tests/imagine-synthesis.test.js +380 -0
- package/dist/tests/imagine.test.d.ts +8 -0
- package/dist/tests/imagine.test.js +406 -0
- package/dist/tests/parse.test.d.ts +4 -0
- package/dist/tests/parse.test.js +285 -0
- package/dist/tests/render.test.d.ts +4 -0
- package/dist/tests/render.test.js +236 -0
- package/dist/tests/specify-agents.test.d.ts +4 -0
- package/dist/tests/specify-agents.test.js +352 -0
- package/dist/tests/specify-journeys.test.d.ts +5 -0
- package/dist/tests/specify-journeys.test.js +440 -0
- package/dist/tests/specify-nfr.test.d.ts +4 -0
- package/dist/tests/specify-nfr.test.js +205 -0
- package/dist/tests/specify-roles.test.d.ts +4 -0
- package/dist/tests/specify-roles.test.js +136 -0
- package/dist/tests/specify.test.d.ts +9 -0
- package/dist/tests/specify.test.js +544 -0
- package/dist/tests/state.test.d.ts +4 -0
- package/dist/tests/state.test.js +316 -0
- package/lib/bootstrap.ts +37 -11
- package/lib/compile.ts +426 -4
- package/lib/imagine-agents.ts +53 -7
- package/lib/imagine-synthesis.ts +170 -6
- package/lib/imagine.ts +59 -5
- package/lib/intake.ts +60 -0
- package/lib/parse.ts +2 -1
- package/lib/render.ts +566 -5
- package/lib/specify-agents.ts +25 -3
- package/lib/state.ts +115 -0
- package/package.json +4 -2
- 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
|
+
});
|