feat(epistemic-disclosure): P0-2 — the DisclosureClaim axis
A served response carries two ORTHOGONAL governance properties: ReachLevel (how far past grounded fact it reaches) and DisclosureClaim (the epistemic claim it makes about its own truth status). "verified" is NOT a reach level — it is a proof-state claim. Keeping the axes separate is the locked Stage-2 decision (no ReachLevel.VERIFIED; a proven answer never inherits an [approximate] surface). core/epistemic_disclosure/disclosure_claim.py: - DisclosureClaim(str, Enum): none | verified | approximate | proposal_only. Default = none. str-valued for stable serialization (EpistemicState convention). - Discipline "no claim without a producer": every member has a real (APPROXIMATE = cognition Step E; PROPOSAL_ONLY = teaching/proposals) or imminent (VERIFIED = Phase 1 target) emitter. PROVEN and ESTIMATED are intentionally ABSENT — nothing emits them; ESTIMATED is a future split of APPROXIMATE, added only when a real estimator producer exists (docs-note only). Also folds the P0-1 review nit: _KIND_TO_STATE / _ACTION_TO_TERMINAL tightened from dict[str, ...] to their Literal key types. 9 tests, non-vacuous: default is NONE; verified is not a ReachLevel (structural); claim axis disjoint from ResolutionAction; PROVEN and ESTIMATED absent; exactly the approved four; str-enum serialization; module imports nothing (off-serving). No bus behaviour, no ServedDisposition mapping (that is P0-3). Off-serving; smoke green (90 passed) locally.
This commit is contained in:
parent
14d81887df
commit
8811917280
4 changed files with 167 additions and 2 deletions
|
|
@ -14,6 +14,10 @@ imports ``generate.derivation`` / ``core.reliability_gate``.
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from core.epistemic_disclosure.disclosure_claim import (
|
||||||
|
DEFAULT_DISCLOSURE_CLAIM,
|
||||||
|
DisclosureClaim,
|
||||||
|
)
|
||||||
from core.epistemic_disclosure.limitation import (
|
from core.epistemic_disclosure.limitation import (
|
||||||
PENDING_Q1B_RECLASSIFICATION,
|
PENDING_Q1B_RECLASSIFICATION,
|
||||||
LimitationAssessment,
|
LimitationAssessment,
|
||||||
|
|
@ -25,7 +29,9 @@ from core.epistemic_disclosure.limitation import (
|
||||||
)
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
"DEFAULT_DISCLOSURE_CLAIM",
|
||||||
"PENDING_Q1B_RECLASSIFICATION",
|
"PENDING_Q1B_RECLASSIFICATION",
|
||||||
|
"DisclosureClaim",
|
||||||
"LimitationAssessment",
|
"LimitationAssessment",
|
||||||
"LimitationKind",
|
"LimitationKind",
|
||||||
"ResolutionAction",
|
"ResolutionAction",
|
||||||
|
|
|
||||||
65
core/epistemic_disclosure/disclosure_claim.py
Normal file
65
core/epistemic_disclosure/disclosure_claim.py
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
"""P0-2 — the DisclosureClaim axis (the epistemic claim a served response makes).
|
||||||
|
|
||||||
|
A served response carries two ORTHOGONAL governance properties:
|
||||||
|
|
||||||
|
* ``ReachLevel`` (``core/response_governance/policy.py``) — how far PAST
|
||||||
|
fully-grounded fact the response reaches (STRICT < APPROXIMATE < EXTRAPOLATE
|
||||||
|
< CREATIVE).
|
||||||
|
* ``DisclosureClaim`` (here) — the EPISTEMIC CLAIM the response makes about its
|
||||||
|
own truth status.
|
||||||
|
|
||||||
|
These are deliberately separate axes. ``verified`` is **not** a reach level — it is
|
||||||
|
a claim about *proof state*, not about how far the response speculates. Conflating
|
||||||
|
them (e.g. a hypothetical ``ReachLevel.VERIFIED``) would let a proven answer inherit
|
||||||
|
an "approximate" surface, or an approximation inherit a "verified" badge. Keeping the
|
||||||
|
axes distinct is the architectural commitment behind the Stage-2 lockfile
|
||||||
|
(``docs/analysis/stage2-epistemic-disclosure-bus-verified-v1-scoping-2026-06-08.md`` §0).
|
||||||
|
|
||||||
|
**Discipline — no claim without a producer** (the spine enforces on itself what it
|
||||||
|
enforces on answers). Every member has a real or imminent emitter:
|
||||||
|
|
||||||
|
* ``NONE`` — every response today (the baseline; STRICT + NONE).
|
||||||
|
* ``APPROXIMATE`` — active: the cognition Step-E disclosed estimate
|
||||||
|
(``estimation_enabled`` / ADR-0206 §5).
|
||||||
|
* ``PROPOSAL_ONLY`` — active: ``teaching/proposals`` emits review-only proposals.
|
||||||
|
* ``VERIFIED`` — the imminent frontier: its producer is Phase 1 (P1-A..),
|
||||||
|
declared because it is the v1 target the bus is built around.
|
||||||
|
|
||||||
|
Two claims are intentionally ABSENT, because nothing can emit them — and the spine
|
||||||
|
will not declare a label it cannot earn:
|
||||||
|
|
||||||
|
* ``PROVEN`` — a claim stronger than VERIFIED; no plan to build a producer.
|
||||||
|
* ``ESTIMATED`` — a *future* split of ``APPROXIMATE`` into a distinct
|
||||||
|
numeric-estimate claim, added ONLY once a real estimator producer
|
||||||
|
exists. Until then the cognition estimate is ``APPROXIMATE``.
|
||||||
|
|
||||||
|
P0-2 ships ONLY the axis + its default. No bus behaviour, no mapping to a disposition
|
||||||
|
(that is P0-3 / ServedDisposition). Off-serving: this module imports nothing.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from enum import Enum, unique
|
||||||
|
|
||||||
|
|
||||||
|
@unique
|
||||||
|
class DisclosureClaim(str, Enum):
|
||||||
|
"""The epistemic claim a served surface makes about its own truth status.
|
||||||
|
|
||||||
|
Orthogonal to ``ReachLevel``. ``str``-valued for stable serialization into
|
||||||
|
telemetry / disposition records (the same convention as ``EpistemicState``).
|
||||||
|
"""
|
||||||
|
|
||||||
|
NONE = "none" # no epistemic claim beyond the plain surface (the default)
|
||||||
|
VERIFIED = "verified" # independently confirmed under a canonical-comparison contract
|
||||||
|
APPROXIMATE = "approximate" # a disclosed best-estimate from incomplete evidence
|
||||||
|
PROPOSAL_ONLY = "proposal_only" # offered as a proposal for review, not asserted
|
||||||
|
|
||||||
|
|
||||||
|
#: The default claim: a surface asserts nothing about its truth status unless a
|
||||||
|
#: producer upgrades it. ``STRICT`` reach + ``NONE`` claim is today's every-response
|
||||||
|
#: baseline.
|
||||||
|
DEFAULT_DISCLOSURE_CLAIM: DisclosureClaim = DisclosureClaim.NONE
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["DEFAULT_DISCLOSURE_CLAIM", "DisclosureClaim"]
|
||||||
|
|
@ -89,7 +89,7 @@ class LimitationAssessment:
|
||||||
#: ``SCOPE_BOUNDARY`` state with a real producer; ``contradiction`` / ``ambiguous``
|
#: ``SCOPE_BOUNDARY`` state with a real producer; ``contradiction`` / ``ambiguous``
|
||||||
#: reuse the ACTIVE states. The refuse/propose kinds share ``UNDETERMINED`` — the
|
#: reuse the ACTIVE states. The refuse/propose kinds share ``UNDETERMINED`` — the
|
||||||
#: *kind* carries the distinction, the *state* only says "no answer determined".
|
#: *kind* carries the distinction, the *state* only says "no answer determined".
|
||||||
_KIND_TO_STATE: dict[str, EpistemicState] = {
|
_KIND_TO_STATE: dict[LimitationKind, EpistemicState] = {
|
||||||
"missing_information": EpistemicState.UNDETERMINED,
|
"missing_information": EpistemicState.UNDETERMINED,
|
||||||
"ambiguous_structure": EpistemicState.AMBIGUOUS,
|
"ambiguous_structure": EpistemicState.AMBIGUOUS,
|
||||||
"scope_boundary": EpistemicState.SCOPE_BOUNDARY,
|
"scope_boundary": EpistemicState.SCOPE_BOUNDARY,
|
||||||
|
|
@ -104,7 +104,7 @@ _KIND_TO_STATE: dict[str, EpistemicState] = {
|
||||||
#: is ``None`` — it is the ONE action the spine adds that has no terminal yet (Q1/ASK).
|
#: is ``None`` — it is the ONE action the spine adds that has no terminal yet (Q1/ASK).
|
||||||
#: This map is the proof that the limitation pass is a consolidating view, not a new
|
#: This map is the proof that the limitation pass is a consolidating view, not a new
|
||||||
#: universe: five of six actions already exist as terminals.
|
#: universe: five of six actions already exist as terminals.
|
||||||
_ACTION_TO_TERMINAL: dict[str, Terminal | None] = {
|
_ACTION_TO_TERMINAL: dict[ResolutionAction, Terminal | None] = {
|
||||||
"answer": Terminal.SOLVED_VERIFIED,
|
"answer": Terminal.SOLVED_VERIFIED,
|
||||||
"emit_proposal": Terminal.PROPOSAL_EMITTED,
|
"emit_proposal": Terminal.PROPOSAL_EMITTED,
|
||||||
"refuse_known_boundary": Terminal.REFUSED_KNOWN_BOUNDARY,
|
"refuse_known_boundary": Terminal.REFUSED_KNOWN_BOUNDARY,
|
||||||
|
|
|
||||||
94
tests/test_disclosure_claim.py
Normal file
94
tests/test_disclosure_claim.py
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
"""P0-2 — tests for the DisclosureClaim axis.
|
||||||
|
|
||||||
|
Each test guards a specific confusion: that ``verified`` is a reach level, that the
|
||||||
|
claim axis is entangled with the resolution-action axis, that a value with no
|
||||||
|
producer (``PROVEN``) crept in, or that the default is anything but ``NONE``. None
|
||||||
|
passes under the confusion it is written to catch.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import ast
|
||||||
|
import pathlib
|
||||||
|
from enum import Enum
|
||||||
|
from typing import get_args
|
||||||
|
|
||||||
|
import core.epistemic_disclosure.disclosure_claim as claim_module
|
||||||
|
from core.epistemic_disclosure.disclosure_claim import (
|
||||||
|
DEFAULT_DISCLOSURE_CLAIM,
|
||||||
|
DisclosureClaim,
|
||||||
|
)
|
||||||
|
from core.epistemic_disclosure.limitation import ResolutionAction
|
||||||
|
from core.response_governance.policy import ReachLevel
|
||||||
|
|
||||||
|
|
||||||
|
def test_default_claim_is_none():
|
||||||
|
assert DEFAULT_DISCLOSURE_CLAIM is DisclosureClaim.NONE
|
||||||
|
|
||||||
|
|
||||||
|
def test_verified_is_not_a_reach_level():
|
||||||
|
"""The locked Stage-2 decision: VERIFIED is an epistemic claim, never a reach."""
|
||||||
|
assert not any(level.name == "VERIFIED" for level in ReachLevel)
|
||||||
|
assert "verified" not in {level.value for level in ReachLevel}
|
||||||
|
assert not isinstance(DisclosureClaim.VERIFIED, ReachLevel)
|
||||||
|
assert DisclosureClaim is not ReachLevel
|
||||||
|
|
||||||
|
|
||||||
|
def test_disclosure_claim_independent_of_resolution_action():
|
||||||
|
"""Orthogonal axes: disjoint value spaces (knowing the action doesn't fix the claim)."""
|
||||||
|
claim_values = {c.value for c in DisclosureClaim}
|
||||||
|
action_values = set(get_args(ResolutionAction))
|
||||||
|
assert claim_values.isdisjoint(action_values)
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_proven_claim_without_a_producer():
|
||||||
|
"""Discipline: the spine enforces on itself what it enforces on answers."""
|
||||||
|
assert not hasattr(DisclosureClaim, "PROVEN")
|
||||||
|
|
||||||
|
|
||||||
|
def test_members_are_exactly_the_approved_four():
|
||||||
|
# Four claims, each with a real or imminent producer. ESTIMATED and PROVEN are
|
||||||
|
# intentionally absent (no producer → no declared label).
|
||||||
|
assert {c.name for c in DisclosureClaim} == {
|
||||||
|
"NONE",
|
||||||
|
"VERIFIED",
|
||||||
|
"APPROXIMATE",
|
||||||
|
"PROPOSAL_ONLY",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_estimated_absent_no_claim_without_a_producer():
|
||||||
|
# ESTIMATED is a future split of APPROXIMATE; not declared until a producer exists.
|
||||||
|
assert not hasattr(DisclosureClaim, "ESTIMATED")
|
||||||
|
|
||||||
|
|
||||||
|
def test_values_serialize_stably_as_snake_case():
|
||||||
|
for c in DisclosureClaim:
|
||||||
|
assert c.value == c.value.lower()
|
||||||
|
assert " " not in c.value
|
||||||
|
assert "-" not in c.value
|
||||||
|
|
||||||
|
|
||||||
|
def test_is_str_enum_for_stable_serialization():
|
||||||
|
assert issubclass(DisclosureClaim, str)
|
||||||
|
assert issubclass(DisclosureClaim, Enum)
|
||||||
|
# str-valued: a member compares/serializes as its value
|
||||||
|
assert DisclosureClaim.VERIFIED == "verified"
|
||||||
|
|
||||||
|
|
||||||
|
def test_module_is_off_serving_and_dependency_free():
|
||||||
|
"""P0-2 is the bare axis: it imports nothing, so it trivially cannot touch serving."""
|
||||||
|
module_path = claim_module.__file__
|
||||||
|
assert module_path is not None
|
||||||
|
src = pathlib.Path(module_path).read_text()
|
||||||
|
tree = ast.parse(src)
|
||||||
|
from_imports = [n.module or "" for n in ast.walk(tree) if isinstance(n, ast.ImportFrom)]
|
||||||
|
plain_imports = [a.name for n in ast.walk(tree) if isinstance(n, ast.Import) for a in n.names]
|
||||||
|
all_modules = from_imports + plain_imports
|
||||||
|
forbidden = [
|
||||||
|
m for m in all_modules
|
||||||
|
if "generate.derivation" in m or "reliability_gate" in m or m.endswith("verify")
|
||||||
|
]
|
||||||
|
assert forbidden == []
|
||||||
|
# the only import is the stdlib enum — no project coupling at all (P0-2 is the bare axis)
|
||||||
|
assert all(m in {"", "__future__", "enum"} for m in all_modules), all_modules
|
||||||
Loading…
Reference in a new issue