// Core Engine area map shared by core-guard.mjs and core-release.mjs (AGENTS.md, "Core Engine and engines"). // Paths are repository-relative and lowercase; the first matching prefix wins. import path from 'node:path'; export const RELEASE_FILE = '.claude/core-release.json'; export const ALL_AREAS = 'alle'; export const GOVERNANCE = 'Governance'; export const WHOLE_REPOSITORY = 'Gesamtes Repository'; // Engine paths precede the Core areas they sit in. export const ENGINE_PATHS = [ ['Context Engine', ['src/assessment-context/', 'src/features/context-layer/', 'tests/assessment-context/', 'tests/features/finding-context-layer.test.tsx', 'tests/features/pathfinder-context-layer.test.tsx', 'tests/features/context-cluster-facet.test.tsx', 'tests/context-browser-check.mjs']], ['Report Engine', ['src/reporting/', 'server/reporting/', 'tests/reporting/', 'tests/server/report-archive.test.ts', 'tests/postgres/report-read-only-runtime.test.ts']], ['Compliance Engine', ['src/compliance/', 'server/compliance/', 'tests/compliance/']], ]; export const CORE_AREAS = [ [GOVERNANCE, ['agents.md', 'claude.md', '.claude/hooks/', '.claude/settings.json']], // Client-owned records (reviews, risk acceptances, review transfer on reimport). They must survive // every reimport; their server persistence stays inside the Store repository. ['Client Records', ['src/domain/review.ts', 'src/domain/risk-acceptance.ts', 'src/domain/reimport-review-transfer.ts', 'src/persistence/review-store.ts', 'src/features/review-workspace/', 'src/features/dashboard/riskregister.tsx']], ['Disclosure', ['src/domain/result-db-disclosure.ts', 'src/exports/']], ['Ingest', ['src/importers/', 'src/recognition/', 'src/profiles/', 'src/analysis/', 'src/gpo/', 'server/gpo/', 'server/import-executor.ts', 'server/import-worker-client.ts', 'server/import-worker.ts', 'server/node-import-runtime-adapters.ts', 'server/artifacts/']], ['Normalization', ['src/domain/']], ['Evaluation', ['src/evaluation/', 'src/comparison/', 'src/scoring/']], ['Store', ['src/persistence/', 'server/postgres/', 'server/repository-factory.ts', 'docker-compose.postgres.yml']], ['Reference Catalogs', ['src/references/', 'server/reference-data/']], ['Shared Kernel', ['src/shared/', 'src/vite-env.d.ts']], ['Presentation', ['src/app/', 'src/features/', 'src/components/', 'src/styles/', 'src/hooks/', 'src/assets/', 'src/main.tsx', 'index.html']], ['Platform', ['server/', 'src/api/', 'src/platform/', 'src/legacy/', 'package.json', 'package-lock.json', 'vite.config.ts', 'vite.client.config.ts', 'vite.server.config.ts', 'vitest.config.ts', 'tsconfig.json', 'tsconfig.app.json', 'tsconfig.node.json', 'tsconfig.server.json', 'dockerfile', 'docker-compose.deploy.yml', 'deploy/', 'scripts/']], ['Core Tests', ['tests/', 'fixtures/']], // Any other source path stays Core until it is registered as an engine above. ['Unclassified Source', ['src/']], ]; // Spellings accepted after "Core-Freigabe:", mapped to the area names above. const AREA_ALIASES = new Map([ ['alle', ALL_AREAS], ['all', ALL_AREAS], ['core', ALL_AREAS], ['governance', GOVERNANCE], ['client records', 'Client Records'], ['kundendaten', 'Client Records'], ['reviews', 'Client Records'], ['disclosure', 'Disclosure'], ['result db', 'Disclosure'], ['ingest', 'Ingest'], ['import', 'Ingest'], ['normalization', 'Normalization'], ['normalisierung', 'Normalization'], ['evaluation', 'Evaluation'], ['bewertung', 'Evaluation'], ['scoring', 'Evaluation'], ['store', 'Store'], ['persistenz', 'Store'], ['datenbank', 'Store'], ['reference catalogs', 'Reference Catalogs'], ['referenzkataloge', 'Reference Catalogs'], ['shared kernel', 'Shared Kernel'], ['presentation', 'Presentation'], ['präsentation', 'Presentation'], ['praesentation', 'Presentation'], ['ui', 'Presentation'], ['platform', 'Platform'], ['plattform', 'Platform'], ['core tests', 'Core Tests'], ['tests', 'Core Tests'], ['unclassified source', 'Unclassified Source'], ]); export function resolveAreaName(raw) { return AREA_ALIASES.get(raw.trim().toLowerCase().replace(/\s+/g, ' ')) ?? null; } export function relativeToProject(filePath, projectDir) { const normalized = path.resolve(projectDir, filePath); const relative = path.relative(projectDir, normalized).split(path.sep).join('/'); if (relative.startsWith('..') || path.isAbsolute(relative)) return null; return relative.toLowerCase(); } function matchPrefix(relative, table) { for (const [name, prefixes] of table) { // A bare folder name (no trailing slash, e.g. a renamed directory) belongs to the prefix it names. if (prefixes.some((prefix) => relative === prefix || relative.startsWith(prefix) || `${relative}/` === prefix)) return name; } return null; } export function classifyPath(relative) { if (relative === null) return null; if (matchPrefix(relative, ENGINE_PATHS)) return null; return matchPrefix(relative, CORE_AREAS); } export function isReleased(area, releasedAreas) { if (releasedAreas.includes(area)) return true; // "alle" never covers Governance: the guard and these rules must always be named. return area !== GOVERNANCE && releasedAreas.includes(ALL_AREAS); }