@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.
- package/CHANGELOG.md +79 -0
- package/bin/cli.js +1178 -64
- package/docs/installation.md +74 -8
- package/docs/reference.md +60 -12
- package/lib/asset-catalog.js +52 -0
- package/package.json +9 -2
- package/{.claude → src/claude}/agents/install-toolkit.md +30 -103
- package/{.claude → src/claude}/workflows/am-phase1.js +9 -12
- package/{.claude → src/claude}/workflows/pm-phase2.js +2 -2
- package/.claude/.ai-toolkit-version +0 -1
- package/.claude/scripts/.gitkeep +0 -0
- package/.claude/scripts/tests/fixtures/ac-table-format.md +0 -48
- package/.claude/scripts/tests/fixtures/test-feature/TEST-001-Work-Breakdown.json +0 -234
- package/.claude/scripts/tests/fixtures/test-feature/TEST-001-semantic-output.json +0 -4
- package/.claude/scripts/tests/fixtures/test-feature/feature.md +0 -45
- package/.claude/scripts/tests/fixtures/wb-invalid-bad-domain.json +0 -39
- package/.claude/scripts/tests/fixtures/wb-invalid-cycle.json +0 -61
- package/.claude/scripts/tests/fixtures/wb-invalid-duplicate-id.json +0 -72
- package/.claude/scripts/tests/fixtures/wb-invalid-missing-field.json +0 -38
- package/.claude/scripts/tests/fixtures/wb-valid.json +0 -189
- package/.claude/scripts/tests/helpers/buildWaves.js +0 -21
- package/.claude/scripts/tests/helpers/pm-phase2-flow.js +0 -29
- package/.claude/scripts/tests/helpers/pm-phase2-gate2.js +0 -55
- package/.claude/scripts/tests/helpers/pm-phase2-ledger.js +0 -108
- package/.claude/scripts/tests/installer.scripts-distribution.test.js +0 -131
- package/.claude/scripts/tests/pm-phase2.gate2-payload.test.js +0 -353
- package/.claude/scripts/tests/pm-phase2.ledger-transitions.test.js +0 -346
- package/.claude/scripts/tests/pm-phase2.orchestration-flow.test.js +0 -276
- package/.claude/scripts/tests/verify-us-01-t03.js +0 -288
- package/.claude/scripts/tests/verify-us-03-t04.js +0 -262
- package/.claude/scripts/tests/wb-render.checks-md-csv-deps.test.js +0 -337
- package/.claude/scripts/tests/wb-render.checks-sanitize-commit-paths.test.js +0 -352
- package/.claude/scripts/tests/wb-render.regression-pm-phase3-csv.test.js +0 -219
- package/.claude/scripts/tests/wb-validate.checks-1-7.test.js +0 -330
- package/.claude/scripts/tests/wb-validate.checks-11.test.js +0 -166
- package/.claude/scripts/tests/wb-validate.checks-12-14.test.js +0 -222
- package/.claude/scripts/tests/wb-validate.checks-15-18.test.js +0 -359
- package/.claude/scripts/tests/wb-validate.checks-19-21.test.js +0 -362
- package/.claude/scripts/tests/wb-validate.checks-22-23-report.test.js +0 -297
- package/.claude/scripts/tests/wb-validate.checks-8-10.test.js +0 -243
- package/.claude/settings.json +0 -5
- /package/{.claude → src/claude}/agents/concurrency-safety-assessment.md +0 -0
- /package/{.claude → src/claude}/agents/define-feature.md +0 -0
- /package/{.claude → src/claude}/agents/dependency-injection-refactoring.md +0 -0
- /package/{.claude → src/claude}/agents/dependency-supply-chain-security.md +0 -0
- /package/{.claude → src/claude}/agents/developer-backend.md +0 -0
- /package/{.claude → src/claude}/agents/developer-frontend.md +0 -0
- /package/{.claude → src/claude}/agents/developer-testing.md +0 -0
- /package/{.claude → src/claude}/agents/domain-model-refactoring.md +0 -0
- /package/{.claude → src/claude}/agents/generate-requirements.md +0 -0
- /package/{.claude → src/claude}/agents/generate-tech-spec.md +0 -0
- /package/{.claude → src/claude}/agents/generate-work-breakdown.md +0 -0
- /package/{.claude → src/claude}/agents/generic-software-assessment.md +0 -0
- /package/{.claude → src/claude}/agents/god-class-decomposition.md +0 -0
- /package/{.claude → src/claude}/agents/init-agents-md.md +0 -0
- /package/{.claude → src/claude}/agents/intervention-documentation-standard.md +0 -0
- /package/{.claude → src/claude}/agents/layered-architecture-assessment.md +0 -0
- /package/{.claude → src/claude}/agents/review-solution.md +0 -0
- /package/{.claude → src/claude}/agents/security-hardening.md +0 -0
- /package/{.claude → src/claude}/agents/validate-feature-docs.md +0 -0
- /package/{.claude → src/claude}/agents/validate-work-breakdown-semantic.md +0 -0
- /package/{.claude → src/claude}/commands/assessment-status.md +0 -0
- /package/{.claude → src/claude}/commands/check-docs.md +0 -0
- /package/{.claude → src/claude}/commands/check-interventions.md +0 -0
- /package/{.claude → src/claude}/commands/feature-status.md +0 -0
- /package/{.claude → src/claude}/commands/next-intervention.md +0 -0
- /package/{.claude → src/claude}/commands/next-task.md +0 -0
- /package/{.claude → src/claude}/commands/pr-description.md +0 -0
- /package/{.claude → src/claude}/scripts/wb-render.js +0 -0
- /package/{.claude → src/claude}/scripts/wb-validate.js +0 -0
- /package/{.claude → src/claude}/skills/assess-codebase/SKILL.md +0 -0
- /package/{.claude → src/claude}/skills/define-feature/SKILL.md +0 -0
- /package/{.claude → src/claude}/skills/hi-gaia/SKILL.md +0 -0
- /package/{.claude → src/claude}/skills/implement-feature/SKILL.md +0 -0
- /package/{.claude → src/claude}/skills/init-agents/SKILL.md +0 -0
- /package/{.claude → src/claude}/skills/install-toolkit/SKILL.md +0 -0
- /package/{.claude → src/claude}/workflows/am-phase2.js +0 -0
- /package/{.claude → src/claude}/workflows/pm-phase1.js +0 -0
- /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
|
-
})
|