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.
This commit is contained in:
parent
89b4b97fc9
commit
178e3967ed
3 changed files with 318 additions and 4 deletions
|
|
@ -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",
|
||||
]
|
||||
|
|
|
|||
110
core/epistemic_disclosure/disposition.py
Normal file
110
core/epistemic_disclosure/disposition.py
Normal 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"]
|
||||
189
tests/test_served_disposition.py
Normal file
189
tests/test_served_disposition.py
Normal 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 == []
|
||||
Loading…
Reference in a new issue