@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,503 @@
1
+ /**
2
+ * @file report.test.mjs
3
+ * @description Tests for report CLI tool
4
+ */
5
+
6
+ import { describe, it, beforeEach } from 'node:test';
7
+ import assert from 'node:assert/strict';
8
+ import { exec } from 'node:child_process';
9
+ import { promisify } from 'node:util';
10
+ import { mkdir, writeFile, rm } from 'node:fs/promises';
11
+ import { join } from 'node:path';
12
+ import { fileURLToPath } from 'node:url';
13
+ import { dirname } from 'node:path';
14
+
15
+ const execAsync = promisify(exec);
16
+ const __filename = fileURLToPath(import.meta.url);
17
+ const __dirname = dirname(__filename);
18
+ const binPath = join(__dirname, '..', 'bin', 'report.mjs');
19
+ const testArtifactsDir = join(__dirname, 'fixtures', 'test-artifacts');
20
+
21
+ /**
22
+ * Create test inventory files
23
+ */
24
+ async function setupTestInventories() {
25
+ const diataxisDir = join(testArtifactsDir, 'ARTIFACTS', 'diataxis');
26
+ await mkdir(diataxisDir, { recursive: true });
27
+
28
+ // Inventory 1: High confidence package
29
+ const inventory1 = {
30
+ packageName: '@unrdf/test-pkg-1',
31
+ version: '1.0.0',
32
+ generatedAt: '2025-01-01T00:00:00.000Z',
33
+ confidence: {
34
+ tutorials: 0.9,
35
+ howtos: 0.85,
36
+ reference: 1.0,
37
+ explanation: 0.8
38
+ },
39
+ tutorials: [
40
+ {
41
+ id: 'tutorial-getting-started',
42
+ title: 'Getting Started',
43
+ goal: 'Learn basics',
44
+ prerequisites: [],
45
+ stepsOutline: ['Install', 'Import', 'Use'],
46
+ confidenceScore: 0.9,
47
+ source: ['readme', 'examples']
48
+ }
49
+ ],
50
+ howtos: [
51
+ {
52
+ id: 'howto-configure',
53
+ title: 'Configure Package',
54
+ task: 'Set up configuration',
55
+ context: 'When customizing',
56
+ steps: ['Create config', 'Set options'],
57
+ confidenceScore: 0.85,
58
+ source: ['readme']
59
+ },
60
+ {
61
+ id: 'howto-use-cli',
62
+ title: 'Use CLI',
63
+ task: 'Use command line',
64
+ context: 'When running CLI',
65
+ steps: ['Install', 'Run'],
66
+ confidenceScore: 1.0,
67
+ source: ['bin']
68
+ }
69
+ ],
70
+ reference: {
71
+ id: 'reference-api',
72
+ title: 'API Reference',
73
+ items: [
74
+ { name: 'export1', type: 'export', description: 'Export 1', example: 'import x' },
75
+ { name: 'cli-tool', type: 'bin', description: 'CLI tool', example: 'cli-tool' }
76
+ ],
77
+ confidenceScore: 1.0,
78
+ source: ['exports', 'bin']
79
+ },
80
+ explanation: {
81
+ id: 'explanation-overview',
82
+ title: 'Understanding Package',
83
+ concepts: ['concept1', 'concept2'],
84
+ architecture: 'This is the architecture',
85
+ tradeoffs: ['Tradeoff 1', 'Tradeoff 2'],
86
+ confidenceScore: 0.8,
87
+ source: ['readme', 'keywords']
88
+ },
89
+ evidence: {
90
+ readmeHeadings: ['Usage', 'API', 'Examples'],
91
+ docsFiles: ['guide.md'],
92
+ examplesFiles: ['example1.mjs', 'example2.mjs'],
93
+ fingerprint: 'abc123'
94
+ }
95
+ };
96
+
97
+ // Inventory 2: Low confidence package
98
+ const inventory2 = {
99
+ packageName: '@unrdf/test-pkg-2',
100
+ version: '0.5.0',
101
+ generatedAt: '2025-01-01T00:00:00.000Z',
102
+ confidence: {
103
+ tutorials: 0.3,
104
+ howtos: 0.4,
105
+ reference: 0.5,
106
+ explanation: 0.4
107
+ },
108
+ tutorials: [
109
+ {
110
+ id: 'tutorial-basic',
111
+ title: 'Basic Tutorial',
112
+ goal: 'Learn',
113
+ prerequisites: [],
114
+ stepsOutline: ['Step 1'],
115
+ confidenceScore: 0.3,
116
+ source: ['inferred']
117
+ }
118
+ ],
119
+ howtos: [
120
+ {
121
+ id: 'howto-use',
122
+ title: 'Use Package',
123
+ task: 'Use it',
124
+ context: 'When needed',
125
+ steps: ['Import', 'Use'],
126
+ confidenceScore: 0.4,
127
+ source: ['inferred']
128
+ },
129
+ {
130
+ id: 'howto-troubleshoot',
131
+ title: 'Troubleshoot',
132
+ task: 'Fix issues',
133
+ context: 'When broken',
134
+ steps: ['Check', 'Fix'],
135
+ confidenceScore: 0.4,
136
+ source: ['inferred']
137
+ }
138
+ ],
139
+ reference: {
140
+ id: 'reference-api',
141
+ title: 'API Reference',
142
+ items: [
143
+ { name: 'unknown', type: 'unknown', description: 'To be added', example: null }
144
+ ],
145
+ confidenceScore: 0.5,
146
+ source: ['inferred']
147
+ },
148
+ explanation: {
149
+ id: 'explanation-overview',
150
+ title: 'Understanding Package',
151
+ concepts: ['core'],
152
+ architecture: 'Basic package',
153
+ tradeoffs: ['Generic tradeoff'],
154
+ confidenceScore: 0.4,
155
+ source: ['inferred']
156
+ },
157
+ evidence: {
158
+ readmeHeadings: [],
159
+ docsFiles: [],
160
+ examplesFiles: [],
161
+ fingerprint: 'def456'
162
+ }
163
+ };
164
+
165
+ // Inventory 3: Medium confidence package
166
+ const inventory3 = {
167
+ packageName: '@unrdf/test-pkg-3',
168
+ version: '2.1.0',
169
+ generatedAt: '2025-01-01T00:00:00.000Z',
170
+ confidence: {
171
+ tutorials: 0.6,
172
+ howtos: 0.7,
173
+ reference: 0.8,
174
+ explanation: 0.65
175
+ },
176
+ tutorials: [
177
+ {
178
+ id: 'tutorial-start',
179
+ title: 'Start Here',
180
+ goal: 'Get started',
181
+ prerequisites: [],
182
+ stepsOutline: ['Install', 'Use'],
183
+ confidenceScore: 0.6,
184
+ source: ['readme']
185
+ }
186
+ ],
187
+ howtos: [
188
+ {
189
+ id: 'howto-config',
190
+ title: 'Configure',
191
+ task: 'Configure',
192
+ context: 'Setup',
193
+ steps: ['Edit config'],
194
+ confidenceScore: 0.7,
195
+ source: ['readme']
196
+ },
197
+ {
198
+ id: 'howto-integrate',
199
+ title: 'Integrate',
200
+ task: 'Integrate',
201
+ context: 'Integration',
202
+ steps: ['Setup'],
203
+ confidenceScore: 0.7,
204
+ source: ['keywords']
205
+ }
206
+ ],
207
+ reference: {
208
+ id: 'reference-api',
209
+ title: 'API Reference',
210
+ items: [
211
+ { name: 'export1', type: 'export', description: 'Export', example: 'import' }
212
+ ],
213
+ confidenceScore: 0.8,
214
+ source: ['exports']
215
+ },
216
+ explanation: {
217
+ id: 'explanation-overview',
218
+ title: 'Understanding Package',
219
+ concepts: ['concept'],
220
+ architecture: 'Architecture description',
221
+ tradeoffs: ['Tradeoff'],
222
+ confidenceScore: 0.65,
223
+ source: ['readme']
224
+ },
225
+ evidence: {
226
+ readmeHeadings: ['Getting Started', 'Usage'],
227
+ docsFiles: [],
228
+ examplesFiles: ['example.mjs'],
229
+ fingerprint: 'ghi789'
230
+ }
231
+ };
232
+
233
+ // Write inventory files
234
+ await writeFile(
235
+ join(diataxisDir, 'test-pkg-1.inventory.json'),
236
+ JSON.stringify(inventory1, null, 2)
237
+ );
238
+ await writeFile(
239
+ join(diataxisDir, 'test-pkg-2.inventory.json'),
240
+ JSON.stringify(inventory2, null, 2)
241
+ );
242
+ await writeFile(
243
+ join(diataxisDir, 'test-pkg-3.inventory.json'),
244
+ JSON.stringify(inventory3, null, 2)
245
+ );
246
+ }
247
+
248
+ /**
249
+ * Clean up test artifacts
250
+ */
251
+ async function cleanupTestInventories() {
252
+ try {
253
+ await rm(testArtifactsDir, { recursive: true, force: true });
254
+ } catch (error) {
255
+ // Ignore cleanup errors
256
+ }
257
+ }
258
+
259
+ describe('report.mjs', () => {
260
+ beforeEach(async () => {
261
+ await cleanupTestInventories();
262
+ await setupTestInventories();
263
+ });
264
+
265
+ describe('Test 1: Coverage summary calculation', () => {
266
+ it('should calculate correct coverage percentages', async () => {
267
+ const { stdout } = await execAsync(
268
+ `cd "${testArtifactsDir}" && node "${binPath}"`
269
+ );
270
+
271
+ // Check summary section
272
+ assert.ok(stdout.includes('Total packages: 3'));
273
+ assert.ok(stdout.includes('With tutorials: 3 (100%)'));
274
+ assert.ok(stdout.includes('With 2+ how-tos: 3 (100%)'));
275
+ assert.ok(stdout.includes('With reference: 3 (100%)'));
276
+ assert.ok(stdout.includes('With explanation: 3 (100%)'));
277
+ });
278
+ });
279
+
280
+ describe('Test 2: Confidence stats (avg, min, max)', () => {
281
+ it('should calculate correct confidence statistics', async () => {
282
+ const { stdout } = await execAsync(
283
+ `cd "${testArtifactsDir}" && node "${binPath}"`
284
+ );
285
+
286
+ // Check confidence section exists
287
+ assert.ok(stdout.includes('CONFIDENCE'));
288
+
289
+ // Check format (avg=X.XX, min=X.XX, max=X.XX)
290
+ assert.match(stdout, /Tutorials\s+: avg=\d+\.\d{2}, min=\d+\.\d{2}, max=\d+\.\d{2}/);
291
+ assert.match(stdout, /Howtos\s+: avg=\d+\.\d{2}, min=\d+\.\d{2}, max=\d+\.\d{2}/);
292
+ assert.match(stdout, /Reference\s+: avg=\d+\.\d{2}, min=\d+\.\d{2}, max=\d+\.\d{2}/);
293
+ assert.match(stdout, /Explanation\s+: avg=\d+\.\d{2}, min=\d+\.\d{2}, max=\d+\.\d{2}/);
294
+
295
+ // Tutorials: avg=(0.9+0.3+0.6)/3=0.60, min=0.30, max=0.90
296
+ assert.ok(stdout.includes('Tutorials : avg=0.60, min=0.30, max=0.90'));
297
+ });
298
+ });
299
+
300
+ describe('Test 3: Lowest confidence packages ranking', () => {
301
+ it('should rank packages by lowest confidence', async () => {
302
+ const { stdout } = await execAsync(
303
+ `cd "${testArtifactsDir}" && node "${binPath}" --top 3`
304
+ );
305
+
306
+ // Check lowest confidence section
307
+ assert.ok(stdout.includes('LOWEST CONFIDENCE (3 packages)'));
308
+
309
+ // Package 2 should be first (avg=0.40)
310
+ assert.ok(stdout.includes('@unrdf/test-pkg-2'));
311
+ assert.ok(stdout.includes('(0.40)'));
312
+
313
+ // Package 3 should be second (avg=0.69)
314
+ assert.ok(stdout.includes('@unrdf/test-pkg-3'));
315
+ assert.ok(stdout.includes('(0.69)'));
316
+
317
+ // Package 1 should be last (avg=0.89)
318
+ assert.ok(stdout.includes('@unrdf/test-pkg-1'));
319
+ assert.ok(stdout.includes('(0.89)'));
320
+ });
321
+
322
+ it('should respect --top N option', async () => {
323
+ const { stdout } = await execAsync(
324
+ `cd "${testArtifactsDir}" && node "${binPath}" --top 1`
325
+ );
326
+
327
+ assert.ok(stdout.includes('LOWEST CONFIDENCE (1 packages)'));
328
+
329
+ // Should only show one package
330
+ const lines = stdout.split('\n').filter(line => line.match(/^\s*\d+\./));
331
+ assert.equal(lines.length, 1);
332
+ });
333
+ });
334
+
335
+ describe('Test 4: Missing evidence detection', () => {
336
+ it('should detect and count missing evidence', async () => {
337
+ const { stdout } = await execAsync(
338
+ `cd "${testArtifactsDir}" && node "${binPath}"`
339
+ );
340
+
341
+ // Check missing evidence section
342
+ assert.ok(stdout.includes('MISSING EVIDENCE'));
343
+
344
+ // Package 2 has all evidence missing
345
+ // Package 3 has no docs/
346
+ // Package 1 has all evidence present
347
+
348
+ // no examples/: 1 package (pkg-2)
349
+ assert.match(stdout, /no examples\/\s*:\s*1 packages?/);
350
+
351
+ // no docs/: 2 packages (pkg-2, pkg-3)
352
+ assert.match(stdout, /no docs\/\s*:\s*2 packages?/);
353
+
354
+ // empty README: 1 package (pkg-2)
355
+ assert.match(stdout, /empty README\s*:\s*1 packages?/);
356
+
357
+ // no bin entries: 2 packages (pkg-2, pkg-3)
358
+ assert.match(stdout, /no bin entries\s*:\s*2 packages?/);
359
+ });
360
+ });
361
+
362
+ describe('Test 5: JSON output format', () => {
363
+ it('should generate valid JSON output', async () => {
364
+ const { stdout } = await execAsync(
365
+ `cd "${testArtifactsDir}" && node "${binPath}" --json`
366
+ );
367
+
368
+ // Parse JSON
369
+ const report = JSON.parse(stdout);
370
+
371
+ // Check structure
372
+ assert.ok(report.summary);
373
+ assert.equal(report.summary.total, 3);
374
+ assert.equal(report.summary.withTutorials, 3);
375
+ assert.equal(report.summary.with2PlusHowtos, 3);
376
+
377
+ // Check confidence
378
+ assert.ok(report.confidence);
379
+ assert.ok(report.confidence.tutorials);
380
+ assert.equal(report.confidence.tutorials.avg.toFixed(2), '0.60');
381
+ assert.equal(report.confidence.tutorials.min, 0.3);
382
+ assert.equal(report.confidence.tutorials.max, 0.9);
383
+
384
+ // Check lowestConfidence
385
+ assert.ok(Array.isArray(report.lowestConfidence));
386
+ assert.equal(report.lowestConfidence.length, 3); // Only 3 packages total
387
+
388
+ // First should be lowest (pkg-2)
389
+ assert.equal(report.lowestConfidence[0].packageName, '@unrdf/test-pkg-2');
390
+ assert.equal(report.lowestConfidence[0].avgConfidence, 0.40);
391
+
392
+ // Check missingEvidence
393
+ assert.ok(report.missingEvidence);
394
+ assert.equal(report.missingEvidence['no docs/'], 2);
395
+ assert.equal(report.missingEvidence['no examples/'], 1);
396
+
397
+ // Check generatedAt
398
+ assert.ok(report.generatedAt);
399
+ assert.match(report.generatedAt, /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/);
400
+ });
401
+ });
402
+
403
+ describe('Test 6: CSV output format', () => {
404
+ it('should generate valid CSV output', async () => {
405
+ const { stdout } = await execAsync(
406
+ `cd "${testArtifactsDir}" && node "${binPath}" --csv`
407
+ );
408
+
409
+ const lines = stdout.trim().split('\n');
410
+
411
+ // Check header
412
+ assert.equal(
413
+ lines[0],
414
+ 'Package,Tutorials,HowTos,HasReference,HasExplanation,AvgConfidence,TutorialsConf,HowtosConf,ReferenceConf,ExplanationConf,MissingEvidence'
415
+ );
416
+
417
+ // Check data rows (3 packages)
418
+ assert.equal(lines.length, 4); // Header + 3 rows
419
+
420
+ // Check pkg-1 row
421
+ const pkg1Row = lines.find(line => line.startsWith('@unrdf/test-pkg-1'));
422
+ assert.ok(pkg1Row);
423
+ assert.ok(pkg1Row.includes(',1,2,yes,yes,0.89,0.90,0.85,1.00,0.80,'));
424
+
425
+ // Check pkg-2 row (low confidence)
426
+ const pkg2Row = lines.find(line => line.startsWith('@unrdf/test-pkg-2'));
427
+ assert.ok(pkg2Row);
428
+ assert.ok(pkg2Row.includes(',1,2,yes,yes,0.40,0.30,0.40,0.50,0.40,'));
429
+ assert.ok(pkg2Row.includes('"no examples/; no docs/; empty README; no bin entries"'));
430
+ });
431
+ });
432
+
433
+ describe('Test 7: Filter option', () => {
434
+ it('should filter packages by keyword', async () => {
435
+ const { stdout } = await execAsync(
436
+ `cd "${testArtifactsDir}" && node "${binPath}" --filter "pkg-1"`
437
+ );
438
+
439
+ // Should only show pkg-1
440
+ assert.ok(stdout.includes('Total packages: 1'));
441
+ assert.ok(stdout.includes('@unrdf/test-pkg-1'));
442
+ assert.ok(!stdout.includes('@unrdf/test-pkg-2'));
443
+ assert.ok(!stdout.includes('@unrdf/test-pkg-3'));
444
+ });
445
+ });
446
+
447
+ describe('Test 8: Sort option', () => {
448
+ it('should sort by tutorials count', async () => {
449
+ const { stdout } = await execAsync(
450
+ `cd "${testArtifactsDir}" && node "${binPath}" --sort tutorials --top 3 --csv`
451
+ );
452
+
453
+ const lines = stdout.trim().split('\n').slice(1); // Skip header
454
+
455
+ // All have 1 tutorial, so order should be stable (alphabetical)
456
+ assert.ok(lines[0].startsWith('@unrdf/test-pkg-1'));
457
+ assert.ok(lines[1].startsWith('@unrdf/test-pkg-2'));
458
+ assert.ok(lines[2].startsWith('@unrdf/test-pkg-3'));
459
+ });
460
+
461
+ it('should sort by confidence (default)', async () => {
462
+ const { stdout } = await execAsync(
463
+ `cd "${testArtifactsDir}" && node "${binPath}" --sort confidence --csv`
464
+ );
465
+
466
+ const lines = stdout.trim().split('\n').slice(1); // Skip header
467
+
468
+ // Should be sorted by avgConfidence descending
469
+ // pkg-1: 0.89, pkg-3: 0.69, pkg-2: 0.40
470
+ assert.ok(lines[0].startsWith('@unrdf/test-pkg-1'));
471
+ assert.ok(lines[1].startsWith('@unrdf/test-pkg-3'));
472
+ assert.ok(lines[2].startsWith('@unrdf/test-pkg-2'));
473
+ });
474
+ });
475
+
476
+ describe('Test 9: Empty inventory handling', () => {
477
+ it('should handle missing ARTIFACTS directory gracefully', async () => {
478
+ await cleanupTestInventories();
479
+ // Create test directory again so cd works, but without ARTIFACTS
480
+ await mkdir(testArtifactsDir, { recursive: true });
481
+
482
+ const { stdout } = await execAsync(
483
+ `cd "${testArtifactsDir}" && node "${binPath}"`
484
+ );
485
+
486
+ assert.ok(stdout.includes('No inventory generated'));
487
+ });
488
+ });
489
+
490
+ describe('Test 10: Help option', () => {
491
+ it('should display help message', async () => {
492
+ const { stdout } = await execAsync(`node "${binPath}" --help`);
493
+
494
+ assert.ok(stdout.includes('Diátaxis Coverage Report Generator'));
495
+ assert.ok(stdout.includes('Usage:'));
496
+ assert.ok(stdout.includes('--json'));
497
+ assert.ok(stdout.includes('--csv'));
498
+ assert.ok(stdout.includes('--top'));
499
+ assert.ok(stdout.includes('--filter'));
500
+ assert.ok(stdout.includes('--sort'));
501
+ });
502
+ });
503
+ });