Merge pull request #661 from AssetOverflow/feat/p0-3-served-disposition

feat(epistemic-disclosure): P0-3 — ServedDisposition mapping scaffold
This commit is contained in:
Shay 2026-06-08 16:09:56 -07:00 committed by GitHub
commit 4fc251da2d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 318 additions and 4 deletions

View file

@ -6,10 +6,19 @@ disposition + disclosure-claim decide what reaches the user and under what epist
claim. This package is the *owner* of that machine ``generate/derivation/verify.py``
and other serving sites may eventually *consume* it, but must never *define* it.
P0-1 ships only :mod:`core.epistemic_disclosure.limitation` the typed limitation
vocabulary and its mapping as a CONSOLIDATING VIEW over the shipped failure-family
registry + contemplation terminals (no fourth taxonomy). Off-serving: nothing here
imports ``generate.derivation`` / ``core.reliability_gate``.
Shipped so far (all off-serving nothing here imports ``generate.derivation`` /
``core.reliability_gate``):
* :mod:`~core.epistemic_disclosure.limitation` (P0-1) the typed limitation
vocabulary, a CONSOLIDATING VIEW over the shipped failure-family registry +
contemplation terminals (no fourth taxonomy).
* :mod:`~core.epistemic_disclosure.disclosure_claim` (P0-2) the ``DisclosureClaim``
axis (the epistemic claim a response makes), kept SEPARATE from ``ReachLevel``.
* :mod:`~core.epistemic_disclosure.disposition` (P0-3) ``ServedDisposition`` and
``choose_served_disposition``: the pure mapping
``EpistemicState × LimitationAssessment × DisclosureClaim ServedDisposition``.
Mapping scaffold only no rendering, no bus, no ``verify.py``; nothing consumes
it yet.
"""
from __future__ import annotations
@ -18,6 +27,10 @@ from core.epistemic_disclosure.disclosure_claim import (
DEFAULT_DISCLOSURE_CLAIM,
DisclosureClaim,
)
from core.epistemic_disclosure.disposition import (
ServedDisposition,
choose_served_disposition,
)
from core.epistemic_disclosure.limitation import (
PENDING_Q1B_RECLASSIFICATION,
LimitationAssessment,
@ -35,7 +48,9 @@ __all__ = [
"LimitationAssessment",
"LimitationKind",
"ResolutionAction",
"ServedDisposition",
"assess_from_attempt",
"assess_from_family",
"choose_served_disposition",
"terminal_for_action",
]

View file

@ -0,0 +1,110 @@
"""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"]

View file

@ -0,0 +1,189 @@
"""P0-3 — tests for the ServedDisposition mapping scaffold.
Mapping-only: no rendering, no bus, no serving. Tests assert the load-bearing rules,
the Phase-1 VERIFIED guard (a verified claim never discloses without the verified
state), limitation-dominates-claim, the explicit scope_boundary -> EXPLAIN choice,
and totality over both axes.
"""
from __future__ import annotations
import ast
import pathlib
from enum import Enum
import pytest
import core.epistemic_disclosure.disposition as disposition_module
from core.comprehension_attempt.failure_family import REGISTRY, family_by_name
from core.epistemic_disclosure.disclosure_claim import DisclosureClaim
from core.epistemic_disclosure.disposition import (
ServedDisposition,
choose_served_disposition,
)
from core.epistemic_disclosure.limitation import LimitationAssessment, assess_from_family
from core.epistemic_state import EpistemicState
def _disp_for_family(name, *, state=EpistemicState.UNDETERMINED, claim=DisclosureClaim.NONE):
family = family_by_name(name)
assert family is not None, name
return choose_served_disposition(
epistemic_state=state, limitation=assess_from_family(family), disclosure_claim=claim
)
# --- serve branch (no blocking limitation) ----------------------------------------- #
def test_no_limitation_none_claim_commits():
assert (
choose_served_disposition(
epistemic_state=EpistemicState.DECODED,
limitation=None,
disclosure_claim=DisclosureClaim.NONE,
)
is ServedDisposition.COMMIT
)
def test_verified_state_and_claim_discloses():
assert (
choose_served_disposition(
epistemic_state=EpistemicState.VERIFIED,
limitation=None,
disclosure_claim=DisclosureClaim.VERIFIED,
)
is ServedDisposition.DISCLOSE
)
@pytest.mark.parametrize(
"state", [s for s in EpistemicState if s is not EpistemicState.VERIFIED]
)
def test_verified_claim_without_verified_state_never_discloses(state):
"""THE Phase-1 guard: a verified claim must not produce DISCLOSE unless the state
is actually VERIFIED protects the lane from 'verified-looking' serving before the
producer exists."""
disp = choose_served_disposition(
epistemic_state=state, limitation=None, disclosure_claim=DisclosureClaim.VERIFIED
)
assert disp is not ServedDisposition.DISCLOSE
assert disp is ServedDisposition.COMMIT
def test_approximate_claim_discloses():
assert (
choose_served_disposition(
epistemic_state=EpistemicState.EVIDENCED_INCOMPLETE,
limitation=None,
disclosure_claim=DisclosureClaim.APPROXIMATE,
)
is ServedDisposition.DISCLOSE
)
def test_proposal_only_claim_proposes():
assert (
choose_served_disposition(
epistemic_state=EpistemicState.UNDETERMINED,
limitation=None,
disclosure_claim=DisclosureClaim.PROPOSAL_ONLY,
)
is ServedDisposition.PROPOSE
)
# --- limitation-driven dispositions (via real P0-1 assessments) -------------------- #
def test_ask_question_limitation_asks():
assert _disp_for_family("cmb_underdetermined") is ServedDisposition.ASK
def test_emit_proposal_limitation_proposes():
assert _disp_for_family("cmb_unsupported_reciprocal") is ServedDisposition.PROPOSE
def test_report_contradiction_limitation_reports():
assert _disp_for_family("answer_key_contradiction") is ServedDisposition.REPORT
def test_step_aside_limitation_steps_aside():
assert _disp_for_family("input_shape") is ServedDisposition.STEP_ASIDE
def test_hard_boundary_limitation_refuses():
assert _disp_for_family("cmb_non_positive_net") is ServedDisposition.REFUSE
def test_scope_boundary_limitation_explains_not_refuses():
"""The explicit P0-3 choice: scope is EXPLAIN, not a hard REFUSE."""
disp = _disp_for_family("unsupported_system_size")
assert disp is ServedDisposition.EXPLAIN
assert disp is not ServedDisposition.REFUSE
def test_limitation_dominates_disclosure_claim():
"""Even a fully-backed verified claim cannot serve an answer when a limitation blocks."""
family = family_by_name("cmb_underdetermined")
assert family is not None
disp = choose_served_disposition(
epistemic_state=EpistemicState.VERIFIED,
limitation=assess_from_family(family),
disclosure_claim=DisclosureClaim.VERIFIED,
)
assert disp is ServedDisposition.ASK
def test_answer_action_falls_through_to_serve():
"""A non-blocking limitation (resolution_action == 'answer') yields the serve decision."""
la = LimitationAssessment(
limitation_kind="missing_information",
resolution_action="answer",
epistemic_state=EpistemicState.UNDETERMINED,
owner_organ="r2",
blocking_reason="synthetic",
)
assert (
choose_served_disposition(
epistemic_state=EpistemicState.DECODED,
limitation=la,
disclosure_claim=DisclosureClaim.NONE,
)
is ServedDisposition.COMMIT
)
# --- totality over both axes ------------------------------------------------------- #
def test_every_family_maps_to_a_disposition():
for family in REGISTRY:
disp = choose_served_disposition(
epistemic_state=EpistemicState.UNDETERMINED, limitation=assess_from_family(family)
)
assert isinstance(disp, ServedDisposition), family.name
def test_every_disclosure_claim_serves_to_a_disposition():
for claim in DisclosureClaim:
disp = choose_served_disposition(
epistemic_state=EpistemicState.VERIFIED, limitation=None, disclosure_claim=claim
)
assert isinstance(disp, ServedDisposition), claim
# --- hygiene ----------------------------------------------------------------------- #
def test_is_str_enum():
assert issubclass(ServedDisposition, str)
assert issubclass(ServedDisposition, Enum)
def test_module_is_off_serving():
module_path = disposition_module.__file__
assert module_path is not None
src = pathlib.Path(module_path).read_text()
mods = [n.module or "" for n in ast.walk(ast.parse(src)) if isinstance(n, ast.ImportFrom)]
forbidden = [
m for m in mods if "generate.derivation" in m or "reliability_gate" in m or m.endswith("verify")
]
assert forbidden == []