/** * Architectural layer guard. Asserts dependencies point inward only: * * domain/ → no imports from storage/app/ui/pi (pure, only node: + relative) * ui/ → may import domain + pi-tui (NO node:fs — view must not persist) * storage/ → may import domain (implements the ports) * root → pi-tracker.ts + index.ts: the only place wiring pi-coding-agent * * Run: npm run check:layers (node --import tsx ./check-layers.ts) * Exits non-zero on any violation. */ import { readdirSync, readFileSync, statSync } from "node:fs"; import { extname, relative, resolve } from "node:path"; const ROOT = import.meta.dirname; function walk(dir: string): string[] { const out: string[] = []; let entries: string[]; try { entries = readdirSync(dir); } catch { return []; } for (const e of entries) { const full = resolve(dir, e); if (statSync(full).isDirectory()) { out.push(...walk(full)); } else if (extname(e) === ".ts") { out.push(full); } } return out; } const files = [ ...walk(resolve(ROOT, "src/domain")), ...walk(resolve(ROOT, "src/app")), ...walk(resolve(ROOT, "src/ui")), ...walk(resolve(ROOT, "src/storage")), resolve(ROOT, "pi-tracker.ts"), resolve(ROOT, "index.ts"), ]; const layer = (p: string): "domain" | "app" | "ui" | "storage" | "root" => { const r = relative(ROOT, p); if (r.startsWith("src/domain/")) return "domain"; if (r.startsWith("src/app/")) return "app"; if (r.startsWith("src/ui/")) return "ui"; if (r.startsWith("src/storage/")) return "storage"; return "root"; }; const IMPORT_RE = /(?:import|export)\s[^;]*?from\s+["']([^"']+)["']/g; let violations = 0; for (const file of files) { const src = readFileSync(file, "utf8"); const L = layer(file); IMPORT_RE.lastIndex = 0; let m: RegExpExecArray | null; while ((m = IMPORT_RE.exec(src))) { const spec = m[1]; const errs: string[] = []; // domain must be pure: no fs, no pi, no upper layers if (L === "domain") { if (spec.startsWith("node:fs")) errs.push("imports node:fs (must stay pure)"); if (spec.includes("pi-coding-agent") || spec.includes("pi-tui")) errs.push("imports pi (must stay pure)"); if (spec.includes("/storage") || spec.includes("/app") || spec.includes("/ui")) errs.push("imports an outer layer"); } // ui must not persist: no direct node:fs if (L === "ui") { if (spec.startsWith("node:fs")) errs.push("imports node:fs (view must not persist)"); } if (errs.length) { console.error(`✗ ${relative(ROOT, file)}: "${spec}" → ${errs.join(", ")}`); violations++; } } } if (violations > 0) { console.error(`\n${violations} layer violation(s)`); process.exit(1); } console.log(`✓ layer dependencies point inward (${files.length} files checked, no violations)`);