aboutsummaryrefslogtreecommitdiff
path: root/src/test/java
diff options
context:
space:
mode:
authorMatthias Andreas Benkard <code@mail.matthias.benkard.de>2026-08-29 18:16:18 +0200
committerMatthias Andreas Benkard <code@mail.matthias.benkard.de>2026-08-29 18:16:18 +0200
commit86c7d994e2fc7b469b660e8adee3d5f1a4e0f11c (patch)
tree0a62598372af06fe69742b89a9675ec734b47ec1 /src/test/java
parentc1013fc0ec0b1e3276cdf92e692f4333b1d5eb3a (diff)
Der Länderkreis schließt sich mit Saarland und Sachsen-Anhalt
Sechzehn Länder hat die Bundesrepublik, vierzehn waren erfaßt. Die beiden letzten fehlten aus demselben Grund: Ihre Landesrechtsportale führen keine Fassungsliste, und ohne Vorfassung gibt es keinen Belegfall. Für das Saarland hilft die Änderungshistorie aus. Weist sie für das fragliche Jahr genau eine Änderung aus, so ist die Vorfassung die Gesamtausgabe ohne diese eine. Für das Gesetz über den Saarlandpakt und die Verordnung zu seiner Ausführung — beide geändert durch das Gesetz Nr. 2211 vom 24. Juni 2026 — ist das so hergeleitet und aus den Gesetzblättern von 2019, 2020 und 2024 Norm für Norm gegengeprüft worden: Die Ursprungsfassung nebst den beiden zwischenzeitlichen Änderungen ergibt genau die heutige Gesamtausgabe. Ein Heft, zwei Stammwerke, zwei Aufträge; zehn von zehn Normen der Verordnung und neunzehn von zwanzig des Gesetzes stimmen zeichengenau. Sachsen-Anhalt bleibt beim rheinland-pfälzischen Weg, und zwar doppelt begründet: Das Portal führt keine Vorfassung, und das Gesetz- und Verordnungsblatt des Landes ist überhaupt nicht frei. Frei ist die Parlamentsdokumentation, weshalb hier die Drucksache 8/6357 des Landtags das Änderungsdokument ist; aussagekräftig ist allein die Zahl der erschlossenen Befehle, vierzehn an der Zahl. Fünf Funde sind allgemein und stehen im Produktivcode: der Kolumnentitel des saarländischen Amtsblattes; die Aufzählungsmarke, die Kennzeichnung ist und nicht Wortlaut, sodaß „in Nummer 8 den Punkt ersetzen“ nicht mehr am eigenen Markenpunkt scheitert; die Artikelwahl bei Ausführungsverordnungen, die ihren eigenen Namen nicht mehr wegstreicht; die Wortfuge am markerlosen Umbruch, die der Wortbestand ohne die zeilenletzten Wörter beweist; und die Buchstabengliederung mit Punkt nebst den kleinen römischen Zahlen, bei denen „i.“ aus dem Stand der Gliederung entschieden wird. Nicht selbsttätig verarbeitbar bleiben die auslassende Anfügung, die Anfügung an die Überschrift des Werkes selbst, der Verbund aus Bereichsumnummerierung und Wortstreichung sowie die kurze Wortfuge; sie sind im Handbuch und in den Herkunftsnachweisen als Grenzen benannt. Die Prüfung geht auf: 456 Testfälle, „mvnw verify“ grün, „reuse lint“ 223 von 223, und der Korpuslauf hält seine Grundlinie bei vierunddreißig Aufträgen, 1429 Befehlen, 1295 angewandt, 661 von 671 gleichen Normen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014dzFZB6rfFjpVMxhF5mQWN Change-Id: I046353512a672afda7b908f6aee425e4daba507c
Diffstat (limited to 'src/test/java')
-rw-r--r--src/test/java/eu/mulk/aendggner/EndToEndTest.java148
-rw-r--r--src/test/java/eu/mulk/aendggner/aenderung/parse/TextBereinigerTest.java27
-rw-r--r--src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java25
3 files changed, 200 insertions, 0 deletions
diff --git a/src/test/java/eu/mulk/aendggner/EndToEndTest.java b/src/test/java/eu/mulk/aendggner/EndToEndTest.java
index ea014b0..2466d4c 100644
--- a/src/test/java/eu/mulk/aendggner/EndToEndTest.java
+++ b/src/test/java/eu/mulk/aendggner/EndToEndTest.java
@@ -7,6 +7,7 @@ import static org.junit.jupiter.api.Assumptions.assumeTrue;
import eu.mulk.aendggner.aenderung.Aenderungsbefehl;
import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Ersetzung;
+import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Neufassung;
import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Sammelbefehl;
import eu.mulk.aendggner.aenderung.Aenderungsbefehl.StrukturEinfuegung;
import eu.mulk.aendggner.aenderung.Aenderungsbefehl.UnbekannterBefehl;
@@ -2311,4 +2312,151 @@ class EndToEndTest {
assertThat(abgleich.abweichungen()).isEmpty();
assertThat(abgleich.gehtAuf()).as(abgleich.kurzbericht()).isTrue();
}
+
+ /**
+ * Saarland, Artikel 1: Gesetz Nr. 2211 zur Änderung des Gesetzes über den Saarlandpakt (Amtsbl.
+ * des Saarlandes Teil I Nr. 31 vom 13. August 2026, S. 756). Voller Akzeptanztest — der eine
+ * Befehl wird angewandt, und alle zwanzig Normen stimmen mit der amtlichen Gesamtausgabe überein.
+ *
+ * <p>Das Portal recht.saarland.de führt wie das rheinland-pfälzische keine Fassungsliste. Die
+ * Vorfassung ist deshalb der Änderungshistorie entnommen — sie weist für das Jahr 2026 genau eine
+ * Änderung aus (§ 8a neu eingefügt) — und aus den Gesetzblättern von 2019, 2020 und 2024 Norm für
+ * Norm gegengeprüft; das Nähere sagt {@code Saarland/SOURCES}.
+ *
+ * <p>Neunzehn der zwanzig Normen stimmen zeichengenau; die zwanzigste — der eingefügte § 8a —
+ * weicht an einer Stelle ab („zuergreifen“ statt „zu ergreifen“), weil der Kopf vor dem Umbruch
+ * mit zwei Zeichen zu kurz ist, um ihn von einer Vorsilbe zu unterscheiden.
+ *
+ * <p>Zwei allgemeine Funde stammen aus diesem Heft: der Kolumnentitel des Amtsblattes, der im
+ * Inhaltsstrom steht und an der Seitenzahl klebt, und die <em>Wortfuge am markerlosen
+ * Umbruch</em> — im engen Zweispaltensatz verliert der Auszug an einer echten Wortgrenze den
+ * Zwischenraum („zusätzlich“ + „jahresbezogene“). Entschieden wird das wie beim Trennstrich am
+ * Wortbestand des Dokuments, und zwar an demjenigen ohne die zeilenletzten Wörter: Ein durch den
+ * Umbruch abgerissenes Bruchstück steht immer am Zeilenende, ein wirkliches Wort auch mitten in
+ * der Zeile.
+ */
+ @Test
+ void saarlandpaktGesetzSaarland() throws Exception {
+ var alt = SAMPLEDATA.resolve("Saarland/SPaktG-alt.txt");
+ var pdf = SAMPLEDATA.resolve("Saarland/ABl-2026-31_Saarlandpakt-AendG.pdf");
+ assumeTrue(Files.exists(alt) && Files.exists(pdf), "Saarländische Beispieldaten fehlen");
+
+ var gesetz = new eu.mulk.aendggner.gesetz.land.LandesRechtLoader().load(alt);
+ assertThat(gesetz.jurabk()).isEqualTo("SPaktG");
+ assertThat(gesetz.normen()).hasSize(19);
+
+ var text = TextBereiniger.bereinige(new PatchTextExtraktor().extrahiere(pdf));
+ // Der Kolumnentitel ist heraus; ohne ihn steht das Zitat des eingefügten § 8a zusammenhängend
+ // da.
+ assertThat(text).doesNotContain("Amtsblatt des Saarlandes Teil I vom 13. August 2026");
+ // Die Wortfuge am markerlosen Umbruch ist geheilt.
+ assertThat(text).contains("zusätzlich jahresbezogene");
+ assertThat(text).doesNotContain("zusätzlichjahresbezogene");
+
+ var parseErgebnis = new AenderungsgesetzParser().parse(text, gesetz, null);
+ // Das Heft ändert zwei Werke; für das Gesetz wird allein Artikel 1 gewählt.
+ assertThat(parseErgebnis.artikel()).containsExactly("1");
+ assertThat(parseErgebnis.befehle()).hasSize(1);
+ assertThat(parseErgebnis.befehle()).noneMatch(b -> b instanceof UnbekannterBefehl);
+
+ var anwendung = BefehlAnwender.anwenden(gesetz, parseErgebnis.befehle());
+ assertThat(anwendung.anzahlAngewandt()).isEqualTo(1);
+ var soll =
+ new eu.mulk.aendggner.gesetz.land.LandesRechtLoader()
+ .load(SAMPLEDATA.resolve("Saarland/SPaktG-neu.txt"));
+ assertThat(soll.normen()).hasSize(20);
+ var abgleich = Nachfassungsabgleich.vergleiche(soll, anwendung.neu());
+ assertThat(abgleich.fehlende()).isEmpty();
+ assertThat(abgleich.ueberzaehlige()).isEmpty();
+ // Neunzehn der zwanzig Normen stimmen zeichengenau; allein der neu eingefügte § 8a weicht ab,
+ // und zwar an einer einzigen Stelle: „zu ergreifen“ steht dort als „zuergreifen“. Der Kopf vor
+ // dem Umbruch ist mit zwei Zeichen zu kurz, um ihn von einer Vorsilbe zu unterscheiden — und
+ // wer „aus“ + „geschlossen“ trennte, machte es schlimmer. Geraten wird nicht.
+ assertThat(abgleich.abweichungen()).hasSize(1);
+ assertThat(abgleich.abweichungen().getFirst().enbez()).isEqualTo("§ 8a");
+
+ var synopse = Pipeline.erzeugeSynopse(Pipeline.Auftrag.von(alt, List.of(pdf)));
+ assertThat(synopse.anzahlAngewandt()).isEqualTo(1);
+ }
+
+ /**
+ * Saarland, Artikel 2 desselben Heftes: die Verordnung zur Ausführung des Gesetzes über den
+ * Saarlandpakt. Ein Heft, zwei Stammwerke — und der Prüfstein für die Artikelwahl.
+ *
+ * <p>Der Fund: Die Artikelwahl streicht aus dem Vorspann die Wendung „zur Ausführung des
+ * <em>X-Gesetzes</em>“, weil ein Ausführungstitel das Stammgesetz nur als Genitiv-Attribut nennt
+ * und ein solcher Artikel nicht das Stammgesetz betrifft (so beim bayerischen AVBayJG). Ist das
+ * Ziel aber selbst eine solche Verordnung, so stand nach dem Streichen gerade ihr eigener Name
+ * nicht mehr da und <em>kein</em> Artikel wurde gewählt. Die Streichung unterbleibt nun, wenn das
+ * Ziel selbst einen Ausführungstitel führt; getroffen wird dann nur, wer den vollen Titel nennt.
+ */
+ @Test
+ void saarlandpaktVerordnungSaarland() throws Exception {
+ var alt = SAMPLEDATA.resolve("Saarland/AusfSPaktGVO-alt.txt");
+ var pdf = SAMPLEDATA.resolve("Saarland/ABl-2026-31_Saarlandpakt-AendG.pdf");
+ assumeTrue(Files.exists(alt) && Files.exists(pdf), "Saarländische Beispieldaten fehlen");
+
+ var verordnung = new eu.mulk.aendggner.gesetz.land.LandesRechtLoader().load(alt);
+ assertThat(verordnung.jurabk()).isEqualTo("AusfSPaktGVO");
+ assertThat(verordnung.normen()).hasSize(10);
+
+ var text = TextBereiniger.bereinige(new PatchTextExtraktor().extrahiere(pdf));
+ var parseErgebnis = new AenderungsgesetzParser().parse(text, verordnung, null);
+ assertThat(parseErgebnis.artikel()).containsExactly("2");
+ assertThat(parseErgebnis.befehle()).hasSize(1);
+ assertThat(parseErgebnis.befehle()).noneMatch(b -> b instanceof UnbekannterBefehl);
+
+ pruefeGegenNachfassung(verordnung, parseErgebnis, "Saarland/AusfSPaktGVO-neu.txt");
+ }
+
+ /**
+ * Sachsen-Anhalt: der Entwurf eines Zweiten Gesetzes zur Änderung des Fischereigesetzes (Landtag
+ * von Sachsen-Anhalt, Drucksache 8/6357).
+ *
+ * <p>Wie in Rheinland-Pfalz reicht die Prüfung nur bis zur Befehlserkennung, und aus demselben
+ * Grund: Das Portal landesrecht.sachsen-anhalt.de führt keine Fassungsliste, der Stamm ist
+ * deshalb die <em>Nachfassung</em> (die Vorlage ist am 21. März 2026 Gesetz geworden). Das
+ * Gesetz- und Verordnungsblatt des Landes ist überdies nicht frei; frei ist allein die
+ * Parlamentsdokumentation, weshalb hier die Drucksache das Änderungsdokument ist.
+ *
+ * <p>Der Fund liegt in der Gliederung: Sachsen-Anhalt setzt unter die Nummern nicht „a)“ und
+ * „aa)“, sondern <b>„a.“ und kleine römische Zahlen „i.“, „ii.“</b>. Beide Formen sind den
+ * Klammerformen wesensgleich; nur das Satzzeichen unterscheidet sie. „i.“ ist dabei zweideutig —
+ * als Buchstabe die neunte, als Zahl die erste Marke; entschieden wird es aus dem Stand der
+ * Gliederung. Dazu kommt die Nebenform „erhält die Fassung“ und der wahlfreie Doppelpunkt vor dem
+ * Zitat.
+ *
+ * <p>Nicht erkannt bleiben zwei Formen, und zwar zu Recht: „In § 31 Abs. 2 wie folgt geändert:“ —
+ * dem Satz fehlt das Verb, ein Verkündungsfehler der Vorlage — und die auslassende Anfügung „Als
+ * Abs. 2 neu angefügt: „…““. Geraten wird nicht.
+ */
+ @Test
+ void fischereigesetzSachsenAnhalt() throws Exception {
+ var stamm = SAMPLEDATA.resolve("SachsenAnhalt/FischG-LSA-neu.txt");
+ var pdf = SAMPLEDATA.resolve("SachsenAnhalt/Ltg-Drs-8-6357_Fischereigesetz-AendG-Entwurf.pdf");
+ assumeTrue(Files.exists(stamm) && Files.exists(pdf), "Beispieldaten Sachsen-Anhalts fehlen");
+
+ var gesetz = new eu.mulk.aendggner.gesetz.land.LandesRechtLoader().load(stamm);
+ assertThat(gesetz.jurabk()).isEqualTo("FischG");
+ assertThat(gesetz.normen()).hasSize(59);
+
+ var text = TextBereiniger.bereinige(new PatchTextExtraktor().extrahiere(pdf));
+ var parseErgebnis = new AenderungsgesetzParser().parse(text, gesetz, null);
+ assertThat(parseErgebnis.artikel()).containsExactly("1");
+ assertThat(parseErgebnis.befehle()).hasSize(14);
+
+ // Die Buchstabengliederung mit Punkt und die römische Unterebene sind aufgeschlossen: Nummer 1
+ // trägt fünf Buchstaben, und unter b. und e. stehen je zwei römische Punkte.
+ assertThat(befehlAn(parseErgebnis, "1. b) i)")).isNotNull();
+ assertThat(befehlAn(parseErgebnis, "1. b) ii)")).isNotNull();
+ assertThat(befehlAn(parseErgebnis, "1. e) ii)")).isNotNull();
+
+ // Die Nebenform „erhält die Fassung“ ohne Doppelpunkt wird als Neufassung gelesen.
+ assertThat(befehlAn(parseErgebnis, "1. a)")).isInstanceOf(Neufassung.class);
+
+ // Dass die meisten Befehle ihr Ziel nicht mehr finden, liegt am Stand des Stammes und nicht am
+ // Erzeugnis: Die Vorlage ist bereits Gesetz geworden.
+ var anwendung = BefehlAnwender.anwenden(gesetz, parseErgebnis.befehle());
+ assertThat(anwendung.anzahlAngewandt()).isEqualTo(4);
+ }
}
diff --git a/src/test/java/eu/mulk/aendggner/aenderung/parse/TextBereinigerTest.java b/src/test/java/eu/mulk/aendggner/aenderung/parse/TextBereinigerTest.java
index 4ef441d..34678f5 100644
--- a/src/test/java/eu/mulk/aendggner/aenderung/parse/TextBereinigerTest.java
+++ b/src/test/java/eu/mulk/aendggner/aenderung/parse/TextBereinigerTest.java
@@ -136,6 +136,33 @@ class TextBereinigerTest {
.isEqualTo("unterhalb eines Schwellenwertes von 50\nWohnungen.");
}
+ /**
+ * Amtsbl. des Saarlandes: Im engen Zweispaltensatz verliert der Auszug auch an einer echten
+ * Wortfuge den Zwischenraum. Der Wortbestand entscheidet — und zwar derjenige <em>ohne</em> die
+ * zeilenletzten Wörter, denn ein durch den Umbruch abgerissenes Bruchstück steht immer am
+ * Zeilenende.
+ */
+ @Test
+ void setztAnDerWortfugeEinenZwischenraum() {
+ var roh =
+ "Abweichend werden zusätzlich Fehlbeträge in Höhe zugelassen. \n"
+ + "Bei jeder Gemeinde werden in Planung und Ausführung zusätzlich\n"
+ + "jahresbezogene Fehlbeträge in Höhe der Ergebnisse zugelassen. ";
+
+ assertThat(TextBereiniger.bereinige(roh)).contains("zusätzlich jahresbezogene Fehlbeträge");
+ }
+
+ /** Ein Kopf von weniger als vier Zeichen ist von einer Vorsilbe nicht zu unterscheiden. */
+ @Test
+ void laesstKurzeKoepfeAmUmbruchZusammengezogen() {
+ var roh =
+ "Als Gutachterin darf nicht bestellt werden, wer aus \n"
+ + "dem Amt des ehrenamtlichen Richters ist davon aus\n"
+ + "geschlossen und wird deshalb nicht berufen werden. ";
+
+ assertThat(TextBereiniger.bereinige(roh)).contains("ausgeschlossen und wird");
+ }
+
@Test
void ziehtOhneTrailingSpaceKonventionKeineMarkerlosenTrennungenZusammen() {
// Handgeschriebene Klartextdateien: kein Trailing-Space-Signal → kein Join.
diff --git a/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java b/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java
index 3ed028b..8eed3f7 100644
--- a/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java
+++ b/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java
@@ -90,6 +90,31 @@ class BefehlAnwenderTest {
assertThat(ergebnis.protokoll().get(0).begruendung()).contains("mehrdeutig");
}
+ /**
+ * Die Aufzählungsmarke am Anfang einer Einheit ist deren Kennzeichnung und nicht ihr Wortlaut:
+ * Wer „in Nummer 3 den Punkt durch ein Komma ersetzt“, meint den Schlusspunkt und nicht den Punkt
+ * der Marke. Ohne diese Schranke stünde der Punkt zweimal da und der Befehl bliebe als mehrdeutig
+ * liegen (Amtsbl. des Saarlandes 2020 S. 1339, Artikel 2 Nummer 1).
+ */
+ @Test
+ void zaehltDieAufzaehlungsmarkeNichtZumWortlaut() {
+ var befehl =
+ new Ersetzung(
+ stelle(new Stelle.Paragraph("1"), new Stelle.AbsatzNr("2"), new Stelle.NummerNr("3")),
+ ".",
+ ",",
+ false,
+ false,
+ PROV);
+
+ var ergebnis = BefehlAnwender.anwenden(gesetz(), List.of(befehl));
+
+ assertThat(ergebnis.protokoll().get(0).status()).isEqualTo(Status.ANGEWANDT);
+ assertThat(absatzText(ergebnis.neu(), "§ 1", 1))
+ .contains("3. die Ausgabe von Synopsen,")
+ .doesNotContain("3, die Ausgabe");
+ }
+
@Test
void wendetJeweilsErsetzungAufAlleVorkommenAn() {
var befehl =