/**
 * Multi-Model Consensus Calculator
 *
 * Calculates inter-model agreement and merges findings from multiple LLM providers.
 *
 * @module multimodel/consensus
 */
/**
 * Default thresholds for consensus calculation
 */
const DEFAULT_THRESHOLDS = {
    unanimous: 100,
    majority: 50,
    goodAgreement: 0.6,
    matchThreshold: 70,
};
/**
 * Calculate similarity between two findings
 * Returns a score from 0-100
 */
export function calculateFindingSimilarity(a, b) {
    let score = 0;
    let weights = 0;
    // Same file is a strong indicator (weight: 30)
    if (a.file && b.file && a.file === b.file) {
        score += 30;
    }
    else if (a.file && b.file) {
        // Partial file match (same filename, different path)
        const aFile = a.file.split("/").pop();
        const bFile = b.file.split("/").pop();
        if (aFile === bFile) {
            score += 15;
        }
    }
    weights += 30;
    // Same line number (weight: 20)
    if (a.line && b.line) {
        if (a.line === b.line) {
            score += 20;
        }
        else if (Math.abs(a.line - b.line) <= 5) {
            // Within 5 lines
            score += 10;
        }
    }
    weights += 20;
    // Same category (weight: 25)
    if (a.category === b.category) {
        score += 25;
    }
    weights += 25;
    // Same severity (weight: 10)
    if (a.severity === b.severity) {
        score += 10;
    }
    weights += 10;
    // Description similarity (weight: 15)
    const descSim = calculateStringSimilarity(a.description, b.description);
    score += descSim * 15;
    weights += 15;
    // Normalize to 0-100
    return Math.round((score / weights) * 100);
}
/**
 * Calculate string similarity using Jaccard index on words
 */
export function calculateStringSimilarity(a, b) {
    const wordsA = new Set(a.toLowerCase().split(/\W+/).filter(Boolean));
    const wordsB = new Set(b.toLowerCase().split(/\W+/).filter(Boolean));
    if (wordsA.size === 0 && wordsB.size === 0)
        return 1;
    if (wordsA.size === 0 || wordsB.size === 0)
        return 0;
    const intersection = new Set([...wordsA].filter((x) => wordsB.has(x)));
    const union = new Set([...wordsA, ...wordsB]);
    return intersection.size / union.size;
}
/**
 * Match findings across multiple model results
 */
export function matchFindings(results, threshold = DEFAULT_THRESHOLDS.matchThreshold) {
    const matches = [];
    const usedFindings = new Map();
    // Initialize used findings tracker
    for (const result of results) {
        usedFindings.set(result.model, new Set());
    }
    // Use first successful model as reference
    const reference = results.find((r) => r.success && r.findings.length > 0);
    if (!reference)
        return matches;
    // Match each finding from reference model
    for (const refFinding of reference.findings) {
        const match = {
            referenceId: refFinding.id,
            matchedIds: { [reference.model]: refFinding.id },
            confidence: 0,
            matchType: "none",
            finding: refFinding,
            foundBy: [reference.model],
            missedBy: [],
        };
        // Try to match against other models
        for (const result of results) {
            if (result.model === reference.model || !result.success)
                continue;
            const used = usedFindings.get(result.model);
            let bestMatch = null;
            let bestScore = 0;
            for (const finding of result.findings) {
                if (used.has(finding.id))
                    continue;
                const similarity = calculateFindingSimilarity(refFinding, finding);
                if (similarity >= threshold && similarity > bestScore) {
                    bestMatch = finding;
                    bestScore = similarity;
                }
            }
            if (bestMatch) {
                match.matchedIds[result.model] = bestMatch.id;
                match.foundBy.push(result.model);
                used.add(bestMatch.id);
            }
            else {
                match.matchedIds[result.model] = null;
                match.missedBy.push(result.model);
            }
        }
        // Calculate confidence based on agreement
        const totalModels = results.filter((r) => r.success).length;
        const agreementRate = (match.foundBy.length / totalModels) * 100;
        match.confidence = Math.round(agreementRate);
        // Determine match type
        if (agreementRate === 100) {
            match.matchType = "exact";
        }
        else if (agreementRate >= 50) {
            match.matchType = "similar";
        }
        else if (agreementRate > 0) {
            match.matchType = "partial";
        }
        else {
            match.matchType = "none";
        }
        matches.push(match);
    }
    // Find findings unique to other models (not in reference)
    for (const result of results) {
        if (result.model === reference.model || !result.success)
            continue;
        const used = usedFindings.get(result.model);
        for (const finding of result.findings) {
            if (used.has(finding.id))
                continue;
            // This finding wasn't matched to any reference finding
            const uniqueMatch = {
                referenceId: finding.id,
                matchedIds: { [result.model]: finding.id },
                confidence: Math.round((1 / results.filter((r) => r.success).length) * 100),
                matchType: "partial",
                finding: finding,
                foundBy: [result.model],
                missedBy: results
                    .filter((r) => r.success && r.model !== result.model)
                    .map((r) => r.model),
            };
            // Initialize matchedIds for all models
            for (const r of results) {
                if (r.success && r.model !== result.model) {
                    uniqueMatch.matchedIds[r.model] = null;
                }
            }
            matches.push(uniqueMatch);
        }
    }
    return matches;
}
/**
 * Identify disagreements between models
 */
export function findDisagreements(matches, results) {
    const disagreements = [];
    for (const match of matches) {
        // Check for existence disagreement (some models found, some didn't)
        if (match.foundBy.length > 0 && match.missedBy.length > 0) {
            disagreements.push({
                type: "existence",
                findingId: match.referenceId,
                models: [...match.foundBy, ...match.missedBy],
                details: {
                    values: Object.fromEntries([
                        ...match.foundBy.map((m) => [m, "found"]),
                        ...match.missedBy.map((m) => [m, "not found"]),
                    ]),
                    resolution: match.foundBy.length > match.missedBy.length
                        ? "Include (majority found)"
                        : "Review (minority found)",
                },
                severity: match.finding.severity === "critical" || match.finding.severity === "high"
                    ? "high"
                    : match.finding.severity === "medium"
                        ? "medium"
                        : "low",
            });
        }
        // Check for severity disagreement
        if (match.foundBy.length >= 2) {
            const severities = new Map();
            for (const modelId of match.foundBy) {
                const result = results.find((r) => r.model === modelId);
                if (!result)
                    continue;
                const findingId = match.matchedIds[modelId];
                if (!findingId)
                    continue;
                const finding = result.findings.find((f) => f.id === findingId);
                if (finding) {
                    severities.set(modelId, finding.severity);
                }
            }
            // Check if severities differ
            const uniqueSeverities = new Set(severities.values());
            if (uniqueSeverities.size > 1) {
                disagreements.push({
                    type: "severity",
                    findingId: match.referenceId,
                    models: Array.from(severities.keys()),
                    details: {
                        values: Object.fromEntries(severities),
                        resolution: `Use highest severity: ${getHighestSeverity(Array.from(severities.values()))}`,
                    },
                    severity: "medium",
                });
            }
        }
    }
    return disagreements;
}
/**
 * Get highest severity from a list
 */
function getHighestSeverity(severities) {
    const order = ["critical", "high", "medium", "low", "info"];
    for (const sev of order) {
        if (severities.includes(sev))
            return sev;
    }
    return "info";
}
/**
 * Calculate Fleiss' kappa for inter-rater reliability
 * https://en.wikipedia.org/wiki/Fleiss%27_kappa
 */
export function calculateFleissKappa(matches, results) {
    const n = results.filter((r) => r.success).length; // Number of raters
    const N = matches.length; // Number of subjects
    if (n < 2 || N === 0)
        return 0;
    // For each finding, count how many models agreed on "found" vs "not found"
    const ratings = matches.map((match) => {
        const found = match.foundBy.length;
        const notFound = match.missedBy.length;
        return [found, notFound];
    });
    // Calculate P_i for each subject (proportion of agreement)
    const P_i = ratings.map((r) => {
        const [found, notFound] = r;
        const total = found + notFound;
        if (total <= 1)
            return 1; // Single rater, perfect agreement
        return (found * (found - 1) + notFound * (notFound - 1)) / (total * (total - 1));
    });
    // Calculate P-bar (mean of P_i)
    const P_bar = P_i.reduce((sum, p) => sum + p, 0) / N;
    // Calculate p_j for each category (proportion of all ratings in category j)
    const totalRatings = ratings.reduce((sum, r) => sum + r[0] + r[1], 0);
    const p_found = ratings.reduce((sum, r) => sum + r[0], 0) / totalRatings;
    const p_notFound = ratings.reduce((sum, r) => sum + r[1], 0) / totalRatings;
    // Calculate P_e (expected agreement by chance)
    const P_e = p_found * p_found + p_notFound * p_notFound;
    // Calculate kappa
    if (P_e === 1)
        return 1; // Perfect agreement expected
    const kappa = (P_bar - P_e) / (1 - P_e);
    return Math.max(-1, Math.min(1, kappa)); // Clamp to [-1, 1]
}
/**
 * Merge findings from multiple models into a single list
 */
export function mergeFindings(matches, results, options) {
    const merged = [];
    const totalModels = results.filter((r) => r.success).length;
    const majorityThreshold = Math.ceil(totalModels / 2);
    for (const match of matches) {
        // Skip if requiring majority and not met
        if (options?.requireMajority && match.foundBy.length < majorityThreshold) {
            continue;
        }
        // Create merged finding
        const mergedFinding = {
            ...match.finding,
            // Add metadata about multi-model consensus
            confidence: match.confidence,
        };
        // If multiple models found it with different severities, use highest
        if (match.foundBy.length >= 2) {
            const severities = [];
            for (const modelId of match.foundBy) {
                const result = results.find((r) => r.model === modelId);
                if (!result)
                    continue;
                const findingId = match.matchedIds[modelId];
                if (!findingId)
                    continue;
                const finding = result.findings.find((f) => f.id === findingId);
                if (finding) {
                    severities.push(finding.severity);
                }
            }
            if (severities.length > 0) {
                mergedFinding.severity = getHighestSeverity(severities);
            }
        }
        merged.push(mergedFinding);
    }
    return merged;
}
/**
 * Calculate multi-model consensus
 */
export function calculateMultiModelConsensus(certificationId, agent, results, options) {
    const threshold = options?.matchThreshold ?? DEFAULT_THRESHOLDS.matchThreshold;
    const successfulResults = results.filter((r) => r.success);
    const models = successfulResults.map((r) => r.model);
    // Match findings across models
    const matches = matchFindings(results, threshold);
    // Find disagreements
    const disagreements = findDisagreements(matches, results);
    // Calculate Fleiss' kappa
    const fleissKappa = calculateFleissKappa(matches, results);
    // Calculate metrics
    const totalModels = successfulResults.length;
    const unanimousFindings = matches.filter((m) => m.foundBy.length === totalModels).length;
    const majorityFindings = matches.filter((m) => m.foundBy.length >= Math.ceil(totalModels / 2)).length;
    const disputedFindings = matches.filter((m) => m.foundBy.length > 0 && m.foundBy.length < Math.ceil(totalModels / 2)).length;
    const uniqueFindings = matches.filter((m) => m.foundBy.length === 1).length;
    // Agreement rate (percentage of findings with majority agreement)
    const agreementRate = matches.length > 0
        ? (majorityFindings / matches.length) * 100
        : 100;
    // By severity breakdown
    const severities = ["critical", "high", "medium", "low", "info"];
    const bySeverity = Object.fromEntries(severities.map((sev) => {
        const sevMatches = matches.filter((m) => m.finding.severity === sev);
        return [
            sev,
            {
                unanimous: sevMatches.filter((m) => m.foundBy.length === totalModels).length,
                majority: sevMatches.filter((m) => m.foundBy.length >= Math.ceil(totalModels / 2) && m.foundBy.length < totalModels).length,
                disputed: sevMatches.filter((m) => m.foundBy.length > 0 && m.foundBy.length < Math.ceil(totalModels / 2)).length,
            },
        ];
    }));
    // Merge findings
    const mergedFindings = mergeFindings(matches, results, options);
    return {
        certificationId,
        agent,
        models,
        results,
        matches,
        disagreements,
        metrics: {
            agreementRate: Math.round(agreementRate),
            unanimousFindings,
            majorityFindings,
            disputedFindings,
            uniqueFindings,
            fleissKappa: Math.round(fleissKappa * 100) / 100,
            bySeverity,
        },
        mergedFindings,
        calculatedAt: new Date().toISOString(),
    };
}
/**
 * Generate a human-readable summary of consensus
 */
export function generateConsensusSummary(consensus) {
    const lines = [
        "# Multi-Model Consensus Report",
        "",
        "## Overview",
        "",
        `- **Certification**: ${consensus.certificationId}`,
        `- **Agent**: ${consensus.agent}`,
        `- **Models**: ${consensus.models.join(", ")}`,
        `- **Calculated**: ${consensus.calculatedAt}`,
        "",
        "## Metrics",
        "",
        `- **Agreement Rate**: ${consensus.metrics.agreementRate}%`,
        `- **Fleiss' Kappa**: ${consensus.metrics.fleissKappa} (${interpretKappa(consensus.metrics.fleissKappa)})`,
        `- **Unanimous Findings**: ${consensus.metrics.unanimousFindings}`,
        `- **Majority Findings**: ${consensus.metrics.majorityFindings}`,
        `- **Disputed Findings**: ${consensus.metrics.disputedFindings}`,
        `- **Unique Findings**: ${consensus.metrics.uniqueFindings}`,
        "",
    ];
    // By severity
    lines.push("## By Severity", "");
    lines.push("| Severity | Unanimous | Majority | Disputed |");
    lines.push("|----------|-----------|----------|----------|");
    for (const sev of ["critical", "high", "medium", "low", "info"]) {
        const data = consensus.metrics.bySeverity[sev];
        lines.push(`| ${sev} | ${data.unanimous} | ${data.majority} | ${data.disputed} |`);
    }
    lines.push("");
    // Disagreements
    if (consensus.disagreements.length > 0) {
        lines.push("## Disagreements", "");
        for (const d of consensus.disagreements) {
            lines.push(`### ${d.type.toUpperCase()}: Finding ${d.findingId}`);
            lines.push("");
            lines.push(`- **Severity**: ${d.severity}`);
            lines.push(`- **Models**: ${d.models.join(", ")}`);
            lines.push(`- **Values**: ${JSON.stringify(d.details.values)}`);
            if (d.details.resolution) {
                lines.push(`- **Resolution**: ${d.details.resolution}`);
            }
            lines.push("");
        }
    }
    return lines.join("\n");
}
/**
 * Interpret Fleiss' kappa value
 */
function interpretKappa(kappa) {
    if (kappa < 0)
        return "Poor (less than chance)";
    if (kappa < 0.2)
        return "Slight";
    if (kappa < 0.4)
        return "Fair";
    if (kappa < 0.6)
        return "Moderate";
    if (kappa < 0.8)
        return "Substantial";
    return "Almost Perfect";
}
//# sourceMappingURL=consensus.js.map