"""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