@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,425 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Diátaxis schema definitions and validation
|
|
3
|
+
* @module diataxis-schema
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { hashString } from './hash.mjs';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {Object} DiataxisConfidence
|
|
10
|
+
* @property {number} tutorials - Confidence score 0-1
|
|
11
|
+
* @property {number} howtos - Confidence score 0-1
|
|
12
|
+
* @property {number} reference - Confidence score 0-1
|
|
13
|
+
* @property {number} explanation - Confidence score 0-1
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {Object} Tutorial
|
|
18
|
+
* @property {string} id - Generated from title
|
|
19
|
+
* @property {string} title - Tutorial title
|
|
20
|
+
* @property {string} goal - Learning goal
|
|
21
|
+
* @property {string[]} prerequisites - Required knowledge
|
|
22
|
+
* @property {string[]} stepsOutline - Step-by-step outline
|
|
23
|
+
* @property {number} confidenceScore - 0-1 confidence
|
|
24
|
+
* @property {string[]} source - Evidence sources
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @typedef {Object} HowTo
|
|
29
|
+
* @property {string} id - Generated from title
|
|
30
|
+
* @property {string} title - How-to title
|
|
31
|
+
* @property {string} task - Task description
|
|
32
|
+
* @property {string} context - When to use this
|
|
33
|
+
* @property {string[]} steps - Action steps
|
|
34
|
+
* @property {number} confidenceScore - 0-1 confidence
|
|
35
|
+
* @property {string[]} source - Evidence sources
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @typedef {Object} ReferenceItem
|
|
40
|
+
* @property {string} name - Item name
|
|
41
|
+
* @property {'export'|'bin'|'env'|'option'|'unknown'} type - Reference type
|
|
42
|
+
* @property {string} description - Item description
|
|
43
|
+
* @property {string|null} example - Usage example
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @typedef {Object} Reference
|
|
48
|
+
* @property {string} id - Generated identifier
|
|
49
|
+
* @property {string} title - Reference title
|
|
50
|
+
* @property {ReferenceItem[]} items - Reference items
|
|
51
|
+
* @property {number} confidenceScore - 0-1 confidence
|
|
52
|
+
* @property {string[]} source - Evidence sources
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @typedef {Object} Explanation
|
|
57
|
+
* @property {string} id - Generated identifier
|
|
58
|
+
* @property {string} title - Explanation title
|
|
59
|
+
* @property {string[]} concepts - Key concepts
|
|
60
|
+
* @property {string} architecture - Architecture overview
|
|
61
|
+
* @property {string[]} tradeoffs - Design tradeoffs
|
|
62
|
+
* @property {number} confidenceScore - 0-1 confidence
|
|
63
|
+
* @property {string[]} source - Evidence sources
|
|
64
|
+
*/
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @typedef {Object} DiataxisEvidence
|
|
68
|
+
* @property {string[]} readmeHeadings - Headings from README
|
|
69
|
+
* @property {string[]} docsFiles - Documentation files found
|
|
70
|
+
* @property {string[]} examplesFiles - Example files found
|
|
71
|
+
* @property {string} fingerprint - Content fingerprint hash
|
|
72
|
+
*/
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @typedef {Object} DiataxisEntry
|
|
76
|
+
* @property {string} packageName - Package name
|
|
77
|
+
* @property {string} version - Package version
|
|
78
|
+
* @property {string} generatedAt - ISO-8601 timestamp
|
|
79
|
+
* @property {DiataxisConfidence} confidence - Confidence scores
|
|
80
|
+
* @property {Tutorial[]} tutorials - Tutorial entries
|
|
81
|
+
* @property {HowTo[]} howtos - How-to entries
|
|
82
|
+
* @property {Reference} reference - Reference documentation
|
|
83
|
+
* @property {Explanation} explanation - Explanation documentation
|
|
84
|
+
* @property {DiataxisEvidence} evidence - Evidence metadata
|
|
85
|
+
*/
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @typedef {Object} ValidationResult
|
|
89
|
+
* @property {boolean} valid - Whether entry is valid
|
|
90
|
+
* @property {string[]} errors - Validation errors
|
|
91
|
+
*/
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Generate a stable ID from a title string
|
|
95
|
+
* @param {string} title - Title to convert
|
|
96
|
+
* @returns {string} Kebab-case ID
|
|
97
|
+
*/
|
|
98
|
+
function generateId(title) {
|
|
99
|
+
return title
|
|
100
|
+
.toLowerCase()
|
|
101
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
102
|
+
.replace(/^-+|-+$/g, '');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Get current timestamp, respecting DETERMINISTIC mode
|
|
107
|
+
* @returns {string} ISO-8601 timestamp
|
|
108
|
+
*/
|
|
109
|
+
function getTimestamp() {
|
|
110
|
+
if (process.env.DETERMINISTIC === '1') {
|
|
111
|
+
return '2000-01-01T00:00:00.000Z';
|
|
112
|
+
}
|
|
113
|
+
return new Date().toISOString();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Create a validated Diátaxis entry
|
|
118
|
+
* @param {string} packageName - Package name
|
|
119
|
+
* @param {string} version - Package version
|
|
120
|
+
* @param {Object} evidence - Evidence object
|
|
121
|
+
* @param {string[]} [evidence.readmeHeadings=[]] - README headings
|
|
122
|
+
* @param {string[]} [evidence.docsFiles=[]] - Docs files
|
|
123
|
+
* @param {string[]} [evidence.examplesFiles=[]] - Example files
|
|
124
|
+
* @param {Tutorial[]} [evidence.tutorials=[]] - Tutorial entries
|
|
125
|
+
* @param {HowTo[]} [evidence.howtos=[]] - How-to entries
|
|
126
|
+
* @param {Reference} [evidence.reference] - Reference entry
|
|
127
|
+
* @param {Explanation} [evidence.explanation] - Explanation entry
|
|
128
|
+
* @param {DiataxisConfidence} [evidence.confidence] - Confidence scores
|
|
129
|
+
* @returns {DiataxisEntry} Validated entry
|
|
130
|
+
*/
|
|
131
|
+
export function createDiataxisEntry(packageName, version, evidence = {}) {
|
|
132
|
+
const {
|
|
133
|
+
readmeHeadings = [],
|
|
134
|
+
docsFiles = [],
|
|
135
|
+
examplesFiles = [],
|
|
136
|
+
tutorials = [],
|
|
137
|
+
howtos = [],
|
|
138
|
+
reference = null,
|
|
139
|
+
explanation = null,
|
|
140
|
+
confidence = {
|
|
141
|
+
tutorials: 0,
|
|
142
|
+
howtos: 0,
|
|
143
|
+
reference: 0,
|
|
144
|
+
explanation: 0
|
|
145
|
+
}
|
|
146
|
+
} = evidence;
|
|
147
|
+
|
|
148
|
+
// Generate fingerprint from evidence
|
|
149
|
+
const fingerprintInput = JSON.stringify({
|
|
150
|
+
readmeHeadings: [...readmeHeadings].sort(),
|
|
151
|
+
docsFiles: [...docsFiles].sort(),
|
|
152
|
+
examplesFiles: [...examplesFiles].sort()
|
|
153
|
+
});
|
|
154
|
+
const fingerprint = hashString(fingerprintInput);
|
|
155
|
+
|
|
156
|
+
// Ensure IDs on all items
|
|
157
|
+
const tutorialsWithIds = tutorials.map(t => ({
|
|
158
|
+
...t,
|
|
159
|
+
id: t.id || generateId(t.title)
|
|
160
|
+
}));
|
|
161
|
+
|
|
162
|
+
const howtosWithIds = howtos.map(h => ({
|
|
163
|
+
...h,
|
|
164
|
+
id: h.id || generateId(h.title)
|
|
165
|
+
}));
|
|
166
|
+
|
|
167
|
+
const referenceWithId = reference ? {
|
|
168
|
+
...reference,
|
|
169
|
+
id: reference.id || generateId(reference.title)
|
|
170
|
+
} : {
|
|
171
|
+
id: 'reference',
|
|
172
|
+
title: `${packageName} Reference`,
|
|
173
|
+
items: [],
|
|
174
|
+
confidenceScore: 0,
|
|
175
|
+
source: []
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const explanationWithId = explanation ? {
|
|
179
|
+
...explanation,
|
|
180
|
+
id: explanation.id || generateId(explanation.title)
|
|
181
|
+
} : {
|
|
182
|
+
id: 'explanation',
|
|
183
|
+
title: `${packageName} Explanation`,
|
|
184
|
+
concepts: [],
|
|
185
|
+
architecture: '',
|
|
186
|
+
tradeoffs: [],
|
|
187
|
+
confidenceScore: 0,
|
|
188
|
+
source: []
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
/** @type {DiataxisEntry} */
|
|
192
|
+
const entry = {
|
|
193
|
+
packageName,
|
|
194
|
+
version,
|
|
195
|
+
generatedAt: getTimestamp(),
|
|
196
|
+
confidence: {
|
|
197
|
+
tutorials: confidence.tutorials ?? 0,
|
|
198
|
+
howtos: confidence.howtos ?? 0,
|
|
199
|
+
reference: confidence.reference ?? 0,
|
|
200
|
+
explanation: confidence.explanation ?? 0
|
|
201
|
+
},
|
|
202
|
+
tutorials: tutorialsWithIds,
|
|
203
|
+
howtos: howtosWithIds,
|
|
204
|
+
reference: referenceWithId,
|
|
205
|
+
explanation: explanationWithId,
|
|
206
|
+
evidence: {
|
|
207
|
+
readmeHeadings,
|
|
208
|
+
docsFiles,
|
|
209
|
+
examplesFiles,
|
|
210
|
+
fingerprint
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
return entry;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Validate a Diátaxis entry
|
|
219
|
+
* @param {any} entry - Entry to validate
|
|
220
|
+
* @returns {ValidationResult} Validation result
|
|
221
|
+
*/
|
|
222
|
+
export function validateDiataxisEntry(entry) {
|
|
223
|
+
/** @type {string[]} */
|
|
224
|
+
const errors = [];
|
|
225
|
+
|
|
226
|
+
// Check required top-level fields
|
|
227
|
+
if (!entry || typeof entry !== 'object') {
|
|
228
|
+
errors.push('Entry must be an object');
|
|
229
|
+
return { valid: false, errors };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (typeof entry.packageName !== 'string' || entry.packageName === '') {
|
|
233
|
+
errors.push('packageName must be a non-empty string');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (typeof entry.version !== 'string' || entry.version === '') {
|
|
237
|
+
errors.push('version must be a non-empty string');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (typeof entry.generatedAt !== 'string') {
|
|
241
|
+
errors.push('generatedAt must be an ISO-8601 string');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Validate confidence
|
|
245
|
+
if (!entry.confidence || typeof entry.confidence !== 'object') {
|
|
246
|
+
errors.push('confidence must be an object');
|
|
247
|
+
} else {
|
|
248
|
+
const confidenceKeys = ['tutorials', 'howtos', 'reference', 'explanation'];
|
|
249
|
+
for (const key of confidenceKeys) {
|
|
250
|
+
const val = entry.confidence[key];
|
|
251
|
+
if (typeof val !== 'number' || val < 0 || val > 1) {
|
|
252
|
+
errors.push(`confidence.${key} must be a number between 0 and 1`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Validate tutorials
|
|
258
|
+
if (!Array.isArray(entry.tutorials)) {
|
|
259
|
+
errors.push('tutorials must be an array');
|
|
260
|
+
} else {
|
|
261
|
+
entry.tutorials.forEach((t, i) => {
|
|
262
|
+
if (!t.id || typeof t.id !== 'string') {
|
|
263
|
+
errors.push(`tutorials[${i}].id must be a string`);
|
|
264
|
+
}
|
|
265
|
+
if (!t.title || typeof t.title !== 'string') {
|
|
266
|
+
errors.push(`tutorials[${i}].title must be a string`);
|
|
267
|
+
}
|
|
268
|
+
if (typeof t.confidenceScore !== 'number') {
|
|
269
|
+
errors.push(`tutorials[${i}].confidenceScore must be a number`);
|
|
270
|
+
}
|
|
271
|
+
if (!Array.isArray(t.source)) {
|
|
272
|
+
errors.push(`tutorials[${i}].source must be an array`);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Validate howtos
|
|
278
|
+
if (!Array.isArray(entry.howtos)) {
|
|
279
|
+
errors.push('howtos must be an array');
|
|
280
|
+
} else {
|
|
281
|
+
entry.howtos.forEach((h, i) => {
|
|
282
|
+
if (!h.id || typeof h.id !== 'string') {
|
|
283
|
+
errors.push(`howtos[${i}].id must be a string`);
|
|
284
|
+
}
|
|
285
|
+
if (!h.title || typeof h.title !== 'string') {
|
|
286
|
+
errors.push(`howtos[${i}].title must be a string`);
|
|
287
|
+
}
|
|
288
|
+
if (typeof h.confidenceScore !== 'number') {
|
|
289
|
+
errors.push(`howtos[${i}].confidenceScore must be a number`);
|
|
290
|
+
}
|
|
291
|
+
if (!Array.isArray(h.source)) {
|
|
292
|
+
errors.push(`howtos[${i}].source must be an array`);
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Validate reference
|
|
298
|
+
if (!entry.reference || typeof entry.reference !== 'object') {
|
|
299
|
+
errors.push('reference must be an object');
|
|
300
|
+
} else {
|
|
301
|
+
if (!entry.reference.id || typeof entry.reference.id !== 'string') {
|
|
302
|
+
errors.push('reference.id must be a string');
|
|
303
|
+
}
|
|
304
|
+
if (!entry.reference.title || typeof entry.reference.title !== 'string') {
|
|
305
|
+
errors.push('reference.title must be a string');
|
|
306
|
+
}
|
|
307
|
+
if (!Array.isArray(entry.reference.items)) {
|
|
308
|
+
errors.push('reference.items must be an array');
|
|
309
|
+
}
|
|
310
|
+
if (typeof entry.reference.confidenceScore !== 'number') {
|
|
311
|
+
errors.push('reference.confidenceScore must be a number');
|
|
312
|
+
}
|
|
313
|
+
if (!Array.isArray(entry.reference.source)) {
|
|
314
|
+
errors.push('reference.source must be an array');
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Validate explanation
|
|
319
|
+
if (!entry.explanation || typeof entry.explanation !== 'object') {
|
|
320
|
+
errors.push('explanation must be an object');
|
|
321
|
+
} else {
|
|
322
|
+
if (!entry.explanation.id || typeof entry.explanation.id !== 'string') {
|
|
323
|
+
errors.push('explanation.id must be a string');
|
|
324
|
+
}
|
|
325
|
+
if (!entry.explanation.title || typeof entry.explanation.title !== 'string') {
|
|
326
|
+
errors.push('explanation.title must be a string');
|
|
327
|
+
}
|
|
328
|
+
if (!Array.isArray(entry.explanation.concepts)) {
|
|
329
|
+
errors.push('explanation.concepts must be an array');
|
|
330
|
+
}
|
|
331
|
+
if (typeof entry.explanation.architecture !== 'string') {
|
|
332
|
+
errors.push('explanation.architecture must be a string');
|
|
333
|
+
}
|
|
334
|
+
if (!Array.isArray(entry.explanation.tradeoffs)) {
|
|
335
|
+
errors.push('explanation.tradeoffs must be an array');
|
|
336
|
+
}
|
|
337
|
+
if (typeof entry.explanation.confidenceScore !== 'number') {
|
|
338
|
+
errors.push('explanation.confidenceScore must be a number');
|
|
339
|
+
}
|
|
340
|
+
if (!Array.isArray(entry.explanation.source)) {
|
|
341
|
+
errors.push('explanation.source must be an array');
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Validate evidence
|
|
346
|
+
if (!entry.evidence || typeof entry.evidence !== 'object') {
|
|
347
|
+
errors.push('evidence must be an object');
|
|
348
|
+
} else {
|
|
349
|
+
if (!Array.isArray(entry.evidence.readmeHeadings)) {
|
|
350
|
+
errors.push('evidence.readmeHeadings must be an array');
|
|
351
|
+
}
|
|
352
|
+
if (!Array.isArray(entry.evidence.docsFiles)) {
|
|
353
|
+
errors.push('evidence.docsFiles must be an array');
|
|
354
|
+
}
|
|
355
|
+
if (!Array.isArray(entry.evidence.examplesFiles)) {
|
|
356
|
+
errors.push('evidence.examplesFiles must be an array');
|
|
357
|
+
}
|
|
358
|
+
if (typeof entry.evidence.fingerprint !== 'string') {
|
|
359
|
+
errors.push('evidence.fingerprint must be a string');
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return {
|
|
364
|
+
valid: errors.length === 0,
|
|
365
|
+
errors
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Ensure entry has minimum Diátaxis structure (empty stubs if needed)
|
|
371
|
+
* @param {DiataxisEntry} entry - Entry to ensure
|
|
372
|
+
* @returns {DiataxisEntry} Entry with guaranteed structure
|
|
373
|
+
*/
|
|
374
|
+
export function ensureMinimumDiataxis(entry) {
|
|
375
|
+
// Ensure tutorials array exists
|
|
376
|
+
if (!Array.isArray(entry.tutorials) || entry.tutorials.length === 0) {
|
|
377
|
+
entry.tutorials = [];
|
|
378
|
+
entry.confidence.tutorials = 0;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Ensure howtos array exists
|
|
382
|
+
if (!Array.isArray(entry.howtos) || entry.howtos.length === 0) {
|
|
383
|
+
entry.howtos = [];
|
|
384
|
+
entry.confidence.howtos = 0;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Ensure reference exists
|
|
388
|
+
if (!entry.reference || typeof entry.reference !== 'object') {
|
|
389
|
+
entry.reference = {
|
|
390
|
+
id: 'reference',
|
|
391
|
+
title: `${entry.packageName} Reference`,
|
|
392
|
+
items: [],
|
|
393
|
+
confidenceScore: 0,
|
|
394
|
+
source: []
|
|
395
|
+
};
|
|
396
|
+
entry.confidence.reference = 0;
|
|
397
|
+
} else if (!Array.isArray(entry.reference.items) || entry.reference.items.length === 0) {
|
|
398
|
+
entry.reference.items = [];
|
|
399
|
+
entry.reference.confidenceScore = 0;
|
|
400
|
+
entry.confidence.reference = 0;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Ensure explanation exists
|
|
404
|
+
if (!entry.explanation || typeof entry.explanation !== 'object') {
|
|
405
|
+
entry.explanation = {
|
|
406
|
+
id: 'explanation',
|
|
407
|
+
title: `${entry.packageName} Explanation`,
|
|
408
|
+
concepts: [],
|
|
409
|
+
architecture: '',
|
|
410
|
+
tradeoffs: [],
|
|
411
|
+
confidenceScore: 0,
|
|
412
|
+
source: []
|
|
413
|
+
};
|
|
414
|
+
entry.confidence.explanation = 0;
|
|
415
|
+
} else if (
|
|
416
|
+
(!Array.isArray(entry.explanation.concepts) || entry.explanation.concepts.length === 0) &&
|
|
417
|
+
(!entry.explanation.architecture || entry.explanation.architecture === '') &&
|
|
418
|
+
(!Array.isArray(entry.explanation.tradeoffs) || entry.explanation.tradeoffs.length === 0)
|
|
419
|
+
) {
|
|
420
|
+
entry.explanation.confidenceScore = 0;
|
|
421
|
+
entry.confidence.explanation = 0;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return entry;
|
|
425
|
+
}
|
package/src/evidence.mjs
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file evidence.mjs
|
|
3
|
+
* @description Collect evidence from package files to guide Diátaxis classification
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFile, readdir, access } from 'node:fs/promises';
|
|
7
|
+
import { join, extname } from 'node:path';
|
|
8
|
+
import { createHash } from 'node:crypto';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {Object} EvidenceSnapshot
|
|
12
|
+
* @property {string|null} readmeContent - Full README.md content or null if missing
|
|
13
|
+
* @property {string[]} readmeHeadings - Extracted markdown headings (## level)
|
|
14
|
+
* @property {string[]} examplesFiles - Files in examples/ directory
|
|
15
|
+
* @property {Record<string, string>} examplesSnippets - First 200 chars of sample files
|
|
16
|
+
* @property {string[]} docsFiles - Files in docs/ directory
|
|
17
|
+
* @property {Record<string, string>} docsSnippets - First 200 chars of sample files
|
|
18
|
+
* @property {string[]} srcFiles - Top-level .mjs/.js/.ts files from src/
|
|
19
|
+
* @property {number} testFileCount - Count of test files
|
|
20
|
+
* @property {Record<string, string>} binEntries - bin field from package.json
|
|
21
|
+
* @property {Record<string, string>} exportSurface - exports field from package.json
|
|
22
|
+
* @property {string[]} keywords - keywords from package.json
|
|
23
|
+
* @property {boolean} hasLicense - LICENSE file presence
|
|
24
|
+
* @property {boolean} hasTsConfig - tsconfig.json presence
|
|
25
|
+
* @property {string} fingerprint - SHA256 hash of concatenated evidence
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Extract markdown headings (## level) from content
|
|
30
|
+
* @param {string} content - Markdown content
|
|
31
|
+
* @returns {string[]} Array of heading text (without ## prefix)
|
|
32
|
+
*/
|
|
33
|
+
function extractHeadings(content) {
|
|
34
|
+
const headingRegex = /^##\s+(.+)$/gm;
|
|
35
|
+
const headings = [];
|
|
36
|
+
let match;
|
|
37
|
+
while ((match = headingRegex.exec(content)) !== null) {
|
|
38
|
+
headings.push(match[1].trim());
|
|
39
|
+
}
|
|
40
|
+
return headings;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Read first N chars from a file safely
|
|
45
|
+
* @param {string} filePath - Path to file
|
|
46
|
+
* @param {number} maxChars - Maximum characters to read
|
|
47
|
+
* @returns {Promise<string>} File content snippet
|
|
48
|
+
*/
|
|
49
|
+
async function readSnippet(filePath, maxChars = 200) {
|
|
50
|
+
try {
|
|
51
|
+
const content = await readFile(filePath, 'utf-8');
|
|
52
|
+
return content.slice(0, maxChars);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.warn(`Failed to read ${filePath}: ${error.message}`);
|
|
55
|
+
return '';
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Check if a file exists
|
|
61
|
+
* @param {string} filePath - Path to check
|
|
62
|
+
* @returns {Promise<boolean>} True if file exists
|
|
63
|
+
*/
|
|
64
|
+
async function fileExists(filePath) {
|
|
65
|
+
try {
|
|
66
|
+
await access(filePath);
|
|
67
|
+
return true;
|
|
68
|
+
} catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* List files in a directory (non-recursive)
|
|
75
|
+
* @param {string} dirPath - Directory path
|
|
76
|
+
* @returns {Promise<string[]>} Sorted array of filenames
|
|
77
|
+
*/
|
|
78
|
+
async function listFiles(dirPath) {
|
|
79
|
+
try {
|
|
80
|
+
const entries = await readdir(dirPath, { withFileTypes: true });
|
|
81
|
+
return entries
|
|
82
|
+
.filter(entry => entry.isFile())
|
|
83
|
+
.map(entry => entry.name)
|
|
84
|
+
.sort();
|
|
85
|
+
} catch {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Sample first N files from directory and extract snippets
|
|
92
|
+
* Only processes .mjs, .js, .md, .ts files
|
|
93
|
+
* @param {string} dirPath - Directory path
|
|
94
|
+
* @param {number} maxSamples - Maximum files to sample
|
|
95
|
+
* @returns {Promise<Record<string, string>>} Map of filename to snippet
|
|
96
|
+
*/
|
|
97
|
+
async function sampleSnippets(dirPath, maxSamples = 10) {
|
|
98
|
+
const files = await listFiles(dirPath);
|
|
99
|
+
const snippets = {};
|
|
100
|
+
|
|
101
|
+
// Filter to relevant extensions and take first N
|
|
102
|
+
const sampleFiles = files
|
|
103
|
+
.filter(file => {
|
|
104
|
+
const ext = extname(file);
|
|
105
|
+
return ['.mjs', '.js', '.md', '.ts'].includes(ext);
|
|
106
|
+
})
|
|
107
|
+
.slice(0, maxSamples);
|
|
108
|
+
|
|
109
|
+
for (const file of sampleFiles) {
|
|
110
|
+
const filePath = join(dirPath, file);
|
|
111
|
+
snippets[file] = await readSnippet(filePath);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return snippets;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Count files in test directory
|
|
119
|
+
* @param {string} packageDir - Package directory path
|
|
120
|
+
* @returns {Promise<number>} Count of test files
|
|
121
|
+
*/
|
|
122
|
+
async function countTestFiles(packageDir) {
|
|
123
|
+
const testDir = join(packageDir, 'test');
|
|
124
|
+
const files = await listFiles(testDir);
|
|
125
|
+
return files.length;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Compute SHA256 fingerprint from evidence components
|
|
130
|
+
* @param {string[]} readmeHeadings - README headings
|
|
131
|
+
* @param {string[]} examplesFiles - Examples file list
|
|
132
|
+
* @param {string[]} docsFiles - Docs file list
|
|
133
|
+
* @param {string[]} srcFiles - Source file list
|
|
134
|
+
* @param {string[]} keywords - Package keywords
|
|
135
|
+
* @returns {string} Hex-encoded SHA256 hash
|
|
136
|
+
*/
|
|
137
|
+
function computeFingerprint(readmeHeadings, examplesFiles, docsFiles, srcFiles, keywords) {
|
|
138
|
+
const combined = [
|
|
139
|
+
readmeHeadings.join('|'),
|
|
140
|
+
examplesFiles.join('|'),
|
|
141
|
+
docsFiles.join('|'),
|
|
142
|
+
srcFiles.join('|'),
|
|
143
|
+
keywords.join('|')
|
|
144
|
+
].join('|');
|
|
145
|
+
|
|
146
|
+
return createHash('sha256').update(combined).digest('hex');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Collect evidence from package files to guide Diátaxis classification
|
|
151
|
+
* @param {string} packageDir - Absolute path to package directory
|
|
152
|
+
* @param {Object} packageJson - Parsed package.json object
|
|
153
|
+
* @returns {Promise<EvidenceSnapshot>} Evidence snapshot
|
|
154
|
+
* @throws {Error} If package.json is invalid or cannot be processed
|
|
155
|
+
*/
|
|
156
|
+
export async function collectEvidence(packageDir, packageJson) {
|
|
157
|
+
// Validate inputs
|
|
158
|
+
if (!packageJson || typeof packageJson !== 'object') {
|
|
159
|
+
throw new Error('Invalid package.json: must be a valid object');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Read README
|
|
163
|
+
let readmeContent = null;
|
|
164
|
+
let readmeHeadings = [];
|
|
165
|
+
const readmePath = join(packageDir, 'README.md');
|
|
166
|
+
if (await fileExists(readmePath)) {
|
|
167
|
+
try {
|
|
168
|
+
readmeContent = await readFile(readmePath, 'utf-8');
|
|
169
|
+
readmeHeadings = extractHeadings(readmeContent);
|
|
170
|
+
} catch (error) {
|
|
171
|
+
console.warn(`Failed to read README.md: ${error.message}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// List and sample examples/
|
|
176
|
+
const examplesDir = join(packageDir, 'examples');
|
|
177
|
+
const examplesFiles = await listFiles(examplesDir);
|
|
178
|
+
const examplesSnippets = await sampleSnippets(examplesDir);
|
|
179
|
+
|
|
180
|
+
// List and sample docs/
|
|
181
|
+
const docsDir = join(packageDir, 'docs');
|
|
182
|
+
const docsFiles = await listFiles(docsDir);
|
|
183
|
+
const docsSnippets = await sampleSnippets(docsDir);
|
|
184
|
+
|
|
185
|
+
// List src/ top-level .mjs/.js/.ts files
|
|
186
|
+
const srcDir = join(packageDir, 'src');
|
|
187
|
+
const allSrcFiles = await listFiles(srcDir);
|
|
188
|
+
const srcFiles = allSrcFiles.filter(file => {
|
|
189
|
+
const ext = extname(file);
|
|
190
|
+
return ['.mjs', '.js', '.ts'].includes(ext);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// Count test files
|
|
194
|
+
const testFileCount = await countTestFiles(packageDir);
|
|
195
|
+
|
|
196
|
+
// Extract from package.json
|
|
197
|
+
const binEntries = typeof packageJson.bin === 'object' ? packageJson.bin : {};
|
|
198
|
+
const exportSurface = typeof packageJson.exports === 'object' ? packageJson.exports : {};
|
|
199
|
+
const keywords = Array.isArray(packageJson.keywords) ? packageJson.keywords : [];
|
|
200
|
+
|
|
201
|
+
// Check file presence
|
|
202
|
+
const hasLicense = await fileExists(join(packageDir, 'LICENSE'));
|
|
203
|
+
const hasTsConfig = await fileExists(join(packageDir, 'tsconfig.json'));
|
|
204
|
+
|
|
205
|
+
// Compute fingerprint
|
|
206
|
+
const fingerprint = computeFingerprint(
|
|
207
|
+
readmeHeadings,
|
|
208
|
+
examplesFiles,
|
|
209
|
+
docsFiles,
|
|
210
|
+
srcFiles,
|
|
211
|
+
keywords
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
readmeContent,
|
|
216
|
+
readmeHeadings,
|
|
217
|
+
examplesFiles,
|
|
218
|
+
examplesSnippets,
|
|
219
|
+
docsFiles,
|
|
220
|
+
docsSnippets,
|
|
221
|
+
srcFiles,
|
|
222
|
+
testFileCount,
|
|
223
|
+
binEntries,
|
|
224
|
+
exportSurface,
|
|
225
|
+
keywords,
|
|
226
|
+
hasLicense,
|
|
227
|
+
hasTsConfig,
|
|
228
|
+
fingerprint
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Hash all fields in evidence snapshot to detect changes
|
|
234
|
+
* Creates deterministic hash from all evidence fields
|
|
235
|
+
* @param {EvidenceSnapshot} evidenceSnapshot - Evidence snapshot to hash
|
|
236
|
+
* @returns {string} Hex-encoded SHA256 hash
|
|
237
|
+
*/
|
|
238
|
+
export function hashEvidence(evidenceSnapshot) {
|
|
239
|
+
// Create stable string representation of all fields
|
|
240
|
+
const parts = [
|
|
241
|
+
evidenceSnapshot.readmeContent || '',
|
|
242
|
+
evidenceSnapshot.readmeHeadings.join('|'),
|
|
243
|
+
evidenceSnapshot.examplesFiles.join('|'),
|
|
244
|
+
Object.entries(evidenceSnapshot.examplesSnippets)
|
|
245
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
246
|
+
.map(([k, v]) => `${k}:${v}`)
|
|
247
|
+
.join('|'),
|
|
248
|
+
evidenceSnapshot.docsFiles.join('|'),
|
|
249
|
+
Object.entries(evidenceSnapshot.docsSnippets)
|
|
250
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
251
|
+
.map(([k, v]) => `${k}:${v}`)
|
|
252
|
+
.join('|'),
|
|
253
|
+
evidenceSnapshot.srcFiles.join('|'),
|
|
254
|
+
String(evidenceSnapshot.testFileCount),
|
|
255
|
+
Object.entries(evidenceSnapshot.binEntries)
|
|
256
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
257
|
+
.map(([k, v]) => `${k}:${v}`)
|
|
258
|
+
.join('|'),
|
|
259
|
+
JSON.stringify(evidenceSnapshot.exportSurface),
|
|
260
|
+
evidenceSnapshot.keywords.join('|'),
|
|
261
|
+
String(evidenceSnapshot.hasLicense),
|
|
262
|
+
String(evidenceSnapshot.hasTsConfig),
|
|
263
|
+
evidenceSnapshot.fingerprint
|
|
264
|
+
];
|
|
265
|
+
|
|
266
|
+
const combined = parts.join('||');
|
|
267
|
+
return createHash('sha256').update(combined).digest('hex');
|
|
268
|
+
}
|