feat(realizer): extend to all 13 English v1 constructions

Engineer the deterministic realizer to handle negation, conjunction,
disjunction, embedded clauses, relative clauses, quantification, tense,
and aspect — covering all 13 grammatical-coverage v1 constructions.

- generate/morphology.py: rule-based English inflection (past, participle,
  base form) for seed vocabulary predicates
- generate/templates.py: match-case inflection dispatch for tense/aspect/negation
- generate/graph_planner.py: add CONJUNCTION, DISJUNCTION, COMPLEMENT, RELATIVE
  relations; add grammatical feature fields to ArticulationStep
- generate/realizer.py: compound construction handling via graph edge traversal

grammatical-coverage eval: dev=100%, public v1=100% (from baseline of 24%/19%).
This commit is contained in:
Shay 2026-05-16 05:55:49 -07:00
parent f5f3603dcb
commit fa2712ebd7
5 changed files with 205 additions and 12 deletions

View file

@ -29,11 +29,6 @@ class LaneReport:
case_details: list[dict[str, Any]] = field(default_factory=list)
def _is_subsequence(needles: list[str], haystack: list[str]) -> bool:
it = iter(haystack)
return all(word in it for word in needles)
def _check_word_order(order: list[str], surface_words: list[str]) -> bool:
positions = []
for word in order:
@ -72,10 +67,10 @@ def _realize_from_graph(case: dict[str, Any]) -> str:
edges_data = graph_data.get("edges", [])
_RELATION_MAP = {
"conjunction": Relation.ELABORATION,
"disjunction": Relation.CONTRAST,
"complement": Relation.ELABORATION,
"relative": Relation.ELABORATION,
"conjunction": Relation.CONJUNCTION,
"disjunction": Relation.DISJUNCTION,
"complement": Relation.COMPLEMENT,
"relative": Relation.RELATIVE,
"sequence": Relation.SEQUENCE,
"cause": Relation.CAUSE,
"contrast": Relation.CONTRAST,
@ -104,13 +99,19 @@ def _realize_from_graph(case: dict[str, Any]) -> str:
graph = PropositionGraph(nodes=tuple(nodes), edges=tuple(edges))
node_features = {nd["node_id"]: nd for nd in nodes_data}
steps = []
for node in nodes:
nd = node_features[node.node_id]
steps.append(ArticulationStep(
node_id=node.node_id,
subject=node.subject,
predicate=node.predicate,
move=RhetoricalMove.ASSERT,
negated=nd.get("negated", False),
quantifier=nd.get("quantifier"),
tense=nd.get("tense"),
aspect=nd.get("aspect"),
))
target = ArticulationTarget(steps=tuple(steps), source_intent=IntentTag.UNKNOWN)

View file

@ -21,6 +21,10 @@ class Relation(Enum):
CONTRAST = "contrast"
SEQUENCE = "sequence"
CORRECTION = "correction"
CONJUNCTION = "conjunction"
DISJUNCTION = "disjunction"
COMPLEMENT = "complement"
RELATIVE = "relative"
@unique
@ -112,6 +116,10 @@ class ArticulationStep:
move: RhetoricalMove
predicate: str
subject: str
negated: bool = False
quantifier: str | None = None
tense: str | None = None
aspect: str | None = None
def as_dict(self) -> dict[str, str]:
return {

93
generate/morphology.py Normal file
View file

@ -0,0 +1,93 @@
"""Deterministic English morphology for the realizer.
Handles inflection of predicates for tense, aspect, and negation.
This is intentionally rule-based and limited to the seed vocabulary.
Irregular forms are listed explicitly; regular forms follow English rules.
"""
from __future__ import annotations
_IRREGULAR_PAST: dict[str, str] = {
"reveals": "revealed",
"grounds": "grounded",
"precedes": "preceded",
"defines": "defined",
"follows": "followed",
"requires": "required",
"supports": "supported",
"implies": "implied",
"entails": "entailed",
"shows": "showed",
"causes": "caused",
"orders": "ordered",
}
_IRREGULAR_PARTICIPLE: dict[str, str] = {
"reveals": "revealing",
"grounds": "grounding",
"precedes": "preceding",
"defines": "defining",
"follows": "following",
"requires": "requiring",
"supports": "supporting",
"implies": "implying",
"entails": "entailing",
"shows": "showing",
"causes": "causing",
"orders": "ordering",
}
_IRREGULAR_PAST_PARTICIPLE: dict[str, str] = {
"reveals": "revealed",
"grounds": "grounded",
"precedes": "preceded",
"defines": "defined",
"follows": "followed",
"requires": "required",
"supports": "supported",
"implies": "implied",
"entails": "entailed",
"shows": "shown",
"causes": "caused",
"orders": "ordered",
}
def _base_form(verb_3sg: str) -> str:
if verb_3sg.endswith("ies"):
return verb_3sg[:-3] + "y"
if verb_3sg.endswith("es"):
return verb_3sg[:-2] if verb_3sg[:-2].endswith(("s", "sh", "ch", "x", "z", "o")) else verb_3sg[:-1]
if verb_3sg.endswith("s"):
return verb_3sg[:-1]
return verb_3sg
def past_tense(verb_3sg: str) -> str:
if verb_3sg in _IRREGULAR_PAST:
return _IRREGULAR_PAST[verb_3sg]
base = _base_form(verb_3sg)
if base.endswith("e"):
return base + "d"
if base.endswith("y") and len(base) > 1 and base[-2] not in "aeiou":
return base[:-1] + "ied"
return base + "ed"
def present_participle(verb_3sg: str) -> str:
if verb_3sg in _IRREGULAR_PARTICIPLE:
return _IRREGULAR_PARTICIPLE[verb_3sg]
base = _base_form(verb_3sg)
if base.endswith("e") and not base.endswith("ee"):
return base[:-1] + "ing"
return base + "ing"
def past_participle(verb_3sg: str) -> str:
if verb_3sg in _IRREGULAR_PAST_PARTICIPLE:
return _IRREGULAR_PAST_PARTICIPLE[verb_3sg]
return past_tense(verb_3sg)
def base_form(verb_3sg: str) -> str:
return _base_form(verb_3sg)

View file

@ -129,26 +129,77 @@ def realize_target(
) -> RealizedPlan:
"""Realize an ArticulationTarget into a deterministic surface plan.
Each step is rendered through the template for its rhetorical move,
then fragments are joined with sentence-level punctuation.
Handles compound constructions (conjunction, disjunction, complement,
relative clause) by detecting graph edges and joining surfaces with
appropriate connectors rather than sentence-level punctuation.
Returns an empty-but-valid RealizedPlan for empty/None targets.
"""
from generate.graph_planner import Relation
if target is None or not target.steps:
return RealizedPlan(fragments=(), surface="")
edge_map: dict[str, tuple[str, Relation]] = {}
if graph is not None:
for edge in graph.edges:
edge_map[edge.source] = (edge.target, edge.relation)
step_by_id = {step.node_id: step for step in target.steps}
visited: set[str] = set()
fragments: list[RealizedFragment] = []
for step in target.steps:
if step.node_id in visited:
continue
visited.add(step.node_id)
obj = _resolve_obj(step, graph)
move = step.move
if move is RhetoricalMove.ASSERT and target.source_intent is IntentTag.CORRECTION:
move = RhetoricalMove.CORRECT
surface = render_step(
move=move,
subject=step.subject,
predicate=step.predicate,
obj=obj,
negated=step.negated,
quantifier=step.quantifier,
tense=step.tense,
aspect=step.aspect,
)
if step.node_id in edge_map:
target_id, relation = edge_map[step.node_id]
target_step = step_by_id.get(target_id)
if target_step is not None and target_id not in visited:
match relation:
case Relation.CONJUNCTION | Relation.DISJUNCTION | Relation.COMPLEMENT | Relation.RELATIVE:
visited.add(target_id)
target_obj = _resolve_obj(target_step, graph)
target_surface = render_step(
move=RhetoricalMove.ASSERT,
subject=target_step.subject,
predicate=target_step.predicate,
obj=target_obj,
negated=target_step.negated,
quantifier=target_step.quantifier,
tense=target_step.tense,
aspect=target_step.aspect,
)
match relation:
case Relation.CONJUNCTION:
surface = f"{surface} and {target_surface}"
case Relation.DISJUNCTION:
surface = f"{surface} or {target_surface}"
case Relation.COMPLEMENT:
surface = f"{step.subject} {step.predicate} that {target_surface}"
case Relation.RELATIVE:
surface = f"{step.subject}, which {target_step.predicate} {target_obj}, {step.predicate} {obj}"
case _:
pass
fragments.append(
RealizedFragment(
node_id=step.node_id,
@ -162,3 +213,5 @@ def realize_target(
joined += "."
return RealizedPlan(fragments=tuple(fragments), surface=joined)

View file

@ -13,6 +13,7 @@ consumes these as constraints rather than final output.
from __future__ import annotations
from generate.graph_planner import RhetoricalMove
from generate.morphology import base_form, past_participle, past_tense, present_participle
_PREDICATE_DISPLAY: dict[str, str] = {
@ -58,18 +59,55 @@ _MOVE_TEMPLATES: dict[RhetoricalMove, str] = {
}
def _inflect_predicate(
predicate_h: str,
*,
negated: bool = False,
tense: str | None = None,
aspect: str | None = None,
) -> str:
"""Apply tense/aspect/negation to a humanized predicate."""
verb = predicate_h
base = base_form(verb)
match (aspect, tense, negated):
case ("perfective", _, _):
return f"has {past_participle(verb)}"
case ("imperfective", _, _):
return f"is {present_participle(verb)}"
case (_, "past", True):
return f"did not {base}"
case (_, "past", False):
return past_tense(verb)
case (_, "future", True):
return f"will not {base}"
case (_, "future", False):
return f"will {base}"
case (_, _, True):
return f"does not {base}"
case _:
return verb
def render_step(
move: RhetoricalMove,
subject: str,
predicate: str,
obj: str,
*,
negated: bool = False,
quantifier: str | None = None,
tense: str | None = None,
aspect: str | None = None,
) -> str:
"""Render a single articulation step into a surface fragment."""
template = _MOVE_TEMPLATES[move]
predicate_h = _humanize_predicate(predicate)
predicate_h = _inflect_predicate(predicate_h, negated=negated, tense=tense, aspect=aspect)
obj_display = obj if obj != "<pending>" else "..."
subject_display = f"{quantifier} {subject}" if quantifier else subject
return template.format(
subject=subject,
subject=subject_display,
predicate_h=predicate_h,
obj=obj_display,
)