Skip to content

fix(inflection): correct -ves singularization (derives -> derife) and audit the rule table - #108

Merged
pyramation merged 2 commits into
mainfrom
fix/inflection-ves-singularization
Jul 31, 2026
Merged

fix(inflection): correct -ves singularization (derives -> derife) and audit the rule table#108
pyramation merged 2 commits into
mainfrom
fix/inflection-ves-singularization

Conversation

@pyramation

@pyramation pyramation commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Summary

inflection.singular('derives') returned derife — which is what named Derife in constructive-db's generated SDK/CLI — because the rule table carries the Rails-derived ([^fo])ves$ -> \1fe. The rule is backwards: -ves normally just drops the s, and the f/fe stems are the exception. So the generic rule is inverted and the stems are enumerated, in inflection_rules/fixtures/1589249334312_fixture.sql:

-('singular', '([lr])ves$', E'\1f'),      -- valves -> valf, solves -> solf
-('singular', '([a])ves$',  E'\1ve'),
-('singular', '([^fo])ves$', E'\1fe'),    -- derives -> derife, olives -> olife
+('singular', 'selves$', E'self'), ('singular', 'knives$', E'knife'), ... ('singular', 'wolves$', E'wolf'),
+('singular', 'ves$', E've'),             -- derives -> derive, valves -> valve

The stems are matched as whole words / compound tails rather than bare suffix groups: a (li)ves$ group also fires on olives -> olife, (el)ves$ on delves -> delf, and (c)leaves$ on cleaves -> cleaf — hence the (^|[^o])lives$ / (^|[^c])leaves$ forms. Plural is symmetric: only a stem takes -ves, everything else falls through to the final $ -> s rule (cafe -> cafes, roof -> roofs, belief -> beliefs).

Three more bugs found while sweeping the table against the full 104k-word wamerican dictionary (dictionary as oracle: output must be a real word, and plural→singular→plural must round-trip):

  • ((a)naly|(b)a|...)ses$ -> \1\2sis — PostgreSQL numbers those nested groups differently from node.js inflection, so analyses -> analyasis. Flattened to (analy|ba|diagno|...)ses$ -> \1sis.
  • ([m|l])ice$ -> \1ouse — the character class also matches a literal |, so police -> polouse, service -> servouse, chalice -> chalouse. Anchored to ^(m|l|dorm|titm|woodl)ice$ with ice$ guarded as already-singular; ditto ([m|l])ouse$, which turned spouse -> spice and blouse -> blice.
  • No English plural ends in us, but s$ -> '' stripped them: census -> censu, radius -> radiu. Added a us$ guard, preceded by a rule for the -u nouns whose plural genuinely is -us (menus -> menu, haikus -> haiku), plus focuses -> focus and lenses -> lens.

The -is guard has to stay anchored. The same reasoning suggests a blanket ('singular', 'is$', NULL) + ('plural', 'is$', E'ises'), and that is true of English words but not of identifiers: constructive-db's platform schema has a table named apis, so those two rules stopped singular('apis') from reaching api and made plural('apis') = apises, renaming generated objects downstream (apis_pkey -> apises_pkey, tg_apis_catalog_sync -> tg_apises_catalog_sync). The plural rule is dropped; the singular guard is sis$ (analysis/basis/thesis/diagnosis) plus an anchored ^(iris|chassis|axis|oasis|...)$ word list, so no guard can ever suffix-match an identifier.

Rule placement matters generally: inflection.singular/plural loop over an unordered SELECT ... WHERE type=$1, so evaluation order is the fixture's insertion order — every guard above is inserted ahead of the generic fallback it must beat.

The matching JS fix is constructive-io/dev-utils#99 (inflekt, merged); over ~9.6k common words the two implementations agree on all but 41 singulars, each a lexical case only inflekt's dictionary-generated table can know (cookies -> cookie not cooky, invariants like jeans/gas/atlas) or an uncountable only the SQL side lists.

__tests__/inflection.test.ts goes 110 -> 151 cases, covering the -ves families in both directions, -ice, -is/-us, analyses, apis, and dotted identifiers (metaschema_public.derives). sql/pgpm-inflection--0.33.0.sql is regenerated with pgpm package; the version is untouched.

Consumer-side vendor sync (and the end-to-end check that generated platform modules no longer drift): constructive-io/constructive-db#2624.

Link to Devin session: https://app.devin.ai/sessions/60b2dc2dcb71403f9388d9785a9a78cf
Requested by: @pyramation

@pyramation pyramation self-assigned this Jul 31, 2026
@devin-ai-integration

Copy link
Copy Markdown

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

…till singularize

A blanket is$ guard kept "apis" from reaching "api", and plural 'is$ -> ises' turned it into "apises", renaming generated objects downstream.
@pyramation
pyramation merged commit d9ee0fd into main Jul 31, 2026
25 checks passed
@pyramation
pyramation deleted the fix/inflection-ves-singularization branch July 31, 2026 22:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant