/** * S8: Doctor thread - Docs and example smoke checks * * Checks: * - Docs examples match current config schema * - README/APOPHIS.md exists and is readable * - In CI mode: fail if docs drift from reality */ import { readFileSync, existsSync, readdirSync } from 'node:fs'; import { resolve } from 'node:path'; // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- export interface DocsCheckResult { name: string; status: 'pass' | 'fail' | 'warn'; message: string; detail?: string; remediation?: string; mode?: 'all' | 'verify' | 'observe' | 'qualify'; } export interface DocsCheckOptions { cwd: string; isCI: boolean; } // --------------------------------------------------------------------------- // README / APOPHIS.md check // --------------------------------------------------------------------------- /** * Check if project has documentation files. */ export function checkDocsExist(options: DocsCheckOptions): DocsCheckResult { const { cwd } = options; const readmePath = resolve(cwd, 'README.md'); const apophisPath = resolve(cwd, 'APOPHIS.md'); const hasReadme = existsSync(readmePath); const hasApophis = existsSync(apophisPath); if (hasApophis) { return { name: 'docs-exist', status: 'pass', message: 'APOPHIS.md documentation found.', mode: 'all', }; } if (hasReadme) { return { name: 'docs-exist', status: 'pass', message: 'README.md found (no APOPHIS.md).', detail: 'Consider creating APOPHIS.md for APOPHIS-specific documentation.', mode: 'all', }; } return { name: 'docs-exist', status: 'warn', message: 'No README.md or APOPHIS.md found.', detail: 'Documentation helps team members understand your APOPHIS setup.', remediation: 'Create APOPHIS.md with setup instructions for your team.', mode: 'all', }; } // --------------------------------------------------------------------------- // Config schema drift check // --------------------------------------------------------------------------- /** * Known legacy field names that should not appear in docs. */ const LEGACY_FIELD_NAMES = [ 'testMode', 'testProfiles', 'testPresets', 'envPolicies', 'usesPreset', 'routeFilter', 'testDepth', 'maxDuration', 'canVerify', ]; /** * Check if docs contain legacy field names (indicating stale docs). */ export function checkDocsSchemaDrift(options: DocsCheckOptions): DocsCheckResult { const { cwd, isCI } = options; const docsFiles = findDocsFiles(cwd); if (docsFiles.length === 0) { return { name: 'docs-schema-drift', status: 'warn', message: 'No documentation files found to check for schema drift.', mode: 'all', }; } const drift: Array<{ file: string; legacyFields: string[] }> = []; for (const file of docsFiles) { try { const content = readFileSync(file, 'utf-8'); const foundLegacy = LEGACY_FIELD_NAMES.filter(field => content.includes(field)); if (foundLegacy.length > 0) { drift.push({ file, legacyFields: foundLegacy }); } } catch { // Skip unreadable files } } if (drift.length > 0) { const details = drift .map(d => ` ${d.file}: ${d.legacyFields.join(', ')}`) .join('\n'); return { name: 'docs-schema-drift', status: isCI ? 'fail' : 'warn', message: `Found ${drift.length} documentation file(s) with legacy field names.`, detail: `Update docs to use current config schema:\n${details}\n\nRun "apophis migrate --dry-run" to see rewrites.`, remediation: 'Update docs to use current field names, or run "apophis migrate --dry-run" to see rewrites.', mode: 'all', }; } return { name: 'docs-schema-drift', status: 'pass', message: 'No schema drift detected in documentation.', mode: 'all', }; } /** * Find documentation files in the project. */ function findDocsFiles(cwd: string): string[] { const files: string[] = []; const candidates = [ 'README.md', 'APOPHIS.md', 'docs', ]; for (const candidate of candidates) { const fullPath = resolve(cwd, candidate); if (existsSync(fullPath)) { if (candidate.endsWith('.md')) { files.push(fullPath); } else { // It's a directory, scan for .md files try { const entries = readdirSync(fullPath, { withFileTypes: true }); for (const entry of entries) { if (entry.isFile() && entry.name.endsWith('.md')) { files.push(resolve(fullPath, entry.name)); } } } catch { // Skip unreadable directories } } } } return files; } // --------------------------------------------------------------------------- // Example code check // --------------------------------------------------------------------------- /** * Check if docs contain runnable examples that match current API. */ export function checkExamplesValid(options: DocsCheckOptions): DocsCheckResult { const { cwd } = options; const apophisPath = resolve(cwd, 'APOPHIS.md'); if (!existsSync(apophisPath)) { return { name: 'examples-valid', status: 'pass', message: 'No APOPHIS.md to check for examples.', mode: 'all', }; } try { const content = readFileSync(apophisPath, 'utf-8'); // Check for common example patterns const hasVerifyExample = content.includes('apophis verify'); const hasObserveExample = content.includes('apophis observe'); const hasQualifyExample = content.includes('apophis qualify'); const issues: string[] = []; if (!hasVerifyExample) { issues.push('No verify example found.'); } if (!hasObserveExample) { issues.push('No observe example found.'); } if (issues.length > 0) { return { name: 'examples-valid', status: 'warn', message: 'APOPHIS.md is missing some command examples.', detail: issues.join('\n'), remediation: 'Add examples for verify, observe, and qualify commands to APOPHIS.md.', mode: 'all', }; } return { name: 'examples-valid', status: 'pass', message: 'APOPHIS.md contains examples for core commands.', mode: 'all', }; } catch { return { name: 'examples-valid', status: 'warn', message: 'Could not read APOPHIS.md to check examples.', mode: 'all', }; } } // --------------------------------------------------------------------------- // Main docs check runner // --------------------------------------------------------------------------- /** * Run all docs checks. */ export function runDocsChecks(options: DocsCheckOptions): DocsCheckResult[] { const results: DocsCheckResult[] = []; results.push(checkDocsExist(options)); results.push(checkDocsSchemaDrift(options)); results.push(checkExamplesValid(options)); return results; }