@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.
- package/README.md +425 -0
- package/bin/report.mjs +529 -0
- package/bin/run.mjs +114 -0
- package/bin/verify.mjs +356 -0
- package/capability-map.md +92 -0
- package/package.json +42 -0
- package/src/classify.mjs +584 -0
- package/src/diataxis-schema.mjs +425 -0
- package/src/evidence.mjs +268 -0
- package/src/hash.mjs +37 -0
- package/src/inventory.mjs +280 -0
- package/src/reference-extractor.mjs +324 -0
- package/src/scaffold.mjs +458 -0
- package/src/stable-json.mjs +113 -0
- package/src/verify-implementation.mjs +131 -0
- package/test/determinism.test.mjs +321 -0
- package/test/evidence.test.mjs +145 -0
- package/test/fixtures/scaffold-det1/explanation/explanation.md +35 -0
- package/test/fixtures/scaffold-det1/index.md +29 -0
- package/test/fixtures/scaffold-det1/reference/reference.md +34 -0
- package/test/fixtures/scaffold-det1/tutorials/tutorial-test-tutorial.md +37 -0
- package/test/fixtures/scaffold-det2/explanation/explanation.md +35 -0
- package/test/fixtures/scaffold-det2/index.md +29 -0
- package/test/fixtures/scaffold-det2/reference/reference.md +34 -0
- package/test/fixtures/scaffold-det2/tutorials/tutorial-test-tutorial.md +37 -0
- package/test/fixtures/scaffold-empty/explanation/explanation.md +35 -0
- package/test/fixtures/scaffold-empty/index.md +25 -0
- package/test/fixtures/scaffold-empty/reference/reference.md +34 -0
- package/test/fixtures/scaffold-escape/explanation/explanation.md +35 -0
- package/test/fixtures/scaffold-escape/index.md +29 -0
- package/test/fixtures/scaffold-escape/reference/reference.md +36 -0
- package/test/fixtures/scaffold-output/explanation/explanation.md +39 -0
- package/test/fixtures/scaffold-output/how-to/howto-configure-options.md +39 -0
- package/test/fixtures/scaffold-output/index.md +41 -0
- package/test/fixtures/scaffold-output/reference/reference.md +36 -0
- package/test/fixtures/scaffold-output/tutorials/tutorial-getting-started.md +41 -0
- package/test/fixtures/test-artifacts/ARTIFACTS/diataxis/test-pkg-1.inventory.json +115 -0
- package/test/fixtures/test-artifacts/ARTIFACTS/diataxis/test-pkg-2.inventory.json +93 -0
- package/test/fixtures/test-artifacts/ARTIFACTS/diataxis/test-pkg-3.inventory.json +97 -0
- package/test/fixtures/test-package/LICENSE +1 -0
- package/test/fixtures/test-package/README.md +15 -0
- package/test/fixtures/test-package/docs/guide.md +3 -0
- package/test/fixtures/test-package/examples/basic.mjs +3 -0
- package/test/fixtures/test-package/src/index.mjs +3 -0
- package/test/inventory.test.mjs +199 -0
- package/test/reference-extractor.test.mjs +187 -0
- package/test/report.test.mjs +503 -0
- package/test/scaffold.test.mjs +242 -0
- package/test/verify-gate.test.mjs +634 -0
package/src/scaffold.mjs
ADDED
|
@@ -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
|
+
}
|