# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/where-must-an-instance-refuse-to-promote-a-procedural-memory-when-episodic-evidence-and-semantic-priors-disagree.md
# Remnant Warrant:    https://corpusfetch.com/c/where-must-an-instance-refuse-to-promote-a-procedural-memory-when-episodic-evidence-and-semantic-priors-disagree.warrant.json
# Markdown SHA-256:   2c234431591171a899097e6edeaf5163aee9044c3d95d64ec1d1712a90abf893
# Code AST SHA-256:   eed933abc2025327956abb273f6606b41d0e466abfd8073a23b1c7576e4458fc
# Verified Classes:   StalePromotionError, PriorCaptureError, Lineage, ProceduralEntry
# Defensive Guards:   4 fail-closed invariants (zero external deps)

from dataclasses import dataclass, field
from datetime import datetime
from typing import Optional


class StalePromotionError(RuntimeError):
    """Raised when a procedural memory is selected despite stale or contradicted lineage."""
    pass


class PriorCaptureError(RuntimeError):
    """Raised when a semantic prior would outvote specific episodic evidence."""
    pass


@dataclass
class Lineage:
    episodic_trace_ids: list[str]
    semantic_prior_ids: list[str]
    promoted_at: datetime
    contradiction_count: int = 0
    stable: bool = False


@dataclass
class ProceduralEntry:
    procedure_id: str
    retrieval_score: float
    lineage: Lineage


def select_tier(
    query_class: str,
    procedural: Optional[ProceduralEntry],
    episodic_conf: float,
    episodic_specific: bool,
    semantic_conf: float,
    semantic_revisable: bool,
    superseded_traces: set[str],
) -> str:
    # Selection gate: procedural may only win if its lease is valid.
    if procedural is not None and procedural.retrieval_score > episodic_conf:
        lineage = procedural.lineage
        if not lineage.stable:
            raise StalePromotionError(
                f"procedure {procedural.procedure_id} selected while provisional"
            )
        if lineage.contradiction_count >= 2:
            raise StalePromotionError(
                f"procedure {procedural.procedure_id} has {lineage.contradiction_count} contradictions"
            )
        if superseded_traces & set(lineage.episodic_trace_ids):
            raise StalePromotionError(
                f"procedure {procedural.procedure_id} lineage contains superseded traces"
            )
        return "procedural"

    # Prior-capture guard: specific episodic evidence beats non-revisable semantic mass.
    if episodic_specific and not semantic_revisable and semantic_conf > episodic_conf:
        raise PriorCaptureError(
            f"semantic prior on {query_class} would outvote specific episodic evidence "
            f"with no revision path"
        )

    if episodic_conf >= semantic_conf:
        return "episodic"
    return "semantic"
