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

@ -309,3 +309,31 @@ def test_repair_leaves_correct_words_and_identifiers_alone():
assert fix_transliterations(richtig) == richtig
namen = "Bindend: Spaet_zu_Frueh_Verbot und KeinFruehNachSpaet sowie Kapazitaet_M1."
assert fix_transliterations(namen) == namen
def _dijkstra_run(title, note, steps=True):
from core.model_schema import GraphSolution, ResultTable
graph = GraphSolution.model_validate({
"nodes": ["0", "1"], "arcs": [{"source": "0", "target": "1", "weight": 8}], "path": ["0", "1"],
"steps": [{"node": "0", "label": 0}, {"node": "1", "label": 8}] if steps else [],
"node_meaning": "Zeitpunkt", "arc_meaning": "Kauf und Nutzung"})
return SolverRun(status=SolverStatus.OPTIMAL, objective_value=8, graph=graph,
tables=[ResultTable(title=title, columns=["Knoten", "Marke"], rows=[["0", 0]], note=note)])
def test_dijkstra_marks_explained_by_the_report_are_not_flagged():
# beobachtet: „Dijkstra-Markierungen“ gemeldet, obwohl der Report den Markierungsverlauf erklärt
from solving.solver_stage import check_text_quality
run = _dijkstra_run("Dijkstra-Markierungen", "Die Knoten in der Reihenfolge ihrer Markierung.")
assert check_text_quality(run, "Ersatzplanung als kürzester Weg.") == []
def test_marks_without_dijkstra_steps_are_still_flagged():
from solving.solver_stage import check_text_quality
run = _dijkstra_run("Marken je Knoten", None, steps=False)
assert any("Markierung" in b for b in check_text_quality(run, "Ersatzplanung als kürzester Weg."))
def test_everyday_marked_is_no_jargon():
from checks.model_checks import unexplained_jargon
assert unexplained_jargon("nur die mit 'ja' markierten Kombinationen gehören zum Plan", "Zusammenfassung") == []

View file

@ -64,3 +64,33 @@ def test_fewer_errors_preferred_when_all_have_errors():
def test_mixed_units_trigger_revision():
# beobachtet: Rendite/Kosten in Euro, Risiko als dimensionslose Summe w_i^2 sigma_i^2 (Faktor 100.000 zu klein)
assert make_review(units_consistent=False).needs_revision
# --- Grund jeder Korrekturrunde im Log (vorher nur „Probleme gefunden“) ---
def test_revision_reasons_name_every_trigger():
from checks.review import needs_work, revision_reasons
from core.model_schema import CheckIssue, CheckSeverity
fehlend = {"unclear_point": "Einkaufspreis nicht beziffert", "assumption": "-", "alternative": "-", "impact": "-"}
review = make_review(matches=False, verdict="weaker_than_task", missing=[fehlend])
issues = [CheckIssue(severity=CheckSeverity.ERROR, code="MIXED_OBJECTIVE_UNITS", message="Risiko dimensionslos"),
CheckIssue(severity=CheckSeverity.WARNING, code="MISSING_DATA", message="kein Wert für p")]
gruende = revision_reasons(review, issues)
assert needs_work(review, issues)
assert any(g.startswith("Zielgröße passt nicht") for g in gruende)
assert any("weaker_than_task" in g for g in gruende)
assert any("Einkaufspreis nicht beziffert" in g for g in gruende)
assert any("MIXED_OBJECTIVE_UNITS" in g for g in gruende)
assert not any("MISSING_DATA" in g for g in gruende) # löst keine Runde aus, gehört nicht in den Grund
def test_clean_state_has_no_revision_reasons():
from checks.review import revision_reasons
assert revision_reasons(make_review(), []) == []
def test_review_prompt_allows_both_forms_of_decision_independent_costs():
# wagner_whitin_dijkstra: Einkaufspreis ohne Zahl – je 1–2 Korrekturrunden, weil Regel 4 beide Formen beanstanden konnte
from pathlib import Path
prompt = (Path(__file__).parent.parent / "checks" / "Pruef-Prompt.txt").read_text(encoding="utf-8")
assert "entscheidungsunabhängige Terme" in prompt and "Verlange keine Umstellung" in prompt

View file

@ -811,3 +811,26 @@ def test_scenario_without_result_is_a_finding_unless_proven_unsolvable(wert, sta
assert bool(befunde) == befund
if befund:
assert "Ausgewogene Skalarisierung" in befunde[0] and "Startlösung" in befunde[0]
@pytest.mark.parametrize("modus, bwrap_da, erwartet", [
("auto", False, "bubblewrap ist nicht installiert"), # früher stillschweigend ungeschützt
("aus", True, "abgeschaltet"),
("auto", True, None),
("bwrap", False, None), # Lauf wird ohnehin verweigert
])
def test_sandbox_warning_names_unprotected_runs(monkeypatch, modus, bwrap_da, erwartet):
from solving import solver_stage
monkeypatch.setattr(solver_stage, "SANDBOX", modus)
monkeypatch.setattr(solver_stage.shutil, "which", lambda name: "/usr/bin/bwrap" if bwrap_da else None)
warnung = solver_stage.sandbox_warning()
assert (warnung is None) if erwartet is None else (erwartet in warnung)
def test_unprotected_run_carries_the_warning_into_the_result(monkeypatch):
from solving import solver_stage
from test_model_checks import make_spec
monkeypatch.setattr(solver_stage, "SANDBOX", "aus")
code = 'def main():\n """Test."""\n report("optimal", 1.0, {})\n\n\nif __name__ == "__main__":\n main()\n'
run = solver_stage._check_and_run(code, 30, make_spec())
assert any("Sandbox abgeschaltet" in w for w in run.warnings)