import { readdirSync, readFileSync } from 'node:fs'; import { dirname, join, relative, resolve, sep } from 'node:path'; import { fileURLToPath } from 'node:url'; import { describe, expect, it } from 'vitest'; // Architecture boundary of the Report Engine (Phase 7, decided 2026-09-30): // the engine lives in src/reporting with its server adapters in server/reporting, // only the server route reaches into it, and it never reaches a writing repository method. const root = resolve(dirname(fileURLToPath(import.meta.url)), '../..'); const reporting = join(root, 'src', 'reporting'); const serverReporting = join(root, 'server', 'reporting'); 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 read = (path: string) => readFileSync(path, 'utf8'); const posix = (path: string) => relative(root, path).split(sep).join('/'); interface ImportUse { specifier: string; typeOnly: boolean } function imports(source: string): ImportUse[] { const uses: ImportUse[] = []; for (const match of source.matchAll(/\bimport\s+(type\s+)?[^'";]*?\bfrom\s+['"]([^'"]+)['"]/g)) { uses.push({ specifier: match[2], typeOnly: Boolean(match[1]) }); } for (const match of source.matchAll(/\bexport\s+(type\s+)?[^'";]*?\bfrom\s+['"]([^'"]+)['"]/g)) { uses.push({ specifier: match[2], typeOnly: Boolean(match[1]) }); } for (const match of source.matchAll(/\bimport\(\s*['"]([^'"]+)['"]\s*\)/g)) { uses.push({ specifier: match[1], typeOnly: false }); } return uses; } function resolvesIntoReporting(file: string, specifier: string): boolean { if (!specifier.startsWith('.')) return false; const target = resolve(dirname(file), specifier); return target === reporting || target.startsWith(reporting + sep); } /** * Importers allowed to reach the engine: the server route and the engine's own server adapters. * Client Info additionally consumes the published impact distribution through its pinned read-only extension. */ const allowedImporters: Record = { 'src/features/search-filter/business-impact-controls.ts': 'any', 'src/features/dashboard/DashboardPanel.tsx': 'any', 'server/assessor-http-server.ts': 'any' }; const outsideFiles = [...sourceFiles(join(root, 'src')), ...sourceFiles(join(root, 'server'))] .filter(file => !file.startsWith(reporting + sep) && !file.startsWith(serverReporting + sep)); const reportingFiles = sourceFiles(reporting); const serverReportingFiles = sourceFiles(serverReporting); describe('Report Engine boundary', () => { it('keeps the report modules together in src/reporting', () => { expect(reportingFiles.map(posix).sort()).toEqual([ 'src/reporting/client-report-projection.ts', 'src/reporting/context-management-report.ts', 'src/reporting/management-business-impact.ts', 'src/reporting/management-overview-report.ts', 'src/reporting/management-report-texts.ts', 'src/reporting/report-brand.ts', 'src/reporting/report-charts.ts', 'src/reporting/report-context.ts', 'src/reporting/report-correlation.ts', 'src/reporting/report-font.ts', 'src/reporting/report-header-facts.ts', 'src/reporting/report-input-assembler.ts', 'src/reporting/report-narrative.ts', 'src/reporting/report-projection.ts', 'src/reporting/report-read-port.ts', 'src/reporting/report-rich-text.ts', 'src/reporting/report-run.ts', 'src/reporting/report-statement-text.ts', 'src/reporting/report-workspace-view.ts', 'src/reporting/security-view-report.ts', 'src/reporting/technical-report-model.ts', 'src/reporting/technical-report.ts' ]); }); it('is reached only by the server route and its own server adapters', () => { const violations = outsideFiles.flatMap(file => imports(read(file)) .filter(use => resolvesIntoReporting(file, use.specifier)) .flatMap(use => { const rule = allowedImporters[posix(file)]; if (rule === 'any' || (rule === 'type-only' && use.typeOnly)) return []; return [`${posix(file)} -> ${use.specifier}${use.typeOnly ? ' (type)' : ''}`]; })); expect(violations).toEqual([]); }); it('never reaches a writing repository method', () => { const repositorySource = read(join(root, 'src', 'persistence', 'assessor-repository.ts')); const body = repositorySource.slice(repositorySource.indexOf('export interface AssessorRepository')); const methods = [...body.matchAll(/^\s{2}(\w+)\(/gm)].map(match => match[1]); const readPrefixes = /^(?:list|get|load|query|find|export)[A-Z]/; const writeMethods = methods.filter(name => !readPrefixes.test(name)); expect(writeMethods).toContain('saveReview'); expect(writeMethods).toContain('commitCanonicalImport'); const violations = [...reportingFiles, ...serverReportingFiles].flatMap(file => writeMethods .filter(name => new RegExp(`\\.${name}\\s*\\(`).test(read(file))) .map(name => `${posix(file)} calls ${name}`)); expect(violations).toEqual([]); }); it('stays independent of UI runtimes, the server and database clients', () => { const forbidden = /^(?:react|react-dom|@tauri|pg|postgres)(?:\/|$)/; const violations = reportingFiles.flatMap(file => imports(read(file)) .filter(use => forbidden.test(use.specifier) || (use.specifier.startsWith('.') && [join(root, 'server'), join(root, 'src', 'app'), join(root, 'src', 'components')] .some(directory => resolve(dirname(file), use.specifier).startsWith(directory + sep)))) .map(use => `${posix(file)} -> ${use.specifier}`)); expect(violations).toEqual([]); }); });