From b39c75b7eac15070587387b45e546f951841518f Mon Sep 17 00:00:00 2001 From: HyperCriSiS Date: Sun, 2 Aug 2026 01:16:07 +0200 Subject: [PATCH 1/2] Regeln: sichere Genderformen gezielt erweitern --- CHANGELOG.md | 16 ++ data/flexion-regression-cases.json | 24 +++ src/rules/singular-context-guard.ts | 103 ++++++++++++ src/rules/singular-context.ts | 130 ++++++++++++++- src/rules/singular-double-forms.ts | 2 +- src/rules/source-audit-person-forms.ts | 153 +++++++++++++++--- src/rules/special-gender-forms.ts | 1 + src/rules/substantivized-adjectives.ts | 4 + src/rules/unmarked-singular.ts | 38 +---- tests/rules/known-plural-separators.test.ts | 152 ++++++++++++++++- tests/rules/singular-context.test.ts | 19 ++- tests/rules/singular-double-forms.test.ts | 2 + tests/rules/special-gender-forms.test.ts | 2 + tests/rules/substantivized-adjectives.test.ts | 1 + tests/rules/unmarked-singular.test.ts | 3 + 15 files changed, 591 insertions(+), 59 deletions(-) create mode 100644 src/rules/singular-context-guard.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 65c1c45..25b5a36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,22 @@ dokumentiert. ## Unveröffentlicht +### Hinzugefügt + +- 90 einzeln geprüfte Personenbezeichnungen mit sicheren Singular- und + Pluralzielen, darunter unveränderte Plurale, schwache Flexionen, + Sonderplurale und Binnen-I-Singularformen. +- Sichere Artikel-Adjektiv-Ketten wie `der:die neue Lehrer:in`, + `den:die fleißige:n Schüler:in` und + `ein*e gut ausgebildete*r Jurist*in`. +- Weitere explizite Formen wie `jene:r Nutzer:in`, `Arzt:Ärztin`, + `Kolleg:en:innen`, `Pat*innenschaft` und `Themenpat*in`. + +### Korrigiert + +- Unvollständige Teilkorrekturen hinter femininen oder markierten Artikeln + werden auch bei zwischengeschalteten Adjektiven verhindert. + ## 0.6.1 – RC2 (2026-07-29) ### Korrigiert diff --git a/data/flexion-regression-cases.json b/data/flexion-regression-cases.json index 3807328..45baa3b 100644 --- a/data/flexion-regression-cases.json +++ b/data/flexion-regression-cases.json @@ -76,12 +76,36 @@ { "id": "lexicon-correspondents", "source": "curated-lexicon", "input": "Korrespondent:innen", "expected": "Korrespondenten" }, { "id": "lexicon-parliamentarians", "source": "curated-lexicon", "input": "Parlamentarier:innen", "expected": "Parlamentarier" }, { "id": "lexicon-chancellors", "source": "curated-lexicon", "input": "Bundeskanzler:innen", "expected": "Bundeskanzler" }, + { "id": "lexicon-validated-binnen-i", "source": "curated-lexicon", "input": "VerteidigerIn", "expected": "Verteidiger" }, + { "id": "lexicon-validated-irregular-plural", "source": "curated-lexicon", "input": "Steinmetz:innen", "expected": "Steinmetze" }, + { "id": "lexicon-validated-english-plural", "source": "curated-lexicon", "input": "Coach:innen", "expected": "Coaches" }, + { "id": "lexicon-validated-weak-plural", "source": "curated-lexicon", "input": "Rassist:innen", "expected": "Rassisten" }, + { "id": "lexicon-patenschaft-compound", "source": "curated-lexicon", "input": "Pat*innenschaft", "expected": "Patenschaft" }, + { "id": "lexicon-themenpate", "source": "curated-lexicon", "input": "Themenpat*in", "expected": "Themenpate" }, + + { "id": "syntax-jene-determiner", "source": "curated-lexicon", "input": "jene:r Nutzer:in", "expected": "jener Nutzer" }, + { "id": "syntax-attributive-nominative", "source": "curated-lexicon", "input": "der:die neue Lehrer:in", "expected": "der neue Lehrer" }, + { "id": "syntax-attributive-accusative", "source": "curated-lexicon", "input": "den:die fleißige:n Schüler:in", "expected": "den fleißigen Schüler" }, + { "id": "syntax-attributive-multiword", "source": "curated-lexicon", "input": "ein*e gut ausgebildete*r Jurist*in", "expected": "ein gut ausgebildeter Jurist" }, + { "id": "syntax-full-stem-colon-pair", "source": "curated-lexicon", "input": "der:die Arzt:Ärztin", "expected": "der Arzt" }, + { "id": "syntax-duplicated-colleague-ending", "source": "curated-lexicon", "input": "Kolleg:en:innen", "expected": "Kollegen" }, + + { "id": "compound-readership", "source": "curated-lexicon", "input": "Leser*innenschaft", "expected": "Leserschaft" }, + { "id": "compound-citizen-municipality", "source": "curated-lexicon", "input": "Bürger*innenkommune", "expected": "Bürgerkommune" }, + { "id": "compound-migrant-organization", "source": "curated-lexicon", "input": "Migrant*innenorganisation", "expected": "Migrantenorganisation" }, + { "id": "compound-worker-protection", "source": "curated-lexicon", "input": "ArbeitnehmerInnenschutz", "expected": "Arbeitnehmerschutz" }, { "id": "negative-feminine-politicians", "source": "field-reports", "input": "Politikerinnen", "expected": "Politikerinnen" }, { "id": "negative-ambiguous-inline-binnen-i", "source": "field-reports", "input": "Vielleicht eine NutzerIn im Team", "expected": "Vielleicht eine NutzerIn im Team" }, { "id": "negative-adjective-use", "source": "duden-declension", "input": "erwachsene Kinder", "expected": "erwachsene Kinder" }, { "id": "negative-wrong-adjective-marker", "source": "duden-declension", "input": "ein:e Erwachsene:n", "expected": "ein:e Erwachsene:n" }, { "id": "negative-robot", "source": "curated-lexicon", "input": "Robot:innen", "expected": "Robot:innen" }, + { "id": "negative-plain-validated-feminine", "source": "curated-lexicon", "input": "Verteidigerin", "expected": "Verteidigerin" }, + { "id": "negative-uppercase-feminine", "source": "curated-lexicon", "input": "VERTEIDIGERIN", "expected": "VERTEIDIGERIN" }, + { "id": "negative-attributive-wrong-ending", "source": "curated-lexicon", "input": "eine:n neue Lehrer:in", "expected": "eine:n neue Lehrer:in" }, + { "id": "negative-unpaired-colon-form", "source": "curated-lexicon", "input": "Arzt:Arzt", "expected": "Arzt:Arzt" }, + { "id": "negative-incomplete-colleague-ending", "source": "curated-lexicon", "input": "Kolleg:en", "expected": "Kolleg:en" }, + { "id": "negative-ordinary-patenschaft", "source": "curated-lexicon", "input": "Patenschaft", "expected": "Patenschaft" }, { "id": "negative-plain-romnja", "source": "sinti-roma-zentralrat", "input": "Romnja", "expected": "Romnja" }, { "id": "negative-plain-sintizze", "source": "sinti-roma-zentralrat", "input": "Sintizze", "expected": "Sintizze" }, { "id": "negative-ordinary-hobbys", "source": "phettberg-bpb", "input": "Hobbys und Handys", "expected": "Hobbys und Handys" } diff --git a/src/rules/singular-context-guard.ts b/src/rules/singular-context-guard.ts new file mode 100644 index 0000000..649a67b --- /dev/null +++ b/src/rules/singular-context-guard.ts @@ -0,0 +1,103 @@ +const locale = "de-DE"; +const plainAmbiguousDeterminers = new Set([ + "die", + "eine", + "einer", + "ihre", + "ihrer", + "jene", + "jener", + "jede", + "keine", + "keiner", + "meine", + "meiner", + "seine", + "seiner", + "deine", + "deiner", + "diese", + "dieser", + "unsere", + "unserer", + "eure", + "eurer", + "welche", + "welcher" +]); +const markedDeterminerPrefixes = new Set([ + "der", + "die", + "den", + "dem", + "des", + "ein", + "eine", + "einem", + "eines", + "kein", + "keine", + "keines", + "jede", + "jedes", + "welche", + "welches", + "diese", + "dieses", + "jene", + "jenes", + "mein", + "meine", + "meines", + "dein", + "deine", + "deines", + "sein", + "seine", + "seinen", + "seinem", + "seines", + "seiner", + "ihr", + "ihre", + "ihren", + "ihrem", + "ihres", + "ihrer", + "unser", + "unsere", + "unseres", + "euer", + "eure", + "eures" +]); +const separatorPattern = /[:*_\/·•.’‘]/u; +const precedingTokensPattern = + /([\p{L}\p{M}:*_\/·•.’‘-]+(?:\s+[\p{L}\p{M}:*_\/·•.’‘-]+){0,3})\s+$/u; + +function isAmbiguousDeterminer(token: string): boolean { + const normalized = token.toLocaleLowerCase(locale); + if (plainAmbiguousDeterminers.has(normalized)) { + return true; + } + + const separatorIndex = normalized.search(separatorPattern); + return ( + separatorIndex > 0 && + markedDeterminerPrefixes.has(normalized.slice(0, separatorIndex)) + ); +} + +export function hasAmbiguousSingularDeterminer( + input: string, + nounIndex: number +): boolean { + const prefix = input.slice(0, nounIndex); + const match = precedingTokensPattern.exec(prefix); + const tokens = match?.[1]; + return Boolean( + tokens + ?.split(/\s+/u) + .some((token) => isAmbiguousDeterminer(token)) + ); +} diff --git a/src/rules/singular-context.ts b/src/rules/singular-context.ts index ed2766b..5866341 100644 --- a/src/rules/singular-context.ts +++ b/src/rules/singular-context.ts @@ -82,6 +82,10 @@ addDeterminer("diese", "r", "dieser", "nominative"); addDeterminer("diese", "n", "diesen", "accusative"); addDeterminer("diese", "m", "diesem", "dative"); addDeterminer("dieses", "dieser", "dieses", "genitive"); +addDeterminer("jene", "r", "jener", "nominative"); +addDeterminer("jene", "n", "jenen", "accusative"); +addDeterminer("jene", "m", "jenem", "dative"); +addDeterminer("jenes", "jener", "jenes", "genitive"); addPossessiveDeterminers("mein"); addPossessiveDeterminers("dein"); @@ -112,6 +116,16 @@ const binnenISingularPattern = new RegExp( String.raw`(?(); const nominativeFeminineDeterminers = new Map(); @@ -175,6 +189,14 @@ addOrdinaryDeterminerSet( "diese", "dieser" ); +addOrdinaryDeterminerSet( + "jener", + "jenen", + "jenem", + "jenes", + "jene", + "jener" +); addOrdinaryDeterminerSet("mein", "meinen", "meinem", "meines", "meine", "meiner"); addOrdinaryDeterminerSet("dein", "deinen", "deinem", "deines", "deine", "deiner"); addOrdinaryDeterminerSet("sein", "seinen", "seinem", "seines", "seine", "seiner"); @@ -374,6 +396,99 @@ function transformPattern(input: string, pattern: RegExp): TransformResult { return { text, replacements }; } +const strongNominativeDeterminers = new Set([ + "ein", + "kein", + "mein", + "dein", + "sein", + "ihr", + "unser", + "euer" +]); + +function expectedAdjectiveEnding( + form: DeterminerForm +): "e" | "en" | "er" { + if (form.grammaticalCase !== "nominative") { + return "en"; + } + + return strongNominativeDeterminers.has(form.masculine) ? "er" : "e"; +} + +function mapAttributiveAdjective( + adjective: string, + markerEnding: string | undefined, + expectedEnding: "e" | "en" | "er" +): string | undefined { + const normalized = adjective.toLocaleLowerCase(locale); + + if (!markerEnding) { + return normalized.endsWith(expectedEnding) ? adjective : undefined; + } + + const normalizedMarkerEnding = markerEnding.toLocaleLowerCase(locale); + const expectedMarkerEnding = expectedEnding === "er" ? "r" : "n"; + if ( + expectedEnding === "e" || + normalizedMarkerEnding !== expectedMarkerEnding || + !normalized.endsWith("e") + ) { + return undefined; + } + + return applyTokenCase(adjective, normalized + expectedMarkerEnding); +} + +function transformAttributivePhrases( + input: string, + pattern: RegExp +): TransformResult { + let replacements = 0; + const text = input.replace( + pattern, + ( + match: string, + determiner: string, + firstWhitespace: string, + modifiers: string, + adjective: string, + _marker: string | undefined, + markerEnding: string | undefined, + secondWhitespace: string, + base: string + ) => { + const form = determinerForms.get(determiner.toLocaleLowerCase(locale)); + if (!form) { + return match; + } + + const mappedAdjective = mapAttributiveAdjective( + adjective, + markerEnding, + expectedAdjectiveEnding(form) + ); + const noun = mapSingular(base, form.grammaticalCase); + if (!mappedAdjective || !noun) { + return match; + } + + replacements += 1; + return ( + applyTokenCase(determiner, form.masculine) + + firstWhitespace + + modifiers + + mappedAdjective + + secondWhitespace + + noun + ); + } + ); + + return { text, replacements }; +} + function transformOrdinaryMasculineDeterminers(input: string): TransformResult { let replacements = 0; const text = input.replace( @@ -537,7 +652,18 @@ export const singularContextRule: Rule = { risk: "safe", apply(input) { - const separatorResult = transformPattern(input, separatorSingularPattern); + const attributiveSeparatorResult = transformAttributivePhrases( + input, + attributiveSeparatorSingularPattern + ); + const attributiveBinnenIResult = transformAttributivePhrases( + attributiveSeparatorResult.text, + attributiveBinnenISingularPattern + ); + const separatorResult = transformPattern( + attributiveBinnenIResult.text, + separatorSingularPattern + ); const markedBinnenIResult = transformPattern( separatorResult.text, binnenISingularPattern @@ -581,6 +707,8 @@ export const singularContextRule: Rule = { return { text: ordinaryResult.text, replacements: + attributiveSeparatorResult.replacements + + attributiveBinnenIResult.replacements + separatorResult.replacements + markedBinnenIResult.replacements + accusativeResult.replacements + diff --git a/src/rules/singular-double-forms.ts b/src/rules/singular-double-forms.ts index 7638fc0..ab9a669 100644 --- a/src/rules/singular-double-forms.ts +++ b/src/rules/singular-double-forms.ts @@ -17,7 +17,7 @@ interface DeterminerPair { const locale = "de-DE"; const word = String.raw`[\p{L}\p{M}’'-]+`; -const connector = String.raw`(?:\s*(?:/|&)\s*|\s+(?:und|oder|bzw\.|beziehungsweise)\s+)`; +const connector = String.raw`(?:\s*(?:/|&|:)\s*|\s+(?:und|oder|bzw\.|beziehungsweise)\s+)`; const singularDoubleFormPattern = new RegExp( String.raw`(? right.stem.length - left.stem.length); function applyCase(source: string, replacement: string): string { @@ -380,18 +469,34 @@ function mapAllSingular(base: string): string | undefined { function transformPattern( input: string, pattern: RegExp, - mapper: (base: string) => string | undefined + mapper: (base: string) => string | undefined, + protectAmbiguousDeterminer = false ): TransformResult { let replacements = 0; - const text = input.replace(pattern, (match: string, base: string) => { - const replacement = mapper(base); - if (replacement === undefined) { - return match; - } + const text = input.replace( + pattern, + ( + match: string, + base: string, + offset: number, + fullInput: string + ) => { + if ( + protectAmbiguousDeterminer && + hasAmbiguousSingularDeterminer(fullInput, offset) + ) { + return match; + } + + const replacement = mapper(base); + if (replacement === undefined) { + return match; + } - replacements += 1; - return replacement; - }); + replacements += 1; + return replacement; + } + ); return { text, replacements }; } @@ -411,6 +516,8 @@ function combine(input: string, transforms: readonly ((text: string) => Transfor const supplementalBinnenIPluralPattern = /(? + transformPattern( + text, + supplementalBinnenISingularPattern, + mapSupplementalSingular, + true ) ]); } diff --git a/src/rules/special-gender-forms.ts b/src/rules/special-gender-forms.ts index a859d02..c2fe570 100644 --- a/src/rules/special-gender-forms.ts +++ b/src/rules/special-gender-forms.ts @@ -8,6 +8,7 @@ const replacements = new Map([ ["sinti*zze", "sinti"], ["sinti:zze", "sinti"], ["sinti_zze", "sinti"], + ["kolleg:en:innen", "kollegen"], ["studentys", "studenten"], ["lesys", "leser"], ["lehrys", "lehrer"], diff --git a/src/rules/substantivized-adjectives.ts b/src/rules/substantivized-adjectives.ts index 0f48cb4..118e08b 100644 --- a/src/rules/substantivized-adjectives.ts +++ b/src/rules/substantivized-adjectives.ts @@ -128,6 +128,10 @@ addDeterminer("diese", "r", "dieser", "nominative", false); addDeterminer("diese", "n", "diesen", "accusative", false); addDeterminer("diese", "m", "diesem", "dative", false); addDeterminer("dieses", "dieser", "dieses", "genitive", false); +addDeterminer("jene", "r", "jener", "nominative", false); +addDeterminer("jene", "n", "jenen", "accusative", false); +addDeterminer("jene", "m", "jenem", "dative", false); +addDeterminer("jenes", "jener", "jenes", "genitive", false); function addPossessive(base: string, feminineBase = `${base}e`): void { addDeterminer(base, "e", base, "nominative", true); diff --git a/src/rules/unmarked-singular.ts b/src/rules/unmarked-singular.ts index e287bf5..5ec73f4 100644 --- a/src/rules/unmarked-singular.ts +++ b/src/rules/unmarked-singular.ts @@ -1,6 +1,7 @@ import type { Rule, TransformResult } from "../core/rule"; import { mapKnownSingular } from "./known-plural-separators"; import { mapMappedSingular } from "./person-lexicon"; +import { hasAmbiguousSingularDeterminer } from "./singular-context-guard"; const locale = "de-DE"; const markerPattern = @@ -16,30 +17,6 @@ const additionalSingularForms = new Map([ ["pat", "pate"], ["solist", "solist"] ]); -const ambiguousFeminineDeterminers = new Set([ - "die", - "eine", - "einer", - "ihre", - "ihrer", - "jede", - "keine", - "keiner", - "meine", - "meiner", - "seine", - "seiner", - "deine", - "deiner", - "diese", - "dieser", - "unsere", - "unserer", - "eure", - "eurer", - "welche", - "welcher" -]); function applyTokenCase(source: string, replacement: string): string { const lowerSource = source.toLocaleLowerCase(locale); @@ -86,15 +63,6 @@ function transformMarkedAdjectives(input: string): TransformResult { return { text, replacements }; } -function hasAmbiguousFeminineDeterminer(input: string, index: number): boolean { - const prefix = input.slice(0, index); - const match = /([\p{L}\p{M}]+)\s+$/u.exec(prefix); - return Boolean( - match?.[1] && - ambiguousFeminineDeterminers.has(match[1].toLocaleLowerCase(locale)) - ); -} - function transformMarkerPattern( input: string, pattern: RegExp, @@ -106,7 +74,7 @@ function transformMarkerPattern( (match: string, base: string, offset: number, fullInput: string) => { if ( protectAmbiguousDeterminer && - hasAmbiguousFeminineDeterminer(fullInput, offset) + hasAmbiguousSingularDeterminer(fullInput, offset) ) { return match; } @@ -133,7 +101,7 @@ export const unmarkedSingularRule: Rule = { const separatorResult = transformMarkerPattern( adjectiveResult.text, markerPattern, - false + true ); const binnenIResult = transformMarkerPattern( separatorResult.text, diff --git a/tests/rules/known-plural-separators.test.ts b/tests/rules/known-plural-separators.test.ts index 183286b..6cec56b 100644 --- a/tests/rules/known-plural-separators.test.ts +++ b/tests/rules/known-plural-separators.test.ts @@ -5,6 +5,99 @@ import { sourceAuditSingularRule } from "../../src/rules/source-audit-person-forms"; +const validatedSupplementalForms = [ + { singular: "Abenteurer", plural: "Abenteurer" }, + { singular: "Alleinverdiener", plural: "Alleinverdiener" }, + { singular: "Altenpfleger", plural: "Altenpfleger" }, + { singular: "Amateur", plural: "Amateure" }, + { singular: "Anforderer", plural: "Anforderer" }, + { singular: "Anhänger", plural: "Anhänger" }, + { singular: "Anteilseigner", plural: "Anteilseigner" }, + { singular: "Antragsteller", plural: "Antragsteller" }, + { singular: "Anwärter", plural: "Anwärter" }, + { singular: "Auftragnehmer", plural: "Auftragnehmer" }, + { singular: "Augenoptiker", plural: "Augenoptiker" }, + { singular: "Ausländer", plural: "Ausländer" }, + { singular: "Außenseiter", plural: "Außenseiter" }, + { singular: "Aussteiger", plural: "Aussteiger" }, + { singular: "Befrager", plural: "Befrager" }, + { singular: "Befürworter", plural: "Befürworter" }, + { singular: "Beisitzer", plural: "Beisitzer" }, + { singular: "Berliner", plural: "Berliner" }, + { singular: "Bestatter", plural: "Bestatter" }, + { singular: "Betrüger", plural: "Betrüger" }, + { singular: "Bewahrer", plural: "Bewahrer" }, + { singular: "Bezieher", plural: "Bezieher" }, + { singular: "Bieter", plural: "Bieter" }, + { singular: "Botschafter", plural: "Botschafter" }, + { singular: "Bürokrat", plural: "Bürokraten" }, + { singular: "Chauffeur", plural: "Chauffeure" }, + { singular: "Chemiker", plural: "Chemiker" }, + { singular: "Coach", plural: "Coaches" }, + { singular: "Dekan", plural: "Dekane" }, + { singular: "Detektiv", plural: "Detektive" }, + { singular: "Diabetiker", plural: "Diabetiker" }, + { singular: "Dichter", plural: "Dichter" }, + { singular: "Dieb", plural: "Diebe" }, + { singular: "Doppelgänger", plural: "Doppelgänger" }, + { singular: "Editor", plural: "Editoren" }, + { singular: "Ehrenamtler", plural: "Ehrenamtler" }, + { singular: "Einbrecher", plural: "Einbrecher" }, + { singular: "Eigner", plural: "Eigner" }, + { singular: "Einwohner", plural: "Einwohner" }, + { singular: "Einzelgänger", plural: "Einzelgänger" }, + { singular: "Elektriker", plural: "Elektriker" }, + { singular: "Elektroniker", plural: "Elektroniker" }, + { singular: "Erbauer", plural: "Erbauer" }, + { singular: "Erblasser", plural: "Erblasser" }, + { singular: "Favorit", plural: "Favoriten" }, + { singular: "Fahrzeughalter", plural: "Fahrzeughalter" }, + { singular: "Feind", plural: "Feinde" }, + { singular: "Förster", plural: "Förster" }, + { singular: "Fußgänger", plural: "Fußgänger" }, + { singular: "Gesellschafter", plural: "Gesellschafter" }, + { singular: "Gestalter", plural: "Gestalter" }, + { singular: "Gläubiger", plural: "Gläubiger" }, + { singular: "Herrscher", plural: "Herrscher" }, + { singular: "Inspekteur", plural: "Inspekteure" }, + { singular: "Kläger", plural: "Kläger" }, + { singular: "Kontrahent", plural: "Kontrahenten" }, + { singular: "Krankenpfleger", plural: "Krankenpfleger" }, + { singular: "Kritiker", plural: "Kritiker" }, + { singular: "Läufer", plural: "Läufer" }, + { singular: "Masseur", plural: "Masseure" }, + { singular: "Muslim", plural: "Muslime" }, + { singular: "Nachahmer", plural: "Nachahmer" }, + { singular: "Nachfolger", plural: "Nachfolger" }, + { singular: "Peiniger", plural: "Peiniger" }, + { singular: "Pfleger", plural: "Pfleger" }, + { singular: "Prediger", plural: "Prediger" }, + { singular: "Priester", plural: "Priester" }, + { singular: "Rassist", plural: "Rassisten" }, + { singular: "Schlepper", plural: "Schlepper" }, + { singular: "Schmied", plural: "Schmiede" }, + { singular: "Schulabbrecher", plural: "Schulabbrecher" }, + { singular: "Schulabgänger", plural: "Schulabgänger" }, + { singular: "Schuldner", plural: "Schuldner" }, + { singular: "Seelsorger", plural: "Seelsorger" }, + { singular: "Späher", plural: "Späher" }, + { singular: "Spion", plural: "Spione" }, + { singular: "Stakeholder", plural: "Stakeholder" }, + { singular: "Steinmetz", plural: "Steinmetze" }, + { singular: "Störer", plural: "Störer" }, + { singular: "Supporter", plural: "Supporter" }, + { singular: "Tänzer", plural: "Tänzer" }, + { singular: "Tierschützer", plural: "Tierschützer" }, + { singular: "Unterzeichner", plural: "Unterzeichner" }, + { singular: "Urheber", plural: "Urheber" }, + { singular: "Urlauber", plural: "Urlauber" }, + { singular: "User", plural: "User" }, + { singular: "Veganer", plural: "Veganer" }, + { singular: "Vegetarier", plural: "Vegetarier" }, + { singular: "Verbrecher", plural: "Verbrecher" }, + { singular: "Verteidiger", plural: "Verteidiger" } +] as const; + describe("knownPluralSeparatorsRule", () => { it("entfernt verbreitete Separatoren bei sicheren Pluralformen", () => { const result = knownPluralSeparatorsRule.apply( @@ -76,6 +169,16 @@ describe("knownPluralSeparatorsRule", () => { }); describe("sourceAuditPluralRule", () => { + it.each(validatedSupplementalForms)( + "normalisiert $singular als einzeln geprüften Plural", + ({ singular, plural }) => { + expect(sourceAuditPluralRule.apply(`${singular}:innen`)).toEqual({ + text: plural, + replacements: 1 + }); + } + ); + it.each([ ["Follower*innen", "Follower"], ["Praktiker*innen", "Praktiker"], @@ -140,6 +243,26 @@ describe("sourceAuditPluralRule", () => { }); describe("sourceAuditSingularRule", () => { + it.each(validatedSupplementalForms)( + "normalisiert $singular als einzeln geprüften Singular", + ({ singular }) => { + expect(sourceAuditSingularRule.apply(`${singular}:in`)).toEqual({ + text: singular, + replacements: 1 + }); + } + ); + + it.each(validatedSupplementalForms)( + "normalisiert $singular als einzeln geprüften Binnen-I-Singular", + ({ singular }) => { + expect(sourceAuditSingularRule.apply(`${singular}In`)).toEqual({ + text: singular, + replacements: 1 + }); + } + ); + it.each([ ["Follower*in", "Follower"], ["Proband:in", "Proband"], @@ -154,12 +277,37 @@ describe("sourceAuditSingularRule", () => { }); }); - it("schützt normale Feminina, Binnen-I-Singularformen und unbekannte Markerformen", () => { + it("schützt normale Feminina und unbekannte Markerformen", () => { const input = - "Followerin, Köchin, eine FollowerIn, LogopädIn und Fantasiefigur*in"; + "Followerin, Köchin, Fantasiefigur*in und FantasiefigurIn"; expect(sourceAuditSingularRule.apply(input)).toEqual({ text: input, replacements: 0 }); }); + + it.each([ + "eine:n neue Verteidiger:in", + "die neue Verteidiger:in", + "eine:n neue VerteidigerIn", + "die neue VerteidigerIn" + ])("vermeidet eine grammatisch unvollständige Teilkorrektur bei %s", (input) => { + expect(sourceAuditSingularRule.apply(input)).toEqual({ + text: input, + replacements: 0 + }); + }); + + it.each([ + ["Pat*innenschaft", "Patenschaft"], + ["Themenpat*in", "Themenpate"] + ])("normalisiert das geprüfte Kompositum %s", (input, expected) => { + const rule = input.includes("innenschaft") + ? sourceAuditPluralRule + : sourceAuditSingularRule; + expect(rule.apply(input)).toEqual({ + text: expected, + replacements: 1 + }); + }); }); diff --git a/tests/rules/singular-context.test.ts b/tests/rules/singular-context.test.ts index a509ac8..d01c1ae 100644 --- a/tests/rules/singular-context.test.ts +++ b/tests/rules/singular-context.test.ts @@ -22,6 +22,10 @@ describe("singularContextRule", () => { ["keine:m Kund:in", "keinem Kunden"], ["welche:r Kolleg:in", "welcher Kollege"], ["diese:n Pilot:in", "diesen Piloten"], + ["jene:r Nutzer:in", "jener Nutzer"], + ["jene:n Student:in", "jenen Studenten"], + ["jene:m Kund:in", "jenem Kunden"], + ["jenes:jener Bürger:in", "jenes Bürgers"], ["der:die NutzerIn", "der Nutzer"], ["jede:r Tierärzt:in", "jeder Tierarzt"], ["eine:n KoautorIn", "einen Koautor"], @@ -41,6 +45,7 @@ describe("singularContextRule", () => { ["für eine ÄrztIn", "für einen Arzt"], ["wegen einer StudentIn", "wegen eines Studenten"], ["Eine NutzerIn arbeitet heute.", "Ein Nutzer arbeitet heute."], + ["Jene NutzerIn arbeitet heute.", "Jener Nutzer arbeitet heute."], ["eine NutzerIn", "ein Nutzer"], ["Ich sehe eine NutzerIn.", "Ich sehe einen Nutzer."], ["Wir suchen eine StudentIn.", "Wir suchen einen Studenten."], @@ -48,6 +53,12 @@ describe("singularContextRule", () => { ["Das ist eine NutzerIn.", "Das ist ein Nutzer."], ["sein:ihr Nutzer:in", "sein Nutzer"], ["ihrem:seinem Student:in", "seinem Studenten"], + ["der:die neue Lehrer:in", "der neue Lehrer"], + ["den:die fleißige:n Schüler:in", "den fleißigen Schüler"], + ["ein*e gut ausgebildete*r Jurist*in", "ein gut ausgebildeter Jurist"], + ["eine:n neue:n Student:in", "einen neuen Studenten"], + ["dem:der neuen LehrerIn", "dem neuen Lehrer"], + ["des:der neuen Nutzer:in", "des neuen Nutzers"], ["JEDE:R NUTZER:IN", "JEDER NUTZER"], ["DES:DER NUTZER:IN", "DES NUTZERS"] ])("wandelt %s in %s um", (input, expected) => { @@ -71,6 +82,7 @@ describe("singularContextRule", () => { it.each([ "NutzerIn", "Vielleicht eine NutzerIn im Team", + "Vielleicht jene NutzerIn im Team", "jede:r Innenstadt", "ein:e Ausgabe", "eine:n Kundin", @@ -78,7 +90,12 @@ describe("singularContextRule", () => { "mein:e Hebamme:in", "jede:r NutzerInnen", "jede:r Nutzer:innen", - "des:der Messebauer:in" + "des:der Messebauer:in", + "eine:n neue Lehrer:in", + "der:die neuen Lehrer:in", + "den:die fleißige:r Schüler:in", + "der:die neue Hebamme:in", + "der:die neue Lehrerin" ])("lässt den mehrdeutigen oder unbekannten Fall %s unverändert", (input) => { expect(singularContextRule.apply(input)).toEqual({ text: input, diff --git a/tests/rules/singular-double-forms.test.ts b/tests/rules/singular-double-forms.test.ts index acc1282..f3b3be5 100644 --- a/tests/rules/singular-double-forms.test.ts +++ b/tests/rules/singular-double-forms.test.ts @@ -4,8 +4,10 @@ import { singularDoubleFormsRule } from "../../src/rules/singular-double-forms"; describe("singularDoubleFormsRule", () => { it.each([ ["Kunde/Kundin", "Kunde"], + ["Kunde:Kundin", "Kunde"], ["Kundin / Kunde", "Kunde"], ["Arzt und Ärztin", "Arzt"], + ["Arzt:Ärztin", "Arzt"], ["Studentin oder Student", "Student"], ["Kollege bzw. Kollegin", "Kollege"], ["Nutzer & Nutzerin", "Nutzer"], diff --git a/tests/rules/special-gender-forms.test.ts b/tests/rules/special-gender-forms.test.ts index e0fd90e..42ae5d0 100644 --- a/tests/rules/special-gender-forms.test.ts +++ b/tests/rules/special-gender-forms.test.ts @@ -5,6 +5,7 @@ describe("specialGenderFormsRule", () => { it.each([ ["Rom*nja", "Roma"], ["Sinti*zze", "Sinti"], + ["Kolleg:en:innen", "Kollegen"], ["Studentys", "Studenten"], ["Lesys", "Leser"], ["Lehrys", "Lehrer"], @@ -25,6 +26,7 @@ describe("specialGenderFormsRule", () => { "Sinti", "Romnja", "Sintizze", + "Kolleg:en", "Studenten", "Analyseys" ])("lässt %s unverändert", (input) => { diff --git a/tests/rules/substantivized-adjectives.test.ts b/tests/rules/substantivized-adjectives.test.ts index ad4be70..bb54a9d 100644 --- a/tests/rules/substantivized-adjectives.test.ts +++ b/tests/rules/substantivized-adjectives.test.ts @@ -12,6 +12,7 @@ describe("substantivizedAdjectivesRule", () => { ["der:die Beschäftigte:r", "der Beschäftigte"], ["den:die Beschäftigte:n", "den Beschäftigten"], ["jede:r Volljährige:r", "jeder Volljährige"], + ["jene:r Volljährige:r", "jener Volljährige"], ["mein:e Angehörige:r", "mein Angehöriger"], ["Sachverständige:r", "Sachverständiger"], ["Vorgesetzte:r", "Vorgesetzter"], diff --git a/tests/rules/unmarked-singular.test.ts b/tests/rules/unmarked-singular.test.ts index e9e2833..8a6837a 100644 --- a/tests/rules/unmarked-singular.test.ts +++ b/tests/rules/unmarked-singular.test.ts @@ -31,6 +31,9 @@ describe("unmarkedSingularRule", () => { it.each([ "Politikerinnen", "Vielleicht eine NutzerIn im Team", + "Vielleicht jene NutzerIn im Team", + "eine:n neue Lehrer:in", + "die neue Lehrer:in", "Hebamme:in", "Mutter:in", "Prof.in", From 23a8bca27440864856cccb860105de3136723eab Mon Sep 17 00:00:00 2001 From: HyperCriSiS Date: Sun, 2 Aug 2026 01:45:36 +0200 Subject: [PATCH 2/2] Code und Regelbestand bereinigen Signed-off-by: Codex --- CHANGELOG.md | 12 +- CONTRIBUTING.md | 4 +- UPSTREAMS.md | 13 +- data/flexion-regression-cases.json | 176 ++++++++---------- docs/ARCHITECTURE.md | 2 +- docs/GENDER-SOURCE-AUDIT.md | 107 ----------- docs/LEXICON.md | 33 +--- docs/MIGRATION.md | 108 ----------- ...on-forms.ts => additional-person-forms.ts} | 15 +- src/rules/catalog.ts | 8 +- src/rules/index.ts | 10 +- tests/flexion-regression-cases.test.ts | 13 +- tests/rules/known-plural-separators.test.ts | 40 ++-- 13 files changed, 124 insertions(+), 417 deletions(-) delete mode 100644 docs/GENDER-SOURCE-AUDIT.md delete mode 100644 docs/MIGRATION.md rename src/rules/{source-audit-person-forms.ts => additional-person-forms.ts} (96%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25b5a36..66e6b28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -94,8 +94,8 @@ dokumentiert. `Lehrys`, `Kollegys` und `Mitarbeitys`. - Eigene literale Ersetzungen, vollständig getrennt von persönlichen Ausnahmen, lokal gespeichert, case-sensitive, nicht rekursiv und ohne Regex-Unterstützung. -- Maschinenlesbarer Flexions- und Sonderformenkatalog mit quellenbezogenen - Positiv- und Negativregressionen. +- Maschinenlesbarer Flexions- und Sonderformenkatalog mit Positiv- und + Negativregressionen. - Stark erweitertes Flexionslexikon für häufige schwache, regelmäßige und unregelmäßige Personenformen. - Lokale Lizenz- und Quelltextseite in der Erweiterung. @@ -132,18 +132,14 @@ dokumentiert. - Automatisierter GitHub-Prerelease mit geprüften Chromium-, Firefox- und Quellcodepaketen sowie SHA-256-Prüfsummen. -- Öffentliche Projektvorstellung mit Hinweisen zu Lesbarkeit, Datenschutz, - Recherchequellen und Danksagung. +- Öffentliche Projektvorstellung mit Hinweisen zu Lesbarkeit, Datenschutz und + Danksagung. - Aktualisierte Roadmap und Beta-Testanleitung für die noch offenen Praxisprüfungen. ### Geändert - Projektbeschreibung und Manifesttexte verwenden nun den Leitsatz **„Macht Webseiten leichter lesbar.“** -- Die Herkunftsdokumentation unterscheidet genauer zwischen übernommenem - Quelltext und öffentlich dokumentierten Sprachbeispielen, Flexionsfällen und - Fehlertreffern als Recherchebasis. - ### Teststand - Der reale Waterfox-Test und die daraus entstandenen Popup- und Icon-Korrekturen diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a2aacdb..7230a71 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -28,9 +28,7 @@ Neue Sprachregeln benötigen: 3. korrekte Flexion für die betroffenen Kasus und Numeri, 4. Berücksichtigung von Großschreibung und Komposita, 5. eine Einordnung in eine sichtbare Regelgruppe, -6. automatisierte Tests, -7. einen Quellenhinweis, wenn öffentlich dokumentierte Fundstellen oder - Wörterbuchangaben ausgewertet wurden. +6. automatisierte Tests. Vollständige fremde Wörterbücher, Regelbestände, Regexsammlungen oder lizenzrechtlich unklare Datensätze dürfen nicht ohne vorherige Prüfung diff --git a/UPSTREAMS.md b/UPSTREAMS.md index 8a6247c..700c179 100644 --- a/UPSTREAMS.md +++ b/UPSTREAMS.md @@ -1,10 +1,8 @@ -# Recherchequellen +# Berücksichtigte Projekte Sprachverstand ist eine eigenständige Neuimplementierung. -Folgende Projekte wurden als Recherchequellen für bekannte Schreibweisen, -Flexionsfälle, Fehlerfälle, offene Issues und mögliche Regressionstests -ausgewertet: +Folgende Projekte wurden bei der Planung von Sprachverstand berücksichtigt: - https://github.com/brilliance-richter-huh/gendersprache-korrigieren - https://github.com/sternth/no-gender @@ -18,14 +16,7 @@ Hinweise für Anforderungen, Grenzfälle und Sicherheitsprüfungen geliefert. Es wurden keine Codebestandteile oder fremden Regexketten übernommen. Die Regel-Engine, DOM-Verarbeitung und Browser-Architektur wurden neu entwickelt. -Öffentlich dokumentierte Sprachbeispiele, Flexionsfälle, Fehlertreffer und -fachliche Kategorien dienten als Recherchebasis und wurden in eigenständig -formulierte Regeln und Tests überführt. - `rggl` enthält eine umfangreiche, über Jahre gewachsene Kasus- und Flexionssammlung und steht unter GPL-3.0. Deshalb wird besonders darauf geachtet, keine Quelltextpassagen, Regexketten oder vollständigen Datenbestände zu übernehmen. - -Die Herkunft einzelner fachlicher Anregungen bleibt in `docs/MIGRATION.md`, der -Git-Historie und den Pull Requests nachvollziehbar. diff --git a/data/flexion-regression-cases.json b/data/flexion-regression-cases.json index 45baa3b..c397f79 100644 --- a/data/flexion-regression-cases.json +++ b/data/flexion-regression-cases.json @@ -1,113 +1,87 @@ { "schemaVersion": 1, - "description": "Kuratierte reale und recherchierte Flexionsfälle. Die Einträge sind Testfälle, kein importiertes Wörterbuch.", - "sources": [ - { - "id": "field-reports", - "label": "Gemeldete Fundstellen und manuelle Praxistests im Sprachverstand-Projekt" - }, - { - "id": "duden-declension", - "label": "Duden: Deklination und substantivierte Adjektive", - "url": "https://www.duden.de/deklination" - }, - { - "id": "phettberg-bpb", - "label": "Bundeszentrale für politische Bildung: Entgendern nach Phettberg", - "url": "https://www.bpb.de/shop/zeitschriften/apuz/geschlechtergerechte-sprache-2022/346085/entgendern-nach-phettberg/" - }, - { - "id": "sinti-roma-zentralrat", - "label": "Zentralrat Deutscher Sinti und Roma: Kontroverse zur gegenderten Selbstbezeichnung", - "url": "https://zentralrat.sintiundroma.de/mitgliederversammlung-beschliesst-papier-ueber-die-kontroverse-zum-gendern-der-selbstbezeichnung-sinti-und-roma/" - }, - { - "id": "curated-lexicon", - "label": "Eigenständig kuratierte deutsche Personen- und Flexionsformen" - } - ], "cases": [ - { "id": "binnen-i-bare-user", "source": "field-reports", "input": "NutzerIn", "expected": "Nutzer" }, - { "id": "binnen-i-standalone-user", "source": "field-reports", "input": "eine NutzerIn", "expected": "ein Nutzer" }, - { "id": "binnen-i-nominative-sentence", "source": "field-reports", "input": "Eine NutzerIn arbeitet heute.", "expected": "Ein Nutzer arbeitet heute." }, - { "id": "binnen-i-accusative-verb", "source": "field-reports", "input": "Ich sehe eine NutzerIn.", "expected": "Ich sehe einen Nutzer." }, - { "id": "binnen-i-accusative-preposition", "source": "field-reports", "input": "für eine ÄrztIn", "expected": "für einen Arzt" }, - { "id": "binnen-i-dative-preposition", "source": "field-reports", "input": "mit einer StudentIn", "expected": "mit einem Studenten" }, - { "id": "binnen-i-genitive-preposition", "source": "field-reports", "input": "wegen einer ÄrztIn", "expected": "wegen eines Arztes" }, + { "id": "binnen-i-bare-user", "input": "NutzerIn", "expected": "Nutzer" }, + { "id": "binnen-i-standalone-user", "input": "eine NutzerIn", "expected": "ein Nutzer" }, + { "id": "binnen-i-nominative-sentence", "input": "Eine NutzerIn arbeitet heute.", "expected": "Ein Nutzer arbeitet heute." }, + { "id": "binnen-i-accusative-verb", "input": "Ich sehe eine NutzerIn.", "expected": "Ich sehe einen Nutzer." }, + { "id": "binnen-i-accusative-preposition", "input": "für eine ÄrztIn", "expected": "für einen Arzt" }, + { "id": "binnen-i-dative-preposition", "input": "mit einer StudentIn", "expected": "mit einem Studenten" }, + { "id": "binnen-i-genitive-preposition", "input": "wegen einer ÄrztIn", "expected": "wegen eines Arztes" }, - { "id": "double-singular-nominative", "source": "curated-lexicon", "input": "eine Nutzerin oder ein Nutzer", "expected": "ein Nutzer" }, - { "id": "double-singular-accusative", "source": "curated-lexicon", "input": "einen Studenten und eine Studentin", "expected": "einen Studenten" }, - { "id": "double-singular-dative", "source": "curated-lexicon", "input": "einer Studentin und einem Studenten", "expected": "einem Studenten" }, - { "id": "double-singular-genitive", "source": "curated-lexicon", "input": "eines Arztes oder einer Ärztin", "expected": "eines Arztes" }, - { "id": "double-plural-repeated-article", "source": "curated-lexicon", "input": "die Nutzerinnen und die Nutzer", "expected": "die Nutzer" }, - { "id": "double-plural-dative-article", "source": "curated-lexicon", "input": "den Ärztinnen und den Ärzten", "expected": "den Ärzten" }, - { "id": "double-plural-genitive-article", "source": "curated-lexicon", "input": "der Studentinnen und der Studenten", "expected": "der Studenten" }, + { "id": "double-singular-nominative", "input": "eine Nutzerin oder ein Nutzer", "expected": "ein Nutzer" }, + { "id": "double-singular-accusative", "input": "einen Studenten und eine Studentin", "expected": "einen Studenten" }, + { "id": "double-singular-dative", "input": "einer Studentin und einem Studenten", "expected": "einem Studenten" }, + { "id": "double-singular-genitive", "input": "eines Arztes oder einer Ärztin", "expected": "eines Arztes" }, + { "id": "double-plural-repeated-article", "input": "die Nutzerinnen und die Nutzer", "expected": "die Nutzer" }, + { "id": "double-plural-dative-article", "input": "den Ärztinnen und den Ärzten", "expected": "den Ärzten" }, + { "id": "double-plural-genitive-article", "input": "der Studentinnen und der Studenten", "expected": "der Studenten" }, - { "id": "adjective-adult-nominative", "source": "duden-declension", "input": "Erwachsene:r", "expected": "Erwachsener" }, - { "id": "adjective-adult-accusative", "source": "duden-declension", "input": "Erwachsene:n", "expected": "Erwachsenen" }, - { "id": "adjective-adult-dative", "source": "duden-declension", "input": "Erwachsene:m", "expected": "Erwachsenem" }, - { "id": "adjective-adult-mixed", "source": "duden-declension", "input": "ein:e Erwachsene:r", "expected": "ein Erwachsener" }, - { "id": "adjective-employed-weak", "source": "duden-declension", "input": "der:die Beschäftigte:r", "expected": "der Beschäftigte" }, - { "id": "adjective-unemployed-dative", "source": "duden-declension", "input": "einem:einer Arbeitslose:n", "expected": "einem Arbeitslosen" }, - { "id": "adjective-superior", "source": "curated-lexicon", "input": "Vorgesetzte:r", "expected": "Vorgesetzter" }, - { "id": "adjective-authorized", "source": "curated-lexicon", "input": "Erziehungsberechtigte:r", "expected": "Erziehungsberechtigter" }, + { "id": "adjective-adult-nominative", "input": "Erwachsene:r", "expected": "Erwachsener" }, + { "id": "adjective-adult-accusative", "input": "Erwachsene:n", "expected": "Erwachsenen" }, + { "id": "adjective-adult-dative", "input": "Erwachsene:m", "expected": "Erwachsenem" }, + { "id": "adjective-adult-mixed", "input": "ein:e Erwachsene:r", "expected": "ein Erwachsener" }, + { "id": "adjective-employed-weak", "input": "der:die Beschäftigte:r", "expected": "der Beschäftigte" }, + { "id": "adjective-unemployed-dative", "input": "einem:einer Arbeitslose:n", "expected": "einem Arbeitslosen" }, + { "id": "adjective-superior", "input": "Vorgesetzte:r", "expected": "Vorgesetzter" }, + { "id": "adjective-authorized", "input": "Erziehungsberechtigte:r", "expected": "Erziehungsberechtigter" }, - { "id": "special-romnja", "source": "sinti-roma-zentralrat", "input": "Rom*nja", "expected": "Roma" }, - { "id": "special-sintizze", "source": "sinti-roma-zentralrat", "input": "Sinti*zze", "expected": "Sinti" }, - { "id": "phettberg-studentys", "source": "phettberg-bpb", "input": "Studentys", "expected": "Studenten" }, - { "id": "phettberg-lesys", "source": "phettberg-bpb", "input": "Lesys", "expected": "Leser" }, - { "id": "phettberg-lehrys", "source": "phettberg-bpb", "input": "Lehrys", "expected": "Lehrer" }, - { "id": "phettberg-wirtys", "source": "phettberg-bpb", "input": "Wirtys", "expected": "Wirte" }, - { "id": "phettberg-kollegys", "source": "phettberg-bpb", "input": "Kollegys", "expected": "Kollegen" }, - { "id": "phettberg-mitarbeitys", "source": "phettberg-bpb", "input": "Mitarbeitys", "expected": "Mitarbeiter" }, - { "id": "phettberg-kommilitonys", "source": "phettberg-bpb", "input": "Kommilitonys", "expected": "Kommilitonen" }, + { "id": "special-romnja", "input": "Rom*nja", "expected": "Roma" }, + { "id": "special-sintizze", "input": "Sinti*zze", "expected": "Sinti" }, + { "id": "special-studentys", "input": "Studentys", "expected": "Studenten" }, + { "id": "special-lesys", "input": "Lesys", "expected": "Leser" }, + { "id": "special-lehrys", "input": "Lehrys", "expected": "Lehrer" }, + { "id": "special-wirtys", "input": "Wirtys", "expected": "Wirte" }, + { "id": "special-kollegys", "input": "Kollegys", "expected": "Kollegen" }, + { "id": "special-mitarbeitys", "input": "Mitarbeitys", "expected": "Mitarbeiter" }, + { "id": "special-kommilitonys", "input": "Kommilitonys", "expected": "Kommilitonen" }, - { "id": "lexicon-lawyers", "source": "curated-lexicon", "input": "Anwält:innen", "expected": "Anwälte" }, - { "id": "lexicon-guests", "source": "curated-lexicon", "input": "Gäst:innen", "expected": "Gäste" }, - { "id": "lexicon-cooks", "source": "curated-lexicon", "input": "Köch:innen", "expected": "Köche" }, - { "id": "lexicon-officials", "source": "curated-lexicon", "input": "Beamt:innen", "expected": "Beamte" }, - { "id": "lexicon-board", "source": "curated-lexicon", "input": "Vorständ:innen", "expected": "Vorstände" }, - { "id": "lexicon-ministers", "source": "curated-lexicon", "input": "Minister:innen", "expected": "Minister" }, - { "id": "lexicon-apprentices", "source": "curated-lexicon", "input": "Lehrling:innen", "expected": "Lehrlinge" }, - { "id": "lexicon-witnesses", "source": "curated-lexicon", "input": "Zeitzeug:innen", "expected": "Zeitzeugen" }, - { "id": "lexicon-photographers", "source": "curated-lexicon", "input": "Fotograf:innen", "expected": "Fotografen" }, - { "id": "lexicon-librarians", "source": "curated-lexicon", "input": "Bibliothekar:innen", "expected": "Bibliothekare" }, - { "id": "lexicon-psychologists", "source": "curated-lexicon", "input": "Psycholog:innen", "expected": "Psychologen" }, - { "id": "lexicon-correspondents", "source": "curated-lexicon", "input": "Korrespondent:innen", "expected": "Korrespondenten" }, - { "id": "lexicon-parliamentarians", "source": "curated-lexicon", "input": "Parlamentarier:innen", "expected": "Parlamentarier" }, - { "id": "lexicon-chancellors", "source": "curated-lexicon", "input": "Bundeskanzler:innen", "expected": "Bundeskanzler" }, - { "id": "lexicon-validated-binnen-i", "source": "curated-lexicon", "input": "VerteidigerIn", "expected": "Verteidiger" }, - { "id": "lexicon-validated-irregular-plural", "source": "curated-lexicon", "input": "Steinmetz:innen", "expected": "Steinmetze" }, - { "id": "lexicon-validated-english-plural", "source": "curated-lexicon", "input": "Coach:innen", "expected": "Coaches" }, - { "id": "lexicon-validated-weak-plural", "source": "curated-lexicon", "input": "Rassist:innen", "expected": "Rassisten" }, - { "id": "lexicon-patenschaft-compound", "source": "curated-lexicon", "input": "Pat*innenschaft", "expected": "Patenschaft" }, - { "id": "lexicon-themenpate", "source": "curated-lexicon", "input": "Themenpat*in", "expected": "Themenpate" }, + { "id": "plural-lawyers", "input": "Anwält:innen", "expected": "Anwälte" }, + { "id": "plural-guests", "input": "Gäst:innen", "expected": "Gäste" }, + { "id": "plural-cooks", "input": "Köch:innen", "expected": "Köche" }, + { "id": "plural-officials", "input": "Beamt:innen", "expected": "Beamte" }, + { "id": "plural-board", "input": "Vorständ:innen", "expected": "Vorstände" }, + { "id": "plural-ministers", "input": "Minister:innen", "expected": "Minister" }, + { "id": "plural-apprentices", "input": "Lehrling:innen", "expected": "Lehrlinge" }, + { "id": "plural-witnesses", "input": "Zeitzeug:innen", "expected": "Zeitzeugen" }, + { "id": "plural-photographers", "input": "Fotograf:innen", "expected": "Fotografen" }, + { "id": "plural-librarians", "input": "Bibliothekar:innen", "expected": "Bibliothekare" }, + { "id": "plural-psychologists", "input": "Psycholog:innen", "expected": "Psychologen" }, + { "id": "plural-correspondents", "input": "Korrespondent:innen", "expected": "Korrespondenten" }, + { "id": "plural-parliamentarians", "input": "Parlamentarier:innen", "expected": "Parlamentarier" }, + { "id": "plural-chancellors", "input": "Bundeskanzler:innen", "expected": "Bundeskanzler" }, + { "id": "singular-verteidiger-binnen-i", "input": "VerteidigerIn", "expected": "Verteidiger" }, + { "id": "plural-stonemasons", "input": "Steinmetz:innen", "expected": "Steinmetze" }, + { "id": "plural-coaches", "input": "Coach:innen", "expected": "Coaches" }, + { "id": "plural-racists", "input": "Rassist:innen", "expected": "Rassisten" }, + { "id": "compound-patenschaft", "input": "Pat*innenschaft", "expected": "Patenschaft" }, + { "id": "singular-themenpate", "input": "Themenpat*in", "expected": "Themenpate" }, - { "id": "syntax-jene-determiner", "source": "curated-lexicon", "input": "jene:r Nutzer:in", "expected": "jener Nutzer" }, - { "id": "syntax-attributive-nominative", "source": "curated-lexicon", "input": "der:die neue Lehrer:in", "expected": "der neue Lehrer" }, - { "id": "syntax-attributive-accusative", "source": "curated-lexicon", "input": "den:die fleißige:n Schüler:in", "expected": "den fleißigen Schüler" }, - { "id": "syntax-attributive-multiword", "source": "curated-lexicon", "input": "ein*e gut ausgebildete*r Jurist*in", "expected": "ein gut ausgebildeter Jurist" }, - { "id": "syntax-full-stem-colon-pair", "source": "curated-lexicon", "input": "der:die Arzt:Ärztin", "expected": "der Arzt" }, - { "id": "syntax-duplicated-colleague-ending", "source": "curated-lexicon", "input": "Kolleg:en:innen", "expected": "Kollegen" }, + { "id": "syntax-jene-determiner", "input": "jene:r Nutzer:in", "expected": "jener Nutzer" }, + { "id": "syntax-attributive-nominative", "input": "der:die neue Lehrer:in", "expected": "der neue Lehrer" }, + { "id": "syntax-attributive-accusative", "input": "den:die fleißige:n Schüler:in", "expected": "den fleißigen Schüler" }, + { "id": "syntax-attributive-multiword", "input": "ein*e gut ausgebildete*r Jurist*in", "expected": "ein gut ausgebildeter Jurist" }, + { "id": "syntax-full-stem-colon-pair", "input": "der:die Arzt:Ärztin", "expected": "der Arzt" }, + { "id": "syntax-duplicated-colleague-ending", "input": "Kolleg:en:innen", "expected": "Kollegen" }, - { "id": "compound-readership", "source": "curated-lexicon", "input": "Leser*innenschaft", "expected": "Leserschaft" }, - { "id": "compound-citizen-municipality", "source": "curated-lexicon", "input": "Bürger*innenkommune", "expected": "Bürgerkommune" }, - { "id": "compound-migrant-organization", "source": "curated-lexicon", "input": "Migrant*innenorganisation", "expected": "Migrantenorganisation" }, - { "id": "compound-worker-protection", "source": "curated-lexicon", "input": "ArbeitnehmerInnenschutz", "expected": "Arbeitnehmerschutz" }, + { "id": "compound-readership", "input": "Leser*innenschaft", "expected": "Leserschaft" }, + { "id": "compound-citizen-municipality", "input": "Bürger*innenkommune", "expected": "Bürgerkommune" }, + { "id": "compound-migrant-organization", "input": "Migrant*innenorganisation", "expected": "Migrantenorganisation" }, + { "id": "compound-worker-protection", "input": "ArbeitnehmerInnenschutz", "expected": "Arbeitnehmerschutz" }, - { "id": "negative-feminine-politicians", "source": "field-reports", "input": "Politikerinnen", "expected": "Politikerinnen" }, - { "id": "negative-ambiguous-inline-binnen-i", "source": "field-reports", "input": "Vielleicht eine NutzerIn im Team", "expected": "Vielleicht eine NutzerIn im Team" }, - { "id": "negative-adjective-use", "source": "duden-declension", "input": "erwachsene Kinder", "expected": "erwachsene Kinder" }, - { "id": "negative-wrong-adjective-marker", "source": "duden-declension", "input": "ein:e Erwachsene:n", "expected": "ein:e Erwachsene:n" }, - { "id": "negative-robot", "source": "curated-lexicon", "input": "Robot:innen", "expected": "Robot:innen" }, - { "id": "negative-plain-validated-feminine", "source": "curated-lexicon", "input": "Verteidigerin", "expected": "Verteidigerin" }, - { "id": "negative-uppercase-feminine", "source": "curated-lexicon", "input": "VERTEIDIGERIN", "expected": "VERTEIDIGERIN" }, - { "id": "negative-attributive-wrong-ending", "source": "curated-lexicon", "input": "eine:n neue Lehrer:in", "expected": "eine:n neue Lehrer:in" }, - { "id": "negative-unpaired-colon-form", "source": "curated-lexicon", "input": "Arzt:Arzt", "expected": "Arzt:Arzt" }, - { "id": "negative-incomplete-colleague-ending", "source": "curated-lexicon", "input": "Kolleg:en", "expected": "Kolleg:en" }, - { "id": "negative-ordinary-patenschaft", "source": "curated-lexicon", "input": "Patenschaft", "expected": "Patenschaft" }, - { "id": "negative-plain-romnja", "source": "sinti-roma-zentralrat", "input": "Romnja", "expected": "Romnja" }, - { "id": "negative-plain-sintizze", "source": "sinti-roma-zentralrat", "input": "Sintizze", "expected": "Sintizze" }, - { "id": "negative-ordinary-hobbys", "source": "phettberg-bpb", "input": "Hobbys und Handys", "expected": "Hobbys und Handys" } + { "id": "negative-feminine-politicians", "input": "Politikerinnen", "expected": "Politikerinnen" }, + { "id": "negative-ambiguous-inline-binnen-i", "input": "Vielleicht eine NutzerIn im Team", "expected": "Vielleicht eine NutzerIn im Team" }, + { "id": "negative-adjective-use", "input": "erwachsene Kinder", "expected": "erwachsene Kinder" }, + { "id": "negative-wrong-adjective-marker", "input": "ein:e Erwachsene:n", "expected": "ein:e Erwachsene:n" }, + { "id": "negative-robot", "input": "Robot:innen", "expected": "Robot:innen" }, + { "id": "negative-plain-validated-feminine", "input": "Verteidigerin", "expected": "Verteidigerin" }, + { "id": "negative-uppercase-feminine", "input": "VERTEIDIGERIN", "expected": "VERTEIDIGERIN" }, + { "id": "negative-attributive-wrong-ending", "input": "eine:n neue Lehrer:in", "expected": "eine:n neue Lehrer:in" }, + { "id": "negative-unpaired-colon-form", "input": "Arzt:Arzt", "expected": "Arzt:Arzt" }, + { "id": "negative-incomplete-colleague-ending", "input": "Kolleg:en", "expected": "Kolleg:en" }, + { "id": "negative-ordinary-patenschaft", "input": "Patenschaft", "expected": "Patenschaft" }, + { "id": "negative-plain-romnja", "input": "Romnja", "expected": "Romnja" }, + { "id": "negative-plain-sintizze", "input": "Sintizze", "expected": "Sintizze" }, + { "id": "negative-ordinary-hobbys", "input": "Hobbys und Handys", "expected": "Hobbys und Handys" } ] } diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 1f7d5a9..55435e3 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -94,7 +94,7 @@ Sprachverstand erzeugten Wert entspricht. - `person-lexicon.ts`: Singular, Plural, oblique Formen, Genitiv, Komposita und unregelmäßige Flexionen - `substantivized-adjectives.ts`: geprüfte substantivierte Adjektive - `special-gender-forms.ts`: einzelne ausdrücklich zugeordnete Sonderformen -- `data/flexion-regression-cases.json`: quellenbezogene Positiv- und Negativfälle +- `data/flexion-regression-cases.json`: kuratierte Positiv- und Negativfälle Das Aufnahmeverfahren ist in [`LEXICON.md`](LEXICON.md) beschrieben. diff --git a/docs/GENDER-SOURCE-AUDIT.md b/docs/GENDER-SOURCE-AUDIT.md deleted file mode 100644 index e911918..0000000 --- a/docs/GENDER-SOURCE-AUDIT.md +++ /dev/null @@ -1,107 +0,0 @@ -# Quellenprüfung für Genderformen - -Stand: 28. Juli 2026 - -## Geprüfte Quellen - -- Geschickt gendern: vollständiges Genderwörterbuch von A bis Z - - https://geschicktgendern.de/#A -- Schreibportal der Universität Leipzig - - https://home.uni-leipzig.de/schreibportal/gendern/ -- Leitfaden zur genderinklusiven Kommunikation der TU Dresden, 2021 - - https://tu-dresden.de/tu-dresden/organisation/ressourcen/dateien/Gleichstellungsbeauftragte/Unsere-Themen/genderinklusive-kommunikation/leitfaden-genderinklusive-kommunikation-2021?lang=de -- Hannover.de in Leichter Sprache - - https://www.hannover.de/Leichte-Sprache/Hilfe-und-Tipps/Wie-benutze-ich-Hannover.de-in-Leichter-Sprache/Gendern-und-der-Gender%E2%88%99stern - -## Auswahlkriterien - -Sprachverstand übernimmt aus den Quellen keine pauschalen stilistischen oder -inhaltlichen Umschreibungen. Eine Alternative wie `Autor` → `schreibende -Person` kann je nach Kontext eine andere Bedeutung oder Konnotation haben. -Auch das Genderwörterbuch weist ausdrücklich darauf hin, dass viele Vorschläge -nicht eins zu eins passen. - -Automatisch aufgenommen werden nur Formen, bei denen die sichtbare -Genderschreibweise morphologisch eindeutig auf die generisch-maskuline Form -zurückgeführt werden kann: - -1. unveränderte Plurale, zum Beispiel `Follower*innen` → `Follower`; -2. regulär oder schwach flektierte Formen, zum Beispiel - `Proband:innen` → `Probanden`; -3. eindeutige Singularmarker, zum Beispiel - `Kommunikator*in` → `Kommunikator`; -4. problematische Umlautstämme, wenn das Zielwort ausdrücklich hinterlegt ist, - zum Beispiel `Köch*in` → `Koch` und `Ärzt:innen` → `Ärzte`; -5. sichtbare Lehrschreibweisen mit getrennten Zeichen, zum Beispiel - `Leser _ innen`, `Leser I nnen` und `Student + innen`. - -## Ergebnis des A-bis-Z-Abgleichs - -Die vollständige A-bis-Z-Liste wurde zeilenweise geprüft. Aufgenommen wurde ein -breiter Nachtrag aus sicher bestimmbaren Personenbezeichnungen, darunter: - -- unveränderte Pluralstämme wie `Akademiker`, `Babysitter`, `Coder`, - `Follower`, `Historiker`, `Influencer`, `Kassierer`, `Layouter`, - `Minijobber`, `Recruiter`, `Streamer`, `Teamplayer`, `Youtuber` und - `Zweifler`; -- schwache Flexionen wie `Abiturient`, `Alpinist`, `Finalist`, `Gymnasiast`, - `Intendant`, `Kommunist`, `Proband`, `Prokurist`, `Protagonist`, - `Rezeptionist`, `Rezipient`, `Stipendiat` und `Zivilist`; -- reguläre Sonderpluralformen wie `Auditoren`, `Evaluatoren`, - `Illustratoren`, `Kommunikatoren`, `Konstrukteure`, `Kuratoren`, - `Mediatoren`, `Operateure`, `Passagiere` und `Tutoren`; -- ausdrücklich hinterlegte Problemstämme wie `Arzt/Ärzt`, `Koch/Köch`, - `Logopäd`, `Matros`, `Schöff`, `Schütz` und `Strateg`. - -Ein automatisierter Test verwendet für jeden Buchstaben A bis Z mindestens -eine repräsentative neue oder bereits angebundene Form. - -## Ergebnisse der zusätzlichen Quellen - -### Universität Leipzig - -Geprüft wurden Sternchen, Doppelpunkt, Unterstrich und Binnen-I sowie die dort -beschriebenen Umlautprobleme. Insbesondere abgedeckt sind nun unter anderem: - -- `Leser*in`, `Intendant*in`, `Patient*in`; -- `Proband:innen`; -- `Matros_innen`, `Künstler_innen`, `Fahrer_innen`; -- `LehrerInnen`, `PolitikerInnen`; -- `Arzt*in`, `Ärzt*in`, `Koch*in`, `Köch*in`. - -### TU Dresden - -Geprüft wurden insbesondere Doppelpunkt, Schrägstrich, Doppelnennungen, -Pronomen- und Artikelkombinationen sowie akademische Titel. Bereits vorhandene -Regeln decken unter anderem `Professor:innen`, `Student:innen`, -`Mitarbeiter:innen`, `Jede:r Besitzer:in`, `der:die Arbeitnehmer:in`, -`Prof.in` und `Dr.in` ab. Zusätzlich werden die im Leitfaden als gesprochene -Trennung dargestellten Formen `Student + innen` und `Professor + innen` -erkannt. - -### Hannover.de - -Die normalen Beispiele `Leser:innen`, `Leser*innen`, `Schüler*innen` und -`Kolleg*innen` waren bereits abgedeckt. Neu unterstützt werden die auf der -Seite sichtbar getrennten Demonstrationsformen: - -- `Leser _ innen` → `Leser`; -- `Leser I nnen` → `Leser`. - -Der Medio-Punkt in Leichter Sprache, etwa in `Geschlechts·merkmale`, bleibt -bewusst unverändert. Er dient auf Hannover.de der Wortgliederung und ist dort -kein Genderzeichen. - -## Bewusst nicht automatisiert - -Nicht automatisch übernommen werden: - -- freie Synonyme und Satzumbauten aus dem Genderwörterbuch; -- Partizipformen ohne ausreichend eindeutigen Kontext; -- normale feminine Formen ohne sichtbares Genderzeichen; -- Begriffe, die bereits grammatisch neutral sind; -- Lehrbeispiele, deren automatische Änderung normalen Fließtext beschädigen - könnte. - -Diese Fälle können später einzeln und mit konkreten Kontexttests ergänzt -werden. diff --git a/docs/LEXICON.md b/docs/LEXICON.md index 62276f1..201030c 100644 --- a/docs/LEXICON.md +++ b/docs/LEXICON.md @@ -5,30 +5,13 @@ unregelmäßige oder kontextabhängige Personenform wird mit ihrer erwarteten Singular-, Plural- und Kasusflexion modelliert und durch Positiv- und Negativtests abgesichert. -## Datenquellen - -Der produktive Bestand wird eigenständig kuratiert. Grundlage sind: - -- reale Fundstellen aus Praxistests und Fehlermeldungen, -- allgemeinsprachliche Flexionsangaben aus öffentlich zugänglichen - Wörterbuch- und Grammatikseiten, -- dokumentierte Sonderformen und ihre beschriebenen Bildungsregeln, -- negative Beispiele, bei denen eine ähnlich aussehende Form ausdrücklich - unverändert bleiben muss. - -Es wird kein vollständiges fremdes Wörterbuch und keine fremde Regel- oder -Regexsammlung importiert. Wörterbuchangaben werden zur Prüfung einzelner -Flexionen herangezogen; die Auswahl, Datenstruktur, Regelimplementierung und -Tests entstehen im Projekt neu. - ## Maschinenlesbarer Regressionskatalog `data/flexion-regression-cases.json` enthält kuratierte Einzelfälle mit: - eindeutiger ID, - Ausgangstext, -- erwarteter Darstellung, -- Herkunfts- oder Prüfkategorie, +- erwarteter Darstellung sowie - positiven und negativen Regressionen. `tests/flexion-regression-cases.test.ts` führt den gesamten Katalog gegen die @@ -40,7 +23,7 @@ eine bereits geprüfte Form beschädigen. Eine neue Form wird nur produktiv aufgenommen, wenn folgende Punkte geklärt sind: -1. Die Fundstelle liegt mit ausreichend Satz- oder UI-Kontext vor. +1. Die Ausgangsform liegt mit ausreichend Satz- oder UI-Kontext vor. 2. Singular, Plural und relevante Kasusformen sind bestimmt. 3. Komposita und Großschreibung wurden berücksichtigt. 4. Mindestens ein positiver Regressionstest wurde ergänzt. @@ -62,14 +45,4 @@ Der Bestand umfasst unter anderem: - substantivierte Adjektive wie `Erwachsener`, `Beschäftigter`, `Volljähriger`, `Vorgesetzter` und `Erziehungsberechtigter`, - exakt geprüfte Sonderformen wie `Rom*nja`, `Sinti*zze` und mehrere - Phettberg-Pluralformen auf `-ys`. - -## Quellenhinweise - -- Duden: Deklination und substantivierte Adjektive -- Bundeszentrale für politische Bildung: Entgendern nach Phettberg -- Zentralrat Deutscher Sinti und Roma: Kontroverse zur gegenderten - Selbstbezeichnung -- die in `UPSTREAMS.md` dokumentierten öffentlichen Fehler- und Grenzfälle - -Die konkreten Links und Zuordnungen stehen direkt im maschinenlesbaren Katalog. + Sonderformen auf `-ys`. diff --git a/docs/MIGRATION.md b/docs/MIGRATION.md deleted file mode 100644 index df678e1..0000000 --- a/docs/MIGRATION.md +++ /dev/null @@ -1,108 +0,0 @@ -# Auswertung der Altprojekte - -Diese Datei dokumentiert, welche Fehlerfälle und Konzepte aus den geprüften -Altprojekten in Sprachverstand eingeflossen sind. Fremder Quelltext wird nicht -übernommen; produktiver Code wird unabhängig neu implementiert. - -## Geprüfte Quellen - -- `brilliance-richter-huh/gendersprache-korrigieren` -- `sternth/no-gender` -- `motsiw/rggl` - -Die vorhandenen funktionalen Issues und Pull Requests aller drei Projekte wurden -ausgewertet. Ein erneuter Komplettaudit ist erst nötig, wenn dort neue Einträge -oder relevante Commits erscheinen. - -## Durch die Architektur gelöst - -| Quelle | Fundstelle | Umsetzung in Sprachverstand | -|---|---|---| -| gendersprache-korrigieren #19 | Änderungen in `code` und `pre` | zentrale DOM-Sicherheitsprüfung | -| gendersprache-korrigieren #31 / rggl #4 | dynamisch nachgeladene Inhalte | gezielter `MutationObserver`, kein periodischer Vollscan | -| no-gender #1 | springender Cursor in Editoren | Eingaben und `contenteditable` ausgeschlossen | -| no-gender #8 | beschädigte Base64-Inhalte | technische Inhalte werden übersprungen | -| rggl #7 | Genderformen in Alternativtexten | Positivliste für `alt`, `aria-label`, `aria-description` und `title` | -| rggl #10 | beschädigte Seiten im Hervorhebungsmodus | kein Umschreiben von `innerHTML` | - -## Umgesetzte Sprachfälle - -| Quelle | Fundstelle | Umsetzung | -|---|---|---| -| alle | `:`, `*`, `_`, `/`, `·`, `•`, `.`, `’`, `‘` | lexikalisch geprüfte Separatorregeln | -| alle | Binnen-I | gemeinsames Flexionslexikon | -| rggl #3 | `Messebauer*innen` | getrennte Formen für `Bauer` und Komposita | -| rggl #5 | `Innen- und Außendienst` | Negativregression | -| rggl #6 | `Kunde/Kundin` | sichere Singular-Doppelformen | -| rggl #11 | typografische Apostrophe | explizit unterstützte Separatoren | -| rggl PR #1/#2 | `LogIn`, `AddIn`, `PlugIn`, `DriveIn` | Negativregressionen | -| rggl PR #1/#2 | `Prof.in`, `Dr.in` | eigene kontextabhängige Titelregel | -| no-gender #10 | `den:die Arbeitnehmer:in` | beide Artikelreihenfolgen und Kasusformen | -| no-gender #11 | `Privatkund*in` | lexikalisch geprüfte Singularformen ohne Artikel | -| no-gender #15 | Artikel und Possessivformen | Nominativ, Akkusativ, Dativ und Genitiv | -| no-gender #24 | `Jede:r` | eindeutiger Singular-Kontext | -| gendersprache-korrigieren #22 | `(m/w/d)` | eigene optionale, standardmäßig deaktivierte Gruppe | - -## Partizipien und neutrale Umschreibungen - -`rggl` #8 und ähnliche Meldungen aus den anderen Projekten zeigen, dass Wörter -wie `Mitarbeitende`, `Studierende` und `Lesende` nicht sicher durch eine einzige -breite Regel ersetzt werden können. - -Sprachverstand teilt dies deshalb auf: - -- eindeutige Anreden wie `Sehr geehrte Mitarbeitende` werden standardmäßig - korrigiert; -- außerhalb dieser Anreden werden nur einzeln geprüfte Wendungen aus dem - Kontextkatalog ersetzt; -- `Benutzungshandbuch` kann in der optionalen Stilgruppe zu - `Benutzerhandbuch` werden; -- legitime Begriffe wie `Testpersonen`, `Persönlichkeiten`, `Kollegium` und - `ärztliche Sprechstunde` bleiben unverändert. - -## Bewusst nicht pauschal umgesetzt - -Eine normale feminine Personenbezeichnung wird nicht maskulinisiert. -`Die Organspenderin widersprach`, `Professorin Müller` oder `Politikerinnen` -können ausdrücklich Frauen bezeichnen und bleiben unverändert. - -Auch breite Schlussregeln, die beliebige Endungen `in` oder `innen` entfernen, -werden verworfen. Sie würden unter anderem `Innen- und Außendienst`, `LinkedIn` -und viele normale Wörter beschädigen. - -## Seit Beta 11 zusätzlich umgesetzt - -| Quelle | Fundstelle | Umsetzung | -|---|---|---| -| rggl #12 | `Jüdinnen und Juden` | explizite unregelmäßige Doppelform und Regressionen | -| no-gender #20/#21 | substantivierte Adjektive | eigene lexikonbasierte Flexionslogik | -| no-gender #18/#27 | `Studentys`, `Rom*nja`, `Sinti*zze` | exakte optionale Sonderformzuordnungen ohne breite Suffixregel | - -## Weiterhin bewusst offen - -| Quelle | Fundstelle | Status | -|---|---|---| -| mehrere | sprachlich falsche oder abgeschnittene Formen wie `Zuhörer*inne` | keine allgemeine Rechtschreibkorrektur; nur bei belastbaren realen Mustern | -| reale Webseiten | weitere Kontext- und Flexionsfundstellen | erst mit vollständigem Satz- und Seitenkontext aufnehmen | - -## Qualitätsgrenzen - -- Keine Regel ohne positive und negative Tests. -- Eine ausgelassene Ersetzung ist besser als eine falsche. -- Keine periodischen Komplettscans. -- Kein Umschreiben von `innerHTML`. -- Eingaben, Editoren, Code und technische Daten bleiben unberührt. -- Riskantere Bedeutungsänderungen erscheinen als eigene, standardmäßig - deaktivierte Gruppen. -- Zitate werden standardmäßig korrigiert; auf Wunsch können Inhalte innerhalb - üblicher Anführungszeichen geschützt werden. - -## Kontextkatalog statt pauschaler Partizipregel - -Beta 6 ersetzt die globale optionale Wortliste für `Studierende`, `Lesende` und -ähnliche Formen durch einen versionierten Kontextkatalog. Sichere Anreden bleiben -aktiv. Weitere Fälle werden als `collect`, `implemented` oder `reject` erfasst. -Das verhindert, dass wörtliche Tätigkeitsbeschreibungen durch eine bloße -Endungsregel verändert werden. - -Siehe `data/neutral-context-catalog.json` und `docs/NEUTRAL-CONTEXTS.md`. diff --git a/src/rules/source-audit-person-forms.ts b/src/rules/additional-person-forms.ts similarity index 96% rename from src/rules/source-audit-person-forms.ts rename to src/rules/additional-person-forms.ts index a8b006f..10453ad 100644 --- a/src/rules/source-audit-person-forms.ts +++ b/src/rules/additional-person-forms.ts @@ -18,10 +18,9 @@ interface SupplementalForm { const locale = "de-DE"; /* - * Kuratierter Quellenabgleich, zuletzt erweitert 2026-08. Kandidaten aus - * vollständigen Datensätzen werden nicht ungeprüft importiert. Aufgenommen - * werden ausschließlich mehrfach bestätigte Formen, deren Singular und Plural - * einzeln geprüft und ohne semantische Neuformulierung bestimmbar sind. + * Ergänzende Personenformen mit einzeln bestimmten Singular- und Pluralzielen. + * Aufgenommen werden ausschließlich morphologisch eindeutige Formen, die ohne + * semantische Neuformulierung umgewandelt werden können. */ const unchangedForms = [ "abenteurer", @@ -527,8 +526,8 @@ const plusPluralPattern = const spacedBinnenIPluralPattern = /(? { - it("enthält eindeutige und vollständig referenzierte Einträge", () => { + it("enthält eindeutige und vollständige Einträge", () => { expect(catalog.schemaVersion).toBe(1); - const sourceIds = new Set(catalog.sources.map((source) => source.id)); const caseIds = catalog.cases.map((entry) => entry.id); expect(new Set(caseIds).size).toBe(caseIds.length); for (const entry of catalog.cases) { expect(entry.id.length).toBeGreaterThan(0); expect(entry.input.length).toBeGreaterThan(0); - expect(sourceIds.has(entry.source)).toBe(true); + expect(typeof entry.expected).toBe("string"); } }); diff --git a/tests/rules/known-plural-separators.test.ts b/tests/rules/known-plural-separators.test.ts index 6cec56b..0567de4 100644 --- a/tests/rules/known-plural-separators.test.ts +++ b/tests/rules/known-plural-separators.test.ts @@ -1,9 +1,9 @@ import { describe, expect, it } from "vitest"; import { knownPluralSeparatorsRule } from "../../src/rules/known-plural-separators"; import { - sourceAuditPluralRule, - sourceAuditSingularRule -} from "../../src/rules/source-audit-person-forms"; + additionalPersonPluralRule, + additionalPersonSingularRule +} from "../../src/rules/additional-person-forms"; const validatedSupplementalForms = [ { singular: "Abenteurer", plural: "Abenteurer" }, @@ -168,11 +168,11 @@ describe("knownPluralSeparatorsRule", () => { }); }); -describe("sourceAuditPluralRule", () => { +describe("additionalPersonPluralRule", () => { it.each(validatedSupplementalForms)( "normalisiert $singular als einzeln geprüften Plural", ({ singular, plural }) => { - expect(sourceAuditPluralRule.apply(`${singular}:innen`)).toEqual({ + expect(additionalPersonPluralRule.apply(`${singular}:innen`)).toEqual({ text: plural, replacements: 1 }); @@ -192,8 +192,8 @@ describe("sourceAuditPluralRule", () => { ["Rezeptionist·innen", "Rezeptionisten"], ["Online-FollowerInnen", "Online-Follower"], ["Follower*innenzahl", "Followerzahl"] - ])("normalisiert den quellengeprüften Plural %s", (input, expected) => { - expect(sourceAuditPluralRule.apply(input)).toEqual({ + ])("normalisiert den einzeln geprüften Plural %s", (input, expected) => { + expect(additionalPersonPluralRule.apply(input)).toEqual({ text: expected, replacements: 1 }); @@ -215,15 +215,15 @@ describe("sourceAuditPluralRule", () => { "Quereinsteiger, Rezeptionisten, Streamer, Tutoren, Überbringer, " + "Visionäre, Wikipedianer, Xylophonspieler, Youtuber und Zivilisten"; - expect(sourceAuditPluralRule.apply(input)).toEqual({ + expect(additionalPersonPluralRule.apply(input)).toEqual({ text: expected, replacements: 26 }); }); - it("verarbeitet sichtbar getrennte Lehrbeispiele der geprüften Quellen", () => { + it("verarbeitet sichtbar getrennte Schreibweisen", () => { expect( - sourceAuditPluralRule.apply( + additionalPersonPluralRule.apply( "Leser _ innen, Leser I nnen, Student + innen und Professor+innen" ) ).toEqual({ @@ -235,18 +235,18 @@ describe("sourceAuditPluralRule", () => { it("lässt normale Wörter, Medio-Punkte und unbekannte Plusformen unverändert", () => { const input = "Followerinnen, kommunikatorisch, Geschlechts·merkmale und Zahl + innen"; - expect(sourceAuditPluralRule.apply(input)).toEqual({ + expect(additionalPersonPluralRule.apply(input)).toEqual({ text: input, replacements: 0 }); }); }); -describe("sourceAuditSingularRule", () => { +describe("additionalPersonSingularRule", () => { it.each(validatedSupplementalForms)( "normalisiert $singular als einzeln geprüften Singular", ({ singular }) => { - expect(sourceAuditSingularRule.apply(`${singular}:in`)).toEqual({ + expect(additionalPersonSingularRule.apply(`${singular}:in`)).toEqual({ text: singular, replacements: 1 }); @@ -256,7 +256,7 @@ describe("sourceAuditSingularRule", () => { it.each(validatedSupplementalForms)( "normalisiert $singular als einzeln geprüften Binnen-I-Singular", ({ singular }) => { - expect(sourceAuditSingularRule.apply(`${singular}In`)).toEqual({ + expect(additionalPersonSingularRule.apply(`${singular}In`)).toEqual({ text: singular, replacements: 1 }); @@ -270,8 +270,8 @@ describe("sourceAuditSingularRule", () => { ["Köch*in", "Koch"], ["Arzt/in", "Arzt"], ["Matros_in", "Matrose"] - ])("normalisiert den quellengeprüften Singular %s", (input, expected) => { - expect(sourceAuditSingularRule.apply(input)).toEqual({ + ])("normalisiert den einzeln geprüften Singular %s", (input, expected) => { + expect(additionalPersonSingularRule.apply(input)).toEqual({ text: expected, replacements: 1 }); @@ -280,7 +280,7 @@ describe("sourceAuditSingularRule", () => { it("schützt normale Feminina und unbekannte Markerformen", () => { const input = "Followerin, Köchin, Fantasiefigur*in und FantasiefigurIn"; - expect(sourceAuditSingularRule.apply(input)).toEqual({ + expect(additionalPersonSingularRule.apply(input)).toEqual({ text: input, replacements: 0 }); @@ -292,7 +292,7 @@ describe("sourceAuditSingularRule", () => { "eine:n neue VerteidigerIn", "die neue VerteidigerIn" ])("vermeidet eine grammatisch unvollständige Teilkorrektur bei %s", (input) => { - expect(sourceAuditSingularRule.apply(input)).toEqual({ + expect(additionalPersonSingularRule.apply(input)).toEqual({ text: input, replacements: 0 }); @@ -303,8 +303,8 @@ describe("sourceAuditSingularRule", () => { ["Themenpat*in", "Themenpate"] ])("normalisiert das geprüfte Kompositum %s", (input, expected) => { const rule = input.includes("innenschaft") - ? sourceAuditPluralRule - : sourceAuditSingularRule; + ? additionalPersonPluralRule + : additionalPersonSingularRule; expect(rule.apply(input)).toEqual({ text: expected, replacements: 1