feat: Warnung bei ungeschützter Sandbox, Prüf-Regel für konstante Kosten, Grund je Korrekturrunde

- sandbox_warning: fehlt bubblewrap oder ist SOLVER_SANDBOX=aus, steht das im Log
  und in SolverRun.warnings (vorher lief der Code stillschweigend ungeschützt)
- Dijkstra-Markierung: der Report erklärt den Begriff selbst (DIJKSTRA_EXPLANATION),
  check_text_quality zählt das als Erklärung; Stamm 'markier' -> 'markierung'
- Pruef-Prompt Regel 7: entscheidungsunabhängige Terme ohne Zahlenwert dürfen
  weggelassen oder symbolisch mitgeführt werden (wagner_whitin_dijkstra: 1+1 statt
  2+1 Korrekturrunden, n=2)
- revision_reasons: jede Korrekturrunde nennt ihren Auslöser im Log

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-09-17 16:28:39 +02:00
commit 0f1ab04a8d

View file

@ -444,6 +444,23 @@ def check_constraint_coverage(constraints: list, spec: OptimizationModelSpecific
return problems
def sandbox_warning() -> Optional[str]:
"""Hinweis, wenn der erzeugte Code ungeschützt läuft – sonst None.
Mit SOLVER_SANDBOX=auto lief der Code ohne bubblewrap bisher stillschweigend mit den Rechten des
Nutzers; aufgefallen beim Schreiben der Bedienungsanleitung. Der Hinweis steht deshalb im Log und
in `SolverRun.warnings` (damit im Report), bewusst ohne den Lauf zu verweigern – dafür gibt es `bwrap`.
"""
if SANDBOX == "aus":
return ("Sandbox abgeschaltet (SOLVER_SANDBOX=aus): der vom LLM erzeugte Code lief ohne Isolierung "
"mit den Rechten des Benutzers.")
if SANDBOX != "bwrap" and not shutil.which("bwrap"):
return ("Sandbox fehlt: bubblewrap ist nicht installiert, der vom LLM erzeugte Code lief ohne Isolierung "
"mit den Rechten des Benutzers. Abhilfe: 'sudo apt install bubblewrap'; "
"SOLVER_SANDBOX=bwrap verweigert ungeschützte Läufe.")
return None
def sandbox_command(script: Path, tmp: str) -> List[str]:
"""Aufrufzeile für den Solver-Lauf – mit bubblewrap isoliert, sonst direkt.
@ -667,18 +684,30 @@ def fix_transliterations(text: str, extra: dict | None = None) -> str:
return re.sub(r"\w+", wort, text or "")
# Feste Erklärung im Report (Graphen-Abschnitt, steht VOR den Ergebnistabellen), sobald Dijkstra-Schritte da sind
DIJKSTRA_EXPLANATION = ("Dijkstra nimmt in jedem Schritt den noch nicht endgültigen Knoten mit der kleinsten Marke. "
"Weil die Marken dabei nie fallen, steht jede endgültige Marke schon fest, sobald sie "
"vergeben wird – das ist der Grund, warum ein Durchlauf genügt.")
def check_text_quality(run, summary: str = "") -> list:
"""Texte für Menschen prüfen (ohne LLM): unerklärte Fachbegriffe in Antwortsatz und Tabellen.
Umschriebene Umlaute werden nicht mehr gemeldet, sondern still berichtigt (`repair_texts`):
die Warnung stand in fast jedem Lauf und bewirkte nichts – im Code sind Umschreibungen erlaubt.
Als Erklärung zählt neben `executive_summary` auch, was der Report selbst vor den Tabellen erklärt:
der Markierungsverlauf bei Dijkstra. Beobachtet (20260917-160618): „Marke“ in beiden Dijkstra-Läufen
gemeldet, obwohl die Aufgabe Dijkstra ausdrücklich verlangt und der Report den Begriff erklärt – die
Zusammenfassung entsteht vor der Solver-Stufe und kennt ihn noch nicht.
"""
texte = [("headline", run.headline or "")]
for table in run.tables:
texte.append((f"Tabelle '{table.title}'", " ".join([table.title, *map(str, table.columns), table.note or ""])))
befunde = []
if summary:
offen = unexplained_jargon(" ".join(text for _, text in texte), summary)
erklaert = summary + (" " + DIJKSTRA_EXPLANATION if run.graph and run.graph.steps else "")
offen = unexplained_jargon(" ".join(text for _, text in texte), erklaert)
if offen:
befunde.append(f"Fachbegriffe in Antwortsatz oder Ergebnistabellen, die die Zusammenfassung nicht "
f"erklärt: {', '.join(offen)}.")
@ -953,6 +982,8 @@ def _check_and_run(code: str, timeout: int, spec: OptimizationModelSpecification
except CodeRejected as exc:
return SolverRun(status=SolverStatus.ERROR, error=f"Code abgelehnt: {exc}")
run = run_solver_code(code, timeout=timeout, data_files=data_files)
if (ungeschuetzt := sandbox_warning()):
run.warnings.append(ungeschuetzt)
run.feasibility = check_feasibility(run, spec)
run.warnings += check_sensitivity(run, spec.objective.sense.value)
run.warnings += check_multistart(run, spec.objective.sense.value)
@ -974,6 +1005,8 @@ def solve(session: LLMSession, task: str, spec: OptimizationModelSpecification,
infeasible/unbounded gelten nicht als Fehler und werden nicht repariert.
"""
if (ungeschuetzt := sandbox_warning()):
log(f"WARNUNG – {ungeschuetzt}")
solver_code = generate_solver_code(session, task, spec, understanding=understanding,
data_prompt=data_prompt)
solver_code.code = clean_generated_code(solver_code.code)