update-stale-docs.js
1 #!/usr/bin/env node 2 3 /** 4 * Stale Documentation Auto-Update 5 * 6 * Uses Claude CLI to update stale documentation based on current codebase state. 7 * 8 * Process: 9 * 1. Identify stale documentation files (not modified in 30+ days) 10 * 2. For each doc, read current content + related code 11 * 3. Use Claude CLI to update documentation 12 * 4. Validate changes make sense 13 * 5. Commit to autofix branch 14 * 15 * Skips: Architectural decisions and breaking changes (flagged for human review) 16 */ 17 18 import { execSync, execFileSync } from 'child_process'; 19 import { readFileSync, writeFileSync, existsSync, statSync } from 'fs'; 20 import { join, basename } from 'path'; 21 import { fileURLToPath } from 'url'; 22 import { dirname } from 'path'; 23 24 const __filename = fileURLToPath(import.meta.url); 25 const __dirname = dirname(__filename); 26 const projectRoot = join(__dirname, '..'); 27 28 // Configuration 29 const MODEL = process.env.DOC_UPDATE_MODEL || 'sonnet'; 30 const STALE_THRESHOLD_DAYS = parseInt(process.env.DOC_STALE_DAYS || '30', 10); 31 32 console.log('Stale Documentation Auto-Update\n'); 33 console.log(`Model: ${MODEL}`); 34 console.log(`Stale Threshold: ${STALE_THRESHOLD_DAYS} days\n`); 35 36 // Validate claude CLI is available 37 try { 38 execSync('which claude', { stdio: 'ignore' }); 39 } catch { 40 console.error('Error: claude CLI not found in PATH'); 41 process.exit(1); 42 } 43 44 const stats = { 45 filesChecked: 0, 46 filesUpdated: 0, 47 filesSkipped: 0, 48 humanReviewFlagged: 0, 49 updatedDocs: [], 50 humanReviewQueue: [], 51 }; 52 53 /** 54 * Get documentation files that are stale 55 */ 56 function getStaleDocFiles() { 57 const docPaths = [ 58 'README.md', 59 'CLAUDE.md', 60 'docs/TODO.md', 61 'docs/DASHBOARD.md', 62 'docs/BEST-PRACTICES-EMAIL.md', 63 'docs/BEST-PRACTICES-SMS.md', 64 'docs/CULTURAL-PRICING.md', 65 'docs/MAINTENANCE.md', 66 '.env.example', 67 ]; 68 69 const staleFiles = []; 70 71 for (const docPath of docPaths) { 72 const fullPath = join(projectRoot, docPath); 73 if (!existsSync(fullPath)) continue; 74 75 const fileStats = statSync(fullPath); 76 const daysSinceModified = Math.floor((Date.now() - fileStats.mtimeMs) / (1000 * 60 * 60 * 24)); 77 78 if (daysSinceModified > STALE_THRESHOLD_DAYS) { 79 staleFiles.push({ 80 path: docPath, 81 fullPath, 82 daysSinceModified, 83 }); 84 } 85 } 86 87 return staleFiles; 88 } 89 90 /** 91 * Get package.json scripts for context 92 */ 93 function getPackageScripts() { 94 const packagePath = join(projectRoot, 'package.json'); 95 const packageJson = JSON.parse(readFileSync(packagePath, 'utf-8')); 96 return packageJson.scripts || {}; 97 } 98 99 /** 100 * Get recent git commits for context 101 */ 102 function getRecentCommits(count = 10) { 103 try { 104 const commits = execSync(`git log -${count} --pretty=format:"%h|%s|%ar"`, { 105 cwd: projectRoot, 106 encoding: 'utf-8', 107 }); 108 return commits.trim(); 109 } catch { 110 return ''; 111 } 112 } 113 114 /** 115 * Update a stale documentation file 116 */ 117 async function updateDocFile(file) { 118 console.log(`\nš Updating ${file.path}`); 119 console.log(` Last modified: ${file.daysSinceModified} days ago`); 120 121 stats.filesChecked++; 122 123 // Read current content 124 const currentContent = readFileSync(file.fullPath, 'utf-8'); 125 126 // Get context 127 const packageScripts = getPackageScripts(); 128 const recentCommits = getRecentCommits(); 129 130 // Build prompt for Claude 131 const prompt = `You are a technical documentation expert. Update this stale documentation to reflect current state of the codebase. 132 133 DOCUMENTATION FILE: ${file.path} 134 LAST MODIFIED: ${file.daysSinceModified} days ago 135 136 CURRENT CONTENT: 137 \`\`\`markdown 138 ${currentContent} 139 \`\`\` 140 141 CONTEXT - Recent npm scripts (from package.json): 142 ${Object.entries(packageScripts) 143 .slice(0, 20) 144 .map(([name, cmd]) => ` ${name}: ${cmd}`) 145 .join('\n')} 146 147 CONTEXT - Recent commits: 148 ${recentCommits} 149 150 INSTRUCTIONS: 151 - Update outdated information to match current codebase 152 - Add documentation for new features/scripts found in context 153 - Remove documentation for deprecated features 154 - Keep the same tone and structure 155 - Preserve all existing formatting and markdown 156 - If you encounter ARCHITECTURAL DECISIONS or BREAKING CHANGES, flag them with: 157 <!-- HUMAN_REVIEW_REQUIRED: [reason] --> 158 - Do NOT make architectural decisions yourself 159 - Do NOT remove important historical context 160 - Focus on factual accuracy, not style changes 161 162 FLAGS FOR HUMAN REVIEW: 163 - Architecture decisions (e.g., "should we use X or Y?") 164 - Breaking changes (e.g., "removed support for Z") 165 - New major dependencies or tools 166 - Security policy changes 167 - API design decisions 168 169 OUTPUT THE COMPLETE UPDATED DOCUMENTATION:`; 170 171 try { 172 const updatedContent = execFileSync('claude', ['-p', '--model', MODEL, '--output-format', 'text'], { 173 input: prompt, 174 encoding: 'utf-8', 175 timeout: 180000, 176 maxBuffer: 10 * 1024 * 1024, 177 }).trim(); 178 179 // Check if human review is required 180 const needsHumanReview = updatedContent.includes('HUMAN_REVIEW_REQUIRED'); 181 182 if (needsHumanReview) { 183 console.log(' ā ļø Contains items requiring human review - flagging'); 184 185 // Extract review items 186 const reviewMatches = updatedContent.matchAll(/<!-- HUMAN_REVIEW_REQUIRED: (.*?) -->/g); 187 for (const match of reviewMatches) { 188 stats.humanReviewQueue.push({ 189 file: file.path, 190 reason: match[1], 191 type: 'documentation', 192 }); 193 } 194 195 stats.humanReviewFlagged++; 196 197 // Still write the updated content (with flags intact) 198 writeFileSync(file.fullPath, updatedContent); 199 stats.filesUpdated++; 200 stats.updatedDocs.push(file.path); 201 202 console.log(' ā Updated (with human review flags)'); 203 } else { 204 // No human review needed - safe to apply 205 writeFileSync(file.fullPath, updatedContent); 206 stats.filesUpdated++; 207 stats.updatedDocs.push(file.path); 208 209 console.log(' ā Updated successfully'); 210 } 211 212 return true; 213 } catch (error) { 214 console.error(` ā Update failed: ${error.message}`); 215 stats.filesSkipped++; 216 return false; 217 } 218 } 219 220 /** 221 * Save human review queue to database 222 */ 223 async function saveHumanReviewQueue() { 224 if (stats.humanReviewQueue.length === 0) return; 225 226 const Database = await import('better-sqlite3'); 227 const db = new Database.default(join(projectRoot, 'db/sites.db')); 228 229 try { 230 // Create table if it doesn't exist 231 db.exec(` 232 CREATE TABLE IF NOT EXISTS human_review_queue ( 233 id INTEGER PRIMARY KEY AUTOINCREMENT, 234 file TEXT, 235 reason TEXT, 236 type TEXT, 237 status TEXT DEFAULT 'pending', 238 created_at TEXT DEFAULT (datetime('now')), 239 reviewed_at TEXT, 240 reviewed_by TEXT 241 ) 242 `); 243 244 // Insert items 245 const stmt = db.prepare(` 246 INSERT INTO human_review_queue (file, reason, type) 247 VALUES (?, ?, ?) 248 `); 249 250 for (const item of stats.humanReviewQueue) { 251 stmt.run(item.file, item.reason, item.type); 252 } 253 254 console.log(`\nš Saved ${stats.humanReviewQueue.length} items to human review queue`); 255 } finally { 256 db.close(); 257 } 258 } 259 260 /** 261 * Main execution 262 */ 263 async function main() { 264 // Get stale documentation files 265 console.log('š Checking for stale documentation...\n'); 266 const staleFiles = getStaleDocFiles(); 267 268 if (staleFiles.length === 0) { 269 console.log('ā All documentation is up to date!'); 270 return; 271 } 272 273 console.log(`Found ${staleFiles.length} stale documentation files:`); 274 staleFiles.forEach(file => { 275 console.log(` - ${file.path} (${file.daysSinceModified} days old)`); 276 }); 277 278 // Update each file 279 for (const file of staleFiles) { 280 await updateDocFile(file); 281 } 282 283 // Save human review queue 284 if (stats.humanReviewQueue.length > 0) { 285 await saveHumanReviewQueue(); 286 } 287 288 // Print summary 289 console.log(`\n${'ā'.repeat(60)}`); 290 console.log('\nš Documentation Update Summary\n'); 291 console.log('ā'.repeat(60)); 292 console.log(`Files Checked: ${stats.filesChecked}`); 293 console.log(`Files Updated: ${stats.filesUpdated}`); 294 console.log(`Files Skipped: ${stats.filesSkipped}`); 295 console.log(`Human Review Flagged: ${stats.humanReviewFlagged}`); 296 297 if (stats.updatedDocs.length > 0) { 298 console.log('\nUpdated Documentation:'); 299 stats.updatedDocs.forEach(doc => console.log(` ā ${doc}`)); 300 } 301 302 if (stats.humanReviewQueue.length > 0) { 303 console.log('\nā ļø Items Requiring Human Review:'); 304 stats.humanReviewQueue.forEach(item => { 305 console.log(` š ${item.file}: ${item.reason}`); 306 }); 307 console.log('\nView in System Health dashboard or run:'); 308 console.log( 309 ' sqlite3 db/sites.db "SELECT * FROM human_review_queue WHERE status = \'pending\'"' 310 ); 311 } 312 313 console.log(`\n${'ā'.repeat(60)}\n`); 314 315 process.exit(stats.filesUpdated > 0 ? 0 : 1); 316 } 317 318 // Run 319 main().catch(error => { 320 console.error('ā Fatal error:', error); 321 process.exit(1); 322 });