From fa2712ebd7388f6158eb705898ba7eb2b57d1355 Mon Sep 17 00:00:00 2001 From: Shay Date: Sat, 16 May 2026 05:55:49 -0700 Subject: [PATCH] feat(realizer): extend to all 13 English v1 constructions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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%). --- evals/grammatical_coverage/runner.py | 19 +++--- generate/graph_planner.py | 8 +++ generate/morphology.py | 93 ++++++++++++++++++++++++++++ generate/realizer.py | 57 ++++++++++++++++- generate/templates.py | 40 +++++++++++- 5 files changed, 205 insertions(+), 12 deletions(-) create mode 100644 generate/morphology.py diff --git a/evals/grammatical_coverage/runner.py b/evals/grammatical_coverage/runner.py index b99d5f54..8d50326b 100644 --- a/evals/grammatical_coverage/runner.py +++ b/evals/grammatical_coverage/runner.py @@ -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) diff --git a/generate/graph_planner.py b/generate/graph_planner.py index f1712038..aa692072 100644 --- a/generate/graph_planner.py +++ b/generate/graph_planner.py @@ -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 { diff --git a/generate/morphology.py b/generate/morphology.py new file mode 100644 index 00000000..c5a189e8 --- /dev/null +++ b/generate/morphology.py @@ -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) diff --git a/generate/realizer.py b/generate/realizer.py index f1dea678..bdcba519 100644 --- a/generate/realizer.py +++ b/generate/realizer.py @@ -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) + + diff --git a/generate/templates.py b/generate/templates.py index f609f9a1..cf4f9fbb 100644 --- a/generate/templates.py +++ b/generate/templates.py @@ -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 != "" 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, )