@unrdf/diataxis-kit 26.4.2

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 (49) hide show
  1. package/README.md +425 -0
  2. package/bin/report.mjs +529 -0
  3. package/bin/run.mjs +114 -0
  4. package/bin/verify.mjs +356 -0
  5. package/capability-map.md +92 -0
  6. package/package.json +42 -0
  7. package/src/classify.mjs +584 -0
  8. package/src/diataxis-schema.mjs +425 -0
  9. package/src/evidence.mjs +268 -0
  10. package/src/hash.mjs +37 -0
  11. package/src/inventory.mjs +280 -0
  12. package/src/reference-extractor.mjs +324 -0
  13. package/src/scaffold.mjs +458 -0
  14. package/src/stable-json.mjs +113 -0
  15. package/src/verify-implementation.mjs +131 -0
  16. package/test/determinism.test.mjs +321 -0
  17. package/test/evidence.test.mjs +145 -0
  18. package/test/fixtures/scaffold-det1/explanation/explanation.md +35 -0
  19. package/test/fixtures/scaffold-det1/index.md +29 -0
  20. package/test/fixtures/scaffold-det1/reference/reference.md +34 -0
  21. package/test/fixtures/scaffold-det1/tutorials/tutorial-test-tutorial.md +37 -0
  22. package/test/fixtures/scaffold-det2/explanation/explanation.md +35 -0
  23. package/test/fixtures/scaffold-det2/index.md +29 -0
  24. package/test/fixtures/scaffold-det2/reference/reference.md +34 -0
  25. package/test/fixtures/scaffold-det2/tutorials/tutorial-test-tutorial.md +37 -0
  26. package/test/fixtures/scaffold-empty/explanation/explanation.md +35 -0
  27. package/test/fixtures/scaffold-empty/index.md +25 -0
  28. package/test/fixtures/scaffold-empty/reference/reference.md +34 -0
  29. package/test/fixtures/scaffold-escape/explanation/explanation.md +35 -0
  30. package/test/fixtures/scaffold-escape/index.md +29 -0
  31. package/test/fixtures/scaffold-escape/reference/reference.md +36 -0
  32. package/test/fixtures/scaffold-output/explanation/explanation.md +39 -0
  33. package/test/fixtures/scaffold-output/how-to/howto-configure-options.md +39 -0
  34. package/test/fixtures/scaffold-output/index.md +41 -0
  35. package/test/fixtures/scaffold-output/reference/reference.md +36 -0
  36. package/test/fixtures/scaffold-output/tutorials/tutorial-getting-started.md +41 -0
  37. package/test/fixtures/test-artifacts/ARTIFACTS/diataxis/test-pkg-1.inventory.json +115 -0
  38. package/test/fixtures/test-artifacts/ARTIFACTS/diataxis/test-pkg-2.inventory.json +93 -0
  39. package/test/fixtures/test-artifacts/ARTIFACTS/diataxis/test-pkg-3.inventory.json +97 -0
  40. package/test/fixtures/test-package/LICENSE +1 -0
  41. package/test/fixtures/test-package/README.md +15 -0
  42. package/test/fixtures/test-package/docs/guide.md +3 -0
  43. package/test/fixtures/test-package/examples/basic.mjs +3 -0
  44. package/test/fixtures/test-package/src/index.mjs +3 -0
  45. package/test/inventory.test.mjs +199 -0
  46. package/test/reference-extractor.test.mjs +187 -0
  47. package/test/report.test.mjs +503 -0
  48. package/test/scaffold.test.mjs +242 -0
  49. package/test/verify-gate.test.mjs +634 -0
@@ -0,0 +1,634 @@
1
+ /**
2
+ * @file Tests for Diátaxis verification gate
3
+ */
4
+
5
+ import { describe, it } from 'node:test';
6
+ import assert from 'node:assert/strict';
7
+ import { mkdir, writeFile, rm } from 'node:fs/promises';
8
+ import { join } from 'node:path';
9
+ import { tmpdir } from 'node:os';
10
+ import { execFile } from 'node:child_process';
11
+ import { promisify } from 'node:util';
12
+ import { main, verifyPackage, isReferencPopulated, isExplanationPopulated } from '../bin/verify.mjs';
13
+ import { stableStringify } from '../src/stable-json.mjs';
14
+
15
+ const execFileAsync = promisify(execFile);
16
+
17
+ /**
18
+ * Create a test workspace with inventory and package files
19
+ * @param {string} testDir - Test directory path
20
+ * @param {Object} config - Configuration
21
+ * @param {Array} config.packages - Array of package configurations
22
+ * @returns {Promise<void>}
23
+ */
24
+ async function createTestWorkspace(testDir, config) {
25
+ const artifactsDir = join(testDir, 'ARTIFACTS', 'diataxis');
26
+ await mkdir(artifactsDir, { recursive: true });
27
+
28
+ // Create inventory.json
29
+ const inventory = {
30
+ packages: config.packages.map(pkg => ({
31
+ name: pkg.name,
32
+ version: pkg.version || '1.0.0'
33
+ }))
34
+ };
35
+
36
+ await writeFile(
37
+ join(artifactsDir, 'inventory.json'),
38
+ stableStringify(inventory)
39
+ );
40
+
41
+ // Create per-package diataxis.json files
42
+ for (const pkg of config.packages) {
43
+ const packageDir = join(artifactsDir, pkg.name);
44
+ await mkdir(packageDir, { recursive: true });
45
+
46
+ if (pkg.diataxis) {
47
+ await writeFile(
48
+ join(packageDir, 'diataxis.json'),
49
+ stableStringify(pkg.diataxis)
50
+ );
51
+ }
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Create a valid diataxis entry
57
+ * @param {string} packageName - Package name
58
+ * @param {Object} overrides - Override default values
59
+ * @returns {Object} Diataxis entry
60
+ */
61
+ function createDiataxisEntry(packageName, overrides = {}) {
62
+ return {
63
+ packageName,
64
+ version: '1.0.0',
65
+ generatedAt: '2000-01-01T00:00:00.000Z',
66
+ confidence: {
67
+ tutorials: 0.8,
68
+ howtos: 0.9,
69
+ reference: 0.95,
70
+ explanation: 0.85
71
+ },
72
+ tutorials: [
73
+ {
74
+ id: 'getting-started',
75
+ title: 'Getting Started',
76
+ goal: 'Learn the basics',
77
+ prerequisites: [],
78
+ stepsOutline: ['Step 1', 'Step 2'],
79
+ confidenceScore: 0.8,
80
+ source: ['README.md']
81
+ }
82
+ ],
83
+ howtos: [
84
+ {
85
+ id: 'install',
86
+ title: 'How to Install',
87
+ task: 'Install the package',
88
+ context: 'Setup',
89
+ steps: ['npm install'],
90
+ confidenceScore: 0.9,
91
+ source: ['README.md']
92
+ },
93
+ {
94
+ id: 'configure',
95
+ title: 'How to Configure',
96
+ task: 'Configure the package',
97
+ context: 'Setup',
98
+ steps: ['Edit config'],
99
+ confidenceScore: 0.85,
100
+ source: ['README.md']
101
+ }
102
+ ],
103
+ reference: {
104
+ id: 'reference',
105
+ title: `${packageName} Reference`,
106
+ items: [
107
+ {
108
+ name: 'someFunction',
109
+ type: 'export',
110
+ description: 'A function',
111
+ example: 'someFunction()'
112
+ }
113
+ ],
114
+ confidenceScore: 0.95,
115
+ source: ['src/index.js']
116
+ },
117
+ explanation: {
118
+ id: 'explanation',
119
+ title: `${packageName} Explanation`,
120
+ concepts: ['Core concept'],
121
+ architecture: 'Simple architecture',
122
+ tradeoffs: ['Tradeoff 1'],
123
+ confidenceScore: 0.85,
124
+ source: ['README.md']
125
+ },
126
+ evidence: {
127
+ readmeHeadings: ['Installation', 'Usage'],
128
+ docsFiles: ['README.md'],
129
+ examplesFiles: [],
130
+ fingerprint: 'abc123'
131
+ },
132
+ ...overrides
133
+ };
134
+ }
135
+
136
+ describe('Diátaxis Verification Gate', () => {
137
+ it('Test 1: Pass when all packages have required stubs', async () => {
138
+ const testDir = join(tmpdir(), `verify-test-${Date.now()}-1`);
139
+
140
+ try {
141
+ await createTestWorkspace(testDir, {
142
+ packages: [
143
+ {
144
+ name: '@unrdf/package-a',
145
+ diataxis: createDiataxisEntry('@unrdf/package-a')
146
+ },
147
+ {
148
+ name: '@unrdf/package-b',
149
+ diataxis: createDiataxisEntry('@unrdf/package-b')
150
+ }
151
+ ]
152
+ });
153
+
154
+ // Run verification from test directory
155
+ const originalCwd = process.cwd();
156
+ try {
157
+ process.chdir(testDir);
158
+ const exitCode = await main({ json: false, failFast: false, threshold: 0 });
159
+ assert.equal(exitCode, 0, 'Should exit with 0 when all packages pass');
160
+ } finally {
161
+ process.chdir(originalCwd);
162
+ }
163
+ } finally {
164
+ await rm(testDir, { recursive: true, force: true });
165
+ }
166
+ });
167
+
168
+ it('Test 2: Fail when a package missing tutorials', async () => {
169
+ const testDir = join(tmpdir(), `verify-test-${Date.now()}-2`);
170
+
171
+ try {
172
+ await createTestWorkspace(testDir, {
173
+ packages: [
174
+ {
175
+ name: '@unrdf/package-a',
176
+ diataxis: createDiataxisEntry('@unrdf/package-a', {
177
+ tutorials: [] // Missing tutorials
178
+ })
179
+ }
180
+ ]
181
+ });
182
+
183
+ const originalCwd = process.cwd();
184
+ try {
185
+ process.chdir(testDir);
186
+ const exitCode = await main({ json: false, failFast: false, threshold: 0 });
187
+ assert.equal(exitCode, 1, 'Should exit with 1 when tutorials missing');
188
+ } finally {
189
+ process.chdir(originalCwd);
190
+ }
191
+ } finally {
192
+ await rm(testDir, { recursive: true, force: true });
193
+ }
194
+ });
195
+
196
+ it('Test 3: Fail when a package missing 2+ how-tos', async () => {
197
+ const testDir = join(tmpdir(), `verify-test-${Date.now()}-3`);
198
+
199
+ try {
200
+ await createTestWorkspace(testDir, {
201
+ packages: [
202
+ {
203
+ name: '@unrdf/package-a',
204
+ diataxis: createDiataxisEntry('@unrdf/package-a', {
205
+ howtos: [
206
+ {
207
+ id: 'install',
208
+ title: 'How to Install',
209
+ task: 'Install',
210
+ context: 'Setup',
211
+ steps: [],
212
+ confidenceScore: 0.9,
213
+ source: []
214
+ }
215
+ ] // Only 1 how-to, need 2
216
+ })
217
+ }
218
+ ]
219
+ });
220
+
221
+ const originalCwd = process.cwd();
222
+ try {
223
+ process.chdir(testDir);
224
+ const exitCode = await main({ json: false, failFast: false, threshold: 0 });
225
+ assert.equal(exitCode, 1, 'Should exit with 1 when how-tos < 2');
226
+ } finally {
227
+ process.chdir(originalCwd);
228
+ }
229
+ } finally {
230
+ await rm(testDir, { recursive: true, force: true });
231
+ }
232
+ });
233
+
234
+ it('Test 4: Fail when missing reference', async () => {
235
+ const testDir = join(tmpdir(), `verify-test-${Date.now()}-4`);
236
+
237
+ try {
238
+ await createTestWorkspace(testDir, {
239
+ packages: [
240
+ {
241
+ name: '@unrdf/package-a',
242
+ diataxis: createDiataxisEntry('@unrdf/package-a', {
243
+ reference: {
244
+ id: 'reference',
245
+ title: 'Reference',
246
+ items: [], // Empty items
247
+ confidenceScore: 0,
248
+ source: []
249
+ }
250
+ })
251
+ }
252
+ ]
253
+ });
254
+
255
+ const originalCwd = process.cwd();
256
+ try {
257
+ process.chdir(testDir);
258
+ const exitCode = await main({ json: false, failFast: false, threshold: 0 });
259
+ assert.equal(exitCode, 1, 'Should exit with 1 when reference empty');
260
+ } finally {
261
+ process.chdir(originalCwd);
262
+ }
263
+ } finally {
264
+ await rm(testDir, { recursive: true, force: true });
265
+ }
266
+ });
267
+
268
+ it('Test 5: Fail when missing explanation', async () => {
269
+ const testDir = join(tmpdir(), `verify-test-${Date.now()}-5`);
270
+
271
+ try {
272
+ await createTestWorkspace(testDir, {
273
+ packages: [
274
+ {
275
+ name: '@unrdf/package-a',
276
+ diataxis: createDiataxisEntry('@unrdf/package-a', {
277
+ explanation: {
278
+ id: 'explanation',
279
+ title: 'Explanation',
280
+ concepts: [],
281
+ architecture: '',
282
+ tradeoffs: [],
283
+ confidenceScore: 0,
284
+ source: []
285
+ }
286
+ })
287
+ }
288
+ ]
289
+ });
290
+
291
+ const originalCwd = process.cwd();
292
+ try {
293
+ process.chdir(testDir);
294
+ const exitCode = await main({ json: false, failFast: false, threshold: 0 });
295
+ assert.equal(exitCode, 1, 'Should exit with 1 when explanation empty');
296
+ } finally {
297
+ process.chdir(originalCwd);
298
+ }
299
+ } finally {
300
+ await rm(testDir, { recursive: true, force: true });
301
+ }
302
+ });
303
+
304
+ it('Test 6: JSON output mode', async () => {
305
+ const testDir = join(tmpdir(), `verify-test-${Date.now()}-6`);
306
+
307
+ try {
308
+ await createTestWorkspace(testDir, {
309
+ packages: [
310
+ {
311
+ name: '@unrdf/package-a',
312
+ diataxis: createDiataxisEntry('@unrdf/package-a')
313
+ },
314
+ {
315
+ name: '@unrdf/package-b',
316
+ diataxis: createDiataxisEntry('@unrdf/package-b', {
317
+ tutorials: []
318
+ })
319
+ }
320
+ ]
321
+ });
322
+
323
+ // Capture stdout
324
+ const originalCwd = process.cwd();
325
+ const originalLog = console.log;
326
+ let output = '';
327
+ console.log = (msg) => { output += msg; };
328
+
329
+ try {
330
+ process.chdir(testDir);
331
+ const exitCode = await main({ json: true, failFast: false, threshold: 0 });
332
+
333
+ assert.equal(exitCode, 1, 'Should exit with 1');
334
+
335
+ // Parse JSON output
336
+ const result = JSON.parse(output);
337
+ assert.equal(result.total, 2, 'Should report 2 total packages');
338
+ assert.equal(result.passing, 1, 'Should report 1 passing');
339
+ assert.equal(result.failing, 1, 'Should report 1 failing');
340
+ assert.equal(result.exitCode, 1, 'Should include exit code');
341
+ assert.ok(Array.isArray(result.failures), 'Should include failures array');
342
+ assert.equal(result.failures.length, 1, 'Should have 1 failure');
343
+ assert.equal(result.failures[0].package, '@unrdf/package-b', 'Should identify failing package');
344
+ } finally {
345
+ console.log = originalLog;
346
+ process.chdir(originalCwd);
347
+ }
348
+ } finally {
349
+ await rm(testDir, { recursive: true, force: true });
350
+ }
351
+ });
352
+
353
+ it('Test 7: --threshold option', async () => {
354
+ const testDir = join(tmpdir(), `verify-test-${Date.now()}-7`);
355
+
356
+ try {
357
+ await createTestWorkspace(testDir, {
358
+ packages: [
359
+ {
360
+ name: '@unrdf/package-a',
361
+ diataxis: createDiataxisEntry('@unrdf/package-a', {
362
+ tutorials: []
363
+ })
364
+ },
365
+ {
366
+ name: '@unrdf/package-b',
367
+ diataxis: createDiataxisEntry('@unrdf/package-b')
368
+ }
369
+ ]
370
+ });
371
+
372
+ const originalCwd = process.cwd();
373
+ try {
374
+ process.chdir(testDir);
375
+
376
+ // With threshold = 0, should fail (1 failure > 0)
377
+ let exitCode = await main({ json: false, failFast: false, threshold: 0 });
378
+ assert.equal(exitCode, 1, 'Should fail with threshold 0');
379
+
380
+ // With threshold = 1, should pass (1 failure <= 1)
381
+ exitCode = await main({ json: false, failFast: false, threshold: 1 });
382
+ assert.equal(exitCode, 0, 'Should pass with threshold 1');
383
+
384
+ // With threshold = 2, should pass (1 failure <= 2)
385
+ exitCode = await main({ json: false, failFast: false, threshold: 2 });
386
+ assert.equal(exitCode, 0, 'Should pass with threshold 2');
387
+ } finally {
388
+ process.chdir(originalCwd);
389
+ }
390
+ } finally {
391
+ await rm(testDir, { recursive: true, force: true });
392
+ }
393
+ });
394
+
395
+ it('Unit test: verifyPackage function', () => {
396
+ // Valid package
397
+ const validEntry = createDiataxisEntry('test-pkg');
398
+ const result1 = verifyPackage('test-pkg', validEntry);
399
+ assert.equal(result1.passing, true, 'Valid entry should pass');
400
+ assert.equal(result1.failures.length, 0, 'Valid entry should have no failures');
401
+
402
+ // Missing tutorials
403
+ const noTutorials = createDiataxisEntry('test-pkg', { tutorials: [] });
404
+ const result2 = verifyPackage('test-pkg', noTutorials);
405
+ assert.equal(result2.passing, false, 'Should fail without tutorials');
406
+ assert.ok(result2.failures.some(f => f.includes('tutorials')), 'Should mention tutorials');
407
+
408
+ // Not enough how-tos
409
+ const oneHowto = createDiataxisEntry('test-pkg', {
410
+ howtos: [
411
+ {
412
+ id: 'install',
413
+ title: 'Install',
414
+ task: 'Install',
415
+ context: 'Setup',
416
+ steps: [],
417
+ confidenceScore: 0.9,
418
+ source: []
419
+ }
420
+ ]
421
+ });
422
+ const result3 = verifyPackage('test-pkg', oneHowto);
423
+ assert.equal(result3.passing, false, 'Should fail with only 1 how-to');
424
+ assert.ok(result3.failures.some(f => f.includes('how-tos')), 'Should mention how-tos');
425
+ });
426
+
427
+ it('Unit test: isReferencePopulated function', () => {
428
+ // Reference with items
429
+ assert.equal(
430
+ isReferencPopulated({
431
+ items: [{ name: 'test', type: 'export', description: 'test' }],
432
+ confidenceScore: 0.9
433
+ }),
434
+ true,
435
+ 'Reference with items should be populated'
436
+ );
437
+
438
+ // Reference with confidence but no items
439
+ assert.equal(
440
+ isReferencPopulated({
441
+ items: [],
442
+ confidenceScore: 0.5
443
+ }),
444
+ true,
445
+ 'Reference with confidence > 0 should be populated'
446
+ );
447
+
448
+ // Empty reference
449
+ assert.equal(
450
+ isReferencPopulated({
451
+ items: [],
452
+ confidenceScore: 0
453
+ }),
454
+ false,
455
+ 'Reference with no items and 0 confidence should not be populated'
456
+ );
457
+
458
+ // Null/undefined
459
+ assert.equal(isReferencPopulated(null), false, 'Null should not be populated');
460
+ assert.equal(isReferencPopulated(undefined), false, 'Undefined should not be populated');
461
+ });
462
+
463
+ it('Unit test: isExplanationPopulated function', () => {
464
+ // Explanation with concepts
465
+ assert.equal(
466
+ isExplanationPopulated({
467
+ concepts: ['Concept 1'],
468
+ architecture: '',
469
+ tradeoffs: [],
470
+ confidenceScore: 0
471
+ }),
472
+ true,
473
+ 'Explanation with concepts should be populated'
474
+ );
475
+
476
+ // Explanation with architecture
477
+ assert.equal(
478
+ isExplanationPopulated({
479
+ concepts: [],
480
+ architecture: 'Some architecture',
481
+ tradeoffs: [],
482
+ confidenceScore: 0
483
+ }),
484
+ true,
485
+ 'Explanation with architecture should be populated'
486
+ );
487
+
488
+ // Explanation with tradeoffs
489
+ assert.equal(
490
+ isExplanationPopulated({
491
+ concepts: [],
492
+ architecture: '',
493
+ tradeoffs: ['Tradeoff 1'],
494
+ confidenceScore: 0
495
+ }),
496
+ true,
497
+ 'Explanation with tradeoffs should be populated'
498
+ );
499
+
500
+ // Explanation with confidence but no content
501
+ assert.equal(
502
+ isExplanationPopulated({
503
+ concepts: [],
504
+ architecture: '',
505
+ tradeoffs: [],
506
+ confidenceScore: 0.5
507
+ }),
508
+ true,
509
+ 'Explanation with confidence > 0 should be populated'
510
+ );
511
+
512
+ // Empty explanation
513
+ assert.equal(
514
+ isExplanationPopulated({
515
+ concepts: [],
516
+ architecture: '',
517
+ tradeoffs: [],
518
+ confidenceScore: 0
519
+ }),
520
+ false,
521
+ 'Explanation with no content should not be populated'
522
+ );
523
+
524
+ // Null/undefined
525
+ assert.equal(isExplanationPopulated(null), false, 'Null should not be populated');
526
+ assert.equal(isExplanationPopulated(undefined), false, 'Undefined should not be populated');
527
+ });
528
+
529
+ it('Edge case: --fail-fast option', async () => {
530
+ const testDir = join(tmpdir(), `verify-test-${Date.now()}-failfast`);
531
+
532
+ try {
533
+ await createTestWorkspace(testDir, {
534
+ packages: [
535
+ {
536
+ name: '@unrdf/package-a',
537
+ diataxis: createDiataxisEntry('@unrdf/package-a', { tutorials: [] })
538
+ },
539
+ {
540
+ name: '@unrdf/package-b',
541
+ diataxis: createDiataxisEntry('@unrdf/package-b', { tutorials: [] })
542
+ },
543
+ {
544
+ name: '@unrdf/package-c',
545
+ diataxis: createDiataxisEntry('@unrdf/package-c')
546
+ }
547
+ ]
548
+ });
549
+
550
+ // Capture stdout to check behavior
551
+ const originalCwd = process.cwd();
552
+ const originalLog = console.log;
553
+ let output = '';
554
+ console.log = (msg) => { output += msg; };
555
+
556
+ try {
557
+ process.chdir(testDir);
558
+ const exitCode = await main({ json: true, failFast: true, threshold: 0 });
559
+
560
+ assert.equal(exitCode, 1, 'Should exit with 1');
561
+
562
+ const result = JSON.parse(output);
563
+ // With fail-fast, should stop after first failure
564
+ // Total should be 1 (only checked package-a)
565
+ assert.equal(result.total, 1, 'Should only check until first failure');
566
+ assert.equal(result.failing, 1, 'Should have 1 failing');
567
+ } finally {
568
+ console.log = originalLog;
569
+ process.chdir(originalCwd);
570
+ }
571
+ } finally {
572
+ await rm(testDir, { recursive: true, force: true });
573
+ }
574
+ });
575
+
576
+ it('Edge case: Missing diataxis.json file', async () => {
577
+ const testDir = join(tmpdir(), `verify-test-${Date.now()}-missing`);
578
+
579
+ try {
580
+ // Create inventory but no diataxis.json files
581
+ const artifactsDir = join(testDir, 'ARTIFACTS', 'diataxis');
582
+ await mkdir(artifactsDir, { recursive: true });
583
+
584
+ await writeFile(
585
+ join(artifactsDir, 'inventory.json'),
586
+ stableStringify({
587
+ packages: [{ name: '@unrdf/package-a', version: '1.0.0' }]
588
+ })
589
+ );
590
+
591
+ const originalCwd = process.cwd();
592
+ try {
593
+ process.chdir(testDir);
594
+ const exitCode = await main({ json: false, failFast: false, threshold: 0 });
595
+ assert.equal(exitCode, 1, 'Should fail when diataxis.json missing');
596
+ } finally {
597
+ process.chdir(originalCwd);
598
+ }
599
+ } finally {
600
+ await rm(testDir, { recursive: true, force: true });
601
+ }
602
+ });
603
+
604
+ it('Edge case: Invalid JSON in diataxis.json', async () => {
605
+ const testDir = join(tmpdir(), `verify-test-${Date.now()}-invalid`);
606
+
607
+ try {
608
+ const artifactsDir = join(testDir, 'ARTIFACTS', 'diataxis');
609
+ await mkdir(artifactsDir, { recursive: true });
610
+
611
+ await writeFile(
612
+ join(artifactsDir, 'inventory.json'),
613
+ stableStringify({
614
+ packages: [{ name: '@unrdf/package-a', version: '1.0.0' }]
615
+ })
616
+ );
617
+
618
+ const packageDir = join(artifactsDir, '@unrdf/package-a');
619
+ await mkdir(packageDir, { recursive: true });
620
+ await writeFile(join(packageDir, 'diataxis.json'), 'invalid json{');
621
+
622
+ const originalCwd = process.cwd();
623
+ try {
624
+ process.chdir(testDir);
625
+ const exitCode = await main({ json: false, failFast: false, threshold: 0 });
626
+ assert.equal(exitCode, 1, 'Should fail when JSON invalid');
627
+ } finally {
628
+ process.chdir(originalCwd);
629
+ }
630
+ } finally {
631
+ await rm(testDir, { recursive: true, force: true });
632
+ }
633
+ });
634
+ });