266 lines
6.9 KiB
TypeScript
266 lines
6.9 KiB
TypeScript
|
|
/**
|
||
|
|
* 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;
|
||
|
|
}
|