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,676 @@
1
+ "use strict";
2
+ /**
3
+ * GSWD Specify Workflow Orchestrator
4
+ *
5
+ * Full pipeline: roles checkpoint -> journey mapping -> FR extraction ->
6
+ * NFR generation -> architecture + integrations -> write artifacts -> state
7
+ * Implements GSWD_SPEC Section 8.3 end-to-end.
8
+ *
9
+ * Both interactive and auto modes are supported:
10
+ * - Interactive: presents checkpoints for roles, journey review, FR confirmation
11
+ * - Auto: uses defaults, skips reviews, auto-defers paid integrations
12
+ *
13
+ * Schema: GSWD_SPEC.md Section 8.3
14
+ */
15
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ var desc = Object.getOwnPropertyDescriptor(m, k);
18
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
19
+ desc = { enumerable: true, get: function() { return m[k]; } };
20
+ }
21
+ Object.defineProperty(o, k2, desc);
22
+ }) : (function(o, m, k, k2) {
23
+ if (k2 === undefined) k2 = k;
24
+ o[k2] = m[k];
25
+ }));
26
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
27
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
28
+ }) : function(o, v) {
29
+ o["default"] = v;
30
+ });
31
+ var __importStar = (this && this.__importStar) || (function () {
32
+ var ownKeys = function(o) {
33
+ ownKeys = Object.getOwnPropertyNames || function (o) {
34
+ var ar = [];
35
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
36
+ return ar;
37
+ };
38
+ return ownKeys(o);
39
+ };
40
+ return function (mod) {
41
+ if (mod && mod.__esModule) return mod;
42
+ var result = {};
43
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
44
+ __setModuleDefault(result, mod);
45
+ return result;
46
+ };
47
+ })();
48
+ Object.defineProperty(exports, "__esModule", { value: true });
49
+ exports.runSpecify = runSpecify;
50
+ const fs = __importStar(require("node:fs"));
51
+ const path = __importStar(require("node:path"));
52
+ const state_js_1 = require("./state.js");
53
+ const config_js_1 = require("./config.js");
54
+ const parse_js_1 = require("./parse.js");
55
+ const specify_roles_js_1 = require("./specify-roles.js");
56
+ const specify_journeys_js_1 = require("./specify-journeys.js");
57
+ const specify_nfr_js_1 = require("./specify-nfr.js");
58
+ const specify_agents_js_1 = require("./specify-agents.js");
59
+ // ─── Default Journey Content (for auto/skipAgents mode) ─────────────────────
60
+ /**
61
+ * Build default journeys from DECISIONS.md context.
62
+ * Used when skipAgents=true or as fallback.
63
+ */
64
+ function buildDefaultJourneys(decisionsContent) {
65
+ const journeys = [];
66
+ let counter = 1;
67
+ // Onboarding journey
68
+ const onboarding = (0, specify_journeys_js_1.generateJourneyStructure)('onboarding', counter++);
69
+ onboarding.name = 'New User Onboarding';
70
+ onboarding.preconditions = ['User has not previously used the application'];
71
+ onboarding.steps = [
72
+ { number: 1, action: 'User navigates to application landing page', frIds: [] },
73
+ { number: 2, action: 'User clicks sign up / get started button', frIds: [] },
74
+ { number: 3, action: 'User provides account details', frIds: [] },
75
+ { number: 4, action: 'User confirms account creation', frIds: [] },
76
+ { number: 5, action: 'User completes onboarding walkthrough', frIds: [] },
77
+ ];
78
+ onboarding.success = 'User has an active account and sees the main dashboard';
79
+ onboarding.failureModes = [
80
+ { scenario: 'Invalid email format provided during sign up', handling: 'Show inline validation error, prevent form submission' },
81
+ { scenario: 'Network timeout during account creation', handling: 'Show retry button with "Connection lost" message' },
82
+ ];
83
+ onboarding.acceptanceTests = ['User can create an account and reach the dashboard within 3 steps after landing'];
84
+ journeys.push(onboarding);
85
+ // Core action journey
86
+ const coreAction = (0, specify_journeys_js_1.generateJourneyStructure)('core_action', counter++);
87
+ coreAction.name = 'Core Action';
88
+ coreAction.preconditions = ['User is authenticated', 'User has completed onboarding'];
89
+ coreAction.steps = [
90
+ { number: 1, action: 'User opens main dashboard', frIds: [] },
91
+ { number: 2, action: 'User clicks create new item button', frIds: [] },
92
+ { number: 3, action: 'User fills in required fields in creation form', frIds: [] },
93
+ { number: 4, action: 'User reviews item details before submission', frIds: [] },
94
+ { number: 5, action: 'User submits the new item', frIds: [] },
95
+ { number: 6, action: 'User sees confirmation and item appears in list', frIds: [] },
96
+ ];
97
+ coreAction.success = 'New item is created and visible in the user list';
98
+ coreAction.failureModes = [
99
+ { scenario: 'Required fields left empty on submission', handling: 'Highlight missing fields with error messages' },
100
+ { scenario: 'Server error during item creation', handling: 'Show error toast, preserve form data for retry' },
101
+ ];
102
+ coreAction.acceptanceTests = ['User can create a new item and see it in the list immediately after creation'];
103
+ journeys.push(coreAction);
104
+ // View results journey
105
+ const viewResults = (0, specify_journeys_js_1.generateJourneyStructure)('view_results', counter++);
106
+ viewResults.name = 'View Results and History';
107
+ viewResults.preconditions = ['User is authenticated', 'User has created at least one item'];
108
+ viewResults.steps = [
109
+ { number: 1, action: 'User navigates to history or results view', frIds: [] },
110
+ { number: 2, action: 'User sees list of previously created items', frIds: [] },
111
+ { number: 3, action: 'User clicks on a specific item to view details', frIds: [] },
112
+ { number: 4, action: 'User reviews item details and status', frIds: [] },
113
+ { number: 5, action: 'User returns to the list view', frIds: [] },
114
+ ];
115
+ viewResults.success = 'User can browse and view details of all their items';
116
+ viewResults.failureModes = [
117
+ { scenario: 'Item details fail to load', handling: 'Show error state with retry option' },
118
+ { scenario: 'List takes too long to load', handling: 'Show skeleton loading state, load in batches' },
119
+ ];
120
+ viewResults.acceptanceTests = ['User can view a list of items and navigate to any item detail page'];
121
+ journeys.push(viewResults);
122
+ // Settings journey
123
+ const settings = (0, specify_journeys_js_1.generateJourneyStructure)('settings', counter++);
124
+ settings.name = 'Settings and Preferences';
125
+ settings.preconditions = ['User is authenticated'];
126
+ settings.steps = [
127
+ { number: 1, action: 'User navigates to settings page', frIds: [] },
128
+ { number: 2, action: 'User views current account settings', frIds: [] },
129
+ { number: 3, action: 'User modifies a setting value', frIds: [] },
130
+ { number: 4, action: 'User saves updated settings', frIds: [] },
131
+ { number: 5, action: 'User sees confirmation of saved changes', frIds: [] },
132
+ ];
133
+ settings.success = 'User settings are updated and persisted';
134
+ settings.failureModes = [
135
+ { scenario: 'Settings save fails due to validation error', handling: 'Show specific validation error next to the field' },
136
+ { scenario: 'Network error during save', handling: 'Show error toast, keep unsaved changes in form' },
137
+ ];
138
+ settings.acceptanceTests = ['User can modify settings and see them persisted after page reload'];
139
+ journeys.push(settings);
140
+ // Error states journey
141
+ const errorStates = (0, specify_journeys_js_1.generateJourneyStructure)('error_states', counter++);
142
+ errorStates.name = 'Error States';
143
+ errorStates.preconditions = ['User is authenticated'];
144
+ errorStates.steps = [
145
+ { number: 1, action: 'User performs an action that triggers an error', frIds: [] },
146
+ { number: 2, action: 'System displays contextual error message', frIds: [] },
147
+ { number: 3, action: 'User follows recovery action suggested in error message', frIds: [] },
148
+ ];
149
+ errorStates.success = 'User understands the error and can recover or retry';
150
+ errorStates.failureModes = [
151
+ { scenario: 'Error message is too vague to act on', handling: 'Include specific error code and link to help' },
152
+ { scenario: 'Recovery action fails repeatedly', handling: 'Escalate to support contact with error context attached' },
153
+ ];
154
+ errorStates.acceptanceTests = ['Every error state shows a message with a clear recovery action'];
155
+ journeys.push(errorStates);
156
+ // Empty states journey
157
+ const emptyStates = (0, specify_journeys_js_1.generateJourneyStructure)('empty_states', counter++);
158
+ emptyStates.name = 'Empty States';
159
+ emptyStates.preconditions = ['User is authenticated', 'User has no items yet'];
160
+ emptyStates.steps = [
161
+ { number: 1, action: 'User navigates to a section with no content', frIds: [] },
162
+ { number: 2, action: 'System shows empty state with call-to-action', frIds: [] },
163
+ { number: 3, action: 'User clicks the call-to-action to create first item', frIds: [] },
164
+ ];
165
+ emptyStates.success = 'User is guided from empty state to creating their first item';
166
+ emptyStates.failureModes = [
167
+ { scenario: 'Call-to-action button not visible on small screens', handling: 'Make CTA responsive and always visible' },
168
+ { scenario: 'Empty state appears even when items exist', handling: 'Show loading indicator before checking data' },
169
+ ];
170
+ emptyStates.acceptanceTests = ['Empty state shows a clear call-to-action that leads to item creation'];
171
+ journeys.push(emptyStates);
172
+ return journeys;
173
+ }
174
+ // ─── Artifact Formatting ────────────────────────────────────────────────────
175
+ /**
176
+ * Format journeys for JOURNEYS.md content.
177
+ */
178
+ function formatJourneysContent(journeys) {
179
+ return journeys.map((j) => {
180
+ const stepsContent = j.steps
181
+ .map((s) => `${s.number}. ${s.action}`)
182
+ .join('\n');
183
+ const fmContent = j.failureModes
184
+ .map((fm) => `- **${fm.scenario}:** ${fm.handling}`)
185
+ .join('\n');
186
+ const atContent = j.acceptanceTests
187
+ .map((at) => `- ${at}`)
188
+ .join('\n');
189
+ const frRefs = j.linkedFRs.length > 0 ? j.linkedFRs.join(', ') : 'None yet';
190
+ const nfrRefs = j.linkedNFRs.length > 0 ? j.linkedNFRs.join(', ') : 'None yet';
191
+ return `### ${j.id}: ${j.name}
192
+
193
+ **Type:** ${j.type}
194
+
195
+ **Preconditions:**
196
+ ${j.preconditions.map((p) => `- ${p}`).join('\n')}
197
+
198
+ **Steps:**
199
+ ${stepsContent}
200
+
201
+ **Success:** ${j.success}
202
+
203
+ **Failure Modes:**
204
+ ${fmContent}
205
+
206
+ **Acceptance Tests:**
207
+ ${atContent}
208
+
209
+ **Linked FRs:** ${frRefs}
210
+ **Linked NFRs:** ${nfrRefs}`;
211
+ }).join('\n\n---\n\n');
212
+ }
213
+ /**
214
+ * Format FRs for SPEC.md content.
215
+ */
216
+ function formatFRsContent(frs) {
217
+ return frs.map((fr) => {
218
+ const journeyRefs = fr.sourceSteps.join(', ');
219
+ return `### ${fr.id}: ${fr.description}
220
+ **Scope:** ${fr.scope}
221
+ **Priority:** ${fr.priority}
222
+ **Source:** ${journeyRefs}`;
223
+ }).join('\n\n');
224
+ }
225
+ /**
226
+ * Format acceptance criteria from journeys for SPEC.md.
227
+ */
228
+ function formatAcceptanceCriteria(journeys) {
229
+ const criteria = [];
230
+ for (const j of journeys) {
231
+ for (const at of j.acceptanceTests) {
232
+ criteria.push(`- **${j.id}:** ${at}`);
233
+ }
234
+ }
235
+ return criteria.join('\n');
236
+ }
237
+ /**
238
+ * Format traceability table for SPEC.md.
239
+ */
240
+ function formatTraceabilityTable(frs) {
241
+ const map = (0, specify_journeys_js_1.buildTraceabilityMap)(frs);
242
+ const lines = [
243
+ '| FR | Description | Source Journeys |',
244
+ '|----|-------------|----------------|',
245
+ ];
246
+ for (const entry of map) {
247
+ lines.push(`| ${entry.frId} | ${entry.description} | ${entry.journeyRefs.join(', ')} |`);
248
+ }
249
+ return lines.join('\n');
250
+ }
251
+ /**
252
+ * Format NFRs for NFR.md content, grouped by category.
253
+ */
254
+ function formatNFRsContent(nfrs) {
255
+ const categories = ['security', 'privacy', 'performance', 'observability'];
256
+ return categories.map((cat) => {
257
+ const catNfrs = nfrs.filter((n) => n.category === cat);
258
+ const catContent = catNfrs.map((n) => {
259
+ const frRefs = n.linkedFRs.length > 0 ? n.linkedFRs.join(', ') : 'All';
260
+ return `#### ${n.id}: ${n.description}
261
+ **Threshold:** ${n.threshold}
262
+ **Linked FRs:** ${frRefs}`;
263
+ }).join('\n\n');
264
+ return `### ${cat.charAt(0).toUpperCase() + cat.slice(1)}\n\n${catContent}`;
265
+ }).join('\n\n');
266
+ }
267
+ /**
268
+ * Format default architecture content (used when skipAgents=true).
269
+ */
270
+ function buildDefaultArchitectureContent(frs) {
271
+ const components = [];
272
+ components.push(`#### C-001: Application Core
273
+
274
+ **Responsibility:** Handles main application logic and user workflows
275
+ **Dependencies:** None
276
+ **Linked FRs:** ${frs.filter(f => f.priority === 'P0').map(f => f.id).join(', ') || 'All P0 FRs'}`);
277
+ components.push(`#### C-002: Data Layer
278
+
279
+ **Responsibility:** Manages data persistence and retrieval
280
+ **Dependencies:** C-001
281
+ **Linked FRs:** ${frs.filter(f => f.scope === 'v1').map(f => f.id).slice(0, 5).join(', ') || 'All v1 FRs'}`);
282
+ components.push(`#### C-003: User Interface
283
+
284
+ **Responsibility:** Renders views and handles user input
285
+ **Dependencies:** C-001
286
+ **Linked FRs:** ${frs.filter(f => f.scope === 'v1').map(f => f.id).slice(0, 5).join(', ') || 'All v1 FRs'}`);
287
+ const componentsContent = components.join('\n\n');
288
+ const dataModel = `#### Item
289
+
290
+ | Field | Type | Notes |
291
+ |-------|------|-------|
292
+ | id | string | Primary key |
293
+ | title | string | Required |
294
+ | created_at | timestamp | Auto-set |
295
+ | updated_at | timestamp | Auto-set |
296
+
297
+ #### User
298
+
299
+ | Field | Type | Notes |
300
+ |-------|------|-------|
301
+ | id | string | Primary key |
302
+ | email | string | Unique |
303
+ | created_at | timestamp | Auto-set |
304
+
305
+ **Relationships:**
306
+ - User has many Items
307
+ - Item belongs to User`;
308
+ const ownership = `| Component | Owns | Operations |
309
+ |-----------|------|------------|
310
+ | C-001: Application Core | Business logic | Workflow orchestration |
311
+ | C-002: Data Layer | Item, User entities | CRUD operations |
312
+ | C-003: User Interface | Views, Forms | Render, Input handling |`;
313
+ return `${componentsContent}\n\n---\n\n${dataModel}\n\n---\n\n${ownership}`;
314
+ }
315
+ /**
316
+ * Format default integrations content (no external integrations for v1).
317
+ */
318
+ function buildDefaultIntegrationsContent() {
319
+ return 'No external integrations required for v1 MVP. All functionality is self-contained.';
320
+ }
321
+ // ─── Template Injection ─────────────────────────────────────────────────────
322
+ /**
323
+ * Inject content into a template at a GSWD:CONTENT marker.
324
+ */
325
+ function injectContent(template, marker, content) {
326
+ const markerComment = `<!-- GSWD:CONTENT:${marker} -->`;
327
+ return template.replace(markerComment, content);
328
+ }
329
+ // ─── Main Workflow ──────────────────────────────────────────────────────────
330
+ /**
331
+ * Run the Specify workflow end-to-end.
332
+ *
333
+ * Implements GSWD_SPEC Section 8.3:
334
+ * 1. Roles & Permissions checkpoint
335
+ * 2. Journey mapping
336
+ * 3. FR extraction
337
+ * 4. NFR generation
338
+ * 5. Architecture + Integrations (parallel agents)
339
+ * 6. Write 5 artifacts
340
+ * 7. Update state
341
+ */
342
+ async function runSpecify(options) {
343
+ const planningDir = options.planningDir || path.join(process.cwd(), '.planning');
344
+ const gswdDir = path.join(planningDir, 'gswd');
345
+ const statePath = path.join(gswdDir, 'STATE.json');
346
+ const configPath = options.configPath || path.join(planningDir, 'config.json');
347
+ const templatesDir = options.templatesDir || path.join(path.dirname(planningDir), 'templates', 'gswd');
348
+ const errors = [];
349
+ try {
350
+ // ── Step 1: Load state and config ──────────────────────────────────
351
+ const state = (0, state_js_1.readState)(statePath);
352
+ if (!state) {
353
+ return {
354
+ status: 'failed',
355
+ artifacts: [],
356
+ journeyCount: 0,
357
+ frCount: 0,
358
+ nfrCount: 0,
359
+ integrationCount: 0,
360
+ componentCount: 0,
361
+ errors: ['No STATE.json found. Run init first.'],
362
+ };
363
+ }
364
+ const config = (0, config_js_1.getGswdConfig)(configPath);
365
+ // Mark specify as in_progress
366
+ state.stage = 'specify';
367
+ state.stage_status.specify = 'in_progress';
368
+ (0, state_js_1.writeState)(statePath, state);
369
+ // Load DECISIONS.md
370
+ const decisionsPath = path.join(planningDir, 'DECISIONS.md');
371
+ let decisionsContent = '';
372
+ try {
373
+ decisionsContent = fs.readFileSync(decisionsPath, 'utf-8');
374
+ }
375
+ catch {
376
+ return {
377
+ status: 'failed',
378
+ artifacts: [],
379
+ journeyCount: 0,
380
+ frCount: 0,
381
+ nfrCount: 0,
382
+ integrationCount: 0,
383
+ componentCount: 0,
384
+ errors: ['DECISIONS.md not found. Run imagine stage first.'],
385
+ };
386
+ }
387
+ // Load IMAGINE.md (optional)
388
+ let imagineContent = '';
389
+ try {
390
+ imagineContent = fs.readFileSync(path.join(planningDir, 'IMAGINE.md'), 'utf-8');
391
+ }
392
+ catch {
393
+ // Optional — continue without it
394
+ }
395
+ // ── Step 2: Roles checkpoint ───────────────────────────────────────
396
+ const rolesConfig = await (0, specify_roles_js_1.collectRoles)({ auto: options.auto });
397
+ const rolesContent = (0, specify_roles_js_1.formatRolesForSpec)(rolesConfig);
398
+ // ── Step 3: Journey mapping ────────────────────────────────────────
399
+ let journeys;
400
+ if (options.providedJourneys) {
401
+ journeys = options.providedJourneys;
402
+ }
403
+ else if (options.skipAgents) {
404
+ journeys = buildDefaultJourneys(decisionsContent);
405
+ }
406
+ else {
407
+ // Use agent orchestration for journey-mapper
408
+ const agentContext = {
409
+ decisionsContent,
410
+ imagineContent,
411
+ };
412
+ const sequentialAgents = specify_agents_js_1.SPECIFY_AGENTS.filter((a) => a.phase === 'sequential');
413
+ const spawnFn = options.spawnFn;
414
+ if (spawnFn) {
415
+ // Allocate ID ranges for specify agents before spawning (FNDN-05)
416
+ if (options.spawnFn && !options.skipAgents) {
417
+ try {
418
+ (0, state_js_1.allocateIdRange)(statePath, 'J', 'journey-mapper', 50);
419
+ (0, state_js_1.allocateIdRange)(statePath, 'FR', 'architecture-drafter', 50);
420
+ (0, state_js_1.allocateIdRange)(statePath, 'NFR', 'architecture-drafter', 50);
421
+ (0, state_js_1.allocateIdRange)(statePath, 'I', 'integrations-checker', 50);
422
+ (0, state_js_1.allocateIdRange)(statePath, 'C', 'architecture-drafter', 50);
423
+ }
424
+ catch {
425
+ // Non-fatal — ID allocation failure should not block specify
426
+ errors.push('Warning: ID range allocation failed');
427
+ }
428
+ }
429
+ const jmResults = await (0, specify_agents_js_1.orchestrateSpecifyAgents)(sequentialAgents, agentContext, spawnFn);
430
+ const jmResult = jmResults.find((r) => r.agent === 'journey-mapper');
431
+ if (jmResult && jmResult.status === 'complete' && jmResult.content.trim()) {
432
+ // Use agent's raw journey content for template injection
433
+ // Store raw content for JOURNEYS.md, but still need Journey[] for FR extraction
434
+ journeys = buildDefaultJourneys(decisionsContent);
435
+ // Override: if agent content contains J-XXX patterns, use it as journeys source
436
+ const jPattern = /###\s+J-\d{3}/;
437
+ if (jPattern.test(jmResult.content)) {
438
+ // Agent produced structured journey content — store for template injection
439
+ options._agentJourneyContent = jmResult.content;
440
+ }
441
+ }
442
+ else {
443
+ // Fallback to default journeys
444
+ journeys = buildDefaultJourneys(decisionsContent);
445
+ errors.push('Journey mapper agent failed, using default journeys');
446
+ }
447
+ }
448
+ else {
449
+ journeys = buildDefaultJourneys(decisionsContent);
450
+ }
451
+ }
452
+ // Mid-stage checkpoint: journeys complete (RESM-02)
453
+ (0, state_js_1.writeCheckpoint)(statePath, 'gswd/specify', 'journeys-complete');
454
+ // Validate journeys
455
+ for (const journey of journeys) {
456
+ const validation = (0, specify_journeys_js_1.validateJourney)(journey);
457
+ if (!validation.valid) {
458
+ errors.push(...validation.errors);
459
+ }
460
+ }
461
+ // ── Step 4: FR extraction ──────────────────────────────────────────
462
+ const frs = (0, specify_journeys_js_1.extractFRsFromJourneys)(journeys);
463
+ // Validate FR coverage
464
+ const coverageResult = (0, specify_journeys_js_1.validateFRCoverage)(journeys, frs);
465
+ if (!coverageResult.valid) {
466
+ errors.push(...coverageResult.errors);
467
+ }
468
+ // Mid-stage checkpoint: FRs complete (RESM-02)
469
+ (0, state_js_1.writeCheckpoint)(statePath, 'gswd/specify', 'frs-complete');
470
+ // ── Step 5: NFR generation ─────────────────────────────────────────
471
+ const nfrs = (0, specify_nfr_js_1.generateNFRs)(frs, decisionsContent);
472
+ // Validate NFRs
473
+ const nfrValidation = (0, specify_nfr_js_1.validateNFRs)(nfrs);
474
+ if (!nfrValidation.valid) {
475
+ errors.push(...nfrValidation.errors);
476
+ }
477
+ // ── Step 6: Architecture + Integrations ────────────────────────────
478
+ let architectureContent;
479
+ let integrationsContent = buildDefaultIntegrationsContent();
480
+ if (options.providedArchitecture) {
481
+ architectureContent = options.providedArchitecture;
482
+ }
483
+ else if (options.skipAgents) {
484
+ architectureContent = buildDefaultArchitectureContent(frs);
485
+ }
486
+ else if (options.spawnFn) {
487
+ // Run parallel agents
488
+ const agentContext = {
489
+ decisionsContent,
490
+ imagineContent,
491
+ journeys,
492
+ frs,
493
+ autoPolicy: JSON.stringify(config.auto),
494
+ };
495
+ const parallelAgents = specify_agents_js_1.SPECIFY_AGENTS.filter((a) => a.phase === 'parallel');
496
+ const parallelResults = await (0, specify_agents_js_1.orchestrateSpecifyAgents)(parallelAgents, agentContext, options.spawnFn);
497
+ const archResult = parallelResults.find((r) => r.agent === 'architecture-drafter');
498
+ architectureContent = archResult && archResult.status === 'complete'
499
+ ? archResult.content
500
+ : buildDefaultArchitectureContent(frs);
501
+ const intResult = parallelResults.find((r) => r.agent === 'integrations-checker');
502
+ integrationsContent = intResult && intResult.status === 'complete'
503
+ ? intResult.content
504
+ : buildDefaultIntegrationsContent();
505
+ }
506
+ else {
507
+ architectureContent = buildDefaultArchitectureContent(frs);
508
+ integrationsContent = buildDefaultIntegrationsContent();
509
+ }
510
+ if (!options.providedIntegrations && !integrationsContent) {
511
+ integrationsContent = buildDefaultIntegrationsContent();
512
+ }
513
+ if (options.providedIntegrations) {
514
+ integrationsContent = options.providedIntegrations;
515
+ }
516
+ // ── Step 6b: Validate integrations and components ──────────────────
517
+ // Extract I-XXX IDs from integrationsContent; for each found, validate
518
+ const integrationIdPattern = /###\s+(I-\d{3}):\s*(.+)/g;
519
+ let iMatch;
520
+ while ((iMatch = integrationIdPattern.exec(integrationsContent)) !== null) {
521
+ const integrationData = {
522
+ id: iMatch[1],
523
+ name: iMatch[2].trim(),
524
+ setupSteps: [],
525
+ authMethod: '',
526
+ costQuota: '',
527
+ fallback: '',
528
+ status: 'approved', // default — will be overridden by actual status parsing
529
+ };
530
+ // Try to extract actual status from content following the ID
531
+ const statusMatch = integrationsContent
532
+ .slice(iMatch.index)
533
+ .match(/\*\*Status:\*\*\s*(approved|deferred[^\n]*|rejected)/i);
534
+ if (statusMatch) {
535
+ integrationData.status = statusMatch[1].toLowerCase().startsWith('approved')
536
+ ? 'approved'
537
+ : statusMatch[1].toLowerCase().startsWith('rejected')
538
+ ? 'rejected'
539
+ : 'deferred with fallback';
540
+ }
541
+ // Try to extract fallback from content following the ID
542
+ const fallbackMatch = integrationsContent
543
+ .slice(iMatch.index)
544
+ .match(/\*\*Fallback:\*\*\s*(.+)/i);
545
+ if (fallbackMatch) {
546
+ integrationData.fallback = fallbackMatch[1].trim();
547
+ }
548
+ const intValidation = (0, specify_agents_js_1.validateIntegration)(integrationData);
549
+ if (!intValidation.valid) {
550
+ errors.push(...intValidation.errors);
551
+ }
552
+ }
553
+ // Validate architecture components — parse C-XXX IDs from architectureContent
554
+ const componentIdPattern = /###\s+(C-\d{3}):\s*(.+)/g;
555
+ let cMatch;
556
+ while ((cMatch = componentIdPattern.exec(architectureContent)) !== null) {
557
+ const componentData = {
558
+ id: cMatch[1],
559
+ name: cMatch[2].trim(),
560
+ responsibility: '',
561
+ dependencies: [],
562
+ linkedFRs: [],
563
+ };
564
+ // Extract responsibility
565
+ const respMatch = architectureContent
566
+ .slice(cMatch.index)
567
+ .match(/\*\*Responsibility:\*\*\s*(.+)/i);
568
+ if (respMatch) {
569
+ componentData.responsibility = respMatch[1].trim();
570
+ }
571
+ const compValidation = (0, specify_agents_js_1.validateComponent)(componentData);
572
+ if (!compValidation.valid) {
573
+ errors.push(...compValidation.errors);
574
+ }
575
+ }
576
+ // ── Step 7: Build and write artifacts ──────────────────────────────
577
+ // Load templates
578
+ const specTemplate = loadTemplate(templatesDir, 'SPEC.template.md');
579
+ const journeysTemplate = loadTemplate(templatesDir, 'JOURNEYS.template.md');
580
+ const nfrTemplate = loadTemplate(templatesDir, 'NFR.template.md');
581
+ const archTemplate = loadTemplate(templatesDir, 'ARCHITECTURE.template.md');
582
+ const intTemplate = loadTemplate(templatesDir, 'INTEGRATIONS.template.md');
583
+ // Build artifact content by injecting into templates
584
+ let specContent = specTemplate;
585
+ specContent = injectContent(specContent, 'ROLES', rolesContent);
586
+ specContent = injectContent(specContent, 'FRS', formatFRsContent(frs));
587
+ specContent = injectContent(specContent, 'ACCEPTANCE', formatAcceptanceCriteria(journeys));
588
+ specContent = injectContent(specContent, 'TRACEABILITY', formatTraceabilityTable(frs));
589
+ let journeysContent = journeysTemplate;
590
+ const agentJourneyContent = options._agentJourneyContent;
591
+ if (agentJourneyContent) {
592
+ journeysContent = injectContent(journeysContent, 'JOURNEYS', agentJourneyContent);
593
+ }
594
+ else {
595
+ journeysContent = injectContent(journeysContent, 'JOURNEYS', formatJourneysContent(journeys));
596
+ }
597
+ let nfrContent = nfrTemplate;
598
+ nfrContent = injectContent(nfrContent, 'NFRS', formatNFRsContent(nfrs));
599
+ // For architecture — parse components/data model/ownership from agent content or defaults
600
+ let archContent = archTemplate;
601
+ archContent = injectContent(archContent, 'COMPONENTS', architectureContent.split('---')[0]?.trim() || architectureContent);
602
+ archContent = injectContent(archContent, 'DATA_MODEL', architectureContent.split('---')[1]?.trim() || '');
603
+ archContent = injectContent(archContent, 'OWNERSHIP', architectureContent.split('---')[2]?.trim() || '');
604
+ let intContent = intTemplate;
605
+ intContent = injectContent(intContent, 'INTEGRATIONS', integrationsContent);
606
+ // Write all 5 artifacts
607
+ const artifacts = [
608
+ { name: 'SPEC.md', content: specContent, fileType: 'SPEC.md' },
609
+ { name: 'JOURNEYS.md', content: journeysContent, fileType: 'JOURNEYS.md' },
610
+ { name: 'NFR.md', content: nfrContent, fileType: 'NFR.md' },
611
+ { name: 'ARCHITECTURE.md', content: archContent, fileType: 'ARCHITECTURE.md' },
612
+ { name: 'INTEGRATIONS.md', content: intContent, fileType: 'INTEGRATIONS.md' },
613
+ ];
614
+ const artifactsWritten = [];
615
+ for (const artifact of artifacts) {
616
+ const artifactPath = path.join(planningDir, artifact.name);
617
+ (0, state_js_1.safeWriteFile)(artifactPath, artifact.content);
618
+ artifactsWritten.push(artifactPath);
619
+ // Validate headings
620
+ const headingValidation = (0, parse_js_1.validateHeadings)(artifact.content, artifact.fileType);
621
+ if (!headingValidation.valid) {
622
+ errors.push(`${artifact.name} missing headings: ${headingValidation.missing.join(', ')}`);
623
+ }
624
+ }
625
+ // ── Step 8: Update state ───────────────────────────────────────────
626
+ const finalState = (0, state_js_1.readState)(statePath);
627
+ if (finalState) {
628
+ finalState.stage_status.specify = 'done';
629
+ finalState.stage = 'specify';
630
+ finalState.last_checkpoint = {
631
+ workflow: 'gswd/specify',
632
+ checkpoint_id: 'complete',
633
+ timestamp: new Date().toISOString(),
634
+ };
635
+ (0, state_js_1.writeState)(statePath, finalState);
636
+ }
637
+ // ── Step 9: Return result ──────────────────────────────────────────
638
+ return {
639
+ status: errors.length > 0 ? 'complete' : 'complete', // Non-fatal errors don't block
640
+ artifacts: artifactsWritten,
641
+ journeyCount: journeys.length,
642
+ frCount: frs.length,
643
+ nfrCount: nfrs.length,
644
+ integrationCount: 0, // Would be populated from parsed integrations
645
+ componentCount: 3, // Default component count
646
+ errors: errors.length > 0 ? errors : undefined,
647
+ };
648
+ }
649
+ catch (err) {
650
+ const message = err instanceof Error ? err.message : String(err);
651
+ return {
652
+ status: 'failed',
653
+ artifacts: [],
654
+ journeyCount: 0,
655
+ frCount: 0,
656
+ nfrCount: 0,
657
+ integrationCount: 0,
658
+ componentCount: 0,
659
+ errors: [message],
660
+ };
661
+ }
662
+ }
663
+ // ─── Helpers ────────────────────────────────────────────────────────────────
664
+ /**
665
+ * Load a template file. Returns a minimal template if not found.
666
+ */
667
+ function loadTemplate(templatesDir, filename) {
668
+ try {
669
+ return fs.readFileSync(path.join(templatesDir, filename), 'utf-8');
670
+ }
671
+ catch {
672
+ // Fallback: return a minimal template
673
+ const name = filename.replace('.template.md', '');
674
+ return `# ${name}\n\n<!-- GSWD:COMPLETE -->\n`;
675
+ }
676
+ }