fix(readmes): doppelten TESTAKTEN-SECTION-Block aus 84 Plugin-READMEs entfernt

Der frueher autogenerierte '<!-- BEGIN TESTAKTEN-SECTION (auto-generated) -->'-
Block ist in 84 Plugin-READMEs wieder aufgetaucht und stand redundant
direkt neben dem aktuellen 'plugin-sofort-download-section'-Block.
Beispiel: normenkontrolle-bauleitplanung/README.md, wo der alte Block
zusaetzlich nur 1 von 2 vorhandenen Testakten zeigte.

Die Plugins selbst sind technisch unveraendert (plugin.json + skills/
in Ordnung); Aenderung ist reine README-Kosmetik. Kein Versions-Bump
noetig, Release-ZIPs sind nicht betroffen.

scripts/inject-testakten-section.py: komplett auf 'Entferner' umgebaut.
Es injiziert nichts mehr in Plugin-READMEs, sondern bereinigt nur noch
zurueckkehrende Altblocke idempotent. Plugin-READMEs erhalten ihre
Testakten-Auflistung ausschliesslich aus inject-plugin-sofort-download-section.
This commit is contained in:
Klotzkette
2026-06-03 21:50:19 +00:00
parent 5c92805eec
commit 6868098d60
85 changed files with 25 additions and 1392 deletions
+25 -339
View File
@@ -1,366 +1,52 @@
#!/usr/bin/env python3
"""
Fuegt in jeder Plugin-README, fuer die Testakten existieren, eine
Sektion '## Testakte(n)' direkt unter dem Direkt-Download-Block ein.
Bestehende, manuell formulierte Testakten-Sektionen werden NICHT
geloescht; stattdessen wird sie nach oben verschoben, indem ein
ein-/ausgegrenzter automatischer Block ergaenzt wird.
Entfernt den frueher autogenerierten '<!-- BEGIN TESTAKTEN-SECTION (auto-generated) -->'-
Block aus allen Plugin-READMEs. Plugin-READMEs erhalten ihre Testakten-Auflistung
inzwischen ausschliesslich aus 'scripts/inject-plugin-sofort-download-section.py'.
Idempotent: HTML-Kommentar-Marker BEGIN/END TESTAKTEN.
Dieses Skript dient nur noch der Bereinigung: Wenn ein alter Block irgendwo
zurueckkehrt (z. B. durch Wiedereinspielen alter Skripte), entfernt der Aufruf
ihn idempotent. Es wird NICHTS injiziert.
"""
from __future__ import annotations
import os
import re
from pathlib import Path
BEGIN = "<!-- BEGIN TESTAKTEN-SECTION (auto-generated) -->"
END = "<!-- END TESTAKTEN-SECTION (auto-generated) -->"
REPO = Path(__file__).resolve().parent.parent
# Mapping Plugin -> Liste der Testakten-Ordnernamen
PLUGIN_TESTAKTEN: dict[str, list[str]] = {
"arbeitszeugnis-analyse": ["arbeitszeugnis-analyse-bluehendes-leben"],
"aussenwirtschaft-zoll-sanktionen": ["aussenwirtschaft-zoll-sanktionen-globalmaschinen"],
"bav-strategie-konzern": ["bav-strategie-konzern-meissner-rheinwerk-ag"],
"beamtenrecht": [
"beamtenrecht-auslandseinsatz-mexiko-wasserbau-besoldung",
"beamtenrecht-befoerderungskaskade-landesamt-weserbruecke",
"beamtenrecht-dienstunfall-burnout-wiedereingliederung-hafenpolizei",
"beamtenrecht-richterlaufbahn-besoldung-mondsee",
"beamtenrecht-schulleitung-hannover-konkurrentenstreit",
],
"betaeubungsmittelrecht": ["betaeubungsmittelrecht-apotheke-substitution-festival"],
"betreuungsrecht": [
"betreuung-hildegard-sauer",
"betreuung-schmalfeld-kontodaten-vertraege",
],
"bgb-at-pruefer": ["bgb-at-altfraenkische-werkstatt"],
"common-law-kompass": ["common-law-kompass-crossborder-contract"],
"datenschutzrecht": ["datenschutz-us-transfer-cloudsuite-rheinmain"],
"deutsche-rechtsgeschichte": [
"deutsche-rechtsgeschichte-restitution-bgb-ddr-kontinuitaet"
],
"designrecht-geschmacksmusterrecht": [
"designrecht-geschmacksmuster-lichtbogen-stuhl-copycat"
],
"dsa-dma-digitalregulierung": ["dsa-dma-bayrische-baustube-meissner"],
"einigungsvertrag-vermoegensrecht": [
"einigungsvertrag-treuhand-mauergrundstueck-lindenau"
],
"einfache-leichte-sprache-jura": ["einfache-leichte-sprache-jura-mandantenbrief"],
"energierecht": ["energierecht-stadtwerke-quartier"],
"europarecht-kompass": ["europarecht-kompass-beihilfe-richtlinie"],
"fachanwalt-verwaltungsrecht": ["bvg-widerspruchsstelle-abschleppen-mobg"],
"festlandchina-wirtschaftsverkehr": [
"festlandchina-wirtschaftsverkehr-fabrik-import-investition"
],
"fluggastrechte": ["fluggastrechte-familie-braeutigam"],
"forschungszulage-antragstellung": ["forschungszulage-sensorik-startup-taunus"],
"forderungsmanagement-klagewerkstatt": ["inkasso-zahlungsklage-modefuchs"],
"geldwaeschepraevention-aml-kyc": ["geldwaesche-aml-kyc-musterholding"],
"gesellschaftsgruender": ["gesellschaftsgruender-streit-roeschen-tech"],
"gesellschaftsrecht-legal-english": ["gesellschaftsrecht-legal-english-frankfurt-startup"],
"gebrauchsmusterrecht": ["gebrauchsmusterrecht-schnellverschluss-sensorhalter"],
"haushaltsrecht-bho-bund-laender": [
"haushaltsrecht-bho-szenario-buergergeld-verteidigung"
],
"hoai-leistungsphasen-praxis": ["hoai-leistungsphasen-kita-muehlenhof-lichtenrade-2026"],
"informationsfreiheit-presseauskunft": [
"informationsfreiheit-presseauskunft-klinikdaten-hafenstadt"
],
"insolvenzforderungsanmeldungspruefung": [
"insolvenzforderungsanmeldungspruefung-phoenix-solar"
],
"insolvenzplan-starug-planwerkstatt": [
"insolvenzplan-starug-planwerkstatt-metallbau-hansa"
],
"insolvenzverwaltung": [
"insolvenzverwaltung-moebelwerk-havelberg-regelverfahren",
"insolvenzverwaltung-nordlicht-handels-kiel",
],
"internationales-handelsrecht-lex-mercatoria": [
"internationales-handelsrecht-cisg-incoterms-schiedsfall"
],
"jveg-kostenpruefer": ["jveg-zeugin-berger-lg-tuebingen"],
"kanzlei-allgemein": ["kanzlei-allgemein-alltag"],
"kanzlei-management": ["kanzlei-management-falkenried-partnerkreis-q2-2026"],
"ki-richtlinie-kanzleien": ["ki-richtlinie-musterkanzlei"],
"ki-vo-ai-act-pruefer": ["ki-vo-konformitaetsbescheinigung-bewerberpilot"],
"kommunalrecht-laender": [
"kommunalrecht-rathaus-morgenfurt-buergerentscheid-haushalt"
],
"krisenfrueherkennung-starug": ["krisenfrueherkennung-starug-vier-varianten"],
"legistik-werkstatt": ["legistik-pflichtpostfach"],
"lobbyregister-bundestag": [
"lobbyregister-buergerinitiative-waldmoor",
"lobbyregister-dublin-bank-frankfurt-branch",
"lobbyregister-public-affairs-agentur-wasserstoff",
],
"luftrecht-flughafenrecht": [
"luftrecht-airline-insolvenz-flugzeugpfand-flughafen"
],
"markenrecht-fashion-luxus": [
"markenrecht-fashion-klotzzkette-vs-brezelmann-donauzon"
],
"meinungspruefer": ["meinungspruefer-grenzfaelle-alltag"],
"nachbarschaftsstreit-pruefer": ["nachbarschaftsstreit-horrorfall-rosengarten"],
"normenkontrolle-bauleitplanung": ["bebauungsplan-augsburg-bahnhofsareal"],
"oeffentliches-wirtschaftsrecht": [
"oeffentliches-wirtschaftsrecht-oepp-schulcampus-havelstadt"
],
"ordnungswidrigkeitenrecht": [
"ordnungswidrigkeitenrecht-bussgeldmix-gewerbe-datenschutz-tier"
],
"patentrecht": ["patentrecht-erfindungsakten-ravenstein-moll"],
"phishing-vorfall-pruefer": ["phishing-vorfall-mayer-sparkasse-berlin"],
"arbeitsrecht": [
"befristungskontrollklage-vogt-stadtwerke",
"kuendigungsschutzklage-weber-techlogix",
],
"schriftform-und-textform-bgb": [
"schriftform-maklervertrag-muenchen-eheleute-haspelbeck",
"schriftform-mietkuendigung-bielefeld-online-pferdedrescher",
],
"roemisches-recht": ["roemisches-recht-kauf-besitz-erbschaft-pergamentfall"],
"selbstvertreter-amtsgericht": ["selbstvertreter-amtsgericht-kuechentisch-kaufpreis"],
"selbstvertreter-sozialgericht": ["selbstvertreter-sozialgericht-heizkosten-eilantrag"],
"solo-selbststaendige-praxis": ["solo-selbststaendige-designstudio-luise-falkensee-2026"],
"fachanwalt-sozialrecht": ["sozialrecht-rollstuhl-tannenberg"],
"fashion-law-moderecht": ["fashion-law-moderecht-nachtfalter-kollektion-2026"],
"seerecht-schifffahrtsrecht": ["seerecht-schiffshypothek-werft-wrack-bermuda"],
"steuerrecht-anwalt-und-berater": [
"beispielakte-edelholz-berlin",
"fortbestehensprognose-paragrafix-gmbh",
"grosskanzlei-corporate-ma-datenraum",
"grunderwerbsteuer-sharedeal-closing-waldkrone",
"grundsteuer-rosenwinkel-bescheidkette",
],
"strafbefehl-verteidiger": ["strafbefehl-ladendiebstahl-fahrerflucht"],
"strassenrecht-infrastruktur": ["strassenrecht-ortsdurchfahrt-bruecke-auenfeld"],
"strassenverkehrsrecht-stvo": [
"strassenverkehrsrecht-stvo-schulstrasse-lieferzone"
],
"tierschutzrecht": ["tierschutzrecht-veterinaeramt-pferdehof-auenwiese"],
"umweltrecht": ["umweltrecht-industrieanlage-genehmigung"],
"umweltschutzverband-verbandsklage": [
"umweltschutzverband-windpark-moorbach-umwrg"
],
"urteilsbauer-relationsmacher": [
"lumen-studios-insolvenz-strafverfahren",
"solis-vision-x-smartglasses",
],
"verkehr-infrastrukturrecht": ["verkehr-infrastrukturrecht-strassenbahn-ladezonen"],
"verkehrsowi-verteidiger": ["verkehrsowi-rotlicht-tempo"],
"venture-capital-geber": ["venture-capital-geber-nebelstern-portfolio-berlin"],
"verbraucherschutzrecht-pruefer": [
"verbraucherschutzrecht-smartmeter-abo-plattform"
],
"verbraucherschutzverband-durchsetzung": [
"verbraucherschutzverband-abo-falle-sammelklage"
],
"wahlkampfrecht-praxis": ["wahlkampfrecht-landtagswahl-morgenstadt-2026"],
"verlagsredaktion": ["verlagsredaktion-morgenlage-fachverlag"],
"vertragsausfueller": ["vertragsausfueller-bsag-kiosk-huckelriede"],
"vertragsrecht": ["sachverstaendigengutachten-ki-vorwurf-lg-regensburg-sieglinger"],
"wandeldarlehen-lebenszyklus": ["wandeldarlehen-beispielcase"],
"weg-hausverwaltung": ["weg-hausverwaltung-hohenzollernhof"],
"zwangsverwaltung-zvg": [
"zwangsverwaltung-friedrichshoefe-berlin",
"zwangsverwaltung-zvg-mietshaus-parkstrasse",
"zwangsverwaltung-zvg-versteigerung-eppendorf-altbau",
],
"handelsregister-praxis": ["handelsregister-registergericht-rabenhof-gmbh-2026"],
"grundbuchamt-praxis": ["grundbuchamt-briefgrundschuld-wegerecht-altenau-2026"],
"erbbaurecht-praxis": ["erbbaurecht-erbbauzins-heimfall-lindenwerder-2026"],
"zwangsvollstreckung": ["vollstreckungsmappe-mueller-sparkasse-niederrhein"],
}
def get_testakte_title(akte_dir: Path) -> str:
readme = akte_dir / "README.md"
if not readme.is_file():
return akte_dir.name
first = readme.read_text(encoding="utf-8").splitlines()[0]
# Strip leading '# ' and 'Akte:' / 'Akte ' Prefix
title = re.sub(r"^#+\s*", "", first).strip()
title = re.sub(r"^Akte\s*:?\s*", "", title, flags=re.IGNORECASE).strip()
return title or akte_dir.name
def build_testakten_section(plugin: str, akten: list[str]) -> str:
if not akten:
return ""
lines = [
BEGIN,
"",
"## Testakte" + ("n" if len(akten) > 1 else ""),
"",
]
if len(akten) == 1:
akte = akten[0]
title = get_testakte_title(REPO / "testakten" / akte)
lines.append(
f"Zu diesem Plugin existiert eine vollständige Beispielakte: "
f"**{title}** ([`testakten/{akte}/`](../testakten/{akte}/))."
)
lines.append("")
lines.append(
f"Direkt-Download als ZIP: "
f"[testakte-{akte}.zip]("
f"https://github.com/Klotzkette/claude-fuer-deutsches-recht/releases/latest/download/testakte-{akte}.zip)"
)
else:
lines.append(
f"Zu diesem Plugin existieren {len(akten)} vollständige Beispielakten:"
)
lines.append("")
lines.append("| Akte | Quelle | Direkt-Download |")
lines.append("|---|---|---|")
for a in akten:
title = get_testakte_title(REPO / "testakten" / a)
lines.append(
f"| {title} | [`testakten/{a}/`](../testakten/{a}/) | "
f"[testakte-{a}.zip](https://github.com/Klotzkette/claude-fuer-deutsches-recht/releases/latest/download/testakte-{a}.zip) |"
)
lines.append("")
lines.append("Die Akte ist absichtlich unordentlich, widersprüchlich und ungefiltert. Sie eignet sich für End-to-End-Tests, Demos und zum Üben.")
lines.append("")
lines.append(END)
return "\n".join(lines)
def find_insert_position(text: str) -> int:
"""Finde die Position direkt nach dem Direkt-Download-Block.
Heuristik: Suche '## Direkt-Download', danach die naechste '## ' Section.
Falls nicht gefunden, suche nach Plugin-Description (vor erstem '## Start' o.ae.).
Falls auch das fehlt: nach H1-Titel + leerer Zeile + erstem Absatz.
"""
# Neuere READMEs haben oben einen markierten Sofort-Download-Block. Die
# Testakten-Sektion darf nicht in diesen Marker hineingeschoben werden.
sofort_end = "<!-- END plugin-sofort-download-section (autogen) -->"
idx = text.find(sofort_end)
if idx != -1:
return idx + len(sofort_end)
# Variant 1: nach Direkt-Download-Sektion. Heading darf beliebig dekoriert sein,
# z.B. '## Direkt-Download', '## ⬇️ Direkt-Download (einzelnes ZIP)',
# '## Arbeitsakte (Direkt-Download)', '## Direkt-Download (je ein ZIP pro Akte)'.
m = re.search(
r"^##[^\n]*Direkt-Download[^\n]*\n.*?(?=^## |\Z)",
text,
re.MULTILINE | re.DOTALL,
)
if m:
return m.end()
# Variant 2: vor erster '##'-Section
m = re.search(r"^## ", text, re.MULTILINE)
if m:
return m.start()
# Variant 3: ans Ende
return len(text)
OLD_HEADERS = (
"## Testakte",
"## Testakten",
"## Beispielakte",
"## Beispielakten",
"## Mit-Testakte",
"## Mit Testakte",
PATTERN = re.compile(
r"\n*" + re.escape(BEGIN) + r".*?" + re.escape(END) + r"\n*",
re.DOTALL,
)
def remove_old_manual_sections(text: str) -> str:
"""Entfernt manuell platzierte H2-Sektionen mit Testakten-Titel,
AUSSER sie liegen innerhalb des Auto-Blocks BEGIN/END."""
# Maskiere zuerst den Auto-Block, damit wir ihn nicht anfassen.
auto_match = re.search(
re.escape(BEGIN) + r".*?" + re.escape(END),
text,
re.DOTALL,
)
placeholder = "<<<AUTO_BLOCK_PLACEHOLDER>>>"
if auto_match:
auto_content = auto_match.group(0)
text = text.replace(auto_content, placeholder, 1)
else:
auto_content = None
# Entferne alle alten Testakten-H2-Sektionen (bis zur naechsten H2).
for header in OLD_HEADERS:
pattern = (
r"^" + re.escape(header) + r"\b.*?(?=^## |\Z)"
)
text = re.sub(pattern, "", text, flags=re.MULTILINE | re.DOTALL)
if auto_content is not None:
text = text.replace(placeholder, auto_content, 1)
# Mehrfach-Leerzeilen auf max. 2 reduzieren
text = re.sub(r"\n{3,}", "\n\n", text)
return text
def update_readme(readme: Path, plugin: str, akten: list[str]) -> bool:
if not readme.is_file():
def clean_readme(readme: Path) -> bool:
txt = readme.read_text(encoding="utf-8")
if BEGIN not in txt:
return False
original = readme.read_text(encoding="utf-8")
block = build_testakten_section(plugin, akten)
if not block:
return False
new = original
# 1. Falls vorhandener Auto-Block existiert: entfernen
if BEGIN in new and END in new:
new = re.sub(
re.escape(BEGIN) + r".*?" + re.escape(END) + r"\n*",
"",
new,
count=1,
flags=re.DOTALL,
)
# 2. Alte manuell platzierte Testakten-Sektionen entfernen
new = remove_old_manual_sections(new)
# 3. Block oben einfuegen (nach Direkt-Download)
pos = find_insert_position(new)
before = new[:pos].rstrip("\n") + "\n\n"
after = new[pos:].lstrip("\n")
new = before + block + "\n\n" + after
if new == original:
new = PATTERN.sub("\n\n", txt)
new = re.sub(r"\n{3,}", "\n\n", new)
if new == txt:
return False
readme.write_text(new, encoding="utf-8")
return True
def main() -> int:
os.chdir(REPO)
changed = 0
skipped = 0
for plugin, akten in sorted(PLUGIN_TESTAKTEN.items()):
readme = REPO / plugin / "README.md"
if not readme.is_file():
print(f" SKIP {plugin}: keine README.md")
skipped += 1
def main() -> None:
removed = 0
for readme in sorted(REPO.glob("*/README.md")):
# testakten/-Ordner ausschliessen
if readme.parts[-2] == "testakten":
continue
# Pruefe, ob alle Testakten-Ordner existieren
existing = [a for a in akten if (REPO / "testakten" / a).is_dir()]
if not existing:
print(f" SKIP {plugin}: keine Testakten-Ordner gefunden")
skipped += 1
continue
if update_readme(readme, plugin, existing):
changed += 1
print(f" UPD {plugin}: {len(existing)} Akte(n)")
print(f"\nFertig: {changed} READMEs aktualisiert, {skipped} uebersprungen.")
return 0
if clean_readme(readme):
removed += 1
print(f" RM {readme.relative_to(REPO)}")
print(f"\nFertig: {removed} Plugin-READMEs bereinigt.")
if __name__ == "__main__":
raise SystemExit(main())
main()