gswd 1.0.1 → 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 (85) hide show
  1. package/bin/gswd-tools.cjs +228 -0
  2. package/commands/gswd/imagine.md +7 -1
  3. package/commands/gswd/start.md +507 -32
  4. package/dist/lib/audit.d.ts +205 -0
  5. package/dist/lib/audit.js +805 -0
  6. package/dist/lib/bootstrap.d.ts +103 -0
  7. package/dist/lib/bootstrap.js +563 -0
  8. package/dist/lib/compile.d.ts +239 -0
  9. package/dist/lib/compile.js +1152 -0
  10. package/dist/lib/config.d.ts +49 -0
  11. package/dist/lib/config.js +150 -0
  12. package/dist/lib/imagine-agents.d.ts +54 -0
  13. package/dist/lib/imagine-agents.js +185 -0
  14. package/dist/lib/imagine-gate.d.ts +47 -0
  15. package/dist/lib/imagine-gate.js +131 -0
  16. package/dist/lib/imagine-input.d.ts +46 -0
  17. package/dist/lib/imagine-input.js +233 -0
  18. package/dist/lib/imagine-synthesis.d.ts +90 -0
  19. package/dist/lib/imagine-synthesis.js +453 -0
  20. package/dist/lib/imagine.d.ts +56 -0
  21. package/dist/lib/imagine.js +413 -0
  22. package/dist/lib/intake.d.ts +27 -0
  23. package/dist/lib/intake.js +82 -0
  24. package/dist/lib/parse.d.ts +59 -0
  25. package/dist/lib/parse.js +171 -0
  26. package/dist/lib/render.d.ts +309 -0
  27. package/dist/lib/render.js +624 -0
  28. package/dist/lib/specify-agents.d.ts +120 -0
  29. package/dist/lib/specify-agents.js +269 -0
  30. package/dist/lib/specify-journeys.d.ts +124 -0
  31. package/dist/lib/specify-journeys.js +279 -0
  32. package/dist/lib/specify-nfr.d.ts +45 -0
  33. package/dist/lib/specify-nfr.js +159 -0
  34. package/dist/lib/specify-roles.d.ts +46 -0
  35. package/dist/lib/specify-roles.js +88 -0
  36. package/dist/lib/specify.d.ts +70 -0
  37. package/dist/lib/specify.js +676 -0
  38. package/dist/lib/state.d.ts +140 -0
  39. package/dist/lib/state.js +340 -0
  40. package/dist/tests/audit.test.d.ts +4 -0
  41. package/dist/tests/audit.test.js +1579 -0
  42. package/dist/tests/bootstrap.test.d.ts +5 -0
  43. package/dist/tests/bootstrap.test.js +611 -0
  44. package/dist/tests/compile.test.d.ts +4 -0
  45. package/dist/tests/compile.test.js +862 -0
  46. package/dist/tests/config.test.d.ts +4 -0
  47. package/dist/tests/config.test.js +191 -0
  48. package/dist/tests/imagine-agents.test.d.ts +6 -0
  49. package/dist/tests/imagine-agents.test.js +179 -0
  50. package/dist/tests/imagine-gate.test.d.ts +6 -0
  51. package/dist/tests/imagine-gate.test.js +264 -0
  52. package/dist/tests/imagine-input.test.d.ts +6 -0
  53. package/dist/tests/imagine-input.test.js +283 -0
  54. package/dist/tests/imagine-synthesis.test.d.ts +7 -0
  55. package/dist/tests/imagine-synthesis.test.js +380 -0
  56. package/dist/tests/imagine.test.d.ts +8 -0
  57. package/dist/tests/imagine.test.js +406 -0
  58. package/dist/tests/parse.test.d.ts +4 -0
  59. package/dist/tests/parse.test.js +285 -0
  60. package/dist/tests/render.test.d.ts +4 -0
  61. package/dist/tests/render.test.js +236 -0
  62. package/dist/tests/specify-agents.test.d.ts +4 -0
  63. package/dist/tests/specify-agents.test.js +352 -0
  64. package/dist/tests/specify-journeys.test.d.ts +5 -0
  65. package/dist/tests/specify-journeys.test.js +440 -0
  66. package/dist/tests/specify-nfr.test.d.ts +4 -0
  67. package/dist/tests/specify-nfr.test.js +205 -0
  68. package/dist/tests/specify-roles.test.d.ts +4 -0
  69. package/dist/tests/specify-roles.test.js +136 -0
  70. package/dist/tests/specify.test.d.ts +9 -0
  71. package/dist/tests/specify.test.js +544 -0
  72. package/dist/tests/state.test.d.ts +4 -0
  73. package/dist/tests/state.test.js +316 -0
  74. package/lib/bootstrap.ts +37 -11
  75. package/lib/compile.ts +426 -4
  76. package/lib/imagine-agents.ts +53 -7
  77. package/lib/imagine-synthesis.ts +170 -6
  78. package/lib/imagine.ts +59 -5
  79. package/lib/intake.ts +60 -0
  80. package/lib/parse.ts +2 -1
  81. package/lib/render.ts +566 -5
  82. package/lib/specify-agents.ts +25 -3
  83. package/lib/state.ts +115 -0
  84. package/package.json +3 -2
  85. package/templates/gswd/DECISIONS.template.md +3 -0
@@ -0,0 +1,544 @@
1
+ "use strict";
2
+ /**
3
+ * Tests for GSWD Specify Workflow Orchestrator
4
+ *
5
+ * Integration-level tests using skipAgents mode and temp directories.
6
+ * Covers: full workflow, artifact generation, auto mode, ID validation,
7
+ * state management, error handling.
8
+ * Minimum 15 test cases per Plan 03-05.
9
+ */
10
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ var desc = Object.getOwnPropertyDescriptor(m, k);
13
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
14
+ desc = { enumerable: true, get: function() { return m[k]; } };
15
+ }
16
+ Object.defineProperty(o, k2, desc);
17
+ }) : (function(o, m, k, k2) {
18
+ if (k2 === undefined) k2 = k;
19
+ o[k2] = m[k];
20
+ }));
21
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
22
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
23
+ }) : function(o, v) {
24
+ o["default"] = v;
25
+ });
26
+ var __importStar = (this && this.__importStar) || (function () {
27
+ var ownKeys = function(o) {
28
+ ownKeys = Object.getOwnPropertyNames || function (o) {
29
+ var ar = [];
30
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
31
+ return ar;
32
+ };
33
+ return ownKeys(o);
34
+ };
35
+ return function (mod) {
36
+ if (mod && mod.__esModule) return mod;
37
+ var result = {};
38
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
39
+ __setModuleDefault(result, mod);
40
+ return result;
41
+ };
42
+ })();
43
+ Object.defineProperty(exports, "__esModule", { value: true });
44
+ const node_test_1 = require("node:test");
45
+ const assert = __importStar(require("node:assert"));
46
+ const fs = __importStar(require("node:fs"));
47
+ const path = __importStar(require("node:path"));
48
+ const os = __importStar(require("node:os"));
49
+ const specify_js_1 = require("../lib/specify.js");
50
+ const state_js_1 = require("../lib/state.js");
51
+ const config_js_1 = require("../lib/config.js");
52
+ // ─── Test Fixtures ───────────────────────────────────────────────────────────
53
+ /** Create a temporary test environment with initialized state + config + DECISIONS.md */
54
+ function createTestEnv() {
55
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gswd-specify-test-'));
56
+ const planningDir = tmpDir;
57
+ const gswdDir = path.join(planningDir, 'gswd');
58
+ fs.mkdirSync(gswdDir, { recursive: true });
59
+ // Initialize STATE.json with imagine:done
60
+ const state = (0, state_js_1.createDefaultState)('test-project');
61
+ state.stage = 'imagine';
62
+ state.stage_status.imagine = 'done';
63
+ const statePath = path.join(gswdDir, 'STATE.json');
64
+ (0, state_js_1.safeWriteJson)(statePath, state);
65
+ // Initialize config.json
66
+ const configPath = path.join(planningDir, 'config.json');
67
+ (0, config_js_1.mergeGswdConfig)(configPath);
68
+ // Create DECISIONS.md (required for specify stage)
69
+ const decisionsContent = `# Decisions
70
+
71
+ ## Frozen Decisions
72
+ - **ICP:** Solo SaaS founders
73
+ - **Problem Statement:** Founders skip specs, leading to rework
74
+ - **Product Direction:** CLI Spec Generator
75
+ - **Wedge / Entry Point:** 3-question intake
76
+ - **Differentiator:** AI agents do PM research
77
+ - **Target User:** Technical solo founders
78
+ - **Timing Rationale:** LLMs now understand product requirements
79
+ - **MVP Boundary:** Imagine stage only
80
+
81
+ ## Success Metrics
82
+ - Activation rate
83
+ - Week-1 retention
84
+
85
+ ## Out of Scope
86
+ - Multi-user collaboration
87
+ - Paid integrations
88
+
89
+ ## Risks & Mitigations
90
+ - Risk: AI specs may be too generic
91
+ - Risk: Solo founders may not pay
92
+ - Risk: LLM quality degrades
93
+ - Risk: Manual spec habit hard to break
94
+ - Risk: Competitors add spec features
95
+
96
+ ## Open Questions
97
+ - None at this time
98
+ `;
99
+ (0, state_js_1.safeWriteFile)(path.join(planningDir, 'DECISIONS.md'), decisionsContent);
100
+ // Create IMAGINE.md (optional but nice to have)
101
+ (0, state_js_1.safeWriteFile)(path.join(planningDir, 'IMAGINE.md'), '# Imagine\n\n## Vision\nA CLI spec generator for solo founders.\n');
102
+ // Create templates directory with proper templates
103
+ const templatesDir = path.join(tmpDir, 'templates', 'gswd');
104
+ fs.mkdirSync(templatesDir, { recursive: true });
105
+ (0, state_js_1.safeWriteFile)(path.join(templatesDir, 'SPEC.template.md'), `# Specification\n\n## Roles & Permissions\n\n<!-- GSWD:CONTENT:ROLES -->\n\n## Functional Requirements\n\n<!-- GSWD:CONTENT:FRS -->\n\n## Acceptance Criteria\n\n<!-- GSWD:CONTENT:ACCEPTANCE -->\n\n## Traceability\n\n<!-- GSWD:CONTENT:TRACEABILITY -->\n\n<!-- GSWD:COMPLETE -->\n`);
106
+ (0, state_js_1.safeWriteFile)(path.join(templatesDir, 'JOURNEYS.template.md'), `# User Journeys\n\n## Journeys\n\n<!-- GSWD:CONTENT:JOURNEYS -->\n\n<!-- GSWD:COMPLETE -->\n`);
107
+ (0, state_js_1.safeWriteFile)(path.join(templatesDir, 'NFR.template.md'), `# Non-Functional Requirements\n\n## Non-Functional Requirements\n\n<!-- GSWD:CONTENT:NFRS -->\n\n<!-- GSWD:COMPLETE -->\n`);
108
+ (0, state_js_1.safeWriteFile)(path.join(templatesDir, 'ARCHITECTURE.template.md'), `# Architecture\n\n## Architecture\n\n### Components\n\n<!-- GSWD:CONTENT:COMPONENTS -->\n\n### Data Model\n\n<!-- GSWD:CONTENT:DATA_MODEL -->\n\n### Ownership Boundaries\n\n<!-- GSWD:CONTENT:OWNERSHIP -->\n\n<!-- GSWD:COMPLETE -->\n`);
109
+ (0, state_js_1.safeWriteFile)(path.join(templatesDir, 'INTEGRATIONS.template.md'), `# External Integrations\n\n## Integrations\n\n<!-- GSWD:CONTENT:INTEGRATIONS -->\n\n<!-- GSWD:COMPLETE -->\n`);
110
+ return {
111
+ planningDir,
112
+ configPath,
113
+ templatesDir,
114
+ cleanup: () => {
115
+ try {
116
+ fs.rmSync(tmpDir, { recursive: true, force: true });
117
+ }
118
+ catch {
119
+ // ignore cleanup errors
120
+ }
121
+ },
122
+ };
123
+ }
124
+ // ─── Workflow Sequence ───────────────────────────────────────────────────────
125
+ (0, node_test_1.describe)('Specify workflow sequence', () => {
126
+ let env;
127
+ (0, node_test_1.beforeEach)(() => {
128
+ env = createTestEnv();
129
+ });
130
+ (0, node_test_1.afterEach)(() => {
131
+ env.cleanup();
132
+ });
133
+ (0, node_test_1.it)('runSpecify with skipAgents produces all 5 artifacts', async () => {
134
+ const result = await (0, specify_js_1.runSpecify)({
135
+ auto: true,
136
+ skipAgents: true,
137
+ planningDir: env.planningDir,
138
+ configPath: env.configPath,
139
+ templatesDir: env.templatesDir,
140
+ });
141
+ assert.strictEqual(result.status, 'complete', `Expected complete but got errors: ${result.errors?.join(', ')}`);
142
+ assert.strictEqual(result.artifacts.length, 5, 'Should write 5 artifacts');
143
+ });
144
+ (0, node_test_1.it)('workflow produces 6 journeys', async () => {
145
+ const result = await (0, specify_js_1.runSpecify)({
146
+ auto: true,
147
+ skipAgents: true,
148
+ planningDir: env.planningDir,
149
+ configPath: env.configPath,
150
+ templatesDir: env.templatesDir,
151
+ });
152
+ assert.strictEqual(result.journeyCount, 6);
153
+ });
154
+ (0, node_test_1.it)('workflow extracts FRs from journeys', async () => {
155
+ const result = await (0, specify_js_1.runSpecify)({
156
+ auto: true,
157
+ skipAgents: true,
158
+ planningDir: env.planningDir,
159
+ configPath: env.configPath,
160
+ templatesDir: env.templatesDir,
161
+ });
162
+ assert.ok(result.frCount > 0, 'Should have extracted FRs');
163
+ });
164
+ (0, node_test_1.it)('workflow generates NFRs', async () => {
165
+ const result = await (0, specify_js_1.runSpecify)({
166
+ auto: true,
167
+ skipAgents: true,
168
+ planningDir: env.planningDir,
169
+ configPath: env.configPath,
170
+ templatesDir: env.templatesDir,
171
+ });
172
+ assert.ok(result.nfrCount > 0, 'Should have generated NFRs');
173
+ });
174
+ });
175
+ // ─── Artifact Generation ────────────────────────────────────────────────────
176
+ (0, node_test_1.describe)('Artifact generation', () => {
177
+ let env;
178
+ (0, node_test_1.beforeEach)(() => {
179
+ env = createTestEnv();
180
+ });
181
+ (0, node_test_1.afterEach)(() => {
182
+ env.cleanup();
183
+ });
184
+ (0, node_test_1.it)('SPEC.md exists and has required headings', async () => {
185
+ await (0, specify_js_1.runSpecify)({
186
+ auto: true,
187
+ skipAgents: true,
188
+ planningDir: env.planningDir,
189
+ configPath: env.configPath,
190
+ templatesDir: env.templatesDir,
191
+ });
192
+ const specPath = path.join(env.planningDir, 'SPEC.md');
193
+ assert.ok(fs.existsSync(specPath), 'SPEC.md should exist');
194
+ const content = fs.readFileSync(specPath, 'utf-8');
195
+ assert.ok(content.includes('## Roles & Permissions'), 'Should have Roles heading');
196
+ assert.ok(content.includes('## Functional Requirements'), 'Should have FR heading');
197
+ assert.ok(content.includes('## Acceptance Criteria'), 'Should have Acceptance heading');
198
+ });
199
+ (0, node_test_1.it)('JOURNEYS.md exists and has journey entries', async () => {
200
+ await (0, specify_js_1.runSpecify)({
201
+ auto: true,
202
+ skipAgents: true,
203
+ planningDir: env.planningDir,
204
+ configPath: env.configPath,
205
+ templatesDir: env.templatesDir,
206
+ });
207
+ const journeysPath = path.join(env.planningDir, 'JOURNEYS.md');
208
+ assert.ok(fs.existsSync(journeysPath), 'JOURNEYS.md should exist');
209
+ const content = fs.readFileSync(journeysPath, 'utf-8');
210
+ assert.ok(content.includes('## Journeys'), 'Should have Journeys heading');
211
+ assert.ok(content.includes('### J-001'), 'Should have at least J-001');
212
+ assert.ok(content.includes('### J-006'), 'Should have J-006 (6 journeys)');
213
+ });
214
+ (0, node_test_1.it)('NFR.md exists and has NFR content', async () => {
215
+ await (0, specify_js_1.runSpecify)({
216
+ auto: true,
217
+ skipAgents: true,
218
+ planningDir: env.planningDir,
219
+ configPath: env.configPath,
220
+ templatesDir: env.templatesDir,
221
+ });
222
+ const nfrPath = path.join(env.planningDir, 'NFR.md');
223
+ assert.ok(fs.existsSync(nfrPath), 'NFR.md should exist');
224
+ const content = fs.readFileSync(nfrPath, 'utf-8');
225
+ assert.ok(content.includes('## Non-Functional Requirements'), 'Should have NFR heading');
226
+ assert.ok(content.includes('### Security'), 'Should have Security category');
227
+ assert.ok(content.includes('### Performance'), 'Should have Performance category');
228
+ });
229
+ (0, node_test_1.it)('ARCHITECTURE.md exists and has required sub-headings', async () => {
230
+ await (0, specify_js_1.runSpecify)({
231
+ auto: true,
232
+ skipAgents: true,
233
+ planningDir: env.planningDir,
234
+ configPath: env.configPath,
235
+ templatesDir: env.templatesDir,
236
+ });
237
+ const archPath = path.join(env.planningDir, 'ARCHITECTURE.md');
238
+ assert.ok(fs.existsSync(archPath), 'ARCHITECTURE.md should exist');
239
+ const content = fs.readFileSync(archPath, 'utf-8');
240
+ assert.ok(content.includes('### Components'), 'Should have Components heading');
241
+ assert.ok(content.includes('### Data Model'), 'Should have Data Model heading');
242
+ assert.ok(content.includes('### Ownership Boundaries'), 'Should have Ownership heading');
243
+ });
244
+ (0, node_test_1.it)('INTEGRATIONS.md exists and has Integrations heading', async () => {
245
+ await (0, specify_js_1.runSpecify)({
246
+ auto: true,
247
+ skipAgents: true,
248
+ planningDir: env.planningDir,
249
+ configPath: env.configPath,
250
+ templatesDir: env.templatesDir,
251
+ });
252
+ const intPath = path.join(env.planningDir, 'INTEGRATIONS.md');
253
+ assert.ok(fs.existsSync(intPath), 'INTEGRATIONS.md should exist');
254
+ const content = fs.readFileSync(intPath, 'utf-8');
255
+ assert.ok(content.includes('## Integrations'), 'Should have Integrations heading');
256
+ });
257
+ });
258
+ // ─── ID Validation ──────────────────────────────────────────────────────────
259
+ (0, node_test_1.describe)('ID validation', () => {
260
+ let env;
261
+ (0, node_test_1.beforeEach)(() => {
262
+ env = createTestEnv();
263
+ });
264
+ (0, node_test_1.afterEach)(() => {
265
+ env.cleanup();
266
+ });
267
+ (0, node_test_1.it)('JOURNEYS.md IDs match J-XXX format', async () => {
268
+ await (0, specify_js_1.runSpecify)({
269
+ auto: true,
270
+ skipAgents: true,
271
+ planningDir: env.planningDir,
272
+ configPath: env.configPath,
273
+ templatesDir: env.templatesDir,
274
+ });
275
+ const content = fs.readFileSync(path.join(env.planningDir, 'JOURNEYS.md'), 'utf-8');
276
+ const journeyIds = content.match(/### J-\d{3}/g) || [];
277
+ assert.ok(journeyIds.length >= 6, `Should have 6+ J-XXX IDs, found ${journeyIds.length}`);
278
+ });
279
+ (0, node_test_1.it)('SPEC.md contains FR-XXX format IDs', async () => {
280
+ await (0, specify_js_1.runSpecify)({
281
+ auto: true,
282
+ skipAgents: true,
283
+ planningDir: env.planningDir,
284
+ configPath: env.configPath,
285
+ templatesDir: env.templatesDir,
286
+ });
287
+ const content = fs.readFileSync(path.join(env.planningDir, 'SPEC.md'), 'utf-8');
288
+ const frIds = content.match(/FR-\d{3}/g) || [];
289
+ assert.ok(frIds.length > 0, 'SPEC.md should contain FR IDs');
290
+ });
291
+ (0, node_test_1.it)('NFR.md contains NFR-XXX format IDs', async () => {
292
+ await (0, specify_js_1.runSpecify)({
293
+ auto: true,
294
+ skipAgents: true,
295
+ planningDir: env.planningDir,
296
+ configPath: env.configPath,
297
+ templatesDir: env.templatesDir,
298
+ });
299
+ const content = fs.readFileSync(path.join(env.planningDir, 'NFR.md'), 'utf-8');
300
+ const nfrIds = content.match(/NFR-\d{3}/g) || [];
301
+ assert.ok(nfrIds.length > 0, 'NFR.md should contain NFR IDs');
302
+ });
303
+ });
304
+ // ─── State Management ───────────────────────────────────────────────────────
305
+ (0, node_test_1.describe)('State management', () => {
306
+ let env;
307
+ (0, node_test_1.beforeEach)(() => {
308
+ env = createTestEnv();
309
+ });
310
+ (0, node_test_1.afterEach)(() => {
311
+ env.cleanup();
312
+ });
313
+ (0, node_test_1.it)('STATE.json specify status is done after success', async () => {
314
+ await (0, specify_js_1.runSpecify)({
315
+ auto: true,
316
+ skipAgents: true,
317
+ planningDir: env.planningDir,
318
+ configPath: env.configPath,
319
+ templatesDir: env.templatesDir,
320
+ });
321
+ const statePath = path.join(env.planningDir, 'gswd', 'STATE.json');
322
+ const state = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
323
+ assert.strictEqual(state.stage_status.specify, 'done');
324
+ });
325
+ (0, node_test_1.it)('STATE.json NOT updated to done on missing DECISIONS.md', async () => {
326
+ // Remove DECISIONS.md
327
+ fs.unlinkSync(path.join(env.planningDir, 'DECISIONS.md'));
328
+ const result = await (0, specify_js_1.runSpecify)({
329
+ auto: true,
330
+ skipAgents: true,
331
+ planningDir: env.planningDir,
332
+ configPath: env.configPath,
333
+ templatesDir: env.templatesDir,
334
+ });
335
+ assert.strictEqual(result.status, 'failed');
336
+ const statePath = path.join(env.planningDir, 'gswd', 'STATE.json');
337
+ const state = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
338
+ assert.notStrictEqual(state.stage_status.specify, 'done');
339
+ });
340
+ });
341
+ // ─── Auto Mode ──────────────────────────────────────────────────────────────
342
+ (0, node_test_1.describe)('Auto mode', () => {
343
+ let env;
344
+ (0, node_test_1.beforeEach)(() => {
345
+ env = createTestEnv();
346
+ });
347
+ (0, node_test_1.afterEach)(() => {
348
+ env.cleanup();
349
+ });
350
+ (0, node_test_1.it)('auto mode uses default single admin role', async () => {
351
+ await (0, specify_js_1.runSpecify)({
352
+ auto: true,
353
+ skipAgents: true,
354
+ planningDir: env.planningDir,
355
+ configPath: env.configPath,
356
+ templatesDir: env.templatesDir,
357
+ });
358
+ const specContent = fs.readFileSync(path.join(env.planningDir, 'SPEC.md'), 'utf-8');
359
+ assert.ok(specContent.includes('admin'), 'SPEC.md should include admin role');
360
+ });
361
+ });
362
+ // ─── Error Handling ─────────────────────────────────────────────────────────
363
+ (0, node_test_1.describe)('Error handling', () => {
364
+ (0, node_test_1.it)('missing DECISIONS.md produces clear error', async () => {
365
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gswd-specify-nodata-'));
366
+ const gswdDir = path.join(tmpDir, 'gswd');
367
+ fs.mkdirSync(gswdDir, { recursive: true });
368
+ const state = (0, state_js_1.createDefaultState)('test');
369
+ state.stage_status.imagine = 'done';
370
+ (0, state_js_1.safeWriteJson)(path.join(gswdDir, 'STATE.json'), state);
371
+ (0, config_js_1.mergeGswdConfig)(path.join(tmpDir, 'config.json'));
372
+ const result = await (0, specify_js_1.runSpecify)({
373
+ auto: true,
374
+ skipAgents: true,
375
+ planningDir: tmpDir,
376
+ configPath: path.join(tmpDir, 'config.json'),
377
+ });
378
+ assert.strictEqual(result.status, 'failed');
379
+ assert.ok(result.errors?.some((e) => e.includes('DECISIONS.md')), 'Error should mention DECISIONS.md');
380
+ try {
381
+ fs.rmSync(tmpDir, { recursive: true, force: true });
382
+ }
383
+ catch { /* ignore */ }
384
+ });
385
+ (0, node_test_1.it)('missing STATE.json produces clear error', async () => {
386
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gswd-specify-nostate-'));
387
+ fs.mkdirSync(path.join(tmpDir, 'gswd'), { recursive: true });
388
+ const result = await (0, specify_js_1.runSpecify)({
389
+ auto: true,
390
+ skipAgents: true,
391
+ planningDir: tmpDir,
392
+ configPath: path.join(tmpDir, 'config.json'),
393
+ });
394
+ assert.strictEqual(result.status, 'failed');
395
+ assert.ok(result.errors?.some((e) => e.includes('STATE.json')), 'Error should mention STATE.json');
396
+ try {
397
+ fs.rmSync(tmpDir, { recursive: true, force: true });
398
+ }
399
+ catch { /* ignore */ }
400
+ });
401
+ });
402
+ // ─── Cross-references ───────────────────────────────────────────────────────
403
+ (0, node_test_1.describe)('Cross-references', () => {
404
+ let env;
405
+ (0, node_test_1.beforeEach)(() => {
406
+ env = createTestEnv();
407
+ });
408
+ (0, node_test_1.afterEach)(() => {
409
+ env.cleanup();
410
+ });
411
+ (0, node_test_1.it)('SPEC.md traceability table references journey IDs', async () => {
412
+ await (0, specify_js_1.runSpecify)({
413
+ auto: true,
414
+ skipAgents: true,
415
+ planningDir: env.planningDir,
416
+ configPath: env.configPath,
417
+ templatesDir: env.templatesDir,
418
+ });
419
+ const specContent = fs.readFileSync(path.join(env.planningDir, 'SPEC.md'), 'utf-8');
420
+ assert.ok(specContent.includes('## Traceability'), 'Should have Traceability section');
421
+ assert.ok(specContent.includes('J-001'), 'Traceability should reference J-001');
422
+ });
423
+ });
424
+ // ─── Phase 8 Plan 02: FNDN-05 ID Allocation Regression Tests ─────────────────
425
+ (0, node_test_1.describe)('Phase 8: FNDN-05 ID allocation', () => {
426
+ let env;
427
+ (0, node_test_1.beforeEach)(() => {
428
+ env = createTestEnv();
429
+ });
430
+ (0, node_test_1.afterEach)(() => {
431
+ env.cleanup();
432
+ });
433
+ (0, node_test_1.it)('runSpecify with spawnFn writes id_registry to STATE.json', async () => {
434
+ // spawnFn that returns minimal agent output to avoid crashes
435
+ const spawnFn = async (_prompt) => {
436
+ return '## Journeys\n\n### J-001: Default Journey\nUser does something.\n\n#### Acceptance Tests\n- Test passes\n';
437
+ };
438
+ await (0, specify_js_1.runSpecify)({
439
+ auto: true,
440
+ skipAgents: false,
441
+ spawnFn,
442
+ planningDir: env.planningDir,
443
+ configPath: env.configPath,
444
+ templatesDir: env.templatesDir,
445
+ });
446
+ // Read STATE.json and verify id_registry exists with allocated ranges
447
+ const statePath = path.join(env.planningDir, 'gswd', 'STATE.json');
448
+ assert.ok(fs.existsSync(statePath), 'STATE.json must exist');
449
+ const state = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
450
+ assert.ok(state.id_registry, 'id_registry should exist in STATE.json after spawnFn run');
451
+ assert.ok(state.id_registry.allocated_ranges, 'allocated_ranges should exist');
452
+ // J range should be allocated (journey-mapper)
453
+ assert.ok(state.id_registry.allocated_ranges['J'], 'J range should be allocated for journey-mapper');
454
+ assert.ok(state.id_registry.allocated_ranges['J'].length > 0, 'J range should have at least one entry');
455
+ });
456
+ (0, node_test_1.it)('runSpecify with skipAgents=true does NOT call allocateIdRange', async () => {
457
+ await (0, specify_js_1.runSpecify)({
458
+ auto: true,
459
+ skipAgents: true,
460
+ planningDir: env.planningDir,
461
+ configPath: env.configPath,
462
+ templatesDir: env.templatesDir,
463
+ });
464
+ // STATE.json should exist but have no id_registry (skipAgents=true skips allocation)
465
+ const statePath = path.join(env.planningDir, 'gswd', 'STATE.json');
466
+ assert.ok(fs.existsSync(statePath), 'STATE.json must exist');
467
+ const state = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
468
+ // id_registry should not exist or have no allocated ranges
469
+ if (state.id_registry && state.id_registry.allocated_ranges) {
470
+ const totalRanges = Object.values(state.id_registry.allocated_ranges)
471
+ .reduce((sum, arr) => sum + arr.length, 0);
472
+ assert.strictEqual(totalRanges, 0, 'No ranges should be allocated when skipAgents=true');
473
+ }
474
+ // Passing either way — no id_registry is the expected state
475
+ });
476
+ });
477
+ // ─── Phase 8: Integration Fixes Regression Tests ─────────────────────────────
478
+ (0, node_test_1.describe)('Phase 8: Integration fixes', () => {
479
+ let env;
480
+ (0, node_test_1.beforeEach)(() => {
481
+ env = createTestEnv();
482
+ });
483
+ (0, node_test_1.afterEach)(() => {
484
+ env.cleanup();
485
+ });
486
+ (0, node_test_1.it)('providedArchitecture without providedIntegrations does not crash', async () => {
487
+ // This tests the code path where providedArchitecture is set but
488
+ // providedIntegrations is absent and skipAgents is false and spawnFn is absent.
489
+ // Before fix: integrationsContent was undefined, causing runtime crash.
490
+ const result = await (0, specify_js_1.runSpecify)({
491
+ auto: true,
492
+ skipAgents: false,
493
+ providedArchitecture: '## Architecture\n\n### C-001: App Core\n**Responsibility:** Core logic\n---\n### Data Model\n---\n### Ownership',
494
+ // providedIntegrations intentionally absent
495
+ planningDir: env.planningDir,
496
+ configPath: env.configPath,
497
+ templatesDir: env.templatesDir,
498
+ });
499
+ // Should complete without throwing — integrationsContent initialised to default
500
+ assert.ok(result.status !== 'failed' || !result.errors?.some(e => e.includes('undefined')), 'Should not have undefined-related errors');
501
+ const intPath = path.join(env.planningDir, 'INTEGRATIONS.md');
502
+ assert.ok(fs.existsSync(intPath), 'INTEGRATIONS.md should exist even without providedIntegrations');
503
+ });
504
+ (0, node_test_1.it)('validateIntegration is called on content with I-XXX IDs', async () => {
505
+ // Provide integration content with a valid I-001 integration
506
+ const result = await (0, specify_js_1.runSpecify)({
507
+ auto: true,
508
+ skipAgents: true,
509
+ providedIntegrations: '## Integrations\n\n### I-001: Stripe\n**Status:** approved\n**Fallback:** None\n',
510
+ planningDir: env.planningDir,
511
+ configPath: env.configPath,
512
+ templatesDir: env.templatesDir,
513
+ });
514
+ // Should complete — valid integration produces no validation errors
515
+ const integrationErrors = result.errors?.filter(e => e.toLowerCase().includes('integration')) || [];
516
+ assert.strictEqual(integrationErrors.length, 0, 'Valid integration should produce no validation errors');
517
+ });
518
+ (0, node_test_1.it)('deferred with fallback integration passes validateIntegration without error', async () => {
519
+ const result = await (0, specify_js_1.runSpecify)({
520
+ auto: true,
521
+ skipAgents: true,
522
+ providedIntegrations: '## Integrations\n\n### I-001: Stripe\n**Status:** deferred with fallback\n**Fallback:** Use manual invoicing\n',
523
+ planningDir: env.planningDir,
524
+ configPath: env.configPath,
525
+ templatesDir: env.templatesDir,
526
+ });
527
+ const integrationErrors = result.errors?.filter(e => e.toLowerCase().includes('integration') || e.toLowerCase().includes('invalid status')) || [];
528
+ assert.strictEqual(integrationErrors.length, 0, `Deferred with fallback integration should produce no validation errors, got: ${integrationErrors.join(', ')}`);
529
+ });
530
+ (0, node_test_1.it)('default journeys path still works when no agent content available', async () => {
531
+ const result = await (0, specify_js_1.runSpecify)({
532
+ auto: true,
533
+ skipAgents: true,
534
+ planningDir: env.planningDir,
535
+ configPath: env.configPath,
536
+ templatesDir: env.templatesDir,
537
+ });
538
+ assert.ok(result.status === 'complete' || result.status === 'success', 'Should complete with default journeys');
539
+ const jPath = path.join(env.planningDir, 'JOURNEYS.md');
540
+ assert.ok(fs.existsSync(jPath), 'JOURNEYS.md should exist with default journeys');
541
+ const content = fs.readFileSync(jPath, 'utf-8');
542
+ assert.ok(content.includes('J-001'), 'Default journeys should contain J-001');
543
+ });
544
+ });
@@ -0,0 +1,4 @@
1
+ /**
2
+ * State module tests — atomic writes, STATE.json CRUD, idempotent init, ID registry
3
+ */
4
+ export {};