Files
claude-regelwerk/.claude/skills/plane-abgleich/scripts/plane/abgleich.py
T
Pascal MichelandClaude Opus 5 c064c729ed 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]>
2026-09-17 22:38:15 +02:00

143 lines
3.9 KiB
Python

"""Drei-Wege-Vergleich zwischen todo.md, Plane und dem Stand des letzten Laufs."""
from __future__ import annotations
from dataclasses import dataclass
ANLEGEN = "ANLEGEN"
NACH_PLANE = "NACH_PLANE"
NACH_VAULT = "NACH_VAULT"
KONFLIKT = "KONFLIKT"
ANHAENGEN = "ANHAENGEN"
ZEILE_ENTFERNT = "ZEILE_ENTFERNT"
ITEM_ENTFERNT = "ITEM_ENTFERNT"
UNVERAENDERT = "UNVERAENDERT"
@dataclass(frozen=True)
class Aufgabe:
"""Eine Checkbox-Zeile aus todo.md."""
zeilennummer: int
text: str
erledigt: bool
marker: str | None
einrueckung: str = ""
@dataclass(frozen=True)
class WorkItem:
id: str
marker: str
titel: str
erledigt: bool
@dataclass(frozen=True)
class Basiseintrag:
marker: str
work_item_id: str
titel: str
erledigt: bool
@dataclass(frozen=True)
class Aktion:
art: str
marker: str | None
vault: Aufgabe | None = None
plane: WorkItem | None = None
basis: Basiseintrag | None = None
ERLEDIGT_GRUPPEN = ("completed", "cancelled")
ZIEL_GRUPPE_ERLEDIGT = "completed"
ZIEL_GRUPPE_OFFEN = "unstarted"
def status_fuer(states: list[dict], erledigt: bool) -> str:
"""Erste State-UUID der Zielgruppe, sortiert nach sequence."""
gruppe = ZIEL_GRUPPE_ERLEDIGT if erledigt else ZIEL_GRUPPE_OFFEN
passend = [s for s in states if s.get("group") == gruppe]
if not passend:
raise ValueError(
f"Das Projekt hat keinen Status der Gruppe „{gruppe}“. "
f"Vorhanden sind: {sorted({s.get('group') for s in states})}."
)
return min(passend, key=lambda s: s.get("sequence", 0))["id"]
def item_erledigt(state_gruppe: str) -> bool:
return state_gruppe in ERLEDIGT_GRUPPEN
def _abweichung(titel_a: str, erledigt_a: bool, titel_b: str, erledigt_b: bool) -> bool:
return titel_a != titel_b or erledigt_a != erledigt_b
def berechne(
aufgaben: list[Aufgabe],
work_items: list[WorkItem],
basis: dict[str, Basiseintrag],
) -> list[Aktion]:
"""Drei-Wege-Vergleich. Ruft nichts auf und schreibt nichts."""
aktionen: list[Aktion] = []
items_nach_marker = {item.marker: item for item in work_items}
gesehene_marker: set[str] = set()
for aufgabe in aufgaben:
if aufgabe.marker is None:
aktionen.append(Aktion(art=ANLEGEN, marker=None, vault=aufgabe))
continue
gesehene_marker.add(aufgabe.marker)
item = items_nach_marker.get(aufgabe.marker)
eintrag = basis.get(aufgabe.marker)
if item is None:
aktionen.append(
Aktion(art=ITEM_ENTFERNT, marker=aufgabe.marker, vault=aufgabe, basis=eintrag)
)
continue
if eintrag is None:
# Ohne Basis lässt sich nicht entscheiden, wer sich bewegt hat.
art = (
UNVERAENDERT
if not _abweichung(aufgabe.text, aufgabe.erledigt, item.titel, item.erledigt)
else KONFLIKT
)
aktionen.append(
Aktion(art=art, marker=aufgabe.marker, vault=aufgabe, plane=item)
)
continue
vault_geaendert = _abweichung(
aufgabe.text, aufgabe.erledigt, eintrag.titel, eintrag.erledigt
)
plane_geaendert = _abweichung(
item.titel, item.erledigt, eintrag.titel, eintrag.erledigt
)
if vault_geaendert and plane_geaendert:
art = KONFLIKT
elif vault_geaendert:
art = NACH_PLANE
elif plane_geaendert:
art = NACH_VAULT
else:
art = UNVERAENDERT
aktionen.append(
Aktion(art=art, marker=aufgabe.marker, vault=aufgabe, plane=item, basis=eintrag)
)
for item in work_items:
if item.marker in gesehene_marker:
continue
eintrag = basis.get(item.marker)
art = ZEILE_ENTFERNT if eintrag else ANHAENGEN
aktionen.append(Aktion(art=art, marker=item.marker, plane=item, basis=eintrag))
return aktionen