doc-code-check.js
1 #!/usr/bin/env node 2 /** 3 * Automated Documentation/Code Consistency Check 4 * 5 * Verifies alignment between documentation and actual codebase: 6 * - README.md vs package.json scripts 7 * - CLAUDE.md instructions vs code patterns 8 * - API endpoint documentation 9 * - Database schema docs vs migrations 10 * - TODO.md completed tasks 11 * - Documentation examples accuracy 12 * - Environment variables consistency 13 * 14 * Generates a detailed report with discrepancies and recommendations. 15 */ 16 17 import { execSync } from 'child_process'; 18 import fs from 'fs'; 19 import path from 'path'; 20 import { fileURLToPath } from 'url'; 21 22 const __filename = fileURLToPath(import.meta.url); 23 const __dirname = path.dirname(__filename); 24 25 const log = { 26 info: msg => console.log(`[INFO] ${msg}`), 27 success: msg => console.log(`[SUCCESS] ${msg}`), 28 warn: msg => console.log(`[WARN] ${msg}`), 29 error: msg => console.error(`[ERROR] ${msg}`), 30 }; 31 32 // Utility to run commands 33 function runCommand(command, options = {}) { 34 const { silent = false, ignoreError = false } = options; 35 36 try { 37 const output = execSync(command, { 38 encoding: 'utf8', 39 stdio: silent ? 'pipe' : 'inherit', 40 cwd: path.join(__dirname, '..'), 41 }); 42 return { success: true, output }; 43 } catch (error) { 44 if (!ignoreError) { 45 log.error(`Command failed: ${command}`); 46 } 47 return { success: false, error, output: error.stdout || '' }; 48 } 49 } 50 51 // Generate timestamp for reports 52 function getTimestamp() { 53 return new Date().toISOString().replace(/[:.]/g, '-').split('T')[0]; 54 } 55 56 // Extract npm scripts from package.json 57 function getNpmScripts() { 58 const packagePath = path.join(__dirname, '..', 'package.json'); 59 const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8')); 60 return Object.keys(packageJson.scripts || {}); 61 } 62 63 // Extract code blocks from markdown 64 function extractCodeBlocks(content) { 65 const codeBlockRegex = /```(?:bash|sh|shell)?\n([\s\S]*?)```/g; 66 const blocks = []; 67 let match; 68 69 while ((match = codeBlockRegex.exec(content)) !== null) { 70 blocks.push(match[1].trim()); 71 } 72 73 return blocks; 74 } 75 76 // Check if script is documented in README 77 function checkScriptsDocumentation(scripts, readmeContent) { 78 const undocumented = []; 79 const documented = []; 80 81 for (const script of scripts) { 82 // Skip internal/utility scripts 83 if (script.startsWith('pre') || script.startsWith('post') || script === 'prepare') { 84 continue; 85 } 86 87 // Check if mentioned in README 88 const patterns = [`npm run ${script}`, `\`${script}\``, `"${script}"`, `'${script}'`]; 89 90 const isDocumented = patterns.some(pattern => readmeContent.includes(pattern)); 91 92 if (isDocumented) { 93 documented.push(script); 94 } else { 95 undocumented.push(script); 96 } 97 } 98 99 return { documented, undocumented }; 100 } 101 102 // Extract environment variables from .env.example 103 function getEnvVariables(envContent) { 104 return envContent 105 .split('\n') 106 .filter(line => line.trim() && !line.trim().startsWith('#')) 107 .map(line => line.split('=')[0].trim()); 108 } 109 110 // Check if env var is documented 111 function checkEnvDocumentation(envVars, readmeContent, claudeContent) { 112 const undocumented = []; 113 114 for (const envVar of envVars) { 115 const inReadme = readmeContent.includes(envVar); 116 const inClaude = claudeContent.includes(envVar); 117 118 if (!inReadme && !inClaude) { 119 undocumented.push(envVar); 120 } 121 } 122 123 return undocumented; 124 } 125 126 async function main() { 127 log.info('=== Automated Documentation/Code Check ==='); 128 log.info('Verifying documentation accuracy...\n'); 129 130 const reportDir = path.join(__dirname, '..', '.analysis-reports'); 131 if (!fs.existsSync(reportDir)) { 132 fs.mkdirSync(reportDir, { recursive: true }); 133 } 134 135 const timestamp = getTimestamp(); 136 const reportPath = path.join(reportDir, `doc-check-${timestamp}.md`); 137 138 // Initialize report 139 const reportSections = []; 140 reportSections.push('# Documentation/Code Consistency Report'); 141 reportSections.push(`\nGenerated: ${new Date().toISOString()}\n`); 142 143 // Load documentation files 144 const readmePath = path.join(__dirname, '..', 'README.md'); 145 const claudePath = path.join(__dirname, '..', 'CLAUDE.md'); 146 const envExamplePath = path.join(__dirname, '..', '.env.example'); 147 const todoPath = path.join(__dirname, '..', 'docs', 'TODO.md'); 148 149 const readmeContent = fs.existsSync(readmePath) ? fs.readFileSync(readmePath, 'utf8') : ''; 150 const claudeContent = fs.existsSync(claudePath) ? fs.readFileSync(claudePath, 'utf8') : ''; 151 const envExampleContent = fs.existsSync(envExamplePath) 152 ? fs.readFileSync(envExamplePath, 'utf8') 153 : ''; 154 const todoContent = fs.existsSync(todoPath) ? fs.readFileSync(todoPath, 'utf8') : ''; 155 156 // 1. Check npm scripts vs README 157 log.info('1. Comparing package.json scripts with README.md...'); 158 reportSections.push('## 1. NPM Scripts Documentation\n'); 159 160 const scripts = getNpmScripts(); 161 const { documented, undocumented } = checkScriptsDocumentation(scripts, readmeContent); 162 163 reportSections.push(`- Total npm scripts: ${scripts.length}`); 164 reportSections.push(`- Documented: ${documented.length}`); 165 reportSections.push(`- Undocumented: ${undocumented.length}`); 166 167 if (undocumented.length > 0) { 168 reportSections.push('\n### Undocumented Scripts\n'); 169 undocumented.forEach(script => { 170 reportSections.push(`- \`npm run ${script}\``); 171 }); 172 reportSections.push( 173 '\n**Recommendation**: Add these scripts to README.md with usage examples\n' 174 ); 175 } else { 176 reportSections.push('\n- ✅ All scripts are documented\n'); 177 } 178 179 // 2. Check environment variables 180 log.info('2. Checking environment variables documentation...'); 181 reportSections.push('## 2. Environment Variables Documentation\n'); 182 183 if (envExampleContent) { 184 const envVars = getEnvVariables(envExampleContent); 185 const undocumentedEnv = checkEnvDocumentation(envVars, readmeContent, claudeContent); 186 187 reportSections.push(`- Total env variables: ${envVars.length}`); 188 reportSections.push(`- Undocumented: ${undocumentedEnv.length}`); 189 190 if (undocumentedEnv.length > 0) { 191 reportSections.push('\n### Undocumented Environment Variables\n'); 192 undocumentedEnv.forEach(envVar => { 193 reportSections.push(`- \`${envVar}\``); 194 }); 195 reportSections.push( 196 '\n**Recommendation**: Document these variables in README.md or CLAUDE.md\n' 197 ); 198 } else { 199 reportSections.push('\n- ✅ All environment variables are documented\n'); 200 } 201 } else { 202 reportSections.push('- ⚠️ .env.example not found\n'); 203 } 204 205 // 3. Check TODO.md for completed tasks 206 log.info('3. Checking TODO.md for completed tasks...'); 207 reportSections.push('## 3. TODO.md Review\n'); 208 209 if (todoContent) { 210 const completedTasks = todoContent 211 .split('\n') 212 .filter(line => line.includes('✅') || line.includes('[x]')); 213 214 reportSections.push(`- Completed tasks: ${completedTasks.length}`); 215 216 if (completedTasks.length > 10) { 217 reportSections.push('\n- ⚠️ Many completed tasks found'); 218 reportSections.push('- **Recommendation**: Archive completed tasks to CHANGELOG.md\n'); 219 } else if (completedTasks.length > 0) { 220 reportSections.push('\n- ✅ Completed tasks are manageable\n'); 221 } else { 222 reportSections.push('\n- ✅ No completed tasks to archive\n'); 223 } 224 } else { 225 reportSections.push('- ⚠️ TODO.md not found\n'); 226 } 227 228 // 4. Check code examples in documentation 229 log.info('4. Validating code examples...'); 230 reportSections.push('## 4. Code Example Validation\n'); 231 232 const readmeBlocks = extractCodeBlocks(readmeContent); 233 const claudeBlocks = extractCodeBlocks(claudeContent); 234 235 reportSections.push(`- README.md code blocks: ${readmeBlocks.length}`); 236 reportSections.push(`- CLAUDE.md code blocks: ${claudeBlocks.length}`); 237 238 // Check if common npm scripts in examples exist 239 const allBlocks = [...readmeBlocks, ...claudeBlocks]; 240 const missingScripts = []; 241 242 for (const block of allBlocks) { 243 const npmRunMatches = block.match(/npm run ([\w:-]+)/g) || []; 244 245 for (const match of npmRunMatches) { 246 const scriptName = match.replace('npm run ', ''); 247 if (!scripts.includes(scriptName)) { 248 missingScripts.push(scriptName); 249 } 250 } 251 } 252 253 const uniqueMissing = [...new Set(missingScripts)]; 254 255 if (uniqueMissing.length > 0) { 256 reportSections.push('\n### Scripts Referenced but Not Found\n'); 257 uniqueMissing.forEach(script => { 258 reportSections.push(`- \`npm run ${script}\``); 259 }); 260 reportSections.push('\n**Recommendation**: Update documentation to use correct script names\n'); 261 } else { 262 reportSections.push('\n- ✅ All script references are valid\n'); 263 } 264 265 // 5. Database schema consistency 266 log.info('5. Checking database schema documentation...'); 267 reportSections.push('## 5. Database Schema Documentation\n'); 268 269 const schemaPath = path.join(__dirname, '..', 'db', 'schema.sql'); 270 const migrationsDir = path.join(__dirname, '..', 'db', 'migrations'); 271 272 if (fs.existsSync(schemaPath)) { 273 const schemaContent = fs.readFileSync(schemaPath, 'utf8'); 274 const tables = (schemaContent.match(/CREATE TABLE (\w+)/gi) || []).map(match => 275 match.replace(/CREATE TABLE /i, '') 276 ); 277 278 reportSections.push(`- Tables in schema.sql: ${tables.length}`); 279 280 // Check if migrations directory exists 281 if (fs.existsSync(migrationsDir)) { 282 const migrations = fs.readdirSync(migrationsDir).filter(f => f.endsWith('.sql')); 283 reportSections.push(`- Migration files: ${migrations.length}`); 284 285 if (migrations.length > 0) { 286 reportSections.push('\n- ✅ Migration system in place'); 287 reportSections.push('- **Recommendation**: Verify schema.sql reflects latest migrations\n'); 288 } else { 289 reportSections.push('\n- ⚠️ No migration files found\n'); 290 } 291 } else { 292 reportSections.push('- ⚠️ Migrations directory not found\n'); 293 } 294 295 // Check if tables are documented 296 const undocumentedTables = tables.filter( 297 table => !readmeContent.includes(table) && !claudeContent.includes(table) 298 ); 299 300 if (undocumentedTables.length > 0) { 301 reportSections.push('\n### Undocumented Tables\n'); 302 undocumentedTables.forEach(table => { 303 reportSections.push(`- \`${table}\``); 304 }); 305 reportSections.push('\n**Recommendation**: Document table schemas in CLAUDE.md\n'); 306 } else { 307 reportSections.push('\n- ✅ All tables are documented\n'); 308 } 309 } else { 310 reportSections.push('- ⚠️ schema.sql not found\n'); 311 } 312 313 // 6. Summary 314 reportSections.push('## Summary\n'); 315 316 const issueCount = 317 undocumented.length + 318 (envExampleContent 319 ? checkEnvDocumentation(getEnvVariables(envExampleContent), readmeContent, claudeContent) 320 .length 321 : 0) + 322 uniqueMissing.length; 323 324 if (issueCount === 0) { 325 reportSections.push('✅ **All documentation is consistent with code!**\n'); 326 } else { 327 reportSections.push(`⚠️ Found ${issueCount} documentation inconsistencies.\n`); 328 reportSections.push('Review the recommendations above and update documentation accordingly.\n'); 329 } 330 331 reportSections.push('### Quick Actions\n'); 332 reportSections.push('```bash'); 333 reportSections.push('# Update README.md with missing scripts'); 334 reportSections.push('# Update .env.example documentation'); 335 reportSections.push('# Archive completed TODO items'); 336 reportSections.push('# Verify code examples are current'); 337 reportSections.push('```\n'); 338 339 // Write report 340 const reportContent = reportSections.join('\n'); 341 fs.writeFileSync(reportPath, reportContent); 342 343 log.success(`\nReport generated: ${reportPath}`); 344 log.info('\nPreview:'); 345 console.log(`\n${reportContent}`); 346 347 log.info('\n=== Check Complete ==='); 348 } 349 350 main().catch(error => { 351 log.error('Unexpected error:'); 352 console.error(error); 353 process.exit(1); 354 });