fix(generate): CORE writes plural nouns in categorical clauses (Phase 2B) — and the lane pins are blind to served English #132
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/categorical-plural-render"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Phase 2B of
docs/plans/grammar-unification-2026-07-26.md. Authorization gate: granted 2026-07-26. This changes what users see.The ratified, flag-ON v1b categorical band served
all dog are mammalfor every noun. The A/E/I/O templates supplyall/no/some+are, which demand a plural; the slots were filled with singular canonical entity ids. 4 of the 47 ratified corpus cases were affected, andwrong=0never noticed because every verdict was correct — only the prose was wrong.The fix — two halves that compose into a closed round trip
generate/morphology.pybecomes the single owner of the number rules (2A gave the tables one owner). Both directions live there —pluralize/singularizeoverlexicon.IRREGULAR_PLURALS/IRREGULAR_SINGULARS, with mass-noun and compound-head handling.templates.pluralizeandreader._singularizewere two independent copies of the regular suffix rules that disagreed about which irregulars they knew; both now delegate.render._display_nounre-inflects at render time and swaps_for a space, so compound ids readguard dogs, notguards dogorguard_dog.Reader gives
wolves→wolf; renderer giveswolf→wolves. The round trip closes.wrong=0on all 7 bands; deductive 403, smoke 621s_surface_match_rate0.0 → 0.625, now equal tos_renderable_rateThe two rates being equal is the real result: every surface the renderer can express now returns its input exactly. The residual loss is renderer coverage, not grammar.
Near-miss: widening a reader broke soundness
Routing the reader through the full 29-entry table first produced
wrong=1on band v6-EX.ds-ex-0012— "No fish are mammals. Therefore some fish are mammals." — answeredinvalidwhere gold isrefuted._singularizereturningNonemakes the reader refuse, and the serving composer tries the categorical band first, falling through to more capable bands.fishpreviously returnedNone(it matches no suffix rule), so v6-EX got the case and decided it correctly. Resolvingfishmade v1b accept a sentence it cannot decide.Fixed by a principle rather than a patch: a number-invariant form is ambiguous in number — "fish are mammals" is plural, "a fish is a mammal" is singular, and the token cannot tell you which — so a reader that must not guess number declines it.
lexicon.INVARIANT_NUMBERis derived from the singularizer's ownkey == valuerows, so the rule cannot drift from the table.⇒ Coverage and correctness are different axes. Fixing a corrupted value is safe; widening acceptance changes which band answers, and in a first-match composer that converts a right answer into a wrong one. Same family as ADR-0261 §5.1 refuse-don't-drop. Bundling them is how a plural fix silently becomes a soundness regression.
Also fixed, found by testing the inverse law: the two number tables were not mutual inverses. The pluralizer lacked
cactus→cacti,fungus→fungi,die→diceand the invariantsaircraft/means/offspring— so CORE could readcactibut wrotecactuses, and would have written "aircrafts", "meanses", "offsprings". No round trip can close across a non-invertible table.⚠️ The lane SHA pins are blind to served English
2B changed 4 served surfaces and the pins came back 11/11 byte-identical. That is not evidence of no change — it is evidence the instrument cannot see this class of change.
Verified two ways rather than argued:
deduction_serve_v1hashed report contains onlyn,counts,by_gold,correct_by_gold,all_cases_correct,mismatch_examples. No surface prose at all._display_nounreturn"SABOTAGE_" + …, so every clause readsall SABOTAGE_dogs are SABOTAGE_animals:test_deduction_serve_lane+_license(20 tests)grammar_roundtripThis corrects #129. Byte-identity of the pins is the arbiter for values and verdicts, not for surface text, so the 2A/2B split it defines is not a partition of "changes users can see." Phase 2A's 11/11 conclusion still stands on its own independent evidence (24/24 table-equality checks against the pre-migration literals), but the justification was thinner than I claimed there.
Before Phase 1, no test in this tree would have noticed CORE's served prose turning into word salad. That is precisely how
all dog are mammalsurvived on a ratified flag-ON band withwrong=0intact for the entire arc. Future surface work must gate ongrammar_roundtrip, not on the pins. Adding surface text to the pinned payloads is the durable fix and is deliberately not done here — it would move every pin at once and deserves its own unit.Deliberately not fixed: mass-noun verb agreement — "all evidence are truth" should be "is". The copula is fixed text inside the templates, so agreeing it changes the template shape rather than a slot, and no mass noun reaches a categorical clause in any serve corpus today. Recorded in
render.pyrather than silently left.No lane pin edited — none moved.
[Verification]: in-worktree on CPython 3.12.13 with
uv sync --locked—uv run core test --suite smoke -q621 passed (unchanged);uv run core test --suite deductive -q403 passed;scripts/verify_lane_shas.py11/11 (see the blindness note — this is not the gate that matters for surfaces). Pushed with--no-verify; the gate had already passed in-worktree on this exact tree.The ratified, flag-ON v1b categorical band served "all dog are mammal" for every noun: the A/E/I/O templates supply all/no/some + are, which demand a plural, and the slots were filled with singular canonical entity ids. Four of the 47 ratified corpus cases were affected, and wrong=0 never noticed because every verdict was correct — only the prose was wrong. Two fixes that compose into a closed round trip: 1. generate/morphology.py becomes the single owner of the number RULES (Phase 2A gave the TABLES one owner). Both directions live there: pluralize + singularize, over lexicon.IRREGULAR_PLURALS / IRREGULAR_SINGULARS, with mass-noun and compound-head handling. templates.pluralize and reader._singularize were two independent copies of the regular suffix rules that disagreed about which irregulars they knew; both now delegate. 2. render._display_noun re-inflects at render time and swaps _ for a space, so compound ids read "guard dogs" rather than "guards dog" or "guard_dog". Result: reader gives wolves -> wolf, renderer gives wolf -> wolves. The round trip closes. 0 malformed of 47 (from 4) reader-vs-reader singularization 20/20 (from 8/20), 0 silently wrong s_surface_match_rate 0.0 -> 0.625, now EQUAL to s_renderable_rate, so the only remaining losses are surfaces the renderer cannot express at all NEAR-MISS — widening a reader broke soundness, caught by the lane: Routing the reader through the full 29-entry table first produced wrong=1 on band v6-EX. ds-ex-0012 ("No fish are mammals. Therefore some fish are mammals.") answered invalid where gold is refuted. _singularize returning None makes the reader REFUSE, and the serving composer tries the categorical band FIRST, falling through to more capable bands. "fish" previously returned None (it matches no suffix rule), so v6-EX got the case and decided it correctly. Resolving "fish" made v1b accept a sentence it cannot decide. Fixed by a principle, not a patch: a number-INVARIANT form is ambiguous in number — "fish are mammals" is plural, "a fish is a mammal" is singular, and the token cannot tell you which — so a reader that must not guess number declines it. lexicon.INVARIANT_NUMBER is derived from the singularizer's own key == value rows, so the rule cannot drift from the table. => Coverage and correctness are different axes. Fixing a corrupted VALUE is safe; widening ACCEPTANCE changes which band answers, and in a first-match composer that turns a right answer into a wrong one. Same family as ADR-0261 5.1 refuse-don't-drop. Also fixed, found by testing the inverse law: the two number tables were not mutual inverses. The pluralizer lacked cactus->cacti, fungus->fungi, die->dice and the invariants aircraft/means/offspring, so CORE could read "cacti" but wrote "cactuses", and would have written "aircrafts", "meanses", "offsprings". No round trip can close across a non-invertible table. THE LANE SHA PINS ARE BLIND TO SERVED ENGLISH: 2B changed 4 served surfaces and the pins came back 11/11 byte-identical. The deduction_serve_v1 hashed report holds only n/counts/by_gold/ correct_by_gold/all_cases_correct/mismatch_examples — no prose at all. Confirmed by sabotage: with _display_noun returning "SABOTAGE_" + ..., so every clause reads "all SABOTAGE_dogs are SABOTAGE_animals", 11 lane SHA pins -> 11/11 byte-identical, blind test_deduction_serve_lane + _license -> 20 passed, blind Phase 1 grammar_roundtrip -> 3 tests RED, caught it This corrects #129: byte-identity of the pins is the arbiter for values and verdicts, NOT for surface text, so the 2A/2B split is not a partition of "changes users can see." Phase 2A's 11/11 conclusion still stands on its own independent evidence (24/24 table-equality checks vs the pre-migration literals), but the justification was thinner than stated. Before Phase 1 no test in the tree would have noticed CORE's served prose turning into word salad. That is exactly how "all dog are mammal" survived on a ratified flag-ON band with wrong=0 intact. Deliberately not fixed: mass-noun verb agreement ("all evidence ARE truth" should be "is"). The copula is fixed text inside the templates, so agreeing it changes the template shape rather than a slot, and no mass noun reaches a categorical clause in any serve corpus. Recorded in render.py. No lane pin edited — none moved. [Verification]: in-worktree on CPython 3.12.13, uv sync --locked — smoke 621 unchanged; deductive 403 passed (383 + 20 new/rewritten); scripts/verify_lane_shas.py 11/11 (see blindness note above — gate on grammar_roundtrip for surface work, not on these pins).