* 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
96 lines
4.3 KiB
Python
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
|