Regelwerk, Agenten, Skills und Werkzeuge veröffentlichen

Erster Stand des öffentlichen Ausschnitts aus dem Vault. Die .gitignore
arbeitet als Positivliste: ausgeschlossen ist alles, eingeschlossen nur,
was ausdrücklich wieder aufgenommen wird. Notizen, Projekte, Finanzen,
das Sitzungsgedächtnis und settings.local.json bleiben damit außen vor.

Der Kundenname in AI/Regelwerk/seo.md ist durch eine neutrale
Bezeichnung ersetzt; die Regeln selbst sind unverändert.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
Pascal MichelandClaude Opus 5 committed 2026-09-17 22:38:15 +02:00
commit c064c729ed
82 files changed
+13428

No files matched your search

@@ -0,0 +1,130 @@
"""Liest und schreibt die todo.md eines Vault-Projekts."""
from __future__ import annotations
import re
from dataclasses import dataclass
from pathlib import Path
from .abgleich import Aufgabe
CHECKBOX_MUSTER = re.compile(r"^(?P<einrueckung>[ \t]*)- \[(?P<haken>[ xX])\] (?P<rest>.*)$")
MARKER_MUSTER = re.compile(r"\s*<!--\s*plane:(?P<marker>[A-Z0-9]+-\d+)\s*-->\s*$")
ABSCHNITT_AUS_PLANE = "## Aus Plane"
@dataclass
class TodoDatei:
pfad: Path
zeilen: list[str]
frontmatter: dict[str, str]
aufgaben: list[Aufgabe]
frontmatter_zeilen: int
def _lies_frontmatter(zeilen: list[str]) -> tuple[dict[str, str], int]:
"""Gibt die flachen Schlüssel-Wert-Paare und die Anzahl belegter Zeilen zurück."""
if not zeilen or zeilen[0].strip() != "---":
return {}, 0
for index in range(1, len(zeilen)):
if zeilen[index].strip() == "---":
werte: dict[str, str] = {}
for zeile in zeilen[1:index]:
schluessel, trenner, wert = zeile.partition(":")
if trenner:
werte[schluessel.strip()] = wert.strip()
return werte, index + 1
return {}, 0
def lies(pfad: Path) -> TodoDatei:
# utf-8-sig statt utf-8: ein BOM am Dateianfang wuerde sonst als  in
# Zeile 0 stehen bleiben, das Frontmatter waere unsichtbar und der Skill
# meldete "plane_project fehlt", obwohl es dasteht. Windows-Editoren und
# PowerShell 5.1 schreiben UTF-8 regelmaessig mit BOM. Geschrieben wird
# weiterhin ohne BOM.
text = Path(pfad).read_text(encoding="utf-8-sig")
zeilen = text.split("\n")
frontmatter, belegt = _lies_frontmatter(zeilen)
aufgaben: list[Aufgabe] = []
for nummer, zeile in enumerate(zeilen):
if nummer < belegt:
continue
treffer = CHECKBOX_MUSTER.match(zeile)
if not treffer:
continue
rest = treffer.group("rest")
marker_treffer = MARKER_MUSTER.search(rest)
marker = marker_treffer.group("marker") if marker_treffer else None
text_ohne_marker = MARKER_MUSTER.sub("", rest).strip() if marker else rest.strip()
aufgaben.append(
Aufgabe(
zeilennummer=nummer,
text=text_ohne_marker,
erledigt=treffer.group("haken").lower() == "x",
marker=marker,
einrueckung=treffer.group("einrueckung"),
)
)
return TodoDatei(
pfad=Path(pfad),
zeilen=zeilen,
frontmatter=frontmatter,
aufgaben=aufgaben,
frontmatter_zeilen=belegt,
)
def plane_projekt(frontmatter: dict[str, str]) -> str | None:
wert = frontmatter.get("plane_project", "").strip()
return wert or None
def baue_zeile(einrueckung: str, text: str, erledigt: bool, marker: str | None) -> str:
haken = "x" if erledigt else " "
zeile = f"{einrueckung}- [{haken}] {text}"
if marker:
zeile = f"{zeile} <!-- plane:{marker} -->"
return zeile
def schreibe(
datei: TodoDatei,
aenderungen: dict[int, tuple[str, bool, str | None]],
neue_zeilen: list[str],
) -> str:
"""Baut den neuen Dateiinhalt. Unberührte Zeilen bleiben byteweise gleich."""
zeilen = list(datei.zeilen)
nach_nummer = {aufgabe.zeilennummer: aufgabe for aufgabe in datei.aufgaben}
for nummer, (text, erledigt, marker) in aenderungen.items():
alt = nach_nummer.get(nummer)
einrueckung = alt.einrueckung if alt else ""
zeilen[nummer] = baue_zeile(einrueckung, text, erledigt, marker)
if neue_zeilen:
zeilen = _haenge_an_aus_plane(zeilen, neue_zeilen)
return "\n".join(zeilen)
def _haenge_an_aus_plane(zeilen: list[str], neue_zeilen: list[str]) -> list[str]:
ergebnis = list(zeilen)
if ABSCHNITT_AUS_PLANE in [z.strip() for z in ergebnis]:
letzte = max(
i for i, z in enumerate(ergebnis) if z.strip() == ABSCHNITT_AUS_PLANE
)
einfuegen_ab = len(ergebnis)
for i in range(letzte + 1, len(ergebnis)):
if ergebnis[i].startswith("#"):
einfuegen_ab = i
break
while einfuegen_ab > letzte + 1 and not ergebnis[einfuegen_ab - 1].strip():
einfuegen_ab -= 1
return ergebnis[:einfuegen_ab] + neue_zeilen + ergebnis[einfuegen_ab:]
while ergebnis and not ergebnis[-1].strip():
ergebnis.pop()
return ergebnis + ["", ABSCHNITT_AUS_PLANE, ""] + neue_zeilen + [""]