@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
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Determinism tests for Diátaxis Kit
|
|
3
|
+
* @description Validates that DETERMINISTIC=1 produces identical output across runs
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { execFile } from 'node:child_process';
|
|
7
|
+
import { promisify } from 'node:util';
|
|
8
|
+
import { readFile, readdir, rm } from 'node:fs/promises';
|
|
9
|
+
import { join, resolve, dirname } from 'node:path';
|
|
10
|
+
import { createHash } from 'node:crypto';
|
|
11
|
+
import { existsSync } from 'node:fs';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
|
|
14
|
+
const execFileAsync = promisify(execFile);
|
|
15
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
16
|
+
const __dirname = dirname(__filename);
|
|
17
|
+
const packageRoot = resolve(__dirname, '..');
|
|
18
|
+
|
|
19
|
+
let passCount = 0;
|
|
20
|
+
let failCount = 0;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Test runner with timeout support
|
|
24
|
+
* @param {string} name - Test name
|
|
25
|
+
* @param {Function} fn - Async test function
|
|
26
|
+
* @param {number} [timeout=30000] - Timeout in milliseconds
|
|
27
|
+
*/
|
|
28
|
+
async function test(name, fn, timeout = 30000) {
|
|
29
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
30
|
+
setTimeout(() => reject(new Error(`Test timeout after ${timeout}ms`)), timeout);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
await Promise.race([fn(), timeoutPromise]);
|
|
35
|
+
console.log(`✅ ${name}`);
|
|
36
|
+
passCount++;
|
|
37
|
+
} catch (err) {
|
|
38
|
+
console.log(`❌ ${name}: ${err.message}`);
|
|
39
|
+
if (err.stack && process.env.VERBOSE) {
|
|
40
|
+
console.log(err.stack);
|
|
41
|
+
}
|
|
42
|
+
failCount++;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Hash a file's contents
|
|
48
|
+
* @param {string} filePath - Path to file
|
|
49
|
+
* @returns {Promise<string>} SHA256 hash
|
|
50
|
+
*/
|
|
51
|
+
async function hashFile(filePath) {
|
|
52
|
+
const content = await readFile(filePath, 'utf8');
|
|
53
|
+
return createHash('sha256').update(content, 'utf8').digest('hex');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Recursively find all files in a directory
|
|
58
|
+
* @param {string} dir - Directory to search
|
|
59
|
+
* @param {string[]} results - Accumulator for results
|
|
60
|
+
* @returns {Promise<string[]>} Array of file paths
|
|
61
|
+
*/
|
|
62
|
+
async function findAllFiles(dir, results = []) {
|
|
63
|
+
if (!existsSync(dir)) {
|
|
64
|
+
return results;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
68
|
+
|
|
69
|
+
for (const entry of entries) {
|
|
70
|
+
const fullPath = join(dir, entry.name);
|
|
71
|
+
if (entry.isDirectory()) {
|
|
72
|
+
await findAllFiles(fullPath, results);
|
|
73
|
+
} else if (entry.isFile()) {
|
|
74
|
+
results.push(fullPath);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return results;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Clean output directories
|
|
83
|
+
* @returns {Promise<void>}
|
|
84
|
+
*/
|
|
85
|
+
async function cleanOutputs() {
|
|
86
|
+
const artifactsDir = join(packageRoot, 'ARTIFACTS');
|
|
87
|
+
const outDir = join(packageRoot, 'OUT');
|
|
88
|
+
|
|
89
|
+
if (existsSync(artifactsDir)) {
|
|
90
|
+
await rm(artifactsDir, { recursive: true, force: true });
|
|
91
|
+
}
|
|
92
|
+
if (existsSync(outDir)) {
|
|
93
|
+
await rm(outDir, { recursive: true, force: true });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Run bin/run.mjs with DETERMINISTIC=1
|
|
99
|
+
* @returns {Promise<void>}
|
|
100
|
+
*/
|
|
101
|
+
async function runPipeline() {
|
|
102
|
+
const binPath = join(packageRoot, 'bin', 'run.mjs');
|
|
103
|
+
|
|
104
|
+
await execFileAsync('node', [binPath], {
|
|
105
|
+
cwd: packageRoot,
|
|
106
|
+
env: {
|
|
107
|
+
...process.env,
|
|
108
|
+
DETERMINISTIC: '1'
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Test: Determinism - running twice produces identical hashes
|
|
115
|
+
*/
|
|
116
|
+
async function testDeterminism() {
|
|
117
|
+
// Run 1
|
|
118
|
+
await cleanOutputs();
|
|
119
|
+
await runPipeline();
|
|
120
|
+
|
|
121
|
+
const artifactsDir = join(packageRoot, 'ARTIFACTS');
|
|
122
|
+
const outDir = join(packageRoot, 'OUT');
|
|
123
|
+
|
|
124
|
+
const files1 = await findAllFiles(artifactsDir);
|
|
125
|
+
const hashes1 = new Map();
|
|
126
|
+
|
|
127
|
+
for (const file of files1) {
|
|
128
|
+
const hash = await hashFile(file);
|
|
129
|
+
const relativePath = file.replace(artifactsDir + '/', '');
|
|
130
|
+
hashes1.set(relativePath, hash);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Sleep 100ms
|
|
134
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
135
|
+
|
|
136
|
+
// Run 2
|
|
137
|
+
await cleanOutputs();
|
|
138
|
+
await runPipeline();
|
|
139
|
+
|
|
140
|
+
const files2 = await findAllFiles(artifactsDir);
|
|
141
|
+
const hashes2 = new Map();
|
|
142
|
+
|
|
143
|
+
for (const file of files2) {
|
|
144
|
+
const hash = await hashFile(file);
|
|
145
|
+
const relativePath = file.replace(artifactsDir + '/', '');
|
|
146
|
+
hashes2.set(relativePath, hash);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Compare file counts
|
|
150
|
+
if (files1.length !== files2.length) {
|
|
151
|
+
throw new Error(`File count mismatch: ${files1.length} vs ${files2.length}`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Compare hashes
|
|
155
|
+
const mismatches = [];
|
|
156
|
+
for (const [path, hash1] of hashes1) {
|
|
157
|
+
const hash2 = hashes2.get(path);
|
|
158
|
+
if (!hash2) {
|
|
159
|
+
mismatches.push(`Missing in run 2: ${path}`);
|
|
160
|
+
} else if (hash1 !== hash2) {
|
|
161
|
+
mismatches.push(`Hash mismatch: ${path}\n Run 1: ${hash1}\n Run 2: ${hash2}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (mismatches.length > 0) {
|
|
166
|
+
throw new Error(`Determinism failed:\n${mismatches.join('\n')}`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
console.log(` Verified ${hashes1.size} files identical across runs`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Test: Consistent timestamps when DETERMINISTIC=1
|
|
174
|
+
*/
|
|
175
|
+
async function testConsistentTimestamps() {
|
|
176
|
+
await cleanOutputs();
|
|
177
|
+
await runPipeline();
|
|
178
|
+
|
|
179
|
+
const artifactsDir = join(packageRoot, 'ARTIFACTS', 'diataxis');
|
|
180
|
+
|
|
181
|
+
// Check inventory.json
|
|
182
|
+
const inventoryPath = join(artifactsDir, 'inventory.json');
|
|
183
|
+
const inventory = JSON.parse(await readFile(inventoryPath, 'utf8'));
|
|
184
|
+
|
|
185
|
+
if (inventory.generatedAt !== '2000-01-01T00:00:00.000Z') {
|
|
186
|
+
throw new Error(`Inventory timestamp not fixed: ${inventory.generatedAt}`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Check 5 random diataxis.json files
|
|
190
|
+
const packageDirs = await readdir(artifactsDir, { withFileTypes: true });
|
|
191
|
+
const validDirs = packageDirs.filter(d => d.isDirectory()).slice(0, 5);
|
|
192
|
+
|
|
193
|
+
for (const dir of validDirs) {
|
|
194
|
+
const diataxisPath = join(artifactsDir, dir.name, 'diataxis.json');
|
|
195
|
+
if (existsSync(diataxisPath)) {
|
|
196
|
+
const diataxis = JSON.parse(await readFile(diataxisPath, 'utf8'));
|
|
197
|
+
if (diataxis.generatedAt !== '2000-01-01T00:00:00.000Z') {
|
|
198
|
+
throw new Error(`${dir.name} timestamp not fixed: ${diataxis.generatedAt}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
console.log(` Verified timestamps fixed at 2000-01-01T00:00:00.000Z`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Test: Stable ordering in output files
|
|
208
|
+
*/
|
|
209
|
+
async function testStableOrdering() {
|
|
210
|
+
await cleanOutputs();
|
|
211
|
+
await runPipeline();
|
|
212
|
+
|
|
213
|
+
const artifactsDir = join(packageRoot, 'ARTIFACTS', 'diataxis');
|
|
214
|
+
const inventoryPath = join(artifactsDir, 'inventory.json');
|
|
215
|
+
const inventory = JSON.parse(await readFile(inventoryPath, 'utf8'));
|
|
216
|
+
|
|
217
|
+
// Verify packages are sorted by name
|
|
218
|
+
const packageNames = inventory.packages.map(p => p.name);
|
|
219
|
+
const sortedNames = [...packageNames].sort((a, b) => a.localeCompare(b));
|
|
220
|
+
|
|
221
|
+
for (let i = 0; i < packageNames.length; i++) {
|
|
222
|
+
if (packageNames[i] !== sortedNames[i]) {
|
|
223
|
+
throw new Error(`Packages not sorted: ${packageNames[i]} at index ${i}, expected ${sortedNames[i]}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Check one diataxis.json for sorted tutorials/howtos
|
|
228
|
+
const firstPackageDir = inventory.packages[0].name;
|
|
229
|
+
const diataxisPath = join(artifactsDir, firstPackageDir, 'diataxis.json');
|
|
230
|
+
|
|
231
|
+
if (existsSync(diataxisPath)) {
|
|
232
|
+
const diataxis = JSON.parse(await readFile(diataxisPath, 'utf8'));
|
|
233
|
+
|
|
234
|
+
// Verify tutorials sorted by id
|
|
235
|
+
if (diataxis.tutorials && diataxis.tutorials.length > 1) {
|
|
236
|
+
const tutorialIds = diataxis.tutorials.map(t => t.id);
|
|
237
|
+
const sortedIds = [...tutorialIds].sort((a, b) => a.localeCompare(b));
|
|
238
|
+
for (let i = 0; i < tutorialIds.length; i++) {
|
|
239
|
+
if (tutorialIds[i] !== sortedIds[i]) {
|
|
240
|
+
throw new Error(`Tutorials not sorted in ${firstPackageDir}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Verify howtos sorted by id
|
|
246
|
+
if (diataxis.howtos && diataxis.howtos.length > 1) {
|
|
247
|
+
const howtoIds = diataxis.howtos.map(h => h.id);
|
|
248
|
+
const sortedIds = [...howtoIds].sort((a, b) => a.localeCompare(b));
|
|
249
|
+
for (let i = 0; i < howtoIds.length; i++) {
|
|
250
|
+
if (howtoIds[i] !== sortedIds[i]) {
|
|
251
|
+
throw new Error(`How-tos not sorted in ${firstPackageDir}: ${howtoIds[i]} at index ${i}, expected ${sortedIds[i]}`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
console.log(` Verified stable ordering for ${packageNames.length} packages`);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Test: Proof hashes in generated markdown files
|
|
262
|
+
*/
|
|
263
|
+
async function testProofHashes() {
|
|
264
|
+
await cleanOutputs();
|
|
265
|
+
await runPipeline();
|
|
266
|
+
|
|
267
|
+
const outDir = join(packageRoot, 'OUT');
|
|
268
|
+
const allMarkdownFiles = await findAllFiles(outDir);
|
|
269
|
+
const markdownFiles = allMarkdownFiles.filter(f => f.endsWith('.md'));
|
|
270
|
+
|
|
271
|
+
if (markdownFiles.length === 0) {
|
|
272
|
+
throw new Error('No markdown files generated');
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
let proofCount = 0;
|
|
276
|
+
const invalidProofs = [];
|
|
277
|
+
|
|
278
|
+
for (const mdFile of markdownFiles) {
|
|
279
|
+
const content = await readFile(mdFile, 'utf8');
|
|
280
|
+
|
|
281
|
+
// Parse frontmatter for proof field
|
|
282
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]+?)\n---/);
|
|
283
|
+
if (!frontmatterMatch) {
|
|
284
|
+
continue; // Skip files without frontmatter
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const frontmatter = frontmatterMatch[1];
|
|
288
|
+
const proofMatch = frontmatter.match(/^proof:\s*([a-f0-9]{64})$/m);
|
|
289
|
+
|
|
290
|
+
if (proofMatch) {
|
|
291
|
+
const proof = proofMatch[1];
|
|
292
|
+
proofCount++;
|
|
293
|
+
|
|
294
|
+
// Verify it's a valid 64-character hex string (SHA256)
|
|
295
|
+
if (!/^[a-f0-9]{64}$/.test(proof)) {
|
|
296
|
+
invalidProofs.push(`${mdFile}: invalid proof format "${proof}"`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (invalidProofs.length > 0) {
|
|
302
|
+
throw new Error(`Invalid proofs:\n${invalidProofs.join('\n')}`);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
console.log(` Verified ${proofCount} proof hashes in ${markdownFiles.length} markdown files`);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Run all tests
|
|
309
|
+
async function runTests() {
|
|
310
|
+
console.log('Running determinism tests...\n');
|
|
311
|
+
|
|
312
|
+
await test('determinism: inventory and artifacts hashes match', testDeterminism, 60000);
|
|
313
|
+
await test('determinism: timestamps fixed at 2000-01-01', testConsistentTimestamps, 30000);
|
|
314
|
+
await test('determinism: stable ordering', testStableOrdering, 30000);
|
|
315
|
+
await test('determinism: proof hashes', testProofHashes, 30000);
|
|
316
|
+
|
|
317
|
+
console.log(`\n${passCount}/${passCount + failCount} tests passed`);
|
|
318
|
+
process.exit(failCount > 0 ? 1 : 0);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
runTests();
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file evidence.test.mjs
|
|
3
|
+
* @description Tests for evidence collection module
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it } from 'node:test';
|
|
7
|
+
import assert from 'node:assert/strict';
|
|
8
|
+
import { collectEvidence, hashEvidence } from '../src/evidence.mjs';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import { dirname, join } from 'node:path';
|
|
11
|
+
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
+
const __dirname = dirname(__filename);
|
|
14
|
+
const fixturesDir = join(__dirname, 'fixtures', 'test-package');
|
|
15
|
+
|
|
16
|
+
describe('evidence.mjs', () => {
|
|
17
|
+
describe('collectEvidence', () => {
|
|
18
|
+
it('should return valid EvidenceSnapshot structure', async () => {
|
|
19
|
+
const packageJson = {
|
|
20
|
+
name: 'test-package',
|
|
21
|
+
version: '1.0.0',
|
|
22
|
+
keywords: ['test', 'example'],
|
|
23
|
+
bin: { 'test-cli': './bin/cli.js' },
|
|
24
|
+
exports: { '.': './src/index.mjs' }
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const evidence = await collectEvidence(fixturesDir, packageJson);
|
|
28
|
+
|
|
29
|
+
// Verify structure
|
|
30
|
+
assert.ok(evidence);
|
|
31
|
+
assert.ok(typeof evidence.readmeContent === 'string' || evidence.readmeContent === null);
|
|
32
|
+
assert.ok(Array.isArray(evidence.readmeHeadings));
|
|
33
|
+
assert.ok(Array.isArray(evidence.examplesFiles));
|
|
34
|
+
assert.ok(typeof evidence.examplesSnippets === 'object');
|
|
35
|
+
assert.ok(Array.isArray(evidence.docsFiles));
|
|
36
|
+
assert.ok(typeof evidence.docsSnippets === 'object');
|
|
37
|
+
assert.ok(Array.isArray(evidence.srcFiles));
|
|
38
|
+
assert.ok(typeof evidence.testFileCount === 'number');
|
|
39
|
+
assert.ok(typeof evidence.binEntries === 'object');
|
|
40
|
+
assert.ok(typeof evidence.exportSurface === 'object');
|
|
41
|
+
assert.ok(Array.isArray(evidence.keywords));
|
|
42
|
+
assert.ok(typeof evidence.hasLicense === 'boolean');
|
|
43
|
+
assert.ok(typeof evidence.hasTsConfig === 'boolean');
|
|
44
|
+
assert.ok(typeof evidence.fingerprint === 'string');
|
|
45
|
+
assert.equal(evidence.fingerprint.length, 64); // SHA256 hex = 64 chars
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should extract package.json fields correctly', async () => {
|
|
49
|
+
const packageJson = {
|
|
50
|
+
keywords: ['rdf', 'semantic'],
|
|
51
|
+
bin: { 'my-cli': './bin/cli.js' },
|
|
52
|
+
exports: { '.': './index.js' }
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const evidence = await collectEvidence(fixturesDir, packageJson);
|
|
56
|
+
|
|
57
|
+
assert.deepEqual(evidence.keywords, ['rdf', 'semantic']);
|
|
58
|
+
assert.deepEqual(evidence.binEntries, { 'my-cli': './bin/cli.js' });
|
|
59
|
+
assert.deepEqual(evidence.exportSurface, { '.': './index.js' });
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should handle missing directories gracefully', async () => {
|
|
63
|
+
const packageJson = { name: 'test' };
|
|
64
|
+
const evidence = await collectEvidence('/tmp/nonexistent-package-dir', packageJson);
|
|
65
|
+
|
|
66
|
+
assert.equal(evidence.readmeContent, null);
|
|
67
|
+
assert.deepEqual(evidence.readmeHeadings, []);
|
|
68
|
+
assert.deepEqual(evidence.examplesFiles, []);
|
|
69
|
+
assert.deepEqual(evidence.docsFiles, []);
|
|
70
|
+
assert.deepEqual(evidence.srcFiles, []);
|
|
71
|
+
assert.equal(evidence.testFileCount, 0);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should throw on invalid package.json', async () => {
|
|
75
|
+
await assert.rejects(
|
|
76
|
+
async () => await collectEvidence('/tmp', null),
|
|
77
|
+
{ message: /Invalid package.json/ }
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should compute deterministic fingerprint', async () => {
|
|
82
|
+
const packageJson = { name: 'test', keywords: ['a', 'b'] };
|
|
83
|
+
const evidence1 = await collectEvidence(fixturesDir, packageJson);
|
|
84
|
+
const evidence2 = await collectEvidence(fixturesDir, packageJson);
|
|
85
|
+
|
|
86
|
+
assert.equal(evidence1.fingerprint, evidence2.fingerprint);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('hashEvidence', () => {
|
|
91
|
+
it('should produce deterministic hash', () => {
|
|
92
|
+
const evidence = {
|
|
93
|
+
readmeContent: 'test',
|
|
94
|
+
readmeHeadings: ['Heading 1'],
|
|
95
|
+
examplesFiles: ['example.js'],
|
|
96
|
+
examplesSnippets: { 'example.js': 'code' },
|
|
97
|
+
docsFiles: ['doc.md'],
|
|
98
|
+
docsSnippets: { 'doc.md': 'docs' },
|
|
99
|
+
srcFiles: ['index.js'],
|
|
100
|
+
testFileCount: 5,
|
|
101
|
+
binEntries: {},
|
|
102
|
+
exportSurface: {},
|
|
103
|
+
keywords: ['test'],
|
|
104
|
+
hasLicense: true,
|
|
105
|
+
hasTsConfig: false,
|
|
106
|
+
fingerprint: 'abc123'
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const hash1 = hashEvidence(evidence);
|
|
110
|
+
const hash2 = hashEvidence(evidence);
|
|
111
|
+
|
|
112
|
+
assert.equal(hash1, hash2);
|
|
113
|
+
assert.equal(hash1.length, 64); // SHA256 hex
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should produce different hashes for different evidence', () => {
|
|
117
|
+
const evidence1 = {
|
|
118
|
+
readmeContent: 'test1',
|
|
119
|
+
readmeHeadings: [],
|
|
120
|
+
examplesFiles: [],
|
|
121
|
+
examplesSnippets: {},
|
|
122
|
+
docsFiles: [],
|
|
123
|
+
docsSnippets: {},
|
|
124
|
+
srcFiles: [],
|
|
125
|
+
testFileCount: 0,
|
|
126
|
+
binEntries: {},
|
|
127
|
+
exportSurface: {},
|
|
128
|
+
keywords: [],
|
|
129
|
+
hasLicense: false,
|
|
130
|
+
hasTsConfig: false,
|
|
131
|
+
fingerprint: 'abc'
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const evidence2 = {
|
|
135
|
+
...evidence1,
|
|
136
|
+
readmeContent: 'test2'
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const hash1 = hashEvidence(evidence1);
|
|
140
|
+
const hash2 = hashEvidence(evidence2);
|
|
141
|
+
|
|
142
|
+
assert.notEqual(hash1, hash2);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "test-deterministic Explanation"
|
|
3
|
+
type: "explanation"
|
|
4
|
+
packageName: "test-deterministic"
|
|
5
|
+
version: "1.0.0"
|
|
6
|
+
generatedAt: "2000-01-01T00:00:00.000Z"
|
|
7
|
+
confidenceScore: 0
|
|
8
|
+
proof: "3f8072cee79349fae3971f82f5c1d412e5a32fbe09ed9860a00bdc84d583b953"
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Concepts
|
|
12
|
+
|
|
13
|
+
- (No concepts documented)
|
|
14
|
+
|
|
15
|
+
## Architecture
|
|
16
|
+
|
|
17
|
+
(No architecture documented)
|
|
18
|
+
|
|
19
|
+
## Tradeoffs
|
|
20
|
+
|
|
21
|
+
- (No tradeoffs documented)
|
|
22
|
+
|
|
23
|
+
## Proof
|
|
24
|
+
|
|
25
|
+
This file was generated from the following evidence sources:
|
|
26
|
+
|
|
27
|
+
- (No sources recorded)
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"fingerprintInput": "|test-deterministic Explanation|0",
|
|
32
|
+
"hash": "3f8072cee79349fae3971f82f5c1d412e5a32fbe09ed9860a00bdc84d583b953",
|
|
33
|
+
"sources": []
|
|
34
|
+
}
|
|
35
|
+
```
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "test-deterministic Documentation"
|
|
3
|
+
packageName: "test-deterministic"
|
|
4
|
+
version: "1.0.0"
|
|
5
|
+
generatedAt: "2000-01-01T00:00:00.000Z"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# test-deterministic Documentation
|
|
9
|
+
|
|
10
|
+
Generated documentation using the Diátaxis framework.
|
|
11
|
+
|
|
12
|
+
## Quick Stats
|
|
13
|
+
|
|
14
|
+
- **Tutorials**: 1
|
|
15
|
+
- **How-To Guides**: 0
|
|
16
|
+
- **Reference**: Not available
|
|
17
|
+
- **Explanation**: Not available
|
|
18
|
+
|
|
19
|
+
## Documentation Sections
|
|
20
|
+
|
|
21
|
+
### Tutorials
|
|
22
|
+
|
|
23
|
+
- [Test Tutorial](tutorials/tutorial-test-tutorial.md)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
*Generated at 2000-01-01T00:00:00.000Z*
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "test-deterministic Reference"
|
|
3
|
+
type: "reference"
|
|
4
|
+
packageName: "test-deterministic"
|
|
5
|
+
version: "1.0.0"
|
|
6
|
+
generatedAt: "2000-01-01T00:00:00.000Z"
|
|
7
|
+
confidenceScore: 0
|
|
8
|
+
proof: "1db83d70e898a847f11d662317d65d4a5326b670fe1cc181774ba086078fd6a7"
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Overview
|
|
12
|
+
|
|
13
|
+
This reference documentation provides technical details for test-deterministic.
|
|
14
|
+
|
|
15
|
+
## API Reference
|
|
16
|
+
|
|
17
|
+
| Name | Type | Description |
|
|
18
|
+
|------|------|-------------|
|
|
19
|
+
| - | - | (No items documented) |
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
## Proof
|
|
23
|
+
|
|
24
|
+
This file was generated from the following evidence sources:
|
|
25
|
+
|
|
26
|
+
- (No sources recorded)
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"fingerprintInput": "|test-deterministic Reference|0",
|
|
31
|
+
"hash": "1db83d70e898a847f11d662317d65d4a5326b670fe1cc181774ba086078fd6a7",
|
|
32
|
+
"sources": []
|
|
33
|
+
}
|
|
34
|
+
```
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Test Tutorial"
|
|
3
|
+
type: "tutorial"
|
|
4
|
+
packageName: "test-deterministic"
|
|
5
|
+
version: "1.0.0"
|
|
6
|
+
generatedAt: "2000-01-01T00:00:00.000Z"
|
|
7
|
+
confidenceScore: 0.9
|
|
8
|
+
proof: "fa5ec3edd8fd24ada2fbe509ea3369703fea30ef1c5d0707430893d876099862"
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Overview
|
|
12
|
+
|
|
13
|
+
Test
|
|
14
|
+
|
|
15
|
+
## Prerequisites
|
|
16
|
+
|
|
17
|
+
- None
|
|
18
|
+
|
|
19
|
+
## Steps
|
|
20
|
+
|
|
21
|
+
1. Step 1
|
|
22
|
+
|
|
23
|
+
## Proof
|
|
24
|
+
|
|
25
|
+
This file was generated from the following evidence sources:
|
|
26
|
+
|
|
27
|
+
- test.md
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"fingerprintInput": "test.md|Test Tutorial|0.9",
|
|
32
|
+
"hash": "fa5ec3edd8fd24ada2fbe509ea3369703fea30ef1c5d0707430893d876099862",
|
|
33
|
+
"sources": [
|
|
34
|
+
"test.md"
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
```
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "test-deterministic Explanation"
|
|
3
|
+
type: "explanation"
|
|
4
|
+
packageName: "test-deterministic"
|
|
5
|
+
version: "1.0.0"
|
|
6
|
+
generatedAt: "2000-01-01T00:00:00.000Z"
|
|
7
|
+
confidenceScore: 0
|
|
8
|
+
proof: "3f8072cee79349fae3971f82f5c1d412e5a32fbe09ed9860a00bdc84d583b953"
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Concepts
|
|
12
|
+
|
|
13
|
+
- (No concepts documented)
|
|
14
|
+
|
|
15
|
+
## Architecture
|
|
16
|
+
|
|
17
|
+
(No architecture documented)
|
|
18
|
+
|
|
19
|
+
## Tradeoffs
|
|
20
|
+
|
|
21
|
+
- (No tradeoffs documented)
|
|
22
|
+
|
|
23
|
+
## Proof
|
|
24
|
+
|
|
25
|
+
This file was generated from the following evidence sources:
|
|
26
|
+
|
|
27
|
+
- (No sources recorded)
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"fingerprintInput": "|test-deterministic Explanation|0",
|
|
32
|
+
"hash": "3f8072cee79349fae3971f82f5c1d412e5a32fbe09ed9860a00bdc84d583b953",
|
|
33
|
+
"sources": []
|
|
34
|
+
}
|
|
35
|
+
```
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "test-deterministic Documentation"
|
|
3
|
+
packageName: "test-deterministic"
|
|
4
|
+
version: "1.0.0"
|
|
5
|
+
generatedAt: "2000-01-01T00:00:00.000Z"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# test-deterministic Documentation
|
|
9
|
+
|
|
10
|
+
Generated documentation using the Diátaxis framework.
|
|
11
|
+
|
|
12
|
+
## Quick Stats
|
|
13
|
+
|
|
14
|
+
- **Tutorials**: 1
|
|
15
|
+
- **How-To Guides**: 0
|
|
16
|
+
- **Reference**: Not available
|
|
17
|
+
- **Explanation**: Not available
|
|
18
|
+
|
|
19
|
+
## Documentation Sections
|
|
20
|
+
|
|
21
|
+
### Tutorials
|
|
22
|
+
|
|
23
|
+
- [Test Tutorial](tutorials/tutorial-test-tutorial.md)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
*Generated at 2000-01-01T00:00:00.000Z*
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "test-deterministic Reference"
|
|
3
|
+
type: "reference"
|
|
4
|
+
packageName: "test-deterministic"
|
|
5
|
+
version: "1.0.0"
|
|
6
|
+
generatedAt: "2000-01-01T00:00:00.000Z"
|
|
7
|
+
confidenceScore: 0
|
|
8
|
+
proof: "1db83d70e898a847f11d662317d65d4a5326b670fe1cc181774ba086078fd6a7"
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Overview
|
|
12
|
+
|
|
13
|
+
This reference documentation provides technical details for test-deterministic.
|
|
14
|
+
|
|
15
|
+
## API Reference
|
|
16
|
+
|
|
17
|
+
| Name | Type | Description |
|
|
18
|
+
|------|------|-------------|
|
|
19
|
+
| - | - | (No items documented) |
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
## Proof
|
|
23
|
+
|
|
24
|
+
This file was generated from the following evidence sources:
|
|
25
|
+
|
|
26
|
+
- (No sources recorded)
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"fingerprintInput": "|test-deterministic Reference|0",
|
|
31
|
+
"hash": "1db83d70e898a847f11d662317d65d4a5326b670fe1cc181774ba086078fd6a7",
|
|
32
|
+
"sources": []
|
|
33
|
+
}
|
|
34
|
+
```
|