@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/classify.mjs
ADDED
|
@@ -0,0 +1,584 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Diátaxis classifier - evidence-driven documentation classification
|
|
3
|
+
* @module classify
|
|
4
|
+
* @description Consumes InventoryEntry + EvidenceSnapshot and produces DiataxisEntry
|
|
5
|
+
* with stubs for tutorials, how-tos, reference, and explanation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createDiataxisEntry, validateDiataxisEntry, ensureMinimumDiataxis } from './diataxis-schema.mjs';
|
|
9
|
+
import { hashEvidence } from './evidence.mjs';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Generate a stable ID from a title string with type prefix
|
|
13
|
+
* @param {string} title - Title to convert
|
|
14
|
+
* @param {string} type - Type prefix (tutorial, howto, reference, explanation)
|
|
15
|
+
* @returns {string} Prefixed kebab-case ID
|
|
16
|
+
*/
|
|
17
|
+
function generateId(title, type) {
|
|
18
|
+
const slug = title
|
|
19
|
+
.toLowerCase()
|
|
20
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
21
|
+
.replace(/^-+|-+$/g, '');
|
|
22
|
+
return `${type}-${slug}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Calculate confidence score based on evidence strength
|
|
27
|
+
* @param {Object} evidence - Evidence object containing various sources
|
|
28
|
+
* @param {string} type - Type of documentation (tutorials, howtos, reference, explanation)
|
|
29
|
+
* @returns {number} Confidence score 0-1
|
|
30
|
+
*/
|
|
31
|
+
function confidenceScore(evidence, type) {
|
|
32
|
+
let score = 0;
|
|
33
|
+
|
|
34
|
+
switch (type) {
|
|
35
|
+
case 'tutorials':
|
|
36
|
+
// +0.3 for examples/, +0.3 for README, +0.1 for keywords
|
|
37
|
+
if (evidence.examplesFiles && evidence.examplesFiles.length > 0) {
|
|
38
|
+
score += 0.3;
|
|
39
|
+
}
|
|
40
|
+
if (evidence.readmeHeadings && evidence.readmeHeadings.some(h =>
|
|
41
|
+
/tutorial|getting started|quick start|first steps/i.test(h)
|
|
42
|
+
)) {
|
|
43
|
+
score += 0.3;
|
|
44
|
+
}
|
|
45
|
+
if (evidence.keywords && evidence.keywords.length > 0) {
|
|
46
|
+
score += 0.1;
|
|
47
|
+
}
|
|
48
|
+
// Additional boost for README content with intro sections
|
|
49
|
+
if (evidence.readmeContent && /## (tutorial|getting started|quick start)/i.test(evidence.readmeContent)) {
|
|
50
|
+
score += 0.3;
|
|
51
|
+
}
|
|
52
|
+
break;
|
|
53
|
+
|
|
54
|
+
case 'howtos':
|
|
55
|
+
// +0.25 README sections, +0.25 bin, +0.25 keywords, +0.25 tests
|
|
56
|
+
if (evidence.readmeHeadings && evidence.readmeHeadings.some(h =>
|
|
57
|
+
/usage|api|options|configuration|how to/i.test(h)
|
|
58
|
+
)) {
|
|
59
|
+
score += 0.25;
|
|
60
|
+
}
|
|
61
|
+
if (evidence.binEntries && Object.keys(evidence.binEntries).length > 0) {
|
|
62
|
+
score += 0.25;
|
|
63
|
+
}
|
|
64
|
+
if (evidence.keywords && evidence.keywords.length > 2) {
|
|
65
|
+
score += 0.25;
|
|
66
|
+
}
|
|
67
|
+
if (evidence.testFileCount && evidence.testFileCount > 0) {
|
|
68
|
+
score += 0.25;
|
|
69
|
+
}
|
|
70
|
+
break;
|
|
71
|
+
|
|
72
|
+
case 'reference':
|
|
73
|
+
// +0.5 exports, +0.3 bin, +0.2 README API
|
|
74
|
+
if (evidence.exportSurface && Object.keys(evidence.exportSurface).length > 0) {
|
|
75
|
+
score += 0.5;
|
|
76
|
+
}
|
|
77
|
+
if (evidence.binEntries && Object.keys(evidence.binEntries).length > 0) {
|
|
78
|
+
score += 0.3;
|
|
79
|
+
}
|
|
80
|
+
if (evidence.readmeHeadings && evidence.readmeHeadings.some(h =>
|
|
81
|
+
/api|reference|exports|methods/i.test(h)
|
|
82
|
+
)) {
|
|
83
|
+
score += 0.2;
|
|
84
|
+
}
|
|
85
|
+
break;
|
|
86
|
+
|
|
87
|
+
case 'explanation':
|
|
88
|
+
// +0.3 README, +0.3 docs/, +0.4 keywords
|
|
89
|
+
if (evidence.readmeContent && evidence.readmeContent.length > 100) {
|
|
90
|
+
score += 0.3;
|
|
91
|
+
}
|
|
92
|
+
if (evidence.docsFiles && evidence.docsFiles.length > 0) {
|
|
93
|
+
score += 0.3;
|
|
94
|
+
}
|
|
95
|
+
if (evidence.keywords && evidence.keywords.length > 0) {
|
|
96
|
+
score += 0.4;
|
|
97
|
+
}
|
|
98
|
+
break;
|
|
99
|
+
|
|
100
|
+
default:
|
|
101
|
+
return 0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return Math.min(score, 1.0);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Extract API section content from README (first 500 chars after API heading)
|
|
109
|
+
* @param {string|null} readmeContent - README content
|
|
110
|
+
* @returns {string} API section snippet or empty string
|
|
111
|
+
*/
|
|
112
|
+
function extractApiSection(readmeContent) {
|
|
113
|
+
if (!readmeContent) return '';
|
|
114
|
+
|
|
115
|
+
const apiMatch = readmeContent.match(/##\s+(API|Reference|Exports|Methods)\s*\n([\s\S]{0,500})/i);
|
|
116
|
+
return apiMatch ? apiMatch[2].trim() : '';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Extract first sentence from README intro (architecture description)
|
|
121
|
+
* @param {string|null} readmeContent - README content
|
|
122
|
+
* @returns {string} First sentence or empty string
|
|
123
|
+
*/
|
|
124
|
+
function extractFirstSentence(readmeContent) {
|
|
125
|
+
if (!readmeContent) return '';
|
|
126
|
+
|
|
127
|
+
// Skip over title and find first paragraph
|
|
128
|
+
const paragraphMatch = readmeContent.match(/^#[^\n]*\n+([^#\n][^\n]*\.)/m);
|
|
129
|
+
return paragraphMatch ? paragraphMatch[1].trim() : '';
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Check if README contains tradeoff/comparison language
|
|
134
|
+
* @param {string|null} readmeContent - README content
|
|
135
|
+
* @returns {boolean} True if tradeoff language detected
|
|
136
|
+
*/
|
|
137
|
+
function hasTradeoffLanguage(readmeContent) {
|
|
138
|
+
if (!readmeContent) return false;
|
|
139
|
+
return /\b(trade[-\s]?off|vs\.?|versus|compared to|alternative|choice)\b/i.test(readmeContent);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Generate tutorial stubs based on evidence
|
|
144
|
+
* @param {string} packageName - Package name
|
|
145
|
+
* @param {Object} evidence - Evidence snapshot
|
|
146
|
+
* @returns {Array<{id: string, title: string, goal: string, prerequisites: string[], stepsOutline: string[], confidenceScore: number, source: string[]}>} Tutorial stubs
|
|
147
|
+
*/
|
|
148
|
+
function generateTutorials(packageName, evidence) {
|
|
149
|
+
const tutorials = [];
|
|
150
|
+
const sources = [];
|
|
151
|
+
|
|
152
|
+
// Determine confidence and sources
|
|
153
|
+
let confidence = 0;
|
|
154
|
+
const hasExamples = evidence.examplesFiles && evidence.examplesFiles.length > 0;
|
|
155
|
+
const hasReadmeIntro = evidence.readmeHeadings && evidence.readmeHeadings.some(h =>
|
|
156
|
+
/tutorial|getting started|quick start/i.test(h)
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
if (hasExamples) {
|
|
160
|
+
sources.push('examples');
|
|
161
|
+
confidence += 0.6;
|
|
162
|
+
}
|
|
163
|
+
if (hasReadmeIntro) {
|
|
164
|
+
sources.push('readme');
|
|
165
|
+
confidence += 0.3;
|
|
166
|
+
}
|
|
167
|
+
if (evidence.keywords && evidence.keywords.length > 0) {
|
|
168
|
+
sources.push('keywords');
|
|
169
|
+
confidence += 0.1;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
confidence = Math.min(confidence, 1.0);
|
|
173
|
+
|
|
174
|
+
// Always generate "Getting Started" tutorial
|
|
175
|
+
const prerequisites = [];
|
|
176
|
+
if (evidence.keywords) {
|
|
177
|
+
// Infer prerequisites from keywords
|
|
178
|
+
if (evidence.keywords.some(k => /rdf|semantic|triple/i.test(k))) {
|
|
179
|
+
prerequisites.push('Basic RDF knowledge');
|
|
180
|
+
}
|
|
181
|
+
if (evidence.keywords.some(k => /graph|query|sparql/i.test(k))) {
|
|
182
|
+
prerequisites.push('Understanding of graph databases');
|
|
183
|
+
}
|
|
184
|
+
if (evidence.keywords.some(k => /node|javascript|npm/i.test(k))) {
|
|
185
|
+
prerequisites.push('Node.js environment');
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Generate steps from evidence headings or use generic
|
|
190
|
+
let stepsOutline = [];
|
|
191
|
+
if (hasReadmeIntro) {
|
|
192
|
+
// Try to extract steps from README headings
|
|
193
|
+
const stepHeadings = evidence.readmeHeadings.filter(h =>
|
|
194
|
+
/install|setup|usage|example|run/i.test(h)
|
|
195
|
+
).slice(0, 5);
|
|
196
|
+
|
|
197
|
+
if (stepHeadings.length >= 3) {
|
|
198
|
+
stepsOutline = stepHeadings;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (stepsOutline.length === 0) {
|
|
203
|
+
// Use generic steps
|
|
204
|
+
stepsOutline = [
|
|
205
|
+
'Install the package',
|
|
206
|
+
'Import required modules',
|
|
207
|
+
'Create a basic example',
|
|
208
|
+
'Run and verify output'
|
|
209
|
+
];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
tutorials.push({
|
|
213
|
+
id: generateId(`Getting Started with ${packageName}`, 'tutorial'),
|
|
214
|
+
title: `Getting Started with ${packageName}`,
|
|
215
|
+
goal: `Learn how to set up and use ${packageName} in your project`,
|
|
216
|
+
prerequisites,
|
|
217
|
+
stepsOutline,
|
|
218
|
+
confidenceScore: confidence,
|
|
219
|
+
source: sources.length > 0 ? sources : ['inferred']
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// Generate "First Steps" if multiple examples exist
|
|
223
|
+
if (hasExamples && evidence.examplesFiles.length > 2) {
|
|
224
|
+
tutorials.push({
|
|
225
|
+
id: generateId('First Steps', 'tutorial'),
|
|
226
|
+
title: 'First Steps',
|
|
227
|
+
goal: 'Complete your first practical examples',
|
|
228
|
+
prerequisites: [`Completed "Getting Started with ${packageName}"`],
|
|
229
|
+
stepsOutline: evidence.examplesFiles.slice(0, 5).map(file => `Work through ${file}`),
|
|
230
|
+
confidenceScore: 0.8,
|
|
231
|
+
source: ['examples']
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Sort by id for stable ordering
|
|
236
|
+
tutorials.sort((a, b) => a.id.localeCompare(b.id));
|
|
237
|
+
|
|
238
|
+
return tutorials;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Generate how-to stubs based on evidence (minimum 2 required)
|
|
243
|
+
* @param {string} packageName - Package name
|
|
244
|
+
* @param {Object} evidence - Evidence snapshot
|
|
245
|
+
* @returns {Array<{id: string, title: string, task: string, context: string, steps: string[], confidenceScore: number, source: string[]}>} How-to stubs
|
|
246
|
+
*/
|
|
247
|
+
function generateHowTos(packageName, evidence) {
|
|
248
|
+
const howtos = [];
|
|
249
|
+
|
|
250
|
+
// Check for Configuration section
|
|
251
|
+
if (evidence.readmeHeadings && evidence.readmeHeadings.some(h => /configuration|config|options/i.test(h))) {
|
|
252
|
+
howtos.push({
|
|
253
|
+
id: generateId(`Configure ${packageName}`, 'howto'),
|
|
254
|
+
title: `Configure ${packageName}`,
|
|
255
|
+
task: `Set up custom configuration for ${packageName}`,
|
|
256
|
+
context: 'When you need to customize package behavior',
|
|
257
|
+
steps: [
|
|
258
|
+
'Create configuration file',
|
|
259
|
+
'Define configuration options',
|
|
260
|
+
'Apply configuration to package'
|
|
261
|
+
],
|
|
262
|
+
confidenceScore: 1.0,
|
|
263
|
+
source: ['readme']
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Check for CLI usage (bin entries)
|
|
268
|
+
if (evidence.binEntries && Object.keys(evidence.binEntries).length > 0) {
|
|
269
|
+
const binNames = Object.keys(evidence.binEntries);
|
|
270
|
+
howtos.push({
|
|
271
|
+
id: generateId('Use the CLI', 'howto'),
|
|
272
|
+
title: 'Use the CLI',
|
|
273
|
+
task: `Execute ${binNames[0]} command-line interface`,
|
|
274
|
+
context: 'When you need to use the package from the command line',
|
|
275
|
+
steps: [
|
|
276
|
+
'Install package globally or locally',
|
|
277
|
+
'Run CLI command with options',
|
|
278
|
+
'Process command output'
|
|
279
|
+
],
|
|
280
|
+
confidenceScore: 1.0,
|
|
281
|
+
source: ['bin']
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Check for integration opportunities (keywords)
|
|
286
|
+
if (evidence.keywords && evidence.keywords.length > 2) {
|
|
287
|
+
const keyword = evidence.keywords[0];
|
|
288
|
+
howtos.push({
|
|
289
|
+
id: generateId(`Integrate with ${keyword}`, 'howto'),
|
|
290
|
+
title: `Integrate with ${keyword}`,
|
|
291
|
+
task: `Connect ${packageName} with ${keyword}`,
|
|
292
|
+
context: `When working with ${keyword} in your project`,
|
|
293
|
+
steps: [
|
|
294
|
+
'Install dependencies',
|
|
295
|
+
'Set up integration',
|
|
296
|
+
'Test integration'
|
|
297
|
+
],
|
|
298
|
+
confidenceScore: 0.7,
|
|
299
|
+
source: ['keywords', 'inferred']
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Check for error handling (test files exist)
|
|
304
|
+
if (evidence.testFileCount && evidence.testFileCount > 0) {
|
|
305
|
+
howtos.push({
|
|
306
|
+
id: generateId('Handle Errors', 'howto'),
|
|
307
|
+
title: 'Handle Errors',
|
|
308
|
+
task: `Implement error handling for ${packageName}`,
|
|
309
|
+
context: 'When you need robust error handling in production',
|
|
310
|
+
steps: [
|
|
311
|
+
'Set up try-catch blocks',
|
|
312
|
+
'Handle common error types',
|
|
313
|
+
'Log errors appropriately'
|
|
314
|
+
],
|
|
315
|
+
confidenceScore: 0.5,
|
|
316
|
+
source: ['tests']
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Ensure minimum 2 how-tos (add generic ones if needed)
|
|
321
|
+
if (howtos.length === 0) {
|
|
322
|
+
howtos.push({
|
|
323
|
+
id: generateId(`Use ${packageName}`, 'howto'),
|
|
324
|
+
title: `Use ${packageName}`,
|
|
325
|
+
task: `Perform basic operations with ${packageName}`,
|
|
326
|
+
context: 'When you need to accomplish common tasks',
|
|
327
|
+
steps: [
|
|
328
|
+
'Import the package',
|
|
329
|
+
'Initialize with options',
|
|
330
|
+
'Execute operations'
|
|
331
|
+
],
|
|
332
|
+
confidenceScore: 0.4,
|
|
333
|
+
source: ['inferred']
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (howtos.length === 1) {
|
|
338
|
+
howtos.push({
|
|
339
|
+
id: generateId('Troubleshoot Common Issues', 'howto'),
|
|
340
|
+
title: 'Troubleshoot Common Issues',
|
|
341
|
+
task: 'Resolve common problems and errors',
|
|
342
|
+
context: 'When encountering issues during usage',
|
|
343
|
+
steps: [
|
|
344
|
+
'Check configuration',
|
|
345
|
+
'Review error messages',
|
|
346
|
+
'Apply fixes'
|
|
347
|
+
],
|
|
348
|
+
confidenceScore: 0.4,
|
|
349
|
+
source: ['inferred']
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Sort by id for stable ordering
|
|
354
|
+
howtos.sort((a, b) => a.id.localeCompare(b.id));
|
|
355
|
+
|
|
356
|
+
return howtos;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Generate reference documentation based on exports and bin
|
|
361
|
+
* @param {string} packageName - Package name
|
|
362
|
+
* @param {Object} evidence - Evidence snapshot
|
|
363
|
+
* @returns {{id: string, title: string, items: Array, confidenceScore: number, source: string[]}} Reference object
|
|
364
|
+
*/
|
|
365
|
+
function generateReference(packageName, evidence) {
|
|
366
|
+
const items = [];
|
|
367
|
+
const sources = [];
|
|
368
|
+
let confidence = 0;
|
|
369
|
+
|
|
370
|
+
// Extract from exports field
|
|
371
|
+
if (evidence.exportSurface && typeof evidence.exportSurface === 'object') {
|
|
372
|
+
const exportKeys = Object.keys(evidence.exportSurface);
|
|
373
|
+
if (exportKeys.length > 0) {
|
|
374
|
+
sources.push('exports');
|
|
375
|
+
confidence = 1.0;
|
|
376
|
+
|
|
377
|
+
for (const key of exportKeys.sort()) {
|
|
378
|
+
const exportPath = evidence.exportSurface[key];
|
|
379
|
+
items.push({
|
|
380
|
+
name: key === '.' ? packageName : key,
|
|
381
|
+
type: 'export',
|
|
382
|
+
description: `Export: ${key}`,
|
|
383
|
+
example: typeof exportPath === 'string' ? `import from "${exportPath}"` : null
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Extract from bin field
|
|
390
|
+
if (evidence.binEntries && typeof evidence.binEntries === 'object') {
|
|
391
|
+
const binKeys = Object.keys(evidence.binEntries);
|
|
392
|
+
if (binKeys.length > 0) {
|
|
393
|
+
sources.push('bin');
|
|
394
|
+
if (confidence === 0) confidence = 0.8;
|
|
395
|
+
|
|
396
|
+
for (const binName of binKeys.sort()) {
|
|
397
|
+
const binPath = evidence.binEntries[binName];
|
|
398
|
+
items.push({
|
|
399
|
+
name: binName,
|
|
400
|
+
type: 'bin',
|
|
401
|
+
description: `CLI command: ${binName}`,
|
|
402
|
+
example: `${binName} [options]`
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Parse README for API section
|
|
409
|
+
const apiSection = extractApiSection(evidence.readmeContent);
|
|
410
|
+
if (apiSection) {
|
|
411
|
+
sources.push('readme');
|
|
412
|
+
if (confidence === 0) confidence = 0.6;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Add placeholder if no exports or bin found
|
|
416
|
+
if (items.length === 0) {
|
|
417
|
+
items.push({
|
|
418
|
+
name: 'unknown',
|
|
419
|
+
type: 'unknown',
|
|
420
|
+
description: 'Reference documentation to be added',
|
|
421
|
+
example: null
|
|
422
|
+
});
|
|
423
|
+
sources.push('inferred');
|
|
424
|
+
confidence = 0.5;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Sort items by name for stable ordering
|
|
428
|
+
items.sort((a, b) => a.name.localeCompare(b.name));
|
|
429
|
+
|
|
430
|
+
return {
|
|
431
|
+
id: generateId(`${packageName} Reference`, 'reference'),
|
|
432
|
+
title: `${packageName} Reference`,
|
|
433
|
+
items,
|
|
434
|
+
confidenceScore: confidence,
|
|
435
|
+
source: sources
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Generate explanation documentation based on README and docs
|
|
441
|
+
* @param {string} packageName - Package name
|
|
442
|
+
* @param {Object} evidence - Evidence snapshot
|
|
443
|
+
* @returns {{id: string, title: string, concepts: string[], architecture: string, tradeoffs: string[], confidenceScore: number, source: string[]}} Explanation object
|
|
444
|
+
*/
|
|
445
|
+
function generateExplanation(packageName, evidence) {
|
|
446
|
+
const concepts = [];
|
|
447
|
+
const tradeoffs = [];
|
|
448
|
+
const sources = [];
|
|
449
|
+
let confidence = 0;
|
|
450
|
+
|
|
451
|
+
// Extract concepts from keywords
|
|
452
|
+
if (evidence.keywords && evidence.keywords.length > 0) {
|
|
453
|
+
concepts.push(...evidence.keywords.slice(0, 5));
|
|
454
|
+
sources.push('keywords');
|
|
455
|
+
confidence += 0.4;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Generate architecture description from README intro
|
|
459
|
+
let architecture = '';
|
|
460
|
+
if (evidence.readmeContent) {
|
|
461
|
+
architecture = extractFirstSentence(evidence.readmeContent);
|
|
462
|
+
if (architecture) {
|
|
463
|
+
sources.push('readme');
|
|
464
|
+
confidence += 0.3;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (!architecture) {
|
|
469
|
+
architecture = `${packageName} is a package in the UNRDF ecosystem`;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Check for docs directory
|
|
473
|
+
if (evidence.docsFiles && evidence.docsFiles.length > 0) {
|
|
474
|
+
sources.push('docs');
|
|
475
|
+
confidence += 0.3;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// List tradeoffs if detected
|
|
479
|
+
if (hasTradeoffLanguage(evidence.readmeContent)) {
|
|
480
|
+
tradeoffs.push('Performance vs. flexibility tradeoffs discussed in README');
|
|
481
|
+
if (!sources.includes('readme')) {
|
|
482
|
+
sources.push('readme');
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Generate default tradeoff stubs if none found
|
|
487
|
+
if (tradeoffs.length === 0) {
|
|
488
|
+
tradeoffs.push(
|
|
489
|
+
'Ease of use vs. advanced configuration options',
|
|
490
|
+
'Memory usage vs. processing speed',
|
|
491
|
+
'Bundle size vs. feature completeness'
|
|
492
|
+
);
|
|
493
|
+
if (!sources.includes('inferred')) {
|
|
494
|
+
sources.push('inferred');
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
confidence = Math.min(confidence, 1.0);
|
|
499
|
+
|
|
500
|
+
return {
|
|
501
|
+
id: generateId(`${packageName} Explanation`, 'explanation'),
|
|
502
|
+
title: `Understanding ${packageName}`,
|
|
503
|
+
concepts: concepts.length > 0 ? concepts : ['core functionality'],
|
|
504
|
+
architecture,
|
|
505
|
+
tradeoffs,
|
|
506
|
+
confidenceScore: confidence,
|
|
507
|
+
source: sources.length > 0 ? sources : ['inferred']
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Classify a package and produce DiataxisEntry with stubs
|
|
513
|
+
* @param {import('./inventory.mjs').PackageEntry} packageEntry - Package metadata from inventory
|
|
514
|
+
* @param {import('./evidence.mjs').EvidenceSnapshot} evidenceSnapshot - Evidence collected from package
|
|
515
|
+
* @returns {Promise<import('./diataxis-schema.mjs').DiataxisEntry>} Complete Diátaxis entry
|
|
516
|
+
* @throws {Error} If packageEntry is invalid
|
|
517
|
+
*/
|
|
518
|
+
export async function classifyPackage(packageEntry, evidenceSnapshot) {
|
|
519
|
+
// Validate packageEntry
|
|
520
|
+
if (!packageEntry || typeof packageEntry !== 'object') {
|
|
521
|
+
throw new Error('Invalid packageEntry: must be an object');
|
|
522
|
+
}
|
|
523
|
+
if (!packageEntry.name || typeof packageEntry.name !== 'string') {
|
|
524
|
+
throw new Error('Invalid packageEntry: name must be a non-empty string');
|
|
525
|
+
}
|
|
526
|
+
if (!packageEntry.version || typeof packageEntry.version !== 'string') {
|
|
527
|
+
throw new Error('Invalid packageEntry: version must be a non-empty string');
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Use empty snapshot if evidenceSnapshot is missing
|
|
531
|
+
const evidence = evidenceSnapshot || {
|
|
532
|
+
readmeContent: null,
|
|
533
|
+
readmeHeadings: [],
|
|
534
|
+
examplesFiles: [],
|
|
535
|
+
examplesSnippets: {},
|
|
536
|
+
docsFiles: [],
|
|
537
|
+
docsSnippets: {},
|
|
538
|
+
srcFiles: [],
|
|
539
|
+
testFileCount: 0,
|
|
540
|
+
binEntries: {},
|
|
541
|
+
exportSurface: {},
|
|
542
|
+
keywords: [],
|
|
543
|
+
hasLicense: false,
|
|
544
|
+
hasTsConfig: false,
|
|
545
|
+
fingerprint: ''
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
// Generate all Diátaxis sections
|
|
549
|
+
const tutorials = generateTutorials(packageEntry.name, evidence);
|
|
550
|
+
const howtos = generateHowTos(packageEntry.name, evidence);
|
|
551
|
+
const reference = generateReference(packageEntry.name, evidence);
|
|
552
|
+
const explanation = generateExplanation(packageEntry.name, evidence);
|
|
553
|
+
|
|
554
|
+
// Calculate overall confidence scores
|
|
555
|
+
const confidence = {
|
|
556
|
+
tutorials: confidenceScore(evidence, 'tutorials'),
|
|
557
|
+
howtos: confidenceScore(evidence, 'howtos'),
|
|
558
|
+
reference: confidenceScore(evidence, 'reference'),
|
|
559
|
+
explanation: confidenceScore(evidence, 'explanation')
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
// Create Diátaxis entry
|
|
563
|
+
const entry = createDiataxisEntry(packageEntry.name, packageEntry.version, {
|
|
564
|
+
readmeHeadings: evidence.readmeHeadings || [],
|
|
565
|
+
docsFiles: evidence.docsFiles || [],
|
|
566
|
+
examplesFiles: evidence.examplesFiles || [],
|
|
567
|
+
tutorials,
|
|
568
|
+
howtos,
|
|
569
|
+
reference,
|
|
570
|
+
explanation,
|
|
571
|
+
confidence
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
// Validate the entry
|
|
575
|
+
const validation = validateDiataxisEntry(entry);
|
|
576
|
+
if (!validation.valid) {
|
|
577
|
+
throw new Error(`Generated invalid DiataxisEntry: ${validation.errors.join(', ')}`);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Ensure minimum structure
|
|
581
|
+
const finalEntry = ensureMinimumDiataxis(entry);
|
|
582
|
+
|
|
583
|
+
return finalEntry;
|
|
584
|
+
}
|