/ scripts / doc-code-check.js
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  });