#!/usr/bin/env bun /** * Fails when an implementation+test pair lives flat at a package's `src/` * root instead of inside a feature folder. * * CLAUDE.md §"Package Structure" mandates the 5-files-per-feature layout: * * src// * index.ts * .ts * .test.ts * .types.ts * .md * * The only flat exceptions allowed at `src/` root are: * - `index.ts` (package barrel) * - `types/`, `utils/`, `constants/` (per the documented exemption list) * * Any other `.ts` paired with `.test.ts` directly under `src/` * (without a `/` folder) is a layout violation and fails CI. * * Exit codes: 0 = clean, 1 = at least one violation. */ import { readdirSync, statSync } from "node:fs"; import { join } from "node:path"; const PACKAGES_DIR = "packages"; const ROOT_FLAT_EXEMPT = new Set(["index.ts"]); const ROOT_DIR_EXEMPT = new Set(["types", "utils", "constants"]); interface Violation { readonly pkg: string; readonly src: string; readonly feature: string; readonly files: readonly string[]; } function listPackages(): readonly string[] { const out: string[] = []; for (const cat of readdirSync(PACKAGES_DIR)) { const catDir = join(PACKAGES_DIR, cat); if (!statSync(catDir).isDirectory()) continue; for (const plugin of readdirSync(catDir)) { const pluginDir = join(catDir, plugin); if (!statSync(pluginDir).isDirectory()) continue; for (const pkg of readdirSync(pluginDir)) { const pkgDir = join(pluginDir, pkg); if (!statSync(pkgDir).isDirectory()) continue; const srcDir = join(pkgDir, "src"); try { if (statSync(srcDir).isDirectory()) out.push(pkgDir); } catch { // no src/ — skip } } } } return out; } function checkPackage(pkgDir: string): Violation[] { const srcDir = join(pkgDir, "src"); const entries = readdirSync(srcDir); const featuresFound = new Map(); for (const name of entries) { const full = join(srcDir, name); if (statSync(full).isDirectory()) continue; if (ROOT_FLAT_EXEMPT.has(name)) continue; const match = name.match(/^(.+?)(\.test|\.types|\.md|)\.(ts|tsx|md)$/); if (!match) continue; const feature = match[1] as string; const list = featuresFound.get(feature) ?? []; list.push(name); featuresFound.set(feature, list); } const violations: Violation[] = []; for (const [feature, files] of featuresFound) { const hasImpl = files.some((f) => f === `${feature}.ts`); const hasTest = files.some((f) => f === `${feature}.test.ts`); if (hasImpl && hasTest) { violations.push({ pkg: pkgDir, src: srcDir, feature, files, }); } } return violations; } function main(): void { const pkgs = listPackages(); const allViolations: Violation[] = []; for (const pkg of pkgs) { allViolations.push(...checkPackage(pkg)); } if (allViolations.length === 0) { console.log( `OK ${pkgs.length} packages: every .ts + .test.ts pair lives in a feature folder.` ); return; } console.error( `Found ${allViolations.length} layout violation${allViolations.length === 1 ? "" : "s"}:\n` ); for (const v of allViolations) { console.error(`FAIL ${v.pkg}`); console.error( ` feature \`${v.feature}\` is flat at src/ root; should live in src/${v.feature}/` ); for (const f of v.files) { console.error(` - src/${f}`); } } console.error( `\nCLAUDE.md §"Package Structure" mandates 5-files-per-feature layout. Only ${[...ROOT_DIR_EXEMPT].map((d) => `\`${d}/\``).join(", ")} dirs and \`index.ts\` are exempt at src/ root.` ); console.error( "Move each violation into a folder: mkdir src// && git mv src/.{ts,test.ts,md} src//, then add a src//index.ts barrel and update sibling imports." ); process.exit(1); } main();