import fs from "node:fs/promises"; import path from "node:path"; import fg from "fast-glob"; import type { CheckError, SyncCheckResult } from "../../lib/sync-check/shared"; /** * Model sync check matches db files against the `### Models` section inside * each model doc (many db files → one doc file). */ export interface ModelCategoryConfig { name: "model"; sourcePattern: string; docPattern: string; exclusions: RegExp[]; } function shouldExclude(fileName: string, exclusions: RegExp[]): boolean { return exclusions.some((pattern) => pattern.test(fileName)); } export async function runModelSyncCheck( config: ModelCategoryConfig, cwd: string, ): Promise { const errors: CheckError[] = []; const sources = await fg(config.sourcePattern, { cwd }); const docs = await fg(config.docPattern, { cwd }); const sourceBasenames = new Map(); const docModelNames = new Map(); for (const sourcePath of sources) { const fileName = path.basename(sourcePath); if (shouldExclude(fileName, config.exclusions)) continue; const basename = path.basename(sourcePath, path.extname(sourcePath)); sourceBasenames.set(basename.toLowerCase(), sourcePath); } for (const docPath of docs) { const content = await fs.readFile(path.resolve(cwd, docPath), "utf-8"); for (const name of parseModelsSection(content)) { docModelNames.set(name.toLowerCase(), docPath); } } for (const [basename, sourcePath] of sourceBasenames) { if (!docModelNames.has(basename)) { errors.push({ type: "missing-doc", category: config.name, sourcePath, expectedBasename: basename, }); } } for (const [basename, docPath] of docModelNames) { if (!sourceBasenames.has(basename)) { errors.push({ type: "orphaned-doc", category: config.name, docPath, expectedBasename: basename, }); } } return { errors, filesChecked: sourceBasenames.size + docs.length }; } /** * Parse the `### Models` section from a markdown doc and return the listed * model names. Each line matching `- ModelName` within the section is * collected until the next heading or end of file. */ export function parseModelsSection(content: string): string[] { const lines = content.split("\n"); let inSection = false; const names: string[] = []; for (const line of lines) { if (/^### Models\s*$/.test(line)) { inSection = true; continue; } if (inSection) { if (/^#{1,3}\s/.test(line)) break; const m = /^-\s+(\S+)/.exec(line); if (m) names.push(m[1]); } } return names; }