From 5b2e6ef635026a62e23fe5ea6df2c85d839ff78c Mon Sep 17 00:00:00 2001 From: Matthias Andreas Benkard Date: Sun, 2 Aug 2026 20:01:37 +0200 Subject: Add Hesse as an extraction and recognition case MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Change-Id: Ief04164ee25653345a011f3ffbbfaa6df1f5c64b --- src/test/java/eu/mulk/aendggner/EndToEndTest.java | 83 ++++++++++++++++++++++ .../aenderung/parse/BefehlErkennerTest.java | 56 +++++++++++++++ .../aenderung/parse/TextBereinigerTest.java | 16 +++++ 3 files changed, 155 insertions(+) (limited to 'src/test') 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. + * + *

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. + * + *

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 @@ -288,6 +288,22 @@ class TextBereinigerTest { assertThat(TextBereiniger.bereinige(roh)).isEqualTo("Erster Satz.\nZweiter Satz."); } + @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. -- cgit v1.2.1