Phase 3 fixed two of the twelve branches of `_inflect_predicate` by hand and
pinned them with a hand-written oracle. The other ten kept handing whole
predicate phrases to single-verb functions, so the same root cause was still
live on **57 of 96** (branch x multi-word predicate) pairs, 9 of 12 branches:
"belongs to" --perfective--> "has belongs toed"
"belongs to" --imperfective--> "is belongs toing"
"belongs to" --past--> "belongs toed"
"is defined as" --future--> "will is defined a"
"contrasts with" --negated--> "does not contrasts with"
Every branch now routes through `morphology.inflect_phrase_head`, which applies
a single-verb inflection to the finite verb and carries tokens 2..n through
byte-identically. Two closed tables were needed because `_base_form` is a
suffix stripper and the head of every copular predicate is a form of BE:
`base_form("is")` returned "i" and `present_participle("is")` returned "iing".
Also fixes predicate-nominal object agreement. `render_step` pluralized the
subject and never the object, so it wrote "all dogs are a mammal". The object
agrees only for a predicate nominal -- a closed set of two -- because a
prepositional object carries its own number and a productive rule would write
"all claims are grounded in evidences".
Measured
--------
tail-mangling pairs 57/96 -> 0/96
g_read_rate (round-trip) 0.0 -> 0.003413 first non-zero in the arc
grammatical_coverage v1 49/49 (2 cases corrected, see below)
english_fluency_ood 117/117 + 39/39 + 13/13 unchanged
discourse_paragraph 12/12 + 6/6 + 5/5 + 1/1 unchanged
zero_code_domain_acquisition 18/18 + 30/30 + 21/21 unchanged
`gram_C14_p01` and `gram_C14_p10` expected "...defined as compound", which is
not English. Corrected, and the old string moved into `reject_surfaces` so the
case now actively rejects what it used to accept. The correction is not my
judgment: the reader independently READS "all molecules are defined as
compounds" and REFUSES the singular form, and that is precisely what took
g_read_rate off zero.
Why the invariant instead of a bigger oracle
--------------------------------------------
A per-branch oracle has to be extended by hand for each new branch, and a
branch added without one is invisible -- which is how eight survived Phase 3.
The pin is structural instead: English marks tense, number and aspect on the
finite verb, so inflection must leave tokens 2..n byte-identical. It needs no
oracle and covers branches nobody has written yet.
It is necessary, not sufficient: "does not contrasts with" preserves its tail
perfectly and is still wrong. `test_do_support_puts_the_head_in_the_bare_
infinitive` is the sufficiency half.
Mutation
--------
baseline 84 pass
inflect_phrase_head applied to the whole phrase 39 FAIL
_IRREGULAR_BASE emptied 2 FAIL
_IRREGULAR_PARTICIPLE auxiliaries removed 2 FAIL
PREDICATIVE_NOMINAL emptied 3 FAIL
PREDICATIVE_NOMINAL widened to every copular predicate 2 FAIL
The last row is the one that matters: the plausible-but-wrong fix -- "pluralize
the object under a plural subject" -- is caught by the mass-noun control.
Not fixed here, deliberately: "has not the following steps" is archaic rather
than wrong, and modernizing it to do-support is a separate judgment call.
[Verification]: smoke 621, deductive 406, lane pins 11/11 unchanged
(grammatical_coverage is not a pinned lane; no pin was edited).
223 lines
9.6 KiB
Python
223 lines
9.6 KiB
Python
"""Deterministic surface templates for rhetorical moves.
|
|
|
|
Each template is a format string keyed by RhetoricalMove. Slots:
|
|
{subject} — primary subject from the articulation step
|
|
{predicate} — semantic predicate (e.g. "is_defined_as", "contrasts_with")
|
|
{obj} — object slot from the graph node (may be "<pending>")
|
|
|
|
Templates are intentionally simple. The goal is structural correctness,
|
|
not fluency — fluency comes in a later phase when the generation stream
|
|
consumes these as constraints rather than final output.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from generate.lexicon import (
|
|
IRREGULAR_PLURALS,
|
|
PLURAL_QUANTIFIERS,
|
|
PREDICATE_DISPLAY,
|
|
PREDICATIVE_NOMINAL,
|
|
)
|
|
from generate.articulation_legality import (
|
|
ArticulationLegality,
|
|
validate_finite_predicate_legality,
|
|
)
|
|
from generate.graph_planner import RhetoricalMove
|
|
from generate.morphology import (
|
|
agree_plural_phrase,
|
|
base_form,
|
|
inflect_phrase_head,
|
|
is_mass_noun,
|
|
past_participle,
|
|
past_tense,
|
|
pluralize,
|
|
present_participle,
|
|
takes_bare_not,
|
|
)
|
|
|
|
|
|
# Noun pluralisation — used under quantifiers (all/some/many/few/most).
|
|
# Closes english_fluency_ood gaps.md G2 (plural agreement).
|
|
#
|
|
# Phase 2B: the rules moved to generate/morphology.py, which now owns number
|
|
# in both directions. Re-exported here because this module's public surface
|
|
# is consumed by the eval runners.
|
|
_IRREGULAR_PLURALS: dict[str, str] = IRREGULAR_PLURALS
|
|
|
|
|
|
# Quantifiers that demand plural agreement on the subject + verb.
|
|
# "the" / "a" stay singular; "every" / "each" are singular by English
|
|
# rule even though semantically universal.
|
|
_PLURAL_QUANTIFIERS: frozenset[str] = PLURAL_QUANTIFIERS
|
|
|
|
_PREDICATE_DISPLAY: dict[str, str] = PREDICATE_DISPLAY
|
|
|
|
|
|
def _humanize_predicate(predicate: str) -> str:
|
|
return _PREDICATE_DISPLAY.get(predicate, predicate.replace("_", " "))
|
|
|
|
|
|
def _drop_indefinite_article(predicate_h: str) -> str:
|
|
"""Strip a trailing ``a``/``an`` from an inflected predicate.
|
|
|
|
``is_a`` humanizes to "is a" and pluralizes to "are a"; the article cannot
|
|
survive a plural nominal ("all dogs are a mammals" is not English).
|
|
"""
|
|
head, sep, rest = predicate_h.rpartition(" ")
|
|
return head if sep and rest in ("a", "an") else predicate_h
|
|
|
|
|
|
_MOVE_TEMPLATES: dict[RhetoricalMove, str] = {
|
|
RhetoricalMove.ASSERT: "{subject} {predicate_h} {obj}",
|
|
RhetoricalMove.ELABORATE: "furthermore, {subject} {predicate_h} {obj}",
|
|
RhetoricalMove.CONTRAST: "in contrast, {subject} {predicate_h} {obj}",
|
|
RhetoricalMove.SEQUENCE: "next, {subject} {predicate_h} {obj}",
|
|
RhetoricalMove.CORRECT: "correction: {subject} {predicate_h} {obj}",
|
|
}
|
|
|
|
|
|
def _inflect_predicate(
|
|
predicate_h: str,
|
|
*,
|
|
negated: bool = False,
|
|
tense: str | None = None,
|
|
aspect: str | None = None,
|
|
plural_subject: bool = False,
|
|
) -> str:
|
|
"""Apply tense/aspect/negation to a humanized predicate.
|
|
|
|
When ``plural_subject`` is true, the conjugation uses plural
|
|
agreement (do not / have / are / bare-base verb in present) so
|
|
surfaces like "all molecules bind enzyme" come out correctly
|
|
instead of "all molecule binds enzyme" (english_fluency_ood G2).
|
|
"""
|
|
verb = predicate_h
|
|
copular = any(
|
|
predicate_h.startswith(prefix)
|
|
for prefix in ("is ", "are ", "has ", "have ", "belongs ")
|
|
)
|
|
match (aspect, tense, negated, plural_subject):
|
|
# Every branch below inflects the phrase HEAD and carries tokens 2..n
|
|
# through untouched. Phase 3 fixed only the two plural branches, so
|
|
# these eight were still handing whole phrases to single-verb
|
|
# functions: "belongs to" came back "has belongs toed" (perfective),
|
|
# "is belongs toing" (imperfective), "belongs toed" (past), and
|
|
# "is defined as" came back "will is defined a" (future).
|
|
case ("perfective", _, _, True):
|
|
return f"have {inflect_phrase_head(verb, past_participle)}"
|
|
case ("perfective", _, _, False):
|
|
return f"has {inflect_phrase_head(verb, past_participle)}"
|
|
case ("imperfective", _, _, True):
|
|
return f"are {inflect_phrase_head(verb, present_participle)}"
|
|
case ("imperfective", _, _, False):
|
|
return f"is {inflect_phrase_head(verb, present_participle)}"
|
|
case (_, "past", True, _):
|
|
# A be/have head negates in place and carries its own past tense
|
|
# ("was not defined as"); anything else takes do-support in the
|
|
# past ("did not belong to"), where the head reverts to the base.
|
|
if takes_bare_not(verb):
|
|
past = inflect_phrase_head(verb, past_tense)
|
|
p_head, sep, rest = past.partition(" ")
|
|
if plural_subject:
|
|
p_head = {"was": "were", "has": "have", "did": "did"}.get(p_head, p_head)
|
|
return f"{p_head} not{sep}{rest}" if rest else f"{p_head} not"
|
|
return f"did not {inflect_phrase_head(verb, base_form)}"
|
|
case (_, "past", False, _):
|
|
past = inflect_phrase_head(verb, past_tense)
|
|
if plural_subject:
|
|
p_head, sep, rest = past.partition(" ")
|
|
return {"was": "were"}.get(p_head, p_head) + sep + rest
|
|
return past
|
|
case (_, "future", True, _):
|
|
return f"will not {inflect_phrase_head(verb, base_form)}"
|
|
case (_, "future", False, _):
|
|
return f"will {inflect_phrase_head(verb, base_form)}"
|
|
case (_, _, True, True):
|
|
# Plural + negated. Agree the head first, then negate around it:
|
|
# a plural copula takes a bare "not" ("are not defined as"), while
|
|
# any other verb needs do-support ("do not have the following
|
|
# steps", "do not belong to"). Previously this was
|
|
# ``f"do not {base}"`` over the whole phrase, which produced
|
|
# "do not is defined a".
|
|
agreed = agree_plural_phrase(predicate_h)
|
|
head, sep, rest = agreed.partition(" ")
|
|
if head in ("are", "were"):
|
|
return f"{head} not{sep}{rest}" if rest else f"{head} not"
|
|
return f"do not {agreed}"
|
|
case (_, _, True, False) if copular:
|
|
if predicate_h.startswith("is "):
|
|
return "is not " + predicate_h[3:]
|
|
if predicate_h.startswith("are "):
|
|
return "are not " + predicate_h[4:]
|
|
if predicate_h.startswith("has "):
|
|
return "has not " + predicate_h[4:]
|
|
if predicate_h.startswith("have "):
|
|
return "have not " + predicate_h[5:]
|
|
if predicate_h.startswith("belongs "):
|
|
return "does not belong " + predicate_h[8:]
|
|
return f"is not {inflect_phrase_head(verb, base_form)}"
|
|
case (_, _, True, False):
|
|
# Do-support puts the head in the bare infinitive. This branch was
|
|
# the ninth instance of the same defect: ``base_form`` on the whole
|
|
# phrase left "contrasts with" untouched (no -s/-es/-ies suffix to
|
|
# strip from "with"), yielding "does not contrasts with".
|
|
return f"does not {inflect_phrase_head(verb, base_form)}"
|
|
case (_, _, False, True):
|
|
# Plural agreement on the whole phrase, not base_form() of it.
|
|
# This is the branch the 9-of-26 defect lived in: it returned the
|
|
# bare base and never consulted ``copular``, so "is defined as"
|
|
# came back "is defined a" instead of "are defined as".
|
|
return agree_plural_phrase(predicate_h)
|
|
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]
|
|
# Mass nouns under a quantifier stay singular ("all evidence
|
|
# supports", not "all evidences support"). Count nouns
|
|
# pluralise and the verb de-conjugates ("all molecules bind").
|
|
plural_q = quantifier is not None and quantifier.lower() in _PLURAL_QUANTIFIERS
|
|
is_mass = is_mass_noun(subject)
|
|
plural = plural_q and not is_mass
|
|
predicate_h = _humanize_predicate(predicate)
|
|
legality = validate_finite_predicate_legality(
|
|
predicate_humanized=predicate_h,
|
|
negated=negated,
|
|
)
|
|
if legality.legality is ArticulationLegality.ILLEGAL_NON_VERB_FINITE_PREDICATE:
|
|
return "I cannot realize that proposition coherently yet."
|
|
predicate_h = _inflect_predicate(
|
|
predicate_h,
|
|
negated=negated, tense=tense, aspect=aspect,
|
|
plural_subject=plural,
|
|
)
|
|
obj_display = obj if obj != "<pending>" else "..."
|
|
# A predicate nominal names the subject's category, so it agrees with the
|
|
# subject in number and sheds its indefinite article: "all dogs are
|
|
# mammals", never "all dogs are a mammal". Restricted to the closed
|
|
# PREDICATIVE_NOMINAL set — a prepositional object carries its own number
|
|
# ("all claims are grounded in evidence") and pluralizing it produces
|
|
# "evidences". The reader accepts the agreeing form and refuses the other,
|
|
# which is why this was the last writer-side blocker on G-round-trip.
|
|
if plural and predicate in PREDICATIVE_NOMINAL:
|
|
obj_display = pluralize(obj_display)
|
|
predicate_h = _drop_indefinite_article(predicate_h)
|
|
subject_form = pluralize(subject) if plural else subject
|
|
subject_display = f"{quantifier} {subject_form}" if quantifier else subject_form
|
|
return template.format(
|
|
subject=subject_display,
|
|
predicate_h=predicate_h,
|
|
obj=obj_display,
|
|
)
|