aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/main/java/eu/mulk/aendggner/aenderung/parse/StellenParser.java19
-rw-r--r--src/main/java/eu/mulk/aendggner/aenderung/parse/TextBereiniger.java13
-rw-r--r--src/main/resources/sampledata/Landesrecht-Beispiele.adoc16
-rw-r--r--src/test/java/eu/mulk/aendggner/EndToEndTest.java22
-rw-r--r--src/test/java/eu/mulk/aendggner/aenderung/parse/StellenParserTest.java15
-rw-r--r--src/test/java/eu/mulk/aendggner/aenderung/parse/TextBereinigerTest.java9
6 files changed, 88 insertions, 6 deletions
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.