This commit is contained in:
2026-04-12 01:06:31 +07:00
commit 10d660cbcb
1066 changed files with 228596 additions and 0 deletions

View File

@@ -0,0 +1,342 @@
#!/usr/bin/env node
/**
* Validate documentation accuracy against codebase.
* Detects potential hallucinations: invented APIs, broken links, missing env vars.
*
* Usage:
* node .claude/scripts/validate-docs.cjs [docs-dir] [--src dir1,dir2]
*
* Checks:
* 1. Code references - verify `functionName()` and `ClassName` exist
* 2. Internal links - verify markdown links point to existing files
* 3. Config keys - verify ENV_VAR exist in .env.example
*
* Exit: Always 0 (non-blocking, warn-only mode)
*/
const fs = require('fs');
const path = require('path');
const { spawnSync } = require('child_process');
// Patterns
const CODE_REF_PATTERN = /`([A-Za-z_][A-Za-z0-9_]*(?:\(\))?)`/g;
const LINK_PATTERN = /\[([^\]]+)\]\(([^)]+)\)/g;
const ENV_PATTERN = /`([A-Z][A-Z0-9_]{2,})`|\$([A-Z][A-Z0-9_]{2,})/g;
// Common code terms to ignore (not actual code refs)
const IGNORE_CODE_REFS = new Set([
'true', 'false', 'null', 'undefined', 'string', 'number', 'boolean',
'object', 'array', 'function', 'async', 'await', 'const', 'let', 'var',
'if', 'else', 'for', 'while', 'return', 'import', 'export', 'default',
'npm', 'npx', 'node', 'yarn', 'pnpm', 'git', 'bash', 'sh', 'zsh',
'GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS',
'JSON', 'XML', 'HTML', 'CSS', 'SQL', 'API', 'URL', 'URI', 'HTTP', 'HTTPS',
'OK', 'ERROR', 'WARNING', 'INFO', 'DEBUG', 'TRACE',
'README', 'LICENSE', 'CHANGELOG', 'TODO', 'FIXME', 'NOTE', 'HACK',
'dev', 'prod', 'test', 'staging', 'production', 'development',
'src', 'lib', 'dist', 'build', 'docs', 'tests', 'config',
'index', 'main', 'app', 'server', 'client', 'utils', 'helpers'
]);
// Common env var prefixes to ignore (not project-specific)
const IGNORE_ENV_PREFIXES = ['NODE_', 'PATH', 'HOME', 'USER', 'SHELL', 'TERM', 'PWD', 'CI'];
// Markdown template variables (not actual env vars)
const IGNORE_ENV_VARS = new Set(['ARGUMENTS']);
/**
* Find all markdown files in directory.
*/
function findMarkdownFiles(dir) {
if (!fs.existsSync(dir)) return [];
return fs.readdirSync(dir)
.filter(f => f.endsWith('.md'))
.map(f => path.join(dir, f));
}
/**
* Extract code references from markdown content.
*/
function extractCodeRefs(content, filepath) {
const refs = [];
let match;
const lines = content.split('\n');
lines.forEach((line, idx) => {
// Skip code blocks
if (line.trim().startsWith('```')) return;
while ((match = CODE_REF_PATTERN.exec(line)) !== null) {
const ref = match[1];
// Filter out common terms
if (IGNORE_CODE_REFS.has(ref.replace('()', '').toLowerCase())) continue;
// Only check function calls and PascalCase classes
if (ref.endsWith('()') || /^[A-Z][a-z]/.test(ref)) {
refs.push({ ref, file: filepath, line: idx + 1 });
}
}
});
return refs;
}
/**
* Extract internal links from markdown content.
*/
function extractLinks(content, filepath) {
const links = [];
let match;
const lines = content.split('\n');
lines.forEach((line, idx) => {
while ((match = LINK_PATTERN.exec(line)) !== null) {
const href = match[2];
// Skip external links and anchors
if (href.startsWith('http') || href.startsWith('#') || href.startsWith('mailto:')) continue;
links.push({ href, file: filepath, line: idx + 1, text: match[1] });
}
});
return links;
}
/**
* Extract env var references from markdown content.
*/
function extractEnvVars(content, filepath) {
const vars = [];
let match;
const lines = content.split('\n');
lines.forEach((line, idx) => {
// Skip code blocks
if (line.trim().startsWith('```')) return;
while ((match = ENV_PATTERN.exec(line)) !== null) {
const envVar = match[1] || match[2];
// Filter common system vars and template variables
if (IGNORE_ENV_PREFIXES.some(p => envVar.startsWith(p))) continue;
if (IGNORE_ENV_VARS.has(envVar)) continue;
vars.push({ envVar, file: filepath, line: idx + 1 });
}
});
return vars;
}
/**
* Check if code reference exists in source directories.
*/
function checkCodeRefExists(ref, srcDirs) {
const name = ref.replace('()', '');
const patterns = [
`function ${name}`,
`const ${name}`,
`class ${name}`,
`def ${name}`,
`export.*${name}`,
`${name}:` // object methods
];
for (const srcDir of srcDirs) {
if (!fs.existsSync(srcDir)) continue;
for (const pattern of patterns) {
// Use spawnSync with args array to prevent command injection
const result = spawnSync('grep', ['-rl', pattern, srcDir], {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
timeout: 5000
});
if (result.status === 0 && result.stdout.trim()) {
return true;
}
}
}
return false;
}
/**
* Check if internal link target exists.
*/
function checkLinkExists(href, sourceFile) {
const sourceDir = path.dirname(sourceFile);
const targetPath = path.resolve(sourceDir, href.split('#')[0]);
return fs.existsSync(targetPath);
}
/**
* Load env vars from .env.example.
*/
function loadEnvExample(projectRoot) {
const envPath = path.join(projectRoot, '.env.example');
if (!fs.existsSync(envPath)) return new Set();
const content = fs.readFileSync(envPath, 'utf8');
const vars = new Set();
content.split('\n').forEach(line => {
const match = line.match(/^([A-Z][A-Z0-9_]+)=/);
if (match) vars.add(match[1]);
});
return vars;
}
/**
* Run all validations and generate report.
*/
function validate(docsDir, srcDirs, projectRoot) {
const issues = {
codeRefs: [],
links: [],
envVars: []
};
const stats = {
filesChecked: 0,
codeRefsChecked: 0,
linksChecked: 0,
envVarsChecked: 0,
codeRefsValid: 0,
linksValid: 0,
envVarsValid: 0
};
const mdFiles = findMarkdownFiles(docsDir);
stats.filesChecked = mdFiles.length;
if (mdFiles.length === 0) {
console.log(`No markdown files found in ${docsDir}`);
return;
}
const envExample = loadEnvExample(projectRoot);
for (const filepath of mdFiles) {
let content;
try {
content = fs.readFileSync(filepath, 'utf8');
} catch (err) {
// File deleted during validation - skip
continue;
}
const relPath = path.relative(projectRoot, filepath);
// Check code references
const codeRefs = extractCodeRefs(content, relPath);
stats.codeRefsChecked += codeRefs.length;
for (const { ref, file, line } of codeRefs) {
if (checkCodeRefExists(ref, srcDirs)) {
stats.codeRefsValid++;
} else {
issues.codeRefs.push({ ref, file, line });
}
}
// Check internal links
const links = extractLinks(content, filepath);
stats.linksChecked += links.length;
for (const { href, file, line, text } of links) {
if (checkLinkExists(href, file)) {
stats.linksValid++;
} else {
issues.links.push({ href, file: relPath, line, text });
}
}
// Check env vars
const envVars = extractEnvVars(content, relPath);
stats.envVarsChecked += envVars.length;
for (const { envVar, file, line } of envVars) {
if (envExample.has(envVar)) {
stats.envVarsValid++;
} else {
issues.envVars.push({ envVar, file, line });
}
}
}
// Generate report
console.log('\n## Docs Validation Report\n');
console.log(`**Files Checked:** ${stats.filesChecked}`);
console.log(`**Scan Date:** ${new Date().toISOString().split('T')[0]}\n`);
const hasIssues = issues.codeRefs.length || issues.links.length || issues.envVars.length;
if (hasIssues) {
console.log('### Potential Issues\n');
if (issues.codeRefs.length) {
console.log(`⚠️ **Code References** (${issues.codeRefs.length} issues)`);
for (const { ref, file, line } of issues.codeRefs.slice(0, 10)) {
console.log(`- \`${ref}\` in ${file}:${line} - not found in codebase`);
}
if (issues.codeRefs.length > 10) {
console.log(`- ... and ${issues.codeRefs.length - 10} more`);
}
console.log('');
}
if (issues.links.length) {
console.log(`⚠️ **Internal Links** (${issues.links.length} issues)`);
for (const { href, file, line } of issues.links.slice(0, 10)) {
console.log(`- \`${href}\` in ${file}:${line} - file not found`);
}
if (issues.links.length > 10) {
console.log(`- ... and ${issues.links.length - 10} more`);
}
console.log('');
}
if (issues.envVars.length) {
console.log(`⚠️ **Config Keys** (${issues.envVars.length} issues)`);
for (const { envVar, file, line } of issues.envVars.slice(0, 10)) {
console.log(`- \`${envVar}\` in ${file}:${line} - not in .env.example`);
}
if (issues.envVars.length > 10) {
console.log(`- ... and ${issues.envVars.length - 10} more`);
}
console.log('');
}
}
console.log('### Verified OK\n');
if (stats.codeRefsValid > 0) console.log(`${stats.codeRefsValid} code references validated`);
if (stats.linksValid > 0) console.log(`${stats.linksValid} internal links working`);
if (stats.envVarsValid > 0) console.log(`${stats.envVarsValid} config keys confirmed`);
if (stats.codeRefsValid === 0 && stats.linksValid === 0 && stats.envVarsValid === 0) {
console.log(' No validatable references found');
}
console.log('');
}
/**
* Parse CLI arguments.
*/
function parseArgs(args) {
const result = {
docsDir: 'docs',
srcDirs: ['src', 'lib', 'app', 'scripts', '.claude']
};
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg === '--src' && args[i + 1]) {
result.srcDirs = args[++i].split(',');
} else if (!arg.startsWith('-')) {
result.docsDir = arg;
}
}
return result;
}
// Main
const args = parseArgs(process.argv.slice(2));
const projectRoot = process.cwd();
const docsDir = path.resolve(projectRoot, args.docsDir);
const srcDirs = args.srcDirs.map(d => path.resolve(projectRoot, d));
validate(docsDir, srcDirs, projectRoot);
// Always exit 0 (non-blocking)
process.exit(0);