core/core/epistemic_disclosure/disposition.py
Shay 178e3967ed feat(epistemic-disclosure): P0-3 — ServedDisposition mapping scaffold
The third axis of the bus: choose_served_disposition composes
EpistemicState × LimitationAssessment × DisclosureClaim → ServedDisposition.
Pure mapping — no rendering, no bus behaviour, no verify.py, no govern_response
mutation. Nothing consumes it yet.

core/epistemic_disclosure/disposition.py:
- ServedDisposition(str, Enum): commit | disclose | ask | propose | report |
  explain | refuse | step_aside.
- choose_served_disposition(*, epistemic_state, limitation, disclosure_claim=NONE):
  a blocking limitation dominates (its resolution_action -> ask/propose/report/
  explain/refuse/step_aside; scope_boundary -> EXPLAIN, NOT a hard REFUSE);
  otherwise the disclosure claim + state pick the serve mode.

The Phase-1 VERIFIED guard (load-bearing): a DisclosureClaim.VERIFIED discloses
ONLY when EpistemicState.VERIFIED backs it; an unbacked verified claim degrades to
a plain COMMIT — never served as verified before the producer exists.

30 tests: the load-bearing rules via REAL P0-1 assessments; the VERIFIED guard
parametrized across all 14 non-VERIFIED states; limitation-dominates-claim;
answer-action fall-through; totality over both axes; off-serving. Smoke 90/0.
2026-06-08 16:00:15 -07:00

110 lines
5.1 KiB
Python

"""P0-3 — ServedDisposition: the served-surface decision (mapping scaffold only).
The third axis of the Epistemic Disclosure bus. Given (a) the epistemic state, (b)
the limitation assessment (if resolution is blocked), and (c) the disclosure claim,
:func:`choose_served_disposition` decides WHAT KIND OF MOVE the served surface makes:
commit / disclose / ask / propose / report / explain / refuse / step-aside.
This is a PURE MAPPING. No rendering, no bus behaviour, no ``verify.py``, no
``govern_response`` mutation — nothing consumes the result yet. P0-3 only fixes the
decision table so a later slice / Phase 1 can wire it.
The load-bearing rule (the Phase-1 guard): a ``DisclosureClaim.VERIFIED`` discloses
ONLY when the epistemic state is actually ``EpistemicState.VERIFIED``. An unbacked
verified claim degrades to a plain ``COMMIT`` — it is NEVER served as verified before
the producer exists. This protects the whole VERIFIED lane from accidental
"verified-looking" serving.
Off-serving: imports no ``generate.derivation`` / ``core.reliability_gate``.
"""
from __future__ import annotations
from enum import Enum, unique
from core.epistemic_disclosure.disclosure_claim import DisclosureClaim
from core.epistemic_disclosure.limitation import LimitationAssessment
from core.epistemic_state import EpistemicState
@unique
class ServedDisposition(str, Enum):
"""What kind of move the served surface makes.
``str``-valued for stable serialization (the ``EpistemicState`` /
``DisclosureClaim`` convention).
"""
COMMIT = "commit" # serve a fully-grounded answer as-is, no epistemic caveat
DISCLOSE = "disclose" # serve under a disclosure claim ([verified] / [approximate])
ASK = "ask" # ask the user for the missing/ambiguous datum (Q1 tenant)
PROPOSE = "propose" # offer a review-only capability proposal, do not assert
REPORT = "report" # report a contradiction with a supplied answer key
EXPLAIN = "explain" # explain that the ask is outside the current envelope (scope)
REFUSE = "refuse" # hard-stop refusal (impossible / unreadable / known boundary)
STEP_ASIDE = "step_aside" # not this organ's domain — cede
def choose_served_disposition(
*,
epistemic_state: EpistemicState,
limitation: LimitationAssessment | None,
disclosure_claim: DisclosureClaim = DisclosureClaim.NONE,
) -> ServedDisposition:
"""Decide the served disposition. Pure, deterministic, total.
A blocking limitation dominates — you do not serve an answer, you ask / propose /
report / explain / refuse / step aside per its resolution action. (A limitation
whose action is ``answer`` is non-blocking and falls through to the serve
decision.) With no blocking limitation, the disclosure claim + epistemic state pick
the serve mode, under the Phase-1 ``VERIFIED`` guard.
NOTE (scaffold trust boundary): a blocking epistemic state (CONTRADICTED,
AMBIGUOUS, UNDETERMINED) reaches this function AS a limitation (e.g. a contradiction
arrives as ``report_contradiction``), not as ``limitation=None``. The serve branch
trusts ``limitation=None`` to mean "servable".
"""
if limitation is not None:
match limitation.resolution_action:
case "ask_question":
return ServedDisposition.ASK
case "emit_proposal":
return ServedDisposition.PROPOSE
case "report_contradiction":
return ServedDisposition.REPORT
case "step_aside":
return ServedDisposition.STEP_ASIDE
case "refuse_known_boundary":
# scope_boundary is the governed "outside the current envelope"
# disposition — it may render as a refusal later, but must NOT collapse
# into a hard boundary here.
if limitation.limitation_kind == "scope_boundary":
return ServedDisposition.EXPLAIN
return ServedDisposition.REFUSE
case "answer":
pass # non-blocking — fall through to the serve decision
return _serve_disposition(epistemic_state, disclosure_claim)
def _serve_disposition(
epistemic_state: EpistemicState, disclosure_claim: DisclosureClaim
) -> ServedDisposition:
"""The no-blocking-limitation branch: claim + state pick the serve mode."""
match disclosure_claim:
case DisclosureClaim.VERIFIED:
# The Phase-1 guard: disclose-as-verified ONLY with the backing state.
if epistemic_state is EpistemicState.VERIFIED:
return ServedDisposition.DISCLOSE
return ServedDisposition.COMMIT
case DisclosureClaim.APPROXIMATE:
return ServedDisposition.DISCLOSE
case DisclosureClaim.PROPOSAL_ONLY:
return ServedDisposition.PROPOSE
case DisclosureClaim.NONE:
return ServedDisposition.COMMIT
case _: # pragma: no cover - exhaustive over the 4-member enum; loud if extended
raise AssertionError(f"unhandled disclosure_claim: {disclosure_claim!r}")
__all__ = ["ServedDisposition", "choose_served_disposition"]