diff options
| author | Matthias Andreas Benkard <code@mail.matthias.benkard.de> | 2026-08-14 18:58:25 +0200 |
|---|---|---|
| committer | Matthias Andreas Benkard <code@mail.matthias.benkard.de> | 2026-08-14 18:58:25 +0200 |
| commit | 7235b34eeb6ff331c152116cbe4df846d7476110 (patch) | |
| tree | febb373089f624511a484316642bad31519ed5ca /src/test | |
| parent | 7b455cd43149834ccd2163b4e5d197809bd34a37 (diff) | |
Quellformate jenseits des verkündeten Gesetzes
Bisher nahm ÄndGgner nur verkündete Artikelgesetze und Gesetzentwürfe an, ohne
den Unterschied zu kennen. Dokumente, die einen Entwurf ändern statt ein
Gesetz, lieferten still null Befehle.
Neu:
* Dokumentart-Erkennung aus dem Rohtext (DokumentErkenner, DokumentArt,
DokumentKopf) — vor der Bereinigung, weil gerade die Drucksachenköpfe als
Kolumnentitel wegfallen. Erkannt wird nie am Dateinamen: Die Beispieldaten
enthalten eine Datei namens Beschlussempfehlung, die in Wahrheit ein
Entschließungsantrag ist. Dokumente ohne Befehle erzeugen jetzt eine
benannte Warnung statt stiller Leere.
* Änderungsanträge (AenderungsantragParser, EntwurfsPatcher): Der Antrag wird
auf den Entwurfstext angewandt, der geänderte Entwurf danach wie gewohnt auf
das Stammgesetz — eine komponierte Synopse. Drucksachen- und Gesetzesstellen
bleiben getrennte Typen; die Zuordnung Antrag→Entwurf läuft über die
Drucksachennummer, nicht über die Argumentreihenfolge. Der 1910 Zeilen große
BefehlAnwender bleibt unberührt.
* Beschlussempfehlungen werden erkannt und mit Begründung übergangen, die den
brauchbaren Entwurf beim Namen nennt. Die Spaltentrennung der
Zusammenstellung ist gebaut (FontgroessenFilter.Spalte, koordinatenbasiert
am Steg) und belegt: Die linke Spalte ergibt Befehl für Befehl den
Regierungsentwurf. Die rechte Spalte bleibt bewusst offen — sie vermerkt
„unverändert“ auch zeilenweise innerhalb zitierter Blöcke, ihre
Anführungszeichen gehen daher nicht auf; nötig ist die zeilenweise Zuordnung
beider Spalten über die gemeinsame Grundlinie.
Die Synopse kennzeichnet eine Entwurfsfassung als solche.
296 Tests grün (vorher 269); alle gepinnten Akzeptanzzahlen unverändert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Change-Id: I0001d25fcbd9969fc06eea675edc4326ce2e02f9
Diffstat (limited to 'src/test')
4 files changed, 406 insertions, 16 deletions
diff --git a/src/test/java/eu/mulk/aendggner/EndToEndTest.java b/src/test/java/eu/mulk/aendggner/EndToEndTest.java index 4e4d547..212e070 100644 --- a/src/test/java/eu/mulk/aendggner/EndToEndTest.java +++ b/src/test/java/eu/mulk/aendggner/EndToEndTest.java @@ -7,8 +7,11 @@ import eu.mulk.aendggner.aenderung.Aenderungsbefehl; import eu.mulk.aendggner.aenderung.Aenderungsbefehl.StrukturEinfuegung; import eu.mulk.aendggner.aenderung.Aenderungsbefehl.UnbekannterBefehl; import eu.mulk.aendggner.aenderung.Aenderungsbefehl.WortAnker; +import eu.mulk.aendggner.aenderung.parse.AenderungsantragParser; import eu.mulk.aendggner.aenderung.parse.AenderungsgesetzParser; +import eu.mulk.aendggner.aenderung.parse.EntwurfsPatcher; import eu.mulk.aendggner.aenderung.parse.PatchTextExtraktor; +import eu.mulk.aendggner.aenderung.parse.SuperskriptModus; import eu.mulk.aendggner.aenderung.parse.TextBereiniger; import eu.mulk.aendggner.aenderung.parse.ZitatExtraktor; import eu.mulk.aendggner.anwendung.BefehlAnwender; @@ -549,8 +552,9 @@ class EndToEndTest { var absatz2 = neu.norm("§ 1").orElseThrow().absaetze().get(1).text(); assertThat(absatz2) .contains("Verordnung (EU) 2016/679") - .contains("Telekommunikation-Digitale-Dienste-Datenschutz-Gesetzes vom 23. Juni 2021 (BGBl." - + " I S. 1982; 2022 I S. 1045)") + .contains( + "Telekommunikation-Digitale-Dienste-Datenschutz-Gesetzes vom 23. Juni 2021 (BGBl." + + " I S. 1982; 2022 I S. 1045)") .doesNotContain("WDR-Rundfunkdatenschutzbeauftragte"); // 3./4. § 2: Fundstellen-Ersetzung im Satzteil vor Nummer 1 und Neufassung der Nummern 1 und 2. var paragraph2 = neu.norm("§ 2").orElseThrow().gesamtText(); @@ -611,8 +615,7 @@ class EndToEndTest { "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15"); assertThat(paragraph3.absaetze().get(2).text()) .startsWith("Der WDR veranstaltet ein landesweites Fernsehprogramm"); - assertThat(paragraph3.absaetze().get(12).text()) - .startsWith("Der WDR strebt Partnerschaften"); + assertThat(paragraph3.absaetze().get(12).text()).startsWith("Der WDR strebt Partnerschaften"); // 2. Nummern-Kaskade in § 15 Absatz 3: Nummer 22 entfällt, die folgenden rücken auf — die // Aufzählungsmarken im Text sind mitgezogen. @@ -694,11 +697,11 @@ class EndToEndTest { * eine koordinatenbasierte Spaltenerkennung ist dafür nicht nötig. * * <p>Kein voller Akzeptanztest: gesetze.berlin.de ist wie das schleswig-holsteinische Portal eine - * anmeldepflichtige juris-Anwendung, die Stammfassungen sind daraus nicht zu beschaffen. Artikel 1 - * ändert zudem eine *Anlage*; anlagenbezogene Befehle wendet ÄndGgner an (siehe GEG), doch der - * {@link eu.mulk.aendggner.gesetz.land.LandesRechtTextParser} kennt nur „§“- und - * „Art.“-Normköpfe — eine handgepflegte Stammfassung könnte diese Anlage nicht tragen (dieselbe - * Grenze, an der Baden-Württemberg zurückgestellt wurde). + * anmeldepflichtige juris-Anwendung, die Stammfassungen sind daraus nicht zu beschaffen. Artikel + * 1 ändert zudem eine *Anlage*; anlagenbezogene Befehle wendet ÄndGgner an (siehe GEG), doch der + * {@link eu.mulk.aendggner.gesetz.land.LandesRechtTextParser} kennt nur „§“- und „Art.“-Normköpfe + * — eine handgepflegte Stammfassung könnte diese Anlage nicht tragen (dieselbe Grenze, an der + * Baden-Württemberg zurückgestellt wurde). */ @Test void asogLafAendGBerlin() throws Exception { @@ -737,8 +740,7 @@ class EndToEndTest { // § 2 Absatz 4 Satz 1 wird wie folgt geändert“). Der Zusatz benennt die Anlage nur — er darf // nicht als Ziel „§ 2“ gelesen werden; die Punkte erben „Anlage“ als Kontext. var asog = - new Gesetz( - "ASOG Bln", "Allgemeines Sicherheits- und Ordnungsgesetz", null, List.of()); + new Gesetz("ASOG Bln", "Allgemeines Sicherheits- und Ordnungsgesetz", null, List.of()); var artikel1 = new AenderungsgesetzParser().parse(text, asog, null); assertThat(artikel1.artikel()).containsExactly("1"); assertThat(artikel1.befehle()) @@ -764,8 +766,7 @@ class EndToEndTest { var einfuegung = (StrukturEinfuegung) bb; assertThat(einfuegung.vorher()).isTrue(); assertThat(einfuegung.bezeichnung()).isEqualTo("5"); - assertThat(einfuegung.anker()) - .isEqualTo(new WortAnker.VorWoertern("Aus dem Bereich Verkehr:")); + assertThat(einfuegung.anker()).isEqualTo(new WortAnker.VorWoertern("Aus dem Bereich Verkehr:")); // Bei diesem Punkt fehlt im amtlichen Satz das schließende Anführungszeichen. Ohne die Grenze // am nächsten Aufzählungspunkt verschlänge das offene Zitat die Punkte cc) und c). @@ -793,8 +794,8 @@ class EndToEndTest { var gesetz = new eu.mulk.aendggner.gesetz.land.LandesRechtLoader().load(alt); // Der Klammerzusatz führt hier nur eine (mehrteilige) Bezeichnung, keinen Kurztitel am // Gedankenstrich; genau sie nennt der Einleitungssatz des Artikels 4. - assertThat(gesetz.jurabk()).isEqualTo("Siebzehnter Rundfunkänderungsstaatsvertrag" - + " Ausführungsgesetz"); + assertThat(gesetz.jurabk()) + .isEqualTo("Siebzehnter Rundfunkänderungsstaatsvertrag" + " Ausführungsgesetz"); assertThat(gesetz.kurzue()).isNull(); assertThat(gesetz.normen()).hasSize(3); // Der alte § 1 besteht aus einem einzigen, unbezeichneten Absatz. @@ -805,7 +806,8 @@ class EndToEndTest { var parseErgebnis = new AenderungsgesetzParser().parse(text, gesetz, null); // Nur Artikel 4 trifft dieses Gesetz — die Auswahl gelingt über die Bezeichnung allein. assertThat(parseErgebnis.artikel()).containsExactly("4"); - // Der Artikel trägt keine nummerierten Punkte: Der Text nach der Änderungsformel ist der Befehl. + // Der Artikel trägt keine nummerierten Punkte: Der Text nach der Änderungsformel ist der + // Befehl. assertThat(parseErgebnis.befehle()).hasSize(1); assertThat(parseErgebnis.befehle().get(0)).isInstanceOf(Aenderungsbefehl.Neufassung.class); assertThat(parseErgebnis.befehle().get(0).stelle().anzeigeText()).isEqualTo("§ 1"); @@ -920,6 +922,140 @@ class EndToEndTest { .containsExactly("§ 14 Nummer 4", "§ 14 Nummer 3"); } + /** + * Änderungsantrag der GRÜNEN (Ltg-Drs. 19/10365) zum Landtags-Gesetzentwurf 19/9707: Er ändert + * nicht das Stammgesetz, sondern die Drucksache — genau den Befehl § 3 Nr. 22, der die Artenliste + * des neuen § 18 AVBayJG zitiert. + */ + @Test + void bayJgAenderungsantragAendertDenEntwurf() throws Exception { + var entwurfPdf = SAMPLEDATA.resolve("BayJG/Ltg-Drs-19-9707_Gesetzentwurf.pdf"); + var antragPdf = SAMPLEDATA.resolve("BayJG/Ltg-Drs-19-10365_Aenderungsantrag-Gruene.pdf"); + assumeTrue(Files.exists(entwurfPdf) && Files.exists(antragPdf), "BayJG-Beispieldaten fehlen"); + + var extraktor = new PatchTextExtraktor(SuperskriptModus.BEHALTEN); + var entwurf = TextBereiniger.bereinige(extraktor.extrahiere(entwurfPdf)); + var antrag = TextBereiniger.bereinige(extraktor.extrahiere(antragPdf)); + + // Der Entwurf stellt Wolf und Goldschakal nebeneinander unter Jagdrecht. + assertThat(entwurf).contains("1.29. Wolf (Canis lupus),"); + assertThat(entwurf).contains("1.30. Goldschakal (Canis aureus);"); + + var parseErgebnis = AenderungsantragParser.parse(antrag); + assertThat(parseErgebnis.warnungen()).isEmpty(); + assertThat(parseErgebnis.befehle()).hasSize(2); + + var patch = EntwurfsPatcher.wendeAn(entwurf, parseErgebnis.befehle()); + assertThat(patch.warnungen()).isEmpty(); + assertThat(patch.angewandt()).isEqualTo(2); + + // Der Goldschakal ist gestrichen, der Wolf schließt die Liste nun mit Semikolon ab. + assertThat(patch.text()).contains("1.29. Wolf (Canis lupus);"); + assertThat(patch.text()).doesNotContain("Goldschakal (Canis aureus)"); + // Die übrigen 154 Befehle des Entwurfs bleiben unangetastet. + assertThat(patch.text()).contains("1.28. Mink (Neovison vison),"); + assertThat(patch.text()).contains("2. Federwild:"); + } + + /** + * Derselbe Antrag durch die volle Pipeline. Er zielt auf § 3 des Entwurfs, und der ändert die + * AVBayJG, nicht das BayJG — die Synopse des Stammgesetzes bleibt deshalb zu Recht dieselbe. Das + * ist die eigentliche Probe: Ein Antrag darf nicht auf das Stammgesetz durchschlagen, nur weil + * seine Stellenangaben zufällig auch dort passen könnten. + */ + @Test + void bayJgAenderungsantragLaesstDasStammgesetzUnberuehrt() throws Exception { + var alt = SAMPLEDATA.resolve("BayJG/BayJG-alt.txt"); + var entwurfPdf = SAMPLEDATA.resolve("BayJG/Ltg-Drs-19-9707_Gesetzentwurf.pdf"); + var antragPdf = SAMPLEDATA.resolve("BayJG/Ltg-Drs-19-10365_Aenderungsantrag-Gruene.pdf"); + assumeTrue( + Files.exists(alt) && Files.exists(entwurfPdf) && Files.exists(antragPdf), + "BayJG-Beispieldaten fehlen"); + + var ohne = Pipeline.erzeugeSynopse(alt, List.of(entwurfPdf), null, false); + var mit = Pipeline.erzeugeSynopse(alt, List.of(entwurfPdf, antragPdf), null, false); + + assertThat(mit.anzahlAngewandt()).isEqualTo(ohne.anzahlAngewandt()).isEqualTo(151); + assertThat(mit.anzahlManuell()).isEqualTo(ohne.anzahlManuell()).isEqualTo(3); + // Beide Läufe zeigen eine Entwurfsfassung, nicht geltendes Recht. + assertThat(mit.html()).contains("Entwurfsfassung"); + assertThat(mit.html()).contains("[Änderungsantrag Drs. 19/10365]"); + } + + /** + * Die Zusammenstellung einer Beschlussempfehlung (BT-Drs. 20/7619) steht zweispaltig: links der + * Entwurf, rechts die Ausschussfassung. Anders als beim alten BGBl und beim Berliner GVBl folgen + * die Spalten <em>nicht</em> nacheinander im Inhaltsstrom, sondern zeilenweise verschränkt; nur + * die Koordinaten trennen sie. + * + * <p>Die Probe aufs Exempel für die Spaltentrennung: Die linke Spalte muss Wort für Wort den + * Regierungsentwurf ergeben, aus dem die Zusammenstellung gebaut ist — dieselbe Befehlszahl wie + * aus BT-Drs. 20/6875, das als eigener Testfall danebensteht. + */ + @Test + void zusammenstellungTrenntDieSpaltenSauber() throws Exception { + var xml = SAMPLEDATA.resolve("GEG/BJNR172810020.xml"); + var empfehlung = SAMPLEDATA.resolve("GEG/BT-Drs-20-7619_Beschlussempfehlung.pdf"); + var entwurf = SAMPLEDATA.resolve("GEG/BT-Drs-20-6875_Regierungsentwurf.pdf"); + assumeTrue( + Files.exists(xml) && Files.exists(empfehlung) && Files.exists(entwurf), + "GEG-Beispieldaten fehlen"); + + var gesetz = new GiiXmlLoader().load(xml); + var extraktor = new PatchTextExtraktor(); + var spalten = extraktor.extrahiereSpalten(empfehlung); + + var ausEntwurfsspalte = + new AenderungsgesetzParser() + .parse(TextBereiniger.bereinige(spalten.links()), gesetz, null, true); + var ausDrucksache = + new AenderungsgesetzParser() + .parse(TextBereiniger.bereinige(extraktor.extrahiere(entwurf)), gesetz, null, true); + + assertThat(ausEntwurfsspalte.artikel()).isEqualTo(ausDrucksache.artikel()); + assertThat(ausEntwurfsspalte.befehle()) + .as("die linke Spalte ist der Regierungsentwurf") + .hasSameSizeAs(ausDrucksache.befehle()); + assertThat(ausDrucksache.befehle()).hasSize(117); + + // Die rechte Spalte trägt überwiegend den Vermerk „unverändert“ — gesperrt gesetzt im PDF, + // vom TextBereiniger auf die Normalform gebracht. + assertThat(TextBereiniger.bereinige(spalten.rechts())).contains("unverändert"); + } + + /** + * Eine Beschlussempfehlung wird als solche erkannt und mit Begründung übergangen, statt eine halb + * aufgelöste Fassung auszugeben. Die Meldung nennt den Entwurf, der stattdessen taugt. + */ + @Test + void beschlussempfehlungWirdMitBegruendungUebergangen() throws Exception { + var xml = SAMPLEDATA.resolve("GEG/BJNR172810020.xml"); + var pdf = SAMPLEDATA.resolve("GEG/BT-Drs-20-7619_Beschlussempfehlung.pdf"); + assumeTrue(Files.exists(xml) && Files.exists(pdf), "GEG-Beispieldaten fehlen"); + + var ergebnis = Pipeline.erzeugeSynopse(xml, List.of(pdf), null, false); + + assertThat(ergebnis.anzahlAngewandt()).isZero(); + assertThat(ergebnis.html()).contains("Beschlussempfehlung"); + assertThat(ergebnis.html()).contains("Drs. 20/6875"); + } + + /** + * Ein Entschließungsantrag trägt keine Rechtsetzungsbefehle. Er wird als solcher erkannt, + * übergangen und gemeldet — nicht stillschweigend zu null Befehlen verarbeitet. + */ + @Test + void entschliessungsantragWirdGemeldetStattStillUebergangen() throws Exception { + var xml = SAMPLEDATA.resolve("GEG/BJNR172810020.xml"); + var pdf = SAMPLEDATA.resolve("GEG/BT-Drs-21-7071_Beschlussempfehlung.pdf"); + assumeTrue(Files.exists(xml) && Files.exists(pdf), "GEG-Beispieldaten fehlen"); + + var ergebnis = Pipeline.erzeugeSynopse(xml, List.of(pdf), null, false); + + assertThat(ergebnis.anzahlAngewandt()).isZero(); + assertThat(ergebnis.html()).contains("keine Änderungsbefehle"); + } + private static Aenderungsbefehl befehlZu( AenderungsgesetzParser.ParseErgebnis ergebnis, String gliederungsPfad) { return ergebnis.befehle().stream() diff --git a/src/test/java/eu/mulk/aendggner/aenderung/parse/AenderungsantragParserTest.java b/src/test/java/eu/mulk/aendggner/aenderung/parse/AenderungsantragParserTest.java new file mode 100644 index 0000000..fcb1c80 --- /dev/null +++ b/src/test/java/eu/mulk/aendggner/aenderung/parse/AenderungsantragParserTest.java @@ -0,0 +1,81 @@ +package eu.mulk.aendggner.aenderung.parse; + +import static org.assertj.core.api.Assertions.assertThat; + +import eu.mulk.aendggner.aenderung.Aenderungsbefehl; +import org.junit.jupiter.api.Test; + +class AenderungsantragParserTest { + + /** Der bayerische Beleg (Ltg-Drs. 19/10365), auf den Beschlussteil verkürzt. */ + private static final String ANTRAG = + """ + Änderungsantrag + der Abgeordneten Katharina Schulze und Fraktion (BÜNDNIS 90/DIE GRÜNEN) + hier: Goldschakal nicht ins Jagdrecht aufnehmen + (Drs. 19/9707) + Der Landtag wolle beschließen: + In § 3 Nr. 22 wird § 18 Nr. 1 wie folgt geändert: + 1. In Nr. 1.29 die Angabe „ ,“ am Ende durch die Angabe „ ;“ ersetzt. + 2. Nr. 1.30 aufgehoben. + Begründung: + Die Aufnahme des Goldschakals ins Jagdrecht ist nicht zielführend. + """; + + @Test + void liestBeideBefehleAusDemBeschlussteil() { + var ergebnis = AenderungsantragParser.parse(ANTRAG); + + assertThat(ergebnis.warnungen()).isEmpty(); + assertThat(ergebnis.befehle()).hasSize(2); + + var erster = ergebnis.befehle().get(0); + assertThat(erster.drucksachenStelle().container()).isEqualTo("§ 3"); + assertThat(erster.drucksachenStelle().punktPfad()).containsExactly("22"); + assertThat(erster.zitatStelle().anzeigeText()).isEqualTo("§ 18 Nummer 1"); + // Die elliptische Antragsform („… ersetzt.“ ohne „wird“) wird zum vollständigen Satz ergänzt. + assertThat(erster.befehl()).isInstanceOf(Aenderungsbefehl.Ersetzung.class); + var ersetzung = (Aenderungsbefehl.Ersetzung) erster.befehl(); + assertThat(ersetzung.alt()).isEqualTo(","); + assertThat(ersetzung.neu()).isEqualTo(";"); + assertThat(ersetzung.amEnde()).isTrue(); + assertThat(ersetzung.stelle().anzeigeText()).isEqualTo("Nummer 1.29"); + + var zweiter = ergebnis.befehle().get(1); + assertThat(zweiter.befehl()).isInstanceOf(Aenderungsbefehl.Aufhebung.class); + assertThat(zweiter.befehl().stelle().anzeigeText()).isEqualTo("Nummer 1.30"); + } + + /** Der Begründungsteil steht hinter den Befehlen und darf keine erzeugen. */ + @Test + void begruendungErzeugtKeineBefehle() { + var mitLangerBegruendung = + ANTRAG + "1. Der Goldschakal ist in Anhang V aufgeführt.\n2. Er ist kein jagdbares Wild.\n"; + assertThat(AenderungsantragParser.parse(mitLangerBegruendung).befehle()).hasSize(2); + } + + @Test + void ohneBeschlussformelWirdGewarntStattStillGeschwiegen() { + var ergebnis = AenderungsantragParser.parse("Änderungsantrag\nIrgendein Fließtext.\n"); + + assertThat(ergebnis.befehle()).isEmpty(); + assertThat(ergebnis.warnungen()).singleElement().asString().contains("wolle beschließen"); + } + + /** Die Bundestagsform des Rahmens: „In Artikel 1 Nummer 3 …“. */ + @Test + void erkenntArtikelAlsDrucksachenContainer() { + var antrag = + """ + Änderungsantrag + Der Bundestag wolle beschließen: + In Artikel 1 Nummer 3 wird § 9a Nr. 2 wie folgt geändert: + 1. Nr. 2.1 aufgehoben. + """; + var ergebnis = AenderungsantragParser.parse(antrag); + + assertThat(ergebnis.befehle()).hasSize(1); + assertThat(ergebnis.befehle().get(0).drucksachenStelle().container()).isEqualTo("Artikel 1"); + assertThat(ergebnis.befehle().get(0).drucksachenStelle().punktPfad()).containsExactly("3"); + } +} diff --git a/src/test/java/eu/mulk/aendggner/aenderung/parse/DokumentErkennerTest.java b/src/test/java/eu/mulk/aendggner/aenderung/parse/DokumentErkennerTest.java new file mode 100644 index 0000000..7271efb --- /dev/null +++ b/src/test/java/eu/mulk/aendggner/aenderung/parse/DokumentErkennerTest.java @@ -0,0 +1,66 @@ +package eu.mulk.aendggner.aenderung.parse; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import eu.mulk.aendggner.aenderung.DokumentArt; +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class DokumentErkennerTest { + + private static final Path SAMPLEDATA = Path.of("src/main/resources/sampledata"); + + /** + * Die Erkennung ist eine Heuristik auf Kopfzeilen; sie wird deshalb gegen wirkliche Dokumente + * festgenagelt, je eines pro Art und Herausgeber. Der Dateiname zählt dabei ausdrücklich nicht — + * {@code BT-Drs-21-7071_Beschlussempfehlung.pdf} heißt so, ist aber ein Entschließungsantrag. + */ + @ParameterizedTest + @CsvSource({ + "BayJG/Ltg-Drs-19-10365_Aenderungsantrag-Gruene.pdf, AENDERUNGSANTRAG", + "BayJG/Ltg-Drs-19-9707_Gesetzentwurf.pdf, GESETZENTWURF", + "BayJG/Plenarprotokoll-19-72_2te-Lesung.pdf, OHNE_BEFEHLE", + "BayJG/gvbl-2026-06.pdf, ARTIKELGESETZ", + "GEG/BT-Drs-20-7619_Beschlussempfehlung.pdf, BESCHLUSSEMPFEHLUNG", + "GEG/BT-Drs-21-7071_Beschlussempfehlung.pdf, OHNE_BEFEHLE", + "GEG/BT-Drs-20-6875_Regierungsentwurf.pdf, GESETZENTWURF", + "GEG/Referentenentwurf_GModG_2026-05-05.pdf, GESETZENTWURF", + "GEG/bgbl123s0280_regelungstext.pdf, ARTIKELGESETZ", + "IfSG/1924334.pdf, BESCHLUSSEMPFEHLUNG", + "NRW/GV-NRW-2026-S202_22-Rundfunkaenderungsgesetz.pdf, ARTIKELGESETZ", + "Sachsen/SaechsGVBl-2026-S134_AendG-SaechsBeamtVG_revosax.pdf, ARTIKELGESETZ", + }) + void ordnetBeispieldokumenteRichtigEin(String datei, DokumentArt erwartet) throws Exception { + var pfad = SAMPLEDATA.resolve(datei.strip()); + assumeTrue(Files.exists(pfad), "Beispieldatei fehlt: " + datei); + + var kopf = DokumentErkenner.erkenne(new PatchTextExtraktor().extrahiere(pfad)); + + assertThat(kopf.art()).isEqualTo(erwartet); + } + + /** Die Drucksachennummern stiften die Verbindung zwischen Antrag und Entwurf. */ + @Test + void liestEigeneUndBezogeneDrucksachennummern() throws Exception { + var antrag = SAMPLEDATA.resolve("BayJG/Ltg-Drs-19-10365_Aenderungsantrag-Gruene.pdf"); + assumeTrue(Files.exists(antrag), "BayJG-Beispieldaten fehlen"); + + var kopf = DokumentErkenner.erkenne(new PatchTextExtraktor().extrahiere(antrag)); + + assertThat(kopf.eigeneDrucksache()).isEqualTo("19/10365"); + assertThat(kopf.bezugsDrucksachen()).containsExactly("19/9707"); + assertThat(kopf.titel()).isEqualTo("Goldschakal nicht ins Jagdrecht aufnehmen"); + } + + @Test + void kennzeichnetEntwurfsfassungen() { + assertThat(DokumentArt.ARTIKELGESETZ.istEntwurfsfassung()).isFalse(); + assertThat(DokumentArt.GESETZENTWURF.istEntwurfsfassung()).isTrue(); + assertThat(DokumentArt.AENDERUNGSANTRAG.istEntwurfsfassung()).isTrue(); + assertThat(DokumentArt.BESCHLUSSEMPFEHLUNG.istEntwurfsfassung()).isTrue(); + } +} diff --git a/src/test/java/eu/mulk/aendggner/aenderung/parse/EntwurfsPatcherTest.java b/src/test/java/eu/mulk/aendggner/aenderung/parse/EntwurfsPatcherTest.java new file mode 100644 index 0000000..5d76106 --- /dev/null +++ b/src/test/java/eu/mulk/aendggner/aenderung/parse/EntwurfsPatcherTest.java @@ -0,0 +1,107 @@ +package eu.mulk.aendggner.aenderung.parse; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +class EntwurfsPatcherTest { + + /** + * Ein Entwurf im Kleinen: § 3 ändert eine Verordnung, sein 22. Befehl fasst § 18 neu und zitiert + * dabei eine Artenliste. Genau in dieses Zitat greift der Antrag hinein. + */ + private static final String ENTWURF = + """ + § 2 + Änderung eines anderen Gesetzes + 1. § 4 wird aufgehoben. + § 3 + Änderung der Verordnung zur Ausführung des Bayerischen Jagdgesetzes + Die Verordnung wird wie folgt geändert: + 21. § 17 wird aufgehoben. + 22. § 18 wird wie folgt gefasst: + „§ 18 + Tierarten + Dem Jagdrecht unterliegen folgende Tierarten: + 1. Haarwild: + 1.28. Mink (Neovison vison), 1.29. Wolf (Canis lupus), + 1.30. Goldschakal (Canis aureus); + 2. Federwild: + 2.1. Rebhuhn (Perdix perdix).“ + 23. § 19 wird aufgehoben. + § 4 + Inkrafttreten + """; + + private static final String ANTRAG = + """ + Änderungsantrag + Der Landtag wolle beschließen: + In § 3 Nr. 22 wird § 18 Nr. 1 wie folgt geändert: + 1. In Nr. 1.29 die Angabe „ ,“ am Ende durch die Angabe „ ;“ ersetzt. + 2. Nr. 1.30 aufgehoben. + """; + + @Test + void aendertGenauDasZitierteAufzaehlungsglied() { + var befehle = AenderungsantragParser.parse(ANTRAG).befehle(); + + var ergebnis = EntwurfsPatcher.wendeAn(ENTWURF, befehle); + + assertThat(ergebnis.warnungen()).isEmpty(); + assertThat(ergebnis.angewandt()).isEqualTo(2); + // Das Komma nach dem Wolf wird zum Semikolon, weil er nun das letzte Glied ist … + assertThat(ergebnis.text()).contains("1.29. Wolf (Canis lupus);"); + // … und der Goldschakal verschwindet. + assertThat(ergebnis.text()).doesNotContain("Goldschakal"); + // Alles andere bleibt unangetastet, insbesondere die Nachbarglieder und die Nachbarbefehle. + assertThat(ergebnis.text()).contains("1.28. Mink (Neovison vison),"); + assertThat(ergebnis.text()).contains("2. Federwild:"); + assertThat(ergebnis.text()).contains("21. § 17 wird aufgehoben."); + assertThat(ergebnis.text()).contains("23. § 19 wird aufgehoben."); + } + + /** + * Die Aufzählung des Zitats („1. Haarwild:“) sieht aus wie ein Gliederungspunkt des Entwurfs. Der + * Patcher darf sie nicht dafür halten, sonst endete der Punkt 22 schon vor der Artenliste. + */ + @Test + void verwechseltZitatAufzaehlungNichtMitEntwurfsGliederung() { + var befehle = AenderungsantragParser.parse(ANTRAG).befehle(); + assertThat(EntwurfsPatcher.wendeAn(ENTWURF, befehle).angewandt()).isEqualTo(2); + } + + @Test + void meldetEinenBefehlAufEineNichtVorhandeneStelle() { + var antrag = + """ + Änderungsantrag + Der Landtag wolle beschließen: + In § 3 Nr. 99 wird § 18 Nr. 1 wie folgt geändert: + 1. Nr. 1.30 aufgehoben. + """; + + var ergebnis = EntwurfsPatcher.wendeAn(ENTWURF, AenderungsantragParser.parse(antrag).befehle()); + + assertThat(ergebnis.angewandt()).isZero(); + assertThat(ergebnis.text()).isEqualTo(ENTWURF); + assertThat(ergebnis.warnungen()).singleElement().asString().contains("§ 3 99"); + } + + /** Ein Befehl auf einen anderen Entwurfsparagraphen darf dessen Nachbarn nicht treffen. */ + @Test + void bleibtImAngesprochenenParagraphen() { + var antrag = + """ + Änderungsantrag + Der Landtag wolle beschließen: + In § 2 Nr. 22 wird § 18 Nr. 1 wie folgt geändert: + 1. Nr. 1.30 aufgehoben. + """; + + var ergebnis = EntwurfsPatcher.wendeAn(ENTWURF, AenderungsantragParser.parse(antrag).befehle()); + + assertThat(ergebnis.angewandt()).isZero(); + assertThat(ergebnis.text()).contains("Goldschakal"); + } +} |
