Collapses the duplicated closed English tables into generate/lexicon.py.
Measured on main @ 0948f7cd: the same facts were encoded up to five times
across the reading and writing paths, each copy written independently as a
successive deduction band landed.
The 2A/2B split is decided empirically per #129 — make the change, run the
11 pinned lanes, let byte-identity rule. Result: 11/11 byte-identical, so
this entire unit is 2A and nothing spilled into the authorization-gated 2B.
Collapsed to one object:
- connectives 3 identical copies (member, verb, cond_member)
- predicate display 2 identical 26-entry copies (semantic_templates,
templates)
- be-form inventory 5 copies, not the 2 §1.3 counted. The structural
test found realizer_guard._BE_AUX and
chat/runtime._BE_FORMS, which a COPULA-shaped grep
could never see.
Derived rather than duplicated, so they can no longer drift:
- STRUCTURAL = CONNECTIVES | {therefore}
- QUANTIFIER_TOKENS = QUANTIFIER_LEAD | QUANTIFIER_NON_LEAD
- NEGATION_BEARING_WITH_NOT = NEGATION_BEARING | {not}
- READER_IRREGULAR_SINGULARS = IRREGULAR_SINGULARS restricted to 8 keys
§1.3 re-measured while doing this, and it overstated the disease (plan
§1.9). Almost nothing contradicts:
- the "3 divergent plural tables" are 1 pluralizer + 2 singularizers,
i.e. inverse directions; comparing them as copies is a category error
- reader's 8 singulars are a STRICT SUBSET of member's 29 with the
difference in reader's favour empty, so its values are all correct and
only its coverage is short
- the "3 divergent quantifier sets" are 3 distinct facts; every/each lead
a clause but take singular nouns, so their absence from
PLURAL_QUANTIFIERS is correct
- english's negation set is a subset of member's, and the difference is
principled: v2-EN normalizes "<copula> not", v3-MEM refuses it
Exactly one genuine contradiction exists in the whole inventory:
discourse_planner maps is_defined_as -> "is" where the others map it to
"is defined as". Preserved as a register choice, pinned by test.
Found while reading, pinned as current behaviour for 2B to flip:
reader._singularize does NOT refuse uncovered plurals despite a comment
claiming it does — it falls through to a bare -s strip, so news -> new and
species -> specy, exactly the corruptions member.py's table comment says
its table exists to prevent.
Two 2A exit criteria were unmeasurable as written and are replaced:
- "Jaccard -> 1.00" cannot work. measure_grammar_seam.py scans source
literals, so unifying a fact removes it from those files and Jaccard
FELL, 0.083 -> 0.023 — the success direction reported by a metric
shaped to read like failure. Replaced with an object-identity count:
3 distinct objects behind 8 names, from 8-behind-8. Value comparison
cannot tell a shared object from two equal copies.
- "one table per fact" presumed §1.3's inventory was right. The report now
separates must-be-one-object (all UNIFIED) from related-but-distinct
(6 relations, all HOLD).
ADR-0258 §5 already decided morphology's destination is a ratified pack,
triggered by a grc/he member band. No such band exists, so the promotion is
correctly deferred and lexicon.py is a staging area, not a rival home — no
new ADR. Destination measured for whoever picks it up: packs/en/morphology
.jsonl has 9 records and zero irregular plurals, features.number has no
consumer, and _pack_morph_roots_for reads a top-level "root" key that 0 of
31 records across all four packs define, so it returns {} every call.
[Verification]: in-worktree on CPython 3.12.13, uv sync --locked —
smoke 621 unchanged; deductive 383 (364 + 19 new);
scripts/verify_lane_shas.py 11/11 byte-identical.
196 lines
7.1 KiB
Python
196 lines
7.1 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,
|
|
)
|
|
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] = IRREGULAR_PLURALS
|
|
|
|
|
|
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] = PLURAL_QUANTIFIERS
|
|
|
|
# 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] = PREDICATE_DISPLAY
|
|
|
|
|
|
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 != "<pending>" 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,
|
|
)
|