import fs from "node:fs"; import path from "node:path"; import fg from "fast-glob"; import { APP_PATHS } from "../../lib/paths"; import type { CheckError, SyncCheckResult } from "../../lib/sync-check/shared"; /** * Parse actor names from the `export type Actor = "a" | "b"` union in e2e/fixtures/user.ts. */ export function parseActorNamesFromFixture(content: string): string[] { const match = /export\s+type\s+Actor\s*=\s*([^;]+)/.exec(content); if (!match) return []; const names: string[] = []; const regex = /["']([^"']+)["']/g; let m: RegExpExecArray | null; while ((m = regex.exec(match[1])) !== null) { names.push(m[1]); } return names; } /** * Actor sync check: validates that each actor defined in e2e/fixtures/user.ts * has a corresponding doc file in docs/actor/, and vice versa. */ export async function runActorSyncCheck(appRoot: string, cwd: string): Promise { const errors: CheckError[] = []; const fixturePath = `${appRoot}/${APP_PATHS.tests.e2e}/fixtures/user.ts`; const fixtureFullPath = path.join(cwd, fixturePath); if (!fs.existsSync(fixtureFullPath)) { return { errors, filesChecked: 0 }; } const content = fs.readFileSync(fixtureFullPath, "utf-8"); const fixtureActors = new Set(parseActorNamesFromFixture(content)); const actorDocPattern = `${appRoot}/${APP_PATHS.docs.actor}/*.md`; const actorDocs = await fg(actorDocPattern, { cwd }); const docActors = new Set(actorDocs.map((p) => path.basename(p, ".md"))); for (const actor of fixtureActors) { if (!docActors.has(actor)) { errors.push({ type: "missing-actor-doc", category: "actor", sourcePath: fixturePath, expectedBasename: `${actor}.md`, }); } } for (const actor of docActors) { if (!fixtureActors.has(actor)) { errors.push({ type: "missing-actor-fixture", category: "actor", docPath: `${appRoot}/${APP_PATHS.docs.actor}/${actor}.md`, expectedBasename: actor, }); } } return { errors, filesChecked: 1 + actorDocs.length }; }