@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/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
|
+
}
|