@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,262 +0,0 @@
1
- 'use strict';
2
-
3
- /**
4
- * US-03-T04: Semantic Validator Output Schema Verification
5
- *
6
- * Reads a JSON file produced by (or simulating) the validate-work-breakdown-semantic
7
- * agent and verifies that it conforms to the declared output contract:
8
- *
9
- * {
10
- * "valid": boolean,
11
- * "findings": [
12
- * {
13
- * "taskId": string,
14
- * "type": "hidden_multiplicity"|"scope_creep"|"estimate_incompatible"|"bundled_verifiable"|"other",
15
- * "severity": "error"|"warning",
16
- * "blocking": boolean,
17
- * "splitRequired": boolean,
18
- * "description": string
19
- * }
20
- * ]
21
- * }
22
- *
23
- * Invariants:
24
- * - valid === false iff any finding has blocking === true
25
- * - severity === "error" when blocking === true
26
- * - severity === "warning" when blocking === false
27
- *
28
- * Usage:
29
- * node verify-us-03-t04.js <path-to-output.json>
30
- *
31
- * Exit 0 = all checks pass; Exit 1 = one or more checks fail.
32
- */
33
-
34
- const fs = require('fs');
35
- const path = require('path');
36
-
37
- // ─── Constants ────────────────────────────────────────────────────────────────
38
-
39
- const VALID_TYPES = new Set([
40
- 'hidden_multiplicity',
41
- 'scope_creep',
42
- 'estimate_incompatible',
43
- 'bundled_verifiable',
44
- 'other',
45
- ]);
46
-
47
- const VALID_SEVERITIES = new Set(['error', 'warning']);
48
-
49
- // ─── CLI argument ─────────────────────────────────────────────────────────────
50
-
51
- const filePath = process.argv[2];
52
- if (!filePath) {
53
- console.error('Usage: node verify-us-03-t04.js <path-to-output.json>');
54
- process.exit(2);
55
- }
56
-
57
- const resolved = path.resolve(filePath);
58
-
59
- // ─── Report accumulators ─────────────────────────────────────────────────────
60
-
61
- const passes = [];
62
- const failures = [];
63
-
64
- function pass(label) {
65
- passes.push(label);
66
- }
67
-
68
- function fail(label, detail) {
69
- failures.push({ label, detail });
70
- }
71
-
72
- // ─── Load and parse the output JSON ──────────────────────────────────────────
73
-
74
- let output;
75
- try {
76
- const raw = fs.readFileSync(resolved, 'utf8');
77
- output = JSON.parse(raw);
78
- } catch (err) {
79
- console.error(`FATAL: Cannot read or parse file at ${resolved}`);
80
- console.error(` ${err.message}`);
81
- process.exit(2);
82
- }
83
-
84
- // ─── Check 1: top-level "valid" field is a boolean ───────────────────────────
85
-
86
- if (typeof output.valid === 'boolean') {
87
- pass('Check 1 — "valid" is a boolean');
88
- } else {
89
- fail(
90
- 'Check 1 — "valid" is a boolean',
91
- `Expected boolean, got: ${JSON.stringify(typeof output.valid)} (value: ${JSON.stringify(output.valid)})`
92
- );
93
- }
94
-
95
- // ─── Check 2: top-level "findings" field is an array ─────────────────────────
96
-
97
- if (Array.isArray(output.findings)) {
98
- pass(`Check 2 — "findings" is an array (${output.findings.length} item(s))`);
99
- } else {
100
- fail(
101
- 'Check 2 — "findings" is an array',
102
- `Expected array, got: ${JSON.stringify(typeof output.findings)}`
103
- );
104
- }
105
-
106
- // ─── Per-finding checks (only when findings is an array) ─────────────────────
107
-
108
- const findings = Array.isArray(output.findings) ? output.findings : [];
109
-
110
- // Check 3: every finding has all 6 required fields with correct types
111
-
112
- {
113
- const REQUIRED_FIELDS = ['taskId', 'type', 'severity', 'blocking', 'splitRequired', 'description'];
114
- let allPresent = true;
115
-
116
- for (let i = 0; i < findings.length; i++) {
117
- const finding = findings[i];
118
- const label = finding.taskId ? `finding[${i}] (taskId="${finding.taskId}")` : `finding[${i}]`;
119
-
120
- for (const field of REQUIRED_FIELDS) {
121
- if (!(field in finding)) {
122
- fail(
123
- 'Check 3 — every finding has all 6 required fields',
124
- `${label} is missing field "${field}"`
125
- );
126
- allPresent = false;
127
- }
128
- }
129
-
130
- // Type-check the boolean fields
131
- if ('blocking' in finding && typeof finding.blocking !== 'boolean') {
132
- fail(
133
- 'Check 3 — every finding has all 6 required fields',
134
- `${label}: "blocking" must be boolean, got ${JSON.stringify(typeof finding.blocking)}`
135
- );
136
- allPresent = false;
137
- }
138
-
139
- if ('splitRequired' in finding && typeof finding.splitRequired !== 'boolean') {
140
- fail(
141
- 'Check 3 — every finding has all 6 required fields',
142
- `${label}: "splitRequired" must be boolean, got ${JSON.stringify(typeof finding.splitRequired)}`
143
- );
144
- allPresent = false;
145
- }
146
- }
147
-
148
- if (allPresent) {
149
- pass(`Check 3 — every finding has all 6 required fields with correct types`);
150
- }
151
- }
152
-
153
- // Check 4: every finding.type is one of the allowed values
154
-
155
- {
156
- let allValid = true;
157
- for (let i = 0; i < findings.length; i++) {
158
- const finding = findings[i];
159
- if (!VALID_TYPES.has(finding.type)) {
160
- fail(
161
- 'Check 4 — every finding.type is a valid enum value',
162
- `finding[${i}] (taskId="${finding.taskId}") has type "${finding.type}"; ` +
163
- `allowed: ${[...VALID_TYPES].join(', ')}`
164
- );
165
- allValid = false;
166
- }
167
- }
168
- if (allValid) {
169
- pass('Check 4 — every finding.type is a valid enum value');
170
- }
171
- }
172
-
173
- // Check 5: every finding.severity is "error" or "warning"
174
-
175
- {
176
- let allValid = true;
177
- for (let i = 0; i < findings.length; i++) {
178
- const finding = findings[i];
179
- if (!VALID_SEVERITIES.has(finding.severity)) {
180
- fail(
181
- 'Check 5 — every finding.severity is "error" or "warning"',
182
- `finding[${i}] (taskId="${finding.taskId}") has severity "${finding.severity}"`
183
- );
184
- allValid = false;
185
- }
186
- }
187
- if (allValid) {
188
- pass('Check 5 — every finding.severity is "error" or "warning"');
189
- }
190
- }
191
-
192
- // Check 6: valid === false iff any finding has blocking === true
193
-
194
- {
195
- const hasBlockingFinding = findings.some(f => f.blocking === true);
196
- const validField = output.valid;
197
-
198
- const expectedValid = !hasBlockingFinding;
199
-
200
- if (typeof validField === 'boolean' && validField === expectedValid) {
201
- pass(
202
- `Check 6 — valid === false iff any finding has blocking:true ` +
203
- `(blocking findings: ${findings.filter(f => f.blocking).length})`
204
- );
205
- } else {
206
- fail(
207
- 'Check 6 — valid === false iff any finding has blocking:true',
208
- `hasBlockingFinding=${hasBlockingFinding}, so valid should be ${expectedValid}, but got ${JSON.stringify(validField)}`
209
- );
210
- }
211
- }
212
-
213
- // Check 7: severity === "error" when blocking === true; "warning" when blocking === false
214
-
215
- {
216
- let allConsistent = true;
217
- for (let i = 0; i < findings.length; i++) {
218
- const finding = findings[i];
219
-
220
- if (finding.blocking === true && finding.severity !== 'error') {
221
- fail(
222
- 'Check 7 — severity is "error" when blocking:true, "warning" when blocking:false',
223
- `finding[${i}] (taskId="${finding.taskId}") has blocking:true but severity="${finding.severity}" (expected "error")`
224
- );
225
- allConsistent = false;
226
- }
227
-
228
- if (finding.blocking === false && finding.severity !== 'warning') {
229
- fail(
230
- 'Check 7 — severity is "error" when blocking:true, "warning" when blocking:false',
231
- `finding[${i}] (taskId="${finding.taskId}") has blocking:false but severity="${finding.severity}" (expected "warning")`
232
- );
233
- allConsistent = false;
234
- }
235
- }
236
-
237
- if (allConsistent) {
238
- pass('Check 7 — severity is "error" when blocking:true, "warning" when blocking:false');
239
- }
240
- }
241
-
242
- // ─── Print report ─────────────────────────────────────────────────────────────
243
-
244
- console.log('\n=== Semantic Validator Output — Schema Contract Verification ===\n');
245
- console.log(`File: ${resolved}\n`);
246
-
247
- for (const p of passes) {
248
- console.log(` PASS ${p}`);
249
- }
250
-
251
- if (failures.length > 0) {
252
- console.log('');
253
- for (const f of failures) {
254
- console.log(` FAIL ${f.label}`);
255
- console.log(` ${f.detail}`);
256
- }
257
- console.log(`\nResult: FAIL — ${failures.length} failure(s), ${passes.length} check(s) passed\n`);
258
- process.exit(1);
259
- } else {
260
- console.log(`\nResult: PASS — all ${passes.length} checks passed\n`);
261
- process.exit(0);
262
- }
@@ -1,337 +0,0 @@
1
- 'use strict'
2
-
3
- const { spawnSync } = require('child_process')
4
- const os = require('os')
5
- const fs = require('fs')
6
- const path = require('path')
7
-
8
- const RENDER = path.resolve(__dirname, '../wb-render.js')
9
- const FIXTURE = path.resolve(__dirname, 'fixtures/wb-valid.json')
10
- const TMPDIR = os.tmpdir()
11
- const PREFIX = 'WB-TEST'
12
-
13
- function runRender(jsonPath, prefix, destDir) {
14
- const args = [jsonPath, prefix]
15
- if (destDir) args.push(destDir)
16
- return spawnSync(process.execPath, [RENDER, ...args], { encoding: 'utf8' })
17
- }
18
-
19
- function mdPath(dir, prefix) { return path.join(dir, `${prefix}-Work-Breakdown.md`) }
20
- function csvPath(dir, prefix) { return path.join(dir, `${prefix}-Work-Breakdown.csv`) }
21
-
22
- // ─────────────────────────────────────────────────────────────────────────────
23
- // Group 1: Exit codes and file creation
24
- // ─────────────────────────────────────────────────────────────────────────────
25
-
26
- describe('wb-render.js — Group 1: exit codes and file creation', () => {
27
-
28
- // ── Test 1: exits 0 and writes both output files ─────────────────────────────
29
-
30
- test('exits 0 and writes both MD and CSV output files when given a valid fixture', () => {
31
- // Arrange
32
- const tmpDir = fs.mkdtempSync(path.join(TMPDIR, 'wb-render-g1a-'))
33
- try {
34
- // Act
35
- const result = runRender(FIXTURE, PREFIX, tmpDir)
36
-
37
- // Assert
38
- expect(result.status).toBe(0)
39
- expect(fs.existsSync(mdPath(tmpDir, PREFIX))).toBe(true)
40
- expect(fs.existsSync(csvPath(tmpDir, PREFIX))).toBe(true)
41
- } finally {
42
- fs.rmSync(tmpDir, { recursive: true, force: true })
43
- }
44
- })
45
-
46
- // ── Test 2: exits 1 when JSON file missing ────────────────────────────────────
47
-
48
- test('exits 1 when the JSON file path does not exist', () => {
49
- // Arrange: path points to a file that does not exist on disk
50
- const nonExistentPath = path.join(TMPDIR, `no-such-wb-file-${Date.now()}.json`)
51
-
52
- // Act
53
- const result = runRender(nonExistentPath, PREFIX)
54
-
55
- // Assert
56
- expect(result.status).toBe(1)
57
- })
58
-
59
- // ── Test 3: exits 1 when JSON file contains invalid JSON ─────────────────────
60
-
61
- test('exits 1 when the JSON file contains invalid JSON', () => {
62
- // Arrange
63
- const tmpFile = path.join(TMPDIR, `wb-render-bad-json-${Date.now()}.json`)
64
- fs.writeFileSync(tmpFile, '{bad json')
65
- try {
66
- // Act
67
- const result = runRender(tmpFile, PREFIX)
68
-
69
- // Assert
70
- expect(result.status).toBe(1)
71
- } finally {
72
- try { fs.unlinkSync(tmpFile) } catch (_) {}
73
- }
74
- })
75
-
76
- // ── Test 4: exits 2 when schemaVersion is wrong ───────────────────────────────
77
-
78
- test('exits 2 when schemaVersion is not 2', () => {
79
- // Arrange
80
- const tmpFile = path.join(TMPDIR, `wb-render-bad-schema-${Date.now()}.json`)
81
- const wb = { schemaVersion: 1, feature: 'TEST', phases: [] }
82
- fs.writeFileSync(tmpFile, JSON.stringify(wb))
83
- try {
84
- // Act
85
- const result = runRender(tmpFile, PREFIX)
86
-
87
- // Assert
88
- expect(result.status).toBe(2)
89
- } finally {
90
- try { fs.unlinkSync(tmpFile) } catch (_) {}
91
- }
92
- })
93
- })
94
-
95
- // ─────────────────────────────────────────────────────────────────────────────
96
- // Groups 2, 3, 4 — share a single render run against the standard fixture
97
- // ─────────────────────────────────────────────────────────────────────────────
98
-
99
- describe('wb-render.js — Groups 2/3/4: output content', () => {
100
- let tmpDir
101
- let csvContent
102
- let mdContent
103
- let csvRows // array of string arrays (each row split on '|'), header excluded
104
-
105
- beforeAll(() => {
106
- // Arrange: render once into a unique temp directory
107
- tmpDir = fs.mkdtempSync(path.join(TMPDIR, 'wb-render-shared-'))
108
- runRender(FIXTURE, PREFIX, tmpDir)
109
- csvContent = fs.readFileSync(csvPath(tmpDir, PREFIX), 'utf8')
110
- mdContent = fs.readFileSync(mdPath(tmpDir, PREFIX), 'utf8')
111
- const allLines = csvContent.split('\n').filter(l => l.trim() !== '')
112
- // Exclude the header row; parse each data row into columns
113
- csvRows = allLines.slice(1).map(l => l.split('|'))
114
- })
115
-
116
- afterAll(() => {
117
- if (tmpDir) fs.rmSync(tmpDir, { recursive: true, force: true })
118
- })
119
-
120
- // ───────────────────────────────────────────────────────────────────────────
121
- // Group 2: CSV structure
122
- // ───────────────────────────────────────────────────────────────────────────
123
-
124
- describe('Group 2: CSV structure', () => {
125
-
126
- // ── Test 5: CSV header is exact ─────────────────────────────────────────────
127
-
128
- test('CSV header line is exactly "phase_id|phase_title|commit_message|depends_on|task_id|task_title|domain|agent_type"', () => {
129
- // Arrange: csvContent contains the full file including the header row
130
-
131
- // Act
132
- const headerLine = csvContent.split('\n')[0]
133
-
134
- // Assert
135
- expect(headerLine).toBe('phase_id|phase_title|commit_message|depends_on|task_id|task_title|domain|agent_type')
136
- })
137
-
138
- // ── Test 6: every data row has exactly 8 columns ────────────────────────────
139
-
140
- test('every non-header data row contains exactly 8 pipe-separated columns', () => {
141
- // Arrange: csvRows are already parsed non-header rows (header excluded in beforeAll)
142
- expect(csvRows.length).toBeGreaterThan(0)
143
-
144
- // Act & Assert — one logical assertion group per row
145
- for (const row of csvRows) {
146
- expect(row).toHaveLength(8)
147
- }
148
- })
149
-
150
- // ── Test 7: phase_id repeats within a phase ──────────────────────────────────
151
-
152
- test('all rows whose task_id starts with "INFRA-" have phase_id "INFRA"', () => {
153
- // Arrange: filter rows by task_id column (index 4)
154
- const infraTaskRows = csvRows.filter(row => row[4].startsWith('INFRA-'))
155
-
156
- // Act (filtering already done)
157
-
158
- // Assert
159
- expect(infraTaskRows.length).toBeGreaterThan(0)
160
- for (const row of infraTaskRows) {
161
- expect(row[0]).toBe('INFRA')
162
- }
163
- })
164
-
165
- // ── Test 8: depends_on is empty for INFRA ────────────────────────────────────
166
-
167
- test('all INFRA phase rows have an empty depends_on column', () => {
168
- // Arrange: filter rows by task_id column (index 4)
169
- const infraTaskRows = csvRows.filter(row => row[4].startsWith('INFRA-'))
170
-
171
- // Act (filtering already done)
172
-
173
- // Assert — depends_on is column index 3
174
- expect(infraTaskRows.length).toBeGreaterThan(0)
175
- for (const row of infraTaskRows) {
176
- expect(row[3]).toBe('')
177
- }
178
- })
179
-
180
- // ── Test 9: depends_on contains INFRA for US-01 ─────────────────────────────
181
-
182
- test('all US-01 phase rows have depends_on containing "INFRA"', () => {
183
- // Arrange: filter rows by task_id column (index 4)
184
- const us01Rows = csvRows.filter(row => row[4].startsWith('US-01-'))
185
-
186
- // Act (filtering already done)
187
-
188
- // Assert — depends_on is column index 3
189
- expect(us01Rows.length).toBeGreaterThan(0)
190
- for (const row of us01Rows) {
191
- expect(row[3]).toContain('INFRA')
192
- }
193
- })
194
-
195
- // ── Test 10: commit_message contains the prefix argument ─────────────────────
196
-
197
- test('all CSV data rows have a commit_message column that contains the prefix argument', () => {
198
- // Arrange: csvRows are all non-header data rows; PREFIX = 'WB-TEST'
199
- expect(csvRows.length).toBeGreaterThan(0)
200
-
201
- // Act & Assert — commit_message is column index 2
202
- for (const row of csvRows) {
203
- expect(row[2]).toContain(PREFIX)
204
- }
205
- })
206
- })
207
-
208
- // ───────────────────────────────────────────────────────────────────────────
209
- // Group 3: Markdown structure
210
- // ───────────────────────────────────────────────────────────────────────────
211
-
212
- describe('Group 3: Markdown structure', () => {
213
-
214
- // ── Test 11: required section headers ───────────────────────────────────────
215
-
216
- test('MD output contains the required top-level section headers', () => {
217
- // Arrange: mdContent is the full Markdown file text
218
-
219
- // Act & Assert
220
- expect(mdContent).toContain('# Work Breakdown')
221
- expect(mdContent).toContain('## Document Info')
222
- expect(mdContent).toContain('## Summary')
223
- expect(mdContent).toContain('## Statistics')
224
- })
225
-
226
- // ── Test 12: Document Info table rows ───────────────────────────────────────
227
-
228
- test('MD Document Info table contains a Feature row and a "Schema | v2" row', () => {
229
- // Arrange: mdContent is the full Markdown file text
230
-
231
- // Act & Assert
232
- expect(mdContent).toContain('| Feature |')
233
- expect(mdContent).toContain('| Schema | v2 |')
234
- })
235
-
236
- // ── Test 13: Summary table has all 7 required rows ──────────────────────────
237
-
238
- test('MD Summary table contains all 7 required metric rows', () => {
239
- // Arrange: mdContent is the full Markdown file text
240
-
241
- // Act & Assert
242
- expect(mdContent).toContain('| Total tasks |')
243
- expect(mdContent).toContain('| Total phases |')
244
- expect(mdContent).toContain('| Within target')
245
- expect(mdContent).toContain('| Above target')
246
- expect(mdContent).toContain('| Warning')
247
- expect(mdContent).toContain('| Split required')
248
- expect(mdContent).toContain('| Domain distribution |')
249
- })
250
-
251
- // ── Test 14: Infrastructure Phase section ───────────────────────────────────
252
-
253
- test('MD output contains the Infrastructure Phase section when the INFRA phase exists in the fixture', () => {
254
- // Arrange: the standard fixture has an INFRA phase
255
-
256
- // Act & Assert
257
- expect(mdContent).toContain('## Infrastructure Phase (INFRA)')
258
- })
259
-
260
- // ── Test 15: User Story Phases section ──────────────────────────────────────
261
-
262
- test('MD output contains the User Story Phases section when non-INFRA phases exist in the fixture', () => {
263
- // Arrange: the standard fixture has a US-01 phase (non-INFRA)
264
-
265
- // Act & Assert
266
- expect(mdContent).toContain('## User Story Phases')
267
- })
268
-
269
- // ── Test 16: Statistics table has domain rows and Total row ─────────────────
270
-
271
- test('MD Statistics table contains rows for all domain categories and a Total row', () => {
272
- // Arrange: mdContent is the full Markdown file text
273
-
274
- // Act & Assert
275
- expect(mdContent).toContain('| BE |')
276
- expect(mdContent).toContain('| FE |')
277
- expect(mdContent).toContain('| DB |')
278
- expect(mdContent).toContain('| INFRA |')
279
- expect(mdContent).toContain('| TEST |')
280
- expect(mdContent).toContain('| **Total** |')
281
- })
282
- })
283
-
284
- // ───────────────────────────────────────────────────────────────────────────
285
- // Group 4: Phase-level dependency aggregation
286
- // ───────────────────────────────────────────────────────────────────────────
287
-
288
- describe('Group 4: phase-level dependency aggregation', () => {
289
-
290
- // ── Test 17: INFRA phase has no external deps ────────────────────────────────
291
-
292
- test('INFRA phase rows have an empty depends_on, confirming no external phase dependencies are aggregated', () => {
293
- // Arrange: filter rows by phase_id column (index 0)
294
- const infraRows = csvRows.filter(row => row[0] === 'INFRA')
295
-
296
- // Act (filtering already done)
297
-
298
- // Assert — depends_on is column index 3
299
- expect(infraRows.length).toBeGreaterThan(0)
300
- for (const row of infraRows) {
301
- expect(row[3]).toBe('')
302
- }
303
- })
304
-
305
- // ── Test 18: US-01 depends on INFRA ─────────────────────────────────────────
306
-
307
- test('US-01 phase rows have depends_on "INFRA" because US-01 tasks reference INFRA-phase tasks', () => {
308
- // Arrange: filter rows by phase_id column (index 0)
309
- // The fixture has US-01-TASK-BE-01 depending on INFRA-TASK-INFRA-02 (INFRA phase)
310
- const us01Rows = csvRows.filter(row => row[0] === 'US-01')
311
-
312
- // Act (filtering already done)
313
-
314
- // Assert — depends_on is column index 3
315
- expect(us01Rows.length).toBeGreaterThan(0)
316
- for (const row of us01Rows) {
317
- expect(row[3]).toContain('INFRA')
318
- }
319
- })
320
-
321
- // ── Test 19: intra-phase deps excluded from depends_on ───────────────────────
322
-
323
- test('intra-phase dependencies within US-01 are excluded from depends_on so "US-01" never appears in that column', () => {
324
- // Arrange: filter rows by phase_id column (index 0)
325
- // US-01-TASK-BE-02 depends on US-01-TASK-BE-01 — these are intra-phase and must be excluded
326
- const us01Rows = csvRows.filter(row => row[0] === 'US-01')
327
-
328
- // Act (filtering already done)
329
-
330
- // Assert — depends_on (column 3) must NOT contain 'US-01'
331
- expect(us01Rows.length).toBeGreaterThan(0)
332
- for (const row of us01Rows) {
333
- expect(row[3]).not.toContain('US-01')
334
- }
335
- })
336
- })
337
- })