From 8191a15cd391d28cd5098e2535d611209c3c6285 Mon Sep 17 00:00:00 2001 From: Matthias Andreas Benkard Date: Sun, 23 Aug 2026 14:56:08 +0200 Subject: Der Browser bekommt, was dem Skript verwehrt bleibt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Die Landesrechtsportale, die auf der juris-Anwendung beruhen, galten als Sackgasse: Jede Skriptabfrage erhält nur ihre leere Anwendungshülle, gleich unter welcher Adresse. Sie geben ihren Wortlaut aber jedem Browser, und über eine Browsersteuerung ist er Werk für Werk auszulesen. Damit ist die hessische Verkehrsrechts-Zuständigkeitsverordnung in beiden maßgeblichen Fassungen beschafft — der vom 1. Januar 2026 und der vom 4. Februar 2026 — und Hessen ist kein bloßer Erkennungsfall mehr, sondern ein voller Belegfall: einundzwanzig Befehle, kein Rest, und alle zweiundfünfzig Normen gleichen zeichengenau der amtlichen Nachfassung. Der Weg dorthin hat fünf allgemeine Lücken aufgedeckt. Der Klartext-Parser verlangte für jeden Normkopf eine Überschrift, die eine Verordnung nicht führt, und kannte die ausgeschriebene Gliederung nicht („Erster Teil“); ein Aufzählungsglied, das auf „und“ endet, galt ihm als Unter-Überschrift. Der Anwender behandelte die Streichung einer Absatzbezeichnung wie die Aufhebung des Absatzes, setzte Platzhalter auch dort, wo kein Platz zu halten war, und ließ ein Wort, das an die Stelle eines Satzzeichens tritt, am Vorwort kleben. Zwei stille Mängel kamen unverhofft mit: „List.remove“ war zweimal ein geboxter Index übergeben, sodass der Aufruf an die suchende Überladung band, nichts fand und nichts meldete. Betroffen war neben der Aufhebung eines Absatzes auch dessen Neufassung durch mehrere — der alte Absatz blieb neben den neuen stehen. Geprüft durch „mvnw verify“: 327 Tests, keine bestehende gepinnte Zahl bewegt, REUSE-Konformität 157 von 157. Co-Authored-By: Claude Opus 5 Change-Id: I20ac8a7bc66357cd66daa167184b06b8c419f843 --- src/test/java/eu/mulk/aendggner/EndToEndTest.java | 70 ++++++++++++++++++---- .../aenderung/parse/BefehlErkennerTest.java | 8 ++- .../aendggner/anwendung/BefehlAnwenderTest.java | 15 +++-- 3 files changed, 74 insertions(+), 19 deletions(-) (limited to 'src/test/java') diff --git a/src/test/java/eu/mulk/aendggner/EndToEndTest.java b/src/test/java/eu/mulk/aendggner/EndToEndTest.java index dde2483..a76f478 100644 --- a/src/test/java/eu/mulk/aendggner/EndToEndTest.java +++ b/src/test/java/eu/mulk/aendggner/EndToEndTest.java @@ -888,12 +888,13 @@ class EndToEndTest { /** * 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. + * Zuständigkeiten (GVBl. 2026 Nr. 5). Voller Akzeptanzfall: Alle 21 Befehle des Artikels 1 werden + * erkannt und angewandt, und jede der 52 Normen gleicht danach zeichengenau der amtlichen + * Nachfassung. * - *

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

Die Stammfassung ist über eine Browsersteuerung aus dem Landesrechtsportal beschafft — es + * ist eine Einseitenanwendung, die Skriptabfragen nur ihre Hülle zurückgibt. Herkunft und + * Aufbereitung stehen in {@code Hessen/SOURCES}. * *

Der hessische Sperrsatz („Der Mi n is t er“) bleibt bewusst unbehandelt: Er trifft nur den * Unterschriftenblock am Dokumentende, keinen einzigen Befehl. @@ -914,12 +915,20 @@ class EndToEndTest { // 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 alt = SAMPLEDATA.resolve("Hessen/StVRZustV-alt.txt"); + assumeTrue(Files.exists(alt), "Hessische Stammfassung fehlt"); + var verordnung = new eu.mulk.aendggner.gesetz.land.LandesRechtLoader().load(alt); + assertThat(verordnung.jurabk()).isEqualTo("StVRZustV HE 2007"); + // Diese Verordnung führt keine Paragraphenüberschriften und gliedert sich in ausgeschriebene + // Ordinalzahlen („Erster Teil“); beides kannte der Klartext-Parser bis dahin nicht. + assertThat(verordnung.normen()).hasSize(52); + assertThat(verordnung.norm("§ 1").orElseThrow().titel()).isNull(); + assertThat(verordnung.gliederungen()).hasSize(28); + assertThat(verordnung.gliederungen()) + .extracting(g -> g.bezeichnung()) + .allMatch(b -> b.endsWith(" Teil")); + assertThat(verordnung.gliederungen().get(0).bezeichnung()).isEqualTo("Erster Teil"); + var ergebnis = new AenderungsgesetzParser().parse(text, verordnung, null); // Artikel 2 regelt nur das Inkrafttreten. assertThat(ergebnis.artikel()).containsExactly("1"); @@ -959,6 +968,45 @@ class EndToEndTest { assertThat(bereich.teilbefehle()) .extracting(b -> b.stelle().anzeigeText()) .containsExactly("§ 14 Nummer 4", "§ 14 Nummer 3"); + + // Auch die Bereichsaufhebung spannt absteigend auf („Nr. 9 bis 11 werden aufgehoben“): Ob eine + // aufgehobene Einheit einen nummerierten Platzhalter hinterlässt, entscheidet sich am Bestand, + // der ihr folgt — von hinten aufgehoben, sieht jede den endgültigen. + var aufhebung = (Aenderungsbefehl.Sammelbefehl) befehlZu(ergebnis, "5. h)"); + assertThat(aufhebung.teilbefehle()) + .extracting(b -> b.stelle().anzeigeText()) + .containsExactly("§ 14 Nummer 11", "§ 14 Nummer 10", "§ 14 Nummer 9"); + + // --- Anwendung --------------------------------------------------------------------------- + var anwendung = BefehlAnwender.anwenden(verordnung, ergebnis.befehle()); + assertThat(anwendung.anzahlAngewandt()).isEqualTo(21); + assertThat(anwendung.protokoll()) + .noneMatch(x -> x.status() == BefehlAnwender.Status.MANUELL_PRUEFEN); + + // § 13: Die Streichung der Absatzbezeichnung „(1)“ nimmt dem Wortlaut seine Nummer und lässt + // ihn im Übrigen stehen; die Aufhebung des Abs. 2 beseitigt ihn ganz, weil eine Norm ohne + // Absatzzählung keinen nummerierten Platzhalter tragen kann. + var dreizehn = anwendung.neu().norm("§ 13").orElseThrow(); + assertThat(dreizehn.absaetze()).hasSize(1); + assertThat(dreizehn.absaetze().get(0).nummer()).isNull(); + + // § 14: Das Wort, das an die Stelle des Kommas tritt, bekommt seinen Zwischenraum. + assertThat(anwendung.neu().norm("§ 14").orElseThrow().gesamtText()) + .contains("Fachkräfte) und") + .doesNotContain("(weggefallen)"); + + // Der Abgleich gegen die amtliche Nachfassung, Norm für Norm. + var soll = SAMPLEDATA.resolve("Hessen/StVRZustV-neu.txt"); + assumeTrue(Files.exists(soll), "Hessische Nachfassung fehlt"); + var amtlich = new eu.mulk.aendggner.gesetz.land.LandesRechtLoader().load(soll); + assertThat(amtlich.normen()).hasSize(52); + for (var normSoll : amtlich.normen()) { + var normIst = anwendung.neu().norm(normSoll.enbez()); + assertThat(normIst).as("Norm %s fehlt", normSoll.enbez()).isPresent(); + assertThat(normIst.orElseThrow().gesamtText().replaceAll("\\s+", " ").strip()) + .as("Norm %s", normSoll.enbez()) + .isEqualTo(normSoll.gesamtText().replaceAll("\\s+", " ").strip()); + } } /** 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 abe3550..fc42ead 100644 --- a/src/test/java/eu/mulk/aendggner/aenderung/parse/BefehlErkennerTest.java +++ b/src/test/java/eu/mulk/aendggner/aenderung/parse/BefehlErkennerTest.java @@ -625,9 +625,12 @@ class BefehlErkennerTest { erkenne("Die Nummern 1 bis 3 werden aufgehoben.", Stelle.LEER).orElseThrow()) .teilbefehle(); assertThat(teile).hasSize(3).allMatch(t -> t instanceof Aufhebung); + // Absteigend seit der hessischen Verkehrsrechts-Zuständigkeitsverordnung: Ob eine aufgehobene + // Einheit einen nummerierten Platzhalter hinterlässt, hängt davon ab, ob ihr noch eine Einheit + // folgt. Von hinten aufgehoben, sieht jede den endgültigen Bestand. assertThat(teile) .extracting(t -> t.stelle().anzeigeText()) - .containsExactly("Nummer 1", "Nummer 2", "Nummer 3"); + .containsExactly("Nummer 3", "Nummer 2", "Nummer 1"); } @Test @@ -636,9 +639,10 @@ class BefehlErkennerTest { ((Sammelbefehl) erkenne("Die Absätze 4 und 5 werden aufgehoben.", Stelle.LEER).orElseThrow()) .teilbefehle(); + // Absteigend, aus demselben Grund wie bei der Bereichsaufhebung. assertThat(teile) .extracting(t -> t.stelle().anzeigeText()) - .containsExactly("Absatz 4", "Absatz 5"); + .containsExactly("Absatz 5", "Absatz 4"); } @Test diff --git a/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java b/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java index 9cd0597..8b60017 100644 --- a/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java +++ b/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java @@ -717,10 +717,12 @@ class BefehlAnwenderTest { void wendetBereichsAufhebungAn() { // „Die Nummern 1 bis 3 werden aufgehoben.“ innerhalb von § 1 Absatz 2. var teile = + // Absteigend, wie der Erkenner eine Bereichsaufhebung aufspannt: Ob eine aufgehobene + // Einheit einen Platzhalter hinterlässt, entscheidet sich am Bestand, der ihr folgt. List.of( new Aufhebung( stelle( - new Stelle.Paragraph("1"), new Stelle.AbsatzNr("2"), new Stelle.NummerNr("1")), + new Stelle.Paragraph("1"), new Stelle.AbsatzNr("2"), new Stelle.NummerNr("3")), PROV), new Aufhebung( stelle( @@ -728,17 +730,18 @@ class BefehlAnwenderTest { PROV), new Aufhebung( stelle( - new Stelle.Paragraph("1"), new Stelle.AbsatzNr("2"), new Stelle.NummerNr("3")), + new Stelle.Paragraph("1"), new Stelle.AbsatzNr("2"), new Stelle.NummerNr("1")), PROV)); var ergebnis = BefehlAnwender.anwenden(gesetz(), List.of(new Sammelbefehl(teile))); assertThat(ergebnis.protokoll().get(0).status()).isEqualTo(Status.ANGEWANDT); var text = absatzText(ergebnis.neu(), "§ 1", 1); - assertThat(text) - .contains("1. (weggefallen)") - .contains("2. (weggefallen)") - .contains("3. (weggefallen)"); + // Ein Platzhalter hält einen Platz nur dort, wo ihm eine weitere Einheit derselben Art folgt. + // Hier ist die ganze Aufzählung aufgehoben — es bleibt nichts, dessen Bezeichnung zu schonen + // wäre, und der Einleitungssatz steht allein. So hält es auch die amtliche Nachfassung des + // § 14 der hessischen Verkehrsrechts-Zuständigkeitsverordnung. + assertThat(text).isEqualTo("Die Erprobung umfasst"); } // --- Anhang/Anlage als Norm-Ziel ----------------------------------------------------------- -- cgit v1.2.1