"""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 "") 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.articulation_legality import ( ArticulationLegality, validate_finite_predicate_legality, ) from generate.graph_planner import RhetoricalMove from generate.morphology import base_form, past_participle, past_tense, present_participle # Noun pluralisation — used under quantifiers (all/some/many/few/most). # Closes english_fluency_ood gaps.md G2 (plural agreement). _IRREGULAR_PLURALS: dict[str, str] = { "child": "children", "ox": "oxen", "foot": "feet", "tooth": "teeth", "man": "men", "woman": "women", "person": "people", "mouse": "mice", "louse": "lice", "goose": "geese", # invariant "sheep": "sheep", "fish": "fish", "deer": "deer", "moose": "moose", "series": "series", "species": "species", # latin/greek-origin domain vocabulary "datum": "data", "criterion": "criteria", "phenomenon": "phenomena", "analysis": "analyses", "axis": "axes", "basis": "bases", "thesis": "theses", "hypothesis": "hypotheses", "mitochondrion": "mitochondria", } def pluralize(noun: str) -> str: if not noun: return noun if noun in _IRREGULAR_PLURALS: return _IRREGULAR_PLURALS[noun] n = noun if n.endswith(("s", "sh", "ch", "x", "z")): return n + "es" if n.endswith("y") and len(n) > 1 and n[-2] not in "aeiou": return n[:-1] + "ies" if n.endswith("fe"): return n[:-2] + "ves" if n.endswith("f"): return n[:-1] + "ves" return n + "s" # 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] = frozenset({ "all", "some", "many", "few", "most", "several", "various", "no", }) # Mass nouns — uncountable in English, so "all evidence", "some wisdom" # stay singular under quantifiers ("all evidences" is wrong). The # verb still agrees (singular: "all evidence supports truth"). # This list covers the abstract/epistemic vocabulary in # en_core_cognition_v1 + common English mass nouns. _MASS_NOUNS: frozenset[str] = frozenset({ # epistemic / abstract (the seed-pack vocabulary) "evidence", "wisdom", "knowledge", "truth", "light", "darkness", "information", "data", "music", "art", "literature", "philosophy", "courage", "patience", "love", "hope", "fear", "grace", "meaning", "purpose", "beauty", "justice", "freedom", # physical mass "water", "air", "fire", "earth", "sand", "rain", "snow", "ice", "wood", "metal", "gold", "silver", "iron", "stone", "blood", "flesh", "bone", # collective / continuous "weather", "traffic", "furniture", "luggage", "advice", "equipment", "machinery", "scenery", "money", "news", "research", "progress", "feedback", }) def is_mass_noun(noun: str) -> bool: return noun.lower() in _MASS_NOUNS _PREDICATE_DISPLAY: dict[str, str] = { "is_defined_as": "is defined as", "is_caused_by": "is caused by", "has_steps": "has the following steps", "contrasts_with": "contrasts with", "corrects": "corrects", "recalls": "recalls", "is_verified_as": "is verified as", "addresses": "addresses", "defines": "defines", "means": "means", "grounds": "grounds", "supports": "supports", "causes": "causes", "reveals": "reveals", "precedes": "precedes", "follows": "follows", "belongs_to": "belongs to", "answers": "answers", "is_grounded_in": "is grounded in", "is_distinguished_from": "is distinguished from", "implies": "implies", "entails": "entails", "requires": "requires", "verifies": "verifies", "evidences": "evidences", "orders": "orders", } def _humanize_predicate(predicate: str) -> str: return _PREDICATE_DISPLAY.get(predicate, predicate.replace("_", " ")) _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 ") ) base = base_form(verb) match (aspect, tense, negated, plural_subject): case ("perfective", _, _, True): return f"have {past_participle(verb)}" case ("perfective", _, _, False): return f"has {past_participle(verb)}" case ("imperfective", _, _, True): return f"are {present_participle(verb)}" case ("imperfective", _, _, False): 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, True): return f"do not {base}" 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 {base}" case (_, _, True, False): return f"does not {base}" case (_, _, False, True): return 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] # 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 != "" else "..." 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, )