gswd 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/gswd-tools.cjs +228 -0
- package/bin/install.js +8 -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 +4 -2
- package/templates/gswd/DECISIONS.template.md +3 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* GSWD Specify Journeys Module — Journey generation, FR extraction, cross-linking
|
|
4
|
+
*
|
|
5
|
+
* Produces journey structures and extracts functional requirements from journey steps.
|
|
6
|
+
* Bidirectional cross-linking: journeys reference FR IDs, FRs reference source journeys.
|
|
7
|
+
*
|
|
8
|
+
* Schema: GSWD_SPEC.md Section 6.1 (ID formats), 6.2 (scope tagging), 8.3 (specify workflow)
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.JOURNEY_TYPES = void 0;
|
|
12
|
+
exports.assignScope = assignScope;
|
|
13
|
+
exports.assignPriority = assignPriority;
|
|
14
|
+
exports.generateJourneyStructure = generateJourneyStructure;
|
|
15
|
+
exports.validateJourney = validateJourney;
|
|
16
|
+
exports.extractFRsFromJourneys = extractFRsFromJourneys;
|
|
17
|
+
exports.buildTraceabilityMap = buildTraceabilityMap;
|
|
18
|
+
exports.validateFRCoverage = validateFRCoverage;
|
|
19
|
+
const parse_js_1 = require("./parse.js");
|
|
20
|
+
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
21
|
+
/**
|
|
22
|
+
* All 6 required journey types from GSWD_SPEC Section 8.3.
|
|
23
|
+
*/
|
|
24
|
+
exports.JOURNEY_TYPES = [
|
|
25
|
+
{ type: 'onboarding', displayName: 'Onboarding' },
|
|
26
|
+
{ type: 'core_action', displayName: 'Core Action' },
|
|
27
|
+
{ type: 'view_results', displayName: 'View Results/History' },
|
|
28
|
+
{ type: 'settings', displayName: 'Settings/Preferences' },
|
|
29
|
+
{ type: 'error_states', displayName: 'Error States' },
|
|
30
|
+
{ type: 'empty_states', displayName: 'Empty States' },
|
|
31
|
+
];
|
|
32
|
+
/** Journey types that allow fewer steps (minimum 3 instead of 5) */
|
|
33
|
+
const REDUCED_STEP_TYPES = ['error_states', 'empty_states'];
|
|
34
|
+
// ─── Scope & Priority Heuristics ────────────────────────────────────────────
|
|
35
|
+
/**
|
|
36
|
+
* Assign scope based on journey type.
|
|
37
|
+
* Heuristic from CONTEXT.md locked decisions:
|
|
38
|
+
* - core_action/onboarding → v1
|
|
39
|
+
* - error_states/empty_states → v1 (required for polish)
|
|
40
|
+
* - view_results → v1
|
|
41
|
+
* - settings → v1 (default; caller can override to v2 for non-essential)
|
|
42
|
+
*/
|
|
43
|
+
function assignScope(journeyType) {
|
|
44
|
+
switch (journeyType) {
|
|
45
|
+
case 'onboarding':
|
|
46
|
+
case 'core_action':
|
|
47
|
+
case 'view_results':
|
|
48
|
+
case 'error_states':
|
|
49
|
+
case 'empty_states':
|
|
50
|
+
case 'settings':
|
|
51
|
+
return 'v1';
|
|
52
|
+
default:
|
|
53
|
+
return 'v1';
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Assign priority based on journey type.
|
|
58
|
+
* Heuristic from CONTEXT.md locked decisions:
|
|
59
|
+
* - core_action/onboarding → P0 (blocks core journey)
|
|
60
|
+
* - error_states/empty_states/view_results/settings → P1 (required for complete v1)
|
|
61
|
+
*/
|
|
62
|
+
function assignPriority(journeyType) {
|
|
63
|
+
switch (journeyType) {
|
|
64
|
+
case 'core_action':
|
|
65
|
+
case 'onboarding':
|
|
66
|
+
return 'P0';
|
|
67
|
+
case 'error_states':
|
|
68
|
+
case 'empty_states':
|
|
69
|
+
case 'view_results':
|
|
70
|
+
case 'settings':
|
|
71
|
+
return 'P1';
|
|
72
|
+
default:
|
|
73
|
+
return 'P1';
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// ─── Journey Structure Generation ───────────────────────────────────────────
|
|
77
|
+
/**
|
|
78
|
+
* Format a journey ID: J-001, J-002, etc.
|
|
79
|
+
*/
|
|
80
|
+
function formatJourneyId(journeyNumber) {
|
|
81
|
+
return (0, parse_js_1.normalizeId)(`J-${journeyNumber}`);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Create a journey skeleton with proper ID and empty arrays.
|
|
85
|
+
*/
|
|
86
|
+
function generateJourneyStructure(type, journeyNumber) {
|
|
87
|
+
const typeInfo = exports.JOURNEY_TYPES.find((t) => t.type === type);
|
|
88
|
+
const displayName = typeInfo ? typeInfo.displayName : type;
|
|
89
|
+
return {
|
|
90
|
+
id: formatJourneyId(journeyNumber),
|
|
91
|
+
name: displayName,
|
|
92
|
+
type,
|
|
93
|
+
preconditions: [],
|
|
94
|
+
steps: [],
|
|
95
|
+
success: '',
|
|
96
|
+
failureModes: [],
|
|
97
|
+
acceptanceTests: [],
|
|
98
|
+
linkedFRs: [],
|
|
99
|
+
linkedNFRs: [],
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
// ─── Journey Validation ─────────────────────────────────────────────────────
|
|
103
|
+
/**
|
|
104
|
+
* Validate a journey meets all structural requirements.
|
|
105
|
+
*/
|
|
106
|
+
function validateJourney(journey) {
|
|
107
|
+
const errors = [];
|
|
108
|
+
// ID format check
|
|
109
|
+
if (!/^J-\d{3,}$/.test(journey.id)) {
|
|
110
|
+
errors.push(`Invalid journey ID format: ${journey.id} (expected J-XXX)`);
|
|
111
|
+
}
|
|
112
|
+
// Minimum step count: 3 for error/empty, 5 for main journeys
|
|
113
|
+
const minSteps = REDUCED_STEP_TYPES.includes(journey.type) ? 3 : 5;
|
|
114
|
+
if (journey.steps.length < minSteps) {
|
|
115
|
+
errors.push(`Journey ${journey.id} has ${journey.steps.length} steps, minimum is ${minSteps} for type '${journey.type}'`);
|
|
116
|
+
}
|
|
117
|
+
// Maximum step count: 8 recommended
|
|
118
|
+
if (journey.steps.length > 8) {
|
|
119
|
+
errors.push(`Journey ${journey.id} has ${journey.steps.length} steps, recommended maximum is 8`);
|
|
120
|
+
}
|
|
121
|
+
// Failure modes >= 2
|
|
122
|
+
if (journey.failureModes.length < 2) {
|
|
123
|
+
errors.push(`Journey ${journey.id} has ${journey.failureModes.length} failure modes, minimum is 2`);
|
|
124
|
+
}
|
|
125
|
+
// Acceptance tests >= 1
|
|
126
|
+
if (journey.acceptanceTests.length < 1) {
|
|
127
|
+
errors.push(`Journey ${journey.id} has ${journey.acceptanceTests.length} acceptance tests, minimum is 1`);
|
|
128
|
+
}
|
|
129
|
+
// Failure mode conciseness (max 200 chars per scenario)
|
|
130
|
+
for (const fm of journey.failureModes) {
|
|
131
|
+
if (fm.scenario.length > 200) {
|
|
132
|
+
errors.push(`Journey ${journey.id} failure mode too long (${fm.scenario.length} chars, max 200): "${fm.scenario.substring(0, 50)}..."`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// Acceptance test conciseness (max 200 chars, single statement)
|
|
136
|
+
for (const at of journey.acceptanceTests) {
|
|
137
|
+
if (at.length > 200) {
|
|
138
|
+
errors.push(`Journey ${journey.id} acceptance test too long (${at.length} chars, max 200): "${at.substring(0, 50)}..."`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// Preconditions non-empty
|
|
142
|
+
if (journey.preconditions.length === 0) {
|
|
143
|
+
errors.push(`Journey ${journey.id} has empty preconditions`);
|
|
144
|
+
}
|
|
145
|
+
// Success non-empty
|
|
146
|
+
if (!journey.success || journey.success.trim() === '') {
|
|
147
|
+
errors.push(`Journey ${journey.id} has empty success outcome`);
|
|
148
|
+
}
|
|
149
|
+
return { valid: errors.length === 0, errors };
|
|
150
|
+
}
|
|
151
|
+
// ─── FR Extraction ──────────────────────────────────────────────────────────
|
|
152
|
+
/**
|
|
153
|
+
* Format an FR ID: FR-001, FR-002, etc.
|
|
154
|
+
*/
|
|
155
|
+
function formatFRId(frNumber) {
|
|
156
|
+
return (0, parse_js_1.normalizeId)(`FR-${frNumber}`);
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Extract FRs from journeys. Each journey step maps to at least 1 FR.
|
|
160
|
+
* Populates bidirectional cross-links:
|
|
161
|
+
* - Each step.frIds gets the generated FR IDs
|
|
162
|
+
* - Each journey.linkedFRs gets all FR IDs from its steps
|
|
163
|
+
* - Each FR.sourceJourneys references the parent journey
|
|
164
|
+
* - Each FR.sourceSteps references "J-XXX step N"
|
|
165
|
+
*
|
|
166
|
+
* Duplicate step descriptions merge into a single FR with multiple sources.
|
|
167
|
+
*/
|
|
168
|
+
function extractFRsFromJourneys(journeys) {
|
|
169
|
+
let frCounter = 1;
|
|
170
|
+
const frMap = new Map();
|
|
171
|
+
/** Map from normalized description to FR ID for deduplication */
|
|
172
|
+
const descriptionToFrId = new Map();
|
|
173
|
+
for (const journey of journeys) {
|
|
174
|
+
for (const step of journey.steps) {
|
|
175
|
+
const normalizedDesc = step.action.trim().toLowerCase();
|
|
176
|
+
if (descriptionToFrId.has(normalizedDesc)) {
|
|
177
|
+
// Merge: add this journey/step as additional source
|
|
178
|
+
const existingFrId = descriptionToFrId.get(normalizedDesc);
|
|
179
|
+
const existingFr = frMap.get(existingFrId);
|
|
180
|
+
if (!existingFr.sourceJourneys.includes(journey.id)) {
|
|
181
|
+
existingFr.sourceJourneys.push(journey.id);
|
|
182
|
+
}
|
|
183
|
+
existingFr.sourceSteps.push(`${journey.id} step ${step.number}`);
|
|
184
|
+
step.frIds.push(existingFrId);
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
// New FR
|
|
188
|
+
const frId = formatFRId(frCounter);
|
|
189
|
+
frCounter++;
|
|
190
|
+
const fr = {
|
|
191
|
+
id: frId,
|
|
192
|
+
description: step.action,
|
|
193
|
+
scope: assignScope(journey.type),
|
|
194
|
+
priority: assignPriority(journey.type),
|
|
195
|
+
sourceJourneys: [journey.id],
|
|
196
|
+
sourceSteps: [`${journey.id} step ${step.number}`],
|
|
197
|
+
};
|
|
198
|
+
frMap.set(frId, fr);
|
|
199
|
+
descriptionToFrId.set(normalizedDesc, frId);
|
|
200
|
+
step.frIds.push(frId);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// Populate journey.linkedFRs from step frIds
|
|
204
|
+
const journeyFrIds = new Set();
|
|
205
|
+
for (const step of journey.steps) {
|
|
206
|
+
for (const frId of step.frIds) {
|
|
207
|
+
journeyFrIds.add(frId);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
journey.linkedFRs = Array.from(journeyFrIds).sort();
|
|
211
|
+
}
|
|
212
|
+
// Return sorted by FR ID
|
|
213
|
+
return Array.from(frMap.values()).sort((a, b) => {
|
|
214
|
+
const aNum = parseInt(a.id.split('-')[1], 10);
|
|
215
|
+
const bNum = parseInt(b.id.split('-')[1], 10);
|
|
216
|
+
return aNum - bNum;
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
// ─── Traceability ───────────────────────────────────────────────────────────
|
|
220
|
+
/**
|
|
221
|
+
* Build traceability map for SPEC.md traceability table.
|
|
222
|
+
* Each entry maps an FR to its source journeys.
|
|
223
|
+
*/
|
|
224
|
+
function buildTraceabilityMap(frs) {
|
|
225
|
+
return frs.map((fr) => ({
|
|
226
|
+
frId: fr.id,
|
|
227
|
+
description: fr.description,
|
|
228
|
+
journeyRefs: fr.sourceSteps.map((stepRef) => {
|
|
229
|
+
// Convert "J-001 step 3" to "J-001 (step 3)"
|
|
230
|
+
const match = stepRef.match(/^(J-\d+)\s+step\s+(\d+)$/);
|
|
231
|
+
if (match) {
|
|
232
|
+
return `${match[1]} (step ${match[2]})`;
|
|
233
|
+
}
|
|
234
|
+
return stepRef;
|
|
235
|
+
}),
|
|
236
|
+
}));
|
|
237
|
+
}
|
|
238
|
+
// ─── Coverage Validation ────────────────────────────────────────────────────
|
|
239
|
+
/**
|
|
240
|
+
* Validate FR coverage: every journey step has FRs, every FR has sources.
|
|
241
|
+
*/
|
|
242
|
+
function validateFRCoverage(journeys, frs) {
|
|
243
|
+
const errors = [];
|
|
244
|
+
// Every journey step must have at least 1 FR
|
|
245
|
+
for (const journey of journeys) {
|
|
246
|
+
for (const step of journey.steps) {
|
|
247
|
+
if (step.frIds.length === 0) {
|
|
248
|
+
errors.push(`Journey ${journey.id} step ${step.number} has no FR coverage`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
// Every FR must have at least 1 source journey
|
|
253
|
+
for (const fr of frs) {
|
|
254
|
+
if (fr.sourceJourneys.length === 0) {
|
|
255
|
+
errors.push(`FR ${fr.id} has no source journey (orphan FR)`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
// All FR IDs use correct format
|
|
259
|
+
for (const fr of frs) {
|
|
260
|
+
if (!/^FR-\d{3,}$/.test(fr.id)) {
|
|
261
|
+
errors.push(`Invalid FR ID format: ${fr.id} (expected FR-XXX)`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
// All scope tags are valid
|
|
265
|
+
const validScopes = ['v1', 'v2', 'out'];
|
|
266
|
+
for (const fr of frs) {
|
|
267
|
+
if (!validScopes.includes(fr.scope)) {
|
|
268
|
+
errors.push(`FR ${fr.id} has invalid scope: ${fr.scope}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
// All priority tags are valid
|
|
272
|
+
const validPriorities = ['P0', 'P1', 'P2'];
|
|
273
|
+
for (const fr of frs) {
|
|
274
|
+
if (!validPriorities.includes(fr.priority)) {
|
|
275
|
+
errors.push(`FR ${fr.id} has invalid priority: ${fr.priority}`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return { valid: errors.length === 0, errors };
|
|
279
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GSWD Specify NFR Module — Rule-based non-functional requirement generation
|
|
3
|
+
*
|
|
4
|
+
* Generates NFRs across 4 required categories: security, privacy, performance, observability.
|
|
5
|
+
* Thresholds calibrated for v1 MVP — conservative and achievable.
|
|
6
|
+
*
|
|
7
|
+
* Schema: GSWD_SPEC.md Section 6.1 (ID formats), 8.3 (specify workflow step 4)
|
|
8
|
+
*/
|
|
9
|
+
import type { FunctionalRequirement } from './specify-journeys.js';
|
|
10
|
+
export type NFRCategory = 'security' | 'privacy' | 'performance' | 'observability';
|
|
11
|
+
export interface NonFunctionalRequirement {
|
|
12
|
+
/** NFR ID in NFR-001 format */
|
|
13
|
+
id: string;
|
|
14
|
+
/** Category: security, privacy, performance, or observability */
|
|
15
|
+
category: NFRCategory;
|
|
16
|
+
/** What this NFR requires */
|
|
17
|
+
description: string;
|
|
18
|
+
/** Measurable threshold or acceptance criteria */
|
|
19
|
+
threshold: string;
|
|
20
|
+
/** FR-XXX IDs this NFR applies to */
|
|
21
|
+
linkedFRs: string[];
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The 4 required NFR categories from GSWD_SPEC Section 8.3.
|
|
25
|
+
*/
|
|
26
|
+
export declare const NFR_CATEGORIES: {
|
|
27
|
+
category: NFRCategory;
|
|
28
|
+
description: string;
|
|
29
|
+
}[];
|
|
30
|
+
/**
|
|
31
|
+
* Generate NFRs from FR list across all 4 required categories.
|
|
32
|
+
* Rule-based: each category produces a standard set of NFRs.
|
|
33
|
+
* Thresholds calibrated for v1 MVP.
|
|
34
|
+
*
|
|
35
|
+
* @param frs - Functional requirements to link NFRs to
|
|
36
|
+
* @param decisionsContext - Optional DECISIONS.md content for calibration
|
|
37
|
+
*/
|
|
38
|
+
export declare function generateNFRs(frs: FunctionalRequirement[], decisionsContext?: string): NonFunctionalRequirement[];
|
|
39
|
+
/**
|
|
40
|
+
* Validate NFRs: all 4 categories represented, IDs correct, fields non-empty.
|
|
41
|
+
*/
|
|
42
|
+
export declare function validateNFRs(nfrs: NonFunctionalRequirement[]): {
|
|
43
|
+
valid: boolean;
|
|
44
|
+
errors: string[];
|
|
45
|
+
};
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* GSWD Specify NFR Module — Rule-based non-functional requirement generation
|
|
4
|
+
*
|
|
5
|
+
* Generates NFRs across 4 required categories: security, privacy, performance, observability.
|
|
6
|
+
* Thresholds calibrated for v1 MVP — conservative and achievable.
|
|
7
|
+
*
|
|
8
|
+
* Schema: GSWD_SPEC.md Section 6.1 (ID formats), 8.3 (specify workflow step 4)
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.NFR_CATEGORIES = void 0;
|
|
12
|
+
exports.generateNFRs = generateNFRs;
|
|
13
|
+
exports.validateNFRs = validateNFRs;
|
|
14
|
+
const parse_js_1 = require("./parse.js");
|
|
15
|
+
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
16
|
+
/**
|
|
17
|
+
* The 4 required NFR categories from GSWD_SPEC Section 8.3.
|
|
18
|
+
*/
|
|
19
|
+
exports.NFR_CATEGORIES = [
|
|
20
|
+
{ category: 'security', description: 'Input validation, authentication, authorization' },
|
|
21
|
+
{ category: 'privacy', description: 'Data handling, user consent, PII protection' },
|
|
22
|
+
{ category: 'performance', description: 'Response time, throughput, resource usage' },
|
|
23
|
+
{ category: 'observability', description: 'Logging, error tracking, health checks' },
|
|
24
|
+
];
|
|
25
|
+
// ─── NFR Generation ─────────────────────────────────────────────────────────
|
|
26
|
+
/**
|
|
27
|
+
* Format an NFR ID: NFR-001, NFR-002, etc.
|
|
28
|
+
*/
|
|
29
|
+
function formatNFRId(nfrNumber) {
|
|
30
|
+
return (0, parse_js_1.normalizeId)(`NFR-${nfrNumber}`);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Generate NFRs from FR list across all 4 required categories.
|
|
34
|
+
* Rule-based: each category produces a standard set of NFRs.
|
|
35
|
+
* Thresholds calibrated for v1 MVP.
|
|
36
|
+
*
|
|
37
|
+
* @param frs - Functional requirements to link NFRs to
|
|
38
|
+
* @param decisionsContext - Optional DECISIONS.md content for calibration
|
|
39
|
+
*/
|
|
40
|
+
function generateNFRs(frs, decisionsContext) {
|
|
41
|
+
const nfrs = [];
|
|
42
|
+
let counter = 1;
|
|
43
|
+
const v1FrIds = frs.filter((fr) => fr.scope === 'v1').map((fr) => fr.id);
|
|
44
|
+
const p0FrIds = frs.filter((fr) => fr.priority === 'P0').map((fr) => fr.id);
|
|
45
|
+
// ─── Security NFRs ───────────────────────────────────────────────────────
|
|
46
|
+
nfrs.push({
|
|
47
|
+
id: formatNFRId(counter++),
|
|
48
|
+
category: 'security',
|
|
49
|
+
description: 'All user-facing input fields must be validated before processing',
|
|
50
|
+
threshold: 'No unvalidated user input reaches data store or business logic',
|
|
51
|
+
linkedFRs: v1FrIds.length > 0 ? v1FrIds : [],
|
|
52
|
+
});
|
|
53
|
+
nfrs.push({
|
|
54
|
+
id: formatNFRId(counter++),
|
|
55
|
+
category: 'security',
|
|
56
|
+
description: 'All authenticated operations must verify session validity',
|
|
57
|
+
threshold: 'Unauthorized access returns 401/403 within 100ms',
|
|
58
|
+
linkedFRs: p0FrIds.length > 0 ? p0FrIds : [],
|
|
59
|
+
});
|
|
60
|
+
nfrs.push({
|
|
61
|
+
id: formatNFRId(counter++),
|
|
62
|
+
category: 'security',
|
|
63
|
+
description: 'Operations must enforce role-based access when roles are defined',
|
|
64
|
+
threshold: 'Users can only access resources they own or are authorized to view',
|
|
65
|
+
linkedFRs: v1FrIds.length > 0 ? v1FrIds : [],
|
|
66
|
+
});
|
|
67
|
+
// ─── Privacy NFRs ────────────────────────────────────────────────────────
|
|
68
|
+
nfrs.push({
|
|
69
|
+
id: formatNFRId(counter++),
|
|
70
|
+
category: 'privacy',
|
|
71
|
+
description: 'User data must not be logged in plain text or exposed in error messages',
|
|
72
|
+
threshold: 'No PII appears in application logs or stack traces',
|
|
73
|
+
linkedFRs: v1FrIds.length > 0 ? v1FrIds : [],
|
|
74
|
+
});
|
|
75
|
+
nfrs.push({
|
|
76
|
+
id: formatNFRId(counter++),
|
|
77
|
+
category: 'privacy',
|
|
78
|
+
description: 'User data collection requires explicit consent where applicable',
|
|
79
|
+
threshold: 'No data stored without user-initiated action',
|
|
80
|
+
linkedFRs: v1FrIds.length > 0 ? v1FrIds : [],
|
|
81
|
+
});
|
|
82
|
+
// ─── Performance NFRs ────────────────────────────────────────────────────
|
|
83
|
+
nfrs.push({
|
|
84
|
+
id: formatNFRId(counter++),
|
|
85
|
+
category: 'performance',
|
|
86
|
+
description: 'Core user actions must complete within acceptable time',
|
|
87
|
+
threshold: 'P95 response time < 2s for core actions, < 5s for complex operations',
|
|
88
|
+
linkedFRs: p0FrIds.length > 0 ? p0FrIds : [],
|
|
89
|
+
});
|
|
90
|
+
nfrs.push({
|
|
91
|
+
id: formatNFRId(counter++),
|
|
92
|
+
category: 'performance',
|
|
93
|
+
description: 'System must handle expected v1 concurrent load',
|
|
94
|
+
threshold: 'Supports 100 concurrent users without degradation',
|
|
95
|
+
linkedFRs: v1FrIds.length > 0 ? v1FrIds : [],
|
|
96
|
+
});
|
|
97
|
+
// ─── Observability NFRs ──────────────────────────────────────────────────
|
|
98
|
+
nfrs.push({
|
|
99
|
+
id: formatNFRId(counter++),
|
|
100
|
+
category: 'observability',
|
|
101
|
+
description: 'All errors must be captured with context for debugging',
|
|
102
|
+
threshold: 'Errors include timestamp, user context (anonymized), stack trace, and request ID',
|
|
103
|
+
linkedFRs: v1FrIds.length > 0 ? v1FrIds : [],
|
|
104
|
+
});
|
|
105
|
+
nfrs.push({
|
|
106
|
+
id: formatNFRId(counter++),
|
|
107
|
+
category: 'observability',
|
|
108
|
+
description: 'System health must be verifiable',
|
|
109
|
+
threshold: 'Health endpoint returns status within 1s',
|
|
110
|
+
linkedFRs: [],
|
|
111
|
+
});
|
|
112
|
+
nfrs.push({
|
|
113
|
+
id: formatNFRId(counter++),
|
|
114
|
+
category: 'observability',
|
|
115
|
+
description: 'Key operations must produce structured logs',
|
|
116
|
+
threshold: 'All state-changing operations produce at least one log entry',
|
|
117
|
+
linkedFRs: v1FrIds.length > 0 ? v1FrIds : [],
|
|
118
|
+
});
|
|
119
|
+
return nfrs;
|
|
120
|
+
}
|
|
121
|
+
// ─── NFR Validation ─────────────────────────────────────────────────────────
|
|
122
|
+
/**
|
|
123
|
+
* Validate NFRs: all 4 categories represented, IDs correct, fields non-empty.
|
|
124
|
+
*/
|
|
125
|
+
function validateNFRs(nfrs) {
|
|
126
|
+
const errors = [];
|
|
127
|
+
const categoriesPresent = new Set();
|
|
128
|
+
for (const nfr of nfrs) {
|
|
129
|
+
categoriesPresent.add(nfr.category);
|
|
130
|
+
// ID format check
|
|
131
|
+
if (!/^NFR-\d{3,}$/.test(nfr.id)) {
|
|
132
|
+
errors.push(`Invalid NFR ID format: ${nfr.id} (expected NFR-XXX)`);
|
|
133
|
+
}
|
|
134
|
+
// Non-empty description
|
|
135
|
+
if (!nfr.description || nfr.description.trim() === '') {
|
|
136
|
+
errors.push(`NFR ${nfr.id} has empty description`);
|
|
137
|
+
}
|
|
138
|
+
// Non-empty threshold
|
|
139
|
+
if (!nfr.threshold || nfr.threshold.trim() === '') {
|
|
140
|
+
errors.push(`NFR ${nfr.id} has empty threshold`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// All 4 categories represented
|
|
144
|
+
const requiredCategories = ['security', 'privacy', 'performance', 'observability'];
|
|
145
|
+
for (const category of requiredCategories) {
|
|
146
|
+
if (!categoriesPresent.has(category)) {
|
|
147
|
+
errors.push(`Missing required NFR category: ${category}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// Sequential IDs (warning-level, but include in errors for strict validation)
|
|
151
|
+
for (let i = 0; i < nfrs.length; i++) {
|
|
152
|
+
const expectedId = formatNFRId(i + 1);
|
|
153
|
+
if (nfrs[i].id !== expectedId) {
|
|
154
|
+
errors.push(`Non-sequential NFR ID: expected ${expectedId}, got ${nfrs[i].id}`);
|
|
155
|
+
break; // Only report first gap
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return { valid: errors.length === 0, errors };
|
|
159
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GSWD Specify Roles Module — Roles & permissions checkpoint
|
|
3
|
+
*
|
|
4
|
+
* Collects roles and permissions from the founder before journey mapping.
|
|
5
|
+
* In --auto mode, returns sensible defaults (single admin for v1 MVP).
|
|
6
|
+
*
|
|
7
|
+
* Schema: GSWD_SPEC.md Section 8.3 step 1
|
|
8
|
+
*/
|
|
9
|
+
export interface Role {
|
|
10
|
+
/** Role name (e.g., 'admin', 'viewer') */
|
|
11
|
+
name: string;
|
|
12
|
+
/** What this role can do */
|
|
13
|
+
description: string;
|
|
14
|
+
/** High-level permission descriptions */
|
|
15
|
+
permissions: string[];
|
|
16
|
+
}
|
|
17
|
+
export interface RolesConfig {
|
|
18
|
+
/** Defined roles */
|
|
19
|
+
roles: Role[];
|
|
20
|
+
/** Permission model type */
|
|
21
|
+
permissionModel: 'simple' | 'rbac' | 'abac';
|
|
22
|
+
/** Additional notes about the roles setup */
|
|
23
|
+
notes: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Sensible v1 MVP default: single admin role with full permissions.
|
|
27
|
+
*/
|
|
28
|
+
export declare function defaultRolesConfig(): RolesConfig;
|
|
29
|
+
export interface CollectRolesOptions {
|
|
30
|
+
/** If true, skip checkpoint and return defaults */
|
|
31
|
+
auto: boolean;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Collect roles & permissions configuration.
|
|
35
|
+
*
|
|
36
|
+
* In auto mode: returns default config immediately (no interaction).
|
|
37
|
+
* In interactive mode: would render checkpoint box for user input.
|
|
38
|
+
* (Interactive mode deferred to Phase 6 — auto is the primary path for now.)
|
|
39
|
+
*/
|
|
40
|
+
export declare function collectRoles(options: CollectRolesOptions): Promise<RolesConfig>;
|
|
41
|
+
/**
|
|
42
|
+
* Format RolesConfig for insertion into SPEC.md "## Roles & Permissions" section.
|
|
43
|
+
*
|
|
44
|
+
* Produces a markdown table of roles with permissions.
|
|
45
|
+
*/
|
|
46
|
+
export declare function formatRolesForSpec(config: RolesConfig): string;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* GSWD Specify Roles Module — Roles & permissions checkpoint
|
|
4
|
+
*
|
|
5
|
+
* Collects roles and permissions from the founder before journey mapping.
|
|
6
|
+
* In --auto mode, returns sensible defaults (single admin for v1 MVP).
|
|
7
|
+
*
|
|
8
|
+
* Schema: GSWD_SPEC.md Section 8.3 step 1
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.defaultRolesConfig = defaultRolesConfig;
|
|
12
|
+
exports.collectRoles = collectRoles;
|
|
13
|
+
exports.formatRolesForSpec = formatRolesForSpec;
|
|
14
|
+
// ─── Default Configuration ───────────────────────────────────────────────────
|
|
15
|
+
/**
|
|
16
|
+
* Sensible v1 MVP default: single admin role with full permissions.
|
|
17
|
+
*/
|
|
18
|
+
function defaultRolesConfig() {
|
|
19
|
+
return {
|
|
20
|
+
roles: [
|
|
21
|
+
{
|
|
22
|
+
name: 'admin',
|
|
23
|
+
description: 'Full access to all features and settings',
|
|
24
|
+
permissions: ['create', 'read', 'update', 'delete', 'manage_settings'],
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
permissionModel: 'simple',
|
|
28
|
+
notes: 'Single admin role for v1 MVP — expand when multi-user is needed',
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Collect roles & permissions configuration.
|
|
33
|
+
*
|
|
34
|
+
* In auto mode: returns default config immediately (no interaction).
|
|
35
|
+
* In interactive mode: would render checkpoint box for user input.
|
|
36
|
+
* (Interactive mode deferred to Phase 6 — auto is the primary path for now.)
|
|
37
|
+
*/
|
|
38
|
+
async function collectRoles(options) {
|
|
39
|
+
if (options.auto) {
|
|
40
|
+
return defaultRolesConfig();
|
|
41
|
+
}
|
|
42
|
+
// Interactive mode: return default with note about deferral
|
|
43
|
+
// Full interactive collection deferred to Phase 6 (Bootstrap + Auto)
|
|
44
|
+
return {
|
|
45
|
+
...defaultRolesConfig(),
|
|
46
|
+
notes: 'Deferred — using admin-only for now',
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
// ─── Formatting ──────────────────────────────────────────────────────────────
|
|
50
|
+
/**
|
|
51
|
+
* Format RolesConfig for insertion into SPEC.md "## Roles & Permissions" section.
|
|
52
|
+
*
|
|
53
|
+
* Produces a markdown table of roles with permissions.
|
|
54
|
+
*/
|
|
55
|
+
function formatRolesForSpec(config) {
|
|
56
|
+
const lines = [];
|
|
57
|
+
// Permission model description
|
|
58
|
+
lines.push(`**Permission Model:** ${formatPermissionModel(config.permissionModel)}`);
|
|
59
|
+
lines.push('');
|
|
60
|
+
// Roles table
|
|
61
|
+
lines.push('| Role | Description | Permissions |');
|
|
62
|
+
lines.push('|------|-------------|-------------|');
|
|
63
|
+
for (const role of config.roles) {
|
|
64
|
+
const perms = role.permissions.join(', ');
|
|
65
|
+
lines.push(`| ${role.name} | ${role.description} | ${perms} |`);
|
|
66
|
+
}
|
|
67
|
+
// Notes
|
|
68
|
+
if (config.notes) {
|
|
69
|
+
lines.push('');
|
|
70
|
+
lines.push(`> ${config.notes}`);
|
|
71
|
+
}
|
|
72
|
+
return lines.join('\n');
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Format permission model name for display.
|
|
76
|
+
*/
|
|
77
|
+
function formatPermissionModel(model) {
|
|
78
|
+
switch (model) {
|
|
79
|
+
case 'simple':
|
|
80
|
+
return 'Simple (single role or flat permissions)';
|
|
81
|
+
case 'rbac':
|
|
82
|
+
return 'RBAC (Role-Based Access Control)';
|
|
83
|
+
case 'abac':
|
|
84
|
+
return 'ABAC (Attribute-Based Access Control)';
|
|
85
|
+
default:
|
|
86
|
+
return model;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GSWD Specify Workflow Orchestrator
|
|
3
|
+
*
|
|
4
|
+
* Full pipeline: roles checkpoint -> journey mapping -> FR extraction ->
|
|
5
|
+
* NFR generation -> architecture + integrations -> write artifacts -> state
|
|
6
|
+
* Implements GSWD_SPEC Section 8.3 end-to-end.
|
|
7
|
+
*
|
|
8
|
+
* Both interactive and auto modes are supported:
|
|
9
|
+
* - Interactive: presents checkpoints for roles, journey review, FR confirmation
|
|
10
|
+
* - Auto: uses defaults, skips reviews, auto-defers paid integrations
|
|
11
|
+
*
|
|
12
|
+
* Schema: GSWD_SPEC.md Section 8.3
|
|
13
|
+
*/
|
|
14
|
+
import type { Journey } from './specify-journeys.js';
|
|
15
|
+
import type { SpawnFn } from './specify-agents.js';
|
|
16
|
+
export interface SpecifyOptions {
|
|
17
|
+
/** Use auto policy for all checkpoints */
|
|
18
|
+
auto: boolean;
|
|
19
|
+
/** Resume from last checkpoint */
|
|
20
|
+
resume?: boolean;
|
|
21
|
+
/** Integration budget override */
|
|
22
|
+
integrationBudget?: number;
|
|
23
|
+
/** Override .planning/ path (for testing) */
|
|
24
|
+
planningDir?: string;
|
|
25
|
+
/** Override config.json path (for testing) */
|
|
26
|
+
configPath?: string;
|
|
27
|
+
/** Override templates directory path (for testing) */
|
|
28
|
+
templatesDir?: string;
|
|
29
|
+
/** Task() wrapper for agent spawning (injectable for testing) */
|
|
30
|
+
spawnFn?: SpawnFn;
|
|
31
|
+
/** Skip agent spawning, use provided journeys directly (for testing) */
|
|
32
|
+
skipAgents?: boolean;
|
|
33
|
+
/** Pre-built journeys (for testing or resume) */
|
|
34
|
+
providedJourneys?: Journey[];
|
|
35
|
+
/** Pre-built architecture content (for testing) */
|
|
36
|
+
providedArchitecture?: string;
|
|
37
|
+
/** Pre-built integrations content (for testing) */
|
|
38
|
+
providedIntegrations?: string;
|
|
39
|
+
}
|
|
40
|
+
export interface SpecifyResult {
|
|
41
|
+
/** Overall status */
|
|
42
|
+
status: 'complete' | 'failed' | 'interrupted';
|
|
43
|
+
/** File paths of written artifacts */
|
|
44
|
+
artifacts: string[];
|
|
45
|
+
/** Count of journeys generated */
|
|
46
|
+
journeyCount: number;
|
|
47
|
+
/** Count of FRs extracted */
|
|
48
|
+
frCount: number;
|
|
49
|
+
/** Count of NFRs generated */
|
|
50
|
+
nfrCount: number;
|
|
51
|
+
/** Count of integrations */
|
|
52
|
+
integrationCount: number;
|
|
53
|
+
/** Count of architecture components */
|
|
54
|
+
componentCount: number;
|
|
55
|
+
/** Error messages if any */
|
|
56
|
+
errors?: string[];
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Run the Specify workflow end-to-end.
|
|
60
|
+
*
|
|
61
|
+
* Implements GSWD_SPEC Section 8.3:
|
|
62
|
+
* 1. Roles & Permissions checkpoint
|
|
63
|
+
* 2. Journey mapping
|
|
64
|
+
* 3. FR extraction
|
|
65
|
+
* 4. NFR generation
|
|
66
|
+
* 5. Architecture + Integrations (parallel agents)
|
|
67
|
+
* 6. Write 5 artifacts
|
|
68
|
+
* 7. Update state
|
|
69
|
+
*/
|
|
70
|
+
export declare function runSpecify(options: SpecifyOptions): Promise<SpecifyResult>;
|