@dtlabs/ai-toolkit 0.8.0 → 0.10.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 (41) hide show
  1. package/.claude/agents/generate-work-breakdown.md +212 -192
  2. package/.claude/agents/install-toolkit.md +6 -1
  3. package/.claude/agents/validate-work-breakdown-semantic.md +96 -0
  4. package/.claude/scripts/.gitkeep +0 -0
  5. package/.claude/scripts/tests/fixtures/ac-table-format.md +48 -0
  6. package/.claude/scripts/tests/fixtures/test-feature/TEST-001-Work-Breakdown.json +234 -0
  7. package/.claude/scripts/tests/fixtures/test-feature/TEST-001-semantic-output.json +4 -0
  8. package/.claude/scripts/tests/fixtures/test-feature/feature.md +45 -0
  9. package/.claude/scripts/tests/fixtures/wb-invalid-bad-domain.json +39 -0
  10. package/.claude/scripts/tests/fixtures/wb-invalid-cycle.json +61 -0
  11. package/.claude/scripts/tests/fixtures/wb-invalid-duplicate-id.json +72 -0
  12. package/.claude/scripts/tests/fixtures/wb-invalid-missing-field.json +38 -0
  13. package/.claude/scripts/tests/fixtures/wb-valid.json +189 -0
  14. package/.claude/scripts/tests/helpers/buildWaves.js +21 -0
  15. package/.claude/scripts/tests/helpers/pm-phase2-flow.js +29 -0
  16. package/.claude/scripts/tests/helpers/pm-phase2-gate2.js +55 -0
  17. package/.claude/scripts/tests/helpers/pm-phase2-ledger.js +108 -0
  18. package/.claude/scripts/tests/installer.scripts-distribution.test.js +131 -0
  19. package/.claude/scripts/tests/pm-phase2.gate2-payload.test.js +353 -0
  20. package/.claude/scripts/tests/pm-phase2.ledger-transitions.test.js +346 -0
  21. package/.claude/scripts/tests/pm-phase2.orchestration-flow.test.js +276 -0
  22. package/.claude/scripts/tests/verify-us-01-t03.js +288 -0
  23. package/.claude/scripts/tests/verify-us-03-t04.js +262 -0
  24. package/.claude/scripts/tests/wb-render.checks-md-csv-deps.test.js +337 -0
  25. package/.claude/scripts/tests/wb-render.checks-sanitize-commit-paths.test.js +352 -0
  26. package/.claude/scripts/tests/wb-render.regression-pm-phase3-csv.test.js +219 -0
  27. package/.claude/scripts/tests/wb-validate.checks-1-7.test.js +330 -0
  28. package/.claude/scripts/tests/wb-validate.checks-11.test.js +166 -0
  29. package/.claude/scripts/tests/wb-validate.checks-12-14.test.js +222 -0
  30. package/.claude/scripts/tests/wb-validate.checks-15-18.test.js +359 -0
  31. package/.claude/scripts/tests/wb-validate.checks-19-21.test.js +362 -0
  32. package/.claude/scripts/tests/wb-validate.checks-22-23-report.test.js +297 -0
  33. package/.claude/scripts/tests/wb-validate.checks-8-10.test.js +243 -0
  34. package/.claude/scripts/wb-render.js +309 -0
  35. package/.claude/scripts/wb-validate.js +1007 -0
  36. package/.claude/workflows/pm-phase1.js +22 -13
  37. package/.claude/workflows/pm-phase2.js +304 -32
  38. package/.claude/workflows/pm-phase3.js +57 -21
  39. package/CHANGELOG.md +82 -0
  40. package/bin/cli.js +1 -0
  41. package/package.json +1 -1
@@ -0,0 +1,276 @@
1
+ 'use strict';
2
+
3
+ const {
4
+ wbValidatorPassed,
5
+ canRunSemantic,
6
+ canRunRender,
7
+ gate2IsBlocked,
8
+ } = require('./helpers/pm-phase2-flow');
9
+
10
+ // ─── shared fixtures ──────────────────────────────────────────────────────────
11
+
12
+ const REPORT_VALID = { valid: true, errors: [] };
13
+ const REPORT_INVALID = { valid: false, errors: [{ category: 'schema_version_invalid', message: 'bad schema' }] };
14
+
15
+ // ─────────────────────────────────────────────────────────────────────────────
16
+
17
+ describe('pm-phase2 orchestration flow — pure predicates', () => {
18
+
19
+ // ── Group 1: wbValidatorPassed ─────────────────────────────────────────────
20
+
21
+ describe('wbValidatorPassed()', () => {
22
+
23
+ test('returns true when validateFailed is false and report is valid with empty errors', () => {
24
+ // Arrange
25
+ const validateFailed = false;
26
+ const report = { valid: true, errors: [] };
27
+ // Act
28
+ const result = wbValidatorPassed(validateFailed, report);
29
+ // Assert
30
+ expect(result).toBe(true);
31
+ });
32
+
33
+ test('returns false when validateFailed is true even though report looks valid (technical failure overrides)', () => {
34
+ // Arrange
35
+ const validateFailed = true;
36
+ const report = { valid: true, errors: [] };
37
+ // Act
38
+ const result = wbValidatorPassed(validateFailed, report);
39
+ // Assert
40
+ expect(result).toBe(false);
41
+ });
42
+
43
+ test('returns false when validateFailed is false but report has valid=false and structural errors', () => {
44
+ // Arrange
45
+ const validateFailed = false;
46
+ const report = { valid: false, errors: [{ category: 'missing_field', message: 'outcome missing' }] };
47
+ // Act
48
+ const result = wbValidatorPassed(validateFailed, report);
49
+ // Assert
50
+ expect(result).toBe(false);
51
+ });
52
+
53
+ test('returns false when validateFailed is false and report is null (no result produced)', () => {
54
+ // Arrange
55
+ const validateFailed = false;
56
+ const report = null;
57
+ // Act
58
+ const result = wbValidatorPassed(validateFailed, report);
59
+ // Assert
60
+ expect(result).toBe(false);
61
+ });
62
+
63
+ test('returns false when validateFailed is false but report has valid=true with non-empty errors array', () => {
64
+ // Arrange
65
+ const validateFailed = false;
66
+ const report = { valid: true, errors: [{ category: 'some_error', message: 'something wrong' }] };
67
+ // Act
68
+ const result = wbValidatorPassed(validateFailed, report);
69
+ // Assert
70
+ expect(result).toBe(false);
71
+ });
72
+
73
+ });
74
+
75
+ // ── Group 2: canRunSemantic ────────────────────────────────────────────────
76
+
77
+ describe('canRunSemantic()', () => {
78
+
79
+ test('returns true (semantic can run) when wb-validate passed with a clean valid report', () => {
80
+ // Arrange
81
+ const validateFailed = false;
82
+ const report = REPORT_VALID;
83
+ // Act
84
+ const result = canRunSemantic(validateFailed, report);
85
+ // Assert
86
+ expect(result).toBe(true);
87
+ });
88
+
89
+ test('returns false (semantic is skipped) when wb-validate failed technically', () => {
90
+ // Arrange
91
+ const validateFailed = true;
92
+ const report = REPORT_VALID;
93
+ // Act
94
+ const result = canRunSemantic(validateFailed, report);
95
+ // Assert
96
+ expect(result).toBe(false);
97
+ });
98
+
99
+ test('returns false (semantic is skipped) when wb-validate exit 1 due to structural errors in the report', () => {
100
+ // Arrange
101
+ const validateFailed = false;
102
+ const report = REPORT_INVALID;
103
+ // Act
104
+ const result = canRunSemantic(validateFailed, report);
105
+ // Assert
106
+ expect(result).toBe(false);
107
+ });
108
+
109
+ });
110
+
111
+ // ── Group 3: canRunRender ──────────────────────────────────────────────────
112
+
113
+ describe('canRunRender()', () => {
114
+
115
+ test('returns true when wb-validate passed and semantic did not fail', () => {
116
+ // Arrange
117
+ const validateFailed = false;
118
+ const report = REPORT_VALID;
119
+ const semanticFailed = false;
120
+ // Act
121
+ const result = canRunRender(validateFailed, report, semanticFailed);
122
+ // Assert
123
+ expect(result).toBe(true);
124
+ });
125
+
126
+ test('returns false when wb-validate passed but semantic did fail', () => {
127
+ // Arrange
128
+ const validateFailed = false;
129
+ const report = REPORT_VALID;
130
+ const semanticFailed = true;
131
+ // Act
132
+ const result = canRunRender(validateFailed, report, semanticFailed);
133
+ // Assert
134
+ expect(result).toBe(false);
135
+ });
136
+
137
+ test('returns false when wb-validate failed technically regardless of semantic outcome', () => {
138
+ // Arrange
139
+ const validateFailed = true;
140
+ const report = REPORT_VALID;
141
+ const semanticFailed = false;
142
+ // Act
143
+ const result = canRunRender(validateFailed, report, semanticFailed);
144
+ // Assert
145
+ expect(result).toBe(false);
146
+ });
147
+
148
+ test('returns false when wb-validate exit 1 (structural errors) regardless of semantic outcome', () => {
149
+ // Arrange
150
+ const validateFailed = false;
151
+ const report = REPORT_INVALID;
152
+ const semanticFailed = false;
153
+ // Act
154
+ const result = canRunRender(validateFailed, report, semanticFailed);
155
+ // Assert
156
+ expect(result).toBe(false);
157
+ });
158
+
159
+ });
160
+
161
+ // ── Group 4: gate2IsBlocked ────────────────────────────────────────────────
162
+
163
+ describe('gate2IsBlocked()', () => {
164
+
165
+ test('returns false (gate is open) when all steps passed and semantic has no blocking findings', () => {
166
+ // Arrange
167
+ const validateFailed = false;
168
+ const report = REPORT_VALID;
169
+ const semanticFailed = false;
170
+ const renderFailed = false;
171
+ const semanticResult = { findings: [{ blocking: false, message: 'style suggestion' }] };
172
+ // Act
173
+ const result = gate2IsBlocked(validateFailed, report, semanticFailed, renderFailed, semanticResult);
174
+ // Assert
175
+ expect(result).toBe(false);
176
+ });
177
+
178
+ test('returns true when validateFailed is true', () => {
179
+ // Arrange
180
+ const validateFailed = true;
181
+ const report = REPORT_VALID;
182
+ const semanticFailed = false;
183
+ const renderFailed = false;
184
+ const semanticResult = null;
185
+ // Act
186
+ const result = gate2IsBlocked(validateFailed, report, semanticFailed, renderFailed, semanticResult);
187
+ // Assert
188
+ expect(result).toBe(true);
189
+ });
190
+
191
+ test('returns true when wb-validate exit 1 (report.valid is false)', () => {
192
+ // Arrange
193
+ const validateFailed = false;
194
+ const report = REPORT_INVALID;
195
+ const semanticFailed = false;
196
+ const renderFailed = false;
197
+ const semanticResult = null;
198
+ // Act
199
+ const result = gate2IsBlocked(validateFailed, report, semanticFailed, renderFailed, semanticResult);
200
+ // Assert
201
+ expect(result).toBe(true);
202
+ });
203
+
204
+ test('returns true when semanticFailed is true', () => {
205
+ // Arrange
206
+ const validateFailed = false;
207
+ const report = REPORT_VALID;
208
+ const semanticFailed = true;
209
+ const renderFailed = false;
210
+ const semanticResult = null;
211
+ // Act
212
+ const result = gate2IsBlocked(validateFailed, report, semanticFailed, renderFailed, semanticResult);
213
+ // Assert
214
+ expect(result).toBe(true);
215
+ });
216
+
217
+ test('returns true when renderFailed is true', () => {
218
+ // Arrange
219
+ const validateFailed = false;
220
+ const report = REPORT_VALID;
221
+ const semanticFailed = false;
222
+ const renderFailed = true;
223
+ const semanticResult = null;
224
+ // Act
225
+ const result = gate2IsBlocked(validateFailed, report, semanticFailed, renderFailed, semanticResult);
226
+ // Assert
227
+ expect(result).toBe(true);
228
+ });
229
+
230
+ test('returns true when semantic result contains at least one blocking finding', () => {
231
+ // Arrange
232
+ const validateFailed = false;
233
+ const report = REPORT_VALID;
234
+ const semanticFailed = false;
235
+ const renderFailed = false;
236
+ const semanticResult = { findings: [{ blocking: true, message: 'critical scope gap' }] };
237
+ // Act
238
+ const result = gate2IsBlocked(validateFailed, report, semanticFailed, renderFailed, semanticResult);
239
+ // Assert
240
+ expect(result).toBe(true);
241
+ });
242
+
243
+ test('returns false when semantic result has only non-blocking findings (gate allows proceed)', () => {
244
+ // Arrange
245
+ const validateFailed = false;
246
+ const report = REPORT_VALID;
247
+ const semanticFailed = false;
248
+ const renderFailed = false;
249
+ const semanticResult = {
250
+ findings: [
251
+ { blocking: false, message: 'minor wording suggestion' },
252
+ { blocking: false, message: 'optional improvement' },
253
+ ],
254
+ };
255
+ // Act
256
+ const result = gate2IsBlocked(validateFailed, report, semanticFailed, renderFailed, semanticResult);
257
+ // Assert
258
+ expect(result).toBe(false);
259
+ });
260
+
261
+ test('returns true when multiple failure conditions are true simultaneously', () => {
262
+ // Arrange
263
+ const validateFailed = true;
264
+ const report = REPORT_INVALID;
265
+ const semanticFailed = true;
266
+ const renderFailed = true;
267
+ const semanticResult = { findings: [{ blocking: true, message: 'critical gap' }] };
268
+ // Act
269
+ const result = gate2IsBlocked(validateFailed, report, semanticFailed, renderFailed, semanticResult);
270
+ // Assert
271
+ expect(result).toBe(true);
272
+ });
273
+
274
+ });
275
+
276
+ });
@@ -0,0 +1,288 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * US-01-T03: Schema v2 Compliance Verification
5
+ *
6
+ * Reads TEST-001-Work-Breakdown.json and performs 9 structural checks
7
+ * derived from the INFRA-T02 constants in wb-validate.js — with no
8
+ * dependency on wb-validate.js itself (runs in parallel with US-02).
9
+ *
10
+ * Uses only Node.js built-ins: fs, path.
11
+ * Exit 0 = all checks pass; Exit 1 = one or more checks fail.
12
+ */
13
+
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+
17
+ // ─── Schema v2 constants (sourced from wb-validate.js — not imported) ────────
18
+
19
+ const REQUIRED_PHASE_FIELDS = ['id', 'type', 'title', 'commit', 'tasks'];
20
+
21
+ const REQUIRED_TASK_FIELDS = [
22
+ 'id',
23
+ 'title',
24
+ 'outcome',
25
+ 'domain',
26
+ 'agentType',
27
+ 'dependsOn',
28
+ 'acceptanceCriteria',
29
+ 'verification',
30
+ 'estimate',
31
+ 'outputCount',
32
+ 'groupingRationale',
33
+ 'commit',
34
+ ];
35
+
36
+ const VALID_DOMAINS = ['BE', 'FE', 'DB', 'DevOps', 'INFRA', 'TEST'];
37
+
38
+ const VALID_AGENT_TYPES = [
39
+ 'developer-backend',
40
+ 'developer-frontend',
41
+ 'developer-testing',
42
+ 'developer-database',
43
+ 'review-solution',
44
+ ];
45
+
46
+ // ─── Fixture path (resolved relative to this script file) ────────────────────
47
+
48
+ const FIXTURE_PATH = path.join(
49
+ __dirname,
50
+ 'fixtures',
51
+ 'test-feature',
52
+ 'TEST-001-Work-Breakdown.json'
53
+ );
54
+
55
+ // ─── Report accumulators ─────────────────────────────────────────────────────
56
+
57
+ const passes = [];
58
+ const failures = [];
59
+
60
+ function pass(label) {
61
+ passes.push(label);
62
+ }
63
+
64
+ function fail(label, detail) {
65
+ failures.push({ label, detail });
66
+ }
67
+
68
+ // ─── Load and parse fixture ───────────────────────────────────────────────────
69
+
70
+ let wb;
71
+ try {
72
+ const raw = fs.readFileSync(FIXTURE_PATH, 'utf8');
73
+ wb = JSON.parse(raw);
74
+ } catch (err) {
75
+ console.error(`FATAL: Cannot read or parse fixture at ${FIXTURE_PATH}`);
76
+ console.error(` ${err.message}`);
77
+ process.exit(2);
78
+ }
79
+
80
+ // ─── Build full task-ID index (used by checks 3, 4, 8) ───────────────────────
81
+
82
+ const allTaskIds = new Set();
83
+ for (const phase of (wb.phases || [])) {
84
+ for (const task of (phase.tasks || [])) {
85
+ if (task.id != null) allTaskIds.add(task.id);
86
+ }
87
+ }
88
+
89
+ // ─── Check 1: schemaVersion === 2 ────────────────────────────────────────────
90
+
91
+ if (wb.schemaVersion === 2) {
92
+ pass('Check 1 — schemaVersion === 2');
93
+ } else {
94
+ fail(
95
+ 'Check 1 — schemaVersion === 2',
96
+ `Expected 2, got: ${JSON.stringify(wb.schemaVersion)}`
97
+ );
98
+ }
99
+
100
+ // ─── Check 2: every phase has all REQUIRED_PHASE_FIELDS ──────────────────────
101
+
102
+ {
103
+ let ok = true;
104
+ for (const phase of (wb.phases || [])) {
105
+ const phaseId = phase.id ?? '(no id)';
106
+ for (const field of REQUIRED_PHASE_FIELDS) {
107
+ if (!(field in phase)) {
108
+ fail(
109
+ 'Check 2 — every phase has all REQUIRED_PHASE_FIELDS',
110
+ `Phase "${phaseId}" is missing field "${field}"`
111
+ );
112
+ ok = false;
113
+ }
114
+ }
115
+ }
116
+ if (ok) pass('Check 2 — every phase has all REQUIRED_PHASE_FIELDS');
117
+ }
118
+
119
+ // ─── Check 3: every task has all REQUIRED_TASK_FIELDS ────────────────────────
120
+
121
+ {
122
+ let ok = true;
123
+ for (const phase of (wb.phases || [])) {
124
+ for (const task of (phase.tasks || [])) {
125
+ const taskId = task.id ?? '(no id)';
126
+ for (const field of REQUIRED_TASK_FIELDS) {
127
+ if (!(field in task)) {
128
+ fail(
129
+ 'Check 3 — every task has all REQUIRED_TASK_FIELDS',
130
+ `Task "${taskId}" is missing field "${field}"`
131
+ );
132
+ ok = false;
133
+ }
134
+ }
135
+ }
136
+ }
137
+ if (ok) pass('Check 3 — every task has all REQUIRED_TASK_FIELDS');
138
+ }
139
+
140
+ // ─── Check 4: every task.domain is in VALID_DOMAINS ──────────────────────────
141
+
142
+ {
143
+ let ok = true;
144
+ for (const phase of (wb.phases || [])) {
145
+ for (const task of (phase.tasks || [])) {
146
+ if (!VALID_DOMAINS.includes(task.domain)) {
147
+ fail(
148
+ 'Check 4 — every task.domain is in VALID_DOMAINS',
149
+ `Task "${task.id}" has domain "${task.domain}"; allowed: ${VALID_DOMAINS.join(', ')}`
150
+ );
151
+ ok = false;
152
+ }
153
+ }
154
+ }
155
+ if (ok) pass('Check 4 — every task.domain is in VALID_DOMAINS');
156
+ }
157
+
158
+ // ─── Check 5: every task.agentType is in VALID_AGENT_TYPES ───────────────────
159
+
160
+ {
161
+ let ok = true;
162
+ for (const phase of (wb.phases || [])) {
163
+ for (const task of (phase.tasks || [])) {
164
+ if (!VALID_AGENT_TYPES.includes(task.agentType)) {
165
+ fail(
166
+ 'Check 5 — every task.agentType is in VALID_AGENT_TYPES',
167
+ `Task "${task.id}" has agentType "${task.agentType}"; allowed: ${VALID_AGENT_TYPES.join(', ')}`
168
+ );
169
+ ok = false;
170
+ }
171
+ }
172
+ }
173
+ if (ok) pass('Check 5 — every task.agentType is in VALID_AGENT_TYPES');
174
+ }
175
+
176
+ // ─── Check 6: every task.estimate.agentMinutes exists and is ≤ 15 ────────────
177
+
178
+ {
179
+ let ok = true;
180
+ for (const phase of (wb.phases || [])) {
181
+ for (const task of (phase.tasks || [])) {
182
+ const minutes = task.estimate?.agentMinutes;
183
+ if (minutes == null) {
184
+ fail(
185
+ 'Check 6 — task.estimate.agentMinutes exists and is <= 15',
186
+ `Task "${task.id}" is missing estimate.agentMinutes`
187
+ );
188
+ ok = false;
189
+ } else if (typeof minutes !== 'number') {
190
+ fail(
191
+ 'Check 6 — task.estimate.agentMinutes exists and is <= 15',
192
+ `Task "${task.id}" has non-numeric estimate.agentMinutes: ${JSON.stringify(minutes)}`
193
+ );
194
+ ok = false;
195
+ } else if (minutes > 15) {
196
+ fail(
197
+ 'Check 6 — task.estimate.agentMinutes exists and is <= 15',
198
+ `Task "${task.id}" has agentMinutes=${minutes} which exceeds the 15-minute target`
199
+ );
200
+ ok = false;
201
+ }
202
+ }
203
+ }
204
+ if (ok) pass('Check 6 — every task.estimate.agentMinutes exists and is <= 15');
205
+ }
206
+
207
+ // ─── Check 7: every task.verification.commands is a non-empty array ──────────
208
+
209
+ {
210
+ let ok = true;
211
+ for (const phase of (wb.phases || [])) {
212
+ for (const task of (phase.tasks || [])) {
213
+ const cmds = task.verification?.commands;
214
+ if (!Array.isArray(cmds)) {
215
+ fail(
216
+ 'Check 7 — task.verification.commands is a non-empty array',
217
+ `Task "${task.id}" has verification.commands that is not an array: ${JSON.stringify(cmds)}`
218
+ );
219
+ ok = false;
220
+ } else if (cmds.length === 0) {
221
+ fail(
222
+ 'Check 7 — task.verification.commands is a non-empty array',
223
+ `Task "${task.id}" has an empty verification.commands array`
224
+ );
225
+ ok = false;
226
+ }
227
+ }
228
+ }
229
+ if (ok) pass('Check 7 — every task.verification.commands is a non-empty array');
230
+ }
231
+
232
+ // ─── Check 8: no task depends on an ID not present in the JSON ───────────────
233
+
234
+ {
235
+ let ok = true;
236
+ for (const phase of (wb.phases || [])) {
237
+ for (const task of (phase.tasks || [])) {
238
+ for (const dep of (task.dependsOn || [])) {
239
+ if (!allTaskIds.has(dep)) {
240
+ fail(
241
+ 'Check 8 — no dangling dependsOn references',
242
+ `Task "${task.id}" depends on "${dep}" which does not exist in the JSON`
243
+ );
244
+ ok = false;
245
+ }
246
+ }
247
+ }
248
+ }
249
+ if (ok) pass('Check 8 — no dangling dependsOn references');
250
+ }
251
+
252
+ // ─── Check 9: fixture has at least one phase and one task ────────────────────
253
+ // (sanity guard: catches a structurally empty fixture that would silently pass)
254
+
255
+ {
256
+ const phaseCount = (wb.phases || []).length;
257
+ const taskCount = allTaskIds.size;
258
+ if (phaseCount >= 1 && taskCount >= 1) {
259
+ pass(`Check 9 — fixture is non-trivial (${phaseCount} phases, ${taskCount} tasks)`);
260
+ } else {
261
+ fail(
262
+ 'Check 9 — fixture is non-trivial',
263
+ `Expected at least 1 phase and 1 task; found ${phaseCount} phase(s) and ${taskCount} task(s)`
264
+ );
265
+ }
266
+ }
267
+
268
+ // ─── Print report ─────────────────────────────────────────────────────────────
269
+
270
+ console.log('\n=== Schema v2 Compliance Report — TEST-001-Work-Breakdown.json ===\n');
271
+ console.log(`Fixture: ${FIXTURE_PATH}\n`);
272
+
273
+ for (const p of passes) {
274
+ console.log(` PASS ${p}`);
275
+ }
276
+
277
+ if (failures.length > 0) {
278
+ console.log('');
279
+ for (const f of failures) {
280
+ console.log(` FAIL ${f.label}`);
281
+ console.log(` ${f.detail}`);
282
+ }
283
+ console.log(`\nResult: FAIL — ${failures.length} failure(s), ${passes.length} check(s) passed\n`);
284
+ process.exit(1);
285
+ } else {
286
+ console.log(`\nResult: PASS — all ${passes.length} checks passed\n`);
287
+ process.exit(0);
288
+ }