@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,458 @@
1
+ /**
2
+ * @file Scaffold generator - converts DiataxisEntry to markdown files
3
+ * @module scaffold
4
+ */
5
+
6
+ import { mkdir, writeFile, readFile } from 'node:fs/promises';
7
+ import { join, resolve } from 'node:path';
8
+ import { stableStringify } from './stable-json.mjs';
9
+ import { hashString, hashObject } from './hash.mjs';
10
+
11
+ /**
12
+ * @typedef {Object} ScaffoldOutput
13
+ * @property {string} packageName - Package name
14
+ * @property {string[]} filesGenerated - Absolute paths to generated files
15
+ * @property {string} indexPath - Absolute path to index.md
16
+ * @property {string} filesHash - SHA256 of concatenated file hashes
17
+ */
18
+
19
+ /**
20
+ * Generate scaffold markdown files from a DiataxisEntry
21
+ * @param {import('./diataxis-schema.mjs').DiataxisEntry} diataxisEntry - Entry to scaffold
22
+ * @param {string} outputDir - Output directory path
23
+ * @returns {Promise<ScaffoldOutput>} Scaffold output metadata
24
+ */
25
+ export async function generateScaffold(diataxisEntry, outputDir) {
26
+ const absOutputDir = resolve(outputDir);
27
+
28
+ // Create output directory structure
29
+ await mkdir(absOutputDir, { recursive: true });
30
+ await mkdir(join(absOutputDir, 'tutorials'), { recursive: true });
31
+ await mkdir(join(absOutputDir, 'how-to'), { recursive: true });
32
+ await mkdir(join(absOutputDir, 'reference'), { recursive: true });
33
+ await mkdir(join(absOutputDir, 'explanation'), { recursive: true });
34
+
35
+ /** @type {string[]} */
36
+ const filesGenerated = [];
37
+
38
+ // Generate tutorial files
39
+ for (const tutorial of diataxisEntry.tutorials) {
40
+ const filePath = join(absOutputDir, 'tutorials', `tutorial-${tutorial.id}.md`);
41
+ const content = generateTutorialMarkdown(tutorial, diataxisEntry);
42
+ await writeFile(filePath, content, 'utf8');
43
+ filesGenerated.push(filePath);
44
+ }
45
+
46
+ // Generate how-to files
47
+ for (const howto of diataxisEntry.howtos) {
48
+ const filePath = join(absOutputDir, 'how-to', `howto-${howto.id}.md`);
49
+ const content = generateHowToMarkdown(howto, diataxisEntry);
50
+ await writeFile(filePath, content, 'utf8');
51
+ filesGenerated.push(filePath);
52
+ }
53
+
54
+ // Generate reference file
55
+ const referencePath = join(absOutputDir, 'reference', 'reference.md');
56
+ const referenceContent = generateReferenceMarkdown(diataxisEntry.reference, diataxisEntry);
57
+ await writeFile(referencePath, referenceContent, 'utf8');
58
+ filesGenerated.push(referencePath);
59
+
60
+ // Generate explanation file
61
+ const explanationPath = join(absOutputDir, 'explanation', 'explanation.md');
62
+ const explanationContent = generateExplanationMarkdown(diataxisEntry.explanation, diataxisEntry);
63
+ await writeFile(explanationPath, explanationContent, 'utf8');
64
+ filesGenerated.push(explanationPath);
65
+
66
+ // Generate index.md
67
+ const indexPath = join(absOutputDir, 'index.md');
68
+ const indexContent = generateIndexMarkdown(diataxisEntry, filesGenerated);
69
+ await writeFile(indexPath, indexContent, 'utf8');
70
+
71
+ // Calculate filesHash (SHA256 of concatenated file hashes)
72
+ const fileHashes = [];
73
+ const allFiles = [...filesGenerated, indexPath].sort();
74
+ for (const filePath of allFiles) {
75
+ const fileContent = await readFile(filePath, 'utf8');
76
+ const hash = hashString(fileContent);
77
+ fileHashes.push(hash);
78
+ }
79
+ const filesHash = hashString(fileHashes.join(''));
80
+
81
+ return {
82
+ packageName: diataxisEntry.packageName,
83
+ filesGenerated: filesGenerated.sort(),
84
+ indexPath,
85
+ filesHash
86
+ };
87
+ }
88
+
89
+ /**
90
+ * Generate markdown content for a tutorial
91
+ * @param {import('./diataxis-schema.mjs').Tutorial} tutorial - Tutorial object
92
+ * @param {import('./diataxis-schema.mjs').DiataxisEntry} entry - Parent entry
93
+ * @returns {string} Markdown content
94
+ */
95
+ function generateTutorialMarkdown(tutorial, entry) {
96
+ const proof = calculateProof({
97
+ source: tutorial.source,
98
+ title: tutorial.title,
99
+ confidenceScore: tutorial.confidenceScore
100
+ });
101
+
102
+ const frontmatter = generateFrontmatter({
103
+ title: tutorial.title,
104
+ type: 'tutorial',
105
+ packageName: entry.packageName,
106
+ version: entry.version,
107
+ generatedAt: entry.generatedAt,
108
+ confidenceScore: tutorial.confidenceScore,
109
+ proof
110
+ });
111
+
112
+ const prerequisites = tutorial.prerequisites?.length > 0
113
+ ? tutorial.prerequisites.map(p => `- ${p}`).join('\n')
114
+ : '- None';
115
+
116
+ const steps = tutorial.stepsOutline?.length > 0
117
+ ? tutorial.stepsOutline.map((step, i) => `${i + 1}. ${step}`).join('\n')
118
+ : '1. (Steps to be defined)';
119
+
120
+ const proofBlock = generateProofBlock({
121
+ sources: tutorial.source,
122
+ fingerprintInput: `${tutorial.source.sort().join('|')}|${tutorial.title}|${tutorial.confidenceScore}`,
123
+ hash: proof
124
+ });
125
+
126
+ return `${frontmatter}
127
+
128
+ ## Overview
129
+
130
+ ${tutorial.goal || 'Learn how to use this feature.'}
131
+
132
+ ## Prerequisites
133
+
134
+ ${prerequisites}
135
+
136
+ ## Steps
137
+
138
+ ${steps}
139
+
140
+ ${proofBlock}
141
+ `;
142
+ }
143
+
144
+ /**
145
+ * Generate markdown content for a how-to
146
+ * @param {import('./diataxis-schema.mjs').HowTo} howto - How-to object
147
+ * @param {import('./diataxis-schema.mjs').DiataxisEntry} entry - Parent entry
148
+ * @returns {string} Markdown content
149
+ */
150
+ function generateHowToMarkdown(howto, entry) {
151
+ const proof = calculateProof({
152
+ source: howto.source,
153
+ title: howto.title,
154
+ confidenceScore: howto.confidenceScore
155
+ });
156
+
157
+ const frontmatter = generateFrontmatter({
158
+ title: howto.title,
159
+ type: 'how-to',
160
+ packageName: entry.packageName,
161
+ version: entry.version,
162
+ generatedAt: entry.generatedAt,
163
+ confidenceScore: howto.confidenceScore,
164
+ proof
165
+ });
166
+
167
+ const steps = howto.steps?.length > 0
168
+ ? howto.steps.map((step, i) => `${i + 1}. ${step}`).join('\n')
169
+ : '1. (Steps to be defined)';
170
+
171
+ const proofBlock = generateProofBlock({
172
+ sources: howto.source,
173
+ fingerprintInput: `${howto.source.sort().join('|')}|${howto.title}|${howto.confidenceScore}`,
174
+ hash: proof
175
+ });
176
+
177
+ return `${frontmatter}
178
+
179
+ ## Task
180
+
181
+ ${howto.task || 'Perform a specific task.'}
182
+
183
+ ## Context
184
+
185
+ ${howto.context || 'Use this guide when you need to accomplish a specific goal.'}
186
+
187
+ ## Steps
188
+
189
+ ${steps}
190
+
191
+ ${proofBlock}
192
+ `;
193
+ }
194
+
195
+ /**
196
+ * Generate markdown content for reference
197
+ * @param {import('./diataxis-schema.mjs').Reference} reference - Reference object
198
+ * @param {import('./diataxis-schema.mjs').DiataxisEntry} entry - Parent entry
199
+ * @returns {string} Markdown content
200
+ */
201
+ function generateReferenceMarkdown(reference, entry) {
202
+ const proof = calculateProof({
203
+ source: reference.source,
204
+ title: reference.title,
205
+ confidenceScore: reference.confidenceScore
206
+ });
207
+
208
+ const frontmatter = generateFrontmatter({
209
+ title: reference.title,
210
+ type: 'reference',
211
+ packageName: entry.packageName,
212
+ version: entry.version,
213
+ generatedAt: entry.generatedAt,
214
+ confidenceScore: reference.confidenceScore,
215
+ proof
216
+ });
217
+
218
+ let tableContent = '| Name | Type | Description |\n|------|------|-------------|\n';
219
+
220
+ if (reference.items?.length > 0) {
221
+ for (const item of reference.items) {
222
+ const name = escapeMarkdown(item.name || '');
223
+ const type = escapeMarkdown(item.type || 'unknown');
224
+ const description = escapeMarkdown(item.description || '');
225
+ tableContent += `| ${name} | ${type} | ${description} |\n`;
226
+ }
227
+ } else {
228
+ tableContent += '| - | - | (No items documented) |\n';
229
+ }
230
+
231
+ const proofBlock = generateProofBlock({
232
+ sources: reference.source,
233
+ fingerprintInput: `${reference.source.sort().join('|')}|${reference.title}|${reference.confidenceScore}`,
234
+ hash: proof
235
+ });
236
+
237
+ return `${frontmatter}
238
+
239
+ ## Overview
240
+
241
+ This reference documentation provides technical details for ${entry.packageName}.
242
+
243
+ ## API Reference
244
+
245
+ ${tableContent}
246
+
247
+ ${proofBlock}
248
+ `;
249
+ }
250
+
251
+ /**
252
+ * Generate markdown content for explanation
253
+ * @param {import('./diataxis-schema.mjs').Explanation} explanation - Explanation object
254
+ * @param {import('./diataxis-schema.mjs').DiataxisEntry} entry - Parent entry
255
+ * @returns {string} Markdown content
256
+ */
257
+ function generateExplanationMarkdown(explanation, entry) {
258
+ const proof = calculateProof({
259
+ source: explanation.source,
260
+ title: explanation.title,
261
+ confidenceScore: explanation.confidenceScore
262
+ });
263
+
264
+ const frontmatter = generateFrontmatter({
265
+ title: explanation.title,
266
+ type: 'explanation',
267
+ packageName: entry.packageName,
268
+ version: entry.version,
269
+ generatedAt: entry.generatedAt,
270
+ confidenceScore: explanation.confidenceScore,
271
+ proof
272
+ });
273
+
274
+ const concepts = explanation.concepts?.length > 0
275
+ ? explanation.concepts.map(c => `- ${c}`).join('\n')
276
+ : '- (No concepts documented)';
277
+
278
+ const architecture = explanation.architecture || '(No architecture documented)';
279
+
280
+ const tradeoffs = explanation.tradeoffs?.length > 0
281
+ ? explanation.tradeoffs.map(t => `- ${t}`).join('\n')
282
+ : '- (No tradeoffs documented)';
283
+
284
+ const proofBlock = generateProofBlock({
285
+ sources: explanation.source,
286
+ fingerprintInput: `${explanation.source.sort().join('|')}|${explanation.title}|${explanation.confidenceScore}`,
287
+ hash: proof
288
+ });
289
+
290
+ return `${frontmatter}
291
+
292
+ ## Concepts
293
+
294
+ ${concepts}
295
+
296
+ ## Architecture
297
+
298
+ ${architecture}
299
+
300
+ ## Tradeoffs
301
+
302
+ ${tradeoffs}
303
+
304
+ ${proofBlock}
305
+ `;
306
+ }
307
+
308
+ /**
309
+ * Generate index.md content
310
+ * @param {import('./diataxis-schema.mjs').DiataxisEntry} entry - Diataxis entry
311
+ * @param {string[]} filesGenerated - List of generated files
312
+ * @returns {string} Markdown content
313
+ */
314
+ function generateIndexMarkdown(entry, filesGenerated) {
315
+ const tutorialCount = entry.tutorials?.length || 0;
316
+ const howtoCount = entry.howtos?.length || 0;
317
+ const hasReference = entry.reference?.items?.length > 0;
318
+ const hasExplanation = entry.explanation?.concepts?.length > 0 ||
319
+ entry.explanation?.architecture?.length > 0 ||
320
+ entry.explanation?.tradeoffs?.length > 0;
321
+
322
+ let tutorialLinks = '';
323
+ if (tutorialCount > 0) {
324
+ tutorialLinks = '### Tutorials\n\n';
325
+ for (const tutorial of entry.tutorials) {
326
+ tutorialLinks += `- [${tutorial.title}](tutorials/tutorial-${tutorial.id}.md)\n`;
327
+ }
328
+ tutorialLinks += '\n';
329
+ }
330
+
331
+ let howtoLinks = '';
332
+ if (howtoCount > 0) {
333
+ howtoLinks = '### How-To Guides\n\n';
334
+ for (const howto of entry.howtos) {
335
+ howtoLinks += `- [${howto.title}](how-to/howto-${howto.id}.md)\n`;
336
+ }
337
+ howtoLinks += '\n';
338
+ }
339
+
340
+ let referenceLink = '';
341
+ if (hasReference) {
342
+ referenceLink = '### Reference\n\n';
343
+ referenceLink += `- [${entry.reference.title}](reference/reference.md)\n\n`;
344
+ }
345
+
346
+ let explanationLink = '';
347
+ if (hasExplanation) {
348
+ explanationLink = '### Explanation\n\n';
349
+ explanationLink += `- [${entry.explanation.title}](explanation/explanation.md)\n\n`;
350
+ }
351
+
352
+ return `---
353
+ title: "${entry.packageName} Documentation"
354
+ packageName: "${entry.packageName}"
355
+ version: "${entry.version}"
356
+ generatedAt: "${entry.generatedAt}"
357
+ ---
358
+
359
+ # ${entry.packageName} Documentation
360
+
361
+ Generated documentation using the Diátaxis framework.
362
+
363
+ ## Quick Stats
364
+
365
+ - **Tutorials**: ${tutorialCount}
366
+ - **How-To Guides**: ${howtoCount}
367
+ - **Reference**: ${hasReference ? 'Available' : 'Not available'}
368
+ - **Explanation**: ${hasExplanation ? 'Available' : 'Not available'}
369
+
370
+ ## Documentation Sections
371
+
372
+ ${tutorialLinks}${howtoLinks}${referenceLink}${explanationLink}
373
+
374
+ ---
375
+
376
+ *Generated at ${entry.generatedAt}*
377
+ `;
378
+ }
379
+
380
+ /**
381
+ * Generate YAML frontmatter
382
+ * @param {Object} metadata - Frontmatter metadata
383
+ * @param {string} metadata.title - Document title
384
+ * @param {string} metadata.type - Document type
385
+ * @param {string} metadata.packageName - Package name
386
+ * @param {string} metadata.version - Package version
387
+ * @param {string} metadata.generatedAt - Generation timestamp
388
+ * @param {number} metadata.confidenceScore - Confidence score
389
+ * @param {string} metadata.proof - Proof hash
390
+ * @returns {string} YAML frontmatter block
391
+ */
392
+ function generateFrontmatter(metadata) {
393
+ // Fixed order for determinism
394
+ return `---
395
+ title: "${metadata.title}"
396
+ type: "${metadata.type}"
397
+ packageName: "${metadata.packageName}"
398
+ version: "${metadata.version}"
399
+ generatedAt: "${metadata.generatedAt}"
400
+ confidenceScore: ${metadata.confidenceScore}
401
+ proof: "${metadata.proof}"
402
+ ---`;
403
+ }
404
+
405
+ /**
406
+ * Generate proof block
407
+ * @param {Object} proofData - Proof data
408
+ * @param {string[]} proofData.sources - Source list
409
+ * @param {string} proofData.fingerprintInput - Fingerprint input
410
+ * @param {string} proofData.hash - Computed hash
411
+ * @returns {string} Proof block markdown
412
+ */
413
+ function generateProofBlock(proofData) {
414
+ const sourcesList = proofData.sources?.length > 0
415
+ ? proofData.sources.map(s => `- ${s}`).join('\n')
416
+ : '- (No sources recorded)';
417
+
418
+ const proofJson = stableStringify({
419
+ sources: proofData.sources || [],
420
+ fingerprintInput: proofData.fingerprintInput,
421
+ hash: proofData.hash
422
+ }, { indent: 2 });
423
+
424
+ return `## Proof
425
+
426
+ This file was generated from the following evidence sources:
427
+
428
+ ${sourcesList}
429
+
430
+ \`\`\`json
431
+ ${proofJson}
432
+ \`\`\``;
433
+ }
434
+
435
+ /**
436
+ * Calculate proof hash for a content object
437
+ * @param {Object} data - Data to hash
438
+ * @param {string[]} data.source - Source array
439
+ * @param {string} data.title - Content title
440
+ * @param {number} data.confidenceScore - Confidence score
441
+ * @returns {string} SHA256 hash (hex)
442
+ */
443
+ function calculateProof(data) {
444
+ const fingerprintInput = `${data.source.sort().join('|')}|${data.title}|${data.confidenceScore}`;
445
+ return hashString(fingerprintInput);
446
+ }
447
+
448
+ /**
449
+ * Escape markdown special characters in table cells
450
+ * @param {string} text - Text to escape
451
+ * @returns {string} Escaped text
452
+ */
453
+ function escapeMarkdown(text) {
454
+ return text
455
+ .replace(/\|/g, '\\|')
456
+ .replace(/\n/g, ' ')
457
+ .replace(/\r/g, '');
458
+ }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * @file Stable JSON stringification for deterministic output
3
+ * @module stable-json
4
+ */
5
+
6
+ /**
7
+ * @typedef {Object} StableStringifyOptions
8
+ * @property {number} [indent=2] - Number of spaces for indentation
9
+ * @property {boolean} [includeNull=true] - Include null values in output
10
+ */
11
+
12
+ /**
13
+ * Deterministically stringify an object with sorted keys
14
+ * @param {any} obj - Object to stringify
15
+ * @param {StableStringifyOptions} [options={}] - Stringification options
16
+ * @returns {string} Deterministically stringified JSON
17
+ */
18
+ export function stableStringify(obj, options = {}) {
19
+ const indent = options.indent ?? 2;
20
+ const includeNull = options.includeNull ?? true;
21
+
22
+ /**
23
+ * Internal recursive stringifier
24
+ * @param {any} value - Value to stringify
25
+ * @param {number} depth - Current indentation depth
26
+ * @returns {string} Stringified value
27
+ */
28
+ function stringify(value, depth = 0) {
29
+ // Handle primitives
30
+ if (value === null) {
31
+ return includeNull ? 'null' : undefined;
32
+ }
33
+ if (value === undefined) {
34
+ return undefined;
35
+ }
36
+ if (typeof value === 'boolean' || typeof value === 'number') {
37
+ return String(value);
38
+ }
39
+ if (typeof value === 'string') {
40
+ return JSON.stringify(value);
41
+ }
42
+
43
+ // Handle arrays - preserve order, do NOT sort
44
+ if (Array.isArray(value)) {
45
+ if (value.length === 0) {
46
+ return '[]';
47
+ }
48
+ const items = value
49
+ .map(item => stringify(item, depth + 1))
50
+ .filter(item => item !== undefined);
51
+
52
+ if (indent === 0) {
53
+ return `[${items.join(',')}]`;
54
+ }
55
+
56
+ const currentIndent = ' '.repeat(depth * indent);
57
+ const nextIndent = ' '.repeat((depth + 1) * indent);
58
+ return `[\n${nextIndent}${items.join(`,\n${nextIndent}`)}\n${currentIndent}]`;
59
+ }
60
+
61
+ // Handle objects - SORT keys
62
+ if (typeof value === 'object') {
63
+ const keys = Object.keys(value).sort((a, b) => a.localeCompare(b));
64
+
65
+ if (keys.length === 0) {
66
+ return '{}';
67
+ }
68
+
69
+ const pairs = [];
70
+ for (const key of keys) {
71
+ const stringified = stringify(value[key], depth + 1);
72
+ if (stringified !== undefined) {
73
+ pairs.push([key, stringified]);
74
+ }
75
+ }
76
+
77
+ if (pairs.length === 0) {
78
+ return '{}';
79
+ }
80
+
81
+ if (indent === 0) {
82
+ return `{${pairs.map(([k, v]) => `${JSON.stringify(k)}:${v}`).join(',')}}`;
83
+ }
84
+
85
+ const currentIndent = ' '.repeat(depth * indent);
86
+ const nextIndent = ' '.repeat((depth + 1) * indent);
87
+ const pairStrings = pairs.map(([k, v]) => `${nextIndent}${JSON.stringify(k)}: ${v}`);
88
+ return `{\n${pairStrings.join(',\n')}\n${currentIndent}}`;
89
+ }
90
+
91
+ // Fallback for functions, symbols, etc.
92
+ return undefined;
93
+ }
94
+
95
+ const result = stringify(obj, 0);
96
+ return result ?? 'null';
97
+ }
98
+
99
+ /**
100
+ * Compare two objects for structural equality using stable stringify
101
+ * @param {any} obj1 - First object
102
+ * @param {any} obj2 - Second object
103
+ * @returns {boolean} True if structurally equal
104
+ */
105
+ export function stableEqual(obj1, obj2) {
106
+ try {
107
+ const str1 = stableStringify(obj1, { indent: 0 });
108
+ const str2 = stableStringify(obj2, { indent: 0 });
109
+ return str1 === str2;
110
+ } catch {
111
+ return false;
112
+ }
113
+ }
@@ -0,0 +1,131 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @file Quick verification of diataxis-kit implementations
4
+ */
5
+
6
+ import { stableStringify, stableEqual } from './stable-json.mjs';
7
+ import { hashObject, hashString, hashFile } from './hash.mjs';
8
+ import { createDiataxisEntry, validateDiataxisEntry, ensureMinimumDiataxis } from './diataxis-schema.mjs';
9
+
10
+ let errors = 0;
11
+
12
+ function assert(condition, message) {
13
+ if (!condition) {
14
+ console.error(`❌ FAIL: ${message}`);
15
+ errors++;
16
+ } else {
17
+ console.log(`✅ PASS: ${message}`);
18
+ }
19
+ }
20
+
21
+ console.log('=== Testing stable-json.mjs ===\n');
22
+
23
+ // Test 1: Key sorting
24
+ const unsorted = { z: 1, a: 2, m: 3 };
25
+ const json = stableStringify(unsorted, { indent: 0 });
26
+ assert(json === '{"a":2,"m":3,"z":1}', 'Keys should be sorted alphabetically');
27
+
28
+ // Test 2: Array order preserved
29
+ const withArray = { b: [3, 1, 2], a: 1 };
30
+ const jsonArray = stableStringify(withArray, { indent: 0 });
31
+ assert(jsonArray === '{"a":1,"b":[3,1,2]}', 'Array order should be preserved');
32
+
33
+ // Test 3: Nested object sorting
34
+ const nested = { z: { z: 1, a: 2 }, a: 1 };
35
+ const jsonNested = stableStringify(nested, { indent: 0 });
36
+ assert(jsonNested === '{"a":1,"z":{"a":2,"z":1}}', 'Nested objects should have sorted keys');
37
+
38
+ // Test 4: stableEqual
39
+ const obj1 = { a: 1, b: 2 };
40
+ const obj2 = { b: 2, a: 1 };
41
+ assert(stableEqual(obj1, obj2), 'Objects with same content should be equal');
42
+
43
+ const obj3 = { a: 1, b: 3 };
44
+ assert(!stableEqual(obj1, obj3), 'Objects with different content should not be equal');
45
+
46
+ console.log('\n=== Testing hash.mjs ===\n');
47
+
48
+ // Test 5: hashString produces consistent output
49
+ const str = 'test string';
50
+ const hash1 = hashString(str);
51
+ const hash2 = hashString(str);
52
+ assert(hash1 === hash2, 'hashString should be deterministic');
53
+ assert(hash1.length === 64, 'SHA256 hash should be 64 hex characters');
54
+ assert(/^[a-f0-9]{64}$/.test(hash1), 'Hash should be valid hex');
55
+
56
+ // Test 6: hashObject uses stable stringify
57
+ const hashObj1 = hashObject({ b: 2, a: 1 });
58
+ const hashObj2 = hashObject({ a: 1, b: 2 });
59
+ assert(hashObj1 === hashObj2, 'hashObject should ignore key order');
60
+
61
+ console.log('\n=== Testing diataxis-schema.mjs ===\n');
62
+
63
+ // Test 7: createDiataxisEntry with minimal input
64
+ const entry1 = createDiataxisEntry('test-package', '1.0.0', {});
65
+ assert(entry1.packageName === 'test-package', 'Package name should be set');
66
+ assert(entry1.version === '1.0.0', 'Version should be set');
67
+ assert(entry1.generatedAt, 'Timestamp should be generated');
68
+ assert(Array.isArray(entry1.tutorials), 'Tutorials should be an array');
69
+ assert(Array.isArray(entry1.howtos), 'Howtos should be an array');
70
+ assert(entry1.reference && typeof entry1.reference === 'object', 'Reference should be an object');
71
+ assert(entry1.explanation && typeof entry1.explanation === 'object', 'Explanation should be an object');
72
+
73
+ // Test 8: DETERMINISTIC mode
74
+ process.env.DETERMINISTIC = '1';
75
+ const entry2 = createDiataxisEntry('test-package', '1.0.0', {});
76
+ assert(entry2.generatedAt === '2000-01-01T00:00:00.000Z', 'DETERMINISTIC=1 should use fixed timestamp');
77
+ delete process.env.DETERMINISTIC;
78
+
79
+ // Test 9: validateDiataxisEntry
80
+ const validation1 = validateDiataxisEntry(entry1);
81
+ assert(validation1.valid, 'Valid entry should pass validation');
82
+ assert(validation1.errors.length === 0, 'Valid entry should have no errors');
83
+
84
+ // Test 10: Invalid entry fails validation
85
+ const invalidEntry = { packageName: 'test' };
86
+ const validation2 = validateDiataxisEntry(invalidEntry);
87
+ assert(!validation2.valid, 'Invalid entry should fail validation');
88
+ assert(validation2.errors.length > 0, 'Invalid entry should have errors');
89
+
90
+ // Test 11: ensureMinimumDiataxis
91
+ const minimalEntry = createDiataxisEntry('test', '1.0.0', {
92
+ tutorials: [],
93
+ howtos: [],
94
+ reference: null,
95
+ explanation: null
96
+ });
97
+ const ensured = ensureMinimumDiataxis(minimalEntry);
98
+ assert(ensured.reference.items.length === 0, 'Empty reference should be created');
99
+ assert(ensured.explanation.concepts.length === 0, 'Empty explanation should be created');
100
+ assert(ensured.confidence.reference === 0, 'Empty reference should have 0 confidence');
101
+ assert(ensured.confidence.explanation === 0, 'Empty explanation should have 0 confidence');
102
+
103
+ // Test 12: Evidence fingerprint
104
+ const entry3 = createDiataxisEntry('test', '1.0.0', {
105
+ readmeHeadings: ['Installation', 'Usage'],
106
+ docsFiles: ['README.md'],
107
+ examplesFiles: ['example.js']
108
+ });
109
+ assert(entry3.evidence.fingerprint.length === 64, 'Fingerprint should be SHA256 hash');
110
+ assert(entry3.evidence.readmeHeadings.length === 2, 'Evidence should preserve headings');
111
+
112
+ // Test 13: Tutorial/HowTo ID generation
113
+ const entry4 = createDiataxisEntry('test', '1.0.0', {
114
+ tutorials: [
115
+ { title: 'Getting Started with Test', goal: 'Learn basics', prerequisites: [], stepsOutline: [], confidenceScore: 0.8, source: ['README'] }
116
+ ],
117
+ howtos: [
118
+ { title: 'How to Install Test', task: 'Install', context: 'Setup', steps: [], confidenceScore: 0.9, source: ['README'] }
119
+ ]
120
+ });
121
+ assert(entry4.tutorials[0].id === 'getting-started-with-test', 'Tutorial ID should be generated from title');
122
+ assert(entry4.howtos[0].id === 'how-to-install-test', 'HowTo ID should be generated from title');
123
+
124
+ console.log('\n=== Summary ===\n');
125
+ if (errors === 0) {
126
+ console.log('✅ All tests passed!');
127
+ process.exit(0);
128
+ } else {
129
+ console.log(`❌ ${errors} test(s) failed`);
130
+ process.exit(1);
131
+ }