"use strict";
/**
 * DataClassifier -- Stamps DataClassTag on every stored record.
 *
 * Usage at every storage write site:
 *
 *   const tag = classifier.classify('audit:chain:append', {
 *     tenantId: 'tenant-abc',
 *     contractId: 'contract-xyz',
 *     activePackIds: ['hipaa', 'soc2'],
 *   });
 *   // attach tag to the record before writing
 *
 * FAIL-CLOSED invariants:
 *   - Unknown write site  => UnclassifiedWriteError thrown synchronously (AC 19)
 *   - Empty packBinding   => UnboundDataError thrown synchronously (AC 1b)
 *
 * Pack-binding change audit (AC 1d):
 *   - emitPackBindingChangedEvent() emits retention:pack-binding-changed
 *   - retentionWindowElapsed() stubs the window check until P4-RET-02 ships
 *
 * Error hierarchy:
 *   RetentionError extends LlmProviderError (unified error hierarchy pattern)
 *     UnclassifiedWriteError extends RetentionError
 *     UnboundDataError extends RetentionError
 *
 * @connexum/ai-governance
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.DataClassifier = exports.defaultRetentionWindowResolver = exports.UnboundDataError = exports.UnclassifiedWriteError = exports.RetentionError = void 0;
exports.emitPackBindingChangedEvent = emitPackBindingChangedEvent;
exports.retentionWindowElapsed = retentionWindowElapsed;
exports.createRetentionWindowResolver = createRetentionWindowResolver;
const index_1 = require("../llm-adapters/index");
const classification_rules_1 = require("./classification-rules");
// ---------------------------------------------------------------------------
// Error hierarchy
// ---------------------------------------------------------------------------
/**
 * Base class for all retention-engine errors.
 * Extends LlmProviderError to sit in the unified error hierarchy.
 */
class RetentionError extends index_1.LlmProviderError {
    constructor(opts) {
        super({
            message: opts.message,
            code: opts.code,
            // Retention errors are not provider-specific. We use sentinel values
            // so callers can distinguish them via instanceof RetentionError without
            // inspecting providerId.
            providerId: 'retention-engine',
            adapterId: 'retention-engine',
        });
        this.name = 'RetentionError';
    }
}
exports.RetentionError = RetentionError;
/**
 * Thrown synchronously when a write site has no matching classification rule.
 * The write is refused at the storage call site -- there is no silent fallback.
 *
 * AC 3, AC 19.
 */
class UnclassifiedWriteError extends RetentionError {
    writeSite;
    constructor(writeSite) {
        super({
            message: `UnclassifiedWriteError: no classification rule for write site "${writeSite}". ` +
                'Register a rule in classification-rules.ts or correct the write-site identifier.',
            code: 'UNCLASSIFIED_WRITE',
        });
        this.name = 'UnclassifiedWriteError';
        this.writeSite = writeSite;
    }
}
exports.UnclassifiedWriteError = UnclassifiedWriteError;
/**
 * Thrown synchronously when the resolved packBinding is empty.
 * This means the tenant has no active packs -- writes are refused until
 * at least one compliance pack is activated.
 *
 * AC 1b.
 */
class UnboundDataError extends RetentionError {
    writeSite;
    tenantId;
    constructor(writeSite, tenantId) {
        super({
            message: `UnboundDataError: empty packBinding for write site "${writeSite}", ` +
                `tenant "${tenantId}". A tenant with no active compliance packs cannot ` +
                'produce compliance-bound data. Activate at least one pack before writing.',
            code: 'UNBOUND_DATA',
        });
        this.name = 'UnboundDataError';
        this.writeSite = writeSite;
        this.tenantId = tenantId;
    }
}
exports.UnboundDataError = UnboundDataError;
/**
 * Emit a pack-binding-changed audit event.
 *
 * Callers should forward the returned event to the tamper-evident audit chain.
 * This function validates that the un-binding attempt is legal -- if the
 * retention window has not elapsed for any removed pack, the call throws.
 *
 * P4-RET-02 (2026-05-12): the window check is now resolver-injected.
 * The default resolver is `defaultRetentionWindowResolver` which always
 * returns `false` (refuse un-binding) — backward-compatible with the
 * pre-2026-05-12 hardcoded stub. Production callers pass a real resolver
 * via `opts.retentionResolver` to enable legitimate post-window un-binding.
 *
 * AC 1d.
 */
function emitPackBindingChangedEvent(before, after, recordId, actor, reason, opts = {}) {
    // Determine packs being removed.
    const removed = before.filter((p) => !after.includes(p));
    const resolver = opts.retentionResolver ?? exports.defaultRetentionWindowResolver;
    for (const packId of removed) {
        if (!resolver(recordId, packId)) {
            throw new RetentionError({
                message: `Cannot remove pack "${packId}" from record "${recordId}": ` +
                    `retention window has not elapsed. Un-binding a pack before its ` +
                    `retention window closes would bypass the retention obligation.`,
                code: 'RETENTION_WINDOW_NOT_ELAPSED',
            });
        }
    }
    return {
        type: 'retention:pack-binding-changed',
        recordId,
        before,
        after,
        actor,
        reason,
        timestamp: new Date().toISOString(),
    };
}
/**
 * Default fail-closed resolver. Always returns `false` — un-binding is
 * refused. This is the safer default and matches the pre-P4-RET-02
 * stub behaviour, so callers that do NOT pass a resolver get exactly
 * the conservative behaviour they had before.
 *
 * Production callers wire a real resolver via
 * `createRetentionWindowResolver()` or supply a fully custom resolver.
 */
const defaultRetentionWindowResolver = () => false;
exports.defaultRetentionWindowResolver = defaultRetentionWindowResolver;
/**
 * Backward-compatible alias for the pre-P4-RET-02 stub function.
 * Kept exported so any external callers continue to compile. New code
 * should pass a `retentionResolver` to `emitPackBindingChangedEvent`
 * instead of calling this directly.
 *
 * @deprecated use `defaultRetentionWindowResolver` or pass a real
 *             `retentionResolver` to `emitPackBindingChangedEvent`.
 */
function retentionWindowElapsed(_recordId, _packId) {
    return (0, exports.defaultRetentionWindowResolver)(_recordId, _packId);
}
/**
 * Build a production retention-window resolver from a record lookup +
 * pack retention table. Returns `true` only when both (a) the record's
 * createdAt is known AND (b) the pack's retention floor is known AND
 * (c) `now() - createdAt > retentionDays * 24h`. Any uncertainty
 * returns `false` (fail-closed).
 *
 * Wiring pattern:
 *   const resolver = createRetentionWindowResolver({
 *     lookupRecordCreatedAt: id => recordCache.get(id)?.createdAt ?? null,
 *     packRetentionDays: new Map(
 *       activePacks.map(p => [p.id, p.retentionPolicy.auditLogRetentionDays]),
 *     ),
 *   });
 *   emitPackBindingChangedEvent(before, after, recordId, actor, reason,
 *     { retentionResolver: resolver });
 */
function createRetentionWindowResolver(options) {
    const now = options.now ?? Date.now;
    return (recordId, packId) => {
        const createdAt = options.lookupRecordCreatedAt(recordId);
        if (!createdAt)
            return false;
        const retentionDays = options.packRetentionDays.get(packId);
        if (typeof retentionDays !== 'number' || retentionDays <= 0)
            return false;
        const retentionMs = retentionDays * 24 * 60 * 60 * 1000;
        return now() - createdAt.getTime() > retentionMs;
    };
}
class DataClassifier {
    rules;
    constructor(options = {}) {
        this.rules = options.rules ?? classification_rules_1.DEFAULT_CLASSIFICATION_RULES;
    }
    /**
     * Classify a storage write.
     *
     * Matches `writeSite` against the rule table. On match, resolves
     * packBinding by intersecting ctx.activePackIds with the rule's resolver
     * output. Throws synchronously on any invariant violation.
     *
     * @param writeSite  Stable identifier for the storage call site
     *                   (e.g. 'audit:chain:append').
     * @param ctx        Tenant + contract context with active pack IDs.
     * @returns DataClassTag stamped with the resolved classification.
     * @throws UnclassifiedWriteError if no rule matches.
     * @throws UnboundDataError if resolved packBinding is empty.
     */
    classify(writeSite, ctx) {
        const fullCtx = { ...ctx };
        // Find matching rule (first match wins; rules are ordered by specificity).
        const rule = this.matchRule(writeSite);
        if (!rule) {
            throw new UnclassifiedWriteError(writeSite);
        }
        // Resolve pack binding.
        const resolvedPacks = rule.packBindingResolver(fullCtx);
        // Intersect with caller's active pack IDs (AC 1b).
        const packBinding = resolvedPacks.filter((p) => fullCtx.activePackIds.includes(p));
        if (packBinding.length === 0) {
            throw new UnboundDataError(writeSite, ctx.tenantId);
        }
        const sovereigntyZone = rule.sovereigntyResolver(fullCtx);
        const tag = {
            tenantId: ctx.tenantId,
            contractId: ctx.contractId,
            dataClass: rule.dataClass,
            packBinding,
            sovereigntyZone,
            classifiedAt: new Date().toISOString(),
        };
        return tag;
    }
    /**
     * Returns the matching rule for a write site, or null if none matches.
     */
    matchRule(writeSite) {
        for (const rule of this.rules) {
            if (typeof rule.writeSitePattern === 'string') {
                if (rule.writeSitePattern === writeSite)
                    return rule;
            }
            else {
                if (rule.writeSitePattern.test(writeSite))
                    return rule;
            }
        }
        return null;
    }
}
exports.DataClassifier = DataClassifier;
//# sourceMappingURL=classifier.js.map