@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.
Files changed (49) hide show
  1. package/README.md +425 -0
  2. package/bin/report.mjs +529 -0
  3. package/bin/run.mjs +114 -0
  4. package/bin/verify.mjs +356 -0
  5. package/capability-map.md +92 -0
  6. package/package.json +42 -0
  7. package/src/classify.mjs +584 -0
  8. package/src/diataxis-schema.mjs +425 -0
  9. package/src/evidence.mjs +268 -0
  10. package/src/hash.mjs +37 -0
  11. package/src/inventory.mjs +280 -0
  12. package/src/reference-extractor.mjs +324 -0
  13. package/src/scaffold.mjs +458 -0
  14. package/src/stable-json.mjs +113 -0
  15. package/src/verify-implementation.mjs +131 -0
  16. package/test/determinism.test.mjs +321 -0
  17. package/test/evidence.test.mjs +145 -0
  18. package/test/fixtures/scaffold-det1/explanation/explanation.md +35 -0
  19. package/test/fixtures/scaffold-det1/index.md +29 -0
  20. package/test/fixtures/scaffold-det1/reference/reference.md +34 -0
  21. package/test/fixtures/scaffold-det1/tutorials/tutorial-test-tutorial.md +37 -0
  22. package/test/fixtures/scaffold-det2/explanation/explanation.md +35 -0
  23. package/test/fixtures/scaffold-det2/index.md +29 -0
  24. package/test/fixtures/scaffold-det2/reference/reference.md +34 -0
  25. package/test/fixtures/scaffold-det2/tutorials/tutorial-test-tutorial.md +37 -0
  26. package/test/fixtures/scaffold-empty/explanation/explanation.md +35 -0
  27. package/test/fixtures/scaffold-empty/index.md +25 -0
  28. package/test/fixtures/scaffold-empty/reference/reference.md +34 -0
  29. package/test/fixtures/scaffold-escape/explanation/explanation.md +35 -0
  30. package/test/fixtures/scaffold-escape/index.md +29 -0
  31. package/test/fixtures/scaffold-escape/reference/reference.md +36 -0
  32. package/test/fixtures/scaffold-output/explanation/explanation.md +39 -0
  33. package/test/fixtures/scaffold-output/how-to/howto-configure-options.md +39 -0
  34. package/test/fixtures/scaffold-output/index.md +41 -0
  35. package/test/fixtures/scaffold-output/reference/reference.md +36 -0
  36. package/test/fixtures/scaffold-output/tutorials/tutorial-getting-started.md +41 -0
  37. package/test/fixtures/test-artifacts/ARTIFACTS/diataxis/test-pkg-1.inventory.json +115 -0
  38. package/test/fixtures/test-artifacts/ARTIFACTS/diataxis/test-pkg-2.inventory.json +93 -0
  39. package/test/fixtures/test-artifacts/ARTIFACTS/diataxis/test-pkg-3.inventory.json +97 -0
  40. package/test/fixtures/test-package/LICENSE +1 -0
  41. package/test/fixtures/test-package/README.md +15 -0
  42. package/test/fixtures/test-package/docs/guide.md +3 -0
  43. package/test/fixtures/test-package/examples/basic.mjs +3 -0
  44. package/test/fixtures/test-package/src/index.mjs +3 -0
  45. package/test/inventory.test.mjs +199 -0
  46. package/test/reference-extractor.test.mjs +187 -0
  47. package/test/report.test.mjs +503 -0
  48. package/test/scaffold.test.mjs +242 -0
  49. package/test/verify-gate.test.mjs +634 -0
package/src/hash.mjs ADDED
@@ -0,0 +1,37 @@
1
+ /**
2
+ * @file Hashing utilities using stable JSON stringification
3
+ * @module hash
4
+ */
5
+
6
+ import { createHash } from 'node:crypto';
7
+ import { readFile } from 'node:fs/promises';
8
+ import { stableStringify } from './stable-json.mjs';
9
+
10
+ /**
11
+ * Hash an object deterministically using SHA256
12
+ * @param {any} obj - Object to hash
13
+ * @returns {string} Hex-encoded SHA256 hash
14
+ */
15
+ export function hashObject(obj) {
16
+ const json = stableStringify(obj, { indent: 0 });
17
+ return hashString(json);
18
+ }
19
+
20
+ /**
21
+ * Hash a string using SHA256
22
+ * @param {string} str - String to hash
23
+ * @returns {string} Hex-encoded SHA256 hash
24
+ */
25
+ export function hashString(str) {
26
+ return createHash('sha256').update(str, 'utf8').digest('hex');
27
+ }
28
+
29
+ /**
30
+ * Hash a file's contents using SHA256
31
+ * @param {string} filePath - Path to file
32
+ * @returns {Promise<string>} Hex-encoded SHA256 hash
33
+ */
34
+ export async function hashFile(filePath) {
35
+ const content = await readFile(filePath, 'utf8');
36
+ return hashString(content);
37
+ }
@@ -0,0 +1,280 @@
1
+ import { readdir, readFile, stat } from 'node:fs/promises';
2
+ import { join, resolve, dirname } from 'node:path';
3
+ import { existsSync } from 'node:fs';
4
+
5
+ /**
6
+ * @typedef {Object} PackageEntry
7
+ * @property {string} name - Package name
8
+ * @property {string} dir - Absolute path to package directory
9
+ * @property {string} version - Package version
10
+ * @property {string} description - Package description
11
+ * @property {Record<string, string>} exports - Package exports map
12
+ * @property {Record<string, string> | string} bin - Binary executables
13
+ * @property {string[]} keywords - Package keywords
14
+ * @property {boolean} hasReadme - Whether README.md exists
15
+ * @property {boolean} hasDocs - Whether docs/ directory exists
16
+ * @property {boolean} hasExamples - Whether examples/ directory exists
17
+ * @property {boolean} hasTests - Whether test/ directory exists
18
+ * @property {Record<string, string>} scripts - Selected scripts (test, dev, build)
19
+ */
20
+
21
+ /**
22
+ * Discovers all workspace packages and returns structured metadata
23
+ * @param {string} workspaceRoot - Path to monorepo root
24
+ * @param {Object} [options={}] - Discovery options
25
+ * @param {string[]} [options.skipDirs=['node_modules', '.git', 'dist', 'build']] - Directories to skip
26
+ * @param {boolean} [options.includeNested=true] - Whether to discover nested workspaces
27
+ * @returns {Promise<PackageEntry[]>} Array of package entries, sorted by name
28
+ */
29
+ export async function discoverPackages(workspaceRoot, options = {}) {
30
+ const {
31
+ skipDirs = ['node_modules', '.git', 'dist', 'build'],
32
+ includeNested = true
33
+ } = options;
34
+
35
+ const rootPath = resolve(workspaceRoot);
36
+
37
+ // Detect workspace configuration
38
+ const workspacePatterns = await detectWorkspacePatterns(rootPath);
39
+
40
+ if (workspacePatterns.length === 0) {
41
+ console.warn('No workspace configuration found');
42
+ }
43
+
44
+ // Find all package.json files
45
+ const packagePaths = await findPackageJsonFiles(rootPath, skipDirs, includeNested);
46
+
47
+ // Process each package
48
+ const packages = [];
49
+ for (const pkgPath of packagePaths) {
50
+ try {
51
+ const entry = await processPackage(pkgPath);
52
+ if (entry) {
53
+ packages.push(entry);
54
+ }
55
+ } catch (error) {
56
+ console.warn(`Skipping package at ${pkgPath}: ${error.message}`);
57
+ }
58
+ }
59
+
60
+ // Sort by package name for stable ordering (deterministic)
61
+ packages.sort((a, b) => a.name.localeCompare(b.name));
62
+
63
+ return packages;
64
+ }
65
+
66
+ /**
67
+ * Validates a package entry structure
68
+ * @param {any} entry - Entry to validate
69
+ * @returns {{ valid: boolean, errors: string[] }} Validation result
70
+ */
71
+ export function validatePackageEntry(entry) {
72
+ const errors = [];
73
+
74
+ if (!entry || typeof entry !== 'object') {
75
+ return { valid: false, errors: ['Entry must be an object'] };
76
+ }
77
+
78
+ // Check required string fields
79
+ const requiredStrings = ['name', 'dir', 'version', 'description'];
80
+ for (const field of requiredStrings) {
81
+ if (typeof entry[field] !== 'string') {
82
+ errors.push(`Field '${field}' must be a string`);
83
+ }
84
+ }
85
+
86
+ // Check exports (must be object)
87
+ if (typeof entry.exports !== 'object' || entry.exports === null) {
88
+ errors.push('Field \'exports\' must be an object');
89
+ }
90
+
91
+ // Check bin (can be object or string)
92
+ if (entry.bin !== null && typeof entry.bin !== 'object' && typeof entry.bin !== 'string') {
93
+ errors.push('Field \'bin\' must be an object, string, or null');
94
+ }
95
+
96
+ // Check keywords array
97
+ if (!Array.isArray(entry.keywords)) {
98
+ errors.push('Field \'keywords\' must be an array');
99
+ }
100
+
101
+ // Check boolean fields
102
+ const booleanFields = ['hasReadme', 'hasDocs', 'hasExamples', 'hasTests'];
103
+ for (const field of booleanFields) {
104
+ if (typeof entry[field] !== 'boolean') {
105
+ errors.push(`Field '${field}' must be a boolean`);
106
+ }
107
+ }
108
+
109
+ // Check scripts (must be object)
110
+ if (typeof entry.scripts !== 'object' || entry.scripts === null) {
111
+ errors.push('Field \'scripts\' must be an object');
112
+ }
113
+
114
+ return {
115
+ valid: errors.length === 0,
116
+ errors
117
+ };
118
+ }
119
+
120
+ /**
121
+ * Detects workspace patterns from pnpm-workspace.yaml and package.json
122
+ * @param {string} rootPath - Root directory path
123
+ * @returns {Promise<string[]>} Array of workspace patterns
124
+ */
125
+ async function detectWorkspacePatterns(rootPath) {
126
+ const patterns = [];
127
+
128
+ // Check pnpm-workspace.yaml
129
+ const pnpmWorkspacePath = join(rootPath, 'pnpm-workspace.yaml');
130
+ try {
131
+ if (existsSync(pnpmWorkspacePath)) {
132
+ const content = await readFile(pnpmWorkspacePath, 'utf-8');
133
+ // Simple YAML parsing for packages array
134
+ const lines = content.split('\n');
135
+ let inPackages = false;
136
+ for (const line of lines) {
137
+ if (line.trim().startsWith('packages:')) {
138
+ inPackages = true;
139
+ continue;
140
+ }
141
+ if (inPackages && line.trim().startsWith('-')) {
142
+ const pattern = line.trim().substring(1).trim().replace(/['"]/g, '');
143
+ patterns.push(pattern);
144
+ } else if (inPackages && line.trim() && !line.trim().startsWith('#')) {
145
+ // End of packages section (non-empty, non-comment line)
146
+ break;
147
+ }
148
+ }
149
+ }
150
+ } catch (error) {
151
+ // Gracefully skip if unable to read
152
+ }
153
+
154
+ // Check package.json workspaces
155
+ const pkgJsonPath = join(rootPath, 'package.json');
156
+ try {
157
+ if (existsSync(pkgJsonPath)) {
158
+ const content = await readFile(pkgJsonPath, 'utf-8');
159
+ const pkg = JSON.parse(content);
160
+ if (pkg.workspaces) {
161
+ if (Array.isArray(pkg.workspaces)) {
162
+ patterns.push(...pkg.workspaces);
163
+ } else if (pkg.workspaces.packages && Array.isArray(pkg.workspaces.packages)) {
164
+ patterns.push(...pkg.workspaces.packages);
165
+ }
166
+ }
167
+ }
168
+ } catch (error) {
169
+ // Gracefully skip if unable to read or parse
170
+ }
171
+
172
+ return patterns;
173
+ }
174
+
175
+ /**
176
+ * Recursively finds all package.json files
177
+ * @param {string} dir - Directory to search
178
+ * @param {string[]} skipDirs - Directories to skip
179
+ * @param {boolean} includeNested - Whether to include nested workspaces
180
+ * @returns {Promise<string[]>} Array of package.json file paths
181
+ */
182
+ async function findPackageJsonFiles(dir, skipDirs, includeNested) {
183
+ const results = [];
184
+
185
+ try {
186
+ const entries = await readdir(dir, { withFileTypes: true });
187
+
188
+ for (const entry of entries) {
189
+ const fullPath = join(dir, entry.name);
190
+
191
+ // Skip directories in skipDirs list
192
+ if (skipDirs.includes(entry.name)) {
193
+ continue;
194
+ }
195
+
196
+ if (entry.isDirectory()) {
197
+ // Recursively search subdirectories
198
+ const subResults = await findPackageJsonFiles(fullPath, skipDirs, includeNested);
199
+ results.push(...subResults);
200
+ } else if (entry.name === 'package.json') {
201
+ // Found a package.json file
202
+ results.push(fullPath);
203
+ }
204
+ }
205
+ } catch (error) {
206
+ // Gracefully ignore errors (e.g., permission denied)
207
+ }
208
+
209
+ return results;
210
+ }
211
+
212
+ /**
213
+ * Processes a single package.json file and extracts metadata
214
+ * @param {string} pkgJsonPath - Path to package.json
215
+ * @returns {Promise<PackageEntry | null>} Package entry or null if invalid
216
+ */
217
+ async function processPackage(pkgJsonPath) {
218
+ try {
219
+ const content = await readFile(pkgJsonPath, 'utf-8');
220
+ const pkg = JSON.parse(content);
221
+
222
+ if (!pkg.name) {
223
+ // Skip packages without a name
224
+ return null;
225
+ }
226
+
227
+ const packageDir = dirname(pkgJsonPath);
228
+
229
+ // Extract selected scripts (test, dev, build only)
230
+ const scripts = {};
231
+ if (pkg.scripts) {
232
+ for (const scriptName of ['test', 'dev', 'build']) {
233
+ if (pkg.scripts[scriptName]) {
234
+ scripts[scriptName] = pkg.scripts[scriptName];
235
+ }
236
+ }
237
+ }
238
+
239
+ // Check for various files and directories
240
+ const hasReadme = existsSync(join(packageDir, 'README.md'));
241
+ const hasDocs = await checkDirectory(join(packageDir, 'docs'));
242
+ const hasExamples = await checkDirectory(join(packageDir, 'examples'));
243
+
244
+ // Check multiple common test directory names
245
+ const hasTests = await checkDirectory(join(packageDir, 'test')) ||
246
+ await checkDirectory(join(packageDir, 'tests')) ||
247
+ await checkDirectory(join(packageDir, '__tests__'));
248
+
249
+ return {
250
+ name: pkg.name,
251
+ dir: packageDir,
252
+ version: pkg.version || '0.0.0',
253
+ description: pkg.description || '',
254
+ exports: pkg.exports || {},
255
+ bin: pkg.bin || {},
256
+ keywords: pkg.keywords || [],
257
+ hasReadme,
258
+ hasDocs,
259
+ hasExamples,
260
+ hasTests,
261
+ scripts
262
+ };
263
+ } catch (error) {
264
+ throw new Error(`Failed to process package: ${error.message}`);
265
+ }
266
+ }
267
+
268
+ /**
269
+ * Checks if a directory exists
270
+ * @param {string} dirPath - Path to check
271
+ * @returns {Promise<boolean>} True if directory exists
272
+ */
273
+ async function checkDirectory(dirPath) {
274
+ try {
275
+ const stats = await stat(dirPath);
276
+ return stats.isDirectory();
277
+ } catch (error) {
278
+ return false;
279
+ }
280
+ }
@@ -0,0 +1,324 @@
1
+ /**
2
+ * @fileoverview Reference Extractor - Extract API surface from package metadata and README
3
+ * @module reference-extractor
4
+ *
5
+ * This module extracts API reference information from:
6
+ * - package.json exports field
7
+ * - package.json bin field
8
+ * - README API sections
9
+ *
10
+ * Used by classify.mjs to generate reference documentation structure.
11
+ */
12
+
13
+ /**
14
+ * @typedef {Object} ReferenceItem
15
+ * @property {string} name - The API item name (export path, bin name, etc)
16
+ * @property {string} type - Item type: "export", "bin", "api", "unknown"
17
+ * @property {string} description - Description of the item
18
+ * @property {string|null} example - Optional usage example
19
+ */
20
+
21
+ /**
22
+ * @typedef {Object} Reference
23
+ * @property {string} id - Always "reference"
24
+ * @property {string} title - Reference title (e.g., "[PackageName] API Reference")
25
+ * @property {ReferenceItem[]} items - List of reference items
26
+ * @property {number} confidenceScore - Confidence score 0-1
27
+ * @property {string[]} source - Sources that contributed data
28
+ */
29
+
30
+ /**
31
+ * Extract API reference from package metadata and README
32
+ *
33
+ * @param {Object} packageEntry - Package.json data
34
+ * @param {Object} evidenceSnapshot - Contains README and other evidence
35
+ * @param {string} [evidenceSnapshot.readme] - README content
36
+ * @returns {Reference} Reference object with API items
37
+ *
38
+ * @example
39
+ * const ref = extractReference(pkg, { readme: '# API\n...' });
40
+ * // => { id: 'reference', title: 'pkg-name API Reference', items: [...], ... }
41
+ */
42
+ export function extractReference(packageEntry, evidenceSnapshot) {
43
+ const items = [];
44
+ const sources = [];
45
+
46
+ // Extract from package.json exports field
47
+ const exportItems = extractFromExports(packageEntry);
48
+ if (exportItems.length > 0) {
49
+ items.push(...exportItems);
50
+ sources.push('exports');
51
+ }
52
+
53
+ // Extract from package.json bin field
54
+ const binItems = extractFromBin(packageEntry);
55
+ if (binItems.length > 0) {
56
+ items.push(...binItems);
57
+ sources.push('bin');
58
+ }
59
+
60
+ // Extract from README API sections
61
+ const readmeItems = extractFromReadme(evidenceSnapshot.readme || '');
62
+ if (readmeItems.length > 0) {
63
+ items.push(...readmeItems);
64
+ sources.push('readme');
65
+ }
66
+
67
+ // Fallback if nothing found
68
+ if (items.length === 0) {
69
+ items.push({
70
+ name: 'unknown',
71
+ type: 'unknown',
72
+ description: 'API reference not found in documentation',
73
+ example: null
74
+ });
75
+ sources.push('inferred');
76
+ }
77
+
78
+ // Sort items by name for deterministic output
79
+ items.sort((a, b) => a.name.localeCompare(b.name));
80
+
81
+ // Calculate confidence score
82
+ const confidenceScore = calculateConfidence(sources);
83
+
84
+ return {
85
+ id: 'reference',
86
+ title: `${packageEntry.name || 'Package'} API Reference`,
87
+ items,
88
+ confidenceScore,
89
+ source: sources
90
+ };
91
+ }
92
+
93
+ /**
94
+ * Extract reference items from package.json exports field
95
+ *
96
+ * @param {Object} packageEntry - Package.json data
97
+ * @returns {ReferenceItem[]} Array of reference items from exports
98
+ *
99
+ * @example
100
+ * extractFromExports({ exports: { '.': './index.js', './utils': './utils.js' } })
101
+ * // => [{ name: '.', type: 'export', description: './index.js', example: null }, ...]
102
+ */
103
+ function extractFromExports(packageEntry) {
104
+ const items = [];
105
+
106
+ if (!packageEntry.exports) {
107
+ return items;
108
+ }
109
+
110
+ const exports = packageEntry.exports;
111
+
112
+ // Handle string exports (single entry point)
113
+ if (typeof exports === 'string') {
114
+ items.push({
115
+ name: '.',
116
+ type: 'export',
117
+ description: exports,
118
+ example: null
119
+ });
120
+ return items;
121
+ }
122
+
123
+ // Handle object exports
124
+ if (typeof exports === 'object' && exports !== null) {
125
+ for (const [exportPath, value] of Object.entries(exports)) {
126
+ // Handle conditional exports (import/require)
127
+ let description = '';
128
+ if (typeof value === 'string') {
129
+ description = value;
130
+ } else if (typeof value === 'object' && value !== null) {
131
+ // Use import condition first, fallback to require, then default
132
+ description = value.import || value.require || value.default || JSON.stringify(value);
133
+ }
134
+
135
+ items.push({
136
+ name: exportPath,
137
+ type: 'export',
138
+ description: String(description),
139
+ example: null
140
+ });
141
+ }
142
+ }
143
+
144
+ return items;
145
+ }
146
+
147
+ /**
148
+ * Extract reference items from package.json bin field
149
+ *
150
+ * @param {Object} packageEntry - Package.json data
151
+ * @returns {ReferenceItem[]} Array of reference items from bin
152
+ *
153
+ * @example
154
+ * extractFromBin({ bin: 'cli.js' })
155
+ * // => [{ name: 'package-name', type: 'bin', description: 'CLI entry point: cli.js', example: null }]
156
+ */
157
+ function extractFromBin(packageEntry) {
158
+ const items = [];
159
+
160
+ if (!packageEntry.bin) {
161
+ return items;
162
+ }
163
+
164
+ const bin = packageEntry.bin;
165
+
166
+ // Handle string bin (single CLI entry point)
167
+ if (typeof bin === 'string') {
168
+ items.push({
169
+ name: packageEntry.name || 'cli',
170
+ type: 'bin',
171
+ description: `CLI entry point: ${bin}`,
172
+ example: null
173
+ });
174
+ return items;
175
+ }
176
+
177
+ // Handle object bin (multiple CLI commands)
178
+ if (typeof bin === 'object' && bin !== null) {
179
+ for (const [binName, binPath] of Object.entries(bin)) {
180
+ items.push({
181
+ name: binName,
182
+ type: 'bin',
183
+ description: `CLI entry point: ${binPath}`,
184
+ example: null
185
+ });
186
+ }
187
+ }
188
+
189
+ return items;
190
+ }
191
+
192
+ /**
193
+ * Extract reference items from README API sections
194
+ *
195
+ * Looks for headings like "API", "API Reference", "Exports", "CLI Options", "Options"
196
+ * and extracts the content following these headings.
197
+ *
198
+ * @param {string} readme - README content
199
+ * @returns {ReferenceItem[]} Array of reference items from README
200
+ *
201
+ * @example
202
+ * extractFromReadme('## API\n\n- foo: Does something\n- bar: Does another thing')
203
+ * // => [{ name: 'foo', type: 'api', description: 'Does something', ... }, ...]
204
+ */
205
+ function extractFromReadme(readme) {
206
+ const items = [];
207
+
208
+ if (!readme || typeof readme !== 'string') {
209
+ return items;
210
+ }
211
+
212
+ // API section heading patterns (case-insensitive)
213
+ const apiHeadingPatterns = [
214
+ /^#{1,6}\s+API\s*$/im,
215
+ /^#{1,6}\s+API\s+Reference\s*$/im,
216
+ /^#{1,6}\s+Exports\s*$/im,
217
+ /^#{1,6}\s+CLI\s+Options\s*$/im,
218
+ /^#{1,6}\s+Options\s*$/im,
219
+ /^#{1,6}\s+Usage\s*$/im,
220
+ /^#{1,6}\s+Methods\s*$/im
221
+ ];
222
+
223
+ for (const pattern of apiHeadingPatterns) {
224
+ const match = readme.match(pattern);
225
+ if (!match) {
226
+ continue;
227
+ }
228
+
229
+ // Extract content after heading (next 500 chars or until next heading)
230
+ const startIndex = match.index + match[0].length;
231
+ const remainingContent = readme.slice(startIndex);
232
+
233
+ // Find next heading or take 500 chars
234
+ const nextHeadingMatch = remainingContent.match(/^#{1,6}\s+/m);
235
+ const endIndex = nextHeadingMatch ? nextHeadingMatch.index : 500;
236
+ const section = remainingContent.slice(0, endIndex);
237
+
238
+ // Parse for list items: "- name: description" or "- name - description"
239
+ const listItemPattern = /^[-*]\s+`?([^:`\n-]+)`?\s*[:\-]\s*(.+)$/gm;
240
+ let itemMatch;
241
+
242
+ while ((itemMatch = listItemPattern.exec(section)) !== null) {
243
+ const name = itemMatch[1].trim();
244
+ const description = itemMatch[2].trim();
245
+
246
+ if (name && description) {
247
+ items.push({
248
+ name,
249
+ type: 'api',
250
+ description,
251
+ example: null
252
+ });
253
+ }
254
+ }
255
+
256
+ // If we found items, we can stop searching
257
+ if (items.length > 0) {
258
+ break;
259
+ }
260
+
261
+ // If no list items found, create single item from section text
262
+ if (items.length === 0 && section.trim()) {
263
+ const sectionText = section.trim().split('\n')[0]; // First line
264
+ if (sectionText && sectionText.length > 10) {
265
+ items.push({
266
+ name: 'API',
267
+ type: 'api',
268
+ description: sectionText.slice(0, 200),
269
+ example: null
270
+ });
271
+ }
272
+ }
273
+ }
274
+
275
+ return items;
276
+ }
277
+
278
+ /**
279
+ * Calculate confidence score based on sources used
280
+ *
281
+ * Confidence levels:
282
+ * - 1.0: exports found
283
+ * - 0.8: bin found (no exports)
284
+ * - 0.6: README API section found (no exports/bin)
285
+ * - 0.3: partial data from multiple sources
286
+ * - 0.1: only "unknown" fallback
287
+ *
288
+ * @param {string[]} sources - Array of source names
289
+ * @returns {number} Confidence score between 0 and 1
290
+ *
291
+ * @example
292
+ * calculateConfidence(['exports', 'readme']) // => 1.0
293
+ * calculateConfidence(['bin']) // => 0.8
294
+ * calculateConfidence(['inferred']) // => 0.1
295
+ */
296
+ function calculateConfidence(sources) {
297
+ // Fallback case
298
+ if (sources.includes('inferred')) {
299
+ return 0.1;
300
+ }
301
+
302
+ // Exports found (highest confidence)
303
+ if (sources.includes('exports')) {
304
+ return 1.0;
305
+ }
306
+
307
+ // Bin found (high confidence for CLI tools)
308
+ if (sources.includes('bin')) {
309
+ return 0.8;
310
+ }
311
+
312
+ // README API section found
313
+ if (sources.includes('readme')) {
314
+ return 0.6;
315
+ }
316
+
317
+ // Partial data (multiple sources but none of the above)
318
+ if (sources.length > 1) {
319
+ return 0.3;
320
+ }
321
+
322
+ // Default low confidence
323
+ return 0.2;
324
+ }