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

  1. Remove divergent accepted forms from the lexeme so all reachable forms collapse to one surface after normalization.
  2. If the noun genuinely has multiple forms, change the owner's capability from 'invariant' to a productive capability.
  3. 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

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


AI-assisted analysis of Humanizr/Humanizer@ffc2b77c0f (2026-08-13). Data as JSON: /api/errors/6d08fe99c532698d. Report an issue: GitHub.