fix(readme): doppelte Testakten-Sektion in 117 Plugin-READMEs entfernen (#206)

Bisher haben zwei Generator-Skripte parallel die gleichen Testakten in
jede Plugin-README eingefuegt:

- scripts/inject-plugin-sofort-download-section.py
  -> Sektion '## Sofort-Downloads' mit Plugin-ZIP + Akten-Tabelle (PDF+ZIP)
- scripts/inject-plugin-testakten-section.py
  -> Sektion '## Demonstrations-Akten' mit derselben Akten-Tabelle (PDF+ZIP)

Ergebnis: 117 von 132 Plugin-READMEs listeten ihre Testakten zweimal
direkt untereinander (z.B. gesellschaftsgruender/README.md).

Konsolidierung:
- Altes Skript scripts/inject-plugin-testakten-section.py entfernt.
- Alle <!-- BEGIN plugin-testakten-section --> Bloecke aus den READMEs entfernt.
- Sofort-Downloads-Sektion regeneriert (idempotent, Marker unveraendert).

Validator: validate-plugin-structure OK.
This commit is contained in:
Klotzkette
2026-06-02 22:49:57 +02:00
committed by GitHub
parent 0d9052275a
commit ebeb39cea9
119 changed files with 0 additions and 1809 deletions
-222
View File
@@ -1,222 +0,0 @@
#!/usr/bin/env python3
"""Fuegt in jede <plugin>/README.md eine autogen-Sektion 'Demonstrations-Akten' ein,
die fuer jede zugeordnete Testakte einen Direktdownload-Link auf das Gesamt-PDF
(im Repo) UND die Akten-ZIP (aus dem GitHub-Release) anzeigt.
Quelle der Zuordnung: jede testakten/<slug>/README.md und die globale
testakten/README.md, soweit dort bestehende Plugin-Namen per Backtick
(`plugin-name`) referenziert werden. Damit bleiben Einzelakten und zentrale
Uebersicht gleichermassen massgeblich.
Idempotent ueber HTML-Marker. Position: direkt nach dem H1, vor anderen Sektionen
und insbesondere vor der bestehenden Installation-Section mit dem Plugin-ZIP.
"""
from __future__ import annotations
import json
import re
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
TESTAKTEN_DIR = REPO_ROOT / "testakten"
MARKER_BEGIN = "<!-- BEGIN plugin-testakten-section (autogen) -->"
MARKER_END = "<!-- END plugin-testakten-section (autogen) -->"
RELEASE_BASE = (
"https://github.com/Klotzkette/claude-fuer-deutsches-recht/releases/latest/download"
)
H1_RE = re.compile(r"^# .+$", re.MULTILINE)
BACKTICK_RE_TPL = r"`{name}`"
# Einzelne ältere Akten nennen Skill- oder Sachgebietsnamen, die fachlich einem
# bestehenden Plugin entsprechen. Diese Aliase werden nur für die README-
# Downloadsektionen verwendet; die Akten selbst bleiben unverändert.
PLUGIN_ALIASES = {
"bauplanungsrecht": ["normenkontrolle-bauleitplanung"],
"cisg-handelskauf": ["urteilsbauer-relationsmacher"],
"dsgvo": ["datenschutzrecht"],
"internationales-privatrecht": ["urteilsbauer-relationsmacher"],
}
def add_mapping(
mapping: dict[str, set[str]],
plugin_names: set[str],
slug: str,
text: str,
) -> None:
"""Fuege alle Plugin-Backticks aus text fuer slug in mapping ein."""
tokens = set(re.findall(r"`([^`]+)`", text))
for token in tokens:
if token in plugin_names:
mapping.setdefault(token, set()).add(slug)
for alias_target in PLUGIN_ALIASES.get(token, []):
if alias_target in plugin_names:
mapping.setdefault(alias_target, set()).add(slug)
def discover_mapping() -> dict[str, list[str]]:
"""Sammle Plugin->Akten-Verbindungen aus Einzel-README und Gesamtuebersicht."""
mapping: dict[str, set[str]] = {}
if not TESTAKTEN_DIR.exists():
return {}
marketplace = json.loads(
(REPO_ROOT / ".claude-plugin" / "marketplace.json").read_text(encoding="utf-8")
)
plugin_names = {p["name"] for p in marketplace["plugins"]}
for sub in sorted(TESTAKTEN_DIR.iterdir()):
if not sub.is_dir():
continue
readme = sub / "README.md"
if readme.exists():
add_mapping(mapping, plugin_names, sub.name, readme.read_text(encoding="utf-8"))
overview = TESTAKTEN_DIR / "README.md"
if overview.exists():
for line in overview.read_text(encoding="utf-8").splitlines():
m = re.match(r"\| \[`([^/]+)/`\]\(\./\1/\) \|.*\| (.*) \|$", line)
if m:
add_mapping(mapping, plugin_names, m.group(1), m.group(2))
return {p: sorted(v) for p, v in mapping.items()}
def short_title(slug: str) -> str:
"""Lies den H1-Titel aus testakten/<slug>/README.md, bereinige Praefixe.
Fallback: slug mit Bindestrichen zu Leerzeichen."""
rd = TESTAKTEN_DIR / slug / "README.md"
if rd.is_file():
for line in rd.read_text(encoding="utf-8").splitlines():
m = re.match(r"^# (.+)$", line.strip())
if m:
title = m.group(1).strip()
title = re.sub(
r"^(Akte|Beispielakte|Testakte|Mandantenakte)\s*[:-]\s*",
"",
title,
flags=re.IGNORECASE,
)
return title
return slug.replace("-", " ")
def section_block(plugin_name: str, akten: list[str]) -> str:
"""Erzeuge die Demonstrations-Akten-Sektion fuer ein Plugin."""
if not akten:
return ""
rows: list[str] = []
for slug in akten:
pdf_repo = f"../testakten/{slug}/gesamt-pdf/{slug}_gesamt.pdf"
zip_release = f"{RELEASE_BASE}/testakte-{slug}.zip"
title = short_title(slug)
rows.append(
f"| **{title}** (`{slug}`) "
f"| [Gesamt-PDF lesen]({pdf_repo}) "
f"| [Akten-ZIP herunterladen]({zip_release}) |"
)
rows_md = "\n".join(rows)
plural = "Akten demonstrieren" if len(akten) > 1 else "Akte demonstriert"
intro = (
f"Folgende anonymisierte {plural} dieses Plugin im laufenden Mandatsbetrieb. "
f"Das Gesamt-PDF ist sofort im Browser lesbar. Das Akten-ZIP enthaelt saemtliche "
f"Originaldateien (Markdown-Aktenstuecke, Tabellen, E-Mails, PDFs, DOCX, XLSX, Bildanlagen) "
f"im Originalordnerlayout."
)
return f"""{MARKER_BEGIN}
## Demonstrations-Akten
{intro}
| Akte | Lesen | Herunterladen |
| --- | --- | --- |
{rows_md}
Die ZIP-URLs sind stabil und zeigen immer auf die aktuelle Version. Eine vollstaendige Aktenuebersicht steht in [`testakten/README.md`](../testakten/README.md).
{MARKER_END}
"""
def inject(plugin_dir: Path, plugin_name: str, akten: list[str]) -> str:
readme = plugin_dir / "README.md"
if not readme.exists():
return "skip (kein README)"
text = readme.read_text(encoding="utf-8")
new_section = section_block(plugin_name, akten)
# Falls Plugin (noch) keine Akten hat: bestehende Sektion entfernen
if not new_section:
pat = re.compile(
re.escape(MARKER_BEGIN) + r".*?" + re.escape(MARKER_END) + r"\n?",
re.DOTALL,
)
if pat.search(text):
new_text = pat.sub("", text, count=1)
readme.write_text(new_text, encoding="utf-8")
return "removed (keine Akten zugeordnet)"
return "skip (keine Akten, nichts zu entfernen)"
# Falls Sektion existiert: ersetzen
pat = re.compile(
re.escape(MARKER_BEGIN) + r".*?" + re.escape(MARKER_END) + r"\n?",
re.DOTALL,
)
if pat.search(text):
new_text = pat.sub(new_section, text, count=1)
if new_text == text:
return "unchanged"
readme.write_text(new_text, encoding="utf-8")
return "updated"
# Erstmalig einfuegen direkt nach H1
m = H1_RE.search(text)
if not m:
new_text = new_section + "\n" + text
else:
end = m.end()
rest = text[end:]
if rest.startswith("\n\n"):
insert_at = end + 2
elif rest.startswith("\n"):
insert_at = end + 1
else:
insert_at = end
new_text = text[:insert_at] + "\n" + new_section + "\n" + text[insert_at:]
readme.write_text(new_text, encoding="utf-8")
return "inserted"
def main() -> int:
marketplace = json.loads(
(REPO_ROOT / ".claude-plugin" / "marketplace.json").read_text(encoding="utf-8")
)
plugin_names = [p["name"] for p in marketplace["plugins"]]
mapping = discover_mapping()
stats = {
"inserted": 0,
"updated": 0,
"unchanged": 0,
"removed": 0,
"skip": 0,
}
for name in sorted(plugin_names):
plugin_dir = REPO_ROOT / name
akten = mapping.get(name, [])
result = inject(plugin_dir, name, akten)
key = result.split()[0]
if key not in stats:
key = "skip"
stats[key] += 1
print(f" {result.upper():<10} {name} ({len(akten)} Akte/n)")
print(
f"\nFertig: {stats['inserted']} neu, {stats['updated']} aktualisiert, "
f"{stats['unchanged']} unveraendert, {stats['removed']} entfernt, "
f"{stats['skip']} uebersprungen"
)
return 0
if __name__ == "__main__":
sys.exit(main())