Extends derive_recognizer to detect VP variation and build a Phase 2 recognizer that lifts tense, polarity, modality, and intentionality alongside the Phase 1 agent/count/unit/relation slots. Three-layer refusal: Layer 1 (unknown VP), Layer 2 (missing count), Layer 3 (contradictory count spans). Phase 1 path preserved when all teaching examples share a single VP. 8/8 tests pass.
249 lines
12 KiB
Python
249 lines
12 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
from recognition.anti_unifier import DerivedRecognizer, derive_recognizer, recognize
|
|
from recognition.outcome import (
|
|
EVIDENCED,
|
|
UNDETERMINED,
|
|
CONTRADICTED,
|
|
EvidenceSpan,
|
|
FeatureBundle,
|
|
NegativeEvidence,
|
|
ShapeRefusal,
|
|
FeatureEvidenceRefusal,
|
|
FeatureConsistencyRefusal,
|
|
)
|
|
|
|
def _span(tokens: tuple[str, ...], start: int, end: int) -> EvidenceSpan:
|
|
return EvidenceSpan(start=start, end=end, text=" ".join(tokens[start:end]))
|
|
|
|
def _examples() -> list[tuple[tuple[str, ...], FeatureBundle]]:
|
|
# 1. "A baker has 24 loaves"
|
|
c1 = ("A", "baker", "has", "24", "loaves")
|
|
b1 = FeatureBundle.from_mapping({
|
|
"agent": ("baker", _span(c1, 1, 2)),
|
|
"count": (24, _span(c1, 3, 4)),
|
|
"unit": ("loaf", _span(c1, 4, 5)),
|
|
"relation": ("has", _span(c1, 2, 3)),
|
|
"tense": ("present", _span(c1, 2, 3)),
|
|
"polarity": ("+", NegativeEvidence(0, len(c1), "no negator present")),
|
|
"modality": ("actual", NegativeEvidence(0, len(c1), "no modal counter-marker present")),
|
|
"intentionality": ("possession", _span(c1, 1, 3)),
|
|
})
|
|
|
|
# 2. "A baker does not have 24 loaves"
|
|
c2 = ("A", "baker", "does", "not", "have", "24", "loaves")
|
|
b2 = FeatureBundle.from_mapping({
|
|
"agent": ("baker", _span(c2, 1, 2)),
|
|
"count": (24, _span(c2, 5, 6)),
|
|
"unit": ("loaf", _span(c2, 6, 7)),
|
|
"relation": ("have", _span(c2, 4, 5)),
|
|
"tense": ("present", _span(c2, 2, 3)),
|
|
"polarity": ("-", _span(c2, 3, 4)),
|
|
"modality": ("actual", NegativeEvidence(0, len(c2), "no modal counter-marker present")),
|
|
"intentionality": ("possession", _span(c2, 1, 5)),
|
|
})
|
|
|
|
# 3. "A baker may have 24 loaves"
|
|
c3 = ("A", "baker", "may", "have", "24", "loaves")
|
|
b3 = FeatureBundle.from_mapping({
|
|
"agent": ("baker", _span(c3, 1, 2)),
|
|
"count": (24, _span(c3, 4, 5)),
|
|
"unit": ("loaf", _span(c3, 5, 6)),
|
|
"relation": ("have", _span(c3, 3, 4)),
|
|
"tense": ("present", _span(c3, 2, 3)),
|
|
"polarity": ("+", NegativeEvidence(0, len(c3), "no negator present")),
|
|
"modality": ("possibility", _span(c3, 2, 3)),
|
|
"intentionality": ("possession", _span(c3, 1, 4)),
|
|
})
|
|
|
|
# 4. "A baker had 24 loaves"
|
|
c4 = ("A", "baker", "had", "24", "loaves")
|
|
b4 = FeatureBundle.from_mapping({
|
|
"agent": ("baker", _span(c4, 1, 2)),
|
|
"count": (24, _span(c4, 3, 4)),
|
|
"unit": ("loaf", _span(c4, 4, 5)),
|
|
"relation": ("had", _span(c4, 2, 3)),
|
|
"tense": ("past", _span(c4, 2, 3)),
|
|
"polarity": ("+", NegativeEvidence(0, len(c4), "no negator present")),
|
|
"modality": ("actual", NegativeEvidence(0, len(c4), "no modal counter-marker present")),
|
|
"intentionality": ("possession", _span(c4, 1, 3)),
|
|
})
|
|
|
|
# 5. "A baker will have 24 loaves"
|
|
c5 = ("A", "baker", "will", "have", "24", "loaves")
|
|
b5 = FeatureBundle.from_mapping({
|
|
"agent": ("baker", _span(c5, 1, 2)),
|
|
"count": (24, _span(c5, 4, 5)),
|
|
"unit": ("loaf", _span(c5, 5, 6)),
|
|
"relation": ("have", _span(c5, 3, 4)),
|
|
"tense": ("future", _span(c5, 2, 3)),
|
|
"polarity": ("+", NegativeEvidence(0, len(c5), "no negator present")),
|
|
"modality": ("actual", NegativeEvidence(0, len(c5), "no modal counter-marker present")),
|
|
"intentionality": ("possession", _span(c5, 1, 4)),
|
|
})
|
|
|
|
return [(c1, b1), (c2, b2), (c3, b3), (c4, b4), (c5, b5)]
|
|
|
|
def test_derive_recognizer_phase2_is_byte_identical() -> 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_cases_admitted() -> None:
|
|
recognizer = derive_recognizer(_examples())
|
|
|
|
# Case 1
|
|
o1 = recognize(recognizer, ("A", "baker", "has", "24", "loaves"))
|
|
assert o1.state == EVIDENCED
|
|
assert o1.refusal_reason is None
|
|
assert o1.proposition is not None
|
|
assert o1.proposition.get("agent").value == "baker"
|
|
assert o1.proposition.get("agent").evidence == EvidenceSpan(1, 2, "baker")
|
|
assert o1.proposition.get("count").value == 24
|
|
assert o1.proposition.get("count").evidence == EvidenceSpan(3, 4, "24")
|
|
assert o1.proposition.get("unit").value == "loaf"
|
|
assert o1.proposition.get("unit").evidence == EvidenceSpan(4, 5, "loaves")
|
|
assert o1.proposition.get("relation").value == "has"
|
|
assert o1.proposition.get("relation").evidence == EvidenceSpan(2, 3, "has")
|
|
assert o1.proposition.get("tense").value == "present"
|
|
assert o1.proposition.get("tense").evidence == EvidenceSpan(2, 3, "has")
|
|
assert o1.proposition.get("polarity").value == "+"
|
|
assert isinstance(o1.proposition.get("polarity").evidence, NegativeEvidence)
|
|
assert o1.proposition.get("modality").value == "actual"
|
|
assert isinstance(o1.proposition.get("modality").evidence, NegativeEvidence)
|
|
assert o1.proposition.get("intentionality").value == "possession"
|
|
assert o1.proposition.get("intentionality").evidence == EvidenceSpan(1, 3, "baker has")
|
|
|
|
# Case 2
|
|
o2 = recognize(recognizer, ("A", "baker", "does", "not", "have", "24", "loaves"))
|
|
assert o2.state == EVIDENCED
|
|
assert o2.refusal_reason is None
|
|
assert o2.proposition is not None
|
|
assert o2.proposition.get("agent").value == "baker"
|
|
assert o2.proposition.get("count").value == 24
|
|
assert o2.proposition.get("unit").value == "loaf"
|
|
assert o2.proposition.get("relation").value == "have"
|
|
assert o2.proposition.get("relation").evidence == EvidenceSpan(4, 5, "have")
|
|
assert o2.proposition.get("tense").value == "present"
|
|
assert o2.proposition.get("tense").evidence == EvidenceSpan(2, 3, "does")
|
|
assert o2.proposition.get("polarity").value == "-"
|
|
assert o2.proposition.get("polarity").evidence == EvidenceSpan(3, 4, "not")
|
|
assert o2.proposition.get("modality").value == "actual"
|
|
assert isinstance(o2.proposition.get("modality").evidence, NegativeEvidence)
|
|
assert o2.proposition.get("intentionality").value == "possession"
|
|
assert o2.proposition.get("intentionality").evidence == EvidenceSpan(1, 5, "baker does not have")
|
|
|
|
# Case 3
|
|
o3 = recognize(recognizer, ("A", "baker", "may", "have", "24", "loaves"))
|
|
assert o3.state == EVIDENCED
|
|
assert o3.refusal_reason is None
|
|
assert o3.proposition is not None
|
|
assert o3.proposition.get("agent").value == "baker"
|
|
assert o3.proposition.get("count").value == 24
|
|
assert o3.proposition.get("unit").value == "loaf"
|
|
assert o3.proposition.get("relation").value == "have"
|
|
assert o3.proposition.get("relation").evidence == EvidenceSpan(3, 4, "have")
|
|
assert o3.proposition.get("tense").value == "present"
|
|
assert o3.proposition.get("tense").evidence == EvidenceSpan(2, 3, "may")
|
|
assert o3.proposition.get("polarity").value == "+"
|
|
assert isinstance(o3.proposition.get("polarity").evidence, NegativeEvidence)
|
|
assert o3.proposition.get("modality").value == "possibility"
|
|
assert o3.proposition.get("modality").evidence == EvidenceSpan(2, 3, "may")
|
|
assert o3.proposition.get("intentionality").value == "possession"
|
|
assert o3.proposition.get("intentionality").evidence == EvidenceSpan(1, 4, "baker may have")
|
|
|
|
# Case 4
|
|
o4 = recognize(recognizer, ("A", "baker", "had", "24", "loaves"))
|
|
assert o4.state == EVIDENCED
|
|
assert o4.refusal_reason is None
|
|
assert o4.proposition is not None
|
|
assert o4.proposition.get("agent").value == "baker"
|
|
assert o4.proposition.get("count").value == 24
|
|
assert o4.proposition.get("unit").value == "loaf"
|
|
assert o4.proposition.get("relation").value == "had"
|
|
assert o4.proposition.get("relation").evidence == EvidenceSpan(2, 3, "had")
|
|
assert o4.proposition.get("tense").value == "past"
|
|
assert o4.proposition.get("tense").evidence == EvidenceSpan(2, 3, "had")
|
|
assert o4.proposition.get("polarity").value == "+"
|
|
assert isinstance(o4.proposition.get("polarity").evidence, NegativeEvidence)
|
|
assert o4.proposition.get("modality").value == "actual"
|
|
assert isinstance(o4.proposition.get("modality").evidence, NegativeEvidence)
|
|
assert o4.proposition.get("intentionality").value == "possession"
|
|
assert o4.proposition.get("intentionality").evidence == EvidenceSpan(1, 3, "baker had")
|
|
|
|
# Case 5
|
|
o5 = recognize(recognizer, ("A", "baker", "will", "have", "24", "loaves"))
|
|
assert o5.state == EVIDENCED
|
|
assert o5.refusal_reason is None
|
|
assert o5.proposition is not None
|
|
assert o5.proposition.get("agent").value == "baker"
|
|
assert o5.proposition.get("count").value == 24
|
|
assert o5.proposition.get("unit").value == "loaf"
|
|
assert o5.proposition.get("relation").value == "have"
|
|
assert o5.proposition.get("relation").evidence == EvidenceSpan(3, 4, "have")
|
|
assert o5.proposition.get("tense").value == "future"
|
|
assert o5.proposition.get("tense").evidence == EvidenceSpan(2, 3, "will")
|
|
assert o5.proposition.get("polarity").value == "+"
|
|
assert isinstance(o5.proposition.get("polarity").evidence, NegativeEvidence)
|
|
assert o5.proposition.get("modality").value == "actual"
|
|
assert isinstance(o5.proposition.get("modality").evidence, NegativeEvidence)
|
|
assert o5.proposition.get("intentionality").value == "possession"
|
|
assert o5.proposition.get("intentionality").evidence == EvidenceSpan(1, 4, "baker will have")
|
|
|
|
def test_adversarial_refusals() -> None:
|
|
recognizer = derive_recognizer(_examples())
|
|
|
|
# Case 6: "John gave 5 apples to Mary" -> Layer 1 ShapeRefusal (wrong relation)
|
|
o6 = recognize(recognizer, ("John", "gave", "5", "apples", "to", "Mary"))
|
|
assert o6.state == UNDETERMINED
|
|
assert o6.proposition is None
|
|
assert isinstance(o6.refusal_reason, ShapeRefusal)
|
|
|
|
# Case 7: "A baker has loaves" -> Layer 2 FeatureEvidenceRefusal (missing count)
|
|
o7 = recognize(recognizer, ("A", "baker", "has", "loaves"))
|
|
assert o7.state == UNDETERMINED
|
|
assert o7.proposition is None
|
|
assert isinstance(o7.refusal_reason, FeatureEvidenceRefusal)
|
|
assert o7.refusal_reason.missing_feature == "count"
|
|
|
|
# Case 8: "A baker has 24 loaves and 12 loaves" -> Layer 3 FeatureConsistencyRefusal (count contradiction)
|
|
o8 = recognize(recognizer, ("A", "baker", "has", "24", "loaves", "and", "12", "loaves"))
|
|
assert o8.state == CONTRADICTED
|
|
assert o8.proposition is None
|
|
assert isinstance(o8.refusal_reason, FeatureConsistencyRefusal)
|
|
assert o8.refusal_reason.feature == "count"
|
|
assert len(o8.refusal_reason.spans) == 2
|
|
assert o8.refusal_reason.spans[0] == EvidenceSpan(3, 4, "24")
|
|
assert o8.refusal_reason.spans[1] == EvidenceSpan(6, 7, "12")
|
|
|
|
def test_byte_identity_across_runs() -> None:
|
|
recognizer = derive_recognizer(_examples())
|
|
cases = [
|
|
("A", "baker", "has", "24", "loaves"),
|
|
("A", "baker", "does", "not", "have", "24", "loaves"),
|
|
("A", "baker", "may", "have", "24", "loaves"),
|
|
("A", "baker", "had", "24", "loaves"),
|
|
("A", "baker", "will", "have", "24", "loaves"),
|
|
("John", "gave", "5", "apples", "to", "Mary"),
|
|
("A", "baker", "has", "loaves"),
|
|
("A", "baker", "has", "24", "loaves", "and", "12", "loaves"),
|
|
]
|
|
|
|
for case in cases:
|
|
out1 = recognize(recognizer, case)
|
|
out2 = recognize(recognizer, case)
|
|
assert out1 == out2
|
|
|
|
# Serialize and deserialize to ensure exact identical JSON payload
|
|
d1 = out1.as_dict()
|
|
d2 = out2.as_dict()
|
|
assert d1 == d2
|
|
|
|
j1 = json.dumps(d1, sort_keys=True, separators=(",", ":"))
|
|
j2 = json.dumps(d2, sort_keys=True, separators=(",", ":"))
|
|
assert j1 == j2
|