From 74a84c56047f3463f9086fcffad5c62f402e047e Mon Sep 17 00:00:00 2001 From: Matthias Andreas Benkard Date: Sun, 16 Aug 2026 06:08:42 +0200 Subject: =?UTF-8?q?Die=20Umnummerierungs-Kaskade=20war=20keine=20Grenze,?= =?UTF-8?q?=20sondern=20drei=20M=C3=A4ngel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Change-Id: Id61babe60226d27b5c05ff971927d61713b01781 --- src/test/java/eu/mulk/aendggner/EndToEndTest.java | 69 ++++++++++---- .../aendggner/anwendung/BefehlAnwenderTest.java | 100 +++++++++++++++++++++ 2 files changed, 153 insertions(+), 16 deletions(-) (limited to 'src/test/java') 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 @@ class EndToEndTest { 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 @@ class EndToEndTest { .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 @@ class EndToEndTest { 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 @@ class EndToEndTest { * wird eine Fassung von 2023. Maßgeblich ist deshalb der Vergleich mit dem Entwurf — die * Ausschussfassung muss mehr Befehle tragen, denn der Ausschuss hat zwei Artikel * hinzugefügt (BGB und Betriebskostenverordnung). + * + *

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 @@ class EndToEndTest { 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 @@ class BefehlAnwenderTest { 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))); + } } -- cgit v1.2.1