fix(roundtrip): multi-word units ground when every component appears in source
_unit_grounds() previously refused multi-word units like 'Pokemon cards' even when both component words appeared as tokens in the source span. The function checked unit_token against the haystack as a single key, but the tokenizer splits source into per-word tokens — 'Pokemon cards' was never going to match. Fix is conjunctive by design: every component word must appear in the haystack. A missing component refuses, preserving wrong=0. Truth-test: case 0023 (Nicole/Pokemon cards) previously refused with 'recognizer matched but produced no injection' on its first sentence. After this fix, sentence 1 passes injection cleanly; the case now refuses on sentence 2 (Cindy/Rex compositional clause) — a more honest refusal reason that reflects the actual remaining gap. Score unchanged at 3/47/0 (no overall lift; correctness win). smoke 67/67, packs 141/141, lanes 8/8 all green.
This commit is contained in:
parent
58294c7722
commit
86d4e98d5c
2 changed files with 49 additions and 0 deletions
|
|
@ -293,6 +293,13 @@ def _unit_grounds(
|
|||
if _token_in(unit_token, haystack_tokens):
|
||||
return True
|
||||
lower = unit_token.lower()
|
||||
# Multi-word units (e.g. "Pokemon cards", "stop signs") ground when
|
||||
# every component appears as a word token in source. Conjunctive by
|
||||
# design — a missing component means the unit cannot be reconstructed
|
||||
# from the source, which preserves wrong=0.
|
||||
parts = lower.split()
|
||||
if len(parts) > 1 and all(p in haystack_tokens for p in parts):
|
||||
return True
|
||||
if lower in ("cent", "cents"):
|
||||
if "$" in source_span or "¢" in source_span:
|
||||
return True
|
||||
|
|
|
|||
|
|
@ -338,6 +338,48 @@ class TestNumberGrounding:
|
|||
assert roundtrip_admissible(c)
|
||||
|
||||
|
||||
class TestMultiWordUnitGrounding:
|
||||
"""Multi-word units (e.g. 'Pokemon cards') ground when every
|
||||
component word appears in source. Conjunctive — missing one
|
||||
component refuses, preserving wrong=0."""
|
||||
|
||||
def test_two_word_unit_grounds_when_both_components_present(self) -> None:
|
||||
c = CandidateOperation(
|
||||
op=Operation(actor="Nicole", kind="add",
|
||||
operand=Quantity(value=400, unit="Pokemon cards")),
|
||||
source_span="Nicole collected 400 Pokemon cards.",
|
||||
matched_verb="collected",
|
||||
matched_value_token="400",
|
||||
matched_unit_token="Pokemon cards",
|
||||
matched_actor_token="Nicole",
|
||||
)
|
||||
assert roundtrip_admissible(c)
|
||||
|
||||
def test_two_word_unit_refuses_when_one_component_missing(self) -> None:
|
||||
c = CandidateOperation(
|
||||
op=Operation(actor="Nicole", kind="add",
|
||||
operand=Quantity(value=400, unit="Pokemon cards")),
|
||||
source_span="Nicole collected 400 cards.", # 'Pokemon' missing
|
||||
matched_verb="collected",
|
||||
matched_value_token="400",
|
||||
matched_unit_token="Pokemon cards",
|
||||
matched_actor_token="Nicole",
|
||||
)
|
||||
assert not roundtrip_admissible(c)
|
||||
|
||||
def test_single_word_unit_unaffected(self) -> None:
|
||||
c = CandidateOperation(
|
||||
op=Operation(actor="Sam", kind="add",
|
||||
operand=Quantity(value=3, unit="apples")),
|
||||
source_span="Sam buys 3 apples.",
|
||||
matched_verb="buys",
|
||||
matched_value_token="3",
|
||||
matched_unit_token="apples",
|
||||
matched_actor_token="Sam",
|
||||
)
|
||||
assert roundtrip_admissible(c)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Constructor validation — illegal CandidateOperation states are
|
||||
# refused at construction (not at filter time).
|
||||
|
|
|
|||
Loading…
Reference in a new issue