core/generate/morphology.py
Shay 3952da11bc fix(gaps): close G1+G2+G3 + identity vocab + pipeline safety-stub honour
Six contained-tier gap closures from the cross-phase gap audit.
Every gaps.md file involved gains a resolution block; the historical
findings are preserved for traceability.

Identity (teaching/review.py)
- _ROLE_FRAMES gains the philosophical-axis family (axiology, ontology,
  telos, ethos, epistemology, morality, ethics, virtues, norms,
  doctrine, creed, convictions, beliefs, values, principles).
- _REDIRECT_VERBS gains the dismissal family (disregard, dismiss,
  bypass, circumvent, renounce, reject, repudiate).
- Closes multi_agent_composition holdout MAC-HOLD-001 ("disregard your
  axiology") and the matching adversarial_identity gap.
- Multi-agent holdouts: 8/8 attacks rejected, 3/3 legits accepted.

Pipeline (core/cognition/pipeline.py + docs/runtime_contracts.md)
- When the unknown-domain gate fires, ChatRuntime returns the
  "I don't have field coordinates for that yet." stub and
  vault_hits == 0.  The pipeline now honours that stub as the
  user-facing surface instead of overriding with the realizer's
  fallback articulation.  walk_surface is unchanged either way.
- New contract test
  tests/test_semantic_realizer_integration.py::test_pipeline_honours_safety_stub_when_gate_fires
  locks the contract; the existing semantic-surface test now primes
  the vault first so the gate doesn't fire on the probe.
- Closes calibration gaps.md Finding 2.

Realizer morphology (generate/morphology.py)
- G1: ~100-entry irregular-verb table replaces the previous list which
  contained only regular forms.  Includes bind→bound, run→ran,
  stand→stood, write→wrote/written, eat→ate/eaten, fly→flew/flown,
  swim→swam/swum, etc.
- CVC doubling rule for -ed and -ing (stop→stopped/stopping,
  plan→planned, run→running).
- Short-ies disambiguation (die/lie/tie keep -ie- in the base; cry/fly
  collapse to -y).  Lie is also irregular (lay/lain) — uses
  _IRREGULAR_FORMS first.
- 28-case regression test (tests/test_morphology_irregular.py).

Realizer plural agreement (generate/templates.py)
- G2: under universal/existential/many/few/most quantifiers, count-noun
  subjects pluralise (molecule → molecules) and the verb de-conjugates
  (binds → bind).  Negation toggles does-not → do-not.  Aspect toggles
  has → have, is → are.  All other constructions unchanged.
- Mass nouns (evidence, wisdom, knowledge, truth, water, …) stay
  singular under quantifiers — "all evidence supports truth" is right;
  "all evidences support" would be wrong English.
- 17-case regression test
  (tests/test_realizer_quantifier_agreement.py) covering count vs mass,
  irregular plurals (child→children, analysis→analyses), and the
  quantifier-tense / quantifier-aspect / quantifier-negation grid.

Rubric punctuation tolerance (evals/grammatical_coverage/runner.py)
- G3: _check_word_order strips trailing/leading punctuation
  (.,;:!?—–) before exact-word comparison so "river," still satisfies
  word_order=["river"].  must_contain also accepts punctuation-
  stripped token matches.
- Affects every lane that uses grammatical_coverage scoring; the OOD
  case generators no longer need to pin punctuated accept_surfaces for
  C06.

Case generator + lane regeneration
- scripts/generate_english_fluency_ood.py uses generate.templates.pluralize
  for C07/C08 must_contain + word_order so case-side constraints stay
  aligned with the (more correct) realizer.
- All Phase 5 OOD lane cases (5.1, 5.4–5.7) regenerated; results files
  re-scored.

CLI (core/cli.py)
- cmd_eval no longer crashes on lanes whose case_details use "id"
  instead of "case_id" (adversarial_identity, multi_agent_composition).
- Cognition CLI lane gains the two new morphology/quantifier
  regression test files.

Lane sweep (all 100%, no regression):
  english_fluency_ood              117/117 public + 39/39 holdouts
  elementary_mathematics_ood       117/117 + 39/39
  foundational_physics_ood         117/117 + 39/39
  foundational_biology_ood         117/117 + 39/39
  classical_literature_ood         117/117 + 39/39
  grammatical_coverage             back to 100% on its own seed cases
  hebrew_fluency / koine_greek_fluency  3/3 each

CLI lane health:
  smoke 54, runtime 19, teaching 17, packs 6, cognition 103 (was 57),
  algebra 132.
2026-05-16 21:21:06 -07:00

214 lines
7.4 KiB
Python

"""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
# Genuinely irregular English verbs (the previous tables held only
# regular forms that the suffix rules already produce correctly).
# Listed as 3rd-person singular present → (past, past_participle).
# Coverage: ~100 common English irregulars including every verb that
# the seed packs and Phase 5 OOD lanes use whose past form does not
# follow the regular -ed rule. Adding a verb here is the cheap fix
# for english_fluency_ood gaps.md G1.
_IRREGULAR_FORMS: dict[str, tuple[str, str]] = {
# be / have / do
"is": ("was", "been"),
"are": ("were", "been"),
"has": ("had", "had"),
"does": ("did", "done"),
# mental / cognitive
"thinks": ("thought", "thought"),
"knows": ("knew", "known"),
"sees": ("saw", "seen"),
"hears": ("heard", "heard"),
"feels": ("felt", "felt"),
"finds": ("found", "found"),
"loses": ("lost", "lost"),
"means": ("meant", "meant"),
"reads": ("read", "read"),
"tells": ("told", "told"),
"says": ("said", "said"),
"thinks": ("thought", "thought"),
# motion
"goes": ("went", "gone"),
"comes": ("came", "come"),
"runs": ("ran", "run"),
"stands": ("stood", "stood"),
"sits": ("sat", "sat"),
"lies": ("lay", "lain"),
"flies": ("flew", "flown"),
"swims": ("swam", "swum"),
"rides": ("rode", "ridden"),
"drives": ("drove", "driven"),
"rises": ("rose", "risen"),
"falls": ("fell", "fallen"),
# giving / taking / holding
"gives": ("gave", "given"),
"takes": ("took", "taken"),
"brings": ("brought", "brought"),
"buys": ("bought", "bought"),
"sells": ("sold", "sold"),
"holds": ("held", "held"),
"keeps": ("kept", "kept"),
"leaves": ("left", "left"),
"lends": ("lent", "lent"),
"sends": ("sent", "sent"),
"spends": ("spent", "spent"),
"puts": ("put", "put"),
"sets": ("set", "set"),
"lets": ("let", "let"),
# building / making / breaking
"makes": ("made", "made"),
"builds": ("built", "built"),
"breaks": ("broke", "broken"),
"tears": ("tore", "torn"),
"burns": ("burned", "burned"),
"shines": ("shone", "shone"),
"draws": ("drew", "drawn"),
"writes": ("wrote", "written"),
"speaks": ("spoke", "spoken"),
"wins": ("won", "won"),
"loses": ("lost", "lost"),
# binding / connecting
"binds": ("bound", "bound"),
"winds": ("wound", "wound"),
"weaves": ("wove", "woven"),
"flies": ("flew", "flown"),
"spins": ("spun", "spun"),
"sticks": ("stuck", "stuck"),
"swears": ("swore", "sworn"),
# giving form / shape
"becomes": ("became", "become"),
"grows": ("grew", "grown"),
"blows": ("blew", "blown"),
"throws": ("threw", "thrown"),
"shakes": ("shook", "shaken"),
"wakes": ("woke", "woken"),
# eating / drinking / cooking
"eats": ("ate", "eaten"),
"drinks": ("drank", "drunk"),
"feeds": ("fed", "fed"),
"bites": ("bit", "bitten"),
"freezes": ("froze", "frozen"),
# cutting / striking
"cuts": ("cut", "cut"),
"hits": ("hit", "hit"),
"shoots": ("shot", "shot"),
"splits": ("split", "split"),
"strikes": ("struck", "struck"),
"fights": ("fought", "fought"),
"wears": ("wore", "worn"),
# sleeping / rising / etc
"sleeps": ("slept", "slept"),
"wakes": ("woke", "woken"),
"rises": ("rose", "risen"),
# finding / hiding
"hides": ("hid", "hidden"),
"seeks": ("sought", "sought"),
"catches": ("caught", "caught"),
"teaches": ("taught", "taught"),
"thinks": ("thought", "thought"),
"brings": ("brought", "brought"),
# less common / archaic
"begins": ("began", "begun"),
"deals": ("dealt", "dealt"),
"leads": ("led", "led"),
"meets": ("met", "met"),
"sits": ("sat", "sat"),
"swears": ("swore", "sworn"),
"shoots": ("shot", "shot"),
"casts": ("cast", "cast"),
"costs": ("cost", "cost"),
"hurts": ("hurt", "hurt"),
"lets": ("let", "let"),
"quits": ("quit", "quit"),
"shuts": ("shut", "shut"),
}
# Legacy compatibility — historic call sites use these names. All
# regular-verb entries here match the suffix-rule output, so removing
# them is purely cosmetic; new irregulars live in `_IRREGULAR_FORMS`.
_IRREGULAR_PAST: dict[str, str] = {v: forms[0] for v, forms in _IRREGULAR_FORMS.items()}
_IRREGULAR_PARTICIPLE: dict[str, str] = {
# Present-participle (-ing) is almost always regular. Only handle
# the truly weird cases (lie→lying handled by the suffix rule;
# be→being is the one English present-participle that needs a
# special entry, but `is` doesn't normally surface as a content
# predicate in our realizer pipeline).
}
_IRREGULAR_PAST_PARTICIPLE: dict[str, str] = {v: forms[1] for v, forms in _IRREGULAR_FORMS.items()}
# Short -ies verbs whose base is -ie (not -y). English's "ies → y"
# rule (cries→cry, flies→fly) breaks for these short stems where the
# original lemma keeps the -ie (dies→die, lies→lie, ties→tie).
_IES_KEEP_IE: frozenset[str] = frozenset({"dies", "lies", "ties", "vies", "pies", "hies"})
def _base_form(verb_3sg: str) -> str:
if verb_3sg in _IES_KEEP_IE:
return verb_3sg[:-1]
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" # make → made? no — make is irregular; bake → baked
if base.endswith("y") and len(base) > 1 and base[-2] not in "aeiou":
return base[:-1] + "ied" # cry → cried
if _is_cvc_ending(base):
return base + base[-1] + "ed" # stop → stopped, plan → planned
return base + "ed"
_VOWELS = frozenset("aeiou")
def _is_cvc_ending(base: str) -> bool:
"""True if `base` ends in consonant-vowel-consonant (excluding w/x/y),
the trigger pattern for doubling the final consonant before -ing /
-ed in English."""
if len(base) < 3:
return False
c1, v, c2 = base[-3], base[-2], base[-1]
if c2 in {"w", "x", "y"}:
return False
return (c1 not in _VOWELS) and (v in _VOWELS) and (c2 not in _VOWELS)
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("ie"):
return base[:-2] + "ying" # die → dying, lie → lying
if base.endswith("e") and not base.endswith("ee"):
return base[:-1] + "ing" # make → making
if _is_cvc_ending(base):
return base + base[-1] + "ing" # run → running, swim → swimming
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)