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,285 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Parse module tests — ID extraction, normalization, heading validation
|
|
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 path = __importStar(require("node:path"));
|
|
43
|
+
const parse_js_1 = require("../lib/parse.js");
|
|
44
|
+
// ─── normalizeId ─────────────────────────────────────────────────────────────
|
|
45
|
+
(0, node_test_1.describe)('normalizeId', () => {
|
|
46
|
+
(0, node_test_1.it)('normalizes FR-1 to FR-001', () => {
|
|
47
|
+
assert.strictEqual((0, parse_js_1.normalizeId)('FR-1'), 'FR-001');
|
|
48
|
+
});
|
|
49
|
+
(0, node_test_1.it)('normalizes FR-01 to FR-001', () => {
|
|
50
|
+
assert.strictEqual((0, parse_js_1.normalizeId)('FR-01'), 'FR-001');
|
|
51
|
+
});
|
|
52
|
+
(0, node_test_1.it)('leaves FR-001 unchanged', () => {
|
|
53
|
+
assert.strictEqual((0, parse_js_1.normalizeId)('FR-001'), 'FR-001');
|
|
54
|
+
});
|
|
55
|
+
(0, node_test_1.it)('normalizes J-42 to J-042', () => {
|
|
56
|
+
assert.strictEqual((0, parse_js_1.normalizeId)('J-42'), 'J-042');
|
|
57
|
+
});
|
|
58
|
+
(0, node_test_1.it)('leaves NFR-100 unchanged (already 3 digits)', () => {
|
|
59
|
+
assert.strictEqual((0, parse_js_1.normalizeId)('NFR-100'), 'NFR-100');
|
|
60
|
+
});
|
|
61
|
+
(0, node_test_1.it)('keeps 4-digit numbers as-is (no truncation)', () => {
|
|
62
|
+
assert.strictEqual((0, parse_js_1.normalizeId)('NFR-1000'), 'NFR-1000');
|
|
63
|
+
});
|
|
64
|
+
(0, node_test_1.it)('returns unrecognized format as-is', () => {
|
|
65
|
+
assert.strictEqual((0, parse_js_1.normalizeId)('INVALID'), 'INVALID');
|
|
66
|
+
});
|
|
67
|
+
(0, node_test_1.it)('returns empty string as-is', () => {
|
|
68
|
+
assert.strictEqual((0, parse_js_1.normalizeId)(''), '');
|
|
69
|
+
});
|
|
70
|
+
(0, node_test_1.it)('normalizes C-5 to C-005', () => {
|
|
71
|
+
assert.strictEqual((0, parse_js_1.normalizeId)('C-5'), 'C-005');
|
|
72
|
+
});
|
|
73
|
+
(0, node_test_1.it)('normalizes I-99 to I-099', () => {
|
|
74
|
+
assert.strictEqual((0, parse_js_1.normalizeId)('I-99'), 'I-099');
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
// ─── extractIds ──────────────────────────────────────────────────────────────
|
|
78
|
+
(0, node_test_1.describe)('extractIds', () => {
|
|
79
|
+
(0, node_test_1.it)('extracts FR-001 from heading', () => {
|
|
80
|
+
const ids = (0, parse_js_1.extractIds)('### FR-001: Login');
|
|
81
|
+
assert.strictEqual(ids.length, 1);
|
|
82
|
+
assert.strictEqual(ids[0].id, 'FR-001');
|
|
83
|
+
});
|
|
84
|
+
(0, node_test_1.it)('extracts multiple IDs from mixed content', () => {
|
|
85
|
+
const content = `
|
|
86
|
+
### J-001: Onboarding
|
|
87
|
+
Links to FR-001 and FR-002.
|
|
88
|
+
Also see NFR-001 and I-001.
|
|
89
|
+
`;
|
|
90
|
+
const ids = (0, parse_js_1.extractIds)(content);
|
|
91
|
+
assert.strictEqual(ids.length, 5);
|
|
92
|
+
const idStrings = ids.map(i => i.id);
|
|
93
|
+
assert.ok(idStrings.includes('J-001'));
|
|
94
|
+
assert.ok(idStrings.includes('FR-001'));
|
|
95
|
+
assert.ok(idStrings.includes('FR-002'));
|
|
96
|
+
assert.ok(idStrings.includes('NFR-001'));
|
|
97
|
+
assert.ok(idStrings.includes('I-001'));
|
|
98
|
+
});
|
|
99
|
+
(0, node_test_1.it)('deduplicates: same ID appearing twice returns once', () => {
|
|
100
|
+
const content = 'See FR-001 and then again FR-001';
|
|
101
|
+
const ids = (0, parse_js_1.extractIds)(content);
|
|
102
|
+
assert.strictEqual(ids.length, 1);
|
|
103
|
+
});
|
|
104
|
+
(0, node_test_1.it)('sorts ascending by normalized ID', () => {
|
|
105
|
+
const content = 'FR-003 FR-001 FR-002';
|
|
106
|
+
const ids = (0, parse_js_1.extractIds)(content);
|
|
107
|
+
assert.strictEqual(ids[0].id, 'FR-001');
|
|
108
|
+
assert.strictEqual(ids[1].id, 'FR-002');
|
|
109
|
+
assert.strictEqual(ids[2].id, 'FR-003');
|
|
110
|
+
});
|
|
111
|
+
(0, node_test_1.it)('tracks normalization flag: FR-1 has normalized: true', () => {
|
|
112
|
+
const ids = (0, parse_js_1.extractIds)('FR-1');
|
|
113
|
+
assert.strictEqual(ids[0].normalized, true);
|
|
114
|
+
assert.strictEqual(ids[0].raw, 'FR-1');
|
|
115
|
+
assert.strictEqual(ids[0].id, 'FR-001');
|
|
116
|
+
});
|
|
117
|
+
(0, node_test_1.it)('tracks normalization flag: FR-001 has normalized: false', () => {
|
|
118
|
+
const ids = (0, parse_js_1.extractIds)('FR-001');
|
|
119
|
+
assert.strictEqual(ids[0].normalized, false);
|
|
120
|
+
});
|
|
121
|
+
(0, node_test_1.it)('filters by idType: only returns matching prefix', () => {
|
|
122
|
+
const content = 'FR-001 J-001 NFR-001';
|
|
123
|
+
const ids = (0, parse_js_1.extractIds)(content, 'FR');
|
|
124
|
+
assert.strictEqual(ids.length, 1);
|
|
125
|
+
assert.strictEqual(ids[0].id, 'FR-001');
|
|
126
|
+
});
|
|
127
|
+
(0, node_test_1.it)('does not match partial: INFRASTRUCTURE-001 should NOT match', () => {
|
|
128
|
+
const content = 'INFRASTRUCTURE-001 is not I-001';
|
|
129
|
+
const ids = (0, parse_js_1.extractIds)(content);
|
|
130
|
+
// Should only find I-001, not the "001" from INFRASTRUCTURE
|
|
131
|
+
assert.strictEqual(ids.length, 1);
|
|
132
|
+
assert.strictEqual(ids[0].id, 'I-001');
|
|
133
|
+
});
|
|
134
|
+
(0, node_test_1.it)('returns empty array for content with no IDs', () => {
|
|
135
|
+
const ids = (0, parse_js_1.extractIds)('No IDs here at all.');
|
|
136
|
+
assert.strictEqual(ids.length, 0);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
// ─── validateHeadings ────────────────────────────────────────────────────────
|
|
140
|
+
(0, node_test_1.describe)('validateHeadings', () => {
|
|
141
|
+
(0, node_test_1.it)('valid DECISIONS.md passes with all 5 headings', () => {
|
|
142
|
+
const content = `# Decisions
|
|
143
|
+
## Frozen Decisions
|
|
144
|
+
Content here
|
|
145
|
+
## Success Metrics
|
|
146
|
+
Content here
|
|
147
|
+
## Out of Scope
|
|
148
|
+
Content here
|
|
149
|
+
## Risks & Mitigations
|
|
150
|
+
Content here
|
|
151
|
+
## Open Questions
|
|
152
|
+
Content here`;
|
|
153
|
+
const result = (0, parse_js_1.validateHeadings)(content, 'DECISIONS.md');
|
|
154
|
+
assert.strictEqual(result.valid, true);
|
|
155
|
+
assert.strictEqual(result.missing.length, 0);
|
|
156
|
+
assert.strictEqual(result.present.length, 5);
|
|
157
|
+
});
|
|
158
|
+
(0, node_test_1.it)('missing heading fails with specific missing heading', () => {
|
|
159
|
+
const content = `# Decisions
|
|
160
|
+
## Success Metrics
|
|
161
|
+
Content here`;
|
|
162
|
+
const result = (0, parse_js_1.validateHeadings)(content, 'DECISIONS.md');
|
|
163
|
+
assert.strictEqual(result.valid, false);
|
|
164
|
+
assert.ok(result.missing.includes('## Frozen Decisions'));
|
|
165
|
+
});
|
|
166
|
+
(0, node_test_1.it)('case-insensitive match: lowercase heading found', () => {
|
|
167
|
+
const content = `## frozen decisions
|
|
168
|
+
## success metrics
|
|
169
|
+
## out of scope
|
|
170
|
+
## risks & mitigations
|
|
171
|
+
## open questions`;
|
|
172
|
+
const result = (0, parse_js_1.validateHeadings)(content, 'DECISIONS.md');
|
|
173
|
+
assert.strictEqual(result.valid, true);
|
|
174
|
+
});
|
|
175
|
+
(0, node_test_1.it)('unknown file type returns valid (no required headings)', () => {
|
|
176
|
+
const result = (0, parse_js_1.validateHeadings)('anything', 'UNKNOWN.md');
|
|
177
|
+
assert.strictEqual(result.valid, true);
|
|
178
|
+
assert.strictEqual(result.missing.length, 0);
|
|
179
|
+
});
|
|
180
|
+
(0, node_test_1.it)('empty content fails for file type with required headings', () => {
|
|
181
|
+
const result = (0, parse_js_1.validateHeadings)('', 'DECISIONS.md');
|
|
182
|
+
assert.strictEqual(result.valid, false);
|
|
183
|
+
assert.strictEqual(result.missing.length, 5);
|
|
184
|
+
});
|
|
185
|
+
(0, node_test_1.it)('validates SPEC.md with 3 required headings', () => {
|
|
186
|
+
const content = `## Roles & Permissions
|
|
187
|
+
Content
|
|
188
|
+
## Functional Requirements
|
|
189
|
+
Content
|
|
190
|
+
## Acceptance Criteria
|
|
191
|
+
Content`;
|
|
192
|
+
const result = (0, parse_js_1.validateHeadings)(content, 'SPEC.md');
|
|
193
|
+
assert.strictEqual(result.valid, true);
|
|
194
|
+
assert.strictEqual(result.present.length, 3);
|
|
195
|
+
});
|
|
196
|
+
(0, node_test_1.it)('validates JOURNEYS.md with 1 required heading', () => {
|
|
197
|
+
const content = `## Journeys
|
|
198
|
+
### J-001: Test`;
|
|
199
|
+
const result = (0, parse_js_1.validateHeadings)(content, 'JOURNEYS.md');
|
|
200
|
+
assert.strictEqual(result.valid, true);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
// ─── extractHeadingContent ───────────────────────────────────────────────────
|
|
204
|
+
(0, node_test_1.describe)('extractHeadingContent', () => {
|
|
205
|
+
(0, node_test_1.it)('extracts content between two headings', () => {
|
|
206
|
+
const content = `## Section A
|
|
207
|
+
Content of section A
|
|
208
|
+
More content
|
|
209
|
+
## Section B
|
|
210
|
+
Content of section B`;
|
|
211
|
+
const result = (0, parse_js_1.extractHeadingContent)(content, '## Section A');
|
|
212
|
+
assert.strictEqual(result, 'Content of section A\nMore content');
|
|
213
|
+
});
|
|
214
|
+
(0, node_test_1.it)('returns null for missing heading', () => {
|
|
215
|
+
const content = `## Section A\nContent`;
|
|
216
|
+
const result = (0, parse_js_1.extractHeadingContent)(content, '## Missing');
|
|
217
|
+
assert.strictEqual(result, null);
|
|
218
|
+
});
|
|
219
|
+
(0, node_test_1.it)('handles last heading (extracts to end of file)', () => {
|
|
220
|
+
const content = `## Section A
|
|
221
|
+
Content A
|
|
222
|
+
## Section B
|
|
223
|
+
Content of last section
|
|
224
|
+
More content`;
|
|
225
|
+
const result = (0, parse_js_1.extractHeadingContent)(content, '## Section B');
|
|
226
|
+
assert.strictEqual(result, 'Content of last section\nMore content');
|
|
227
|
+
});
|
|
228
|
+
(0, node_test_1.it)('does not include sub-headings of lower level', () => {
|
|
229
|
+
const content = `## Section A
|
|
230
|
+
Content A
|
|
231
|
+
### Subsection
|
|
232
|
+
Sub content
|
|
233
|
+
## Section B
|
|
234
|
+
Content B`;
|
|
235
|
+
const result = (0, parse_js_1.extractHeadingContent)(content, '## Section A');
|
|
236
|
+
assert.ok(result.includes('### Subsection'));
|
|
237
|
+
assert.ok(result.includes('Sub content'));
|
|
238
|
+
assert.ok(!result.includes('Content B'));
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
// ─── Template Heading Validation ─────────────────────────────────────────────
|
|
242
|
+
(0, node_test_1.describe)('template heading validation', () => {
|
|
243
|
+
const templateDir = path.join(process.cwd(), 'templates', 'gswd');
|
|
244
|
+
(0, node_test_1.it)('DECISIONS.template.md passes heading validation', () => {
|
|
245
|
+
const content = fs.readFileSync(path.join(templateDir, 'DECISIONS.template.md'), 'utf-8');
|
|
246
|
+
const result = (0, parse_js_1.validateHeadings)(content, 'DECISIONS.md');
|
|
247
|
+
assert.strictEqual(result.valid, true, `Missing: ${result.missing.join(', ')}`);
|
|
248
|
+
});
|
|
249
|
+
(0, node_test_1.it)('JOURNEYS.template.md passes heading validation', () => {
|
|
250
|
+
const content = fs.readFileSync(path.join(templateDir, 'JOURNEYS.template.md'), 'utf-8');
|
|
251
|
+
const result = (0, parse_js_1.validateHeadings)(content, 'JOURNEYS.md');
|
|
252
|
+
assert.strictEqual(result.valid, true, `Missing: ${result.missing.join(', ')}`);
|
|
253
|
+
});
|
|
254
|
+
(0, node_test_1.it)('SPEC.template.md passes heading validation', () => {
|
|
255
|
+
const content = fs.readFileSync(path.join(templateDir, 'SPEC.template.md'), 'utf-8');
|
|
256
|
+
const result = (0, parse_js_1.validateHeadings)(content, 'SPEC.md');
|
|
257
|
+
assert.strictEqual(result.valid, true, `Missing: ${result.missing.join(', ')}`);
|
|
258
|
+
});
|
|
259
|
+
(0, node_test_1.it)('NFR.template.md passes heading validation', () => {
|
|
260
|
+
const content = fs.readFileSync(path.join(templateDir, 'NFR.template.md'), 'utf-8');
|
|
261
|
+
const result = (0, parse_js_1.validateHeadings)(content, 'NFR.md');
|
|
262
|
+
assert.strictEqual(result.valid, true, `Missing: ${result.missing.join(', ')}`);
|
|
263
|
+
});
|
|
264
|
+
(0, node_test_1.it)('INTEGRATIONS.template.md passes heading validation', () => {
|
|
265
|
+
const content = fs.readFileSync(path.join(templateDir, 'INTEGRATIONS.template.md'), 'utf-8');
|
|
266
|
+
const result = (0, parse_js_1.validateHeadings)(content, 'INTEGRATIONS.md');
|
|
267
|
+
assert.strictEqual(result.valid, true, `Missing: ${result.missing.join(', ')}`);
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
// ─── REQUIRED_HEADINGS constant ──────────────────────────────────────────────
|
|
271
|
+
(0, node_test_1.describe)('REQUIRED_HEADINGS', () => {
|
|
272
|
+
(0, node_test_1.it)('defines headings for all 5 artifact types', () => {
|
|
273
|
+
assert.ok(parse_js_1.REQUIRED_HEADINGS['JOURNEYS.md']);
|
|
274
|
+
assert.ok(parse_js_1.REQUIRED_HEADINGS['SPEC.md']);
|
|
275
|
+
assert.ok(parse_js_1.REQUIRED_HEADINGS['NFR.md']);
|
|
276
|
+
assert.ok(parse_js_1.REQUIRED_HEADINGS['INTEGRATIONS.md']);
|
|
277
|
+
assert.ok(parse_js_1.REQUIRED_HEADINGS['DECISIONS.md']);
|
|
278
|
+
});
|
|
279
|
+
(0, node_test_1.it)('DECISIONS.md has 5 required headings', () => {
|
|
280
|
+
assert.strictEqual(parse_js_1.REQUIRED_HEADINGS['DECISIONS.md'].length, 5);
|
|
281
|
+
});
|
|
282
|
+
(0, node_test_1.it)('SPEC.md has 3 required headings', () => {
|
|
283
|
+
assert.strictEqual(parse_js_1.REQUIRED_HEADINGS['SPEC.md'].length, 3);
|
|
284
|
+
});
|
|
285
|
+
});
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Render module tests — banner, checkpoint box, Next Up, status line, progress bar
|
|
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 render_js_1 = require("../lib/render.js");
|
|
42
|
+
// ─── SYMBOLS ─────────────────────────────────────────────────────────────────
|
|
43
|
+
(0, node_test_1.describe)('SYMBOLS', () => {
|
|
44
|
+
(0, node_test_1.it)('defines all 6 symbols', () => {
|
|
45
|
+
assert.ok(render_js_1.SYMBOLS.complete);
|
|
46
|
+
assert.ok(render_js_1.SYMBOLS.failed);
|
|
47
|
+
assert.ok(render_js_1.SYMBOLS.inProgress);
|
|
48
|
+
assert.ok(render_js_1.SYMBOLS.pending);
|
|
49
|
+
assert.ok(render_js_1.SYMBOLS.autoApproved);
|
|
50
|
+
assert.ok(render_js_1.SYMBOLS.warning);
|
|
51
|
+
});
|
|
52
|
+
(0, node_test_1.it)('each symbol is a non-empty string', () => {
|
|
53
|
+
for (const [key, value] of Object.entries(render_js_1.SYMBOLS)) {
|
|
54
|
+
assert.strictEqual(typeof value, 'string', `${key} should be a string`);
|
|
55
|
+
assert.ok(value.length > 0, `${key} should be non-empty`);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
// ─── padRight ────────────────────────────────────────────────────────────────
|
|
60
|
+
(0, node_test_1.describe)('padRight', () => {
|
|
61
|
+
(0, node_test_1.it)('pads short strings to exact width', () => {
|
|
62
|
+
assert.strictEqual((0, render_js_1.padRight)('hi', 5), 'hi ');
|
|
63
|
+
assert.strictEqual((0, render_js_1.padRight)('hi', 5).length, 5);
|
|
64
|
+
});
|
|
65
|
+
(0, node_test_1.it)('truncates long strings with ellipsis', () => {
|
|
66
|
+
const result = (0, render_js_1.padRight)('a very long string here', 10);
|
|
67
|
+
assert.strictEqual(result.length, 10);
|
|
68
|
+
assert.ok(result.endsWith('...'));
|
|
69
|
+
});
|
|
70
|
+
(0, node_test_1.it)('returns exact string when length matches width', () => {
|
|
71
|
+
assert.strictEqual((0, render_js_1.padRight)('exact', 5), 'exact');
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
// ─── renderBanner ────────────────────────────────────────────────────────────
|
|
75
|
+
(0, node_test_1.describe)('renderBanner', () => {
|
|
76
|
+
(0, node_test_1.it)('contains GSWD \u25B6 prefix (not GSD)', () => {
|
|
77
|
+
const result = (0, render_js_1.renderBanner)('IMAGINE');
|
|
78
|
+
assert.ok(result.includes('GSWD \u25B6'), 'should contain GSWD \u25B6 prefix');
|
|
79
|
+
assert.ok(!result.includes('GSD \u25B6'), 'should NOT contain GSD prefix');
|
|
80
|
+
});
|
|
81
|
+
(0, node_test_1.it)('uppercases the stage name', () => {
|
|
82
|
+
const result = (0, render_js_1.renderBanner)('imagine');
|
|
83
|
+
assert.ok(result.includes('IMAGINE'));
|
|
84
|
+
});
|
|
85
|
+
(0, node_test_1.it)('has exactly 3 lines', () => {
|
|
86
|
+
const lines = (0, render_js_1.renderBanner)('SPECIFY').split('\n');
|
|
87
|
+
assert.strictEqual(lines.length, 3);
|
|
88
|
+
});
|
|
89
|
+
(0, node_test_1.it)('top and bottom border lines are identical', () => {
|
|
90
|
+
const lines = (0, render_js_1.renderBanner)('AUDIT').split('\n');
|
|
91
|
+
assert.strictEqual(lines[0], lines[2]);
|
|
92
|
+
});
|
|
93
|
+
(0, node_test_1.it)('borders use \u2501 character and are 55 chars wide', () => {
|
|
94
|
+
const lines = (0, render_js_1.renderBanner)('COMPILE').split('\n');
|
|
95
|
+
assert.strictEqual(lines[0].length, 55);
|
|
96
|
+
assert.ok(lines[0].split('').every(ch => ch === '\u2501'));
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
// ─── renderCheckpoint ────────────────────────────────────────────────────────
|
|
100
|
+
(0, node_test_1.describe)('renderCheckpoint', () => {
|
|
101
|
+
const result = (0, render_js_1.renderCheckpoint)('Decision Required', 'Choose a data store.', ['PostgreSQL', 'SQLite', 'Let me explain'], 'Select: 1-3');
|
|
102
|
+
const lines = result.split('\n');
|
|
103
|
+
(0, node_test_1.it)('output contains top border with \u250C', () => {
|
|
104
|
+
assert.ok(lines[0].startsWith('\u250C'));
|
|
105
|
+
assert.ok(lines[0].endsWith('\u2510'));
|
|
106
|
+
});
|
|
107
|
+
(0, node_test_1.it)('output contains bottom border with \u2514', () => {
|
|
108
|
+
const last = lines[lines.length - 1];
|
|
109
|
+
assert.ok(last.startsWith('\u2514'));
|
|
110
|
+
assert.ok(last.endsWith('\u2518'));
|
|
111
|
+
});
|
|
112
|
+
(0, node_test_1.it)('each content line starts and ends with \u2502', () => {
|
|
113
|
+
// Content lines are between first and last (excluding mid-border)
|
|
114
|
+
for (let i = 1; i < lines.length - 1; i++) {
|
|
115
|
+
if (lines[i].startsWith('\u251C'))
|
|
116
|
+
continue; // mid-border
|
|
117
|
+
assert.ok(lines[i].startsWith('\u2502'), `line ${i} should start with \u2502`);
|
|
118
|
+
assert.ok(lines[i].endsWith('\u2502'), `line ${i} should end with \u2502`);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
(0, node_test_1.it)('all lines (borders and content) have identical length', () => {
|
|
122
|
+
const expectedWidth = 56;
|
|
123
|
+
for (let i = 0; i < lines.length; i++) {
|
|
124
|
+
assert.strictEqual(lines[i].length, expectedWidth, `line ${i} should be ${expectedWidth} chars: "${lines[i]}"`);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
(0, node_test_1.it)('options are numbered (1, 2, 3...)', () => {
|
|
128
|
+
assert.ok(result.includes('1) PostgreSQL'));
|
|
129
|
+
assert.ok(result.includes('2) SQLite'));
|
|
130
|
+
assert.ok(result.includes('3) Let me explain'));
|
|
131
|
+
});
|
|
132
|
+
(0, node_test_1.it)('action prompt starts with \u2192', () => {
|
|
133
|
+
assert.ok(result.includes('\u2192 Select: 1-3'));
|
|
134
|
+
});
|
|
135
|
+
(0, node_test_1.it)('type header appears in first content line', () => {
|
|
136
|
+
assert.ok(lines[1].includes('Decision Required'));
|
|
137
|
+
});
|
|
138
|
+
(0, node_test_1.it)('truncates long content', () => {
|
|
139
|
+
const longResult = (0, render_js_1.renderCheckpoint)('A'.repeat(100), 'context', ['option'], 'go');
|
|
140
|
+
const longLines = longResult.split('\n');
|
|
141
|
+
// All lines should still be exactly 56 chars
|
|
142
|
+
for (const line of longLines) {
|
|
143
|
+
assert.strictEqual(line.length, 56, `line should be 56 chars: "${line}"`);
|
|
144
|
+
}
|
|
145
|
+
// Header should be truncated with ...
|
|
146
|
+
assert.ok(longLines[1].includes('...'));
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
// ─── renderNextUp ────────────────────────────────────────────────────────────
|
|
150
|
+
(0, node_test_1.describe)('renderNextUp', () => {
|
|
151
|
+
(0, node_test_1.it)('contains \u2713 Wrote: prefix', () => {
|
|
152
|
+
const result = (0, render_js_1.renderNextUp)(['STATE.md'], '/gswd:specify');
|
|
153
|
+
assert.ok(result.includes('\u2713 Wrote:'));
|
|
154
|
+
});
|
|
155
|
+
(0, node_test_1.it)('contains file names', () => {
|
|
156
|
+
const result = (0, render_js_1.renderNextUp)(['IMAGINE.md', 'ICP.md'], '/gswd:specify');
|
|
157
|
+
assert.ok(result.includes('IMAGINE.md'));
|
|
158
|
+
assert.ok(result.includes('ICP.md'));
|
|
159
|
+
});
|
|
160
|
+
(0, node_test_1.it)('contains Next up: section', () => {
|
|
161
|
+
const result = (0, render_js_1.renderNextUp)(['file.md'], '/gswd:specify');
|
|
162
|
+
assert.ok(result.includes('Next up:'));
|
|
163
|
+
assert.ok(result.includes('/gswd:specify'));
|
|
164
|
+
});
|
|
165
|
+
(0, node_test_1.it)('contains /clear tip', () => {
|
|
166
|
+
const result = (0, render_js_1.renderNextUp)(['file.md'], '/gswd:specify');
|
|
167
|
+
assert.ok(result.includes('/clear'));
|
|
168
|
+
});
|
|
169
|
+
(0, node_test_1.it)('includes also available section when provided', () => {
|
|
170
|
+
const result = (0, render_js_1.renderNextUp)(['file.md'], '/gswd:specify', ['/gswd:audit', '/gswd:status']);
|
|
171
|
+
assert.ok(result.includes('Also available:'));
|
|
172
|
+
assert.ok(result.includes('/gswd:audit'));
|
|
173
|
+
assert.ok(result.includes('/gswd:status'));
|
|
174
|
+
});
|
|
175
|
+
(0, node_test_1.it)('omits also available section when empty', () => {
|
|
176
|
+
const result = (0, render_js_1.renderNextUp)(['file.md'], '/gswd:specify');
|
|
177
|
+
assert.ok(!result.includes('Also available:'));
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
// ─── renderStatusLine ────────────────────────────────────────────────────────
|
|
181
|
+
(0, node_test_1.describe)('renderStatusLine', () => {
|
|
182
|
+
(0, node_test_1.it)('done status maps to \u2713', () => {
|
|
183
|
+
const result = (0, render_js_1.renderStatusLine)('imagine', 'done');
|
|
184
|
+
assert.ok(result.includes('\u2713'));
|
|
185
|
+
});
|
|
186
|
+
(0, node_test_1.it)('pass status maps to \u2713', () => {
|
|
187
|
+
const result = (0, render_js_1.renderStatusLine)('audit', 'pass');
|
|
188
|
+
assert.ok(result.includes('\u2713'));
|
|
189
|
+
});
|
|
190
|
+
(0, node_test_1.it)('fail status maps to \u2717', () => {
|
|
191
|
+
const result = (0, render_js_1.renderStatusLine)('audit', 'fail');
|
|
192
|
+
assert.ok(result.includes('\u2717'));
|
|
193
|
+
});
|
|
194
|
+
(0, node_test_1.it)('in_progress maps to \u25C6', () => {
|
|
195
|
+
const result = (0, render_js_1.renderStatusLine)('specify', 'in_progress');
|
|
196
|
+
assert.ok(result.includes('\u25C6'));
|
|
197
|
+
});
|
|
198
|
+
(0, node_test_1.it)('not_started maps to \u25CB', () => {
|
|
199
|
+
const result = (0, render_js_1.renderStatusLine)('compile', 'not_started');
|
|
200
|
+
assert.ok(result.includes('\u25CB'));
|
|
201
|
+
});
|
|
202
|
+
(0, node_test_1.it)('contains stage name and status', () => {
|
|
203
|
+
const result = (0, render_js_1.renderStatusLine)('imagine', 'done');
|
|
204
|
+
assert.ok(result.includes('imagine'));
|
|
205
|
+
assert.ok(result.includes('done'));
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
// ─── renderProgressBar ───────────────────────────────────────────────────────
|
|
209
|
+
(0, node_test_1.describe)('renderProgressBar', () => {
|
|
210
|
+
(0, node_test_1.it)('0% shows all empty blocks', () => {
|
|
211
|
+
const result = (0, render_js_1.renderProgressBar)(0, 10, 10);
|
|
212
|
+
assert.ok(result.includes('\u2591'.repeat(10)));
|
|
213
|
+
assert.ok(!result.includes('\u2588'));
|
|
214
|
+
assert.ok(result.includes('0%'));
|
|
215
|
+
});
|
|
216
|
+
(0, node_test_1.it)('100% shows all filled blocks', () => {
|
|
217
|
+
const result = (0, render_js_1.renderProgressBar)(10, 10, 10);
|
|
218
|
+
assert.ok(result.includes('\u2588'.repeat(10)));
|
|
219
|
+
assert.ok(!result.includes('\u2591'));
|
|
220
|
+
assert.ok(result.includes('100%'));
|
|
221
|
+
});
|
|
222
|
+
(0, node_test_1.it)('50% shows half and half', () => {
|
|
223
|
+
const result = (0, render_js_1.renderProgressBar)(5, 10, 10);
|
|
224
|
+
assert.ok(result.includes('\u2588'.repeat(5)));
|
|
225
|
+
assert.ok(result.includes('\u2591'.repeat(5)));
|
|
226
|
+
assert.ok(result.includes('50%'));
|
|
227
|
+
});
|
|
228
|
+
(0, node_test_1.it)('contains percentage number', () => {
|
|
229
|
+
const result = (0, render_js_1.renderProgressBar)(3, 4, 10);
|
|
230
|
+
assert.ok(result.includes('75%'));
|
|
231
|
+
});
|
|
232
|
+
(0, node_test_1.it)('handles zero total gracefully', () => {
|
|
233
|
+
const result = (0, render_js_1.renderProgressBar)(0, 0, 10);
|
|
234
|
+
assert.ok(result.includes('0%'));
|
|
235
|
+
});
|
|
236
|
+
});
|