Files
apophis-fastify/src/cli/commands/doctor/checks/docs.ts
T

266 lines
6.9 KiB
TypeScript
Raw Normal View History

/**
* 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;
}