import { readdirSync, readFileSync } from 'node:fs'; import { dirname, join, relative, resolve, sep } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { beforeAll, describe, expect, it } from 'vitest'; // Binding Core/Engine rule (AGENTS.md, "Core Engine and engines"): an engine depends on the Core, // the Core never depends on an engine, and engines do not build on each other. Engine extension // points inside Core views are Core code; they are the only Core files allowed to reach an engine // and are pinned below, so every new Core -> Engine dependency fails here until it is deliberately // added as an extension point. Engine paths come from the same table the Core guard uses, so a // newly registered engine is checked without changing this test. const root = resolve(dirname(fileURLToPath(import.meta.url)), '../..'); interface AreaTable { ENGINE_PATHS: Array<[string, string[]]>; classifyPath(relative: string | null): string | null; } let areas: AreaTable; beforeAll(async () => { const modulePath = pathToFileURL(join(root, '.claude', 'hooks', 'core-areas.mjs')).href; areas = await import(/* @vite-ignore */ modulePath) as AreaTable; }); function sourceFiles(directory: string): string[] { return readdirSync(directory, { withFileTypes: true }).flatMap(entry => { const path = join(directory, entry.name); if (entry.isDirectory()) return sourceFiles(path); return /\.(?:ts|tsx)$/.test(entry.name) && !entry.name.endsWith('.d.ts') ? [path] : []; }); } const posix = (path: string) => relative(root, path).split(sep).join('/'); function relativeImports(file: string): Array<{ target: string; specifier: string; typeOnly: boolean }> { const source = readFileSync(file, 'utf8'); return [ ...[...source.matchAll(/\b(?:import|export)\s+(type\s+)?[^'";]*?\bfrom\s+['"]([^'"]+)['"]/g)] .map(match => ({ specifier: match[2], typeOnly: Boolean(match[1]) })), ...[...source.matchAll(/\bimport\(\s*['"]([^'"]+)['"]\s*\)/g)].map(match => ({ specifier: match[1], typeOnly: false })) ].filter(use => use.specifier.startsWith('.')) .map(use => ({ ...use, target: posix(resolve(dirname(file), use.specifier)) })); } function engineOf(path: string): string | null { const lower = path.toLowerCase(); for (const [engine, prefixes] of areas.ENGINE_PATHS) { if (prefixes.some(prefix => lower === prefix || lower.startsWith(prefix) || `${lower}/` === prefix)) return engine; } return null; } const allSources = () => [...sourceFiles(join(root, 'src')), ...sourceFiles(join(root, 'server'))]; /** * Pinned Core -> Engine dependencies: the documented extension points ( * `Context Cluster` Work facet, Finding Reader `Context` section, Pathfinder and sidebar cluster * readers; Finding Reader ISO mapping, Result DB ISO batch and `Export JSON`, Data Hub ISO corpus upload, * the Client Info `AI Overview` tab and the Work `ISO control`/`ISO theme` facets; the Client Info `Compare` tab, the Work `Change` facet and the Finding Reader `Compare` section), the * type-only API contract and client, and the server wiring of the engines. */ const EXTENSION_POINTS: Record = { 'src/features/related-findings/RelatedFindings.tsx': { engine: 'Remediation Engine' }, 'src/features/search-filter/business-impact-controls.ts': { engine: ['Report Engine', 'Context Engine'] }, 'src/features/dashboard/DashboardPanel.tsx': { engine: ['Context Engine', 'Report Engine', 'Compliance Engine', 'Comparison Engine'] }, 'src/features/dashboard/ClientWorkPackagesPanel.tsx': { engine: ['Remediation Engine', 'Context Engine'] }, 'src/api/assessor-api-contract.ts': { engine: ['Context Engine', 'Remediation Engine', 'Compliance Engine'], typeOnly: true }, 'src/api/assessor-api-client.ts': { engine: 'Compliance Engine', typeOnly: true }, 'src/features/finding-reader/FindingReader.tsx': { engine: ['Remediation Engine', 'Context Engine', 'Compliance Engine', 'Comparison Engine'] }, 'src/features/results-browser/ResultsBrowser.tsx': { engine: 'Compliance Engine' }, 'src/features/project-import/DataHub.tsx': { engine: 'Compliance Engine' }, 'src/features/actionlist/ActionlistControls.tsx': { engine: ['Context Engine', 'Remediation Engine', 'Compliance Engine', 'Comparison Engine'] }, 'src/features/finding-reader/FindingContext.tsx': { engine: 'Context Engine' }, 'src/features/mitre-graph/graph-filter-bridge.ts': { engine: 'Context Engine' }, 'src/features/mitre-graph/ObsidianGraphView.tsx': { engine: 'Context Engine' }, 'src/features/search-filter/filter-records.ts': { engine: ['Context Engine', 'Remediation Engine', 'Compliance Engine', 'Comparison Engine'] }, 'src/features/search-filter/SearchFilterBar.tsx': { engine: ['Context Engine', 'Remediation Engine', 'Compliance Engine', 'Comparison Engine'] }, 'src/features/search-filter/use-filtered-records.ts': { engine: ['Context Engine', 'Remediation Engine', 'Compliance Engine', 'Comparison Engine'] }, 'src/features/search-filter/work-facets.ts': { engine: ['Context Engine', 'Remediation Engine', 'Compliance Engine', 'Comparison Engine'] }, 'src/features/sidebar/SidebarSecurityDomains.tsx': { engine: ['Context Engine', 'Remediation Engine', 'Compliance Engine', 'Comparison Engine'] }, 'server/assessor-http-server.ts': { engine: 'any' }, 'server/index.ts': { engine: ['Report Engine', 'Remediation Engine', 'Compliance Engine'] } }; // Core areas that hold data, evaluation and rules. No file in them may reach any engine, // not even through an extension point. const ENGINE_FREE_AREAS = ['Ingest', 'Normalization', 'Evaluation', 'Store', 'Disclosure', 'Client Records', 'Reference Catalogs', 'Shared Kernel']; describe('Core Engine and engines boundary', () => { it('knows the registered engines and finds source files for each of them', () => { const engines = areas.ENGINE_PATHS.map(([engine]) => engine); expect(engines).toEqual(expect.arrayContaining(['Context Engine', 'Report Engine', 'Remediation Engine', 'Compliance Engine', 'Comparison Engine'])); for (const engine of engines) { expect(allSources().some(file => engineOf(posix(file)) === engine), engine).toBe(true); } }); it('keeps data, evaluation and rule areas of the Core free of every engine', () => { const violations = allSources() .filter(file => ENGINE_FREE_AREAS.includes(areas.classifyPath(posix(file).toLowerCase()) ?? '')) .flatMap(file => relativeImports(file) .filter(use => engineOf(use.target)) .map(use => `${posix(file)} (${areas.classifyPath(posix(file).toLowerCase())}) -> ${use.specifier}`)); expect(violations).toEqual([]); }); it('lets the Core reach an engine only through a pinned extension point', () => { const violations = allSources() .filter(file => !engineOf(posix(file))) .flatMap(file => relativeImports(file).flatMap(use => { const engine = engineOf(use.target); if (!engine) return []; const allowed = EXTENSION_POINTS[posix(file)]; const ok = allowed && (allowed.engine === 'any' || allowed.engine === engine || (Array.isArray(allowed.engine) && allowed.engine.includes(engine))) && (!allowed.typeOnly || use.typeOnly); return ok ? [] : [`${posix(file)} -> ${use.specifier} (${engine}${use.typeOnly ? ', type' : ''})`]; })); expect(violations).toEqual([]); }); it('keeps every pinned extension point real, so the list cannot silently grow stale', () => { const stale = Object.keys(EXTENSION_POINTS).filter(path => { const file = join(root, ...path.split('/')); return !relativeImports(file).some(use => engineOf(use.target)); }); expect(stale).toEqual([]); }); it('keeps engines independent of each other', () => { const violations = allSources().flatMap(file => { const own = engineOf(posix(file)); if (!own) return []; return relativeImports(file) .filter(use => { const other = engineOf(use.target); return other !== null && other !== own; }) .map(use => `${posix(file)} (${own}) -> ${use.specifier} (${engineOf(use.target)})`); }); expect(violations).toEqual([]); }); });