/ scripts / update-stale-docs.js
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  });