fix(generate): CORE writes plural nouns in categorical clauses (Phase 2B) — and the lane pins are blind to served English #132

Merged
core-labs merged 1 commit from feat/categorical-plural-render into main 2026-07-27 01:48:11 +00:00
Owner

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 mammal for every noun. The A/E/I/O templates supply all/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, and wrong=0 never noticed because every verdict was correct — only the prose was wrong.

The fix — two halves that compose into a closed round trip

  1. generate/morphology.py becomes the single owner of the number rules (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, not guards dog or guard_dog.

Reader gives wolveswolf; renderer gives wolfwolves. The round trip closes.

criterion measured
surfaces well-formed 0 malformed of 47 (from 4)
readers agree 20/20, 0 silently wrong (from 8/20)
soundness wrong=0 on all 7 bands; deductive 403, smoke 621
round-trip s_surface_match_rate 0.0 → 0.625, now equal to s_renderable_rate

The 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=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 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_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 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→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. That is not evidence of no change — it is evidence the instrument cannot see this class of change.

Verified two ways rather than argued:

  1. By inspection — the deduction_serve_v1 hashed report contains only n, counts, by_gold, correct_by_gold, all_cases_correct, mismatch_examples. No surface prose at all.
  2. By sabotage — making _display_noun return "SABOTAGE_" + …, so every clause reads all SABOTAGE_dogs are SABOTAGE_animals:
guard caught it?
11 lane SHA pins no — 11/11 still byte-identical
test_deduction_serve_lane + _license (20 tests) no — 20 passed
Phase 1 grammar_roundtrip yes — 3 tests red

This 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 mammal survived on a ratified flag-ON band with wrong=0 intact for the entire arc. Future surface work must gate on grammar_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.py rather than silently left.

No lane pin edited — none moved.

[Verification]: in-worktree on CPython 3.12.13 with uv sync --lockeduv run core test --suite smoke -q 621 passed (unchanged); uv run core test --suite deductive -q 403 passed; scripts/verify_lane_shas.py 11/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.

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 mammal`** for every noun. The A/E/I/O templates supply `all`/`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, and `wrong=0` never noticed because every *verdict* was correct — only the prose was wrong. ## The fix — two halves that compose into a closed round trip 1. **`generate/morphology.py` becomes the single owner of the number *rules*** (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`, not `guards dog` or `guard_dog`. Reader gives `wolves` → `wolf`; renderer gives `wolf` → `wolves`. **The round trip closes.** | criterion | measured | |---|---| | surfaces well-formed | **0 malformed of 47** (from 4) | | readers agree | **20/20**, 0 silently wrong (from 8/20) | | soundness | `wrong=0` on all 7 bands; deductive **403**, smoke **621** | | round-trip | `s_surface_match_rate` **0.0 → 0.625**, now *equal* to `s_renderable_rate` | The 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=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 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_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 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→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**. That is not evidence of no change — it is evidence the instrument cannot see this class of change. Verified two ways rather than argued: 1. **By inspection** — the `deduction_serve_v1` hashed report contains only `n`, `counts`, `by_gold`, `correct_by_gold`, `all_cases_correct`, `mismatch_examples`. No surface prose at all. 2. **By sabotage** — making `_display_noun` return `"SABOTAGE_" + …`, so every clause reads `all SABOTAGE_dogs are SABOTAGE_animals`: | guard | caught it? | |---|---| | 11 lane SHA pins | **no — 11/11 still byte-identical** | | `test_deduction_serve_lane` + `_license` (20 tests) | **no — 20 passed** | | Phase 1 `grammar_roundtrip` | **yes — 3 tests red** | **This 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 mammal` survived on a ratified flag-ON band with `wrong=0` intact for the entire arc. Future surface work must gate on `grammar_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.py` rather 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 -q` **621 passed** (unchanged); `uv run core test --suite deductive -q` **403 passed**; `scripts/verify_lane_shas.py` **11/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.
core-labs added 1 commit 2026-07-27 01:43:31 +00:00
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).
core-labs merged commit a856687f5e into main 2026-07-27 01:48:11 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: core-labs/core#132
No description provided.