From 2b614b00b5becbdef8a401ec0d4b73da6b9b1bd2 Mon Sep 17 00:00:00 2001 From: Matthias Andreas Benkard Date: Sun, 23 Aug 2026 20:55:14 +0200 Subject: Der Stamm bekommt das Alter, das der Einleitungssatz nennt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Die beiden größten Restzahlen des Prüfbestands — GEG einundfünfzig, IfSG siebenundzwanzig manuell zu prüfende Befehle — waren nie gepinnt, und niemand hatte sie je gegen ein Sollergebnis gehalten. Der Verdacht stand im Raum, sie seien kein Mangel des Werkzeugs, sondern ein Altersunterschied: Das gii-XML ist die heutige konsolidierte Fassung, während das Änderungsgesetz von 2023 bzw. 2020 auf die damalige zielt. Der Verdacht trifft zu, aber nicht ganz. Beschafft sind die zeitrichtigen Fassungen aus dem Wayback-Schnappschuss des amtlichen Archivs „xml.zip“ — dasselbe Format, das der Loader ohnehin liest. Maßgeblich für die Wahl ist der Einleitungssatz des Änderungsgesetzes; beide Schnappschüsse tragen genau den dort genannten Stand. Gegen sie gerechnet, blieben zunächst fünf und sieben Reste. Diese zwölf sind erstmals echte Befunde gewesen, und neun von ihnen benannten vier allgemeine Mängel, die alle behoben sind: Eine Wortersetzung griff in längere Wörter hinein und verbrauchte mit „schwerwiegende“ auch „schwerwiegender“; eine eingerückte Aufzählungsmarke galt als Satzende, weshalb ein Absatz mit Aufzählung mehr Sätze zählte als das Gesetzblatt; eine Ordnungszahl vor einem Gliederungswort ebenso; eine Nummer wurde im ganzen Absatz gesucht statt im benannten Satz; und eine Gliederungseinheit, die Rahmen und Befehl beide nennen, suchte sich innerhalb ihrer selbst. Übrig bleiben drei Reste im GEG, jeder im Test einzeln begründet: zweimal dieselbe Kaskade des § 108 — die Aufhebung einer „bisherigen“ Nummer, deren Bezeichnung ein anderer Punkt gerade neu vergibt, und eine Begleitklausel ohne eigene Ortsangabe — und einmal die Überschrift einer Nummer der Anlage 8. Das letzte ist dieselbe offene Modellfrage wie bei Berlins Anlage. Vier gepinnte Zahlen sind dadurch gestiegen; der Grund steht jeweils im Test. Das WDR-Gesetz trägt seither keinen Rest mehr. Geprüft mit „mvnw verify“: 330 Prüfungen, kein Fehlschlag; REUSE 167/167. Co-Authored-By: Claude Opus 5 Change-Id: I338e78ec3d46e0d0d8e57e77831ebae34497ae7b --- src/test/java/eu/mulk/aendggner/EndToEndTest.java | 134 ++++++++++++++++++++-- 1 file changed, 124 insertions(+), 10 deletions(-) (limited to 'src/test/java/eu') diff --git a/src/test/java/eu/mulk/aendggner/EndToEndTest.java b/src/test/java/eu/mulk/aendggner/EndToEndTest.java index 8fa50f5..66b7f00 100644 --- a/src/test/java/eu/mulk/aendggner/EndToEndTest.java +++ b/src/test/java/eu/mulk/aendggner/EndToEndTest.java @@ -67,7 +67,8 @@ class EndToEndTest { // Warnung gemeldet werden, nicht als Abbruch. assertThat(parseErgebnis.warnungen()).isNotEmpty(); - // 4. Anwenden: wirft nicht; jeder Befehl erhält einen Protokolleintrag. + // 4. Anwenden: wirft nicht; jeder Befehl erhält einen Protokolleintrag. Auch hier ist das XML + // jünger als das Änderungsgesetz — die Sollzahlen stehen in ifsgGegenZeitrichtigenStamm(). var anwendung = BefehlAnwender.anwenden(gesetz, parseErgebnis.befehle()); assertThat(anwendung.protokoll()).hasSameSizeAs(parseErgebnis.befehle()); assertThat(anwendung.anzahlAngewandt()).isGreaterThan(0); @@ -80,6 +81,49 @@ class EndToEndTest { assertThat(html).containsAnyOf("", ""); } + /** + * Dasselbe Änderungsgesetz gegen den zeitrichtigen Stamm: das IfSG in der Fassung, die + * der Einleitungssatz nennt („zuletzt durch Artikel 5 des Gesetzes vom 19. Juni 2020 geändert“). + * + *

Der Fall daneben ({@link #gesamtePipelineAufIfSgBeispiel}) rechnet gegen die heutige + * konsolidierte Fassung und kann deshalb keine Sollzahl pinnen — dort blieben 27 von 75 Befehlen + * liegen, weil ihr Zieltext im Stamm längst anders lautet. Gegen den Stand vom 22. Juli 2020 geht + * die Rechnung vollständig auf: 75 von 75, kein Rest. Das ist zugleich der Beleg, dass die + * 27 nie ein Mangel des Werkzeugs waren. + */ + @Test + void ifsgGegenZeitrichtigenStamm() throws Exception { + var xml = SAMPLEDATA.resolve("IfSG/BJNR104510000-2020.xml"); + assumeTrue(Files.exists(xml) && Files.exists(AENDERUNGSGESETZ), "IfSG-Beispieldaten fehlen"); + + var gesetz = new GiiXmlLoader().load(xml); + var text = TextBereiniger.bereinige(new PatchTextExtraktor().extrahiere(AENDERUNGSGESETZ)); + var parseErgebnis = new AenderungsgesetzParser().parse(text, gesetz, null); + + assertThat(parseErgebnis.artikel()).containsExactly("1", "2"); + assertThat(parseErgebnis.befehle()).hasSize(75); + + var anwendung = BefehlAnwender.anwenden(gesetz, parseErgebnis.befehle()); + var manuellPfade = + anwendung.protokoll().stream() + .filter(a -> a.status() == BefehlAnwender.Status.MANUELL_PRUEFEN) + .map(a -> a.befehl().provenienz().gliederungsPfad()) + .toList(); + assertThat(manuellPfade).isEmpty(); + assertThat(anwendung.anzahlAngewandt()).isEqualTo(75); + + // Der neu eingefügte § 28a steht an seiner Stelle und trägt seine Überschrift. + var neu = anwendung.neu(); + assertThat(neu.norm("§ 28a")).isPresent(); + assertThat(neu.norm("§ 28a").orElseThrow().titel()) + .contains("Besondere Schutzmaßnahmen zur Verhinderung der Verbreitung"); + // „In Absatz 6 wird jeweils das Wort ‚schwerwiegende‘ durch ‚bedrohliche‘ … ersetzt“ — die + // Ersetzung greift nur ganze Wörter, sonst verbrauchte das erste Wort das zweite mit. + assertThat(neu.norm("§ 36").orElseThrow().gesamtText()) + .doesNotContain("schwerwiegend") + .contains("bedrohlicher übertragbarer Krankheiten"); + } + /** Neues digitales BGBl-Format (recht.bund.de, ab 2023): 3. UWGÄndG 2026. */ @Test void uwgNeuesBgblFormat() throws Exception { @@ -150,8 +194,9 @@ class EndToEndTest { parseErgebnis.befehle().stream().filter(b -> b instanceof UnbekannterBefehl).count(); assertThat(unbekannt).isZero(); - // Das XML ist bereits konsolidiert; entscheidend ist, dass die Anwendung sauber terminiert - // und jeden Befehl protokolliert. + // Das XML ist die heutige konsolidierte Fassung und damit jünger als das Änderungsgesetz; + // deshalb wird hier keine Sollzahl gepinnt, sondern nur, dass die Anwendung sauber terminiert + // und jeden Befehl protokolliert. Die Sollzahlen stehen in gegGegenZeitrichtigenStamm(). var anwendung = BefehlAnwender.anwenden(gesetz, parseErgebnis.befehle()); assertThat(anwendung.protokoll()).hasSameSizeAs(parseErgebnis.befehle()); // Gliederungs-Überschriften (Teil/Abschnitt) werden als Befehle erkannt und angewandt. @@ -160,6 +205,64 @@ class EndToEndTest { assertThat(HtmlRenderer.rendere(synopse, "E2E-Test")).contains("Manuell prüfen"); } + /** + * Dasselbe Änderungsgesetz gegen den zeitrichtigen Stamm: das GEG in der Fassung, die + * der Einleitungssatz nennt („durch Artikel 18a des Gesetzes vom 20. Juli 2022 geändert“). + * + *

Gegen die heutige konsolidierte Fassung ({@link #gegGrossesAenderungsgesetz}) blieben 51 von + * 119 Befehlen liegen, und der Test konnte deshalb keine Sollzahl pinnen. Gegen den Stand vom 24. + * April 2023 sind es 116 angewandte und drei Reste — und diese drei sind erstmals echte + * Befunde, die hier einzeln festgehalten sind: + * + *

    + *
  1. {@code 40. a) ff)} „Die bisherige Nummer 9 wird aufgehoben.“ — In derselben Kaskade macht + * Punkt bb) aus den bisherigen Nummern 4 bis 6 die Nummern 8 bis 10. Damit tragen + * vorübergehend zwei Einheiten die Bezeichnung 9, und die Aufhebung findet keine + * eindeutige. Die Schritt-Ordnung kennt bislang nur Umnummerierungen als Räumende; ein + * Schritt, dessen Ziel eine Bezeichnung ist, die ein anderer neu vergibt, müsste + * ebenso vorgezogen werden. + *
  2. {@code 40. a) hh)} „Die bisherige Nummer 18 wird Nummer 29 und nach der Angabe ‚Absatz 1‘ + * werden die Wörter ‚oder Absatz 4‘ eingefügt.“ — Die Begleitklausel löst norm-weit auf + * (bewusst so, siehe {@code BefehlErkenner}) und trifft dort 21 Vorkommen. Gemeint ist die + * soeben umnummerierte Einheit. + *
  3. {@code 43. b) aa)} „In der Überschrift werden die Wörter … gestrichen.“ — Ziel ist die + * Überschrift der Nummer 1 der Anlage 8. Das ist dieselbe offene Modellfrage wie + * bei Berlins Anlage: Die Nummern einer Anlage brauchen einen eigenen Rang zwischen Norm + * und Absatz. + *
+ */ + @Test + void gegGegenZeitrichtigenStamm() throws Exception { + var xml = SAMPLEDATA.resolve("GEG/BJNR172810020-2023.xml"); + var pdf = SAMPLEDATA.resolve("GEG/bgbl123s0280_regelungstext.pdf"); + assumeTrue(Files.exists(xml) && Files.exists(pdf), "GEG-Beispieldaten fehlen"); + + var gesetz = new GiiXmlLoader().load(xml); + var text = TextBereiniger.bereinige(new PatchTextExtraktor().extrahiere(pdf)); + var parseErgebnis = new AenderungsgesetzParser().parse(text, gesetz, "1"); + + assertThat(parseErgebnis.artikel()).containsExactly("1"); + assertThat(parseErgebnis.befehle()).hasSize(119); + assertThat(parseErgebnis.befehle()).noneMatch(b -> b instanceof UnbekannterBefehl); + + var anwendung = BefehlAnwender.anwenden(gesetz, parseErgebnis.befehle()); + var manuellPfade = + anwendung.protokoll().stream() + .filter(a -> a.status() == BefehlAnwender.Status.MANUELL_PRUEFEN) + .map(a -> a.befehl().provenienz().gliederungsPfad()) + .toList(); + assertThat(manuellPfade).containsExactly("40. a) ff)", "40. a) hh)", "43. b) aa)"); + assertThat(anwendung.anzahlAngewandt()).isEqualTo(116); + + var neu = anwendung.neu(); + // Der neue § 9a steht im Gesetz und in der Inhaltsübersicht. + assertThat(neu.norm("§ 9a")).isPresent(); + assertThat(neu.norm("Inhaltsübersicht").orElseThrow().gesamtText()) + .contains("§ 9a") + // „Die Angabe zur Überschrift von Teil 2 Abschnitt 4 wird gestrichen“ — sie ist fort. + .doesNotContain("Abschnitt 4 | Nutzung"); + } + /** * Gesetzentwurf (Bundestags-Drucksache mit markerloser Silbentrennung). Das AGG-XML ist NICHT * konsolidiert (Stand 2023, Entwurf von 2026) — hier entstehen echte Diffs. @@ -685,11 +788,15 @@ class EndToEndTest { .filter(a -> a.status() == BefehlAnwender.Status.MANUELL_PRUEFEN) .map(a -> a.befehl().provenienz().gliederungsPfad()) .toList(); - // Einziges Residuum: „In Satz 2 Nummer 1 wird … der Punkt am Ende des Satzes durch ein - // Semikolon ersetzt …“ — die adressierte Nummer 1 trägt zwei Sätze und endet selbst auf ein - // Komma; welchen Punkt der Befehl meint, ist dem Wortlaut nicht sicher zu entnehmen. - assertThat(manuellPfade).containsExactly("13. e) aa)"); - assertThat(anwendung.anzahlAngewandt()).isEqualTo(parseErgebnis.befehle().size() - 1); + // Kein Rest mehr. Das frühere Residuum „13. e) aa)“ — „In Satz 2 Nummer 1 wird … der Punkt am + // Ende des Satzes durch ein Semikolon ersetzt …“ — trifft seit dieser Welle seine Stelle: Die + // benannte Nummer wird innerhalb des benannten Satzes gesucht (StellenAufloeser.satzRahmen), + // und § 16 Abs. 6 Nr. 1 liest sich danach wie die amtliche Nachfassung („… überschreitet; der + // WDR hat …“). + assertThat(manuellPfade).isEmpty(); + assertThat(anwendung.anzahlAngewandt()).isEqualTo(parseErgebnis.befehle().size()); + assertThat(anwendung.neu().norm("§ 16").orElseThrow().absaetze().get(5).text()) + .contains("insgesamt drei Millionen Euro überschreitet; der WDR hat"); var neu = anwendung.neu(); // 1. Umnummerierungs-Kaskade in § 3: Absatz 2 wird durch zwei Absätze ersetzt, die bisherigen @@ -1305,7 +1412,10 @@ class EndToEndTest { var ausEmpfehlung = Pipeline.erzeugeSynopse(xml, List.of(empfehlung), null, false); var ausEntwurf = Pipeline.erzeugeSynopse(xml, List.of(entwurf), null, false); - assertThat(ausEmpfehlung.anzahlAngewandt()).isEqualTo(68); + // 69 statt der früheren 68: Nennt der Rahmen dieselbe Gliederungseinheit wie der Befehl („… + // Teil 2 wird wie folgt geändert: … die Angabe zur Überschrift von Teil 2 Abschnitt 4 …“), + // so gilt sie jetzt einmal statt zweimal (InhaltsuebersichtAnwender.zielKette). + assertThat(ausEmpfehlung.anzahlAngewandt()).isEqualTo(69); assertThat(ausEmpfehlung.anzahlAngewandt() + ausEmpfehlung.anzahlManuell()) .as("die Ausschussfassung trägt mehr Befehle als der Entwurf") .isGreaterThan(ausEntwurf.anzahlAngewandt() + ausEntwurf.anzahlManuell()); @@ -1329,7 +1439,11 @@ class EndToEndTest { var ergebnis = Pipeline.erzeugeSynopse(xml, List.of(pdf), null, false); - assertThat(ergebnis.anzahlAngewandt()).isEqualTo(47); + // 51 statt der früheren 47: Die Satzzählung folgt jetzt der amtlichen — eine eingerückte + // Aufzählungsmarke beendet keinen Satz, und eine Ordnungszahl vor einem Gliederungswort („nach + // dem 5. Abschnitt“) auch nicht (SatzTeiler); dazu wird eine benannte Nummer innerhalb des + // benannten Satzes gesucht. Vier satzbezogene Befehle treffen dadurch ihre Stelle. + assertThat(ergebnis.anzahlAngewandt()).isEqualTo(51); assertThat(ergebnis.html()).contains(", Ausschussfassung]"); } -- cgit v1.2.1