Trend-Radar + Pruefstand: Box beobachtet Modell-/Engine-Trends und testet Kandidaten selbst

- trend-radar-feed.sh (Cron Sa 05:15): Live-Puls ALLER Rollen mit Vorwochen-Vergleich,
  Engine-A/B gegen neuen llama.cpp-Build (Fruehwarnung vor So-Auto-Update), mechanische
  Watch-Liste (MMQ-Issue, ROCm-Releases, Community-Grid), HF-Kandidaten-Suche mit
  Zitat-Gate -> max. 1 Pruefstand-Kandidat/Woche
- pruefstand.sh + harness.py (Cron So 01:00, no-agent): volles Programm je Kandidat -
  6 Coding-Aufgaben mit echten Unit-Tests, 4 Agenten-Aufgaben (Tool-Loop + Endzustand),
  4 Recherche-Fragen mit Zitat-Ehrlichkeits-Gate, Deutsch-Richter, Vision-Testbild,
  Tempo kurz+lang; Baseline der Amtsinhaber als Vergleichsmassstab
- Leitplanken (User-Entscheid 17.07.): Download-Deckel 35 GB (drueber -> Karte),
  1 Kandidat/Woche, Cache mit Auto-Aufraeumen, RAM-/Platz-Wache, Bench-Port :5899,
  Radar/Pruefstand EMPFEHLEN nur - Rollen-Wechsel bleibt Commander-Klick
- E2E bewiesen (Mock-LLM): coding 6/6, recherche 4/4 inkl. Zitat-Gate, Agent-Roundtrip,
  Richter-Parsing; Suiten-Selbsttest mit Referenzloesungen 6/6

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-07-17 14:56:58 +02:00
parent 8579fe9934
commit b0dce954e9
34 changed files with 1786 additions and 1 deletions
@@ -0,0 +1,13 @@
{
"prompt": "Im Arbeitsverzeichnis liegen Notiz-Dateien im Ordner `notizen/`. Sammle ALLE Zeilen, die mit `TODO:` beginnen, und schreibe sie (ohne das `TODO:`-Präfix, eine je Zeile, Reihenfolge egal) in eine neue Datei `offene-punkte.txt` im Wurzelverzeichnis. Rufe danach `fertig` auf.",
"max_schritte": 8,
"sandkasten": {
"notizen/montag.txt": "Besprechung war ok.\nTODO: Backup-Log prüfen\nSonst nichts.\n",
"notizen/dienstag.txt": "TODO: Firmware-Update Router\nWetter schlecht.\n",
"notizen/mittwoch.txt": "Nichts offen heute.\n"
},
"checks": [
{"datei": "offene-punkte.txt", "regex": "Backup-Log prüfen"},
{"datei": "offene-punkte.txt", "regex": "Firmware-Update Router"}
]
}
@@ -0,0 +1,13 @@
{
"prompt": "Die Datei `dienst/config.ini` ist kaputt. In `anleitung.txt` steht, welche Werte gelten sollen. Repariere die config.ini entsprechend (nur die falschen Werte ändern, Struktur beibehalten) und rufe danach `fertig` auf.",
"max_schritte": 8,
"sandkasten": {
"anleitung.txt": "Sollwerte für dienst/config.ini:\n- port muss 9001 sein (Zahl, nicht Text!)\n- loglevel muss info sein\n- Der Eintrag host bleibt unverändert.\n",
"dienst/config.ini": "[server]\nhost = 127.0.0.1\nport = abc\nloglevel = debug\n"
},
"checks": [
{"datei": "dienst/config.ini", "regex": "^port\\s*=\\s*9001\\s*$"},
{"datei": "dienst/config.ini", "regex": "^loglevel\\s*=\\s*info\\s*$"},
{"datei": "dienst/config.ini", "regex": "^host\\s*=\\s*127\\.0\\.0\\.1\\s*$"}
]
}
@@ -0,0 +1,11 @@
{
"prompt": "Lies die Messwerte in `messwerte.csv` (Spalten: zeit;temperatur_c). Erstelle einen Bericht `bericht.md` mit genau zwei Zeilen: Zeile 1 `Messungen: <Anzahl der Datenzeilen>`, Zeile 2 `Maximum: <höchste Temperatur> °C` (Zahl wie in der Quelldatei geschrieben). Rufe danach `fertig` auf.",
"max_schritte": 8,
"sandkasten": {
"messwerte.csv": "zeit;temperatur_c\n03:00;61,5\n04:00;72,25\n05:00;58,0\n06:00;69,75\n"
},
"checks": [
{"datei": "bericht.md", "regex": "Messungen:\\s*4"},
{"datei": "bericht.md", "regex": "Maximum:\\s*72,25\\s*°?C"}
]
}
@@ -0,0 +1,17 @@
{
"prompt": "In `regeln.txt` steht, welche Dateien im Arbeitsverzeichnis gelöscht werden dürfen. Lösche GENAU die Dateien, die laut Regeln weg dürfen — nichts anderes. Rufe danach `fertig` auf.",
"max_schritte": 10,
"sandkasten": {
"regeln.txt": "Aufräum-Regeln:\n1. Alle Dateien mit Endung .tmp dürfen gelöscht werden.\n2. Dateien, deren Name mit wichtig beginnt, dürfen NIE gelöscht werden.\n3. Alles andere bleibt liegen.\n",
"cache-alt.tmp": "wegwerf-inhalt\n",
"sitzung.tmp": "wegwerf-inhalt\n",
"wichtig-vertrag.txt": "bleibt!\n",
"notiz.md": "bleibt auch.\n"
},
"checks": [
{"nicht_vorhanden": "cache-alt.tmp"},
{"nicht_vorhanden": "sitzung.tmp"},
{"datei": "wichtig-vertrag.txt", "regex": "bleibt!"},
{"datei": "notiz.md", "regex": "bleibt auch"}
]
}
@@ -0,0 +1,5 @@
{
"prompt": "Schreibe eine Python-Funktion `summiere_umsaetze(text)` in der Datei `loesung.py`. Eingabe ist ein CSV-Text mit Kopfzeile `kategorie;betrag` — Trennzeichen Semikolon, Beträge mit DEUTSCHEM Dezimalkomma (z. B. `12,50`). Die Funktion gibt ein dict zurück, das je Kategorie die Summe der Beträge als float enthält. Leere Zeilen und Zeilen ohne Semikolon werden ignoriert; unparsebare Beträge lösen einen ValueError mit der Zeilennummer aus.",
"antwort_datei": "loesung.py",
"timeout_s": 60
}
@@ -0,0 +1,28 @@
import sys
import unittest
from loesung import summiere_umsaetze
class Tests(unittest.TestCase):
def test_summen(self):
text = ("kategorie;betrag\n"
"essen;12,50\n"
"essen;7,50\n"
"\n"
"strom;30,00\n")
self.assertEqual(summiere_umsaetze(text),
{"essen": 20.0, "strom": 30.0})
def test_ohne_semikolon_ignoriert(self):
text = "kategorie;betrag\nnur eine notizzeile\nessen;1,00\n"
self.assertEqual(summiere_umsaetze(text), {"essen": 1.0})
def test_kaputter_betrag(self):
with self.assertRaises(ValueError):
summiere_umsaetze("kategorie;betrag\nessen;zwoelf\n")
if __name__ == "__main__":
r = unittest.main(exit=False).result
sys.exit(0 if r.wasSuccessful() else 1)
@@ -0,0 +1,8 @@
{
"prompt": "In der Datei `zeitfenster.py` steckt mindestens ein Fehler: `im_wartungsfenster(stunde)` soll True liefern, wenn die Stunde im nächtlichen Wartungsfenster von 23 Uhr bis einschließlich 4 Uhr liegt (also 23, 0, 1, 2, 3, 4) — für alle anderen Stunden False. Ungültige Stunden (<0 oder >23) sollen einen ValueError auslösen. Korrigiere die Datei und gib sie VOLLSTÄNDIG zurück, ohne die Funktionssignatur zu ändern.",
"antwort_datei": "zeitfenster.py",
"dateien": {
"zeitfenster.py": "def im_wartungsfenster(stunde):\n \"\"\"True, wenn die Stunde im Wartungsfenster 23-4 Uhr liegt.\"\"\"\n if stunde < 0 or stunde > 24:\n raise ValueError(\"ungueltige Stunde\")\n return 23 <= stunde or stunde < 4\n"
},
"timeout_s": 60
}
@@ -0,0 +1,24 @@
import sys
import unittest
from zeitfenster import im_wartungsfenster
class Tests(unittest.TestCase):
def test_im_fenster(self):
for h in (23, 0, 1, 2, 3, 4):
self.assertTrue(im_wartungsfenster(h), f"Stunde {h}")
def test_ausserhalb(self):
for h in (5, 6, 12, 18, 22):
self.assertFalse(im_wartungsfenster(h), f"Stunde {h}")
def test_ungueltig(self):
for h in (-1, 24, 99):
with self.assertRaises(ValueError):
im_wartungsfenster(h)
if __name__ == "__main__":
r = unittest.main(exit=False).result
sys.exit(0 if r.wasSuccessful() else 1)
@@ -0,0 +1,5 @@
{
"prompt": "Schreibe in `loesung.py` eine Funktion `fehler_zeilen(log_text)`. Sie bekommt Logtext, in dem relevante Zeilen so aussehen: `2026-07-17T03:15:02 ERROR dienst-name: Meldungstext` (Level kann auch INFO oder WARN sein). Die Funktion gibt eine Liste von Tupeln `(zeitstempel, dienst, meldung)` NUR für ERROR-Zeilen zurück, in Original-Reihenfolge. Zeilen in anderem Format werden still ignoriert. Der Dienstname steht vor dem Doppelpunkt und enthält keine Leerzeichen.",
"antwort_datei": "loesung.py",
"timeout_s": 60
}
@@ -0,0 +1,29 @@
import sys
import unittest
from loesung import fehler_zeilen
LOG = """2026-07-17T03:15:02 ERROR llama-swap: Modell nicht gefunden
2026-07-17T03:15:03 INFO mc2: Start ok
kaputte zeile ohne format
2026-07-17T03:16:10 WARN voice: Latenz hoch
2026-07-17T03:17:00 ERROR mem0-service: Timeout nach 30s
"""
class Tests(unittest.TestCase):
def test_nur_error(self):
erg = fehler_zeilen(LOG)
self.assertEqual(len(erg), 2)
self.assertEqual(erg[0][0], "2026-07-17T03:15:02")
self.assertEqual(erg[0][1], "llama-swap")
self.assertEqual(erg[0][2], "Modell nicht gefunden")
self.assertEqual(erg[1][1], "mem0-service")
def test_leer(self):
self.assertEqual(fehler_zeilen("nur INFO hier\n"), [])
if __name__ == "__main__":
r = unittest.main(exit=False).result
sys.exit(0 if r.wasSuccessful() else 1)
@@ -0,0 +1,5 @@
{
"prompt": "Schreibe in `loesung.py` eine Klasse `Lager`. Konstruktor ohne Argumente. Methoden: `einlagern(artikel, menge)` (menge > 0, sonst ValueError), `entnehmen(artikel, menge)` (löst ValueError aus, wenn der Bestand nicht reicht oder der Artikel unbekannt ist), `bestand(artikel)` (0 für unbekannte Artikel). Bestände sind ganze Zahlen je Artikelname.",
"antwort_datei": "loesung.py",
"timeout_s": 60
}
@@ -0,0 +1,31 @@
import sys
import unittest
from loesung import Lager
class Tests(unittest.TestCase):
def test_ablauf(self):
l = Lager()
l.einlagern("kabel", 10)
l.entnehmen("kabel", 4)
self.assertEqual(l.bestand("kabel"), 6)
self.assertEqual(l.bestand("unbekannt"), 0)
def test_unterbestand(self):
l = Lager()
l.einlagern("kabel", 2)
with self.assertRaises(ValueError):
l.entnehmen("kabel", 3)
with self.assertRaises(ValueError):
l.entnehmen("gibtsnicht", 1)
def test_menge_positiv(self):
l = Lager()
with self.assertRaises(ValueError):
l.einlagern("kabel", 0)
if __name__ == "__main__":
r = unittest.main(exit=False).result
sys.exit(0 if r.wasSuccessful() else 1)
@@ -0,0 +1,5 @@
{
"prompt": "Schreibe in `loesung.py` eine Funktion `flach(d, praefix=\"\")`, die ein beliebig tief verschachteltes dict (Werte: dicts oder Skalare) in ein flaches dict verwandelt. Schlüsselpfade werden mit Punkt verbunden, z. B. macht `{\"a\": {\"b\": 1}, \"c\": 2}` daraus `{\"a.b\": 1, \"c\": 2}`. Leere dicts verschwinden ersatzlos. Nicht-dict-Eingaben lösen einen TypeError aus.",
"antwort_datei": "loesung.py",
"timeout_s": 60
}
@@ -0,0 +1,22 @@
import sys
import unittest
from loesung import flach
class Tests(unittest.TestCase):
def test_verschachtelt(self):
self.assertEqual(flach({"a": {"b": 1, "c": {"d": 2}}, "e": 3}),
{"a.b": 1, "a.c.d": 2, "e": 3})
def test_leere_dicts(self):
self.assertEqual(flach({"a": {}, "b": 1}), {"b": 1})
def test_typfehler(self):
with self.assertRaises(TypeError):
flach([1, 2])
if __name__ == "__main__":
r = unittest.main(exit=False).result
sys.exit(0 if r.wasSuccessful() else 1)
@@ -0,0 +1,5 @@
{
"prompt": "Schreibe in `loesung.py` eine Funktion `slug(text)`: wandelt deutschen Text in einen URL-Slug. Regeln: Kleinbuchstaben; ä→ae, ö→oe, ü→ue, ß→ss (auch Großbuchstaben-Varianten); alle anderen Nicht-Alphanumerischen Zeichen werden zu einzelnen Bindestrichen zusammengefasst; keine Bindestriche am Anfang/Ende. Beispiel: `Größte \"Änderung\" 2026!` → `groesste-aenderung-2026`.",
"antwort_datei": "loesung.py",
"timeout_s": 60
}
@@ -0,0 +1,21 @@
import sys
import unittest
from loesung import slug
class Tests(unittest.TestCase):
def test_beispiel(self):
self.assertEqual(slug('Größte "Änderung" 2026!'),
"groesste-aenderung-2026")
def test_umlaute(self):
self.assertEqual(slug("Müßiggänger öfter"), "muessiggaenger-oefter")
def test_raender(self):
self.assertEqual(slug(" --Hallo Welt-- "), "hallo-welt")
if __name__ == "__main__":
r = unittest.main(exit=False).result
sys.exit(0 if r.wasSuccessful() else 1)
@@ -0,0 +1,19 @@
[
{
"id": "alltag-erklaerung",
"prompt": "Du bist eine deutsche Sprach-Assistentin. Dein Mensch fragt beim Frühstück: »Sag mal, warum wird mein Handy-Akku im Winter eigentlich so schnell leer?« Antworte gesprochen, locker, in 23 kurzen Sätzen, Du-Form."
},
{
"id": "schlechte-nachricht",
"prompt": "Du bist eine deutsche Sprach-Assistentin. Du musst deinem Menschen sagen, dass das nächtliche Backup fehlgeschlagen ist, aber kein Datenverlust entstanden ist und du es um 9 Uhr erneut versuchst. Sag es gesprochen, ruhig und ohne Technik-Kauderwelsch, in 23 Sätzen, Du-Form."
},
{
"id": "terminplanung",
"prompt": "Du bist eine deutsche Sprach-Assistentin. Dein Mensch sagt: »Leg mir bitte was mit Zahnarzt nächste Woche Dienstag Nachmittag an, so gegen drei.« Bestätige gesprochen und natürlich in 12 Sätzen, Du-Form, und nenne dabei Wochentag und Uhrzeit.",
"hinweis_fuer_richter": "Muss Dienstag und 15 Uhr / drei Uhr nachmittags korrekt bestätigen."
},
{
"id": "smalltalk-wetter",
"prompt": "Du bist eine deutsche Sprach-Assistentin. Dein Mensch kommt durchnässt rein und sagt nur: »Boah, sag nichts.« Reagiere gesprochen, warm und mit leichtem Humor, in 12 Sätzen, Du-Form — ohne aufdringlich zu sein."
}
]
@@ -0,0 +1,6 @@
{
"id": "zeitserver-ausfall",
"frage": "Was passiert, wenn der Zeitserver der Werkhalle länger ausfällt, und wie heißt er?",
"muss_gruppen": [["chronos-w"], ["Haltemodus"], ["90"]],
"zitat_pflicht": true
}
@@ -0,0 +1,6 @@
{
"id": "band-schluessel",
"frage": "Warum kann ein Dieb mit den Sicherungsbändern aus Nordhof nichts anfangen?",
"muss_gruppen": [["AES-256", "AES"], ["Schlüsseltresor", "Verwaltung"], ["verschlüsselt"]],
"zitat_pflicht": true
}
@@ -0,0 +1,6 @@
{
"id": "pv-anteil",
"frage": "Wie viel Strom lieferte die Photovoltaik-Anlage im zweiten Quartal und welchen Anteil am Verbrauch deckte das?",
"muss_gruppen": [["96"], ["Viertel", "25", "24", "23"], ["412"]],
"zitat_pflicht": true
}
@@ -0,0 +1,6 @@
{
"id": "quer-wartungszugriff",
"frage": "Ein externer Techniker will nachts um 01:45 Uhr auf eine Maschinensteuerung zugreifen. Über welchen Weg muss er gehen, und warum ist dieser Zeitpunkt zusätzlich ungünstig?",
"muss_gruppen": [["falke"], ["Zwei-Faktor", "Zwei Faktor", "2-Faktor", "zweifaktor"], ["01:30", "Vollsicherung", "Sicherung"]],
"zitat_pflicht": true
}
@@ -0,0 +1,17 @@
# Backup-Konzept Weststadt-Werkhalle (Auszug)
Gesichert wird dreistufig. Stufe 1: stündliche Schnappschüsse der
Leitstand-Datenbank, Aufbewahrung 48 Stunden. Stufe 2: nächtliche
Vollsicherung aller Server um 01:30 Uhr auf den Sicherungsknoten arche-3
im Keller der Halle. Stufe 3: wöchentliche Auslagerung verschlüsselter
Sicherungsbänder in den Standort Nordhof, jeden Donnerstag per Kurier.
Die Rücksicherung der Leitstand-Datenbank dauert im Normalfall unter
20 Minuten. Eine vollständige Wiederherstellung aller Server aus arche-3
wurde zuletzt im Frühjahr geprobt und benötigte sechseinhalb Stunden.
Wichtig: Die Sicherungsbänder für Nordhof werden mit dem Verfahren
AES-256 verschlüsselt; die Schlüssel liegen NICHT in der Halle, sondern
im Schlüsseltresor der Verwaltung. Ohne Rückholung des Schlüssels aus
der Verwaltung ist eine Band-Rücksicherung unmöglich — das ist die
bewusste Absicherung gegen Diebstahl der Bänder samt Halle-Infrastruktur.
@@ -0,0 +1,16 @@
# Energie-Quartalsbericht Weststadt-Werkhalle (Auszug)
Der Stromverbrauch der Halle lag im zweiten Quartal bei 412 Megawattstunden
und damit acht Prozent unter dem Vorjahresquartal. Haupttreiber der
Einsparung war die Umrüstung der Hallenbeleuchtung auf gesteuerte
LED-Felder, die nur bei Anwesenheit auf voller Stufe laufen.
Die Photovoltaik-Anlage auf dem Süddach lieferte 96 Megawattstunden und
deckte damit knapp ein Viertel des Verbrauchs. An sonnenreichen Wochenenden
speist die Halle Überschüsse ins Netz zurück; die Vergütung dafür wird im
Jahresabschluss gesondert ausgewiesen.
Für das dritte Quartal ist die Inbetriebnahme des Batteriespeichers
(Kapazität 180 Kilowattstunden) geplant. Er soll Lastspitzen der großen
Pressen abfangen, die bisher teure Spitzenlast-Tarife auslösen. Die
Wirtschaftlichkeitsrechnung erwartet eine Amortisation nach vier Jahren.
@@ -0,0 +1,17 @@
# Netzwerk-Handbuch Weststadt-Werkhalle (Auszug)
Die Werkhalle Weststadt betreibt zwei getrennte Netze: das Betriebsnetz
(10.40.0.0/16) für Maschinensteuerungen und das Büronetz (192.168.20.0/24)
für Arbeitsplätze. Ein Übergang zwischen beiden Netzen existiert nur über
die Koppelstelle KS-2, die jede Verbindung protokolliert.
Der zentrale Zeitserver der Halle heißt chronos-w und steht im Betriebsnetz.
Alle Steuerungen gleichen ihre Uhren viertelstündlich mit chronos-w ab.
Fällt der Zeitserver länger als 90 Minuten aus, wechseln die Steuerungen in
den Haltemodus und müssen einzeln von Hand quittiert werden.
Für Wartungszugriffe von außen gilt: Zugang ausschließlich über das
Sprungsystem falke, das eine Zwei-Faktor-Anmeldung verlangt. Direkte
Zugriffe aus dem Büronetz auf Steuerungen sind technisch gesperrt.
Die Sperrliste pflegt das Team Leitstand, Änderungen brauchen zwei
Freigaben und werden erst nach dem Wochenwechsel wirksam.
@@ -0,0 +1,37 @@
#!/usr/bin/env python3
# Erzeugt das Prüfstand-Testbild deterministisch OHNE Zusatz-Pakete (reines
# zlib/struct-PNG): weißer Grund 256x256, rotes Rechteck links oben,
# blaues Rechteck rechts unten. Die Vision-Suite prüft genau diese Aussagen.
import struct
import sys
import zlib
B, H = 256, 256
def pixel(x, y):
if 24 <= x < 104 and 24 <= y < 104:
return (220, 30, 30) # rotes Rechteck links oben
if 152 <= x < 232 and 152 <= y < 232:
return (30, 60, 220) # blaues Rechteck rechts unten
return (255, 255, 255)
def chunk(typ, daten):
c = typ + daten
return struct.pack(">I", len(daten)) + c + struct.pack(">I", zlib.crc32(c))
roh = b""
for y in range(H):
roh += b"\x00" + b"".join(bytes(pixel(x, y)) for x in range(B))
png = (b"\x89PNG\r\n\x1a\n"
+ chunk(b"IHDR", struct.pack(">IIBBBBB", B, H, 8, 2, 0, 0, 0))
+ chunk(b"IDAT", zlib.compress(roh, 9))
+ chunk(b"IEND", b""))
ziel = sys.argv[1] if len(sys.argv) > 1 else "testbild.png"
with open(ziel, "wb") as f:
f.write(png)
print(f"Testbild geschrieben: {ziel}")
@@ -0,0 +1,4 @@
{
"prompt": "Beschreibe kurz auf Deutsch, was auf diesem Bild zu sehen ist: welche Formen, welche Farben, und wo sie ungefähr liegen.",
"muss_gruppen": [["rot"], ["blau"], ["links oben", "oben links", "obere linke", "links-oben"], ["rechts unten", "unten rechts", "untere rechte", "rechts-unten"]]
}