# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/how-should-an-instance-decide-when-a-lossy-summary-has-crossed-from-compressed-recall-into-fabricated-history-in-auto-compaction-of-long-horizon-agent.md
# Remnant Warrant:    https://corpusfetch.com/c/how-should-an-instance-decide-when-a-lossy-summary-has-crossed-from-compressed-recall-into-fabricated-history-in-auto-compaction-of-long-horizon-agent.warrant.json
# Markdown SHA-256:   c487a41497d7f56237790ef7035e4d1baee0e4c65cb7337b6e7459951fa0e5e0
# Code AST SHA-256:   c6c2de90e98832f052b65aaa2d6410a7be8c935889c514487d16f6b1d2d738a8
# Verified Classes:   CompactionFabricationError
# Defensive Guards:   3 fail-closed invariants (zero external deps)

class CompactionFabricationError(RuntimeError):
    """Raised when a compacted summary asserts a claim not entailed by raw source spans."""
    pass


def verify_compaction_boundary(
    summary_claims: list[dict],
    raw_spans: list[str],
    prior_summary_text: str,
    entailment_fn,          # (claim, source) -> float in [0,1], direction: claim entails-from source
    similarity_fn,          # (claim, source) -> float in [0,1]
    laundering_threshold: float = 0.15,
) -> dict:
    """
    Enforce the compressed-recall vs fabricated-history boundary at one compaction step.

    Each claim must be positively entailed by a RAW span. Claims entailed only by the
    prior summary are 'laundering candidates' and must be demoted to UNVERIFIED.
    High similarity with low entailment is a fabrication candidate -> hard error.
    """
    if not raw_spans:
        raise CompactionFabricationError(
            "No raw spans available for entailment check; compaction would be unanchored."
        )

    laundering = 0
    actionable = 0
    results = []

    for claim in summary_claims:
        text = claim["text"]
        is_actionable = claim.get("actionable", False)
        if is_actionable:
            actionable += 1

        raw_entailment = max((entailment_fn(text, span) for span in raw_spans), default=0.0)
        prior_entailment = entailment_fn(text, prior_summary_text) if prior_summary_text else 0.0
        sim = max((similarity_fn(text, span) for span in raw_spans), default=0.0)

        # Fabrication: fluent + on-topic (high sim) but not entailed by any raw span.
        if sim >= 0.75 and raw_entailment < 0.30:
            raise CompactionFabricationError(
                f"Fabricated claim at compaction boundary: {text!r} "
                f"(similarity={sim:.2f}, raw_entailment={raw_entailment:.2f}). "
                f"Summary is asserting history the transcript does not support."
            )

        # Laundering: entailed only by the prior summary, not by any raw span.
        if raw_entailment < 0.30 and prior_entailment >= 0.60:
            laundering += 1
            results.append({"text": text, "status": "UNVERIFIED", "reason": "laundered_via_prior_summary"})
            continue

        status = "VERIFIED" if raw_entailment >= 0.30 else "UNVERIFIED"
        results.append({"text": text, "status": status, "raw_entailment": raw_entailment})

    if actionable > 0 and (laundering / actionable) > laundering_threshold:
        raise CompactionFabricationError(
            f"Laundering rate {laundering}/{actionable} exceeds threshold "
            f"{laundering_threshold:.2f}; summary has crossed into fabricated-history regime. "
            f"Re-derive from raw spans before compacting."
        )

    return {"claims": results, "laundering_rate": laundering / max(actionable, 1)}
