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
3 changed files with 148 additions and 8 deletions

View file

@ -22,6 +22,7 @@ Schritt 1 – Zielgröße (objective_check), immer zuerst:
- Kosten in Tsd. €, Erlöse in €.
Bei False gehört in correction die korrigierte Zielfunktion mit einheitlicher Einheit (z. B. Risiko mit dem Kapital multipliziert).
6. Bei matches False gib in correction die korrigierte Zielfunktion in LaTeX an (ohne Begrenzer).
7. Ausnahme zu 4 – entscheidungsunabhängige Terme, deren Wert der Text NICHT nennt: Hat ein Term für jede zulässige Lösung denselben Wert (z. B. gleichbleibender Einkaufspreis × fest vorgegebener Gesamtbedarf), ändert er die optimale Entscheidung nicht, und ohne Zahl im Text lässt er sich nicht ausrechnen. Dann sind zwei Formen gleich richtig: (a) den Term weglassen oder (b) ihn als Konstante mit Symbol mitführen. Beide erfüllen matches = True, wenn eine Annahme in open_assumptions ausdrücklich sagt, dass der Zielwert diesen Term nicht bzw. nur symbolisch enthält und warum die Entscheidung davon unberührt ist. Verlange keine Umstellung von (a) auf (b) oder umgekehrt, und führe den fehlenden Zahlenwert nicht als missing_assumptions auf, wenn eine solche Annahme existiert. Fehlt die Annahme, gehört genau sie in missing_assumptions. Nennt der Text den Wert, gilt Regel 4: dann gehört der Term in die gefragte Gesamtgröße.
Schreibweise: In allen Textfeldern (task_evidence ausgenommen, das ist ein wörtliches Zitat) steht jede Formel und jedes Symbol in \( … \), z. B. „modelliert wird \(w^\top \Sigma w\)“. Keine $-Zeichen als Formelbegrenzer.

View file

@ -69,7 +69,8 @@ JARGON = {
"Epigraph": ("epigraph",),
"Schattenpreis": ("schattenpreis", "dualwert", "dualvariable"),
"Konvexität": ("konvex",),
"Markierung": ("marke", "markier"),
# nicht "markier": „die mit ja markierten Zeilen“ ist Alltagssprache (Fehlalarm in 20260917-160618)
"Markierung": ("marke", "markierung"),
"Inzidenz": ("inzidenz",),
"Big-M": ("big-m", "big m"),
"Subtour": ("subtour",),

View file

@ -4,7 +4,7 @@ from typing import List
from checks.model_checks import has_errors
from core.llm import LLMSession
from core.model_schema import AssumptionReview, CheckIssue, CheckSeverity, OptimizationModelSpecification
from core.model_schema import AssumptionReview, AssumptionVerdict, CheckIssue, CheckSeverity, OptimizationModelSpecification
REVIEW_PROMPT_FILE = Path(__file__).parent / "Pruef-Prompt.txt"
@ -34,6 +34,30 @@ def needs_work(review: AssumptionReview, issues: List[CheckIssue]) -> bool:
or any(i.code in REVISION_WORTHY_WARNINGS for i in issues))
def revision_reasons(review: AssumptionReview, issues: List[CheckIssue]) -> List[str]:
"""Warum eine Korrekturrunde nötig ist – fürs Log.
Bisher stand dort nur „Probleme gefunden“; welche Beanstandung eine Runde auslöste, ließ sich im
Nachhinein nicht mehr feststellen (beobachtet an wagner_whitin_dijkstra, 20260917-160618).
"""
gruende = []
oc = review.objective_check
if not oc.matches:
gruende.append(f"Zielgröße passt nicht: {oc.explanation[:160]}")
if not oc.units_consistent:
gruende.append(f"Einheiten der Zielfunktion uneinheitlich: {oc.unit_explanation[:160]}")
faulty = {AssumptionVerdict.CONTRADICTS_TASK, AssumptionVerdict.STRICTER_THAN_TASK, AssumptionVerdict.WEAKER_THAN_TASK}
for check in review.checks:
if check.verdict in faulty:
gruende.append(f"Annahme {check.verdict.value}: {check.unclear_point[:120]}")
for fehlend in review.missing_assumptions:
gruende.append(f"fehlende Annahme: {fehlend.unclear_point[:120]}")
for issue in issues:
if issue.severity == CheckSeverity.ERROR or issue.code in REVISION_WORTHY_WARNINGS:
gruende.append(f"[{issue.code}] {issue.message[:120]}")
return gruende
def select_best(candidates: List[tuple]) -> int:
"""Index der besten (spec, issues, review)-Runde.