aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--FASSUNGEN.txt110
-rw-r--r--README.md38
-rw-r--r--src/main/java/eu/mulk/aendggner/Pipeline.java4
-rw-r--r--src/main/java/eu/mulk/aendggner/aenderung/parse/InkrafttretensLeser.java18
-rw-r--r--src/main/java/eu/mulk/aendggner/aenderung/parse/PunktPfad.java61
-rw-r--r--src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java148
-rw-r--r--src/main/java/eu/mulk/aendggner/anwendung/InhaltsuebersichtAnwender.java93
-rw-r--r--src/main/java/eu/mulk/aendggner/anwendung/InhaltsuebersichtsProbe.java136
-rw-r--r--src/test/java/eu/mulk/aendggner/EndToEndTest.java20
-rw-r--r--src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java76
-rw-r--r--src/test/java/eu/mulk/aendggner/anwendung/InhaltsuebersichtsProbeTest.java101
11 files changed, 722 insertions, 83 deletions
diff --git a/FASSUNGEN.txt b/FASSUNGEN.txt
index db80d88..f7a760e 100644
--- a/FASSUNGEN.txt
+++ b/FASSUNGEN.txt
@@ -21,6 +21,116 @@ Vorbemerkung zur Führung dieses Verzeichnisses
════════════════════════════════════════════════════════════════════════════════
+ Fassung vom 26. August 2026 (zweite Fassung des Tages),
+ zuletzt geändert durch die am 26. August 2026 vorgenommenen Änderungen
+════════════════════════════════════════════════════════════════════════════════
+
+Der verweisende Befehl („Die Inhaltsübersicht wird entsprechend der vorstehenden
+Nummer 8 Buchst. a geändert“) wurde bislang gelesen, aber nicht ausgeführt; das
+Handbuch wies ihn in § 7 Absatz 2 Nummer 17 als Grenze aus. Die Grenze bestand
+nicht in der Sache: Zu übertragen ist nicht der Wortlaut des verwiesenen Punktes,
+sondern sein Ergebnis. Jener ändert die Überschrift eines Paragraphen; die Angabe
+der Inhaltsübersicht ist alsdann auf den Titel zu setzen, den der Paragraph danach
+trägt.
+
+Hinzu tritt eine Prüfung, die es bisher nicht gab. Ein Änderungsgesetz muss die
+Angaben der Inhaltsübersicht eigens mitändern; unterbleibt das oder greifen die
+Befehle nicht, so bleibt die Übersicht hinter dem Text zurück, ohne dass ein
+einziger Befehl liegenbliebe. Die neue Probe hat auf Anhieb drei Mängel des
+Erzeugnisses aufgedeckt, die keine Zahl des Protokolls angezeigt hatte.
+
+
+Artikel 1
+Ausführung des verweisenden Befehls
+
+(1) Die Lesart der Gliederungspunkte, mit denen ein Änderungsgesetz auf sich
+ selbst verweist („Nummer 11 Buchstabe a Doppelbuchstabe bb“, abgekürzt „Nr. 8
+ Buchst. a“), wird an einer Stelle zusammengefasst (PunktPfad). Der Leser des
+ Inkrafttretens (InkrafttretensLeser) bezieht sie fortan von dort.
+
+(2) Der Anwender (BefehlAnwender) führt den verweisenden Befehl in der Schleife
+ aus, die als einzige die Befehlsliste kennt. Er sucht den verwiesenen Punkt im
+ selben Artikel — auch unterhalb seiner, wenn der Verweis eine Nummer und nicht
+ einen Buchstaben nennt —, entnimmt ihm den betroffenen Paragraphen und setzt
+ dessen Angabe auf die jetzige Überschrift (InhaltsuebersichtAnwender.
+ fuehreTitelNach).
+
+(3) Was der Verweis nicht trägt, bleibt liegen und wird benannt: der Punkt, der
+ sich nicht findet, und der Punkt, der etwas anderes als eine Überschrift
+ ändert. Die Inhaltsübersicht führt allein Bezeichnung und Überschrift; was im
+ Absatz eines Paragraphen geschieht, hat in ihr kein Gegenstück.
+
+
+Artikel 2
+Probe auf die Inhaltsübersicht
+
+(1) Nach vollzogener Anwendung wird die Inhaltsübersicht gegen den Normbestand
+ gehalten (InhaltsuebersichtsProbe). Gerügt werden die fehlende Angabe zu einer
+ eingefügten oder geänderten Norm, das Auseinandergehen von Angabe und
+ Überschrift sowie die verbliebene Angabe zu einer beseitigten Norm.
+
+(2) Geprüft wird allein der Unterschied, den dieser Lauf bewirkt hat. Was die
+ Quelle von sich aus ungenau führt, bleibt außer Betracht; es wäre ein Befund
+ über die Quelle und nicht über den Lauf. Ebenso bleiben die Anlagen außer
+ Betracht, denn die Inhaltsübersicht führt sie nicht.
+
+(3) Geheilt wird nichts. Die Rüge nennt überdies den Befund und nicht dessen
+ Ursache: Die amtliche Fassung des Gebäudeenergiegesetzes selbst schreibt in
+ der Überschrift des § 71k „Gas“ und in der Angabe dazu „Erdgas“.
+
+
+Artikel 3
+Berichtigung dreier Mängel, welche die Probe aufgedeckt hat
+
+(1) Die Zerlegung eines Zitats in Angabe-Zeilen (InhaltsuebersichtAnwender)
+ trennte nur an §-Angaben. Ein Zitat, das mit einer Gliederungsmarke beginnt
+ und darauf zwanzig Paragraphen aufführt — so das Gebäudeenergiegesetz-Heft von
+ 2023 mit „Unterabschnitt 4 … § 71 … § 71p …“ —, blieb deshalb eine einzige
+ Zeile, und jede spätere Angabe zu einem dieser Paragraphen wäre unauffindbar
+ gewesen. Die Zerlegung erfolgt fortan an allen Zeilenanfängen einer Übersicht;
+ sie ist zugleich mit derjenigen der Gesamt-Neufassung zusammengeführt, die
+ dasselbe längst richtig tat.
+
+(2) Die Zerlegung eines eingefügten Paragraphen-Blocks (BefehlAnwender) trennte an
+ jedem „§ N“, dem ein großgeschriebenes Wort folgte. Ein Querverweis mitten im
+ Satz sah damit aus wie eine Überschrift: „… mit Systemen für die
+ Gebäudeautomatisierung nach § 71a Projektunterlagen in überprüfbarer Form
+ vorzulegen.“ erzeugte einen Paragraphen „§ 71a“ mit einem halben Satz als
+ Überschrift. Ein Normkopf muss fortan eine Zeile oder wenigstens einen Satz
+ eröffnen; der Wortbestand unterscheidet beide nicht, die Stellung tut es.
+
+(3) Die Überschrift einer zitierten Norm endete bislang an der ersten
+ großgeschrieben beginnenden Zeile. Ein am Spaltenrand umbrochener Titel verlor
+ dadurch seinen zweiten Teil („Verordnungsermächtigung zu dem Einsatz von
+ Kältemitteln in elektrischen Wärmepumpen und“ / „Wärmepumpen-Hybridheizungen“).
+ Endet eine Zeile auf ein Binde- oder Verhältniswort oder auf einen Trennstrich,
+ so ist sie fortan nicht zu Ende.
+
+
+Artikel 4
+Änderung des Handbuchs
+
+Das Handbuch (README.md) wird wie folgt geändert:
+
+1. Nach § 6b wird der § 6c „Probe auf die Inhaltsübersicht“ eingefügt; die
+ Inhaltsübersicht des Handbuchs wird entsprechend ergänzt.
+
+2. § 7 Absatz 2 Nummer 17 wird neu gefasst; die dort ausgewiesene Grenze entfällt.
+
+
+Schlussbestimmung
+
+Die vorstehenden Änderungen sind durch die Prüfung sämtlicher Testfälle
+(vierhundertsechs an der Zahl, zuvor dreihundertsiebenundneunzig) sowie durch die
+vollständige Erstellung (mvnw verify) bestätigt worden. Neu hinzu getreten sind
+drei Prüfungen des verweisenden Befehls (BefehlAnwenderTest) und sechs der Probe
+(InhaltsuebersichtsProbeTest). Das Heft des Gebäudeenergiegesetzes von 2023 wird
+weiterhin vollständig angewandt (119 von 119); die Probe rügt dort nur noch, was
+die amtliche Fassung selbst verschieden führt (§ 71k, § 71l). Die Auszeichnung
+nach REUSE ist vollständig (194 von 194 Dateien).
+
+
+════════════════════════════════════════════════════════════════════════════════
Fassung vom 26. August 2026,
zuletzt geändert durch die am 26. August 2026 vorgenommenen Änderungen
════════════════════════════════════════════════════════════════════════════════
diff --git a/README.md b/README.md
index 0b3dbe2..6eaf452 100644
--- a/README.md
+++ b/README.md
@@ -41,6 +41,7 @@ durchgerechneten Beispiele sind der kürzeste Weg zum ersten Ergebnis
- [§ 6 Betrieb der Befehlszeilenfassung](#-6-betrieb-der-befehlszeilenfassung)
- [§ 6a Die fortgeschriebene Fassung; die Kette](#-6a-die-fortgeschriebene-fassung-die-kette)
- [§ 6b Abgleich mit der amtlichen Nachfassung](#-6b-abgleich-mit-der-amtlichen-nachfassung)
+- [§ 6c Probe auf die Inhaltsübersicht](#-6c-probe-auf-die-inhaltsübersicht)
- [§ 7 Erkannte Änderungsbefehle](#-7-erkannte-änderungsbefehle)
- [§ 8 Reihenfolge der Anwendung](#-8-reihenfolge-der-anwendung)
- [§ 9 Aufbereitung der Druckwerke](#-9-aufbereitung-der-druckwerke)
@@ -332,6 +333,28 @@ Damit messen Werkzeug und Prüfung an einem Maßstab, und die Erschließung eine
weiteren Landes braucht keinen eigenen Prüfcode mehr: Stammfassung, Heft, Nachfassung
— das Erzeugnis sagt selbst, wo es danebenliegt.
+## § 6c Probe auf die Inhaltsübersicht
+
+(1) Ein Änderungsgesetz, das Paragraphen einfügt, aufhebt, umnummeriert oder ihre
+Überschriften neu fasst, muss die Angaben der Inhaltsübersicht eigens mitändern; das
+Handbuch der Rechtsförmlichkeit verlangt dafür eigene Befehle (§ 7 Absatz 2 Nummer 7).
+Bleiben sie aus oder greifen sie nicht, so bleibt die Übersicht hinter dem Text
+zurück — **ohne dass ein einziger Befehl liegenbliebe**. Das zeigt keine Zahl des
+Protokolls an.
+
+(2) Nach vollzogener Anwendung wird die Inhaltsübersicht deshalb gegen den Normbestand
+gehalten und jede Abweichung gerügt. Geprüft wird allein der Unterschied: die Normen,
+deren Bezeichnung oder Überschrift *dieser Lauf* verändert hat. Was die Quelle von sich
+aus ungenau führt, geht die Probe nichts an — das wäre ein Befund über die Quelle und
+nicht über den Lauf.
+
+(3) Geheilt wird nichts. Die Inhaltsübersicht ist eine Norm wie jede andere; sie ohne
+Befehl fortzuschreiben hieße, Recht zu erfinden.
+
+(4) Die Rüge nennt den Befund und nicht dessen Ursache. Es kommt nämlich vor, dass die
+amtliche Fassung selbst beides verschieden führt: Das Gebäudeenergiegesetz schreibt in
+der Überschrift des § 71k „Gas“ und in der Angabe dazu „Erdgas“.
+
## § 7 Erkannte Änderungsbefehle
(1) Erkannt werden die gebräuchlichsten Änderungsbefehle des Handbuchs der
@@ -378,12 +401,15 @@ Streichen und Umnummerierung.
16. die Neufassung der Gesetzesüberschrift sowie
17. die Verweisung auf einen anderen Punkt desselben Artikels („Die
Inhaltsübersicht wird entsprechend der vorstehenden Nummer 8 Buchst. a
- geändert“). Sie wird gelesen, aber nicht ausgeführt: Die sinngemäße
- Übertragung setzte einen Rückgriff auf die Befehlsliste und eine Umdeutung
- des Ziels voraus, die das Erzeugnis nicht leistet. Gelesen zu werden ist
- gleichwohl mehr als gar nichts — die Rüge sagt alsdann, dass hier das
- Erzeugnis die Grenze zieht, und nicht, dass die Vorlage unverständlich sei
- (§ 1 Absatz 5).
+ geändert“). Übertragen wird nicht der Wortlaut des verwiesenen Punktes,
+ sondern sein **Ergebnis**: Jener ändert die Überschrift eines Paragraphen,
+ und die Angabe der Inhaltsübersicht wird auf den Titel gesetzt, den der
+ Paragraph danach trägt. Das trifft, was „entsprechend“ meint, und erspart es,
+ jede Befehlsform ein zweites Mal auf dem Zeilenmodell der Übersicht
+ nachzubilden. Zielt der verwiesene Punkt auf etwas anderes als eine
+ Überschrift, so bleibt der Befehl liegen, und die Rüge sagt es: Die Übersicht
+ führt allein Bezeichnung und Überschrift, und was im Absatz eines Paragraphen
+ geschieht, hat in ihr kein Gegenstück.
(3) Landesrechtliche Befehlsformen bestimmt ergänzend § 13.
diff --git a/src/main/java/eu/mulk/aendggner/Pipeline.java b/src/main/java/eu/mulk/aendggner/Pipeline.java
index fee0ce9..0fdfa91 100644
--- a/src/main/java/eu/mulk/aendggner/Pipeline.java
+++ b/src/main/java/eu/mulk/aendggner/Pipeline.java
@@ -15,6 +15,7 @@ import eu.mulk.aendggner.aenderung.parse.SuperskriptModus;
import eu.mulk.aendggner.aenderung.parse.TextBereiniger;
import eu.mulk.aendggner.anwendung.BefehlAnwender;
import eu.mulk.aendggner.anwendung.Grund;
+import eu.mulk.aendggner.anwendung.InhaltsuebersichtsProbe;
import eu.mulk.aendggner.anwendung.Nachfassungsabgleich;
import eu.mulk.aendggner.gesetz.Fortschreibung;
import eu.mulk.aendggner.gesetz.Gesetz;
@@ -215,6 +216,9 @@ public final class Pipeline {
if (stichtag == null && inkrafttreten != null && inkrafttreten.gestaffelt()) {
warnungen.add(staffelungsWarnung(inkrafttreten));
}
+ // Erst nach allen Heften: Ob die Inhaltsübersicht dem Text folgt, steht am Ende fest und nicht
+ // nach jedem einzelnen Schritt.
+ warnungen.addAll(InhaltsuebersichtsProbe.pruefe(altesGesetz, gesetz));
var gesamtErgebnis = new BefehlAnwender.AnwendungsErgebnis(gesetz, protokoll);
var synopse =
SynopseBuilder.baue(
diff --git a/src/main/java/eu/mulk/aendggner/aenderung/parse/InkrafttretensLeser.java b/src/main/java/eu/mulk/aendggner/aenderung/parse/InkrafttretensLeser.java
index ee62205..966917b 100644
--- a/src/main/java/eu/mulk/aendggner/aenderung/parse/InkrafttretensLeser.java
+++ b/src/main/java/eu/mulk/aendggner/aenderung/parse/InkrafttretensLeser.java
@@ -39,11 +39,7 @@ public final class InkrafttretensLeser {
* deren Schlussvorschrift verweist entsprechend („§ 3 Nr. 2“).
*/
private static final Pattern PUNKTBEZUG =
- Pattern.compile(
- "(?:Artikel|Art\\.|§)\\s*(\\d+[a-z]?)"
- + "(?:\\s+(?:Nummer|Nr\\.)\\s+(\\d+[a-z]?))?"
- + "(?:\\s+Buchstabe\\s+(\\p{Ll})\\b)?"
- + "(?:\\s+Doppelbuchstabe\\s+(\\p{Ll}{2})\\b)?");
+ Pattern.compile("(?:Artikel|Art\\.|§)\\s*(\\d+[a-z]?)" + PunktPfad.MUSTER);
private static final Pattern VERB = Pattern.compile("\\b(?:tritt|treten)\\b");
@@ -164,17 +160,7 @@ public final class InkrafttretensLeser {
var bezuege = new ArrayList<Punktbezug>();
var treffer = PUNKTBEZUG.matcher(satz);
while (treffer.find()) {
- var pfad = new StringBuilder();
- if (treffer.group(2) != null) {
- pfad.append(treffer.group(2)).append('.');
- if (treffer.group(3) != null) {
- pfad.append(' ').append(treffer.group(3)).append(')');
- if (treffer.group(4) != null) {
- pfad.append(' ').append(treffer.group(4)).append(')');
- }
- }
- }
- bezuege.add(new Punktbezug(treffer.group(1), pfad.toString()));
+ bezuege.add(new Punktbezug(treffer.group(1), PunktPfad.baue(treffer, 1)));
}
return List.copyOf(bezuege);
}
diff --git a/src/main/java/eu/mulk/aendggner/aenderung/parse/PunktPfad.java b/src/main/java/eu/mulk/aendggner/aenderung/parse/PunktPfad.java
new file mode 100644
index 0000000..17792e2
--- /dev/null
+++ b/src/main/java/eu/mulk/aendggner/aenderung/parse/PunktPfad.java
@@ -0,0 +1,61 @@
+// SPDX-FileCopyrightText: 2026 Matthias Andreas Benkard <code@mail.matthias.benkard.de>
+// SPDX-License-Identifier: AGPL-3.0-or-later
+package eu.mulk.aendggner.aenderung.parse;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Die Gliederungspunkte, mit denen ein Änderungsgesetz auf sich selbst verweist: „Nummer 11
+ * Buchstabe a Doppelbuchstabe bb“, abgekürzt auch „Nr. 8 Buchst. a“.
+ *
+ * <p>Zwei Stellen brauchen dieselbe Lesart, und beide meinen dasselbe Gebilde: der Schlussartikel,
+ * der das Inkrafttreten eines einzelnen Punktes anordnet ({@link InkrafttretensLeser}), und der
+ * verweisende Befehl, der einen anderen Punkt desselben Artikels sinngemäß übernimmt ({@code
+ * Aenderungsbefehl.VerweisenderBefehl}). Geschrieben wird der Pfad so, wie {@link
+ * eu.mulk.aendggner.aenderung.Provenienz#gliederungsPfad()} ihn führt — „8. a) bb)“ —, denn an ihm
+ * wird verglichen.
+ */
+public final class PunktPfad {
+
+ /**
+ * Nummer, Buchstabe und Doppelbuchstabe als Gruppen 1 bis 3. Ohne Anker; zum Einbetten gedacht,
+ * weshalb die Zählung der Gruppen beim Einbetten um den vorangehenden Teil zu versetzen ist.
+ */
+ public static final String MUSTER =
+ "(?:\\s*(?:Nummer|Nr\\.)\\s+(\\d+[a-z]?))?"
+ + "(?:\\s*(?:Buchstabe|Buchst\\.)\\s+(\\p{Ll})\\b)?"
+ + "(?:\\s*(?:Doppelbuchstabe|Doppelbuchst\\.)\\s+(\\p{Ll}{2})\\b)?";
+
+ private static final Pattern GANZ = Pattern.compile("^" + MUSTER + "\\s*$");
+
+ private PunktPfad() {}
+
+ /**
+ * Der Pfad in der Schreibweise der Provenienz; leer, wenn der Text keinen Punkt nennt. Leer heißt
+ * dabei: der ganze Artikel — nicht etwa „nichts gefunden“; wer das unterscheiden muss, prüfe den
+ * Text zuvor.
+ */
+ public static String aus(String text) {
+ var m = GANZ.matcher(text.strip());
+ return m.matches() ? baue(m, 0) : "";
+ }
+
+ /**
+ * Baut den Pfad aus einem Treffer, dessen Gruppen {@code versatz + 1} bis {@code versatz + 3} die
+ * Nummer, den Buchstaben und den Doppelbuchstaben tragen.
+ */
+ public static String baue(Matcher treffer, int versatz) {
+ var pfad = new StringBuilder();
+ if (treffer.group(versatz + 1) != null) {
+ pfad.append(treffer.group(versatz + 1)).append('.');
+ if (treffer.group(versatz + 2) != null) {
+ pfad.append(' ').append(treffer.group(versatz + 2)).append(')');
+ if (treffer.group(versatz + 3) != null) {
+ pfad.append(' ').append(treffer.group(versatz + 3)).append(')');
+ }
+ }
+ }
+ return pfad.toString();
+ }
+}
diff --git a/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java b/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java
index 8d9b7e7..2dfb7db 100644
--- a/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java
+++ b/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java
@@ -25,6 +25,7 @@ import eu.mulk.aendggner.aenderung.Aenderungsbefehl.WortlautVoranstellung;
import eu.mulk.aendggner.aenderung.Aenderungsbefehl.WortlautZuAbsatz;
import eu.mulk.aendggner.aenderung.Aenderungsbefehl.WortlautZuSatz;
import eu.mulk.aendggner.aenderung.Stelle;
+import eu.mulk.aendggner.aenderung.parse.PunktPfad;
import eu.mulk.aendggner.gesetz.Absatz;
import eu.mulk.aendggner.gesetz.Gesetz;
import eu.mulk.aendggner.gesetz.Gliederung;
@@ -109,6 +110,14 @@ public final class BefehlAnwender {
ergebnisse[index] = angewandt(schritt.teil(), "(Gesetzesüberschrift)");
continue;
}
+ // Der verweisende Befehl braucht die Befehlsliste — er nimmt einen anderen Punkt desselben
+ // Artikels in Bezug —, und die hat nur diese Schleife. Er steht deshalb hier und nicht in der
+ // Weiche. Vorausgesetzt ist, dass der verwiesene Punkt bereits vollzogen ist; das trägt die
+ // Schritt-Ordnung, denn ein Verweis auf einen „vorstehenden“ Punkt steht hinter ihm.
+ if (schritt.ganzerBefehl() && schritt.teil() instanceof VerweisenderBefehl v) {
+ ergebnisse[index] = fuehreVerweisungAus(normen, befehle, v);
+ continue;
+ }
var ergebnis = wendeAn(normen, gliederungen, schritt.teil());
if (ergebnis.status() == Status.MANUELL_PRUEFEN
&& ergebnis.grund() == Grund.MEHRDEUTIG
@@ -658,6 +667,103 @@ public final class BefehlAnwender {
};
}
+ /**
+ * Führt den verweisenden Befehl aus: „Die Inhaltsübersicht wird entsprechend der vorstehenden
+ * Nummer 8 Buchst. a geändert.“
+ *
+ * <p>Ausgeführt wird nicht der Wortlaut des verwiesenen Punktes, sondern sein Ergebnis. Jener
+ * ändert die Überschrift eines Paragraphen; die Angabe der Inhaltsübersicht wird alsdann auf den
+ * Titel gesetzt, den der Paragraph nunmehr trägt. Das ist genau, was „entsprechend“ meint, und es
+ * erspart, jede Befehlsform ein zweites Mal auf dem Zeilenmodell nachzubilden.
+ *
+ * <p>Was der Verweis nicht trägt, bleibt liegen und wird benannt: ein Punkt, der sich nicht
+ * findet, und ein Punkt, der etwas anderes als eine Überschrift ändert. Für letzteren wäre die
+ * Übertragung zu erraten — die Inhaltsübersicht führt allein Bezeichnung und Überschrift, und was
+ * im Absatz eines Paragraphen geschieht, hat in ihr kein Gegenstück.
+ */
+ private static AngewandteAenderung fuehreVerweisungAus(
+ List<Norm> normen, List<Aenderungsbefehl> befehle, VerweisenderBefehl verweisung) {
+ var pfad = PunktPfad.aus(verweisung.verweis());
+ if (pfad.isEmpty()) {
+ return manuell(
+ verweisung,
+ Grund.NICHT_UNTERSTUETZT,
+ "Der Verweis auf „"
+ + verweisung.verweis()
+ + "“ nennt keinen Gliederungspunkt, dem sich ein Befehl zuordnen ließe.");
+ }
+ var verwiesene = new ArrayList<Aenderungsbefehl>();
+ for (var befehl : befehle) {
+ if (trifftPfad(befehl, verweisung, pfad)) {
+ if (befehl instanceof Sammelbefehl s) {
+ verwiesene.addAll(s.teilbefehle());
+ } else {
+ verwiesene.add(befehl);
+ }
+ }
+ }
+ if (verwiesene.isEmpty()) {
+ return manuell(
+ verweisung,
+ Grund.STELLE_NICHT_AUFLOESBAR,
+ "Der verwiesene Punkt „"
+ + verweisung.verweis()
+ + "“ findet sich nicht im selben Artikel.");
+ }
+ var betroffen = new LinkedHashSet<String>();
+ for (var befehl : verwiesene) {
+ if (!befehl.stelle().betrifftUeberschrift()) {
+ return manuell(
+ verweisung,
+ Grund.NICHT_UNTERSTUETZT,
+ "Der verwiesene Punkt „"
+ + verweisung.verweis()
+ + "“ ändert keine Überschrift; nur deren Änderung hat in der Inhaltsübersicht ein"
+ + " Gegenstück.");
+ }
+ var paragraph = befehl.stelle().paragraph().orElse(null);
+ if (paragraph == null) {
+ return manuell(
+ verweisung,
+ Grund.STELLE_NICHT_AUFLOESBAR,
+ "Der verwiesene Punkt „" + verweisung.verweis() + "“ nennt keinen Paragraphen.");
+ }
+ int index = StellenAufloeser.normIndex(gesetzAus(normen), paragraph.enbez());
+ if (index < 0) {
+ return manuell(
+ verweisung,
+ Grund.BESTAND_WIDERSPRICHT,
+ "Das Gesetz führt keinen " + paragraph.enbez() + ".");
+ }
+ var titel = normen.get(index).titel();
+ if (titel == null || titel.isBlank()) {
+ return manuell(
+ verweisung,
+ Grund.BESTAND_WIDERSPRICHT,
+ paragraph.enbez() + " trägt keine Überschrift, die nachzuführen wäre.");
+ }
+ var ergebnis =
+ InhaltsuebersichtAnwender.fuehreTitelNach(normen, verweisung, paragraph, titel);
+ if (ergebnis.status() != Status.ANGEWANDT) {
+ return ergebnis;
+ }
+ betroffen.add(paragraph.enbez());
+ }
+ return new AngewandteAenderung(
+ verweisung, Status.ANGEWANDT, "", new LinkedHashSet<>(List.of("Inhaltsübersicht")), null);
+ }
+
+ /** Ob der Befehl an dem Punkt steht, auf den die Verweisung zielt (oder unterhalb seiner). */
+ private static boolean trifftPfad(
+ Aenderungsbefehl befehl, VerweisenderBefehl verweisung, String pfad) {
+ var herkunft = befehl.provenienz();
+ if (!herkunft.artikel().equals(verweisung.provenienz().artikel())) {
+ return false;
+ }
+ var eigener = herkunft.gliederungsPfad();
+ return eigener.equals(pfad) || eigener.startsWith(pfad + " ");
+ }
+
private static boolean istNurUeberschrift(Stelle stelle) {
return stelle.komponenten().size() == 1
&& stelle.komponenten().get(0) instanceof Stelle.Ueberschrift;
@@ -668,19 +774,17 @@ public final class BefehlAnwender {
if (befehl instanceof UnbekannterBefehl) {
return manuell(befehl, Grund.NICHT_ERKANNT, "Befehl nicht erkannt.");
}
- // Gelesen, aber bewusst nicht ausgeführt. Die Weiche steht vor der Inhaltsübersichts-Weiche,
- // damit die Rüge den Verweis nennt und nicht bloß die allgemeine Grenze jener Norm: Die
- // sinngemäße Übertragung eines anderen Punktes setzte einen Rückgriff auf die Befehlsliste und
- // eine Umdeutung des Ziels voraus („Überschrift des § 13“ → Titelspalte der Übersichtszeile),
- // die dieser Anwender nicht leistet. Sie zu erraten wäre schlimmer, als sie zu benennen.
+ // Ausgeführt wird der verweisende Befehl in der Schleife von anwenden(), die allein die
+ // Befehlsliste kennt. Hierher gerät er nur als Teil eines Sammelbefehls oder wenn ihn jemand
+ // einzeln anwendet; dann fehlt der Bezugspunkt, und das ist zu sagen.
if (befehl instanceof VerweisenderBefehl v) {
return manuell(
befehl,
- Grund.NICHT_UNTERSTUETZT,
+ Grund.STELLE_NICHT_AUFLOESBAR,
"Der Befehl verweist auf „"
+ v.verweis()
- + "“ desselben Artikels; die sinngemäße Übertragung auf die Inhaltsübersicht ist"
- + " nicht umgesetzt.");
+ + "“ desselben Artikels; die übrigen Befehle des Artikels liegen an dieser Stelle"
+ + " nicht vor.");
}
// Sammelbefehle vor den Spezialweichen dispatchen (jeder Teil wird einzeln geroutet).
if (befehl instanceof Sammelbefehl s) {
@@ -2711,12 +2815,21 @@ public final class BefehlAnwender {
// Eine §-Überschrift beginnt mit „§ N“, gefolgt von einem großgeschriebenen Titelwort — im
// Gegensatz zu Querverweisen wie „§ 71 Absatz 1“ oder „§§ 42 bis 45“. Die Negativliste schließt
// die Untergliederungs- und Verbindungswörter aus, sodass an solchen Stellen nicht getrennt wird.
+ // Ein Normkopf steht am Anfang einer Zeile oder wenigstens am Anfang eines Satzes. Ohne diese
+ // Bedingung zerschnitte ein Querverweis mitten im Satz den Block: „… mit Systemen für die
+ // Gebäudeautomatisierung nach § 71a Projektunterlagen in überprüfbarer Form vorzulegen.“ trägt
+ // hinter dem Verweis ein großgeschriebenes Wort und sähe damit aus wie eine Überschrift. Der
+ // Wortbestand unterscheidet beide nicht — die Stellung tut es. Der Satzanfang muss neben dem
+ // Zeilenanfang gelten, weil ein Zitat auch flach ankommen kann („§ 1a Erstes Neu (1) Inhalt
+ // eins. § 1b Zweites Neu …“).
private static final Pattern PARAGRAPH_UEBERSCHRIFT =
Pattern.compile(
- "(?=(?:§|Art\\.)\\s*\\d+[a-z]?\\s+"
+ "(?:^[ \\t]*|(?<=[.:!?] )|(?<=[.:!?]“ ))"
+ + "(?=(?:§|Art\\.)\\s*\\d+[a-z]?(?:[ \\t]*$|\\s+"
+ "(?!Absatz|Absätze|Abs|Satz|Sätze|Nummer|Nummern|Nr|Buchstabe|Buchstaben|Buchst"
+ "|und|bis|oder|sowie|des|der|dieses|genannten)"
- + "\\p{Lu})");
+ + "\\p{Lu}))",
+ Pattern.MULTILINE);
/**
* Zerlegt einen Zitatblock mehrerer Paragraphen an den §-Überschriften (nicht an Querverweisen)
@@ -2831,12 +2944,25 @@ public final class BefehlAnwender {
int i = von + 1;
while (i < zeilen.size()
&& !zeilen.get(i).isEmpty()
- && Character.isLowerCase(zeilen.get(i).codePointAt(0))) {
+ && (Character.isLowerCase(zeilen.get(i).codePointAt(0))
+ || UNVOLLENDET.matcher(zeilen.get(i - 1)).find())) {
i++;
}
return i;
}
+ /**
+ * Eine Zeile, die so endet, ist nicht zu Ende: Kein Normtitel schließt mit einem Binde- oder
+ * Verhältniswort. Die Großschreibung der Folgezeile besagt dann nichts — „… in elektrischen
+ * Wärmepumpen und“ / „Wärmepumpen-Hybridheizungen“ ist eine Überschrift, die der Satz am
+ * Spaltenrand umbrochen hat, und keine zwei Sachen.
+ */
+ private static final Pattern UNVOLLENDET =
+ Pattern.compile(
+ "(?:\\b(?:und|oder|sowie|mit|von|vom|zu|zur|zum|in|im|an|am|auf|für|bei|beim|über|unter"
+ + "|nach|aus|durch|gegen|ohne|des|der|die|das|dem|den|eines|einer|einem|einen)|-)$",
+ Pattern.CASE_INSENSITIVE);
+
/** Zerlegt zitierten Text in Absätze anhand der „(n)“-Marker. */
static List<Absatz> parseAbsaetze(String zitat) {
var text = zitat.strip();
diff --git a/src/main/java/eu/mulk/aendggner/anwendung/InhaltsuebersichtAnwender.java b/src/main/java/eu/mulk/aendggner/anwendung/InhaltsuebersichtAnwender.java
index 9284160..ecf1bdd 100644
--- a/src/main/java/eu/mulk/aendggner/anwendung/InhaltsuebersichtAnwender.java
+++ b/src/main/java/eu/mulk/aendggner/anwendung/InhaltsuebersichtAnwender.java
@@ -105,6 +105,22 @@ final class InhaltsuebersichtAnwender {
}
/**
+ * Führt die Angabe einer Norm ihrer Überschrift nach: Die Zeile der Inhaltsübersicht, die den
+ * Paragraphen führt, wird auf den jetzigen Titel gesetzt.
+ *
+ * <p>Das ist die Ausführung des verweisenden Befehls („Die Inhaltsübersicht wird entsprechend der
+ * vorstehenden Nummer 8 Buchst. a geändert“). Übertragen wird nicht der Wortlaut jenes Punktes,
+ * sondern sein Ergebnis: Was die Überschrift nach seiner Anwendung besagt, besagt fortan auch die
+ * Angabe. Der Umweg über das Ergebnis erspart es, jede Befehlsform ein zweites Mal auf dem
+ * Zeilenmodell nachzubilden — und er trifft genau das, was der Verweis meint.
+ */
+ static AngewandteAenderung fuehreTitelNach(
+ List<Norm> normen, Aenderungsbefehl befehl, Stelle.Paragraph paragraph, String neuerTitel) {
+ var ziel = List.<Stelle.Komponente>of(paragraph);
+ return ersetzeZeilen(normen, befehl, ziel, ziel, paragraph.enbez() + " " + neuerTitel);
+ }
+
+ /**
* Ersetzt die Zeilen von {@code von} bis {@code bis} durch die Angaben des Zitats (oder nichts).
*/
private static AngewandteAenderung ersetzeZeilen(
@@ -308,23 +324,7 @@ final class InhaltsuebersichtAnwender {
"Das Zitat der neuen Inhaltsübersicht enthält Befehlstext — vermutlich ist ein"
+ " Anführungszeichen unbalanciert; bitte manuell prüfen.");
}
- var zeilen = new ArrayList<String>();
- for (var stueck : UEBERSICHT_MARKE.split(flach)) {
- var s = stueck.strip();
- if (s.isEmpty()) {
- continue;
- }
- var m =
- Pattern.compile(
- "^((?:Teil|Abschnitt|Unterabschnitt|Kapitel|Buch)\\s+\\d+[a-z]?|Anhang"
- + "|(?:§|Art\\.)\\s*\\d+[a-z]*)\\s*(.*)$")
- .matcher(s);
- if (m.matches() && !m.group(2).isEmpty()) {
- zeilen.add(m.group(1) + " | " + m.group(2));
- } else {
- zeilen.add(s);
- }
- }
+ var zeilen = uebersichtsZeilen(flach, "");
if (zeilen.size() < 2) {
return manuell(
befehl, Grund.ZITAT_UNBRAUCHBAR, "Das Zitat enthält keine erkennbare Inhaltsübersicht.");
@@ -348,40 +348,47 @@ final class InhaltsuebersichtAnwender {
+ "|(?<!\\S)Anhang(?!\\S))");
/**
- * Zerlegt das Zitat in Angabe-Zeilen: bei §-Angaben eine Zeile je Paragraph (im Zeilenformat der
- * Inhaltsübersicht, „§ N | Titel“), sonst eine einzelne Zeile.
+ * Zerlegt das Zitat in Angabe-Zeilen im Zeilenmodell der Inhaltsübersicht („§ N | Titel“).
+ *
+ * <p>Getrennt wird an <em>allen</em> Zeilenanfängen, die eine Übersicht kennt — an §-Angaben wie
+ * an Gliederungsmarken. Beides ist nötig: Ein Zitat, das mit „Unterabschnitt 4 …“ beginnt und
+ * darauf zwanzig Paragraphen aufführt, bliebe sonst eine einzige Zeile, und jede spätere Angabe
+ * zu einem dieser Paragraphen fände sie nicht mehr. (Das GEG-Heft von 2023 tut genau das.)
*/
private static List<String> angabenZeilen(String zitat, String einrueckung) {
- var flach = zitat.strip().replaceAll("\\s+", " ");
- var zeilen = new ArrayList<String>();
- if (flach.startsWith("§") || flach.startsWith("Art.")) {
- for (var stueck : PARAGRAPH_ANGABE.split(flach)) {
- var s = stueck.strip();
- if (s.isEmpty()) {
- continue;
- }
- var m = Pattern.compile("^((?:§|Art\\.)\\s*\\d+[a-z]*)\\s*(.*)$").matcher(s);
- if (m.matches() && !m.group(2).isEmpty()) {
- zeilen.add(einrueckung + m.group(1) + " | " + m.group(2));
- } else {
- zeilen.add(einrueckung + s);
- }
- }
- }
+ var zeilen = uebersichtsZeilen(zitat.strip().replaceAll("\\s+", " "), einrueckung);
if (zeilen.isEmpty()) {
- zeilen.add(einrueckung + flach);
+ zeilen.add(einrueckung + zitat.strip().replaceAll("\\s+", " "));
}
return zeilen;
}
- // Trennt einen Block mehrerer §-Angaben an den §-Anfängen; Querverweise („… zu § 3 Absatz 3“)
- // trennen nicht (nach ihnen folgt ein Kleinwort oder eine Strukturangabe statt eines Titels).
- private static final Pattern PARAGRAPH_ANGABE =
+ /** Der Kopf einer Übersichtszeile: die Bezeichnung, hinter der der Titel steht. */
+ private static final Pattern ZEILEN_KOPF =
Pattern.compile(
- "(?=(?:§|Art\\.)\\s*\\d+[a-z]?\\s+"
- + "(?!Absatz|Absätze|Abs|Satz|Sätze|Nummer|Nummern|Nr|Buchstabe|Buchstaben"
- + "|und|bis|oder|sowie|des|der|dieses)"
- + "(?:\\(|\\p{Lu}))");
+ "^((?:Teil|Abschnitt|Unterabschnitt|Kapitel|Buch)\\s+\\d+[a-z]?|Anhang"
+ + "|(?:§|Art\\.)\\s*\\d+[a-z]*)\\s*(.*)$");
+
+ /**
+ * Der flache Zitattext, zerlegt in Übersichtszeilen. Die Trennung besorgt {@link
+ * #UEBERSICHT_MARKE}; wo eine Bezeichnung einen Titel bei sich führt, tritt der Strich zwischen
+ * beide.
+ */
+ private static List<String> uebersichtsZeilen(String flach, String einrueckung) {
+ var zeilen = new ArrayList<String>();
+ for (var stueck : UEBERSICHT_MARKE.split(flach)) {
+ var s = stueck.strip();
+ if (s.isEmpty()) {
+ continue;
+ }
+ var m = ZEILEN_KOPF.matcher(s);
+ zeilen.add(
+ m.matches() && !m.group(2).isEmpty()
+ ? einrueckung + m.group(1) + " | " + m.group(2)
+ : einrueckung + s);
+ }
+ return zeilen;
+ }
private static List<String> zeilenVon(Norm norm, int absatzIndex) {
return norm.absaetze().get(absatzIndex).text().lines().toList();
diff --git a/src/main/java/eu/mulk/aendggner/anwendung/InhaltsuebersichtsProbe.java b/src/main/java/eu/mulk/aendggner/anwendung/InhaltsuebersichtsProbe.java
new file mode 100644
index 0000000..54ba54f
--- /dev/null
+++ b/src/main/java/eu/mulk/aendggner/anwendung/InhaltsuebersichtsProbe.java
@@ -0,0 +1,136 @@
+// SPDX-FileCopyrightText: 2026 Matthias Andreas Benkard <code@mail.matthias.benkard.de>
+// SPDX-License-Identifier: AGPL-3.0-or-later
+package eu.mulk.aendggner.anwendung;
+
+import eu.mulk.aendggner.gesetz.Gesetz;
+import eu.mulk.aendggner.gesetz.Norm;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+import org.jspecify.annotations.Nullable;
+
+/**
+ * Hält die Inhaltsübersicht gegen den Bestand der Normen — aber nur dort, wo dieser Lauf ihn
+ * angerührt hat.
+ *
+ * <p>Ein Änderungsgesetz, das Paragraphen einfügt, aufhebt, umnummeriert oder ihre Überschriften
+ * neu fasst, muss die Angaben der Inhaltsübersicht eigens mitändern; das Handbuch der
+ * Rechtsförmlichkeit verlangt dafür eigene Befehle. Bleiben sie aus oder greifen sie nicht, so
+ * bleibt die Übersicht hinter dem Text zurück, ohne dass ein einziger Befehl liegenbliebe — ein
+ * Fehler, den keine Zahl des Protokolls anzeigt.
+ *
+ * <p>Geprüft wird deshalb <em>der Unterschied</em>: allein die Normen, deren Bezeichnung oder
+ * Überschrift dieser Lauf verändert hat. Die Übersicht einer fremden Quelle mag im Übrigen ungenau
+ * sein — das ist ein Befund über die Quelle und nicht über den Lauf, und ihn hier zu rügen hieße,
+ * den Leser mit Fremdem zu behelligen.
+ *
+ * <p>Geheilt wird nichts. Die Übersicht ist eine Norm wie jede andere; sie ohne Befehl
+ * fortzuschreiben hieße, Recht zu erfinden.
+ */
+public final class InhaltsuebersichtsProbe {
+
+ private static final String ENBEZ = "Inhaltsübersicht";
+
+ /** Eine Norm, die in der Übersicht ein Gegenstück haben kann: der Paragraph, der Artikel. */
+ private static final Pattern EIGENE_NORM = Pattern.compile("^(?:§|Art\\.)\\s*\\d+[a-z]?$");
+
+ /** Eine Angabezeile im Zeilenmodell der Übersicht: „§ 5 | Titel“. */
+ private static final Pattern ANGABE =
+ Pattern.compile("^\\s*((?:§|Art\\.)\\s*\\d+[a-z]?)\\s*\\|\\s*(.*)$");
+
+ private InhaltsuebersichtsProbe() {}
+
+ /**
+ * Die Rügen, die der Vergleich ergibt; leer, wenn alles stimmt oder das Gesetz keine
+ * Inhaltsübersicht führt.
+ */
+ public static List<String> pruefe(Gesetz alt, Gesetz neu) {
+ var uebersicht = neu.norm(ENBEZ).orElse(null);
+ if (uebersicht == null) {
+ return List.of();
+ }
+ var angaben = angaben(uebersicht);
+ var vorher = titelJeNorm(alt);
+ var ruegen = new ArrayList<String>();
+ for (var norm : neu.normen()) {
+ if (norm.weggefallen() || !EIGENE_NORM.matcher(norm.enbez()).matches()) {
+ // Die Übersicht führt Paragraphen; Anlagen, ihre Nummern und die Übersicht selbst stehen
+ // nicht in ihr. Sie hier zu vermissen hieße, ihr etwas abzuverlangen, was sie nie leistet.
+ continue;
+ }
+ var altTitel = vorher.get(norm.enbez());
+ boolean neuHinzu = !vorher.containsKey(norm.enbez());
+ boolean titelGeaendert = altTitel != null && !gleich(altTitel, norm.titel());
+ if (!neuHinzu && !titelGeaendert) {
+ continue;
+ }
+ if (!angaben.containsKey(norm.enbez())) {
+ ruegen.add(
+ "Die Inhaltsübersicht führt keine Angabe zu "
+ + norm.enbez()
+ + ", obgleich dieser Lauf die Norm "
+ + (neuHinzu ? "eingefügt" : "geändert")
+ + " hat. Ein Angabe-Befehl, der das nachholte, ist nicht ersichtlich.");
+ continue;
+ }
+ if (!gleich(angaben.get(norm.enbez()), norm.titel())) {
+ // Gesagt wird der Befund, nicht seine Ursache: Es kann am fehlenden Angabe-Befehl liegen —
+ // es kommt aber auch vor, dass die amtliche Fassung selbst beide verschieden führt (so das
+ // GEG bei § 71k: „Erdgas“ in der Übersicht, „Gas“ in der Überschrift).
+ ruegen.add(
+ "Die Angabe zu "
+ + norm.enbez()
+ + " und ihre Überschrift gehen auseinander: „"
+ + angaben.get(norm.enbez())
+ + "“ gegen „"
+ + (norm.titel() == null ? "" : norm.titel())
+ + "“. Dieser Lauf hat die Überschrift geändert, die Angabe nicht.");
+ }
+ }
+ for (var enbez : vorher.keySet()) {
+ if (EIGENE_NORM.matcher(enbez).matches()
+ && neu.norm(enbez).isEmpty()
+ && angaben.containsKey(enbez)) {
+ ruegen.add(
+ "Die Inhaltsübersicht führt weiterhin eine Angabe zu "
+ + enbez
+ + ", obgleich dieser Lauf die Norm beseitigt hat.");
+ }
+ }
+ return List.copyOf(ruegen);
+ }
+
+ /** Die Angaben der Übersicht, Bezeichnung auf Titel. */
+ private static Map<String, String> angaben(Norm uebersicht) {
+ var angaben = new LinkedHashMap<String, String>();
+ for (var absatz : uebersicht.absaetze()) {
+ for (var zeile : absatz.text().lines().toList()) {
+ var m = ANGABE.matcher(zeile);
+ if (m.matches()) {
+ angaben.putIfAbsent(m.group(1).replaceAll("\\s+", " ").strip(), m.group(2).strip());
+ }
+ }
+ }
+ return angaben;
+ }
+
+ private static Map<String, String> titelJeNorm(Gesetz gesetz) {
+ var titel = new LinkedHashMap<String, String>();
+ for (var norm : gesetz.normen()) {
+ if (!norm.enbez().equals(ENBEZ)) {
+ titel.put(norm.enbez(), norm.titel() == null ? "" : norm.titel());
+ }
+ }
+ return titel;
+ }
+
+ private static boolean gleich(@Nullable String a, @Nullable String b) {
+ return normiere(a).equals(normiere(b));
+ }
+
+ private static String normiere(@Nullable String text) {
+ return text == null ? "" : text.replaceAll("\\s+", " ").strip();
+ }
+}
diff --git a/src/test/java/eu/mulk/aendggner/EndToEndTest.java b/src/test/java/eu/mulk/aendggner/EndToEndTest.java
index 109bbf6..8c9743d 100644
--- a/src/test/java/eu/mulk/aendggner/EndToEndTest.java
+++ b/src/test/java/eu/mulk/aendggner/EndToEndTest.java
@@ -1927,17 +1927,23 @@ class EndToEndTest {
// Kein Befehl des Heftes bleibt mehr ungelesen.
assertThat(parseErgebnis.befehle()).noneMatch(b -> b instanceof UnbekannterBefehl);
- // Die Verweisung auf einen anderen Punkt desselben Artikels wird gelesen, aber bewusst nicht
- // ausgeführt. Der Unterschied ist kein kosmetischer: Die Synopse sagt fortan, dass das
- // Erzeugnis hier die Grenze zieht, und nicht, dass die Vorlage unverständlich sei.
+ // Die Verweisung auf einen anderen Punkt desselben Artikels wird gelesen und ausgeführt: Der
+ // verwiesene Punkt ändert die Überschrift des § 13, und die Angabe der Inhaltsübersicht wird
+ // ihr nachgeführt. Hier scheitert das nicht am Erzeugnis, sondern am Gegenstand — diese
+ // Verordnung führt gar keine Inhaltsübersicht, und die Rüge sagt genau das.
var verweisung = befehlAn(parseErgebnis, "12.");
assertThat(verweisung).isInstanceOf(VerweisenderBefehl.class);
assertThat(((VerweisenderBefehl) verweisung).verweis()).isEqualTo("Nummer 8 Buchst. a");
+ assertThat(gesetz.norm("Inhaltsübersicht")).isEmpty();
- var anwendung = BefehlAnwender.anwenden(gesetz, List.of(verweisung));
- assertThat(anwendung.protokoll().get(0).grund()).isEqualTo(Grund.NICHT_UNTERSTUETZT);
- assertThat(anwendung.protokoll().get(0).begruendung())
- .contains("verweist auf „Nummer 8 Buchst. a“");
+ var anwendung = BefehlAnwender.anwenden(gesetz, parseErgebnis.befehle());
+ var protokoll =
+ anwendung.protokoll().stream()
+ .filter(a -> a.befehl() == verweisung)
+ .findFirst()
+ .orElseThrow();
+ assertThat(protokoll.grund()).isEqualTo(Grund.BESTAND_WIDERSPRICHT);
+ assertThat(protokoll.begruendung()).contains("keine Inhaltsübersicht");
}
private static Aenderungsbefehl befehlAn(
diff --git a/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java b/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java
index 39c71cc..3ed028b 100644
--- a/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java
+++ b/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java
@@ -18,6 +18,7 @@ import eu.mulk.aendggner.aenderung.Aenderungsbefehl.StrukturEinfuegung;
import eu.mulk.aendggner.aenderung.Aenderungsbefehl.StrukturErsetzung;
import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Umnummerierung;
import eu.mulk.aendggner.aenderung.Aenderungsbefehl.UnbekannterBefehl;
+import eu.mulk.aendggner.aenderung.Aenderungsbefehl.VerweisenderBefehl;
import eu.mulk.aendggner.aenderung.Aenderungsbefehl.WoerterEinfuegung;
import eu.mulk.aendggner.aenderung.Aenderungsbefehl.WortAnker;
import eu.mulk.aendggner.aenderung.Provenienz;
@@ -1029,6 +1030,81 @@ class BefehlAnwenderTest {
.contains("Teil 2 | Verfahren\n§ 3 | Ablauf");
}
+ // --- Der verweisende Befehl ------------------------------------------------------------------
+
+ /**
+ * „Die Inhaltsübersicht wird entsprechend der vorstehenden Nummer 8 Buchst. a geändert.“ Der
+ * verwiesene Punkt ändert die Überschrift des § 1; die Angabe wird auf den Titel nachgeführt, den
+ * der Paragraph danach trägt. Übertragen wird also das Ergebnis jenes Punktes und nicht sein
+ * Wortlaut — das trifft, was „entsprechend“ meint.
+ */
+ @Test
+ void fuehrtDieInhaltsuebersichtDemVerwiesenenPunktNach() {
+ var ueberschrift =
+ new Ersetzung(
+ new Stelle(List.of(new Stelle.Paragraph("1"), new Stelle.Ueberschrift())),
+ "Zweck",
+ "Zweck und Ziel",
+ false,
+ false,
+ new Provenienz("1", "8. a)", "(Test)"));
+ var verweisung =
+ new VerweisenderBefehl(
+ iuStelle(), "Nummer 8 Buchst. a", new Provenienz("1", "12.", "(Test)"));
+
+ var ergebnis =
+ BefehlAnwender.anwenden(gesetzMitInhaltsuebersicht(), List.of(ueberschrift, verweisung));
+
+ assertThat(ergebnis.protokoll()).allMatch(a -> a.status() == Status.ANGEWANDT);
+ assertThat(ergebnis.neu().norm("§ 1").orElseThrow().titel()).isEqualTo("Zweck und Ziel");
+ assertThat(absatzText(ergebnis.neu(), "Inhaltsübersicht", 0))
+ .contains("§ 1 | Zweck und Ziel")
+ .doesNotContain("§ 1 | Zweck\n");
+ }
+
+ /**
+ * Was der Verweis nicht trägt, bleibt liegen und wird benannt. Die Inhaltsübersicht führt allein
+ * Bezeichnung und Überschrift; was im Absatz eines Paragraphen geschieht, hat in ihr kein
+ * Gegenstück, und es zu erraten wäre schlimmer, als die Grenze zu benennen.
+ */
+ @Test
+ void ruegtDenVerweisAufEinenPunktOhneGegenstueckInDerUebersicht() {
+ var imAbsatz =
+ new Ersetzung(
+ new Stelle(List.of(new Stelle.Paragraph("1"))),
+ "Text",
+ "Wortlaut",
+ false,
+ false,
+ new Provenienz("1", "8. a)", "(Test)"));
+ var verweisung =
+ new VerweisenderBefehl(
+ iuStelle(), "Nummer 8 Buchst. a", new Provenienz("1", "12.", "(Test)"));
+
+ var ergebnis =
+ BefehlAnwender.anwenden(gesetzMitInhaltsuebersicht(), List.of(imAbsatz, verweisung));
+
+ var ruege = ergebnis.protokoll().get(1);
+ assertThat(ruege.status()).isEqualTo(Status.MANUELL_PRUEFEN);
+ assertThat(ruege.grund()).isEqualTo(Grund.NICHT_UNTERSTUETZT);
+ assertThat(ruege.begruendung()).contains("ändert keine Überschrift");
+ }
+
+ /** Ein Verweis ins Leere ist keine Grenze des Erzeugnisses, sondern ein Befund am Dokument. */
+ @Test
+ void ruegtDenVerweisAufEinenNichtVorhandenenPunkt() {
+ var verweisung =
+ new VerweisenderBefehl(
+ iuStelle(), "Nummer 8 Buchst. a", new Provenienz("1", "12.", "(Test)"));
+
+ var ergebnis = BefehlAnwender.anwenden(gesetzMitInhaltsuebersicht(), List.of(verweisung));
+
+ var ruege = ergebnis.protokoll().get(0);
+ assertThat(ruege.status()).isEqualTo(Status.MANUELL_PRUEFEN);
+ assertThat(ruege.grund()).isEqualTo(Grund.STELLE_NICHT_AUFLOESBAR);
+ assertThat(ruege.begruendung()).contains("findet sich nicht im selben Artikel");
+ }
+
// --- Gliederungs-Überschriften ---------------------------------------------------------------
private static Gesetz gesetzMitGliederungen() {
diff --git a/src/test/java/eu/mulk/aendggner/anwendung/InhaltsuebersichtsProbeTest.java b/src/test/java/eu/mulk/aendggner/anwendung/InhaltsuebersichtsProbeTest.java
new file mode 100644
index 0000000..742430b
--- /dev/null
+++ b/src/test/java/eu/mulk/aendggner/anwendung/InhaltsuebersichtsProbeTest.java
@@ -0,0 +1,101 @@
+// SPDX-FileCopyrightText: 2026 Matthias Andreas Benkard <code@mail.matthias.benkard.de>
+// SPDX-License-Identifier: AGPL-3.0-or-later
+package eu.mulk.aendggner.anwendung;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import eu.mulk.aendggner.gesetz.Absatz;
+import eu.mulk.aendggner.gesetz.Gesetz;
+import eu.mulk.aendggner.gesetz.Norm;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Die Probe schlägt an, wo ein Lauf den Bestand angerührt und die Inhaltsübersicht zurückgelassen
+ * hat — und nur dort. Was die Quelle von sich aus ungenau führt, geht sie nichts an.
+ */
+class InhaltsuebersichtsProbeTest {
+
+ private static Gesetz gesetz(String uebersicht, Norm... normen) {
+ var alle = new ArrayList<Norm>();
+ alle.add(
+ new Norm("Inhaltsübersicht", null, null, List.of(new Absatz(null, uebersicht)), false));
+ alle.addAll(List.of(normen));
+ return new Gesetz("TestG", "Testgesetz", null, alle);
+ }
+
+ private static Norm norm(String enbez, String titel) {
+ return new Norm(enbez, titel, null, List.of(new Absatz(null, "Text.")), false);
+ }
+
+ private static final String UEBERSICHT = "§ 1 | Zweck\n§ 2 | Begriffe";
+
+ @Test
+ void ruegtDieNichtNachgefuehrteAngabe() {
+ var alt = gesetz(UEBERSICHT, norm("§ 1", "Zweck"), norm("§ 2", "Begriffe"));
+ var neu = gesetz(UEBERSICHT, norm("§ 1", "Zweck und Ziel"), norm("§ 2", "Begriffe"));
+
+ assertThat(InhaltsuebersichtsProbe.pruefe(alt, neu))
+ .singleElement()
+ .asString()
+ .contains("§ 1")
+ .contains("gehen auseinander");
+ }
+
+ @Test
+ void schweigtWennDieAngabeMitgefuehrtWurde() {
+ var alt = gesetz(UEBERSICHT, norm("§ 1", "Zweck"), norm("§ 2", "Begriffe"));
+ var neu =
+ gesetz(
+ "§ 1 | Zweck und Ziel\n§ 2 | Begriffe",
+ norm("§ 1", "Zweck und Ziel"),
+ norm("§ 2", "Begriffe"));
+
+ assertThat(InhaltsuebersichtsProbe.pruefe(alt, neu)).isEmpty();
+ }
+
+ @Test
+ void ruegtDenEingefuegtenParagraphenOhneAngabe() {
+ var alt = gesetz(UEBERSICHT, norm("§ 1", "Zweck"), norm("§ 2", "Begriffe"));
+ var neu =
+ gesetz(UEBERSICHT, norm("§ 1", "Zweck"), norm("§ 1a", "Neues"), norm("§ 2", "Begriffe"));
+
+ assertThat(InhaltsuebersichtsProbe.pruefe(alt, neu))
+ .singleElement()
+ .asString()
+ .contains("keine Angabe zu § 1a");
+ }
+
+ @Test
+ void ruegtDieAngabeZurBeseitigtenNorm() {
+ var alt = gesetz(UEBERSICHT, norm("§ 1", "Zweck"), norm("§ 2", "Begriffe"));
+ var neu = gesetz(UEBERSICHT, norm("§ 1", "Zweck"));
+
+ assertThat(InhaltsuebersichtsProbe.pruefe(alt, neu))
+ .singleElement()
+ .asString()
+ .contains("weiterhin eine Angabe zu § 2");
+ }
+
+ /**
+ * Eine Anlage steht nicht in der Inhaltsübersicht; sie dort zu vermissen hieße, ihr etwas
+ * abzuverlangen, was sie nie leistet.
+ */
+ @Test
+ void schweigtZurGeaendertenAnlage() {
+ var alt = gesetz(UEBERSICHT, norm("§ 1", "Zweck"), norm("Anlage 8", "Muster"));
+ var neu = gesetz(UEBERSICHT, norm("§ 1", "Zweck"), norm("Anlage 8", "Anderes Muster"));
+
+ assertThat(InhaltsuebersichtsProbe.pruefe(alt, neu)).isEmpty();
+ }
+
+ /** Ohne Inhaltsübersicht gibt es nichts zu prüfen — und nichts zu rügen. */
+ @Test
+ void schweigtOhneInhaltsuebersicht() {
+ var ohne = new Gesetz("TestG", "Testgesetz", null, List.of(norm("§ 1", "Zweck")));
+ var geaendert = new Gesetz("TestG", "Testgesetz", null, List.of(norm("§ 1", "Zweck und Ziel")));
+
+ assertThat(InhaltsuebersichtsProbe.pruefe(ohne, geaendert)).isEmpty();
+ }
+}