diff options
7 files changed, 128 insertions, 12 deletions
diff --git a/FASSUNGEN.txt b/FASSUNGEN.txt index 55d9be6..a9fa599 100644 --- a/FASSUNGEN.txt +++ b/FASSUNGEN.txt @@ -30,7 +30,8 @@ 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. +Hindernisse liegen anderswo — davon werden vier behoben. Von den sechs Befehlen +des Heftes werden danach fünf erkannt. Artikel 1 @@ -67,11 +68,44 @@ die Befehle beider Artikel stehen unverschränkt in Lesereihenfolge — und prü 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). +schleswig-holsteinische Portal eine anmeldepflichtige juris-Anwendung. + + +Artikel 4 +Unnummerierte, benannte Anlagen + +Ein Gesetz mit einer einzigen Anlage benennt sie nach der Vorschrift, zu der sie +gehört („Die Anlage zu § 2 Absatz 4 Satz 1“). Der Stellenparser (StellenParser) +verlangte hinter „Anlage“ stets eine Nummer und wies solche Angaben zurück; der +Rahmenbefehl des Artikels 1 blieb deshalb unerkannt und seine Unterpunkte ohne +Kontext. Die Anlage trägt nunmehr — wie der „Anhang“ — die Bezeichnung „Anlage“. +Der benennende Zusatz wird übersprungen: Er reicht bis zum Ende der +Stellenangabe, und würde er mitgelesen, so läse der Parser das darin genannte +„§ 2“ als Änderungsziel und änderte die falsche Norm. + +Damit ist zugleich die Annahme der Fassung vom 26. Juli 2026 richtigzustellen: +Anlagen sind als Änderungsziel unterstützt (im gii-XML sind sie eigene Normen, +Stelle.anlagenEnbez() löst sie auf). Offen bleibt allein, dass der +Landesrecht-Textparser keine Anlagen-Normköpfe kennt, eine handgepflegte +Stammfassung also keine Anlage tragen kann. + + +Artikel 5 +Steuerzeichen und zerrissene Befehlsvokabeln + +Der Textbereiniger (TextBereiniger) entfernt nunmehr C0-Steuerzeichen außer +Tabulator und Zeilenumbruch (Muster STEUERZEICHEN). Im GVBl. für Berlin trägt +der Einzug der Aufzählungsglieder ein U+0007; es steht unsichtbar vor dem Befehl +und ließ dessen Zeilenanfangs-Anker ins Leere greifen. Ebenso werden +Befehlsvokabeln zusammengeführt, die der amtliche Satz durch ein Leerzeichen +zerreißt („ein gefügt“ statt „eingefügt“) — beschränkt auf Fügungen, die als +Wortfolge im Deutschen nicht vorkommen. + +Von den vier Befehlen des Artikels 1 werden damit drei erkannt. Der vierte +bestimmt die Einfügestelle über einen Wortanker („Vor den Wörtern „Aus dem +Bereich Verkehr:“ wird folgender Absatz 5 eingefügt“); eine Struktureinfügung +trägt bislang nur eine Stellenangabe. Beides ist in +sampledata/Landesrecht-Beispiele.adoc vermerkt. ════════════════════════════════════════════════════════════════════════════════ diff --git a/src/main/java/eu/mulk/aendggner/aenderung/parse/StellenParser.java b/src/main/java/eu/mulk/aendggner/aenderung/parse/StellenParser.java index 473eb14..786c812 100644 --- a/src/main/java/eu/mulk/aendggner/aenderung/parse/StellenParser.java +++ b/src/main/java/eu/mulk/aendggner/aenderung/parse/StellenParser.java @@ -133,7 +133,7 @@ public final class StellenParser { i++; } case "Teil", "Teils", "Buch", "Buches", "Kapitel", "Kapitels", "Abschnitt", "Abschnitts", - "Unterabschnitt", "Unterabschnitts", "Anlage", "Anlagen" -> { + "Unterabschnitt", "Unterabschnitts" -> { var wert = naechstesWort(woerter, i); if (wert == null || !NUMMER_WERT.matcher(wert).matches()) { return Optional.empty(); @@ -141,6 +141,23 @@ public final class StellenParser { komponenten.add(new Stelle.Gliederungseinheit(gliederungsArt(wort), wert)); i++; } + case "Anlage", "Anlagen" -> { + var wert = naechstesWort(woerter, i); + if (wert != null && NUMMER_WERT.matcher(wert).matches()) { + komponenten.add(new Stelle.Gliederungseinheit("Anlage", wert)); + i++; + continue; + } + // „die Anlage zu § 2 Absatz 4 Satz 1“ — ein Gesetz mit einer einzigen Anlage benennt sie + // nach der Vorschrift, zu der sie gehört. Wie beim „Anhang“ trägt sie dann die enbez + // „Anlage“; der Zusatz identifiziert sie nur und ist nicht selbst Änderungsziel. Er + // reicht bis zum Ende der Stellenangabe und wird deshalb übersprungen — sonst läse der + // Parser das darin genannte „§ 2“ als Ziel und änderte die falsche Norm. + komponenten.add(new Stelle.Gliederungseinheit("Anlage", "")); + if ("zu".equals(wert)) { + i = woerter.length; + } + } case "Anhang" -> komponenten.add(new Stelle.Gliederungseinheit("Anhang", "")); case "Inhaltsübersicht" -> komponenten.add(new Stelle.Inhaltsuebersicht()); case "Überschrift" -> komponenten.add(new Stelle.Ueberschrift()); 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 d0e37f8..fc54554 100644 --- a/src/main/java/eu/mulk/aendggner/aenderung/parse/TextBereiniger.java +++ b/src/main/java/eu/mulk/aendggner/aenderung/parse/TextBereiniger.java @@ -206,12 +206,19 @@ public final class TextBereiniger { private static final Pattern SACHNUMMER_MIT_LEERZEICHEN = Pattern.compile("(§|Art\\.) (\\d+) ([a-z])(?![a-zäöüß).])"); + /** C0-Steuerzeichen außer Tabulator und Zeilenumbruch; im Fließtext stets Extraktionsmüll. */ + private static final Pattern STEUERZEICHEN = Pattern.compile("[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F]"); + private TextBereiniger() {} public static String bereinige(String rohText) { // Geschützte Leerzeichen (GVBl-Satz: „§ 1“, „Abs. 2“) sind für Javas \s und // String.strip unsichtbar — früh auf gewöhnliche Leerzeichen normalisieren. var text = rohText.replace(' ', ' ').replace(' ', ' '); + // Steuerzeichen aus fehlgeleiteten Glyphenzuordnungen (im GVBl. für Berlin trägt der Einzug + // der Aufzählungsglieder ein U+0007). Sie sind unsichtbar, stehen aber vor dem Befehlstext und + // ließen dessen Zeilenanfangs-Anker ins Leere greifen. + text = STEUERZEICHEN.matcher(text).replaceAll(""); text = normalisiereAnfuehrungszeichen(text); text = INVERTIERTES_ZITAT.matcher(text).replaceAll("$1„($2) "); text = INVERTIERTES_PARAGRAPH_ZITAT.matcher(text).replaceAll("$1„$2"); @@ -290,6 +297,12 @@ public final class TextBereiniger { .replace("undwird ", "und wird ") .replace("Kommaeingefügt", "Komma eingefügt") .replace("Kommaersetzt", "Komma ersetzt") + // Umgekehrter Satzfehler: ein Leerzeichen mitten in der Befehlsvokabel („ein ge-“ + + // „fügt“ → „ein gefügt“, GVBl. Berlin 2026/17). Nur Fügungen, die als Wortfolge im + // Deutschen nicht vorkommen — „er setzt“ etwa bliebe unangetastet. + .replace("ein gefügt", "eingefügt") + .replace("an gefügt", "angefügt") + .replace("auf gehoben", "aufgehoben") // Kontextrahmen, an den der folgende Unterpunkt geklebt wurde („geändertaa) In …“). .replaceAll("(wie folgt geändert:?)(?=[a-z]{1,3}\\)|\\d+[a-z]?\\.)", "$1\n"); } diff --git a/src/main/resources/sampledata/Landesrecht-Beispiele.adoc b/src/main/resources/sampledata/Landesrecht-Beispiele.adoc index 482ceac..00cb08b 100644 --- a/src/main/resources/sampledata/Landesrecht-Beispiele.adoc +++ b/src/main/resources/sampledata/Landesrecht-Beispiele.adoc @@ -106,7 +106,8 @@ folgt geändert"). 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“). + Änderungsformel. Von Artikel 1 werden drei der vier Befehle erkannt, einschließlich der + Änderungen an der unnummerierten Anlage; der vierte nutzt einen Wortanker (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. @@ -121,10 +122,15 @@ 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. +* *Anlagen im kanonischen Klartext*: Anlagen sind als Änderungsziel unterstützt — im gii-XML sind + sie eigene Normen („Anlage 8“, „Anhang“), und `Stelle.anlagenEnbez()` löst sie auf; im GEG werden + anlagenbezogene Befehle angewandt. Der `LandesRechtTextParser` kennt jedoch nur „§“- und + „Art.“-Normköpfe: Eine handgepflegte Stammfassung kann *keine* Anlage tragen. Das ist die + konkrete Lücke für Berlin (Artikel 1) und Baden-Württemberg (Anlagen 1/3b/4b) — sie wird erst + behebbar, wenn für eines der beiden eine Stammfassung vorliegt. +* *Struktureinfügung mit Wortanker*: „Vor den Wörtern „Aus dem Bereich Verkehr:“ wird folgender + Absatz 5 eingefügt“ (Berlin Artikel 1 Nr. 2 b) bb)) — die Position der neuen Einheit bestimmt + ein Wortanker statt einer Stellenangabe. `StrukturEinfuegung` trägt bislang nur eine Stelle. * *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). diff --git a/src/test/java/eu/mulk/aendggner/EndToEndTest.java b/src/test/java/eu/mulk/aendggner/EndToEndTest.java index 19a7c8c..b3bea08 100644 --- a/src/test/java/eu/mulk/aendggner/EndToEndTest.java +++ b/src/test/java/eu/mulk/aendggner/EndToEndTest.java @@ -597,5 +597,27 @@ class EndToEndTest { assertThat(ergebnis.befehle()) .extracting(b -> b.stelle().anzeigeText()) .containsExactly("§ 2 Satz 1 Nummer 2", "§ 2 Satz 1 Nummer 2"); + + // Artikel 1 ändert die unnummerierte, nach ihrer Vorschrift benannte Anlage („Die Anlage zu + // § 2 Absatz 4 Satz 1 wird wie folgt geändert“). Der Zusatz benennt die Anlage nur — er darf + // nicht als Ziel „§ 2“ gelesen werden; die Punkte erben „Anlage“ als Kontext. + var asog = + new Gesetz( + "ASOG Bln", "Allgemeines Sicherheits- und Ordnungsgesetz", null, List.of()); + var artikel1 = new AenderungsgesetzParser().parse(text, asog, null); + assertThat(artikel1.artikel()).containsExactly("1"); + assertThat(artikel1.befehle()) + .extracting(b -> b.stelle().anzeigeText()) + .containsExactly( + "§ 67 Absatz 2", + "Anlage Nummer 6 Absatz 2", + "Anlage Nummer 23 Absatz 4a", + "Anlage Nummer 23"); + // Bekannte Grenze: „Vor den Wörtern „…“ wird folgender Absatz 5 eingefügt“ — eine + // Struktureinfügung, deren Position ein Wortanker statt einer Stelle bestimmt. + assertThat(artikel1.befehle().stream().filter(b -> b instanceof UnbekannterBefehl).toList()) + .singleElement() + .extracting(b -> b.provenienz().gliederungsPfad()) + .isEqualTo("2. b) bb)"); } } diff --git a/src/test/java/eu/mulk/aendggner/aenderung/parse/StellenParserTest.java b/src/test/java/eu/mulk/aendggner/aenderung/parse/StellenParserTest.java index f15b98f..d5b960a 100644 --- a/src/test/java/eu/mulk/aendggner/aenderung/parse/StellenParserTest.java +++ b/src/test/java/eu/mulk/aendggner/aenderung/parse/StellenParserTest.java @@ -110,6 +110,21 @@ class StellenParserTest { } @Test + void parstUnnummerierteBenannteAnlage() { + // Ein Gesetz mit einer einzigen Anlage benennt sie nach der Vorschrift, zu der sie gehört. + // Der Zusatz identifiziert die Anlage und ist nicht selbst Ziel — sonst würde das darin + // genannte „§ 2“ als Ziel gelesen und die falsche Norm geändert. + var anlage = StellenParser.parse("Die Anlage zu § 2 Absatz 4 Satz 1").orElseThrow(); + assertThat(anlage.anzeigeText()).isEqualTo("Anlage"); + assertThat(anlage.anlagenEnbez()).contains("Anlage"); + assertThat(anlage.paragraph()).isEmpty(); + + // Nummerierte Anlagen bleiben unverändert. + assertThat(StellenParser.parse("Anlage 8 Nummer 1 Buchstabe b").orElseThrow().anzeigeText()) + .isEqualTo("Anlage 8 Nummer 1 Buchstabe b"); + } + + @Test void ignoriertChapeauZusatz() { // „in der Angabe vor Nummer 1“ ist ein verfeinernder Zusatz ohne eigene Komponente. assertThat(StellenParser.parse("Absatz 1 in der Angabe vor Nummer 1").orElseThrow().anzeigeText()) 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 5ed8b93..5b7e850 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,15 @@ class TextBereinigerTest { } @Test + void entferntSteuerzeichenUndRepariertZerrisseneBefehlsvokabeln() { + // Im GVBl. für Berlin trägt der Einzug der Aufzählungsglieder ein U+0007; es steht unsichtbar + // vor dem Befehl und ließ dessen Zeilenanfangs-Anker ins Leere greifen. Im selben Heft ist + // „eingefügt“ als „ein ge-“ + „fügt“ gesetzt — ein Leerzeichen mitten in der Befehlsvokabel. + assertThat(TextBereiniger.bereinige("a)\t Nach Nummer 6 wird folgender Absatz ein ge-\nfügt:")) + .isEqualTo("a)\t Nach Nummer 6 wird folgender Absatz eingefügt:"); + } + + @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. |
