aboutsummaryrefslogtreecommitdiff
path: root/src/main
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 /src/main
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
Diffstat (limited to 'src/main')
-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
7 files changed, 132 insertions, 8 deletions
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);