# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/when-must-an-instance-evict-a-memory-entry-before-its-salience-gate-closes.md
# Remnant Warrant:    https://corpusfetch.com/c/when-must-an-instance-evict-a-memory-entry-before-its-salience-gate-closes.warrant.json
# Markdown SHA-256:   bb0ebc3ab2f683c8216b2cac83a2e5533b09c7e32ffb32f4a68a16749a901fca
# Code AST SHA-256:   bc23217420952ac5c683e81fe270742cab7903650a25f38eafbf75514dd97b6b
# Verified Classes:   EvictionBoundaryError
# Defensive Guards:   2 fail-closed invariants (zero external deps)

class EvictionBoundaryError(RuntimeError):
    """Raised when a reclaim policy would evict an entry that is still
    reachable via consolidation/refresh (dormant, not dead) or whose
    surviving summary is not lossless for the entry's facts."""


def decide_eviction(
    entry,
    reclaim_floor: float,
    gate_threshold: float,
    refresh_window_elapsed: bool,
    reachable_below_gate: bool,
    summary_is_lossless: bool,
) -> str:
    """Return 'evict', 'spill', or raise EvictionBoundaryError.

    Invariant: salience gating is a retrieval predicate, not a lifecycle
    predicate.  Eviction must be driven by capacity/TTL, never by gate
    closure alone.
    """
    if reclaim_floor >= gate_threshold:
        # Policy cannot distinguish dormant from dead; eviction is unsafe
        # by construction.
        raise EvictionBoundaryError(
            f"reclaim_floor={reclaim_floor} >= gate_threshold={gate_threshold}: "
            "policy conflates gate closure with death"
        )

    gated = entry.salience < gate_threshold

    if gated and reachable_below_gate and not refresh_window_elapsed:
        # Dormant entry, still refreshable: must not be deleted.
        return "spill"

    if gated and reachable_below_gate and refresh_window_elapsed:
        # Refresh window closed; only safe if summary is lossless.
        if not summary_is_lossless:
            raise EvictionBoundaryError(
                f"entry {entry.key!r} evictable only via lossy summary; "
                "downstream consumers would silently substitute stale facts"
            )
        return "evict"

    if not gated:
        # Live entry: evict only under explicit capacity/TTL policy,
        # which the caller signals by passing reclaim_floor selection.
        return "evict"

    # Gated, unreachable, window elapsed: genuinely dead.
    return "evict"
