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'; import { createAssessmentContext } from '../../src/assessment-context/context-run'; import { LocalAssessorRepository } from '../../src/persistence/local-assessor-repository'; // Binding boundary of the context layer (AGENTS.md, Phase 13E): a parallel, read-only second // reading. It is reached only through the server route and type-only API contract, it never calls // a writing repository method and it never changes stored ASSESSOR data. const root = resolve(dirname(fileURLToPath(import.meta.url)), '../..'); const engine = join(root, 'src', 'assessment-context'); 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('/'); function imports(source: string): Array<{ specifier: string; typeOnly: boolean }> { 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 })) ]; } const allowedImporters: Record = { 'src/features/search-filter/business-impact-controls.ts': 'type-only', 'server/assessor-http-server.ts': 'any', 'src/api/assessor-api-contract.ts': 'type-only', // Context-owned UI reads only the published heatmap projection; no Core view reads engine internals. 'src/features/context-layer/ContextHeatmap.tsx': 'any', 'src/features/context-layer/context-cluster-facet.ts': 'any' }; const engineFiles = sourceFiles(engine); describe('context layer boundary', () => { it('keeps the engine together in src/assessment-context', () => { expect(engineFiles.map(posix).sort()).toEqual([ 'src/assessment-context/context-assignment.ts', 'src/assessment-context/context-catalog.ts', 'src/assessment-context/context-cluster-severity.ts', 'src/assessment-context/context-engine.ts', 'src/assessment-context/context-gaps.ts', 'src/assessment-context/context-heatmap.ts', 'src/assessment-context/context-recognition.ts', 'src/assessment-context/context-report-publication.ts', 'src/assessment-context/context-run.ts' ]); }); it('is reached only by the server, type-only API contract and its own heatmap projection consumer', () => { const outside = [...sourceFiles(join(root, 'src')), ...sourceFiles(join(root, 'server'))] .filter(file => !file.startsWith(engine + sep)); const violations = outside.flatMap(file => imports(read(file)) .filter(use => use.specifier.startsWith('.') && resolve(dirname(file), use.specifier).startsWith(engine + sep)) .flatMap(use => { const rule = allowedImporters[posix(file)]; const publishedImpact = posix(file) === 'src/features/dashboard/DashboardPanel.tsx' && use.specifier === '../../assessment-context/context-report-publication'; return publishedImpact || rule === 'any' || (rule === 'type-only' && use.typeOnly) ? [] : [`${posix(file)} -> ${use.specifier}`]; })); expect(violations).toEqual([]); }); it('never calls a writing repository method and depends on no UI, server, report or database module', () => { const repository = read(join(root, 'src', 'persistence', 'assessor-repository.ts')); const methods = [...repository.slice(repository.indexOf('export interface AssessorRepository')).matchAll(/^\s{2}(\w+)\(/gm)].map(match => match[1]); const writeMethods = methods.filter(name => !/^(?:list|get|load|query|find|export)[A-Z]/.test(name)); expect(writeMethods).toContain('savePolicyEvaluation'); // User decision 2026-10-01: the engine reads everything Raw Data shows. That data comes from the workspace loader, // a read-only data projection (no rendering), which the Report Engine also uses in the backend. It is the only // permitted module under src/features. const rawDataLoader = join(root, 'src', 'features', 'project-import', 'load-canonical-project-workspace'); const violations = engineFiles.flatMap(file => [ ...writeMethods.filter(name => new RegExp(`\\.${name}\\s*\\(`).test(read(file))).map(name => `${posix(file)} calls ${name}`), ...imports(read(file)).filter(use => /^(?:react|react-dom|@tauri|pg|postgres)(?:\/|$)/.test(use.specifier) || (use.specifier.startsWith('.') && resolve(dirname(file), use.specifier) !== rawDataLoader && ['server', 'src/app', 'src/components', 'src/features', 'src/reporting'] .some(directory => resolve(dirname(file), use.specifier).startsWith(join(root, directory) + sep)))) .map(use => `${posix(file)} -> ${use.specifier}`) ]); expect(violations).toEqual([]); }); it('leaves every stored ASSESSOR value and the repository revision unchanged', async () => { const repository = new LocalAssessorRepository(); const timestamp = '2026-09-30T08:00:00.000Z'; await repository.saveProject({ id: 'dummy-project', name: 'Dummy Customer', createdAt: timestamp, updatedAt: timestamp, attributes: {} }); await repository.saveAssessment({ id: 'dummy-assessment', projectId: 'dummy-project', name: 'Dummy', createdAt: timestamp, updatedAt: timestamp }); const before = { revision: repository.revision, data: JSON.stringify(await repository.exportProject('dummy-project')) }; const context = await createAssessmentContext({ queryUnifiedResults: query => repository.queryUnifiedResults(query), listGpoPolicyAnalyses: projectId => repository.listGpoPolicyAnalyses(projectId), listReferenceRuns: projectId => repository.listReferenceRuns(projectId), listPolicyEvaluations: projectId => repository.listPolicyEvaluations(projectId), getEnvironmentSnapshot: query => repository.getEnvironmentSnapshot(query), currentRevision: async () => repository.revision }, { projectId: 'dummy-project', assessmentIds: ['dummy-assessment'], generatedAt: timestamp }); expect(context.repositoryRevision).toBe(before.revision); expect(repository.revision).toBe(before.revision); expect(JSON.stringify(await repository.exportProject('dummy-project'))).toBe(before.data); }); }); describe('context layer cannot influence Core score or Result DB', () => { it('allows only the isolated Client Info heatmap extension and keeps Core calculations free of context', () => { const guarded = [ ...sourceFiles(join(root, 'src', 'features', 'dashboard')), ...sourceFiles(join(root, 'src', 'scoring')), join(root, 'src', 'domain', 'security-scoring.ts'), join(root, 'src', 'domain', 'result-db-materialization.ts'), join(root, 'src', 'domain', 'result-db-disclosure.ts'), ...sourceFiles(join(root, 'server', 'postgres')) ]; const contextDirectories = [join(root, 'src', 'assessment-context'), join(root, 'src', 'features', 'context-layer')]; const violations = guarded.flatMap(file => imports(read(file)) .filter(use => use.specifier.startsWith('.') && contextDirectories.some(directory => resolve(dirname(file), use.specifier).startsWith(directory + sep))) .filter(use => !(posix(file) === 'src/features/dashboard/DashboardPanel.tsx' && ['../context-layer/ContextHeatmap', '../context-layer/context-cluster-facet', '../context-layer/use-assessment-context', '../../assessment-context/context-report-publication'].includes(use.specifier))) .map(use => `${posix(file)} -> ${use.specifier}`)); expect(violations).toEqual([]); }); });