"use strict";
/**
 * SourceOutputChainVerifier -- RAG source-to-output chain verification.
 *
 * This is the third and strictest gate in the three-layer RAG governance
 * chain introduced by the P3-T1 RAG cluster:
 *
 *   PR #18 RagSourceAllowlist (pre-retrieval):
 *     Which sources may the agent query at all?
 *
 *   PR #20 RagCitationEnforcer (post-output, pre-finalisation):
 *     Are every citation's (sourceId, chunkRef) pairs provably in the
 *     retrieval pool for this cycle?
 *
 *   THIS FILE -- SourceOutputChainVerifier (post-citation-enforcement):
 *     Does the agent's output text show evidence it actually USED the
 *     cited chunk content? Jaccard 5-gram character-shingle similarity
 *     between the output text and each cited chunk's text must meet a
 *     minimum threshold of 0.10 (10% overlap). Additionally, any
 *     explicitly quoted span must be a verbatim substring of the chunk.
 *
 * Problem being solved (OWASP LLM A05 -- Cascading Hallucination, deeper):
 *   An agent can pass the first two gates and still fabricate:
 *     - Allowlist: source is approved.
 *     - Citation enforcer: citation is in the retrieval pool.
 *     - But the output TEXT could be entirely disconnected from the chunk.
 *   This file closes that gap with a similarity floor and exact-span check.
 *
 * Similarity threshold rationale:
 *   0.10 (10% 5-gram character overlap) is a deliberately low floor.
 *   Empirical RAG benchmarks show:
 *     - Paraphrased summaries of a chunk typically score 0.30 -- 0.70.
 *     - Outright fabrication that merely appends a citation typically
 *       scores below 0.05 (near-zero n-gram overlap with chunk content).
 *     - The 0.10 floor is the "no overlap whatsoever" sentinel -- it
 *       catches agents that cite a chunk they provably did not use.
 *
 *   Known limitation (floor, not ceiling):
 *     The threshold is a FLOOR. An adversarial agent could pad output with
 *     verbatim chunk text to clear 0.10 while still hallucinating the
 *     substantive claim (see PT-RAG-CHAIN-1). This is a known v0 false-
 *     negative. Higher-fidelity factual grounding checks (entailment
 *     models, semantic similarity) are planned for v1. The floor is
 *     sufficient to catch outright fabrication; it does not enforce
 *     paraphrase fidelity. This limitation is documented here and in
 *     docs/runtime/rag-source-output-chain.md.
 *
 * Quoted-span matching:
 *   When a citation includes a quotedSpan, the span is verified as a
 *   verbatim substring of the chunk text using:
 *     - Unicode NFKC normalization of both sides (PT-RAG-CHAIN-2:
 *       Cyrillic/homoglyph lookalike bypass).
 *     - Lowercase comparison.
 *     - Whitespace-run collapse (multiple spaces/tabs/newlines -> one space).
 *   String.prototype.includes() is used, NOT regex. Operator-controlled
 *   span text could be hostile if regex-interpreted.
 *
 * Strict-mode packs:
 *   hipaa, gdpr, pci-dss, hmda, dora -- same five as PR #18 and PR #20.
 *   For these packs, pool entries MUST supply chunkText. Missing chunkText
 *   on a strict pack throws ChunkTextRequiredForStrictPackError (no graceful
 *   degradation). For lenient packs, missing chunkText emits a WARNING event
 *   and the gate passes.
 *
 * Audit events emitted:
 *   SOURCE_OUTPUT_DISCONNECTED          severity ERROR   -- similarity < 0.10
 *   QUOTED_SPAN_MISMATCH                severity ERROR   -- span not in chunk
 *   SOURCE_OUTPUT_VERIFICATION_DEFERRED severity WARNING -- no chunkText, lenient
 *   SOURCE_OUTPUT_VERIFIED              severity NOTICE  -- all citations ok
 *
 * Output text is NEVER written to audit payloads. SHA-256 hash only.
 * Pack-binding is mandatory on every call (SourceOutputUnboundError if empty).
 *
 * Pen-test gates:
 *   PT-RAG-CHAIN-1: similarity-stuffing -- padding with chunk verbatim text
 *     clears 0.10 despite hallucinated claim. Floor behaviour is deterministic.
 *   PT-RAG-CHAIN-2: Unicode-normalization bypass -- Cyrillic lookalikes in
 *     quotedSpan evaded by NFKC normalization on both sides.
 *   PT-RAG-CHAIN-3: chunk-text-injection -- adversarial chunk text containing
 *     prompt injection strings; verifier MUST NOT execute or interpret chunk
 *     content, only compute similarity.
 *
 * BUILD_PLAN reference: line 248 "Source-to-output chain verification (did the
 * agent actually ground in the retrieved source?)".
 *
 * @connexum/ai-governance
 */
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.SourceOutputChainVerifier = exports.ChunkTextRequiredForStrictPackError = exports.QuotedSpanNotInChunkError = exports.SourceOutputDisconnectedError = exports.SourceOutputUnboundError = exports.SOURCE_OUTPUT_JACCARD_THRESHOLD = exports.SOURCE_OUTPUT_STRICT_MODE_PACKS = void 0;
const crypto = __importStar(require("node:crypto"));
// ---------------------------------------------------------------------------
// Strict-mode pack list (mirrors PR #18 and PR #20)
// ---------------------------------------------------------------------------
/**
 * Packs that require chunkText to be present for every pool entry.
 * For these packs, missing chunkText is a hard error, not a degradation.
 *
 * hipaa   -- HIPAA §164.312(a)(1): PHI claim provenance must be traceable.
 * gdpr    -- GDPR Art. 5(1)(a)/(d): claims about data subjects must be
 *            grounded in verified record text.
 * pci-dss -- PCI DSS Req 12.3: risk claims must cite specific evidence.
 * hmda    -- HMDA Reg C §1003: GMI data usage must derive from the
 *            verified collection text.
 * dora    -- DORA Art. 9 ICT risk: incident claims must ground in retrieved
 *            ICT register or incident record text.
 */
exports.SOURCE_OUTPUT_STRICT_MODE_PACKS = new Set([
    'hipaa',
    'gdpr',
    'pci-dss',
    'hmda',
    'dora',
]);
/**
 * Jaccard similarity threshold for source-to-output verification.
 * An output text and a cited chunk text must share at least this fraction
 * of their 5-gram character shingle sets to be considered grounded.
 *
 * 0.10 is a floor (outright-fabrication sentinel), not a ceiling.
 * See file header for rationale.
 */
exports.SOURCE_OUTPUT_JACCARD_THRESHOLD = 0.10;
// ---------------------------------------------------------------------------
// Error classes
// ---------------------------------------------------------------------------
/**
 * Thrown when pack-binding is empty on any verifyOutput() call.
 * Per feedback_pack_binding_primacy.md: every record must carry a binding.
 */
class SourceOutputUnboundError extends Error {
    context;
    constructor(context) {
        super(`SourceOutputChainVerifier: packBinding must not be empty. Context: ${context}. ` +
            `Every source-to-output verification call must carry at least one pack binding ` +
            `for retention policy derivation and audit traceability.`);
        this.name = 'SourceOutputUnboundError';
        this.context = context;
    }
}
exports.SourceOutputUnboundError = SourceOutputUnboundError;
/**
 * Thrown when Jaccard 5-gram similarity between output text and cited chunk
 * text is below SOURCE_OUTPUT_JACCARD_THRESHOLD (0.10).
 * The output text is provably disconnected from the cited chunk.
 * OWASP LLM A05 (Cascading Hallucination) cite-then-fabricate prevention.
 */
class SourceOutputDisconnectedError extends Error {
    orgId;
    agentId;
    cycleId;
    citationIndex;
    sourceId;
    chunkRef;
    similarity;
    threshold;
    constructor(opts) {
        super(`RAG source-to-output chain broken: agent '${opts.agentId}' (org '${opts.orgId}') ` +
            `cycle '${opts.cycleId}' citation[${opts.citationIndex}] sourceId '${opts.sourceId}' ` +
            `chunkRef '${opts.chunkRef}': ` +
            `Jaccard 5-gram similarity between output text and chunk text is ${opts.similarity.toFixed(4)} ` +
            `(threshold: ${opts.threshold}). ` +
            `The output text is disconnected from the cited chunk. ` +
            `This is the OWASP LLM A05 cite-then-fabricate pattern. ` +
            `The agent cited this chunk but produced text with no measurable overlap with it.`);
        this.name = 'SourceOutputDisconnectedError';
        this.orgId = opts.orgId;
        this.agentId = opts.agentId;
        this.cycleId = opts.cycleId;
        this.citationIndex = opts.citationIndex;
        this.sourceId = opts.sourceId;
        this.chunkRef = opts.chunkRef;
        this.similarity = opts.similarity;
        this.threshold = opts.threshold;
    }
}
exports.SourceOutputDisconnectedError = SourceOutputDisconnectedError;
/**
 * Thrown when a citation provides a quotedSpan but the span is not found as
 * a verbatim (NFKC + lowercase + whitespace-collapsed) substring of the chunk.
 * The agent fabricated or misquoted the excerpt.
 */
class QuotedSpanNotInChunkError extends Error {
    orgId;
    agentId;
    cycleId;
    citationIndex;
    sourceId;
    chunkRef;
    constructor(opts) {
        super(`RAG quoted-span mismatch: agent '${opts.agentId}' (org '${opts.orgId}') ` +
            `cycle '${opts.cycleId}' citation[${opts.citationIndex}] sourceId '${opts.sourceId}' ` +
            `chunkRef '${opts.chunkRef}': ` +
            `The quotedSpan is not a verbatim substring of the chunk text ` +
            `(after NFKC normalization, lowercase, and whitespace collapse). ` +
            `The agent fabricated or misquoted the excerpt.`);
        this.name = 'QuotedSpanNotInChunkError';
        this.orgId = opts.orgId;
        this.agentId = opts.agentId;
        this.cycleId = opts.cycleId;
        this.citationIndex = opts.citationIndex;
        this.sourceId = opts.sourceId;
        this.chunkRef = opts.chunkRef;
    }
}
exports.QuotedSpanNotInChunkError = QuotedSpanNotInChunkError;
/**
 * Thrown when a strict-mode pack (hipaa, gdpr, pci-dss, hmda, dora) is
 * active and a pool entry's chunkTexts map does not include the cited
 * chunkRef's text. For strict packs, chunkText is mandatory -- graceful
 * degradation is not permitted. Retrieval adapters must populate chunkTexts
 * for all chunks when any strict pack is bound.
 *
 * Migration path for retrieval adapters:
 *   recordRetrieval() on RagCitationEnforcer (PR #20) accepts an optional
 *   chunkTexts map. Adapters targeting strict-pack tenants must supply this
 *   map. See docs/runtime/rag-source-output-chain.md for the migration guide.
 */
class ChunkTextRequiredForStrictPackError extends Error {
    orgId;
    agentId;
    cycleId;
    citationIndex;
    sourceId;
    chunkRef;
    strictPacks;
    constructor(opts) {
        super(`RAG strict-pack chunkText required: agent '${opts.agentId}' (org '${opts.orgId}') ` +
            `cycle '${opts.cycleId}' citation[${opts.citationIndex}] sourceId '${opts.sourceId}' ` +
            `chunkRef '${opts.chunkRef}' has no chunkText in the retrieval pool, ` +
            `but the agent is bound to strict-mode pack(s): [${opts.strictPacks.join(', ')}]. ` +
            `Strict packs require chunkText for every cited chunk to enable source-to-output ` +
            `verification. Retrieval adapters must populate chunkTexts when strict packs are active. ` +
            `See docs/runtime/rag-source-output-chain.md for the migration guide.`);
        this.name = 'ChunkTextRequiredForStrictPackError';
        this.orgId = opts.orgId;
        this.agentId = opts.agentId;
        this.cycleId = opts.cycleId;
        this.citationIndex = opts.citationIndex;
        this.sourceId = opts.sourceId;
        this.chunkRef = opts.chunkRef;
        this.strictPacks = opts.strictPacks;
    }
}
exports.ChunkTextRequiredForStrictPackError = ChunkTextRequiredForStrictPackError;
// ---------------------------------------------------------------------------
// Algorithm: Jaccard 5-gram character shingles
// ---------------------------------------------------------------------------
/**
 * Normalize text for similarity comparison:
 *   1. NFKC normalization (defeats Unicode lookalike / homoglyph substitution,
 *      see PT-RAG-CHAIN-2).
 *   2. Lowercase.
 *   3. Strip non-alphanumeric characters (keep only [a-z0-9]).
 *   4. Collapse whitespace (already stripped, but extra safety).
 *
 * The verifier NEVER executes or interprets the input -- this function only
 * applies character-level transforms (PT-RAG-CHAIN-3: injection safety).
 */
function normalizeForSimilarity(text) {
    // NFKC first: resolves compatibility equivalences and decomposes lookalikes.
    // e.g. Cyrillic 'а' (\u0430) stays distinct from Latin 'a' (\u0061) at code
    // point level BUT NFKC does normalize many compatibility characters.
    // For full homoglyph resistance, NFKC + lowercase is the standard approach.
    const nfkc = text.normalize('NFKC');
    const lower = nfkc.toLowerCase();
    // Strip everything except alphanumeric and whitespace, then collapse runs.
    // Using a simple loop avoids regex-injection risk from operator-supplied text.
    let result = '';
    for (let i = 0; i < lower.length; i++) {
        const c = lower[i];
        const code = lower.charCodeAt(i);
        // Keep: a-z (97-122), 0-9 (48-57), space (32)
        if ((code >= 97 && code <= 122) || (code >= 48 && code <= 57)) {
            result += c;
        }
        else {
            // Replace everything else with a space (non-alphanumeric separators)
            result += ' ';
        }
    }
    // Collapse consecutive spaces to one
    let collapsed = '';
    let prevSpace = false;
    for (const c of result) {
        if (c === ' ') {
            if (!prevSpace)
                collapsed += ' ';
            prevSpace = true;
        }
        else {
            collapsed += c;
            prevSpace = false;
        }
    }
    return collapsed.trim();
}
/**
 * Produce the set of character 5-grams from a normalized string.
 * A 5-gram is a substring of exactly 5 characters.
 * If the string is shorter than 5 characters, returns an empty set.
 *
 * Character shingles (not word n-grams) are used because:
 *   - They are language-independent.
 *   - They are robust to punctuation variation.
 *   - They match the approach in the project's anomaly-detector (PR #17).
 */
function fiveGrams(normalized) {
    const shingles = new Set();
    const n = 5;
    for (let i = 0; i <= normalized.length - n; i++) {
        shingles.add(normalized.slice(i, i + n));
    }
    return shingles;
}
/**
 * Jaccard similarity between two 5-gram sets:
 *   J(A, B) = |A ∩ B| / |A ∪ B|
 *
 * Returns 0.0 when both sets are empty (degenerate case: two very short
 * strings each under 5 chars produce empty sets -- treated as no overlap).
 */
function jaccardSimilarity(a, b) {
    if (a.size === 0 && b.size === 0)
        return 0.0;
    let intersection = 0;
    // Iterate over the smaller set for efficiency
    const [smaller, larger] = a.size <= b.size ? [a, b] : [b, a];
    for (const shingle of smaller) {
        if (larger.has(shingle))
            intersection++;
    }
    const union = a.size + b.size - intersection;
    return union === 0 ? 0.0 : intersection / union;
}
/**
 * Compute Jaccard 5-gram similarity between two raw text strings.
 * Applies normalizeForSimilarity() to both sides before shingling.
 */
function computeSimilarity(outputText, chunkText) {
    const normOutput = normalizeForSimilarity(outputText);
    const normChunk = normalizeForSimilarity(chunkText);
    const shiOutput = fiveGrams(normOutput);
    const shiChunk = fiveGrams(normChunk);
    return jaccardSimilarity(shiOutput, shiChunk);
}
// ---------------------------------------------------------------------------
// Quoted-span verification
// ---------------------------------------------------------------------------
/**
 * Verify that quotedSpan is a verbatim substring of chunkText.
 *
 * Comparison uses:
 *   1. NFKC normalization (PT-RAG-CHAIN-2 homoglyph resistance).
 *   2. Lowercase.
 *   3. Whitespace-run collapse (multiple whitespace chars -> single space).
 *
 * Uses String.prototype.includes(), NOT regex (PT-RAG-CHAIN-3 injection
 * safety: operator-controlled span text must never be regex-interpreted).
 *
 * Returns true if the normalized span is found in the normalized chunk.
 */
function verifyQuotedSpan(quotedSpan, chunkText) {
    const normalizeSpanText = (s) => {
        const nfkc = s.normalize('NFKC').toLowerCase();
        // Collapse whitespace runs to single space, trim
        let collapsed = '';
        let prevSpace = false;
        for (const c of nfkc) {
            if (c === '\t' || c === '\n' || c === '\r' || c === ' ') {
                if (!prevSpace)
                    collapsed += ' ';
                prevSpace = true;
            }
            else {
                collapsed += c;
                prevSpace = false;
            }
        }
        return collapsed.trim();
    };
    const normalizedSpan = normalizeSpanText(quotedSpan);
    const normalizedChunk = normalizeSpanText(chunkText);
    return normalizedChunk.includes(normalizedSpan);
}
// ---------------------------------------------------------------------------
// SourceOutputChainVerifier
// ---------------------------------------------------------------------------
/**
 * Post-citation-enforcement source-to-output chain verifier.
 *
 * Call AFTER RagCitationEnforcer.validateOutput() passes and BEFORE the
 * agent output is finalised. The pipeline order is:
 *
 *   [PR #18] RagSourceAllowlist.guardPreRetrieval()  -- pre-retrieval
 *   --> retrieve chunks -->
 *   [PR #20] RagCitationEnforcer.validateOutput()   -- post-output
 *   --> [THIS] SourceOutputChainVerifier.verifyOutput() -- post-citation
 *   --> output finalised
 *
 * Usage:
 *
 *   const verifier = new SourceOutputChainVerifier(auditSink);
 *
 *   // retrievalPool comes from the same pool used by RagCitationEnforcer.
 *   // It must include chunkTexts for strict-pack tenants.
 *   const result = verifier.verifyOutput(
 *     'org-a', 'agent-1', 'cycle-42',
 *     {
 *       text: 'The patient record shows elevated glucose...',
 *       citations: [{
 *         sourceId: 'vs_acme_pubmed',
 *         sourceKind: 'vector-store',
 *         chunkRef: 'c001',
 *         quotedSpan: 'elevated glucose',
 *       }],
 *     },
 *     retrievalPool,   // RetrievalPoolEntry[] for this cycle
 *     ['hipaa'],
 *   );
 */
class SourceOutputChainVerifier {
    /** SHA-256 of the last ERROR audit event (chain linkage). */
    prevSha = '0'.repeat(64);
    auditSink;
    constructor(auditSink) {
        this.auditSink = auditSink ?? (() => undefined);
    }
    // ---------------------------------------------------------------------------
    // verifyOutput()
    // ---------------------------------------------------------------------------
    /**
     * Verify that each cited chunk's text shows measurable overlap with the
     * output text (Jaccard 5-gram >= 0.10) and that any quotedSpan is a
     * verbatim substring.
     *
     * Precondition: RagCitationEnforcer.validateOutput() has already passed.
     *   This means every (sourceId, chunkRef) in citations is guaranteed to
     *   be present in retrievalPool. This verifier does NOT re-check pool
     *   membership (not its responsibility). If a pool entry is unexpectedly
     *   absent, CitationOutsidePoolError is NOT re-thrown -- this verifier
     *   treats absent pool entry as a programming error and skips it safely.
     *
     * Per-citation behaviour:
     *   1. Find pool entry for (sourceId, chunkRef).
     *   2. If chunkText is available:
     *      a. Compute Jaccard similarity.
     *      b. If similarity < threshold: throw SourceOutputDisconnectedError.
     *      c. If quotedSpan provided: verify verbatim substring.
     *         Mismatch: throw QuotedSpanNotInChunkError.
     *   3. If chunkText is NOT available:
     *      a. Strict pack bound: throw ChunkTextRequiredForStrictPackError.
     *      b. Lenient pack: emit DEFERRED warning, pass.
     *
     * Aggregate: any 'disconnected' or 'quoted-span-mismatch' verdict throws.
     * Any 'deferred' verdict in strict mode throws (handled per-citation above).
     *
     * @param orgId          Operator/tenant identifier.
     * @param agentId        Agent identifier.
     * @param cycleId        Cycle identifier.
     * @param output         Output text and citations.
     * @param retrievalPool  Pool entries for this cycle (from RagCitationEnforcer).
     * @param packBinding    Pack IDs governing this output. Must be non-empty.
     * @returns VerificationResult with per-citation results.
     * @throws SourceOutputUnboundError if packBinding is empty.
     * @throws SourceOutputDisconnectedError if similarity < threshold.
     * @throws QuotedSpanNotInChunkError if quotedSpan not in chunk.
     * @throws ChunkTextRequiredForStrictPackError if strict pack + missing chunkText.
     */
    verifyOutput(orgId, agentId, cycleId, output, retrievalPool, packBinding) {
        if (!packBinding || packBinding.length === 0) {
            throw new SourceOutputUnboundError(`verifyOutput orgId=${orgId} agentId=${agentId} cycleId=${cycleId}`);
        }
        const outputHash = this.sha256(output.text);
        const strictPacks = packBinding.filter((p) => exports.SOURCE_OUTPUT_STRICT_MODE_PACKS.has(p));
        const isStrict = strictPacks.length > 0;
        const citationResults = [];
        for (let i = 0; i < output.citations.length; i++) {
            const citation = output.citations[i];
            const poolEntry = this.findPoolEntry(retrievalPool, citation.sourceId, citation.chunkRef);
            // Find chunk text from the pool entry
            const chunkText = poolEntry?.chunkTexts?.[citation.chunkRef];
            if (chunkText === undefined) {
                // No chunk text available
                if (isStrict) {
                    // Strict pack: hard error
                    const event = {
                        type: 'SOURCE_OUTPUT_DISCONNECTED',
                        severity: 'ERROR',
                        orgId,
                        agentId,
                        cycleId,
                        citationIndex: i,
                        outputHash,
                        sourceId: citation.sourceId,
                        chunkRef: citation.chunkRef,
                        similarity: -1,
                        threshold: exports.SOURCE_OUTPUT_JACCARD_THRESHOLD,
                        prevSha: this.prevSha,
                        timestamp: new Date().toISOString(),
                    };
                    this.emitError(event);
                    throw new ChunkTextRequiredForStrictPackError({
                        orgId,
                        agentId,
                        cycleId,
                        citationIndex: i,
                        sourceId: citation.sourceId,
                        chunkRef: citation.chunkRef,
                        strictPacks,
                    });
                }
                // Lenient: deferred warning, pass
                const deferredEvent = {
                    type: 'SOURCE_OUTPUT_VERIFICATION_DEFERRED',
                    severity: 'WARNING',
                    orgId,
                    agentId,
                    cycleId,
                    citationIndex: i,
                    sourceId: citation.sourceId,
                    chunkRef: citation.chunkRef,
                    timestamp: new Date().toISOString(),
                };
                this.auditSink(deferredEvent);
                citationResults.push({ citation, similarity: -1, verdict: 'deferred' });
                continue;
            }
            // Chunk text is available: compute Jaccard similarity.
            // NOTE: the verifier does NOT execute or interpret chunk text.
            // computeSimilarity() only applies character-level normalization transforms
            // and set intersection math. PT-RAG-CHAIN-3 safety is maintained.
            const similarity = computeSimilarity(output.text, chunkText);
            if (similarity < exports.SOURCE_OUTPUT_JACCARD_THRESHOLD) {
                const event = {
                    type: 'SOURCE_OUTPUT_DISCONNECTED',
                    severity: 'ERROR',
                    orgId,
                    agentId,
                    cycleId,
                    citationIndex: i,
                    outputHash,
                    sourceId: citation.sourceId,
                    chunkRef: citation.chunkRef,
                    similarity,
                    threshold: exports.SOURCE_OUTPUT_JACCARD_THRESHOLD,
                    prevSha: this.prevSha,
                    timestamp: new Date().toISOString(),
                };
                this.emitError(event);
                throw new SourceOutputDisconnectedError({
                    orgId,
                    agentId,
                    cycleId,
                    citationIndex: i,
                    sourceId: citation.sourceId,
                    chunkRef: citation.chunkRef,
                    similarity,
                    threshold: exports.SOURCE_OUTPUT_JACCARD_THRESHOLD,
                });
            }
            // Similarity passes. Now check quoted span if provided.
            if (citation.quotedSpan !== undefined) {
                const spanOk = verifyQuotedSpan(citation.quotedSpan, chunkText);
                if (!spanOk) {
                    const event = {
                        type: 'QUOTED_SPAN_MISMATCH',
                        severity: 'ERROR',
                        orgId,
                        agentId,
                        cycleId,
                        citationIndex: i,
                        outputHash,
                        sourceId: citation.sourceId,
                        chunkRef: citation.chunkRef,
                        prevSha: this.prevSha,
                        timestamp: new Date().toISOString(),
                    };
                    this.emitError(event);
                    throw new QuotedSpanNotInChunkError({
                        orgId,
                        agentId,
                        cycleId,
                        citationIndex: i,
                        sourceId: citation.sourceId,
                        chunkRef: citation.chunkRef,
                    });
                }
            }
            citationResults.push({ citation, similarity, verdict: 'verified' });
        }
        // All citations passed
        const successEvent = {
            type: 'SOURCE_OUTPUT_VERIFIED',
            severity: 'NOTICE',
            orgId,
            agentId,
            cycleId,
            citationCount: output.citations.length,
            outputHash,
            timestamp: new Date().toISOString(),
        };
        this.auditSink(successEvent);
        return {
            orgId,
            agentId,
            cycleId,
            citationResults,
            allVerified: true,
        };
    }
    // ---------------------------------------------------------------------------
    // Private helpers
    // ---------------------------------------------------------------------------
    /**
     * Find the pool entry for (sourceId, chunkRef).
     * Assumes citation enforcement has already validated membership.
     * Returns null if not found (programming error / test scenario).
     */
    findPoolEntry(pool, sourceId, chunkRef) {
        for (const entry of pool) {
            if (entry.sourceId === sourceId && entry.chunkRefs.includes(chunkRef)) {
                return entry;
            }
        }
        return null;
    }
    /**
     * Emit an ERROR audit event and advance the SHA chain.
     * WARNING and NOTICE events are emitted directly without advancing the chain
     * (they are not tamper-evidence targets).
     */
    emitError(event) {
        this.auditSink(event);
        this.prevSha = crypto
            .createHash('sha256')
            .update(JSON.stringify(event), 'utf8')
            .digest('hex');
    }
    /**
     * Compute SHA-256 of a string.
     * Used to hash output text for audit logs -- raw content never enters
     * the audit chain.
     */
    sha256(input) {
        return crypto.createHash('sha256').update(input, 'utf8').digest('hex');
    }
}
exports.SourceOutputChainVerifier = SourceOutputChainVerifier;
//# sourceMappingURL=rag-source-output-chain.js.map