The intake gate of the Epistemic Disclosure spine: classify WHAT KIND of limitation blocks resolution before choosing a served disposition. Asking is one resolution among six; mis-classifying breaks intake (refuse-when-should-ask loses the unlocking datum; ask-when-should-propose wastes the channel). New off-serving package core/epistemic_disclosure/: - LimitationKind (8) / ResolutionAction (6) / LimitationAssessment - the mapping DERIVED from the shipped failure_family REGISTRY + contemplation Terminal set — a consolidating VIEW, not a fourth taxonomy. The only genuinely new action is ask_question (the one action with no shipped terminal); every other action maps onto an existing Terminal (terminal_for_action proves it). - assess_from_family / assess_from_attempt; conservative refuse default (an unclassified reason NEVER becomes an answerable question — wrong=0-safe). The missing_total_count / missing_weighted_total reclassification (capability_gap -> missing_information) is DEFERRED to Q1-B with its own tests, not made here; PENDING_Q1B_RECLASSIFICATION + a tripwire test document it. 27 tests, all non-vacuous (each fails under a single mis-keyed entry): totality over REGISTRY, flag-consistency invariants (proposal<->propose; contradiction verdict reports; foreign text steps aside), the wrong=0 invariant (hard_boundary never asks), load-bearing specific mappings, the consolidation proof, and the attempt-level paths. Off-serving: imports no generate.derivation / core.reliability_gate (AST-checked); cannot move the sealed GSM8K metric. Nothing consumes resolution_action yet. Smoke green (90 passed) locally.
212 lines
8.6 KiB
Python
212 lines
8.6 KiB
Python
"""P0-1 — exhaustive tests for the limitation-assessment consolidating view.
|
|
|
|
These prove the mapping is (a) TOTAL over the shipped failure-family registry,
|
|
(b) CONSISTENT with the registry flags, and (c) NON-VACUOUSLY correct on the
|
|
wrong=0-relevant cases: a hard boundary never becomes an answerable question, a
|
|
proposal stays a proposal, a contradiction reports, foreign text steps aside. Each
|
|
test would fail under a single mis-keyed mapping entry — none passes under a broken
|
|
impl (CLAUDE.md "Schema-Defined Proof Obligations").
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import get_args
|
|
|
|
import pytest
|
|
|
|
from core.comprehension_attempt.failure_family import REGISTRY, family_by_name
|
|
from core.comprehension_attempt.model import ComprehensionAttempt
|
|
from core.epistemic_disclosure.limitation import (
|
|
PENDING_Q1B_RECLASSIFICATION,
|
|
LimitationKind,
|
|
ResolutionAction,
|
|
_FAMILY_TO_LIMITATION,
|
|
assess_from_attempt,
|
|
assess_from_family,
|
|
terminal_for_action,
|
|
)
|
|
from core.epistemic_state import EpistemicState
|
|
from generate.contemplation.findings import Terminal
|
|
|
|
VALID_KINDS = frozenset(get_args(LimitationKind))
|
|
VALID_ACTIONS = frozenset(get_args(ResolutionAction))
|
|
REGISTRY_NAMES = frozenset(f.name for f in REGISTRY)
|
|
|
|
|
|
# --- totality + type validity -------------------------------------------------------- #
|
|
|
|
def test_mapping_is_total_over_registry():
|
|
"""Exactly the shipped families — no extra keys, no missing ones."""
|
|
assert set(_FAMILY_TO_LIMITATION) == set(REGISTRY_NAMES)
|
|
|
|
|
|
def test_assess_from_family_produces_valid_typed_values():
|
|
for family in REGISTRY:
|
|
a = assess_from_family(family)
|
|
assert a.limitation_kind in VALID_KINDS, family.name
|
|
assert a.resolution_action in VALID_ACTIONS, family.name
|
|
assert a.blocking_reason == family.name
|
|
assert a.owner_organ == family.owner
|
|
|
|
|
|
# --- flag-consistency invariants ----------------------------------------------------- #
|
|
|
|
def test_proposal_allowed_iff_emit_proposal():
|
|
"""INV-A: the growth surfaces — and only those — propose."""
|
|
for family in REGISTRY:
|
|
a = assess_from_family(family)
|
|
assert family.proposal_allowed == (a.resolution_action == "emit_proposal"), family.name
|
|
|
|
|
|
def test_emit_proposal_implies_capability_gap():
|
|
for family in REGISTRY:
|
|
a = assess_from_family(family)
|
|
if a.resolution_action == "emit_proposal":
|
|
assert a.limitation_kind == "capability_gap", family.name
|
|
|
|
|
|
def test_report_contradiction_iff_answer_key_contradiction():
|
|
"""INV-B: the answer-key verdict is the sole reporter."""
|
|
reporters = [f.name for f in REGISTRY if assess_from_family(f).resolution_action == "report_contradiction"]
|
|
assert reporters == ["answer_key_contradiction"]
|
|
|
|
|
|
def test_step_aside_iff_input_shape():
|
|
"""INV-C: only foreign-shape text steps aside."""
|
|
for family in REGISTRY:
|
|
a = assess_from_family(family)
|
|
is_input_shape = family.name == "input_shape"
|
|
assert (a.resolution_action == "step_aside") == is_input_shape, family.name
|
|
if is_input_shape:
|
|
assert a.limitation_kind == "input_shape"
|
|
|
|
|
|
def test_hard_boundary_never_asks():
|
|
"""INV-E (wrong=0-relevant): an impossible problem is never turned into a question."""
|
|
for family in REGISTRY:
|
|
a = assess_from_family(family)
|
|
if a.limitation_kind == "hard_boundary":
|
|
assert a.resolution_action == "refuse_known_boundary", family.name
|
|
|
|
|
|
def test_ask_question_only_for_resolvable_kinds():
|
|
"""INV-D: asking is licensed only for kinds a user answer could resolve."""
|
|
askable = {"missing_information", "ambiguous_structure", "renderability_gap"}
|
|
for family in REGISTRY:
|
|
a = assess_from_family(family)
|
|
if a.resolution_action == "ask_question":
|
|
assert a.limitation_kind in askable, family.name
|
|
|
|
|
|
# --- non-vacuous, load-bearing specific mappings ------------------------------------- #
|
|
|
|
@pytest.mark.parametrize(
|
|
"name,kind,action",
|
|
[
|
|
("cmb_non_positive_net", "hard_boundary", "refuse_known_boundary"),
|
|
("cmb_non_integer", "hard_boundary", "refuse_known_boundary"),
|
|
("cmb_underdetermined", "missing_information", "ask_question"),
|
|
("cmb_combine_ambiguous", "ambiguous_structure", "ask_question"),
|
|
("rate_underdetermined", "missing_information", "ask_question"),
|
|
("answer_key_contradiction", "contradiction", "report_contradiction"),
|
|
("input_shape", "input_shape", "step_aside"),
|
|
("cmb_unsupported_reciprocal", "capability_gap", "emit_proposal"),
|
|
("unsupported_system_size", "scope_boundary", "refuse_known_boundary"),
|
|
("admissibility_incompatible", "hard_boundary", "refuse_known_boundary"),
|
|
],
|
|
)
|
|
def test_specific_load_bearing_mappings(name, kind, action):
|
|
family = family_by_name(name)
|
|
assert family is not None, name
|
|
a = assess_from_family(family)
|
|
assert (a.limitation_kind, a.resolution_action) == (kind, action)
|
|
|
|
|
|
def test_pending_reclassification_is_currently_capability_gap():
|
|
"""Documents the DEFERRED Q1-B change. Today these propose; Q1-B will flip them to
|
|
missing_information/ask. When that lands this test changes deliberately — it is the
|
|
tripwire proving the reclassification was a conscious, tested act, not a silent re-key."""
|
|
for name in PENDING_Q1B_RECLASSIFICATION:
|
|
family = family_by_name(name)
|
|
assert family is not None, name
|
|
a = assess_from_family(family)
|
|
assert (a.limitation_kind, a.resolution_action) == ("capability_gap", "emit_proposal"), name
|
|
assert PENDING_Q1B_RECLASSIFICATION == {"missing_total_count", "missing_weighted_total"}
|
|
|
|
|
|
# --- the epistemic-state axis -------------------------------------------------------- #
|
|
|
|
def test_epistemic_state_axis():
|
|
by_name = {f.name: assess_from_family(f) for f in REGISTRY}
|
|
assert by_name["unsupported_system_size"].epistemic_state == EpistemicState.SCOPE_BOUNDARY
|
|
assert by_name["answer_key_contradiction"].epistemic_state == EpistemicState.CONTRADICTED
|
|
assert by_name["cmb_combine_ambiguous"].epistemic_state == EpistemicState.AMBIGUOUS
|
|
assert by_name["cmb_underdetermined"].epistemic_state == EpistemicState.UNDETERMINED
|
|
|
|
|
|
# --- the consolidation proof: actions live on shipped terminals (except ask) --------- #
|
|
|
|
def test_actions_consolidate_onto_terminals_except_ask():
|
|
"""The proof of 'consolidating view, not a fourth taxonomy': every action maps to a
|
|
shipped Terminal except ask_question (the one genuinely new action / Q1 tenant)."""
|
|
for action in VALID_ACTIONS:
|
|
terminal = terminal_for_action(action)
|
|
if action == "ask_question":
|
|
assert terminal is None
|
|
else:
|
|
assert isinstance(terminal, Terminal), action
|
|
|
|
|
|
def test_only_ask_question_is_new():
|
|
new_actions = sorted(a for a in VALID_ACTIONS if terminal_for_action(a) is None)
|
|
assert new_actions == ["ask_question"]
|
|
|
|
|
|
# --- attempt-level classification ---------------------------------------------------- #
|
|
|
|
def test_assess_attempt_refusal_underdetermined_asks():
|
|
attempt = ComprehensionAttempt(
|
|
organ="r4_combined_rate",
|
|
outcome="setup_refused",
|
|
refusal_reason="cmb_missing_second_rate",
|
|
)
|
|
a = assess_from_attempt(attempt)
|
|
assert a is not None
|
|
assert (a.limitation_kind, a.resolution_action) == ("missing_information", "ask_question")
|
|
|
|
|
|
def test_assess_attempt_hard_boundary_refuses_not_asks():
|
|
attempt = ComprehensionAttempt(
|
|
organ="r4_combined_rate",
|
|
outcome="setup_refused",
|
|
refusal_reason="cmb_non_positive_net_rate",
|
|
)
|
|
a = assess_from_attempt(attempt)
|
|
assert a is not None
|
|
assert a.resolution_action == "refuse_known_boundary"
|
|
|
|
|
|
def test_assess_attempt_contradiction_reports():
|
|
attempt = ComprehensionAttempt(organ="r2_constraints", outcome="contradiction")
|
|
a = assess_from_attempt(attempt)
|
|
assert a is not None
|
|
assert (a.limitation_kind, a.resolution_action) == ("contradiction", "report_contradiction")
|
|
assert a.epistemic_state == EpistemicState.CONTRADICTED
|
|
|
|
|
|
def test_assess_attempt_solved_is_not_a_limitation():
|
|
attempt = ComprehensionAttempt(organ="r1_quantitative", outcome="setup_correct")
|
|
assert assess_from_attempt(attempt) is None
|
|
|
|
|
|
def test_assess_attempt_unclassified_reason_refuses_never_asks():
|
|
"""wrong=0-safe default: an unknown refusal reason refuses, never becomes a question."""
|
|
attempt = ComprehensionAttempt(
|
|
organ="r2_constraints",
|
|
outcome="setup_refused",
|
|
refusal_reason="some_unmapped_future_reason",
|
|
)
|
|
a = assess_from_attempt(attempt)
|
|
assert a is not None
|
|
assert a.resolution_action == "refuse_known_boundary"
|
|
assert a.resolution_action != "ask_question"
|