Humanizr/Humanizer · error · InvalidOperationException
Invariant inflection lexeme '{lexeme.Id}' has divergent reac
Error message
Invariant inflection lexeme '{lexeme.Id}' has divergent reachable forms. What it means
For invariant inflection owners, every reachable form of a lexeme (singular accepted forms, dictionary plural accepted forms, and all display-category accepted forms) must collapse to the same surface after case-folding. The validator concatenates all accepted forms, deduplicates using SimpleCaseComparer (for lower-title-upper casing) or Ordinal, and rejects any lexeme where more than one distinct form survives.
Source
Thrown at src/Humanizer.SourceGenerators/Generators/ProfileCatalogs/InflectionCatalogValidation.cs:178
}
if (!rules.IsEmpty)
{
throw new InvalidOperationException(
"Invariant bundle cannot define productive rules.");
}
foreach (var lexeme in lexemes)
{
var forms = lexeme.Singular.Accepted
.Concat(lexeme.DictionaryPlural.Accepted)
.Concat(lexeme.Display.Values.SelectMany(static form => form.Accepted))
.Distinct(casing == "lower-title-upper"
? global::Humanizer.InflectionUnicodeData.SimpleCaseComparer.Instance
: StringComparer.Ordinal);
if (forms.Skip(1).Any())
{
throw new InvalidOperationException(
$"Invariant inflection lexeme '{lexeme.Id}' has divergent reachable forms.");
}
}
}
static ImmutableArray<string> NormalizeAuthoredTexts(
ImmutableArray<string> values,
string casing,
ImmutableArray<string> ownerScripts,
string subject,
bool allowStemPlaceholder,
bool allowNonLetters)
{
var normalized = ImmutableArray.CreateBuilder<string>(values.Length);
var seen = new HashSet<string>(StringComparer.Ordinal);
foreach (var value in values)
{
var form = NormalizeAuthoredText(View on GitHub (pinned to ffc2b77c0f)
Solutions
- Remove divergent accepted forms from the lexeme so all reachable forms collapse to one surface after normalization.
- If the noun genuinely has multiple forms, change the owner's capability from 'invariant' to a productive capability.
- Ensure the casing mode matches the owner's intent — 'lower-title-upper' allows case variants, ordinal does not.
Example fix
# before
lexemes:
- id: 'deer'
singular:
accepted: ['deer', 'deers']
dictionaryPlural:
accepted: ['deer']
# after
lexemes:
- id: 'deer'
singular:
accepted: ['deer']
dictionaryPlural:
accepted: ['deer'] Defensive patterns
Strategy: validation
Validate before calling
# Before building, verify invariant lexemes have no divergent reachable forms.
python3 -c "
import yaml, sys, unicodedata
for f in sys.argv[1:]:
d = yaml.safe_load(open(f))
owners = (d.get('inflection',{}).get('owners') or [])
for owner in owners:
if owner.get('capability') != 'invariant': continue
casing = owner.get('casing','')
for lex in (owner.get('lexemes') or []):
forms = set()
def collect(f):
for v in (f.get('accepted') or []):
n = unicodedata.normalize('NFC', v)
if casing == 'lower-title-upper': n = n.lower()
forms.add(n)
collect(lex.get('singular') or {})
collect(lex.get('dictionaryPlural') or {})
for disp in (lex.get('display') or {}).values():
collect(disp)
if len(forms) > 1:
print(f'{f}: invariant lexeme {lex.get("id","")!r} has divergent forms: {forms}')
" src/Humanizer/Locales/*.yml Prevention
- For invariant nouns, keep all accepted forms lists identical across singular, dictionaryPlural, and display.
- If a noun has a genuinely different plural, do not mark it as invariant.
- Test case folding behavior: 'lower-title-upper' casing collapses case differences, ordinal does not.
When it happens
Trigger: An invariant-capability lexeme has accepted forms that differ beyond case. For example, singular preferred 'fish' with an accepted variant 'fishes', or a display form 'FISH' alongside singular 'fish' with an ordinal comparer (non-case-folding). Only fires when capability is 'invariant'.
Common situations: Marking a noun as invariant when it actually has a distinct plural. Adding display-category forms that introduce a new surface variant. Using a non-case-folding casing mode so 'Fish' and 'fish' count as different forms.
Related errors
- Invariant bundle cannot define productive rules.
- Productive evidence '{direction}' requires at least 100 atte
- {subject} references unknown source '{source}'.
- Inflection source '{entry.Key}' must define non-empty kind a
- Inflection source '{entry.Key}' {optionalProperty} must be a
AI-assisted analysis of Humanizr/Humanizer@ffc2b77c0f (2026-08-13).
Data as JSON: /api/errors/6d08fe99c532698d.
Report an issue: GitHub.