fix(kernel): complete unary-delta conformance alignment
This commit is contained in:
parent
61964c5cd4
commit
59a919e347
4 changed files with 133 additions and 55 deletions
|
|
@ -516,11 +516,19 @@ _UNARY_DELTA_FAMILY = ConstructionFamily(
|
||||||
"unary_delta_relation_ambiguous",
|
"unary_delta_relation_ambiguous",
|
||||||
"action_cue_unbound",
|
"action_cue_unbound",
|
||||||
"delta_quantity_unbound",
|
"delta_quantity_unbound",
|
||||||
|
"delta_quantity_ambiguous",
|
||||||
"changed_object_unbound",
|
"changed_object_unbound",
|
||||||
|
"changed_object_ambiguous",
|
||||||
|
"local_binding_relation_unbound",
|
||||||
"direction_unbound",
|
"direction_unbound",
|
||||||
"quantity_kind_unresolved",
|
"quantity_kind_unresolved",
|
||||||
|
"unit_object_conflict",
|
||||||
"provenance_span_inexact",
|
"provenance_span_inexact",
|
||||||
"quantity_entity_nonlocal",
|
"quantity_entity_nonlocal",
|
||||||
|
"pronoun_antecedent_unresolved",
|
||||||
|
"event_assertion_unlicensed",
|
||||||
|
"passive_voice_unsupported",
|
||||||
|
"multiple_actor_surface",
|
||||||
),
|
),
|
||||||
diagnostic_only=True,
|
diagnostic_only=True,
|
||||||
serving_allowed=False,
|
serving_allowed=False,
|
||||||
|
|
|
||||||
|
|
@ -139,10 +139,14 @@ class GroundedUnaryDeltaCue:
|
||||||
span: SourceSpan
|
span: SourceSpan
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
if self.action_kind not in {"gain", "loss"}:
|
if self.surface not in {"gained", "lost"}:
|
||||||
raise ValueError(f"invalid action_kind: {self.action_kind!r}")
|
raise ValueError(f"invalid surface: {self.surface!r}")
|
||||||
if self.direction not in {"increase", "decrease"}:
|
if self.surface == "gained":
|
||||||
raise ValueError(f"invalid direction: {self.direction!r}")
|
if self.action_kind != "gain" or self.direction != "increase":
|
||||||
|
raise ValueError("invalid gained triple")
|
||||||
|
elif self.surface == "lost":
|
||||||
|
if self.action_kind != "loss" or self.direction != "decrease":
|
||||||
|
raise ValueError("invalid lost triple")
|
||||||
if self.surface != self.span.text:
|
if self.surface != self.span.text:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"surface {self.surface!r} must match span text {self.span.text!r}"
|
f"surface {self.surface!r} must match span text {self.span.text!r}"
|
||||||
|
|
@ -276,6 +280,14 @@ class ProblemFrameBuilder:
|
||||||
def add_unary_delta_cue(self, cue: GroundedUnaryDeltaCue) -> None:
|
def add_unary_delta_cue(self, cue: GroundedUnaryDeltaCue) -> None:
|
||||||
self._unary_delta_cues.append(cue)
|
self._unary_delta_cues.append(cue)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unary_delta_cue_count(self) -> int:
|
||||||
|
return len(self._unary_delta_cues)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unary_delta_cues(self) -> tuple[GroundedUnaryDeltaCue, ...]:
|
||||||
|
return tuple(self._unary_delta_cues)
|
||||||
|
|
||||||
def build(self) -> ProblemFrame:
|
def build(self) -> ProblemFrame:
|
||||||
"""Produce the immutable ProblemFrame."""
|
"""Produce the immutable ProblemFrame."""
|
||||||
return ProblemFrame(
|
return ProblemFrame(
|
||||||
|
|
|
||||||
|
|
@ -363,12 +363,7 @@ _DECREASE_DELTA_QUESTION_RE = re.compile(
|
||||||
r"\bwhat\s+will\s+the\s+(?P<entity>[A-Za-z][A-Za-z'-]*)\s+decrease\s+by\??",
|
r"\bwhat\s+will\s+the\s+(?P<entity>[A-Za-z][A-Za-z'-]*)\s+decrease\s+by\??",
|
||||||
re.IGNORECASE,
|
re.IGNORECASE,
|
||||||
)
|
)
|
||||||
_UNARY_DELTA_RE = re.compile(
|
|
||||||
r"\b(?P<subject>(?:[A-Z][A-Za-z'-]*|[Tt]he\s+[A-Za-z][A-Za-z'-]*))\s+"
|
|
||||||
r"(?P<cue>gained|lost)\s+"
|
|
||||||
r"(?P<quantity>\d+(?:\.\d+)?)\s+"
|
|
||||||
r"(?P<object>[A-Za-z][A-Za-z'-]*)\b"
|
|
||||||
)
|
|
||||||
_ACTOR_VERB_RE = re.compile(
|
_ACTOR_VERB_RE = re.compile(
|
||||||
r"\b(?P<actor>[A-Z][A-Za-z'-]*)\s+"
|
r"\b(?P<actor>[A-Z][A-Za-z'-]*)\s+"
|
||||||
r"(?:gave|gives|give|received|receives|spent|spends|ate|eats|bought|buys|sold|sells)\b"
|
r"(?:gave|gives|give|received|receives|spent|spends|ate|eats|bought|buys|sold|sells)\b"
|
||||||
|
|
@ -546,14 +541,17 @@ def _unary_delta_proposals(
|
||||||
"per",
|
"per",
|
||||||
"each",
|
"each",
|
||||||
"ratio",
|
"ratio",
|
||||||
|
"than",
|
||||||
"more than",
|
"more than",
|
||||||
"less than",
|
"less than",
|
||||||
"fewer than",
|
"fewer than",
|
||||||
"greater than",
|
"greater than",
|
||||||
"times as",
|
"times as",
|
||||||
}
|
}
|
||||||
if any(c in text.lower() for c in confusers):
|
for c in confusers:
|
||||||
return ()
|
pattern = rf"\b{re.escape(c)}\b" if c[0].isalnum() and c[-1].isalnum() else re.escape(c)
|
||||||
|
if re.search(pattern, text, re.IGNORECASE):
|
||||||
|
return ()
|
||||||
|
|
||||||
# Transfer / transaction verbs
|
# Transfer / transaction verbs
|
||||||
transfer_verbs = {
|
transfer_verbs = {
|
||||||
|
|
@ -595,7 +593,10 @@ def _unary_delta_proposals(
|
||||||
return ()
|
return ()
|
||||||
|
|
||||||
# List coordination / enumeration
|
# List coordination / enumeration
|
||||||
if any(coord in text.lower() for coord in {"and", "or", ","}):
|
for coord in {"and", "or"}:
|
||||||
|
if re.search(rf"\b{coord}\b", text, re.IGNORECASE):
|
||||||
|
return ()
|
||||||
|
if "," in text:
|
||||||
return ()
|
return ()
|
||||||
|
|
||||||
evidence = SourceSpan(
|
evidence = SourceSpan(
|
||||||
|
|
@ -809,6 +810,7 @@ def _bound_relations(
|
||||||
mentions: tuple[GroundedMention, ...],
|
mentions: tuple[GroundedMention, ...],
|
||||||
bindings: tuple[MentionBinding, ...],
|
bindings: tuple[MentionBinding, ...],
|
||||||
proposals: tuple[ConstructionProposal, ...],
|
proposals: tuple[ConstructionProposal, ...],
|
||||||
|
unary_delta_cues: tuple[GroundedUnaryDeltaCue, ...],
|
||||||
) -> tuple[BoundRelation, ...]:
|
) -> tuple[BoundRelation, ...]:
|
||||||
by_id = {m.mention_id: m for m in mentions}
|
by_id = {m.mention_id: m for m in mentions}
|
||||||
relations: list[BoundRelation] = []
|
relations: list[BoundRelation] = []
|
||||||
|
|
@ -945,48 +947,52 @@ def _bound_relations(
|
||||||
if cue_span.text == cue_surface and cue_surface in {"gained", "lost"}:
|
if cue_span.text == cue_surface and cue_surface in {"gained", "lost"}:
|
||||||
direction = "increase" if cue_surface == "gained" else "decrease"
|
direction = "increase" if cue_surface == "gained" else "decrease"
|
||||||
# Locate corresponding GroundedUnaryDeltaCue's cue_id
|
# Locate corresponding GroundedUnaryDeltaCue's cue_id
|
||||||
cue_id = "cue-0000"
|
cue_id = None
|
||||||
|
for cue in unary_delta_cues:
|
||||||
matching_bindings = []
|
if cue.span.start == cue_span.start and cue.span.end == cue_span.end:
|
||||||
for binding in quantity_entity:
|
cue_id = cue.cue_id
|
||||||
qty = by_id.get(binding.source_mention_id)
|
break
|
||||||
obj = by_id.get(binding.target_mention_id)
|
if cue_id is not None:
|
||||||
if qty is not None and obj is not None:
|
matching_bindings = []
|
||||||
if (
|
for binding in quantity_entity:
|
||||||
cue_span.end <= qty.span.start
|
qty = by_id.get(binding.source_mention_id)
|
||||||
and qty.span.end <= obj.span.start
|
obj = by_id.get(binding.target_mention_id)
|
||||||
):
|
if qty is not None and obj is not None:
|
||||||
segment = text[cue_span.start : obj.span.end]
|
if (
|
||||||
if not any(marker in segment for marker in ".!?"):
|
cue_span.end <= qty.span.start
|
||||||
matching_bindings.append((binding, qty, obj))
|
and qty.span.end <= obj.span.start
|
||||||
if len(matching_bindings) == 1:
|
):
|
||||||
binding, quantity, obj = matching_bindings[0]
|
segment = text[cue_span.start : obj.span.end]
|
||||||
roles = (
|
if not any(marker in segment for marker in ".!?"):
|
||||||
BoundRole(
|
matching_bindings.append((binding, qty, obj))
|
||||||
"action_cue",
|
if len(matching_bindings) == 1:
|
||||||
cue_id,
|
binding, quantity, obj = matching_bindings[0]
|
||||||
"span",
|
roles = (
|
||||||
(cue_span,),
|
BoundRole(
|
||||||
),
|
"action_cue",
|
||||||
BoundRole(
|
cue_id,
|
||||||
"delta_quantity",
|
"span",
|
||||||
quantity.mention_id,
|
(cue_span,),
|
||||||
quantity.kind,
|
),
|
||||||
(quantity.span,),
|
BoundRole(
|
||||||
),
|
"delta_quantity",
|
||||||
BoundRole(
|
quantity.mention_id,
|
||||||
"changed_object", obj.mention_id, obj.kind, (obj.span,)
|
quantity.kind,
|
||||||
),
|
(quantity.span,),
|
||||||
BoundRole("direction", direction, "direction", (cue_span,)),
|
),
|
||||||
)
|
BoundRole(
|
||||||
relations.append(
|
"changed_object", obj.mention_id, obj.kind, (obj.span,)
|
||||||
BoundRelation(
|
),
|
||||||
relation_id="",
|
BoundRole("direction", direction, "direction", (cue_span,)),
|
||||||
relation_type="unary_delta",
|
)
|
||||||
roles=roles,
|
relations.append(
|
||||||
evidence_spans=(cue_span, quantity.span, obj.span),
|
BoundRelation(
|
||||||
|
relation_id="",
|
||||||
|
relation_type="unary_delta",
|
||||||
|
roles=roles,
|
||||||
|
evidence_spans=(cue_span, quantity.span, obj.span),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
decrease_matches = list(_DECREASE_TO_FRACTION_RE.finditer(text))
|
decrease_matches = list(_DECREASE_TO_FRACTION_RE.finditer(text))
|
||||||
if len(decrease_matches) == 1:
|
if len(decrease_matches) == 1:
|
||||||
|
|
@ -1302,7 +1308,7 @@ def build_problem_frame(problem_text: str) -> ProblemFrame:
|
||||||
action_kind = "gain" if surface == "gained" else "loss"
|
action_kind = "gain" if surface == "gained" else "loss"
|
||||||
direction = "increase" if surface == "gained" else "decrease"
|
direction = "increase" if surface == "gained" else "decrease"
|
||||||
cue = GroundedUnaryDeltaCue(
|
cue = GroundedUnaryDeltaCue(
|
||||||
cue_id=f"cue-{len(builder._unary_delta_cues):04d}",
|
cue_id=f"cue-{builder.unary_delta_cue_count:04d}",
|
||||||
surface=surface,
|
surface=surface,
|
||||||
action_kind=action_kind,
|
action_kind=action_kind,
|
||||||
direction=direction,
|
direction=direction,
|
||||||
|
|
@ -1332,6 +1338,7 @@ def build_problem_frame(problem_text: str) -> ProblemFrame:
|
||||||
mentions,
|
mentions,
|
||||||
bindings,
|
bindings,
|
||||||
(*quantity_entity_proposals, *unary_delta_proposals),
|
(*quantity_entity_proposals, *unary_delta_proposals),
|
||||||
|
builder.unary_delta_cues,
|
||||||
):
|
):
|
||||||
builder.add_bound_relation(relation)
|
builder.add_bound_relation(relation)
|
||||||
bound_target = _bound_question_target(problem_text, mentions)
|
bound_target = _bound_question_target(problem_text, mentions)
|
||||||
|
|
|
||||||
|
|
@ -561,3 +561,54 @@ def test_forbidden_import_coupling_checks() -> None:
|
||||||
assert forbid not in imported
|
assert forbid not in imported
|
||||||
for imp in imported:
|
for imp in imported:
|
||||||
assert not imp.startswith(forbid)
|
assert not imp.startswith(forbid)
|
||||||
|
|
||||||
|
|
||||||
|
def test_unary_delta_cue_conformance() -> None:
|
||||||
|
from generate.problem_frame import GroundedUnaryDeltaCue
|
||||||
|
from generate.kernel_facts import SourceSpan
|
||||||
|
|
||||||
|
# 1. Invalid surface/action_kind/direction triples must raise ValueError
|
||||||
|
span = SourceSpan("gained", 0, 6)
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
GroundedUnaryDeltaCue("cue-0000", "gained", "loss", "increase", span)
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
GroundedUnaryDeltaCue("cue-0000", "gained", "gain", "decrease", span)
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
GroundedUnaryDeltaCue("cue-0000", "lost", "gain", "decrease", span)
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
GroundedUnaryDeltaCue("cue-0000", "lost", "loss", "increase", span)
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
GroundedUnaryDeltaCue("cue-0000", "gained", "gain", "increase", SourceSpan("lost", 0, 4))
|
||||||
|
|
||||||
|
# Valid triples must succeed
|
||||||
|
c1 = GroundedUnaryDeltaCue("cue-0001", "gained", "gain", "increase", span)
|
||||||
|
assert c1.cue_id == "cue-0001"
|
||||||
|
|
||||||
|
# 2. Test exact span resolution in _bound_relations
|
||||||
|
from generate.problem_frame_builder import _bound_relations, GroundedMention, MentionBinding, ConstructionProposal
|
||||||
|
mentions = (
|
||||||
|
GroundedMention("m-0000", "quantity", "3", SourceSpan("3", 7, 8)),
|
||||||
|
GroundedMention("m-0001", "object", "apples", SourceSpan("apples", 9, 15)),
|
||||||
|
)
|
||||||
|
bindings = (
|
||||||
|
MentionBinding("b-0000", "quantity_entity", "m-0000", "m-0001", (SourceSpan("3 apples", 7, 15),)),
|
||||||
|
)
|
||||||
|
proposals = (
|
||||||
|
ConstructionProposal(
|
||||||
|
family_id="state_change.unary_delta",
|
||||||
|
relation_type="unary_delta",
|
||||||
|
candidate_organ="unary_delta_transition",
|
||||||
|
evidence_spans=(span,),
|
||||||
|
status="proposed",
|
||||||
|
diagnostic_only=True,
|
||||||
|
serving_allowed=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
# If the cue has a different ID and is passed in, _bound_relations must resolve and use it
|
||||||
|
cues = (
|
||||||
|
GroundedUnaryDeltaCue("cue-1234", "gained", "gain", "increase", span),
|
||||||
|
)
|
||||||
|
relations = _bound_relations("gained 3 apples", mentions, bindings, proposals, cues)
|
||||||
|
assert len(relations) == 1
|
||||||
|
action_cue_role = next(r for r in relations[0].roles if r.role == "action_cue")
|
||||||
|
assert action_cue_role.target_id == "cue-1234"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue