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:
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 + [""]
|
||||
Reference in new issue
Block a user