aboutsummaryrefslogtreecommitdiff
path: root/src/test
diff options
context:
space:
mode:
authorMatthias Andreas Benkard <code@mail.matthias.benkard.de>2026-08-02 20:01:37 +0200
committerMatthias Andreas Benkard <code@mail.matthias.benkard.de>2026-08-02 20:01:37 +0200
commit5b2e6ef635026a62e23fe5ea6df2c85d839ff78c (patch)
tree6c7af175312d929ea698a6567ba8f2fd3367abc4 /src/test
parenta201e83e59562e0e662196a616ff746cdaddd74e (diff)
Add Hesse as an extraction and recognition case
The Hesse GVBl issue 5/2026 amends the ordinance on traffic-law competences. All 21 commands of its article 1 are now recognised. The blocked letterspacing that was expected to be the hard part turns out to affect only the signature block, not a single command. What did need work was three other things: the running GVBl footer and the masthead remainder are now stripped as page furniture, and so is the heading's asterisk footnote ("* Ändert FFN 61-60"), which sits at the foot of a page between two enumeration items and stuck to the command above it. Two command forms are new, both general rather than Hessian: "am Ende" may be absent from a punctuation replacement — the definite article then requires the unit to carry exactly one such mark, which the applier checks — and the object after "durch" may be elided in the form without a location, as it already could in the form with one. Where a punctuation replacement follows a renumbering in the same sentence it binds to the renumbered unit: unlike a word operation it has no distinguishing target text, so resolving it norm-wide would be useless. A full acceptance test stays out of reach: hessenrecht.hessen.de is the same login-only juris application as the Schleswig-Holstein and Berlin portals. Checking landesrecht-bw.de too, that now covers all four portals that could have supplied an annex-bearing statute, so the LandesRechtTextParser's missing annex headings will need a stem from recht.nrw.de instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Change-Id: Ief04164ee25653345a011f3ffbbfaa6df1f5c64b
Diffstat (limited to 'src/test')
-rw-r--r--src/test/java/eu/mulk/aendggner/EndToEndTest.java83
-rw-r--r--src/test/java/eu/mulk/aendggner/aenderung/parse/BefehlErkennerTest.java56
-rw-r--r--src/test/java/eu/mulk/aendggner/aenderung/parse/TextBereinigerTest.java16
3 files changed, 155 insertions, 0 deletions
diff --git a/src/test/java/eu/mulk/aendggner/EndToEndTest.java b/src/test/java/eu/mulk/aendggner/EndToEndTest.java
index 035c3f1..4e4d547 100644
--- a/src/test/java/eu/mulk/aendggner/EndToEndTest.java
+++ b/src/test/java/eu/mulk/aendggner/EndToEndTest.java
@@ -844,4 +844,87 @@ class EndToEndTest {
assertThat(neu.norm("§ 3").orElseThrow().gesamtText())
.isEqualTo(gesetz.norm("§ 3").orElseThrow().gesamtText());
}
+
+ /**
+ * Hessen: Die Elfte Verordnung zur Änderung der Verordnung zur Bestimmung verkehrsrechtlicher
+ * Zuständigkeiten (GVBl. 2026 Nr. 5). Der Test hält Extraktion und Befehlserkennung fest — alle
+ * 21 Befehle des Artikels 1 werden erkannt.
+ *
+ * <p>Kein voller Akzeptanztest: {@code hessenrecht.hessen.de} ist dieselbe anmeldepflichtige
+ * juris-Anwendung wie die Portale Schleswig-Holsteins und Berlins, die Stammfassung ist daraus
+ * nicht zu beschaffen.
+ *
+ * <p>Der hessische Sperrsatz („Der Mi n is t er“) bleibt bewusst unbehandelt: Er trifft nur den
+ * Unterschriftenblock am Dokumentende, keinen einzigen Befehl.
+ */
+ @Test
+ void verkehrsZustVAendVOHessen() throws Exception {
+ var pdf =
+ SAMPLEDATA.resolve("Hessen/GVBl-2026-05_11-AendVO-verkehrsrechtl-Zustaendigkeiten.pdf");
+ assumeTrue(Files.exists(pdf), "GVBl-Hessen-Beispiel-PDF fehlt");
+
+ var text = TextBereiniger.bereinige(new PatchTextExtraktor().extrahiere(pdf));
+
+ // Die laufende Fußzeile stand zwischen den Punkten 11. und 12. und hing sonst an Punkt 12.
+ assertThat(text).doesNotContain("Gesetz- und Verordnungsblatt für das Land Hessen");
+ // Die amtliche Sternchen-Fußnote der Überschrift steht am Seitenfuß zwischen 4. b) und 5. und
+ // hing sonst an Punkt 4. b).
+ assertThat(text).doesNotContain("Ändert FFN");
+ // Der Sperrsatz beschränkt sich auf den Unterschriftenblock — hier bewusst nicht geheilt.
+ assertThat(text).contains("Min isterpräsident");
+
+ var verordnung =
+ new Gesetz(
+ "VerkZustV",
+ "Verordnung zur Bestimmung verkehrsrechtlicher Zuständigkeiten",
+ null,
+ List.of());
+ var ergebnis = new AenderungsgesetzParser().parse(text, verordnung, null);
+ // Artikel 2 regelt nur das Inkrafttreten.
+ assertThat(ergebnis.artikel()).containsExactly("1");
+ assertThat(ergebnis.befehle()).hasSize(21);
+ assertThat(ergebnis.befehle()).noneMatch(b -> b instanceof UnbekannterBefehl);
+
+ // Hessen kürzt im §-Kontext durchweg ab („In Abs. 1 …“, „Nr. 1 wird aufgehoben“); der Rahmen
+ // liefert den Paragraphen.
+ assertThat(befehlZu(ergebnis, "1. a)").stelle().anzeigeText()).isEqualTo("§ 6 Absatz 1");
+ assertThat(befehlZu(ergebnis, "5. a)").stelle().anzeigeText()).isEqualTo("§ 14 Nummer 1");
+ // „Die Absatzbezeichnung „(1)“ wird gestrichen.“ — die Bezeichnung selbst ist das Ziel.
+ assertThat(befehlZu(ergebnis, "4. a)").stelle().anzeigeText())
+ .isEqualTo("§ 13 Absatzbezeichnung (1)");
+
+ // 5. b) und 5. f) sind Verbünde aus Umnummerierung und einer Folgeoperation ohne eigenen
+ // Lokativ. Die Wortoperation löst norm-weit auf (ihr Zieltext ist unterscheidend), die
+ // Satzzeichen-Operation dagegen wird auf die umnummerierte Nummer festgelegt — „das Komma“
+ // träfe sonst beliebig viele Vorkommen.
+ var umnummerierungUndErsetzung = (Aenderungsbefehl.Sammelbefehl) befehlZu(ergebnis, "5. b)");
+ assertThat(umnummerierungUndErsetzung.teilbefehle())
+ .extracting(b -> b.stelle().anzeigeText())
+ .containsExactly("§ 14 Nummer 2", "§ 14");
+ var umnummerierungUndKomma = (Aenderungsbefehl.Sammelbefehl) befehlZu(ergebnis, "5. f)");
+ assertThat(umnummerierungUndKomma.teilbefehle())
+ .extracting(b -> b.stelle().anzeigeText())
+ .containsExactly("§ 14 Nummer 7", "§ 14 Nummer 5");
+ var komma = (Aenderungsbefehl.Ersetzung) umnummerierungUndKomma.teilbefehle().get(1);
+ assertThat(komma.alt()).isEqualTo(",");
+ assertThat(komma.neu()).isEqualTo("und");
+ // Ohne den Zusatz „am Ende“ ist das Satzzeichen nicht ans Einheitsende geheftet; der Anwender
+ // verlangt dann, dass die Einheit genau eines trägt.
+ assertThat(komma.amEnde()).isFalse();
+
+ // Bereichs-Umnummerierung, absteigend angewandt: „Die bisherigen Nr. 3 und 4 werden die
+ // Nr. 2 und 3.“
+ var bereich = (Aenderungsbefehl.Sammelbefehl) befehlZu(ergebnis, "5. c)");
+ assertThat(bereich.teilbefehle())
+ .extracting(b -> b.stelle().anzeigeText())
+ .containsExactly("§ 14 Nummer 4", "§ 14 Nummer 3");
+ }
+
+ private static Aenderungsbefehl befehlZu(
+ AenderungsgesetzParser.ParseErgebnis ergebnis, String gliederungsPfad) {
+ return ergebnis.befehle().stream()
+ .filter(b -> b.provenienz().gliederungsPfad().equals(gliederungsPfad))
+ .findFirst()
+ .orElseThrow();
+ }
}
diff --git a/src/test/java/eu/mulk/aendggner/aenderung/parse/BefehlErkennerTest.java b/src/test/java/eu/mulk/aendggner/aenderung/parse/BefehlErkennerTest.java
index 8fcf3e5..46c5ad6 100644
--- a/src/test/java/eu/mulk/aendggner/aenderung/parse/BefehlErkennerTest.java
+++ b/src/test/java/eu/mulk/aendggner/aenderung/parse/BefehlErkennerTest.java
@@ -78,6 +78,62 @@ class BefehlErkennerTest {
assertThat(ersetzung.amEnde()).isTrue();
}
+ /**
+ * Hessisches GVBl: Der Zusatz „am Ende“ darf fehlen. Der bestimmte Artikel setzt dann voraus,
+ * dass die Einheit genau ein solches Satzzeichen trägt — das prüft der Anwender, statt dass die
+ * Erkennung es ans Einheitsende heftet.
+ */
+ @Test
+ void erkenntSatzzeichenErsetzungOhneEndeAngabe() {
+ var befehl =
+ erkenne(
+ "In Nummer 5 wird das Komma durch das Wort „und“ ersetzt.",
+ new Stelle(List.of(new Stelle.Paragraph("14"))));
+
+ assertThat(befehl).containsInstanceOf(Ersetzung.class);
+ var ersetzung = (Ersetzung) befehl.orElseThrow();
+ assertThat(ersetzung.stelle().anzeigeText()).isEqualTo("§ 14 Nummer 5");
+ assertThat(ersetzung.alt()).isEqualTo(",");
+ assertThat(ersetzung.neu()).isEqualTo("und");
+ assertThat(ersetzung.amEnde()).isFalse();
+ }
+
+ /** Ohne Fundstelle und mit verkürztem Objekt hinter „durch“ (hessisches GVBl). */
+ @Test
+ void erkenntErsetzungOhneStelleMitVerkuerztemObjekt() {
+ var befehl =
+ erkenne(
+ "Die Angabe „(§ 19 Abs. 3 Satz 1 Nr. 3 oder 4)“ wird durch „(§ 19 Abs. 3 Satz 1"
+ + " Nr. 3)“ ersetzt.",
+ new Stelle(List.of(new Stelle.Paragraph("14"))));
+
+ assertThat(befehl).containsInstanceOf(Ersetzung.class);
+ var ersetzung = (Ersetzung) befehl.orElseThrow();
+ assertThat(ersetzung.stelle().anzeigeText()).isEqualTo("§ 14");
+ assertThat(ersetzung.alt()).isEqualTo("(§ 19 Abs. 3 Satz 1 Nr. 3 oder 4)");
+ assertThat(ersetzung.neu()).isEqualTo("(§ 19 Abs. 3 Satz 1 Nr. 3)");
+ }
+
+ /**
+ * Verbund aus Umnummerierung und Satzzeichen-Operation: Anders als eine Wortoperation trägt sie
+ * keinen unterscheidenden Zieltext und meint deshalb die soeben umnummerierte Einheit.
+ */
+ @Test
+ void bindetSatzzeichenKlauselAnDieUmnummerierteEinheit() {
+ var befehl =
+ erkenne(
+ "Die bisherige Nr. 7 wird Nr. 5 und das Komma wird durch das Wort „und“ ersetzt.",
+ new Stelle(List.of(new Stelle.Paragraph("14"))));
+
+ assertThat(befehl).containsInstanceOf(Sammelbefehl.class);
+ var teile = ((Sammelbefehl) befehl.orElseThrow()).teilbefehle();
+ assertThat(teile).hasSize(2);
+ assertThat(teile.get(0)).isInstanceOf(Umnummerierung.class);
+ assertThat(teile.get(0).stelle().anzeigeText()).isEqualTo("§ 14 Nummer 7");
+ assertThat(teile.get(1).stelle().anzeigeText()).isEqualTo("§ 14 Nummer 5");
+ assertThat(((Ersetzung) teile.get(1)).neu()).isEqualTo("und");
+ }
+
@Test
void erkenntNeufassung() {
var befehl =
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 284c506..1591a74 100644
--- a/src/test/java/eu/mulk/aendggner/aenderung/parse/TextBereinigerTest.java
+++ b/src/test/java/eu/mulk/aendggner/aenderung/parse/TextBereinigerTest.java
@@ -289,6 +289,22 @@ class TextBereinigerTest {
}
@Test
+ void entferntSeitenmoebelDesGvblHessen() {
+ // Der Trenner der Fußzeile ist ein weiches Trennzeichen (U+00AD), kein Gedankenstrich; die
+ // Sternchen-Fußnote der Überschrift steht am Seitenfuß mitten zwischen zwei Punkten.
+ var roh =
+ """
+ für das Land Hessen
+ 2026 Wiesbaden, den 3. Februar 2026 Nr. 5
+ Erster Satz.
+ * Ändert FFN 61-60
+ Gesetz- und Verordnungsblatt für das Land Hessen ­ Nr. 5 vom 3. Februar 2026 2
+ Zweiter Satz.""";
+
+ assertThat(TextBereiniger.bereinige(roh)).isEqualTo("Erster Satz.\nZweiter Satz.");
+ }
+
+ @Test
void repariertInvertierteZitatzeichenAnAbsatzmarkern() {
// BMJV-Vorlagen zeichnen das hängende „ nach dem Absatzmarker bzw. der Paragraphenangabe.
assertThat(TextBereiniger.bereinige("(1) „ Ungeachtet des § 8 gilt.“"))