gswd 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/bin/gswd-tools.cjs +228 -0
  2. package/bin/install.js +8 -0
  3. package/commands/gswd/imagine.md +7 -1
  4. package/commands/gswd/start.md +507 -32
  5. package/dist/lib/audit.d.ts +205 -0
  6. package/dist/lib/audit.js +805 -0
  7. package/dist/lib/bootstrap.d.ts +103 -0
  8. package/dist/lib/bootstrap.js +563 -0
  9. package/dist/lib/compile.d.ts +239 -0
  10. package/dist/lib/compile.js +1152 -0
  11. package/dist/lib/config.d.ts +49 -0
  12. package/dist/lib/config.js +150 -0
  13. package/dist/lib/imagine-agents.d.ts +54 -0
  14. package/dist/lib/imagine-agents.js +185 -0
  15. package/dist/lib/imagine-gate.d.ts +47 -0
  16. package/dist/lib/imagine-gate.js +131 -0
  17. package/dist/lib/imagine-input.d.ts +46 -0
  18. package/dist/lib/imagine-input.js +233 -0
  19. package/dist/lib/imagine-synthesis.d.ts +90 -0
  20. package/dist/lib/imagine-synthesis.js +453 -0
  21. package/dist/lib/imagine.d.ts +56 -0
  22. package/dist/lib/imagine.js +413 -0
  23. package/dist/lib/intake.d.ts +27 -0
  24. package/dist/lib/intake.js +82 -0
  25. package/dist/lib/parse.d.ts +59 -0
  26. package/dist/lib/parse.js +171 -0
  27. package/dist/lib/render.d.ts +309 -0
  28. package/dist/lib/render.js +624 -0
  29. package/dist/lib/specify-agents.d.ts +120 -0
  30. package/dist/lib/specify-agents.js +269 -0
  31. package/dist/lib/specify-journeys.d.ts +124 -0
  32. package/dist/lib/specify-journeys.js +279 -0
  33. package/dist/lib/specify-nfr.d.ts +45 -0
  34. package/dist/lib/specify-nfr.js +159 -0
  35. package/dist/lib/specify-roles.d.ts +46 -0
  36. package/dist/lib/specify-roles.js +88 -0
  37. package/dist/lib/specify.d.ts +70 -0
  38. package/dist/lib/specify.js +676 -0
  39. package/dist/lib/state.d.ts +140 -0
  40. package/dist/lib/state.js +340 -0
  41. package/dist/tests/audit.test.d.ts +4 -0
  42. package/dist/tests/audit.test.js +1579 -0
  43. package/dist/tests/bootstrap.test.d.ts +5 -0
  44. package/dist/tests/bootstrap.test.js +611 -0
  45. package/dist/tests/compile.test.d.ts +4 -0
  46. package/dist/tests/compile.test.js +862 -0
  47. package/dist/tests/config.test.d.ts +4 -0
  48. package/dist/tests/config.test.js +191 -0
  49. package/dist/tests/imagine-agents.test.d.ts +6 -0
  50. package/dist/tests/imagine-agents.test.js +179 -0
  51. package/dist/tests/imagine-gate.test.d.ts +6 -0
  52. package/dist/tests/imagine-gate.test.js +264 -0
  53. package/dist/tests/imagine-input.test.d.ts +6 -0
  54. package/dist/tests/imagine-input.test.js +283 -0
  55. package/dist/tests/imagine-synthesis.test.d.ts +7 -0
  56. package/dist/tests/imagine-synthesis.test.js +380 -0
  57. package/dist/tests/imagine.test.d.ts +8 -0
  58. package/dist/tests/imagine.test.js +406 -0
  59. package/dist/tests/parse.test.d.ts +4 -0
  60. package/dist/tests/parse.test.js +285 -0
  61. package/dist/tests/render.test.d.ts +4 -0
  62. package/dist/tests/render.test.js +236 -0
  63. package/dist/tests/specify-agents.test.d.ts +4 -0
  64. package/dist/tests/specify-agents.test.js +352 -0
  65. package/dist/tests/specify-journeys.test.d.ts +5 -0
  66. package/dist/tests/specify-journeys.test.js +440 -0
  67. package/dist/tests/specify-nfr.test.d.ts +4 -0
  68. package/dist/tests/specify-nfr.test.js +205 -0
  69. package/dist/tests/specify-roles.test.d.ts +4 -0
  70. package/dist/tests/specify-roles.test.js +136 -0
  71. package/dist/tests/specify.test.d.ts +9 -0
  72. package/dist/tests/specify.test.js +544 -0
  73. package/dist/tests/state.test.d.ts +4 -0
  74. package/dist/tests/state.test.js +316 -0
  75. package/lib/bootstrap.ts +37 -11
  76. package/lib/compile.ts +426 -4
  77. package/lib/imagine-agents.ts +53 -7
  78. package/lib/imagine-synthesis.ts +170 -6
  79. package/lib/imagine.ts +59 -5
  80. package/lib/intake.ts +60 -0
  81. package/lib/parse.ts +2 -1
  82. package/lib/render.ts +566 -5
  83. package/lib/specify-agents.ts +25 -3
  84. package/lib/state.ts +115 -0
  85. package/package.json +4 -2
  86. package/templates/gswd/DECISIONS.template.md +3 -0
@@ -0,0 +1,440 @@
1
+ "use strict";
2
+ /**
3
+ * Specify Journeys module tests — Journey types, scope/priority heuristics,
4
+ * FR extraction, bidirectional cross-linking, traceability, coverage validation
5
+ */
6
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
7
+ if (k2 === undefined) k2 = k;
8
+ var desc = Object.getOwnPropertyDescriptor(m, k);
9
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
10
+ desc = { enumerable: true, get: function() { return m[k]; } };
11
+ }
12
+ Object.defineProperty(o, k2, desc);
13
+ }) : (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ o[k2] = m[k];
16
+ }));
17
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
18
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
19
+ }) : function(o, v) {
20
+ o["default"] = v;
21
+ });
22
+ var __importStar = (this && this.__importStar) || (function () {
23
+ var ownKeys = function(o) {
24
+ ownKeys = Object.getOwnPropertyNames || function (o) {
25
+ var ar = [];
26
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
27
+ return ar;
28
+ };
29
+ return ownKeys(o);
30
+ };
31
+ return function (mod) {
32
+ if (mod && mod.__esModule) return mod;
33
+ var result = {};
34
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
35
+ __setModuleDefault(result, mod);
36
+ return result;
37
+ };
38
+ })();
39
+ Object.defineProperty(exports, "__esModule", { value: true });
40
+ const node_test_1 = require("node:test");
41
+ const assert = __importStar(require("node:assert"));
42
+ const specify_journeys_js_1 = require("../lib/specify-journeys.js");
43
+ // ─── Helpers ────────────────────────────────────────────────────────────────
44
+ function makeStep(number, action) {
45
+ return { number, action, frIds: [] };
46
+ }
47
+ function makeValidJourney(overrides = {}) {
48
+ return {
49
+ id: 'J-001',
50
+ name: 'Test Journey',
51
+ type: 'core_action',
52
+ preconditions: ['User is logged in'],
53
+ steps: [
54
+ makeStep(1, 'User opens dashboard'),
55
+ makeStep(2, 'User clicks create button'),
56
+ makeStep(3, 'User fills in form fields'),
57
+ makeStep(4, 'User submits the form'),
58
+ makeStep(5, 'System confirms creation'),
59
+ ],
60
+ success: 'New item is created and visible',
61
+ failureModes: [
62
+ { scenario: 'Network timeout during submission', handling: 'Show retry dialog' },
63
+ { scenario: 'Validation error on required fields', handling: 'Highlight invalid fields' },
64
+ ],
65
+ acceptanceTests: ['User can create a new item without errors'],
66
+ linkedFRs: [],
67
+ linkedNFRs: [],
68
+ ...overrides,
69
+ };
70
+ }
71
+ // ─── JOURNEY_TYPES constant ──────────────────────────────────────────────────
72
+ (0, node_test_1.describe)('JOURNEY_TYPES', () => {
73
+ (0, node_test_1.it)('has exactly 6 entries', () => {
74
+ assert.strictEqual(specify_journeys_js_1.JOURNEY_TYPES.length, 6);
75
+ });
76
+ (0, node_test_1.it)('contains all required types', () => {
77
+ const types = specify_journeys_js_1.JOURNEY_TYPES.map((t) => t.type);
78
+ assert.ok(types.includes('onboarding'));
79
+ assert.ok(types.includes('core_action'));
80
+ assert.ok(types.includes('view_results'));
81
+ assert.ok(types.includes('settings'));
82
+ assert.ok(types.includes('error_states'));
83
+ assert.ok(types.includes('empty_states'));
84
+ });
85
+ (0, node_test_1.it)('each has a non-empty displayName', () => {
86
+ for (const entry of specify_journeys_js_1.JOURNEY_TYPES) {
87
+ assert.ok(entry.displayName.length > 0, `${entry.type} has empty displayName`);
88
+ }
89
+ });
90
+ });
91
+ // ─── assignScope ─────────────────────────────────────────────────────────────
92
+ (0, node_test_1.describe)('assignScope', () => {
93
+ (0, node_test_1.it)('onboarding -> v1', () => {
94
+ assert.strictEqual((0, specify_journeys_js_1.assignScope)('onboarding'), 'v1');
95
+ });
96
+ (0, node_test_1.it)('core_action -> v1', () => {
97
+ assert.strictEqual((0, specify_journeys_js_1.assignScope)('core_action'), 'v1');
98
+ });
99
+ (0, node_test_1.it)('error_states -> v1', () => {
100
+ assert.strictEqual((0, specify_journeys_js_1.assignScope)('error_states'), 'v1');
101
+ });
102
+ (0, node_test_1.it)('empty_states -> v1', () => {
103
+ assert.strictEqual((0, specify_journeys_js_1.assignScope)('empty_states'), 'v1');
104
+ });
105
+ (0, node_test_1.it)('view_results -> v1', () => {
106
+ assert.strictEqual((0, specify_journeys_js_1.assignScope)('view_results'), 'v1');
107
+ });
108
+ (0, node_test_1.it)('settings -> v1', () => {
109
+ assert.strictEqual((0, specify_journeys_js_1.assignScope)('settings'), 'v1');
110
+ });
111
+ });
112
+ // ─── assignPriority ──────────────────────────────────────────────────────────
113
+ (0, node_test_1.describe)('assignPriority', () => {
114
+ (0, node_test_1.it)('core_action -> P0', () => {
115
+ assert.strictEqual((0, specify_journeys_js_1.assignPriority)('core_action'), 'P0');
116
+ });
117
+ (0, node_test_1.it)('onboarding -> P0', () => {
118
+ assert.strictEqual((0, specify_journeys_js_1.assignPriority)('onboarding'), 'P0');
119
+ });
120
+ (0, node_test_1.it)('error_states -> P1', () => {
121
+ assert.strictEqual((0, specify_journeys_js_1.assignPriority)('error_states'), 'P1');
122
+ });
123
+ (0, node_test_1.it)('empty_states -> P1', () => {
124
+ assert.strictEqual((0, specify_journeys_js_1.assignPriority)('empty_states'), 'P1');
125
+ });
126
+ (0, node_test_1.it)('view_results -> P1', () => {
127
+ assert.strictEqual((0, specify_journeys_js_1.assignPriority)('view_results'), 'P1');
128
+ });
129
+ (0, node_test_1.it)('settings -> P1', () => {
130
+ assert.strictEqual((0, specify_journeys_js_1.assignPriority)('settings'), 'P1');
131
+ });
132
+ });
133
+ // ─── generateJourneyStructure ────────────────────────────────────────────────
134
+ (0, node_test_1.describe)('generateJourneyStructure', () => {
135
+ (0, node_test_1.it)('produces J-001 format ID for journeyNumber=1', () => {
136
+ const j = (0, specify_journeys_js_1.generateJourneyStructure)('onboarding', 1);
137
+ assert.strictEqual(j.id, 'J-001');
138
+ });
139
+ (0, node_test_1.it)('produces J-012 format ID for journeyNumber=12', () => {
140
+ const j = (0, specify_journeys_js_1.generateJourneyStructure)('core_action', 12);
141
+ assert.strictEqual(j.id, 'J-012');
142
+ });
143
+ (0, node_test_1.it)('has correct type field', () => {
144
+ const j = (0, specify_journeys_js_1.generateJourneyStructure)('settings', 3);
145
+ assert.strictEqual(j.type, 'settings');
146
+ });
147
+ (0, node_test_1.it)('has empty but initialized arrays', () => {
148
+ const j = (0, specify_journeys_js_1.generateJourneyStructure)('core_action', 1);
149
+ assert.ok(Array.isArray(j.steps));
150
+ assert.strictEqual(j.steps.length, 0);
151
+ assert.ok(Array.isArray(j.failureModes));
152
+ assert.strictEqual(j.failureModes.length, 0);
153
+ assert.ok(Array.isArray(j.acceptanceTests));
154
+ assert.strictEqual(j.acceptanceTests.length, 0);
155
+ assert.ok(Array.isArray(j.linkedFRs));
156
+ assert.strictEqual(j.linkedFRs.length, 0);
157
+ assert.ok(Array.isArray(j.linkedNFRs));
158
+ assert.strictEqual(j.linkedNFRs.length, 0);
159
+ });
160
+ (0, node_test_1.it)('uses display name from JOURNEY_TYPES', () => {
161
+ const j = (0, specify_journeys_js_1.generateJourneyStructure)('view_results', 3);
162
+ assert.strictEqual(j.name, 'View Results/History');
163
+ });
164
+ });
165
+ // ─── validateJourney ─────────────────────────────────────────────────────────
166
+ (0, node_test_1.describe)('validateJourney', () => {
167
+ (0, node_test_1.it)('valid journey with all required fields passes', () => {
168
+ const result = (0, specify_journeys_js_1.validateJourney)(makeValidJourney());
169
+ assert.strictEqual(result.valid, true);
170
+ assert.strictEqual(result.errors.length, 0);
171
+ });
172
+ (0, node_test_1.it)('journey with < 2 failure modes fails', () => {
173
+ const j = makeValidJourney({
174
+ failureModes: [{ scenario: 'Only one', handling: 'Handle' }],
175
+ });
176
+ const result = (0, specify_journeys_js_1.validateJourney)(j);
177
+ assert.strictEqual(result.valid, false);
178
+ assert.ok(result.errors.some((e) => e.includes('failure modes')));
179
+ });
180
+ (0, node_test_1.it)('journey with 0 acceptance tests fails', () => {
181
+ const j = makeValidJourney({ acceptanceTests: [] });
182
+ const result = (0, specify_journeys_js_1.validateJourney)(j);
183
+ assert.strictEqual(result.valid, false);
184
+ assert.ok(result.errors.some((e) => e.includes('acceptance tests')));
185
+ });
186
+ (0, node_test_1.it)('journey with empty preconditions fails', () => {
187
+ const j = makeValidJourney({ preconditions: [] });
188
+ const result = (0, specify_journeys_js_1.validateJourney)(j);
189
+ assert.strictEqual(result.valid, false);
190
+ assert.ok(result.errors.some((e) => e.includes('preconditions')));
191
+ });
192
+ (0, node_test_1.it)('journey with empty success fails', () => {
193
+ const j = makeValidJourney({ success: '' });
194
+ const result = (0, specify_journeys_js_1.validateJourney)(j);
195
+ assert.strictEqual(result.valid, false);
196
+ assert.ok(result.errors.some((e) => e.includes('success')));
197
+ });
198
+ (0, node_test_1.it)('error_states journey with 3 steps passes (lower minimum)', () => {
199
+ const j = makeValidJourney({
200
+ type: 'error_states',
201
+ steps: [
202
+ makeStep(1, 'Error occurs'),
203
+ makeStep(2, 'System displays error'),
204
+ makeStep(3, 'User acknowledges'),
205
+ ],
206
+ });
207
+ const result = (0, specify_journeys_js_1.validateJourney)(j);
208
+ assert.strictEqual(result.valid, true);
209
+ });
210
+ (0, node_test_1.it)('core_action journey with 4 steps fails (needs >= 5)', () => {
211
+ const j = makeValidJourney({
212
+ type: 'core_action',
213
+ steps: [
214
+ makeStep(1, 'Step 1'),
215
+ makeStep(2, 'Step 2'),
216
+ makeStep(3, 'Step 3'),
217
+ makeStep(4, 'Step 4'),
218
+ ],
219
+ });
220
+ const result = (0, specify_journeys_js_1.validateJourney)(j);
221
+ assert.strictEqual(result.valid, false);
222
+ assert.ok(result.errors.some((e) => e.includes('steps')));
223
+ });
224
+ (0, node_test_1.it)('failure mode > 200 chars fails conciseness check', () => {
225
+ const longScenario = 'A'.repeat(201);
226
+ const j = makeValidJourney({
227
+ failureModes: [
228
+ { scenario: longScenario, handling: 'Handle' },
229
+ { scenario: 'Short', handling: 'Handle' },
230
+ ],
231
+ });
232
+ const result = (0, specify_journeys_js_1.validateJourney)(j);
233
+ assert.strictEqual(result.valid, false);
234
+ assert.ok(result.errors.some((e) => e.includes('too long')));
235
+ });
236
+ });
237
+ // ─── extractFRsFromJourneys ─────────────────────────────────────────────────
238
+ (0, node_test_1.describe)('extractFRsFromJourneys', () => {
239
+ (0, node_test_1.it)('single journey with 5 steps produces 5 FRs', () => {
240
+ const journeys = [makeValidJourney()];
241
+ const frs = (0, specify_journeys_js_1.extractFRsFromJourneys)(journeys);
242
+ assert.strictEqual(frs.length, 5);
243
+ });
244
+ (0, node_test_1.it)('FR IDs are sequential: FR-001 through FR-005', () => {
245
+ const journeys = [makeValidJourney()];
246
+ const frs = (0, specify_journeys_js_1.extractFRsFromJourneys)(journeys);
247
+ assert.strictEqual(frs[0].id, 'FR-001');
248
+ assert.strictEqual(frs[1].id, 'FR-002');
249
+ assert.strictEqual(frs[2].id, 'FR-003');
250
+ assert.strictEqual(frs[3].id, 'FR-004');
251
+ assert.strictEqual(frs[4].id, 'FR-005');
252
+ });
253
+ (0, node_test_1.it)('core action journey FRs have scope=v1 and priority=P0', () => {
254
+ const journeys = [makeValidJourney({ type: 'core_action' })];
255
+ const frs = (0, specify_journeys_js_1.extractFRsFromJourneys)(journeys);
256
+ for (const fr of frs) {
257
+ assert.strictEqual(fr.scope, 'v1');
258
+ assert.strictEqual(fr.priority, 'P0');
259
+ }
260
+ });
261
+ (0, node_test_1.it)('settings journey FRs have scope=v1 and priority=P1', () => {
262
+ const journeys = [
263
+ makeValidJourney({
264
+ id: 'J-004',
265
+ type: 'settings',
266
+ }),
267
+ ];
268
+ const frs = (0, specify_journeys_js_1.extractFRsFromJourneys)(journeys);
269
+ for (const fr of frs) {
270
+ assert.strictEqual(fr.scope, 'v1');
271
+ assert.strictEqual(fr.priority, 'P1');
272
+ }
273
+ });
274
+ (0, node_test_1.it)('FR sourceJourneys contains the parent journey ID', () => {
275
+ const journeys = [makeValidJourney()];
276
+ const frs = (0, specify_journeys_js_1.extractFRsFromJourneys)(journeys);
277
+ for (const fr of frs) {
278
+ assert.ok(fr.sourceJourneys.includes('J-001'));
279
+ }
280
+ });
281
+ (0, node_test_1.it)('FR sourceSteps contains J-XXX step N format', () => {
282
+ const journeys = [makeValidJourney()];
283
+ const frs = (0, specify_journeys_js_1.extractFRsFromJourneys)(journeys);
284
+ assert.ok(frs[0].sourceSteps.includes('J-001 step 1'));
285
+ assert.ok(frs[2].sourceSteps.includes('J-001 step 3'));
286
+ });
287
+ (0, node_test_1.it)('multiple journeys produce sequentially numbered FRs', () => {
288
+ const j1 = makeValidJourney({
289
+ id: 'J-001',
290
+ steps: [makeStep(1, 'Action A'), makeStep(2, 'Action B'), makeStep(3, 'Action C'), makeStep(4, 'Action D'), makeStep(5, 'Action E')],
291
+ });
292
+ const j2 = makeValidJourney({
293
+ id: 'J-002',
294
+ type: 'settings',
295
+ steps: [makeStep(1, 'Action F'), makeStep(2, 'Action G'), makeStep(3, 'Action H'), makeStep(4, 'Action I'), makeStep(5, 'Action J')],
296
+ });
297
+ const frs = (0, specify_journeys_js_1.extractFRsFromJourneys)([j1, j2]);
298
+ assert.strictEqual(frs.length, 10);
299
+ assert.strictEqual(frs[0].id, 'FR-001');
300
+ assert.strictEqual(frs[9].id, 'FR-010');
301
+ });
302
+ (0, node_test_1.it)('duplicate step descriptions merge into single FR with multiple sources', () => {
303
+ const j1 = makeValidJourney({
304
+ id: 'J-001',
305
+ steps: [makeStep(1, 'User logs in'), makeStep(2, 'Step B'), makeStep(3, 'Step C'), makeStep(4, 'Step D'), makeStep(5, 'Step E')],
306
+ });
307
+ const j2 = makeValidJourney({
308
+ id: 'J-002',
309
+ steps: [makeStep(1, 'User logs in'), makeStep(2, 'Step F'), makeStep(3, 'Step G'), makeStep(4, 'Step H'), makeStep(5, 'Step I')],
310
+ });
311
+ const frs = (0, specify_journeys_js_1.extractFRsFromJourneys)([j1, j2]);
312
+ // "User logs in" should be merged, so 5 + 5 - 1 = 9 FRs
313
+ assert.strictEqual(frs.length, 9);
314
+ const loginFr = frs.find((fr) => fr.description === 'User logs in');
315
+ assert.ok(loginFr);
316
+ assert.ok(loginFr.sourceJourneys.includes('J-001'));
317
+ assert.ok(loginFr.sourceJourneys.includes('J-002'));
318
+ assert.strictEqual(loginFr.sourceSteps.length, 2);
319
+ });
320
+ });
321
+ // ─── Bidirectional cross-linking ─────────────────────────────────────────────
322
+ (0, node_test_1.describe)('Bidirectional cross-linking', () => {
323
+ (0, node_test_1.it)('journey.linkedFRs contains all FRs from its steps', () => {
324
+ const journeys = [makeValidJourney()];
325
+ (0, specify_journeys_js_1.extractFRsFromJourneys)(journeys);
326
+ assert.strictEqual(journeys[0].linkedFRs.length, 5);
327
+ assert.ok(journeys[0].linkedFRs.includes('FR-001'));
328
+ assert.ok(journeys[0].linkedFRs.includes('FR-005'));
329
+ });
330
+ (0, node_test_1.it)('each step.frIds contains the FR IDs generated from it', () => {
331
+ const journeys = [makeValidJourney()];
332
+ (0, specify_journeys_js_1.extractFRsFromJourneys)(journeys);
333
+ assert.ok(journeys[0].steps[0].frIds.includes('FR-001'));
334
+ assert.ok(journeys[0].steps[4].frIds.includes('FR-005'));
335
+ });
336
+ (0, node_test_1.it)('FR.sourceJourneys correctly references back to the journey', () => {
337
+ const journeys = [makeValidJourney()];
338
+ const frs = (0, specify_journeys_js_1.extractFRsFromJourneys)(journeys);
339
+ for (const fr of frs) {
340
+ assert.ok(fr.sourceJourneys.includes('J-001'));
341
+ }
342
+ });
343
+ });
344
+ // ─── buildTraceabilityMap ────────────────────────────────────────────────────
345
+ (0, node_test_1.describe)('buildTraceabilityMap', () => {
346
+ (0, node_test_1.it)('produces one entry per FR', () => {
347
+ const journeys = [makeValidJourney()];
348
+ const frs = (0, specify_journeys_js_1.extractFRsFromJourneys)(journeys);
349
+ const map = (0, specify_journeys_js_1.buildTraceabilityMap)(frs);
350
+ assert.strictEqual(map.length, frs.length);
351
+ });
352
+ (0, node_test_1.it)('each entry has correct journeyRefs format', () => {
353
+ const journeys = [makeValidJourney()];
354
+ const frs = (0, specify_journeys_js_1.extractFRsFromJourneys)(journeys);
355
+ const map = (0, specify_journeys_js_1.buildTraceabilityMap)(frs);
356
+ assert.ok(map[0].journeyRefs[0].includes('J-001 (step 1)'));
357
+ });
358
+ (0, node_test_1.it)('entries sorted by FR ID', () => {
359
+ const journeys = [makeValidJourney()];
360
+ const frs = (0, specify_journeys_js_1.extractFRsFromJourneys)(journeys);
361
+ const map = (0, specify_journeys_js_1.buildTraceabilityMap)(frs);
362
+ for (let i = 1; i < map.length; i++) {
363
+ assert.ok(map[i - 1].frId < map[i].frId);
364
+ }
365
+ });
366
+ (0, node_test_1.it)('FR with multiple journey sources shows all references', () => {
367
+ const j1 = makeValidJourney({
368
+ id: 'J-001',
369
+ steps: [makeStep(1, 'Shared action'), makeStep(2, 'B'), makeStep(3, 'C'), makeStep(4, 'D'), makeStep(5, 'E')],
370
+ });
371
+ const j2 = makeValidJourney({
372
+ id: 'J-002',
373
+ steps: [makeStep(1, 'Shared action'), makeStep(2, 'F'), makeStep(3, 'G'), makeStep(4, 'H'), makeStep(5, 'I')],
374
+ });
375
+ const frs = (0, specify_journeys_js_1.extractFRsFromJourneys)([j1, j2]);
376
+ const map = (0, specify_journeys_js_1.buildTraceabilityMap)(frs);
377
+ const sharedEntry = map.find((e) => e.description === 'Shared action');
378
+ assert.ok(sharedEntry);
379
+ assert.strictEqual(sharedEntry.journeyRefs.length, 2);
380
+ assert.ok(sharedEntry.journeyRefs.some((r) => r.includes('J-001')));
381
+ assert.ok(sharedEntry.journeyRefs.some((r) => r.includes('J-002')));
382
+ });
383
+ });
384
+ // ─── validateFRCoverage ──────────────────────────────────────────────────────
385
+ (0, node_test_1.describe)('validateFRCoverage', () => {
386
+ (0, node_test_1.it)('valid extraction passes coverage check', () => {
387
+ const journeys = [makeValidJourney()];
388
+ const frs = (0, specify_journeys_js_1.extractFRsFromJourneys)(journeys);
389
+ const result = (0, specify_journeys_js_1.validateFRCoverage)(journeys, frs);
390
+ assert.strictEqual(result.valid, true);
391
+ assert.strictEqual(result.errors.length, 0);
392
+ });
393
+ (0, node_test_1.it)('journey with step having no FR fails', () => {
394
+ const journeys = [makeValidJourney()];
395
+ // Don't run extraction — steps have empty frIds
396
+ const frs = [];
397
+ const result = (0, specify_journeys_js_1.validateFRCoverage)(journeys, frs);
398
+ assert.strictEqual(result.valid, false);
399
+ assert.ok(result.errors.some((e) => e.includes('no FR coverage')));
400
+ });
401
+ (0, node_test_1.it)('FR with no source journey fails', () => {
402
+ const orphanFr = {
403
+ id: 'FR-999',
404
+ description: 'Orphan',
405
+ scope: 'v1',
406
+ priority: 'P0',
407
+ sourceJourneys: [],
408
+ sourceSteps: [],
409
+ };
410
+ const result = (0, specify_journeys_js_1.validateFRCoverage)([], [orphanFr]);
411
+ assert.strictEqual(result.valid, false);
412
+ assert.ok(result.errors.some((e) => e.includes('orphan FR')));
413
+ });
414
+ (0, node_test_1.it)('invalid FR ID format fails', () => {
415
+ const badFr = {
416
+ id: 'INVALID',
417
+ description: 'Bad ID',
418
+ scope: 'v1',
419
+ priority: 'P0',
420
+ sourceJourneys: ['J-001'],
421
+ sourceSteps: ['J-001 step 1'],
422
+ };
423
+ const result = (0, specify_journeys_js_1.validateFRCoverage)([], [badFr]);
424
+ assert.strictEqual(result.valid, false);
425
+ assert.ok(result.errors.some((e) => e.includes('Invalid FR ID')));
426
+ });
427
+ (0, node_test_1.it)('invalid scope tag fails', () => {
428
+ const badFr = {
429
+ id: 'FR-001',
430
+ description: 'Bad scope',
431
+ scope: 'invalid',
432
+ priority: 'P0',
433
+ sourceJourneys: ['J-001'],
434
+ sourceSteps: ['J-001 step 1'],
435
+ };
436
+ const result = (0, specify_journeys_js_1.validateFRCoverage)([], [badFr]);
437
+ assert.strictEqual(result.valid, false);
438
+ assert.ok(result.errors.some((e) => e.includes('invalid scope')));
439
+ });
440
+ });
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Specify NFR module tests — NFR categories, generation, validation
3
+ */
4
+ export {};
@@ -0,0 +1,205 @@
1
+ "use strict";
2
+ /**
3
+ * Specify NFR module tests — NFR categories, generation, validation
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 specify_nfr_js_1 = require("../lib/specify-nfr.js");
42
+ // ─── Helpers ────────────────────────────────────────────────────────────────
43
+ function makeSampleFRs() {
44
+ return [
45
+ { id: 'FR-001', description: 'User opens dashboard', scope: 'v1', priority: 'P0', sourceJourneys: ['J-001'], sourceSteps: ['J-001 step 1'] },
46
+ { id: 'FR-002', description: 'User clicks create', scope: 'v1', priority: 'P0', sourceJourneys: ['J-001'], sourceSteps: ['J-001 step 2'] },
47
+ { id: 'FR-003', description: 'User fills form', scope: 'v1', priority: 'P1', sourceJourneys: ['J-001'], sourceSteps: ['J-001 step 3'] },
48
+ { id: 'FR-004', description: 'User views settings', scope: 'v1', priority: 'P1', sourceJourneys: ['J-004'], sourceSteps: ['J-004 step 1'] },
49
+ { id: 'FR-005', description: 'Future feature', scope: 'v2', priority: 'P2', sourceJourneys: ['J-004'], sourceSteps: ['J-004 step 2'] },
50
+ ];
51
+ }
52
+ // ─── NFR_CATEGORIES constant ────────────────────────────────────────────────
53
+ (0, node_test_1.describe)('NFR_CATEGORIES', () => {
54
+ (0, node_test_1.it)('has exactly 4 entries', () => {
55
+ assert.strictEqual(specify_nfr_js_1.NFR_CATEGORIES.length, 4);
56
+ });
57
+ (0, node_test_1.it)('contains security, privacy, performance, observability', () => {
58
+ const cats = specify_nfr_js_1.NFR_CATEGORIES.map((c) => c.category);
59
+ assert.ok(cats.includes('security'));
60
+ assert.ok(cats.includes('privacy'));
61
+ assert.ok(cats.includes('performance'));
62
+ assert.ok(cats.includes('observability'));
63
+ });
64
+ (0, node_test_1.it)('each has non-empty description', () => {
65
+ for (const entry of specify_nfr_js_1.NFR_CATEGORIES) {
66
+ assert.ok(entry.description.length > 0, `${entry.category} has empty description`);
67
+ }
68
+ });
69
+ });
70
+ // ─── generateNFRs ───────────────────────────────────────────────────────────
71
+ (0, node_test_1.describe)('generateNFRs', () => {
72
+ (0, node_test_1.it)('produces NFRs in all 4 categories', () => {
73
+ const frs = makeSampleFRs();
74
+ const nfrs = (0, specify_nfr_js_1.generateNFRs)(frs);
75
+ const cats = new Set(nfrs.map((n) => n.category));
76
+ assert.ok(cats.has('security'));
77
+ assert.ok(cats.has('privacy'));
78
+ assert.ok(cats.has('performance'));
79
+ assert.ok(cats.has('observability'));
80
+ });
81
+ (0, node_test_1.it)('NFR IDs are sequential: NFR-001, NFR-002, etc.', () => {
82
+ const frs = makeSampleFRs();
83
+ const nfrs = (0, specify_nfr_js_1.generateNFRs)(frs);
84
+ for (let i = 0; i < nfrs.length; i++) {
85
+ const expected = `NFR-${String(i + 1).padStart(3, '0')}`;
86
+ assert.strictEqual(nfrs[i].id, expected);
87
+ }
88
+ });
89
+ (0, node_test_1.it)('security NFRs include input validation NFR', () => {
90
+ const frs = makeSampleFRs();
91
+ const nfrs = (0, specify_nfr_js_1.generateNFRs)(frs);
92
+ const securityNfrs = nfrs.filter((n) => n.category === 'security');
93
+ assert.ok(securityNfrs.some((n) => n.description.toLowerCase().includes('input')));
94
+ });
95
+ (0, node_test_1.it)('privacy NFRs include data handling NFR', () => {
96
+ const frs = makeSampleFRs();
97
+ const nfrs = (0, specify_nfr_js_1.generateNFRs)(frs);
98
+ const privacyNfrs = nfrs.filter((n) => n.category === 'privacy');
99
+ assert.ok(privacyNfrs.some((n) => n.description.toLowerCase().includes('data')));
100
+ });
101
+ (0, node_test_1.it)('performance NFRs include response time with < 2s threshold', () => {
102
+ const frs = makeSampleFRs();
103
+ const nfrs = (0, specify_nfr_js_1.generateNFRs)(frs);
104
+ const perfNfrs = nfrs.filter((n) => n.category === 'performance');
105
+ assert.ok(perfNfrs.some((n) => n.threshold.includes('2s')));
106
+ });
107
+ (0, node_test_1.it)('observability NFRs include error tracking NFR', () => {
108
+ const frs = makeSampleFRs();
109
+ const nfrs = (0, specify_nfr_js_1.generateNFRs)(frs);
110
+ const obsNfrs = nfrs.filter((n) => n.category === 'observability');
111
+ assert.ok(obsNfrs.some((n) => n.description.toLowerCase().includes('error')));
112
+ });
113
+ (0, node_test_1.it)('NFRs linked to relevant FRs (v1 FRs linked to security)', () => {
114
+ const frs = makeSampleFRs();
115
+ const nfrs = (0, specify_nfr_js_1.generateNFRs)(frs);
116
+ const inputValidation = nfrs.find((n) => n.description.includes('input'));
117
+ assert.ok(inputValidation);
118
+ // Should link to v1 FRs
119
+ assert.ok(inputValidation.linkedFRs.includes('FR-001'));
120
+ assert.ok(inputValidation.linkedFRs.includes('FR-004'));
121
+ // Should NOT include v2 FRs
122
+ assert.ok(!inputValidation.linkedFRs.includes('FR-005'));
123
+ });
124
+ (0, node_test_1.it)('empty FR list still produces base NFRs', () => {
125
+ const nfrs = (0, specify_nfr_js_1.generateNFRs)([]);
126
+ assert.ok(nfrs.length > 0);
127
+ const cats = new Set(nfrs.map((n) => n.category));
128
+ assert.ok(cats.has('security'));
129
+ assert.ok(cats.has('privacy'));
130
+ assert.ok(cats.has('performance'));
131
+ assert.ok(cats.has('observability'));
132
+ });
133
+ (0, node_test_1.it)('all thresholds are non-empty measurable strings', () => {
134
+ const frs = makeSampleFRs();
135
+ const nfrs = (0, specify_nfr_js_1.generateNFRs)(frs);
136
+ for (const nfr of nfrs) {
137
+ assert.ok(nfr.threshold.length > 0, `NFR ${nfr.id} has empty threshold`);
138
+ }
139
+ });
140
+ });
141
+ // ─── validateNFRs ───────────────────────────────────────────────────────────
142
+ (0, node_test_1.describe)('validateNFRs', () => {
143
+ (0, node_test_1.it)('valid NFR set passes', () => {
144
+ const frs = makeSampleFRs();
145
+ const nfrs = (0, specify_nfr_js_1.generateNFRs)(frs);
146
+ const result = (0, specify_nfr_js_1.validateNFRs)(nfrs);
147
+ assert.strictEqual(result.valid, true);
148
+ assert.strictEqual(result.errors.length, 0);
149
+ });
150
+ (0, node_test_1.it)('missing security category fails', () => {
151
+ const nfrs = [
152
+ { id: 'NFR-001', category: 'privacy', description: 'Desc', threshold: 'Threshold', linkedFRs: ['FR-001'] },
153
+ { id: 'NFR-002', category: 'performance', description: 'Desc', threshold: 'Threshold', linkedFRs: ['FR-001'] },
154
+ { id: 'NFR-003', category: 'observability', description: 'Desc', threshold: 'Threshold', linkedFRs: ['FR-001'] },
155
+ ];
156
+ const result = (0, specify_nfr_js_1.validateNFRs)(nfrs);
157
+ assert.strictEqual(result.valid, false);
158
+ assert.ok(result.errors.some((e) => e.includes('security')));
159
+ });
160
+ (0, node_test_1.it)('missing privacy category fails', () => {
161
+ const nfrs = [
162
+ { id: 'NFR-001', category: 'security', description: 'Desc', threshold: 'Threshold', linkedFRs: ['FR-001'] },
163
+ { id: 'NFR-002', category: 'performance', description: 'Desc', threshold: 'Threshold', linkedFRs: ['FR-001'] },
164
+ { id: 'NFR-003', category: 'observability', description: 'Desc', threshold: 'Threshold', linkedFRs: ['FR-001'] },
165
+ ];
166
+ const result = (0, specify_nfr_js_1.validateNFRs)(nfrs);
167
+ assert.strictEqual(result.valid, false);
168
+ assert.ok(result.errors.some((e) => e.includes('privacy')));
169
+ });
170
+ (0, node_test_1.it)('NFR with invalid ID format fails', () => {
171
+ const nfrs = [
172
+ { id: 'INVALID', category: 'security', description: 'Desc', threshold: 'Threshold', linkedFRs: ['FR-001'] },
173
+ ];
174
+ const result = (0, specify_nfr_js_1.validateNFRs)(nfrs);
175
+ assert.strictEqual(result.valid, false);
176
+ assert.ok(result.errors.some((e) => e.includes('Invalid NFR ID')));
177
+ });
178
+ (0, node_test_1.it)('NFR with empty description fails', () => {
179
+ const nfrs = [
180
+ { id: 'NFR-001', category: 'security', description: '', threshold: 'Threshold', linkedFRs: ['FR-001'] },
181
+ ];
182
+ const result = (0, specify_nfr_js_1.validateNFRs)(nfrs);
183
+ assert.strictEqual(result.valid, false);
184
+ assert.ok(result.errors.some((e) => e.includes('empty description')));
185
+ });
186
+ (0, node_test_1.it)('NFR with empty threshold fails', () => {
187
+ const nfrs = [
188
+ { id: 'NFR-001', category: 'security', description: 'Desc', threshold: '', linkedFRs: ['FR-001'] },
189
+ ];
190
+ const result = (0, specify_nfr_js_1.validateNFRs)(nfrs);
191
+ assert.strictEqual(result.valid, false);
192
+ assert.ok(result.errors.some((e) => e.includes('empty threshold')));
193
+ });
194
+ (0, node_test_1.it)('non-sequential IDs produce error', () => {
195
+ const nfrs = [
196
+ { id: 'NFR-001', category: 'security', description: 'Desc', threshold: 'T', linkedFRs: [] },
197
+ { id: 'NFR-003', category: 'privacy', description: 'Desc', threshold: 'T', linkedFRs: [] },
198
+ { id: 'NFR-004', category: 'performance', description: 'Desc', threshold: 'T', linkedFRs: [] },
199
+ { id: 'NFR-005', category: 'observability', description: 'Desc', threshold: 'T', linkedFRs: [] },
200
+ ];
201
+ const result = (0, specify_nfr_js_1.validateNFRs)(nfrs);
202
+ assert.strictEqual(result.valid, false);
203
+ assert.ok(result.errors.some((e) => e.includes('Non-sequential')));
204
+ });
205
+ });
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Specify Roles module tests — Default config, auto mode, formatting
3
+ */
4
+ export {};