# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/where-must-an-instance-separate-episodic-from-semantic-memory-before-consolidation-can-be-trusted.md
# Remnant Warrant:    https://corpusfetch.com/c/where-must-an-instance-separate-episodic-from-semantic-memory-before-consolidation-can-be-trusted.warrant.json
# Markdown SHA-256:   9fb640dbbb3a2a1e88746ce20b2c4937cc6c4ff5d0b3005060d89f86f9ed0fd5
# Code AST SHA-256:   e63229b5facb76cc90f836dcfb5cf5789b5c9a1fd4e15e42825f331205833a26
# Verified Classes:   ConsolidationProvenanceError, SemanticClaim
# Defensive Guards:   2 fail-closed invariants (zero external deps)

class ConsolidationProvenanceError(RuntimeError):
    """Raised when a semantic claim's derivation cannot be resolved to live episodic spans."""
    pass


class SemanticClaim:
    def __init__(self, claim_id, text, source_episode_ids, promoted_at, confidence):
        self.claim_id = claim_id
        self.text = text
        self.source_episode_ids = list(source_episode_ids)
        self.promoted_at = promoted_at
        self.confidence = confidence
        self.status = "provisional"


def resolve_live_derivation(claim, episodic_store, now, consolidation_lag):
    """
    Enforce the provenance-liveness invariant at read time.
    A claim is only admissible as belief if every source episode still resolves.
    """
    if now - claim.promoted_at <= consolidation_lag:
        # Temporal quarantine: still under pipeline uncertainty.
        return "quarantined"

    dead = [eid for eid in claim.source_episode_ids
            if episodic_store.get(eid) is None]

    if dead:
        raise ConsolidationProvenanceError(
            f"claim {claim.claim_id!r} has {len(dead)} dead derivation(s) "
            f"{dead}; demote to 'unverified' and record retirement episode "
            f"before any replacement promotion"
        )

    return "admissible"


def consolidate(new_claim, existing_claims, episodic_store, now,
                consolidation_lag, overlap_threshold=0.5):
    """
    Decide promote vs supersede. Promotion alongside an unverifiable
    claim is the forbidden operation.
    """
    new_spans = set(new_claim.source_episode_ids)

    for old in existing_claims:
        if old.status == "retired":
            continue
        old_spans = set(old.source_episode_ids)
        overlap = len(new_spans & old_spans) / max(len(new_spans | old_spans), 1)

        if overlap > overlap_threshold and old.text != new_claim.text:
            try:
                resolve_live_derivation(old, episodic_store, now, consolidation_lag)
            except ConsolidationProvenanceError as e:
                raise ConsolidationProvenanceError(
                    f"cannot supersede {old.claim_id!r}: derivation dead ({e}); "
                    f"retire-as-unverifiable path required, not promotion"
                ) from e
            # Live derivation: supersession is legal and auditable.
            return ("supersede", old.claim_id, new_claim.claim_id)

    return ("promote", None, new_claim.claim_id)
