Files
ai-codebase-doc-drift-detector/src/index.ts
T
2026-06-17 12:27:02 +00:00

127 lines
4.0 KiB
TypeScript

/**
* Core library — orchestrates scan / check / update operations.
*/
import { readFileSync, readdirSync, statSync } from 'node:fs';
import { resolve, relative, extname } from 'node:path';
import type { Config, ScannedFile } from './types.js';
import { loadConfig, hasConfig } from './config.js';
import { scanFile } from './scanner.js';
import { generateDocs, readExistingDocs } from './doc-generator.js';
import { detectDrift, renderDriftReport } from './drift-detector.js';
import { getGitRoot, getAllTrackedFiles } from './git-tracker.js';
function shouldInclude(filePath: string, config: Config, cwd: string): boolean {
const rel = relative(cwd, filePath);
const ext = extname(filePath);
if (!config.extensions.includes(ext)) return false;
for (const exc of config.exclude) {
const pattern = exc.replace(/\*\*/g, '.*').replace(/\*/g, '[^/]*');
const re = new RegExp(pattern);
if (re.test(rel)) return false;
}
for (const inc of config.include) {
const incPath = resolve(cwd, inc);
const incStat = statSync(incPath);
if (incStat.isDirectory()) {
if (rel.startsWith(relative(cwd, incPath))) return true;
} else if (rel === relative(cwd, incPath)) {
return true;
}
}
return false;
}
export function scanProject(cwd: string): { scanned: ScannedFile[]; config: Config } {
if (!hasConfig(cwd)) {
throw new Error(`No ai-doc-drift.json found in ${cwd}. Run \`ai-doc-drift init\` first.`);
}
const config = loadConfig(cwd);
const gitRoot = getGitRoot(cwd);
let files: string[] = [];
if (gitRoot) {
files = getAllTrackedFiles(cwd)
.map((f) => resolve(cwd, f))
.filter((f) => shouldInclude(f, config, cwd));
} else {
// Fallback: recursive walk of include dirs
const walk = (dir: string): string[] => {
const entries = readdirSync(dir, { withFileTypes: true });
const result: string[] = [];
for (const e of entries) {
const p = resolve(dir, e.name);
if (e.isDirectory()) {
const rel = relative(cwd, p);
let excluded = false;
for (const exc of config.exclude) {
const pattern = exc.replace(/\*\*/g, '.*').replace(/\*/g, '[^/]*');
const re = new RegExp(pattern);
if (re.test(rel)) { excluded = true; break; }
}
if (!excluded) result.push(...walk(p));
} else if (e.isFile() && shouldInclude(p, config, cwd)) {
result.push(p);
}
}
return result;
};
for (const inc of config.include) {
const incPath = resolve(cwd, inc);
if (statSync(incPath).isDirectory()) {
files.push(...walk(incPath));
} else {
files.push(incPath);
}
}
}
const scanned: ScannedFile[] = [];
for (const f of files) {
try {
const source = readFileSync(f, 'utf-8');
scanned.push(scanFile(relative(cwd, f), source));
} catch {
// skip unreadable
}
}
return { scanned, config };
}
export function runCheck(cwd: string): { report: string; exitCode: number } {
const { scanned, config } = scanProject(cwd);
const existing = readExistingDocs(resolve(cwd, config.docsDir));
if (existing.size === 0) {
return {
report: `No existing docs found in ${config.docsDir}. Run \`ai-doc-drift scan\` first.`,
exitCode: 1,
};
}
const drift = detectDrift(scanned, existing);
drift.projectName = config.projectName;
const report = renderDriftReport(drift);
return { report, exitCode: drift.items.length > 0 ? 2 : 0 };
}
export function runScan(cwd: string): { report: string; exitCode: number } {
const { scanned, config } = scanProject(cwd);
const written = generateDocs(scanned, config, cwd);
return {
report: `Generated docs:\n${written.map((w) => ` - ${w}`).join('\n')}`,
exitCode: 0,
};
}
export function runUpdate(cwd: string): { report: string; exitCode: number } {
const { scanned, config } = scanProject(cwd);
const written = generateDocs(scanned, config, cwd);
return {
report: `Updated docs:\n${written.map((w) => ` - ${w}`).join('\n')}`,
exitCode: 0,
};
}