"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.detectApplicationLayeringFindings = detectApplicationLayeringFindings;
const path = __importStar(require("node:path"));
const node_fs_1 = require("node:fs");
const ts = __importStar(require("typescript"));
const moduleDiscovery_1 = require("./moduleDiscovery");
const archpilotIgnore_1 = require("./smartInit/archpilotIgnore");
const validationProfiling_1 = require("./validationProfiling");
const sourceScopeClassification_1 = require("./sourceScopeClassification");
const ApplicationLayeringRuleIds = {
    CONTROLLER_BUSINESS_LOGIC: 'AP-APP-001',
    REPOSITORY_BUSINESS_LOGIC: 'AP-APP-002',
    SERVICE_BYPASSES_REPOSITORY: 'AP-APP-003',
    PACKAGE_RESPONSIBILITY_MISMATCH: 'AP-APP-004',
    SERVICE_GOD_CLASS: 'AP-APP-005',
};
const maxFileSizeBytes = 384 * 1024;
const supportedExtensions = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.java', '.cs', '.py', '.php', '.go']);
const ignoredDirectoryNames = new Set([
    '.git',
    '.archpilot',
    '.next',
    '.vscode-test',
    'node_modules',
    'dist',
    'build',
    'out',
    'coverage',
    'test',
    'tests',
    '__tests__',
    'fixtures',
    'smoke',
    'examples',
    'docs',
    'reports',
    'generated',
]);
function normalizePath(value) {
    return value.replace(/\\/g, '/');
}
function toPosixRelative(workspaceRoot, absolutePath) {
    return normalizePath(path.relative(workspaceRoot, absolutePath));
}
async function readTextFileIfReasonable(filePath, sourceInventory) {
    if (sourceInventory) {
        return sourceInventory.readText(filePath, { maxFileSizeBytes });
    }
    try {
        const stats = await node_fs_1.promises.stat(filePath);
        if (stats.size > maxFileSizeBytes) {
            return undefined;
        }
        return await node_fs_1.promises.readFile(filePath, { encoding: 'utf8' });
    }
    catch {
        return undefined;
    }
}
async function collectSourceFiles(workspaceRoot, sourceInventory) {
    if (sourceInventory) {
        const files = await sourceInventory.getTextFiles({
            extensions: [...supportedExtensions],
            ignoredDirectoryNames: [...ignoredDirectoryNames],
            maxFileSizeBytes,
        });
        return files.map((file) => ({
            relativePath: file.relativePath,
            content: file.content,
            extension: file.extension,
        }));
    }
    const ignoreMatcher = (0, archpilotIgnore_1.loadArchpilotIgnoreMatcher)(workspaceRoot);
    const collected = [];
    const stack = [workspaceRoot];
    while (stack.length > 0) {
        const current = stack.pop();
        if (!current) {
            continue;
        }
        let entries;
        try {
            entries = await node_fs_1.promises.readdir(current, { withFileTypes: true });
        }
        catch {
            continue;
        }
        for (const entry of entries) {
            const absoluteEntryPath = path.join(current, entry.name);
            const relativeEntryPath = toPosixRelative(workspaceRoot, absoluteEntryPath);
            if (ignoreMatcher.isIgnored(relativeEntryPath)) {
                continue;
            }
            if (entry.isDirectory()) {
                if (entry.name.startsWith('.') || ignoredDirectoryNames.has(entry.name)) {
                    continue;
                }
                stack.push(absoluteEntryPath);
                continue;
            }
            const extension = path.extname(entry.name).toLowerCase();
            if (!supportedExtensions.has(extension)) {
                continue;
            }
            const content = await readTextFileIfReasonable(absoluteEntryPath, sourceInventory);
            if (content === undefined) {
                continue;
            }
            collected.push({
                relativePath: relativeEntryPath,
                content,
                extension,
            });
        }
    }
    return collected.sort((left, right) => left.relativePath.localeCompare(right.relativePath));
}
function lineNumberForOffset(content, offset) {
    let line = 1;
    for (let index = 0; index < offset && index < content.length; index += 1) {
        if (content[index] === '\n') {
            line += 1;
        }
    }
    return line;
}
function findLine(content, pattern) {
    const match = pattern.exec(content);
    return match ? lineNumberForOffset(content, match.index) : undefined;
}
function hasAnyPattern(content, patterns) {
    return patterns.some((pattern) => pattern.test(content));
}
function maskNonCodeEvidence(value) {
    let masked = '';
    let index = 0;
    let previousSignificant = '';
    const pushMasked = (count) => {
        masked += ' '.repeat(count);
    };
    const pushCode = (character) => {
        masked += character;
        if (!/\s/u.test(character)) {
            previousSignificant = character;
        }
    };
    while (index < value.length) {
        const character = value[index] ?? '';
        const next = value[index + 1] ?? '';
        if (character === '/' && next === '/') {
            const end = value.indexOf('\n', index + 2);
            const length = end === -1 ? value.length - index : end - index;
            pushMasked(length);
            index += length;
            continue;
        }
        if (character === '/' && next === '*') {
            const end = value.indexOf('*/', index + 2);
            const length = end === -1 ? value.length - index : end + 2 - index;
            pushMasked(length);
            index += length;
            continue;
        }
        if (character === '\'' || character === '"' || character === '`') {
            const quote = character;
            let cursor = index + 1;
            while (cursor < value.length) {
                const current = value[cursor] ?? '';
                if (current === '\\') {
                    cursor += 2;
                    continue;
                }
                if (current === quote) {
                    cursor += 1;
                    break;
                }
                cursor += 1;
            }
            pushMasked(cursor - index);
            index = cursor;
            continue;
        }
        if (character === '/' &&
            (previousSignificant === '' ||
                /[([{:;,=!?&|]/u.test(previousSignificant) ||
                /\b(?:return|case|throw|yield)\s*$/u.test(value.slice(Math.max(0, index - 24), index)))) {
            let cursor = index + 1;
            let inCharacterClass = false;
            while (cursor < value.length) {
                const current = value[cursor] ?? '';
                if (current === '\\') {
                    cursor += 2;
                    continue;
                }
                if (current === '[') {
                    inCharacterClass = true;
                }
                else if (current === ']') {
                    inCharacterClass = false;
                }
                else if (current === '/' && !inCharacterClass) {
                    cursor += 1;
                    while (/[a-z]/iu.test(value[cursor] ?? '')) {
                        cursor += 1;
                    }
                    break;
                }
                cursor += 1;
            }
            pushMasked(cursor - index);
            index = cursor;
            continue;
        }
        pushCode(character);
        index += 1;
    }
    return masked;
}
function stripJavaCommentsAndStrings(content) {
    return content
        .replace(/\/\*[\s\S]*?\*\//g, ' ')
        .replace(/\/\/.*$/gm, ' ')
        .replace(/"(?:\\.|[^"\\])*"/g, '""')
        .replace(/'(?:\\.|[^'\\])*'/g, "''");
}
function getJavaInterfaceBody(content) {
    const interfaceMatch = /\binterface\s+\w+\s+(?:extends\s+[^{]+)?\{/m.exec(content);
    if (!interfaceMatch) {
        return undefined;
    }
    let depth = 0;
    for (let index = interfaceMatch.index; index < content.length; index += 1) {
        const character = content[index];
        if (character === '{') {
            depth += 1;
            continue;
        }
        if (character === '}') {
            depth -= 1;
            if (depth === 0) {
                return content.slice(interfaceMatch.index + interfaceMatch[0].length, index);
            }
        }
    }
    return undefined;
}
function isSpringDataRepositoryInterface(content) {
    const normalized = stripJavaCommentsAndStrings(content);
    return /\binterface\s+\w+\s+extends\s+(?:[\w.]+\s*,\s*)*(?:JpaRepository|CrudRepository|PagingAndSortingRepository|MongoRepository|Repository)\s*<[^>{}]+>/m.test(normalized);
}
function isPureSpringDataRepositoryInterface(content) {
    if (!isSpringDataRepositoryInterface(content)) {
        return false;
    }
    const body = getJavaInterfaceBody(stripJavaCommentsAndStrings(content));
    if (!body) {
        return false;
    }
    return !/\)\s*\{|\b(?:default|private|static)\s+[\w<>, ?\[\].]+\s+\w+\s*\(/m.test(body);
}
function findMatchingBrace(content, openBraceIndex) {
    let depth = 0;
    for (let index = openBraceIndex; index < content.length; index += 1) {
        const char = content[index];
        if (char === '{') {
            depth += 1;
        }
        else if (char === '}') {
            depth -= 1;
            if (depth === 0) {
                return index;
            }
        }
    }
    return Math.min(content.length - 1, openBraceIndex + 5000);
}
function extractMethodBodies(entry) {
    if (entry.extension === '.py') {
        return extractPythonMethodBodies(entry);
    }
    const methods = [];
    const pattern = braceMethodPatternForExtension(entry.extension);
    for (const match of entry.content.matchAll(pattern)) {
        const openBrace = (match.index ?? 0) + match[0].lastIndexOf('{');
        const closeBrace = findMatchingBrace(entry.content, openBrace);
        methods.push({
            name: match[1] ?? 'method',
            body: entry.content.slice(openBrace, closeBrace + 1),
            line: lineNumberForOffset(entry.content, match.index ?? 0),
        });
    }
    return methods;
}
function braceMethodPatternForExtension(extension) {
    if (extension === '.java') {
        return /(?:public|private|protected)\s+[\w<>, ?\[\]]+\s+(\w+)\s*\([^)]*\)\s*\{/gu;
    }
    if (extension === '.cs') {
        return /(?:public|private|protected|internal)\s+(?:async\s+)?[\w<>, ?\[\]]+\s+(\w+)\s*\([^)]*\)\s*\{/gu;
    }
    if (extension === '.php') {
        return /(?:(?:public|private|protected|static|final)\s+)*function\s+(\w+)\s*\([^)]*\)\s*(?::\s*[^{]+)?\s*\{/gu;
    }
    if (extension === '.go') {
        return /func\s+(?:\([^)]*\)\s*)?([A-Za-z_]\w*)\s*\([^)]*\)\s*(?:\([^)]*\)|[\w*.[\]]+)?\s*\{/gu;
    }
    return /(?:export\s+)?(?:public\s+|private\s+|protected\s+)?(?:async\s+)?(?:function\s+)?(\w+)\s*\([^)]*\)\s*(?::\s*[^{]+)?\s*\{/gu;
}
function leadingWhitespaceLength(value) {
    return value.length - value.trimStart().length;
}
function extractPythonMethodBodies(entry) {
    const methods = [];
    const lineStarts = [0];
    for (let index = 0; index < entry.content.length; index += 1) {
        if (entry.content[index] === '\n') {
            lineStarts.push(index + 1);
        }
    }
    for (let lineIndex = 0; lineIndex < lineStarts.length; lineIndex += 1) {
        const lineStart = lineStarts[lineIndex] ?? 0;
        const lineEnd = entry.content.indexOf('\n', lineStart);
        const line = entry.content.slice(lineStart, lineEnd === -1 ? entry.content.length : lineEnd);
        const match = /^(?:async\s+)?def\s+([A-Za-z_]\w*)\s*\([^)]*\)\s*:/u.exec(line.trimStart());
        if (!match?.[1]) {
            continue;
        }
        const definitionIndent = leadingWhitespaceLength(line);
        let bodyEnd = entry.content.length;
        for (let cursor = lineIndex + 1; cursor < lineStarts.length; cursor += 1) {
            const cursorStart = lineStarts[cursor] ?? 0;
            const cursorLineEnd = entry.content.indexOf('\n', cursorStart);
            const cursorLine = entry.content.slice(cursorStart, cursorLineEnd === -1 ? entry.content.length : cursorLineEnd);
            if (cursorLine.trim().length === 0) {
                continue;
            }
            if (leadingWhitespaceLength(cursorLine) <= definitionIndent) {
                bodyEnd = cursorStart;
                break;
            }
        }
        methods.push({
            name: match[1],
            body: entry.content.slice(lineStart, bodyEnd),
            line: lineNumberForOffset(entry.content, lineStart),
        });
    }
    return methods;
}
function getModulePathForName(contract, moduleName) {
    if (!moduleName) {
        return undefined;
    }
    const modulesRoot = (0, moduleDiscovery_1.resolveModulesRootFromContract)(contract);
    const reference = contract.modules?.[moduleName];
    return normalizePath(reference?.path ?? `${modulesRoot}/${moduleName}`);
}
function getPathSegmentsInsideModule(relativePath, modulePath) {
    const normalized = normalizePath(relativePath).toLowerCase();
    if (!modulePath) {
        return normalized.split('/');
    }
    const normalizedModulePath = normalizePath(modulePath).toLowerCase();
    if (normalized === normalizedModulePath) {
        return [];
    }
    if (normalized.startsWith(`${normalizedModulePath}/`)) {
        return normalized.slice(normalizedModulePath.length + 1).split('/');
    }
    return normalized.split('/');
}
function getPathPackageRole(relativePath, modulePath, scope) {
    if (!(0, sourceScopeClassification_1.isProductionBackendGovernanceScope)(scope)) {
        return undefined;
    }
    const segments = getPathSegmentsInsideModule(relativePath, modulePath);
    const parentSegments = segments.slice(0, -1);
    if (parentSegments.some((segment) => /^(controllers?|web|rest)$/.test(segment))) {
        return 'controller';
    }
    if (parentSegments.some((segment) => /^(services?|application)$/.test(segment))) {
        return 'service';
    }
    if (parentSegments.some((segment) => /^(repositories?|repository|persistence|dao|data|data-access)$/.test(segment)) ||
        segments.some((segment, index) => segment === 'infrastructure' &&
            /^(repositories?|repository)$/.test(segments[index + 1] ?? '')) ||
        segments.some((segment, index) => segment === 'adapters' &&
            segments[index + 1] === 'persistence')) {
        return 'repository';
    }
    return undefined;
}
function getTypeScriptDeclaredRoles(entry, astCache) {
    const sourceFile = astCache
        ? astCache.parseSourceFile({
            filePath: entry.relativePath,
            sourceText: entry.content,
            scriptTarget: ts.ScriptTarget.Latest,
            setParentNodes: true,
        })
        : ts.createSourceFile(entry.relativePath, entry.content, ts.ScriptTarget.Latest, true);
    const roles = new Set();
    const visit = (node) => {
        if ((ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node)) && node.name) {
            const name = node.name.text;
            if (/Controller$/u.test(name)) {
                roles.add('controller');
            }
            if (/Service$/u.test(name)) {
                roles.add('service');
            }
            if (/(Repository|Dao)$/u.test(name)) {
                roles.add('repository');
            }
        }
        ts.forEachChild(node, visit);
    };
    visit(sourceFile);
    return [...roles];
}
function getJavaDeclaredRoles(content) {
    const code = maskNonCodeEvidence(content);
    const roles = new Set();
    if (/(class|interface)\s+\w*Controller\b/.test(code) || /@(?:Controller|RestController)\b|org\.springframework\.web\.bind\.annotation/u.test(code)) {
        roles.add('controller');
    }
    if (/(class|interface)\s+\w*Service\b/.test(code) || /@Service\b/.test(code)) {
        roles.add('service');
    }
    if (/(class|interface)\s+\w*(?:Repository|Dao)\b/.test(code) || /@Repository\b/.test(code)) {
        roles.add('repository');
    }
    return [...roles];
}
function getTextDeclaredRoles(content, extension) {
    const code = maskNonCodeEvidence(content);
    const roles = new Set();
    if (extension === '.java') {
        return getJavaDeclaredRoles(content);
    }
    if (/@Controller\b|\b(?:router|app)\.(?:get|post|put|patch|delete)\s*\(|\bAPIRouter\s*\(|@(?:app|router)\.(?:get|post|put|patch|delete)\b|\[ApiController\]|\bControllerBase\b|Illuminate\\Routing\\Controller|extends\s+Controller\b|\bRoute::(?:get|post|put|patch|delete)\s*\(|\bhttp\.(?:HandleFunc|HandlerFunc)\b|\b(?:gin|mux|chi)\.\w+/u.test(code)) {
        roles.add('controller');
    }
    if (/@Injectable\b|@Service\b|\bclass\s+\w*Service\b/u.test(code)) {
        roles.add('service');
    }
    if (/@Repository\b|\bclass\s+\w*(?:Repository|Repo|Dao)\b|\binterface\s+\w*(?:Repository|Dao)\b/u.test(code)) {
        roles.add('repository');
    }
    return [...roles];
}
function classifySourceFile(entry, modulePath, astCache) {
    const lowerPath = entry.relativePath.toLowerCase();
    const scope = (0, sourceScopeClassification_1.classifyValidationSourceScope)(entry.relativePath);
    const effectiveDeclaredRoles = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'].includes(entry.extension)
        ? [...new Set([...getTypeScriptDeclaredRoles(entry, astCache), ...getTextDeclaredRoles(entry.content, entry.extension)])]
        : getTextDeclaredRoles(entry.content, entry.extension);
    const isController = /(^|[./_-])controller\.(ts|tsx|js|jsx|mjs|cjs|java|cs|py|php|go)$/i.test(lowerPath) ||
        effectiveDeclaredRoles.includes('controller');
    const isService = /(^|[./_-])service\.(ts|tsx|js|jsx|mjs|cjs|java|cs|py|php|go)$/i.test(lowerPath) ||
        effectiveDeclaredRoles.includes('service');
    const isRepository = /(^|[./_-])(?:repository|repo|dao)\.(ts|tsx|js|jsx|mjs|cjs|java|cs|py|php|go)$/i.test(lowerPath) ||
        effectiveDeclaredRoles.includes('repository');
    return {
        isController,
        isService,
        isRepository,
        packageRole: getPathPackageRole(entry.relativePath, modulePath, scope),
        declaredRoles: effectiveDeclaredRoles,
        scope,
    };
}
function inferModuleForPath(contract, relativePath) {
    const normalized = normalizePath(relativePath);
    const modulesRoot = (0, moduleDiscovery_1.resolveModulesRootFromContract)(contract);
    for (const [moduleName, reference] of Object.entries(contract.modules ?? {})) {
        const modulePath = normalizePath(reference.path ?? `${modulesRoot}/${moduleName}`);
        if (normalized === modulePath || normalized.startsWith(`${modulePath}/`)) {
            return moduleName;
        }
    }
    const rootPrefix = `${normalizePath(modulesRoot)}/`;
    if (normalized.startsWith(rootPrefix)) {
        const [moduleName] = normalized.slice(rootPrefix.length).split('/');
        return moduleName || undefined;
    }
    return undefined;
}
function createFinding(input) {
    return {
        id: input.id,
        severity: 'warning',
        passed: false,
        message: input.message,
        findingType: 'file-scope',
        filePath: input.file.relativePath,
        sourceFile: input.file.relativePath,
        ...(input.line ? { sourceLine: input.line } : {}),
        ...(input.module ? { module: input.module } : {}),
        ...(input.target ? { target: input.target } : {}),
    };
}
function hasRepositoryLayer(files, contract) {
    return files.some((file) => {
        const moduleName = inferModuleForPath(contract, file.relativePath);
        return classifySourceFile(file, getModulePathForName(contract, moduleName)).isRepository;
    });
}
function hasServiceLayer(files) {
    return files.some((file) => classifySourceFile(file).isService);
}
function hasDirectDatabaseAccess(value) {
    const code = maskNonCodeEvidence(value);
    return [
        /\b(?:this\.)?prisma\.[a-z]\w*\.(?:find|create|update|upsert|delete|count|aggregate|groupBy|createMany|updateMany|deleteMany)\w*\s*\(/iu,
        /\b(?:this\.)?prisma\.\$transaction\s*\(/u,
        /\b(?:PrismaClient|DataSource|EntityManager|JdbcTemplate|QueryRunner|DbContext|DbSet|TransactionScope|sql\.DB|sqlx\.DB|gorm\.DB)\b/u,
        /\b(?:this\.)?(?:entityManager|dataSource|jdbcTemplate|queryRunner)\.(?:createQuery|query|execute|save|persist|merge|remove|find)\w*\s*\(/iu,
        /\b(?:db|session)\.(?:query|execute|add|delete|commit|rollback)\s*\(/iu,
        /\b(?:DB::table|DB::select|DB::insert|DB::update|DB::delete|DB::transaction)\s*\(/u,
        /\b[A-Z]\w*::(?:query|where|find|findOrFail|create|update|delete)\s*\(/u,
        /\b(?:db|tx)\.(?:Query|QueryContext|QueryRow|QueryRowContext|Exec|ExecContext|Begin|BeginTx)\s*\(/u,
        /\bknex\s*\(/iu,
        /\b(?:knex|sequelize|typeorm|dataSource|repository)\.(?:transaction|query|createQueryBuilder|getRepository)\s*\(/iu,
        /\b(?:this\.)?db\.[A-Z]\w*\.(?:Where|Find|First|FirstOrDefault|SingleOrDefault|Add|Update|Remove|ExecuteDelete|ExecuteUpdate)\s*\(/u,
        /\bcreateQueryBuilder\s*\(/u,
        /\bquery\s*\(\s*(?:`|'|")\s*(?:select|insert|update|delete)\b/iu,
    ].some((pattern) => pattern.test(code));
}
function hasRepositoryPersistenceCall(value) {
    return /\b(?:this\.)?\w*(?:Repository|Repo|Dao)\.(?:save|create|insert|update|delete|remove|persist|merge|upsert|deleteMany|updateMany|createMany)\w*\s*\(/u.test(value);
}
function hasTransactionHandling(value) {
    return /\b(?:\$transaction|@Transactional|transactional\s*\(|startTransaction|beginTransaction|commitTransaction|rollbackTransaction|QueryRunner|TransactionTemplate|TransactionScope|BeginTransaction|BeginTransactionAsync|DB::transaction|session\.begin|BeginTx|db\.Transaction)\b/iu.test(maskNonCodeEvidence(value));
}
function hasWebFrameworkEvidence(value) {
    return /@(?:Controller|RestController|RequestMapping|GetMapping|PostMapping)\b|@Controller\b|@(?:Get|Post|Put|Patch|Delete)\b|\b(?:express|Request|Response|APIRouter|FastAPI|ControllerBase|IActionResult|HttpContext|Illuminate\\Http|Illuminate\\Routing|Route::|http\.ResponseWriter|http\.Request|gin\.Context)\b|\b(?:router|app)\.(?:get|post|put|patch|delete)\s*\(|@(?:app|router)\.(?:get|post|put|patch|delete)\b|\[(?:ApiController|HttpGet|HttpPost|HttpPut|HttpPatch|HttpDelete)\b/u.test(maskNonCodeEvidence(value));
}
function hasControllerWorkflowEvidence(methods) {
    return methods.some((method) => {
        const nonInfrastructureCalls = [...method.body.matchAll(/\bthis\.(\w+)\.(\w+)\s*\(/gu)]
            .filter((match) => !/auth|guard|validator|validation|mapper|dto|parser/iu.test(match[1] ?? ''))
            .filter((match) => !/^require|^authorize|^authenticate|^validate|^parse|^map|^to/iu.test(match[2] ?? ''));
        return (nonInfrastructureCalls.length >= 3 &&
            /\b(?:approve|reject|create|update|delete|remove|invite|checkout|sync|provision|transition|settle)\b/iu.test(method.body) &&
            /\bif\s*\(|\bswitch\s*\(|\bthrow\s+new\b/u.test(method.body));
    });
}
function detectControllerBusinessLogic(entry, classification, moduleName, context) {
    if (!classification.isController || !(0, sourceScopeClassification_1.isProductionBackendGovernanceScope)(classification.scope)) {
        return undefined;
    }
    const methods = extractMethodBodies(entry);
    const directDatabaseMethod = methods.find((method) => hasDirectDatabaseAccess(method.body));
    const repositoryPersistenceMethod = context.serviceLayerPresent && context.repositoryLayerPresent
        ? methods.find((method) => hasRepositoryPersistenceCall(method.body))
        : undefined;
    const transactionMethod = methods.find((method) => hasTransactionHandling(method.body));
    const hasWorkflowEvidence = hasControllerWorkflowEvidence(methods);
    if (!directDatabaseMethod && !repositoryPersistenceMethod && !transactionMethod && !hasWorkflowEvidence) {
        return undefined;
    }
    const evidence = [
        directDatabaseMethod ? 'directDatabase=true' : undefined,
        repositoryPersistenceMethod ? 'repositoryPersistence=true' : undefined,
        transactionMethod ? 'transaction=true' : undefined,
        hasWorkflowEvidence ? 'workflow=true' : undefined,
    ].filter((value) => Boolean(value));
    return createFinding({
        id: ApplicationLayeringRuleIds.CONTROLLER_BUSINESS_LOGIC,
        message: `Controller appears to contain persistence or business workflow logic instead of delegating to an application service (${evidence.join(', ')}).`,
        file: entry,
        line: directDatabaseMethod?.line ?? repositoryPersistenceMethod?.line ?? transactionMethod?.line ?? methods[0]?.line,
        module: moduleName,
        target: 'controller',
    });
}
function detectRepositoryBusinessLogic(entry, classification, moduleName) {
    if (!classification.isRepository) {
        return undefined;
    }
    if (entry.extension === '.java' && isPureSpringDataRepositoryInterface(entry.content)) {
        return undefined;
    }
    const businessWorkflowPatterns = [
        /\b(?:HttpService|RestTemplate|WebClient|axios|fetch)\b/,
        /\b(?:EventEmitter|ApplicationEventPublisher|CommandBus|QueryBus)\b/,
        /\b(?:Mailer|Email|Sms|Queue|Kafka|Rabbit|Sqs)\b/i,
        /\b(?:Controller|Service)\b/,
    ];
    const code = maskNonCodeEvidence(entry.content);
    if (!hasAnyPattern(code, businessWorkflowPatterns) && !hasWebFrameworkEvidence(entry.content)) {
        return undefined;
    }
    return createFinding({
        id: ApplicationLayeringRuleIds.REPOSITORY_BUSINESS_LOGIC,
        message: 'Repository appears to contain workflow or integration logic that belongs in an application service.',
        file: entry,
        line: findLine(code, /\b(?:HttpService|RestTemplate|WebClient|axios|fetch|EventEmitter|ApplicationEventPublisher|CommandBus|QueryBus|Mailer|Email|Sms|Queue|Kafka|Rabbit|Sqs|Controller|Service|APIRouter|ControllerBase|Illuminate\\Http|http\.Request)\b/i),
        module: moduleName,
        target: 'repository',
    });
}
function detectServiceBypassesRepository(input) {
    if (!input.classification.isService ||
        !(0, sourceScopeClassification_1.isProductionBackendGovernanceScope)(input.classification.scope) ||
        input.classification.scope.isInfrastructure ||
        !input.repositoryLayerPresent) {
        return undefined;
    }
    const directDatabasePatterns = [
        /\b(?:DataSource|EntityManager|JdbcTemplate|PrismaClient|DbContext|DbSet|sql\.DB|sqlx\.DB|gorm\.DB)\b/,
        /\bcreateQueryBuilder\s*\(/,
        /\bquery\s*\(\s*(?:`|'|")\s*(?:select|insert|update|delete)\b/i,
        /\b(?:db|session)\.(?:query|execute|add|delete|commit|rollback)\s*\(/i,
        /\b(?:DB::table|DB::select|DB::insert|DB::update|DB::delete|DB::transaction)\s*\(/,
        /\b[A-Z]\w*::(?:query|where|find|findOrFail|create|update|delete)\s*\(/,
        /\b(?:db|tx)\.(?:Query|QueryContext|QueryRow|QueryRowContext|Exec|ExecContext|Begin|BeginTx)\s*\(/,
        /\bknex\s*\(/i,
        /\b(?:knex|sequelize|typeorm|dataSource|repository)\.(?:transaction|query|createQueryBuilder|getRepository)\s*\(/i,
        /\b(?:this\.)?db\.[A-Z]\w*\.(?:Where|Find|First|FirstOrDefault|SingleOrDefault|Add|Update|Remove|ExecuteDelete|ExecuteUpdate)\s*\(/,
    ];
    const prismaPatterns = [/\b(?:PrismaClient|prisma)\b/i];
    const code = maskNonCodeEvidence(input.entry.content);
    const hasDirectDatabase = hasAnyPattern(code, directDatabasePatterns);
    const hasPrismaBypass = hasAnyPattern(code, prismaPatterns);
    if (!hasDirectDatabase && !hasPrismaBypass) {
        return undefined;
    }
    return createFinding({
        id: ApplicationLayeringRuleIds.SERVICE_BYPASSES_REPOSITORY,
        message: 'Service appears to access the database directly instead of using the repository/persistence layer.',
        file: input.entry,
        line: findLine(input.entry.content, /\b(?:DataSource|EntityManager|JdbcTemplate|createQueryBuilder|query|PrismaClient|prisma)\b/i),
        module: input.moduleName,
        target: 'service',
    });
}
function detectPackageResponsibilityMismatch(entry, classification, moduleName) {
    if (!(0, sourceScopeClassification_1.isProductionBackendGovernanceScope)(classification.scope) ||
        !classification.packageRole ||
        classification.declaredRoles.length === 0) {
        return undefined;
    }
    const mismatchedRole = classification.declaredRoles.find((role) => role !== classification.packageRole);
    if (!mismatchedRole) {
        return undefined;
    }
    return createFinding({
        id: ApplicationLayeringRuleIds.PACKAGE_RESPONSIBILITY_MISMATCH,
        message: `File is located in a ${classification.packageRole} package but declares ${mismatchedRole} responsibility.`,
        file: entry,
        line: findLine(entry.content, /\b(?:class|interface)\s+\w*(?:Controller|Service|Repository|Dao)\b|@(?:Controller|RestController|Injectable|Service|Repository)\b/),
        module: moduleName,
        target: classification.packageRole,
    });
}
function countTypeScriptClassMethods(sourceFile) {
    let count = 0;
    const visit = (node) => {
        if (ts.isClassDeclaration(node)) {
            for (const member of node.members) {
                if (ts.isMethodDeclaration(member)) {
                    count += 1;
                }
            }
        }
        ts.forEachChild(node, visit);
    };
    visit(sourceFile);
    return count;
}
function countConstructorParameters(sourceFile) {
    let count = 0;
    const visit = (node) => {
        if (ts.isConstructorDeclaration(node)) {
            count += node.parameters.length;
        }
        ts.forEachChild(node, visit);
    };
    visit(sourceFile);
    return count;
}
function countJavaMethods(content) {
    return [...content.matchAll(/\b(?:public|private|protected)\s+(?!class\b)[\w<>, ?\[\]]+\s+\w+\s*\([^)]*\)\s*\{/g)].length;
}
function countJavaInjectedFields(content) {
    return [...content.matchAll(/@(Autowired|Inject)\b|private\s+final\s+\w+(?:<[^>]+>)?\s+\w+\s*;/g)].length;
}
function sortUnique(values) {
    return [...new Set(values)].sort((left, right) => left.localeCompare(right));
}
function isDbModelEvidenceToken(value) {
    const normalized = value.trim();
    if (normalized.length <= 1) {
        return false;
    }
    if (!/^[A-Za-z][A-Za-z0-9_]*$/u.test(normalized)) {
        return false;
    }
    const lower = normalized.toLowerCase();
    const excluded = new Set([
        'a',
        'all',
        'an',
        'doing',
        'first',
        'is',
        'outside',
        'resend',
        'scratch',
        'swagger',
        'the',
    ]);
    if (excluded.has(lower)) {
        return false;
    }
    return !/(?:client|provider|service|controller|guard|swagger|openapi)$/iu.test(normalized);
}
function isStructuredSqlTableEvidence(value, keyword, quote, following = '') {
    const normalized = value.trim();
    const hasStructuralEvidence = /^(?:from|update|into)$/iu.test(keyword) ||
        quote !== undefined && quote.length > 0 ||
        /[_.]/u.test(normalized) ||
        (/^join$/iu.test(keyword) && /\bon\b/iu.test(following));
    return hasStructuralEvidence && isDbModelEvidenceToken(normalized);
}
function extractSqlLikeLiterals(content) {
    return [...content.matchAll(/(['"`])([\s\S]{0,1200}?)\1/gu)]
        .map((match) => match[2] ?? '')
        .filter((literal) => /\b(?:select|update|delete|insert)\b[\s\S]{0,400}\b(?:from|join|into|update)\b/iu.test(literal));
}
function extractDbModelsTouched(content) {
    const models = new Set();
    for (const match of content.matchAll(/\b(?:this\.)?prisma\.([a-z]\w*)\./giu)) {
        if (match[1] && isDbModelEvidenceToken(match[1])) {
            models.add(match[1]);
        }
    }
    for (const sqlLiteral of extractSqlLikeLiterals(content)) {
        for (const match of sqlLiteral.matchAll(/\b(?:from|join|update|into)\s+(["`]?)([A-Za-z_][\w]*(?:\.[A-Za-z_][\w]*)?)\1/giu)) {
            const keyword = /\b(from|join|update|into)\b/iu.exec(match[0])?.[1] ?? '';
            const following = sqlLiteral.slice((match.index ?? 0) + match[0].length, (match.index ?? 0) + match[0].length + 120);
            if (match[2] && isStructuredSqlTableEvidence(match[2], keyword, match[1], following)) {
                models.add(match[2]);
            }
        }
    }
    return sortUnique(models).slice(0, 6);
}
function extractExternalIntegrations(content) {
    const integrations = new Set();
    const patterns = [
        [/\b(?:HttpService|httpClient|RestTemplate|WebClient|axios|fetch)\b/iu, 'http'],
        [/\b(?:Mailer|mailClient|emailClient|sendEmail|send\w*Email)\b/iu, 'email'],
        [/\b(?:Kafka|Rabbit|Sqs|Queue|EventEmitter|ApplicationEventPublisher)\b/iu, 'messaging'],
        [/\b(?:Paddle|Stripe|Checkout|PortalSession)\b/iu, 'billing-provider'],
        [/\b(?:S3|putObject|upload)\b/iu, 'object-storage'],
    ];
    for (const [pattern, label] of patterns) {
        if (pattern.test(content)) {
            integrations.add(label);
        }
    }
    return sortUnique(integrations);
}
function countPersistenceCollaborators(content) {
    const collaborators = new Set();
    for (const match of content.matchAll(/\b(?:private\s+readonly\s+|private\s+|readonly\s+)?(\w*(?:Repository|Repo|Dao|Prisma|DataSource|EntityManager|JdbcTemplate))\b/gu)) {
        if (match[1]) {
            collaborators.add(match[1]);
        }
    }
    return collaborators.size;
}
function summarizeServiceEvidence(entry, methodCount, dependencyCount) {
    return {
        lines: entry.content.split(/\r?\n/u).length,
        publicMethods: methodCount,
        injectedCollaborators: dependencyCount,
        persistenceCollaborators: countPersistenceCollaborators(entry.content),
        dbModelsTouched: extractDbModelsTouched(entry.content),
        externalIntegrations: extractExternalIntegrations(entry.content),
    };
}
function formatServiceEvidence(evidence) {
    const parts = [
        `loc=${evidence.lines}`,
        `methods=${evidence.publicMethods}`,
        `collaborators=${evidence.injectedCollaborators}`,
        `persistence=${evidence.persistenceCollaborators}`,
    ];
    if (evidence.dbModelsTouched.length > 0) {
        parts.push(`db=${evidence.dbModelsTouched.join('|')}`);
    }
    if (evidence.externalIntegrations.length > 0) {
        parts.push(`external=${evidence.externalIntegrations.join('|')}`);
    }
    return parts.join(', ');
}
function detectServiceGodClass(entry, classification, moduleName, astCache) {
    if (!classification.isService || !(0, sourceScopeClassification_1.isProductionBackendGovernanceScope)(classification.scope)) {
        return undefined;
    }
    const lineCount = entry.content.split(/\r?\n/u).length;
    const sourceFile = entry.extension === '.ts' || entry.extension === '.tsx'
        ? astCache
            ? astCache.parseSourceFile({
                filePath: entry.relativePath,
                sourceText: entry.content,
                scriptTarget: ts.ScriptTarget.Latest,
                setParentNodes: true,
            })
            : ts.createSourceFile(entry.relativePath, entry.content, ts.ScriptTarget.Latest, true)
        : undefined;
    const methodCount = sourceFile
        ? countTypeScriptClassMethods(sourceFile)
        : countJavaMethods(entry.content);
    const dependencyCount = sourceFile
        ? countConstructorParameters(sourceFile)
        : countJavaInjectedFields(entry.content);
    const oversized = lineCount >= 450 ||
        (methodCount >= 25 && dependencyCount >= 5) ||
        (methodCount >= 12 && dependencyCount >= 8);
    if (!oversized) {
        return undefined;
    }
    const evidence = summarizeServiceEvidence(entry, methodCount, dependencyCount);
    return createFinding({
        id: ApplicationLayeringRuleIds.SERVICE_GOD_CLASS,
        message: `Service is large or highly coupled enough to indicate multiple application responsibilities in one class (${formatServiceEvidence(evidence)}).`,
        file: entry,
        line: 1,
        module: moduleName,
        target: 'service',
    });
}
async function detectApplicationLayeringFindings(input) {
    const disabled = input.disabledRuleIds ?? new Set();
    const started = Date.now();
    const files = await collectSourceFiles(input.workspaceRoot, input.sourceInventory);
    (0, validationProfiling_1.emitValidationProfile)('AP-APP analyzer file scan', Date.now() - started, [
        { name: 'files', value: files.length },
    ]);
    const repositoryLayerPresent = hasRepositoryLayer(files, input.contract);
    const serviceLayerPresent = hasServiceLayer(files);
    const findings = [];
    for (const file of files) {
        const moduleName = inferModuleForPath(input.contract, file.relativePath);
        const classification = classifySourceFile(file, getModulePathForName(input.contract, moduleName), input.astCache);
        const candidates = [
            disabled.has(ApplicationLayeringRuleIds.CONTROLLER_BUSINESS_LOGIC)
                ? undefined
                : detectControllerBusinessLogic(file, classification, moduleName, {
                    serviceLayerPresent,
                    repositoryLayerPresent,
                }),
            disabled.has(ApplicationLayeringRuleIds.REPOSITORY_BUSINESS_LOGIC)
                ? undefined
                : detectRepositoryBusinessLogic(file, classification, moduleName),
            disabled.has(ApplicationLayeringRuleIds.SERVICE_BYPASSES_REPOSITORY)
                ? undefined
                : detectServiceBypassesRepository({
                    entry: file,
                    classification,
                    moduleName,
                    repositoryLayerPresent,
                }),
            disabled.has(ApplicationLayeringRuleIds.PACKAGE_RESPONSIBILITY_MISMATCH)
                ? undefined
                : detectPackageResponsibilityMismatch(file, classification, moduleName),
            disabled.has(ApplicationLayeringRuleIds.SERVICE_GOD_CLASS)
                ? undefined
                : detectServiceGodClass(file, classification, moduleName, input.astCache),
        ];
        findings.push(...candidates.filter((candidate) => !!candidate));
    }
    return findings.sort((left, right) => {
        const fileCompare = (left.sourceFile ?? '').localeCompare(right.sourceFile ?? '');
        if (fileCompare !== 0) {
            return fileCompare;
        }
        return left.id.localeCompare(right.id);
    });
}
//# sourceMappingURL=applicationLayeringAnalysis.js.map