/**
 * Antagonist Challenger Module
 *
 * The internal critic that:
 * 1. Checks for missing coverage (areas no agent scanned)
 * 2. Identifies findings with low cross-verification
 * 3. Flags potential false positives
 * 4. Challenges assumptions
 * 5. Detects inconsistent findings
 *
 * @module agents/antagonist/challenger
 */
import Anthropic from "@anthropic-ai/sdk";
import { ATTACK_VECTORS, CATEGORY_TO_VECTOR } from "./types.js";
import { CHALLENGER_SYSTEM_PROMPT, buildChallengerPrompt } from "./prompts.js";
import { logger } from "../../logger.js";
/**
 * Check which attack vectors are not covered by findings
 */
function findUncoveredVectors(findings) {
    const coveredVectors = new Set();
    for (const finding of findings) {
        const vector = CATEGORY_TO_VECTOR[finding.category];
        if (vector) {
            coveredVectors.add(vector);
        }
    }
    return ATTACK_VECTORS.filter((v) => !coveredVectors.has(v));
}
/**
 * Find findings with low cross-verification
 */
function findUnverifiedFindings(findings) {
    return findings.filter((f) => {
        if (!f.verifications || f.verifications.length === 0) {
            return f.severity === "critical" || f.severity === "high";
        }
        const confirmations = f.verifications.filter((v) => v.verdict === "confirmed").length;
        const disputes = f.verifications.filter((v) => v.verdict === "disputed").length;
        return disputes > confirmations;
    });
}
/**
 * Detect potential false positives based on patterns
 */
function detectPotentialFalsePositives(findings) {
    const suspects = [];
    for (const finding of findings) {
        const isSuspect = (finding.file?.includes("test") ||
            finding.file?.includes("spec") ||
            finding.file?.includes("mock")) &&
            finding.severity !== "critical";
        const isLowConfidence = finding.confidence < 70;
        const isGenericDescription = finding.description.length < 50 ||
            finding.description.includes("TODO") ||
            finding.description.includes("FIXME");
        if (isSuspect || isLowConfidence || isGenericDescription) {
            suspects.push(finding);
        }
    }
    return suspects;
}
/**
 * Find inconsistent findings across agents
 */
function findInconsistencies(findings) {
    const inconsistencies = [];
    const byFile = new Map();
    for (const f of findings) {
        if (!f.file)
            continue;
        const existing = byFile.get(f.file) || [];
        existing.push(f);
        byFile.set(f.file, existing);
    }
    for (const [file, fileFindings] of byFile) {
        if (fileFindings.length < 2)
            continue;
        for (let i = 0; i < fileFindings.length; i++) {
            for (let j = i + 1; j < fileFindings.length; j++) {
                const f1 = fileFindings[i];
                const f2 = fileFindings[j];
                if (f1.category === f2.category &&
                    Math.abs((f1.line || 0) - (f2.line || 0)) < 5 &&
                    f1.severity !== f2.severity) {
                    inconsistencies.push({
                        finding1: f1,
                        finding2: f2,
                        reason: `Same category (${f1.category}) at similar location but different severities (${f1.severity} vs ${f2.severity})`,
                    });
                }
            }
        }
    }
    return inconsistencies;
}
/**
 * Run challenger analysis without LLM
 */
export function runChallengerDeterministic(findings, agentSummaries, config) {
    const assessments = [];
    const uncoveredVectors = findUncoveredVectors(findings);
    for (const vector of uncoveredVectors.slice(0, 5)) {
        assessments.push({
            id: `chal-gap-${vector}`,
            type: "missed_check",
            targetAgent: "security",
            challenge: `Attack vector "${vector}" was not tested`,
            evidence: `No findings in categories mapped to ${vector}`,
            suggestedAction: `Add explicit checks for ${vector} vulnerabilities`,
            severity: "medium",
            confidence: 80,
        });
    }
    const unverifiedFindings = findUnverifiedFindings(findings);
    for (const finding of unverifiedFindings.slice(0, 5)) {
        const disputes = finding.verifications?.filter((v) => v.verdict === "disputed") || [];
        assessments.push({
            id: `chal-unverified-${finding.id}`,
            type: "insufficient_evidence",
            targetAgent: finding.scanner_source || "security",
            targetFindingId: finding.id,
            challenge: `Finding has ${disputes.length} disputes and insufficient verification`,
            evidence: disputes.map((d) => d.evidence).join("; ") || "No cross-verification",
            suggestedAction: "Re-verify this finding with additional evidence",
            severity: finding.severity,
            confidence: 75,
        });
    }
    const potentialFPs = detectPotentialFalsePositives(findings);
    for (const finding of potentialFPs.slice(0, 5)) {
        const reasons = [];
        if (finding.file?.includes("test"))
            reasons.push("in test file");
        if (finding.confidence < 70)
            reasons.push("low confidence");
        if (finding.description.length < 50)
            reasons.push("generic description");
        assessments.push({
            id: `chal-fp-${finding.id}`,
            type: "false_positive_likely",
            targetAgent: finding.scanner_source || "security",
            targetFindingId: finding.id,
            challenge: `Likely false positive: ${reasons.join(", ")}`,
            evidence: `File: ${finding.file}, Confidence: ${finding.confidence}%`,
            suggestedAction: "Review and potentially dismiss this finding",
            severity: "low",
            confidence: 65,
        });
    }
    const inconsistencies = findInconsistencies(findings);
    for (const { finding1, finding2, reason } of inconsistencies.slice(0, 3)) {
        assessments.push({
            id: `chal-inconsistent-${finding1.id}-${finding2.id}`,
            type: "wrong_assumption",
            targetAgent: finding1.scanner_source || "security",
            targetFindingId: finding1.id,
            challenge: `Inconsistent findings: ${reason}`,
            evidence: `${finding1.id} vs ${finding2.id}`,
            suggestedAction: "Reconcile these findings to determine correct severity",
            severity: "medium",
            confidence: 70,
        });
    }
    const agentsWithNoFindings = Object.entries(agentSummaries)
        .filter(([_, summary]) => summary.completed && summary.findingCount === 0)
        .map(([agent]) => agent);
    for (const agent of agentsWithNoFindings) {
        assessments.push({
            id: `chal-empty-${agent}`,
            type: "missed_check",
            targetAgent: agent,
            challenge: `Agent "${agent}" completed but found no issues`,
            evidence: "Zero findings submitted",
            suggestedAction: "Verify agent ran correctly and coverage was adequate",
            severity: "low",
            confidence: 60,
        });
    }
    const gapAnalysis = {
        untestedAttackVectors: uncoveredVectors,
        missingControls: uncoveredVectors.map((v) => `No ${v} testing`),
        blindSpots: agentsWithNoFindings.map((a) => `${a} agent found nothing`),
        recommendations: [
            ...uncoveredVectors.slice(0, 3).map((v) => `Add ${v} security checks`),
            ...agentsWithNoFindings.map((a) => `Verify ${a} agent coverage`),
        ],
        coverageScore: Math.round(((ATTACK_VECTORS.length - uncoveredVectors.length) / ATTACK_VECTORS.length) * 100),
    };
    return {
        assessments: assessments.filter((a) => a.confidence >= config.challengeThreshold),
        gapAnalysis,
    };
}
/**
 * Run challenger analysis with LLM
 */
export async function runChallengerLlm(findings, agentSummaries, config) {
    const client = new Anthropic();
    const findingsStr = findings
        .slice(0, 30)
        .map((f) => `- ${f.id}: [${f.severity}] ${f.category} at ${f.file}:${f.line} - ${f.description} (confidence: ${f.confidence}%)`)
        .join("\n");
    const agentStr = Object.entries(agentSummaries)
        .map(([agent, summary]) => `- ${agent}: ${summary.findingCount} findings`)
        .join("\n");
    const vectorsStr = ATTACK_VECTORS.join(", ");
    const prompt = buildChallengerPrompt(findingsStr, agentStr, vectorsStr);
    try {
        const response = await client.messages.create({
            model: config.llmModel || "claude-sonnet-4-20250514",
            max_tokens: 2048,
            system: CHALLENGER_SYSTEM_PROMPT,
            messages: [{ role: "user", content: prompt }],
        });
        const content = response.content[0];
        if (content.type !== "text") {
            throw new Error("Unexpected response type");
        }
        const jsonMatch = content.text.match(/\{[\s\S]*\}/);
        if (!jsonMatch) {
            throw new Error("No JSON in response");
        }
        const parsed = JSON.parse(jsonMatch[0]);
        const assessments = parsed.challenges.map((c, i) => ({
            id: `chal-llm-${Date.now().toString(36)}-${i}`,
            ...c,
        }));
        const tokensUsed = (response.usage?.input_tokens || 0) +
            (response.usage?.output_tokens || 0);
        return { assessments, tokensUsed };
    }
    catch (error) {
        logger.warn("antagonist.llm_challenger_failed", {
            error: error instanceof Error ? error.message : String(error),
        });
        return { assessments: [], tokensUsed: 0 };
    }
}
/**
 * Main challenger function - combines deterministic and LLM approaches
 */
export async function runChallenger(findings, agentSummaries, config) {
    const { assessments: deterministicAssessments, gapAnalysis } = runChallengerDeterministic(findings, agentSummaries, config);
    if (!config.useLlm || findings.length < 5) {
        return { assessments: deterministicAssessments, gapAnalysis, tokensUsed: 0 };
    }
    const { assessments: llmAssessments, tokensUsed } = await runChallengerLlm(findings, agentSummaries, config);
    const existingIds = new Set(deterministicAssessments.map((a) => a.targetFindingId).filter(Boolean));
    const uniqueLlmAssessments = llmAssessments.filter((a) => !a.targetFindingId || !existingIds.has(a.targetFindingId));
    return {
        assessments: [...deterministicAssessments, ...uniqueLlmAssessments],
        gapAnalysis,
        tokensUsed,
    };
}
//# sourceMappingURL=challenger.js.map