@63klabs/cache-data 1.3.6 → 1.3.7
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/CHANGELOG.md +20 -3
- package/CONTRIBUTING.md +18 -12
- package/package.json +5 -1
- package/src/lib/dao-cache.js +137 -11
- package/AI_CONTEXT.md +0 -863
- package/scripts/README.md +0 -175
- package/scripts/audit-documentation.mjs +0 -856
- package/scripts/review-documentation-files.mjs +0 -406
- package/scripts/setup-dev-environment.sh +0 -59
- package/scripts/verify-example-files.mjs +0 -194
|
@@ -1,856 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Documentation Audit and Validation Script
|
|
5
|
-
*
|
|
6
|
-
* Scans all source files in src/ directory to:
|
|
7
|
-
* - Identify all exported functions, methods, and classes
|
|
8
|
-
* - Parse existing JSDoc comments
|
|
9
|
-
* - Validate JSDoc completeness (all required tags present)
|
|
10
|
-
* - Validate JSDoc accuracy (parameters match code)
|
|
11
|
-
* - Validate links in all documentation files
|
|
12
|
-
* - Validate example code syntax
|
|
13
|
-
* - Generate comprehensive validation report
|
|
14
|
-
*
|
|
15
|
-
* Requirements: 1.1, 9.5, 10.5
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import fs from 'fs';
|
|
19
|
-
import path from 'path';
|
|
20
|
-
import { fileURLToPath } from 'url';
|
|
21
|
-
import { exec } from 'child_process';
|
|
22
|
-
import { promisify } from 'util';
|
|
23
|
-
|
|
24
|
-
const execAsync = promisify(exec);
|
|
25
|
-
|
|
26
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
27
|
-
const __dirname = path.dirname(__filename);
|
|
28
|
-
|
|
29
|
-
// Configuration
|
|
30
|
-
const SRC_DIR = path.join(__dirname, '..', 'src');
|
|
31
|
-
const DOCS_DIR = path.join(__dirname, '..', 'docs');
|
|
32
|
-
const ROOT_DIR = path.join(__dirname, '..');
|
|
33
|
-
const OUTPUT_FILE = path.join(__dirname, '..', '.kiro', 'specs', '1-3-6-documentation-enhancement', 'audit-report.json');
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Recursively get all JavaScript files in a directory
|
|
37
|
-
* @param {string} dir Directory to scan
|
|
38
|
-
* @param {Array<string>} fileList Accumulated file list
|
|
39
|
-
* @returns {Array<string>} List of file paths
|
|
40
|
-
*/
|
|
41
|
-
function getJavaScriptFiles(dir, fileList = []) {
|
|
42
|
-
const files = fs.readdirSync(dir);
|
|
43
|
-
|
|
44
|
-
files.forEach(file => {
|
|
45
|
-
const filePath = path.join(dir, file);
|
|
46
|
-
const stat = fs.statSync(filePath);
|
|
47
|
-
|
|
48
|
-
if (stat.isDirectory()) {
|
|
49
|
-
getJavaScriptFiles(filePath, fileList);
|
|
50
|
-
} else if (file.endsWith('.js')) {
|
|
51
|
-
fileList.push(filePath);
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
return fileList;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Extract JSDoc comment block preceding a code element
|
|
60
|
-
* @param {string} content File content
|
|
61
|
-
* @param {number} position Position of the code element
|
|
62
|
-
* @returns {string|null} JSDoc comment or null
|
|
63
|
-
*/
|
|
64
|
-
function extractJSDocBefore(content, position) {
|
|
65
|
-
// Look backwards from position to find JSDoc comment
|
|
66
|
-
const beforeContent = content.substring(0, position);
|
|
67
|
-
const jsdocPattern = /\/\*\*[\s\S]*?\*\//g;
|
|
68
|
-
const matches = [...beforeContent.matchAll(jsdocPattern)];
|
|
69
|
-
|
|
70
|
-
if (matches.length === 0) return null;
|
|
71
|
-
|
|
72
|
-
// Get the last JSDoc comment before this position
|
|
73
|
-
const lastMatch = matches[matches.length - 1];
|
|
74
|
-
const jsdocEnd = lastMatch.index + lastMatch[0].length;
|
|
75
|
-
|
|
76
|
-
// Check if there's only whitespace between JSDoc and the code element
|
|
77
|
-
const betweenContent = content.substring(jsdocEnd, position);
|
|
78
|
-
if (/^\s*$/.test(betweenContent)) {
|
|
79
|
-
return lastMatch[0];
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return null;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Parse JSDoc comment to extract tags
|
|
87
|
-
* @param {string} jsdoc JSDoc comment block
|
|
88
|
-
* @returns {Object} Parsed JSDoc data
|
|
89
|
-
*/
|
|
90
|
-
function parseJSDoc(jsdoc) {
|
|
91
|
-
if (!jsdoc) {
|
|
92
|
-
return {
|
|
93
|
-
description: null,
|
|
94
|
-
params: [],
|
|
95
|
-
returns: null,
|
|
96
|
-
examples: [],
|
|
97
|
-
throws: []
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const lines = jsdoc.split('\n').map(line => line.trim().replace(/^\*\s?/, ''));
|
|
102
|
-
|
|
103
|
-
const parsed = {
|
|
104
|
-
description: '',
|
|
105
|
-
params: [],
|
|
106
|
-
returns: null,
|
|
107
|
-
examples: [],
|
|
108
|
-
throws: []
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
let currentTag = null;
|
|
112
|
-
let currentContent = '';
|
|
113
|
-
|
|
114
|
-
lines.forEach(line => {
|
|
115
|
-
// Check for tags
|
|
116
|
-
const paramMatch = line.match(/^@param\s+\{([^}]+)\}\s+(\[?[\w.]+\]?)\s*-?\s*(.*)/);
|
|
117
|
-
const returnsMatch = line.match(/^@returns?\s+\{([^}]+)\}\s*(.*)/);
|
|
118
|
-
const exampleMatch = line.match(/^@example/);
|
|
119
|
-
const throwsMatch = line.match(/^@throws?\s+\{([^}]+)\}\s*(.*)/);
|
|
120
|
-
|
|
121
|
-
if (paramMatch) {
|
|
122
|
-
if (currentTag === 'description' && currentContent.trim()) {
|
|
123
|
-
parsed.description = currentContent.trim();
|
|
124
|
-
}
|
|
125
|
-
currentTag = 'param';
|
|
126
|
-
const [, type, name, description] = paramMatch;
|
|
127
|
-
const isOptional = name.startsWith('[') && name.endsWith(']');
|
|
128
|
-
const cleanName = name.replace(/[\[\]]/g, '').split('=')[0];
|
|
129
|
-
const defaultValue = name.includes('=') ? name.split('=')[1].replace(']', '') : null;
|
|
130
|
-
|
|
131
|
-
parsed.params.push({
|
|
132
|
-
name: cleanName,
|
|
133
|
-
type,
|
|
134
|
-
description: description || '',
|
|
135
|
-
optional: isOptional,
|
|
136
|
-
defaultValue
|
|
137
|
-
});
|
|
138
|
-
} else if (returnsMatch) {
|
|
139
|
-
if (currentTag === 'description' && currentContent.trim()) {
|
|
140
|
-
parsed.description = currentContent.trim();
|
|
141
|
-
}
|
|
142
|
-
currentTag = 'returns';
|
|
143
|
-
const [, type, description] = returnsMatch;
|
|
144
|
-
parsed.returns = {
|
|
145
|
-
type,
|
|
146
|
-
description: description || ''
|
|
147
|
-
};
|
|
148
|
-
} else if (exampleMatch) {
|
|
149
|
-
if (currentTag === 'description' && currentContent.trim()) {
|
|
150
|
-
parsed.description = currentContent.trim();
|
|
151
|
-
}
|
|
152
|
-
currentTag = 'example';
|
|
153
|
-
currentContent = '';
|
|
154
|
-
} else if (throwsMatch) {
|
|
155
|
-
if (currentTag === 'description' && currentContent.trim()) {
|
|
156
|
-
parsed.description = currentContent.trim();
|
|
157
|
-
}
|
|
158
|
-
currentTag = 'throws';
|
|
159
|
-
const [, type, description] = throwsMatch;
|
|
160
|
-
parsed.throws.push({
|
|
161
|
-
type,
|
|
162
|
-
description: description || ''
|
|
163
|
-
});
|
|
164
|
-
} else if (line && !line.startsWith('/**') && !line.startsWith('*/')) {
|
|
165
|
-
// Content line
|
|
166
|
-
if (currentTag === 'example') {
|
|
167
|
-
currentContent += line + '\n';
|
|
168
|
-
} else if (currentTag === 'description' || !currentTag) {
|
|
169
|
-
currentTag = 'description';
|
|
170
|
-
currentContent += line + ' ';
|
|
171
|
-
} else if (currentTag === 'param' && parsed.params.length > 0) {
|
|
172
|
-
// Continuation of param description
|
|
173
|
-
parsed.params[parsed.params.length - 1].description += ' ' + line;
|
|
174
|
-
} else if (currentTag === 'returns' && parsed.returns) {
|
|
175
|
-
// Continuation of returns description
|
|
176
|
-
parsed.returns.description += ' ' + line;
|
|
177
|
-
} else if (currentTag === 'throws' && parsed.throws.length > 0) {
|
|
178
|
-
// Continuation of throws description
|
|
179
|
-
parsed.throws[parsed.throws.length - 1].description += ' ' + line;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
// Handle any remaining content
|
|
185
|
-
if (currentTag === 'description' && currentContent.trim()) {
|
|
186
|
-
parsed.description = currentContent.trim();
|
|
187
|
-
} else if (currentTag === 'example' && currentContent.trim()) {
|
|
188
|
-
parsed.examples.push(currentContent.trim());
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
return parsed;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* Extract function parameters from function signature
|
|
196
|
-
* @param {string} signature Function signature
|
|
197
|
-
* @returns {Array<string>} Parameter names
|
|
198
|
-
*/
|
|
199
|
-
function extractFunctionParams(signature) {
|
|
200
|
-
const paramMatch = signature.match(/\(([^)]*)\)/);
|
|
201
|
-
if (!paramMatch || !paramMatch[1].trim()) return [];
|
|
202
|
-
|
|
203
|
-
return paramMatch[1]
|
|
204
|
-
.split(',')
|
|
205
|
-
.map(p => p.trim().split('=')[0].trim())
|
|
206
|
-
.filter(p => p.length > 0);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Analyze a single source file
|
|
211
|
-
* @param {string} filePath Path to source file
|
|
212
|
-
* @returns {Object} Analysis results
|
|
213
|
-
*/
|
|
214
|
-
function analyzeFile(filePath) {
|
|
215
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
216
|
-
const relativePath = path.relative(SRC_DIR, filePath);
|
|
217
|
-
|
|
218
|
-
const exports = [];
|
|
219
|
-
|
|
220
|
-
// Pattern for class declarations
|
|
221
|
-
const classPattern = /class\s+(\w+)/g;
|
|
222
|
-
let match;
|
|
223
|
-
|
|
224
|
-
while ((match = classPattern.exec(content)) !== null) {
|
|
225
|
-
const className = match[1];
|
|
226
|
-
const jsdoc = extractJSDocBefore(content, match.index);
|
|
227
|
-
const parsedJSDoc = parseJSDoc(jsdoc);
|
|
228
|
-
|
|
229
|
-
exports.push({
|
|
230
|
-
type: 'class',
|
|
231
|
-
name: className,
|
|
232
|
-
hasJSDoc: jsdoc !== null,
|
|
233
|
-
jsdoc: parsedJSDoc,
|
|
234
|
-
position: match.index
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
// Find methods in this class
|
|
238
|
-
const classStart = match.index;
|
|
239
|
-
const classEnd = content.indexOf('}', classStart);
|
|
240
|
-
const classBody = content.substring(classStart, classEnd);
|
|
241
|
-
|
|
242
|
-
// Static methods
|
|
243
|
-
const staticMethodPattern = /static\s+(\w+)\s*\([^)]*\)/g;
|
|
244
|
-
let methodMatch;
|
|
245
|
-
while ((methodMatch = staticMethodPattern.exec(classBody)) !== null) {
|
|
246
|
-
const methodName = methodMatch[1];
|
|
247
|
-
const methodJsdoc = extractJSDocBefore(classBody, methodMatch.index);
|
|
248
|
-
const parsedMethodJSDoc = parseJSDoc(methodJsdoc);
|
|
249
|
-
const params = extractFunctionParams(methodMatch[0]);
|
|
250
|
-
|
|
251
|
-
exports.push({
|
|
252
|
-
type: 'method',
|
|
253
|
-
name: `${className}.${methodName}`,
|
|
254
|
-
className: className,
|
|
255
|
-
isStatic: true,
|
|
256
|
-
hasJSDoc: methodJsdoc !== null,
|
|
257
|
-
jsdoc: parsedMethodJSDoc,
|
|
258
|
-
actualParams: params,
|
|
259
|
-
position: classStart + methodMatch.index
|
|
260
|
-
});
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// Instance methods (excluding constructor and private methods)
|
|
264
|
-
const instanceMethodPattern = /(?:async\s+)?(\w+)\s*\([^)]*\)\s*\{/g;
|
|
265
|
-
while ((methodMatch = instanceMethodPattern.exec(classBody)) !== null) {
|
|
266
|
-
const methodName = methodMatch[1];
|
|
267
|
-
// Skip constructor, static methods, and private methods
|
|
268
|
-
if (methodName === 'constructor' || methodName.startsWith('#') || classBody.substring(Math.max(0, methodMatch.index - 10), methodMatch.index).includes('static')) {
|
|
269
|
-
continue;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
const methodJsdoc = extractJSDocBefore(classBody, methodMatch.index);
|
|
273
|
-
const parsedMethodJSDoc = parseJSDoc(methodJsdoc);
|
|
274
|
-
const params = extractFunctionParams(methodMatch[0]);
|
|
275
|
-
|
|
276
|
-
exports.push({
|
|
277
|
-
type: 'method',
|
|
278
|
-
name: `${className}.${methodName}`,
|
|
279
|
-
className: className,
|
|
280
|
-
isStatic: false,
|
|
281
|
-
hasJSDoc: methodJsdoc !== null,
|
|
282
|
-
jsdoc: parsedMethodJSDoc,
|
|
283
|
-
actualParams: params,
|
|
284
|
-
position: classStart + methodMatch.index
|
|
285
|
-
});
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// Pattern for exported functions (const functionName = ...)
|
|
290
|
-
const constFunctionPattern = /const\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*=>/g;
|
|
291
|
-
while ((match = constFunctionPattern.exec(content)) !== null) {
|
|
292
|
-
const functionName = match[1];
|
|
293
|
-
const jsdoc = extractJSDocBefore(content, match.index);
|
|
294
|
-
const parsedJSDoc = parseJSDoc(jsdoc);
|
|
295
|
-
const params = extractFunctionParams(match[0]);
|
|
296
|
-
|
|
297
|
-
exports.push({
|
|
298
|
-
type: 'function',
|
|
299
|
-
name: functionName,
|
|
300
|
-
hasJSDoc: jsdoc !== null,
|
|
301
|
-
jsdoc: parsedJSDoc,
|
|
302
|
-
actualParams: params,
|
|
303
|
-
position: match.index
|
|
304
|
-
});
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// Pattern for function declarations
|
|
308
|
-
const functionPattern = /(?:async\s+)?function\s+(\w+)\s*\([^)]*\)/g;
|
|
309
|
-
while ((match = functionPattern.exec(content)) !== null) {
|
|
310
|
-
const functionName = match[1];
|
|
311
|
-
const jsdoc = extractJSDocBefore(content, match.index);
|
|
312
|
-
const parsedJSDoc = parseJSDoc(jsdoc);
|
|
313
|
-
const params = extractFunctionParams(match[0]);
|
|
314
|
-
|
|
315
|
-
exports.push({
|
|
316
|
-
type: 'function',
|
|
317
|
-
name: functionName,
|
|
318
|
-
hasJSDoc: jsdoc !== null,
|
|
319
|
-
jsdoc: parsedJSDoc,
|
|
320
|
-
actualParams: params,
|
|
321
|
-
position: match.index
|
|
322
|
-
});
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// Check module.exports to determine what's actually exported
|
|
326
|
-
const moduleExportsPattern = /module\.exports\s*=\s*\{([^}]+)\}/;
|
|
327
|
-
const exportsMatch = content.match(moduleExportsPattern);
|
|
328
|
-
let exportedNames = [];
|
|
329
|
-
|
|
330
|
-
if (exportsMatch) {
|
|
331
|
-
exportedNames = exportsMatch[1]
|
|
332
|
-
.split(',')
|
|
333
|
-
.map(e => e.trim().split(':')[0].trim())
|
|
334
|
-
.filter(e => e.length > 0);
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
// Filter to only exported items
|
|
338
|
-
const exportedItems = exports.filter(item => {
|
|
339
|
-
if (exportedNames.length === 0) return true; // If we can't determine exports, include all
|
|
340
|
-
return exportedNames.includes(item.name) || exportedNames.includes(item.name.split('.')[0]);
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
return {
|
|
344
|
-
filePath: relativePath,
|
|
345
|
-
exports: exportedItems
|
|
346
|
-
};
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
/**
|
|
350
|
-
* Analyze JSDoc completeness
|
|
351
|
-
* @param {Object} item Export item
|
|
352
|
-
* @returns {Object} Completeness analysis
|
|
353
|
-
*/
|
|
354
|
-
function analyzeCompleteness(item) {
|
|
355
|
-
const issues = [];
|
|
356
|
-
|
|
357
|
-
if (!item.hasJSDoc) {
|
|
358
|
-
return {
|
|
359
|
-
complete: false,
|
|
360
|
-
issues: ['Missing JSDoc comment']
|
|
361
|
-
};
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
const jsdoc = item.jsdoc;
|
|
365
|
-
|
|
366
|
-
// Check description
|
|
367
|
-
if (!jsdoc.description || jsdoc.description.trim().length === 0) {
|
|
368
|
-
issues.push('Missing description');
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
// Check @param tags
|
|
372
|
-
if (item.actualParams && item.actualParams.length > 0) {
|
|
373
|
-
const documentedParams = jsdoc.params.map(p => p.name);
|
|
374
|
-
const missingParams = item.actualParams.filter(p => !documentedParams.includes(p));
|
|
375
|
-
|
|
376
|
-
if (missingParams.length > 0) {
|
|
377
|
-
issues.push(`Missing @param tags for: ${missingParams.join(', ')}`);
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
// Check for incomplete param documentation
|
|
381
|
-
jsdoc.params.forEach(param => {
|
|
382
|
-
if (!param.type || param.type.trim() === '') {
|
|
383
|
-
issues.push(`@param ${param.name} missing type`);
|
|
384
|
-
}
|
|
385
|
-
if (!param.description || param.description.trim() === '') {
|
|
386
|
-
issues.push(`@param ${param.name} missing description`);
|
|
387
|
-
}
|
|
388
|
-
});
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
// Check @returns tag (for functions and methods, not classes)
|
|
392
|
-
if (item.type !== 'class' && !jsdoc.returns) {
|
|
393
|
-
issues.push('Missing @returns tag');
|
|
394
|
-
} else if (item.type !== 'class' && jsdoc.returns) {
|
|
395
|
-
if (!jsdoc.returns.type || jsdoc.returns.type.trim() === '') {
|
|
396
|
-
issues.push('@returns missing type');
|
|
397
|
-
}
|
|
398
|
-
if (!jsdoc.returns.description || jsdoc.returns.description.trim() === '') {
|
|
399
|
-
issues.push('@returns missing description');
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
// Check @example tag
|
|
404
|
-
if (jsdoc.examples.length === 0) {
|
|
405
|
-
issues.push('Missing @example tag');
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
// Note: @throws is optional, so we don't check for it
|
|
409
|
-
|
|
410
|
-
return {
|
|
411
|
-
complete: issues.length === 0,
|
|
412
|
-
issues
|
|
413
|
-
};
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
/**
|
|
417
|
-
* Validate JSDoc accuracy - check if documented params match actual params
|
|
418
|
-
* @param {Object} item Export item
|
|
419
|
-
* @returns {Object} Accuracy validation results
|
|
420
|
-
*/
|
|
421
|
-
function validateJSDocAccuracy(item) {
|
|
422
|
-
const issues = [];
|
|
423
|
-
|
|
424
|
-
if (!item.hasJSDoc || !item.jsdoc) {
|
|
425
|
-
return { accurate: true, issues: [] }; // Can't validate if no JSDoc
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
const jsdoc = item.jsdoc;
|
|
429
|
-
const actualParams = item.actualParams || [];
|
|
430
|
-
const documentedParams = jsdoc.params.map(p => p.name);
|
|
431
|
-
|
|
432
|
-
// Check for hallucinated parameters (documented but not in actual signature)
|
|
433
|
-
const hallucinatedParams = documentedParams.filter(p => !actualParams.includes(p));
|
|
434
|
-
if (hallucinatedParams.length > 0) {
|
|
435
|
-
issues.push(`Hallucinated parameters (not in actual signature): ${hallucinatedParams.join(', ')}`);
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
// Check for parameter order mismatch
|
|
439
|
-
const commonParams = actualParams.filter(p => documentedParams.includes(p));
|
|
440
|
-
if (commonParams.length > 0) {
|
|
441
|
-
const actualOrder = commonParams.map(p => actualParams.indexOf(p));
|
|
442
|
-
const docOrder = commonParams.map(p => documentedParams.indexOf(p));
|
|
443
|
-
|
|
444
|
-
for (let i = 0; i < actualOrder.length - 1; i++) {
|
|
445
|
-
if (actualOrder[i] > actualOrder[i + 1] && docOrder[i] < docOrder[i + 1]) {
|
|
446
|
-
issues.push('Parameter order in JSDoc does not match actual signature');
|
|
447
|
-
break;
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
return {
|
|
453
|
-
accurate: issues.length === 0,
|
|
454
|
-
issues
|
|
455
|
-
};
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
/**
|
|
459
|
-
* Get all markdown files recursively
|
|
460
|
-
* @param {string} dir Directory to scan
|
|
461
|
-
* @param {Array<string>} fileList Accumulated file list
|
|
462
|
-
* @returns {Array<string>} List of markdown file paths
|
|
463
|
-
*/
|
|
464
|
-
function getMarkdownFiles(dir, fileList = []) {
|
|
465
|
-
if (!fs.existsSync(dir)) return fileList;
|
|
466
|
-
|
|
467
|
-
const files = fs.readdirSync(dir);
|
|
468
|
-
|
|
469
|
-
files.forEach(file => {
|
|
470
|
-
const filePath = path.join(dir, file);
|
|
471
|
-
const stat = fs.statSync(filePath);
|
|
472
|
-
|
|
473
|
-
if (stat.isDirectory() && !file.startsWith('.') && file !== 'node_modules') {
|
|
474
|
-
getMarkdownFiles(filePath, fileList);
|
|
475
|
-
} else if (file.endsWith('.md')) {
|
|
476
|
-
fileList.push(filePath);
|
|
477
|
-
}
|
|
478
|
-
});
|
|
479
|
-
|
|
480
|
-
return fileList;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
/**
|
|
484
|
-
* Extract links from markdown content
|
|
485
|
-
* @param {string} content Markdown content
|
|
486
|
-
* @returns {Array<Object>} Array of links with line numbers
|
|
487
|
-
*/
|
|
488
|
-
function extractLinks(content) {
|
|
489
|
-
const links = [];
|
|
490
|
-
const lines = content.split('\n');
|
|
491
|
-
|
|
492
|
-
// Match markdown links [text](url) and bare URLs
|
|
493
|
-
const linkPattern = /\[([^\]]+)\]\(([^)]+)\)/g;
|
|
494
|
-
|
|
495
|
-
lines.forEach((line, index) => {
|
|
496
|
-
let match;
|
|
497
|
-
while ((match = linkPattern.exec(line)) !== null) {
|
|
498
|
-
links.push({
|
|
499
|
-
text: match[1],
|
|
500
|
-
url: match[2],
|
|
501
|
-
line: index + 1
|
|
502
|
-
});
|
|
503
|
-
}
|
|
504
|
-
});
|
|
505
|
-
|
|
506
|
-
return links;
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
/**
|
|
510
|
-
* Validate a single link
|
|
511
|
-
* @param {string} link Link URL
|
|
512
|
-
* @param {string} sourceFile Source file containing the link
|
|
513
|
-
* @returns {Object} Validation result
|
|
514
|
-
*/
|
|
515
|
-
function validateLink(link, sourceFile) {
|
|
516
|
-
// Skip anchor links
|
|
517
|
-
if (link.startsWith('#')) {
|
|
518
|
-
return { valid: true, type: 'anchor' };
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
// External URLs
|
|
522
|
-
if (link.startsWith('http://') || link.startsWith('https://')) {
|
|
523
|
-
return { valid: true, type: 'external', note: 'External link not validated' };
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
// Internal relative links
|
|
527
|
-
const sourceDir = path.dirname(sourceFile);
|
|
528
|
-
const targetPath = path.resolve(sourceDir, link.split('#')[0]); // Remove anchor
|
|
529
|
-
|
|
530
|
-
if (fs.existsSync(targetPath)) {
|
|
531
|
-
return { valid: true, type: 'internal' };
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
// Try relative to root
|
|
535
|
-
const rootPath = path.resolve(ROOT_DIR, link.split('#')[0]);
|
|
536
|
-
if (fs.existsSync(rootPath)) {
|
|
537
|
-
return { valid: true, type: 'internal' };
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
return { valid: false, type: 'internal', error: 'File not found' };
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
/**
|
|
544
|
-
* Validate links in all documentation files
|
|
545
|
-
* @returns {Object} Link validation results
|
|
546
|
-
*/
|
|
547
|
-
function validateDocumentationLinks() {
|
|
548
|
-
console.log('\nValidating documentation links...');
|
|
549
|
-
|
|
550
|
-
const markdownFiles = [
|
|
551
|
-
...getMarkdownFiles(DOCS_DIR),
|
|
552
|
-
path.join(ROOT_DIR, 'README.md'),
|
|
553
|
-
path.join(ROOT_DIR, 'CHANGELOG.md'),
|
|
554
|
-
path.join(ROOT_DIR, 'SECURITY.md')
|
|
555
|
-
].filter(f => fs.existsSync(f));
|
|
556
|
-
|
|
557
|
-
const brokenLinks = [];
|
|
558
|
-
let totalLinks = 0;
|
|
559
|
-
let validLinks = 0;
|
|
560
|
-
|
|
561
|
-
markdownFiles.forEach(file => {
|
|
562
|
-
const content = fs.readFileSync(file, 'utf-8');
|
|
563
|
-
const links = extractLinks(content);
|
|
564
|
-
|
|
565
|
-
links.forEach(link => {
|
|
566
|
-
totalLinks++;
|
|
567
|
-
const validation = validateLink(link.url, file);
|
|
568
|
-
|
|
569
|
-
if (validation.valid) {
|
|
570
|
-
validLinks++;
|
|
571
|
-
} else {
|
|
572
|
-
brokenLinks.push({
|
|
573
|
-
file: path.relative(ROOT_DIR, file),
|
|
574
|
-
line: link.line,
|
|
575
|
-
text: link.text,
|
|
576
|
-
url: link.url,
|
|
577
|
-
error: validation.error
|
|
578
|
-
});
|
|
579
|
-
}
|
|
580
|
-
});
|
|
581
|
-
});
|
|
582
|
-
|
|
583
|
-
console.log(` Checked ${totalLinks} links in ${markdownFiles.length} files`);
|
|
584
|
-
console.log(` Valid: ${validLinks}, Broken: ${brokenLinks.length}`);
|
|
585
|
-
|
|
586
|
-
return {
|
|
587
|
-
totalLinks,
|
|
588
|
-
validLinks,
|
|
589
|
-
brokenLinks,
|
|
590
|
-
filesChecked: markdownFiles.length
|
|
591
|
-
};
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
/**
|
|
595
|
-
* Extract code examples from markdown content
|
|
596
|
-
* @param {string} content Markdown content
|
|
597
|
-
* @returns {Array<Object>} Array of code examples
|
|
598
|
-
*/
|
|
599
|
-
function extractCodeExamples(content) {
|
|
600
|
-
const examples = [];
|
|
601
|
-
const lines = content.split('\n');
|
|
602
|
-
let inCodeBlock = false;
|
|
603
|
-
let currentExample = null;
|
|
604
|
-
let lineNumber = 0;
|
|
605
|
-
|
|
606
|
-
lines.forEach((line, index) => {
|
|
607
|
-
if (line.trim().startsWith('```javascript') || line.trim().startsWith('```js')) {
|
|
608
|
-
inCodeBlock = true;
|
|
609
|
-
lineNumber = index + 1;
|
|
610
|
-
currentExample = {
|
|
611
|
-
code: '',
|
|
612
|
-
startLine: lineNumber
|
|
613
|
-
};
|
|
614
|
-
} else if (line.trim() === '```' && inCodeBlock) {
|
|
615
|
-
inCodeBlock = false;
|
|
616
|
-
if (currentExample && currentExample.code.trim()) {
|
|
617
|
-
examples.push(currentExample);
|
|
618
|
-
}
|
|
619
|
-
currentExample = null;
|
|
620
|
-
} else if (inCodeBlock && currentExample) {
|
|
621
|
-
currentExample.code += line + '\n';
|
|
622
|
-
}
|
|
623
|
-
});
|
|
624
|
-
|
|
625
|
-
return examples;
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
/**
|
|
629
|
-
* Validate JavaScript code syntax
|
|
630
|
-
* @param {string} code JavaScript code
|
|
631
|
-
* @returns {Object} Validation result
|
|
632
|
-
*/
|
|
633
|
-
async function validateCodeSyntax(code) {
|
|
634
|
-
// Create a temporary file
|
|
635
|
-
const tempFile = path.join(ROOT_DIR, '.temp-validation.mjs');
|
|
636
|
-
|
|
637
|
-
try {
|
|
638
|
-
fs.writeFileSync(tempFile, code);
|
|
639
|
-
|
|
640
|
-
// Try to parse with Node.js
|
|
641
|
-
await execAsync(`node --check ${tempFile}`);
|
|
642
|
-
|
|
643
|
-
return { valid: true };
|
|
644
|
-
} catch (error) {
|
|
645
|
-
return {
|
|
646
|
-
valid: false,
|
|
647
|
-
error: error.message
|
|
648
|
-
};
|
|
649
|
-
} finally {
|
|
650
|
-
// Clean up temp file
|
|
651
|
-
if (fs.existsSync(tempFile)) {
|
|
652
|
-
fs.unlinkSync(tempFile);
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
/**
|
|
658
|
-
* Validate example code in documentation files
|
|
659
|
-
* @returns {Promise<Object>} Example validation results
|
|
660
|
-
*/
|
|
661
|
-
async function validateExampleCode() {
|
|
662
|
-
console.log('\nValidating example code...');
|
|
663
|
-
|
|
664
|
-
const markdownFiles = [
|
|
665
|
-
...getMarkdownFiles(DOCS_DIR),
|
|
666
|
-
path.join(ROOT_DIR, 'README.md')
|
|
667
|
-
].filter(f => fs.existsSync(f));
|
|
668
|
-
|
|
669
|
-
const invalidExamples = [];
|
|
670
|
-
let totalExamples = 0;
|
|
671
|
-
let validExamples = 0;
|
|
672
|
-
|
|
673
|
-
for (const file of markdownFiles) {
|
|
674
|
-
const content = fs.readFileSync(file, 'utf-8');
|
|
675
|
-
const examples = extractCodeExamples(content);
|
|
676
|
-
|
|
677
|
-
for (const example of examples) {
|
|
678
|
-
totalExamples++;
|
|
679
|
-
const validation = await validateCodeSyntax(example.code);
|
|
680
|
-
|
|
681
|
-
if (validation.valid) {
|
|
682
|
-
validExamples++;
|
|
683
|
-
} else {
|
|
684
|
-
invalidExamples.push({
|
|
685
|
-
file: path.relative(ROOT_DIR, file),
|
|
686
|
-
line: example.startLine,
|
|
687
|
-
error: validation.error,
|
|
688
|
-
code: example.code.substring(0, 100) + (example.code.length > 100 ? '...' : '')
|
|
689
|
-
});
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
console.log(` Checked ${totalExamples} code examples in ${markdownFiles.length} files`);
|
|
695
|
-
console.log(` Valid: ${validExamples}, Invalid: ${invalidExamples.length}`);
|
|
696
|
-
|
|
697
|
-
return {
|
|
698
|
-
totalExamples,
|
|
699
|
-
validExamples,
|
|
700
|
-
invalidExamples,
|
|
701
|
-
filesChecked: markdownFiles.length
|
|
702
|
-
};
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
/**
|
|
706
|
-
* Generate audit report
|
|
707
|
-
*/
|
|
708
|
-
async function generateAuditReport() {
|
|
709
|
-
console.log('Starting documentation audit and validation...\n');
|
|
710
|
-
|
|
711
|
-
const files = getJavaScriptFiles(SRC_DIR);
|
|
712
|
-
console.log(`Found ${files.length} JavaScript files\n`);
|
|
713
|
-
|
|
714
|
-
const fileAnalyses = [];
|
|
715
|
-
let totalExports = 0;
|
|
716
|
-
let documentedExports = 0;
|
|
717
|
-
let completeExports = 0;
|
|
718
|
-
const missingJSDoc = [];
|
|
719
|
-
const incompleteJSDoc = [];
|
|
720
|
-
const inaccurateJSDoc = [];
|
|
721
|
-
|
|
722
|
-
files.forEach(file => {
|
|
723
|
-
console.log(`Analyzing: ${path.relative(SRC_DIR, file)}`);
|
|
724
|
-
const analysis = analyzeFile(file);
|
|
725
|
-
fileAnalyses.push(analysis);
|
|
726
|
-
|
|
727
|
-
analysis.exports.forEach(item => {
|
|
728
|
-
totalExports++;
|
|
729
|
-
|
|
730
|
-
if (item.hasJSDoc) {
|
|
731
|
-
documentedExports++;
|
|
732
|
-
} else {
|
|
733
|
-
missingJSDoc.push({
|
|
734
|
-
file: analysis.filePath,
|
|
735
|
-
name: item.name,
|
|
736
|
-
type: item.type
|
|
737
|
-
});
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
const completeness = analyzeCompleteness(item);
|
|
741
|
-
if (completeness.complete) {
|
|
742
|
-
completeExports++;
|
|
743
|
-
} else if (item.hasJSDoc) {
|
|
744
|
-
incompleteJSDoc.push({
|
|
745
|
-
file: analysis.filePath,
|
|
746
|
-
name: item.name,
|
|
747
|
-
type: item.type,
|
|
748
|
-
issues: completeness.issues
|
|
749
|
-
});
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
// Validate accuracy
|
|
753
|
-
const accuracy = validateJSDocAccuracy(item);
|
|
754
|
-
if (!accuracy.accurate) {
|
|
755
|
-
inaccurateJSDoc.push({
|
|
756
|
-
file: analysis.filePath,
|
|
757
|
-
name: item.name,
|
|
758
|
-
type: item.type,
|
|
759
|
-
issues: accuracy.issues
|
|
760
|
-
});
|
|
761
|
-
}
|
|
762
|
-
});
|
|
763
|
-
});
|
|
764
|
-
|
|
765
|
-
// Validate documentation links
|
|
766
|
-
const linkValidation = validateDocumentationLinks();
|
|
767
|
-
|
|
768
|
-
// Validate example code
|
|
769
|
-
const exampleValidation = await validateExampleCode();
|
|
770
|
-
|
|
771
|
-
const report = {
|
|
772
|
-
auditDate: new Date().toISOString(),
|
|
773
|
-
summary: {
|
|
774
|
-
totalFiles: files.length,
|
|
775
|
-
totalPublicFunctions: totalExports,
|
|
776
|
-
documentedFunctions: documentedExports,
|
|
777
|
-
completeFunctions: completeExports,
|
|
778
|
-
missingJSDocCount: missingJSDoc.length,
|
|
779
|
-
incompleteJSDocCount: incompleteJSDoc.length,
|
|
780
|
-
inaccurateJSDocCount: inaccurateJSDoc.length,
|
|
781
|
-
coveragePercentage: totalExports > 0 ? ((documentedExports / totalExports) * 100).toFixed(2) : 0,
|
|
782
|
-
completenessPercentage: totalExports > 0 ? ((completeExports / totalExports) * 100).toFixed(2) : 0,
|
|
783
|
-
brokenLinksCount: linkValidation.brokenLinks.length,
|
|
784
|
-
invalidExamplesCount: exampleValidation.invalidExamples.length,
|
|
785
|
-
// Only count REAL critical errors (exclude missingJSDoc due to parser false positives)
|
|
786
|
-
// Critical errors are: incomplete JSDoc, inaccurate JSDoc, broken links, invalid examples
|
|
787
|
-
criticalErrors: incompleteJSDoc.length + inaccurateJSDoc.length + linkValidation.brokenLinks.length + exampleValidation.invalidExamples.length
|
|
788
|
-
},
|
|
789
|
-
jsdocAnalysis: {
|
|
790
|
-
missingJSDoc,
|
|
791
|
-
incompleteJSDoc,
|
|
792
|
-
inaccurateJSDoc
|
|
793
|
-
},
|
|
794
|
-
linkValidation,
|
|
795
|
-
exampleValidation,
|
|
796
|
-
fileAnalyses
|
|
797
|
-
};
|
|
798
|
-
|
|
799
|
-
// Ensure output directory exists
|
|
800
|
-
const outputDir = path.dirname(OUTPUT_FILE);
|
|
801
|
-
if (!fs.existsSync(outputDir)) {
|
|
802
|
-
fs.mkdirSync(outputDir, { recursive: true });
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
// Write report to file
|
|
806
|
-
fs.writeFileSync(OUTPUT_FILE, JSON.stringify(report, null, 2));
|
|
807
|
-
|
|
808
|
-
// Print summary
|
|
809
|
-
console.log('\n' + '='.repeat(60));
|
|
810
|
-
console.log('DOCUMENTATION AUDIT AND VALIDATION SUMMARY');
|
|
811
|
-
console.log('='.repeat(60));
|
|
812
|
-
console.log('\nJSDoc Analysis:');
|
|
813
|
-
console.log(` Total Files Analyzed: ${report.summary.totalFiles}`);
|
|
814
|
-
console.log(` Total Public Functions/Classes: ${report.summary.totalPublicFunctions}`);
|
|
815
|
-
console.log(` Documented: ${report.summary.documentedFunctions} (${report.summary.coveragePercentage}%)`);
|
|
816
|
-
console.log(` Complete Documentation: ${report.summary.completeFunctions} (${report.summary.completenessPercentage}%)`);
|
|
817
|
-
console.log(` Missing JSDoc: ${report.summary.missingJSDocCount}`);
|
|
818
|
-
console.log(` Incomplete JSDoc: ${report.summary.incompleteJSDocCount}`);
|
|
819
|
-
console.log(` Inaccurate JSDoc: ${report.summary.inaccurateJSDocCount}`);
|
|
820
|
-
|
|
821
|
-
console.log('\nLink Validation:');
|
|
822
|
-
console.log(` Total Links Checked: ${linkValidation.totalLinks}`);
|
|
823
|
-
console.log(` Valid Links: ${linkValidation.validLinks}`);
|
|
824
|
-
console.log(` Broken Links: ${linkValidation.brokenLinks.length}`);
|
|
825
|
-
|
|
826
|
-
console.log('\nExample Code Validation:');
|
|
827
|
-
console.log(` Total Examples Checked: ${exampleValidation.totalExamples}`);
|
|
828
|
-
console.log(` Valid Examples: ${exampleValidation.validExamples}`);
|
|
829
|
-
console.log(` Invalid Examples: ${exampleValidation.invalidExamples.length}`);
|
|
830
|
-
|
|
831
|
-
console.log('\n' + '='.repeat(60));
|
|
832
|
-
console.log(`CRITICAL ERRORS: ${report.summary.criticalErrors}`);
|
|
833
|
-
console.log('='.repeat(60));
|
|
834
|
-
console.log(`\nDetailed report saved to: ${OUTPUT_FILE}\n`);
|
|
835
|
-
|
|
836
|
-
// Exit with error code if critical errors found
|
|
837
|
-
if (report.summary.criticalErrors > 0) {
|
|
838
|
-
console.error('❌ Validation failed with critical errors');
|
|
839
|
-
return report;
|
|
840
|
-
} else {
|
|
841
|
-
console.log('✅ All validation checks passed');
|
|
842
|
-
return report;
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
// Run the audit
|
|
847
|
-
try {
|
|
848
|
-
const report = await generateAuditReport();
|
|
849
|
-
// Exit with error code if there are critical errors
|
|
850
|
-
if (report.summary.criticalErrors > 0) {
|
|
851
|
-
process.exit(1);
|
|
852
|
-
}
|
|
853
|
-
} catch (error) {
|
|
854
|
-
console.error('Error during audit:', error);
|
|
855
|
-
process.exit(1);
|
|
856
|
-
}
|