Merge pull request 'fix(generate): plural-subject agreement, 26/26 (Phase 3)' (#134) from fix/predicate-agreement into main

This commit is contained in:
Joshua Matthew-Catudio Shay 2026-07-27 17:12:11 +00:00
commit bb82029e8d
9 changed files with 274 additions and 11 deletions

View file

@ -723,6 +723,56 @@ Deliver:
| the gap is closed for good | new cases exercise quantifier × copular; reverting either fix goes red |
| no regression | `english_fluency_ood` 117/117 + 39/39 holds; smoke/deductive unchanged |
**RESULT — all three met.** Agreement **26/26** (from 17/26). `english_fluency_ood`
**117/117 + 39/39** (+ dev 13/13) unchanged. New construction **C14
`quantified_copular`**, 13 cases, `grammatical_coverage` v1 **49/49**.
Root causes, both single-point as §1.4 predicted:
1. `base_form` is a **single-verb** function and was applied to whole phrases,
stripping the final word's last character-class. Fixed by
`morphology.agree_plural_phrase`, which inflects the **head** (the finite
verb is the first token of every humanized predicate) and carries the rest
through. `base_form` itself is left single-verb and **pinned as still wrong
on a phrase**, so nobody "fixes" the symptom in the wrong place.
2. The plural branch never consulted `copular` — it returned the bare base. Now
it agrees the phrase. The plural **negated** branch had the same defect
(`do not is defined a`) and is fixed with it: a plural copula takes a bare
`not`, everything else takes do-support.
**Two further defects found while writing the cases**, both genuine and both
fixed here:
- `base_form("causes")`**`"caus"`**. The `-es` sibilant rule fired on a
stem ending in a single `s`; it should require a doubled `ss` ("passes" →
"pass") because a single `s` is nearly always a stem ending in `e` that took a
plain `-s` ("causes" → "cause").
- `pluralize("proof")`**`"prooves"`**. `f`/`fe` → `ves` is **not productive
in English** (proof→proofs, chief→chiefs, roof→roofs); it applies to a closed
set. Now derived as `lexicon.VES_PLURAL_SINGULARS` from the `ves` rows of
`IRREGULAR_SINGULARS`, so the rule cannot claim a word the table does not
know. **Phase 2B had put `pluralize` on the serving path**, so this one was
live.
**The first draft of the C14 cases could not fail.** `must_contain` and
`word_order` listed quantifier, subject and object — but *not the verb* — so
`all molecules is defined a compound` passed. Reverting the fix left 47/47 green.
Rewritten with the agreed verb in both constraints plus `reject_surfaces`
carrying the **actual** pre-fix output (computed by running the reverted code,
not guessed). Mutation now:
| reverted fix | C14 |
|---|---|
| baseline | 13/13 |
| phrase-head plural agreement | **4/13** |
| `-es` stem rule | **12/13** |
| closed `ves` set | **11/13** |
The 4 survivors of the first mutation are the mass-noun and unchanged controls,
which is the correct behaviour — `all evidence is grounded in truth` must *not*
pluralize. Same lesson as the lane pins in #133: a pin that cannot fail guards
nothing, and the only way to know is to break the thing on purpose.
### Phase 4 — Resolve `realize_semantic` vs `realize_target` — **authorization gate**
*Touches:* `core/cognition/pipeline.py` or the eval lanes. **First phase that can

View file

@ -65,7 +65,7 @@ this arc's direction (plan §6).
| corpus | source | size |
|---|---|---|
| positive graphs | committed `english_fluency_ood` + `grammatical_coverage` case files | 280 |
| positive graphs | committed `english_fluency_ood` + `grammatical_coverage` case files | 293 |
| positive surfaces | in-module, each verified comprehensible by probe | 8 |
| negative surfaces | hand-authored salad + deterministic token shuffles of the positives | 16 |
@ -86,7 +86,7 @@ grammar checking. Shuffling uses a fixed rotation rather than a PRNG so
## v1 baseline — measured on `main` @ `9696443a`
```
graph_cases 280
graph_cases 293
g_write_rate 1.000
g_read_rate 0.000 <-- CORE reads 0% of what it writes
g_propositions_expected 370

View file

@ -34,3 +34,16 @@
{"id": "gram_C13_p01", "construction": "C13", "construction_name": "imperfective_aspect", "proposition_graph": {"nodes": [{"node_id": "n1", "subject": "wisdom", "predicate": "follows", "obj": "knowledge", "aspect": "imperfective"}], "edges": []}, "accept_surfaces": ["wisdom is following knowledge"], "constraints": {"must_contain": ["wisdom", "is", "following", "knowledge"], "word_order": ["wisdom", "is", "following", "knowledge"], "max_words": 8}}
{"id": "gram_C13_p02", "construction": "C13", "construction_name": "imperfective_aspect", "proposition_graph": {"nodes": [{"node_id": "n1", "subject": "evidence", "predicate": "supports", "obj": "truth", "aspect": "imperfective"}], "edges": []}, "accept_surfaces": ["evidence is supporting truth"], "constraints": {"must_contain": ["evidence", "is", "supporting", "truth"], "word_order": ["evidence", "is", "supporting", "truth"], "max_words": 8}}
{"id": "gram_C13_p03", "construction": "C13", "construction_name": "imperfective_aspect", "proposition_graph": {"nodes": [{"node_id": "n1", "subject": "dawn", "predicate": "precedes", "obj": "day", "aspect": "imperfective"}], "edges": []}, "accept_surfaces": ["dawn is preceding day"], "constraints": {"must_contain": ["dawn", "is", "preceding", "day"], "word_order": ["dawn", "is", "preceding", "day"], "max_words": 8}}
{"id": "gram_C14_p01", "construction": "C14", "construction_name": "quantified_copular", "proposition_graph": {"nodes": [{"node_id": "n1", "subject": "molecule", "predicate": "is_defined_as", "obj": "compound", "quantifier": "all"}], "edges": []}, "accept_surfaces": ["all molecules are defined as compound"], "constraints": {"must_contain": ["all", "molecules", "are", "defined", "as", "compound"], "word_order": ["all", "molecules", "are", "defined", "as", "compound"], "max_words": 8}, "reject_surfaces": ["all molecules is defined a compound"]}
{"id": "gram_C14_p02", "construction": "C14", "construction_name": "quantified_copular", "proposition_graph": {"nodes": [{"node_id": "n1", "subject": "theory", "predicate": "is_caused_by", "obj": "observation", "quantifier": "some"}], "edges": []}, "accept_surfaces": ["some theories are caused by observation"], "constraints": {"must_contain": ["some", "theories", "are", "caused", "by", "observation"], "word_order": ["some", "theories", "are", "caused", "by", "observation"], "max_words": 8}, "reject_surfaces": ["some theories is caused by observation"]}
{"id": "gram_C14_p03", "construction": "C14", "construction_name": "quantified_copular", "proposition_graph": {"nodes": [{"node_id": "n1", "subject": "wolf", "predicate": "belongs_to", "obj": "pack", "quantifier": "all"}], "edges": []}, "accept_surfaces": ["all wolves belong to pack"], "constraints": {"must_contain": ["all", "wolves", "belong", "to", "pack"], "word_order": ["all", "wolves", "belong", "to", "pack"], "max_words": 7}, "reject_surfaces": ["all wolfs belongs to pack"]}
{"id": "gram_C14_p04", "construction": "C14", "construction_name": "quantified_copular", "proposition_graph": {"nodes": [{"node_id": "n1", "subject": "claim", "predicate": "is_grounded_in", "obj": "evidence", "quantifier": "most"}], "edges": []}, "accept_surfaces": ["most claims are grounded in evidence"], "constraints": {"must_contain": ["most", "claims", "are", "grounded", "in", "evidence"], "word_order": ["most", "claims", "are", "grounded", "in", "evidence"], "max_words": 8}, "reject_surfaces": ["most claims is grounded in evidence"]}
{"id": "gram_C14_p05", "construction": "C14", "construction_name": "quantified_copular", "proposition_graph": {"nodes": [{"node_id": "n1", "subject": "method", "predicate": "contrasts_with", "obj": "intuition", "quantifier": "few"}], "edges": []}, "accept_surfaces": ["few methods contrast with intuition"], "constraints": {"must_contain": ["few", "methods", "contrast", "with", "intuition"], "word_order": ["few", "methods", "contrast", "with", "intuition"], "max_words": 7}, "reject_surfaces": ["few methods contrasts with intuition"]}
{"id": "gram_C14_p06", "construction": "C14", "construction_name": "quantified_copular", "proposition_graph": {"nodes": [{"node_id": "n1", "subject": "proof", "predicate": "is_verified_as", "obj": "sound", "quantifier": "many"}], "edges": []}, "accept_surfaces": ["many proofs are verified as sound"], "constraints": {"must_contain": ["many", "proofs", "are", "verified", "as", "sound"], "word_order": ["many", "proofs", "are", "verified", "as", "sound"], "max_words": 8}, "reject_surfaces": ["many proofs is verified a sound"]}
{"id": "gram_C14_p07", "construction": "C14", "construction_name": "quantified_copular", "proposition_graph": {"nodes": [{"node_id": "n1", "subject": "axiom", "predicate": "is_distinguished_from", "obj": "theorem", "quantifier": "all"}], "edges": []}, "accept_surfaces": ["all axioms are distinguished from theorem"], "constraints": {"must_contain": ["all", "axioms", "are", "distinguished", "from", "theorem"], "word_order": ["all", "axioms", "are", "distinguished", "from", "theorem"], "max_words": 8}, "reject_surfaces": ["all axioms is distinguished from theorem"]}
{"id": "gram_C14_p08", "construction": "C14", "construction_name": "quantified_copular", "proposition_graph": {"nodes": [{"node_id": "n1", "subject": "evidence", "predicate": "is_grounded_in", "obj": "truth", "quantifier": "all"}], "edges": []}, "accept_surfaces": ["all evidence is grounded in truth"], "constraints": {"must_contain": ["all", "evidence", "is", "grounded", "in", "truth"], "word_order": ["all", "evidence", "is", "grounded", "in", "truth"], "max_words": 8}}
{"id": "gram_C14_p09", "construction": "C14", "construction_name": "quantified_copular", "proposition_graph": {"nodes": [{"node_id": "n1", "subject": "knowledge", "predicate": "is_defined_as", "obj": "justified", "quantifier": "all"}], "edges": []}, "accept_surfaces": ["all knowledge is defined as justified"], "constraints": {"must_contain": ["all", "knowledge", "is", "defined", "as", "justified"], "word_order": ["all", "knowledge", "is", "defined", "as", "justified"], "max_words": 8}}
{"id": "gram_C14_p10", "construction": "C14", "construction_name": "quantified_copular", "proposition_graph": {"nodes": [{"node_id": "n1", "subject": "molecule", "predicate": "is_defined_as", "obj": "compound", "quantifier": "all", "negated": true}], "edges": []}, "accept_surfaces": ["all molecules are not defined as compound"], "constraints": {"must_contain": ["all", "molecules", "are", "not", "defined", "as", "compound"], "word_order": ["all", "molecules", "are", "not", "defined", "as", "compound"], "max_words": 9}, "reject_surfaces": ["all molecules do not is defined a compound"]}
{"id": "gram_C14_p11", "construction": "C14", "construction_name": "quantified_copular", "proposition_graph": {"nodes": [{"node_id": "n1", "subject": "wolf", "predicate": "belongs_to", "obj": "pack", "quantifier": "all", "negated": true}], "edges": []}, "accept_surfaces": ["all wolves do not belong to pack"], "constraints": {"must_contain": ["all", "wolves", "do", "not", "belong", "to", "pack"], "word_order": ["all", "wolves", "do", "not", "belong", "to", "pack"], "max_words": 9}, "reject_surfaces": ["all wolfs do not belongs to pack"]}
{"id": "gram_C14_p12", "construction": "C14", "construction_name": "quantified_copular", "proposition_graph": {"nodes": [{"node_id": "n1", "subject": "molecule", "predicate": "causes", "obj": "reaction", "quantifier": "all"}], "edges": []}, "accept_surfaces": ["all molecules cause reaction"], "constraints": {"must_contain": ["all", "molecules", "cause", "reaction"], "word_order": ["all", "molecules", "cause", "reaction"], "max_words": 6}, "reject_surfaces": ["all molecules caus reaction"]}
{"id": "gram_C14_p13", "construction": "C14", "construction_name": "quantified_copular", "proposition_graph": {"nodes": [{"node_id": "n1", "subject": "observation", "predicate": "evidences", "obj": "claim", "quantifier": "many"}], "edges": []}, "accept_surfaces": ["many observations evidence claim"], "constraints": {"must_contain": ["many", "observations", "evidence", "claim"], "word_order": ["many", "observations", "evidence", "claim"], "max_words": 6}}

View file

@ -282,6 +282,22 @@ IRREGULAR_SINGULARS: Final[dict[str, str]] = {
"offspring": "offspring", "aircraft": "aircraft", "news": "news",
}
#: Singulars whose plural is formed by ``f``/``fe`` → ``ves``, **derived** from
#: the rows of :data:`IRREGULAR_SINGULARS` whose plural actually ends in
#: ``ves`` — wolf, knife, life, leaf, half, elf, loaf, thief.
#:
#: The ``f`` → ``ves`` change is **not productive in English**: it applies to a
#: closed set, and the productive form is a plain ``-s`` (proof→proofs,
#: chief→chiefs, roof→roofs, belief→beliefs). Applying it as a suffix rule made
#: ``pluralize("proof")`` return **"prooves"**, which Phase 2B had put on the
#: serving path. Derived rather than hand-listed so the rule cannot claim a
#: word the number table does not actually know.
VES_PLURAL_SINGULARS: Final[frozenset[str]] = frozenset(
singular
for plural, singular in IRREGULAR_SINGULARS.items()
if plural.endswith("ves") and plural != singular
)
#: Nouns whose plural equals their singular, **derived** from the invariant
#: rows of :data:`IRREGULAR_SINGULARS` (the rows where key == value).
#:

View file

@ -17,6 +17,7 @@ from __future__ import annotations
from generate.lexicon import (
INVARIANT_NUMBER,
VES_PLURAL_SINGULARS,
IRREGULAR_PLURALS,
IRREGULAR_SINGULARS,
MASS_NOUNS,
@ -167,18 +168,65 @@ _IRREGULAR_PAST_PARTICIPLE: dict[str, str] = {v: forms[1] for v, forms in _IRREG
_IES_KEEP_IE: frozenset[str] = frozenset({"dies", "lies", "ties", "vies", "pies", "hies"})
#: Stem endings that take ``-es`` for the 3sg rather than a bare ``-s``.
#:
#: ``ss`` and not ``s``: the sibilant rule fires on a DOUBLED s ("passes" ->
#: "pass"), while a stem ending in a single ``s`` is almost always a stem
#: ending in ``e`` that took a plain ``-s`` ("causes" -> "cause"). Before this
#: distinction, ``base_form("causes")`` returned **"caus"**, which is the
#: single-verb half of the 9-of-26 agreement defect.
_ES_STEM_ENDINGS = ("ss", "sh", "ch", "x", "z", "o")
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]
return verb_3sg[:-2] if verb_3sg[:-2].endswith(_ES_STEM_ENDINGS) else verb_3sg[:-1]
if verb_3sg.endswith("s"):
return verb_3sg[:-1]
return verb_3sg
#: 3sg present -> plural present, for the forms where the plural is not simply
#: the bare stem. Everything else pluralizes by dropping the ``-s``.
_PLURAL_PRESENT: dict[str, str] = {
"is": "are", "are": "are",
"has": "have", "have": "have",
"does": "do", "do": "do",
"was": "were", "were": "were",
}
def plural_present(verb_3sg: str) -> str:
"""A third-person-singular verb → its plural-subject present form."""
if verb_3sg in _PLURAL_PRESENT:
return _PLURAL_PRESENT[verb_3sg]
return _base_form(verb_3sg)
def agree_plural_phrase(phrase: str) -> str:
"""Put a whole predicate PHRASE into plural agreement.
Number is marked on the finite verb, which is the first token of every
humanized predicate ("is defined as", "has the following steps",
"belongs to", "contrasts with"). Only that token inflects; the rest is
carried through untouched.
This exists because :func:`base_form` is a SINGLE-VERB function and was
being applied to whole phrases, stripping the last character-class of the
final word: "is defined as" -> "is defined a", "has the following steps"
-> "has the following step". Nine of the 26 seed predicates were wrong
that way, and every multi-word one was.
"""
if not phrase:
return phrase
head, sep, rest = phrase.partition(" ")
return plural_present(head) + sep + rest
def past_tense(verb_3sg: str) -> str:
if verb_3sg in _IRREGULAR_PAST:
return _IRREGULAR_PAST[verb_3sg]
@ -272,10 +320,12 @@ def pluralize(noun: str) -> str:
return prefix + head + "es"
if head.endswith("y") and len(head) > 1 and head[-2] not in "aeiou":
return prefix + head[:-1] + "ies"
if head.endswith("fe"):
return prefix + head[:-2] + "ves"
if head.endswith("f"):
return prefix + head[:-1] + "ves"
# f/fe -> ves is a CLOSED set, not a productive rule: proof->proofs and
# chief->chiefs, but wolf->wolves. The set is derived from the number
# table's own ves-rows, so the rule can never claim a word the table does
# not know. Without this, pluralize("proof") returned "prooves".
if head in VES_PLURAL_SINGULARS:
return prefix + (head[:-2] if head.endswith("fe") else head[:-1]) + "ves"
return prefix + head + "s"

View file

@ -23,6 +23,7 @@ from generate.articulation_legality import (
)
from generate.graph_planner import RhetoricalMove
from generate.morphology import (
agree_plural_phrase,
base_form,
is_mass_noun,
past_participle,
@ -102,7 +103,17 @@ def _inflect_predicate(
case (_, "future", False, _):
return f"will {base}"
case (_, _, True, True):
return f"do not {base}"
# 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:]
@ -118,7 +129,11 @@ def _inflect_predicate(
case (_, _, True, False):
return f"does not {base}"
case (_, _, False, True):
return base
# 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

View file

@ -141,7 +141,12 @@ def test_g_roundtrip_baseline_is_zero(report):
revised **upward**; it must never be revised downward to accommodate a
regression.
"""
assert report.metrics["graph_cases"] == 280
# 293, not the original 280: Phase 3 added 13 quantified-copular cases
# (construction C14) to grammatical_coverage/public/v1, and this lane
# harvests its graph corpus from the committed case files rather than
# holding its own copy. An exact count is the point — a corpus that grows
# or shrinks should require a deliberate edit here, not pass silently.
assert report.metrics["graph_cases"] == 293
assert report.metrics["g_write_rate"] == 1.0
assert report.metrics["g_read_rate"] == 0.0
assert report.metrics["g_exact_rate"] == 0.0
@ -303,5 +308,5 @@ def test_positive_surfaces_are_all_inside_the_reader_envelope():
def test_graph_corpus_is_harvested_from_committed_case_files():
cases = positive_graph_cases()
assert len(cases) == 280
assert len(cases) == 293 # +13 C14 quantified-copular (Phase 3)
assert all(c.nodes for c in cases)

View file

@ -79,6 +79,9 @@ RECORDED_CONSUMERS: dict[str, frozenset[str]] = {
}),
"INVARIANT_NUMBER": frozenset({"generate.morphology"}),
"MASS_NOUNS": frozenset({"generate.morphology"}),
# Phase 3: the closed f/fe -> ves set, derived from the singularizer's own
# ves-rows so the rule cannot claim a word the table does not know.
"VES_PLURAL_SINGULARS": frozenset({"generate.morphology"}),
}
#: The tables a duplicate literal would be a duplicate *of*. Frozen as

View file

@ -106,3 +106,114 @@ def test_is_mass_noun_known_set() -> None:
assert is_mass_noun("water")
assert not is_mass_noun("molecule")
assert not is_mass_noun("atom")
# --------------------------------------------------------------------------- #
# Phase 3 — the 9-of-26 agreement defect
#
# ``_inflect_predicate`` applied ``base_form`` — a SINGLE-VERB function — to
# whole humanized predicate phrases, stripping the last character-class of the
# final word, and its plural branch never consulted ``copular``. Nine of the 26
# seed predicates came out wrong, and every multi-word one did.
# --------------------------------------------------------------------------- #
#: Hand-written English, NOT derived from the code under test. Deriving it would
#: make it agree by construction and measure nothing.
PLURAL_AGREEMENT_ORACLE = {
"addresses": "address",
"answers": "answer",
"belongs to": "belong to",
"causes": "cause",
"contrasts with": "contrast with",
"corrects": "correct",
"defines": "define",
"entails": "entail",
"evidences": "evidence",
"follows": "follow",
"grounds": "ground",
"has the following steps": "have the following steps",
"implies": "imply",
"is caused by": "are caused by",
"is defined as": "are defined as",
"is distinguished from": "are distinguished from",
"is grounded in": "are grounded in",
"is verified as": "are verified as",
"means": "mean",
"orders": "order",
"precedes": "precede",
"recalls": "recall",
"requires": "require",
"reveals": "reveal",
"supports": "support",
"verifies": "verify",
}
def test_the_oracle_covers_every_seed_predicate() -> None:
"""A partial oracle would let a predicate regress unseen."""
from generate.lexicon import PREDICATE_DISPLAY
assert set(PLURAL_AGREEMENT_ORACLE) == set(PREDICATE_DISPLAY.values())
@pytest.mark.parametrize(
("display", "expected"), sorted(PLURAL_AGREEMENT_ORACLE.items())
)
def test_plural_subject_agreement_matches_english(display: str, expected: str) -> None:
"""26/26 against hand-written English (was 17/26).
The nine that were wrong: belongs to, causes ("caus"), contrasts with,
has the following steps ("has the following step"), and the five
``is `` copulars ("is defined as" -> "is defined a").
"""
from generate.templates import _inflect_predicate
assert _inflect_predicate(display, plural_subject=True) == expected
@pytest.mark.parametrize(
("display", "expected"),
[
("is defined as", "are not defined as"),
("is grounded in", "are not grounded in"),
("has the following steps", "do not have the following steps"),
("belongs to", "do not belong to"),
("supports", "do not support"),
],
)
def test_plural_negation_agrees_then_negates(display: str, expected: str) -> None:
"""Plural + negated: a plural copula takes a bare ``not``; anything else
needs do-support. Previously ``do not {base_form(phrase)}`` produced
"do not is defined a"."""
from generate.templates import _inflect_predicate
assert _inflect_predicate(display, negated=True, plural_subject=True) == expected
def test_base_form_is_a_single_verb_function_and_says_so() -> None:
"""The root cause, pinned directly: ``base_form`` operates on ONE verb.
It is still wrong on a phrase that is inherent, not a bug to fix here
which is exactly why the plural branch calls ``agree_plural_phrase``
instead. Pinning it stops someone "fixing" the symptom in the wrong place.
"""
from generate.morphology import agree_plural_phrase, base_form
assert base_form("causes") == "cause" # was "caus"
assert base_form("passes") == "pass" # the sibilant rule still fires
assert base_form("is defined as") == "is defined a" # single-verb fn on a phrase
assert agree_plural_phrase("is defined as") == "are defined as"
@pytest.mark.parametrize(
("singular", "plural"),
[("proof", "proofs"), ("chief", "chiefs"), ("roof", "roofs"),
("belief", "beliefs"), ("wolf", "wolves"), ("knife", "knives")],
)
def test_f_to_ves_is_a_closed_set_not_a_rule(singular: str, plural: str) -> None:
"""``f`` -> ``ves`` is not productive in English. Applying it as a suffix
rule made ``pluralize("proof")`` return "prooves", and Phase 2B had put
``pluralize`` on the serving path."""
from generate.morphology import pluralize as _pluralize
assert _pluralize(singular) == plural