Files
planer2/backend/ledger.py
2026-07-29 15:19:41 +02:00

96 lines
3.5 KiB
Python

"""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 Fehlanzeige, solange sich am Quelltext nichts geändert hat
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 = (
"stil", # Prüfstelle -> ihr Stilurteil (Prüfung 1)
"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)
)
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}