gswd 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/gswd-tools.cjs +228 -0
- package/commands/gswd/imagine.md +7 -1
- package/commands/gswd/start.md +507 -32
- package/dist/lib/audit.d.ts +205 -0
- package/dist/lib/audit.js +805 -0
- package/dist/lib/bootstrap.d.ts +103 -0
- package/dist/lib/bootstrap.js +563 -0
- package/dist/lib/compile.d.ts +239 -0
- package/dist/lib/compile.js +1152 -0
- package/dist/lib/config.d.ts +49 -0
- package/dist/lib/config.js +150 -0
- package/dist/lib/imagine-agents.d.ts +54 -0
- package/dist/lib/imagine-agents.js +185 -0
- package/dist/lib/imagine-gate.d.ts +47 -0
- package/dist/lib/imagine-gate.js +131 -0
- package/dist/lib/imagine-input.d.ts +46 -0
- package/dist/lib/imagine-input.js +233 -0
- package/dist/lib/imagine-synthesis.d.ts +90 -0
- package/dist/lib/imagine-synthesis.js +453 -0
- package/dist/lib/imagine.d.ts +56 -0
- package/dist/lib/imagine.js +413 -0
- package/dist/lib/intake.d.ts +27 -0
- package/dist/lib/intake.js +82 -0
- package/dist/lib/parse.d.ts +59 -0
- package/dist/lib/parse.js +171 -0
- package/dist/lib/render.d.ts +309 -0
- package/dist/lib/render.js +624 -0
- package/dist/lib/specify-agents.d.ts +120 -0
- package/dist/lib/specify-agents.js +269 -0
- package/dist/lib/specify-journeys.d.ts +124 -0
- package/dist/lib/specify-journeys.js +279 -0
- package/dist/lib/specify-nfr.d.ts +45 -0
- package/dist/lib/specify-nfr.js +159 -0
- package/dist/lib/specify-roles.d.ts +46 -0
- package/dist/lib/specify-roles.js +88 -0
- package/dist/lib/specify.d.ts +70 -0
- package/dist/lib/specify.js +676 -0
- package/dist/lib/state.d.ts +140 -0
- package/dist/lib/state.js +340 -0
- package/dist/tests/audit.test.d.ts +4 -0
- package/dist/tests/audit.test.js +1579 -0
- package/dist/tests/bootstrap.test.d.ts +5 -0
- package/dist/tests/bootstrap.test.js +611 -0
- package/dist/tests/compile.test.d.ts +4 -0
- package/dist/tests/compile.test.js +862 -0
- package/dist/tests/config.test.d.ts +4 -0
- package/dist/tests/config.test.js +191 -0
- package/dist/tests/imagine-agents.test.d.ts +6 -0
- package/dist/tests/imagine-agents.test.js +179 -0
- package/dist/tests/imagine-gate.test.d.ts +6 -0
- package/dist/tests/imagine-gate.test.js +264 -0
- package/dist/tests/imagine-input.test.d.ts +6 -0
- package/dist/tests/imagine-input.test.js +283 -0
- package/dist/tests/imagine-synthesis.test.d.ts +7 -0
- package/dist/tests/imagine-synthesis.test.js +380 -0
- package/dist/tests/imagine.test.d.ts +8 -0
- package/dist/tests/imagine.test.js +406 -0
- package/dist/tests/parse.test.d.ts +4 -0
- package/dist/tests/parse.test.js +285 -0
- package/dist/tests/render.test.d.ts +4 -0
- package/dist/tests/render.test.js +236 -0
- package/dist/tests/specify-agents.test.d.ts +4 -0
- package/dist/tests/specify-agents.test.js +352 -0
- package/dist/tests/specify-journeys.test.d.ts +5 -0
- package/dist/tests/specify-journeys.test.js +440 -0
- package/dist/tests/specify-nfr.test.d.ts +4 -0
- package/dist/tests/specify-nfr.test.js +205 -0
- package/dist/tests/specify-roles.test.d.ts +4 -0
- package/dist/tests/specify-roles.test.js +136 -0
- package/dist/tests/specify.test.d.ts +9 -0
- package/dist/tests/specify.test.js +544 -0
- package/dist/tests/state.test.d.ts +4 -0
- package/dist/tests/state.test.js +316 -0
- package/lib/bootstrap.ts +37 -11
- package/lib/compile.ts +426 -4
- package/lib/imagine-agents.ts +53 -7
- package/lib/imagine-synthesis.ts +170 -6
- package/lib/imagine.ts +59 -5
- package/lib/intake.ts +60 -0
- package/lib/parse.ts +2 -1
- package/lib/render.ts +566 -5
- package/lib/specify-agents.ts +25 -3
- package/lib/state.ts +115 -0
- package/package.json +3 -2
- package/templates/gswd/DECISIONS.template.md +3 -0
|
@@ -0,0 +1,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
|
+
}
|