Model and apply structural (Teil/Abschnitt) and TOC commands

Introduce the Gliederungsbaum into the model and support the structural
command families that were the bulk of the remaining unknowns:

- Gliederung gains a kennzahl; Gesetz carries the ordered list of
  Gliederungseinheiten, which the loader now collects.
- New Stelle components Gliederungseinheit (Teil/Abschnitt/Unterabschnitt/
  Anlage/…) and Absatzbezeichnung; StellenParser parses these plus
  "Überschrift von <Gliederung>" and drops "Satzteil/Angabe vor Nummer N"
  chapeau qualifiers.
- Gliederungs-Überschrift Neufassung/Streichung apply to the tree and
  render as a "Geänderte Gliederungs-Überschriften" diff section;
  Absatzbezeichnung-Streichung removes an Absatz number; Inhaltsübersicht
  "Angabe(n) zu …" commands are recognized (applied via the existing TOC
  path).

Reuses Neufassung/Aufhebung with structural Stellen rather than adding new
command types. Final UnbekannterBefehl counts: GEG 50->5, IfSG 11->3,
AGG 2->0 (§ 1 Alters->Lebensalters now applies), UWG 2->0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Change-Id: I0cc91bfe65798140dd982b19f1884dfe60be87c4
diff --git a/src/main/java/eu/mulk/aendggner/aenderung/Stelle.java b/src/main/java/eu/mulk/aendggner/aenderung/Stelle.java
index 0bcceb6..eb135fe 100644
--- a/src/main/java/eu/mulk/aendggner/aenderung/Stelle.java
+++ b/src/main/java/eu/mulk/aendggner/aenderung/Stelle.java
@@ -17,11 +17,29 @@
   }
 
   public sealed interface Komponente
-      permits Paragraph, AbsatzNr, SatzNr, NummerNr, BuchstabeNr, Ueberschrift, Inhaltsuebersicht {}
+      permits Paragraph,
+          AbsatzNr,
+          SatzNr,
+          NummerNr,
+          BuchstabeNr,
+          Ueberschrift,
+          Inhaltsuebersicht,
+          Gliederungseinheit,
+          Absatzbezeichnung {}
 
   /** „§ 5“, „§ 28a“ — die Paragraphennummer ohne „§ “. */
   public record Paragraph(String nummer) implements Komponente {}
 
+  /** Eine Gliederungseinheit oberhalb des Paragraphen: „Teil 2“, „Abschnitt 3“, „Anlage 8“. */
+  public record Gliederungseinheit(String art, String nummer) implements Komponente {
+    public String bezeichnung() {
+      return nummer.isEmpty() ? art : art + " " + nummer;
+    }
+  }
+
+  /** „Die Absatzbezeichnung „(2)““ — die reine Absatznummer als Ziel einer Streichung. */
+  public record Absatzbezeichnung(String nummer) implements Komponente {}
+
   /** „Absatz 2“ */
   public record AbsatzNr(String nummer) implements Komponente {}
 
@@ -75,6 +93,24 @@
     return komponenten.stream().anyMatch(Ueberschrift.class::isInstance);
   }
 
+  public List<Gliederungseinheit> gliederungsPfad() {
+    return komponenten.stream()
+        .filter(Gliederungseinheit.class::isInstance)
+        .map(Gliederungseinheit.class::cast)
+        .toList();
+  }
+
+  public boolean betrifftGliederung() {
+    return komponenten.stream().anyMatch(Gliederungseinheit.class::isInstance);
+  }
+
+  public Optional<Absatzbezeichnung> absatzbezeichnung() {
+    return komponenten.stream()
+        .filter(Absatzbezeichnung.class::isInstance)
+        .map(Absatzbezeichnung.class::cast)
+        .findFirst();
+  }
+
   public String anzeigeText() {
     if (komponenten.isEmpty()) {
       return "(gesamtes Gesetz)";
@@ -93,6 +129,8 @@
             case BuchstabeNr b -> "Buchstabe " + b.kennung();
             case Ueberschrift u -> "Überschrift";
             case Inhaltsuebersicht i -> "Inhaltsübersicht";
+            case Gliederungseinheit g -> g.bezeichnung();
+            case Absatzbezeichnung a -> "Absatzbezeichnung (" + a.nummer() + ")";
           });
     }
     return sb.toString();
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 80f664e..7283d90 100644
--- a/src/main/java/eu/mulk/aendggner/aenderung/parse/BefehlErkenner.java
+++ b/src/main/java/eu/mulk/aendggner/aenderung/parse/BefehlErkenner.java
@@ -128,6 +128,26 @@
 
   private static final Pattern AUFHEBUNG = Pattern.compile("^(.+?) (?:wird|werden) aufgehoben\\.$");
 
+  // „Die Überschrift von Teil 3 Abschnitt 2 wird gestrichen.“ — Streichung einer Gliederungs-
+  // Überschrift (die Wörter-Streichung STREICHUNG erfordert dagegen ein Zitat).
+  private static final Pattern UEBERSCHRIFT_STREICHUNG =
+      Pattern.compile("^(Die Überschrift (?:von|des|der|zu) .+?) wird gestrichen\\.$");
+
+  // „Die Absatzbezeichnung „(2)“ wird gestrichen.“
+  private static final Pattern ABSATZBEZEICHNUNG_STREICHUNG =
+      Pattern.compile("^Die Absatzbezeichnung " + Z + " wird gestrichen\\.$");
+
+  // Inhaltsübersicht: „Die Angabe(n) zu <…> wird/werden wie folgt gefasst / durch … ersetzt /
+  // gestrichen.“ Wird als Änderung der Inhaltsübersicht typisiert (Anwendung erfolgt gesondert).
+  private static final Pattern INHALTSUEBERSICHT_ANGABE =
+      Pattern.compile(
+          "^Die Angaben? (?:zu|zur) .+? (?:wird|werden) "
+              + "(?:wie folgt gefasst: "
+              + Z
+              + "|durch (?:die )?folgende[nrs]? Angaben? ersetzt: "
+              + Z
+              + "|gestrichen)\\.?$");
+
   private static final Pattern STREICHUNG =
       Pattern.compile(
           "^(?:In )?(.+?) (?:wird|werden) (?:jeweils )?" + WOERTER + " " + Z + " gestrichen\\.$");
@@ -284,6 +304,19 @@
 
     Matcher m;
 
+    // Inhaltsübersichts-Angaben zuerst prüfen, bevor NEUFASSUNG/STRUKTUR_ERSETZUNG die Phrase
+    // strukturell (aber mit unparsbarer Stelle) an sich ziehen.
+    if ((m = INHALTSUEBERSICHT_ANGABE.matcher(text)).matches()) {
+      var stelle = kontext.plus(new Stelle(List.of(new Stelle.Inhaltsuebersicht())));
+      if (m.group(1) != null) {
+        return Optional.of(new Neufassung(stelle, zitat(zitate, m.group(1)), provenienz));
+      }
+      if (m.group(2) != null) {
+        return Optional.of(new Neufassung(stelle, zitat(zitate, m.group(2)), provenienz));
+      }
+      return Optional.of(new Aufhebung(stelle, provenienz));
+    }
+
     if ((m = PARAGRAPH_BEREICH_NEUFASSUNG.matcher(text)).matches()) {
       return paragraphBereichNeufassung(zitat(zitate, m.group(3)), kontext, provenienz);
     }
@@ -497,6 +530,17 @@
       return ausStellen(m.group(1), s -> new Aufhebung(kontext.plus(s), provenienz));
     }
 
+    if ((m = UEBERSCHRIFT_STREICHUNG.matcher(text)).matches()) {
+      return StellenParser.parse(m.group(1))
+          .map(s -> new Aufhebung(kontext.plus(s), provenienz));
+    }
+
+    if ((m = ABSATZBEZEICHNUNG_STREICHUNG.matcher(text)).matches()) {
+      var nummer = zitat(zitate, m.group(1)).replaceAll("[^0-9a-z]", "");
+      var stelle = new Stelle(List.of(new Stelle.Absatzbezeichnung(nummer)));
+      return Optional.of(new Aufhebung(kontext.plus(stelle), provenienz));
+    }
+
     if ((m = STREICHUNG.matcher(text)).matches()) {
       var woerter = wortZitat(zitate, m.group(2));
       return ausStellen(m.group(1), s -> new Streichung(kontext.plus(s), woerter, provenienz));
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 2b2b80e..4660343 100644
--- a/src/main/java/eu/mulk/aendggner/aenderung/parse/StellenParser.java
+++ b/src/main/java/eu/mulk/aendggner/aenderung/parse/StellenParser.java
@@ -30,6 +30,9 @@
           "dem",
           "den",
           "des",
+          "von",
+          "zu",
+          "zur",
           "neue",
           "neuen",
           "bisherige",
@@ -41,7 +44,15 @@
 
   private StellenParser() {}
 
+  // „in dem Satzteil vor Nummer 1“, „in der Angabe vor Nummer 1“ — verfeinernde Chapeau-Angaben
+  // ohne eigene Stelle-Komponente; die Ersetzung sucht ohnehin im Text der umgebenden Stelle.
+  private static final Pattern CHAPEAU_QUALIFIER =
+      Pattern.compile(
+          "(?i)(?:in dem |im |in der )?(?:Satzteil|Angabe) vor "
+              + "(?:Nummer|Buchstabe|Satz|Absatz) \\S+");
+
   public static Optional<Stelle> parse(String phrase) {
+    phrase = CHAPEAU_QUALIFIER.matcher(phrase).replaceAll(" ").strip();
     var woerter = phrase.strip().split("\\s+");
     var komponenten = new ArrayList<Stelle.Komponente>();
 
@@ -91,6 +102,16 @@
           komponenten.add(new Stelle.BuchstabeNr(wert));
           i++;
         }
+        case "Teil", "Teils", "Buch", "Buches", "Kapitel", "Kapitels", "Abschnitt", "Abschnitts",
+            "Unterabschnitt", "Unterabschnitts", "Anlage", "Anlagen" -> {
+          var wert = naechstesWort(woerter, i);
+          if (wert == null || !NUMMER_WERT.matcher(wert).matches()) {
+            return Optional.empty();
+          }
+          komponenten.add(new Stelle.Gliederungseinheit(gliederungsArt(wort), wert));
+          i++;
+        }
+        case "Anhang" -> komponenten.add(new Stelle.Gliederungseinheit("Anhang", ""));
         case "Inhaltsübersicht" -> komponenten.add(new Stelle.Inhaltsuebersicht());
         case "Überschrift" -> komponenten.add(new Stelle.Ueberschrift());
         default -> {
@@ -251,8 +272,10 @@
       case Stelle.SatzNr s -> new Stelle.SatzNr(label);
       case Stelle.NummerNr n -> new Stelle.NummerNr(label);
       case Stelle.BuchstabeNr b -> new Stelle.BuchstabeNr(label);
+      case Stelle.Gliederungseinheit g -> new Stelle.Gliederungseinheit(g.art(), label);
       case Stelle.Ueberschrift u -> null;
       case Stelle.Inhaltsuebersicht i -> null;
+      case Stelle.Absatzbezeichnung a -> null;
     };
   }
 
@@ -305,6 +328,19 @@
     return Optional.of(new Stelle(komponenten));
   }
 
+  /** Normalisiert Genitiv-/Pluralformen der Gliederungsart auf den Nominativ Singular. */
+  private static String gliederungsArt(String wort) {
+    return switch (wort) {
+      case "Teils" -> "Teil";
+      case "Buches" -> "Buch";
+      case "Kapitels" -> "Kapitel";
+      case "Abschnitts" -> "Abschnitt";
+      case "Unterabschnitts" -> "Unterabschnitt";
+      case "Anlagen" -> "Anlage";
+      default -> wort;
+    };
+  }
+
   private static String naechstesWort(String[] woerter, int i) {
     return i + 1 < woerter.length ? entfernePunktuation(woerter[i + 1]) : null;
   }
diff --git a/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java b/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java
index 94c5db1..86eccaf 100644
--- a/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java
+++ b/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java
@@ -17,6 +17,7 @@
 import eu.mulk.aendggner.aenderung.Stelle;
 import eu.mulk.aendggner.gesetz.Absatz;
 import eu.mulk.aendggner.gesetz.Gesetz;
+import eu.mulk.aendggner.gesetz.Gliederung;
 import eu.mulk.aendggner.gesetz.Norm;
 import java.util.ArrayList;
 import java.util.LinkedHashSet;
@@ -60,16 +61,19 @@
 
   public static AnwendungsErgebnis anwenden(Gesetz alt, List<Aenderungsbefehl> befehle) {
     var normen = new ArrayList<>(alt.normen());
+    var gliederungen = new ArrayList<>(alt.gliederungen());
     var protokoll = new ArrayList<AngewandteAenderung>();
 
     for (var befehl : befehle) {
-      protokoll.add(wendeAn(normen, befehl));
+      protokoll.add(wendeAn(normen, gliederungen, befehl));
     }
 
-    return new AnwendungsErgebnis(alt.mitNormen(normen), protokoll);
+    return new AnwendungsErgebnis(
+        alt.mitNormen(normen).mitGliederungen(gliederungen), protokoll);
   }
 
-  private static AngewandteAenderung wendeAn(List<Norm> normen, Aenderungsbefehl befehl) {
+  private static AngewandteAenderung wendeAn(
+      List<Norm> normen, List<Gliederung> gliederungen, Aenderungsbefehl befehl) {
     if (befehl instanceof UnbekannterBefehl) {
       return manuell(befehl, "Befehl nicht erkannt.");
     }
@@ -78,6 +82,14 @@
           befehl, "Änderungen an der Inhaltsübersicht werden nicht automatisch angewandt.");
     }
     try {
+      // Änderungen an Gliederungs-Überschriften (Teil/Abschnitt/…) wirken auf den Gliederungsbaum.
+      if (befehl.stelle().betrifftGliederung()) {
+        return switch (befehl) {
+          case Neufassung n -> wendeGliederungNeufassungAn(gliederungen, n);
+          case Aufhebung a -> wendeGliederungStreichungAn(gliederungen, a);
+          default -> manuell(befehl, "Strukturänderung wird nicht automatisch angewandt.");
+        };
+      }
       return switch (befehl) {
         case Ersetzung e -> wendeErsetzungAn(normen, e);
         case Streichung s -> wendeStreichungAn(normen, s);
@@ -89,7 +101,7 @@
         case Aufhebung a -> wendeAufhebungAn(normen, a);
         case Umnummerierung u -> wendeUmnummerierungAn(normen, u);
         case WortlautZuAbsatz w -> wendeWortlautZuAbsatzAn(normen, w);
-        case Sammelbefehl s -> wendeSammelAn(normen, s);
+        case Sammelbefehl s -> wendeSammelAn(normen, gliederungen, s);
         case UnbekannterBefehl u -> manuell(befehl, "Befehl nicht erkannt.");
       };
     } catch (RuntimeException e) {
@@ -97,6 +109,66 @@
     }
   }
 
+  // --- Gliederungs-Überschriften -------------------------------------------------------------
+
+  private static AngewandteAenderung wendeGliederungNeufassungAn(
+      List<Gliederung> gliederungen, Neufassung befehl) {
+    int idx = findeGliederung(gliederungen, befehl.stelle().gliederungsPfad());
+    if (idx < 0) {
+      return manuell(befehl, "Gliederungseinheit nicht gefunden.");
+    }
+    var alt = gliederungen.get(idx);
+    var titel = befehl.neuerText().replaceAll("\\s+", " ").strip();
+    if (titel.startsWith(alt.bezeichnung())) {
+      titel = titel.substring(alt.bezeichnung().length()).strip();
+    }
+    gliederungen.set(idx, alt.mitTitel(titel.isEmpty() ? null : titel));
+    return angewandt(befehl, alt.bezeichnung());
+  }
+
+  private static AngewandteAenderung wendeGliederungStreichungAn(
+      List<Gliederung> gliederungen, Aufhebung befehl) {
+    int idx = findeGliederung(gliederungen, befehl.stelle().gliederungsPfad());
+    if (idx < 0) {
+      return manuell(befehl, "Gliederungseinheit nicht gefunden.");
+    }
+    var alt = gliederungen.get(idx);
+    gliederungen.set(idx, alt.mitTitel("(weggefallen)"));
+    return angewandt(befehl, alt.bezeichnung());
+  }
+
+  /**
+   * Findet die Gliederungseinheit zum Pfad („Teil 3 Abschnitt 2“): jede Ebene wird per Bezeichnung
+   * innerhalb des Kennzahl-Präfixes der übergeordneten Ebene aufgelöst.
+   */
+  private static int findeGliederung(
+      List<Gliederung> gliederungen, List<Stelle.Gliederungseinheit> pfad) {
+    if (pfad.isEmpty()) {
+      return -1;
+    }
+    String praefix = "";
+    int gefunden = -1;
+    for (var einheit : pfad) {
+      gefunden = -1;
+      for (int i = 0; i < gliederungen.size(); i++) {
+        var g = gliederungen.get(i);
+        if (g.bezeichnung().equals(einheit.bezeichnung())
+            && (g.kennzahl() == null || g.kennzahl().startsWith(praefix))) {
+          gefunden = i;
+          break;
+        }
+      }
+      if (gefunden < 0) {
+        return -1;
+      }
+      var kennzahl = gliederungen.get(gefunden).kennzahl();
+      if (kennzahl != null) {
+        praefix = kennzahl;
+      }
+    }
+    return gefunden;
+  }
+
   // --- Wortweise Textoperationen -------------------------------------------------------------
 
   private static AngewandteAenderung wendeErsetzungAn(List<Norm> normen, Ersetzung befehl) {
@@ -401,6 +473,24 @@
   private static AngewandteAenderung wendeAufhebungAn(List<Norm> normen, Aufhebung befehl) {
     var stelle = befehl.stelle();
 
+    if (stelle.absatzbezeichnung().isPresent()) {
+      var aufloesung = loeseNormAuf(normen, stelle);
+      if (aufloesung.fehler() != null) {
+        return manuell(befehl, aufloesung.fehler());
+      }
+      var norm = normen.get(aufloesung.normIndex());
+      var nummer = stelle.absatzbezeichnung().get().nummer();
+      var absaetze = new ArrayList<>(norm.absaetze());
+      for (int i = 0; i < absaetze.size(); i++) {
+        if (nummer.equals(absaetze.get(i).nummer())) {
+          absaetze.set(i, new Absatz(null, absaetze.get(i).text()));
+          normen.set(aufloesung.normIndex(), norm.mitAbsaetzen(absaetze));
+          return angewandt(befehl, norm.enbez());
+        }
+      }
+      return manuell(befehl, "Absatz (" + nummer + ") nicht gefunden.");
+    }
+
     if (nurParagraph(stelle)) {
       var aufloesung = loeseNormAuf(normen, stelle);
       if (aufloesung.fehler() != null) {
@@ -497,12 +587,13 @@
    * zusammen. Nur wenn alle Teile gelingen, gilt der Befehl als angewandt; sonst wird er zur
    * manuellen Prüfung markiert (bereits angewandte Teile bleiben wirksam).
    */
-  private static AngewandteAenderung wendeSammelAn(List<Norm> normen, Sammelbefehl befehl) {
+  private static AngewandteAenderung wendeSammelAn(
+      List<Norm> normen, List<Gliederung> gliederungen, Sammelbefehl befehl) {
     var betroffene = new LinkedHashSet<String>();
     var fehler = new ArrayList<String>();
     int i = 1;
     for (var teil : befehl.teilbefehle()) {
-      var ergebnis = wendeAn(normen, teil);
+      var ergebnis = wendeAn(normen, gliederungen, teil);
       betroffene.addAll(ergebnis.betroffeneEnbez());
       if (ergebnis.status() != Status.ANGEWANDT) {
         fehler.add("Teil " + i + " (" + teil.stelle().anzeigeText() + "): " + ergebnis.begruendung());
diff --git a/src/main/java/eu/mulk/aendggner/gesetz/Gesetz.java b/src/main/java/eu/mulk/aendggner/gesetz/Gesetz.java
index 5db0860..fc84172 100644
--- a/src/main/java/eu/mulk/aendggner/gesetz/Gesetz.java
+++ b/src/main/java/eu/mulk/aendggner/gesetz/Gesetz.java
@@ -3,11 +3,20 @@
 import java.util.List;
 import java.util.Optional;
 
-/** Ein Stammgesetz, bestehend aus einer geordneten Liste von Einzelnormen. */
-public record Gesetz(String jurabk, String langue, String kurzue, List<Norm> normen) {
+/**
+ * Ein Stammgesetz, bestehend aus einer geordneten Liste von Einzelnormen und den (ebenfalls
+ * geordneten) Gliederungseinheiten (Teil/Abschnitt/Unterabschnitt/…), die die Normen strukturieren.
+ */
+public record Gesetz(
+    String jurabk, String langue, String kurzue, List<Norm> normen, List<Gliederung> gliederungen) {
 
   public Gesetz {
     normen = List.copyOf(normen);
+    gliederungen = List.copyOf(gliederungen);
+  }
+
+  public Gesetz(String jurabk, String langue, String kurzue, List<Norm> normen) {
+    this(jurabk, langue, kurzue, normen, List.of());
   }
 
   public Optional<Norm> norm(String enbez) {
@@ -15,6 +24,10 @@
   }
 
   public Gesetz mitNormen(List<Norm> neueNormen) {
-    return new Gesetz(jurabk, langue, kurzue, neueNormen);
+    return new Gesetz(jurabk, langue, kurzue, neueNormen, gliederungen);
+  }
+
+  public Gesetz mitGliederungen(List<Gliederung> neueGliederungen) {
+    return new Gesetz(jurabk, langue, kurzue, normen, neueGliederungen);
   }
 }
diff --git a/src/main/java/eu/mulk/aendggner/gesetz/Gliederung.java b/src/main/java/eu/mulk/aendggner/gesetz/Gliederung.java
index 21c9c4a..53eb6b9 100644
--- a/src/main/java/eu/mulk/aendggner/gesetz/Gliederung.java
+++ b/src/main/java/eu/mulk/aendggner/gesetz/Gliederung.java
@@ -2,10 +2,26 @@
 
 import org.jspecify.annotations.Nullable;
 
-/** Eine Gliederungseinheit (z.B. „2. Abschnitt — Koordinierung und Früherkennung“). */
-public record Gliederung(String bezeichnung, @Nullable String titel) {
+/**
+ * Eine Gliederungseinheit (z.B. „Teil 2 — Anforderungen an zu errichtende Gebäude“ oder „2.
+ * Abschnitt — Koordinierung und Früherkennung“).
+ *
+ * @param kennzahl die hierarchie-kodierende Gliederungskennzahl aus dem gii-XML (z.B. {@code 020020}
+ *     für Abschnitt 2 in Teil 2); {@code null} bei anderweitig konstruierten Einheiten.
+ * @param bezeichnung die relative Bezeichnung („Teil 2“, „Abschnitt 2“).
+ * @param titel die Überschrift der Einheit ({@code null} bei titellosen Einheiten).
+ */
+public record Gliederung(@Nullable String kennzahl, String bezeichnung, @Nullable String titel) {
+
+  public Gliederung(String bezeichnung, @Nullable String titel) {
+    this(null, bezeichnung, titel);
+  }
 
   public String anzeigeText() {
     return titel == null ? bezeichnung : bezeichnung + " — " + titel;
   }
+
+  public Gliederung mitTitel(@Nullable String neuerTitel) {
+    return new Gliederung(kennzahl, bezeichnung, neuerTitel);
+  }
 }
diff --git a/src/main/java/eu/mulk/aendggner/gesetz/gii/GiiXmlLoader.java b/src/main/java/eu/mulk/aendggner/gesetz/gii/GiiXmlLoader.java
index ca7bdf6..b14ec1c 100644
--- a/src/main/java/eu/mulk/aendggner/gesetz/gii/GiiXmlLoader.java
+++ b/src/main/java/eu/mulk/aendggner/gesetz/gii/GiiXmlLoader.java
@@ -39,6 +39,7 @@
     String langue = null;
     String kurzue = null;
     var normen = new ArrayList<Norm>();
+    var gliederungen = new ArrayList<Gliederung>();
     Gliederung aktuelleGliederung = null;
 
     for (var normElement : kindElemente(wurzel, "norm")) {
@@ -55,10 +56,12 @@
 
       var gliederungselement = erstesKind(metadaten, "gliederungseinheit");
       if (gliederungselement != null) {
+        var kennzahl = kindText(gliederungselement, "gliederungskennzahl");
         var bez = kindText(gliederungselement, "gliederungsbez");
         var titel = kindText(gliederungselement, "gliederungstitel");
         if (bez != null) {
-          aktuelleGliederung = new Gliederung(bez, titel);
+          aktuelleGliederung = new Gliederung(kennzahl, bez, titel);
+          gliederungen.add(aktuelleGliederung);
         }
       }
 
@@ -79,7 +82,7 @@
     if (jurabk == null) {
       throw new SAXException("Keine <norm>-Elemente mit Metadaten gefunden: " + datei);
     }
-    return new Gesetz(jurabk, langue, kurzue, normen);
+    return new Gesetz(jurabk, langue, kurzue, normen, gliederungen);
   }
 
   private static ArrayList<Absatz> leseAbsaetze(Element normElement) {
diff --git a/src/main/java/eu/mulk/aendggner/synopse/HtmlRenderer.java b/src/main/java/eu/mulk/aendggner/synopse/HtmlRenderer.java
index 2f5f735..46e1fa8 100644
--- a/src/main/java/eu/mulk/aendggner/synopse/HtmlRenderer.java
+++ b/src/main/java/eu/mulk/aendggner/synopse/HtmlRenderer.java
@@ -25,6 +25,8 @@
 
     rendereKopf(sb, synopse, quelleBeschreibung);
 
+    rendereGliederungsAenderungen(sb, synopse);
+
     for (var eintrag : synopse.eintraege()) {
       rendereEintrag(sb, eintrag);
     }
@@ -58,6 +60,22 @@
     sb.append("</header>\n");
   }
 
+  private static void rendereGliederungsAenderungen(StringBuilder sb, Synopse synopse) {
+    if (synopse.gliederungsAenderungen().isEmpty()) {
+      return;
+    }
+    sb.append("<section class=\"gliederung-aenderungen\">\n<h2>Geänderte Gliederungs-Überschriften</h2>\n");
+    for (var aenderung : synopse.gliederungsAenderungen()) {
+      var spalten = WortDiff.vergleiche(aenderung.alt().anzeigeText(), aenderung.neu().anzeigeText());
+      sb.append("<div class=\"vergleich\">\n<div class=\"alt\">")
+          .append(spalten.altHtml())
+          .append("</div>\n<div class=\"neu\">")
+          .append(spalten.neuHtml())
+          .append("</div>\n</div>\n");
+    }
+    sb.append("</section>\n");
+  }
+
   private static void rendereEintrag(StringBuilder sb, Synopse.Eintrag eintrag) {
     sb.append("<section class=\"norm ")
         .append(eintrag.art().name().toLowerCase())
diff --git a/src/main/java/eu/mulk/aendggner/synopse/Synopse.java b/src/main/java/eu/mulk/aendggner/synopse/Synopse.java
index 26730a2..4f7a58f 100644
--- a/src/main/java/eu/mulk/aendggner/synopse/Synopse.java
+++ b/src/main/java/eu/mulk/aendggner/synopse/Synopse.java
@@ -2,6 +2,7 @@
 
 import eu.mulk.aendggner.anwendung.BefehlAnwender.AngewandteAenderung;
 import eu.mulk.aendggner.gesetz.Gesetz;
+import eu.mulk.aendggner.gesetz.Gliederung;
 import eu.mulk.aendggner.gesetz.Norm;
 import java.util.List;
 import org.jspecify.annotations.Nullable;
@@ -11,9 +12,13 @@
     Gesetz alt,
     Gesetz neu,
     List<Eintrag> eintraege,
+    List<GliederungsAenderung> gliederungsAenderungen,
     List<AngewandteAenderung> manuellZuPruefen,
     List<String> warnungen) {
 
+  /** Eine geänderte Gliederungs-Überschrift (Teil/Abschnitt/…). */
+  public record GliederungsAenderung(Gliederung alt, Gliederung neu) {}
+
   public enum Aenderungsart {
     UNVERAENDERT,
     GEAENDERT,
diff --git a/src/main/java/eu/mulk/aendggner/synopse/SynopseBuilder.java b/src/main/java/eu/mulk/aendggner/synopse/SynopseBuilder.java
index 0f88538..536bca4 100644
--- a/src/main/java/eu/mulk/aendggner/synopse/SynopseBuilder.java
+++ b/src/main/java/eu/mulk/aendggner/synopse/SynopseBuilder.java
@@ -50,7 +50,21 @@
     var manuell =
         anwendung.protokoll().stream().filter(a -> a.status() == Status.MANUELL_PRUEFEN).toList();
 
-    return new Synopse(alt, neu, eintraege, manuell, parseWarnungen);
+    return new Synopse(alt, neu, eintraege, gliederungsAenderungen(alt, neu), manuell, parseWarnungen);
+  }
+
+  /** Paart die Gliederungseinheiten nach Kennzahl und sammelt die mit geänderter Überschrift. */
+  private static List<Synopse.GliederungsAenderung> gliederungsAenderungen(Gesetz alt, Gesetz neu) {
+    var aenderungen = new ArrayList<Synopse.GliederungsAenderung>();
+    for (var neuG : neu.gliederungen()) {
+      alt.gliederungen().stream()
+          .filter(a -> java.util.Objects.equals(a.kennzahl(), neuG.kennzahl()))
+          .filter(a -> a.kennzahl() != null)
+          .findFirst()
+          .filter(a -> !java.util.Objects.equals(a.titel(), neuG.titel()))
+          .ifPresent(a -> aenderungen.add(new Synopse.GliederungsAenderung(a, neuG)));
+    }
+    return aenderungen;
   }
 
   private static boolean gleicherInhalt(
diff --git a/src/test/java/eu/mulk/aendggner/EndToEndTest.java b/src/test/java/eu/mulk/aendggner/EndToEndTest.java
index bd17d79..44903e7 100644
--- a/src/test/java/eu/mulk/aendggner/EndToEndTest.java
+++ b/src/test/java/eu/mulk/aendggner/EndToEndTest.java
@@ -110,10 +110,18 @@
     assertThat(parseErgebnis.artikel()).contains("1");
     assertThat(parseErgebnis.befehle().size()).isGreaterThanOrEqualTo(100);
 
+    // Der Großteil der Befehle wird erkannt; nur einige Spezialfälle (Mehrquell-Ersetzungen,
+    // §§-Bereiche, Chapeau-Angaben) bleiben unbekannt.
+    var unbekannt =
+        parseErgebnis.befehle().stream().filter(b -> b instanceof UnbekannterBefehl).count();
+    assertThat(unbekannt).isLessThanOrEqualTo(8);
+
     // Das XML ist bereits konsolidiert; entscheidend ist, dass die Anwendung sauber terminiert
     // und jeden Befehl protokolliert.
     var anwendung = BefehlAnwender.anwenden(gesetz, parseErgebnis.befehle());
     assertThat(anwendung.protokoll()).hasSameSizeAs(parseErgebnis.befehle());
+    // Gliederungs-Überschriften (Teil/Abschnitt) werden als Befehle erkannt und angewandt.
+    assertThat(anwendung.anzahlAngewandt()).isGreaterThanOrEqualTo(50);
     var synopse = SynopseBuilder.baue(gesetz, anwendung, parseErgebnis.warnungen(), false);
     assertThat(HtmlRenderer.rendere(synopse, "E2E-Test")).contains("Manuell prüfen");
   }
diff --git a/src/test/java/eu/mulk/aendggner/aenderung/parse/BefehlErkennerTest.java b/src/test/java/eu/mulk/aendggner/aenderung/parse/BefehlErkennerTest.java
index 2d0db8a..230bfca 100644
--- a/src/test/java/eu/mulk/aendggner/aenderung/parse/BefehlErkennerTest.java
+++ b/src/test/java/eu/mulk/aendggner/aenderung/parse/BefehlErkennerTest.java
@@ -519,6 +519,43 @@
   }
 
   @Test
+  void gliederungsUeberschriftNeufassung() {
+    var befehl =
+        erkenne(
+            "Die Überschrift von Teil 3 wird wie folgt gefasst: „Teil 3 Anforderungen an"
+                + " bestehende Gebäude“.",
+            Stelle.LEER);
+    assertThat(befehl).get().isInstanceOf(Neufassung.class);
+    assertThat(befehl.orElseThrow().stelle().betrifftGliederung()).isTrue();
+  }
+
+  @Test
+  void gliederungsUeberschriftStreichung() {
+    var befehl = erkenne("Die Überschrift von Teil 2 Abschnitt 4 wird gestrichen.", Stelle.LEER);
+    assertThat(befehl).get().isInstanceOf(Aufhebung.class);
+    assertThat(befehl.orElseThrow().stelle().gliederungsPfad()).hasSize(2);
+  }
+
+  @Test
+  void absatzbezeichnungStreichung() {
+    var befehl =
+        erkenne(
+            "Die Absatzbezeichnung „(2)“ wird gestrichen.",
+            new Stelle(List.of(new Stelle.Paragraph("64"))));
+    var b = befehl.orElseThrow();
+    assertThat(b).isInstanceOf(Aufhebung.class);
+    assertThat(b.stelle().absatzbezeichnung()).get().extracting(Stelle.Absatzbezeichnung::nummer)
+        .isEqualTo("2");
+  }
+
+  @Test
+  void inhaltsuebersichtAngabeWirdTypisiert() {
+    var befehl = erkenne("Die Angabe zu Teil 3 wird wie folgt gefasst: „Teil 3 Neu“.", Stelle.LEER);
+    assertThat(befehl).get().isInstanceOf(Neufassung.class);
+    assertThat(befehl.orElseThrow().stelle().betrifftInhaltsuebersicht()).isTrue();
+  }
+
+  @Test
   void mehrfachErsetzungWirdSammelbefehl() {
     // Mehrere Ersetzungspaare unter einem gemeinsamen „ersetzt“.
     var befehl =
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 c0ea159..9389534 100644
--- a/src/test/java/eu/mulk/aendggner/aenderung/parse/StellenParserTest.java
+++ b/src/test/java/eu/mulk/aendggner/aenderung/parse/StellenParserTest.java
@@ -67,4 +67,25 @@
     assertThat(stellen).extracting(Stelle::anzeigeText)
         .containsExactly("Absatz 1 Satz 1", "Absatz 1 Satz 2");
   }
+
+  @Test
+  void parstGliederungspfad() {
+    assertThat(StellenParser.parse("Teil 3 Abschnitt 2").orElseThrow().anzeigeText())
+        .isEqualTo("Teil 3 Abschnitt 2");
+  }
+
+  @Test
+  void parstUeberschriftVonGliederung() {
+    var stelle = StellenParser.parse("Die Überschrift von Teil 3 Abschnitt 2").orElseThrow();
+    assertThat(stelle.betrifftUeberschrift()).isTrue();
+    assertThat(stelle.gliederungsPfad()).hasSize(2);
+    assertThat(stelle.anzeigeText()).isEqualTo("Überschrift Teil 3 Abschnitt 2");
+  }
+
+  @Test
+  void ignoriertChapeauZusatz() {
+    // „in der Angabe vor Nummer 1“ ist ein verfeinernder Zusatz ohne eigene Komponente.
+    assertThat(StellenParser.parse("Absatz 1 in der Angabe vor Nummer 1").orElseThrow().anzeigeText())
+        .isEqualTo("Absatz 1");
+  }
 }
diff --git a/src/test/java/eu/mulk/aendggner/synopse/HtmlRendererTest.java b/src/test/java/eu/mulk/aendggner/synopse/HtmlRendererTest.java
index b82778c..01c25cd 100644
--- a/src/test/java/eu/mulk/aendggner/synopse/HtmlRendererTest.java
+++ b/src/test/java/eu/mulk/aendggner/synopse/HtmlRendererTest.java
@@ -3,12 +3,14 @@
 import static org.assertj.core.api.Assertions.assertThat;
 
 import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Ersetzung;
+import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Neufassung;
 import eu.mulk.aendggner.aenderung.Aenderungsbefehl.UnbekannterBefehl;
 import eu.mulk.aendggner.aenderung.Provenienz;
 import eu.mulk.aendggner.aenderung.Stelle;
 import eu.mulk.aendggner.anwendung.BefehlAnwender;
 import eu.mulk.aendggner.gesetz.Absatz;
 import eu.mulk.aendggner.gesetz.Gesetz;
+import eu.mulk.aendggner.gesetz.Gliederung;
 import eu.mulk.aendggner.gesetz.Norm;
 import java.util.List;
 import org.junit.jupiter.api.Test;
@@ -71,6 +73,32 @@
   }
 
   @Test
+  void wendetGliederungsUeberschriftAnUndRendertDiff() {
+    var alt =
+        new Gesetz(
+            "TestG",
+            "Testgesetz",
+            "TestG",
+            List.of(new Norm("§ 1", "Zweck", null, List.of(new Absatz("1", "Text.")), false)),
+            List.of(new Gliederung("030", "Teil 3", "Alte Überschrift")));
+    var befehl =
+        new Neufassung(
+            new Stelle(
+                List.of(new Stelle.Ueberschrift(), new Stelle.Gliederungseinheit("Teil", "3"))),
+            "Teil 3 Neue Überschrift",
+            PROV);
+
+    var anwendung = BefehlAnwender.anwenden(alt, List.of(befehl));
+    assertThat(anwendung.protokoll().get(0).status()).isEqualTo(BefehlAnwender.Status.ANGEWANDT);
+    assertThat(anwendung.neu().gliederungen().get(0).titel()).isEqualTo("Neue Überschrift");
+
+    var html =
+        HtmlRenderer.rendere(SynopseBuilder.baue(alt, anwendung, List.of(), false), "quelle");
+    assertThat(html).contains("Geänderte Gliederungs-Überschriften");
+    assertThat(html).contains("<del>Alte</del>").contains("<ins>Neue</ins>");
+  }
+
+  @Test
   void zeigtWarnungenAn() {
     var html = HtmlRenderer.rendere(synopse(), "quelle");