diff options
6 files changed, 257 insertions, 19 deletions
@@ -212,7 +212,7 @@ java -jar target/aendggner-0.1.0-SNAPSHOT.jar \ **`--stichtag <JJJJ-MM-TT>`** : Die an diesem Tage geltende Fassung erzeugen. Tritt das Änderungsgesetz gestaffelt in Kraft, so bleiben die an jenem Tage noch nicht geltenden Befehle - unangewandt und werden gesondert ausgewiesen (§ 8 Absatz 5). Ohne diese Angabe + unangewandt und werden gesondert ausgewiesen (§ 8 Absatz 6). Ohne diese Angabe werden alle Befehle angewandt. **`--neufassung <file>`** @@ -325,7 +325,9 @@ Streichen und Umnummerierung. 12. Verbünde aus Umnummerierung und Folgeänderung („§ 50 wird zu § 38 und wird wie folgt geändert“, „Die bisherige Nr. 7 wird Nr. 5 und das Komma wird durch das Wort „und“ ersetzt“). Eine Satzzeichen-Operation meint dabei stets die - soeben umnummerierte Einheit, weil ihr Zieltext nichts unterscheidet; + soeben umnummerierte Einheit, weil ihr Zieltext nichts unterscheidet; eine + Wortoperation dagegen löst zunächst norm-weit auf und fällt erst dann auf jene + Einheit zurück, wenn die weite Suche mehrdeutig bleibt (§ 8 Absatz 5); 13. Verb-Rahmen, deren Unterpunkte allein die Fundstelle tragen („Es werden ersetzt: … in § 35 Absatz 3 die Angabe „X“ jeweils durch die Angabe „Y“,“), sowie @@ -371,7 +373,17 @@ Verschoben wird stets nur nach vorn. angewandt, wenn jeder seiner Teile gegriffen hat; sonst nennt die Meldung den Teil und den Grund. -(5) Ein Änderungsgesetz tritt nicht notwendig auf einen Schlag in Kraft. Der +(5) Bleibt die Begleitklausel eines Verbunds bei norm-weiter Auflösung mehrdeutig, +so entscheidet die soeben umnummerierte Einheit. Der Vorrang bleibt bei der weiten +Suche, denn nicht jede Begleitklausel meint jene Einheit; findet sie gar keine +Fundstelle, so meint der Befehl etwas anderes und bleibt zu Recht liegen — nur die +Mehrdeutigkeit wird durch den engeren Skopus aufgelöst. Bleibt der Anker auch dort +mehrdeutig, so steht die Mehrdeutigkeit im Protokoll und nicht die Begründung des +zweiten Versuchs. Diese Regel gehört zur Anwendung und nicht zur Erkennung: Ob ein +Anker mehrdeutig ist, steht erst nach den vorangegangenen Punkten fest — beim Lesen +gibt es die neue Bezeichnung noch gar nicht. + +(6) Ein Änderungsgesetz tritt nicht notwendig auf einen Schlag in Kraft. Der Schlussartikel wird deshalb gelesen und jeder Befehl der besondersten Anordnung zugeordnet, die ihn erfasst („Artikel 1 Nummer 2 Buchstabe c tritt am 19. Juni 2026 in Kraft“); im Übrigen gilt die Grundregel. Mit dem Schalter `--stichtag` @@ -420,7 +432,8 @@ gemeldet. (5) Auf den Beispieldaten (§ 17 Absatz 2) werden hiernach alle Befehle der Fassungen des Bundesgesetzblattes und der aktuellen Entwürfe angewandt; für -UWG, AGG und ProdHaftG verbleibt kein Befehl zur Prüfung von Hand. Im Übrigen +UWG, AGG, ProdHaftG und — seit dieser Welle — für den Artikel 1 der GEG-Novelle +mit seinen 119 Befehlen verbleibt kein Befehl zur Prüfung von Hand. Im Übrigen gilt § 1 Absatz 4. (6) Bleibt die Aufbereitung im Einzelfall fehlerhaft, so ist nach § 6 Absatz 2 diff --git a/src/main/java/eu/mulk/aendggner/aenderung/Aenderungsbefehl.java b/src/main/java/eu/mulk/aendggner/aenderung/Aenderungsbefehl.java index 7fc5e8a..a80e09a 100644 --- a/src/main/java/eu/mulk/aendggner/aenderung/Aenderungsbefehl.java +++ b/src/main/java/eu/mulk/aendggner/aenderung/Aenderungsbefehl.java @@ -40,7 +40,11 @@ public sealed interface Aenderungsbefehl { /** „… werden die Wörter „A“ durch die Wörter „B“ ersetzt.“ */ record Ersetzung( Stelle stelle, String alt, String neu, boolean jeweils, boolean amEnde, Provenienz provenienz) - implements Aenderungsbefehl {} + implements Aenderungsbefehl { + public Ersetzung mitStelle(Stelle neueStelle) { + return new Ersetzung(neueStelle, alt, neu, jeweils, amEnde, provenienz); + } + } /** „§ X wird wie folgt gefasst: „…““ */ record Neufassung(Stelle stelle, String neuerText, Provenienz provenienz) @@ -66,7 +70,11 @@ public sealed interface Aenderungsbefehl { /** „… werden nach dem Wort „A“ die Wörter „B“ eingefügt.“ */ record WoerterEinfuegung(Stelle stelle, WortAnker anker, String woerter, Provenienz provenienz) - implements Aenderungsbefehl {} + implements Aenderungsbefehl { + public WoerterEinfuegung mitStelle(Stelle neueStelle) { + return new WoerterEinfuegung(neueStelle, anker, woerter, provenienz); + } + } /** * „Nach § 28 wird folgender § 28a eingefügt: „…““ @@ -111,7 +119,11 @@ public sealed interface Aenderungsbefehl { /** „… werden die Wörter „A“ gestrichen.“ */ record Streichung(Stelle stelle, String woerter, Provenienz provenienz) - implements Aenderungsbefehl {} + implements Aenderungsbefehl { + public Streichung mitStelle(Stelle neueStelle) { + return new Streichung(neueStelle, woerter, provenienz); + } + } /** „Der bisherige Absatz 2 wird Absatz 3.“ */ record Umnummerierung(Stelle stelle, Stelle neu, Provenienz provenienz) 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 192d244..c825a64 100644 --- a/src/main/java/eu/mulk/aendggner/aenderung/parse/BefehlErkenner.java +++ b/src/main/java/eu/mulk/aendggner/aenderung/parse/BefehlErkenner.java @@ -1757,6 +1757,11 @@ final class BefehlErkenner { // Nach einer Umnummerierung wird die Wortoperation nicht auf die neue Stelle festgenagelt: sie // löst norm-weit auf (findet ihren Zieltext im umbenannten Absatz, ohne eine womöglich schon // bestehende Zielnummer fälschlich zu treffen) — das übernehmen die Zweige weiter unten. + // Bleibt die weite Auflösung mehrdeutig, so entscheidet erst beim Anwenden die soeben + // umnummerierte Einheit (siehe BefehlAnwender.rueckfall). Dort und nicht hier gehört die + // Regel hin: Ob ein Anker mehrdeutig ist, steht erst nach den vorangegangenen Punkten fest — + // der Erkenner sieht nur das Ausgangsgesetz, in dem es die neue Bezeichnung noch gar nicht + // gibt. if (REINE_WORT_OPERATION.matcher(rechts).lookingAt() && !(linksBefehl instanceof Umnummerierung)) { var praefix0 = lokativerPraefix(links); diff --git a/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java b/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java index a46defb..56b610e 100644 --- a/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java +++ b/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java @@ -35,6 +35,7 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import java.util.regex.Pattern; +import java.util.stream.Collectors; import org.jspecify.annotations.Nullable; /** @@ -107,7 +108,29 @@ public final class BefehlAnwender { ergebnisse[index] = angewandt(schritt.teil(), "(Gesetzesüberschrift)"); continue; } - ergebnisse[index] = wendeAn(normen, gliederungen, schritt.teil()); + var ergebnis = wendeAn(normen, gliederungen, schritt.teil()); + if (ergebnis.status() == Status.MANUELL_PRUEFEN + && ergebnis.grund() == Grund.MEHRDEUTIG + && schritt.rueckfallStelle() != null) { + var enger = mitStelle(schritt.teil(), schritt.rueckfallStelle()); + // Der zweite Versuch ist gefahrlos: Jeder Zweig meldet den Fehlschlag, bevor er schreibt. + // Gelingt er nicht, so bleibt der erste Protokolleintrag stehen — die Mehrdeutigkeit ist + // der wahre Befund und darf nicht durch ein „nicht auffindbar“ der zweiten Auflösung + // verdeckt werden. + if (enger != null) { + var zweiter = wendeAn(normen, gliederungen, enger); + if (zweiter.status() == Status.ANGEWANDT) { + ergebnis = + new AngewandteAenderung( + schritt.teil(), + Status.ANGEWANDT, + zweiter.begruendung(), + zweiter.betroffeneEnbez(), + null); + } + } + } + ergebnisse[index] = ergebnis; } protokoll.addAll(verdichte(befehle, schritte, ergebnisse)); @@ -129,8 +152,17 @@ public final class BefehlAnwender { * 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}). + * + * @param rueckfallStelle die Einheit, die eine vorangehende Umnummerierung desselben Verbunds + * soeben neu bezeichnet hat; {@code null} außerhalb eines Verbunds und vor der ersten + * Umnummerierung. Sie kommt nur zum Zuge, wenn die norm-weite Auflösung mehrdeutig bleibt + * (siehe {@link #anwenden}). */ - private record Schritt(int befehlIndex, Aenderungsbefehl teil, boolean ganzerBefehl) {} + private record Schritt( + int befehlIndex, + Aenderungsbefehl teil, + boolean ganzerBefehl, + @Nullable Stelle rueckfallStelle) {} /** Faltet die Befehlsliste zur Schrittliste auf, in Dokumentreihenfolge. */ private static List<Schritt> schritte(List<Aenderungsbefehl> befehle) { @@ -140,23 +172,78 @@ public final class BefehlAnwender { if (befehl instanceof Sammelbefehl s) { falte(s, i, schritte); } else { - schritte.add(new Schritt(i, befehl, true)); + schritte.add(new Schritt(i, befehl, true, null)); } } return schritte; } private static void falte(Sammelbefehl befehl, int befehlIndex, List<Schritt> ziel) { + // Innerhalb eines Verbunds merkt sich das Auffalten die zuletzt gesehene Umnummerierung: Ihre + // neue Bezeichnung ist der Rückfall für die nachfolgenden Wortoperationen desselben Verbunds. + // Nur vorwärts und nur innerhalb dieses Verbunds — ein späterer Punkt des Dokuments hat mit + // ihr nichts zu schaffen. + Stelle zuletztUmnummeriert = null; for (var teil : befehl.teilbefehle()) { if (teil instanceof Sammelbefehl geschachtelt) { falte(geschachtelt, befehlIndex, ziel); - } else { - ziel.add(new Schritt(befehlIndex, teil, false)); + continue; + } + ziel.add(new Schritt(befehlIndex, teil, false, rueckfall(zuletztUmnummeriert, teil))); + if (teil instanceof Umnummerierung um && !um.neu().istLeer()) { + zuletztUmnummeriert = um.neu(); } } } /** + * Die Stelle, auf die eine Begleitklausel zurückfällt, wenn ihre norm-weite Auflösung mehrdeutig + * bleibt: die soeben umnummerierte Einheit. + * + * <p>Der Vorrang bleibt bei der norm-weiten Auflösung, denn nicht jede Begleitklausel meint die + * umnummerierte Einheit — „Die bisherige Nr. 11 wird Nr. 12 und die Angabe ‚schriftliche‘ wird + * gestrichen“ kann sehr wohl anderswo greifen. Erst wenn die weite Suche mehrere Fundstellen + * findet, entscheidet der engere Skopus; findet sie gar keine, so meint der Befehl etwas anderes + * und bleibt zu Recht liegen. + * + * <p>Der Rückfall greift nur bei Wortoperationen — Strukturbefehle nennen ihren Skopus ohnehin — + * und nur, wenn die Klausel keine eigene feinere Einheit nennt: Wer „in Satz 3“ sagt, hat seinen + * Skopus bereits bestimmt. + */ + private static @Nullable Stelle rueckfall( + @Nullable Stelle zuletztUmnummeriert, Aenderungsbefehl teil) { + if (zuletztUmnummeriert == null + || !(teil instanceof Ersetzung + || teil instanceof Streichung + || teil instanceof WoerterEinfuegung)) { + return null; + } + if (zuletztUmnummeriert.paragraph().isPresent()) { + return zuletztUmnummeriert; + } + // Die neue Bezeichnung tritt zum Rahmen der Klausel hinzu — aber nur, wenn dieser nicht + // selbst schon eine Einheit derselben Art nennt. „In Absatz 3 …“ neben einer Umnummerierung + // auf Absatz 8 ergäbe „Absatz 3 Absatz 8“; wo das droht, wird nicht geraten. + var vorhandeneArten = + teil.stelle().komponenten().stream().map(k -> k.getClass()).collect(Collectors.toSet()); + if (zuletztUmnummeriert.komponenten().stream() + .anyMatch(k -> vorhandeneArten.contains(k.getClass()))) { + return null; + } + return teil.stelle().plus(zuletztUmnummeriert); + } + + /** Derselbe Befehl, auf eine engere Stelle festgelegt. */ + private static @Nullable Aenderungsbefehl mitStelle(Aenderungsbefehl befehl, Stelle stelle) { + return switch (befehl) { + case Ersetzung e -> e.mitStelle(stelle); + case Streichung st -> st.mitStelle(stelle); + case WoerterEinfuegung w -> w.mitStelle(stelle); + default -> null; + }; + } + + /** * 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 diff --git a/src/test/java/eu/mulk/aendggner/EndToEndTest.java b/src/test/java/eu/mulk/aendggner/EndToEndTest.java index 7a6dcee..41f30e5 100644 --- a/src/test/java/eu/mulk/aendggner/EndToEndTest.java +++ b/src/test/java/eu/mulk/aendggner/EndToEndTest.java @@ -293,8 +293,8 @@ class EndToEndTest { .filter(a -> a.status() == BefehlAnwender.Status.MANUELL_PRUEFEN) .map(a -> a.befehl().provenienz().gliederungsPfad()) .toList(); - assertThat(manuellPfade).containsExactly("40. a) hh)"); - assertThat(anwendung.anzahlAngewandt()).isEqualTo(118); + assertThat(manuellPfade).isEmpty(); + assertThat(anwendung.anzahlAngewandt()).isEqualTo(119); // Die Kaskade des § 108 Absatz 1 geht lückenlos auf: 32 Nummern, keine doppelt, keine fehlend. var nummern = @@ -312,6 +312,13 @@ class EndToEndTest { assertThat(nummern).hasSize(32).endsWith("32"); var neu = anwendung.neu(); + // Die Begleitklausel der Nummer 18/29 hat allein dort gegriffen — der Wortlaut gleicht der + // amtlichen Nachfassung (BJNR172810020.xml, § 108 Absatz 1 Nummer 29). + var paragraph108 = neu.norm("§ 108").orElseThrow().absaetze().get(0).text(); + assertThat(paragraph108) + .contains("29. entgegen § 96 Absatz 1 oder Absatz 4 eine Bestätigung nicht") + .contains("30. entgegen § 96 Absatz 5 Satz 2 eine Abrechnung nicht"); + // Die Überschrift der Nummer 1 der Anlage 8 ist geändert — und nur sie: Der Titel der Anlage // trägt seinen eigenen Wortlaut unversehrt, den Punkt 43. a) ihm gegeben hat. var anlage8 = neu.norm("Anlage 8").orElseThrow(); @@ -1722,14 +1729,14 @@ class EndToEndTest { assumeTrue(Files.exists(xml) && Files.exists(pdf), "GEG-Beispieldaten fehlen"); var vollstaendig = Pipeline.erzeugeSynopse(Pipeline.Auftrag.von(xml, List.of(pdf))); - assertThat(vollstaendig.anzahlAngewandt()).isEqualTo(118); - assertThat(vollstaendig.anzahlManuell()).isEqualTo(1); + assertThat(vollstaendig.anzahlAngewandt()).isEqualTo(119); + assertThat(vollstaendig.anzahlManuell()).isZero(); var anfang2024 = Pipeline.erzeugeSynopse( Pipeline.Auftrag.von(xml, List.of(pdf)).mitStichtag(LocalDate.of(2024, 1, 1))); - assertThat(anfang2024.anzahlAngewandt()).isEqualTo(117); - assertThat(anfang2024.anzahlManuell()).isEqualTo(1); + assertThat(anfang2024.anzahlAngewandt()).isEqualTo(118); + assertThat(anfang2024.anzahlManuell()).isZero(); assertThat(anfang2024.html()) .contains("Am Stichtag noch nicht in Kraft") .contains("Tritt erst am 1. Oktober 2024 in Kraft"); @@ -1737,7 +1744,7 @@ class EndToEndTest { var oktober2024 = Pipeline.erzeugeSynopse( Pipeline.Auftrag.von(xml, List.of(pdf)).mitStichtag(LocalDate.of(2024, 10, 1))); - assertThat(oktober2024.anzahlAngewandt()).isEqualTo(118); + assertThat(oktober2024.anzahlAngewandt()).isEqualTo(119); } /** diff --git a/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java b/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java index 0e186ad..a1f5cff 100644 --- a/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java +++ b/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java @@ -4,6 +4,7 @@ package eu.mulk.aendggner.anwendung; import static org.assertj.core.api.Assertions.assertThat; +import eu.mulk.aendggner.aenderung.Aenderungsbefehl; import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Anfuegung; import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Aufhebung; import eu.mulk.aendggner.aenderung.Aenderungsbefehl.BereichsUmnummerierung; @@ -1777,4 +1778,117 @@ class BefehlAnwenderTest { assertThat(ergebnis.protokoll().get(0).grund()).isEqualTo(Grund.NICHT_UNTERSTUETZT); assertThat(ergebnis.neu().norm("Anlage 8").orElseThrow().titel()).isEqualTo("Anforderungen"); } + + private static Gesetz bussgeldkatalog() { + return new Gesetz( + "TestG", + "Testgesetz", + "TestG", + List.of( + new Norm( + "§ 108", + "Bußgeldvorschriften", + null, + List.of( + new Absatz( + "1", + "17. entgegen § 90 Absatz 1 etwas tut,\n" + + "18. entgegen § 96 Absatz 1 etwas unterlässt,\n" + + "19. entgegen § 97 Absatz 1 etwas anderes tut,")), + false))); + } + + private static Sammelbefehl verbund(Aenderungsbefehl... teile) { + return new Sammelbefehl(List.of(teile)); + } + + /** + * „Die bisherige Nummer 18 wird Nummer 29 und nach der Angabe ‚Absatz 1‘ werden die Wörter ‚oder + * Absatz 4‘ eingefügt.“ — Der Anker steht in jeder Nummer; norm-weit ist er mehrdeutig. Erst dann + * entscheidet die soeben umnummerierte Einheit. + */ + @Test + void mehrdeutigeBegleitklauselFaelltAufDieUmnummerierteEinheitZurueck() { + var rahmen = stelle(new Stelle.Paragraph("108"), new Stelle.AbsatzNr("1")); + var befehl = + verbund( + new Umnummerierung( + rahmen.plus(stelle(new Stelle.NummerNr("18"))), + stelle(new Stelle.NummerNr("29")), + PROV), + new WoerterEinfuegung( + rahmen, new WortAnker.NachWoertern("Absatz 1"), "oder Absatz 4", PROV)); + + var ergebnis = BefehlAnwender.anwenden(bussgeldkatalog(), List.of(befehl)); + assertThat(ergebnis.protokoll()).allMatch(a -> a.status() == Status.ANGEWANDT); + + var text = ergebnis.neu().norm("§ 108").orElseThrow().absaetze().get(0).text(); + // Allein die umnummerierte Nummer hat die Wörter bekommen. + assertThat(text).contains("29. entgegen § 96 Absatz 1 oder Absatz 4 etwas unterlässt,"); + assertThat(text).contains("17. entgegen § 90 Absatz 1 etwas tut,"); + assertThat(text).contains("19. entgegen § 97 Absatz 1 etwas anderes tut,"); + } + + /** + * Der Rückfall greift nur bei Mehrdeutigkeit, nie bei fehlendem Zieltext: Wer seinen Anker + * nirgends findet, meint etwas anderes — ihn in die umnummerierte Einheit zu zwingen wäre eine + * Falschanwendung, keine Verfeinerung. + */ + @Test + void fehlenderZieltextFaelltNichtZurueck() { + var rahmen = stelle(new Stelle.Paragraph("108"), new Stelle.AbsatzNr("1")); + var befehl = + verbund( + new Umnummerierung( + rahmen.plus(stelle(new Stelle.NummerNr("18"))), + stelle(new Stelle.NummerNr("29")), + PROV), + new WoerterEinfuegung( + rahmen, new WortAnker.NachWoertern("Paragraph 1"), "oder Absatz 4", PROV)); + + var ergebnis = BefehlAnwender.anwenden(bussgeldkatalog(), List.of(befehl)); + assertThat(ergebnis.protokoll().get(0).status()).isEqualTo(Status.MANUELL_PRUEFEN); + assertThat(ergebnis.neu().norm("§ 108").orElseThrow().absaetze().get(0).text()) + .doesNotContain("oder Absatz 4"); + } + + /** + * Bleibt der Anker auch in der umnummerierten Einheit mehrdeutig, so bleibt der Befehl liegen — + * und mit der Begründung des <em>ersten</em> Versuchs: Die Mehrdeutigkeit ist der wahre Befund. + */ + @Test + void mehrdeutigBleibtMehrdeutig() { + var gesetz = + new Gesetz( + "TestG", + "Testgesetz", + "TestG", + List.of( + new Norm( + "§ 108", + null, + null, + List.of( + new Absatz( + "1", + "17. entgegen § 90 Absatz 1 etwas tut,\n" + + "18. entgegen § 96 Absatz 1 und § 97 Absatz 1 etwas unterlässt,")), + false))); + var rahmen = stelle(new Stelle.Paragraph("108"), new Stelle.AbsatzNr("1")); + var befehl = + verbund( + new Umnummerierung( + rahmen.plus(stelle(new Stelle.NummerNr("18"))), + stelle(new Stelle.NummerNr("29")), + PROV), + new WoerterEinfuegung( + rahmen, new WortAnker.NachWoertern("Absatz 1"), "oder Absatz 4", PROV)); + + var ergebnis = BefehlAnwender.anwenden(gesetz, List.of(befehl)); + assertThat(ergebnis.protokoll().get(0).status()).isEqualTo(Status.MANUELL_PRUEFEN); + assertThat(ergebnis.protokoll().get(0).grund()).isEqualTo(Grund.MEHRDEUTIG); + assertThat(ergebnis.protokoll().get(0).begruendung()).contains("mehrdeutig"); + assertThat(ergebnis.neu().norm("§ 108").orElseThrow().absaetze().get(0).text()) + .doesNotContain("oder Absatz 4"); + } } |
