feat: Report-Aufbau neu – Inhaltsverzeichnis, Aufgabe gerendert, Ergebnis nach dem Modell

Reihenfolge (Nutzerentscheidung): Titel → Laufdaten → Warnkasten → klickbares
Inhaltsverzeichnis (HTML, H2+H3) → Aufgabe → beigelegte Daten → Was in der Aufgabe
steht → Einordnung → „Wie das Modell die Aufgabe sieht“ (eigene H2) → Mathematisches
Modell → Ergebnis → Prüfbericht → Solver-Programm.

- Aufgabe als Markdown mit gerenderten Formeln (markdown-it-py, html=False,
  breaks=True; Formeln vorher durch Platzhalter geschützt); Formelsatz in reinem Text
  (nur_formeln.md) bleibt <pre>.
- „$0$“ gilt als Formel, „$5 und $7“ weiter als Geldbetrag (_is_money).
- markdown-it-py ausdrücklich in requirements.txt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-09-18 17:44:26 +02:00
commit 29d058d8f1
3 changed files with 192 additions and 25 deletions

View file

@ -86,10 +86,12 @@ def test_decomposition_table_gets_waterfall():
assert page.count("var(--viz-total)") == 1 # Gesamtbalken nur im Wasserfall
def test_result_comes_first_and_headline_is_lead():
def test_result_follows_the_model_and_headline_is_lead():
# Aufbau seit 18.09.2026: Aufgabe → … → Erklärung → Modell → Ergebnis → Prüfbericht
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 h2 == ["Aufgabe", "Einordnung", "Wie das Modell die Aufgabe sieht", "Mathematisches Modell",
"Ergebnis", "Prüfbericht"]
assert ("lead", "Das Portfolio erreicht 9,76 %.") in doc
assert '<p class="lead">' in to_html(doc)
@ -141,3 +143,51 @@ def test_graph_section_explains_before_it_shows():
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
# --- Kopf, Inhaltsverzeichnis und gerenderte Aufgabe ---
def test_head_is_title_run_data_then_table_of_contents():
doc = build_document(make_result([ResultTable.model_validate(PORTFOLIO_TABLE)]))
assert [b[0] for b in doc[:3]] == ["h1", "list", "toc"] # zuverlässig: kein Warnkasten
assert "toc" not in to_markdown(doc) and "Inhalt" not in to_markdown(doc)
def test_table_of_contents_links_every_section_but_no_h4():
page = to_html(build_document(make_result([ResultTable.model_validate(PORTFOLIO_TABLE)])))
nav = page[page.index('<nav class="toc"'):page.index("</nav>")]
links = re.findall(r'href="#([^"]+)"', nav)
assert links and all(f'id="{anker}"' in page for anker in links) # jeder Link hat ein Ziel
assert len(links) == len(set(links))
for titel in ("Aufgabe", "Wie das Modell die Aufgabe sieht", "Mathematisches Modell", "Ergebnis", "Prüfbericht"):
assert f">{titel}</a>" in nav
assert "Restriktionen</a>" in nav # H3
assert not re.search(r"<h4 id=", page) # H4 bleibt ohne Anker und außerhalb
def test_task_markdown_and_formulas_are_rendered():
from reporting.report import task_html
text = ("### 1. Problemstellung\n\nAus $N = 100$ Aktien werden **genau** $k = 20$ gewählt:\n\n"
"- Rendite $\\mu_i$\n- Risiko \\(w^T \\Sigma w\\)\n\n| A | B |\n|---|---|\n| 1 | 2 |\n\n"
"$$\\max \\sum_{i} \\mu_i w_i$$\n\nDas kostet $5 und $7 Gebühr. <script>alert(1)</script>")
seite = task_html(text)
assert "<h3>1. Problemstellung</h3>" in seite and "<strong>genau</strong>" in seite
assert r"\(N = 100\)" in seite and r"\(\mu_i\)" in seite and r"\(w^T \Sigma w\)" in seite
assert r"\[\max \sum_{i} \mu_i w_i\]" in seite # Unterstrich nicht von Markdown zerstört
assert "<table>" in seite and "<li>" in seite
assert "$5 und $7" in seite # Geldbeträge bleiben Text
assert "<script>" not in seite and "&lt;script&gt;" in seite # kein Roh-HTML aus dem Aufgabentext
def test_plain_formula_text_stays_preformatted():
# tasks/nur_formeln.md: Markdown machte aus „*x1 + 23*“ Kursivschrift und zöge die Zeilen zusammen
from reporting.report import task_html
text = "min z = 17*x1 + 23*x2\n\ns.t.\n\n 6*x1 + 4*x2 >= 120\n 3*x1 + 9*x2 >= 105"
seite = task_html(text)
assert seite.startswith('<pre class="task">') and "<em>" not in seite and " 6*x1 + 4*x2 &gt;= 120" in seite
def test_line_breaks_of_the_task_are_kept():
from reporting.report import task_html
seite = task_html("Pfeile:\n1 → 2: 3\n2 → 4: 5")
assert seite.count("<br") == 2

View file

@ -123,10 +123,10 @@ def test_report_shows_prominent_warning_for_unreliable_result():
session = FakeSession(specs=[HALLUCINATED] * 3, reviews=[MISMATCH] * 3)
result = run_pipeline("Aufgabe", session=session, max_revisions=2, max_restarts=0)
doc = build_document(result)
assert doc[1][0] == "warn" # direkt unter der Überschrift
assert [b[0] for b in doc[:4]] == ["h1", "list", "warn", "toc"] # Titel → Laufdaten → Warnkasten → Inhalt
assert '<div class="warn">⚠ Ergebnis unzuverlässig' in to_html(doc)
assert "> **⚠ Ergebnis unzuverlässig" in to_markdown(doc)
assert "Der Solver wurde nicht gestartet." in doc[1][1]
assert "Der Solver wurde nicht gestartet." in doc[2][1]
def test_report_shows_the_rejected_reading_of_each_assumption():

View file

@ -52,3 +52,9 @@ def test_markdown_table_cell_escapes_pipes_outside_and_inside_math():
assert rendered == "Norm $\\Vert w\\Vert $ \\| Rest"
assert _md_inline(Math("|x|"), in_table=True) == "$\\vert x\\vert $"
assert _md_inline(Math("x"), in_table=False) == "$x$"
def test_number_between_dollars_is_a_formula_but_amounts_stay_text():
# beobachtet in tasks/komplexes_Portfolio_Management_Nr2.md: „$0$ sonst“ blieb Rohtext
assert (True, "0") in split_math("z_i = 1, sonst $0$ sonst")
assert all(not m for m, _ in split_math("Das kostet $5 und $7 Gebühr."))