// Precedent Control — Module 25 (Gap 6)
// Prevents precedent drift by curating normative precedents.
// New evaluations must align with normative precedents or explicitly distinguish.
// F-001–F-005: deterministic enforcement (FloorValidatorV1, never LLM)
// F-006, F-007: advisory layer with precedent library and structured verdicts.
import { sign, verify } from '../crypto/keys.js';
import { canonicalize } from './canonical.js';
// ══════════════════════════════════════
// CREATE LIBRARY
// ══════════════════════════════════════
export function createPrecedentLibrary() {
    const now = new Date().toISOString();
    return { precedents: [], categories: [], createdAt: now, lastUpdatedAt: now };
}
// ══════════════════════════════════════
// MARK AS NORMATIVE
// ══════════════════════════════════════
export function markAsNormative(opts) {
    const { precedent, approverPrivateKey, approverPublicKey, category } = opts;
    const now = new Date().toISOString();
    const signable = canonicalize({
        precedentId: precedent.precedentId,
        subject: precedent.subject,
        decision: precedent.decision,
        category,
        approvedBy: approverPublicKey,
        approvedAt: now,
    });
    const approvalSignature = sign(signable, approverPrivateKey);
    return {
        ...precedent,
        status: 'normative',
        approvedBy: approverPublicKey,
        approvedAt: now,
        approvalSignature,
        category,
    };
}
// ══════════════════════════════════════
// VERIFY NORMATIVE PRECEDENT
// ══════════════════════════════════════
export function verifyNormativePrecedent(precedent) {
    try {
        const signable = canonicalize({
            precedentId: precedent.precedentId,
            subject: precedent.subject,
            decision: precedent.decision,
            category: precedent.category,
            approvedBy: precedent.approvedBy,
            approvedAt: precedent.approvedAt,
        });
        return verify(signable, precedent.approvalSignature, precedent.approvedBy);
    }
    catch {
        return false;
    }
}
// ══════════════════════════════════════
// ADD TO LIBRARY
// ══════════════════════════════════════
export function addToLibrary(library, precedent) {
    if (!library.categories.includes(precedent.category)) {
        library.categories = [...library.categories, precedent.category];
    }
    return {
        ...library,
        precedents: [...library.precedents, precedent],
        categories: library.categories,
        lastUpdatedAt: new Date().toISOString(),
    };
}
// ══════════════════════════════════════
// CHECK ALIGNMENT
// ══════════════════════════════════════
// Simple word-overlap similarity for subject+context matching.
// Production would use embeddings; this is the structural contract.
function tokenize(text) {
    return new Set(text.toLowerCase().split(/\W+/).filter(w => w.length > 2));
}
function jaccardSimilarity(a, b) {
    if (a.size === 0 && b.size === 0)
        return 0;
    const intersection = new Set([...a].filter(x => b.has(x)));
    const union = new Set([...a, ...b]);
    return intersection.size / union.size;
}
export function checkAlignment(library, decision, category) {
    const candidates = category
        ? library.precedents.filter(p => p.status === 'normative' && p.category === category)
        : library.precedents.filter(p => p.status === 'normative');
    if (candidates.length === 0) {
        return { aligned: true, closestPrecedent: null, similarityScore: 0, requiresDistinguishing: false, reason: 'No normative precedents in library' };
    }
    const decisionTokens = tokenize(`${decision.subject} ${decision.context}`);
    let bestMatch = null;
    let bestScore = 0;
    for (const p of candidates) {
        const pTokens = tokenize(`${p.subject} ${p.context}`);
        const score = jaccardSimilarity(decisionTokens, pTokens);
        if (score > bestScore) {
            bestScore = score;
            bestMatch = p;
        }
    }
    // Threshold: >0.3 similarity means similar enough to warrant comparison
    const SIMILARITY_THRESHOLD = 0.3;
    if (bestScore < SIMILARITY_THRESHOLD || !bestMatch) {
        return { aligned: true, closestPrecedent: bestMatch, similarityScore: bestScore, requiresDistinguishing: false, reason: 'No sufficiently similar precedent found' };
    }
    // Same outcome = aligned; different outcome = requires distinguishing
    const outcomeMatch = decision.outcome.toLowerCase().includes(bestMatch.decision.toLowerCase())
        || bestMatch.decision.toLowerCase().includes(decision.outcome.toLowerCase());
    return {
        aligned: outcomeMatch,
        closestPrecedent: bestMatch,
        similarityScore: bestScore,
        requiresDistinguishing: !outcomeMatch,
        reason: outcomeMatch
            ? `Aligned with precedent ${bestMatch.precedentId}: similar situation, consistent outcome`
            : `Diverges from precedent ${bestMatch.precedentId}: similar situation (${(bestScore * 100).toFixed(0)}% match), different outcome. Must distinguish.`,
    };
}
// ══════════════════════════════════════
// SUPERSEDE PRECEDENT
// ══════════════════════════════════════
export function supersedePrecedent(opts) {
    const { library, oldPrecedentId, newPrecedent, distinguishingNote } = opts;
    const updated = library.precedents.map(p => {
        if (p.precedentId === oldPrecedentId && p.status === 'normative') {
            return { ...p, status: 'superseded', supersededBy: newPrecedent.precedentId, distinguishingNote };
        }
        return p;
    });
    return {
        ...library,
        precedents: [...updated, newPrecedent],
        lastUpdatedAt: new Date().toISOString(),
    };
}
// ══════════════════════════════════════
// ANALYZE DRIFT
// ══════════════════════════════════════
export function analyzeDrift(library, recentDecisions, category) {
    let alignedCount = 0;
    let divergentCount = 0;
    const divergentIds = [];
    for (const decision of recentDecisions) {
        const alignment = checkAlignment(library, decision, category);
        if (alignment.closestPrecedent === null)
            continue; // no precedent to compare against
        if (alignment.aligned) {
            alignedCount++;
        }
        else {
            divergentCount++;
            divergentIds.push(decision.id);
        }
    }
    const total = alignedCount + divergentCount;
    const driftScore = total === 0 ? 0 : divergentCount / total;
    return {
        driftDetected: driftScore > 0.2, // >20% divergence = drift detected
        driftScore,
        evaluationCount: recentDecisions.length,
        alignedCount,
        divergentCount,
        divergentIds,
    };
}
//# sourceMappingURL=precedent.js.map