#!/usr/bin/env python3 """quittungen.py — bekannte Befunde quittieren, damit Gruen wieder etwas bedeutet. Hintergrund (#0104): Das Alarmprinzip der Gruppe ist "die rote Pipeline IST der Alarm" (AGENTS.md). Das traegt nur, solange Gruen der Normalfall ist. Am 2026-08-18 waren alle drei geplanten Pruefungen dauerhaft rot — und ein neun Tage alter, echter Ausfall blieb genau deshalb unbemerkt: ein rotes Kreuz mehr neben roten Kreuzen faellt niemandem auf. Eine Quittung nimmt einen bekannten Befund aus der Rot-Wertung. Sie verschweigt ihn NICHT: er erscheint weiterhin im Log, mit Grund und Frist. Rot wird, was nicht quittiert ist — also das Neue. Damit die Liste nicht selbst zur naechsten Blindstelle wird (genau der Vorwurf, den sich diese Plattform schon zweimal gemacht hat), gilt: * Jede Quittung braucht eine Frist. Ist sie abgelaufen, quittiert sie nicht mehr — der Befund faerbt wieder rot, mit Hinweis auf die abgelaufene Zeile. * Dauerhaft (`ADR-00NN` statt Datum) geht nur mit Verweis auf einen Entscheidungs-Record. Eine dauerhafte Ausnahme ohne ADR ist laut AGENTS.md ohnehin ein Fehler; hier ist sie schlicht nicht formulierbar. * Eine Quittung, auf die kein Befund mehr passt, wird gemeldet. Sonst sammelt die Datei stillschweigend Zeilen fuer Probleme, die es nicht mehr gibt. Format von befund_quittungen.tsv (Tab-getrennt, '#' ist Kommentar): pruefung muster gueltig_bis grund `pruefung` ist der Geltungsbereich (`stillstand` oder `gruppe`). Ohne ihn meldet jede Pruefung die Eintraege der jeweils anderen als wirkungslos — beim ersten Lauf sofort passiert. `muster` ist eine Teilzeichenkette des Befundtextes (keine Regex — bewusst, damit die Datei ohne Spezialwissen lesbar bleibt). """ from __future__ import annotations import datetime as _dt import re from pathlib import Path DATEI = "scripts/befund_quittungen.tsv" def _lade(root: Path, pruefung: str) -> list[tuple[str, str, str]]: pfad = root / DATEI if not pfad.exists(): return [] zeilen = [] for roh in pfad.read_text(encoding="utf-8").splitlines(): if not roh.strip() or roh.lstrip().startswith("#"): continue teile = roh.split("\t") if len(teile) < 4 or teile[0].strip() != pruefung: continue zeilen.append((teile[1].strip(), teile[2].strip(), "\t".join(teile[3:]).strip())) return zeilen def anwenden(befunde: list[str], root: Path, pruefung: str, heute: _dt.date | None = None): """Teilt befunde in (offen, quittiert, meldungen). offen -> faerben rot quittiert -> Liste (Befund, Frist, Grund), nur Ausgabe meldungen -> Hinweise zur Liste selbst (abgelaufen, wirkungslos) """ heute = heute or _dt.date.today() regeln = _lade(root, pruefung) offen: list[str] = [] quittiert: list[tuple[str, str, str]] = [] meldungen: list[str] = [] getroffen: set[int] = set() for b in befunde: treffer = None for i, (muster, frist, grund) in enumerate(regeln): if muster not in b: continue getroffen.add(i) if re.fullmatch(r"ADR-\d{4}", frist): treffer = (frist, grund) break try: faellig = _dt.date.fromisoformat(frist) except ValueError: meldungen.append( f"Quittung '{muster}': Frist '{frist}' ist weder ein Datum " f"noch ein ADR-Verweis — sie quittiert nichts.") continue if faellig < heute: meldungen.append( f"Quittung '{muster}' ist am {frist} abgelaufen — der Befund " f"zaehlt wieder. Verlaengern oder erledigen.") continue treffer = (frist, grund) break if treffer: quittiert.append((b, treffer[0], treffer[1])) else: offen.append(b) for i, (muster, _, _) in enumerate(regeln): if i not in getroffen: meldungen.append( f"Quittung '{muster}' trifft auf keinen Befund mehr — Zeile " f"entfernen, sonst verdeckt sie irgendwann etwas Neues.") return offen, quittiert, meldungen def ausgeben(quittiert, meldungen) -> None: """Einheitliche Ausgabe fuer beide Pruefungen.""" if quittiert: print(f"Quittiert ({len(quittiert)}) — bekannt, faerbt nicht rot:") for b, frist, grund in quittiert: print(f" ~ {b}") print(f" bis {frist}: {grund}") print() for m in meldungen: print(f"HINWEIS {m}") if meldungen: print()