/**
 * immutableGuard.ts — the structural "never re-derive curated state" guard (spec §2.4).
 *
 * Curated cluster state — pre-cutover lifecycle status + the human/LLM governance
 * judgment that lives in the notes fields — is IRREPLACEABLE. The migration imports it
 * AS-IS and the processor must never overwrite it. "Structure > Willpower": rather than
 * trust the processor to only ever run over new, post-cutover traffic, we wrap the store
 * so a mutation of an immutable cluster is PHYSICALLY refused (and recorded), not merely
 * discouraged by a comment. A wrong-date backfill therefore cannot overwrite curated
 * lifecycle/governance state — the write simply does not happen.
 *
 * A cluster is immutable when EITHER:
 *   - it carries non-null governance notes (a human/LLM triage decision lives on it), OR
 *   - it predates the cutover (createdAt < cutoverTimestamp) — migrated curated history.
 *
 * The guard is a decorator over any FeedbackStore: reads and non-curated writes pass
 * through untouched; the three methods that can mutate an existing cluster
 * (mergeIntoCluster, applyReopen, upsertClusterFromItem-when-it-exists) are gated.
 */
/** Fields that, when non-empty, mark a cluster as carrying curated governance judgment. */
export const GOVERNANCE_FIELDS = [
    'governanceNotes',
    'processingNotes',
    'actionTaken',
    'researchNotes',
];
/** True when the cluster carries any non-empty curated governance note. */
export function hasGovernanceNotes(cluster) {
    for (const f of GOVERNANCE_FIELDS) {
        const v = cluster[f];
        if (typeof v === 'string' && v.trim().length > 0)
            return true;
    }
    return false;
}
/**
 * The structural invariant: is this cluster curated state the processor must not touch?
 *
 * - Governance notes present → immutable regardless of date (a triage decision lives here).
 * - createdAt < cutoverTimestamp → migrated curated history, immutable.
 * - createdAt present but unparseable → fail SAFE (immutable): a cluster that HAS a
 *   timestamp we cannot read is suspicious; never risk overwriting curated state.
 * - No createdAt and no governance notes → a fresh/post-cutover cluster → mutable
 *   (curated rows always carry a timestamp or notes, so this is new processor state).
 */
export function isClusterImmutable(cluster, cutoverTimestamp) {
    if (hasGovernanceNotes(cluster))
        return true;
    const createdAt = cluster.createdAt;
    if (typeof createdAt === 'string' && createdAt.length > 0) {
        const created = Date.parse(createdAt);
        const cutover = Date.parse(cutoverTimestamp);
        if (Number.isNaN(created) || Number.isNaN(cutover))
            return true; // fail safe
        return created < cutover;
    }
    return false;
}
/** Why a cluster was deemed immutable, for the violation record. */
function immutableReason(cluster) {
    return hasGovernanceNotes(cluster) ? 'governance-notes' : 'pre-cutover';
}
/**
 * A FeedbackStore decorator that enforces the never-re-derive invariant structurally.
 * Mutations targeting an immutable cluster are refused (recorded as a GuardViolation),
 * never delegated to the inner store — so curated state physically cannot be overwritten.
 */
export class GuardedFeedbackStore {
    inner;
    cutoverTimestamp;
    onViolation;
    now;
    _violations = [];
    constructor(inner, opts) {
        this.inner = inner;
        this.cutoverTimestamp = opts.cutoverTimestamp;
        this.onViolation = opts.onViolation;
        // Default clock is fixed at construction so a guarded store has no hidden Date.now() call
        // per mutation; callers that need per-event time pass opts.now.
        const stamp = opts.now;
        this.now = stamp ?? (() => this.cutoverTimestamp);
    }
    /** All refused mutations observed by this guard (in order). */
    get violations() {
        return this._violations;
    }
    refuse(clusterId, operation, cluster, feedbackId) {
        const v = {
            clusterId,
            operation,
            reason: immutableReason(cluster),
            feedbackId,
            at: this.now(),
        };
        this._violations.push(v);
        if (this.onViolation) {
            try {
                this.onViolation(v);
            }
            catch {
                // An audit-sink failure must never break the store or surface as a mutation.
                // @silent-fallback-ok — guard integrity is independent of the audit sink's health.
            }
        }
    }
    // ── Gated mutations (curated-cluster protection) ──────────────────────────
    upsertClusterFromItem(clusterId, item) {
        const existing = this.inner.getCluster(clusterId);
        if (existing && isClusterImmutable(existing, this.cutoverTimestamp)) {
            this.refuse(clusterId, 'upsert', existing, item.feedbackId);
            return;
        }
        this.inner.upsertClusterFromItem(clusterId, item);
    }
    mergeIntoCluster(clusterId, item) {
        const existing = this.inner.getCluster(clusterId);
        if (existing && isClusterImmutable(existing, this.cutoverTimestamp)) {
            this.refuse(clusterId, 'merge', existing, item.feedbackId);
            return;
        }
        this.inner.mergeIntoCluster(clusterId, item);
    }
    applyReopen(clusterId, decision) {
        const existing = this.inner.getCluster(clusterId);
        if (existing && isClusterImmutable(existing, this.cutoverTimestamp)) {
            this.refuse(clusterId, 'reopen', existing);
            return;
        }
        this.inner.applyReopen(clusterId, decision);
    }
    // ── Pass-through (reads + non-curated writes) ─────────────────────────────
    getUnprocessedFeedback() {
        return this.inner.getUnprocessedFeedback();
    }
    getActiveClusters() {
        return this.inner.getActiveClusters();
    }
    getCluster(clusterId) {
        return this.inner.getCluster(clusterId);
    }
    markProcessed(feedbackId, clusterId) {
        this.inner.markProcessed(feedbackId, clusterId);
    }
    hasFeedback(feedbackId) {
        return this.inner.hasFeedback(feedbackId);
    }
    addFeedback(item) {
        this.inner.addFeedback(item);
    }
    listDispatches(filter) {
        return this.inner.listDispatches(filter);
    }
    findDispatchByTitle(title) {
        return this.inner.findDispatchByTitle(title);
    }
    createDispatch(record) {
        this.inner.createDispatch(record);
    }
    metrics() {
        return this.inner.metrics();
    }
}
//# sourceMappingURL=immutableGuard.js.map