{"record":{"id":"4ad90a0bccece853","repo":"Humanizr/Humanizer","slug":"inflection-subject-contains-a-duplicate-after-no","errorCode":null,"errorMessage":"Inflection {subject} contains a duplicate after normalization.","messagePattern":"Inflection (.+?) contains a duplicate after normalization\\.","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"src/Humanizer.SourceGenerators/Generators/ProfileCatalogs/InflectionCatalogValidation.cs","lineNumber":205,"sourceCode":"            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(\n                    value,\n                    casing,\n                    ownerScripts,\n                    subject,\n                    allowStemPlaceholder,\n                    allowNonLetters);\n                if (!seen.Add(form))\n                {\n                    throw new InvalidOperationException(\n                        $\"Inflection {subject} contains a duplicate after normalization.\");\n                }\n\n                normalized.Add(form);\n            }\n\n            return normalized.ToImmutable();\n        }\n\n        static string NormalizeAuthoredText(\n            string value,\n            string casing,\n            ImmutableArray<string> ownerScripts,\n            string subject,\n            bool allowStemPlaceholder,\n            bool allowNonLetters)\n        {\n            string normalized;","sourceCodeStart":187,"sourceCodeEnd":223,"githubUrl":"https://github.com/Humanizr/Humanizer/blob/ffc2b77c0f30d2fb176875841424379319d0ae9b/src/Humanizer.SourceGenerators/Generators/ProfileCatalogs/InflectionCatalogValidation.cs#L187-L223","documentation":"After NFC normalization and optional simple-lowercasing, all values within a single authored-text collection (e.g., accepted forms, guards) must be distinct. The validator builds a HashSet and rejects any value whose normalized form was already seen, preventing the generated catalog from carrying redundant entries that would collapse at runtime.","triggerScenarios":"Two values in the same list normalize to the same string after Unicode FormC normalization and case folding. For example, listing both a precomposed and decomposed form of the same character, or both 'cafe' and 'café' if they normalize identically in the configured casing mode.","commonSituations":"Adding alternative spellings that turn out to be Unicode-equivalent. Copy-pasting a form with different byte representations but identical normalization. Listing both title-case and lower-case variants under a case-folding casing mode.","solutions":["Remove the duplicate entry so each normalized form appears exactly once.","If both forms are genuinely needed, verify the casing mode is set to 'lower-title-upper' to keep them distinct only when they differ after lowercasing.","Check for invisible Unicode differences (combining marks, zero-width characters) that cause unexpected normalization collisions."],"exampleFix":"# before\naccepted:\n  - 'café'        # NFC precomposed\n  - 'cafe\\u0301'  # decomposed, same after NFC\n# after\naccepted:\n  - 'café'","handlingStrategy":"validation","validationCode":"# Before building, check for post-normalization duplicates in accepted/guard lists.\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        casing = owner.get('casing','')\n        for lex in (owner.get('lexemes') or []):\n            for field in ('singular','dictionaryPlural'):\n                vals = ((lex.get(field) or {}).get('accepted') or [])\n                seen = set()\n                for v in vals:\n                    n = unicodedata.normalize('NFC', v)\n                    if casing == 'lower-title-upper': n = n.lower()\n                    if n in seen:\n                        print(f'{f}: duplicate after normalization in {field}: {v!r}')\n                    seen.add(n)\n\" src/Humanizer/Locales/*.yml","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Normalize all YAML text to NFC before committing — use a pre-commit hook or editor setting.","Avoid listing both precomposed and decomposed forms of the same character.","Run a Unicode normalization diff check when adding alternative spellings."],"tags":["inflection","source-generator","normalization","unicode","duplicate","yaml","build-time"],"backgroundTag":null,"analyzedSha":"ffc2b77c0f30d2fb176875841424379319d0ae9b","analyzedAt":"2026-08-13T21:42:34.584Z","schemaVersion":2},"datasetVersion":"2026-08-14T00:17:13.853Z"}