@dtlabs/ai-toolkit 0.10.0 → 0.11.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 (79) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/bin/cli.js +1178 -64
  3. package/docs/installation.md +74 -8
  4. package/docs/reference.md +60 -12
  5. package/lib/asset-catalog.js +52 -0
  6. package/package.json +9 -2
  7. package/{.claude → src/claude}/agents/install-toolkit.md +30 -103
  8. package/{.claude → src/claude}/workflows/am-phase1.js +9 -12
  9. package/{.claude → src/claude}/workflows/pm-phase2.js +2 -2
  10. package/.claude/.ai-toolkit-version +0 -1
  11. package/.claude/scripts/.gitkeep +0 -0
  12. package/.claude/scripts/tests/fixtures/ac-table-format.md +0 -48
  13. package/.claude/scripts/tests/fixtures/test-feature/TEST-001-Work-Breakdown.json +0 -234
  14. package/.claude/scripts/tests/fixtures/test-feature/TEST-001-semantic-output.json +0 -4
  15. package/.claude/scripts/tests/fixtures/test-feature/feature.md +0 -45
  16. package/.claude/scripts/tests/fixtures/wb-invalid-bad-domain.json +0 -39
  17. package/.claude/scripts/tests/fixtures/wb-invalid-cycle.json +0 -61
  18. package/.claude/scripts/tests/fixtures/wb-invalid-duplicate-id.json +0 -72
  19. package/.claude/scripts/tests/fixtures/wb-invalid-missing-field.json +0 -38
  20. package/.claude/scripts/tests/fixtures/wb-valid.json +0 -189
  21. package/.claude/scripts/tests/helpers/buildWaves.js +0 -21
  22. package/.claude/scripts/tests/helpers/pm-phase2-flow.js +0 -29
  23. package/.claude/scripts/tests/helpers/pm-phase2-gate2.js +0 -55
  24. package/.claude/scripts/tests/helpers/pm-phase2-ledger.js +0 -108
  25. package/.claude/scripts/tests/installer.scripts-distribution.test.js +0 -131
  26. package/.claude/scripts/tests/pm-phase2.gate2-payload.test.js +0 -353
  27. package/.claude/scripts/tests/pm-phase2.ledger-transitions.test.js +0 -346
  28. package/.claude/scripts/tests/pm-phase2.orchestration-flow.test.js +0 -276
  29. package/.claude/scripts/tests/verify-us-01-t03.js +0 -288
  30. package/.claude/scripts/tests/verify-us-03-t04.js +0 -262
  31. package/.claude/scripts/tests/wb-render.checks-md-csv-deps.test.js +0 -337
  32. package/.claude/scripts/tests/wb-render.checks-sanitize-commit-paths.test.js +0 -352
  33. package/.claude/scripts/tests/wb-render.regression-pm-phase3-csv.test.js +0 -219
  34. package/.claude/scripts/tests/wb-validate.checks-1-7.test.js +0 -330
  35. package/.claude/scripts/tests/wb-validate.checks-11.test.js +0 -166
  36. package/.claude/scripts/tests/wb-validate.checks-12-14.test.js +0 -222
  37. package/.claude/scripts/tests/wb-validate.checks-15-18.test.js +0 -359
  38. package/.claude/scripts/tests/wb-validate.checks-19-21.test.js +0 -362
  39. package/.claude/scripts/tests/wb-validate.checks-22-23-report.test.js +0 -297
  40. package/.claude/scripts/tests/wb-validate.checks-8-10.test.js +0 -243
  41. package/.claude/settings.json +0 -5
  42. /package/{.claude → src/claude}/agents/concurrency-safety-assessment.md +0 -0
  43. /package/{.claude → src/claude}/agents/define-feature.md +0 -0
  44. /package/{.claude → src/claude}/agents/dependency-injection-refactoring.md +0 -0
  45. /package/{.claude → src/claude}/agents/dependency-supply-chain-security.md +0 -0
  46. /package/{.claude → src/claude}/agents/developer-backend.md +0 -0
  47. /package/{.claude → src/claude}/agents/developer-frontend.md +0 -0
  48. /package/{.claude → src/claude}/agents/developer-testing.md +0 -0
  49. /package/{.claude → src/claude}/agents/domain-model-refactoring.md +0 -0
  50. /package/{.claude → src/claude}/agents/generate-requirements.md +0 -0
  51. /package/{.claude → src/claude}/agents/generate-tech-spec.md +0 -0
  52. /package/{.claude → src/claude}/agents/generate-work-breakdown.md +0 -0
  53. /package/{.claude → src/claude}/agents/generic-software-assessment.md +0 -0
  54. /package/{.claude → src/claude}/agents/god-class-decomposition.md +0 -0
  55. /package/{.claude → src/claude}/agents/init-agents-md.md +0 -0
  56. /package/{.claude → src/claude}/agents/intervention-documentation-standard.md +0 -0
  57. /package/{.claude → src/claude}/agents/layered-architecture-assessment.md +0 -0
  58. /package/{.claude → src/claude}/agents/review-solution.md +0 -0
  59. /package/{.claude → src/claude}/agents/security-hardening.md +0 -0
  60. /package/{.claude → src/claude}/agents/validate-feature-docs.md +0 -0
  61. /package/{.claude → src/claude}/agents/validate-work-breakdown-semantic.md +0 -0
  62. /package/{.claude → src/claude}/commands/assessment-status.md +0 -0
  63. /package/{.claude → src/claude}/commands/check-docs.md +0 -0
  64. /package/{.claude → src/claude}/commands/check-interventions.md +0 -0
  65. /package/{.claude → src/claude}/commands/feature-status.md +0 -0
  66. /package/{.claude → src/claude}/commands/next-intervention.md +0 -0
  67. /package/{.claude → src/claude}/commands/next-task.md +0 -0
  68. /package/{.claude → src/claude}/commands/pr-description.md +0 -0
  69. /package/{.claude → src/claude}/scripts/wb-render.js +0 -0
  70. /package/{.claude → src/claude}/scripts/wb-validate.js +0 -0
  71. /package/{.claude → src/claude}/skills/assess-codebase/SKILL.md +0 -0
  72. /package/{.claude → src/claude}/skills/define-feature/SKILL.md +0 -0
  73. /package/{.claude → src/claude}/skills/hi-gaia/SKILL.md +0 -0
  74. /package/{.claude → src/claude}/skills/implement-feature/SKILL.md +0 -0
  75. /package/{.claude → src/claude}/skills/init-agents/SKILL.md +0 -0
  76. /package/{.claude → src/claude}/skills/install-toolkit/SKILL.md +0 -0
  77. /package/{.claude → src/claude}/workflows/am-phase2.js +0 -0
  78. /package/{.claude → src/claude}/workflows/pm-phase1.js +0 -0
  79. /package/{.claude → src/claude}/workflows/pm-phase3.js +0 -0
@@ -1,362 +0,0 @@
1
- 'use strict';
2
-
3
- const { spawnSync } = require('child_process');
4
- const path = require('path');
5
- const fs = require('fs');
6
- const os = require('os');
7
-
8
- const VALIDATE = path.resolve(__dirname, '../wb-validate.js');
9
- const FIXTURES = path.resolve(__dirname, 'fixtures');
10
- const AC_FIXTURE = path.join(FIXTURES, 'ac-table-format.md');
11
-
12
- function runValidator(wbPath, reqPath) {
13
- const args = [VALIDATE, wbPath];
14
- if (reqPath) args.push(reqPath);
15
- const result = spawnSync('node', args, { encoding: 'utf8' });
16
- let report = null;
17
- try { report = JSON.parse(result.stdout); } catch (_) {}
18
- return { exitCode: result.status, report, stderr: result.stderr };
19
- }
20
-
21
- // ── Shared temp-file lifecycle ─────────────────────────────────────────────────
22
-
23
- let tmpFile, tmpReqFile;
24
-
25
- beforeEach(() => {
26
- tmpFile = path.join(os.tmpdir(), `wb-t20-${Date.now()}.json`);
27
- });
28
-
29
- afterEach(() => {
30
- try { fs.unlinkSync(tmpFile); } catch (_) {}
31
- if (tmpReqFile) { try { fs.unlinkSync(tmpReqFile); } catch (_) {} tmpReqFile = null; }
32
- });
33
-
34
- // ── Domain builders ────────────────────────────────────────────────────────────
35
-
36
- function makeTask(id, acIds = [], dependsOn = []) {
37
- return {
38
- id,
39
- title: `Task ${id}`,
40
- outcome: 'Done',
41
- domain: 'BE',
42
- agentType: 'developer-backend',
43
- dependsOn,
44
- acceptanceCriteria: acIds,
45
- verification: { commands: ['node --version'] },
46
- estimate: { agentMinutes: 10, tokens: 5000 },
47
- outputCount: 1,
48
- groupingRationale: 'Single',
49
- commit: { subject: `feat: ${id}` },
50
- };
51
- }
52
-
53
- function makePhase(id, tasks) {
54
- return { id, type: 'user-story', title: `Phase ${id}`, commit: `feat(${id}): done`, tasks };
55
- }
56
-
57
- function makeWB(phases) { return { schemaVersion: 2, phases }; }
58
-
59
- function writeWB(obj) { fs.writeFileSync(tmpFile, JSON.stringify(obj, null, 2)); return tmpFile; }
60
-
61
- // ── Requirements fixture builder ───────────────────────────────────────────────
62
- // Generates a minimal requirements Markdown file the AC parser can consume.
63
- //
64
- // ucPriorities — { 'UC-01': 'Must', 'UC-02': 'Should', ... }
65
- // acRows — [['AC-01', 'UC-01'], ['AC-02', 'All UCs'], ...]
66
- //
67
- // The resulting string matches the parser's expectations:
68
- // • ### UC-NN: … heading followed by a | Priority | <value> | row
69
- // • ## 7. Acceptance Criteria section with a | ID | Criterion | Related UC | table
70
-
71
- function makeReqMd(ucPriorities, acRows) {
72
- const ucSections = Object.entries(ucPriorities)
73
- .map(([ucId, pri]) =>
74
- `### ${ucId}: Description\n| Field | Value |\n|-------|-------|\n| Priority | ${pri} |\n`
75
- )
76
- .join('\n');
77
- const acTable =
78
- '| ID | Criterion | Related UC |\n|----|-----------|------------|\n' +
79
- acRows.map(([id, relUc]) => `| ${id} | A criterion | ${relUc} |`).join('\n');
80
- return `## 5. Use Cases\n\n${ucSections}\n## 7. Acceptance Criteria\n\n${acTable}\n`;
81
- }
82
-
83
- // ─────────────────────────────────────────────────────────────────────────────
84
- // The AC fixture (ac-table-format.md) defines:
85
- // UC-01 (Must), UC-02 (Should)
86
- // AC-01: UC-01 → priority Must, allowedUserStories: ['US-01']
87
- // AC-02: UC-01 → priority Must, allowedUserStories: ['US-01']
88
- // AC-03: UC-01, UC-02 → priority Must (strongest), allowedUserStories: ['US-01','US-02']
89
- // AC-04: UC-02 → priority Should, allowedUserStories: ['US-02']
90
- // AC-05: All UCs → priority Must (strongest overall), unscoped: true
91
- // ─────────────────────────────────────────────────────────────────────────────
92
-
93
- describe('wb-validate.js — check 19 — AC existence (ac_not_found)', () => {
94
-
95
- // ── Test A: all referenced AC IDs exist in the fixture ───────────────────────
96
-
97
- test('exits 0 and report contains no ac_not_found errors when all referenced ACs exist in the requirements table', () => {
98
- // Arrange — cover all four Must-priority ACs from the fixture so check 21 also passes:
99
- // AC-01 (Must, US-01), AC-02 (Must, US-01), AC-03 (Must, US-01+US-02), AC-05 (Must, unscoped)
100
- const wb = makeWB([
101
- makePhase('US-01', [makeTask('US-01-TASK-BE-01', ['AC-01', 'AC-02', 'AC-03', 'AC-05'])]),
102
- ]);
103
- writeWB(wb);
104
-
105
- // Act
106
- const { exitCode, report } = runValidator(tmpFile, AC_FIXTURE);
107
-
108
- // Assert
109
- expect(exitCode).toBe(0);
110
- expect(report.errors.filter(e => e.category === 'ac_not_found')).toHaveLength(0);
111
- });
112
-
113
- // ── Test B: referenced AC ID is absent from the requirements table ─────────────
114
-
115
- test('exits 1 and report.errors contains ac_not_found with details.acId "AC-99" when task references an AC that does not exist in the requirements table', () => {
116
- // Arrange — AC-99 does not appear in the fixture
117
- const wb = makeWB([makePhase('US-01', [makeTask('US-01-TASK-BE-01', ['AC-99'])])]);
118
- writeWB(wb);
119
-
120
- // Act
121
- const { exitCode, report } = runValidator(tmpFile, AC_FIXTURE);
122
-
123
- // Assert
124
- expect(exitCode).toBe(1);
125
- const error = report.errors.find(
126
- e => e.category === 'ac_not_found' && e.details.acId === 'AC-99'
127
- );
128
- expect(error).toBeDefined();
129
- expect(error.details.acId).toBe('AC-99');
130
- });
131
-
132
- // ── Test C: empty acceptanceCriteria — no ac_not_found is raised ──────────────
133
-
134
- test('exits 0 and report contains no ac_not_found errors when acceptanceCriteria is an empty array and requirements has no Must-priority ACs', () => {
135
- // Arrange — custom req with a single Should-priority AC so check 21 does not block
136
- tmpReqFile = path.join(os.tmpdir(), `req-t20-${Date.now()}.md`);
137
- fs.writeFileSync(tmpReqFile, makeReqMd({ 'UC-01': 'Should' }, [['AC-01', 'UC-01']]));
138
- const wb = makeWB([makePhase('US-01', [makeTask('US-01-TASK-BE-01', [])])]);
139
- writeWB(wb);
140
-
141
- // Act
142
- const { exitCode, report } = runValidator(tmpFile, tmpReqFile);
143
-
144
- // Assert
145
- expect(exitCode).toBe(0);
146
- expect(report.errors.filter(e => e.category === 'ac_not_found')).toHaveLength(0);
147
- });
148
- });
149
-
150
- // ─────────────────────────────────────────────────────────────────────────────
151
-
152
- describe('wb-validate.js — check 20 — AC scope validation (ac_wrong_us)', () => {
153
-
154
- // ── Test A: AC referenced by the correct phase ────────────────────────────────
155
-
156
- test('exits 0 and report contains no ac_wrong_us errors when AC scoped to UC-01 is referenced by a task in the US-01 phase', () => {
157
- // Arrange — custom req: one Must AC scoped to UC-01; task is in the matching US-01 phase
158
- tmpReqFile = path.join(os.tmpdir(), `req-t20-${Date.now()}.md`);
159
- fs.writeFileSync(tmpReqFile, makeReqMd({ 'UC-01': 'Must' }, [['AC-01', 'UC-01']]));
160
- const wb = makeWB([makePhase('US-01', [makeTask('US-01-TASK-BE-01', ['AC-01'])])]);
161
- writeWB(wb);
162
-
163
- // Act
164
- const { exitCode, report } = runValidator(tmpFile, tmpReqFile);
165
-
166
- // Assert
167
- expect(exitCode).toBe(0);
168
- expect(report.errors.filter(e => e.category === 'ac_wrong_us')).toHaveLength(0);
169
- });
170
-
171
- // ── Test B: AC referenced by the wrong phase ──────────────────────────────────
172
-
173
- test('exits 1 and report.errors contains ac_wrong_us when AC scoped to UC-01 is referenced by a task in the US-02 phase', () => {
174
- // Arrange — custom req: UC-01 (Must), UC-02 (Should), AC-01 scoped to UC-01 → allowedUserStories: ['US-01']
175
- // The task is in US-02; AC-01 IS referenced so check 21 (must_ac_uncovered) does not fire,
176
- // but the scope check (check 20) reports ac_wrong_us
177
- tmpReqFile = path.join(os.tmpdir(), `req-t20-${Date.now()}.md`);
178
- fs.writeFileSync(tmpReqFile, makeReqMd({ 'UC-01': 'Must', 'UC-02': 'Should' }, [['AC-01', 'UC-01']]));
179
- const wb = makeWB([makePhase('US-02', [makeTask('US-02-TASK-BE-01', ['AC-01'])])]);
180
- writeWB(wb);
181
-
182
- // Act
183
- const { exitCode, report } = runValidator(tmpFile, tmpReqFile);
184
-
185
- // Assert
186
- expect(exitCode).toBe(1);
187
- const error = report.errors.find(e => e.category === 'ac_wrong_us');
188
- expect(error).toBeDefined();
189
- });
190
-
191
- // ── Test C: unscoped AC (All UCs) — no ac_wrong_us regardless of phase ────────
192
-
193
- test('exits 0 and report contains no ac_wrong_us errors when an unscoped AC (All UCs) is referenced by a task in any phase', () => {
194
- // Arrange — custom req: UC-01 (Must), UC-02 (Should), AC-01 scoped to "All UCs"
195
- // (unscoped = true, priority Must as strongest of Must+Should).
196
- // Task is in US-02; unscoped ACs are never flagged for wrong phase.
197
- // AC-01 is referenced → check 21 passes.
198
- tmpReqFile = path.join(os.tmpdir(), `req-t20-${Date.now()}.md`);
199
- fs.writeFileSync(tmpReqFile, makeReqMd({ 'UC-01': 'Must', 'UC-02': 'Should' }, [['AC-01', 'All UCs']]));
200
- const wb = makeWB([makePhase('US-02', [makeTask('US-02-TASK-BE-01', ['AC-01'])])]);
201
- writeWB(wb);
202
-
203
- // Act
204
- const { exitCode, report } = runValidator(tmpFile, tmpReqFile);
205
-
206
- // Assert
207
- expect(exitCode).toBe(0);
208
- expect(report.errors.filter(e => e.category === 'ac_wrong_us')).toHaveLength(0);
209
- });
210
-
211
- // ── Test D: multi-UC AC referenced by one of its allowed phases ───────────────
212
-
213
- test('exits 0 and report contains no ac_wrong_us errors when a multi-UC AC scoped to UC-01 and UC-02 is referenced by a task in the US-02 phase', () => {
214
- // Arrange — custom req: UC-01 (Must), UC-02 (Should), AC-01 scoped to "UC-01, UC-02"
215
- // → allowedUserStories: ['US-01', 'US-02'], priority Must (strongest of Must+Should).
216
- // Task is in US-02, which is in allowedUserStories → scope is valid.
217
- // AC-01 is referenced → check 21 passes.
218
- tmpReqFile = path.join(os.tmpdir(), `req-t20-${Date.now()}.md`);
219
- fs.writeFileSync(tmpReqFile, makeReqMd({ 'UC-01': 'Must', 'UC-02': 'Should' }, [['AC-01', 'UC-01, UC-02']]));
220
- const wb = makeWB([makePhase('US-02', [makeTask('US-02-TASK-BE-01', ['AC-01'])])]);
221
- writeWB(wb);
222
-
223
- // Act
224
- const { exitCode, report } = runValidator(tmpFile, tmpReqFile);
225
-
226
- // Assert
227
- expect(exitCode).toBe(0);
228
- expect(report.errors.filter(e => e.category === 'ac_wrong_us')).toHaveLength(0);
229
- });
230
- });
231
-
232
- // ─────────────────────────────────────────────────────────────────────────────
233
-
234
- describe('wb-validate.js — check 21 — Must AC coverage (must_ac_uncovered)', () => {
235
-
236
- // ── Test A: Must AC is covered by a task ──────────────────────────────────────
237
-
238
- test('exits 0 and report contains no must_ac_uncovered errors when every Must-priority AC is referenced by at least one task', () => {
239
- // Arrange — custom req: UC-01 (Must), AC-01 (Must). Task references AC-01.
240
- tmpReqFile = path.join(os.tmpdir(), `req-t20-${Date.now()}.md`);
241
- fs.writeFileSync(tmpReqFile, makeReqMd({ 'UC-01': 'Must' }, [['AC-01', 'UC-01']]));
242
- const wb = makeWB([makePhase('US-01', [makeTask('US-01-TASK-BE-01', ['AC-01'])])]);
243
- writeWB(wb);
244
-
245
- // Act
246
- const { exitCode, report } = runValidator(tmpFile, tmpReqFile);
247
-
248
- // Assert
249
- expect(exitCode).toBe(0);
250
- expect(report.errors.filter(e => e.category === 'must_ac_uncovered')).toHaveLength(0);
251
- });
252
-
253
- // ── Test B: Must AC is not covered by any task ────────────────────────────────
254
-
255
- test('exits 1 and report.errors contains must_ac_uncovered with details.acId "AC-01" when a Must-priority AC is not referenced by any task', () => {
256
- // Arrange — custom req: UC-01 (Must), AC-01 (Must). Task has empty acceptanceCriteria.
257
- tmpReqFile = path.join(os.tmpdir(), `req-t20-${Date.now()}.md`);
258
- fs.writeFileSync(tmpReqFile, makeReqMd({ 'UC-01': 'Must' }, [['AC-01', 'UC-01']]));
259
- const wb = makeWB([makePhase('US-01', [makeTask('US-01-TASK-BE-01', [])])]);
260
- writeWB(wb);
261
-
262
- // Act
263
- const { exitCode, report } = runValidator(tmpFile, tmpReqFile);
264
-
265
- // Assert
266
- expect(exitCode).toBe(1);
267
- const error = report.errors.find(e => e.category === 'must_ac_uncovered');
268
- expect(error).toBeDefined();
269
- expect(error.details.acId).toBe('AC-01');
270
- });
271
-
272
- // ── Test C: AC checks skipped entirely when no requirements path is given ──────
273
-
274
- test('exits 0 and report contains no AC-related errors when validator is invoked without a requirements path argument', () => {
275
- // Arrange — WB with a task referencing a fake AC ID (AC-99).
276
- // Without a requirements path, all three AC checks (19, 20, 21) are skipped.
277
- const wb = makeWB([makePhase('US-01', [makeTask('US-01-TASK-BE-01', ['AC-99'])])]);
278
- writeWB(wb);
279
-
280
- // Act — no second argument; requirementsPath is undefined → acMap is null → checks skipped
281
- const { exitCode, report } = runValidator(tmpFile);
282
-
283
- // Assert
284
- expect(exitCode).toBe(0);
285
- const acErrors = report.errors.filter(e =>
286
- e.category === 'ac_not_found' ||
287
- e.category === 'ac_wrong_us' ||
288
- e.category === 'must_ac_uncovered'
289
- );
290
- expect(acErrors).toHaveLength(0);
291
- });
292
- });
293
-
294
- // ─────────────────────────────────────────────────────────────────────────────
295
- // I5 contract-pinning — parseAcTable fatal errors must use exit 2 (not exit 1)
296
- //
297
- // Exit 1 means "validation completed, JSON report on stdout".
298
- // Exit 2 means "fatal/parse error, no JSON on stdout (stderr only)".
299
- //
300
- // Before fix: two parseAcTable paths used exit(1) without emitting a JSON
301
- // report — pm-phase2 would see empty stdout and throw "wb-validate returned
302
- // empty stdout" masking the real cause. Fixed to exit(2).
303
- // ─────────────────────────────────────────────────────────────────────────────
304
-
305
- describe('wb-validate.js — parseAcTable fatal errors exit with code 2 and no JSON on stdout (I5)', () => {
306
-
307
- function writeTmpReq(content) {
308
- tmpReqFile = path.join(os.tmpdir(), `req-i5-${Date.now()}.md`);
309
- fs.writeFileSync(tmpReqFile, content);
310
- return tmpReqFile;
311
- }
312
-
313
- test('exits 2 and emits only stderr (no JSON on stdout) when AC row references a UC that does not exist in the requirements', () => {
314
- // Arrange: requirements declares UC-01 but AC-01 references UC-02 (missing)
315
- const reqContent = makeReqMd(
316
- { 'UC-01': 'Must' },
317
- [['AC-01', 'UC-02']], // UC-02 not declared → parseAcTable exit(1) before fix, exit(2) after
318
- );
319
- writeTmpReq(reqContent);
320
- const wb = makeWB([makePhase('US-01', [makeTask('US-01-TASK-BE-01', ['AC-01'])])]);
321
- writeWB(wb);
322
-
323
- // Act
324
- const result = spawnSync('node', [VALIDATE, tmpFile, tmpReqFile], { encoding: 'utf8' });
325
-
326
- // Assert: exit 2 = fatal/parse error; stdout must NOT be parseable JSON
327
- expect(result.status).toBe(2);
328
- expect(result.stderr).toMatch(/does not exist/i);
329
- let parsed = null;
330
- try { parsed = JSON.parse(result.stdout); } catch (_) {}
331
- expect(parsed).toBeNull();
332
- });
333
-
334
- test('exits 2 and emits only stderr (no JSON on stdout) when a UC heading exists but has no Priority row (parser never adds it to the map, so the "does not exist" guard fires)', () => {
335
- // Arrange: UC-01 heading exists but no "| Priority | ... |" metadata row.
336
- // ucPriorityMap never adds UC-01 → !has(ucRef) → exit 2, "does not exist".
337
- // Note: the !get(ucRef) ("no declared priority") guard is structurally unreachable
338
- // by construction — the parser only adds a UC to the map when it finds a non-empty
339
- // priority value via \S+ regex, so has(ucRef) being true implies get(ucRef) is truthy.
340
- // Both exit(1)→exit(2) fixes are correctness guards; this test verifies that the
341
- // "missing priority" scenario always resolves through exit 2 in practice.
342
- const reqContent =
343
- '## 5. Use Cases\n\n' +
344
- '### UC-01: Description\n| Field | Value |\n|-------|-------|\n| Other | x |\n\n' +
345
- '## 7. Acceptance Criteria\n\n' +
346
- '| ID | Criterion | Related UC |\n|----|-----------|------------|\n' +
347
- '| AC-01 | A criterion | UC-01 |\n';
348
- writeTmpReq(reqContent);
349
- const wb = makeWB([makePhase('US-01', [makeTask('US-01-TASK-BE-01', ['AC-01'])])]);
350
- writeWB(wb);
351
-
352
- // Act
353
- const result = spawnSync('node', [VALIDATE, tmpFile, tmpReqFile], { encoding: 'utf8' });
354
-
355
- // Assert: exit 2 = fatal/parse error; stdout must NOT be parseable JSON
356
- expect(result.status).toBe(2);
357
- expect(result.stderr).toMatch(/does not exist/i);
358
- let parsed = null;
359
- try { parsed = JSON.parse(result.stdout); } catch (_) {}
360
- expect(parsed).toBeNull();
361
- });
362
- });
@@ -1,297 +0,0 @@
1
- 'use strict'
2
- const { spawnSync } = require('child_process')
3
- const path = require('path')
4
- const fs = require('fs')
5
- const os = require('os')
6
-
7
- const VALIDATE = path.resolve(__dirname, '../wb-validate.js')
8
- const FIXTURES = path.resolve(__dirname, 'fixtures')
9
-
10
- function runValidator(wbPath, reqPath) {
11
- const args = [VALIDATE, wbPath]
12
- if (reqPath) args.push(reqPath)
13
- const result = spawnSync('node', args, { encoding: 'utf8' })
14
- let report = null
15
- try { report = JSON.parse(result.stdout) } catch (_) {}
16
- return { exitCode: result.status, report, stderr: result.stderr, rawStdout: result.stdout }
17
- }
18
-
19
- let tmpFile
20
- beforeEach(() => { tmpFile = path.join(os.tmpdir(), `wb-t21-${Date.now()}.json`) })
21
- afterEach(() => { try { fs.unlinkSync(tmpFile) } catch (_) {} })
22
- function writeFixture(obj) { fs.writeFileSync(tmpFile, JSON.stringify(obj, null, 2)); return tmpFile }
23
-
24
- function makeTask(id, overrides = {}) {
25
- return {
26
- id, title: `Task ${id}`, outcome: 'Done', domain: 'BE',
27
- agentType: 'developer-backend', dependsOn: [], acceptanceCriteria: [],
28
- verification: { commands: ['node --version'] },
29
- estimate: { agentMinutes: 10, tokens: 5000 },
30
- outputCount: 1, groupingRationale: 'Single', commit: { subject: `feat: ${id}` },
31
- ...overrides,
32
- }
33
- }
34
- function makePhase(id, tasks, overrides = {}) {
35
- return { id, type: 'user-story', title: `Phase ${id}`, commit: `feat(${id}): done`, tasks, ...overrides }
36
- }
37
- function makeWB(phases) { return { schemaVersion: 2, phases } }
38
-
39
- // ─────────────────────────────────────────────────────────────────────────────
40
- // W1 regression guard — checks 22 and 23 must not crash on missing/non-array phases
41
- // Before fix: both checks iterated wb.phases directly → TypeError: not iterable.
42
- // After fix: both use the guarded `phases` constant (same as checks 3-21).
43
- // ─────────────────────────────────────────────────────────────────────────────
44
-
45
- describe('wb-validate.js — checks 22 and 23 — no crash when wb.phases is absent or non-array', () => {
46
-
47
- test('exits 0 and emits valid JSON when wb.phases is absent (missing field)', () => {
48
- // Arrange: valid schemaVersion but no phases field at all
49
- const wb = { schemaVersion: 2 }
50
- writeFixture(wb)
51
- const { exitCode, report } = runValidator(tmpFile)
52
- // Assert: no TypeError — exits cleanly and produces a structured report
53
- expect(exitCode).toBe(0)
54
- expect(report).not.toBeNull()
55
- expect(Array.isArray(report.errors)).toBe(true)
56
- })
57
-
58
- test('exits 0 and emits valid JSON when wb.phases is null', () => {
59
- // Arrange
60
- const wb = { schemaVersion: 2, phases: null }
61
- writeFixture(wb)
62
- const { exitCode, report } = runValidator(tmpFile)
63
- expect(exitCode).toBe(0)
64
- expect(report).not.toBeNull()
65
- expect(Array.isArray(report.errors)).toBe(true)
66
- })
67
-
68
- test('exits 0 and emits valid JSON when wb.phases is a non-array object', () => {
69
- // Arrange
70
- const wb = { schemaVersion: 2, phases: { id: 'US-01' } }
71
- writeFixture(wb)
72
- const { exitCode, report } = runValidator(tmpFile)
73
- expect(exitCode).toBe(0)
74
- expect(report).not.toBeNull()
75
- expect(Array.isArray(report.errors)).toBe(true)
76
- })
77
- })
78
-
79
- // ─────────────────────────────────────────────────────────────────────────────
80
- // Check 22 — invalid text field characters (invalid_text_field_chars)
81
- // Forbidden characters: | (pipe), \r (CR), \n (LF)
82
- // ─────────────────────────────────────────────────────────────────────────────
83
-
84
- describe('wb-validate.js — check 22 — invalid text field characters (invalid_text_field_chars)', () => {
85
-
86
- // ── Test A: pipe in task title ────────────────────────────────────────────────
87
-
88
- test('exits 1 and report.errors contains invalid_text_field_chars with field "title" when task title contains a pipe character', () => {
89
- // Arrange
90
- const wb = makeWB([makePhase('US-01', [makeTask('US-01-TASK-BE-01', { title: 'Task with | pipe' })])])
91
- writeFixture(wb)
92
-
93
- // Act
94
- const { exitCode, report } = runValidator(tmpFile)
95
-
96
- // Assert
97
- expect(exitCode).toBe(1)
98
- const error = report.errors.find(e => e.category === 'invalid_text_field_chars')
99
- expect(error).toBeDefined()
100
- expect(error.field).toBe('title')
101
- })
102
-
103
- // ── Test B: LF in commit.subject ──────────────────────────────────────────────
104
-
105
- test('exits 1 and report.errors contains invalid_text_field_chars with field "commit.subject" when task commit subject contains a newline character', () => {
106
- // Arrange
107
- const wb = makeWB([makePhase('US-01', [makeTask('US-01-TASK-BE-01', { commit: { subject: 'feat: line1\nline2' } })])])
108
- writeFixture(wb)
109
-
110
- // Act
111
- const { exitCode, report } = runValidator(tmpFile)
112
-
113
- // Assert
114
- expect(exitCode).toBe(1)
115
- const error = report.errors.find(e => e.category === 'invalid_text_field_chars')
116
- expect(error).toBeDefined()
117
- expect(error.field).toBe('commit.subject')
118
- })
119
-
120
- // ── Test C: CR in phase title ─────────────────────────────────────────────────
121
-
122
- test('exits 1 and report.errors contains invalid_text_field_chars with field "title" and phaseId set when phase title contains a carriage return character', () => {
123
- // Arrange
124
- const wb = makeWB([makePhase('US-01', [makeTask('US-01-TASK-BE-01')], { title: 'Phase\rTitle' })])
125
- writeFixture(wb)
126
-
127
- // Act
128
- const { exitCode, report } = runValidator(tmpFile)
129
-
130
- // Assert
131
- expect(exitCode).toBe(1)
132
- const error = report.errors.find(e => e.category === 'invalid_text_field_chars')
133
- expect(error).toBeDefined()
134
- expect(error.field).toBe('title')
135
- expect(error.phaseId).toBe('US-01')
136
- })
137
-
138
- // ── Test D: clean fields — no invalid_text_field_chars error ──────────────────
139
-
140
- test('exits 0 and report contains no invalid_text_field_chars errors when all text fields contain only valid characters', () => {
141
- // Arrange — default makeTask and makePhase produce clean strings with no pipe, CR, or LF
142
- const wb = makeWB([makePhase('US-01', [makeTask('US-01-TASK-BE-01')])])
143
- writeFixture(wb)
144
-
145
- // Act
146
- const { exitCode, report } = runValidator(tmpFile)
147
-
148
- // Assert
149
- expect(exitCode).toBe(0)
150
- expect(report.errors.filter(e => e.category === 'invalid_text_field_chars')).toHaveLength(0)
151
- })
152
- })
153
-
154
- // ─────────────────────────────────────────────────────────────────────────────
155
- // Check 23 — empty phase (empty_phase)
156
- // ─────────────────────────────────────────────────────────────────────────────
157
-
158
- describe('wb-validate.js — check 23 — empty phase (empty_phase)', () => {
159
-
160
- // ── Test A: phase with no tasks ───────────────────────────────────────────────
161
-
162
- test('exits 1 and report.errors contains empty_phase when a phase has an empty tasks array', () => {
163
- // Arrange
164
- const wb = makeWB([makePhase('US-01', [])])
165
- writeFixture(wb)
166
-
167
- // Act
168
- const { exitCode, report } = runValidator(tmpFile)
169
-
170
- // Assert
171
- expect(exitCode).toBe(1)
172
- const error = report.errors.find(e => e.category === 'empty_phase')
173
- expect(error).toBeDefined()
174
- })
175
-
176
- // ── Test B: phase with at least one task ──────────────────────────────────────
177
-
178
- test('exits 0 and report contains no empty_phase errors when a phase has at least one task', () => {
179
- // Arrange
180
- const wb = makeWB([makePhase('US-01', [makeTask('US-01-TASK-BE-01')])])
181
- writeFixture(wb)
182
-
183
- // Act
184
- const { exitCode, report } = runValidator(tmpFile)
185
-
186
- // Assert
187
- expect(exitCode).toBe(0)
188
- expect(report.errors.filter(e => e.category === 'empty_phase')).toHaveLength(0)
189
- })
190
- })
191
-
192
- // ─────────────────────────────────────────────────────────────────────────────
193
- // Report output contract — structure and exit codes
194
- // ─────────────────────────────────────────────────────────────────────────────
195
-
196
- describe('wb-validate.js — report output contract', () => {
197
-
198
- // ── Test A: valid WB — full report shape ──────────────────────────────────────
199
-
200
- test('exits 0 and emits a report with all required top-level keys and correct types when the work breakdown is valid', () => {
201
- // Arrange — use the canonical valid fixture; no requirements path so AC checks are skipped
202
- // Act
203
- const { exitCode, report } = runValidator(path.join(FIXTURES, 'wb-valid.json'))
204
-
205
- // Assert
206
- expect(exitCode).toBe(0)
207
- expect(report).toMatchObject({
208
- valid: true,
209
- schemaVersion: 2,
210
- taskCount: expect.any(Number),
211
- errors: [],
212
- warnings: expect.any(Array),
213
- durationBands: expect.objectContaining({
214
- target: expect.any(Number),
215
- above_target: expect.any(Number),
216
- warning: expect.any(Number),
217
- split_required: expect.any(Number),
218
- }),
219
- domainDistribution: expect.objectContaining({
220
- BE: expect.any(Number),
221
- FE: expect.any(Number),
222
- DB: expect.any(Number),
223
- DevOps: expect.any(Number),
224
- INFRA: expect.any(Number),
225
- TEST: expect.any(Number),
226
- }),
227
- dependencies: expect.objectContaining({
228
- taskGraph: expect.any(Object),
229
- taskCycles: expect.any(Array),
230
- phaseGraph: expect.any(Object),
231
- phaseCycles: expect.any(Array),
232
- phaseWaves: expect.any(Array),
233
- phaseUnschedulable: expect.any(Array),
234
- }),
235
- })
236
- })
237
-
238
- // ── Test B: invalid WB — valid = false and errors non-empty ──────────────────
239
-
240
- test('exits 1 and emits a report with valid=false and at least one error when the work breakdown has a duplicate task ID', () => {
241
- // Arrange — the duplicate-ID fixture contains INFRA-TASK-INFRA-01 in two phases
242
- // Act
243
- const { exitCode, report } = runValidator(path.join(FIXTURES, 'wb-invalid-duplicate-id.json'))
244
-
245
- // Assert
246
- expect(exitCode).toBe(1)
247
- expect(report.valid).toBe(false)
248
- expect(report.errors.length).toBeGreaterThan(0)
249
- })
250
-
251
- // ── Test C: exit code 2 on file not found ─────────────────────────────────────
252
-
253
- test('exits with code 2 when the specified work breakdown file does not exist', () => {
254
- // Arrange — path points to a non-existent file
255
- // Act
256
- const result = spawnSync('node', [VALIDATE, '/nonexistent/path/wb.json'], { encoding: 'utf8' })
257
-
258
- // Assert
259
- expect(result.status).toBe(2)
260
- })
261
-
262
- // ── Test D: exit code 2 on invalid JSON ───────────────────────────────────────
263
-
264
- test('exits with code 2 when the work breakdown file contains invalid JSON', () => {
265
- // Arrange — write a file with malformed JSON content
266
- fs.writeFileSync(tmpFile, '{ invalid json')
267
-
268
- // Act
269
- const result = spawnSync('node', [VALIDATE, tmpFile], { encoding: 'utf8' })
270
-
271
- // Assert
272
- expect(result.status).toBe(2)
273
- })
274
-
275
- // ── Test E: exit code 2 on missing path argument ──────────────────────────────
276
-
277
- test('exits with code 2 when no work breakdown path argument is provided', () => {
278
- // Arrange: invoke with no arguments
279
- // Act
280
- const result = spawnSync('node', [VALIDATE], { encoding: 'utf8' })
281
-
282
- // Assert
283
- expect(result.status).toBe(2)
284
- })
285
-
286
- // ── Test F: stdout is always valid JSON on exit 1 ─────────────────────────────
287
-
288
- test('emits valid JSON to stdout even when validation fails and exits with code 1', () => {
289
- // Arrange — the duplicate-ID fixture produces exit 1 but must still emit valid JSON
290
- // Act
291
- const { exitCode, report } = runValidator(path.join(FIXTURES, 'wb-invalid-duplicate-id.json'))
292
-
293
- // Assert — if report is non-null, JSON.parse succeeded without throwing
294
- expect(exitCode).toBe(1)
295
- expect(report).not.toBeNull()
296
- })
297
- })