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.
Files changed (86) hide show
  1. package/bin/gswd-tools.cjs +228 -0
  2. package/bin/install.js +8 -0
  3. package/commands/gswd/imagine.md +7 -1
  4. package/commands/gswd/start.md +507 -32
  5. package/dist/lib/audit.d.ts +205 -0
  6. package/dist/lib/audit.js +805 -0
  7. package/dist/lib/bootstrap.d.ts +103 -0
  8. package/dist/lib/bootstrap.js +563 -0
  9. package/dist/lib/compile.d.ts +239 -0
  10. package/dist/lib/compile.js +1152 -0
  11. package/dist/lib/config.d.ts +49 -0
  12. package/dist/lib/config.js +150 -0
  13. package/dist/lib/imagine-agents.d.ts +54 -0
  14. package/dist/lib/imagine-agents.js +185 -0
  15. package/dist/lib/imagine-gate.d.ts +47 -0
  16. package/dist/lib/imagine-gate.js +131 -0
  17. package/dist/lib/imagine-input.d.ts +46 -0
  18. package/dist/lib/imagine-input.js +233 -0
  19. package/dist/lib/imagine-synthesis.d.ts +90 -0
  20. package/dist/lib/imagine-synthesis.js +453 -0
  21. package/dist/lib/imagine.d.ts +56 -0
  22. package/dist/lib/imagine.js +413 -0
  23. package/dist/lib/intake.d.ts +27 -0
  24. package/dist/lib/intake.js +82 -0
  25. package/dist/lib/parse.d.ts +59 -0
  26. package/dist/lib/parse.js +171 -0
  27. package/dist/lib/render.d.ts +309 -0
  28. package/dist/lib/render.js +624 -0
  29. package/dist/lib/specify-agents.d.ts +120 -0
  30. package/dist/lib/specify-agents.js +269 -0
  31. package/dist/lib/specify-journeys.d.ts +124 -0
  32. package/dist/lib/specify-journeys.js +279 -0
  33. package/dist/lib/specify-nfr.d.ts +45 -0
  34. package/dist/lib/specify-nfr.js +159 -0
  35. package/dist/lib/specify-roles.d.ts +46 -0
  36. package/dist/lib/specify-roles.js +88 -0
  37. package/dist/lib/specify.d.ts +70 -0
  38. package/dist/lib/specify.js +676 -0
  39. package/dist/lib/state.d.ts +140 -0
  40. package/dist/lib/state.js +340 -0
  41. package/dist/tests/audit.test.d.ts +4 -0
  42. package/dist/tests/audit.test.js +1579 -0
  43. package/dist/tests/bootstrap.test.d.ts +5 -0
  44. package/dist/tests/bootstrap.test.js +611 -0
  45. package/dist/tests/compile.test.d.ts +4 -0
  46. package/dist/tests/compile.test.js +862 -0
  47. package/dist/tests/config.test.d.ts +4 -0
  48. package/dist/tests/config.test.js +191 -0
  49. package/dist/tests/imagine-agents.test.d.ts +6 -0
  50. package/dist/tests/imagine-agents.test.js +179 -0
  51. package/dist/tests/imagine-gate.test.d.ts +6 -0
  52. package/dist/tests/imagine-gate.test.js +264 -0
  53. package/dist/tests/imagine-input.test.d.ts +6 -0
  54. package/dist/tests/imagine-input.test.js +283 -0
  55. package/dist/tests/imagine-synthesis.test.d.ts +7 -0
  56. package/dist/tests/imagine-synthesis.test.js +380 -0
  57. package/dist/tests/imagine.test.d.ts +8 -0
  58. package/dist/tests/imagine.test.js +406 -0
  59. package/dist/tests/parse.test.d.ts +4 -0
  60. package/dist/tests/parse.test.js +285 -0
  61. package/dist/tests/render.test.d.ts +4 -0
  62. package/dist/tests/render.test.js +236 -0
  63. package/dist/tests/specify-agents.test.d.ts +4 -0
  64. package/dist/tests/specify-agents.test.js +352 -0
  65. package/dist/tests/specify-journeys.test.d.ts +5 -0
  66. package/dist/tests/specify-journeys.test.js +440 -0
  67. package/dist/tests/specify-nfr.test.d.ts +4 -0
  68. package/dist/tests/specify-nfr.test.js +205 -0
  69. package/dist/tests/specify-roles.test.d.ts +4 -0
  70. package/dist/tests/specify-roles.test.js +136 -0
  71. package/dist/tests/specify.test.d.ts +9 -0
  72. package/dist/tests/specify.test.js +544 -0
  73. package/dist/tests/state.test.d.ts +4 -0
  74. package/dist/tests/state.test.js +316 -0
  75. package/lib/bootstrap.ts +37 -11
  76. package/lib/compile.ts +426 -4
  77. package/lib/imagine-agents.ts +53 -7
  78. package/lib/imagine-synthesis.ts +170 -6
  79. package/lib/imagine.ts +59 -5
  80. package/lib/intake.ts +60 -0
  81. package/lib/parse.ts +2 -1
  82. package/lib/render.ts +566 -5
  83. package/lib/specify-agents.ts +25 -3
  84. package/lib/state.ts +115 -0
  85. package/package.json +4 -2
  86. 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,4 @@
1
+ /**
2
+ * Render module tests — banner, checkpoint box, Next Up, status line, progress bar
3
+ */
4
+ export {};
@@ -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
+ });
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Specify Agents module tests — Agent definitions, prompt building, orchestration, validation
3
+ */
4
+ export {};