core/tests/test_recognition_phase1.py
Shay a2980bdca2
[codex] Recognition anti-unifier Phase 1 (#224)
* feat(epistemic): populate normative_detail on TurnEvent and ChatResponse

Adds normative_detail_from_verdicts() to core.epistemic_state and wires
it into both the stub and main ChatResponse/TurnEvent construction sites.
The field carries a sorted comma-separated list of violated boundary or
commitment IDs when normative clearance is VIOLATED or SUPPRESSED; empty
string otherwise.

* docs(ADR-0142): ratify epistemic state taxonomy — 14-state vocabulary + normative clearance axis

Formalises the six-subsystem Framing 1 audit findings into a first-class
decision. Accepts the 14-state taxonomy and companion 4-value normative
clearance axis. Documents Phase 3 deliverables already landed and defers
structured provenance + cross-subsystem transition machinery to ADR-0144.

* feat(recognition): output contract + ADR-0143

Adds recognition/outcome.py: RecognitionOutcome, FeatureBundle,
BoundFeature, EvidenceSpan, NegativeEvidence, the three typed refusal
classes (ShapeRefusal, FeatureEvidenceRefusal, FeatureConsistencyRefusal),
and RecognitionProvenance. Frozen dataclasses, JSON-serializable,
byte-deterministic invariants enforced in __post_init__.

ADR-0143 commits to Mechanism D (multi-resolution anti-unification over
token sequences) and defines the two-phase acceptance test.

* feat(recognition): derive phase1 anti-unifier
2026-05-24 12:37:38 -07:00

96 lines
4.3 KiB
Python

from __future__ import annotations
import json
from recognition.anti_unifier import DerivedRecognizer, derive_recognizer, recognize
from recognition.outcome import (
EVIDENCED,
UNDETERMINED,
EvidenceSpan,
FeatureBundle,
NegativeEvidence,
ShapeRefusal,
)
def _span(tokens: tuple[str, ...], start: int, end: int) -> EvidenceSpan:
return EvidenceSpan(start=start, end=end, text=" ".join(tokens[start:end]))
def _bundle(tokens: tuple[str, ...], agent_span: tuple[int, int], count_span: tuple[int, int], unit_span: tuple[int, int], agent: str, count: int, unit: str) -> FeatureBundle:
return FeatureBundle.from_mapping(
{
"agent": (agent, _span(tokens, *agent_span)),
"count": (count, _span(tokens, *count_span)),
"intentionality": ("possession", _span(tokens, 1 if tokens[0] in {"A", "The"} else 0, 3 if tokens[0] in {"A", "The"} else 2)),
"modality": ("actual", NegativeEvidence(0, len(tokens), "no modal counter-marker present")),
"polarity": ("+", NegativeEvidence(0, len(tokens), "no negator present")),
"relation": ("has", _span(tokens, count_span[0] - 1, count_span[0])),
"tense": ("present", _span(tokens, count_span[0] - 1, count_span[0])),
"unit": (unit, _span(tokens, *unit_span)),
}
)
def _examples() -> list[tuple[tuple[str, ...], FeatureBundle]]:
john = ("John", "has", "5", "apples")
mary = ("Mary", "has", "3", "books")
school = ("A", "school", "has", "100", "students")
library = ("The", "library", "has", "12", "chairs")
return [
(john, _bundle(john, (0, 1), (2, 3), (3, 4), "John", 5, "apple")),
(mary, _bundle(mary, (0, 1), (2, 3), (3, 4), "Mary", 3, "book")),
(school, _bundle(school, (1, 2), (3, 4), (4, 5), "school", 100, "student")),
(library, _bundle(library, (1, 2), (3, 4), (4, 5), "library", 12, "chair")),
]
def test_derive_recognizer_is_byte_identical_for_same_teaching_input() -> None:
first = derive_recognizer(_examples())
second = derive_recognizer(_examples())
assert first == second
assert first.to_json() == second.to_json()
assert DerivedRecognizer.from_json(first.to_json()) == first
assert json.dumps(json.loads(first.to_json()), sort_keys=True, separators=(",", ":")) == first.to_json()
def test_positive_heldout_is_admitted_with_full_feature_bundle_and_evidence_spans() -> None:
recognizer = derive_recognizer(_examples())
outcome = recognize(recognizer, ("A", "baker", "has", "24", "loaves"))
assert outcome.state == EVIDENCED
assert outcome.refusal_reason is None
assert outcome.proposition is not None
assert outcome.proposition.get("agent").value == "baker" # type: ignore[union-attr]
assert outcome.proposition.get("relation").value == "has" # type: ignore[union-attr]
assert outcome.proposition.get("count").value == 24 # type: ignore[union-attr]
assert outcome.proposition.get("unit").value == "loaf" # type: ignore[union-attr]
assert outcome.proposition.get("polarity").value == "+" # type: ignore[union-attr]
assert outcome.proposition.get("modality").value == "actual" # type: ignore[union-attr]
assert outcome.proposition.get("tense").value == "present" # type: ignore[union-attr]
assert outcome.proposition.get("intentionality").value == "possession" # type: ignore[union-attr]
assert outcome.proposition.get("agent").evidence == EvidenceSpan(1, 2, "baker") # type: ignore[union-attr]
assert outcome.proposition.get("count").evidence == EvidenceSpan(3, 4, "24") # type: ignore[union-attr]
assert outcome.proposition.get("unit").evidence == EvidenceSpan(4, 5, "loaves") # type: ignore[union-attr]
def test_negative_heldout_is_undetermined_with_shape_refusal() -> None:
recognizer = derive_recognizer(_examples())
outcome = recognize(recognizer, ("John", "gave", "5", "apples", "to", "Mary"))
assert outcome.state == UNDETERMINED
assert outcome.proposition is None
assert isinstance(outcome.refusal_reason, ShapeRefusal)
def test_every_feature_in_admitted_bundle_has_non_none_evidence() -> None:
recognizer = derive_recognizer(_examples())
outcome = recognize(recognizer, ("A", "baker", "has", "24", "loaves"))
assert outcome.proposition is not None
for feature in outcome.proposition.features:
assert feature.evidence is not None