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.
- package/bin/gswd-tools.cjs +228 -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 +3 -2
- 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
|
+
});
|