refactor: Pipeline-Stufen in Ordner mit ihren Prompts

Im Wurzelverzeichnis liegt nur noch TextToModel.py (Ablauf und CLI).
Jede Stufe liegt mit Code und Prompt in einem eigenen Ordner: core,
understanding, modeling, checks, solving, reporting, data, benchmark;
Tests unter tests/, Aufgaben unter tasks/. understand_task,
text_to_model/revise_model und review_assumptions/needs_work/select_best
sind aus TextToModel.py zu ihren Prompts gewandert, inhaltlich
unveraendert. Prompt-Kennung des Benchmarks bleibt 93744636095a.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-09-17 12:11:51 +02:00
commit 7459690bf5

138
tests/test_charts.py Normal file
View file

@ -0,0 +1,138 @@
"""Inline-SVG-Diagramme: gültiges SVG, korrekte Geometrie, Einbindung in den Report."""
import xml.etree.ElementTree as ET
import pytest
from reporting import charts
from core.model_schema import PipelineResult, ResultTable, SolverRun, SolverStatus
from reporting.report import build_document, to_html, to_markdown
from test_model_checks import make_spec
from test_result_tables import PORTFOLIO_TABLE
from test_review import make_review
LABELS = ["A02", "A08", "A03"]
VALUES = [0.03433, 0.03093, 0.02587]
def parse(svg: str):
assert svg.startswith("<svg")
return ET.fromstring(svg)
def test_bar_chart_is_valid_svg_with_one_bar_per_label():
root = parse(charts.bar_chart(LABELS, VALUES, "Portfolio"))
assert len(root.findall(".//{http://www.w3.org/2000/svg}path")) == len(LABELS)
texts = [t.text for t in root.findall(".//{http://www.w3.org/2000/svg}text")]
for label in LABELS:
assert label in texts # Direktbeschriftung statt Legende (eine Serie)
assert root.get("role") == "img" and "Portfolio" in root.get("aria-label")
def test_bars_are_proportional_and_capped_in_thickness():
svg = charts.bar_chart(["groß", "klein"], [1.0, 0.5], "Test")
widths = [float(d.split("H")[1].split(" ")[0].split(",")[0]) for d in
[p.get("d") for p in parse(svg).findall(".//{http://www.w3.org/2000/svg}path")]]
assert widths[0] > widths[1]
heights = {float(p.get("d").split("V")[1].split(" ")[0]) for p in parse(svg).findall(".//{http://www.w3.org/2000/svg}path")}
assert charts.BAR_HEIGHT <= 24
def test_negative_values_use_the_other_colour_role():
svg = charts.bar_chart(["Ertrag", "Kosten"], [1.0, -0.4], "Test")
fills = [p.get("fill") for p in parse(svg).findall(".//{http://www.w3.org/2000/svg}path")]
assert fills == ["var(--viz-pos)", "var(--viz-neg)"]
def test_waterfall_has_step_per_term_plus_total():
svg = charts.waterfall_chart(["Rendite", "Risiko", "Kosten"], [0.1277, -0.0052, -0.0256],
"Zielwert", 0.0969, "Zielwert-Zerlegung", " ")
root = parse(svg)
fills = [p.get("fill") for p in root.findall(".//{http://www.w3.org/2000/svg}path")]
assert fills == ["var(--viz-pos)", "var(--viz-neg)", "var(--viz-neg)", "var(--viz-total)"]
assert "Zielwert" in [t.text for t in root.findall(".//{http://www.w3.org/2000/svg}text")]
def test_tooltip_per_mark():
root = parse(charts.bar_chart(LABELS, VALUES, "Portfolio"))
titles = [t.text for t in root.findall(".//{http://www.w3.org/2000/svg}title")]
assert len(titles) == len(LABELS) and titles[0].startswith("A02: ")
def make_result(tables):
return PipelineResult(
task="Aufgabe", specification=make_spec(), review=make_review(), revisions=0,
solver_run=SolverRun(status=SolverStatus.OPTIMAL, objective_value=0.09756, tables=tables,
headline="Das Portfolio erreicht 9,76 %.", solution={"w[A02]": 0.3}),
)
def test_html_report_embeds_chart_and_markdown_does_not():
doc = build_document(make_result([ResultTable.model_validate(PORTFOLIO_TABLE)]))
page = to_html(doc)
assert '<figure class="viz-root">' in page and "<svg" in page
assert "<svg" not in to_markdown(doc)
def test_decomposition_table_gets_waterfall():
table = ResultTable(title="Zielwert-Zerlegung", columns=["Bestandteil", "Wert"],
rows=[["Rendite", 0.1277], ["Risiko", -0.0052], ["Kosten", -0.0256], ["Zielwert", 0.0969]],
objective_column="Wert")
page = to_html(build_document(make_result([table])))
assert page.count("var(--viz-total)") == 1 # Gesamtbalken nur im Wasserfall
def test_result_comes_first_and_headline_is_lead():
doc = build_document(make_result([ResultTable.model_validate(PORTFOLIO_TABLE)]))
h2 = [b[1] for b in doc if b[0] == "h2"]
assert h2[0] == "Ergebnis" and h2.index("Ergebnis") < h2.index("Mathematisches Modell")
assert ("lead", "Das Portfolio erreicht 9,76 %.") in doc
assert '<p class="lead">' in to_html(doc)
@pytest.mark.parametrize("table", [
ResultTable(title="Ohne Zahlen", columns=["A", "B"], rows=[["x", "1,5 %"]], objective_column="B"),
ResultTable(title="Nur eine Zeile", columns=["A", "B"], rows=[["x", 1.0]], objective_column="B"),
ResultTable(title="Ohne Beitragsspalte", columns=["A", "B"], rows=[["x", 1.0], ["y", 2.0]]),
])
def test_no_chart_without_usable_numbers(table):
assert "<svg" not in to_html(build_document(make_result([table])))
# --- Graphenzeichnung ---
def test_graph_chart_is_valid_svg_and_marks_the_path():
import xml.etree.ElementTree as ET
from reporting.charts import graph_chart
svg = graph_chart(["0", "1", "2", "3"],
[("0", "1", 8), ("0", "2", 18), ("0", "3", 31), ("1", "2", 10), ("1", "3", 21), ("2", "3", 12)],
path=["0", "1", "3"], title="Graph")
ET.fromstring(svg)
assert svg.count("<circle") == 4 # jeder Knoten einmal
assert svg.count("marker-end") == 6 # jeder Bogen einmal, und gerichtet
assert svg.count("--viz-pos") >= 2 # Weg und Knoten des Weges hervorgehoben
def test_graph_chart_refuses_unknown_nodes_and_huge_graphs():
from reporting.charts import graph_chart
assert graph_chart(["a"], [("a", "b", 1)]) == "" # Bogen ins Nichts
assert graph_chart([str(i) for i in range(20)], [("0", "1", 1)]) == "" # zu groß zum Zeichnen
assert graph_chart([], []) == ""
def test_graph_section_explains_before_it_shows():
from core.model_schema import GraphSolution, SolverRun, SolverStatus
from reporting.report import _graph_section
run = SolverRun(status=SolverStatus.OPTIMAL, objective_value=29, graph=GraphSolution.model_validate({
"nodes": ["0", "1", "3"],
"arcs": [{"source": "0", "target": "1", "weight": 8}, {"source": "1", "target": "3", "weight": 21}],
"path": ["0", "1", "3"],
"steps": [{"node": "0", "label": 0}, {"node": "1", "label": 8}, {"node": "3", "label": 29}],
"node_meaning": "ein Zeitpunkt – das Ende von Jahr i.",
"arc_meaning": "Kauf am Ende von Jahr i und Nutzung bis Ende von Jahr j.",
}))
doc = _graph_section(run)
arten = [block[0] for block in doc]
assert arten[0] == "p" and "Ende von Jahr" in doc[0][1] # Erklärung steht vor der Zeichnung
assert "svg" in arten
assert any(b[0] == "table" and "Markierungsverlauf" in str(doc[arten.index("table") - 1]) for b in doc)
assert any("8 + 21 = 29" in str(b) for b in doc) # Rechenweg ausgeschrieben