- .vault-tools/plan-kopie/LIESMICH.md - .vault-tools/plan-kopie/rot_nachweis.py - .vault-tools/plan-kopie/welle_vergleich.py - .vault-tools/plan-kopie/welle_vergleich_treiber.py - .vault-tools/trading-plattform-pruefung/LIESMICH.md - .vault-tools/trading-plattform-pruefung/angriffe.py - .vault-tools/trading-plattform-pruefung/browserlauf.py - .vault-tools/trading-plattform-pruefung/browserlauf_zugang.py - .vault-tools/trading-plattform-pruefung/c_eigenlauf.py - .vault-tools/trading-plattform-pruefung/d_eigenlauf.py - .vault-tools/trading-plattform-pruefung/handelstag_auswertung.py - .vault-tools/trading-plattform-pruefung/ntfy_probe_server.py - … und 7 weitere Erzeugt vom PostToolUse-Hook .vault-tools/auto-push/auto_push.py. Co-Authored-By: Claude Opus 5 <[email protected]>
123 lines
5.1 KiB
Python
123 lines
5.1 KiB
Python
"""Vergleicht zwei JSONL-Protokolle der Messwerkzeuge Wert fuer Wert (Bitgleichheit nach einem Umbau).
|
|
|
|
Koordinator-Werkzeug, nicht fuers Repo. Frage (Herkunft: Projektvorgabe, Anforderung Paket B vom
|
|
2026-09-18, "Die eine Regel ueber allem"): Liefert der umgebaute Code auf denselben echten Daten dieselben
|
|
Zahlen wie das Referenzprotokoll -- jede Gleitkommazahl exakt gleich, nan gleich nan?
|
|
Es unterscheidet drei Dinge und vermischt sie nicht:
|
|
ABWEICHUNG ein Blattwert unter demselben Pfad ist anders -> Befund (Exit 2)
|
|
NUR NEU ein Pfad steht nur im neuen Protokoll (neuer Schluessel) -> gemeldet, kein Befund, ausser mit --streng
|
|
NUR ALT ein Pfad steht nur im alten Protokoll -> Befund (Exit 2)
|
|
Zeilen werden in ihrer Reihenfolge gepaart; verschiedene Zeilenzahlen oder Zeilenarten sind ein Befund.
|
|
Uebergangen werden die Schluessel aus --ohne (Vorgabe: zeit, dauer_s, protokoll, name), weil sie vom Lauf
|
|
abhaengen und nicht von der Rechnung.
|
|
Was es nicht sieht: ob das Referenzprotokoll selbst stimmt, und Zeilen, die in beiden Protokollen fehlen.
|
|
Regressionsnachweis: --sabotage veraendert im neuen Protokoll eine Zahl in der letzten Stelle und muss rot enden.
|
|
|
|
Aufruf: python protokoll_vergleich.py <alt.jsonl> <neu.jsonl> [--ohne k1 k2 ...] [--streng] [--sabotage]
|
|
Exit 0 ohne Befund, 2 mit Befund, 3 wenn eine Datei fehlt oder leer ist.
|
|
"""
|
|
|
|
import json
|
|
import math
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
|
|
def lies(pfad: Path) -> list[dict]:
|
|
zeilen = []
|
|
with open(pfad, encoding="utf-8") as datei:
|
|
for zeile in datei:
|
|
if zeile.strip():
|
|
zeilen.append(json.loads(zeile, parse_constant=float))
|
|
return zeilen
|
|
|
|
|
|
def flach(knoten, ohne: set[str], praefix: str = "") -> dict:
|
|
werte: dict = {}
|
|
if isinstance(knoten, dict):
|
|
for name, inhalt in knoten.items():
|
|
if name in ohne:
|
|
continue
|
|
werte.update(flach(inhalt, ohne, f"{praefix}.{name}"))
|
|
elif isinstance(knoten, list):
|
|
for nummer, inhalt in enumerate(knoten):
|
|
werte.update(flach(inhalt, ohne, f"{praefix}[{nummer}]"))
|
|
else:
|
|
werte[praefix] = knoten
|
|
return werte
|
|
|
|
|
|
def gleich(a, b) -> bool:
|
|
if isinstance(a, float) and isinstance(b, float):
|
|
return a == b or (math.isnan(a) and math.isnan(b))
|
|
return type(a) is type(b) and a == b
|
|
|
|
|
|
def main() -> int:
|
|
argumente = sys.argv[1:]
|
|
streng = "--streng" in argumente
|
|
sabotage = "--sabotage" in argumente
|
|
ohne = {"zeit", "dauer_s", "protokoll", "name"}
|
|
if "--ohne" in argumente:
|
|
stelle = argumente.index("--ohne")
|
|
ohne = set()
|
|
for wort in argumente[stelle + 1 :]:
|
|
if wort.startswith("--"):
|
|
break
|
|
ohne.add(wort)
|
|
pfade = [Path(a) for a in argumente if not a.startswith("--") and a not in ohne]
|
|
if len(pfade) != 2 or not all(p.is_file() for p in pfade):
|
|
print(__doc__)
|
|
return 3
|
|
alt, neu = lies(pfade[0]), lies(pfade[1])
|
|
if not alt or not neu:
|
|
print("VORAUSSETZUNG FEHLT: ein Protokoll ist leer")
|
|
return 3
|
|
if sabotage:
|
|
for zeile in reversed(neu):
|
|
zahlen = [k for k, v in zeile.items() if isinstance(v, float) and math.isfinite(v) and v != 0.0]
|
|
if zahlen:
|
|
zeile[zahlen[0]] = math.nextafter(zeile[zahlen[0]], math.inf)
|
|
print(f"sabotage: {zahlen[0]} in einer Zeile der Art {zeile.get('art')} um ein ULP verschoben")
|
|
break
|
|
|
|
befunde: list[str] = []
|
|
if len(alt) != len(neu):
|
|
befunde.append(f"Zeilenzahl: alt {len(alt)}, neu {len(neu)}")
|
|
verglichen = 0
|
|
nur_neu: dict[str, int] = {}
|
|
for nummer, (a, n) in enumerate(zip(alt, neu), start=1):
|
|
if a.get("art") != n.get("art"):
|
|
befunde.append(f"Zeile {nummer}: Art alt {a.get('art')!r}, neu {n.get('art')!r}")
|
|
continue
|
|
fa, fn = flach(a, ohne), flach(n, ohne)
|
|
for pfad in fa.keys() - fn.keys():
|
|
befunde.append(f"Zeile {nummer} ({a.get('art')}): NUR ALT {pfad}")
|
|
for pfad in fn.keys() - fa.keys():
|
|
kurz = pfad.split("[")[0]
|
|
nur_neu[kurz] = nur_neu.get(kurz, 0) + 1
|
|
if streng:
|
|
befunde.append(f"Zeile {nummer} ({a.get('art')}): NUR NEU {pfad}")
|
|
for pfad in fa.keys() & fn.keys():
|
|
verglichen += 1
|
|
if not gleich(fa[pfad], fn[pfad]):
|
|
befunde.append(f"Zeile {nummer} ({a.get('art')}): ABWEICHUNG {pfad}: alt {fa[pfad]!r}, neu {fn[pfad]!r}")
|
|
|
|
print(f"{min(len(alt), len(neu))} Zeilen gepaart, {verglichen} Werte verglichen, uebergangene Schluessel: {sorted(ohne)}")
|
|
for pfad, zahl in sorted(nur_neu.items()):
|
|
print(f"NUR NEU: {pfad} in {zahl} Zeile(n)")
|
|
for befund in befunde[:25]:
|
|
print("BEFUND:", befund)
|
|
if len(befunde) > 25:
|
|
print(f"... und {len(befunde) - 25} weitere")
|
|
print(f"{len(befunde)} Befund(e).")
|
|
return 2 if befunde else 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
sys.exit(main())
|
|
except Exception as ausnahme: # noqa: BLE001 - ein Absturz des Werkzeugs ist selbst ein Befund
|
|
print(f"BEFUND: das Werkzeug ist abgebrochen: {ausnahme!r}")
|
|
sys.exit(2)
|