From c86a73a6e1da7dd28076054936458b850eda2947 Mon Sep 17 00:00:00 2001 From: Matthias Andreas Benkard Date: Sun, 26 Jul 2026 08:02:10 +0200 Subject: Berlin column spike: stream order already reads correctly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The plan assumed the two-column Berlin gazette would need x-coordinate column detection. It does not: the content stream already emits the left column fully before the right one, so commands come out in reading order. The test pins that finding. What does go wrong there is different, and two parts of it are fixed: * The article's amendment formula carries its own target ("§ 2 Satz 1 des Gesetzes zur Errichtung … wird wie folgt geändert"), but the following items never inherited it. The pattern's leading \b could never match before "§" — a non-word character needs a word character in front of it — so the paragraph branch was dead code and only the Bavarian "Art." branch ever fired. * The running page header is not a line of its own; it sits inside the body text of the following column, so it has to be cut out rather than dropped as a column title. Two blockers remain and are recorded rather than papered over: Article 1 amends an *Anlage* with its own numbering, which the model does not carry (the same gap that shelved Baden-Württemberg), and full-width frames (title block, imprint) sit elsewhere in the stream than on the page, which would need a real XY-cut. Stems are unobtainable either way — gesetze.berlin.de is an authenticated juris app like the Schleswig- Holstein portal. 229 tests green; all pinned figures unchanged. Co-Authored-By: Claude Opus 5 Change-Id: I4619dbe96a023d61104d7107a9a34533123d1bc5 --- .../aenderung/parse/AenderungsgesetzParser.java | 11 ++++-- .../aendggner/aenderung/parse/TextBereiniger.java | 10 +++++ .../sampledata/Landesrecht-Beispiele.adoc | 23 ++++++++++- src/test/java/eu/mulk/aendggner/EndToEndTest.java | 44 ++++++++++++++++++++++ .../parse/AenderungsgesetzParserTest.java | 31 +++++++++++++++ .../aenderung/parse/TextBereinigerTest.java | 13 +++++++ 6 files changed, 126 insertions(+), 6 deletions(-) (limited to 'src') 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( - "(?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 @@ -16,6 +16,37 @@ class AenderungsgesetzParserTest { new Gesetz( "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 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 @@ -223,6 +223,19 @@ class TextBereinigerTest { .isEqualTo("Artikel 2\nDie Kreisordnung"); } + @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“. -- cgit v1.2.1