aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--FASSUNGEN.txt54
-rw-r--r--src/main/java/eu/mulk/aendggner/aenderung/parse/AenderungsgesetzParser.java11
-rw-r--r--src/main/java/eu/mulk/aendggner/aenderung/parse/TextBereiniger.java10
-rw-r--r--src/main/resources/sampledata/Landesrecht-Beispiele.adoc23
-rw-r--r--src/test/java/eu/mulk/aendggner/EndToEndTest.java44
-rw-r--r--src/test/java/eu/mulk/aendggner/aenderung/parse/AenderungsgesetzParserTest.java31
-rw-r--r--src/test/java/eu/mulk/aendggner/aenderung/parse/TextBereinigerTest.java13
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:"))