"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.detectDataQueryRiskFindings = detectDataQueryRiskFindings;
const path = __importStar(require("node:path"));
const node_fs_1 = require("node:fs");
const ts = __importStar(require("typescript"));
const stackAdapters_1 = require("./stackAdapters");
const validationProfiling_1 = require("./validationProfiling");
const sourceScopeClassification_1 = require("./sourceScopeClassification");
const maxFileSizeBytes = 256 * 1024;
const supplementalRiskScanExtensions = ['.json', '.yaml', '.yml', '.sql'];
const backendCodeExtensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.java', '.cs', '.py', '.php', '.go'];
const supplementalIgnoredDirectoryNames = ['.vscode-test', '.tmp'];
const batchingEvidencePattern = /\b(?:DataLoader|Promise\.all|findMany\s*\([\s\S]{0,300}\bin\b|where\s*:\s*\{[\s\S]{0,300}\bin\s*:|include\s*:|join\s*\(|leftJoin|innerJoin|JOIN\b)\b/iu;
const queryReadPattern = /\b((?:this\.)?(?:prisma\.(\w+)|([A-Za-z_$][\w$]*Repository|repository))\.(find\w*|count|aggregate|groupBy)\s*\()/giu;
const unboundedLoadPattern = /\b(?:const|let|var)\s+(\w+)\s*=\s*await\s+((?:this\.)?(?:prisma\.\w+|\w*Repository|repository)\.(?:findMany|findAll|find))\s*\(([\s\S]{0,700}?)\)\s*;?[\s\S]{0,1000}\b\1\.(?:filter|map|reduce)\s*\(/iu;
const boundEvidencePattern = /\b(?:take|limit|page|cursor|skip|offset|PageRequest|Pageable|findOne|findOneOrFail|findOneBy|findOneByOrFail|findUnique|findFirst|getOne|getRawOne|first)\b/iu;
const repeatedQueryPattern = /\b((?:this\.)?(?:prisma\.(\w+)|(\w*Repository|repository))\.(find\w*|count|aggregate|groupBy)\s*\(\s*(?:\{[\s\S]{0,300}?\}|\w+)\s*\))/giu;
const tenantPredicatePattern = /\b(?:tenantId|tenant_id|organizationId|organization_id|orgId|org_id|accountId|account_id|workspaceId|workspace_id|companyId|company_id|customerId|customer_id)\b/u;
const codePaginationEvidencePattern = /\b(?:take|skip|limit|offset|page|perPage|pageSize|cursor|Pageable|PageRequest|paginate|simplePaginate|Limit|Offset|Take|Skip|TOP\s+\d+)\b/iu;
function getLineStarts(file) {
    if (file.lineStarts) {
        return file.lineStarts;
    }
    const lineStarts = [0];
    for (let index = 0; index < file.content.length; index += 1) {
        if (file.content[index] === '\n') {
            lineStarts.push(index + 1);
        }
    }
    file.lineStarts = lineStarts;
    return lineStarts;
}
function lineNumberForOffset(file, offset) {
    const lineStarts = getLineStarts(file);
    let low = 0;
    let high = lineStarts.length - 1;
    while (low <= high) {
        const middle = Math.floor((low + high) / 2);
        const lineStart = lineStarts[middle] ?? 0;
        if (lineStart <= offset) {
            low = middle + 1;
        }
        else {
            high = middle - 1;
        }
    }
    return Math.max(1, high + 1);
}
function inferModuleForPath(contract, relativePath) {
    for (const moduleEntry of listModulePathEntries(contract)) {
        if (relativePath === moduleEntry.relativePath ||
            relativePath.startsWith(`${moduleEntry.relativePath}/`)) {
            return moduleEntry.module;
        }
    }
    return undefined;
}
function createCodeFinding(input) {
    return {
        id: input.id,
        severity: 'warning',
        passed: false,
        message: input.message,
        findingType: 'file-scope',
        sourceFile: input.file.relativePath,
        filePath: input.file.relativePath,
        sourceLine: lineNumberForOffset(input.file, input.offset),
        module: input.file.module,
        target: input.target,
        ...(input.deductionKey ? { deductionKey: input.deductionKey } : {}),
    };
}
const dqrUnboundedOrmDeductionKey = 'quality:AP-DQR-003:orm-unbounded-collection-read';
const dqrUnboundedSqlDeductionKey = 'quality:AP-DQR-003:raw-sql-unbounded-select';
function toPosixRelative(workspaceRoot, absolutePath) {
    return path.relative(workspaceRoot, absolutePath).replace(/\\/g, '/');
}
function normalizeSqlTableName(raw) {
    const trimmed = raw.trim().replace(/^"+|"+$/g, '').toLowerCase();
    if (trimmed.includes('.')) {
        const [, tableName] = trimmed.split('.').slice(-2);
        return tableName ?? trimmed;
    }
    return trimmed;
}
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 collectFilesRecursive(rootPath, includeFile, ignoredDirectoryNames) {
    const collected = [];
    if (!(await pathExists(rootPath))) {
        return collected;
    }
    const stack = [rootPath];
    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) {
            if (entry.name.startsWith('.')) {
                if (!entry.isDirectory()) {
                    continue;
                }
                if (entry.name !== '.archpilot') {
                    continue;
                }
            }
            const absoluteEntryPath = path.join(current, entry.name);
            if (entry.isDirectory()) {
                if (ignoredDirectoryNames.has(entry.name)) {
                    continue;
                }
                stack.push(absoluteEntryPath);
                continue;
            }
            if (!includeFile(absoluteEntryPath)) {
                continue;
            }
            collected.push(absoluteEntryPath);
        }
    }
    return collected.sort((left, right) => left.localeCompare(right));
}
function stripSqlComments(sql) {
    return sql
        .replace(/\/\*[\s\S]*?\*\//g, ' ')
        .replace(/--.*$/gm, ' ');
}
function splitSqlStatements(sql) {
    const statements = [];
    let start = 0;
    for (let index = 0; index < sql.length; index += 1) {
        if (sql[index] !== ';') {
            continue;
        }
        const statement = sql.slice(start, index).trim();
        if (statement.length > 0) {
            const lower = statement.toLowerCase();
            const queryType = lower.startsWith('select ')
                ? 'select'
                : lower.startsWith('update ')
                    ? 'update'
                    : lower.startsWith('delete ')
                        ? 'delete'
                        : 'other';
            statements.push({
                statement,
                queryType,
                startOffset: start,
            });
        }
        start = index + 1;
    }
    const trailing = sql.slice(start).trim();
    if (trailing.length > 0) {
        const lower = trailing.toLowerCase();
        const queryType = lower.startsWith('select ')
            ? 'select'
            : lower.startsWith('update ')
                ? 'update'
                : lower.startsWith('delete ')
                    ? 'delete'
                    : 'other';
        statements.push({
            statement: trailing,
            queryType,
            startOffset: start,
        });
    }
    return statements;
}
function extractStatementTableNames(statement) {
    const sql = statement.statement;
    const tableNames = [];
    const pushMatch = (value) => {
        const normalized = normalizeSqlTableName(value);
        if (normalized.length === 0) {
            return;
        }
        if (!tableNames.includes(normalized)) {
            tableNames.push(normalized);
        }
    };
    if (statement.queryType === 'select') {
        for (const match of sql.matchAll(/\bfrom\s+([a-zA-Z0-9_."-]+)/gi)) {
            pushMatch(match[1]);
        }
        for (const match of sql.matchAll(/\bjoin\s+([a-zA-Z0-9_."-]+)/gi)) {
            pushMatch(match[1]);
        }
    }
    if (statement.queryType === 'update') {
        const match = /\bupdate\s+([a-zA-Z0-9_."-]+)/i.exec(sql);
        if (match?.[1]) {
            pushMatch(match[1]);
        }
    }
    if (statement.queryType === 'delete') {
        const match = /\bdelete\s+from\s+([a-zA-Z0-9_."-]+)/i.exec(sql);
        if (match?.[1]) {
            pushMatch(match[1]);
        }
    }
    return tableNames.sort((left, right) => left.localeCompare(right));
}
function hasTenantFilterEvidence(statement) {
    const normalized = statement.toLowerCase();
    if (!normalized.includes('tenant_id')) {
        return false;
    }
    if (/\bwhere\b[\s\S]*\btenant_id\b\s*(=|in\b|is\b|!=|<>|>=|<=|>|<)/i.test(normalized)) {
        return true;
    }
    if (/\bon\b[\s\S]*\btenant_id\b\s*=\s*[\w."`:$@?]+/i.test(normalized)) {
        return true;
    }
    return false;
}
function hasSelectStar(statement) {
    return /\bselect\s+(distinct\s+)?\*/i.test(statement);
}
function hasPaginationEvidence(statement) {
    return /\blimit\s+\d+\b|\boffset\s+\d+\b|\bfetch\s+(first|next)\b|\btop\s+\d+\b/i.test(statement);
}
function isObviousPointLookup(statement) {
    const normalized = statement.toLowerCase();
    if (!normalized.includes('where')) {
        return false;
    }
    if (normalized.includes(' or ')) {
        return false;
    }
    return /\bwhere\b[\s\S]*\b([a-z_]*id)\b\s*=\s*([\w$:@?]+|'[^']+'|\d+)/i.test(normalized);
}
function listModulePathEntries(contract) {
    return Object.entries(contract.modules ?? {})
        .map(([moduleName, reference]) => {
        const modulePath = reference.path?.replace(/\\/g, '/').trim();
        if (!modulePath) {
            return undefined;
        }
        return {
            module: moduleName,
            relativePath: modulePath,
        };
    })
        .filter((entry) => !!entry)
        .sort((left, right) => left.module.localeCompare(right.module));
}
function dedupeFindings(findings) {
    return findings
        .sort((left, right) => {
        const idCompare = left.id.localeCompare(right.id);
        if (idCompare !== 0) {
            return idCompare;
        }
        const pathCompare = left.filePath.localeCompare(right.filePath);
        if (pathCompare !== 0) {
            return pathCompare;
        }
        const moduleCompare = (left.module ?? '').localeCompare(right.module ?? '');
        if (moduleCompare !== 0) {
            return moduleCompare;
        }
        const targetCompare = (left.target ?? '').localeCompare(right.target ?? '');
        if (targetCompare !== 0) {
            return targetCompare;
        }
        const tableCompare = (left.table ?? '').localeCompare(right.table ?? '');
        if (tableCompare !== 0) {
            return tableCompare;
        }
        return left.message.localeCompare(right.message);
    })
        .filter((entry, index, values) => {
        if (index === 0) {
            return true;
        }
        const previous = values[index - 1];
        return !(entry.id === previous.id &&
            entry.filePath === previous.filePath &&
            (entry.module ?? '') === (previous.module ?? '') &&
            (entry.target ?? '') === (previous.target ?? '') &&
            (entry.table ?? '') === (previous.table ?? '') &&
            entry.message === previous.message);
    });
}
async function collectSqlFiles(workspaceRoot, contract, sourceInventory) {
    const roots = new Set();
    roots.add(path.join(workspaceRoot, 'db', 'sql'));
    if (contract.structure.databaseRoot) {
        roots.add(path.join(workspaceRoot, ...contract.structure.databaseRoot.split('/')));
    }
    for (const moduleEntry of listModulePathEntries(contract)) {
        roots.add(path.join(workspaceRoot, ...moduleEntry.relativePath.split('/'), 'db', 'sql'));
        roots.add(path.join(workspaceRoot, ...moduleEntry.relativePath.split('/'), 'sql'));
    }
    const adapter = (0, stackAdapters_1.resolvePrimaryAdapterFromContract)(contract);
    const ignoredDirectoryNames = new Set([
        ...adapter.ignoreDirectoryNames,
        ...supplementalIgnoredDirectoryNames,
    ]);
    const sqlPaths = new Set();
    for (const rootPath of [...roots].sort((left, right) => left.localeCompare(right))) {
        const filePaths = sourceInventory
            ? await sourceInventory.getFilePaths({
                rootPath,
                extensions: ['.sql'],
                ignoredDirectoryNames: [...ignoredDirectoryNames],
                useArchpilotIgnore: false,
            })
            : await collectFilesRecursive(rootPath, (filePath) => path.extname(filePath).toLowerCase() === '.sql', ignoredDirectoryNames);
        for (const filePath of filePaths) {
            sqlPaths.add(filePath);
        }
    }
    const sqlFiles = [];
    for (const absolutePath of [...sqlPaths].sort((left, right) => left.localeCompare(right))) {
        const content = await readTextFileIfReasonable(absolutePath, sourceInventory);
        if (!content) {
            continue;
        }
        sqlFiles.push({
            absolutePath,
            relativePath: toPosixRelative(workspaceRoot, absolutePath),
            content,
        });
    }
    return sqlFiles;
}
async function collectCodeFiles(workspaceRoot, contract, sourceInventory) {
    const adapter = (0, stackAdapters_1.resolvePrimaryAdapterFromContract)(contract);
    const extensions = new Set([...adapter.relevantFileExtensions, ...backendCodeExtensions]);
    const ignoredDirectoryNames = new Set([
        ...adapter.ignoreDirectoryNames,
        ...supplementalIgnoredDirectoryNames,
    ]);
    const roots = new Set();
    for (const moduleEntry of listModulePathEntries(contract)) {
        roots.add(path.join(workspaceRoot, ...moduleEntry.relativePath.split('/')));
    }
    if (roots.size === 0) {
        roots.add(workspaceRoot);
    }
    const files = [];
    const seen = new Set();
    for (const rootPath of [...roots].sort((left, right) => left.localeCompare(right))) {
        const sourceFiles = sourceInventory
            ? await sourceInventory.getTextFiles({
                rootPath,
                extensions: [...extensions],
                ignoredDirectoryNames: [...ignoredDirectoryNames],
                maxFileSizeBytes,
                useArchpilotIgnore: false,
            })
            : undefined;
        const sourceFileByPath = new Map(sourceFiles?.map((file) => [file.absolutePath, file]) ?? []);
        const filePaths = sourceFiles?.map((file) => file.absolutePath) ??
            await collectFilesRecursive(rootPath, (filePath) => extensions.has(path.extname(filePath).toLowerCase()), ignoredDirectoryNames);
        for (const absolutePath of filePaths) {
            if (seen.has(absolutePath)) {
                continue;
            }
            seen.add(absolutePath);
            const cachedFile = sourceFileByPath.get(absolutePath);
            const content = cachedFile?.content ?? await readTextFileIfReasonable(absolutePath, sourceInventory);
            if (!content) {
                continue;
            }
            const relativePath = toPosixRelative(workspaceRoot, absolutePath);
            files.push({
                absolutePath,
                relativePath,
                content,
                extension: path.extname(absolutePath).toLowerCase(),
                module: inferModuleForPath(contract, relativePath),
            });
        }
    }
    return files.sort((left, right) => left.relativePath.localeCompare(right.relativePath));
}
function getNodeNameText(name) {
    if (!name) {
        return undefined;
    }
    if (ts.isIdentifier(name) || ts.isPrivateIdentifier(name)) {
        return name.text;
    }
    if (ts.isStringLiteral(name) || ts.isNumericLiteral(name)) {
        return name.text;
    }
    return undefined;
}
function extractTypeScriptMethodBodies(file, astCache) {
    const sourceFile = astCache
        ? astCache.parseSourceFile({
            filePath: file.relativePath,
            sourceText: file.content,
            scriptTarget: ts.ScriptTarget.Latest,
            setParentNodes: true,
        })
        : ts.createSourceFile(file.relativePath, file.content, ts.ScriptTarget.Latest, true);
    const methods = [];
    const addBody = (name, body, startNode) => {
        methods.push({
            name,
            body: file.content.slice(body.getStart(sourceFile, false), body.getEnd()),
            startOffset: startNode.getStart(sourceFile, false),
            file,
        });
    };
    const visit = (node) => {
        if (ts.isMethodDeclaration(node) && node.body) {
            addBody(getNodeNameText(node.name) ?? 'method', node.body, node);
            ts.forEachChild(node.body, visit);
            return;
        }
        if (ts.isFunctionDeclaration(node) && node.body) {
            addBody(node.name?.text ?? 'function', node.body, node);
            ts.forEachChild(node.body, visit);
            return;
        }
        if ((ts.isPropertyDeclaration(node) || ts.isVariableDeclaration(node)) &&
            node.initializer &&
            (ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer)) &&
            node.initializer.body) {
            addBody(getNodeNameText(node.name) ?? 'function', node.initializer.body, node);
            ts.forEachChild(node.initializer.body, visit);
            return;
        }
        ts.forEachChild(node, visit);
    };
    visit(sourceFile);
    return methods;
}
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 + 3000);
}
function findMatchingDelimiter(content, openIndex, openChar, closeChar) {
    let depth = 0;
    for (let index = openIndex; index < content.length; index += 1) {
        const char = content[index];
        if (char === openChar) {
            depth += 1;
        }
        else if (char === closeChar) {
            depth -= 1;
            if (depth === 0) {
                return index;
            }
        }
    }
    return -1;
}
function findNextNonWhitespace(content, startOffset) {
    for (let index = startOffset; index < content.length; index += 1) {
        if (!/\s/u.test(content[index] ?? '')) {
            return index;
        }
    }
    return -1;
}
function extractLoopBodyEntries(body) {
    const loops = [];
    for (const match of body.matchAll(/\bfor\s*\(/gu)) {
        const openParen = body.indexOf('(', match.index ?? 0);
        const closeParen = findMatchingDelimiter(body, openParen, '(', ')');
        if (closeParen < 0) {
            continue;
        }
        const bodyStart = findNextNonWhitespace(body, closeParen + 1);
        if (bodyStart < 0) {
            continue;
        }
        if (body[bodyStart] === '{') {
            const closeBrace = findMatchingBrace(body, bodyStart);
            loops.push({ body: body.slice(bodyStart, closeBrace + 1), offset: bodyStart });
        }
        else {
            const statementEnd = body.indexOf(';', bodyStart);
            if (statementEnd > bodyStart) {
                loops.push({ body: body.slice(bodyStart, statementEnd + 1), offset: bodyStart });
            }
        }
    }
    for (const match of body.matchAll(/\b(?:forEach|map)\s*\(/gu)) {
        const openParen = body.indexOf('(', match.index ?? 0);
        const closeParen = findMatchingDelimiter(body, openParen, '(', ')');
        if (closeParen > openParen) {
            loops.push({ body: body.slice(openParen + 1, closeParen), offset: openParen + 1 });
        }
    }
    return loops;
}
function extractJavaMethodBodies(file) {
    const methods = [];
    const pattern = /^\s*(?:@\w+(?:\([^)]*\))?\s*)*(?:public|private|protected)\s+[\w<>, ?\[\]]+\s+(\w+)\s*\([^)]*\)\s*\{/gmu;
    for (const match of file.content.matchAll(pattern)) {
        const openBrace = (match.index ?? 0) + match[0].lastIndexOf('{');
        const end = findMatchingBrace(file.content, openBrace);
        methods.push({
            name: match[1] ?? 'method',
            body: file.content.slice(openBrace, end + 1),
            startOffset: match.index ?? 0,
            file,
        });
    }
    return methods;
}
function braceMethodPatternForExtension(extension) {
    if (extension === '.java') {
        return /^\s*(?:@\w+(?:\([^)]*\))?\s*)*(?:public|private|protected)\s+[\w<>, ?\[\]]+\s+(\w+)\s*\([^)]*\)\s*\{/gmu;
    }
    if (extension === '.cs') {
        return /^\s*(?:\[[^\]]+\]\s*)*(?:public|private|protected|internal)\s+(?:async\s+)?[\w<>, ?\[\]]+\s+(\w+)\s*\([^)]*\)\s*\{/gmu;
    }
    if (extension === '.php') {
        return /^\s*(?:(?:public|private|protected|static|final)\s+)*function\s+(\w+)\s*\([^)]*\)\s*(?::\s*[^{]+)?\s*\{/gmu;
    }
    if (extension === '.go') {
        return /^\s*func\s+(?:\([^)]*\)\s*)?([A-Za-z_]\w*)\s*\([^)]*\)\s*(?:\([^)]*\)|[\w*.[\]]+)?\s*\{/gmu;
    }
    return /^\s*(?:export\s+)?(?:public\s+|private\s+|protected\s+)?(?:async\s+)?(?:function\s+)?(\w+)\s*\([^)]*\)\s*(?::\s*[^{]+)?\s*\{/gmu;
}
function extractBraceMethodBodies(file) {
    const methods = [];
    const pattern = braceMethodPatternForExtension(file.extension);
    for (const match of file.content.matchAll(pattern)) {
        const openBrace = (match.index ?? 0) + match[0].lastIndexOf('{');
        const end = findMatchingBrace(file.content, openBrace);
        methods.push({
            name: match[1] ?? 'method',
            body: file.content.slice(openBrace, end + 1),
            startOffset: match.index ?? 0,
            file,
        });
    }
    return methods;
}
function leadingWhitespaceLength(value) {
    return value.length - value.trimStart().length;
}
function extractPythonMethodBodies(file) {
    const methods = [];
    const lineStarts = getLineStarts(file);
    for (let lineIndex = 0; lineIndex < lineStarts.length; lineIndex += 1) {
        const lineStart = lineStarts[lineIndex] ?? 0;
        const lineEnd = file.content.indexOf('\n', lineStart);
        const line = file.content.slice(lineStart, lineEnd === -1 ? file.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 = file.content.length;
        for (let cursor = lineIndex + 1; cursor < lineStarts.length; cursor += 1) {
            const cursorStart = lineStarts[cursor] ?? 0;
            const cursorEnd = file.content.indexOf('\n', cursorStart);
            const cursorLine = file.content.slice(cursorStart, cursorEnd === -1 ? file.content.length : cursorEnd);
            if (cursorLine.trim().length === 0) {
                continue;
            }
            if (leadingWhitespaceLength(cursorLine) <= definitionIndent) {
                bodyEnd = cursorStart;
                break;
            }
        }
        methods.push({
            name: match[1],
            body: file.content.slice(lineStart, bodyEnd),
            startOffset: lineStart,
            file,
        });
    }
    return methods;
}
function extractMethodBodies(file, astCache) {
    if (file.extension === '.java') {
        return extractJavaMethodBodies(file);
    }
    if (file.extension === '.py') {
        return extractPythonMethodBodies(file);
    }
    if (file.extension === '.cs' || file.extension === '.php' || file.extension === '.go') {
        return extractBraceMethodBodies(file);
    }
    return extractTypeScriptMethodBodies(file, astCache);
}
function collectTenantScopedTables(sqlFiles) {
    const tables = new Set();
    for (const sqlFile of sqlFiles) {
        const statements = splitSqlStatements(stripSqlComments(sqlFile.content));
        for (const statement of statements) {
            if (!/^create\s+table\b/i.test(statement.statement)) {
                continue;
            }
            if (!/\btenant_id\b/i.test(statement.statement)) {
                continue;
            }
            const match = /\bcreate\s+table\s+(if\s+not\s+exists\s+)?([a-zA-Z0-9_."-]+)/i.exec(statement.statement);
            if (!match?.[2]) {
                continue;
            }
            tables.add(normalizeSqlTableName(match[2]));
        }
    }
    return tables;
}
async function detectCrossModuleDirectDatabaseAccess(workspaceRoot, contract, sourceInventory) {
    const started = Date.now();
    const findings = [];
    const adapter = (0, stackAdapters_1.resolvePrimaryAdapterFromContract)(contract);
    const codeFileExtensions = new Set([
        ...adapter.relevantFileExtensions,
        ...supplementalRiskScanExtensions,
    ]);
    const ignoredDirectoryNames = new Set([
        ...adapter.ignoreDirectoryNames,
        ...supplementalIgnoredDirectoryNames,
    ]);
    const moduleEntries = listModulePathEntries(contract);
    let scannedFiles = 0;
    for (const sourceModule of moduleEntries) {
        const sourceRoot = path.join(workspaceRoot, ...sourceModule.relativePath.split('/'));
        const sourceFiles = sourceInventory
            ? await sourceInventory.getTextFiles({
                rootPath: sourceRoot,
                extensions: [...codeFileExtensions],
                ignoredDirectoryNames: [...ignoredDirectoryNames],
                maxFileSizeBytes,
                useArchpilotIgnore: false,
            })
            : undefined;
        const sourceFilePaths = sourceFiles?.map((file) => file.absolutePath) ??
            await collectFilesRecursive(sourceRoot, (filePath) => codeFileExtensions.has(path.extname(filePath).toLowerCase()), ignoredDirectoryNames);
        const sourceFileByPath = new Map(sourceFiles?.map((file) => [file.absolutePath, file]) ?? []);
        scannedFiles += sourceFilePaths.length;
        for (const sourceFilePath of sourceFilePaths) {
            const content = sourceFileByPath.get(sourceFilePath)?.content ??
                await readTextFileIfReasonable(sourceFilePath, sourceInventory);
            if (!content) {
                continue;
            }
            const normalized = content.replace(/\\/g, '/').toLowerCase();
            for (const targetModule of moduleEntries) {
                if (targetModule.module === sourceModule.module) {
                    continue;
                }
                const targetModuleLower = targetModule.module.toLowerCase();
                const targetPathLower = targetModule.relativePath.replace(/\\/g, '/').toLowerCase();
                const directDbPattern = `/${targetModuleLower}/db/`;
                const directSqlPattern = `/${targetModuleLower}/sql/`;
                const targetPathDbPattern = `${targetPathLower}/db/`;
                const targetPathSqlPattern = `${targetPathLower}/sql/`;
                const hasDirectReference = normalized.includes(directDbPattern) ||
                    normalized.includes(directSqlPattern) ||
                    normalized.includes(targetPathDbPattern) ||
                    normalized.includes(targetPathSqlPattern);
                if (!hasDirectReference) {
                    continue;
                }
                findings.push({
                    id: 'AP-DQR-004',
                    severity: 'warning',
                    passed: false,
                    message: `Module '${sourceModule.module}' references database artifacts under module '${targetModule.module}', bypassing module boundary expectations.`,
                    findingType: 'module-dependency',
                    sourceFile: toPosixRelative(workspaceRoot, sourceFilePath),
                    filePath: toPosixRelative(workspaceRoot, sourceFilePath),
                    module: sourceModule.module,
                    target: targetModule.module,
                });
            }
        }
    }
    (0, validationProfiling_1.emitValidationProfile)('data/query risk analyzer cross-module scan', Date.now() - started, [
        { name: 'files', value: scannedFiles },
        { name: 'modules', value: moduleEntries.length },
    ]);
    return findings;
}
function normalizeQueryCall(value) {
    return value
        .replace(/\s+/gu, ' ')
        .replace(/(["'`])[^"'`]{1,120}\1/gu, '<literal>')
        .replace(/\b[a-f0-9-]{8,}\b/giu, '<id>')
        .trim()
        .toLowerCase();
}
function normalizeRepositoryKey(value) {
    return value
        .replace(/^this\./u, '')
        .replace(/Repository$/u, '')
        .toLowerCase();
}
function detectNPlusOneQueryRisk(method) {
    if (batchingEvidencePattern.test(method.body)) {
        return [];
    }
    const evidences = [];
    for (const loop of extractLoopBodyEntries(method.body)) {
        for (const match of loop.body.matchAll(queryReadPattern)) {
            const repositoryName = match[2] ?? match[3] ?? 'repository';
            const operation = match[4] ?? 'find';
            evidences.push({
                method,
                offset: method.startOffset + loop.offset + (match.index ?? 0),
                repositoryKey: normalizeRepositoryKey(repositoryName),
                queryLabel: `${repositoryName}.${operation}`,
            });
        }
    }
    return evidences;
}
function aggregateNPlusOneFindings(evidences) {
    const grouped = new Map();
    for (const evidence of evidences) {
        const key = evidence.method.file.relativePath;
        const group = grouped.get(key);
        if (group) {
            group.push(evidence);
        }
        else {
            grouped.set(key, [evidence]);
        }
    }
    return [...grouped.values()].map((group) => {
        const first = group[0];
        const file = first?.method.file;
        if (!first || !file) {
            throw new Error('Cannot aggregate AP-DQR-005 evidence without a representative method.');
        }
        const affectedMethods = [...new Set(group.map((evidence) => evidence.method.name))]
            .sort((left, right) => left.localeCompare(right));
        const representativeCalls = [...new Set(group.map((evidence) => evidence.queryLabel))]
            .sort((left, right) => left.localeCompare(right));
        const repositoryKeys = [...new Set(group.map((evidence) => evidence.repositoryKey))]
            .sort((left, right) => left.localeCompare(right));
        const methodSummary = affectedMethods.slice(0, 6).join(', ');
        const callSummary = representativeCalls.slice(0, 4).join(', ');
        const repositorySummary = repositoryKeys.slice(0, 6).join(', ');
        const extraMethodCount = Math.max(0, affectedMethods.length - 6);
        const extraRepositoryCount = Math.max(0, repositoryKeys.length - 6);
        return createCodeFinding({
            id: 'AP-DQR-005',
            message: `Potential N+1 query risk: ${affectedMethods.length} method(s) in '${file.relativePath}' contain ` +
                `repository/database reads inside collection loops without clear batching evidence. ` +
                `Affected methods: ${methodSummary}${extraMethodCount > 0 ? `, +${extraMethodCount} more` : ''}. ` +
                `Repository/model families: ${repositorySummary}${extraRepositoryCount > 0 ? `, +${extraRepositoryCount} more` : ''}. ` +
                `Loop query evidence: ${callSummary}. Risk instances: ${group.length}.`,
            file,
            offset: first.offset,
            target: 'n-plus-one',
        });
    });
}
function detectLargeCollectionLoading(method) {
    const match = unboundedLoadPattern.exec(method.body);
    if (!match || boundEvidencePattern.test(match[3] ?? '')) {
        return [];
    }
    return [
        createCodeFinding({
            id: 'AP-DQR-006',
            message: `Large collection loading risk: method '${method.name}' loads '${match[1]}' without bounds and then processes it in memory.`,
            file: method.file,
            offset: method.startOffset + (match.index ?? 0),
            target: method.name,
        }),
    ];
}
function detectMissingProjection(method) {
    const findings = [];
    const pattern = /\b(?:const|let)\s+(\w+)\s*=\s*await\s+((?:this\.)?(?:prisma\.(\w+)|(\w*Repository|repository))\.(?:findMany|findAll))\s*\(([\s\S]{0,700}?)\)\s*;?([\s\S]{0,900})/giu;
    for (const match of method.body.matchAll(pattern)) {
        const variableName = match[1];
        const modelName = match[3] ?? match[4] ?? 'entity';
        const queryArgs = match[5] ?? '';
        const following = match[6] ?? '';
        if (!variableName ||
            /\b(?:select|projection|fields|attributes)\b/iu.test(queryArgs) ||
            new RegExp(`\\breturn\\s+${variableName}\\s*;`, 'u').test(following)) {
            continue;
        }
        const fieldMatches = [...following.matchAll(new RegExp(`\\b${variableName}\\.(?:map|filter)\\s*\\([\\s\\S]{0,500}?\\.([A-Za-z_]\\w*)`, 'gu'))]
            .map((fieldMatch) => fieldMatch[1])
            .filter((field) => !!field);
        const uniqueFields = [...new Set(fieldMatches)];
        if (uniqueFields.length === 0 || uniqueFields.length > 3) {
            continue;
        }
        findings.push(createCodeFinding({
            id: 'AP-DQR-007',
            message: `Missing projection risk: method '${method.name}' reads '${modelName}' entities but only references ${uniqueFields.join(', ')} afterward.`,
            file: method.file,
            offset: method.startOffset + (match.index ?? 0),
            target: method.name,
        }));
    }
    return findings.slice(0, 1);
}
function detectRepeatedQueryPattern(method) {
    const normalizedCounts = new Map();
    for (const match of method.body.matchAll(repeatedQueryPattern)) {
        const call = match[1];
        if (!call) {
            continue;
        }
        const normalized = normalizeQueryCall(call);
        const current = normalizedCounts.get(normalized);
        if (current) {
            current.count += 1;
        }
        else {
            normalizedCounts.set(normalized, {
                count: 1,
                index: match.index ?? 0,
                label: `${match[2] ?? match[3] ?? 'repository'}.${match[4] ?? 'find'}`,
            });
        }
    }
    const repeated = [...normalizedCounts.values()].find((entry) => entry.count >= 2);
    if (!repeated) {
        return [];
    }
    return [
        createCodeFinding({
            id: 'AP-DQR-008',
            message: `Repeated query pattern: method '${method.name}' performs the same '${repeated.label}' query ${repeated.count} times in one flow.`,
            file: method.file,
            offset: method.startOffset + repeated.index,
            target: method.name,
        }),
    ];
}
function extractSqlQueryLiterals(content) {
    const queries = [];
    const patterns = [
        /(?:\b(?:[\w$]+\.)?(?:query|execute|executeQuery|createNativeQuery|createQuery|FromSqlRaw|FromSqlInterpolated|Query|QueryContext|QueryRow|QueryRowContext|Exec|ExecContext|Raw|text)|\$queryRaw|\$executeRaw)\s*\([^'"`)]{0,180}?(['"`])([\s\S]{0,900}?)\1/giu,
        /\b(?:DB::select|DB::statement|DB::raw|DB::table)\s*\([^'"`)]{0,180}?(['"`])([\s\S]{0,900}?)\1/gu,
        /@Query\s*\(\s*(['"`])([\s\S]{0,900}?)\1/gu,
    ];
    for (const pattern of patterns) {
        for (const match of content.matchAll(pattern)) {
            const sql = match[2];
            if (!sql || !/\b(?:select|update|delete|insert)\b/iu.test(sql)) {
                continue;
            }
            queries.push({
                sql,
                offset: (match.index ?? 0) + match[0].indexOf(sql),
            });
        }
    }
    return queries;
}
function detectCodeSqlQueryShapeFindings(file, config) {
    const findings = [];
    for (const query of extractSqlQueryLiterals(file.content)) {
        for (const statement of splitSqlStatements(stripSqlComments(query.sql))) {
            if (statement.queryType === 'other') {
                continue;
            }
            const tableNames = extractStatementTableNames(statement);
            const firstTable = tableNames[0];
            const offset = query.offset + statement.startOffset;
            if (config.tenantModel === 'same_db_same_schema' &&
                (statement.queryType === 'select' || statement.queryType === 'update' || statement.queryType === 'delete') &&
                !hasTenantFilterEvidence(statement.statement) &&
                !tenantPredicatePattern.test(statement.statement)) {
                findings.push(createCodeFinding({
                    id: 'AP-DQR-001',
                    message: `Potential tenant-isolation risk: ${statement.queryType.toUpperCase()} query${firstTable ? ` on table '${firstTable}'` : ''} has no tenant predicate evidence.`,
                    file,
                    offset,
                    target: firstTable ?? 'sql-query',
                }));
            }
            if (statement.queryType === 'select' && hasSelectStar(statement.statement)) {
                findings.push(createCodeFinding({
                    id: 'AP-DQR-002',
                    message: `Broad SELECT * usage detected in code query path${firstTable ? ` (table '${firstTable}')` : ''}.`,
                    file,
                    offset,
                    target: firstTable ?? 'sql-query',
                }));
            }
            if (statement.queryType === 'select' &&
                !isOperationalOrBatchQueryContext(file) &&
                !hasPaginationEvidence(statement.statement) &&
                !isObviousPointLookup(statement.statement) &&
                !/\bcount\s*\(/i.test(statement.statement)) {
                findings.push(createCodeFinding({
                    id: 'AP-DQR-003',
                    message: `Potential unbounded query risk: SELECT query${firstTable ? ` on table '${firstTable}'` : ''} has no LIMIT/OFFSET/FETCH pagination evidence.`,
                    file,
                    offset,
                    target: firstTable ?? 'sql-query',
                    deductionKey: dqrUnboundedSqlDeductionKey,
                }));
            }
        }
    }
    return findings;
}
function collectOrmReadEvidence(method) {
    const patterns = [
        [/\b(?:this\.)?prisma\.\w+\.findMany\s*\(/giu, 'prisma.findMany'],
        [/\b(?:sequelize|model|[A-Z]\w*)\.findAll\s*\(/gu, 'findAll'],
        [/\bsession\.query\s*\(/giu, 'sqlalchemy.query'],
        [/\bselect\s*\(\s*[A-Z]\w*/gu, 'sqlalchemy.select'],
        [/\b(?:DB::table|[A-Z]\w*::(?:query|where|all))\s*\(/g, 'laravel.query'],
        [/->get\s*\(/g, 'laravel.get'],
        [/\b(?:db|tx)\.(?:Query|QueryContext|QueryRow|QueryRowContext)\s*\(/g, 'go.sql'],
        [/\b(?:db|gormDb)\.(?:Find|Raw|Where)\s*\(/g, 'go.orm'],
        [/\b(?:knex\s*\(|db\.query\s*\(|dataSource\.createQueryBuilder\s*\(|createQueryBuilder\s*\()/giu, 'node.query'],
        [/\b(?:this\.)?db\.[A-Z]\w*\.(?:Where|ToList|ToListAsync)\s*\(/g, 'ef.query'],
    ];
    const evidence = [];
    for (const [pattern, label] of patterns) {
        for (const match of method.body.matchAll(pattern)) {
            const relativeOffset = match.index ?? 0;
            evidence.push({
                offset: method.startOffset + relativeOffset,
                snippet: method.body.slice(relativeOffset, relativeOffset + 600),
                label,
            });
        }
    }
    return evidence;
}
function getOrmTerminalOperation(snippet) {
    const match = /\.(getMany|getRawMany|loadMany|getOne|getRawOne|loadOne|getCount|getExists|execute|findOne|findOneOrFail|findOneBy|findOneByOrFail|findUnique|findFirst|count|exists|update|delete|remove|save|insert)\s*\(/iu.exec(snippet);
    return match?.[1];
}
function isOperationalOrBatchQueryContext(file, methodName) {
    const pathValue = file.relativePath.toLowerCase();
    const segments = pathValue.split('/');
    const method = methodName?.toLowerCase() ?? '';
    return (segments.some((segment) => /\b(?:health|cache|job|queue|scheduler|indexer|telemetry|metrics)\b/u.test(segment.replace(/-/g, ' '))) ||
        /(^|\/)(?:health-check|healthchecks?|cache|job-queue|scheduler|search|indexer|telemetry|metrics)(?:\/|-)/u.test(pathValue) ||
        /(?:health-check|healthcheck|cache|job-queue|scheduler|search|indexer|telemetry|metrics)\.(?:ts|tsx|js|jsx|java|cs|py|php|go)$/u.test(pathValue) ||
        /\b(?:health|cache|job|queue|schedule|scheduler|index|telemetry|metrics|buffer|worker|batch)\b/u.test(method) ||
        method.includes('batch') ||
        /^on(?:moduleinit|applicationbootstrap|applicationstart|init)$/u.test(method));
}
function isScopedRelationLoad(snippet) {
    return (/\.relation\s*\(/iu.test(snippet) &&
        /\.of\s*\(\s*[^)]{1,180}\)\s*\.loadMany\s*\(/iu.test(snippet));
}
function hasAssignedBuilderBoundedTerminal(methodBody, snippet) {
    const variableMatch = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:[\s\S]{0,240}?)createQueryBuilder\s*\(/iu.exec(snippet);
    const variableName = variableMatch?.[1];
    if (!variableName) {
        return false;
    }
    return new RegExp(`\\b${variableName}\\s*\\.\\s*(?:getOne|getRawOne|loadOne|getCount|getExists)\\s*\\(`, 'iu').test(methodBody);
}
function isNonCollectionOrmRead(label, snippet, methodBody = snippet) {
    const terminal = getOrmTerminalOperation(snippet);
    return (/\b(?:findOne|findOneOrFail|findOneBy|findOneByOrFail|findUnique|findFirst|count|exists|aggregate)\b/iu.test(label) ||
        !!terminal && /^(?:getOne|getRawOne|loadOne|getCount|getExists|findOne|findOneOrFail|findOneBy|findOneByOrFail|findUnique|findFirst|count|exists|update|delete|remove|save|insert|execute)$/iu.test(terminal) ||
        isScopedRelationLoad(snippet) ||
        hasAssignedBuilderBoundedTerminal(methodBody, snippet));
}
function hasOrmBoundEvidence(snippet) {
    return (codePaginationEvidencePattern.test(snippet) ||
        /\.(?:take|limit|offset|skip)\s*\(/iu.test(snippet) ||
        /\b(?:maxResults|setMaxResults|firstResult|setFirstResult)\s*\(/iu.test(snippet) ||
        /\b(?:pageSize|perPage|limit|take)\s*[:=]\s*\d+\b/iu.test(snippet));
}
function getOrmQueryTarget(method, label, snippet) {
    const terminal = getOrmTerminalOperation(snippet);
    if (terminal) {
        return `${method.name}.${terminal}`;
    }
    const repositoryMatch = /\b(?:this\.)?([A-Za-z_$][\w$]*(?:Repository|Repo|Dao)|repository|prisma\.\w+)\./u.exec(snippet);
    if (repositoryMatch?.[1]) {
        return `${method.name}.${repositoryMatch[1]}`;
    }
    return method.name || label;
}
function getOrmQueryLabel(method, label, snippet) {
    const terminal = getOrmTerminalOperation(snippet);
    if (terminal) {
        return `${label}.${terminal}`;
    }
    return label === 'node.query' ? method.name : label;
}
function detectOrmQueryShapeFindings(method, config) {
    const findings = [];
    for (const evidence of collectOrmReadEvidence(method)) {
        if (config.tenantModel === 'same_db_same_schema' && !tenantPredicatePattern.test(evidence.snippet)) {
            findings.push(createCodeFinding({
                id: 'AP-DQR-001',
                message: `Potential tenant-isolation risk: ORM/query-builder read '${evidence.label}' has no tenant predicate evidence.`,
                file: method.file,
                offset: evidence.offset,
                target: evidence.label,
            }));
        }
        if (!isOperationalOrBatchQueryContext(method.file, method.name) &&
            !isNonCollectionOrmRead(evidence.label, evidence.snippet, method.body) &&
            !hasOrmBoundEvidence(evidence.snippet)) {
            const target = getOrmQueryTarget(method, evidence.label, evidence.snippet);
            const label = getOrmQueryLabel(method, evidence.label, evidence.snippet);
            findings.push(createCodeFinding({
                id: 'AP-DQR-003',
                message: `Potential unbounded query risk: ORM/query-builder read '${label}' in method '${method.name}' has no limit or pagination evidence.`,
                file: method.file,
                offset: evidence.offset,
                target,
                deductionKey: dqrUnboundedOrmDeductionKey,
            }));
        }
    }
    return findings.slice(0, 3);
}
function detectCodeQueryRiskFindings(files, config, astCache) {
    const findings = [];
    const nPlusOneEvidence = [];
    for (const file of files) {
        const scope = (0, sourceScopeClassification_1.classifyValidationSourceScope)(file.relativePath);
        const isProductionBackend = (0, sourceScopeClassification_1.isProductionBackendGovernanceScope)(scope);
        if (isProductionBackend) {
            findings.push(...detectCodeSqlQueryShapeFindings(file, config));
        }
        for (const method of extractMethodBodies(file, astCache)) {
            if (!isProductionBackend) {
                continue;
            }
            nPlusOneEvidence.push(...detectNPlusOneQueryRisk(method));
            findings.push(...detectOrmQueryShapeFindings(method, config), ...detectLargeCollectionLoading(method), ...detectMissingProjection(method), ...detectRepeatedQueryPattern(method));
        }
    }
    return [
        ...aggregateNPlusOneFindings(nPlusOneEvidence),
        ...findings,
    ];
}
async function detectDataQueryRiskFindings(input) {
    const { workspaceRoot, config, contract, disabledRuleIds } = input;
    if (config.database === 'none') {
        return [];
    }
    const findings = [];
    const sqlStarted = Date.now();
    const sqlFiles = await collectSqlFiles(workspaceRoot, contract, input.sourceInventory);
    (0, validationProfiling_1.emitValidationProfile)('data/query risk analyzer SQL scan', Date.now() - sqlStarted, [
        { name: 'files', value: sqlFiles.length },
    ]);
    const tenantScopedTables = collectTenantScopedTables(sqlFiles);
    for (const sqlFile of sqlFiles) {
        const scope = (0, sourceScopeClassification_1.classifyValidationSourceScope)(sqlFile.relativePath);
        const isProductionBackend = (0, sourceScopeClassification_1.isProductionBackendGovernanceScope)(scope);
        const statements = splitSqlStatements(stripSqlComments(sqlFile.content));
        for (const statement of statements) {
            if (statement.queryType === 'other') {
                continue;
            }
            const tableNames = extractStatementTableNames(statement);
            const firstTable = tableNames[0];
            if (config.tenantModel === 'same_db_same_schema' &&
                !(disabledRuleIds?.has('AP-DQR-001') ?? false) &&
                (statement.queryType === 'select' ||
                    statement.queryType === 'update' ||
                    statement.queryType === 'delete') &&
                tableNames.some((tableName) => tenantScopedTables.has(tableName)) &&
                !hasTenantFilterEvidence(statement.statement)) {
                findings.push({
                    id: 'AP-DQR-001',
                    severity: 'warning',
                    passed: false,
                    message: `Potential tenant-isolation risk: ${statement.queryType.toUpperCase()} query on tenant-scoped table${firstTable ? ` '${firstTable}'` : ''} has no tenant_id filter evidence.`,
                    findingType: 'table-scope',
                    sourceFile: sqlFile.relativePath,
                    filePath: sqlFile.relativePath,
                    ...(firstTable ? { table: firstTable } : {}),
                });
            }
            if (statement.queryType === 'select' &&
                isProductionBackend &&
                !(disabledRuleIds?.has('AP-DQR-002') ?? false) &&
                hasSelectStar(statement.statement)) {
                findings.push({
                    id: 'AP-DQR-002',
                    severity: 'warning',
                    passed: false,
                    message: `Broad SELECT * usage detected in query path${firstTable ? ` (table '${firstTable}')` : ''}.`,
                    findingType: 'table-scope',
                    sourceFile: sqlFile.relativePath,
                    filePath: sqlFile.relativePath,
                    ...(firstTable ? { table: firstTable } : {}),
                });
            }
            if (statement.queryType === 'select' &&
                isProductionBackend &&
                !(disabledRuleIds?.has('AP-DQR-003') ?? false) &&
                !hasPaginationEvidence(statement.statement) &&
                !isObviousPointLookup(statement.statement) &&
                !/\bcount\s*\(/i.test(statement.statement)) {
                findings.push({
                    id: 'AP-DQR-003',
                    severity: 'warning',
                    passed: false,
                    message: `Potential unbounded query risk: SELECT query${firstTable ? ` on table '${firstTable}'` : ''} has no LIMIT/OFFSET/FETCH pagination evidence.`,
                    findingType: 'table-scope',
                    sourceFile: sqlFile.relativePath,
                    filePath: sqlFile.relativePath,
                    deductionKey: dqrUnboundedSqlDeductionKey,
                    ...(firstTable ? { table: firstTable } : {}),
                });
            }
        }
    }
    if (!(disabledRuleIds?.has('AP-DQR-004') ?? false)) {
        findings.push(...(await detectCrossModuleDirectDatabaseAccess(workspaceRoot, contract, input.sourceInventory)));
    }
    const codeStarted = Date.now();
    const codeFiles = await collectCodeFiles(workspaceRoot, contract, input.sourceInventory);
    (0, validationProfiling_1.emitValidationProfile)('data/query risk analyzer code scan', Date.now() - codeStarted, [
        { name: 'files', value: codeFiles.length },
    ]);
    findings.push(...detectCodeQueryRiskFindings(codeFiles, config, input.astCache).filter((finding) => !(disabledRuleIds?.has(finding.id) ?? false)));
    return dedupeFindings(findings);
}
//# sourceMappingURL=dataQueryRiskDetection.js.map