This commit is contained in:
team3
2026-07-28 12:31:42 +02:00
parent 58efba988d
commit 2e86d682eb
15 changed files with 1823 additions and 65 deletions

97
backend/ledger.py Normal file
View File

@@ -0,0 +1,97 @@
"""Gedächtnis der Inhaltsprüfungen: was einmal beurteilt wurde, bleibt beurteilt.
Ein Urteil über eine Codestelle kostet sechs Anfragen und fällt bei Grenzfällen mal
so, mal so. Genau daran scheiterte die Wiederholbarkeit: derselbe Stand lieferte in
Runde 1 einen Befund, in Runde 2 keinen, in Runde 3 wieder einen.
Gespeichert wird deshalb das Urteil, nicht nur das Ergebnis. Der nächste Lauf fragt
nur noch, was er nicht schon weiß. Ein Eintrag gilt weiter, solange seine
Voraussetzung nachweisbar dasteht:
- ein Beleg, solange die zitierte Zeile noch in der Datei steht
- eine Abdeckung, solange die nennende Funktion noch auf einer Karte steht
- eine Fehlmeldung, solange keine passende Funktion dazugekommen ist
Fällt die Voraussetzung weg, fällt der Eintrag weg und wird neu gefragt. Damit
bleibt gleicher Stand gleiches Ergebnis, und geänderter Stand wird wirklich neu
beurteilt.
Die Datei liegt im Projektordner. Sie wandert beim Umbenennen mit und verschwindet
beim Löschen — ohne Zutun, weil beides den Ordner bewegt. Wer von vorn anfangen
will, löscht sie; der nächste Lauf fragt dann wieder alles.
"""
import hashlib
import json
import projekte
DATEI = ".pruefung.json"
# Ein Fach je Frage. Getrennt, weil sie unterschiedlich ungültig werden.
FAECHER = (
"belege", # Funktion -> Codestelle, die sie umsetzt (Prüfung 2)
"ohne_code", # Funktion -> kein Code gefunden (Prüfung 2)
"paare", # Funktionspaar -> Dublette oder verschieden (Prüfung 2)
"abdeckung", # Codestelle -> Funktion, die sie beschreibt (Prüfung 3)
"ohne_karte", # Codestelle -> keine Funktion beschreibt sie (Prüfung 3)
)
def _pfad(projekt: str):
return projekte.wurzel(projekt) / DATEI
def lade(projekt: str) -> dict:
"""Der gespeicherte Stand. Kaputte oder fehlende Datei heißt: nichts gemerkt."""
try:
daten = json.loads(_pfad(projekt).read_text())
except (OSError, ValueError):
daten = {}
return {fach: dict(daten.get(fach) or {}) for fach in FAECHER}
def sichere(projekt: str, stand: dict):
"""Sortiert und eingerückt — die Datei soll sich lesen und vergleichen lassen.
Erst daneben schreiben, dann umbenennen. Ein Absturz mitten im Schreiben ließe
sonst eine halbe Datei zurück, und das Gedächtnis eines ganzen Laufs wäre weg —
teuer bezahlt und nicht wiederherstellbar.
"""
text = json.dumps({fach: stand.get(fach) or {} for fach in FAECHER},
indent=1, ensure_ascii=False, sort_keys=True)
ziel = _pfad(projekt)
neben = ziel.with_suffix(".json.neu")
neben.write_text(text + "\n")
neben.replace(ziel)
def abdruck(*teile: str) -> str:
"""Kurzer Fingerabdruck über den Stand, der ein Urteil trägt."""
roh = json.dumps([t or "" for t in teile], ensure_ascii=False).encode()
return hashlib.sha1(roh).hexdigest()[:16]
# Schlüssel sind JSON-Listen. Ein Trennzeichen wäre einfacher, aber Kartennamen und
# Funktionstitel dürfen alles enthalten — und ein Schlüssel, der sich nicht wieder
# zerlegen lässt, ist keiner.
def schluessel(*teile: str) -> str:
return json.dumps(list(teile), ensure_ascii=False)
def paarschluessel(a: str, b: str) -> str:
return json.dumps(sorted((a, b)), ensure_ascii=False)
def teile(wert: str) -> list:
try:
gelesen = json.loads(wert)
except ValueError:
return []
return gelesen if isinstance(gelesen, list) else []
def behalte(fach: dict, gueltig: set) -> dict:
"""Einträge zu verschwundenen Stellen wegräumen — sonst wächst die Datei ewig."""
return {k: v for k, v in fach.items() if k in gueltig}