@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/bin/report.mjs
ADDED
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @file report.mjs
|
|
4
|
+
* @description CLI tool for generating Diátaxis coverage and confidence reports
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
8
|
+
import { join, resolve } from 'node:path';
|
|
9
|
+
import { existsSync } from 'node:fs';
|
|
10
|
+
import { stableStringify } from '../src/stable-json.mjs';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {Object} ReportOptions
|
|
14
|
+
* @property {boolean} json - Output as JSON
|
|
15
|
+
* @property {boolean} csv - Output as CSV
|
|
16
|
+
* @property {number} top - Number of top packages to show
|
|
17
|
+
* @property {string|null} filter - Filter packages by keyword
|
|
18
|
+
* @property {string} sort - Sort field (confidence, tutorials, howtos, reference, explanation)
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {Object} PackageStats
|
|
23
|
+
* @property {string} packageName
|
|
24
|
+
* @property {number} tutorialsCount
|
|
25
|
+
* @property {number} howtosCount
|
|
26
|
+
* @property {boolean} hasReference
|
|
27
|
+
* @property {boolean} hasExplanation
|
|
28
|
+
* @property {number} avgConfidence
|
|
29
|
+
* @property {Object} confidence
|
|
30
|
+
* @property {string[]} missingEvidence
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Parse command-line arguments manually (no external dependencies)
|
|
35
|
+
* @param {string[]} args - Process arguments (from process.argv)
|
|
36
|
+
* @returns {ReportOptions} Parsed options
|
|
37
|
+
*/
|
|
38
|
+
function parseArgs(args) {
|
|
39
|
+
const options = {
|
|
40
|
+
json: false,
|
|
41
|
+
csv: false,
|
|
42
|
+
top: 5,
|
|
43
|
+
filter: null,
|
|
44
|
+
sort: 'confidence'
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
for (let i = 0; i < args.length; i++) {
|
|
48
|
+
const arg = args[i];
|
|
49
|
+
|
|
50
|
+
if (arg === '--json') {
|
|
51
|
+
options.json = true;
|
|
52
|
+
} else if (arg === '--csv') {
|
|
53
|
+
options.csv = true;
|
|
54
|
+
} else if (arg === '--top' && i + 1 < args.length) {
|
|
55
|
+
options.top = parseInt(args[i + 1], 10);
|
|
56
|
+
i++;
|
|
57
|
+
} else if (arg === '--filter' && i + 1 < args.length) {
|
|
58
|
+
options.filter = args[i + 1];
|
|
59
|
+
i++;
|
|
60
|
+
} else if (arg === '--sort' && i + 1 < args.length) {
|
|
61
|
+
options.sort = args[i + 1];
|
|
62
|
+
i++;
|
|
63
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
64
|
+
console.log(`
|
|
65
|
+
Diátaxis Coverage Report Generator
|
|
66
|
+
|
|
67
|
+
Usage: diataxis-report [options]
|
|
68
|
+
|
|
69
|
+
Options:
|
|
70
|
+
--json Output as JSON structure
|
|
71
|
+
--csv Output as CSV (for spreadsheet import)
|
|
72
|
+
--top N Show top N packages instead of 5 (default: 5)
|
|
73
|
+
--filter <keyword> Only report on packages matching keyword
|
|
74
|
+
--sort <field> Sort by: confidence, tutorials, howtos, reference, explanation
|
|
75
|
+
--help, -h Show this help message
|
|
76
|
+
|
|
77
|
+
Examples:
|
|
78
|
+
diataxis-report
|
|
79
|
+
diataxis-report --json
|
|
80
|
+
diataxis-report --top 10 --sort tutorials
|
|
81
|
+
diataxis-report --filter "@unrdf/oxigraph" --json
|
|
82
|
+
`);
|
|
83
|
+
process.exit(0);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return options;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Load all inventory JSON files from ARTIFACTS/diataxis/
|
|
92
|
+
* @param {string} artifactsPath - Path to artifacts directory
|
|
93
|
+
* @returns {Promise<Object[]>} Array of parsed inventory entries
|
|
94
|
+
*/
|
|
95
|
+
async function loadInventories(artifactsPath) {
|
|
96
|
+
const diataxisDir = join(artifactsPath, 'diataxis');
|
|
97
|
+
|
|
98
|
+
if (!existsSync(diataxisDir)) {
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
// Recursively find all diataxis.json files
|
|
104
|
+
const inventories = [];
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Recursively search for diataxis.json files
|
|
108
|
+
*/
|
|
109
|
+
async function findDiataxisFiles(dir) {
|
|
110
|
+
try {
|
|
111
|
+
const files = await readdir(dir);
|
|
112
|
+
for (const file of files) {
|
|
113
|
+
const filePath = join(dir, file);
|
|
114
|
+
if (file === 'diataxis.json') {
|
|
115
|
+
try {
|
|
116
|
+
const content = await readFile(filePath, 'utf-8');
|
|
117
|
+
const data = JSON.parse(content);
|
|
118
|
+
inventories.push(data);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.warn(`Warning: Failed to parse ${filePath}: ${error.message}`);
|
|
121
|
+
}
|
|
122
|
+
} else {
|
|
123
|
+
try {
|
|
124
|
+
const statInfo = await stat(filePath);
|
|
125
|
+
if (statInfo.isDirectory()) {
|
|
126
|
+
await findDiataxisFiles(filePath);
|
|
127
|
+
}
|
|
128
|
+
} catch (error) {
|
|
129
|
+
// Skip
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
} catch (error) {
|
|
134
|
+
// Silently skip directories
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
await findDiataxisFiles(diataxisDir);
|
|
139
|
+
return inventories;
|
|
140
|
+
} catch (error) {
|
|
141
|
+
console.warn(`Warning: Failed to read diataxis directory: ${error.message}`);
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Calculate statistics for a single package
|
|
148
|
+
* @param {Object} inventory - DiataxisEntry inventory
|
|
149
|
+
* @returns {PackageStats} Package statistics
|
|
150
|
+
*/
|
|
151
|
+
function calculatePackageStats(inventory) {
|
|
152
|
+
const tutorialsCount = inventory.tutorials?.length || 0;
|
|
153
|
+
const howtosCount = inventory.howtos?.length || 0;
|
|
154
|
+
const hasReference = (inventory.reference?.items?.length || 0) > 0;
|
|
155
|
+
const hasExplanation = (
|
|
156
|
+
(inventory.explanation?.concepts?.length || 0) > 0 ||
|
|
157
|
+
(inventory.explanation?.architecture || '').length > 0 ||
|
|
158
|
+
(inventory.explanation?.tradeoffs?.length || 0) > 0
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
const confidence = inventory.confidence || {
|
|
162
|
+
tutorials: 0,
|
|
163
|
+
howtos: 0,
|
|
164
|
+
reference: 0,
|
|
165
|
+
explanation: 0
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const avgConfidence = (
|
|
169
|
+
confidence.tutorials +
|
|
170
|
+
confidence.howtos +
|
|
171
|
+
confidence.reference +
|
|
172
|
+
confidence.explanation
|
|
173
|
+
) / 4;
|
|
174
|
+
|
|
175
|
+
// Determine missing evidence
|
|
176
|
+
const missingEvidence = [];
|
|
177
|
+
const evidence = inventory.evidence || {};
|
|
178
|
+
|
|
179
|
+
if (!evidence.examplesFiles || evidence.examplesFiles.length === 0) {
|
|
180
|
+
missingEvidence.push('no examples/');
|
|
181
|
+
}
|
|
182
|
+
if (!evidence.docsFiles || evidence.docsFiles.length === 0) {
|
|
183
|
+
missingEvidence.push('no docs/');
|
|
184
|
+
}
|
|
185
|
+
if (!evidence.readmeHeadings || evidence.readmeHeadings.length === 0) {
|
|
186
|
+
missingEvidence.push('empty README');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Check for bin entries from reference
|
|
190
|
+
const hasBinEntries = inventory.reference?.items?.some(item => item.type === 'bin');
|
|
191
|
+
if (!hasBinEntries) {
|
|
192
|
+
missingEvidence.push('no bin entries');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
packageName: inventory.packageName,
|
|
197
|
+
tutorialsCount,
|
|
198
|
+
howtosCount,
|
|
199
|
+
hasReference,
|
|
200
|
+
hasExplanation,
|
|
201
|
+
avgConfidence,
|
|
202
|
+
confidence,
|
|
203
|
+
missingEvidence
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Generate summary statistics from all packages
|
|
209
|
+
* @param {PackageStats[]} stats - Array of package statistics
|
|
210
|
+
* @returns {Object} Summary statistics
|
|
211
|
+
*/
|
|
212
|
+
function generateSummary(stats) {
|
|
213
|
+
const total = stats.length;
|
|
214
|
+
const withTutorials = stats.filter(s => s.tutorialsCount > 0).length;
|
|
215
|
+
const with2PlusHowtos = stats.filter(s => s.howtosCount >= 2).length;
|
|
216
|
+
const withReference = stats.filter(s => s.hasReference).length;
|
|
217
|
+
const withExplanation = stats.filter(s => s.hasExplanation).length;
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
total,
|
|
221
|
+
withTutorials,
|
|
222
|
+
with2PlusHowtos,
|
|
223
|
+
withReference,
|
|
224
|
+
withExplanation,
|
|
225
|
+
withTutorialsPercent: total > 0 ? (withTutorials / total * 100) : 0,
|
|
226
|
+
with2PlusHowtosPercent: total > 0 ? (with2PlusHowtos / total * 100) : 0,
|
|
227
|
+
withReferencePercent: total > 0 ? (withReference / total * 100) : 0,
|
|
228
|
+
withExplanationPercent: total > 0 ? (withExplanation / total * 100) : 0
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Calculate confidence distribution statistics
|
|
234
|
+
* @param {PackageStats[]} stats - Array of package statistics
|
|
235
|
+
* @returns {Object} Confidence statistics
|
|
236
|
+
*/
|
|
237
|
+
function calculateConfidenceStats(stats) {
|
|
238
|
+
const fields = ['tutorials', 'howtos', 'reference', 'explanation'];
|
|
239
|
+
const result = {};
|
|
240
|
+
|
|
241
|
+
for (const field of fields) {
|
|
242
|
+
const values = stats.map(s => s.confidence[field]).filter(v => v !== undefined);
|
|
243
|
+
if (values.length === 0) {
|
|
244
|
+
result[field] = { avg: 0, min: 0, max: 0 };
|
|
245
|
+
} else {
|
|
246
|
+
const avg = values.reduce((a, b) => a + b, 0) / values.length;
|
|
247
|
+
const min = Math.min(...values);
|
|
248
|
+
const max = Math.max(...values);
|
|
249
|
+
result[field] = { avg, min, max };
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return result;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Find packages with lowest average confidence
|
|
258
|
+
* @param {PackageStats[]} stats - Array of package statistics
|
|
259
|
+
* @param {number} topN - Number of packages to return
|
|
260
|
+
* @returns {PackageStats[]} Top N lowest confidence packages
|
|
261
|
+
*/
|
|
262
|
+
function findLowestConfidence(stats, topN) {
|
|
263
|
+
return [...stats]
|
|
264
|
+
.sort((a, b) => a.avgConfidence - b.avgConfidence)
|
|
265
|
+
.slice(0, topN);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Aggregate missing evidence sources
|
|
270
|
+
* @param {PackageStats[]} stats - Array of package statistics
|
|
271
|
+
* @returns {Object} Map of evidence type to count
|
|
272
|
+
*/
|
|
273
|
+
function aggregateMissingEvidence(stats) {
|
|
274
|
+
const evidenceCounts = {};
|
|
275
|
+
|
|
276
|
+
for (const pkg of stats) {
|
|
277
|
+
for (const evidence of pkg.missingEvidence) {
|
|
278
|
+
evidenceCounts[evidence] = (evidenceCounts[evidence] || 0) + 1;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Sort by count descending
|
|
283
|
+
const sorted = Object.entries(evidenceCounts)
|
|
284
|
+
.sort(([, a], [, b]) => b - a)
|
|
285
|
+
.reduce((acc, [key, val]) => {
|
|
286
|
+
acc[key] = val;
|
|
287
|
+
return acc;
|
|
288
|
+
}, {});
|
|
289
|
+
|
|
290
|
+
return sorted;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Generate text report
|
|
295
|
+
* @param {PackageStats[]} stats - Package statistics
|
|
296
|
+
* @param {ReportOptions} options - Report options
|
|
297
|
+
*/
|
|
298
|
+
function generateTextReport(stats, options) {
|
|
299
|
+
const summary = generateSummary(stats);
|
|
300
|
+
const confidenceStats = calculateConfidenceStats(stats);
|
|
301
|
+
const lowestConfidence = findLowestConfidence(stats, options.top);
|
|
302
|
+
const missingEvidence = aggregateMissingEvidence(stats);
|
|
303
|
+
|
|
304
|
+
console.log('Diátaxis Coverage Report');
|
|
305
|
+
console.log('========================\n');
|
|
306
|
+
|
|
307
|
+
// Summary section
|
|
308
|
+
console.log('SUMMARY');
|
|
309
|
+
console.log('-------');
|
|
310
|
+
console.log(`Total packages: ${summary.total}`);
|
|
311
|
+
console.log(`With tutorials: ${summary.withTutorials} (${summary.withTutorialsPercent.toFixed(0)}%)`);
|
|
312
|
+
console.log(`With 2+ how-tos: ${summary.with2PlusHowtos} (${summary.with2PlusHowtosPercent.toFixed(0)}%)`);
|
|
313
|
+
console.log(`With reference: ${summary.withReference} (${summary.withReferencePercent.toFixed(0)}%)`);
|
|
314
|
+
console.log(`With explanation: ${summary.withExplanation} (${summary.withExplanationPercent.toFixed(0)}%)\n`);
|
|
315
|
+
|
|
316
|
+
// Confidence section
|
|
317
|
+
console.log('CONFIDENCE');
|
|
318
|
+
console.log('----------');
|
|
319
|
+
for (const [field, values] of Object.entries(confidenceStats)) {
|
|
320
|
+
const fieldName = field.charAt(0).toUpperCase() + field.slice(1);
|
|
321
|
+
console.log(`${fieldName.padEnd(12)}: avg=${values.avg.toFixed(2)}, min=${values.min.toFixed(2)}, max=${values.max.toFixed(2)}`);
|
|
322
|
+
}
|
|
323
|
+
console.log();
|
|
324
|
+
|
|
325
|
+
// Lowest confidence section
|
|
326
|
+
console.log(`LOWEST CONFIDENCE (${options.top} packages)`);
|
|
327
|
+
console.log('----------------------------');
|
|
328
|
+
if (lowestConfidence.length === 0) {
|
|
329
|
+
console.log('No packages found\n');
|
|
330
|
+
} else {
|
|
331
|
+
lowestConfidence.forEach((pkg, i) => {
|
|
332
|
+
const reasons = pkg.missingEvidence.length > 0 ? ` - ${pkg.missingEvidence.join(', ')}` : '';
|
|
333
|
+
console.log(`${(i + 1).toString().padStart(2)}. ${pkg.packageName.padEnd(30)} (${pkg.avgConfidence.toFixed(2)})${reasons}`);
|
|
334
|
+
});
|
|
335
|
+
console.log();
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Missing evidence section
|
|
339
|
+
console.log('MISSING EVIDENCE');
|
|
340
|
+
console.log('----------------');
|
|
341
|
+
if (Object.keys(missingEvidence).length === 0) {
|
|
342
|
+
console.log('All evidence sources present\n');
|
|
343
|
+
} else {
|
|
344
|
+
for (const [evidence, count] of Object.entries(missingEvidence)) {
|
|
345
|
+
const evidenceLabel = evidence.padEnd(25);
|
|
346
|
+
console.log(`${evidenceLabel}: ${count} packages`);
|
|
347
|
+
}
|
|
348
|
+
console.log();
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Quick fixes section
|
|
352
|
+
console.log('QUICK FIXES');
|
|
353
|
+
console.log('-----------');
|
|
354
|
+
if (Object.keys(missingEvidence).length === 0) {
|
|
355
|
+
console.log('No immediate improvements needed\n');
|
|
356
|
+
} else {
|
|
357
|
+
console.log('To improve coverage, consider:');
|
|
358
|
+
for (const [evidence, count] of Object.entries(missingEvidence)) {
|
|
359
|
+
const action = evidence.replace('no ', 'Add ').replace('empty ', 'Create ');
|
|
360
|
+
console.log(`- ${action} to ${count} packages`);
|
|
361
|
+
}
|
|
362
|
+
console.log();
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
console.log(`EXIT: 0 (all packages documented)`);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Generate JSON report
|
|
370
|
+
* @param {PackageStats[]} stats - Package statistics
|
|
371
|
+
* @param {ReportOptions} options - Report options
|
|
372
|
+
*/
|
|
373
|
+
function generateJsonReport(stats, options) {
|
|
374
|
+
const summary = generateSummary(stats);
|
|
375
|
+
const confidenceStats = calculateConfidenceStats(stats);
|
|
376
|
+
const lowestConfidence = findLowestConfidence(stats, options.top);
|
|
377
|
+
const missingEvidence = aggregateMissingEvidence(stats);
|
|
378
|
+
|
|
379
|
+
const report = {
|
|
380
|
+
summary: {
|
|
381
|
+
total: summary.total,
|
|
382
|
+
withTutorials: summary.withTutorials,
|
|
383
|
+
withTutorialsPercent: parseFloat(summary.withTutorialsPercent.toFixed(2)),
|
|
384
|
+
with2PlusHowtos: summary.with2PlusHowtos,
|
|
385
|
+
with2PlusHowtosPercent: parseFloat(summary.with2PlusHowtosPercent.toFixed(2)),
|
|
386
|
+
withReference: summary.withReference,
|
|
387
|
+
withReferencePercent: parseFloat(summary.withReferencePercent.toFixed(2)),
|
|
388
|
+
withExplanation: summary.withExplanation,
|
|
389
|
+
withExplanationPercent: parseFloat(summary.withExplanationPercent.toFixed(2))
|
|
390
|
+
},
|
|
391
|
+
confidence: confidenceStats,
|
|
392
|
+
lowestConfidence: lowestConfidence.map(pkg => ({
|
|
393
|
+
packageName: pkg.packageName,
|
|
394
|
+
avgConfidence: parseFloat(pkg.avgConfidence.toFixed(2)),
|
|
395
|
+
missingEvidence: pkg.missingEvidence,
|
|
396
|
+
confidence: {
|
|
397
|
+
tutorials: parseFloat(pkg.confidence.tutorials.toFixed(2)),
|
|
398
|
+
howtos: parseFloat(pkg.confidence.howtos.toFixed(2)),
|
|
399
|
+
reference: parseFloat(pkg.confidence.reference.toFixed(2)),
|
|
400
|
+
explanation: parseFloat(pkg.confidence.explanation.toFixed(2))
|
|
401
|
+
}
|
|
402
|
+
})),
|
|
403
|
+
missingEvidence,
|
|
404
|
+
generatedAt: new Date().toISOString()
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
console.log(stableStringify(report, { indent: 2 }));
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Generate CSV report
|
|
412
|
+
* @param {PackageStats[]} stats - Package statistics
|
|
413
|
+
*/
|
|
414
|
+
function generateCsvReport(stats) {
|
|
415
|
+
// Header
|
|
416
|
+
console.log('Package,Tutorials,HowTos,HasReference,HasExplanation,AvgConfidence,TutorialsConf,HowtosConf,ReferenceConf,ExplanationConf,MissingEvidence');
|
|
417
|
+
|
|
418
|
+
// Rows
|
|
419
|
+
for (const pkg of stats) {
|
|
420
|
+
const row = [
|
|
421
|
+
pkg.packageName,
|
|
422
|
+
pkg.tutorialsCount,
|
|
423
|
+
pkg.howtosCount,
|
|
424
|
+
pkg.hasReference ? 'yes' : 'no',
|
|
425
|
+
pkg.hasExplanation ? 'yes' : 'no',
|
|
426
|
+
pkg.avgConfidence.toFixed(2),
|
|
427
|
+
pkg.confidence.tutorials.toFixed(2),
|
|
428
|
+
pkg.confidence.howtos.toFixed(2),
|
|
429
|
+
pkg.confidence.reference.toFixed(2),
|
|
430
|
+
pkg.confidence.explanation.toFixed(2),
|
|
431
|
+
`"${pkg.missingEvidence.join('; ')}"`
|
|
432
|
+
];
|
|
433
|
+
console.log(row.join(','));
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Filter packages by keyword
|
|
439
|
+
* @param {PackageStats[]} stats - Package statistics
|
|
440
|
+
* @param {string} keyword - Filter keyword
|
|
441
|
+
* @returns {PackageStats[]} Filtered statistics
|
|
442
|
+
*/
|
|
443
|
+
function filterPackages(stats, keyword) {
|
|
444
|
+
if (!keyword) return stats;
|
|
445
|
+
return stats.filter(pkg => pkg.packageName.includes(keyword));
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Sort packages by field
|
|
450
|
+
* @param {PackageStats[]} stats - Package statistics
|
|
451
|
+
* @param {string} field - Sort field
|
|
452
|
+
* @returns {PackageStats[]} Sorted statistics
|
|
453
|
+
*/
|
|
454
|
+
function sortPackages(stats, field) {
|
|
455
|
+
const sorted = [...stats];
|
|
456
|
+
|
|
457
|
+
switch (field) {
|
|
458
|
+
case 'confidence':
|
|
459
|
+
sorted.sort((a, b) => b.avgConfidence - a.avgConfidence);
|
|
460
|
+
break;
|
|
461
|
+
case 'tutorials':
|
|
462
|
+
sorted.sort((a, b) => b.tutorialsCount - a.tutorialsCount);
|
|
463
|
+
break;
|
|
464
|
+
case 'howtos':
|
|
465
|
+
sorted.sort((a, b) => b.howtosCount - a.howtosCount);
|
|
466
|
+
break;
|
|
467
|
+
case 'reference':
|
|
468
|
+
sorted.sort((a, b) => (b.hasReference ? 1 : 0) - (a.hasReference ? 1 : 0));
|
|
469
|
+
break;
|
|
470
|
+
case 'explanation':
|
|
471
|
+
sorted.sort((a, b) => (b.hasExplanation ? 1 : 0) - (a.hasExplanation ? 1 : 0));
|
|
472
|
+
break;
|
|
473
|
+
default:
|
|
474
|
+
// Keep original order (sorted by package name from loading)
|
|
475
|
+
break;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return sorted;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Main entry point
|
|
483
|
+
*/
|
|
484
|
+
async function main() {
|
|
485
|
+
const args = process.argv.slice(2);
|
|
486
|
+
const options = parseArgs(args);
|
|
487
|
+
|
|
488
|
+
// Determine artifacts path (default: ARTIFACTS/ in workspace root)
|
|
489
|
+
const workspaceRoot = resolve(process.cwd());
|
|
490
|
+
const artifactsPath = join(workspaceRoot, 'ARTIFACTS');
|
|
491
|
+
|
|
492
|
+
// Load inventories
|
|
493
|
+
const inventories = await loadInventories(artifactsPath);
|
|
494
|
+
|
|
495
|
+
if (inventories.length === 0) {
|
|
496
|
+
console.log('No inventory generated. Run diataxis-run first.');
|
|
497
|
+
process.exit(0);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Calculate statistics
|
|
501
|
+
let stats = inventories.map(calculatePackageStats);
|
|
502
|
+
|
|
503
|
+
// Apply filter
|
|
504
|
+
stats = filterPackages(stats, options.filter);
|
|
505
|
+
|
|
506
|
+
if (stats.length === 0) {
|
|
507
|
+
console.log('No packages found matching filter.');
|
|
508
|
+
process.exit(0);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Apply sort
|
|
512
|
+
stats = sortPackages(stats, options.sort);
|
|
513
|
+
|
|
514
|
+
// Generate report
|
|
515
|
+
if (options.json) {
|
|
516
|
+
generateJsonReport(stats, options);
|
|
517
|
+
} else if (options.csv) {
|
|
518
|
+
generateCsvReport(stats);
|
|
519
|
+
} else {
|
|
520
|
+
generateTextReport(stats, options);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
process.exit(0);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
main().catch(error => {
|
|
527
|
+
console.error(`Error: ${error.message}`);
|
|
528
|
+
process.exit(1);
|
|
529
|
+
});
|
package/bin/run.mjs
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @file Diátaxis Kit orchestrator - full pipeline execution
|
|
4
|
+
* @description Discovers packages, gathers evidence, classifies, and scaffolds documentation
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { mkdir, writeFile, readFile, rm } from 'node:fs/promises';
|
|
8
|
+
import { join, resolve } from 'node:path';
|
|
9
|
+
import { existsSync } from 'node:fs';
|
|
10
|
+
import { discoverPackages } from '../src/inventory.mjs';
|
|
11
|
+
import { collectEvidence } from '../src/evidence.mjs';
|
|
12
|
+
import { classifyPackage } from '../src/classify.mjs';
|
|
13
|
+
import { generateScaffold } from '../src/scaffold.mjs';
|
|
14
|
+
import { stableStringify } from '../src/stable-json.mjs';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Main orchestration function
|
|
18
|
+
* @returns {Promise<void>}
|
|
19
|
+
*/
|
|
20
|
+
async function main() {
|
|
21
|
+
const isDeterministic = process.env.DETERMINISTIC === '1';
|
|
22
|
+
const workspaceRoot = resolve(process.cwd(), '../..');
|
|
23
|
+
const artifactsDir = resolve(process.cwd(), 'ARTIFACTS', 'diataxis');
|
|
24
|
+
const outDir = resolve(process.cwd(), 'OUT');
|
|
25
|
+
|
|
26
|
+
console.log('🚀 Diátaxis Kit - Full Pipeline');
|
|
27
|
+
console.log(` Workspace: ${workspaceRoot}`);
|
|
28
|
+
console.log(` Deterministic: ${isDeterministic ? 'YES' : 'NO'}`);
|
|
29
|
+
console.log('');
|
|
30
|
+
|
|
31
|
+
// Step 1: Discover packages
|
|
32
|
+
console.log('📦 Discovering packages...');
|
|
33
|
+
const packages = await discoverPackages(workspaceRoot);
|
|
34
|
+
console.log(` Found ${packages.length} packages`);
|
|
35
|
+
|
|
36
|
+
// Step 2: Create ARTIFACTS directory structure
|
|
37
|
+
await mkdir(artifactsDir, { recursive: true });
|
|
38
|
+
await mkdir(outDir, { recursive: true });
|
|
39
|
+
|
|
40
|
+
// Step 3: Gather evidence and classify each package
|
|
41
|
+
console.log('');
|
|
42
|
+
console.log('🔍 Gathering evidence and classifying...');
|
|
43
|
+
|
|
44
|
+
const diataxisEntries = [];
|
|
45
|
+
for (const pkg of packages) {
|
|
46
|
+
// Read package.json for evidence collection
|
|
47
|
+
const pkgJsonPath = join(pkg.dir, 'package.json');
|
|
48
|
+
const pkgJsonContent = await readFile(pkgJsonPath, 'utf8');
|
|
49
|
+
const pkgJson = JSON.parse(pkgJsonContent);
|
|
50
|
+
|
|
51
|
+
const evidence = await collectEvidence(pkg.dir, pkgJson);
|
|
52
|
+
const diataxisEntry = await classifyPackage(pkg, evidence);
|
|
53
|
+
|
|
54
|
+
// Override timestamp if deterministic mode
|
|
55
|
+
if (isDeterministic) {
|
|
56
|
+
diataxisEntry.generatedAt = '2000-01-01T00:00:00.000Z';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
diataxisEntries.push(diataxisEntry);
|
|
60
|
+
|
|
61
|
+
// Save per-package diataxis.json
|
|
62
|
+
const packageDir = join(artifactsDir, pkg.name);
|
|
63
|
+
await mkdir(packageDir, { recursive: true });
|
|
64
|
+
await writeFile(
|
|
65
|
+
join(packageDir, 'diataxis.json'),
|
|
66
|
+
stableStringify(diataxisEntry)
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
console.log(` ✓ ${pkg.name}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Step 4: Save inventory.json
|
|
73
|
+
const inventory = {
|
|
74
|
+
generatedAt: isDeterministic ? '2000-01-01T00:00:00.000Z' : new Date().toISOString(),
|
|
75
|
+
packageCount: packages.length,
|
|
76
|
+
packages: packages.map(pkg => ({
|
|
77
|
+
name: pkg.name,
|
|
78
|
+
version: pkg.version,
|
|
79
|
+
dir: pkg.dir
|
|
80
|
+
}))
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
await writeFile(
|
|
84
|
+
join(artifactsDir, 'inventory.json'),
|
|
85
|
+
stableStringify(inventory)
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
// Step 5: Generate scaffolds
|
|
89
|
+
console.log('');
|
|
90
|
+
console.log('📝 Generating scaffolds...');
|
|
91
|
+
|
|
92
|
+
for (const diataxisEntry of diataxisEntries) {
|
|
93
|
+
const packageOutDir = join(outDir, diataxisEntry.packageName);
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
await generateScaffold(diataxisEntry, packageOutDir);
|
|
97
|
+
console.log(` ✓ ${diataxisEntry.packageName}`);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.error(` ✗ ${diataxisEntry.packageName}: ${error.message}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
console.log('');
|
|
104
|
+
console.log('✅ Pipeline complete');
|
|
105
|
+
console.log(` Artifacts: ${artifactsDir}`);
|
|
106
|
+
console.log(` Output: ${outDir}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Execute main
|
|
110
|
+
main().catch(error => {
|
|
111
|
+
console.error('❌ Pipeline failed:', error.message);
|
|
112
|
+
console.error(error.stack);
|
|
113
|
+
process.exit(1);
|
|
114
|
+
});
|