From 61b05405253cafe43d991af9ee8089b40bc3bca8 Mon Sep 17 00:00:00 2001 From: Thore Cimbal Date: Tue, 18 Aug 2026 12:00:00 +0000 Subject: [PATCH] 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 --- .../__pycache__/quittungen.cpython-312.pyc | Bin 0 -> 5871 bytes scripts/befund_quittungen.tsv | 20 +++ scripts/gruppenpruefung.py | 14 +- scripts/quittungen.py | 120 ++++++++++++++++++ scripts/stillstandspruefung.py | 21 ++- 5 files changed, 167 insertions(+), 8 deletions(-) create mode 100644 scripts/__pycache__/quittungen.cpython-312.pyc create mode 100644 scripts/befund_quittungen.tsv create mode 100644 scripts/quittungen.py diff --git a/scripts/__pycache__/quittungen.cpython-312.pyc b/scripts/__pycache__/quittungen.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ebdd4cfddf3a8285c6650f48392a93a07adaed79 GIT binary patch literal 5871 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 HqY(c811Ln4 literal 0 HcmV?d00001 diff --git a/scripts/befund_quittungen.tsv b/scripts/befund_quittungen.tsv new file mode 100644 index 0000000..f99c3d8 --- /dev/null +++ b/scripts/befund_quittungen.tsv @@ -0,0 +1,20 @@ +# Quittierte Befunde — bekannt, faerben nicht rot (#0104). +# +# Format: pruefung muster gueltig_bis grund +# pruefung Geltungsbereich: stillstand | gruppe +# muster Teilzeichenkette des Befundtextes (keine Regex) +# gueltig_bis Datum JJJJ-MM-TT, oder ADR-00NN fuer dauerhaft +# grund warum das hier steht — vollstaendiger Satz, kein Stichwort +# +# Dauerhaft geht NUR mit ADR-Verweis. Alles andere braucht eine Frist; laeuft sie +# ab, faerbt der Befund wieder rot. Eine Quittung, auf die nichts mehr passt, +# meldet sich selbst. Details im Kopf von scripts/quittungen.py. +# +# Vor dem Eintragen die Frage beantworten: Ist das wirklich bekannt und +# entschieden — oder faellt es mir nur gerade lastig? Im zweiten Fall gehoert es +# nicht hierher. + +stillstand notfallhandbuch: kein aktiver Push-Mirror ADR-0016 Das Notfallhandbuch wird bewusst nicht gespiegelt: es beschreibt Ablageort der Sicherungen und Fundort der Schluessel und waere auf dem oeffentlich erreichbaren Gitea die Landkarte fuer einen Angreifer. Vertraulichkeit vor Verfuegbarkeit; die Verfuegbarkeit deckt ein lokaler Clone ab. +stillstand threadnet-wiki: kein aktiver Push-Mirror ADR-0015 Beim Wiki laeuft die Spiegelung umgekehrt: Wiki.js schreibt aus dem Cluster nach Gitea, der CI-Job canonize_wiki holt es nach git.lab. Ein Push-Mirror git.lab->Gitea waere hier falsch herum und wuerde die Wiki-Inhalte ueberschreiben. +stillstand gameserver: kein aktiver Push-Mirror 2026-09-30 sorb 2026-08-18: der gameserver ist nicht Teil von ThreadNet, die Spiegel-Topologie aus ADR-0001 gilt fuer ihn nicht. Bis zur Frist ist zu entscheiden, ob das als ADR festgehalten wird oder das Projekt die Gruppe verlaesst — siehe #0032. Danach faerbt der Befund wieder rot. +stillstand ThreadNet-Web: Pipeline 424 (main) ist rot, hat aber NULL Jobs 2026-10-31 Artefakt der workflow-Regeln: ein Commit, der keinen Pfad aus .pfade_mit_pipeline beruehrt, erzeugt eine Pipeline ohne Jobs, die GitLab als rot fuehrt. Das Skript benennt es selbst als "kein Fehler". Bis zur Frist klaeren, ob die Regel so bleiben soll. diff --git a/scripts/gruppenpruefung.py b/scripts/gruppenpruefung.py index b03e0e0..1dc5a77 100644 --- a/scripts/gruppenpruefung.py +++ b/scripts/gruppenpruefung.py @@ -41,6 +41,8 @@ import urllib.parse import urllib.request from pathlib import Path +import quittungen + API = "https://git.lab/api/v4" GRUPPE = "axion1337.chat" GRENZE = "2026-08-07" @@ -255,13 +257,17 @@ def main() -> int: befunde.append(f"{slug} {c['short_id']}: nicht-kanonische " f"Identität {wer}") - for b in befunde: + # Bekanntes quittieren (#0104) — siehe scripts/quittungen.py. + offen, quittiert, quittungsmeldungen = quittungen.anwenden(befunde, root, "gruppe") + quittungen.ausgeben(quittiert, quittungsmeldungen) + + for b in offen: print(f"BEFUND {b}") for h in hinweise: print(f"HINWEIS {h}") - print(f"gruppenpruefung: {len(befunde)} Befunde, " - f"{len(hinweise)} Hinweise") - return 1 if befunde else 0 + print(f"gruppenpruefung: {len(offen)} offene Befunde, " + f"{len(quittiert)} quittiert, {len(hinweise)} Hinweise") + return 1 if offen else 0 if __name__ == "__main__": diff --git a/scripts/quittungen.py b/scripts/quittungen.py new file mode 100644 index 0000000..4d871c2 --- /dev/null +++ b/scripts/quittungen.py @@ -0,0 +1,120 @@ +#!/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() diff --git a/scripts/stillstandspruefung.py b/scripts/stillstandspruefung.py index 9822335..45ff029 100755 --- a/scripts/stillstandspruefung.py +++ b/scripts/stillstandspruefung.py @@ -26,6 +26,12 @@ import subprocess import sys import urllib.parse from datetime import datetime, timedelta, timezone +from pathlib import Path + +import quittungen + +# Repo-Wurzel: das Skript liegt in scripts/, die Quittungsliste daneben. +ROOT = Path(__file__).resolve().parent.parent GITLAB = "https://git.lab/api/v4" GITEA = "https://rohana.axion1337.de/api/v1" @@ -235,13 +241,20 @@ def main(): for u in uebersprungen: print(f" - {u}") print() - if befunde: - print(f"BEFUNDE ({len(befunde)}):") - for b in befunde: + + # Bekanntes quittieren (#0104): Rot bleibt dem Neuen vorbehalten, sonst + # verliert die Alarmanlage ihre Aussage. Quittiertes verschwindet nicht, + # es zaehlt nur nicht mehr gegen die Pipeline. + offen, quittiert, meldungen = quittungen.anwenden(befunde, ROOT, "stillstand") + quittungen.ausgeben(quittiert, meldungen) + + if offen: + print(f"BEFUNDE ({len(offen)}):") + for b in offen: print(f" ! {b}") print("\nJeder Befund gehoert als Issue erfasst, nicht weggeklickt.") return 1 - print("Keine Befunde.") + print("Keine offenen Befunde.") return 0