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,1579 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Audit module tests — coverage matrix, checks, report generation, auto-fix, gate
|
|
4
|
+
*/
|
|
5
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
6
|
+
if (k2 === undefined) k2 = k;
|
|
7
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
8
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
9
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
10
|
+
}
|
|
11
|
+
Object.defineProperty(o, k2, desc);
|
|
12
|
+
}) : (function(o, m, k, k2) {
|
|
13
|
+
if (k2 === undefined) k2 = k;
|
|
14
|
+
o[k2] = m[k];
|
|
15
|
+
}));
|
|
16
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
17
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
18
|
+
}) : function(o, v) {
|
|
19
|
+
o["default"] = v;
|
|
20
|
+
});
|
|
21
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
22
|
+
var ownKeys = function(o) {
|
|
23
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
24
|
+
var ar = [];
|
|
25
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
26
|
+
return ar;
|
|
27
|
+
};
|
|
28
|
+
return ownKeys(o);
|
|
29
|
+
};
|
|
30
|
+
return function (mod) {
|
|
31
|
+
if (mod && mod.__esModule) return mod;
|
|
32
|
+
var result = {};
|
|
33
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
34
|
+
__setModuleDefault(result, mod);
|
|
35
|
+
return result;
|
|
36
|
+
};
|
|
37
|
+
})();
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
const node_test_1 = require("node:test");
|
|
40
|
+
const assert = __importStar(require("node:assert"));
|
|
41
|
+
const fs = __importStar(require("node:fs"));
|
|
42
|
+
const os = __importStar(require("node:os"));
|
|
43
|
+
const path = __importStar(require("node:path"));
|
|
44
|
+
const audit_js_1 = require("../lib/audit.js");
|
|
45
|
+
const parse_js_1 = require("../lib/parse.js");
|
|
46
|
+
const state_js_1 = require("../lib/state.js");
|
|
47
|
+
// ─── Test Fixtures ──────────────────────────────────────────────────────────
|
|
48
|
+
const MINIMAL_JOURNEYS = `## Journeys
|
|
49
|
+
|
|
50
|
+
### J-001: Onboarding
|
|
51
|
+
User onboards to the system.
|
|
52
|
+
References FR-001 and FR-002.
|
|
53
|
+
|
|
54
|
+
#### Acceptance Tests
|
|
55
|
+
- User can complete onboarding flow
|
|
56
|
+
- User sees welcome message after onboarding
|
|
57
|
+
|
|
58
|
+
### J-002: Core Action
|
|
59
|
+
User performs the core action.
|
|
60
|
+
References FR-003.
|
|
61
|
+
Uses I-001 for external data.
|
|
62
|
+
|
|
63
|
+
#### Acceptance Tests
|
|
64
|
+
- User can perform core action successfully
|
|
65
|
+
`;
|
|
66
|
+
const MINIMAL_SPEC = `## Roles & Permissions
|
|
67
|
+
Admin role only.
|
|
68
|
+
|
|
69
|
+
## Functional Requirements
|
|
70
|
+
|
|
71
|
+
### FR-001: Create account
|
|
72
|
+
**Scope:** v1
|
|
73
|
+
**Priority:** P0
|
|
74
|
+
|
|
75
|
+
### FR-002: Set profile
|
|
76
|
+
**Scope:** v1
|
|
77
|
+
**Priority:** P1
|
|
78
|
+
|
|
79
|
+
### FR-003: Perform action
|
|
80
|
+
**Scope:** v1
|
|
81
|
+
**Priority:** P0
|
|
82
|
+
|
|
83
|
+
### FR-004: View history
|
|
84
|
+
**Scope:** v2
|
|
85
|
+
**Priority:** P2
|
|
86
|
+
|
|
87
|
+
## Acceptance Criteria
|
|
88
|
+
All tests must pass.
|
|
89
|
+
`;
|
|
90
|
+
const MINIMAL_NFR = `## Non-Functional Requirements
|
|
91
|
+
|
|
92
|
+
### NFR-001: Response time
|
|
93
|
+
Response time under 200ms.
|
|
94
|
+
`;
|
|
95
|
+
const MINIMAL_INTEGRATIONS = `## Integrations
|
|
96
|
+
|
|
97
|
+
### I-001: External API
|
|
98
|
+
**Status:** approved
|
|
99
|
+
**Fallback:** None needed
|
|
100
|
+
`;
|
|
101
|
+
// ─── extractArtifactIds ─────────────────────────────────────────────────────
|
|
102
|
+
(0, node_test_1.describe)('extractArtifactIds', () => {
|
|
103
|
+
(0, node_test_1.it)('extracts J-001 from journey heading content', () => {
|
|
104
|
+
const result = (0, audit_js_1.extractArtifactIds)('### J-001: Onboarding');
|
|
105
|
+
assert.strictEqual(result.journeyIds.length, 1);
|
|
106
|
+
assert.strictEqual(result.journeyIds[0].id, 'J-001');
|
|
107
|
+
});
|
|
108
|
+
(0, node_test_1.it)('extracts FR-001, FR-002 from spec content with mixed IDs', () => {
|
|
109
|
+
const result = (0, audit_js_1.extractArtifactIds)('FR-001 and FR-002 with J-001');
|
|
110
|
+
assert.strictEqual(result.frIds.length, 2);
|
|
111
|
+
assert.strictEqual(result.frIds[0].id, 'FR-001');
|
|
112
|
+
assert.strictEqual(result.frIds[1].id, 'FR-002');
|
|
113
|
+
assert.strictEqual(result.journeyIds.length, 1);
|
|
114
|
+
});
|
|
115
|
+
(0, node_test_1.it)('returns empty arrays for empty content', () => {
|
|
116
|
+
const result = (0, audit_js_1.extractArtifactIds)('');
|
|
117
|
+
assert.strictEqual(result.journeyIds.length, 0);
|
|
118
|
+
assert.strictEqual(result.frIds.length, 0);
|
|
119
|
+
assert.strictEqual(result.nfrIds.length, 0);
|
|
120
|
+
assert.strictEqual(result.integrationIds.length, 0);
|
|
121
|
+
});
|
|
122
|
+
(0, node_test_1.it)('handles content with no IDs', () => {
|
|
123
|
+
const result = (0, audit_js_1.extractArtifactIds)('This has no IDs at all.');
|
|
124
|
+
assert.strictEqual(result.journeyIds.length, 0);
|
|
125
|
+
assert.strictEqual(result.frIds.length, 0);
|
|
126
|
+
});
|
|
127
|
+
(0, node_test_1.it)('extracts NFR and I IDs correctly', () => {
|
|
128
|
+
const result = (0, audit_js_1.extractArtifactIds)('NFR-001 and I-002');
|
|
129
|
+
assert.strictEqual(result.nfrIds.length, 1);
|
|
130
|
+
assert.strictEqual(result.nfrIds[0].id, 'NFR-001');
|
|
131
|
+
assert.strictEqual(result.integrationIds.length, 1);
|
|
132
|
+
assert.strictEqual(result.integrationIds[0].id, 'I-002');
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
// ─── parseJourneySections ───────────────────────────────────────────────────
|
|
136
|
+
(0, node_test_1.describe)('parseJourneySections', () => {
|
|
137
|
+
(0, node_test_1.it)('splits 2 journeys into 2 sections', () => {
|
|
138
|
+
const content = `## Journeys
|
|
139
|
+
### J-001: First
|
|
140
|
+
Content of first journey.
|
|
141
|
+
### J-002: Second
|
|
142
|
+
Content of second journey.`;
|
|
143
|
+
const sections = (0, audit_js_1.parseJourneySections)(content);
|
|
144
|
+
assert.strictEqual(sections.size, 2);
|
|
145
|
+
assert.ok(sections.has('J-001'));
|
|
146
|
+
assert.ok(sections.has('J-002'));
|
|
147
|
+
});
|
|
148
|
+
(0, node_test_1.it)('each section contains only content between headings', () => {
|
|
149
|
+
const content = `### J-001: First
|
|
150
|
+
First content here.
|
|
151
|
+
### J-002: Second
|
|
152
|
+
Second content here.`;
|
|
153
|
+
const sections = (0, audit_js_1.parseJourneySections)(content);
|
|
154
|
+
assert.ok(sections.get('J-001').includes('First content'));
|
|
155
|
+
assert.ok(!sections.get('J-001').includes('Second content'));
|
|
156
|
+
assert.ok(sections.get('J-002').includes('Second content'));
|
|
157
|
+
});
|
|
158
|
+
(0, node_test_1.it)('returns empty map for content without journey headings', () => {
|
|
159
|
+
const sections = (0, audit_js_1.parseJourneySections)('No journeys here.');
|
|
160
|
+
assert.strictEqual(sections.size, 0);
|
|
161
|
+
});
|
|
162
|
+
(0, node_test_1.it)('returns empty map for empty string', () => {
|
|
163
|
+
const sections = (0, audit_js_1.parseJourneySections)('');
|
|
164
|
+
assert.strictEqual(sections.size, 0);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
// ─── parseFRScope ───────────────────────────────────────────────────────────
|
|
168
|
+
(0, node_test_1.describe)('parseFRScope', () => {
|
|
169
|
+
(0, node_test_1.it)('extracts "v1" scope from "**Scope:** v1" pattern', () => {
|
|
170
|
+
const content = `### FR-001: Test\n**Scope:** v1\n**Priority:** P0`;
|
|
171
|
+
const scopes = (0, audit_js_1.parseFRScope)(content);
|
|
172
|
+
assert.strictEqual(scopes.get('FR-001'), 'v1');
|
|
173
|
+
});
|
|
174
|
+
(0, node_test_1.it)('extracts "v2" from "Scope: v2" pattern', () => {
|
|
175
|
+
const content = `### FR-001: Test\nScope: v2\nPriority: P2`;
|
|
176
|
+
const scopes = (0, audit_js_1.parseFRScope)(content);
|
|
177
|
+
assert.strictEqual(scopes.get('FR-001'), 'v2');
|
|
178
|
+
});
|
|
179
|
+
(0, node_test_1.it)('extracts "out" scope correctly', () => {
|
|
180
|
+
const content = `### FR-001: Test\n**Scope:** out`;
|
|
181
|
+
const scopes = (0, audit_js_1.parseFRScope)(content);
|
|
182
|
+
assert.strictEqual(scopes.get('FR-001'), 'out');
|
|
183
|
+
});
|
|
184
|
+
(0, node_test_1.it)('defaults to "v1" when scope not found', () => {
|
|
185
|
+
const content = `### FR-001: Test\nNo scope line here.`;
|
|
186
|
+
const scopes = (0, audit_js_1.parseFRScope)(content);
|
|
187
|
+
assert.strictEqual(scopes.get('FR-001'), 'v1');
|
|
188
|
+
});
|
|
189
|
+
(0, node_test_1.it)('handles multiple FRs with different scopes', () => {
|
|
190
|
+
const content = `### FR-001: A\n**Scope:** v1\n### FR-002: B\n**Scope:** v2`;
|
|
191
|
+
const scopes = (0, audit_js_1.parseFRScope)(content);
|
|
192
|
+
assert.strictEqual(scopes.get('FR-001'), 'v1');
|
|
193
|
+
assert.strictEqual(scopes.get('FR-002'), 'v2');
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
// ─── parseIntegrationStatus ─────────────────────────────────────────────────
|
|
197
|
+
(0, node_test_1.describe)('parseIntegrationStatus', () => {
|
|
198
|
+
(0, node_test_1.it)('parses "approved" status correctly', () => {
|
|
199
|
+
const content = `### I-001: API\n**Status:** approved`;
|
|
200
|
+
const statuses = (0, audit_js_1.parseIntegrationStatus)(content);
|
|
201
|
+
assert.strictEqual(statuses.get('I-001').status, 'approved');
|
|
202
|
+
});
|
|
203
|
+
(0, node_test_1.it)('parses "deferred" status with fallback present -> hasFallback: true', () => {
|
|
204
|
+
const content = `### I-001: API\n**Status:** deferred\n**Fallback:** Use local mode`;
|
|
205
|
+
const statuses = (0, audit_js_1.parseIntegrationStatus)(content);
|
|
206
|
+
assert.strictEqual(statuses.get('I-001').status, 'deferred');
|
|
207
|
+
assert.strictEqual(statuses.get('I-001').hasFallback, true);
|
|
208
|
+
});
|
|
209
|
+
(0, node_test_1.it)('parses "rejected" status correctly', () => {
|
|
210
|
+
const content = `### I-001: API\n**Status:** rejected`;
|
|
211
|
+
const statuses = (0, audit_js_1.parseIntegrationStatus)(content);
|
|
212
|
+
assert.strictEqual(statuses.get('I-001').status, 'rejected');
|
|
213
|
+
});
|
|
214
|
+
(0, node_test_1.it)('missing fallback -> hasFallback: false', () => {
|
|
215
|
+
const content = `### I-001: API\n**Status:** deferred`;
|
|
216
|
+
const statuses = (0, audit_js_1.parseIntegrationStatus)(content);
|
|
217
|
+
assert.strictEqual(statuses.get('I-001').hasFallback, false);
|
|
218
|
+
});
|
|
219
|
+
(0, node_test_1.it)('handles empty content', () => {
|
|
220
|
+
const statuses = (0, audit_js_1.parseIntegrationStatus)('');
|
|
221
|
+
assert.strictEqual(statuses.size, 0);
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
// ─── buildCoverageMatrix ────────────────────────────────────────────────────
|
|
225
|
+
(0, node_test_1.describe)('buildCoverageMatrix', () => {
|
|
226
|
+
(0, node_test_1.it)('matrix journeyToFRs populated from journey sections', () => {
|
|
227
|
+
const matrix = (0, audit_js_1.buildCoverageMatrix)({
|
|
228
|
+
journeys: MINIMAL_JOURNEYS,
|
|
229
|
+
spec: MINIMAL_SPEC,
|
|
230
|
+
nfr: MINIMAL_NFR,
|
|
231
|
+
integrations: MINIMAL_INTEGRATIONS,
|
|
232
|
+
});
|
|
233
|
+
assert.ok(matrix.journeyToFRs.has('J-001'));
|
|
234
|
+
const j1FRs = matrix.journeyToFRs.get('J-001');
|
|
235
|
+
assert.ok(j1FRs.has('FR-001'));
|
|
236
|
+
assert.ok(j1FRs.has('FR-002'));
|
|
237
|
+
});
|
|
238
|
+
(0, node_test_1.it)('matrix frToJourneys is reverse of journeyToFRs', () => {
|
|
239
|
+
const matrix = (0, audit_js_1.buildCoverageMatrix)({
|
|
240
|
+
journeys: MINIMAL_JOURNEYS,
|
|
241
|
+
spec: MINIMAL_SPEC,
|
|
242
|
+
});
|
|
243
|
+
const fr1Journeys = matrix.frToJourneys.get('FR-001');
|
|
244
|
+
assert.ok(fr1Journeys);
|
|
245
|
+
assert.ok(fr1Journeys.has('J-001'));
|
|
246
|
+
});
|
|
247
|
+
(0, node_test_1.it)('matrix journeyToTests populated from acceptance test sections', () => {
|
|
248
|
+
const matrix = (0, audit_js_1.buildCoverageMatrix)({
|
|
249
|
+
journeys: MINIMAL_JOURNEYS,
|
|
250
|
+
spec: MINIMAL_SPEC,
|
|
251
|
+
});
|
|
252
|
+
const j1Tests = matrix.journeyToTests.get('J-001');
|
|
253
|
+
assert.ok(j1Tests);
|
|
254
|
+
assert.ok(j1Tests.length >= 1);
|
|
255
|
+
});
|
|
256
|
+
(0, node_test_1.it)('matrix integrationStatus populated from INTEGRATIONS.md', () => {
|
|
257
|
+
const matrix = (0, audit_js_1.buildCoverageMatrix)({
|
|
258
|
+
journeys: MINIMAL_JOURNEYS,
|
|
259
|
+
integrations: MINIMAL_INTEGRATIONS,
|
|
260
|
+
});
|
|
261
|
+
assert.ok(matrix.integrationStatus.has('I-001'));
|
|
262
|
+
assert.strictEqual(matrix.integrationStatus.get('I-001').status, 'approved');
|
|
263
|
+
});
|
|
264
|
+
(0, node_test_1.it)('matrix frScopes populated from SPEC.md', () => {
|
|
265
|
+
const matrix = (0, audit_js_1.buildCoverageMatrix)({
|
|
266
|
+
journeys: MINIMAL_JOURNEYS,
|
|
267
|
+
spec: MINIMAL_SPEC,
|
|
268
|
+
});
|
|
269
|
+
assert.strictEqual(matrix.frScopes.get('FR-001'), 'v1');
|
|
270
|
+
assert.strictEqual(matrix.frScopes.get('FR-004'), 'v2');
|
|
271
|
+
});
|
|
272
|
+
(0, node_test_1.it)('allJourneyIds contains all journeys found', () => {
|
|
273
|
+
const matrix = (0, audit_js_1.buildCoverageMatrix)({ journeys: MINIMAL_JOURNEYS });
|
|
274
|
+
assert.ok(matrix.allJourneyIds.includes('J-001'));
|
|
275
|
+
assert.ok(matrix.allJourneyIds.includes('J-002'));
|
|
276
|
+
});
|
|
277
|
+
(0, node_test_1.it)('handles empty files gracefully', () => {
|
|
278
|
+
const matrix = (0, audit_js_1.buildCoverageMatrix)({});
|
|
279
|
+
assert.strictEqual(matrix.allJourneyIds.length, 0);
|
|
280
|
+
assert.strictEqual(matrix.allFRIds.length, 0);
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
// ─── checkJourneyFRCoverage ─────────────────────────────────────────────────
|
|
284
|
+
(0, node_test_1.describe)('checkJourneyFRCoverage', () => {
|
|
285
|
+
(0, node_test_1.it)('journey with FRs passes', () => {
|
|
286
|
+
const matrix = (0, audit_js_1.buildCoverageMatrix)({
|
|
287
|
+
journeys: MINIMAL_JOURNEYS,
|
|
288
|
+
spec: MINIMAL_SPEC,
|
|
289
|
+
});
|
|
290
|
+
const result = (0, audit_js_1.checkJourneyFRCoverage)(matrix);
|
|
291
|
+
assert.strictEqual(result.passed, true);
|
|
292
|
+
});
|
|
293
|
+
(0, node_test_1.it)('journey with zero FRs fails with correct finding', () => {
|
|
294
|
+
const journeys = `## Journeys\n### J-001: Empty\nNo FR references here.\n#### Acceptance Tests\n- test`;
|
|
295
|
+
const matrix = (0, audit_js_1.buildCoverageMatrix)({ journeys });
|
|
296
|
+
const result = (0, audit_js_1.checkJourneyFRCoverage)(matrix);
|
|
297
|
+
assert.strictEqual(result.passed, false);
|
|
298
|
+
assert.strictEqual(result.findings[0].id, 'J-001');
|
|
299
|
+
assert.ok(result.findings[0].issue.includes('no functional requirements'));
|
|
300
|
+
});
|
|
301
|
+
(0, node_test_1.it)('multiple journeys: one passing, one failing', () => {
|
|
302
|
+
const journeys = `## Journeys
|
|
303
|
+
### J-001: Good
|
|
304
|
+
References FR-001.
|
|
305
|
+
#### Acceptance Tests
|
|
306
|
+
- test
|
|
307
|
+
### J-002: Bad
|
|
308
|
+
No refs here.
|
|
309
|
+
#### Acceptance Tests
|
|
310
|
+
- test`;
|
|
311
|
+
const matrix = (0, audit_js_1.buildCoverageMatrix)({ journeys });
|
|
312
|
+
const result = (0, audit_js_1.checkJourneyFRCoverage)(matrix);
|
|
313
|
+
assert.strictEqual(result.passed, false);
|
|
314
|
+
assert.strictEqual(result.findings.length, 1);
|
|
315
|
+
assert.strictEqual(result.findings[0].id, 'J-002');
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
// ─── checkJourneyTestCoverage ───────────────────────────────────────────────
|
|
319
|
+
(0, node_test_1.describe)('checkJourneyTestCoverage', () => {
|
|
320
|
+
(0, node_test_1.it)('journey with acceptance test passes', () => {
|
|
321
|
+
const matrix = (0, audit_js_1.buildCoverageMatrix)({
|
|
322
|
+
journeys: MINIMAL_JOURNEYS,
|
|
323
|
+
});
|
|
324
|
+
const result = (0, audit_js_1.checkJourneyTestCoverage)(matrix);
|
|
325
|
+
assert.strictEqual(result.passed, true);
|
|
326
|
+
});
|
|
327
|
+
(0, node_test_1.it)('journey with empty tests fails', () => {
|
|
328
|
+
const journeys = `## Journeys\n### J-001: No Tests\nReferences FR-001.\n#### Acceptance Tests\n`;
|
|
329
|
+
const matrix = (0, audit_js_1.buildCoverageMatrix)({ journeys });
|
|
330
|
+
const result = (0, audit_js_1.checkJourneyTestCoverage)(matrix);
|
|
331
|
+
assert.strictEqual(result.passed, false);
|
|
332
|
+
assert.strictEqual(result.findings[0].id, 'J-001');
|
|
333
|
+
});
|
|
334
|
+
(0, node_test_1.it)('journey without acceptance tests section fails', () => {
|
|
335
|
+
const journeys = `## Journeys\n### J-001: Missing Section\nReferences FR-001.\nNo test section at all.`;
|
|
336
|
+
const matrix = (0, audit_js_1.buildCoverageMatrix)({ journeys });
|
|
337
|
+
const result = (0, audit_js_1.checkJourneyTestCoverage)(matrix);
|
|
338
|
+
assert.strictEqual(result.passed, false);
|
|
339
|
+
assert.strictEqual(result.findings[0].id, 'J-001');
|
|
340
|
+
assert.ok(result.findings[0].issue.includes('no acceptance tests'));
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
// ─── checkOrphanV1FRs ───────────────────────────────────────────────────────
|
|
344
|
+
(0, node_test_1.describe)('checkOrphanV1FRs', () => {
|
|
345
|
+
(0, node_test_1.it)('v1 FR referenced by a journey passes', () => {
|
|
346
|
+
const matrix = (0, audit_js_1.buildCoverageMatrix)({
|
|
347
|
+
journeys: MINIMAL_JOURNEYS,
|
|
348
|
+
spec: MINIMAL_SPEC,
|
|
349
|
+
});
|
|
350
|
+
const result = (0, audit_js_1.checkOrphanV1FRs)(matrix);
|
|
351
|
+
// FR-001, FR-002, FR-003 are all v1 and referenced by journeys
|
|
352
|
+
// FR-004 is v2 so should be skipped
|
|
353
|
+
assert.strictEqual(result.passed, true);
|
|
354
|
+
});
|
|
355
|
+
(0, node_test_1.it)('v1 FR NOT referenced by any journey fails (orphan detection)', () => {
|
|
356
|
+
const spec = `## Roles & Permissions\n## Functional Requirements\n### FR-001: Orphan\n**Scope:** v1\n## Acceptance Criteria\n`;
|
|
357
|
+
const matrix = (0, audit_js_1.buildCoverageMatrix)({
|
|
358
|
+
journeys: `## Journeys\n### J-001: Test\nNo FR refs.\n#### Acceptance Tests\n- test`,
|
|
359
|
+
spec,
|
|
360
|
+
});
|
|
361
|
+
const result = (0, audit_js_1.checkOrphanV1FRs)(matrix);
|
|
362
|
+
assert.strictEqual(result.passed, false);
|
|
363
|
+
assert.ok(result.findings.some((f) => f.id === 'FR-001'));
|
|
364
|
+
assert.ok(result.findings[0].issue.includes('v1 FR not referenced'));
|
|
365
|
+
});
|
|
366
|
+
(0, node_test_1.it)('v2 FR not referenced by any journey still passes (scope filter works)', () => {
|
|
367
|
+
const spec = `## Roles & Permissions\n## Functional Requirements\n### FR-001: V2 Feature\n**Scope:** v2\n## Acceptance Criteria\n`;
|
|
368
|
+
const journeys = `## Journeys\n### J-001: Test\nNo FR refs.\n#### Acceptance Tests\n- test`;
|
|
369
|
+
const matrix = (0, audit_js_1.buildCoverageMatrix)({ journeys, spec });
|
|
370
|
+
const result = (0, audit_js_1.checkOrphanV1FRs)(matrix);
|
|
371
|
+
assert.strictEqual(result.passed, true);
|
|
372
|
+
});
|
|
373
|
+
(0, node_test_1.it)('out-of-scope FR not referenced passes', () => {
|
|
374
|
+
const spec = `## Roles & Permissions\n## Functional Requirements\n### FR-001: Out\n**Scope:** out\n## Acceptance Criteria\n`;
|
|
375
|
+
const journeys = `## Journeys\n### J-001: Test\nNo FR refs.\n#### Acceptance Tests\n- test`;
|
|
376
|
+
const matrix = (0, audit_js_1.buildCoverageMatrix)({ journeys, spec });
|
|
377
|
+
const result = (0, audit_js_1.checkOrphanV1FRs)(matrix);
|
|
378
|
+
assert.strictEqual(result.passed, true);
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
// ─── checkIntegrationApproval ───────────────────────────────────────────────
|
|
382
|
+
(0, node_test_1.describe)('checkIntegrationApproval', () => {
|
|
383
|
+
(0, node_test_1.it)('approved integration passes', () => {
|
|
384
|
+
const matrix = (0, audit_js_1.buildCoverageMatrix)({
|
|
385
|
+
journeys: MINIMAL_JOURNEYS,
|
|
386
|
+
integrations: MINIMAL_INTEGRATIONS,
|
|
387
|
+
});
|
|
388
|
+
const result = (0, audit_js_1.checkIntegrationApproval)(matrix);
|
|
389
|
+
assert.strictEqual(result.passed, true);
|
|
390
|
+
});
|
|
391
|
+
(0, node_test_1.it)('deferred integration WITH fallback passes', () => {
|
|
392
|
+
const integrations = `## Integrations\n### I-001: API\n**Status:** deferred\n**Fallback:** Use local data`;
|
|
393
|
+
const matrix = (0, audit_js_1.buildCoverageMatrix)({
|
|
394
|
+
journeys: MINIMAL_JOURNEYS,
|
|
395
|
+
integrations,
|
|
396
|
+
});
|
|
397
|
+
const result = (0, audit_js_1.checkIntegrationApproval)(matrix);
|
|
398
|
+
assert.strictEqual(result.passed, true);
|
|
399
|
+
});
|
|
400
|
+
(0, node_test_1.it)('deferred integration WITHOUT fallback fails', () => {
|
|
401
|
+
const integrations = `## Integrations\n### I-001: API\n**Status:** deferred`;
|
|
402
|
+
const matrix = (0, audit_js_1.buildCoverageMatrix)({
|
|
403
|
+
journeys: MINIMAL_JOURNEYS,
|
|
404
|
+
integrations,
|
|
405
|
+
});
|
|
406
|
+
const result = (0, audit_js_1.checkIntegrationApproval)(matrix);
|
|
407
|
+
assert.strictEqual(result.passed, false);
|
|
408
|
+
assert.ok(result.findings.some((f) => f.id === 'I-001'));
|
|
409
|
+
});
|
|
410
|
+
(0, node_test_1.it)('rejected integration fails', () => {
|
|
411
|
+
const integrations = `## Integrations\n### I-001: API\n**Status:** rejected`;
|
|
412
|
+
const matrix = (0, audit_js_1.buildCoverageMatrix)({
|
|
413
|
+
journeys: MINIMAL_JOURNEYS,
|
|
414
|
+
integrations,
|
|
415
|
+
});
|
|
416
|
+
const result = (0, audit_js_1.checkIntegrationApproval)(matrix);
|
|
417
|
+
assert.strictEqual(result.passed, false);
|
|
418
|
+
});
|
|
419
|
+
(0, node_test_1.it)('integration not referenced by any journey is not checked', () => {
|
|
420
|
+
const integrations = `## Integrations\n### I-099: Unused\n**Status:** rejected`;
|
|
421
|
+
const journeys = `## Journeys\n### J-001: Test\nReferences FR-001.\n#### Acceptance Tests\n- test`;
|
|
422
|
+
const matrix = (0, audit_js_1.buildCoverageMatrix)({ journeys, integrations });
|
|
423
|
+
const result = (0, audit_js_1.checkIntegrationApproval)(matrix);
|
|
424
|
+
assert.strictEqual(result.passed, true); // I-099 not referenced, not checked
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
// ─── checkRequiredHeadings ──────────────────────────────────────────────────
|
|
428
|
+
(0, node_test_1.describe)('checkRequiredHeadings', () => {
|
|
429
|
+
(0, node_test_1.it)('all headings present passes', () => {
|
|
430
|
+
const result = (0, audit_js_1.checkRequiredHeadings)({
|
|
431
|
+
'JOURNEYS.md': MINIMAL_JOURNEYS,
|
|
432
|
+
'SPEC.md': MINIMAL_SPEC,
|
|
433
|
+
'NFR.md': MINIMAL_NFR,
|
|
434
|
+
'INTEGRATIONS.md': MINIMAL_INTEGRATIONS,
|
|
435
|
+
'DECISIONS.md': `## Frozen Decisions\n## Success Metrics\n## Out of Scope\n## Risks & Mitigations\n## Open Questions`,
|
|
436
|
+
});
|
|
437
|
+
assert.strictEqual(result.passed, true);
|
|
438
|
+
});
|
|
439
|
+
(0, node_test_1.it)('missing heading in JOURNEYS.md fails with specific heading', () => {
|
|
440
|
+
const result = (0, audit_js_1.checkRequiredHeadings)({
|
|
441
|
+
'JOURNEYS.md': 'No headings here',
|
|
442
|
+
'SPEC.md': MINIMAL_SPEC,
|
|
443
|
+
'NFR.md': MINIMAL_NFR,
|
|
444
|
+
'INTEGRATIONS.md': MINIMAL_INTEGRATIONS,
|
|
445
|
+
});
|
|
446
|
+
assert.strictEqual(result.passed, false);
|
|
447
|
+
assert.ok(result.findings.some((f) => f.id === 'JOURNEYS.md'));
|
|
448
|
+
});
|
|
449
|
+
(0, node_test_1.it)('missing heading in SPEC.md fails', () => {
|
|
450
|
+
const result = (0, audit_js_1.checkRequiredHeadings)({
|
|
451
|
+
'JOURNEYS.md': MINIMAL_JOURNEYS,
|
|
452
|
+
'SPEC.md': 'Only some content',
|
|
453
|
+
'NFR.md': MINIMAL_NFR,
|
|
454
|
+
'INTEGRATIONS.md': MINIMAL_INTEGRATIONS,
|
|
455
|
+
});
|
|
456
|
+
assert.strictEqual(result.passed, false);
|
|
457
|
+
assert.ok(result.findings.some((f) => f.id === 'SPEC.md'));
|
|
458
|
+
});
|
|
459
|
+
(0, node_test_1.it)('unknown file type with no required headings is not checked', () => {
|
|
460
|
+
const result = (0, audit_js_1.checkRequiredHeadings)({
|
|
461
|
+
'JOURNEYS.md': MINIMAL_JOURNEYS,
|
|
462
|
+
'SPEC.md': MINIMAL_SPEC,
|
|
463
|
+
'NFR.md': MINIMAL_NFR,
|
|
464
|
+
'INTEGRATIONS.md': MINIMAL_INTEGRATIONS,
|
|
465
|
+
'UNKNOWN.md': 'whatever',
|
|
466
|
+
});
|
|
467
|
+
// Should not fail just because UNKNOWN.md has no required headings
|
|
468
|
+
// Only required files from REQUIRED_HEADINGS are checked
|
|
469
|
+
assert.strictEqual(result.passed, true);
|
|
470
|
+
});
|
|
471
|
+
(0, node_test_1.it)('empty file content produces finding', () => {
|
|
472
|
+
const result = (0, audit_js_1.checkRequiredHeadings)({
|
|
473
|
+
'JOURNEYS.md': '',
|
|
474
|
+
'SPEC.md': MINIMAL_SPEC,
|
|
475
|
+
'NFR.md': MINIMAL_NFR,
|
|
476
|
+
'INTEGRATIONS.md': MINIMAL_INTEGRATIONS,
|
|
477
|
+
});
|
|
478
|
+
assert.strictEqual(result.passed, false);
|
|
479
|
+
assert.ok(result.findings.some((f) => f.id === 'JOURNEYS.md' && f.issue.includes('missing or empty')));
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
// ─── runAudit ───────────────────────────────────────────────────────────────
|
|
483
|
+
(0, node_test_1.describe)('runAudit', () => {
|
|
484
|
+
(0, node_test_1.it)('full passing spec bundle returns passed: true', () => {
|
|
485
|
+
const result = (0, audit_js_1.runAudit)({
|
|
486
|
+
journeys: MINIMAL_JOURNEYS,
|
|
487
|
+
spec: MINIMAL_SPEC,
|
|
488
|
+
nfr: MINIMAL_NFR,
|
|
489
|
+
integrations: MINIMAL_INTEGRATIONS,
|
|
490
|
+
});
|
|
491
|
+
assert.strictEqual(result.passed, true);
|
|
492
|
+
assert.strictEqual(result.summary.failed, 0);
|
|
493
|
+
});
|
|
494
|
+
(0, node_test_1.it)('spec bundle with orphan v1 FR returns passed: false', () => {
|
|
495
|
+
const spec = `## Roles & Permissions\n## Functional Requirements\n### FR-001: Used\n**Scope:** v1\n### FR-002: Used\n**Scope:** v1\n### FR-003: Used\n**Scope:** v1\n### FR-099: Orphan\n**Scope:** v1\n## Acceptance Criteria\n`;
|
|
496
|
+
const result = (0, audit_js_1.runAudit)({
|
|
497
|
+
journeys: MINIMAL_JOURNEYS,
|
|
498
|
+
spec,
|
|
499
|
+
nfr: MINIMAL_NFR,
|
|
500
|
+
integrations: MINIMAL_INTEGRATIONS,
|
|
501
|
+
});
|
|
502
|
+
assert.strictEqual(result.passed, false);
|
|
503
|
+
// Find the orphan check
|
|
504
|
+
const orphanCheck = result.checks.find((c) => c.check === 'orphan_v1_frs');
|
|
505
|
+
assert.ok(orphanCheck);
|
|
506
|
+
assert.strictEqual(orphanCheck.passed, false);
|
|
507
|
+
assert.ok(orphanCheck.findings.some((f) => f.id === 'FR-099'));
|
|
508
|
+
});
|
|
509
|
+
(0, node_test_1.it)('summary counts are accurate', () => {
|
|
510
|
+
const result = (0, audit_js_1.runAudit)({
|
|
511
|
+
journeys: MINIMAL_JOURNEYS,
|
|
512
|
+
spec: MINIMAL_SPEC,
|
|
513
|
+
nfr: MINIMAL_NFR,
|
|
514
|
+
integrations: MINIMAL_INTEGRATIONS,
|
|
515
|
+
});
|
|
516
|
+
assert.strictEqual(result.summary.total_checks, 5);
|
|
517
|
+
assert.strictEqual(result.summary.passed + result.summary.failed, result.summary.total_checks);
|
|
518
|
+
});
|
|
519
|
+
(0, node_test_1.it)('empty/missing files produce appropriate findings', () => {
|
|
520
|
+
const result = (0, audit_js_1.runAudit)({});
|
|
521
|
+
assert.strictEqual(result.passed, false);
|
|
522
|
+
// Required headings check should fail for missing files
|
|
523
|
+
const headingCheck = result.checks.find((c) => c.check === 'required_headings');
|
|
524
|
+
assert.ok(headingCheck);
|
|
525
|
+
assert.strictEqual(headingCheck.passed, false);
|
|
526
|
+
});
|
|
527
|
+
(0, node_test_1.it)('check results have correct check names', () => {
|
|
528
|
+
const result = (0, audit_js_1.runAudit)({
|
|
529
|
+
journeys: MINIMAL_JOURNEYS,
|
|
530
|
+
spec: MINIMAL_SPEC,
|
|
531
|
+
nfr: MINIMAL_NFR,
|
|
532
|
+
integrations: MINIMAL_INTEGRATIONS,
|
|
533
|
+
});
|
|
534
|
+
const checkNames = result.checks.map((c) => c.check);
|
|
535
|
+
assert.ok(checkNames.includes('journey_fr_coverage'));
|
|
536
|
+
assert.ok(checkNames.includes('journey_test_coverage'));
|
|
537
|
+
assert.ok(checkNames.includes('orphan_v1_frs'));
|
|
538
|
+
assert.ok(checkNames.includes('integration_approval'));
|
|
539
|
+
assert.ok(checkNames.includes('required_headings'));
|
|
540
|
+
});
|
|
541
|
+
});
|
|
542
|
+
// ─── Integration: journey without tests ─────────────────────────────────────
|
|
543
|
+
(0, node_test_1.describe)('integration: journey without tests (TEST-02)', () => {
|
|
544
|
+
(0, node_test_1.it)('journeys content with no acceptance tests section produces FAIL', () => {
|
|
545
|
+
const journeys = `## Journeys\n### J-001: No Tests\nReferences FR-001.\nNo test section.`;
|
|
546
|
+
const result = (0, audit_js_1.runAudit)({
|
|
547
|
+
journeys,
|
|
548
|
+
spec: `## Roles & Permissions\n## Functional Requirements\n### FR-001: A\n**Scope:** v1\n## Acceptance Criteria\n`,
|
|
549
|
+
nfr: MINIMAL_NFR,
|
|
550
|
+
integrations: MINIMAL_INTEGRATIONS,
|
|
551
|
+
});
|
|
552
|
+
assert.strictEqual(result.passed, false);
|
|
553
|
+
const testCheck = result.checks.find((c) => c.check === 'journey_test_coverage');
|
|
554
|
+
assert.ok(testCheck);
|
|
555
|
+
assert.strictEqual(testCheck.passed, false);
|
|
556
|
+
});
|
|
557
|
+
(0, node_test_1.it)('journeys content with acceptance tests section produces PASS for that check', () => {
|
|
558
|
+
const result = (0, audit_js_1.runAudit)({
|
|
559
|
+
journeys: MINIMAL_JOURNEYS,
|
|
560
|
+
spec: MINIMAL_SPEC,
|
|
561
|
+
nfr: MINIMAL_NFR,
|
|
562
|
+
integrations: MINIMAL_INTEGRATIONS,
|
|
563
|
+
});
|
|
564
|
+
const testCheck = result.checks.find((c) => c.check === 'journey_test_coverage');
|
|
565
|
+
assert.ok(testCheck);
|
|
566
|
+
assert.strictEqual(testCheck.passed, true);
|
|
567
|
+
});
|
|
568
|
+
});
|
|
569
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
570
|
+
// Plan 04-02: Report Generation, Auto-Fix, Hard Gate
|
|
571
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
572
|
+
// ─── Helper: create tmp STATE.json ──────────────────────────────────────────
|
|
573
|
+
function createTestStateDir(overrides) {
|
|
574
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'audit-gate-'));
|
|
575
|
+
const statePath = path.join(dir, 'STATE.json');
|
|
576
|
+
const state = (0, state_js_1.createDefaultState)('test-project');
|
|
577
|
+
if (overrides?.audit) {
|
|
578
|
+
state.stage_status.audit = overrides.audit;
|
|
579
|
+
}
|
|
580
|
+
if (overrides?.stage) {
|
|
581
|
+
state.stage = overrides.stage;
|
|
582
|
+
}
|
|
583
|
+
fs.writeFileSync(statePath, JSON.stringify(state, null, 2), 'utf-8');
|
|
584
|
+
return {
|
|
585
|
+
dir,
|
|
586
|
+
statePath,
|
|
587
|
+
cleanup: () => fs.rmSync(dir, { recursive: true, force: true }),
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
// ─── formatCoverageMatrixTable ──────────────────────────────────────────────
|
|
591
|
+
(0, node_test_1.describe)('formatCoverageMatrixTable', () => {
|
|
592
|
+
(0, node_test_1.it)('produces table with correct headers', () => {
|
|
593
|
+
const matrix = (0, audit_js_1.buildCoverageMatrix)({
|
|
594
|
+
journeys: MINIMAL_JOURNEYS,
|
|
595
|
+
spec: MINIMAL_SPEC,
|
|
596
|
+
integrations: MINIMAL_INTEGRATIONS,
|
|
597
|
+
});
|
|
598
|
+
const table = (0, audit_js_1.formatCoverageMatrixTable)(matrix);
|
|
599
|
+
assert.ok(table.includes('| Journey |'));
|
|
600
|
+
assert.ok(table.includes('| Linked FRs |'));
|
|
601
|
+
assert.ok(table.includes('| Acceptance Tests |'));
|
|
602
|
+
assert.ok(table.includes('| Status |'));
|
|
603
|
+
});
|
|
604
|
+
(0, node_test_1.it)('journey with FRs shows FR IDs comma-separated', () => {
|
|
605
|
+
const matrix = (0, audit_js_1.buildCoverageMatrix)({
|
|
606
|
+
journeys: MINIMAL_JOURNEYS,
|
|
607
|
+
spec: MINIMAL_SPEC,
|
|
608
|
+
});
|
|
609
|
+
const table = (0, audit_js_1.formatCoverageMatrixTable)(matrix);
|
|
610
|
+
// J-001 has FR-001 and FR-002
|
|
611
|
+
assert.ok(table.includes('FR-001, FR-002'));
|
|
612
|
+
});
|
|
613
|
+
(0, node_test_1.it)('journey with no FRs shows "(none)"', () => {
|
|
614
|
+
const journeys = `## Journeys\n### J-001: Empty\nNo FR refs here.\n#### Acceptance Tests\n- test`;
|
|
615
|
+
const matrix = (0, audit_js_1.buildCoverageMatrix)({ journeys });
|
|
616
|
+
const table = (0, audit_js_1.formatCoverageMatrixTable)(matrix);
|
|
617
|
+
assert.ok(table.includes('(none)'));
|
|
618
|
+
});
|
|
619
|
+
(0, node_test_1.it)('test count shown correctly', () => {
|
|
620
|
+
const matrix = (0, audit_js_1.buildCoverageMatrix)({
|
|
621
|
+
journeys: MINIMAL_JOURNEYS,
|
|
622
|
+
spec: MINIMAL_SPEC,
|
|
623
|
+
});
|
|
624
|
+
const table = (0, audit_js_1.formatCoverageMatrixTable)(matrix);
|
|
625
|
+
// J-001 has 2 tests
|
|
626
|
+
assert.ok(table.includes('2 tests'));
|
|
627
|
+
// J-002 has 1 test
|
|
628
|
+
assert.ok(table.includes('1 test'));
|
|
629
|
+
});
|
|
630
|
+
(0, node_test_1.it)('sorted by journey ID', () => {
|
|
631
|
+
const journeys = `## Journeys\n### J-002: Second\nFR-002.\n#### Acceptance Tests\n- test\n### J-001: First\nFR-001.\n#### Acceptance Tests\n- test`;
|
|
632
|
+
const matrix = (0, audit_js_1.buildCoverageMatrix)({ journeys });
|
|
633
|
+
const table = (0, audit_js_1.formatCoverageMatrixTable)(matrix);
|
|
634
|
+
const j1Pos = table.indexOf('J-001');
|
|
635
|
+
const j2Pos = table.indexOf('J-002');
|
|
636
|
+
assert.ok(j1Pos < j2Pos, 'J-001 should appear before J-002');
|
|
637
|
+
});
|
|
638
|
+
});
|
|
639
|
+
// ─── formatCheckSection ─────────────────────────────────────────────────────
|
|
640
|
+
(0, node_test_1.describe)('formatCheckSection', () => {
|
|
641
|
+
(0, node_test_1.it)('passing check shows "PASS"', () => {
|
|
642
|
+
const result = { check: 'journey_fr_coverage', passed: true, findings: [] };
|
|
643
|
+
const section = (0, audit_js_1.formatCheckSection)(result);
|
|
644
|
+
assert.ok(section.includes('PASS'));
|
|
645
|
+
assert.ok(section.includes('Journey FR Coverage'));
|
|
646
|
+
});
|
|
647
|
+
(0, node_test_1.it)('failing check shows "FAIL" with checkbox items', () => {
|
|
648
|
+
const result = {
|
|
649
|
+
check: 'journey_fr_coverage',
|
|
650
|
+
passed: false,
|
|
651
|
+
findings: [
|
|
652
|
+
{ id: 'J-003', issue: 'Journey references no FRs', fix: 'Add FR refs', severity: 'error' },
|
|
653
|
+
],
|
|
654
|
+
};
|
|
655
|
+
const section = (0, audit_js_1.formatCheckSection)(result);
|
|
656
|
+
assert.ok(section.includes('FAIL'));
|
|
657
|
+
assert.ok(section.includes('- [ ]'));
|
|
658
|
+
assert.ok(section.includes('**J-003**'));
|
|
659
|
+
});
|
|
660
|
+
(0, node_test_1.it)('each finding has ID in bold and fix instruction', () => {
|
|
661
|
+
const result = {
|
|
662
|
+
check: 'orphan_v1_frs',
|
|
663
|
+
passed: false,
|
|
664
|
+
findings: [
|
|
665
|
+
{ id: 'FR-099', issue: 'orphan FR', fix: 'Add reference to FR-099', severity: 'error' },
|
|
666
|
+
],
|
|
667
|
+
};
|
|
668
|
+
const section = (0, audit_js_1.formatCheckSection)(result);
|
|
669
|
+
assert.ok(section.includes('**FR-099**'));
|
|
670
|
+
assert.ok(section.includes('Fix: Add reference to FR-099'));
|
|
671
|
+
});
|
|
672
|
+
});
|
|
673
|
+
// ─── generateAuditReport ────────────────────────────────────────────────────
|
|
674
|
+
(0, node_test_1.describe)('generateAuditReport', () => {
|
|
675
|
+
(0, node_test_1.it)('PASS result includes "Result: PASS" header', () => {
|
|
676
|
+
const result = (0, audit_js_1.runAudit)({
|
|
677
|
+
journeys: MINIMAL_JOURNEYS,
|
|
678
|
+
spec: MINIMAL_SPEC,
|
|
679
|
+
nfr: MINIMAL_NFR,
|
|
680
|
+
integrations: MINIMAL_INTEGRATIONS,
|
|
681
|
+
});
|
|
682
|
+
const report = (0, audit_js_1.generateAuditReport)(result);
|
|
683
|
+
assert.ok(report.includes('**Result:** PASS'));
|
|
684
|
+
});
|
|
685
|
+
(0, node_test_1.it)('FAIL result includes "Result: FAIL" header', () => {
|
|
686
|
+
const result = (0, audit_js_1.runAudit)({
|
|
687
|
+
journeys: `## Journeys\n### J-001: Bad\nNo FR refs.\n`,
|
|
688
|
+
spec: MINIMAL_SPEC,
|
|
689
|
+
nfr: MINIMAL_NFR,
|
|
690
|
+
integrations: MINIMAL_INTEGRATIONS,
|
|
691
|
+
});
|
|
692
|
+
const report = (0, audit_js_1.generateAuditReport)(result);
|
|
693
|
+
assert.ok(report.includes('**Result:** FAIL'));
|
|
694
|
+
});
|
|
695
|
+
(0, node_test_1.it)('coverage matrix table included', () => {
|
|
696
|
+
const result = (0, audit_js_1.runAudit)({
|
|
697
|
+
journeys: MINIMAL_JOURNEYS,
|
|
698
|
+
spec: MINIMAL_SPEC,
|
|
699
|
+
nfr: MINIMAL_NFR,
|
|
700
|
+
integrations: MINIMAL_INTEGRATIONS,
|
|
701
|
+
});
|
|
702
|
+
const report = (0, audit_js_1.generateAuditReport)(result);
|
|
703
|
+
assert.ok(report.includes('## Coverage Matrix'));
|
|
704
|
+
assert.ok(report.includes('| Journey |'));
|
|
705
|
+
});
|
|
706
|
+
(0, node_test_1.it)('coverage statistics table included', () => {
|
|
707
|
+
const result = (0, audit_js_1.runAudit)({
|
|
708
|
+
journeys: MINIMAL_JOURNEYS,
|
|
709
|
+
spec: MINIMAL_SPEC,
|
|
710
|
+
nfr: MINIMAL_NFR,
|
|
711
|
+
integrations: MINIMAL_INTEGRATIONS,
|
|
712
|
+
});
|
|
713
|
+
const report = (0, audit_js_1.generateAuditReport)(result);
|
|
714
|
+
assert.ok(report.includes('## Coverage Statistics'));
|
|
715
|
+
assert.ok(report.includes('| Journeys |'));
|
|
716
|
+
assert.ok(report.includes('| Functional Requirements |'));
|
|
717
|
+
});
|
|
718
|
+
(0, node_test_1.it)('check sections present for all checks', () => {
|
|
719
|
+
const result = (0, audit_js_1.runAudit)({
|
|
720
|
+
journeys: MINIMAL_JOURNEYS,
|
|
721
|
+
spec: MINIMAL_SPEC,
|
|
722
|
+
nfr: MINIMAL_NFR,
|
|
723
|
+
integrations: MINIMAL_INTEGRATIONS,
|
|
724
|
+
});
|
|
725
|
+
const report = (0, audit_js_1.generateAuditReport)(result);
|
|
726
|
+
assert.ok(report.includes('## Check Results'));
|
|
727
|
+
assert.ok(report.includes('### Journey FR Coverage'));
|
|
728
|
+
assert.ok(report.includes('### Journey Test Coverage'));
|
|
729
|
+
assert.ok(report.includes('### Orphan v1 FRs'));
|
|
730
|
+
assert.ok(report.includes('### Integration Approval'));
|
|
731
|
+
assert.ok(report.includes('### Required Headings'));
|
|
732
|
+
});
|
|
733
|
+
(0, node_test_1.it)('FAIL result includes actionable checklist section', () => {
|
|
734
|
+
const result = (0, audit_js_1.runAudit)({
|
|
735
|
+
journeys: `## Journeys\n### J-001: Bad\nNo FR refs.\n`,
|
|
736
|
+
spec: MINIMAL_SPEC,
|
|
737
|
+
nfr: MINIMAL_NFR,
|
|
738
|
+
integrations: MINIMAL_INTEGRATIONS,
|
|
739
|
+
});
|
|
740
|
+
const report = (0, audit_js_1.generateAuditReport)(result);
|
|
741
|
+
assert.ok(report.includes('## Actionable Checklist'));
|
|
742
|
+
assert.ok(report.includes('- [ ]'));
|
|
743
|
+
});
|
|
744
|
+
(0, node_test_1.it)('PASS result does NOT include actionable checklist', () => {
|
|
745
|
+
const result = (0, audit_js_1.runAudit)({
|
|
746
|
+
journeys: MINIMAL_JOURNEYS,
|
|
747
|
+
spec: MINIMAL_SPEC,
|
|
748
|
+
nfr: MINIMAL_NFR,
|
|
749
|
+
integrations: MINIMAL_INTEGRATIONS,
|
|
750
|
+
});
|
|
751
|
+
const report = (0, audit_js_1.generateAuditReport)(result);
|
|
752
|
+
assert.ok(!report.includes('## Actionable Checklist'));
|
|
753
|
+
});
|
|
754
|
+
(0, node_test_1.it)('summary counts are accurate', () => {
|
|
755
|
+
const result = (0, audit_js_1.runAudit)({
|
|
756
|
+
journeys: MINIMAL_JOURNEYS,
|
|
757
|
+
spec: MINIMAL_SPEC,
|
|
758
|
+
nfr: MINIMAL_NFR,
|
|
759
|
+
integrations: MINIMAL_INTEGRATIONS,
|
|
760
|
+
});
|
|
761
|
+
const report = (0, audit_js_1.generateAuditReport)(result);
|
|
762
|
+
assert.ok(report.includes('5/5 checks passed'));
|
|
763
|
+
assert.ok(report.includes('0 issue(s) found'));
|
|
764
|
+
});
|
|
765
|
+
});
|
|
766
|
+
// ─── applyAutoFix — allowed changes ─────────────────────────────────────────
|
|
767
|
+
(0, node_test_1.describe)('applyAutoFix — allowed changes', () => {
|
|
768
|
+
(0, node_test_1.it)('adds stub acceptance test to journey missing tests', () => {
|
|
769
|
+
const journeys = `## Journeys\n### J-001: Onboarding\nReferences FR-001.\nNo tests section.`;
|
|
770
|
+
const result = (0, audit_js_1.runAudit)({
|
|
771
|
+
journeys,
|
|
772
|
+
spec: `## Roles & Permissions\n## Functional Requirements\n### FR-001: A\n**Scope:** v1\n## Acceptance Criteria\n`,
|
|
773
|
+
nfr: MINIMAL_NFR,
|
|
774
|
+
integrations: MINIMAL_INTEGRATIONS,
|
|
775
|
+
});
|
|
776
|
+
const fixResult = (0, audit_js_1.applyAutoFix)(result, {
|
|
777
|
+
'JOURNEYS.md': journeys,
|
|
778
|
+
'SPEC.md': `## Roles & Permissions\n## Functional Requirements\n### FR-001: A\n**Scope:** v1\n## Acceptance Criteria\n`,
|
|
779
|
+
'NFR.md': MINIMAL_NFR,
|
|
780
|
+
'INTEGRATIONS.md': MINIMAL_INTEGRATIONS,
|
|
781
|
+
});
|
|
782
|
+
assert.ok(fixResult.applied.length > 0);
|
|
783
|
+
assert.ok(fixResult.applied.some((c) => c.change.includes('stub acceptance test')));
|
|
784
|
+
});
|
|
785
|
+
(0, node_test_1.it)('stub test contains journey ID', () => {
|
|
786
|
+
const journeys = `## Journeys\n### J-001: Onboarding\nReferences FR-001.\nNo tests.`;
|
|
787
|
+
const result = (0, audit_js_1.runAudit)({
|
|
788
|
+
journeys,
|
|
789
|
+
spec: `## Roles & Permissions\n## Functional Requirements\n### FR-001: A\n**Scope:** v1\n## Acceptance Criteria\n`,
|
|
790
|
+
nfr: MINIMAL_NFR,
|
|
791
|
+
integrations: MINIMAL_INTEGRATIONS,
|
|
792
|
+
});
|
|
793
|
+
const fixResult = (0, audit_js_1.applyAutoFix)(result, {
|
|
794
|
+
'JOURNEYS.md': journeys,
|
|
795
|
+
'SPEC.md': `## Roles & Permissions\n## Functional Requirements\n### FR-001: A\n**Scope:** v1\n## Acceptance Criteria\n`,
|
|
796
|
+
'NFR.md': MINIMAL_NFR,
|
|
797
|
+
'INTEGRATIONS.md': MINIMAL_INTEGRATIONS,
|
|
798
|
+
});
|
|
799
|
+
const updatedJourneys = fixResult.updatedFiles.get('JOURNEYS.md') || '';
|
|
800
|
+
assert.ok(updatedJourneys.includes('J-001'));
|
|
801
|
+
assert.ok(updatedJourneys.includes('Acceptance Tests'));
|
|
802
|
+
});
|
|
803
|
+
(0, node_test_1.it)('adds fallback stub for unapproved integration', () => {
|
|
804
|
+
const integrations = `## Integrations\n### I-001: External API\n**Status:** deferred`;
|
|
805
|
+
const journeys = `## Journeys\n### J-001: Test\nReferences FR-001 and uses I-001.\n#### Acceptance Tests\n- test`;
|
|
806
|
+
const result = (0, audit_js_1.runAudit)({
|
|
807
|
+
journeys,
|
|
808
|
+
spec: `## Roles & Permissions\n## Functional Requirements\n### FR-001: A\n**Scope:** v1\n## Acceptance Criteria\n`,
|
|
809
|
+
nfr: MINIMAL_NFR,
|
|
810
|
+
integrations,
|
|
811
|
+
});
|
|
812
|
+
const fixResult = (0, audit_js_1.applyAutoFix)(result, {
|
|
813
|
+
'JOURNEYS.md': journeys,
|
|
814
|
+
'SPEC.md': `## Roles & Permissions\n## Functional Requirements\n### FR-001: A\n**Scope:** v1\n## Acceptance Criteria\n`,
|
|
815
|
+
'NFR.md': MINIMAL_NFR,
|
|
816
|
+
'INTEGRATIONS.md': integrations,
|
|
817
|
+
});
|
|
818
|
+
assert.ok(fixResult.applied.some((c) => c.change.includes('fallback')));
|
|
819
|
+
const updatedInt = fixResult.updatedFiles.get('INTEGRATIONS.md') || '';
|
|
820
|
+
assert.ok(updatedInt.includes('**Fallback:**'));
|
|
821
|
+
});
|
|
822
|
+
(0, node_test_1.it)('returns the change in applied[] with human-readable description', () => {
|
|
823
|
+
const journeys = `## Journeys\n### J-001: Test\nReferences FR-001.\nNo tests.`;
|
|
824
|
+
const result = (0, audit_js_1.runAudit)({
|
|
825
|
+
journeys,
|
|
826
|
+
spec: `## Roles & Permissions\n## Functional Requirements\n### FR-001: A\n**Scope:** v1\n## Acceptance Criteria\n`,
|
|
827
|
+
nfr: MINIMAL_NFR,
|
|
828
|
+
integrations: MINIMAL_INTEGRATIONS,
|
|
829
|
+
});
|
|
830
|
+
const fixResult = (0, audit_js_1.applyAutoFix)(result, {
|
|
831
|
+
'JOURNEYS.md': journeys,
|
|
832
|
+
'SPEC.md': `## Roles & Permissions\n## Functional Requirements\n### FR-001: A\n**Scope:** v1\n## Acceptance Criteria\n`,
|
|
833
|
+
'NFR.md': MINIMAL_NFR,
|
|
834
|
+
'INTEGRATIONS.md': MINIMAL_INTEGRATIONS,
|
|
835
|
+
});
|
|
836
|
+
for (const change of fixResult.applied) {
|
|
837
|
+
assert.ok(change.file.length > 0);
|
|
838
|
+
assert.ok(change.change.length > 0);
|
|
839
|
+
assert.ok(change.finding);
|
|
840
|
+
}
|
|
841
|
+
});
|
|
842
|
+
});
|
|
843
|
+
// ─── applyAutoFix — prohibited changes ──────────────────────────────────────
|
|
844
|
+
(0, node_test_1.describe)('applyAutoFix — prohibited changes', () => {
|
|
845
|
+
(0, node_test_1.it)('does NOT create new FRs (orphan FR finding goes to skipped[])', () => {
|
|
846
|
+
const spec = `## Roles & Permissions\n## Functional Requirements\n### FR-099: Orphan\n**Scope:** v1\n## Acceptance Criteria\n`;
|
|
847
|
+
const journeys = `## Journeys\n### J-001: Test\nNo FR refs.\n#### Acceptance Tests\n- test`;
|
|
848
|
+
const result = (0, audit_js_1.runAudit)({ journeys, spec, nfr: MINIMAL_NFR, integrations: MINIMAL_INTEGRATIONS });
|
|
849
|
+
const fixResult = (0, audit_js_1.applyAutoFix)(result, {
|
|
850
|
+
'JOURNEYS.md': journeys,
|
|
851
|
+
'SPEC.md': spec,
|
|
852
|
+
'NFR.md': MINIMAL_NFR,
|
|
853
|
+
'INTEGRATIONS.md': MINIMAL_INTEGRATIONS,
|
|
854
|
+
});
|
|
855
|
+
// Orphan FR finding should be in skipped
|
|
856
|
+
assert.ok(fixResult.skipped.some((f) => f.id === 'FR-099'));
|
|
857
|
+
});
|
|
858
|
+
(0, node_test_1.it)('does NOT create new journeys (journey_fr_coverage finding goes to skipped[])', () => {
|
|
859
|
+
const journeys = `## Journeys\n### J-001: Empty\nNo FR refs.\n#### Acceptance Tests\n- test`;
|
|
860
|
+
const result = (0, audit_js_1.runAudit)({ journeys, spec: MINIMAL_SPEC, nfr: MINIMAL_NFR, integrations: MINIMAL_INTEGRATIONS });
|
|
861
|
+
const fixResult = (0, audit_js_1.applyAutoFix)(result, {
|
|
862
|
+
'JOURNEYS.md': journeys,
|
|
863
|
+
'SPEC.md': MINIMAL_SPEC,
|
|
864
|
+
'NFR.md': MINIMAL_NFR,
|
|
865
|
+
'INTEGRATIONS.md': MINIMAL_INTEGRATIONS,
|
|
866
|
+
});
|
|
867
|
+
// Journey FR coverage finding should be in skipped
|
|
868
|
+
assert.ok(fixResult.skipped.some((f) => f.id === 'J-001'));
|
|
869
|
+
});
|
|
870
|
+
(0, node_test_1.it)('does NOT approve paid integrations (adds fallback instead)', () => {
|
|
871
|
+
const integrations = `## Integrations\n### I-001: Paid API\n**Status:** deferred`;
|
|
872
|
+
const journeys = `## Journeys\n### J-001: Test\nReferences FR-001. Uses I-001.\n#### Acceptance Tests\n- test`;
|
|
873
|
+
const result = (0, audit_js_1.runAudit)({
|
|
874
|
+
journeys,
|
|
875
|
+
spec: `## Roles & Permissions\n## Functional Requirements\n### FR-001: A\n**Scope:** v1\n## Acceptance Criteria\n`,
|
|
876
|
+
nfr: MINIMAL_NFR,
|
|
877
|
+
integrations,
|
|
878
|
+
});
|
|
879
|
+
const fixResult = (0, audit_js_1.applyAutoFix)(result, {
|
|
880
|
+
'JOURNEYS.md': journeys,
|
|
881
|
+
'SPEC.md': `## Roles & Permissions\n## Functional Requirements\n### FR-001: A\n**Scope:** v1\n## Acceptance Criteria\n`,
|
|
882
|
+
'NFR.md': MINIMAL_NFR,
|
|
883
|
+
'INTEGRATIONS.md': integrations,
|
|
884
|
+
});
|
|
885
|
+
const updatedInt = fixResult.updatedFiles.get('INTEGRATIONS.md') || '';
|
|
886
|
+
// Should NOT change status to approved
|
|
887
|
+
assert.ok(!updatedInt.includes('**Status:** approved'));
|
|
888
|
+
// Should add fallback instead
|
|
889
|
+
assert.ok(updatedInt.includes('**Fallback:**'));
|
|
890
|
+
});
|
|
891
|
+
(0, node_test_1.it)('skipped findings include the original finding object', () => {
|
|
892
|
+
const spec = `## Roles & Permissions\n## Functional Requirements\n### FR-099: Orphan\n**Scope:** v1\n## Acceptance Criteria\n`;
|
|
893
|
+
const journeys = `## Journeys\n### J-001: Test\nNo FR refs.\n#### Acceptance Tests\n- test`;
|
|
894
|
+
const result = (0, audit_js_1.runAudit)({ journeys, spec, nfr: MINIMAL_NFR, integrations: MINIMAL_INTEGRATIONS });
|
|
895
|
+
const fixResult = (0, audit_js_1.applyAutoFix)(result, {
|
|
896
|
+
'JOURNEYS.md': journeys,
|
|
897
|
+
'SPEC.md': spec,
|
|
898
|
+
'NFR.md': MINIMAL_NFR,
|
|
899
|
+
'INTEGRATIONS.md': MINIMAL_INTEGRATIONS,
|
|
900
|
+
});
|
|
901
|
+
for (const finding of fixResult.skipped) {
|
|
902
|
+
assert.ok(finding.id);
|
|
903
|
+
assert.ok(finding.issue);
|
|
904
|
+
assert.ok(finding.fix);
|
|
905
|
+
}
|
|
906
|
+
});
|
|
907
|
+
});
|
|
908
|
+
// ─── applyAutoFix — surgical principle ──────────────────────────────────────
|
|
909
|
+
(0, node_test_1.describe)('applyAutoFix — surgical principle', () => {
|
|
910
|
+
(0, node_test_1.it)('original file content preserved except for additions', () => {
|
|
911
|
+
const journeys = `## Journeys\n### J-001: Onboarding\nReferences FR-001.\nOriginal content here.`;
|
|
912
|
+
const result = (0, audit_js_1.runAudit)({
|
|
913
|
+
journeys,
|
|
914
|
+
spec: `## Roles & Permissions\n## Functional Requirements\n### FR-001: A\n**Scope:** v1\n## Acceptance Criteria\n`,
|
|
915
|
+
nfr: MINIMAL_NFR,
|
|
916
|
+
integrations: MINIMAL_INTEGRATIONS,
|
|
917
|
+
});
|
|
918
|
+
const fixResult = (0, audit_js_1.applyAutoFix)(result, {
|
|
919
|
+
'JOURNEYS.md': journeys,
|
|
920
|
+
'SPEC.md': `## Roles & Permissions\n## Functional Requirements\n### FR-001: A\n**Scope:** v1\n## Acceptance Criteria\n`,
|
|
921
|
+
'NFR.md': MINIMAL_NFR,
|
|
922
|
+
'INTEGRATIONS.md': MINIMAL_INTEGRATIONS,
|
|
923
|
+
});
|
|
924
|
+
const updated = fixResult.updatedFiles.get('JOURNEYS.md') || '';
|
|
925
|
+
// Original content is still there
|
|
926
|
+
assert.ok(updated.includes('Original content here.'));
|
|
927
|
+
assert.ok(updated.includes('### J-001: Onboarding'));
|
|
928
|
+
});
|
|
929
|
+
(0, node_test_1.it)('changes are appended, not replacing existing content', () => {
|
|
930
|
+
const journeys = `## Journeys\n### J-001: Test\nReferences FR-001.\nExisting text.`;
|
|
931
|
+
const result = (0, audit_js_1.runAudit)({
|
|
932
|
+
journeys,
|
|
933
|
+
spec: `## Roles & Permissions\n## Functional Requirements\n### FR-001: A\n**Scope:** v1\n## Acceptance Criteria\n`,
|
|
934
|
+
nfr: MINIMAL_NFR,
|
|
935
|
+
integrations: MINIMAL_INTEGRATIONS,
|
|
936
|
+
});
|
|
937
|
+
const fixResult = (0, audit_js_1.applyAutoFix)(result, {
|
|
938
|
+
'JOURNEYS.md': journeys,
|
|
939
|
+
'SPEC.md': `## Roles & Permissions\n## Functional Requirements\n### FR-001: A\n**Scope:** v1\n## Acceptance Criteria\n`,
|
|
940
|
+
'NFR.md': MINIMAL_NFR,
|
|
941
|
+
'INTEGRATIONS.md': MINIMAL_INTEGRATIONS,
|
|
942
|
+
});
|
|
943
|
+
const updated = fixResult.updatedFiles.get('JOURNEYS.md') || '';
|
|
944
|
+
// Existing text preserved AND new content added
|
|
945
|
+
assert.ok(updated.includes('Existing text.'));
|
|
946
|
+
assert.ok(updated.length > journeys.length);
|
|
947
|
+
});
|
|
948
|
+
(0, node_test_1.it)('updated file content in updatedFiles map', () => {
|
|
949
|
+
const journeys = `## Journeys\n### J-001: Test\nReferences FR-001.\nNo tests.`;
|
|
950
|
+
const result = (0, audit_js_1.runAudit)({
|
|
951
|
+
journeys,
|
|
952
|
+
spec: `## Roles & Permissions\n## Functional Requirements\n### FR-001: A\n**Scope:** v1\n## Acceptance Criteria\n`,
|
|
953
|
+
nfr: MINIMAL_NFR,
|
|
954
|
+
integrations: MINIMAL_INTEGRATIONS,
|
|
955
|
+
});
|
|
956
|
+
const fixResult = (0, audit_js_1.applyAutoFix)(result, {
|
|
957
|
+
'JOURNEYS.md': journeys,
|
|
958
|
+
'SPEC.md': `## Roles & Permissions\n## Functional Requirements\n### FR-001: A\n**Scope:** v1\n## Acceptance Criteria\n`,
|
|
959
|
+
'NFR.md': MINIMAL_NFR,
|
|
960
|
+
'INTEGRATIONS.md': MINIMAL_INTEGRATIONS,
|
|
961
|
+
});
|
|
962
|
+
assert.ok(fixResult.updatedFiles instanceof Map);
|
|
963
|
+
assert.ok(fixResult.updatedFiles.has('JOURNEYS.md'));
|
|
964
|
+
});
|
|
965
|
+
});
|
|
966
|
+
// ─── checkAuditGate ─────────────────────────────────────────────────────────
|
|
967
|
+
(0, node_test_1.describe)('checkAuditGate', () => {
|
|
968
|
+
(0, node_test_1.it)('returns true when stage_status.audit is "pass"', () => {
|
|
969
|
+
const { statePath, cleanup } = createTestStateDir({ audit: 'pass' });
|
|
970
|
+
try {
|
|
971
|
+
assert.strictEqual((0, audit_js_1.checkAuditGate)(statePath), true);
|
|
972
|
+
}
|
|
973
|
+
finally {
|
|
974
|
+
cleanup();
|
|
975
|
+
}
|
|
976
|
+
});
|
|
977
|
+
(0, node_test_1.it)('returns false when stage_status.audit is "fail"', () => {
|
|
978
|
+
const { statePath, cleanup } = createTestStateDir({ audit: 'fail' });
|
|
979
|
+
try {
|
|
980
|
+
assert.strictEqual((0, audit_js_1.checkAuditGate)(statePath), false);
|
|
981
|
+
}
|
|
982
|
+
finally {
|
|
983
|
+
cleanup();
|
|
984
|
+
}
|
|
985
|
+
});
|
|
986
|
+
(0, node_test_1.it)('returns false when stage_status.audit is "not_started"', () => {
|
|
987
|
+
const { statePath, cleanup } = createTestStateDir({ audit: 'not_started' });
|
|
988
|
+
try {
|
|
989
|
+
assert.strictEqual((0, audit_js_1.checkAuditGate)(statePath), false);
|
|
990
|
+
}
|
|
991
|
+
finally {
|
|
992
|
+
cleanup();
|
|
993
|
+
}
|
|
994
|
+
});
|
|
995
|
+
(0, node_test_1.it)('returns false when stage_status.audit is "in_progress"', () => {
|
|
996
|
+
const { statePath, cleanup } = createTestStateDir({ audit: 'in_progress' });
|
|
997
|
+
try {
|
|
998
|
+
assert.strictEqual((0, audit_js_1.checkAuditGate)(statePath), false);
|
|
999
|
+
}
|
|
1000
|
+
finally {
|
|
1001
|
+
cleanup();
|
|
1002
|
+
}
|
|
1003
|
+
});
|
|
1004
|
+
(0, node_test_1.it)('returns false when STATE.json does not exist (fail-closed)', () => {
|
|
1005
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'audit-gate-'));
|
|
1006
|
+
try {
|
|
1007
|
+
assert.strictEqual((0, audit_js_1.checkAuditGate)(path.join(tmpDir, 'nonexistent.json')), false);
|
|
1008
|
+
}
|
|
1009
|
+
finally {
|
|
1010
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
1011
|
+
}
|
|
1012
|
+
});
|
|
1013
|
+
});
|
|
1014
|
+
// ─── updateAuditState ───────────────────────────────────────────────────────
|
|
1015
|
+
(0, node_test_1.describe)('updateAuditState', () => {
|
|
1016
|
+
(0, node_test_1.it)('sets stage_status.audit to "pass" when passed=true', () => {
|
|
1017
|
+
const { statePath, cleanup } = createTestStateDir();
|
|
1018
|
+
try {
|
|
1019
|
+
(0, audit_js_1.updateAuditState)(statePath, true);
|
|
1020
|
+
const content = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
|
|
1021
|
+
assert.strictEqual(content.stage_status.audit, 'pass');
|
|
1022
|
+
}
|
|
1023
|
+
finally {
|
|
1024
|
+
cleanup();
|
|
1025
|
+
}
|
|
1026
|
+
});
|
|
1027
|
+
(0, node_test_1.it)('sets stage_status.audit to "fail" when passed=false', () => {
|
|
1028
|
+
const { statePath, cleanup } = createTestStateDir();
|
|
1029
|
+
try {
|
|
1030
|
+
(0, audit_js_1.updateAuditState)(statePath, false);
|
|
1031
|
+
const content = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
|
|
1032
|
+
assert.strictEqual(content.stage_status.audit, 'fail');
|
|
1033
|
+
}
|
|
1034
|
+
finally {
|
|
1035
|
+
cleanup();
|
|
1036
|
+
}
|
|
1037
|
+
});
|
|
1038
|
+
(0, node_test_1.it)('updates stage field to "audit" on pass', () => {
|
|
1039
|
+
const { statePath, cleanup } = createTestStateDir();
|
|
1040
|
+
try {
|
|
1041
|
+
(0, audit_js_1.updateAuditState)(statePath, true);
|
|
1042
|
+
const content = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
|
|
1043
|
+
assert.strictEqual(content.stage, 'audit');
|
|
1044
|
+
}
|
|
1045
|
+
finally {
|
|
1046
|
+
cleanup();
|
|
1047
|
+
}
|
|
1048
|
+
});
|
|
1049
|
+
});
|
|
1050
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1051
|
+
// Plan 04-03: Workflow Orchestrator
|
|
1052
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1053
|
+
// ─── Helper: create tmp planning dir with fixture files ──────────────────────
|
|
1054
|
+
function createTestPlanningDir(fixtures) {
|
|
1055
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'audit-test-'));
|
|
1056
|
+
const gswdDir = path.join(dir, 'gswd');
|
|
1057
|
+
fs.mkdirSync(gswdDir, { recursive: true });
|
|
1058
|
+
// Write fixture files
|
|
1059
|
+
for (const [name, content] of Object.entries(fixtures)) {
|
|
1060
|
+
fs.writeFileSync(path.join(dir, name), content, 'utf-8');
|
|
1061
|
+
}
|
|
1062
|
+
// Create default STATE.json
|
|
1063
|
+
const statePath = path.join(gswdDir, 'STATE.json');
|
|
1064
|
+
const state = (0, state_js_1.createDefaultState)('test-project');
|
|
1065
|
+
fs.writeFileSync(statePath, JSON.stringify(state, null, 2), 'utf-8');
|
|
1066
|
+
return {
|
|
1067
|
+
dir,
|
|
1068
|
+
statePath,
|
|
1069
|
+
cleanup: () => fs.rmSync(dir, { recursive: true, force: true }),
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
// ─── readArtifactFiles ───────────────────────────────────────────────────────
|
|
1073
|
+
(0, node_test_1.describe)('readArtifactFiles', () => {
|
|
1074
|
+
(0, node_test_1.it)('reads all 4 required artifact files from directory', () => {
|
|
1075
|
+
const { dir, cleanup } = createTestPlanningDir({
|
|
1076
|
+
'JOURNEYS.md': MINIMAL_JOURNEYS,
|
|
1077
|
+
'SPEC.md': MINIMAL_SPEC,
|
|
1078
|
+
'NFR.md': MINIMAL_NFR,
|
|
1079
|
+
'INTEGRATIONS.md': MINIMAL_INTEGRATIONS,
|
|
1080
|
+
});
|
|
1081
|
+
try {
|
|
1082
|
+
const files = (0, audit_js_1.readArtifactFiles)(dir);
|
|
1083
|
+
assert.ok(files.journeys.length > 0);
|
|
1084
|
+
assert.ok(files.spec.length > 0);
|
|
1085
|
+
assert.ok(files.nfr.length > 0);
|
|
1086
|
+
assert.ok(files.integrations.length > 0);
|
|
1087
|
+
}
|
|
1088
|
+
finally {
|
|
1089
|
+
cleanup();
|
|
1090
|
+
}
|
|
1091
|
+
});
|
|
1092
|
+
(0, node_test_1.it)('missing file returns empty string (not error)', () => {
|
|
1093
|
+
const { dir, cleanup } = createTestPlanningDir({
|
|
1094
|
+
'JOURNEYS.md': MINIMAL_JOURNEYS,
|
|
1095
|
+
// SPEC.md, NFR.md, INTEGRATIONS.md not created
|
|
1096
|
+
});
|
|
1097
|
+
try {
|
|
1098
|
+
const files = (0, audit_js_1.readArtifactFiles)(dir);
|
|
1099
|
+
assert.strictEqual(files.spec, '');
|
|
1100
|
+
assert.strictEqual(files.nfr, '');
|
|
1101
|
+
assert.strictEqual(files.integrations, '');
|
|
1102
|
+
}
|
|
1103
|
+
finally {
|
|
1104
|
+
cleanup();
|
|
1105
|
+
}
|
|
1106
|
+
});
|
|
1107
|
+
(0, node_test_1.it)('returns correct content for each file', () => {
|
|
1108
|
+
const { dir, cleanup } = createTestPlanningDir({
|
|
1109
|
+
'JOURNEYS.md': MINIMAL_JOURNEYS,
|
|
1110
|
+
'SPEC.md': MINIMAL_SPEC,
|
|
1111
|
+
});
|
|
1112
|
+
try {
|
|
1113
|
+
const files = (0, audit_js_1.readArtifactFiles)(dir);
|
|
1114
|
+
assert.strictEqual(files.journeys, MINIMAL_JOURNEYS);
|
|
1115
|
+
assert.strictEqual(files.spec, MINIMAL_SPEC);
|
|
1116
|
+
}
|
|
1117
|
+
finally {
|
|
1118
|
+
cleanup();
|
|
1119
|
+
}
|
|
1120
|
+
});
|
|
1121
|
+
});
|
|
1122
|
+
// ─── writeAuditReport ────────────────────────────────────────────────────────
|
|
1123
|
+
(0, node_test_1.describe)('writeAuditReport', () => {
|
|
1124
|
+
(0, node_test_1.it)('writes AUDIT.md to specified directory', () => {
|
|
1125
|
+
const { dir, cleanup } = createTestPlanningDir({});
|
|
1126
|
+
try {
|
|
1127
|
+
(0, audit_js_1.writeAuditReport)(dir, '# Audit Report\n**Result:** PASS');
|
|
1128
|
+
assert.ok(fs.existsSync(path.join(dir, 'AUDIT.md')));
|
|
1129
|
+
}
|
|
1130
|
+
finally {
|
|
1131
|
+
cleanup();
|
|
1132
|
+
}
|
|
1133
|
+
});
|
|
1134
|
+
(0, node_test_1.it)('file content matches input', () => {
|
|
1135
|
+
const { dir, cleanup } = createTestPlanningDir({});
|
|
1136
|
+
const content = '# Audit Report\n**Result:** PASS\nSome details.';
|
|
1137
|
+
try {
|
|
1138
|
+
(0, audit_js_1.writeAuditReport)(dir, content);
|
|
1139
|
+
const written = fs.readFileSync(path.join(dir, 'AUDIT.md'), 'utf-8');
|
|
1140
|
+
assert.strictEqual(written, content);
|
|
1141
|
+
}
|
|
1142
|
+
finally {
|
|
1143
|
+
cleanup();
|
|
1144
|
+
}
|
|
1145
|
+
});
|
|
1146
|
+
(0, node_test_1.it)('returns correct path', () => {
|
|
1147
|
+
const { dir, cleanup } = createTestPlanningDir({});
|
|
1148
|
+
try {
|
|
1149
|
+
const returned = (0, audit_js_1.writeAuditReport)(dir, '# Report');
|
|
1150
|
+
assert.strictEqual(returned, path.join(dir, 'AUDIT.md'));
|
|
1151
|
+
}
|
|
1152
|
+
finally {
|
|
1153
|
+
cleanup();
|
|
1154
|
+
}
|
|
1155
|
+
});
|
|
1156
|
+
});
|
|
1157
|
+
// ─── runAuditWorkflow — passing ──────────────────────────────────────────────
|
|
1158
|
+
(0, node_test_1.describe)('runAuditWorkflow — passing', () => {
|
|
1159
|
+
(0, node_test_1.it)('full passing spec bundle -> passed: true', () => {
|
|
1160
|
+
const { dir, statePath, cleanup } = createTestPlanningDir({
|
|
1161
|
+
'JOURNEYS.md': MINIMAL_JOURNEYS,
|
|
1162
|
+
'SPEC.md': MINIMAL_SPEC,
|
|
1163
|
+
'NFR.md': MINIMAL_NFR,
|
|
1164
|
+
'INTEGRATIONS.md': MINIMAL_INTEGRATIONS,
|
|
1165
|
+
});
|
|
1166
|
+
try {
|
|
1167
|
+
const result = (0, audit_js_1.runAuditWorkflow)({ planningDir: dir, statePath });
|
|
1168
|
+
assert.strictEqual(result.passed, true);
|
|
1169
|
+
}
|
|
1170
|
+
finally {
|
|
1171
|
+
cleanup();
|
|
1172
|
+
}
|
|
1173
|
+
});
|
|
1174
|
+
(0, node_test_1.it)('AUDIT.md written to planning dir', () => {
|
|
1175
|
+
const { dir, statePath, cleanup } = createTestPlanningDir({
|
|
1176
|
+
'JOURNEYS.md': MINIMAL_JOURNEYS,
|
|
1177
|
+
'SPEC.md': MINIMAL_SPEC,
|
|
1178
|
+
'NFR.md': MINIMAL_NFR,
|
|
1179
|
+
'INTEGRATIONS.md': MINIMAL_INTEGRATIONS,
|
|
1180
|
+
});
|
|
1181
|
+
try {
|
|
1182
|
+
(0, audit_js_1.runAuditWorkflow)({ planningDir: dir, statePath });
|
|
1183
|
+
assert.ok(fs.existsSync(path.join(dir, 'AUDIT.md')));
|
|
1184
|
+
}
|
|
1185
|
+
finally {
|
|
1186
|
+
cleanup();
|
|
1187
|
+
}
|
|
1188
|
+
});
|
|
1189
|
+
(0, node_test_1.it)('STATE.json updated to audit: "pass"', () => {
|
|
1190
|
+
const { dir, statePath, cleanup } = createTestPlanningDir({
|
|
1191
|
+
'JOURNEYS.md': MINIMAL_JOURNEYS,
|
|
1192
|
+
'SPEC.md': MINIMAL_SPEC,
|
|
1193
|
+
'NFR.md': MINIMAL_NFR,
|
|
1194
|
+
'INTEGRATIONS.md': MINIMAL_INTEGRATIONS,
|
|
1195
|
+
});
|
|
1196
|
+
try {
|
|
1197
|
+
(0, audit_js_1.runAuditWorkflow)({ planningDir: dir, statePath });
|
|
1198
|
+
const state = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
|
|
1199
|
+
assert.strictEqual(state.stage_status.audit, 'pass');
|
|
1200
|
+
}
|
|
1201
|
+
finally {
|
|
1202
|
+
cleanup();
|
|
1203
|
+
}
|
|
1204
|
+
});
|
|
1205
|
+
(0, node_test_1.it)('autoFixCycles: 0 when passing without auto-fix', () => {
|
|
1206
|
+
const { dir, statePath, cleanup } = createTestPlanningDir({
|
|
1207
|
+
'JOURNEYS.md': MINIMAL_JOURNEYS,
|
|
1208
|
+
'SPEC.md': MINIMAL_SPEC,
|
|
1209
|
+
'NFR.md': MINIMAL_NFR,
|
|
1210
|
+
'INTEGRATIONS.md': MINIMAL_INTEGRATIONS,
|
|
1211
|
+
});
|
|
1212
|
+
try {
|
|
1213
|
+
const result = (0, audit_js_1.runAuditWorkflow)({ planningDir: dir, statePath });
|
|
1214
|
+
assert.strictEqual(result.autoFixCycles, 0);
|
|
1215
|
+
}
|
|
1216
|
+
finally {
|
|
1217
|
+
cleanup();
|
|
1218
|
+
}
|
|
1219
|
+
});
|
|
1220
|
+
});
|
|
1221
|
+
// ─── runAuditWorkflow — failing without auto-fix ─────────────────────────────
|
|
1222
|
+
(0, node_test_1.describe)('runAuditWorkflow — failing without auto-fix', () => {
|
|
1223
|
+
(0, node_test_1.it)('spec with orphan FR -> passed: false', () => {
|
|
1224
|
+
const specWithOrphan = MINIMAL_SPEC.replace('## Acceptance Criteria', '### FR-099: Orphan\n**Scope:** v1\n\n## Acceptance Criteria');
|
|
1225
|
+
const { dir, statePath, cleanup } = createTestPlanningDir({
|
|
1226
|
+
'JOURNEYS.md': MINIMAL_JOURNEYS,
|
|
1227
|
+
'SPEC.md': specWithOrphan,
|
|
1228
|
+
'NFR.md': MINIMAL_NFR,
|
|
1229
|
+
'INTEGRATIONS.md': MINIMAL_INTEGRATIONS,
|
|
1230
|
+
});
|
|
1231
|
+
try {
|
|
1232
|
+
const result = (0, audit_js_1.runAuditWorkflow)({ planningDir: dir, statePath });
|
|
1233
|
+
assert.strictEqual(result.passed, false);
|
|
1234
|
+
}
|
|
1235
|
+
finally {
|
|
1236
|
+
cleanup();
|
|
1237
|
+
}
|
|
1238
|
+
});
|
|
1239
|
+
(0, node_test_1.it)('AUDIT.md written with FAIL report', () => {
|
|
1240
|
+
const specWithOrphan = MINIMAL_SPEC.replace('## Acceptance Criteria', '### FR-099: Orphan\n**Scope:** v1\n\n## Acceptance Criteria');
|
|
1241
|
+
const { dir, statePath, cleanup } = createTestPlanningDir({
|
|
1242
|
+
'JOURNEYS.md': MINIMAL_JOURNEYS,
|
|
1243
|
+
'SPEC.md': specWithOrphan,
|
|
1244
|
+
'NFR.md': MINIMAL_NFR,
|
|
1245
|
+
'INTEGRATIONS.md': MINIMAL_INTEGRATIONS,
|
|
1246
|
+
});
|
|
1247
|
+
try {
|
|
1248
|
+
(0, audit_js_1.runAuditWorkflow)({ planningDir: dir, statePath });
|
|
1249
|
+
const auditMd = fs.readFileSync(path.join(dir, 'AUDIT.md'), 'utf-8');
|
|
1250
|
+
assert.ok(auditMd.includes('**Result:** FAIL'));
|
|
1251
|
+
}
|
|
1252
|
+
finally {
|
|
1253
|
+
cleanup();
|
|
1254
|
+
}
|
|
1255
|
+
});
|
|
1256
|
+
(0, node_test_1.it)('STATE.json updated to audit: "fail"', () => {
|
|
1257
|
+
const specWithOrphan = MINIMAL_SPEC.replace('## Acceptance Criteria', '### FR-099: Orphan\n**Scope:** v1\n\n## Acceptance Criteria');
|
|
1258
|
+
const { dir, statePath, cleanup } = createTestPlanningDir({
|
|
1259
|
+
'JOURNEYS.md': MINIMAL_JOURNEYS,
|
|
1260
|
+
'SPEC.md': specWithOrphan,
|
|
1261
|
+
'NFR.md': MINIMAL_NFR,
|
|
1262
|
+
'INTEGRATIONS.md': MINIMAL_INTEGRATIONS,
|
|
1263
|
+
});
|
|
1264
|
+
try {
|
|
1265
|
+
(0, audit_js_1.runAuditWorkflow)({ planningDir: dir, statePath });
|
|
1266
|
+
const state = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
|
|
1267
|
+
assert.strictEqual(state.stage_status.audit, 'fail');
|
|
1268
|
+
}
|
|
1269
|
+
finally {
|
|
1270
|
+
cleanup();
|
|
1271
|
+
}
|
|
1272
|
+
});
|
|
1273
|
+
(0, node_test_1.it)('autoFixCycles: 0 when failing without auto-fix', () => {
|
|
1274
|
+
const specWithOrphan = MINIMAL_SPEC.replace('## Acceptance Criteria', '### FR-099: Orphan\n**Scope:** v1\n\n## Acceptance Criteria');
|
|
1275
|
+
const { dir, statePath, cleanup } = createTestPlanningDir({
|
|
1276
|
+
'JOURNEYS.md': MINIMAL_JOURNEYS,
|
|
1277
|
+
'SPEC.md': specWithOrphan,
|
|
1278
|
+
'NFR.md': MINIMAL_NFR,
|
|
1279
|
+
'INTEGRATIONS.md': MINIMAL_INTEGRATIONS,
|
|
1280
|
+
});
|
|
1281
|
+
try {
|
|
1282
|
+
const result = (0, audit_js_1.runAuditWorkflow)({ planningDir: dir, statePath });
|
|
1283
|
+
assert.strictEqual(result.autoFixCycles, 0);
|
|
1284
|
+
}
|
|
1285
|
+
finally {
|
|
1286
|
+
cleanup();
|
|
1287
|
+
}
|
|
1288
|
+
});
|
|
1289
|
+
});
|
|
1290
|
+
// ─── runAuditWorkflow — auto-fix success ─────────────────────────────────────
|
|
1291
|
+
(0, node_test_1.describe)('runAuditWorkflow — auto-fix success', () => {
|
|
1292
|
+
(0, node_test_1.it)('spec with missing test + autoFix: true -> passes after auto-fix', () => {
|
|
1293
|
+
// Journey without acceptance tests (fixable by auto-fix)
|
|
1294
|
+
const journeysNoTests = `## Journeys\n\n### J-001: Onboarding\nReferences FR-001 and FR-002.\n\n### J-002: Core Action\nReferences FR-003.\nUses I-001.\n\n#### Acceptance Tests\n- User can perform core action successfully\n`;
|
|
1295
|
+
const { dir, statePath, cleanup } = createTestPlanningDir({
|
|
1296
|
+
'JOURNEYS.md': journeysNoTests,
|
|
1297
|
+
'SPEC.md': MINIMAL_SPEC,
|
|
1298
|
+
'NFR.md': MINIMAL_NFR,
|
|
1299
|
+
'INTEGRATIONS.md': MINIMAL_INTEGRATIONS,
|
|
1300
|
+
});
|
|
1301
|
+
try {
|
|
1302
|
+
const result = (0, audit_js_1.runAuditWorkflow)({ planningDir: dir, statePath, autoFix: true });
|
|
1303
|
+
// After auto-fix adds stub test, J-001 should have acceptance test
|
|
1304
|
+
assert.strictEqual(result.autoFixCycles, 1);
|
|
1305
|
+
}
|
|
1306
|
+
finally {
|
|
1307
|
+
cleanup();
|
|
1308
|
+
}
|
|
1309
|
+
});
|
|
1310
|
+
(0, node_test_1.it)('autoFixChanges contains the stub addition', () => {
|
|
1311
|
+
const journeysNoTests = `## Journeys\n\n### J-001: Onboarding\nReferences FR-001 and FR-002.\n\n### J-002: Core Action\nReferences FR-003.\nUses I-001.\n\n#### Acceptance Tests\n- User can perform core action successfully\n`;
|
|
1312
|
+
const { dir, statePath, cleanup } = createTestPlanningDir({
|
|
1313
|
+
'JOURNEYS.md': journeysNoTests,
|
|
1314
|
+
'SPEC.md': MINIMAL_SPEC,
|
|
1315
|
+
'NFR.md': MINIMAL_NFR,
|
|
1316
|
+
'INTEGRATIONS.md': MINIMAL_INTEGRATIONS,
|
|
1317
|
+
});
|
|
1318
|
+
try {
|
|
1319
|
+
const result = (0, audit_js_1.runAuditWorkflow)({ planningDir: dir, statePath, autoFix: true });
|
|
1320
|
+
assert.ok(result.autoFixChanges.length > 0);
|
|
1321
|
+
assert.ok(result.autoFixChanges.some((c) => c.change.includes('stub acceptance test')));
|
|
1322
|
+
}
|
|
1323
|
+
finally {
|
|
1324
|
+
cleanup();
|
|
1325
|
+
}
|
|
1326
|
+
});
|
|
1327
|
+
});
|
|
1328
|
+
// ─── runAuditWorkflow — auto-fix exhausted ───────────────────────────────────
|
|
1329
|
+
(0, node_test_1.describe)('runAuditWorkflow — auto-fix exhausted', () => {
|
|
1330
|
+
(0, node_test_1.it)('unfixable issue (orphan FR) + autoFix: true -> still FAIL after max cycles', () => {
|
|
1331
|
+
const specWithOrphan = MINIMAL_SPEC.replace('## Acceptance Criteria', '### FR-099: Orphan\n**Scope:** v1\n\n## Acceptance Criteria');
|
|
1332
|
+
const { dir, statePath, cleanup } = createTestPlanningDir({
|
|
1333
|
+
'JOURNEYS.md': MINIMAL_JOURNEYS,
|
|
1334
|
+
'SPEC.md': specWithOrphan,
|
|
1335
|
+
'NFR.md': MINIMAL_NFR,
|
|
1336
|
+
'INTEGRATIONS.md': MINIMAL_INTEGRATIONS,
|
|
1337
|
+
});
|
|
1338
|
+
try {
|
|
1339
|
+
const result = (0, audit_js_1.runAuditWorkflow)({
|
|
1340
|
+
planningDir: dir,
|
|
1341
|
+
statePath,
|
|
1342
|
+
autoFix: true,
|
|
1343
|
+
maxCycles: 2,
|
|
1344
|
+
});
|
|
1345
|
+
assert.strictEqual(result.passed, false);
|
|
1346
|
+
}
|
|
1347
|
+
finally {
|
|
1348
|
+
cleanup();
|
|
1349
|
+
}
|
|
1350
|
+
});
|
|
1351
|
+
(0, node_test_1.it)('auto-fix cycles reach maxCycles when issue is not fixable', () => {
|
|
1352
|
+
const specWithOrphan = MINIMAL_SPEC.replace('## Acceptance Criteria', '### FR-099: Orphan\n**Scope:** v1\n\n## Acceptance Criteria');
|
|
1353
|
+
const { dir, statePath, cleanup } = createTestPlanningDir({
|
|
1354
|
+
'JOURNEYS.md': MINIMAL_JOURNEYS,
|
|
1355
|
+
'SPEC.md': specWithOrphan,
|
|
1356
|
+
'NFR.md': MINIMAL_NFR,
|
|
1357
|
+
'INTEGRATIONS.md': MINIMAL_INTEGRATIONS,
|
|
1358
|
+
});
|
|
1359
|
+
try {
|
|
1360
|
+
const result = (0, audit_js_1.runAuditWorkflow)({
|
|
1361
|
+
planningDir: dir,
|
|
1362
|
+
statePath,
|
|
1363
|
+
autoFix: true,
|
|
1364
|
+
maxCycles: 2,
|
|
1365
|
+
});
|
|
1366
|
+
assert.strictEqual(result.autoFixCycles, 2);
|
|
1367
|
+
}
|
|
1368
|
+
finally {
|
|
1369
|
+
cleanup();
|
|
1370
|
+
}
|
|
1371
|
+
});
|
|
1372
|
+
});
|
|
1373
|
+
// ─── State transitions ───────────────────────────────────────────────────────
|
|
1374
|
+
(0, node_test_1.describe)('State transitions', () => {
|
|
1375
|
+
(0, node_test_1.it)('state goes from not_started to in_progress during audit (checkpoint)', () => {
|
|
1376
|
+
// We verify this via the final state (in_progress is transient)
|
|
1377
|
+
// After completion, state should be pass or fail, not in_progress
|
|
1378
|
+
const { dir, statePath, cleanup } = createTestPlanningDir({
|
|
1379
|
+
'JOURNEYS.md': MINIMAL_JOURNEYS,
|
|
1380
|
+
'SPEC.md': MINIMAL_SPEC,
|
|
1381
|
+
'NFR.md': MINIMAL_NFR,
|
|
1382
|
+
'INTEGRATIONS.md': MINIMAL_INTEGRATIONS,
|
|
1383
|
+
});
|
|
1384
|
+
try {
|
|
1385
|
+
// Before workflow, state is not_started
|
|
1386
|
+
const before = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
|
|
1387
|
+
assert.strictEqual(before.stage_status.audit, 'not_started');
|
|
1388
|
+
(0, audit_js_1.runAuditWorkflow)({ planningDir: dir, statePath });
|
|
1389
|
+
// After workflow, state has transitioned
|
|
1390
|
+
const after = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
|
|
1391
|
+
assert.ok(['pass', 'fail'].includes(after.stage_status.audit));
|
|
1392
|
+
}
|
|
1393
|
+
finally {
|
|
1394
|
+
cleanup();
|
|
1395
|
+
}
|
|
1396
|
+
});
|
|
1397
|
+
(0, node_test_1.it)('state goes from in_progress to pass on successful audit', () => {
|
|
1398
|
+
const { dir, statePath, cleanup } = createTestPlanningDir({
|
|
1399
|
+
'JOURNEYS.md': MINIMAL_JOURNEYS,
|
|
1400
|
+
'SPEC.md': MINIMAL_SPEC,
|
|
1401
|
+
'NFR.md': MINIMAL_NFR,
|
|
1402
|
+
'INTEGRATIONS.md': MINIMAL_INTEGRATIONS,
|
|
1403
|
+
});
|
|
1404
|
+
try {
|
|
1405
|
+
(0, audit_js_1.runAuditWorkflow)({ planningDir: dir, statePath });
|
|
1406
|
+
const state = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
|
|
1407
|
+
assert.strictEqual(state.stage_status.audit, 'pass');
|
|
1408
|
+
}
|
|
1409
|
+
finally {
|
|
1410
|
+
cleanup();
|
|
1411
|
+
}
|
|
1412
|
+
});
|
|
1413
|
+
(0, node_test_1.it)('state goes from in_progress to fail on failed audit', () => {
|
|
1414
|
+
const specWithOrphan = MINIMAL_SPEC.replace('## Acceptance Criteria', '### FR-099: Orphan\n**Scope:** v1\n\n## Acceptance Criteria');
|
|
1415
|
+
const { dir, statePath, cleanup } = createTestPlanningDir({
|
|
1416
|
+
'JOURNEYS.md': MINIMAL_JOURNEYS,
|
|
1417
|
+
'SPEC.md': specWithOrphan,
|
|
1418
|
+
'NFR.md': MINIMAL_NFR,
|
|
1419
|
+
'INTEGRATIONS.md': MINIMAL_INTEGRATIONS,
|
|
1420
|
+
});
|
|
1421
|
+
try {
|
|
1422
|
+
(0, audit_js_1.runAuditWorkflow)({ planningDir: dir, statePath });
|
|
1423
|
+
const state = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
|
|
1424
|
+
assert.strictEqual(state.stage_status.audit, 'fail');
|
|
1425
|
+
}
|
|
1426
|
+
finally {
|
|
1427
|
+
cleanup();
|
|
1428
|
+
}
|
|
1429
|
+
});
|
|
1430
|
+
(0, node_test_1.it)('checkpoint written with workflow: "gswd/audit-spec"', () => {
|
|
1431
|
+
const { dir, statePath, cleanup } = createTestPlanningDir({
|
|
1432
|
+
'JOURNEYS.md': MINIMAL_JOURNEYS,
|
|
1433
|
+
'SPEC.md': MINIMAL_SPEC,
|
|
1434
|
+
'NFR.md': MINIMAL_NFR,
|
|
1435
|
+
'INTEGRATIONS.md': MINIMAL_INTEGRATIONS,
|
|
1436
|
+
});
|
|
1437
|
+
try {
|
|
1438
|
+
(0, audit_js_1.runAuditWorkflow)({ planningDir: dir, statePath });
|
|
1439
|
+
const state = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
|
|
1440
|
+
assert.ok(state.last_checkpoint);
|
|
1441
|
+
assert.strictEqual(state.last_checkpoint.workflow, 'gswd/audit-spec');
|
|
1442
|
+
}
|
|
1443
|
+
finally {
|
|
1444
|
+
cleanup();
|
|
1445
|
+
}
|
|
1446
|
+
});
|
|
1447
|
+
});
|
|
1448
|
+
// ─── getAuditableFileTypes ────────────────────────────────────────────────────
|
|
1449
|
+
(0, node_test_1.describe)('getAuditableFileTypes', () => {
|
|
1450
|
+
(0, node_test_1.it)('returns array of 4 file names', () => {
|
|
1451
|
+
const types = (0, audit_js_1.getAuditableFileTypes)();
|
|
1452
|
+
assert.strictEqual(types.length, 4);
|
|
1453
|
+
});
|
|
1454
|
+
(0, node_test_1.it)('includes JOURNEYS.md, SPEC.md, NFR.md, INTEGRATIONS.md', () => {
|
|
1455
|
+
const types = (0, audit_js_1.getAuditableFileTypes)();
|
|
1456
|
+
assert.ok(types.includes('JOURNEYS.md'));
|
|
1457
|
+
assert.ok(types.includes('SPEC.md'));
|
|
1458
|
+
assert.ok(types.includes('NFR.md'));
|
|
1459
|
+
assert.ok(types.includes('INTEGRATIONS.md'));
|
|
1460
|
+
});
|
|
1461
|
+
});
|
|
1462
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1463
|
+
// Plan 04-03 Task 2: AUDIT.md template and heading validation
|
|
1464
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1465
|
+
// ─── End-to-end integration test ────────────────────────────────────────────
|
|
1466
|
+
(0, node_test_1.describe)('End-to-end integration: full spec bundle to AUDIT.md', () => {
|
|
1467
|
+
(0, node_test_1.it)('runAuditWorkflow with minimal complete spec bundle writes AUDIT.md', () => {
|
|
1468
|
+
const { dir, statePath, cleanup } = createTestPlanningDir({
|
|
1469
|
+
'JOURNEYS.md': MINIMAL_JOURNEYS,
|
|
1470
|
+
'SPEC.md': MINIMAL_SPEC,
|
|
1471
|
+
'NFR.md': MINIMAL_NFR,
|
|
1472
|
+
'INTEGRATIONS.md': MINIMAL_INTEGRATIONS,
|
|
1473
|
+
});
|
|
1474
|
+
try {
|
|
1475
|
+
(0, audit_js_1.runAuditWorkflow)({ planningDir: dir, statePath });
|
|
1476
|
+
assert.ok(fs.existsSync(path.join(dir, 'AUDIT.md')));
|
|
1477
|
+
}
|
|
1478
|
+
finally {
|
|
1479
|
+
cleanup();
|
|
1480
|
+
}
|
|
1481
|
+
});
|
|
1482
|
+
(0, node_test_1.it)('generated AUDIT.md content passes heading validation for AUDIT.md type', () => {
|
|
1483
|
+
const { dir, statePath, cleanup } = createTestPlanningDir({
|
|
1484
|
+
'JOURNEYS.md': MINIMAL_JOURNEYS,
|
|
1485
|
+
'SPEC.md': MINIMAL_SPEC,
|
|
1486
|
+
'NFR.md': MINIMAL_NFR,
|
|
1487
|
+
'INTEGRATIONS.md': MINIMAL_INTEGRATIONS,
|
|
1488
|
+
});
|
|
1489
|
+
try {
|
|
1490
|
+
(0, audit_js_1.runAuditWorkflow)({ planningDir: dir, statePath });
|
|
1491
|
+
const auditMd = fs.readFileSync(path.join(dir, 'AUDIT.md'), 'utf-8');
|
|
1492
|
+
const validation = (0, parse_js_1.validateHeadings)(auditMd, 'AUDIT.md');
|
|
1493
|
+
assert.strictEqual(validation.valid, true, `Missing headings: ${validation.missing.join(', ')}`);
|
|
1494
|
+
}
|
|
1495
|
+
finally {
|
|
1496
|
+
cleanup();
|
|
1497
|
+
}
|
|
1498
|
+
});
|
|
1499
|
+
(0, node_test_1.it)('STATE.json updated after workflow completes', () => {
|
|
1500
|
+
const { dir, statePath, cleanup } = createTestPlanningDir({
|
|
1501
|
+
'JOURNEYS.md': MINIMAL_JOURNEYS,
|
|
1502
|
+
'SPEC.md': MINIMAL_SPEC,
|
|
1503
|
+
'NFR.md': MINIMAL_NFR,
|
|
1504
|
+
'INTEGRATIONS.md': MINIMAL_INTEGRATIONS,
|
|
1505
|
+
});
|
|
1506
|
+
try {
|
|
1507
|
+
(0, audit_js_1.runAuditWorkflow)({ planningDir: dir, statePath });
|
|
1508
|
+
const state = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
|
|
1509
|
+
assert.ok(['pass', 'fail'].includes(state.stage_status.audit));
|
|
1510
|
+
}
|
|
1511
|
+
finally {
|
|
1512
|
+
cleanup();
|
|
1513
|
+
}
|
|
1514
|
+
});
|
|
1515
|
+
});
|
|
1516
|
+
// ─── AUDIT.md heading validation ────────────────────────────────────────────
|
|
1517
|
+
(0, node_test_1.describe)('AUDIT.md heading validation (self-consistency)', () => {
|
|
1518
|
+
(0, node_test_1.it)('generated AUDIT.md from PASS result passes validateHeadings for AUDIT.md', () => {
|
|
1519
|
+
const result = (0, audit_js_1.runAudit)({
|
|
1520
|
+
journeys: MINIMAL_JOURNEYS,
|
|
1521
|
+
spec: MINIMAL_SPEC,
|
|
1522
|
+
nfr: MINIMAL_NFR,
|
|
1523
|
+
integrations: MINIMAL_INTEGRATIONS,
|
|
1524
|
+
});
|
|
1525
|
+
assert.strictEqual(result.passed, true);
|
|
1526
|
+
const report = (0, audit_js_1.generateAuditReport)(result);
|
|
1527
|
+
const validation = (0, parse_js_1.validateHeadings)(report, 'AUDIT.md');
|
|
1528
|
+
assert.strictEqual(validation.valid, true, `Missing headings in PASS report: ${validation.missing.join(', ')}`);
|
|
1529
|
+
});
|
|
1530
|
+
(0, node_test_1.it)('generated AUDIT.md from FAIL result also passes validateHeadings for AUDIT.md', () => {
|
|
1531
|
+
const specWithOrphan = MINIMAL_SPEC.replace('## Acceptance Criteria', '### FR-099: Orphan\n**Scope:** v1\n\n## Acceptance Criteria');
|
|
1532
|
+
const result = (0, audit_js_1.runAudit)({
|
|
1533
|
+
journeys: MINIMAL_JOURNEYS,
|
|
1534
|
+
spec: specWithOrphan,
|
|
1535
|
+
nfr: MINIMAL_NFR,
|
|
1536
|
+
integrations: MINIMAL_INTEGRATIONS,
|
|
1537
|
+
});
|
|
1538
|
+
assert.strictEqual(result.passed, false);
|
|
1539
|
+
const report = (0, audit_js_1.generateAuditReport)(result);
|
|
1540
|
+
const validation = (0, parse_js_1.validateHeadings)(report, 'AUDIT.md');
|
|
1541
|
+
assert.strictEqual(validation.valid, true, `Missing headings in FAIL report: ${validation.missing.join(', ')}`);
|
|
1542
|
+
});
|
|
1543
|
+
});
|
|
1544
|
+
// ─── Phase 8: AUDT-05 ARCHITECTURE.md heading check ─────────────────────────
|
|
1545
|
+
(0, node_test_1.describe)('Phase 8: AUDT-05 ARCHITECTURE.md heading check', () => {
|
|
1546
|
+
(0, node_test_1.it)('ARCHITECTURE.md with missing headings fails required heading check', () => {
|
|
1547
|
+
// Content has ## Architecture but is missing ### Components and ### Data Model
|
|
1548
|
+
const result = (0, audit_js_1.runAudit)({
|
|
1549
|
+
journeys: MINIMAL_JOURNEYS,
|
|
1550
|
+
spec: MINIMAL_SPEC,
|
|
1551
|
+
nfr: MINIMAL_NFR,
|
|
1552
|
+
integrations: MINIMAL_INTEGRATIONS,
|
|
1553
|
+
architecture: '## Architecture\n\nIncomplete content — missing required subsections.',
|
|
1554
|
+
});
|
|
1555
|
+
// Should have a heading-related finding for ARCHITECTURE.md
|
|
1556
|
+
const headingCheck = result.checks.find((c) => c.check === 'required_headings');
|
|
1557
|
+
assert.ok(headingCheck, 'Required headings check should exist');
|
|
1558
|
+
const archFindings = headingCheck.findings.filter((f) => f.id === 'ARCHITECTURE.md');
|
|
1559
|
+
assert.ok(archFindings.length > 0, 'Should report missing ARCHITECTURE.md headings');
|
|
1560
|
+
// Confirm the specific missing heading is identified
|
|
1561
|
+
const findingText = archFindings.map((f) => f.issue).join(' ');
|
|
1562
|
+
assert.ok(findingText.includes('Components') || findingText.includes('Data Model'), `Should identify missing Components or Data Model heading, got: ${findingText}`);
|
|
1563
|
+
});
|
|
1564
|
+
(0, node_test_1.it)('ARCHITECTURE.md with all required headings passes heading check', () => {
|
|
1565
|
+
const validArch = '## Architecture\n\n### Components\n\n#### C-001: Core\n**Responsibility:** Main logic\n\n### Data Model\n\nEntities here.\n';
|
|
1566
|
+
const result = (0, audit_js_1.runAudit)({
|
|
1567
|
+
journeys: MINIMAL_JOURNEYS,
|
|
1568
|
+
spec: MINIMAL_SPEC,
|
|
1569
|
+
nfr: MINIMAL_NFR,
|
|
1570
|
+
integrations: MINIMAL_INTEGRATIONS,
|
|
1571
|
+
architecture: validArch,
|
|
1572
|
+
});
|
|
1573
|
+
const headingCheck = result.checks.find((c) => c.check === 'required_headings');
|
|
1574
|
+
assert.ok(headingCheck, 'Required headings check should exist');
|
|
1575
|
+
// No ARCHITECTURE.md findings when headings are valid
|
|
1576
|
+
const archFindings = headingCheck.findings.filter((f) => f.id === 'ARCHITECTURE.md');
|
|
1577
|
+
assert.strictEqual(archFindings.length, 0, 'Valid ARCHITECTURE.md should produce no heading findings');
|
|
1578
|
+
});
|
|
1579
|
+
});
|