diff options
7 files changed, 180 insertions, 6 deletions
diff --git a/FASSUNGEN.txt b/FASSUNGEN.txt index e1528ef..55d9be6 100644 --- a/FASSUNGEN.txt +++ b/FASSUNGEN.txt @@ -21,6 +21,60 @@ Vorbemerkung zur Führung dieses Verzeichnisses ════════════════════════════════════════════════════════════════════════════════ + Fassung vom 26. Juli 2026, + zuletzt geändert durch die am 26. Juli 2026 vorgenommenen Änderungen +════════════════════════════════════════════════════════════════════════════════ + +Der zweispaltige Belegfall Berlin (Gesetz zur Änderung des Allgemeinen +Sicherheits- und Ordnungsgesetzes und des LAF-Errichtungsgesetzes vom 3. Juni +2026, GVBl. S. 234) wird untersucht. Der Befund widerlegt die bisherige Annahme: +Die Reihenfolge des Inhaltsstroms ist bereits die Lesereihenfolge; eine +koordinatenbasierte Spaltenerkennung ist nicht erforderlich. Die tatsächlichen +Hindernisse liegen anderswo — davon werden zwei behoben. + + +Artikel 1 +Eingebettetes Rahmenziel in Paragraphenform + +Der Änderungsgesetz-Parser (AenderungsgesetzParser) übernahm ein in der +Änderungsformel selbst genanntes Ziel („§ 2 Satz 1 des Gesetzes zur Errichtung +eines Landesamtes … wird wie folgt geändert:“) nicht als Kontext der +nachfolgenden Gliederungspunkte. Ursache war die vordere Wortgrenze des Musters +EINGEBETTETES_ZIEL: „§“ ist selbst kein Wortzeichen, eine Wortgrenze davor +verlangte also ein vorangehendes Wortzeichen. Der §-Zweig des Musters war damit +unerreichbar und allein der bayerische „Art.“-Zweig wirksam. An die Stelle der +Wortgrenze tritt nunmehr die Bedingung, dass kein Buchstabe und keine Ziffer +vorangeht. + + +Artikel 2 +Laufender Seitenkopf des Gesetz- und Verordnungsblatts für Berlin + +Der laufende Seitenkopf des GVBl. für Berlin („235Gesetz- und Verordnungsblatt +für Berlin 82. Jahrgang Nr. 17 11. Juni 2026“) steht im Inhaltsstrom nicht auf +einer eigenen Zeile, sondern klebt samt Seitenzahl mitten im Fließtext der +folgenden Spalte. Er ist deshalb nicht als Kolumnentitel zu entfernen, sondern +aus dem laufenden Text herauszuschneiden (TextBereiniger, Muster +GVBL_BERLIN_KOPF). + + +Artikel 3 +Prüffall Berlin + +Ein Test bis einschließlich der Befehlserkennung tritt hinzu +(EndToEndTest.asogLafAendGBerlin). Er hält das Ergebnis des Spaltenspikes fest — +die Befehle beider Artikel stehen unverschränkt in Lesereihenfolge — und prüft, +dass die beiden Punkte des Artikels 2 ihr Ziel aus der Änderungsformel erben. + +Ein voller Akzeptanztest ist nicht möglich: gesetze.berlin.de ist wie das +schleswig-holsteinische Portal eine anmeldepflichtige juris-Anwendung. Artikel 1 +ändert überdies eine Anlage mit eigener Nummern-Gliederung; Anlagen sind kein +Änderungsziel des Modells. Dieselbe Lücke hatte die Zurückstellung von +Baden-Württemberg veranlasst; sie ist damit als größte verbleibende Modelllücke +des Landesrechts bezeichnet (sampledata/Landesrecht-Beispiele.adoc). + + +════════════════════════════════════════════════════════════════════════════════ Fassung vom 25. Juli 2026, zuletzt geändert durch die am 25. Juli 2026 vorgenommenen Änderungen ════════════════════════════════════════════════════════════════════════════════ diff --git a/src/main/java/eu/mulk/aendggner/aenderung/parse/AenderungsgesetzParser.java b/src/main/java/eu/mulk/aendggner/aenderung/parse/AenderungsgesetzParser.java index 976cd5e..407a4cb 100644 --- a/src/main/java/eu/mulk/aendggner/aenderung/parse/AenderungsgesetzParser.java +++ b/src/main/java/eu/mulk/aendggner/aenderung/parse/AenderungsgesetzParser.java @@ -102,12 +102,15 @@ public final class AenderungsgesetzParser { } // Eingebettetes Rahmenziel in der Änderungsformel selbst: „Art. 7 Abs. 2 des Bayerischen - // Umweltinformationsgesetzes … wird wie folgt geändert:“. Der Lookbehind auf „durch “ schließt - // die Zitierkette der Änderungshistorie aus („das zuletzt durch § 5 des Gesetzes vom … geändert - // worden ist“) — dort ist die Norm nie das Subjekt der Formel. + // Umweltinformationsgesetzes … wird wie folgt geändert:“, „§ 2 Satz 1 des Gesetzes zur + // Errichtung … wird wie folgt geändert:“. Der Lookbehind auf „durch “ schließt die Zitierkette + // der Änderungshistorie aus („das zuletzt durch § 5 des Gesetzes vom … geändert worden ist“) — + // dort ist die Norm nie das Subjekt der Formel. Die vordere Grenze darf nicht \b sein: „§“ ist + // selbst kein Wortzeichen, ein \b davor verlangte also ein vorangehendes Wortzeichen und machte + // den §-Zweig unerreichbar. private static final Pattern EINGEBETTETES_ZIEL = Pattern.compile( - "(?<!durch )\\b((?:§|Art\\.)\\s*\\d+[a-z]?" + "(?<!durch )(?<![\\p{L}\\d])((?:§|Art\\.)\\s*\\d+[a-z]?" + "(?:\\s+(?:Absatz|Abs\\.|Satz|Nummer|Nr\\.|Buchstabe|Buchst\\.)\\s+\\d+[a-z]?)*)" + "\\s+(?:des|der)\\s+\\p{Lu}"); diff --git a/src/main/java/eu/mulk/aendggner/aenderung/parse/TextBereiniger.java b/src/main/java/eu/mulk/aendggner/aenderung/parse/TextBereiniger.java index f1b75e1..d0e37f8 100644 --- a/src/main/java/eu/mulk/aendggner/aenderung/parse/TextBereiniger.java +++ b/src/main/java/eu/mulk/aendggner/aenderung/parse/TextBereiniger.java @@ -190,6 +190,15 @@ public final class TextBereiniger { private static final Pattern INVERTIERTES_LISTEN_ZITAT = Pattern.compile("(?m)^(\\s*)(\\d+[a-z]?\\.|[a-z]{1,3}\\))[ \\t]+„[ \\t]+"); + // GVBl. für Berlin (Wolters-Kluwer-Satz): Der laufende Seitenkopf („235Gesetz- und Verordnungs- + // blatt für Berlin 82. Jahrgang Nr. 17 11. Juni 2026“) steht im Inhaltsstrom nicht als eigene + // Zeile, sondern klebt samt Seitenzahl mitten im Fließtext der folgenden Spalte. Er ist deshalb + // nicht als Kolumnentitel zu entfernen, sondern als Textstück herauszuschneiden. + private static final Pattern GVBL_BERLIN_KOPF = + Pattern.compile( + "\\s*\\d{0,4}\\s*Gesetz- und Verordnungsblatt für Berlin\\s+\\d+\\. Jahrgang" + + "\\s+Nr\\.\\s*\\d+\\s+\\d{1,2}\\. \\p{L}+ \\d{4}\\s*"); + // Sachnummern mit Leerzeichen: Niedersachsen zitiert eingeschobene Paragraphen als „§ 2 a“, der // Rest der Rechtssprache als „§ 2a“. Auf die kanonische Form ziehen, damit Stellen- und // Ebenenparser greifen. Ein folgendes „)“ oder „.“ schließt Aufzählungsmarker des @@ -209,6 +218,7 @@ public final class TextBereiniger { text = INVERTIERTES_LISTEN_ZITAT.matcher(text).replaceAll("$1„$2 "); text = trenneVerklebteZitatgrenzen(text); text = VORABFASSUNG.matcher(text).replaceAll("\n"); + text = GVBL_BERLIN_KOPF.matcher(text).replaceAll("\n"); var zeilen = entferneKolumnentitel(zerlegeInZeilen(text)); var verbunden = verbindeUmbrueche(zeilen); // Falsch-positive markerlose Zusammenzüge („durch“ + „die“ → „durchdie“) reparieren — die diff --git a/src/main/resources/sampledata/Landesrecht-Beispiele.adoc b/src/main/resources/sampledata/Landesrecht-Beispiele.adoc index 52ab028..482ceac 100644 --- a/src/main/resources/sampledata/Landesrecht-Beispiele.adoc +++ b/src/main/resources/sampledata/Landesrecht-Beispiele.adoc @@ -25,8 +25,12 @@ hessenrecht, gesetze-rechtsprechung.sh.juris.de; Eigensystem: Berlin) oder als h |Berlin |`Berlin/GVBl-2026-17_ASOG-LAF-AendG.pdf` — GVBl. für Berlin Nr. 17 vom 11.06.2026, S. 234 (Gesetz zur Änderung des ASOG und des LAF-Errichtungsgesetzes vom 03.06.2026) -|*Zweispaltiges Layout* (Herausgabe über Wolters Kluwer) — höchster Extraktionswert, analog -zum zweispaltigen Alt-BGBl. Tiefe Verschachtelung a)/aa)/bb)/cc), Anlage-Ziele. +|*Zweispaltiges Layout* (Herausgabe über Wolters Kluwer). Der Spaltenspike ist erledigt: Die +Reihenfolge des Inhaltsstroms *ist* bereits die Lesereihenfolge, eine koordinatenbasierte +Spaltenerkennung ist nicht nötig. Verbleibende Layout-Eigenheit sind ganzseitenbreite Rahmen +(Titelblock, laufender Seitenkopf), die an falscher Stelle im Strom stehen — der Seitenkopf wird +herausgeschnitten, der Titelblock landet weiterhin mitten im Text. Tiefe Verschachtelung +a)/aa)/bb)/cc); Artikel 1 ändert eine *Anlage* mit eigener Nummern-Gliederung. |Schleswig-Holstein |`SchleswigHolstein/GVOBl-2026-27_Kommunalrecht-AendG.pdf` — GVOBl. Schl.-H. 2026/27 vom @@ -100,6 +104,9 @@ folgt geändert"). `EndToEndTest.tmzGesetzAcceptance` (Artikel 3 des 22. Rundfunkänderungsgesetzes; alle vier Befehle angewandt, kein Rest). Das Ergebnis ist gegen die amtliche Fassung vom 01.04.2026 abgeglichen. Die übrigen Artikel (WDR-Gesetz, Landesmediengesetz, 17. RÄStV-AusfG) bleiben offen. +* *Berlin* ist bis zur Befehlserkennung umgesetzt (`EndToEndTest.asogLafAendGBerlin`): Artikel 2 + (LAF-Errichtungsgesetz) wird vollständig erkannt, beide Punkte erben ihr Ziel aus der + Änderungsformel. Artikel 1 bleibt offen — er ändert eine Anlage (siehe „Noch offen“). * Baden-Württemberg (GBl 2026 Nr. 26): großes Anlage-lastiges Änderungsgesetz (§§ 4–57a + Anlagen 1/3b/4b); Stammfassung/Akzeptanztest noch offen. @@ -114,6 +121,18 @@ folgt geändert"). vom 05.02.2025 eingefügt). Auch die archivierten Einzelnorm-Seiten (113 Stück) sind bereits SPA-Hüllen ohne Inhalt. Ein voller Akzeptanztest bleibt daher offen, bis die konsolidierte Fassung aus einer anderen Quelle vorliegt. +* *Anlagen als Änderungsziel*: Berlin Artikel 1 („Die Anlage zu § 2 Absatz 4 Satz 1 wird wie folgt + geändert“ mit Unterpunkten auf „Nummer 23 Absatz 4a“) und Baden-Württemberg (Anlagen 1/3b/4b) + scheitern an derselben Stelle: ÄndGgner modelliert Anlagen nicht als eigene Einheiten mit + interner Nummern-Gliederung. Das ist die größte verbleibende Modelllücke im Landesrecht. +* *Unbalancierte Anführungszeichen innerhalb eines Artikels*: Die Strukturgrenze des + Zitatextraktors greift nur an Artikel-Überschriften. In Berlin Artikel 1 fehlt bei Nummer 2 b) bb) + das schließende Anführungszeichen; das Zitat verschlingt daher die folgenden Punkte cc) und c). + Eine Grenze an Aufzählungsmarkern ist nicht möglich — zitierter Gesetzestext enthält selbst + Aufzählungen (dafür gibt es die Fortführungszeichen-Erkennung). +* *Berlin, ganzseitenbreite Rahmen*: Titelblock und Impressum stehen im Inhaltsstrom an anderer + Stelle als im Satzbild (der Titelblock mitten im Fließtext der ersten Seite). Das zu beheben + verlangt eine geometrische Lesereihenfolge (XY-Cut), nicht bloß eine Spaltenerkennung. * *Rheinland-Pfalz*: Die Verkündungsplattform `verkuendung.rlp.de` verkündet erst seit dem 01.07.2026 elektronisch; ältere GVBl-Hefte liegen nicht als freie Direkt-PDF vor. * Bremen, Hamburg, Mecklenburg-Vorpommern, Saarland, Brandenburg, Sachsen-Anhalt: noch nicht diff --git a/src/test/java/eu/mulk/aendggner/EndToEndTest.java b/src/test/java/eu/mulk/aendggner/EndToEndTest.java index e7f7a00..19a7c8c 100644 --- a/src/test/java/eu/mulk/aendggner/EndToEndTest.java +++ b/src/test/java/eu/mulk/aendggner/EndToEndTest.java @@ -554,4 +554,48 @@ class EndToEndTest { var synopse = SynopseBuilder.baue(gesetz, anwendung, parseErgebnis.warnungen(), false); assertThat(HtmlRenderer.rendere(synopse, "E2E-Test TMZ-Gesetz")).contains("§ 2"); } + + /** + * Berlin: Das GVBl. ist zweispaltig gesetzt (Wolters-Kluwer-Satz). Der Test hält das Ergebnis des + * Spaltenspikes fest — die Spaltenreihenfolge des Inhaltsstroms ist bereits die Lesereihenfolge, + * 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* mit eigener Nummern-Gliederung — ein Änderungsziel, das ÄndGgner + * nicht modelliert (dieselbe Grenze, an der Baden-Württemberg zurückgestellt wurde). + */ + @Test + void asogLafAendGBerlin() throws Exception { + var pdf = SAMPLEDATA.resolve("Berlin/GVBl-2026-17_ASOG-LAF-AendG.pdf"); + assumeTrue(Files.exists(pdf), "GVBl-Berlin-Beispiel-PDF fehlt"); + + var text = TextBereiniger.bereinige(new PatchTextExtraktor().extrahiere(pdf)); + + // Spaltenspike: Die Befehle beider Artikel stehen in Lesereihenfolge im Text — die linke Spalte + // läuft vollständig vor der rechten, ohne Verschränkung. + assertThat(text.indexOf("§ 67 Absatz 2 wird wie folgt gefasst")) + .isLessThan(text.indexOf("Die Anlage zu § 2 Absatz 4 Satz 1 wird wie folgt geändert")); + assertThat(text.indexOf("Die Anlage zu § 2 Absatz 4 Satz 1 wird wie folgt geändert")) + .isLessThan(text.indexOf("In der Nummer 2 wird der Punkt am Ende")); + // Der laufende Seitenkopf ist auch dort entfernt, wo er mitten im Fließtext klebt. + assertThat(text).doesNotContain("Gesetz- und Verordnungsblatt für Berlin"); + + // Artikel 2 trägt sein Ziel in der Änderungsformel („§ 2 Satz 1 des Gesetzes zur Errichtung + // …“); beide Punkte erben es als Kontext. + var laf = + new Gesetz( + "LAF-ErrichtungsG", + "Gesetz zur Errichtung eines Landesamtes für Flüchtlingsangelegenheiten und" + + " Unterbringung", + null, + List.of()); + var ergebnis = new AenderungsgesetzParser().parse(text, laf, null); + assertThat(ergebnis.artikel()).containsExactly("2"); + assertThat(ergebnis.befehle()).hasSize(2); + assertThat(ergebnis.befehle()).noneMatch(b -> b instanceof UnbekannterBefehl); + assertThat(ergebnis.befehle()) + .extracting(b -> b.stelle().anzeigeText()) + .containsExactly("§ 2 Satz 1 Nummer 2", "§ 2 Satz 1 Nummer 2"); + } } diff --git a/src/test/java/eu/mulk/aendggner/aenderung/parse/AenderungsgesetzParserTest.java b/src/test/java/eu/mulk/aendggner/aenderung/parse/AenderungsgesetzParserTest.java index a8ec3aa..2dc53bb 100644 --- a/src/test/java/eu/mulk/aendggner/aenderung/parse/AenderungsgesetzParserTest.java +++ b/src/test/java/eu/mulk/aendggner/aenderung/parse/AenderungsgesetzParserTest.java @@ -17,6 +17,37 @@ class AenderungsgesetzParserTest { "UWG", "Gesetz gegen den unlauteren Wettbewerb", "UWG-Langtitel", List.of(), List.of()); @Test + void nimmtEingebettetesParagraphenZielAlsKontext() { + // Berliner Zuschnitt: Die Änderungsformel trägt ihr Ziel selbst („§ 2 Satz 1 des Gesetzes zur + // Errichtung … wird wie folgt geändert“), die Punkte nennen nur noch die Feinstelle. Der + // §-Zweig des Musters war bislang unerreichbar (\b vor dem Nicht-Wortzeichen „§“). + var laf = + new Gesetz( + "LAF-ErrichtungsG", + "Gesetz zur Errichtung eines Landesamtes für Flüchtlingsangelegenheiten und" + + " Unterbringung", + null, + List.of(), + List.of()); + var text = + """ + Artikel 1 + Änderung des Gesetzes zur Errichtung eines Landesamtes für Flüchtlingsangelegenheiten und Unterbringung + § 2 Satz 1 des Gesetzes zur Errichtung eines Landesamtes für Flüchtlingsangelegenheiten und Unterbringung vom 14. März 2016 (GVBl. S. 93), das zuletzt durch Artikel 2 des Gesetzes vom 1. April 2026 (GVBl. S. 154) geändert worden ist, wird wie folgt geändert: + 1. In der Nummer 2 wird der Punkt am Ende durch ein Semikolon ersetzt. + 2. Nach Nummer 2 wird folgende Nummer 3 eingefügt: + „3. Beratung sowie Hilfen zur freiwilligen Rückkehr.“ + """; + + var ergebnis = new AenderungsgesetzParser().parse(text, laf, null); + + assertThat(ergebnis.artikel()).containsExactly("1"); + assertThat(ergebnis.befehle()) + .extracting(b -> b.stelle().anzeigeText()) + .containsExactly("§ 2 Satz 1 Nummer 2", "§ 2 Satz 1 Nummer 2"); + } + + @Test void teiltParagraphenGegliedertesAenderungsgesetz() { // Bayerischer Stil: das Änderungsgesetz gliedert sich in §§; § 1 und § 2 treffen dasselbe // Stammgesetz, § 3 ein anderes; Inkrafttretens-§ ohne Änderungsformel wird nicht gewählt. 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 4da5c79..5ed8b93 100644 --- a/src/test/java/eu/mulk/aendggner/aenderung/parse/TextBereinigerTest.java +++ b/src/test/java/eu/mulk/aendggner/aenderung/parse/TextBereinigerTest.java @@ -224,6 +224,19 @@ class TextBereinigerTest { } @Test + void schneidetDenBerlinerSeitenkopfAusDemFliesstext() { + // Im GVBl. für Berlin steht der laufende Seitenkopf nicht auf einer eigenen Zeile, sondern + // klebt samt Seitenzahl mitten im Text der folgenden Spalte. + var roh = + "zur Sicherung des Betriebs\n" + + "235Gesetz- und Verordnungsblatt für Berlin 82. Jahrgang Nr. 17" + + " 11. Juni 2026 von Unterkünften."; + + assertThat(TextBereiniger.bereinige(roh)) + .isEqualTo("zur Sicherung des Betriebs\nvon Unterkünften."); + } + + @Test void ziehtSachnummernMitLeerzeichenZusammen() { // Niedersachsen zitiert eingeschobene Paragraphen als „§ 2 a“; kanonisch ist „§ 2a“. assertThat(TextBereiniger.bereinige("Nach § 2 wird der folgende § 2 a eingefügt:")) |
