{"record":{"id":"6d08fe99c532698d","repo":"Humanizr/Humanizer","slug":"invariant-inflection-lexeme-lexeme-id-has-dive","errorCode":null,"errorMessage":"Invariant inflection lexeme '{lexeme.Id}' has divergent reachable forms.","messagePattern":"Invariant inflection lexeme '(.+?)' has divergent reachable forms\\.","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"src/Humanizer.SourceGenerators/Generators/ProfileCatalogs/InflectionCatalogValidation.cs","lineNumber":178,"sourceCode":"            }\n\n            if (!rules.IsEmpty)\n            {\n                throw new InvalidOperationException(\n                    \"Invariant bundle cannot define productive rules.\");\n            }\n\n            foreach (var lexeme in lexemes)\n            {\n                var forms = lexeme.Singular.Accepted\n                    .Concat(lexeme.DictionaryPlural.Accepted)\n                    .Concat(lexeme.Display.Values.SelectMany(static form => form.Accepted))\n                    .Distinct(casing == \"lower-title-upper\"\n                        ? global::Humanizer.InflectionUnicodeData.SimpleCaseComparer.Instance\n                        : StringComparer.Ordinal);\n                if (forms.Skip(1).Any())\n                {\n                    throw new InvalidOperationException(\n                        $\"Invariant inflection lexeme '{lexeme.Id}' has divergent reachable forms.\");\n                }\n            }\n        }\n\n        static ImmutableArray<string> NormalizeAuthoredTexts(\n            ImmutableArray<string> values,\n            string casing,\n            ImmutableArray<string> ownerScripts,\n            string subject,\n            bool allowStemPlaceholder,\n            bool allowNonLetters)\n        {\n            var normalized = ImmutableArray.CreateBuilder<string>(values.Length);\n            var seen = new HashSet<string>(StringComparer.Ordinal);\n            foreach (var value in values)\n            {\n                var form = NormalizeAuthoredText(","sourceCodeStart":160,"sourceCodeEnd":196,"githubUrl":"https://github.com/Humanizr/Humanizer/blob/ffc2b77c0f30d2fb176875841424379319d0ae9b/src/Humanizer.SourceGenerators/Generators/ProfileCatalogs/InflectionCatalogValidation.cs#L160-L196","documentation":"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.","triggerScenarios":"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'.","commonSituations":"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.","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."],"exampleFix":"# before\nlexemes:\n  - id: 'deer'\n    singular:\n      accepted: ['deer', 'deers']\n    dictionaryPlural:\n      accepted: ['deer']\n# after\nlexemes:\n  - id: 'deer'\n    singular:\n      accepted: ['deer']\n    dictionaryPlural:\n      accepted: ['deer']","handlingStrategy":"validation","validationCode":"# Before building, verify invariant lexemes have no divergent reachable forms.\npython3 -c \"\nimport yaml, sys, unicodedata\nfor f in sys.argv[1:]:\n    d = yaml.safe_load(open(f))\n    owners = (d.get('inflection',{}).get('owners') or [])\n    for owner in owners:\n        if owner.get('capability') != 'invariant': continue\n        casing = owner.get('casing','')\n        for lex in (owner.get('lexemes') or []):\n            forms = set()\n            def collect(f):\n                for v in (f.get('accepted') or []):\n                    n = unicodedata.normalize('NFC', v)\n                    if casing == 'lower-title-upper': n = n.lower()\n                    forms.add(n)\n            collect(lex.get('singular') or {})\n            collect(lex.get('dictionaryPlural') or {})\n            for disp in (lex.get('display') or {}).values():\n                collect(disp)\n            if len(forms) > 1:\n                print(f'{f}: invariant lexeme {lex.get(\"id\",\"\")!r} has divergent forms: {forms}')\n\" src/Humanizer/Locales/*.yml","typeGuard":null,"tryCatchPattern":null,"preventionTips":["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."],"tags":["inflection","source-generator","invariant","lexeme","yaml","build-time"],"backgroundTag":null,"analyzedSha":"ffc2b77c0f30d2fb176875841424379319d0ae9b","analyzedAt":"2026-08-13T21:42:34.584Z","schemaVersion":2},"datasetVersion":"2026-08-14T00:17:13.853Z"}