aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMatthias Andreas Benkard <code@mail.matthias.benkard.de>2026-08-28 08:36:29 +0200
committerMatthias Andreas Benkard <code@mail.matthias.benkard.de>2026-08-28 08:36:29 +0200
commitd51e3dad33edb14c048372c035b635313d7a1dc9 (patch)
treeed8cb2cf39e141f04ba05d99f5b913fa6ac93409
parent0be6e41ca5ae1c16aab308931616f1ec84ea3eba (diff)
Die Anlage gliedert sich nach eigener Wahl
Der Korpuslauf sollte klären, woran die Anlagen-Reste liegen, und hat die Frage anders beantwortet als erwartet: Unter den 116 liegengebliebenen Befehlen des Korpus ist kein einziger ein Tabellenproblem. Die Annahme, die vier mecklenburgischen Reste scheiterten an fehlender Tabellenunterstützung, war unrichtig. Beschafft man das Ursprungsheft (GVOBl. M-V 2016 Nr. 12, über das Internet-Archiv, weil der Wirt einer Skriptabfrage nicht antwortet), so zeigt sich: Der Ausbildungsrahmenplan ist keine Tabelle, sondern gegliederter Fließtext — je Abschnitt eine Kopfzeile, die Ausbildungsstelle, die Dauer und eine Spiegelstrichliste. Er fehlte in der Stammfassung, das war alles. Eine Anlage gliedert sich nach eigener Wahl. Neben den Nummern eines Zuständigkeitskatalogs kennt das Erzeugnis deshalb fortan die benannte Einheit und führt sie wie jene als eigene Norm, deren Bezeichnung die Anlage voranstellt: „Anlage 1 Ausbildungsabschnitt 1“. Erkannt wird sie an ihrem Bau und nicht an einer Wortliste — verlangt sind Buchstaben vor dem Wortstamm —, denn jede Anlage darf ihre Einheiten nennen, wie sie will, und eine Liste wäre am nächsten Land wieder unvollständig. Leser und Ausgeber tragen sie (der Rundlauf geht auf), der Stellenparser nimmt sie als Stellenangabe, der Auflöser setzt sie mit der Anlage zur Normbezeichnung zusammen, und der Anwender benennt sie um. Dabei ist ein stiller Fehler zutage getreten: Die Umnummerierung lief für alles, was weder Paragraph noch Absatz noch Aufzählungsmarke noch Satz war, in den Auffangzweig „keine Textänderung nötig“ und meldete Erfolg, ohne etwas zu tun. Erst der Belegfall hat das gezeigt. Von den vier mecklenburgischen Resten sind zwei erledigt (22 statt 20 von 24 Befehlen). Die beiden übrigen haben benannte Gründe: Der eine fügt einen ganzen Ausbildungsabschnitt ein — eine neue Norm innerhalb einer Anlage setzt das Erzeugnis nicht —, der andere verweist auf einen Anhang des ändernden Gesetzes, der nicht vorliegt. Die Anlage 2 (Vordruck des Befähigungsberichts) bleibt aus demselben Grund fort; sie ist der einzige echte Tabellensatz des ganzen Korpus. Die Grundlinie hat sich sogleich bewährt: Die Erweiterung kostete einen Befehl, der zuvor angewandt wurde — „Die Überschrift zu Unterabschnitt 4 wird gestrichen“ des GEG-Entwurfs fiel aus dem Gliederungszweig, weil auch „Unterabschnitt“ auf den Wortstamm endet. Der Korpuslauf rügte das im selben Durchgang („angewandte Befehle 86 statt 87“), und zwar in einem anderen Land und einem anderen Dokument, wo keine Einzelprüfung es bemerkt hätte. Die Grenze ist gezogen und durch zwei Prüffälle gepinnt. Geprüft: mvnw verify (445 Testfälle) und reuse lint (214/214) gehen durch. Der Akzeptanzfall pinnt 35 statt 29 Normen und 22 statt 20 angewandte Befehle; die Grundlinie des Korpuslaufs ist auf 1289 statt 1287 angewandter Befehle fortgeschrieben. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Change-Id: I3e6046c3a91ecc45406058b367ec5a9a19c84967
-rw-r--r--FASSUNGEN.txt83
-rw-r--r--README.md11
-rw-r--r--src/main/java/eu/mulk/aendggner/aenderung/Stelle.java30
-rw-r--r--src/main/java/eu/mulk/aendggner/aenderung/parse/BefehlErkenner.java14
-rw-r--r--src/main/java/eu/mulk/aendggner/aenderung/parse/StellenParser.java35
-rw-r--r--src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java32
-rw-r--r--src/main/java/eu/mulk/aendggner/anwendung/StellenAufloeser.java10
-rw-r--r--src/main/java/eu/mulk/aendggner/gesetz/land/LandesRechtTextAusgeber.java4
-rw-r--r--src/main/java/eu/mulk/aendggner/gesetz/land/LandesRechtTextParser.java15
-rw-r--r--src/test/java/eu/mulk/aendggner/EndToEndTest.java34
-rw-r--r--src/test/java/eu/mulk/aendggner/aenderung/parse/StellenParserTest.java20
-rw-r--r--src/test/resources/sampledata/MecklenburgVorpommern/APOAmtsTA-MV-alt.txt51
-rw-r--r--src/test/resources/sampledata/MecklenburgVorpommern/SOURCES45
-rw-r--r--src/test/resources/sampledata/korpus-grundlinie.tsv4
14 files changed, 362 insertions, 26 deletions
diff --git a/FASSUNGEN.txt b/FASSUNGEN.txt
index 17572b5..cf1e6e6 100644
--- a/FASSUNGEN.txt
+++ b/FASSUNGEN.txt
@@ -21,6 +21,89 @@ Vorbemerkung zur Führung dieses Verzeichnisses
════════════════════════════════════════════════════════════════════════════════
+ Fassung vom 28. August 2026 (dritte Fassung des Tages),
+ zuletzt geändert durch die am 28. August 2026 vorgenommenen Änderungen
+════════════════════════════════════════════════════════════════════════════════
+
+Der Korpuslauf hat seine erste Frucht getragen, und zwar eine unerwartete: Unter
+den einhundertsechzehn liegengebliebenen Befehlen des Korpus ist kein einziger
+ein Tabellenproblem. Die Annahme, die Anlagen des mecklenburgischen Falles
+scheiterten an fehlender Tabellenunterstützung, war unrichtig. Beschafft man das
+Ursprungsheft, so zeigt sich: Der Ausbildungsrahmenplan (Anlage 1) ist keine
+Tabelle, sondern gegliederter Fließtext — je Abschnitt eine Kopfzeile, die
+Ausbildungsstelle, die Dauer und eine Spiegelstrichliste. Er fehlte in der
+Stammfassung, das war alles. Zwei der vier Reste sind damit erledigt.
+
+
+Artikel 1
+Die benannte Einheit einer Anlage
+
+(1) Eine Anlage gliedert sich nach eigener Wahl. Neben den Nummern eines
+ Zuständigkeitskatalogs erkennt der Leser (LandesRechtTextParser) fortan die
+ benannte Einheit und führt sie als eigene Norm, deren Bezeichnung die Anlage
+ voranstellt („Anlage 1 Ausbildungsabschnitt 1“). Der Ausgeber
+ (LandesRechtTextAusgeber) schreibt sie ebenso zurück; der Rundlauf geht auf.
+
+(2) Erkannt wird die Einheit an ihrem Bau und nicht an einer Wortliste: verlangt
+ sind Buchstaben vor dem Wortstamm. „Ausbildungsabschnitt“ trifft, „Abschnitt“
+ nicht — und „Unterabschnitt“ ebenso wenig, obwohl er auf denselben Stamm
+ endet; er gliedert das Gesetz.
+
+(3) Der Stellenparser (StellenParser) nimmt die Einheit als Stellenangabe („Im
+ Ausbildungsabschnitt 1 …“), die Stelle (Stelle) unterscheidet sie von der
+ echten Gliederung, und der Auflöser (StellenAufloeser) setzt sie mit der
+ Anlage zur Normbezeichnung zusammen — ebenso, wie er es mit deren Nummern
+ tut.
+
+(4) Der Anwender (BefehlAnwender) benennt eine solche Norm um („Der bisherige
+ Ausbildungsabschnitt 5 wird zu Ausbildungsabschnitt 6“). Die Anlage nimmt er
+ dabei aus dem Bestand und nicht aus dem Befehl: Dieser nennt nur die Einheit.
+ Zuvor lief die Umnummerierung in den Auffangzweig „keine Textänderung nötig“
+ und meldete Erfolg, ohne etwas zu tun — ein stiller Fehler, den erst der
+ Belegfall zutage gefördert hat.
+
+
+Artikel 2
+Die Beispieldaten
+
+(1) Die mecklenburgische Stammfassung führt fortan die Anlage 1. Sie stammt aus
+ dem Ursprungsheft GVOBl. M-V 2016 Nr. 12, das über das Internet-Archiv
+ beschafft worden ist; der Wirt regierung-mv.de antwortet einer Skriptabfrage
+ nicht. Die Herkunft ist in „MecklenburgVorpommern/SOURCES“ nachgewiesen.
+
+(2) Die Anlage 2 (Vordruck des Befähigungsberichts) bleibt fort. Sie ist der
+ einzige echte Tabellensatz des ganzen Beispielkorpus, und der einzige auf sie
+ zielende Befehl ersetzt sie ohnehin durch eine Fassung, die im Anhang des
+ ändernden Gesetzes steht und nicht vorliegt.
+
+(3) Der Akzeptanzfall pinnt fortan 35 statt 29 Normen und 22 statt 20 angewandte
+ Befehle. Von den beiden Resten fügt der eine einen ganzen
+ Ausbildungsabschnitt ein — eine neue Norm innerhalb einer Anlage setzt das
+ Erzeugnis nicht —, der andere verweist auf den fehlenden Anhang.
+
+
+Artikel 3
+Was die Grundlinie geleistet hat
+
+Die Erweiterung hat einen Befehl gekostet, der zuvor angewandt wurde: „Die
+Überschrift zu Unterabschnitt 4 wird gestrichen“ des GEG-Entwurfs fiel aus dem
+Gliederungszweig, weil auch „Unterabschnitt“ auf den Wortstamm endet. Der
+Korpuslauf hat das im selben Durchgang gerügt („angewandte Befehle 86 statt 87“),
+und zwar bevor eine Einzelprüfung davon etwas gemerkt hätte — keine hätte das,
+denn der Fall steht in einem anderen Land und einem anderen Dokument. Die Grenze
+ist gezogen und durch zwei Prüffälle (StellenParserTest) gepinnt.
+
+
+Schlussbestimmung
+
+Die vorstehenden Änderungen sind durch die vollständige Prüfung (mvnw verify,
+vierhundertfünfundvierzig Testfälle) und durch die Lizenzprüfung (reuse lint,
+zweihundertvierzehn von zweihundertvierzehn Dateien) bestätigt worden. Die
+Grundlinie des Korpuslaufs ist fortgeschrieben: 1289 statt 1287 angewandter
+Befehle.
+
+
+════════════════════════════════════════════════════════════════════════════════
Fassung vom 28. August 2026 (zweite Fassung des Tages),
zuletzt geändert durch die am 28. August 2026 vorgenommenen Änderungen
════════════════════════════════════════════════════════════════════════════════
diff --git a/README.md b/README.md
index 4d06524..e2c6a01 100644
--- a/README.md
+++ b/README.md
@@ -815,6 +815,17 @@ Punkt desselben Artikels (§ 7 Absatz 2 Nummer 17) stammt von dort; sie wird
gelesen und ausdrücklich als Grenze gerügt. Sämtliche 23 Befehle des Heftes sind
damit erschlossen.
+(4a) Eine Anlage gliedert sich nach eigener Wahl. Neben den Nummern eines
+Zuständigkeitskatalogs („Anlage Nummer 6“, § 13 Absatz 3) kennt das Erzeugnis
+deshalb die **benannte Einheit**: den „Ausbildungsabschnitt 1“ eines
+Ausbildungsrahmenplans und was ihm gleicht. Erkannt wird sie an ihrem Bau und
+nicht an einer Wortliste — verlangt sind Buchstaben vor dem Wortstamm, sodass
+„Ausbildungsabschnitt“ trifft, „Abschnitt“ und „Unterabschnitt“ dagegen nicht;
+die gliedern das Gesetz und sind anderswo geregelt. Solche Einheiten werden wie
+die Nummern als eigene Normen geführt („Anlage 1 Ausbildungsabschnitt 1“), lassen
+sich adressieren und umnummerieren. Die Einfügung einer *ganzen* neuen Einheit
+innerhalb einer Anlage ist nicht umgesetzt und wird als solche gerügt.
+
(5) Welche Konvention welches Land beisteuert, welche Stammfassungen woher
stammen und was noch offen ist, verzeichnet
`src/test/resources/sampledata/Landesrecht-Beispiele.adoc`.
diff --git a/src/main/java/eu/mulk/aendggner/aenderung/Stelle.java b/src/main/java/eu/mulk/aendggner/aenderung/Stelle.java
index 12a152e..fe29437 100644
--- a/src/main/java/eu/mulk/aendggner/aenderung/Stelle.java
+++ b/src/main/java/eu/mulk/aendggner/aenderung/Stelle.java
@@ -144,7 +144,35 @@ public record Stelle(List<Komponente> komponenten) {
return komponenten.stream()
.filter(Gliederungseinheit.class::isInstance)
.map(Gliederungseinheit.class::cast)
- .anyMatch(g -> !istAnlagenArt(g.art()));
+ .anyMatch(g -> !istAnlagenArt(g.art()) && !istAnlagenEinheitsArt(g.art()));
+ }
+
+ /**
+ * Die benannte Einheit einer Anlage („Ausbildungsabschnitt 1“), falls vorhanden.
+ *
+ * <p>Sie gliedert nicht das Gesetz, sondern die Anlage, und wird — wie deren Nummern — als eigene
+ * Norm geführt, deren Bezeichnung die Anlage voranstellt („Anlage 1 Ausbildungsabschnitt 1“).
+ */
+ public Optional<Gliederungseinheit> anlagenEinheit() {
+ return komponenten.stream()
+ .filter(Gliederungseinheit.class::isInstance)
+ .map(Gliederungseinheit.class::cast)
+ .filter(g -> istAnlagenEinheitsArt(g.art()))
+ .findFirst();
+ }
+
+ /**
+ * Trägt die Art den Bau einer benannten Anlagen-Einheit? Buchstaben <em>vor</em> dem Wortstamm
+ * sind verlangt: „Ausbildungsabschnitt“ trifft, „Abschnitt“ nicht — der gliedert das Gesetz.
+ */
+ private static boolean istAnlagenEinheitsArt(String art) {
+ // „Unterabschnitt“ endet zwar auf den Wortstamm, gliedert aber das Gesetz und ist keine
+ // Einheit einer Anlage; ebenso der schlichte „Abschnitt“.
+ if (art.equalsIgnoreCase("Abschnitt") || art.equalsIgnoreCase("Unterabschnitt")) {
+ return false;
+ }
+ int stelle = art.length() - "abschnitt".length();
+ return stelle > 0 && art.regionMatches(true, stelle, "abschnitt", 0, "abschnitt".length());
}
/** Die enbez der adressierten Anhang-/Anlagen-Norm („Anhang“, „Anlage 2“), falls vorhanden. */
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 dc12398..1d0685b 100644
--- a/src/main/java/eu/mulk/aendggner/aenderung/parse/BefehlErkenner.java
+++ b/src/main/java/eu/mulk/aendggner/aenderung/parse/BefehlErkenner.java
@@ -484,7 +484,7 @@ final class BefehlErkenner {
Pattern.compile(
"^(?:Der bisherige |Die bisherige |Das bisherige )?(.+?) wird (?:zu )?(?:der |die |das )?"
+ "(§|Art\\.|Absatz|Abs\\.|Satz|Nummer|Nr\\.|Buchstabe|Buchst\\."
- + "|Teil|Abschnitt|Unterabschnitt|Buch|Kapitel|Anlage) "
+ + "|Teil|Abschnitt|Unterabschnitt|Buch|Kapitel|Anlage|\\p{L}+abschnitt) "
+ "(\\d+[a-z]?)\\.$");
// „Die bisherigen Absätze 2 bis 4 werden zu den Absätzen 3 bis 5.“ bzw. „Die bisherigen Nummern 4
@@ -722,7 +722,7 @@ final class BefehlErkenner {
Pattern.compile(
"^(?:Der bisherige |Die bisherige |Das bisherige )?(.+?) wird (?:zu )?"
+ "(§|Art\\.|Absatz|Abs\\.|Satz|Nummer|Nr\\.|Buchstabe|Buchst\\."
- + "|Teil|Abschnitt|Unterabschnitt|Buch|Kapitel|Anlage)"
+ + "|Teil|Abschnitt|Unterabschnitt|Buch|Kapitel|Anlage|\\p{L}+abschnitt)"
// Der Wächter „(?!wird |werden )“ ist nötig: ohne ihn verschlänge die Untereinheit
// in der schlichten Form („Abs. 3 wird Abs. 2 und wird wie folgt geändert:“) das
// Verb und wäre als Stelle unparsbar.
@@ -2194,7 +2194,15 @@ final class BefehlErkenner {
case "Buchstabe", "Buchst." -> new Stelle.BuchstabeNr(nummer);
case "Teil", "Abschnitt", "Unterabschnitt", "Buch", "Kapitel", "Anlage" ->
new Stelle.Gliederungseinheit(ebene, nummer);
- default -> throw new IllegalArgumentException("Unbekannte Ebene: " + ebene);
+ // Die benannte Einheit einer Anlage („Ausbildungsabschnitt 5“) — sie gliedert die Anlage,
+ // nicht das Gesetz, und wird wie deren Nummern als eigene Norm geführt.
+ default -> {
+ if (!ebene.regionMatches(true, Math.max(0, ebene.length() - 9), "abschnitt", 0, 9)
+ || ebene.length() <= 9) {
+ throw new IllegalArgumentException("Unbekannte Ebene: " + ebene);
+ }
+ yield new Stelle.Gliederungseinheit(ebene, nummer);
+ }
};
}
diff --git a/src/main/java/eu/mulk/aendggner/aenderung/parse/StellenParser.java b/src/main/java/eu/mulk/aendggner/aenderung/parse/StellenParser.java
index f6e7360..e772846 100644
--- a/src/main/java/eu/mulk/aendggner/aenderung/parse/StellenParser.java
+++ b/src/main/java/eu/mulk/aendggner/aenderung/parse/StellenParser.java
@@ -230,6 +230,18 @@ public final class StellenParser {
i++;
}
default -> {
+ // Die benannte Einheit einer Anlage: „Im Ausbildungsabschnitt 1 …“. Sie steht nicht in
+ // der Liste der Gliederungsarten, weil ihr Name der Anlage gehört und nicht dem Gesetz;
+ // erkannt wird sie an ihrem Bau (siehe istAnlagenEinheit).
+ if (istAnlagenEinheit(wort)) {
+ var wert = naechstesWort(woerter, i);
+ if (wert != null && NUMMER_WERT.matcher(wert).matches()) {
+ komponenten.add(new Stelle.Gliederungseinheit(anlagenEinheitsArt(wort), wert));
+ i++;
+ continue;
+ }
+ return Optional.empty();
+ }
// Ordinal vor der Gliederungsart: „zum zweiten Abschnitt“, „des 2. Abschnitts“.
var ordinal = ordinalZahl(wort);
var art = ordinal != null ? naechstesWort(woerter, i) : null;
@@ -573,6 +585,29 @@ public final class StellenParser {
};
}
+ /**
+ * Die benannte Einheit einer Anlage („Ausbildungsabschnitt 1“ des Ausbildungsrahmenplans).
+ *
+ * <p>Erkannt wird sie an ihrem Bau und nicht an einer Wortliste: Jede Anlage darf ihre Einheiten
+ * nennen, wie sie will, und eine Liste wäre am nächsten Land wieder unvollständig. Verlangt sind
+ * Buchstaben <em>vor</em> dem Wortstamm — „Ausbildungsabschnitt“ trifft, „Abschnitt“ nicht, denn
+ * der gliedert das Gesetz und ist anderswo geregelt.
+ */
+ private static final Pattern ANLAGEN_EINHEIT =
+ Pattern.compile("^(\\p{L}+abschnitt)(?:s|e|es|en)?$", Pattern.CASE_INSENSITIVE);
+
+ private static boolean istAnlagenEinheit(String wort) {
+ // „Unterabschnitt“ endet auf den Wortstamm, gliedert aber das Gesetz; er ist oben abgehandelt
+ // und darf hier nicht noch einmal zugreifen.
+ return !istGliederungsArt(wort) && ANLAGEN_EINHEIT.matcher(wort).matches();
+ }
+
+ /** Der Nominativ Singular der benannten Einheit („Ausbildungsabschnitts“ → „…abschnitt“). */
+ private static String anlagenEinheitsArt(String wort) {
+ var treffer = ANLAGEN_EINHEIT.matcher(wort);
+ return treffer.matches() ? treffer.group(1) : wort;
+ }
+
/** Normalisiert Genitiv-/Pluralformen der Gliederungsart auf den Nominativ Singular. */
private static String gliederungsArt(String wort) {
return switch (wort) {
diff --git a/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java b/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java
index eb69423..a73ab77 100644
--- a/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java
+++ b/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java
@@ -2217,6 +2217,38 @@ public final class BefehlAnwender {
return angewandt(befehl, enbezNeu);
}
+ // „Der bisherige Ausbildungsabschnitt 5 wird zu Ausbildungsabschnitt 6.“ — die benannte
+ // Einheit einer Anlage ist eine eigene Norm; umnummeriert wird deren Bezeichnung. Der Befehl
+ // nennt nur die Einheit, die Norm heißt aber „Anlage 1 Ausbildungsabschnitt 6“: Die Anlage
+ // steht im Bestand und wird von dort übernommen, nicht aus dem Befehl erraten.
+ var altEinheit = befehl.stelle().anlagenEinheit();
+ var neuEinheit = befehl.neu().anlagenEinheit();
+ if (altEinheit.isPresent() && neuEinheit.isPresent()) {
+ var ergebnis = StellenAufloeser.aufloese(gesetzAus(normen), befehl.stelle());
+ if (ergebnis instanceof StellenAufloeser.Ergebnis.NichtGefunden nicht) {
+ return manuell(befehl, nicht.grund(), nicht.begruendung());
+ }
+ var idx = ((StellenAufloeser.Ergebnis.Gefunden) ergebnis).fundstelle().normIndex();
+ var enbezAlt = normen.get(idx).enbez();
+ var alteBezeichnung = altEinheit.get().bezeichnung();
+ if (!enbezAlt.endsWith(alteBezeichnung)) {
+ return manuell(
+ befehl,
+ Grund.BESTAND_WIDERSPRICHT,
+ "„" + enbezAlt + "“ trägt nicht die Bezeichnung „" + alteBezeichnung + "“.");
+ }
+ var enbezNeu =
+ enbezAlt.substring(0, enbezAlt.length() - alteBezeichnung.length())
+ + neuEinheit.get().bezeichnung();
+ if (!enbezNeu.equals(enbezAlt)
+ && StellenAufloeser.normIndex(gesetzAus(normen), enbezNeu) >= 0) {
+ return manuell(
+ befehl, Grund.BESTAND_WIDERSPRICHT, enbezNeu + " existiert bereits im Gesetz.");
+ }
+ normen.set(idx, normen.get(idx).mitEnbez(enbezNeu));
+ return angewandt(befehl, enbezNeu);
+ }
+
var altAbsatz = befehl.stelle().absatz();
var neuAbsatz = befehl.neu().absatz();
// Nur wenn der Absatz selbst das Umnummerierungsziel ist — bei „Satz 5 wird Satz 4“ stammt
diff --git a/src/main/java/eu/mulk/aendggner/anwendung/StellenAufloeser.java b/src/main/java/eu/mulk/aendggner/anwendung/StellenAufloeser.java
index 32d9c8b..f9d4ff2 100644
--- a/src/main/java/eu/mulk/aendggner/anwendung/StellenAufloeser.java
+++ b/src/main/java/eu/mulk/aendggner/anwendung/StellenAufloeser.java
@@ -64,6 +64,16 @@ final class StellenAufloeser {
stelle = stelle.ohne(nummer.get());
}
}
+ // Ebenso die benannte Einheit einer Anlage („Anlage 1 Ausbildungsabschnitt 1“): Sie gliedert
+ // nicht das Gesetz, sondern die Anlage, und der Ausbildungsrahmenplan führt sie so.
+ var einheit = stelle.anlagenEinheit();
+ if (einheit.isPresent()) {
+ var alsNorm = enbez + " " + einheit.get().bezeichnung();
+ if (normIndex(gesetz, alsNorm) >= 0) {
+ enbez = alsNorm;
+ stelle = stelle.ohne(einheit.get());
+ }
+ }
} else {
return new Ergebnis.NichtGefunden(
"Stelle nennt keinen Paragraphen: " + stelle.anzeigeText(),
diff --git a/src/main/java/eu/mulk/aendggner/gesetz/land/LandesRechtTextAusgeber.java b/src/main/java/eu/mulk/aendggner/gesetz/land/LandesRechtTextAusgeber.java
index 3a82945..74d6e11 100644
--- a/src/main/java/eu/mulk/aendggner/gesetz/land/LandesRechtTextAusgeber.java
+++ b/src/main/java/eu/mulk/aendggner/gesetz/land/LandesRechtTextAusgeber.java
@@ -40,7 +40,9 @@ public final class LandesRechtTextAusgeber {
* Anlagen-Norm stellt den Bezug schon her.
*/
private static final Pattern ANLAGEN_NUMMER =
- Pattern.compile("^(?:Anlage|Anhang)(?:\\s+\\d+[a-z]?)?\\s+((?:Nummer|Nr\\.)\\s+\\d+[a-z]?)$");
+ Pattern.compile(
+ "^(?:Anlage|Anhang)(?:\\s+\\d+[a-z]?)?"
+ + "\\s+((?:Nummer|Nr\\.|\\p{L}+abschnitt)\\s+\\d+[a-z]?)$");
private LandesRechtTextAusgeber() {}
diff --git a/src/main/java/eu/mulk/aendggner/gesetz/land/LandesRechtTextParser.java b/src/main/java/eu/mulk/aendggner/gesetz/land/LandesRechtTextParser.java
index 7f7e2f4..b93ef5d 100644
--- a/src/main/java/eu/mulk/aendggner/gesetz/land/LandesRechtTextParser.java
+++ b/src/main/java/eu/mulk/aendggner/gesetz/land/LandesRechtTextParser.java
@@ -149,6 +149,16 @@ final class LandesRechtTextParser {
"^((?:\\(X+\\) )?(?:§§|Artt\\.) \\S.*?) {2}(\\((?:weggefallen|aufgehoben)\\))$");
/**
+ * Eine benannte Einheit innerhalb einer Anlage: die Nummer eines Zuständigkeitskatalogs („Nummer
+ * 6“) ebenso wie der Abschnitt eines Ausbildungsrahmenplans („Ausbildungsabschnitt 1“).
+ *
+ * <p>Die zweite Form ist nicht als „Abschnitt“ zu lesen: Ein Abschnitt gliedert das Gesetz,
+ * innerhalb einer Anlage gliedert nichts mehr. Verlangt sind deshalb Buchstaben <em>vor</em> dem
+ * Wortstamm — „Ausbildungsabschnitt“ trifft, „Abschnitt“ nicht.
+ */
+ private static final String ANLAGEN_EINHEIT = "(?:Nummer|Nr\\.|\\p{L}+abschnitt)\\s+\\d+[a-z]?";
+
+ /**
* Der Kopf einer Nummer innerhalb einer Anlage („Nummer 6“). Die Nummern eines
* Zuständigkeitskatalogs sind keine Aufzählungsglieder, sondern eigene Einheiten mit eigener
* Absatzzählung — das Landesrechtsportal führt jede als eigene Einzelnorm, und die
@@ -157,7 +167,7 @@ final class LandesRechtTextParser {
* voranstellt: „Anlage Nummer 6“.
*/
private static final Pattern ANLAGEN_NUMMER_KOPF =
- Pattern.compile("^(?:Nummer|Nr\\.)\\s+(\\d+[a-z]?)\\s*$");
+ Pattern.compile("^(" + ANLAGEN_EINHEIT + ")\\s*$");
private static final Pattern WEGGEFALLEN_TITEL =
Pattern.compile("^\\((?:aufgehoben|weggefallen)\\)$");
@@ -331,8 +341,7 @@ final class LandesRechtTextParser {
} else if (neueAnlagenNummer) {
// „Anlage Nummer 6“ — die Anlage selbst bleibt als Norm vor ihnen stehen; trägt sie
// keinen eigenen Wortlaut, so ist sie eben leer.
- normEnbez =
- (anlagenEnbez != null ? anlagenEnbez + " " : "") + "Nummer " + anlagenNummer.group(1);
+ normEnbez = (anlagenEnbez != null ? anlagenEnbez + " " : "") + anlagenNummer.group(1);
normTitel = null;
} else if (neueNorm) {
normEnbez = normKopf.group(1) + " " + normKopf.group(2);
diff --git a/src/test/java/eu/mulk/aendggner/EndToEndTest.java b/src/test/java/eu/mulk/aendggner/EndToEndTest.java
index 34bc8e6..ea014b0 100644
--- a/src/test/java/eu/mulk/aendggner/EndToEndTest.java
+++ b/src/test/java/eu/mulk/aendggner/EndToEndTest.java
@@ -1931,8 +1931,12 @@ class EndToEndTest {
var gesetz = new eu.mulk.aendggner.gesetz.land.LandesRechtLoader().load(alt);
assertThat(gesetz.jurabk()).isEqualTo("APOAmtsTA M-V");
- // Die Inhaltsübersicht ist eine Norm gleichen Namens; dazu die 28 Paragraphen.
- assertThat(gesetz.normen()).hasSize(29);
+ // Die Inhaltsübersicht ist eine Norm gleichen Namens; dazu die 28 Paragraphen, die Anlage 1
+ // (Ausbildungsrahmenplan) und deren fünf Ausbildungsabschnitte, die das Portal wie die Nummern
+ // eines Zuständigkeitskatalogs als eigene Einheiten führt.
+ assertThat(gesetz.normen()).hasSize(35);
+ assertThat(gesetz.norm("Anlage 1 Ausbildungsabschnitt 1")).isPresent();
+ assertThat(gesetz.norm("Anlage 1 Ausbildungsabschnitt 5")).isPresent();
var text = TextBereiniger.bereinige(new PatchTextExtraktor().extrahiere(pdf));
// Kolumnentitel und Seitenfuß stehen im Inhaltsstrom, und zwar mitten im Befehlstext.
@@ -1945,7 +1949,7 @@ class EndToEndTest {
assertThat(parseErgebnis.befehle()).hasSize(24);
var anwendung = BefehlAnwender.anwenden(gesetz, parseErgebnis.befehle());
- assertThat(anwendung.anzahlAngewandt()).isEqualTo(20);
+ assertThat(anwendung.anzahlAngewandt()).isEqualTo(22);
// Die Nummern 2 und 3 entfallen, die Nummern 4 bis 6 rücken auf 2 bis 4 — ohne Platzhalter,
// denn sie stehen am Ende ihres Blockes nicht.
@@ -1965,15 +1969,31 @@ class EndToEndTest {
assertThat(anwendung.neu().norm("Inhaltsübersicht").orElseThrow().absaetze().get(0).text())
.contains("§ 10 | Ausbildungsnachweise");
- // Übrig bleiben allein die vier Befehle, die die Anlagen betreffen: Die Stammfassung führt sie
- // nicht mit (Tabellen und Vordrucke), und der elfte Befehl verweist auf einen Anhang des
- // Änderungsgesetzes.
+ // Die benannte Einheit der Anlage trägt die Wortersetzung …
+ assertThat(
+ anwendung
+ .neu()
+ .norm("Anlage 1 Ausbildungsabschnitt 1")
+ .orElseThrow()
+ .absaetze()
+ .get(0)
+ .text())
+ .startsWith("Das für das Veterinärwesen zuständige Ministerium")
+ .doesNotContain("Ministerium für Landwirtschaft, Umwelt und Verbraucherschutz");
+ // … und die Umnummerierung trifft ihre Bezeichnung, nicht ihren Wortlaut.
+ assertThat(anwendung.neu().norm("Anlage 1 Ausbildungsabschnitt 6")).isPresent();
+ assertThat(anwendung.neu().norm("Anlage 1 Ausbildungsabschnitt 5")).isEmpty();
+
+ // Übrig bleiben zwei Befehle, beide mit benanntem Grund: Nummer 10 Buchstabe b fügt einen
+ // ganzen Ausbildungsabschnitt ein — eine neue Norm innerhalb einer Anlage, die das Erzeugnis
+ // nicht setzt —, und Nummer 11 verweist auf einen Anhang des ändernden Gesetzes, der nicht
+ // vorliegt. Was nicht vorliegt, lässt sich nicht anwenden.
var offen =
anwendung.protokoll().stream()
.filter(a -> a.status() == BefehlAnwender.Status.MANUELL_PRUEFEN)
.map(a -> a.befehl().provenienz().gliederungsPfad())
.toList();
- assertThat(offen).containsExactly("10. a)", "10. b)", "10. c)", "11.");
+ assertThat(offen).containsExactly("10. b)", "11.");
}
private static Aenderungsbefehl befehlZu(
diff --git a/src/test/java/eu/mulk/aendggner/aenderung/parse/StellenParserTest.java b/src/test/java/eu/mulk/aendggner/aenderung/parse/StellenParserTest.java
index 13e4442..6c34b8c 100644
--- a/src/test/java/eu/mulk/aendggner/aenderung/parse/StellenParserTest.java
+++ b/src/test/java/eu/mulk/aendggner/aenderung/parse/StellenParserTest.java
@@ -152,4 +152,24 @@ class StellenParserTest {
// Ein Wort, das bloß so anfängt, ist keiner.
assertThat(StellenParser.istNurChapeau("In der Einleitungsformel")).isFalse();
}
+
+ @Test
+ void benannteEinheitEinerAnlage() {
+ // „Im Ausbildungsabschnitt 1 …“ — der Ausbildungsrahmenplan gliedert sich in benannte
+ // Einheiten, die das Portal wie eigene Normen führt.
+ var stelle = StellenParser.parse("Ausbildungsabschnitt 1").orElseThrow();
+ assertThat(stelle.anzeigeText()).isEqualTo("Ausbildungsabschnitt 1");
+ assertThat(stelle.anlagenEinheit()).isPresent();
+ assertThat(stelle.betrifftEchteGliederung()).isFalse();
+ }
+
+ @Test
+ void unterabschnittBleibtGliederungDesGesetzes() {
+ // Er endet auf denselben Wortstamm, gliedert aber das Gesetz und ist keine Einheit einer
+ // Anlage. Ohne diese Grenze fiele „Die Überschrift zu Unterabschnitt 4 wird gestrichen“ aus
+ // dem Gliederungszweig heraus.
+ var stelle = StellenParser.parse("Unterabschnitt 4").orElseThrow();
+ assertThat(stelle.anlagenEinheit()).isEmpty();
+ assertThat(stelle.betrifftEchteGliederung()).isTrue();
+ }
}
diff --git a/src/test/resources/sampledata/MecklenburgVorpommern/APOAmtsTA-MV-alt.txt b/src/test/resources/sampledata/MecklenburgVorpommern/APOAmtsTA-MV-alt.txt
index 32a5bd0..c8749d7 100644
--- a/src/test/resources/sampledata/MecklenburgVorpommern/APOAmtsTA-MV-alt.txt
+++ b/src/test/resources/sampledata/MecklenburgVorpommern/APOAmtsTA-MV-alt.txt
@@ -216,3 +216,54 @@ Für Veterinärreferendarinnen und Veterinärreferendare mit Behinderungen sind
Abschnitt 4 Schlussvorschriften
§ 28 Inkrafttreten, Außerkrafttreten
Diese Verordnung tritt am 1. Oktober 2016 in Kraft. Gleichzeitig tritt die Ausbildungs- und Prüfungsordnung höherer Veterinärdienst vom 29. Dezember 1997 (GVOBl. M-V 1998 S. 85), die zuletzt durch Artikel 3 Absatz 8 des Gesetzes vom 1. Dezember 2008 (GVOBl. M-V S. 461, 464) geändert worden ist, außer Kraft.
+Anlage 1
+Ausbildungsrahmenplan
+(zu § 7 Absatz 3, § 12 Absatz 3)
+Die Lernziele und Lerninhalte richten sich nach dem Modulsystem der Länderarbeitsgemeinschaft Verbraucherschutz.
+Ausbildungsabschnitt 1
+Ministerium für Landwirtschaft, Umwelt und Verbraucherschutz
+Dauer: 7 Monate
+Ausbildungsinhalte:
+ - Aufbau und Funktion der Verwaltung, insbesondere der Veterinärverwaltung,
+ - Aufgaben der Dienst- und Fachaufsicht, Einblick in die laufenden Geschäftsvorgänge, Verwaltungs- und Haushaltsrecht, Bearbeitung von Vorgängen, Fertigung von Vermerken, Vorlagen, Schreiben, Berichten und Halten von Vorträgen,
+ - Erstellung von Entwürfen für Verordnungen, Verfügungen, Genehmigungen, Zulassungen, Obergutachten; konkrete Durchführung der Fachaufsicht bezüglich des Vollzuges des Tierseuchen-, Lebensmittel-, Tierschutz-, Arzneimittel-, Betäubungsmittel-, Heilmittelwerbe-, Futtermittel-, Tierische Nebenprodukte-Beseitigungs- und Abfallrechtes,
+ - Teilnahme an dem Lehrgang für technische Referendare, der einen Gesamtüberblick über die wesentlichen Aufgaben der allgemeinen und der landwirtschaftlichen Verwaltung gibt, Schulung im Vortrag während dieses Lehrganges.
+Ausbildungsabschnitt 2
+Veterinär- und Lebensmittelüberwachungsamt
+Dauer: 6 Monate
+Ausbildungsinhalte:
+ - allgemeine und spezielle Tierseuchenbekämpfungsmaßnahmen bei anzeigepflichtigen Tierseuchen und meldepflichtigen Tierkrankheiten, Tiergesundheit, Tierseuchenprophylaxe, Beseitigung tierischer Nebenprodukte,
+ - Überwachung der Erzeugung, Herstellung, Be- und Verarbeitung und des Inverkehrbringens von Lebensmitteln, Bedarfsgegenständen und Kosmetika,
+ - Beteiligung an der Fortbildung von Hilfskräften,
+ - Vorbereitung, Durchführung und Nachbereitung tierschutzrechtlicher Kontrollen, erforderlichenfalls Anordnung und Durchführung von Maßnahmen zur Behebung vorliegender und zur Vermeidung zukünftiger Verstöße,
+ - Berechnung und Erhebung von Veterinärverwaltungskosten,
+ - Aufgaben im Strahlenschutz und Katastrophenschutz,
+ - Einweisung in die Aufgaben als Sachverständiger vor Gericht,
+ - Koordination der behördlichen Zusammenarbeit, insbesondere mit der Ordnungsbehörde und der Staatsanwaltschaft sowie mit praktizierenden Tierärzten, Fachorganisationen und -verbänden.
+Ausbildungsabschnitt 3
+Landesamt für Landwirtschaft, Lebensmittelsicherheit und Fischerei
+Dauer: 4 Monate
+Ausbildungsinhalte:
+ - Vertiefung der Kenntnisse bezüglich Untersuchungsverfahren zur Diagnostik von Tierseuchen, Tierkrankheiten und Zoonosen,
+ - Grundsätze der Probenplanung unter Berücksichtigung des Risikokonzeptes und Überwachungsprogrammen, Datenmeldung,
+ - risikoorientierte Untersuchung und Beurteilung nach dem Lebensmittel-, Bedarfsgegenstände- und Kosmetikrecht,
+ - Untersuchungen und Beurteilungen nach dem Arzneimittel-, Betäubungsmittel- und Futtermittelrecht,
+ - Überwachung der tierärztlichen Hausapotheken, der Herstellung und des Einsatzes von Fütterungsarzneimitteln,
+ - Probeentnahmen im Rahmen der Tierarzneimittelüberwachung,
+ - Aufgaben der veterinärmedizinischen Bauhygiene,
+ - Erstellung von Stellungnahmen und Gutachten,
+ - Bearbeitung von Zulassungs- und Genehmigungsverfahren,
+ - Grundsätze des Tierschutzrechts und des Tierversuchswesens,
+ - Inhalt und praktische Anwendung des Tierische Nebenprodukte-Beseitigungsrechts.
+Ausbildungsabschnitt 4
+Veterinär- und Lebensmittelüberwachungsamt, Sachgebiet Schlachttier- und Fleischuntersuchung und Hygieneüberwachung
+Dauer: 2 Monate
+Ausbildungsinhalte:
+ - amtliche Überwachung von Frischfleisch, Überprüfungs- und Inspektionsaufgaben, Genusstauglichkeitskennzeichnung und Maßnahmen im Anschluss an die Kontrollen,
+ - Überwachung von Schlachttiertransporten, Kontrolle der Anlieferung der Tiere, gegebenenfalls tierschutzgerechte Tötung, Unterbringung der Tiere und Zutrieb zur Betäubung, Betäubungs- und Tötungsverfahren,
+ - Beseitigung von tierischen Nebenprodukten.
+Ausbildungsabschnitt 5
+Fachseminar
+Dauer: 3 Monate
+Ausbildungsinhalte:
+ - Vertiefung der aufgabenbezogenen wissenschaftlichen Kenntnisse und der Kenntnisse im allgemeinen Verwaltungsrecht und in Teilen des besonderen Verwaltungsrechtes
diff --git a/src/test/resources/sampledata/MecklenburgVorpommern/SOURCES b/src/test/resources/sampledata/MecklenburgVorpommern/SOURCES
index f821362..af7a157 100644
--- a/src/test/resources/sampledata/MecklenburgVorpommern/SOURCES
+++ b/src/test/resources/sampledata/MecklenburgVorpommern/SOURCES
@@ -77,12 +77,39 @@ Beispieldaten Mecklenburg-Vorpommern — Herkunft und Aufbereitung
- Hochgestellte Fußnotenziffern an den Artikel-Überschriften des Mantelgesetzes
(„Änderung der … amtstierärztlicher Dienst⁴⁴“).
-5. Benannte Grenzen
- Vier der 24 Befehle bleiben liegen, sämtlich die *Anlagen* betreffend:
- Nummer 10 Buchstaben a bis c ändern die Anlage 1 (Ausbildungsrahmenplan) über
- deren „Ausbildungsabschnitte“, Nummer 11 ersetzt die Anlage 2 durch eine
- Fassung, die im *Anhang des Änderungsgesetzes* steht. Die Anlagen sind
- Tabellen und Vordrucke; der kanonische Klartext hat für sie keine Form, und
- sie sind in der Stammfassung deshalb nicht mitgeführt. Der Verweis auf einen
- Anhang des ändernden Gesetzes ist überdies eine Form, die das Erzeugnis nicht
- auflöst.
+5. Die Anlage 1 (nachgetragen am 28.08.2026)
+ Die Stammfassung führt seither den Ausbildungsrahmenplan (Anlage 1) mit. Er
+ steht im Ursprungsheft GVOBl. M-V 2016 Nr. 12 auf den Seiten 498 bis 500 und
+ ist über das Internet-Archiv beschafft worden, weil der Wirt regierung-mv.de
+ einer Skriptabfrage nicht antwortet:
+ https://web.archive.org/web/20220302185826/https://www.regierung-mv.de/static/
+ Regierungsportal/Justizministerium/Inhalte/Rechtliches/GVOBI.M-V/Dateien/
+ GVOBl_1245663.pdf (abgerufen am 28.08.2026).
+
+ Der Befund dabei ist wichtiger als der Nachtrag selbst: **Die Anlage 1 ist
+ keine Tabelle.** Sie ist gegliederter Fließtext — je Ausbildungsabschnitt eine
+ Kopfzeile, die Ausbildungsstelle, die Dauer und eine Spiegelstrichliste der
+ Inhalte. Der kanonische Klartext trägt sie unverändert; die frühere Annahme,
+ die Anlagen ließen sich nur als Tabellen führen, trifft allein auf die Anlage 2
+ zu (Vordruck des Befähigungsberichts mit den Spalten Wertung, Wertigkeit,
+ Einzelergebnis).
+
+ Aufbereitet mit `--extract-only`: Kopfzeilen ohne Doppelpunkt, Spiegelstriche
+ als „ - “ eingerückt, der Kolumnentitel („Nr. 12 Tag der Ausgabe …“) entfernt.
+ Die fünf Ausbildungsabschnitte werden — wie die Nummern des Berliner
+ Zuständigkeitskatalogs — als eigene Normen geführt: „Anlage 1
+ Ausbildungsabschnitt 1“ bis „… 5“.
+
+6. Benannte Grenzen
+ Zwei der 24 Befehle bleiben liegen:
+ - Nummer 10 Buchstabe b fügt einen ganzen Ausbildungsabschnitt ein. Eine neue
+ Norm *innerhalb* einer Anlage setzt das Erzeugnis nicht; die Einfügung ganzer
+ Einheiten ist an den §-Kopf gebunden.
+ - Nummer 11 ersetzt die Anlage 2 durch eine Fassung, die im *Anhang des
+ Änderungsgesetzes* steht. Der Verweis auf einen Anhang des ändernden Gesetzes
+ wird nicht aufgelöst, und die neue Fassung liegt damit nicht vor. Was nicht
+ vorliegt, lässt sich nicht anwenden.
+
+ Die Anlage 2 selbst ist aus demselben Grund nicht nachgetragen: Sie ist ein
+ Vordruck, ihr einziger Befehl ersetzt sie ohnehin ganz, und ihre Form wäre die
+ einzige im ganzen Beispielkorpus, für die der kanonische Klartext keine hat.
diff --git a/src/test/resources/sampledata/korpus-grundlinie.tsv b/src/test/resources/sampledata/korpus-grundlinie.tsv
index 60da4f6..b5b360c 100644
--- a/src/test/resources/sampledata/korpus-grundlinie.tsv
+++ b/src/test/resources/sampledata/korpus-grundlinie.tsv
@@ -1,4 +1,4 @@
-# Grundlinie des Korpuslaufs (§ 6d des Handbuchs), Stand 28. August 2026.
+# Grundlinie des Korpuslaufs (§ 6d des Handbuchs), Stand 28. August 2026 (zweite Fassung des Tages).
#
# Jede Zeile hält fest, was ein Auftrag der Liste „korpus.tsv“ zu diesem Zeitpunkt hergegeben hat.
# Der Lauf wird dagegen gehalten (KorpusberichtTest, „--grundlinie“): Gerügt wird allein, was
@@ -34,7 +34,7 @@ TH-KiGaFinVO 10 10 0 0 - - - - - -
BB-FraktG 4 4 0 0 24 24 0 0 0 -
HH-GutachterausschussVO 21 17 4 0 10 14 0 0 4 Befehl nicht erkannt:2; Stelle nicht auffindbar:1; Zieltext nicht vorhanden:1
HB-BerufsfachschulVO 41 37 4 0 26 29 1 0 2 Befehl nicht erkannt:3; Fundstelle mehrdeutig:1
-MV-APOAmtsTA 24 20 4 0 - - - - - Befehl nicht erkannt:4
+MV-APOAmtsTA 24 22 2 0 - - - - - Befehl nicht erkannt:2
RP-UKAPOGS 23 9 14 0 - - - - - Stelle nicht auffindbar:2; Zieltext nicht vorhanden:10; Bestand widerspricht dem Befehl:2
GEG-GModG-RegE 97 87 10 0 - - - - - Befehl nicht erkannt:6; Stelle nicht auffindbar:2; Zieltext nicht vorhanden:2
GEG-Beschlussempfehlung 119 70 49 0 - - - - - Befehl nicht erkannt:1; Stelle nicht auffindbar:3; Zieltext nicht vorhanden:40; Fundstelle mehrdeutig:1; Bestand widerspricht dem Befehl:4