gswd 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/bin/gswd-tools.cjs +228 -0
  2. package/commands/gswd/imagine.md +7 -1
  3. package/commands/gswd/start.md +507 -32
  4. package/dist/lib/audit.d.ts +205 -0
  5. package/dist/lib/audit.js +805 -0
  6. package/dist/lib/bootstrap.d.ts +103 -0
  7. package/dist/lib/bootstrap.js +563 -0
  8. package/dist/lib/compile.d.ts +239 -0
  9. package/dist/lib/compile.js +1152 -0
  10. package/dist/lib/config.d.ts +49 -0
  11. package/dist/lib/config.js +150 -0
  12. package/dist/lib/imagine-agents.d.ts +54 -0
  13. package/dist/lib/imagine-agents.js +185 -0
  14. package/dist/lib/imagine-gate.d.ts +47 -0
  15. package/dist/lib/imagine-gate.js +131 -0
  16. package/dist/lib/imagine-input.d.ts +46 -0
  17. package/dist/lib/imagine-input.js +233 -0
  18. package/dist/lib/imagine-synthesis.d.ts +90 -0
  19. package/dist/lib/imagine-synthesis.js +453 -0
  20. package/dist/lib/imagine.d.ts +56 -0
  21. package/dist/lib/imagine.js +413 -0
  22. package/dist/lib/intake.d.ts +27 -0
  23. package/dist/lib/intake.js +82 -0
  24. package/dist/lib/parse.d.ts +59 -0
  25. package/dist/lib/parse.js +171 -0
  26. package/dist/lib/render.d.ts +309 -0
  27. package/dist/lib/render.js +624 -0
  28. package/dist/lib/specify-agents.d.ts +120 -0
  29. package/dist/lib/specify-agents.js +269 -0
  30. package/dist/lib/specify-journeys.d.ts +124 -0
  31. package/dist/lib/specify-journeys.js +279 -0
  32. package/dist/lib/specify-nfr.d.ts +45 -0
  33. package/dist/lib/specify-nfr.js +159 -0
  34. package/dist/lib/specify-roles.d.ts +46 -0
  35. package/dist/lib/specify-roles.js +88 -0
  36. package/dist/lib/specify.d.ts +70 -0
  37. package/dist/lib/specify.js +676 -0
  38. package/dist/lib/state.d.ts +140 -0
  39. package/dist/lib/state.js +340 -0
  40. package/dist/tests/audit.test.d.ts +4 -0
  41. package/dist/tests/audit.test.js +1579 -0
  42. package/dist/tests/bootstrap.test.d.ts +5 -0
  43. package/dist/tests/bootstrap.test.js +611 -0
  44. package/dist/tests/compile.test.d.ts +4 -0
  45. package/dist/tests/compile.test.js +862 -0
  46. package/dist/tests/config.test.d.ts +4 -0
  47. package/dist/tests/config.test.js +191 -0
  48. package/dist/tests/imagine-agents.test.d.ts +6 -0
  49. package/dist/tests/imagine-agents.test.js +179 -0
  50. package/dist/tests/imagine-gate.test.d.ts +6 -0
  51. package/dist/tests/imagine-gate.test.js +264 -0
  52. package/dist/tests/imagine-input.test.d.ts +6 -0
  53. package/dist/tests/imagine-input.test.js +283 -0
  54. package/dist/tests/imagine-synthesis.test.d.ts +7 -0
  55. package/dist/tests/imagine-synthesis.test.js +380 -0
  56. package/dist/tests/imagine.test.d.ts +8 -0
  57. package/dist/tests/imagine.test.js +406 -0
  58. package/dist/tests/parse.test.d.ts +4 -0
  59. package/dist/tests/parse.test.js +285 -0
  60. package/dist/tests/render.test.d.ts +4 -0
  61. package/dist/tests/render.test.js +236 -0
  62. package/dist/tests/specify-agents.test.d.ts +4 -0
  63. package/dist/tests/specify-agents.test.js +352 -0
  64. package/dist/tests/specify-journeys.test.d.ts +5 -0
  65. package/dist/tests/specify-journeys.test.js +440 -0
  66. package/dist/tests/specify-nfr.test.d.ts +4 -0
  67. package/dist/tests/specify-nfr.test.js +205 -0
  68. package/dist/tests/specify-roles.test.d.ts +4 -0
  69. package/dist/tests/specify-roles.test.js +136 -0
  70. package/dist/tests/specify.test.d.ts +9 -0
  71. package/dist/tests/specify.test.js +544 -0
  72. package/dist/tests/state.test.d.ts +4 -0
  73. package/dist/tests/state.test.js +316 -0
  74. package/lib/bootstrap.ts +37 -11
  75. package/lib/compile.ts +426 -4
  76. package/lib/imagine-agents.ts +53 -7
  77. package/lib/imagine-synthesis.ts +170 -6
  78. package/lib/imagine.ts +59 -5
  79. package/lib/intake.ts +60 -0
  80. package/lib/parse.ts +2 -1
  81. package/lib/render.ts +566 -5
  82. package/lib/specify-agents.ts +25 -3
  83. package/lib/state.ts +115 -0
  84. package/package.json +3 -2
  85. package/templates/gswd/DECISIONS.template.md +3 -0
@@ -0,0 +1,1152 @@
1
+ "use strict";
2
+ /**
3
+ * GSWD Compile Module — Deterministic contract doc generation
4
+ *
5
+ * Reads spec artifacts (IMAGINE.md, DECISIONS.md, SPEC.md, NFR.md, JOURNEYS.md, INTEGRATIONS.md),
6
+ * parses into a SpecBundle, and generates 4 GSD contract docs deterministically.
7
+ * Zero LLM calls, no randomness, no ordering variance.
8
+ *
9
+ * Schema: GSWD_SPEC.md Section 8.5, 9.2, 11.3
10
+ */
11
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
12
+ if (k2 === undefined) k2 = k;
13
+ var desc = Object.getOwnPropertyDescriptor(m, k);
14
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
15
+ desc = { enumerable: true, get: function() { return m[k]; } };
16
+ }
17
+ Object.defineProperty(o, k2, desc);
18
+ }) : (function(o, m, k, k2) {
19
+ if (k2 === undefined) k2 = k;
20
+ o[k2] = m[k];
21
+ }));
22
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
23
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
24
+ }) : function(o, v) {
25
+ o["default"] = v;
26
+ });
27
+ var __importStar = (this && this.__importStar) || (function () {
28
+ var ownKeys = function(o) {
29
+ ownKeys = Object.getOwnPropertyNames || function (o) {
30
+ var ar = [];
31
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
32
+ return ar;
33
+ };
34
+ return ownKeys(o);
35
+ };
36
+ return function (mod) {
37
+ if (mod && mod.__esModule) return mod;
38
+ var result = {};
39
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
40
+ __setModuleDefault(result, mod);
41
+ return result;
42
+ };
43
+ })();
44
+ Object.defineProperty(exports, "__esModule", { value: true });
45
+ exports.PHASE_NAMES = exports.JOURNEY_TYPE_TO_PHASE = exports.NFR_CATEGORY_ORDER = exports.PRIORITY_ORDER = exports.SCOPE_ORDER = void 0;
46
+ exports.sortFRs = sortFRs;
47
+ exports.parseFRsFromSpec = parseFRsFromSpec;
48
+ exports.parseNFRsFromContent = parseNFRsFromContent;
49
+ exports.parseJourneysFromContent = parseJourneysFromContent;
50
+ exports.parseIntegrationsFromContent = parseIntegrationsFromContent;
51
+ exports.parseSpecBundle = parseSpecBundle;
52
+ exports.generateProjectDoc = generateProjectDoc;
53
+ exports.generateRequirementsDoc = generateRequirementsDoc;
54
+ exports.generateRoadmapDoc = generateRoadmapDoc;
55
+ exports.generateResearchSummary = generateResearchSummary;
56
+ exports.validateContracts = validateContracts;
57
+ exports.generateStateDoc = generateStateDoc;
58
+ exports.readSpecArtifacts = readSpecArtifacts;
59
+ exports.runCompileWorkflow = runCompileWorkflow;
60
+ const fs = __importStar(require("node:fs"));
61
+ const path = __importStar(require("node:path"));
62
+ const parse_js_1 = require("./parse.js");
63
+ const state_js_1 = require("./state.js");
64
+ const audit_js_1 = require("./audit.js");
65
+ // ─── Constants (all exported) ─────────────────────────────────────────────────
66
+ exports.SCOPE_ORDER = { v1: 0, v2: 1, out: 2 };
67
+ exports.PRIORITY_ORDER = { P0: 0, P1: 1, P2: 2 };
68
+ exports.NFR_CATEGORY_ORDER = ['security', 'privacy', 'performance', 'observability'];
69
+ exports.JOURNEY_TYPE_TO_PHASE = {
70
+ onboarding: 1,
71
+ core_action: 1,
72
+ view_results: 2,
73
+ error_states: 3,
74
+ empty_states: 3,
75
+ settings: 3,
76
+ };
77
+ exports.PHASE_NAMES = {
78
+ 1: 'Skeleton & Core Loop',
79
+ 2: 'Persistence & History',
80
+ 3: 'Polish: errors, empty states, settings',
81
+ 4: 'Observability & Hardening',
82
+ };
83
+ // ─── Sort Helpers ─────────────────────────────────────────────────────────────
84
+ /**
85
+ * Sort FRs by scope (v1 first), then priority (P0 first), then numeric ID ascending.
86
+ */
87
+ function sortFRs(frs) {
88
+ return [...frs].sort((a, b) => {
89
+ const scopeDiff = (exports.SCOPE_ORDER[a.scope] ?? 99) - (exports.SCOPE_ORDER[b.scope] ?? 99);
90
+ if (scopeDiff !== 0)
91
+ return scopeDiff;
92
+ const priorityDiff = (exports.PRIORITY_ORDER[a.priority] ?? 99) - (exports.PRIORITY_ORDER[b.priority] ?? 99);
93
+ if (priorityDiff !== 0)
94
+ return priorityDiff;
95
+ // Numeric ID sort: FR-001 < FR-002 < FR-010
96
+ return parseInt(a.id.split('-')[1], 10) - parseInt(b.id.split('-')[1], 10);
97
+ });
98
+ }
99
+ // ─── Spec Artifact Parsers ────────────────────────────────────────────────────
100
+ /**
101
+ * Parse FR entries from SPEC.md content.
102
+ * Uses ### FR-NNN heading pattern, extracts description, scope, and priority.
103
+ * Accepts a frToJourneys map to populate sourceJourneys.
104
+ */
105
+ function parseFRsFromSpec(specContent, frToJourneys) {
106
+ if (!specContent)
107
+ return [];
108
+ const frRegex = /^###?\s+(FR-\d{1,4})[:.]?\s*/gm;
109
+ const positions = [];
110
+ let match;
111
+ while ((match = frRegex.exec(specContent)) !== null) {
112
+ positions.push({ id: (0, parse_js_1.normalizeId)(match[1]), start: match.index });
113
+ }
114
+ const frs = positions.map((pos, i) => {
115
+ const end = i + 1 < positions.length ? positions[i + 1].start : specContent.length;
116
+ const section = specContent.slice(pos.start, end);
117
+ const scopeMatch = section.match(/\*{0,2}Scope:?\*{0,2}:?\s*(v1|v2|out)/i);
118
+ const priorityMatch = section.match(/\*{0,2}Priority:?\*{0,2}:?\s*(P0|P1|P2)/i);
119
+ // Get description: first non-empty line after the heading line
120
+ const lines = section.split('\n');
121
+ const headingLine = lines[0]; // e.g., "### FR-001: User can register"
122
+ // Extract description from heading line if present (after the ID)
123
+ const headingDescMatch = headingLine.match(/^###?\s+FR-\d{1,4}:?\s+(.*)/);
124
+ let description = '';
125
+ if (headingDescMatch && headingDescMatch[1].trim().length > 0) {
126
+ description = headingDescMatch[1].trim();
127
+ }
128
+ else {
129
+ // Fall back to first non-empty line after heading
130
+ const descLine = lines.slice(1).find((l) => l.trim().length > 0 && !l.startsWith('**'));
131
+ description = descLine?.trim() ?? '';
132
+ }
133
+ const sourceJourneys = Array.from(frToJourneys.get(pos.id) ?? new Set()).sort();
134
+ // Extract acceptance criteria: lines after "**Acceptance Criteria:**" heading
135
+ const acceptanceCriteria = [];
136
+ const acMatch = section.match(/\*{0,2}Acceptance Criteria:?\*{0,2}:?\s*\n([\s\S]*?)(?=\*\*|###|$)/i);
137
+ if (acMatch) {
138
+ const acLines = acMatch[1].split('\n');
139
+ for (const acLine of acLines) {
140
+ const bulletMatch = acLine.match(/^[-*]\s+(.+)/);
141
+ if (bulletMatch) {
142
+ acceptanceCriteria.push(bulletMatch[1].trim());
143
+ }
144
+ }
145
+ }
146
+ return {
147
+ id: pos.id,
148
+ description,
149
+ scope: scopeMatch?.[1]?.toLowerCase() ?? 'v1',
150
+ priority: priorityMatch?.[1] ?? 'P1',
151
+ sourceJourneys,
152
+ acceptanceCriteria,
153
+ };
154
+ });
155
+ return sortFRs(frs);
156
+ }
157
+ /**
158
+ * Parse NFR entries from NFR.md content.
159
+ * Extracts id, description, category (from **Category:** field), threshold (from **Threshold:** field).
160
+ * Sorts by NFR_CATEGORY_ORDER then numeric ID.
161
+ */
162
+ function parseNFRsFromContent(nfrContent) {
163
+ if (!nfrContent)
164
+ return [];
165
+ const nfrRegex = /^###?\s+(NFR-\d{1,4})[:.]?\s*/gm;
166
+ const positions = [];
167
+ let match;
168
+ while ((match = nfrRegex.exec(nfrContent)) !== null) {
169
+ positions.push({ id: (0, parse_js_1.normalizeId)(match[1]), start: match.index });
170
+ }
171
+ const nfrs = positions.map((pos, i) => {
172
+ const end = i + 1 < positions.length ? positions[i + 1].start : nfrContent.length;
173
+ const section = nfrContent.slice(pos.start, end);
174
+ const categoryMatch = section.match(/\*{0,2}Category:?\*{0,2}:?\s*(\w+)/i);
175
+ const thresholdMatch = section.match(/\*{0,2}Threshold:?\*{0,2}:?\s*(.+)/i);
176
+ // Extract description from heading line
177
+ const headingLine = section.split('\n')[0];
178
+ const headingDescMatch = headingLine.match(/^###?\s+NFR-\d{1,4}:?\s+(.*)/);
179
+ const description = headingDescMatch?.[1]?.trim() ?? '';
180
+ return {
181
+ id: pos.id,
182
+ description,
183
+ category: categoryMatch?.[1]?.toLowerCase() ?? 'observability',
184
+ threshold: thresholdMatch?.[1]?.trim() ?? '',
185
+ };
186
+ });
187
+ // Sort by category order then numeric ID
188
+ return nfrs.sort((a, b) => {
189
+ const aOrder = exports.NFR_CATEGORY_ORDER.indexOf(a.category);
190
+ const bOrder = exports.NFR_CATEGORY_ORDER.indexOf(b.category);
191
+ const aCatOrder = aOrder === -1 ? 99 : aOrder;
192
+ const bCatOrder = bOrder === -1 ? 99 : bOrder;
193
+ if (aCatOrder !== bCatOrder)
194
+ return aCatOrder - bCatOrder;
195
+ return parseInt(a.id.split('-')[1], 10) - parseInt(b.id.split('-')[1], 10);
196
+ });
197
+ }
198
+ /**
199
+ * Parse journey entries from JOURNEYS.md content.
200
+ * Extracts id, name, type, linkedFRs, linkedIntegrations.
201
+ * Sorts by numeric ID.
202
+ */
203
+ function parseJourneysFromContent(journeysContent) {
204
+ if (!journeysContent)
205
+ return [];
206
+ const journeyRegex = /^###?\s+(J-\d{1,4})[:.]?\s*/gm;
207
+ const positions = [];
208
+ let match;
209
+ while ((match = journeyRegex.exec(journeysContent)) !== null) {
210
+ positions.push({ id: (0, parse_js_1.normalizeId)(match[1]), start: match.index });
211
+ }
212
+ const journeys = positions.map((pos, i) => {
213
+ const end = i + 1 < positions.length ? positions[i + 1].start : journeysContent.length;
214
+ const section = journeysContent.slice(pos.start, end);
215
+ // Extract name from heading line
216
+ const headingLine = section.split('\n')[0];
217
+ const headingNameMatch = headingLine.match(/^###?\s+J-\d{1,4}:?\s+(.*)/);
218
+ const name = headingNameMatch?.[1]?.trim() ?? '';
219
+ // Extract type
220
+ const typeMatch = section.match(/\*{0,2}Type:?\*{0,2}:?\s*(\w+)/i);
221
+ const type = typeMatch?.[1]?.toLowerCase() ?? '';
222
+ // Extract linked FRs
223
+ const frLine = section.match(/\*{0,2}Linked FRs:?\*{0,2}:?\s*(.+)/i);
224
+ const linkedFRs = frLine
225
+ ? (0, parse_js_1.extractIds)(frLine[1], 'FR').map((e) => e.id)
226
+ : [];
227
+ // Extract linked integrations
228
+ const intLine = section.match(/\*{0,2}Linked Integrations:?\*{0,2}:?\s*(.+)/i);
229
+ const linkedIntegrations = intLine && intLine[1].trim().toLowerCase() !== 'none'
230
+ ? (0, parse_js_1.extractIds)(intLine[1], 'I').map((e) => e.id)
231
+ : [];
232
+ return {
233
+ id: pos.id,
234
+ name,
235
+ type,
236
+ linkedFRs,
237
+ linkedIntegrations,
238
+ };
239
+ });
240
+ // Sort by numeric ID
241
+ return journeys.sort((a, b) => parseInt(a.id.split('-')[1], 10) - parseInt(b.id.split('-')[1], 10));
242
+ }
243
+ /**
244
+ * Parse integration entries from INTEGRATIONS.md content.
245
+ * Extracts id, name, status, fallback.
246
+ * Sorts by numeric ID.
247
+ */
248
+ function parseIntegrationsFromContent(intContent) {
249
+ if (!intContent)
250
+ return [];
251
+ const intRegex = /^###?\s+(I-\d{1,4})[:.]?\s*/gm;
252
+ const positions = [];
253
+ let match;
254
+ while ((match = intRegex.exec(intContent)) !== null) {
255
+ positions.push({ id: (0, parse_js_1.normalizeId)(match[1]), start: match.index });
256
+ }
257
+ const integrations = positions.map((pos, i) => {
258
+ const end = i + 1 < positions.length ? positions[i + 1].start : intContent.length;
259
+ const section = intContent.slice(pos.start, end);
260
+ // Extract name from heading
261
+ const headingLine = section.split('\n')[0];
262
+ const headingNameMatch = headingLine.match(/^###?\s+I-\d{1,4}:?\s+(.*)/);
263
+ const name = headingNameMatch?.[1]?.trim() ?? '';
264
+ const statusMatch = section.match(/\*{0,2}Status:?\*{0,2}:?\s*(\w+)/i);
265
+ const fallbackMatch = section.match(/\*{0,2}Fallback:?\*{0,2}:?\s*(.+)/i);
266
+ return {
267
+ id: pos.id,
268
+ name,
269
+ status: statusMatch?.[1]?.toLowerCase() ?? 'unknown',
270
+ fallback: fallbackMatch?.[1]?.trim() ?? '',
271
+ };
272
+ });
273
+ return integrations.sort((a, b) => parseInt(a.id.split('-')[1], 10) - parseInt(b.id.split('-')[1], 10));
274
+ }
275
+ /**
276
+ * Parse a list of bullet/numbered items from a section content string.
277
+ * Handles: "1. Text", "- Text", "* Text" patterns.
278
+ */
279
+ function parseListItems(content) {
280
+ if (!content)
281
+ return [];
282
+ const lines = content.split('\n');
283
+ const items = [];
284
+ for (const line of lines) {
285
+ const bulletMatch = line.match(/^[-*]\s+(.+)/);
286
+ const numberedMatch = line.match(/^\d+[.)]\s+(.+)/);
287
+ if (bulletMatch) {
288
+ items.push(bulletMatch[1].trim());
289
+ }
290
+ else if (numberedMatch) {
291
+ items.push(numberedMatch[1].trim());
292
+ }
293
+ }
294
+ return items;
295
+ }
296
+ /**
297
+ * Build SpecBundle from file contents.
298
+ *
299
+ * Uses extractHeadingContent() for IMAGINE.md and DECISIONS.md sections.
300
+ * Populates frToJourneys mapping from journey parsing.
301
+ * If statePath provided, reads STATE.json for approvals and projectSlug.
302
+ * If no statePath provided, projectSlug defaults to '' and approvals defaults to {}.
303
+ */
304
+ function parseSpecBundle(files, statePath) {
305
+ // Parse IMAGINE.md sections
306
+ const vision = (0, parse_js_1.extractHeadingContent)(files.imagine, '## Vision') ?? '';
307
+ const targetUser = (0, parse_js_1.extractHeadingContent)(files.imagine, '## Target User') ?? '';
308
+ const productDirection = (0, parse_js_1.extractHeadingContent)(files.imagine, '## Product Direction') ?? '';
309
+ const wedge = (0, parse_js_1.extractHeadingContent)(files.imagine, '## Wedge') ?? '';
310
+ // Parse DECISIONS.md sections
311
+ const frozenDecisionsContent = (0, parse_js_1.extractHeadingContent)(files.decisions, '## Frozen Decisions') ?? '';
312
+ const frozenDecisions = parseListItems(frozenDecisionsContent);
313
+ const successMetricsContent = (0, parse_js_1.extractHeadingContent)(files.decisions, '## Success Metrics') ?? '';
314
+ const successMetrics = parseListItems(successMetricsContent);
315
+ const outOfScopeContent = (0, parse_js_1.extractHeadingContent)(files.decisions, '## Out of Scope') ?? '';
316
+ const outOfScope = parseListItems(outOfScopeContent);
317
+ const risksContent = (0, parse_js_1.extractHeadingContent)(files.decisions, '## Risks & Mitigations') ?? '';
318
+ const risks = parseListItems(risksContent);
319
+ const openQuestionsContent = (0, parse_js_1.extractHeadingContent)(files.decisions, '## Open Questions') ?? '';
320
+ const openQuestions = parseListItems(openQuestionsContent);
321
+ // Parse journeys first to build frToJourneys map
322
+ const journeys = parseJourneysFromContent(files.journeys);
323
+ // Build frToJourneys reverse map
324
+ const frToJourneys = new Map();
325
+ for (const journey of journeys) {
326
+ for (const frId of journey.linkedFRs) {
327
+ if (!frToJourneys.has(frId)) {
328
+ frToJourneys.set(frId, new Set());
329
+ }
330
+ frToJourneys.get(frId).add(journey.id);
331
+ }
332
+ }
333
+ // Parse FRs from SPEC.md (with frToJourneys for sourceJourneys population)
334
+ const frs = parseFRsFromSpec(files.spec, frToJourneys);
335
+ // Parse roles from SPEC.md
336
+ const roles = (0, parse_js_1.extractHeadingContent)(files.spec, '## Roles & Permissions') ?? '';
337
+ // Parse NFRs
338
+ const nfrs = parseNFRsFromContent(files.nfr);
339
+ // Parse integrations
340
+ const integrations = parseIntegrationsFromContent(files.integrations);
341
+ // Load state for approvals and projectSlug
342
+ let approvals = {};
343
+ let projectSlug = '';
344
+ if (statePath) {
345
+ const state = (0, state_js_1.readState)(statePath);
346
+ if (state) {
347
+ projectSlug = state.project_slug ?? '';
348
+ approvals = {
349
+ auth_model: state.approvals?.auth_model ?? '',
350
+ data_store: state.approvals?.data_store ?? '',
351
+ };
352
+ }
353
+ }
354
+ // Populate new enrichment fields from research files
355
+ const architectureContent = files.architecture ?? '';
356
+ const icpContent = files.icp ?? '';
357
+ const competitionContent = files.competition ?? '';
358
+ const gtmContent = files.gtm ?? '';
359
+ const featuresContent = files.features ?? '';
360
+ const architectureSummary = (0, parse_js_1.extractHeadingContent)(architectureContent, '## Standard Architecture') ??
361
+ (0, parse_js_1.extractHeadingContent)(architectureContent, '## Executive Summary') ??
362
+ '';
363
+ const icpHighlights = (0, parse_js_1.extractHeadingContent)(icpContent, '## ICP Highlights') ??
364
+ (0, parse_js_1.extractHeadingContent)(icpContent, '## Target Users') ??
365
+ (0, parse_js_1.extractHeadingContent)(featuresContent, '### Expected Features') ??
366
+ (0, parse_js_1.extractHeadingContent)(featuresContent, '## Expected Features') ??
367
+ '';
368
+ const competitionLandscape = (0, parse_js_1.extractHeadingContent)(competitionContent, '## Competitor Analysis') ??
369
+ (0, parse_js_1.extractHeadingContent)(competitionContent, '## Landscape') ??
370
+ (0, parse_js_1.extractHeadingContent)(featuresContent, '## Competitor Feature Analysis') ??
371
+ '';
372
+ const gtmPositioning = (0, parse_js_1.extractHeadingContent)(gtmContent, '## GTM Positioning') ??
373
+ (0, parse_js_1.extractHeadingContent)(gtmContent, '## Positioning') ??
374
+ (0, parse_js_1.extractHeadingContent)(gtmContent, '## Executive Summary') ??
375
+ '';
376
+ // Synthesize journeyInsights from journeys array
377
+ const journeyTypeCounts = new Map();
378
+ for (const journey of journeys) {
379
+ const count = journeyTypeCounts.get(journey.type) ?? 0;
380
+ journeyTypeCounts.set(journey.type, count + 1);
381
+ }
382
+ const journeyInsightParts = [];
383
+ for (const journey of journeys) {
384
+ const frCount = journey.linkedFRs.length;
385
+ journeyInsightParts.push(`${journey.id} (${journey.name}, type: ${journey.type}, FRs: ${frCount})`);
386
+ }
387
+ const journeyInsights = journeyInsightParts.length > 0
388
+ ? journeyInsightParts.join('\n')
389
+ : '';
390
+ return {
391
+ vision,
392
+ targetUser,
393
+ productDirection,
394
+ wedge,
395
+ frozenDecisions,
396
+ successMetrics,
397
+ outOfScope,
398
+ risks,
399
+ openQuestions,
400
+ approvals,
401
+ frs,
402
+ roles,
403
+ nfrs,
404
+ journeys,
405
+ integrations,
406
+ projectSlug,
407
+ architectureSummary,
408
+ icpHighlights,
409
+ competitionLandscape,
410
+ gtmPositioning,
411
+ journeyInsights,
412
+ };
413
+ }
414
+ // ─── Document Generators ──────────────────────────────────────────────────────
415
+ /**
416
+ * Generate PROJECT.md content from SpecBundle.
417
+ *
418
+ * Headings: # projectSlug, ## What This Is, ## Target User, ## Problem Statement,
419
+ * ## Wedge / MVP Boundary, ## Success Metrics (bulleted), ## Out of Scope (bulleted).
420
+ * CRITICAL: No Date, Date.now(), or any time-dependent call.
421
+ * CRITICAL: Ends with exactly one trailing newline.
422
+ */
423
+ function generateProjectDoc(bundle) {
424
+ const lines = [];
425
+ lines.push(`# ${bundle.projectSlug}`);
426
+ lines.push('');
427
+ lines.push('## What This Is');
428
+ lines.push('');
429
+ lines.push(bundle.vision);
430
+ lines.push('');
431
+ lines.push('## Target User');
432
+ lines.push('');
433
+ lines.push(bundle.targetUser);
434
+ lines.push('');
435
+ lines.push('## Problem Statement');
436
+ lines.push('');
437
+ lines.push(bundle.productDirection);
438
+ lines.push('');
439
+ lines.push('## Wedge / MVP Boundary');
440
+ lines.push('');
441
+ lines.push(bundle.wedge);
442
+ lines.push('');
443
+ lines.push('## Success Metrics');
444
+ lines.push('');
445
+ for (const metric of bundle.successMetrics) {
446
+ lines.push(`- ${metric}`);
447
+ }
448
+ lines.push('');
449
+ lines.push('## Out of Scope');
450
+ lines.push('');
451
+ for (const item of bundle.outOfScope) {
452
+ lines.push(`- ${item}`);
453
+ }
454
+ lines.push('');
455
+ // ── ## Context section ──────────────────────────────────────────────────────
456
+ lines.push('## Context');
457
+ lines.push('');
458
+ lines.push('Product context synthesized from GSWD research pipeline. Each subsection summarizes key findings and references the source artifact.');
459
+ lines.push('');
460
+ // Architecture Summary
461
+ lines.push('### Architecture Summary');
462
+ lines.push('');
463
+ if (bundle.architectureSummary) {
464
+ lines.push(bundle.architectureSummary);
465
+ }
466
+ else {
467
+ lines.push('*(Architecture research not available — run /gswd:imagine to generate)*');
468
+ }
469
+ lines.push('');
470
+ lines.push('> See: .planning/research/ARCHITECTURE.md for full details');
471
+ lines.push('');
472
+ // ICP Highlights
473
+ lines.push('### ICP Highlights');
474
+ lines.push('');
475
+ if (bundle.icpHighlights) {
476
+ lines.push(bundle.icpHighlights);
477
+ }
478
+ else {
479
+ lines.push('*(ICP research not available)*');
480
+ }
481
+ lines.push('');
482
+ lines.push('> See: .planning/research/ICP.md for full details');
483
+ lines.push('');
484
+ // Competition Landscape
485
+ lines.push('### Competition Landscape');
486
+ lines.push('');
487
+ if (bundle.competitionLandscape) {
488
+ lines.push(bundle.competitionLandscape);
489
+ }
490
+ else {
491
+ lines.push('*(Competition research not available)*');
492
+ }
493
+ lines.push('');
494
+ lines.push('> See: .planning/research/COMPETITION.md for full details');
495
+ lines.push('');
496
+ // GTM Positioning
497
+ lines.push('### GTM Positioning');
498
+ lines.push('');
499
+ if (bundle.gtmPositioning) {
500
+ lines.push(bundle.gtmPositioning);
501
+ }
502
+ else {
503
+ lines.push('*(GTM research not available)*');
504
+ }
505
+ lines.push('');
506
+ lines.push('> See: .planning/research/GTM.md for full details');
507
+ lines.push('');
508
+ // Journey Insights
509
+ lines.push('### Journey Insights');
510
+ lines.push('');
511
+ if (bundle.journeyInsights) {
512
+ lines.push(bundle.journeyInsights);
513
+ }
514
+ else if (bundle.journeys.length > 0) {
515
+ // Synthesize from journeys array
516
+ for (const j of bundle.journeys) {
517
+ lines.push(`${j.id} (${j.name}, type: ${j.type}, FRs: ${j.linkedFRs.length})`);
518
+ }
519
+ }
520
+ else {
521
+ lines.push('*(No journeys defined)*');
522
+ }
523
+ lines.push('');
524
+ lines.push('> See: .planning/research/FEATURES.md for full details');
525
+ lines.push('');
526
+ // GSWD Artifacts table
527
+ lines.push('### GSWD Artifacts');
528
+ lines.push('');
529
+ lines.push('| File | Description |');
530
+ lines.push('|------|-------------|');
531
+ lines.push('| .planning/IMAGINE.md | Product vision, target user, and direction |');
532
+ lines.push('| .planning/DECISIONS.md | Frozen decisions, success metrics, risks |');
533
+ lines.push('| .planning/SPEC.md | Functional requirements with IDs |');
534
+ lines.push('| .planning/NFR.md | Non-functional requirements |');
535
+ lines.push('| .planning/JOURNEYS.md | User journeys with FR linkages |');
536
+ lines.push('| .planning/INTEGRATIONS.md | External integrations and fallbacks |');
537
+ lines.push('| .planning/research/ARCHITECTURE.md | Architecture research |');
538
+ lines.push('| .planning/research/FEATURES.md | Feature landscape research |');
539
+ lines.push('| .planning/research/PITFALLS.md | Common pitfalls research |');
540
+ lines.push('| .planning/research/STACK.md | Technology stack research |');
541
+ lines.push('| .planning/research/SUMMARY.md | Research executive summary |');
542
+ lines.push('| .planning/research/gswd/SUMMARY.md | GSD-format research bridge |');
543
+ lines.push('');
544
+ return lines.join('\n');
545
+ }
546
+ /**
547
+ * Generate REQUIREMENTS.md content from SpecBundle.
548
+ *
549
+ * Structure:
550
+ * - # Requirements
551
+ * - ## Functional Requirements
552
+ * - ### v1 (In Scope) — FRs sorted by priority then ID
553
+ * - ### v2 (Future) — FRs sorted by priority then ID
554
+ * - ## Non-Functional Requirements — grouped by NFR_CATEGORY_ORDER
555
+ * - ## Traceability — FR to journey mapping table (v1 FRs only)
556
+ *
557
+ * CRITICAL: No Date, Date.now(), or any time-dependent call.
558
+ * CRITICAL: Ends with exactly one trailing newline.
559
+ */
560
+ function generateRequirementsDoc(bundle) {
561
+ const lines = [];
562
+ lines.push('# Requirements');
563
+ lines.push('');
564
+ lines.push('## Functional Requirements');
565
+ lines.push('');
566
+ // Sort all FRs deterministically
567
+ const sortedFRs = sortFRs(bundle.frs);
568
+ const v1FRs = sortedFRs.filter((fr) => fr.scope === 'v1');
569
+ const v2FRs = sortedFRs.filter((fr) => fr.scope === 'v2');
570
+ // Build a journey map for quick lookup
571
+ const journeyMap = new Map();
572
+ for (const journey of bundle.journeys) {
573
+ journeyMap.set(journey.id, journey);
574
+ }
575
+ lines.push('### v1 (In Scope)');
576
+ lines.push('');
577
+ for (const fr of v1FRs) {
578
+ lines.push(`#### ${fr.id}: ${fr.description}`);
579
+ lines.push(`**Scope:** ${fr.scope} **Priority:** ${fr.priority}`);
580
+ // Journey origin
581
+ if (fr.sourceJourneys.length > 0) {
582
+ const journeyRefs = fr.sourceJourneys
583
+ .sort()
584
+ .map((jId) => {
585
+ const journey = journeyMap.get(jId);
586
+ return journey ? `${jId}: ${journey.name}` : jId;
587
+ })
588
+ .join(', ');
589
+ lines.push(`**Origin:** From ${journeyRefs}`);
590
+ }
591
+ // Acceptance criteria
592
+ if (fr.acceptanceCriteria && fr.acceptanceCriteria.length > 0) {
593
+ lines.push('**Acceptance Criteria:**');
594
+ for (const criterion of fr.acceptanceCriteria) {
595
+ lines.push(`- ${criterion}`);
596
+ }
597
+ }
598
+ // NFR constraints — apply heuristic based on NFR category
599
+ const applicableNFRs = [];
600
+ for (const nfr of bundle.nfrs) {
601
+ const cat = nfr.category.toLowerCase();
602
+ // Security and performance apply to all v1 FRs
603
+ if (cat === 'security' || cat === 'performance') {
604
+ applicableNFRs.push(`${nfr.id} (${nfr.category.charAt(0).toUpperCase() + nfr.category.slice(1)})`);
605
+ continue;
606
+ }
607
+ // Observability applies to all v1 FRs
608
+ if (cat === 'observability') {
609
+ applicableNFRs.push(`${nfr.id} (${nfr.category.charAt(0).toUpperCase() + nfr.category.slice(1)})`);
610
+ continue;
611
+ }
612
+ // Privacy applies if FR touches user/data/personal/profile/account keywords
613
+ if (cat === 'privacy') {
614
+ const desc = fr.description.toLowerCase();
615
+ if (desc.includes('user') || desc.includes('data') || desc.includes('personal') ||
616
+ desc.includes('profile') || desc.includes('account')) {
617
+ applicableNFRs.push(`${nfr.id} (${nfr.category.charAt(0).toUpperCase() + nfr.category.slice(1)})`);
618
+ }
619
+ }
620
+ }
621
+ if (applicableNFRs.length > 0) {
622
+ lines.push(`**Applicable NFR Constraints:** ${applicableNFRs.join(', ')}`);
623
+ }
624
+ lines.push('');
625
+ }
626
+ if (v1FRs.length === 0) {
627
+ lines.push('*(none)*');
628
+ lines.push('');
629
+ }
630
+ if (v2FRs.length > 0) {
631
+ lines.push('### v2 (Future)');
632
+ lines.push('');
633
+ for (const fr of v2FRs) {
634
+ lines.push(`#### ${fr.id}: ${fr.description}`);
635
+ lines.push(`**Scope:** ${fr.scope} **Priority:** ${fr.priority}`);
636
+ lines.push('');
637
+ }
638
+ }
639
+ // NFRs grouped by category in fixed order
640
+ lines.push('## Non-Functional Requirements');
641
+ lines.push('');
642
+ for (const category of exports.NFR_CATEGORY_ORDER) {
643
+ const categoryNFRs = bundle.nfrs
644
+ .filter((nfr) => nfr.category === category)
645
+ .sort((a, b) => parseInt(a.id.split('-')[1], 10) - parseInt(b.id.split('-')[1], 10));
646
+ if (categoryNFRs.length === 0)
647
+ continue;
648
+ const categoryTitle = category.charAt(0).toUpperCase() + category.slice(1);
649
+ lines.push(`### ${categoryTitle}`);
650
+ lines.push('');
651
+ for (const nfr of categoryNFRs) {
652
+ lines.push(`#### ${nfr.id}: ${nfr.description}`);
653
+ lines.push(`**Threshold:** ${nfr.threshold}`);
654
+ lines.push('');
655
+ }
656
+ }
657
+ // Traceability table — v1 FRs only, journey refs sorted
658
+ lines.push('## Traceability');
659
+ lines.push('');
660
+ lines.push('| FR | Description | Journeys |');
661
+ lines.push('|----|-------------|----------|');
662
+ for (const fr of v1FRs) {
663
+ const journeyRefs = [...fr.sourceJourneys].sort().join(', ') || '(none)';
664
+ lines.push(`| ${fr.id} | ${fr.description} | ${journeyRefs} |`);
665
+ }
666
+ lines.push('');
667
+ return lines.join('\n');
668
+ }
669
+ /**
670
+ * Generate ROADMAP.md content from SpecBundle.
671
+ *
672
+ * Structure:
673
+ * - # Roadmap
674
+ * - ## Overview
675
+ * - ## Phases
676
+ * - ### Phase 1-3: journeys grouped by JOURNEY_TYPE_TO_PHASE, sorted by ID
677
+ * - ### Phase 4: observability+security NFRs sorted by ID
678
+ *
679
+ * CRITICAL: Always emit all 4 phases.
680
+ * CRITICAL: No Date, Date.now(), or any time-dependent call.
681
+ * CRITICAL: Ends with exactly one trailing newline.
682
+ */
683
+ function generateRoadmapDoc(bundle) {
684
+ const lines = [];
685
+ lines.push('# Roadmap');
686
+ lines.push('');
687
+ lines.push('## Overview');
688
+ lines.push('');
689
+ lines.push('v1 journeys assigned to 4 phases following deterministic rules.');
690
+ lines.push('');
691
+ lines.push('## Phases');
692
+ lines.push('');
693
+ // Group journeys by phase (sorted by ID within each phase)
694
+ const phaseJourneys = new Map();
695
+ for (let i = 1; i <= 4; i++) {
696
+ phaseJourneys.set(i, []);
697
+ }
698
+ // Sort journeys by numeric ID before grouping for determinism
699
+ const sortedJourneys = [...bundle.journeys].sort((a, b) => parseInt(a.id.split('-')[1], 10) - parseInt(b.id.split('-')[1], 10));
700
+ for (const journey of sortedJourneys) {
701
+ const phase = exports.JOURNEY_TYPE_TO_PHASE[journey.type] ?? 3;
702
+ phaseJourneys.get(phase).push(journey);
703
+ }
704
+ // Always emit all 4 phases
705
+ for (let phaseNum = 1; phaseNum <= 4; phaseNum++) {
706
+ lines.push(`### Phase ${phaseNum}: ${exports.PHASE_NAMES[phaseNum]}`);
707
+ lines.push('');
708
+ if (phaseNum < 4) {
709
+ const journeys = phaseJourneys.get(phaseNum);
710
+ if (journeys.length === 0) {
711
+ lines.push('*(no journeys assigned)*');
712
+ }
713
+ else {
714
+ // Emit a Goal line with journey IDs
715
+ const goalJourneys = journeys
716
+ .map((j) => `${j.id} (${j.name})`)
717
+ .join(', ');
718
+ lines.push(`**Goal:** Implement ${goalJourneys}`);
719
+ lines.push('');
720
+ for (const j of journeys) {
721
+ lines.push(`- **${j.id}**: ${j.name}`);
722
+ const frRefs = [...j.linkedFRs].sort().join(', ');
723
+ if (frRefs) {
724
+ lines.push(` FRs: ${frRefs}`);
725
+ }
726
+ }
727
+ }
728
+ }
729
+ else {
730
+ // Phase 4: observability + security NFRs sorted by ID
731
+ const obsSecNFRs = bundle.nfrs
732
+ .filter((nfr) => nfr.category === 'observability' || nfr.category === 'security')
733
+ .sort((a, b) => parseInt(a.id.split('-')[1], 10) - parseInt(b.id.split('-')[1], 10));
734
+ if (obsSecNFRs.length === 0) {
735
+ lines.push('*(no NFRs assigned)*');
736
+ }
737
+ else {
738
+ // Emit a Goal line for Phase 4
739
+ const nfrGoal = obsSecNFRs.map((nfr) => nfr.id).join(', ');
740
+ lines.push(`**Goal:** Enforce ${nfrGoal}`);
741
+ lines.push('');
742
+ for (const nfr of obsSecNFRs) {
743
+ lines.push(`- **${nfr.id}**: ${nfr.description}`);
744
+ }
745
+ }
746
+ }
747
+ lines.push('');
748
+ }
749
+ return lines.join('\n');
750
+ }
751
+ /**
752
+ * Generate a GSD-format research SUMMARY.md from SpecBundle.
753
+ *
754
+ * Maps GSWD spec content to GSD research template headings.
755
+ * Pure function — no Date calls, no I/O.
756
+ * Versioned with <!-- gswd-schema-version: 1 --> header.
757
+ */
758
+ function generateResearchSummary(bundle) {
759
+ const lines = [];
760
+ lines.push('<!-- gswd-schema-version: 1 -->');
761
+ lines.push('# Project Research Summary');
762
+ lines.push('');
763
+ lines.push(`**Project:** ${bundle.projectSlug}`);
764
+ lines.push('**Domain:** Product specification');
765
+ lines.push('**Confidence:** HIGH');
766
+ lines.push('');
767
+ // ## Executive Summary
768
+ lines.push('## Executive Summary');
769
+ lines.push('');
770
+ if (bundle.vision) {
771
+ lines.push(bundle.vision);
772
+ lines.push('');
773
+ }
774
+ if (bundle.productDirection) {
775
+ lines.push(bundle.productDirection);
776
+ lines.push('');
777
+ }
778
+ // ## Key Findings
779
+ lines.push('## Key Findings');
780
+ lines.push('');
781
+ // ### Recommended Stack
782
+ lines.push('### Recommended Stack');
783
+ lines.push('');
784
+ if (bundle.architectureSummary) {
785
+ lines.push(bundle.architectureSummary);
786
+ }
787
+ else {
788
+ lines.push('*(Stack research not available from GSWD — will be determined during GSD planning)*');
789
+ }
790
+ lines.push('');
791
+ // ### Expected Features
792
+ lines.push('### Expected Features');
793
+ lines.push('');
794
+ if (bundle.icpHighlights) {
795
+ lines.push(bundle.icpHighlights);
796
+ }
797
+ else {
798
+ // Synthesize from FRs
799
+ const p0FRs = bundle.frs.filter((f) => f.scope === 'v1' && f.priority === 'P0');
800
+ const p1FRs = bundle.frs.filter((f) => f.scope === 'v1' && f.priority === 'P1');
801
+ if (p0FRs.length > 0) {
802
+ lines.push('**Must have (table stakes):**');
803
+ for (const fr of p0FRs) {
804
+ lines.push(`- ${fr.description}`);
805
+ }
806
+ lines.push('');
807
+ }
808
+ if (p1FRs.length > 0) {
809
+ lines.push('**Should have:**');
810
+ for (const fr of p1FRs) {
811
+ lines.push(`- ${fr.description}`);
812
+ }
813
+ }
814
+ }
815
+ lines.push('');
816
+ // ### Architecture Approach
817
+ lines.push('### Architecture Approach');
818
+ lines.push('');
819
+ if (bundle.architectureSummary) {
820
+ lines.push(bundle.architectureSummary);
821
+ }
822
+ else {
823
+ lines.push('*(Architecture to be determined during GSD planning)*');
824
+ }
825
+ lines.push('');
826
+ // ### Critical Pitfalls
827
+ lines.push('### Critical Pitfalls');
828
+ lines.push('');
829
+ if (bundle.risks.length > 0) {
830
+ for (const risk of bundle.risks) {
831
+ lines.push(`- ${risk}`);
832
+ }
833
+ }
834
+ else {
835
+ lines.push('*(No risks identified)*');
836
+ }
837
+ lines.push('');
838
+ // ## Implications for Roadmap
839
+ lines.push('## Implications for Roadmap');
840
+ lines.push('');
841
+ // Group journeys by phase and describe implications
842
+ const phaseJourneyMap = new Map();
843
+ for (let i = 1; i <= 4; i++) {
844
+ phaseJourneyMap.set(i, []);
845
+ }
846
+ for (const journey of bundle.journeys) {
847
+ const phase = exports.JOURNEY_TYPE_TO_PHASE[journey.type] ?? 3;
848
+ phaseJourneyMap.get(phase).push(journey);
849
+ }
850
+ for (let phaseNum = 1; phaseNum <= 3; phaseNum++) {
851
+ const phaseJ = phaseJourneyMap.get(phaseNum);
852
+ if (phaseJ.length > 0) {
853
+ const journeyList = phaseJ.map((j) => `${j.id} (${j.name})`).join(', ');
854
+ lines.push(`- **Phase ${phaseNum}**: ${exports.PHASE_NAMES[phaseNum]} — ${journeyList}`);
855
+ }
856
+ }
857
+ const obsSecNFRs = bundle.nfrs.filter((nfr) => nfr.category === 'observability' || nfr.category === 'security');
858
+ if (obsSecNFRs.length > 0) {
859
+ const nfrList = obsSecNFRs.map((nfr) => nfr.id).join(', ');
860
+ lines.push(`- **Phase 4**: ${exports.PHASE_NAMES[4]} — enforce ${nfrList}`);
861
+ }
862
+ lines.push('');
863
+ // ## Confidence Assessment
864
+ lines.push('## Confidence Assessment');
865
+ lines.push('');
866
+ lines.push('| Area | Confidence | Notes |');
867
+ lines.push('|------|------------|-------|');
868
+ lines.push(`| Stack | ${bundle.architectureSummary ? 'HIGH' : 'LOW'} | ${bundle.architectureSummary ? 'From GSWD research' : 'Not researched in GSWD'} |`);
869
+ lines.push(`| Features | HIGH | ${bundle.frs.length} FRs defined with traceability |`);
870
+ lines.push(`| Architecture | ${bundle.architectureSummary ? 'HIGH' : 'MEDIUM'} | ${bundle.architectureSummary ? 'From GSWD research' : 'Inferred from requirements'} |`);
871
+ lines.push(`| Pitfalls | MEDIUM | ${bundle.risks.length} risks identified |`);
872
+ lines.push('');
873
+ // ## Sources
874
+ lines.push('## Sources');
875
+ lines.push('');
876
+ lines.push('### Primary (HIGH confidence)');
877
+ lines.push('- GSWD specification pipeline — full imagine/specify/audit/compile cycle');
878
+ lines.push('');
879
+ lines.push('---');
880
+ lines.push('*Research completed: via GSWD compile*');
881
+ lines.push('*Ready for roadmap: yes*');
882
+ lines.push('');
883
+ return lines.join('\n');
884
+ }
885
+ // ─── Contract Validator ───────────────────────────────────────────────────────
886
+ /**
887
+ * Validate generated contract docs against the spec bundle.
888
+ *
889
+ * 4 checks:
890
+ * 1. v1_coverage: Every v1 FR ID appears in the generated roadmap content
891
+ * 2. orphan_requirement: Every v1 FR has at least 1 source journey (sourceJourneys.length > 0)
892
+ * 3. integration_sanity: Every integration referenced by any journey is approved or deferred-with-fallback
893
+ * 4. required_headings: All 4 generated docs contain their REQUIRED_HEADINGS entries
894
+ */
895
+ function validateContracts(generatedDocs, bundle) {
896
+ const findings = [];
897
+ // Check 1: v1_coverage — all v1 FR IDs appear in roadmap content
898
+ for (const fr of bundle.frs) {
899
+ if (fr.scope !== 'v1')
900
+ continue;
901
+ if (!generatedDocs.roadmap.includes(fr.id)) {
902
+ findings.push({
903
+ check: 'v1_coverage',
904
+ id: fr.id,
905
+ issue: `v1 FR ${fr.id} is not referenced in the generated roadmap`,
906
+ });
907
+ }
908
+ }
909
+ // Check 2: orphan_requirement — all v1 FRs have at least 1 source journey
910
+ for (const fr of bundle.frs) {
911
+ if (fr.scope !== 'v1')
912
+ continue;
913
+ if (fr.sourceJourneys.length === 0) {
914
+ findings.push({
915
+ check: 'orphan_requirement',
916
+ id: fr.id,
917
+ issue: `v1 FR ${fr.id} is not referenced by any journey`,
918
+ });
919
+ }
920
+ }
921
+ // Check 3: integration_sanity — integrations referenced by journeys must be approved or deferred-with-fallback
922
+ // Build set of integration IDs referenced by journeys
923
+ const referencedIntegrationIds = new Set();
924
+ for (const journey of bundle.journeys) {
925
+ for (const intId of journey.linkedIntegrations) {
926
+ referencedIntegrationIds.add(intId);
927
+ }
928
+ }
929
+ for (const integration of bundle.integrations) {
930
+ if (!referencedIntegrationIds.has(integration.id))
931
+ continue;
932
+ const isApproved = integration.status === 'approved';
933
+ const isDeferredWithFallback = integration.status === 'deferred' && integration.fallback.trim().length > 0;
934
+ if (!isApproved && !isDeferredWithFallback) {
935
+ findings.push({
936
+ check: 'integration_sanity',
937
+ id: integration.id,
938
+ issue: `Integration ${integration.id} (status: ${integration.status}) is referenced by a journey but is not approved or deferred-with-fallback`,
939
+ });
940
+ }
941
+ }
942
+ // Check 4: required_headings — validate each generated doc with its file type key
943
+ const docMap = [
944
+ ['PROJECT.md', generatedDocs.project],
945
+ ['REQUIREMENTS.md', generatedDocs.requirements],
946
+ ['ROADMAP.md', generatedDocs.roadmap],
947
+ ['STATE.md', generatedDocs.state],
948
+ ];
949
+ for (const [fileType, content] of docMap) {
950
+ const validation = (0, parse_js_1.validateHeadings)(content, fileType);
951
+ for (const missing of validation.missing) {
952
+ findings.push({
953
+ check: 'required_headings',
954
+ id: fileType,
955
+ issue: `Generated ${fileType} is missing required heading: ${missing}`,
956
+ });
957
+ }
958
+ }
959
+ return { passed: findings.length === 0, findings };
960
+ }
961
+ /**
962
+ * Generate STATE.md content from SpecBundle.
963
+ *
964
+ * Structure:
965
+ * - # Project State
966
+ * - ## Frozen Decisions (numbered list)
967
+ * - ## Approvals (key-value list from bundle.approvals)
968
+ * - ## Open Questions (bulleted)
969
+ * - ## Risks (bulleted)
970
+ *
971
+ * CRITICAL: No Date, Date.now(), or any time-dependent call.
972
+ * CRITICAL: Ends with exactly one trailing newline.
973
+ */
974
+ function generateStateDoc(bundle) {
975
+ const lines = [];
976
+ lines.push('# Project State');
977
+ lines.push('');
978
+ lines.push('## Frozen Decisions');
979
+ lines.push('');
980
+ for (let i = 0; i < bundle.frozenDecisions.length; i++) {
981
+ lines.push(`${i + 1}. ${bundle.frozenDecisions[i]}`);
982
+ }
983
+ if (bundle.frozenDecisions.length === 0) {
984
+ lines.push('*(none)*');
985
+ }
986
+ lines.push('');
987
+ lines.push('## Approvals');
988
+ lines.push('');
989
+ const approvalKeys = Object.keys(bundle.approvals).sort();
990
+ for (const key of approvalKeys) {
991
+ lines.push(`- **${key}:** ${bundle.approvals[key]}`);
992
+ }
993
+ if (approvalKeys.length === 0) {
994
+ lines.push('*(none)*');
995
+ }
996
+ lines.push('');
997
+ lines.push('## Open Questions');
998
+ lines.push('');
999
+ for (const question of bundle.openQuestions) {
1000
+ lines.push(`- ${question}`);
1001
+ }
1002
+ if (bundle.openQuestions.length === 0) {
1003
+ lines.push('*(none)*');
1004
+ }
1005
+ lines.push('');
1006
+ lines.push('## Risks');
1007
+ lines.push('');
1008
+ for (const risk of bundle.risks) {
1009
+ lines.push(`- ${risk}`);
1010
+ }
1011
+ if (bundle.risks.length === 0) {
1012
+ lines.push('*(none)*');
1013
+ }
1014
+ lines.push('');
1015
+ return lines.join('\n');
1016
+ }
1017
+ // ─── Spec Artifact File Reader ────────────────────────────────────────────────
1018
+ /**
1019
+ * Read all 6 spec artifact files from a planning directory.
1020
+ * Also reads research files from .planning/research/ directory.
1021
+ * Returns a content map keyed by artifact type.
1022
+ * Missing files return empty string (does not throw).
1023
+ */
1024
+ function readSpecArtifacts(planningDir) {
1025
+ const fileMap = [
1026
+ ['IMAGINE.md', 'imagine'],
1027
+ ['DECISIONS.md', 'decisions'],
1028
+ ['SPEC.md', 'spec'],
1029
+ ['NFR.md', 'nfr'],
1030
+ ['JOURNEYS.md', 'journeys'],
1031
+ ['INTEGRATIONS.md', 'integrations'],
1032
+ ];
1033
+ const result = {};
1034
+ for (const [filename, key] of fileMap) {
1035
+ try {
1036
+ result[key] = fs.readFileSync(path.join(planningDir, filename), 'utf-8');
1037
+ }
1038
+ catch {
1039
+ result[key] = '';
1040
+ }
1041
+ }
1042
+ // Read research files from .planning/research/ subdirectory
1043
+ const researchDir = path.join(planningDir, 'research');
1044
+ const researchFileMap = [
1045
+ ['ARCHITECTURE.md', 'architecture'],
1046
+ ['ICP.md', 'icp'],
1047
+ ['COMPETITION.md', 'competition'],
1048
+ ['GTM.md', 'gtm'],
1049
+ ['SUMMARY.md', 'researchSummary'],
1050
+ ['FEATURES.md', 'features'],
1051
+ ];
1052
+ for (const [filename, key] of researchFileMap) {
1053
+ try {
1054
+ result[key] = fs.readFileSync(path.join(researchDir, filename), 'utf-8');
1055
+ }
1056
+ catch {
1057
+ result[key] = '';
1058
+ }
1059
+ }
1060
+ return result;
1061
+ }
1062
+ // ─── Workflow Orchestrator ────────────────────────────────────────────────────
1063
+ /**
1064
+ * Full compile workflow orchestrator.
1065
+ *
1066
+ * Steps:
1067
+ * 1. Check audit gate via checkAuditGate(). If false, return early with error.
1068
+ * 2. Update stage status: compile -> in_progress.
1069
+ * 3. Read spec artifacts from planningDir.
1070
+ * 4. Parse into SpecBundle.
1071
+ * 5. Generate all 4 contract docs (pure functions, no I/O).
1072
+ * 6. Run validator: validateContracts().
1073
+ * 7. If validator FAIL: update state -> fail. Return without writing files.
1074
+ * 8. Write all 4 docs atomically via safeWriteFile().
1075
+ * 9. Update state: stage_status.compile = 'done', stage = 'compile'.
1076
+ * 10. Write checkpoint.
1077
+ * 11. Return result with passed=true and filesWritten list.
1078
+ */
1079
+ function runCompileWorkflow(options) {
1080
+ const { planningDir, statePath } = options;
1081
+ // Step 1: Check audit gate
1082
+ if (!(0, audit_js_1.checkAuditGate)(statePath)) {
1083
+ return { passed: false, error: 'Audit gate not passed', filesWritten: [] };
1084
+ }
1085
+ // Step 2: Update stage status -> in_progress
1086
+ (0, state_js_1.updateStageStatus)(statePath, 'compile', 'in_progress');
1087
+ // Step 3: Read spec artifacts
1088
+ const files = readSpecArtifacts(planningDir);
1089
+ // Step 4: Parse into SpecBundle (includes research file enrichment)
1090
+ const bundle = parseSpecBundle({
1091
+ imagine: files['imagine'] ?? '',
1092
+ decisions: files['decisions'] ?? '',
1093
+ spec: files['spec'] ?? '',
1094
+ nfr: files['nfr'] ?? '',
1095
+ journeys: files['journeys'] ?? '',
1096
+ integrations: files['integrations'] ?? '',
1097
+ architecture: files['architecture'] ?? '',
1098
+ icp: files['icp'] ?? '',
1099
+ competition: files['competition'] ?? '',
1100
+ gtm: files['gtm'] ?? '',
1101
+ researchSummary: files['researchSummary'] ?? '',
1102
+ features: files['features'] ?? '',
1103
+ }, statePath);
1104
+ // Step 5: Generate all 4 contract docs + GSD research bridge (pure functions)
1105
+ const generated = {
1106
+ project: generateProjectDoc(bundle),
1107
+ requirements: generateRequirementsDoc(bundle),
1108
+ roadmap: generateRoadmapDoc(bundle),
1109
+ state: generateStateDoc(bundle),
1110
+ researchSummary: generateResearchSummary(bundle),
1111
+ };
1112
+ // Step 6: Run validator
1113
+ const validatorResult = validateContracts(generated, bundle);
1114
+ // Step 7: If validator FAIL, update state -> fail and return early
1115
+ if (!validatorResult.passed) {
1116
+ (0, state_js_1.updateStageStatus)(statePath, 'compile', 'fail');
1117
+ return { passed: false, validatorResult, filesWritten: [] };
1118
+ }
1119
+ // Step 8: Write all 4 contract docs atomically
1120
+ const fileOutputMap = [
1121
+ ['PROJECT.md', generated.project],
1122
+ ['REQUIREMENTS.md', generated.requirements],
1123
+ ['ROADMAP.md', generated.roadmap],
1124
+ ['STATE.md', generated.state],
1125
+ ];
1126
+ const filesWritten = [];
1127
+ for (const [filename, content] of fileOutputMap) {
1128
+ const filePath = path.join(planningDir, filename);
1129
+ (0, state_js_1.safeWriteFile)(filePath, content);
1130
+ filesWritten.push(filePath);
1131
+ }
1132
+ // Step 8b: Write GSD research bridge SUMMARY.md to .planning/research/gswd/SUMMARY.md
1133
+ // planningDir resolves to .planning/ in production; research/gswd/ is a subdirectory
1134
+ const gsdResearchDir = path.join(planningDir, 'research', 'gswd');
1135
+ if (!fs.existsSync(gsdResearchDir)) {
1136
+ fs.mkdirSync(gsdResearchDir, { recursive: true });
1137
+ }
1138
+ const summaryPath = path.join(gsdResearchDir, 'SUMMARY.md');
1139
+ (0, state_js_1.safeWriteFile)(summaryPath, generated.researchSummary);
1140
+ filesWritten.push(summaryPath);
1141
+ // Step 9: Update state: stage_status.compile = 'done', stage = 'compile'
1142
+ (0, state_js_1.updateStageStatus)(statePath, 'compile', 'done');
1143
+ const state = (0, state_js_1.readState)(statePath);
1144
+ if (state) {
1145
+ state.stage = 'compile';
1146
+ (0, state_js_1.writeState)(statePath, state);
1147
+ }
1148
+ // Step 10: Write checkpoint
1149
+ (0, state_js_1.writeCheckpoint)(statePath, 'gswd/compile', 'compile-done');
1150
+ // Step 11: Return result
1151
+ return { passed: true, validatorResult, filesWritten, generated };
1152
+ }