= ÄndGgner -- Nichtamtliche Zentralstelle für die maschinelle Fortschreibung von Stammgesetzen anhand von Änderungsvorschriften des Bundes und der Länder Matthias Andreas Benkard // Meta :experimental: :data-uri: :sectnums: :toc: :stem: :toclevels: 2 :description: Handbuch der Nichtamtlichen Zentralstelle für die maschinelle Fortschreibung von Stammgesetzen :keywords: mulk // Settings :icons: font :source-highlighter: pygments == Resources |=== |Resource |Links |Public Artifact |https://bintray.com/mulk/maven/aendggner[Bintray] |Bug Tracker |MantisBT |=== [[building]] == Building a JAR To build an executable JAR at `target/aendggner-${REVISION}.jar`: [source,shell script] ---- ./mvnw package ---- `${REVISION}` defaults to `0.1.0-SNAPSHOT` unless you override it by passing the `-Drevision` flag to `mvnw`. == Running the Command Line Application To run the command line application after <> it: [source,shell script] ---- ./mvnw exec:java ---- [[usage]] == Usage ÄndGgner erzeugt aus einem Stammgesetz und einem Änderungsgesetz eine zweispaltige HTML-Synopse (alte Fassung links, neue rechts, Änderungen wortweise hervorgehoben). Eingaben: * Stammgesetz (Bundesrecht): XML im gii-norm-Format von https://www.gesetze-im-internet.de/[gesetze-im-internet.de] * Stammgesetz (Landesrecht): konsolidierte Fassung als PDF oder als kanonischer Klartext im Format der `--extract-only`-Ausgabe. Eine Zeile „Inhaltsübersicht“ eröffnet darin die gleichnamige Norm, auf die die Angabe-Befehle zielen; ihre Zeilen tragen das Übersichtsformat „§ N | Titel“. Amtliche Satznummern und Fußnotenmarker stehen als Unicode-Superskripte im Text („¹Die freilebende Tierwelt …“, „Enteignung⁶)“); ob sie erhalten bleiben, ergibt sich aus der geladenen Stammfassung, nicht aus einer Länderkennung. Ebenso folgt das Zitiersigel (§ oder Art.) aus den Normköpfen der Stammfassung. * Änderungsdokument: BGBl-, GVBl- oder Drucksachen-PDF (Bundestag, Bundesrat, Landtage) oder Klartext. Die Dokumentart wird aus dem Text erschlossen, nicht aus dem Dateinamen (siehe <>); Änderungsanträge dürfen zusammen mit dem Entwurf angegeben werden, den sie ändern. [source,shell script] ---- java -jar target/aendggner-0.1.0-SNAPSHOT.jar \ stammgesetz.xml aenderungsgesetz.pdf -o synopse.html ---- Wichtige Optionen: `-o, --output `:: Ausgabedatei (Default `synopse.html`; `-` = Standardausgabe). `--vollstaendig`:: Auch unveränderte Normen in die Synopse aufnehmen. `--artikel `:: Nur diesen Artikel des Änderungsgesetzes anwenden (Default: alle Artikel, deren Einleitung das Stammgesetz nennt). `--extract-only`:: Nur den bereinigten Lineartext des Änderungsgesetzes ausgeben. Nützlich, wenn die PDF-Extraktion fehlerhaft ist: Text prüfen, von Hand korrigieren und als Klartextdatei wieder einspeisen. Erkannt werden die gebräuchlichsten Änderungsbefehle des Handbuchs der Rechtsförmlichkeit (Ersetzen, Neufassung, Einfügen, Anfügen, Aufheben, Streichen, Umnummerierung) einschließlich zahlreicher Sonderformen: Bereichs- und Koordinationsziele („Die Absätze 8 und 9 werden durch die folgenden Absätze 8 bis 10 ersetzt“), strukturelle Streichungen ganzer Einheiten („§ 9 wird gestrichen“), §- und Gliederungs-Umnummerierungen („§ 9a wird zu § 9“, „Der bisherige Abschnitt 2 wird zu Abschnitt 3“), das Einfügen/Ersetzen ganzer §-Blöcke, Chapeau-Lokatoren („Im Satzteil vor Nummer 1 …“), Änderungen an Anhängen und Anlagen („Der Anhang wird wie folgt geändert: … Nach Nummer 2 wird die folgende Nummer 2a eingefügt“), Angabe-Befehle auf die Inhaltsübersicht (gefasst, ersetzt, eingefügt, gestrichen — sie werden auf die Inhaltsübersichts-Norm angewandt), das Einfügen und Ersetzen von Gliederungs-Überschriften („Nach § 33 werden die folgenden Überschriften zu Teil 3 … eingefügt“), Voranstellungen, Mehrfach-Ersetzungs- und -Einfügepaare, Einfügungen, deren Position ein Wortanker statt einer Stellenangabe bestimmt („Vor den Wörtern „Aus dem Bereich Verkehr:“ wird folgender Absatz 5 eingefügt“), Verbünde aus Umnummerierung und Folgeänderung („§ 50 wird zu § 38 und wird wie folgt geändert“, „Die bisherige Nr. 7 wird Nr. 5 und das Komma wird durch das Wort „und“ ersetzt“ — eine Satzzeichen-Operation meint dabei stets die soeben umnummerierte Einheit, weil ihr Zieltext nichts unterscheidet), Verb-Rahmen, deren Unterpunkte allein die Fundstelle tragen („Es werden ersetzt: … in § 35 Absatz 3 die Angabe „X“ jeweils durch die Angabe „Y“,“) sowie die Neufassung der Gesetzesüberschrift. Angewandt wird nicht stur in Dokumentreihenfolge: Umnummerierungen beziehen sich stets auf die ursprüngliche Zählung, nicht auf den Stand nach den vorangegangenen Punkten. Wer eine Bezeichnung räumt, kommt deshalb vor den, der sie neu besetzt. Aus dieser einen Regel folgt die absteigende Abarbeitung einer aufsteigenden Kaskade („Der bisherige Absatz 3 wird Absatz 4“, „Der bisherige Absatz 4 wird Absatz 5“, …) ebenso wie der Vorrang einer Umnummerierung vor der Einfügung, die deren Bezeichnung neu vergibt. Wer vorrückt, nimmt dabei seine eigenen Räumer mit: Auch eine über mehrere Punkte verschränkte Kaskade („Die bisherige Nr. 15 wird Nr. 16“, „Die bisherigen Nrn. 13 und 14 werden die Nrn. 14 und 15“, „Nach Nr. 12 wird folgende Nr. 13 eingefügt“) tritt deshalb geschlossen vor die Einfügung, nicht bloß mit ihrem letzten Glied. Geordnet werden nicht die Befehle, sondern die einzelnen Anwendungsschritte. Ein Verbund aus Umnummerierung und Folgeänderung trägt nämlich zwei gegenläufige zeitliche Ansprüche: Die Umnummerierung gehört nach vorn, die Folgeänderung dagegen an ihre Dokumentstelle, weil sie die vorangegangenen Punkte als vollzogen voraussetzt — „Die bisherige Nr. 11 wird Nr. 12 und die Angabe „schriftliche“ wird gestrichen“ ist vorgezogen mehrdeutig, weil erst ein früherer Punkt die zweite Fundstelle des Wortes beseitigt. Verschoben wird stets nur nach vorn. Das Protokoll bleibt gleichwohl befehlsweise: Ein Verbund gilt nur dann als angewandt, wenn jeder seiner Teile gegriffen hat, sonst nennt die Meldung den Teil und den Grund. Die PDF-Aufbereitung toleriert Drucksachen-Artefakte (Seitenköpfe und -füße, Vorabfassungs-Wasserzeichen, vertauschte oder gerade Anführungszeichen, verklebte Wortgrenzen, zerlegt kodierte Umlaute) und bestimmt die Brotschrift seitenweise, sodass auch Ministeriumsentwürfe mit gemischten Layouts vollständig extrahiert werden. Fehlt im amtlichen Satz ein schließendes Anführungszeichen, endet das Zitat an der nächsten Strukturgrenze — an einer Artikel-Überschrift oder, wo die Anführungszeichen eines Artikels nachweislich nicht aufgehen, am nächsten Aufzählungspunkt des Änderungsgesetzes; gemeldet wird das als Warnung. Auf den Beispieldaten werden damit alle Befehle der BGBl-Fassungen und der aktuellen Entwürfe angewandt (UWG/AGG/ProdHaftG: 0 manuell); was unsicher bleibt — etwa Befehle gegen eine ältere Gesetzesfassung, deren Zieltext nicht mehr existiert — landet mit Begründung im Abschnitt *Manuell prüfen* der Synopse und wird niemals stillschweigend verworfen. Für das bayerische Landesrecht versteht ÄndGgner die dortigen Konventionen: Stammgesetze gliedern sich in Artikel („Art. 6 Abs. 2 Satz 1 Nr. 2“, durchgängig abgekürzt zitiert), Änderungsgesetze dagegen in Paragraphen — auch mehrere Gesetze in einem GVBl-Heft, aus denen die auf das Stammgesetz zielenden §§ (einschließlich „Weitere Änderung“) anhand des Einleitungssatzes ausgewählt werden. Amtliche Satznummern bleiben als Superskripte erhalten und dienen als exakte Satzgrenzen; zusätzlich erkannt werden die bayerischen Befehlsformen („Fußnote 1 wird aufgehoben“, „In Satz 1 wird die Satznummerierung „1“ gestrichen“, „Dem Wortlaut werden die folgenden Abs. 1 bis 4 vorangestellt“, „Der bisherige Wortlaut wird Abs. 5“, Halbsatz-Ziele, Klauselketten mit gemeinsamem Schlussverb) sowie das Fortführungszeichen des GVBl (jedes neugefasste Aufzählungsglied öffnet erneut mit „). Der Fall ist gegen die amtliche Nachfassung belegt: Alle 154 auf das BayJG zielenden Befehle des Heftes 6/2026 werden an 54 Normen selbsttätig angewandt, auch die verschränkte Neunummerierung des Bußgeldkatalogs in Art. 56. Die übrigen Länder gliedern ihre Stammgesetze wie der Bund in Paragraphen; die Unterschiede liegen im Gesetzblatt-Satz und in Befehlsidiomen. Belegt sind Sachsen (SächsBeamtVG), Niedersachsen (NEFG) und Nordrhein-Westfalen — dort alle vier ändernden Artikel eines Heftes: Telemedienzuständigkeitsgesetz, Landesmediengesetz, Ausführungsgesetz zum 17. Rundfunkänderungsstaatsvertrag und, mit 101 Befehlen an 31 Normen der größte Fall des Heftes, das WDR-Gesetz — mit Akzeptanztests gegen die amtlichen Nachfassungen; für Schleswig-Holstein, Berlin und Hessen reicht die Prüfung bis zur Befehlserkennung — dort vollständig, aber ohne Anwendung —, weil deren Landesportale ihre Stammfassungen nur über eine anmeldepflichtige Schnittstelle ausgeben. Welche Konvention welches Land beisteuert, welche Stammfassungen woher stammen und was noch offen ist, verzeichnet `src/main/resources/sampledata/Landesrecht-Beispiele.adoc`. [[quellformate]] == Quellformate: Gesetz, Entwurf, Antrag Ein Änderungsbefehl steht nicht nur im verkündeten Gesetzblatt. Dasselbe Vorhaben durchläuft als Referenten-, Regierungs- und Fraktionsentwurf, als Änderungsantrag und als Beschlussempfehlung mehrere Fassungen, und die Frage „was gälte, wenn das durchkommt?“ stellt sich in jeder davon. ÄndGgner erschließt die Art eines Dokuments deshalb aus seinem Kopf — nie aus dem Dateinamen, der lügen kann (im Beispielkorpus heißt ein Entschließungsantrag `BT-Drs-21-7071_Beschlussempfehlung.pdf`) und nie aus einer Kennung, die von außen mitzugeben wäre. Unterschieden werden: Änderungsgesetz:: Das verkündete Artikelgesetz aus BGBl, GVBl oder GVOBl. Der Regelfall. Gesetzentwurf:: Referenten-, Regierungs- und Fraktionsentwürfe, auch als Drucksache von Bundestag, Bundesrat oder Landtag. Der Begründungsteil hinter dem Regelungstext erzeugt keine Befehle; erkannt wird er an „Begründung“ ebenso wie an den Entwurfsvarianten („A. Allgemeiner Teil“, „Zu Artikel 1“). Änderungsantrag:: Ändert nicht das Stammgesetz, sondern eine *Drucksache* — siehe unten. Beschlussempfehlung:: Trägt ihre Fassung in einer zweispaltigen Zusammenstellung, die aufgelöst wird — die Synopse zeigt dann die vom Ausschuss beschlossene Fassung (siehe unten). Dokument ohne Änderungsbefehle:: Entschließungs- und schlichter Antrag, Plenarprotokoll, Bericht. Sie werden übergangen und gemeldet — nicht stillschweigend zu null Befehlen verarbeitet. Sobald ein Entwurf, ein Antrag oder eine Beschlussempfehlung beteiligt ist, trägt die Synopse den Hinweis *Entwurfsfassung — nicht geltendes Recht*; die Quellenzeile nennt je Datei die erkannte Art. === Änderungsanträge Ein Änderungsantrag ist eine Meta-Änderung: Er ändert den Entwurf, nicht das Gesetz. Sein Rahmensatz adressiert deshalb zwei Ebenen zugleich — „In § 3 Nr. 22 wird § 18 Nr. 1 wie folgt geändert:“ nennt erst die Stelle _in der Drucksache_ (den 22. Änderungsbefehl ihres dritten Paragraphen) und dann die Stelle _in dem Text, den dieser Befehl zitiert_. Angegeben wird der Antrag zusammen mit seinem Entwurf: [source,shell script] ---- java -jar target/aendggner-0.1.0-SNAPSHOT.jar \ BayJG-alt.txt Ltg-Drs-19-9707_Gesetzentwurf.pdf \ Ltg-Drs-19-10365_Aenderungsantrag-Gruene.pdf -o synopse.html ---- ÄndGgner wendet dann erst den Antrag auf den Entwurf an und danach den so geänderten Entwurf auf das Stammgesetz; die Synopse zeigt also, was gälte, wenn Entwurf _und_ Antrag durchkämen. Welcher Entwurf gemeint ist, entscheidet die Drucksachennummer, die der Antrag selbst nennt („(Drs. 19/9707)“), nicht die Reihenfolge der Argumente. Fehlt der Entwurf, bleibt der Antrag unangewandt und wird gemeldet — ihn ersatzweise auf das Stammgesetz loszulassen wäre falsch, denn seine Stellenangaben zielen auf die Drucksache. Erkannt wird auch die elliptische Antragsform, die das Hilfsverb nur einmal in der Beschlussformel führt („1. In Nr. 1.29 die Angabe „,“ am Ende durch die Angabe „;“ ersetzt.“). === Beschlussempfehlungen: die beschlossene Fassung Die maßgebliche Fassung einer Beschlussempfehlung steht in einer zweispaltigen Zusammenstellung: links der Entwurf, rechts die Beschlüsse des Ausschusses. Anders als beim alten BGBl und beim Berliner GVBl stehen die Spalten *nicht* nacheinander im Inhaltsstrom, sondern zeilenweise verschränkt; getrennt werden sie deshalb über die Koordinaten (`PatchTextExtraktor.extrahiereSpalten`, Schnitt an der Blattmitte, aber nur an einem tatsächlichen Spaltensteg, damit ganzseitenbreite Zeilen ungeschnitten bleiben). Die rechte Spalte für sich gelesen ist allerdings kein vollständiges Dokument: Sie druckt Unverändertes nicht ab, sondern vermerkt bloß „unverändert“ — und zwar nicht nur je Gliederungspunkt, sondern auch zeilenweise innerhalb zitierter Blöcke, weshalb ihre Anführungszeichen nicht aufgehen. Eine Auflösung über die Gliederungspfade scheitert daran nachweislich. Maßgeblich ist stattdessen die *Grundlinie*: Beide Spalten sind zeilensynchron gesetzt, jeder Vermerk steht auf der Höhe der Entwurfszeile, die er meint. Der `ZusammenstellungsLeser` führt beide Spalten über Seite und Grundlinie in eine gemeinsame Lesereihenfolge zusammen und entscheidet dann Zeile für Zeile: „unverändert“ holt den Wortlaut aus der Entwurfsspalte, „entfällt“ streicht ihn, sonst gilt die Ausschussspalte. Dabei meint „unverändert“ den Wortlaut, nicht die Zählung — streicht der Ausschuss einen Punkt, rücken die folgenden auf, und seine Marke tritt an die Stelle der des Entwurfs. Die Quellenzeile der Synopse weist die verwendete Spalte als `[Beschlussempfehlung …, Ausschussfassung]` aus. Lässt sich die Zusammenstellung nicht auflösen, wird die Datei nach wie vor mit Begründung übergangen — samt Hinweis auf die Drucksachennummer des Entwurfs, der sich stattdessen eignet. Eine halb aufgelöste Fassung auszugeben wäre schlimmer als keine. Belegt ist beides: dass die *linke* Spalte Befehl für Befehl den Regierungsentwurf ergibt, aus dem die Zusammenstellung gebaut ist, und dass die aufgelöste Fassung mehr Befehle trägt als er — der Ausschuss hat der GEG-Novelle zwei Artikel hinzugefügt. Zwei Beispiele stehen im Test: BT-Drs. 20/7619 (GEG) und BT-Drs. 19/24334 (Drittes Bevölkerungsschutzgesetz). [[web]] == Web-App Neben der CLI gibt es eine Browserfassung, die dieselbe Pipeline (`eu.mulk.aendggner.Pipeline`) über ein Upload-Formular zugänglich macht: Stammgesetz- und Änderungsgesetz-Datei(en) wählen, Synopse erhalten. Sie braucht keinen Server: Die vollständige Verarbeitung — PDF-Textgewinnung mit PDFBox eingeschlossen — läuft als WebAssembly-Modul im Browser, übersetzt mit GraalVM Web Image aus demselben Java-Quelltext. Ausgeliefert werden nur statische Dateien; die gewählten Dokumente verlassen den Rechner der Nutzer:innen nicht. Bauen (verlangt Oracle GraalVM 25.1 oder neuer — Web Image ist dort enthalten, in der Community Edition nicht): [source,shell script] ---- JAVA_HOME=/pfad/zu/oracle-graalvm ./mvnw -Pwasm package ---- Ergebnis ist `target/web/` mit `index.html`, `app.js`, `worker.js`, `style.css`, `favicon.svg`, `aendggner.js` und `aendggner.js.wasm` (rund 24 MB, komprimiert etwa 7 MB). Das Verzeichnis ist so, wie es dasteht, auslieferbar: `deploy/webpaket.sh` läuft am Ende desselben Befehls, wirft den mehrere hundert Megabyte großen Textzwischenschritt `aendggner.js.wat` fort, legt den Quelltext der gebauten Fassung als `aendggner-quelltext.tar.gz` samt `quelltext-fassung.txt` bei und komprimiert die großen Dateien nach `.gz` und `.br` vor. Trägt der Arbeitsbaum uneingecheckte Änderungen, bricht der Bau ab — der beigelegte Quelltext wäre dann nicht der gebaute; für einen Probelauf hilft `QUELLTEXT_UNGEPRUEFT=1`. Lokal ansehen — `file://` genügt nicht, Browser laden Wasm-Module und Worker nur über HTTP (http://localhost:8000/): [source,shell script] ---- jwebserver -d target/web ---- Die Befehlszeilenfassung bleibt davon unberührt und ist weiterhin der Weg für Massenläufe. === Ausrollen Betrieben wird die Fassung unter einem Unterpfad einer bestehenden Domain, https://matthias.benkard.de/aendggner/ — ein eigener Server und ein eigenes Zertifikat sind dafür nicht nötig. Nötig ist nur ein Webserver für statische Dateien. [source,shell script] ---- JAVA_HOME=/pfad/zu/oracle-graalvm ./mvnw -Pwasm package rsync -av --delete target/web/ server:/var/www/aendggner/ ---- `deploy/nginx-aendggner.conf` ist dafür kein eigener `server`-Block, sondern ein Schnipsel zum Einfügen in den vorhandenen (`include`). Er bringt mit: * die Weiterleitung von `/aendggner` auf `/aendggner/`, ohne die alle relativen Verweise der Seite auf die Domainwurzel zielten; * den MIME-Typ `application/wasm`, ohne den der Browser die Instanziierung des Moduls verweigert; * `gzip_static`/`brotli_static` für die vorkomprimierten Dateien, statt 24 MB je Abruf neu zu packen; * `Cache-Control: no-cache` statt einer Haltefrist: Die Dateinamen tragen keine Fassungskennung, und ein Browser mit altem `app.js` und neuem `.wasm` bekäme sonst eine Mischfassung, die es nie gegeben hat. Revalidiert wird per ETag, das unveränderte Modul kostet dann ein 304 ohne Rumpf; * die Sicherheitskopfzeilen samt einer Content-Security-Policy. Zwei ihrer Freigaben sind unvermeidlich und in der Datei begründet: `'wasm-unsafe-eval'` für die Instanziierung des Moduls und `'unsafe-inline'` für Stile, weil die Synopse als `blob:`-Dokument die Richtlinie der erzeugenden Seite erbt, ihr Stylesheet aber eingebettet trägt. Der Footer der Startseite verweist auf den beigelegten Quelltext-Tarball; das verlangt AGPLv3 §13 für den Netzwerkbetrieb. Als fortlaufende Zweitquelle ist https://gerrit.benkard.de/plugins/gitiles/aendggner genannt. `impressum.html` und `datenschutz.html` tragen die Angaben nach § 5 DDG und Art. 13 DSGVO; die dort genannte Aufbewahrungsfrist der Zugriffsprotokolle (14 Tage) muss zu der des Servers passen. === Warum WebAssembly und nicht ein Java-Server Der Kern ist reines Java ohne Dateisystem- oder Netzzugriff; nur vier Stellen berührten die Plattform (PDFBox, MIME-Erkennung, XML-Parser, Dateizugriff). Sie sind hinter `eu.mulk.aendggner.Quelle` (Name + Bytes) und `eu.mulk.aendggner.DateiTyp` (Signaturbytes statt Tika) gebündelt, sodass Befehlszeile und Browser dieselbe Pipeline speisen. Zwei Eigenheiten von Web Image sind dabei zu beachten und im Quelltext vermerkt: * Die nativen zlib-Bindungen des JDK fehlen (`java.util.zip.Inflater`), ohne die kein PDF lesbar ist. `src/wasm/java/.../InflaterErsatz.java` ersetzt sie durch die reine Java-Umsetzung von jzlib. * Typisierte Felder lassen sich derzeit nicht nach `byte[]` umsetzen; der Dateiinhalt wandert deshalb als Base64-Text über die JS-Grenze. == Running the Tests To build and run the tests: [source,shell script] ---- ./mvnw verify ----