Files
management/scripts/quittungen.py
T
Thore CimbalandClaude Opus 5 61b0540525 feat(pruefungen): acknowledge known findings so red means something again (#0104)
All three scheduled checks were permanently red, which is how a nine-day outage of
the canonize job went unnoticed: one more red cross among red crosses is invisible.
A check that can only ever be red cannot report anything.

An acknowledgement takes a known finding out of the red verdict without hiding it -
it still prints, with its reason and its deadline. Red is reserved for what is not
acknowledged, which is to say: for the new.

Three rules keep the list from becoming the next blind spot, which is the obvious
objection to this whole idea:

- Every entry needs a deadline. Once it passes, the entry stops acknowledging and
  says so, so the finding counts again.
- "Permanent" is only expressible as an ADR reference. A permanent exception
  without a decision record is already an error per AGENTS.md; here it cannot even
  be written down.
- An entry that matches nothing reports itself, so the file cannot quietly
  accumulate lines for problems that no longer exist.

Four entries to start: notfallhandbuch (ADR-0016, deliberately unmirrored),
threadnet-wiki (ADR-0015, the wiki mirrors the other way round), gameserver (sorb:
not part of ThreadNet - dated, so the ADR-or-move decision does not drift), and the
zero-job pipeline artifact.

Verified rather than argued, including the counter-proofs #0104 asks for: today
four are acknowledged and one real finding stays red; adding a fresh finding still
turns it red; and with the clock moved past the deadlines the dated entries stop
acknowledging and report themselves. The scope column was added after the first run
showed each check reporting the other's entries as ineffective.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 12:00:00 +00:00

121 lines
4.6 KiB
Python

#!/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 <TAB> muster <TAB> gueltig_bis <TAB> 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()