aboutsummaryrefslogtreecommitdiff
path: root/src/main
diff options
context:
space:
mode:
authorMatthias Andreas Benkard <code@mail.matthias.benkard.de>2026-07-26 08:53:10 +0200
committerMatthias Andreas Benkard <code@mail.matthias.benkard.de>2026-07-26 08:53:10 +0200
commit81777013dfa582053489efea874e84379f1f0805 (patch)
tree83f227c65b91152d4a8aaca67d418da04d601e9e /src/main
parentc86a73a6e1da7dd28076054936458b850eda2947 (diff)
Support unnumbered named Anlagen; strip control characters
Investigating "Anlagen as amendment target" corrected the diagnosis from yesterday: Anlagen are supported. In gii-XML they are norms of their own ("Anlage 8", "Anhang"), Stelle.anlagenEnbez() resolves them, and the GEG sample applies annex-targeted commands — only two of its fifty manual entries touch annexes, both for content reasons. What actually failed in Berlin was narrower, and is fixed: * A law with a single annex names it after the provision it belongs to ("Die Anlage zu § 2 Absatz 4 Satz 1"). StellenParser demanded a number after "Anlage" and rejected the phrase, so the article's frame command went unrecognised and its items lost their context. The annex now carries the designation "Anlage", like "Anhang". The naming suffix is skipped deliberately: read along, the "§ 2" inside it would become the target and the wrong norm would be amended. * C0 control characters are now stripped. Berlin's enumeration indent carries a U+0007 that sits invisibly in front of the command and made its start-of-line anchor miss. * Command verbs torn apart by a stray space in the official typesetting ("ein gefügt" for "eingefügt") are rejoined, restricted to sequences that are not valid German word order. Three of Article 1's four commands are now recognised. The fourth places the new unit by a word anchor ("Vor den Wörtern … wird folgender Absatz 5 eingefügt"), which StrukturEinfuegung cannot express; that and the inability of the plain-text stem format to carry an Anlage at all are recorded in Landesrecht-Beispiele.adoc. 231 tests green; all pinned figures unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Change-Id: Ic5804bf343dd5d8ce435c07b8f882ca0372890d2
Diffstat (limited to 'src/main')
-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
3 files changed, 42 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).