Die Umnummerierungs-Kaskade war keine Grenze, sondern drei Mängel
Die mehrschrittige Umnummerierungssequenz galt bislang als wesenseigene Grenze:
Ihre Folgeänderungen gingen in den Abschnitt „Manuell prüfen“, im bayerischen
Belegfall drei von 154 Anweisungen (48. a) ee), 48. a) gg), 48. b) cc), sämtlich
aus der Neunummerierung des Bußgeldkatalogs in Art. 56 BayJG). Die Prüfung gegen
die amtliche Nachfassung zeigt, dass es keine Grenze war, sondern drei
voneinander unabhängige Mängel. Erst die Diagnose vor jeder Änderung — die
Begründungen der drei Fälle ausgeben — hat sie getrennt; die Vermutung, alles
sei eine Frage der Reihenfolge, traf nur auf einen von ihnen zu.
Geordnet werden nun Schritte statt Befehle (BefehlAnwender). Ein Verbund aus
Umnummerierung und Begleitänderung trägt zwei gegenläufige zeitliche Ansprüche:
Die Umnummerierung gehört vor denjenigen, der ihre Bezeichnung neu besetzt, die
Begleitänderung an ihre Stelle im Dokument, denn sie setzt die vorangegangenen
Punkte als vollzogen voraus. Bisher setzte sich der erste Anspruch für die ganze
Einheit durch. Bei gg) — „Die bisherige Nr. 11 wird Nr. 12 und die Angabe
„schriftliche“ wird gestrichen“ — stand die Angabe vorgezogen noch zweimal im
Artikel und war zu Recht mehrdeutig; an ihrer Dokumentstelle hat ee) die zweite
Fundstelle längst ersetzt. Die Befehlsliste wird dazu vor der Anwendung zu
Schritten aufgefaltet (je Teilbefehl einer); protokolliert wird unverändert je
Befehl, ein Verbund gilt nur als angewandt, wenn jeder Teil gegriffen hat.
Die Ordnungsregel selbst bleibt wörtlich dieselbe („wer eine Bezeichnung räumt,
kommt vor dem, der sie neu besetzt“), wird aber erst jetzt vollständig
durchgesetzt. Bisher rückte ein Befehl nur einmal vor seinen ersten
Kollisionspartner; auf Schritten zerreißt das die Ketten — von „Nr. 15 wird
Nr. 16“, „Nrn. 13 und 14 werden Nrn. 14 und 15“, „nach Nr. 12 wird Nr. 13
eingefügt“ zog die Einfügung nur das letzte Glied vor sich her. An die Stelle
tritt eine Tiefensuche auf der Dokumentordnung: vor jedem Schritt erst rekursiv
seine Räumer, eine ringförmige Abhängigkeit bricht ab. Kein Kahn mit „frühester
bereiter Knoten“ — der zieht unbeteiligte Schritte vor und bricht genau die
Begleitänderung wieder.
Die beiden übrigen Mängel lagen anderswo:
* Das Muster der lokativen Klausel (BefehlErkenner) schloss mit einer
Wortgrenze. Hinter einem abgekürzten Bezeichnungswort steht aber schon der
Abkürzungspunkt, und zwischen ihm und dem Leerzeichen liegt keine Wortgrenze;
der Klausel entging deshalb jede Kurzform („in Abs. 2 …“, „in Buchst. b …“).
Die unabgekürzten Formen trafen zu, weshalb es nie auffiel.
* Der StellenAufloeser verlangte hinter der Aufzählungsmarke Text auf derselben
Zeile. Eine Einheit, die sich vollständig in ihre Untergliederung ergießt,
führt ihre Marke allein (Art. 56 Abs. 2 Nr. 12 BayJG, darunter nur die
Buchstaben a und b) und galt als nicht auffindbar.
Der bayerische Belegfall steht damit auf 154 angewandten Anweisungen ohne Rest
(zuvor 151), die Ausschussfassung der Beschlussempfehlung zum GEG auf 68 statt
67 — dort greift nun eine Bereichs-Umnummerierung in § 108, die vorher nur ihr
erstes Glied vorziehen konnte; § 108 liegt jedoch in der bekannten Zone, in der
das Beispiel-XML eine andere Fassung ist als die vorausgesetzte, taugt also
nicht als Beleg. Belegt ist die Ordnung an Art. 29a und Art. 56 BayJG und an den
Kaskaden des WDR-Gesetzes. Alle übrigen Bezugszahlen des Bundes und der Länder
sind unverändert; 304 Prüfungen laufen durch.
Der Akzeptanztest hält zwei Mängel fest, die er nicht behebt. Beide bestanden
schon zuvor, was ein Lauf gegen den Ausgangsstand belegt:
* Der Block aus „Nach Nr. 4 werden die folgenden Nrn. 5 bis 7 eingefügt“ tritt
an das Ende des Absatzes statt hinter die Nr. 4, und der leere Platzhalter
„7. (aufgehoben)“ der Altfassung bleibt zwischen den Nrn. 9 und 10 stehen.
Dass jede Anweisung greift, heißt eben nicht, dass die Norm in allem der
amtlichen Nachfassung gleicht; der Test sagt das ausdrücklich.
* Die Heilung doppelter Leerzeichen nach einer Streichung wirkt auf den
gesamten Zieltext und verkürzt dabei die Einrückung der Aufzählungszeilen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Change-Id: Id61babe60226d27b5c05ff971927d61713b01781
diff --git a/FASSUNGEN.txt b/FASSUNGEN.txt
index 5b1a08f..ec8aeb5 100644
--- a/FASSUNGEN.txt
+++ b/FASSUNGEN.txt
@@ -21,6 +21,91 @@
════════════════════════════════════════════════════════════════════════════════
+ Fassung vom 15. August 2026,
+ zuletzt geändert durch die am 15. August 2026 vorgenommenen Änderungen
+════════════════════════════════════════════════════════════════════════════════
+
+Das Erzeugnis hat die mehrschrittige Umnummerierungssequenz bislang als eine ihm
+wesenseigene Grenze behandelt und ihre Folgeänderungen dem Abschnitt „Manuell
+prüfen“ zugewiesen. Die Prüfung des bayerischen Belegfalls (Art. 56 des
+Bayerischen Jagdgesetzes, Neunummerierung des Bußgeldkatalogs durch § 1 Nr. 48
+des Gesetzes vom 31. März 2026) hat ergeben, dass es sich um keine Grenze der
+Sache, sondern um drei voneinander unabhängige Mängel gehandelt hat. Sie werden
+mit dieser Fassung behoben.
+
+
+Artikel 1
+Ordnung der Anwendung nach Schritten
+
+(1) Der Anwender (BefehlAnwender) faltet die Befehlsliste vor der Anwendung zu
+ einer Liste von Schritten auf; ein Verbund (Sammelbefehl) liefert je
+ Teilbefehl einen Schritt, jeder andere Befehl einen einzigen. Geordnet
+ werden fortan die Schritte, protokolliert wird unverändert je Befehl.
+
+(2) Der Grund der Trennung ist folgender: Ein Verbund aus einer Umnummerierung
+ und einer Begleitänderung trägt zwei gegenläufige zeitliche Ansprüche. Die
+ Umnummerierung gehört vor denjenigen Befehl, der ihre Bezeichnung neu
+ besetzt; die Begleitänderung gehört an ihre Stelle im Dokument, denn sie
+ setzt die vorangegangenen Punkte als vollzogen voraus. Bislang setzte sich
+ der erste Anspruch für die ganze Einheit durch und zog die Begleitänderung
+ mit sich; sie traf dann auf einen Stand, den das Änderungsgesetz nicht
+ voraussetzt.
+
+(3) Die Ordnungsregel selbst bleibt unverändert („wer eine Bezeichnung räumt,
+ kommt vor demjenigen, der sie neu besetzt“), wird aber nunmehr vollständig
+ durchgesetzt: Vor jedem Schritt laufen erst diejenigen Schritte, die ihm
+ eine Bezeichnung räumen, und vor diesen wiederum die ihren. Eine ringförmige
+ Abhängigkeit bricht ab; die beteiligten Schritte behalten dann ihre
+ Dokumentreihenfolge.
+
+
+Artikel 2
+Erkennung der abgekürzten lokativen Folgeklausel
+
+Das Muster der lokativen Klausel (BefehlErkenner) schloss mit einer Wortgrenze.
+Hinter einem abgekürzten Bezeichnungswort steht jedoch bereits der
+Abkürzungspunkt, und zwischen ihm und dem folgenden Leerzeichen liegt keine
+Wortgrenze; der Klausel entging deshalb jede Kurzform („in Abs. 2 …“, „in
+Buchst. b …“). An die Stelle der Wortgrenze tritt die Bedingung, dass kein
+Buchstabe folgt.
+
+
+Artikel 3
+Auflösung der Aufzählungseinheit ohne eigenen Text
+
+Der Stellenaufloeser (StellenAufloeser) verlangte hinter der Aufzählungsmarke
+Text auf derselben Zeile. Eine Einheit, die sich vollständig in ihre
+Untergliederung ergießt, führt ihre Marke aber allein auf der Zeile (Art. 56
+Abs. 2 Nr. 12 des Bayerischen Jagdgesetzes, darunter nur die Buchstaben a und
+b); sie galt deshalb als nicht auffindbar. Der Text hinter der Marke ist fortan
+wahlfrei.
+
+
+Schlussbestimmung
+
+(1) Die vorstehenden Änderungen sind durch die Prüfung sämtlicher Testfälle
+ (dreihundertvier an der Zahl) sowie durch die vollständige Erstellung
+ (mvnw verify) bestätigt worden.
+
+(2) Der bayerische Belegfall weist nunmehr sämtliche einhundertvierundfünfzig
+ Anweisungen als angewandt aus (zuvor einhunderteinundfünfzig); die
+ Ausschussfassung der Beschlussempfehlung zum Gebäudeenergiegesetz weist
+ achtundsechzig statt siebenundsechzig aus. Die übrigen Bezugszahlen des
+ Bundes und der Länder sind unverändert geblieben.
+
+(3) Nicht behoben ist die Stellung eines eingefügten Blocks: Die Anweisung „Nach
+ Nr. 4 werden die folgenden Nrn. 5 bis 7 eingefügt“ (Art. 56 Abs. 1 des
+ Bayerischen Jagdgesetzes) wird angewandt, ihr Block tritt aber an das Ende
+ des Absatzes statt hinter die Nummer 4; ebenso verbleibt der leere
+ Platzhalter „7. (aufgehoben)“ der Altfassung zwischen den Nummern 9 und 10.
+ Beides bestand schon vor dieser Fassung und ist im Belegfall festgehalten.
+
+(4) Ferner ist festgehalten: Die Heilung doppelter Leerzeichen nach einer
+ Streichung (BefehlAnwender) wirkt auf den gesamten Zieltext und verkürzt
+ dabei die Einrückung der Aufzählungszeilen. Auch dies bestand schon zuvor.
+
+
+════════════════════════════════════════════════════════════════════════════════
Fassung vom 14. August 2026,
zuletzt geändert durch die am 14. August 2026 vorgenommenen Änderungen
════════════════════════════════════════════════════════════════════════════════
diff --git a/README.adoc b/README.adoc
index 75b66f6..586b17f 100644
--- a/README.adoc
+++ b/README.adoc
@@ -131,12 +131,28 @@
absteigende Abarbeitung einer aufsteigenden Kaskade („Der bisherige
Absatz 3 wird Absatz 4“, „Der bisherige Absatz 4 wird Absatz 5“, …)
ebenso wie der Vorrang einer Umnummerierung vor der Einfügung, die deren
-Bezeichnung neu vergibt. Verschoben wird dabei nur nach vorn, sodass
-jede Folgeänderung hinter der Umnummerierung bleibt, auf deren neue
-Bezeichnung sie zeigt. Die PDF-Aufbereitung toleriert dabei
-Drucksachen-Artefakte (Seitenköpfe und -füße, Vorabfassungs-Wasserzeichen,
-vertauschte oder gerade Anführungszeichen, verklebte Wortgrenzen, zerlegt
-kodierte Umlaute) und bestimmt die Brotschrift seitenweise, sodass auch
+Bezeichnung neu vergibt. Wer vorrückt, nimmt dabei seine eigenen Räumer
+mit: Auch eine über mehrere Punkte verschränkte Kaskade („Die bisherige
+Nr. 15 wird Nr. 16“, „Die bisherigen Nrn. 13 und 14 werden die Nrn. 14
+und 15“, „Nach Nr. 12 wird folgende Nr. 13 eingefügt“) tritt deshalb
+geschlossen vor die Einfügung, nicht bloß mit ihrem letzten Glied.
+
+Geordnet werden nicht die Befehle, sondern die einzelnen
+Anwendungsschritte. Ein Verbund aus Umnummerierung und Folgeänderung
+trägt nämlich zwei gegenläufige zeitliche Ansprüche: Die Umnummerierung
+gehört nach vorn, die Folgeänderung dagegen an ihre Dokumentstelle, weil
+sie die vorangegangenen Punkte als vollzogen voraussetzt — „Die
+bisherige Nr. 11 wird Nr. 12 und die Angabe „schriftliche“ wird
+gestrichen“ ist vorgezogen mehrdeutig, weil erst ein früherer Punkt die
+zweite Fundstelle des Wortes beseitigt. Verschoben wird stets nur nach
+vorn. Das Protokoll bleibt gleichwohl befehlsweise: Ein Verbund gilt nur
+dann als angewandt, wenn jeder seiner Teile gegriffen hat, sonst nennt
+die Meldung den Teil und den Grund.
+
+Die PDF-Aufbereitung toleriert Drucksachen-Artefakte (Seitenköpfe
+und -füße, Vorabfassungs-Wasserzeichen, vertauschte oder gerade
+Anführungszeichen, verklebte Wortgrenzen, zerlegt kodierte Umlaute) und
+bestimmt die Brotschrift seitenweise, sodass auch
Ministeriumsentwürfe mit gemischten Layouts vollständig extrahiert
werden. Fehlt im amtlichen Satz ein schließendes Anführungszeichen, endet
das Zitat an der nächsten Strukturgrenze — an einer Artikel-Überschrift
@@ -161,7 +177,10 @@
„Dem Wortlaut werden die folgenden Abs. 1 bis 4 vorangestellt“, „Der
bisherige Wortlaut wird Abs. 5“, Halbsatz-Ziele, Klauselketten mit
gemeinsamem Schlussverb) sowie das Fortführungszeichen des GVBl (jedes
-neugefasste Aufzählungsglied öffnet erneut mit „).
+neugefasste Aufzählungsglied öffnet erneut mit „). Der Fall ist gegen
+die amtliche Nachfassung belegt: Alle 154 auf das BayJG zielenden
+Befehle des Heftes 6/2026 werden an 54 Normen selbsttätig angewandt,
+auch die verschränkte Neunummerierung des Bußgeldkatalogs in Art. 56.
Die übrigen Länder gliedern ihre Stammgesetze wie der Bund in
Paragraphen; die Unterschiede liegen im Gesetzblatt-Satz und in
@@ -169,7 +188,7 @@
(NEFG) und Nordrhein-Westfalen — dort alle vier ändernden Artikel eines
Heftes: Telemedienzuständigkeitsgesetz, Landesmediengesetz,
Ausführungsgesetz zum 17. Rundfunkänderungsstaatsvertrag und, mit 101
-Befehlen an 31 Normen der größte Landesrechtsfall, das WDR-Gesetz —
+Befehlen an 31 Normen der größte Fall des Heftes, das WDR-Gesetz —
mit Akzeptanztests gegen die
amtlichen Nachfassungen; für Schleswig-Holstein, Berlin und Hessen
reicht die Prüfung bis zur Befehlserkennung — dort vollständig, aber ohne
diff --git a/src/main/java/eu/mulk/aendggner/aenderung/parse/BefehlErkenner.java b/src/main/java/eu/mulk/aendggner/aenderung/parse/BefehlErkenner.java
index bda8e94..e63cfd8 100644
--- a/src/main/java/eu/mulk/aendggner/aenderung/parse/BefehlErkenner.java
+++ b/src/main/java/eu/mulk/aendggner/aenderung/parse/BefehlErkenner.java
@@ -587,11 +587,16 @@
// „nach Satz 2 wird …“). Sie erbt nach einer Umnummerierung deren neue Bezeichnung als Kontext.
// Wortanker („nach der Angabe „…““) zählen ausdrücklich nicht dazu: sie suchen ihren Text
// normweit und dürfen nicht auf die neue Bezeichnung festgenagelt werden.
+ // Der Abschluss ist „kein Buchstabe mehr“ und nicht \b: hinter einem abgekürzten
+ // Bezeichnungswort steht bereits der Abkürzungspunkt, und zwischen ihm und dem folgenden
+ // Leerzeichen liegt keine Wortgrenze — mit \b entginge der Klausel jede bayerische
+ // Kurzform („in Abs. 2 …“, „in Buchst. b …“). Vor der Ausschreibung steht die Kurzform
+ // nur dann zurück, wenn diese länger passt (Alternativen sind absteigend geordnet).
private static final Pattern LOKATIVE_KLAUSEL =
Pattern.compile(
"(?:in|im|nach|vor) (?:dem |der |den )?"
+ "(?:Satzteil|Satz|Sätze[n]?|Absatz|Abs\\.|Absätze[n]?|Nummer[n]?|Nrn?\\."
- + "|Buchstabe[n]?|Buchst\\.|Halbsatz)\\b");
+ + "|Buchstabe[n]?|Buchst\\.|Halbsatz)(?!\\p{L})");
// „In <Stelle> wird nach den Wörtern «1» ein Komma eingefügt.“ (Satzzeichen statt Wörter
// einfügen)
diff --git a/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java b/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java
index b8540c5..32350ff 100644
--- a/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java
+++ b/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java
@@ -73,20 +73,25 @@
var protokoll = new ArrayList<AngewandteAenderung>();
String neuerLangtitel = null;
- // Angewandt wird in Sachreihenfolge, protokolliert in der Reihenfolge des Änderungsgesetzes.
- var ergebnisse = new AngewandteAenderung[befehle.size()];
- for (int index : anwendungsReihenfolge(befehle)) {
- var befehl = befehle.get(index);
+ // Angewandt wird schrittweise in Sachreihenfolge, protokolliert befehlsweise in der
+ // Reihenfolge des Änderungsgesetzes.
+ var schritte = schritte(befehle);
+ var ergebnisse = new AngewandteAenderung[schritte.size()];
+ for (int index : anwendungsReihenfolge(schritte)) {
+ var schritt = schritte.get(index);
// „Die Überschrift wird wie folgt gefasst / durch die folgende Überschrift ersetzt“ auf
- // oberster Ebene meint die Überschrift des Gesetzes selbst.
- if (befehl instanceof Neufassung n && istNurUeberschrift(n.stelle())) {
+ // oberster Ebene meint die Überschrift des Gesetzes selbst — innerhalb eines Verbunds
+ // dagegen nie, dort steht sie für die Überschrift der Einheit, die der Verbund betrifft.
+ if (schritt.ganzerBefehl()
+ && schritt.teil() instanceof Neufassung n
+ && istNurUeberschrift(n.stelle())) {
neuerLangtitel = n.neuerText().replaceAll("\\s+", " ").strip();
- ergebnisse[index] = angewandt(befehl, "(Gesetzesüberschrift)");
+ ergebnisse[index] = angewandt(schritt.teil(), "(Gesetzesüberschrift)");
continue;
}
- ergebnisse[index] = wendeAn(normen, gliederungen, befehl);
+ ergebnisse[index] = wendeAn(normen, gliederungen, schritt.teil());
}
- protokoll.addAll(Arrays.asList(ergebnisse));
+ protokoll.addAll(verdichte(befehle, schritte, ergebnisse));
var neu = alt.mitNormen(normen).mitGliederungen(gliederungen);
if (neuerLangtitel != null) {
@@ -96,46 +101,160 @@
}
/**
- * Reihenfolge, in der die Befehle anzuwenden sind: grundsätzlich die des Änderungsgesetzes.
- *
- * <p>Eine Umnummerierung „Der bisherige § 13 wird § 14“ wird jedoch vor eine vorangehende
- * Einfügung gezogen, die denselben Paragraphen neu besetzt („In Kapitel 4 wird nach § 12 der
- * folgende neue § 13 angefügt“). „Bisherig“ bezeichnet den Stand vor der Änderung; die
- * Umnummerierung geht der Neubesetzung sachlich also voraus. In der Textreihenfolge angewandt
- * liefen beide Befehle dagegen auf zwei Paragraphen gleicher Bezeichnung hinaus.
+ * Ein einzeln anzuwendender Schritt: der Teilbefehl selbst und der Index des Befehls, in dessen
+ * Protokolleintrag sein Ergebnis eingeht. Ein Verbund ({@link Sammelbefehl}) zerfällt in je einen
+ * Schritt pro Teilbefehl; jeder andere Befehl ist sein eigener einziger Schritt ({@code
+ * ganzerBefehl}).
*/
+ private record Schritt(int befehlIndex, Aenderungsbefehl teil, boolean ganzerBefehl) {}
+
+ /** Faltet die Befehlsliste zur Schrittliste auf, in Dokumentreihenfolge. */
+ private static List<Schritt> schritte(List<Aenderungsbefehl> befehle) {
+ var schritte = new ArrayList<Schritt>(befehle.size());
+ for (int i = 0; i < befehle.size(); i++) {
+ var befehl = befehle.get(i);
+ if (befehl instanceof Sammelbefehl s) {
+ falte(s, i, schritte);
+ } else {
+ schritte.add(new Schritt(i, befehl, true));
+ }
+ }
+ return schritte;
+ }
+
+ private static void falte(Sammelbefehl befehl, int befehlIndex, List<Schritt> ziel) {
+ for (var teil : befehl.teilbefehle()) {
+ if (teil instanceof Sammelbefehl geschachtelt) {
+ falte(geschachtelt, befehlIndex, ziel);
+ } else {
+ ziel.add(new Schritt(befehlIndex, teil, false));
+ }
+ }
+ }
+
/**
- * Anwendungsreihenfolge der Befehle. Sie folgt dem Dokument, mit einer Ausnahme: Umnummerierungen
- * beziehen sich stets auf die ursprüngliche Zählung, nicht auf den Stand nach den vorangegangenen
- * Punkten. Wer eine Bezeichnung räumt, muss daher vor den kommen, der sie neu besetzt — sonst
- * trüge das Gesetz vorübergehend zwei gleich bezeichnete Einheiten und die Fundstelle wäre
- * mehrdeutig. Aus dieser einen Regel folgt beides: die absteigende Reihenfolge einer
- * aufsteigenden Kaskade („Abs. 3 wird 4“, „Abs. 4 wird 5“, …) und der Vorrang einer
+ * Anwendungsreihenfolge der Schritte. Sie folgt dem Dokument, mit einer Ausnahme:
+ * Umnummerierungen beziehen sich stets auf die ursprüngliche Zählung, nicht auf den Stand nach
+ * den vorangegangenen Punkten. Wer eine Bezeichnung räumt, muss daher vor den kommen, der sie neu
+ * besetzt — sonst trüge das Gesetz vorübergehend zwei gleich bezeichnete Einheiten und die
+ * Fundstelle wäre mehrdeutig. Aus dieser einen Regel folgt beides: die absteigende Reihenfolge
+ * einer aufsteigenden Kaskade („Abs. 3 wird 4“, „Abs. 4 wird 5“, …) und der Vorrang einer
* Umnummerierung vor einer Einfügung, die deren Bezeichnung neu vergibt.
*
- * <p>Verschoben wird stets nur nach vorn: ein Befehl rückt vor den ersten, mit dem er kollidiert.
- * So bleibt jede Folgeänderung hinter der Umnummerierung, auf deren neue Bezeichnung sie zeigt
- * („Der bisherige Absatz 3 wird Absatz 4 und wie folgt geändert: …“).
+ * <p>Verschoben wird stets nur nach vorn, und wer vorrückt, nimmt mit, was ihm selbst vorausgehen
+ * muss: Vor jedem Schritt laufen erst diejenigen, die ihm eine Bezeichnung räumen, und vor diesen
+ * wiederum die ihren. Ohne diese Mitnahme zerrisse eine Kette wie „Nr. 15 wird Nr. 16“, „Nrn. 13
+ * und 14 werden die Nrn. 14 und 15“, „nach Nr. 12 wird Nr. 13 eingefügt“: die Einfügung zöge nur
+ * das letzte Glied vor sich, und dieses beträfe eine Bezeichnung, die noch besetzt ist. Ein
+ * Schritt ohne solche Voraussetzung bleibt dagegen an seinem Platz — daran hängt, dass eine
+ * Begleitänderung ihre Dokumentstelle behält.
+ *
+ * <p>Geordnet werden <em>Schritte</em> und nicht Befehle, weil ein Verbund aus Umnummerierung und
+ * Begleitänderung zwei gegenläufige zeitliche Ansprüche in einer Einheit trägt: Die
+ * Umnummerierung gehört nach vorn, ihre Begleitänderung aber an ihre Dokumentstelle, denn sie
+ * setzt die vorangegangenen Punkte als vollzogen voraus. „Die bisherige Nr. 11 wird Nr. 12 und
+ * die Angabe „schriftliche“ wird gestrichen“ (BayJG Art. 56 Abs. 1) trifft nach vorn gezogen noch
+ * zwei Fundstellen und wäre mehrdeutig; an seinem Platz belassen genau eine, weil der vorherige
+ * Punkt die andere längst ersetzt hat.
*/
- private static List<Integer> anwendungsReihenfolge(List<Aenderungsbefehl> befehle) {
- var reihenfolge = new ArrayList<Integer>(befehle.size());
- for (int i = 0; i < befehle.size(); i++) {
- reihenfolge.add(i);
+ private static List<Integer> anwendungsReihenfolge(List<Schritt> schritte) {
+ int anzahl = schritte.size();
+ var raeumt = new ArrayList<Set<String>>(anzahl);
+ var belegt = new ArrayList<Set<String>>(anzahl);
+ for (var schritt : schritte) {
+ raeumt.add(geraeumteBezeichnungen(schritt.teil()));
+ belegt.add(belegteBezeichnungen(schritt.teil()));
}
- for (int j = 0; j < befehle.size(); j++) {
- var raeumt = geraeumteBezeichnungen(befehle.get(j));
- if (raeumt.isEmpty()) {
- continue;
- }
- for (int i = 0; i < j; i++) {
- if (belegteBezeichnungen(befehle.get(i)).stream().anyMatch(raeumt::contains)) {
- reihenfolge.remove(Integer.valueOf(j));
- reihenfolge.add(reihenfolge.indexOf(i), j);
- break;
+ var reihenfolge = new ArrayList<Integer>(anzahl);
+ // 0 = offen, 1 = in Arbeit (Zyklus-Bremse), 2 = eingereiht.
+ var stand = new byte[anzahl];
+ for (int i = 0; i < anzahl; i++) {
+ reiheEin(i, stand, raeumt, belegt, reihenfolge);
+ }
+ return reihenfolge;
+ }
+
+ /**
+ * Reiht den Schritt ein, nachdem alle Schritte eingereiht sind, die ihm eine seiner Bezeichnungen
+ * räumen. Eine ringförmige Abhängigkeit — zwei Schritte, die einander räumen — bricht an der
+ * Marke „in Arbeit“ ab; die beteiligten Schritte behalten dann ihre Dokumentreihenfolge.
+ */
+ private static void reiheEin(
+ int schritt,
+ byte[] stand,
+ List<Set<String>> raeumt,
+ List<Set<String>> belegt,
+ List<Integer> reihenfolge) {
+ if (stand[schritt] != 0) {
+ return;
+ }
+ stand[schritt] = 1;
+ var eigene = belegt.get(schritt);
+ if (!eigene.isEmpty()) {
+ for (int vorgaenger = 0; vorgaenger < stand.length; vorgaenger++) {
+ if (vorgaenger != schritt && raeumt.get(vorgaenger).stream().anyMatch(eigene::contains)) {
+ reiheEin(vorgaenger, stand, raeumt, belegt, reihenfolge);
}
}
}
- return reihenfolge;
+ stand[schritt] = 2;
+ reihenfolge.add(schritt);
+ }
+
+ /**
+ * Verdichtet die Schrittergebnisse zu je einem Protokolleintrag pro Befehl. Ein Verbund gilt nur
+ * dann als angewandt, wenn jeder seiner Teile griff; sonst nennt die Begründung die gescheiterten
+ * Teile in ihrer Reihenfolge im Verbund.
+ */
+ private static List<AngewandteAenderung> verdichte(
+ List<Aenderungsbefehl> befehle, List<Schritt> schritte, AngewandteAenderung[] ergebnisse) {
+ var protokoll = new ArrayList<AngewandteAenderung>(befehle.size());
+ int schritt = 0;
+ for (int i = 0; i < befehle.size(); i++) {
+ int von = schritt;
+ while (schritt < schritte.size() && schritte.get(schritt).befehlIndex() == i) {
+ schritt++;
+ }
+ if (von == schritt) {
+ // Ein Verbund ohne Teilbefehle — nichts anzuwenden, aber auch nichts zu verschweigen.
+ protokoll.add(manuell(befehle.get(i), "Verbund ohne Teilbefehle."));
+ continue;
+ }
+ if (schritte.get(von).ganzerBefehl()) {
+ protokoll.add(ergebnisse[von]);
+ continue;
+ }
+ protokoll.add(
+ verbund(befehle.get(i), Arrays.asList(ergebnisse).subList(von, schritt), schritte, von));
+ }
+ return protokoll;
+ }
+
+ private static AngewandteAenderung verbund(
+ Aenderungsbefehl befehl,
+ List<AngewandteAenderung> teilErgebnisse,
+ List<Schritt> schritte,
+ int von) {
+ var betroffene = new LinkedHashSet<String>();
+ var fehler = new ArrayList<String>();
+ for (int k = 0; k < teilErgebnisse.size(); k++) {
+ var ergebnis = teilErgebnisse.get(k);
+ betroffene.addAll(ergebnis.betroffeneEnbez());
+ if (ergebnis.status() != Status.ANGEWANDT) {
+ fehler.add(
+ "Teil "
+ + (k + 1)
+ + " ("
+ + schritte.get(von + k).teil().stelle().anzeigeText()
+ + "): "
+ + ergebnis.begruendung());
+ }
+ }
+ if (fehler.isEmpty()) {
+ return new AngewandteAenderung(befehl, Status.ANGEWANDT, "", betroffene);
+ }
+ return new AngewandteAenderung(
+ befehl, Status.MANUELL_PRUEFEN, String.join(" ", fehler), betroffene);
}
/** Bezeichnungen, die ein Befehl freigibt — die Ausgangsstellen seiner Umnummerierungen. */
@@ -1486,39 +1605,20 @@
* zusammen. Nur wenn alle Teile gelingen, gilt der Befehl als angewandt; sonst wird er zur
* manuellen Prüfung markiert (bereits angewandte Teile bleiben wirksam).
*/
+ /**
+ * Ein Verbund, der erst hier auftaucht (in einem anderen Verbund geschachtelt), wird für sich
+ * genommen aufgefaltet und geordnet. Der Regelfall — der Verbund als eigener Gliederungspunkt —
+ * ist dagegen schon in {@link #anwenden} aufgefaltet und läuft nie hier durch.
+ */
private static AngewandteAenderung wendeSammelAn(
List<Norm> normen, List<Gliederung> gliederungen, Sammelbefehl befehl) {
- var betroffene = new LinkedHashSet<String>();
- var fehler = new ArrayList<String>();
- // Auch innerhalb eines Verbunds gilt die Bezeichnungs-Reihenfolge: „Die bisherigen Nrn. 13 und
- // 14 werden die Nrn. 14 und 15“ zerfällt in zwei Umnummerierungen, die aufsteigend angewandt
- // eine doppelte Nr. 14 erzeugten.
- var teile = befehl.teilbefehle();
- var meldungen = new String[teile.size()];
- for (int index : anwendungsReihenfolge(teile)) {
- var teil = teile.get(index);
- var ergebnis = wendeAn(normen, gliederungen, teil);
- betroffene.addAll(ergebnis.betroffeneEnbez());
- if (ergebnis.status() != Status.ANGEWANDT) {
- meldungen[index] =
- "Teil "
- + (index + 1)
- + " ("
- + teil.stelle().anzeigeText()
- + "): "
- + ergebnis.begruendung();
- }
+ var schritte = new ArrayList<Schritt>(befehl.teilbefehle().size());
+ falte(befehl, 0, schritte);
+ var ergebnisse = new AngewandteAenderung[schritte.size()];
+ for (int index : anwendungsReihenfolge(schritte)) {
+ ergebnisse[index] = wendeAn(normen, gliederungen, schritte.get(index).teil());
}
- for (var meldung : meldungen) {
- if (meldung != null) {
- fehler.add(meldung);
- }
- }
- if (fehler.isEmpty()) {
- return new AngewandteAenderung(befehl, Status.ANGEWANDT, "", betroffene);
- }
- return new AngewandteAenderung(
- befehl, Status.MANUELL_PRUEFEN, String.join(" ", fehler), betroffene);
+ return verbund(befehl, Arrays.asList(ergebnisse), schritte, 0);
}
// --- Gemeinsame Helfer ---------------------------------------------------------------------
diff --git a/src/main/java/eu/mulk/aendggner/anwendung/StellenAufloeser.java b/src/main/java/eu/mulk/aendggner/anwendung/StellenAufloeser.java
index ff77279..d2fe02a 100644
--- a/src/main/java/eu/mulk/aendggner/anwendung/StellenAufloeser.java
+++ b/src/main/java/eu/mulk/aendggner/anwendung/StellenAufloeser.java
@@ -252,7 +252,10 @@
*/
private static SatzTeiler.@Nullable SatzBereich zeilenBlock(
String text, String labelRegex, SatzTeiler.SatzBereich suchbereich) {
- var muster = Pattern.compile("(?m)^([ \\t]*)" + labelRegex + "[ \\t].*$");
+ // Der Text hinter dem Label ist wahlfrei: eine Aufzählungseinheit, die sich vollständig in
+ // ihre Untergliederung ergießt, führt ihre Marke allein auf der Zeile (BayJG Art. 56 Abs. 2
+ // Nr. 12 — darunter nur die Buchstaben a und b). Ihr Block sind dann eben nur die Kindzeilen.
+ var muster = Pattern.compile("(?m)^([ \\t]*)" + labelRegex + "(?:[ \\t].*)?$");
var matcher = muster.matcher(text).region(suchbereich.von(), suchbereich.bis());
SatzTeiler.SatzBereich gefunden = null;
int einrueckung = 0;
diff --git a/src/main/resources/sampledata/Landesrecht-Beispiele.adoc b/src/main/resources/sampledata/Landesrecht-Beispiele.adoc
index be70dfc..4de907f 100644
--- a/src/main/resources/sampledata/Landesrecht-Beispiele.adoc
+++ b/src/main/resources/sampledata/Landesrecht-Beispiele.adoc
@@ -232,10 +232,16 @@
angefasst — ein Sammelheft mit mehreren Verkündungen, also zugleich ein Test der Artikel-/
Verkündungsauswahl.
. *Berlin, ganzseitenbreite Rahmen* (XY-Cut) — die letzte offene Layout-Frage, siehe „Noch offen“.
+. *Stellung eines eingefügten Blocks* — seit die Umnummerierungs-Kaskade des BayJG aufgeht, ist
+ dies der sichtbare Rest an Art. 56: „Nach Nr. 4 werden die folgenden Nrn. 5 bis 7 eingefügt“ gilt
+ als angewandt, der Block tritt aber ans Ende des Absatzes statt hinter die Nr. 4, und der leere
+ Platzhalter „7. (aufgehoben)“ bleibt zwischen den Nrn. 9 und 10 stehen. Beides ist im
+ Akzeptanztest festgehalten, damit es nicht unbemerkt bleibt.
Regressionsschutz: `./mvnw verify` prüft die bestehenden Belegfälle mit. Gepinnt ist dabei nicht
überall dieselbe Art von Zahl — für UWG, AGG und ProdHaftG, dass *kein* Befehl manuell bleibt; für
-das BayJG die 154 erkannten Befehle und namentlich die drei Umnummerierungs-Residuen; für Sachsen,
+das BayJG die 154 erkannten Befehle, die vollständige Anwendung und den Wortlaut des
+Bußgeldkatalogs an drei Stellen; für Sachsen,
Niedersachsen und die vier NRW-Artikel die vollständige Anwendung bis auf das eine benannte
Residuum im WDR-Gesetz; für Schleswig-Holstein, Berlin und Hessen die vollständige *Erkennung*
(dort 21 Befehle). Wer eine dieser Zahlen
diff --git a/src/test/java/eu/mulk/aendggner/EndToEndTest.java b/src/test/java/eu/mulk/aendggner/EndToEndTest.java
index 55579f1..865afe6 100644
--- a/src/test/java/eu/mulk/aendggner/EndToEndTest.java
+++ b/src/test/java/eu/mulk/aendggner/EndToEndTest.java
@@ -260,18 +260,19 @@
assertThat(parseErgebnis.befehle()).hasSize(154);
assertThat(parseErgebnis.befehle()).noneMatch(b -> b instanceof UnbekannterBefehl);
- // 4. Anwenden auf die alte Fassung. Von 154 Befehlen bleiben genau drei als „manuell prüfen“
- // stehen — Folgeänderungen innerhalb einer mehrschrittigen Umnummerierungssequenz, die
- // ÄndGgner bewusst nicht automatisch auflöst (statt fehlerhaft zu raten):
- // * Art. 56 (§ 1 Nr. 48): eine umfangreiche Neunummerierung des Bußgeldkatalogs
- // (Einfügen der Nrn. 5–7 und 13, Verschieben von Nr. 6→9, 11→12 …) verschiebt die
- // Zielnummern der begleitenden Buchstaben-/Wortänderungen.
- // Die Sequenz in Art. 29a (§ 1 Nr. 23) löst sich dagegen seit der Kaskaden-Ordnung des
- // BefehlAnwenders auf: „Der bisherige Abs. 4 wird Abs. 5“ läuft vor der Bereichs-
- // Umnummerierung „Die bisherigen Abs. 1 bis 3 werden die Abs. 2 bis 4“, sodass die auf den
- // neuen Abs. 5 zielenden Wort- und Satzbefehle ihren Alttext finden.
- // Diese Residuen sind exakt gepinnt; sie landen mit Begründung im Abschnitt „Manuell prüfen“
- // der Synopse und werden nie stillschweigend verworfen.
+ // 4. Anwenden auf die alte Fassung. Alle 154 Befehle greifen; kein Rest bleibt manuell.
+ // Das Heft trägt zwei mehrschrittige Umnummerierungssequenzen, an denen sich die
+ // Schritt-Ordnung des BefehlAnwenders bewährt:
+ // * Art. 29a (§ 1 Nr. 23): „Der bisherige Abs. 4 wird Abs. 5“ läuft vor der Bereichs-
+ // Umnummerierung „Die bisherigen Abs. 1 bis 3 werden die Abs. 2 bis 4“, sodass die auf
+ // den neuen Abs. 5 zielenden Wort- und Satzbefehle ihren Alttext finden.
+ // * Art. 56 (§ 1 Nr. 48): die Neunummerierung des Bußgeldkatalogs verschränkt Einfügungen
+ // und Umnummerierungen über zehn Unterpunkte. Sie geht auf, weil die Umnummerierungen
+ // vorrücken, ihre Begleitänderungen aber an ihrer Dokumentstelle bleiben — die Wortfolge
+ // „schriftliche“ etwa steht erst nach dem vorangehenden Punkt nur noch einmal im
+ // Artikel und ist damit eindeutig.
+ // „Kein Rest“ heißt: jeder Befehl hat gegriffen. Es heißt nicht, dass die Norm in allem der
+ // amtlichen Nachfassung gleicht — was daran noch fehlt, steht weiter unten an Art. 56.
var anwendung = BefehlAnwender.anwenden(gesetz, parseErgebnis.befehle());
assertThat(anwendung.protokoll()).hasSameSizeAs(parseErgebnis.befehle());
var manuellPfade =
@@ -279,7 +280,34 @@
.filter(a -> a.status() == BefehlAnwender.Status.MANUELL_PRUEFEN)
.map(a -> a.befehl().provenienz().gliederungsPfad())
.toList();
- assertThat(manuellPfade).containsExactlyInAnyOrder("48. a) ee)", "48. a) gg)", "48. b) cc)");
+ assertThat(manuellPfade).isEmpty();
+
+ // Der Bußgeldkatalog des Art. 56 trägt jetzt an den drei Stellen, an denen die Kaskade zuvor
+ // gescheitert war, den Wortlaut der amtlichen Nachfassung (BayJG.pdf):
+ var art56 = anwendung.neu().norm("Art. 56").orElseThrow();
+ var katalog = art56.absaetze().get(0).text();
+ assertThat(katalog)
+ // aus „Die bisherige Nr. 6 wird Nr. 9 und in Buchst. b …“ — die Begleitklausel findet ihre
+ // Stelle erst, seit die lokative Kurzform („in Buchst. b“) erkannt wird.
+ .contains("9. vorsätzlich oder fahrlässig entgegen Art. 32 Abs. 2 Satz 1, Abs. 4 oder 5")
+ .contains("b) die Abschussmeldung oder die Streckenliste")
+ // aus „Die bisherige Nr. 11 wird Nr. 12 und die Angabe „schriftliche“ wird gestrichen“ —
+ // eindeutig nur, weil die Streichung an ihrer Dokumentstelle bleibt.
+ .contains("12. ohne Begleitung oder Erlaubnis des Revierinhabers")
+ .doesNotContain("schriftliche");
+ // Abs. 2 Nr. 12 Buchst. b führt ihre Marke allein auf der Zeile — sie ist trotzdem auflösbar.
+ assertThat(art56.absaetze().get(1).text()).contains("(§ 2 Abs. 3 BJagdG)");
+
+ // Offen bleibt an dieser Norm eine Frage, die mit der Reihenfolge nichts zu tun hat und schon
+ // vorher so stand: Der eingefügte Block „Nach Nr. 4 werden die folgenden Nrn. 5 bis 7
+ // eingefügt“ landet am Ende des Absatzes statt hinter der Nr. 4, und der leere Platzhalter
+ // „7. (aufgehoben)“ der Altfassung bleibt zwischen den Nrn. 9 und 10 stehen. Beides ist hier
+ // festgehalten, damit es nicht unbemerkt bleibt — der Befehl gilt als angewandt, die Stellung
+ // seines Blocks ist aber falsch.
+ assertThat(katalog).contains("7. (aufgehoben)");
+ assertThat(katalog.indexOf("5.\t den Verboten des Art. 29 Abs. 2"))
+ .as("die eingefügten Nrn. 5 bis 7 stehen (noch) hinter der Nr. 16")
+ .isGreaterThan(katalog.indexOf("16."));
// Art. 29a Abs. 5 trägt nach der Kaskade die neue Behördenbezeichnung und den eingefügten Satz.
var art29a = anwendung.neu().norm("Art. 29a").orElseThrow();
@@ -975,8 +1003,8 @@
var ohne = Pipeline.erzeugeSynopse(alt, List.of(entwurfPdf), null, false);
var mit = Pipeline.erzeugeSynopse(alt, List.of(entwurfPdf, antragPdf), null, false);
- assertThat(mit.anzahlAngewandt()).isEqualTo(ohne.anzahlAngewandt()).isEqualTo(151);
- assertThat(mit.anzahlManuell()).isEqualTo(ohne.anzahlManuell()).isEqualTo(3);
+ assertThat(mit.anzahlAngewandt()).isEqualTo(ohne.anzahlAngewandt()).isEqualTo(154);
+ assertThat(mit.anzahlManuell()).isEqualTo(ohne.anzahlManuell()).isEqualTo(0);
// Beide Läufe zeigen eine Entwurfsfassung, nicht geltendes Recht.
assertThat(mit.html()).contains("Entwurfsfassung");
assertThat(mit.html()).contains("[Änderungsantrag Drs. 19/10365]");
@@ -1033,6 +1061,15 @@
* wird eine Fassung von 2023. Maßgeblich ist deshalb der Vergleich mit dem Entwurf — die
* Ausschussfassung muss <em>mehr</em> Befehle tragen, denn der Ausschuss hat zwei Artikel
* hinzugefügt (BGB und Betriebskostenverordnung).
+ *
+ * <p>Die Zahl stand bei 67, bevor die Anwendungsreihenfolge auf Schritte umgestellt wurde. Der
+ * hinzugekommene Befehl ist die Bereichs-Umnummerierung des Bußgeldkatalogs („Die bisherigen
+ * Nummern 19 bis 21 werden die Nummern 30 bis 32“, § 108 Absatz 1): Sie muss vor die Einfügung
+ * rücken, die ihre Ausgangsnummern neu vergibt, und dorthin trug die frühere einmalige
+ * Verschiebung nur ihr erstes Glied. Was in § 108 dabei herauskommt, sagt über die Ordnung nichts
+ * — die Norm liegt mitten im beschriebenen Stamm-Mismatch. Belegt ist die Ordnung an den
+ * Kaskaden, deren Stammfassung stimmt: BayJG Art. 29a und Art. 56 sowie WDR-Gesetz §§ 3 und 15,
+ * beide gegen die amtliche Nachfassung abgeglichen.
*/
@Test
void beschlussempfehlungLiefertDieAusschussfassung() throws Exception {
@@ -1046,7 +1083,7 @@
var ausEmpfehlung = Pipeline.erzeugeSynopse(xml, List.of(empfehlung), null, false);
var ausEntwurf = Pipeline.erzeugeSynopse(xml, List.of(entwurf), null, false);
- assertThat(ausEmpfehlung.anzahlAngewandt()).isEqualTo(67);
+ assertThat(ausEmpfehlung.anzahlAngewandt()).isEqualTo(68);
assertThat(ausEmpfehlung.anzahlAngewandt() + ausEmpfehlung.anzahlManuell())
.as("die Ausschussfassung trägt mehr Befehle als der Entwurf")
.isGreaterThan(ausEntwurf.anzahlAngewandt() + ausEntwurf.anzahlManuell());
diff --git a/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java b/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java
index c12f118..7e7da85 100644
--- a/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java
+++ b/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java
@@ -1378,4 +1378,104 @@
assertThat(ergebnis.neu().norm("§ 3").orElseThrow().titel()).isEqualTo("Zwischennorm");
assertThat(ergebnis.neu().norm("§ 4").orElseThrow().titel()).isEqualTo("Schlussvorschriften");
}
+
+ @Test
+ void zieehtEineGanzeUmnummerierungsketteVorDieEinfuegung() {
+ // Eine Einfügung besetzt die Nr. 2, die eine Kette von Umnummerierungen erst räumen muss:
+ // „Nr. 3 wird Nr. 4“ hält „Nr. 2 wird Nr. 3“ auf, und diese hält die Einfügung auf. Vorgezogen
+ // werden muss deshalb nicht nur das letzte Glied der Kette, sondern die ganze Kette — sonst
+ // träfe „Nr. 2 wird Nr. 3“ auf eine noch besetzte Nr. 3 (BayJG Art. 56 Abs. 1 Buchst. hh/ii).
+ // Solange der Verbund als Ganzes vorrückte, war das umsonst zu haben; seit die Schritte
+ // einzeln geordnet werden, hängt es an der Mitnahme der Vorgänger.
+ var einfuegung =
+ new StrukturEinfuegung(
+ stelle(new Stelle.Paragraph("1"), new Stelle.AbsatzNr("2"), new Stelle.NummerNr("1")),
+ false,
+ Ebene.NUMMER,
+ "2",
+ "2. die Zulassung von Anträgen,",
+ PROV);
+ // So liefert der Erkenner eine Bereichs-Umnummerierung: das höhere Paar zuerst.
+ var kette =
+ new Sammelbefehl(
+ List.of(umnummerierungNummer("2", "3", "4"), umnummerierungNummer("2", "2", "3")));
+
+ var ergebnis = BefehlAnwender.anwenden(gesetz(), List.of(einfuegung, kette));
+
+ assertThat(ergebnis.anzahlManuell()).isZero();
+ assertThat(absatzText(ergebnis.neu(), "§ 1", 1))
+ .isEqualTo(
+ "Die Erprobung umfasst\n"
+ + " 1. das Einlesen von Gesetzen,\n"
+ + " 2. die Zulassung von Anträgen,\n"
+ + " 3. die Anwendung von Befehlen und\n"
+ + " 4. die Ausgabe von Synopsen.");
+ }
+
+ @Test
+ void laesstDieBegleitaenderungAnIhrerDokumentstelle() {
+ // „Die bisherige Nr. 3 wird Nr. 4 und die Angabe „schriftliche “ wird gestrichen“: Die
+ // Umnummerierung muss vor den Befehl rücken, der die Nr. 3 neu besetzt — ihre Begleitänderung
+ // aber nicht. Vorgezogen träfe die Streichung noch zwei Fundstellen und bliebe mehrdeutig; an
+ // ihrem Platz trifft sie genau eine, weil der Punkt davor die andere längst getilgt hat
+ // (BayJG Art. 56 Abs. 1 Buchst. gg).
+ var neuBesetzung = umnummerierungNummer("1", "2", "3");
+ var tilgtDieErsteFundstelle =
+ new Ersetzung(
+ stelle(new Stelle.Paragraph("1"), new Stelle.AbsatzNr("1")),
+ "die schriftliche Anmeldung",
+ "die Anmeldung",
+ false,
+ false,
+ PROV);
+ var verbund =
+ new Sammelbefehl(
+ List.of(
+ umnummerierungNummer("1", "3", "4"),
+ new Streichung(stelle(new Stelle.Paragraph("1")), "schriftliche ", PROV)));
+
+ var ergebnis =
+ BefehlAnwender.anwenden(
+ kaskadenGesetz(), List.of(neuBesetzung, tilgtDieErsteFundstelle, verbund));
+
+ // Beide Teile des Verbunds greifen — die Streichung ist an ihrer Dokumentstelle eindeutig.
+ assertThat(ergebnis.anzahlManuell()).isZero();
+ var text = absatzText(ergebnis.neu(), "§ 1", 0);
+ assertThat(text).contains("1. die Anmeldung,").contains("3. die Prüfung und");
+ // Die Umnummerierung ist vorgerückt, ihre Begleitänderung nicht: Nr. 4 trägt den Text der
+ // bisherigen Nr. 3, und zwar ohne „schriftliche“.
+ assertThat(text).contains("4. die Bestätigung.").doesNotContain("schriftliche");
+ }
+
+ private static Umnummerierung umnummerierungNummer(String absatz, String alt, String neu) {
+ return new Umnummerierung(
+ stelle(new Stelle.Paragraph("1"), new Stelle.AbsatzNr(absatz), new Stelle.NummerNr(alt)),
+ stelle(new Stelle.Paragraph("1"), new Stelle.AbsatzNr(absatz), new Stelle.NummerNr(neu)),
+ PROV);
+ }
+
+ /**
+ * Ein Gesetz für die Kaskadenprobe: Das Wort „schriftliche“ steht zweimal in derselben Norm, in
+ * der ersten und in der letzten Nummer. Die Nummer 2, die die Kette frei macht, bleibt hier
+ * unbesetzt — die Einfügung, die sie im echten Fall füllt, gehört nicht zur Ordnungsfrage.
+ */
+ private static Gesetz kaskadenGesetz() {
+ return new Gesetz(
+ "TestG",
+ "Gesetz zur Erprobung",
+ "Testgesetz",
+ List.of(
+ new Norm(
+ "§ 1",
+ "Verfahren",
+ null,
+ List.of(
+ new Absatz(
+ "1",
+ "Die Erprobung umfasst\n"
+ + " 1. die schriftliche Anmeldung,\n"
+ + " 2. die Prüfung und\n"
+ + " 3. die schriftliche Bestätigung.")),
+ false)));
+ }
}