"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.detectApiDesignFindings = detectApiDesignFindings;
const path = __importStar(require("node:path"));
const node_fs_1 = require("node:fs");
const moduleDiscovery_1 = require("./moduleDiscovery");
const archpilotIgnore_1 = require("./smartInit/archpilotIgnore");
const sourceScopeClassification_1 = require("./sourceScopeClassification");
const validationProfiling_1 = require("./validationProfiling");
const RuleIds = {
    MISSING_PAGINATION: 'AP-API-010',
    BULK_OPERATION_RISK: 'AP-API-011',
    DTO_ENTITY_LEAKAGE: 'AP-API-012',
    INCONSISTENT_RESOURCE_DESIGN: 'AP-API-013',
    VERSIONING_INCONSISTENCY: 'AP-API-014',
    CONTRACT_IMPLEMENTATION_DRIFT: 'AP-API-015',
};
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',
]);
const httpMethods = new Set(['get', 'post', 'put', 'patch', 'delete']);
const nonBusinessEndpointPattern = /\b(health|ready|live|status|metrics|docs|swagger|openapi|login|logout|callback|oauth|auth\/callback)\b/iu;
const paginationPattern = /\b(page|limit|offset|cursor|take|skip|size|Pageable|Pagination|batchSize|top)\b/u;
const authEvidencePattern = /@(?:UseGuards|Roles|Permissions|RequirePermission|PreAuthorize|Secured|RolesAllowed)\b|\b(?:AuthGuard|RolesGuard|PermissionGuard|admin|ADMIN)\b/u;
const safeguardPattern = /\b(?:dryRun|confirm|confirmation|batchSize|limit|reason|audit|queue|job|enqueue|FeatureFlag|featureFlag|staged)\b/u;
const mapperPattern = /\b(?:mapTo|toDto|toResponse|serializer|serialize|plainToInstance|mapper)\b/u;
const entityTypePattern = /\b\w*(?:Entity|Model|Document)\b/u;
const dtoTypePattern = /\b\w*(?:Dto|DTO|Response|View|Resource|Contract)\b/u;
const routeStringPattern = /['"`]([^'"`]*)['"`]/u;
function normalizePath(value) {
    return value.replace(/\\/g, '/');
}
function normalizeRelativePath(value) {
    return normalizePath(value).replace(/^\/+/, '');
}
function toPosixRelative(workspaceRoot, absolutePath) {
    return normalizePath(path.relative(workspaceRoot, absolutePath));
}
async function pathExists(targetPath) {
    try {
        await node_fs_1.promises.access(targetPath);
        return true;
    }
    catch {
        return false;
    }
}
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,
        })).filter((file) => (0, sourceScopeClassification_1.isProductionBackendGovernanceScope)((0, sourceScopeClassification_1.classifyValidationSourceScope)(file.relativePath)));
    }
    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;
            }
            if (!(0, sourceScopeClassification_1.isProductionBackendGovernanceScope)((0, sourceScopeClassification_1.classifyValidationSourceScope)(relativeEntryPath))) {
                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 findMatchingBrace(content, openBraceIndex) {
    let depth = 0;
    for (let index = openBraceIndex; index < content.length; index += 1) {
        if (content[index] === '{') {
            depth += 1;
        }
        if (content[index] === '}') {
            depth -= 1;
            if (depth === 0) {
                return index;
            }
        }
    }
    return Math.min(content.length - 1, openBraceIndex + 5000);
}
function findMatchingParen(content, openParenIndex) {
    let depth = 0;
    for (let index = openParenIndex; index < content.length; index += 1) {
        if (content[index] === '(') {
            depth += 1;
        }
        if (content[index] === ')') {
            depth -= 1;
            if (depth === 0) {
                return index;
            }
        }
    }
    return Math.min(content.length - 1, openParenIndex + 1000);
}
function findPythonBlockEnd(content, blockStart) {
    const lineStart = content.lastIndexOf('\n', blockStart) + 1;
    const lineEnd = content.indexOf('\n', blockStart);
    const line = content.slice(lineStart, lineEnd === -1 ? content.length : lineEnd);
    const baseIndent = /^\s*/u.exec(line)?.[0].length ?? 0;
    let offset = lineEnd === -1 ? content.length : lineEnd + 1;
    while (offset < content.length) {
        const nextLineEnd = content.indexOf('\n', offset);
        const nextEnd = nextLineEnd === -1 ? content.length : nextLineEnd;
        const nextLine = content.slice(offset, nextEnd);
        if (nextLine.trim().length > 0) {
            const indent = /^\s*/u.exec(nextLine)?.[0].length ?? 0;
            if (indent <= baseIndent) {
                return offset;
            }
        }
        if (nextLineEnd === -1) {
            return content.length;
        }
        offset = nextLineEnd + 1;
    }
    return content.length;
}
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;
        }
    }
    return undefined;
}
function resolveOpenApiRelativePath(contract) {
    if (typeof contract.artifacts?.openApiSpec === 'string' && contract.artifacts.openApiSpec.trim().length > 0) {
        return normalizeRelativePath(contract.artifacts.openApiSpec);
    }
    return `${(0, moduleDiscovery_1.resolveApiRootFromContract)(contract)}/openapi.yaml`;
}
function stringLiteralFromDecorator(value) {
    const match = routeStringPattern.exec(value);
    return match?.[1] ?? '';
}
function stringLiteralFromRouteCall(value) {
    const match = routeStringPattern.exec(value);
    return match?.[1] ?? '';
}
function joinEndpointPath(basePath, methodPath) {
    const joined = `/${[basePath, methodPath].filter((part) => part.trim().length > 0).join('/')}`;
    return joined.replace(/\/+/gu, '/').replace(/\/$/u, '') || '/';
}
function extractControllerBasePath(file) {
    if (file.extension === '.java') {
        const match = /@RequestMapping\s*\(([^)]*)\)/u.exec(file.content);
        return match ? stringLiteralFromDecorator(match[1]) : '';
    }
    const match = /@Controller\s*\(([^)]*)\)/u.exec(file.content);
    return match ? stringLiteralFromDecorator(match[1]) : '';
}
function httpMethodFromDecorator(decorators, extension) {
    const pattern = extension === '.java'
        ? /@(GetMapping|PostMapping|PutMapping|PatchMapping|DeleteMapping|RequestMapping)\b/u
        : /@(Get|Post|Put|Patch|Delete)\b/u;
    const match = pattern.exec(decorators);
    if (!match?.[1]) {
        return undefined;
    }
    const value = match[1].replace('Mapping', '').toLowerCase();
    if (value === 'request') {
        const methodMatch = /method\s*=\s*RequestMethod\.(GET|POST|PUT|PATCH|DELETE)/u.exec(decorators);
        return methodMatch?.[1]?.toLowerCase();
    }
    return value;
}
function methodPathFromDecorators(decorators, extension) {
    const pattern = extension === '.java'
        ? /@(?:GetMapping|PostMapping|PutMapping|PatchMapping|DeleteMapping|RequestMapping)\s*(?:\(([^)]*)\))?/u
        : /@(?:Get|Post|Put|Patch|Delete)\s*(?:\(([^)]*)\))?/u;
    const match = pattern.exec(decorators);
    return match?.[1] ? stringLiteralFromDecorator(match[1]) : '';
}
function extractDecoratorEndpoints(file, contract) {
    const endpoints = [];
    const basePath = extractControllerBasePath(file);
    const pattern = file.extension === '.java'
        ? /((?:\s*@(?:GetMapping|PostMapping|PutMapping|PatchMapping|DeleteMapping|RequestMapping|PreAuthorize|Secured|RolesAllowed)\b[^\n]*\n)+)\s*(public|private|protected)\s+([\w<>, ?\[\]]+)\s+(\w+)\s*\([^)]*\)\s*\{/gu
        : /((?:\s*@(?:Get|Post|Put|Patch|Delete|UseGuards|Roles|Permissions|RequirePermission)\b[^\n]*\n)+)\s*(?:public\s+|private\s+|protected\s+)?(?:async\s+)?(\w+)\s*\([^)]*\)\s*(?::\s*([^{]+))?\s*\{/gu;
    for (const match of file.content.matchAll(pattern)) {
        const decorators = match[1] ?? '';
        const httpMethod = httpMethodFromDecorator(decorators, file.extension);
        if (!httpMethod) {
            continue;
        }
        const name = file.extension === '.java' ? match[4] ?? 'handler' : match[2] ?? 'handler';
        const returnType = file.extension === '.java' ? match[3] ?? '' : match[3] ?? '';
        const openBrace = (match.index ?? 0) + match[0].lastIndexOf('{');
        const end = findMatchingBrace(file.content, openBrace);
        endpoints.push({
            httpMethod,
            path: joinEndpointPath(basePath, methodPathFromDecorators(decorators, file.extension)),
            name,
            line: lineNumberForOffset(file.content, match.index ?? 0),
            decorators,
            signature: `${returnType} ${match[0]}`,
            body: file.content.slice(openBrace, end + 1),
            file,
            moduleName: inferModuleForPath(contract, file.relativePath),
        });
    }
    return endpoints;
}
function extractFastApiEndpoints(file, contract) {
    const endpoints = [];
    const pattern = /((?:\s*@(?:app|router)\.(?:get|post|put|patch|delete)\s*\([^)]*\)\s*\n)+)\s*(?:async\s+)?def\s+(\w+)\s*\(([^)]*)\)\s*:/gu;
    for (const match of file.content.matchAll(pattern)) {
        const decorators = match[1] ?? '';
        const methodMatch = /@(?:app|router)\.(get|post|put|patch|delete)\s*\(([^)]*)\)/u.exec(decorators);
        if (!methodMatch?.[1]) {
            continue;
        }
        const blockStart = (match.index ?? 0) + match[0].length;
        const end = findPythonBlockEnd(file.content, blockStart);
        endpoints.push({
            httpMethod: methodMatch[1].toLowerCase(),
            path: joinEndpointPath('', stringLiteralFromRouteCall(methodMatch[2] ?? '')),
            name: match[2] ?? 'handler',
            line: lineNumberForOffset(file.content, match.index ?? 0),
            decorators,
            signature: match[3] ?? '',
            body: file.content.slice(blockStart, end),
            file,
            moduleName: inferModuleForPath(contract, file.relativePath),
        });
    }
    return endpoints;
}
function extractAspNetControllerBasePath(content, classOffset, controllerName) {
    const prefix = content.slice(Math.max(0, classOffset - 600), classOffset);
    const routeMatches = [...prefix.matchAll(/\[Route\s*\(([^)]*)\)\]/gu)];
    const routeValue = routeMatches.at(-1)?.[1];
    if (!routeValue) {
        return '';
    }
    return stringLiteralFromDecorator(routeValue).replace(/\[controller\]/giu, controllerName);
}
function extractAspNetEndpoints(file, contract) {
    const endpoints = [];
    const classPattern = /class\s+(\w+Controller)\b[^{]*\{/gu;
    for (const classMatch of file.content.matchAll(classPattern)) {
        const classStart = classMatch.index ?? 0;
        const classOpenBrace = classStart + classMatch[0].lastIndexOf('{');
        const classEnd = findMatchingBrace(file.content, classOpenBrace);
        const classBody = file.content.slice(classOpenBrace + 1, classEnd);
        const controllerName = (classMatch[1] ?? '').replace(/Controller$/u, '').toLowerCase();
        const basePath = extractAspNetControllerBasePath(file.content, classStart, controllerName).replace(/\/$/u, '') || controllerName;
        const methodPattern = /((?:\s*\[(?:HttpGet|HttpPost|HttpPut|HttpPatch|HttpDelete|Route|Authorize|AllowAnonymous)[^\]]*\]\s*)+)\s*(?:public|private|protected|internal)\s+(?:async\s+)?[\w<>, ?\[\].]+\s+(\w+)\s*\(([^)]*)\)\s*\{/gu;
        for (const methodMatch of classBody.matchAll(methodPattern)) {
            const attributes = methodMatch[1] ?? '';
            const httpMatch = /\[(HttpGet|HttpPost|HttpPut|HttpPatch|HttpDelete)\s*(?:\(([^)]*)\))?\]/u.exec(attributes);
            if (!httpMatch?.[1]) {
                continue;
            }
            const openBrace = classOpenBrace + 1 + (methodMatch.index ?? 0) + methodMatch[0].lastIndexOf('{');
            const end = findMatchingBrace(file.content, openBrace);
            endpoints.push({
                httpMethod: httpMatch[1].replace('Http', '').toLowerCase(),
                path: joinEndpointPath(basePath, httpMatch[2] ? stringLiteralFromDecorator(httpMatch[2]) : ''),
                name: methodMatch[2] ?? 'handler',
                line: lineNumberForOffset(file.content, classOpenBrace + 1 + (methodMatch.index ?? 0)),
                decorators: attributes,
                signature: methodMatch[3] ?? '',
                body: file.content.slice(openBrace, end + 1),
                file,
                moduleName: inferModuleForPath(contract, file.relativePath),
            });
        }
    }
    return endpoints;
}
function extractLaravelEndpoints(file, contract) {
    const endpoints = [];
    const pattern = /\bRoute::(get|post|put|patch|delete)\s*\(/gu;
    for (const match of file.content.matchAll(pattern)) {
        const openParen = (match.index ?? 0) + match[0].lastIndexOf('(');
        const endParen = findMatchingParen(file.content, openParen);
        const args = file.content.slice(openParen + 1, endParen);
        const routePath = stringLiteralFromRouteCall(args);
        if (!routePath) {
            continue;
        }
        const body = args.includes('function') ? args : match[0];
        endpoints.push({
            httpMethod: (match[1] ?? 'get').toLowerCase(),
            path: joinEndpointPath('', routePath),
            name: routePath,
            line: lineNumberForOffset(file.content, match.index ?? 0),
            decorators: file.content.slice(match.index ?? 0, Math.min(file.content.length, endParen + 1)),
            signature: args,
            body,
            file,
            moduleName: inferModuleForPath(contract, file.relativePath),
        });
    }
    const resourcePattern = /\bRoute::(?:apiResource|resource)\s*\(/gu;
    for (const match of file.content.matchAll(resourcePattern)) {
        const openParen = (match.index ?? 0) + match[0].lastIndexOf('(');
        const endParen = findMatchingParen(file.content, openParen);
        const args = file.content.slice(openParen + 1, endParen);
        const routePath = stringLiteralFromRouteCall(args);
        if (!routePath) {
            continue;
        }
        endpoints.push({
            httpMethod: 'get',
            path: joinEndpointPath('', routePath),
            name: routePath,
            line: lineNumberForOffset(file.content, match.index ?? 0),
            decorators: file.content.slice(match.index ?? 0, Math.min(file.content.length, endParen + 1)),
            signature: args,
            body: args,
            file,
            moduleName: inferModuleForPath(contract, file.relativePath),
        });
    }
    return endpoints;
}
function extractGoEndpoints(file, contract) {
    const endpoints = [];
    const pattern = /\b(?:\w+\.)?(GET|Get|POST|Post|PUT|Put|PATCH|Patch|DELETE|Delete)\s*\(/gu;
    for (const match of file.content.matchAll(pattern)) {
        const openParen = (match.index ?? 0) + match[0].lastIndexOf('(');
        const endParen = findMatchingParen(file.content, openParen);
        const callBody = file.content.slice(openParen + 1, endParen);
        const call = file.content.slice(match.index ?? 0, Math.min(file.content.length, endParen + 1));
        const method = (match[1] ?? 'get').toLowerCase();
        const routePath = stringLiteralFromRouteCall(callBody);
        if (!routePath) {
            continue;
        }
        endpoints.push({
            httpMethod: method,
            path: joinEndpointPath('', routePath),
            name: routePath,
            line: lineNumberForOffset(file.content, match.index ?? 0),
            decorators: call,
            signature: callBody,
            body: callBody,
            file,
            moduleName: inferModuleForPath(contract, file.relativePath),
        });
    }
    return endpoints;
}
function extractExpressEndpoints(file, contract) {
    const endpoints = [];
    const pattern = /\b(?:app|router)\.(get|post|put|patch|delete)\s*\(/gu;
    for (const match of file.content.matchAll(pattern)) {
        const openParen = (match.index ?? 0) + match[0].lastIndexOf('(');
        const endParen = findMatchingParen(file.content, openParen);
        const callBody = file.content.slice(openParen + 1, endParen);
        const routePath = stringLiteralFromRouteCall(callBody);
        if (!routePath) {
            continue;
        }
        endpoints.push({
            httpMethod: (match[1] ?? 'get').toLowerCase(),
            path: joinEndpointPath('', routePath),
            name: routePath,
            line: lineNumberForOffset(file.content, match.index ?? 0),
            decorators: file.content.slice(match.index ?? 0, Math.min(file.content.length, endParen + 1)),
            signature: callBody,
            body: callBody,
            file,
            moduleName: inferModuleForPath(contract, file.relativePath),
        });
    }
    return endpoints;
}
function extractEndpoints(file, contract) {
    if (file.extension === '.py') {
        return extractFastApiEndpoints(file, contract);
    }
    if (file.extension === '.cs') {
        return extractAspNetEndpoints(file, contract);
    }
    if (file.extension === '.php') {
        return extractLaravelEndpoints(file, contract);
    }
    if (file.extension === '.go') {
        return extractGoEndpoints(file, contract);
    }
    if (['.js', '.jsx', '.mjs', '.cjs'].includes(file.extension)) {
        return extractExpressEndpoints(file, contract);
    }
    return [...extractDecoratorEndpoints(file, contract), ...extractExpressEndpoints(file, contract)];
}
function createFinding(input) {
    return {
        id: input.id,
        severity: 'warning',
        passed: false,
        message: input.message,
        findingType: 'api-scope',
        filePath: input.endpoint.file.relativePath,
        sourceFile: input.endpoint.file.relativePath,
        sourceLine: input.endpoint.line,
        api: `${input.endpoint.httpMethod.toUpperCase()} ${input.endpoint.path}`,
        target: input.target ?? input.endpoint.name,
        ...(input.endpoint.moduleName ? { module: input.endpoint.moduleName } : {}),
    };
}
function createContractFinding(input) {
    return {
        id: RuleIds.CONTRACT_IMPLEMENTATION_DRIFT,
        severity: 'warning',
        passed: false,
        message: input.message,
        findingType: 'api-scope',
        filePath: input.filePath,
        sourceFile: input.filePath,
        api: input.api,
        target: input.target,
    };
}
function normalizeApiPath(value) {
    let normalized = value.trim().replace(/\\/gu, '/').replace(/\/+/gu, '/').replace(/\/$/u, '');
    if (!normalized.startsWith('/')) {
        normalized = `/${normalized}`;
    }
    normalized = normalized.replace(/^\/api(?=\/)/u, '');
    normalized = normalized
        .replace(/:([A-Za-z0-9_]+)/gu, '{$1}')
        .replace(/<([A-Za-z0-9_]+)>/gu, '{$1}')
        .replace(/\[([A-Za-z0-9_]+)\]/gu, '{$1}')
        .replace(/\{[^/}]+\}/gu, '{}');
    return normalized || '/';
}
function contractKey(method, apiPath) {
    return `${method.toLowerCase()} ${normalizeApiPath(apiPath)}`;
}
function isSkippedEndpoint(endpoint) {
    return nonBusinessEndpointPattern.test(endpoint.path) || nonBusinessEndpointPattern.test(endpoint.name);
}
function isCollectionGet(endpoint) {
    if (endpoint.httpMethod !== 'get' || isSkippedEndpoint(endpoint)) {
        return false;
    }
    const normalized = normalizeApiPath(endpoint.path);
    if (normalized.includes('{}')) {
        return false;
    }
    const lastSegment = normalized.split('/').filter(Boolean).at(-1) ?? '';
    return /s$/u.test(lastSegment) || /\[\]|Array<|List</u.test(endpoint.signature);
}
function hasPaginationEvidence(endpoint, contractEndpoints) {
    const contractEndpoint = contractEndpoints.get(contractKey(endpoint.httpMethod, endpoint.path));
    return contractEndpoint?.hasPagination === true || paginationPattern.test(`${endpoint.decorators}\n${endpoint.signature}\n${endpoint.body}`);
}
function stripStaticLiteralNoise(value) {
    return value
        .replace(/\/\*[\s\S]*?\*\//gu, ' ')
        .replace(/\/\/.*$/gmu, ' ')
        .replace(/(['"`])(?:\\.|(?!\1)[\s\S])*\1/gu, '""');
}
function isStaticCollectionExpression(expression) {
    const normalized = expression.trim().replace(/;$/u, '').trim();
    return (/^(?:Object\.)?(?:freeze|values|entries|keys)\s*\(/u.test(normalized) ||
        /^(?:\[[\s\S]*\]|\{[\s\S]*\})$/u.test(normalized) ||
        /^(?:[A-Z][A-Za-z0-9_]*|[a-z][A-Za-z0-9_]*(?:Config|Configs|Catalog|Catalogs|Metadata|Definitions|Types|Widgets|Constants))$/u.test(normalized));
}
function hasStaticDeclarationEvidence(fileContent, identifier) {
    const escaped = identifier.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
    return (new RegExp(`\\bimport\\s+(?:\\{[^}]*\\b${escaped}\\b[^}]*\\}|${escaped}\\b)`, 'u').test(fileContent) ||
        new RegExp(`\\b(?:const|readonly)\\s+${escaped}\\s*=\\s*(?:Object\\.freeze\\s*\\(|\\[|\\{)`, 'u').test(fileContent) ||
        new RegExp(`\\benum\\s+${escaped}\\b`, 'u').test(fileContent));
}
function isStaticCollectionBackedEndpoint(endpoint) {
    const body = endpoint.body.trim();
    if (/\b(?:await|repository|Repository|createQueryBuilder|findMany|findAll|find\s*\(|query\s*\(|execute\s*\(|service\.)\b/iu.test(body)) {
        return false;
    }
    const returnMatch = /\breturn\s+([\s\S]{0,500}?);/u.exec(body) ??
        /\bres\.(?:json|send)\s*\(\s*([\s\S]{0,500}?)\s*\)\s*;?/u.exec(body) ??
        /\bOk\s*\(\s*([\s\S]{0,500}?)\s*\)\s*;?/u.exec(body);
    if (!returnMatch?.[1]) {
        return false;
    }
    const expression = returnMatch[1].trim();
    if (/^[A-Za-z_$][\w$]*$/u.test(expression)) {
        return hasStaticDeclarationEvidence(endpoint.file.content, expression);
    }
    return isStaticCollectionExpression(stripStaticLiteralNoise(expression));
}
function detectPaginationFindings(endpoints, contractEndpoints) {
    return endpoints
        .filter((endpoint) => isCollectionGet(endpoint) && !hasPaginationEvidence(endpoint, contractEndpoints) && !isStaticCollectionBackedEndpoint(endpoint))
        .map((endpoint) => createFinding({
        id: RuleIds.MISSING_PAGINATION,
        message: `Collection endpoint ${endpoint.httpMethod.toUpperCase()} ${endpoint.path} lacks visible pagination evidence.`,
        endpoint,
    }));
}
function detectBulkFindings(endpoints) {
    const findings = [];
    for (const endpoint of endpoints) {
        if (isSkippedEndpoint(endpoint)) {
            continue;
        }
        const routeEvidence = `${endpoint.path} ${endpoint.decorators} ${endpoint.signature}`;
        const safetyEvidence = `${routeEvidence} ${endpoint.body}`;
        const looksBulk = /\b(?:bulk|batch|mass|import|export|delete-all|update-all|deleteAll|updateAll|deleteMany|updateMany)\b/iu.test(routeEvidence);
        const destructive = endpoint.httpMethod === 'delete' || (endpoint.httpMethod !== 'get' && /\b(?:delete|update|remove|process|import)\b/iu.test(routeEvidence));
        if (!looksBulk || !destructive) {
            continue;
        }
        const hasSafeguard = authEvidencePattern.test(safetyEvidence) || safeguardPattern.test(safetyEvidence);
        if (!hasSafeguard) {
            findings.push(createFinding({
                id: RuleIds.BULK_OPERATION_RISK,
                message: `Bulk/destructive endpoint ${endpoint.httpMethod.toUpperCase()} ${endpoint.path} lacks visible guard or batch safety evidence.`,
                endpoint,
            }));
        }
    }
    return findings;
}
function detectEntityLeakageFindings(endpoints) {
    const findings = [];
    for (const endpoint of endpoints) {
        if (isSkippedEndpoint(endpoint) || endpoint.name.startsWith('_')) {
            continue;
        }
        const evidence = `${endpoint.signature}\n${endpoint.body}`;
        if (dtoTypePattern.test(endpoint.signature) || mapperPattern.test(endpoint.body)) {
            continue;
        }
        const leaksByType = entityTypePattern.test(endpoint.signature);
        const leaksByDirectReturn = /\breturn\s+(?:await\s+)?(?:this\.)?\w*(?:Repository|Repo|Dao|Model)\.\w+\s*\(/u.test(endpoint.body) || /\breturn\s+(?:await\s+)?prisma\.\w+\.\w+\s*\(/u.test(endpoint.body);
        if (leaksByType || leaksByDirectReturn) {
            findings.push(createFinding({
                id: RuleIds.DTO_ENTITY_LEAKAGE,
                message: `Endpoint '${endpoint.name}' appears to expose persistence/domain entities instead of an API DTO or response contract.`,
                endpoint,
            }));
        }
    }
    return findings;
}
function detectResourceDesignFindings(endpoints) {
    const findings = [];
    const byFile = new Map();
    for (const endpoint of endpoints.filter((entry) => !isSkippedEndpoint(entry))) {
        byFile.set(endpoint.file.relativePath, [...(byFile.get(endpoint.file.relativePath) ?? []), endpoint]);
    }
    for (const entries of byFile.values()) {
        if (entries.length < 3) {
            continue;
        }
        const hasDirectId = entries.some((entry) => /\/\{\}$/u.test(normalizeApiPath(entry.path)));
        const hasById = entries.some((entry) => /\/by-id\/\{\}/u.test(normalizeApiPath(entry.path)));
        const actionRoutes = entries.filter((entry) => /\/(?:get|create|update|delete|remove|find)[A-Z_a-z-]/u.test(entry.path));
        if ((hasDirectId && hasById) || actionRoutes.length >= 2) {
            const first = entries[0];
            if (first) {
                findings.push(createFinding({
                    id: RuleIds.INCONSISTENT_RESOURCE_DESIGN,
                    message: `Controller mixes resource route conventions across ${entries.length} endpoints.`,
                    endpoint: first,
                    target: first.file.relativePath,
                }));
            }
        }
    }
    return findings;
}
function versionKind(apiPath) {
    const normalized = normalizeApiPath(apiPath);
    if (/^\/api\/v\d+(\/|$)/u.test(normalized)) {
        return 'api-versioned';
    }
    if (/^\/v\d+(\/|$)/u.test(normalized)) {
        return 'root-versioned';
    }
    return 'none';
}
function detectVersioningFindings(endpoints) {
    const relevant = endpoints.filter((entry) => !isSkippedEndpoint(entry));
    const kinds = new Set(relevant.map((entry) => versionKind(entry.path)));
    if (!kinds.has('none') || (kinds.size === 1 && kinds.has('none'))) {
        return [];
    }
    const versionedCount = relevant.filter((entry) => versionKind(entry.path) !== 'none').length;
    const unversionedCount = relevant.length - versionedCount;
    if (versionedCount < 1 || unversionedCount < 1 || relevant.length < 3) {
        return [];
    }
    const endpoint = relevant.find((entry) => versionKind(entry.path) === 'none') ?? relevant[0];
    return endpoint
        ? [createFinding({
                id: RuleIds.VERSIONING_INCONSISTENCY,
                message: 'API surface mixes versioned and unversioned routes in the same backend.',
                endpoint,
            })]
        : [];
}
function parseJsonOpenApi(content) {
    try {
        const parsed = JSON.parse(content);
        return Object.entries(parsed.paths ?? {}).flatMap(([apiPath, operations]) => Object.entries(operations)
            .filter(([method]) => httpMethods.has(method.toLowerCase()))
            .map(([method, operation]) => ({
            httpMethod: method.toLowerCase(),
            path: apiPath,
            hasPagination: paginationPattern.test(JSON.stringify(operation)),
        })));
    }
    catch {
        return undefined;
    }
}
function parseYamlOpenApi(content) {
    const endpoints = [];
    const lines = content.split(/\r?\n/u);
    let inPaths = false;
    let currentPath;
    for (let index = 0; index < lines.length; index += 1) {
        const line = lines[index] ?? '';
        if (/^paths:\s*$/u.test(line)) {
            inPaths = true;
            continue;
        }
        if (!inPaths) {
            continue;
        }
        const pathMatch = /^\s{2}(["']?\/[^:"']+[^"']*["']?):\s*$/u.exec(line);
        if (pathMatch?.[1]) {
            currentPath = pathMatch[1].replace(/^['"]|['"]$/gu, '');
            continue;
        }
        const methodMatch = /^\s{4}(get|post|put|patch|delete):\s*$/iu.exec(line);
        if (currentPath && methodMatch?.[1]) {
            const block = lines.slice(index, Math.min(lines.length, index + 30)).join('\n');
            endpoints.push({
                httpMethod: methodMatch[1].toLowerCase(),
                path: currentPath,
                hasPagination: paginationPattern.test(block),
            });
        }
    }
    return endpoints;
}
async function loadContractEndpoints(workspaceRoot, contract) {
    const openApiRelativePath = resolveOpenApiRelativePath(contract);
    const openApiPath = path.join(workspaceRoot, ...openApiRelativePath.split('/'));
    if (!(await pathExists(openApiPath))) {
        return { endpoints: new Map() };
    }
    const content = await readTextFileIfReasonable(openApiPath);
    if (!content) {
        return { endpoints: new Map() };
    }
    const parsed = parseJsonOpenApi(content) ?? parseYamlOpenApi(content);
    return {
        contractPath: openApiRelativePath,
        endpoints: new Map(parsed.map((endpoint) => [contractKey(endpoint.httpMethod, endpoint.path), endpoint])),
    };
}
function detectContractDriftFindings(endpoints, contractInput) {
    if (!contractInput.contractPath || contractInput.endpoints.size === 0 || endpoints.length === 0) {
        return [];
    }
    const businessContractEndpoints = [...contractInput.endpoints.entries()].filter(([, endpoint]) => !isSkippedEndpoint({ path: endpoint.path, name: endpoint.path }));
    if (businessContractEndpoints.length === 0) {
        return [];
    }
    const implementationEndpoints = new Map(endpoints
        .filter((endpoint) => !isSkippedEndpoint(endpoint))
        .map((endpoint) => [contractKey(endpoint.httpMethod, endpoint.path), endpoint]));
    const findings = [];
    if (contractInput.apiStyle !== 'graphql') {
        for (const [key, endpoint] of implementationEndpoints) {
            if (!contractInput.endpoints.has(key)) {
                findings.push(createFinding({
                    id: RuleIds.CONTRACT_IMPLEMENTATION_DRIFT,
                    message: `Implementation endpoint ${endpoint.httpMethod.toUpperCase()} ${endpoint.path} is missing from the OpenAPI contract.`,
                    endpoint,
                }));
            }
        }
    }
    for (const [key, endpoint] of businessContractEndpoints) {
        if (!implementationEndpoints.has(key)) {
            findings.push(createContractFinding({
                message: `OpenAPI contract endpoint ${endpoint.httpMethod.toUpperCase()} ${endpoint.path} has no matching implementation endpoint.`,
                filePath: contractInput.contractPath,
                api: `${endpoint.httpMethod.toUpperCase()} ${endpoint.path}`,
                target: key,
            }));
        }
    }
    return findings;
}
async function detectApiDesignFindings(input) {
    if (input.config.apiStyle === 'none') {
        return [];
    }
    const disabled = input.disabledRuleIds ?? new Set();
    const started = Date.now();
    const [files, contractEndpoints] = await Promise.all([
        collectSourceFiles(input.workspaceRoot, input.sourceInventory),
        loadContractEndpoints(input.workspaceRoot, input.contract),
    ]);
    (0, validationProfiling_1.emitValidationProfile)('AP-API analyzer file scan', Date.now() - started, [
        { name: 'files', value: files.length },
        { name: 'contractEndpoints', value: contractEndpoints.endpoints.size },
    ]);
    const endpoints = files.flatMap((file) => extractEndpoints(file, input.contract));
    const dynamicEndpoints = input.contract.apiStyle === 'graphql'
        ? [...contractEndpoints.endpoints.values()]
            .filter((endpoint) => contractKey(endpoint.httpMethod, endpoint.path) === contractKey('post', '/graphql'))
            .map((endpoint) => ({
            httpMethod: endpoint.httpMethod,
            path: endpoint.path,
            name: 'configured GraphQL endpoint',
            line: 1,
            decorators: 'configured GraphQL API style',
            signature: 'configured GraphQL API style',
            body: '',
            file: {
                relativePath: contractEndpoints.contractPath ?? resolveOpenApiRelativePath(input.contract),
                content: '',
                extension: '.yaml',
            },
            isConfiguredDynamic: true,
        }))
        : [];
    const implementationEndpoints = [...endpoints, ...dynamicEndpoints];
    return [
        ...detectPaginationFindings(endpoints, contractEndpoints.endpoints),
        ...detectBulkFindings(endpoints),
        ...detectEntityLeakageFindings(endpoints),
        ...detectResourceDesignFindings(endpoints),
        ...detectVersioningFindings(endpoints),
        ...detectContractDriftFindings(implementationEndpoints, {
            ...contractEndpoints,
            apiStyle: input.contract.apiStyle,
        }),
    ]
        .filter((finding) => !disabled.has(finding.id))
        .sort((left, right) => {
        const fileCompare = (left.sourceFile ?? '').localeCompare(right.sourceFile ?? '');
        if (fileCompare !== 0) {
            return fileCompare;
        }
        return left.id.localeCompare(right.id);
    });
}
//# sourceMappingURL=apiDesignAnalysis.js.map