Skip to content

fix(inflekt): correct -ves singularization (derives -> derife) and audit rules against a 104k-word dictionary - #99

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

fix(inflekt): correct -ves singularization (derives -> derife) and audit rules against a 104k-word dictionary#99
pyramation merged 2 commits into
mainfrom
fix/inflekt-ves-singularization

Conversation

@pyramation

Copy link
Copy Markdown
Contributor

Summary

singularize('derives') returned derife (and olives -> olife, valves -> valf, solves -> solf, thieves -> thiefe), because inflekt delegated straight to the inflection npm package, whose Rails-derived rule is ([^fo])ves$ -> $1fe. That output is what named Derife in constructive-db's generated SDK/CLI.

The fix is a rule change, not a word list: -ves normally just drops the s, and the f/fe stems are the exception, so the rule is inverted and the exception is enumerated:

// src/rules.ts
GENERIC_SINGULAR_RULES = [..., [/ves$/i, 've']]   // derives -> derive, valves -> valve
F_STEM_PLURALS = [['knife','knives'], ['wolf','wolves'], ['leaf','leaves'], ...]  // both directions

Pluralization is symmetric: only an F_STEM_PLURALS word takes -ves; every other -f/-fe word takes a plain s (cafe -> cafes, roof -> roofs, belief -> beliefs), which inflection also got wrong.

inflekt's own rules now run before the upstream fallback (previously it only patched Latin suffixes and *base/ss afterwards), so upstream's wrong answers never surface. Compound identifiers transform only the final segment (user_derives -> user_derive, UserDerives -> UserDerive) and case is preserved.

Dictionary audit. Rather than fixing one word, singularize/pluralize were swept over the full 104k-word wamerican dictionary using the dictionary itself as the oracle (output must be a real word; plural→singular→plural must round-trip). That surfaced whole broken classes beyond -ves: aches -> ach, pies -> py, shoes -> sho, analyses -> analyasis, police -> polouse (the [m|l]ice character class also matches |), and -is/-us singulars (iris, analysis, bus, status) being stripped as if plural. Rule-level fixes cover the general cases; the residue that is genuinely lexical (cookies -> cookie not cooky, ties -> tie, invariants like jeans, gas, atlas) is a generated table:

  • scripts/generate-exceptions.ts reads /usr/share/dict/words, uses the committed __fixtures__/word-frequency-usa.txt to break dictionary ambiguities (cookie vs cooky), and emits a pair only where singularizeByRules(word) !== dictionaryExpected. It imports from src/rules.ts (suffix-only) rather than the public API — comparing against the exception-aware singularize made generation self-referential and idempotent-empty.
  • src/exceptions.ts is the output: 747 pairs, byte-identical across consecutive runs.

inflekt was not in the CI test matrix, so its tests never ran; added.

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

@pyramation
pyramation merged commit 320c5f6 into main Jul 31, 2026
53 checks passed
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