from checks.model_checks import check_spec, has_errors from core.model_schema import OptimizationModelSpecification def make_spec(**overrides) -> OptimizationModelSpecification: data = { "problem_class": "Linear Programming (LP)", "target_stack": "SciPy (scipy.optimize.linprog)", "routing_rationale": "-", "executive_summary": "-", "indices": ["j \\in J (Produkte)"], "parameters": [ {"symbol": "c_j", "dimension": "Vektor", "unit": "€/t", "description": "Kosten", "extracted_values": "[3, 2]"}, {"symbol": "b", "dimension": "Skalar", "unit": "t", "description": "Bedarf", "extracted_values": "4"}, ], "variables": [ {"name": "x_{j}", "var_type": "continuous", "domain": ">= 0", "description": "Menge"}, ], "objective": {"sense": "minimize", "latex_formula": "\\min \\sum_{j \\in J} c_j x_j", "description": "-"}, "constraints": [ {"identifier": "Bedarf", "latex_formula": "\\sum_{j \\in J} x_j \\ge b", "description": "-"}, ], "suggested_export_format": "Matrix-Dict", } data.update(overrides) return OptimizationModelSpecification.model_validate(data) def codes(spec): return {issue.code for issue in check_spec(spec)} def test_valid_spec_has_no_issues(): assert check_spec(make_spec()) == [] def test_binary_variable_in_lp_is_error(): spec = make_spec(variables=[{"name": "x_j", "var_type": "binary", "domain": "{0,1}", "description": "-"}]) assert {"LP_WITH_DISCRETE_VARS", "SCIPY_WITH_DISCRETE_VARS"} <= codes(spec) assert has_errors(check_spec(spec)) def test_stack_mismatch_is_error(): spec = make_spec(problem_class="Quadratic Programming (QP)") assert "STACK_MISMATCH" in codes(spec) def test_continuous_variable_with_cpsat_is_error(): spec = make_spec(problem_class="Constraint Programming (CP)", target_stack="Google OR-Tools (CP-SAT)") assert "CPSAT_WITH_CONTINUOUS_VARS" in codes(spec) def test_quadratic_term_in_lp_is_warning(): spec = make_spec(objective={"sense": "minimize", "latex_formula": "\\min \\sum_j c_j x_j^2", "description": "-"}) issues = check_spec(spec) assert "NONLINEAR_TERM" in codes(spec) assert not has_errors(issues) def test_undeclared_symbol_is_warning(): spec = make_spec(constraints=[ {"identifier": "Kapazitaet", "latex_formula": "\\sum_{j} a_j x_j \\le K", "description": "-"}, ]) messages = [i.message for i in check_spec(spec) if i.code == "UNDECLARED_SYMBOL"] assert any("'a'" in m for m in messages) assert any("'K'" in m for m in messages) def test_superscript_and_escaped_underscore_names_are_recognized(): spec = make_spec( parameters=[ {"symbol": "C^{prod}_{j}", "dimension": "Vektor", "unit": "€", "description": "-", "extracted_values": "1"}, {"symbol": "min\\_staff_1", "dimension": "Skalar", "unit": "Personen", "description": "-", "extracted_values": "3"}, {"symbol": "\\Sigma", "dimension": "Matrix", "unit": "dimensionslos", "description": "-", "extracted_values": "I"}, ], objective={"sense": "minimize", "latex_formula": "\\min \\sum_{j} C^{prod}_{j} x_{j}", "description": "-"}, constraints=[ {"identifier": "A", "latex_formula": "\\sum_{j} x_j \\ge min\\_staff_1 \\quad \\forall j \\in J", "description": "-"}, {"identifier": "B", "latex_formula": "x^T \\Sigma x \\le 1 \\text{ (Risiko)}", "description": "-"}, ], problem_class="Quadratic Programming (QP)", target_stack="CVXPY", ) assert "UNDECLARED_SYMBOL" not in codes(spec) def test_missing_parameter_values_is_warning(): spec = make_spec(parameters=[{"symbol": "c_j", "dimension": "Vektor", "unit": "€", "description": "-"}]) assert "MISSING_DATA" in codes(spec) def test_no_constraints_is_error(): assert "NO_CONSTRAINTS" in codes(make_spec(constraints=[])) def test_indices_with_inline_math_delimiters_are_declared(): # beobachtet: "Mitarbeitende: \(p \in P\)" führte zu Fehlalarmen für p, P, d, D, Mo, Di … spec = make_spec( indices=["Mitarbeitende: \\(p \\in P = \\{A,B\\}\\)", "Tage: \\(d \\in D = \\{Mo,Di\\}\\) (Folgetag d+1)"], variables=[{"name": "x_{p,d}", "var_type": "continuous", "domain": ">= 0", "description": "-"}], objective={"sense": "minimize", "latex_formula": "\\min \\sum_{p \\in P} \\sum_{d \\in D} x_{p,d}", "description": "-"}, constraints=[{"identifier": "Folgetag", "latex_formula": "x_{p,Mo} + x_{p,Di} \\le 1 \\quad \\forall p \\in P", "description": "-"}], ) assert "UNDECLARED_SYMBOL" not in codes(spec) def test_miqp_with_scip_is_consistent(): spec = make_spec( problem_class="Mixed-Integer Quadratic Programming (MIQP)", target_stack="SCIP (pyscipopt)", variables=[{"name": "w_j", "var_type": "continuous", "domain": "[0,1]", "description": "-"}, {"name": "y_j", "var_type": "binary", "domain": "{0,1}", "description": "-"}], objective={"sense": "maximize", "latex_formula": "\\max \\sum_j c_j w_j - \\sum_j c_j w_j^2 - b \\sum_j y_j", "description": "-"}, constraints=[{"identifier": "Kopplung", "latex_formula": "w_j \\le y_j", "description": "-"}], ) assert check_spec(spec) == [] # insbesondere kein NONLINEAR_TERM, kein Routing-Fehler def test_quadratic_with_binary_on_cvxpy_is_error(): spec = make_spec( problem_class="Quadratic Programming (QP)", target_stack="CVXPY", variables=[{"name": "y_j", "var_type": "binary", "domain": "{0,1}", "description": "-"}], ) assert "CVXPY_WITH_DISCRETE_VARS" in codes(spec) assert has_errors(check_spec(spec)) def test_miqp_without_discrete_vars_is_warning(): spec = make_spec(problem_class="Mixed-Integer Quadratic Programming (MIQP)", target_stack="SCIP (pyscipopt)") assert "MIQP_WITHOUT_DISCRETE_VARS" in codes(spec) assert not has_errors(check_spec(spec)) def test_every_problem_class_has_allowed_stacks(): from checks.model_checks import ALLOWED_STACKS from core.model_schema import ProblemClass assert set(ALLOWED_STACKS) == set(ProblemClass) def test_nlp_with_ipopt_is_consistent(): spec = make_spec(problem_class="Nonlinear Programming (NLP)", target_stack="Ipopt (cyipopt)") assert not has_errors(check_spec(spec)) def test_ipopt_with_integer_vars_is_error(): spec = make_spec(problem_class="Nonlinear Programming (NLP)", target_stack="Ipopt (cyipopt)", variables=[{"name": "n_j", "var_type": "integer", "domain": ">= 0", "description": "-"}]) assert "IPOPT_WITH_DISCRETE_VARS" in codes(spec) # --- Einheiten der Zielfunktion (ParameterDefinition.unit, ohne LLM) --- def portfolio_spec(risk_unit: str, objective: str): """Portfolio: Rendite und Kosten in Euro, Risiko wahlweise dimensionslos oder in Euro.""" return make_spec( problem_class="Quadratic Programming (QP)", target_stack="CVXPY", objective={"sense": "maximize", "latex_formula": objective, "description": "-"}, parameters=[ {"symbol": "\\mu_i", "dimension": "Vektor", "unit": "€", "description": "Ertrag", "extracted_values": "[1]"}, {"symbol": "\\lambda", "dimension": "Skalar", "unit": "dimensionslos", "description": "Risikoaversion", "extracted_values": "0.5"}, {"symbol": "\\Sigma", "dimension": "Matrix", "unit": risk_unit, "description": "Kovarianz", "extracted_values": "[[1]]"}, ], variables=[{"name": "w_i", "var_type": "continuous", "domain": "[0, 1]", "description": "Anteil"}], constraints=[{"identifier": "Budget", "latex_formula": "\\sum_i w_i \\le 1", "description": "-"}], ) def test_dimensionless_risk_next_to_euro_amounts_is_error(): # beobachtet: Risiko als Summe über Anteile ist um den Faktor Kapital zu klein und damit wirkungslos spec = portfolio_spec("dimensionslos", "\\max \\sum_i \\mu_i w_i - \\lambda w^T \\Sigma w") assert "MIXED_OBJECTIVE_UNITS" in codes(spec) assert has_errors(check_spec(spec)) def test_risk_scaled_to_euro_is_accepted(): spec = portfolio_spec("€", "\\max \\sum_i \\mu_i w_i - \\lambda w^T \\Sigma w") assert "MIXED_OBJECTIVE_UNITS" not in codes(spec) def test_single_term_objective_is_never_mixed(): spec = portfolio_spec("dimensionslos", "\\min w^T \\Sigma w") assert "MIXED_OBJECTIVE_UNITS" not in codes(spec) def test_index_range_is_not_split_into_terms(): # \sum_{i \in I} enthält kein Minus, wohl aber Klammern – der Term darf nicht zerschnitten werden from checks.model_checks import _additive_terms assert len(_additive_terms("\\min \\sum_{j \\in J} c_j x_j")) == 1 assert len(_additive_terms("\\max \\sum_i \\mu_i w_i - \\lambda w^T \\Sigma w")) == 2 assert len(_additive_terms("\\min z = a x + b y")) == 2 # linke Seite zählt nicht mit def test_unknown_units_produce_no_finding(): # Stunden neben Stück: kein Fehlalarm, geprüft werden nur Geld gegen dimensionslos spec = make_spec(parameters=[ {"symbol": "c_j", "dimension": "Vektor", "unit": "h", "description": "Zeit", "extracted_values": "[3]"}, {"symbol": "b", "dimension": "Skalar", "unit": "Stück", "description": "Bedarf", "extracted_values": "4"}, ], objective={"sense": "minimize", "latex_formula": "\\min \\sum_j c_j x_j + b", "description": "-"}) assert "MIXED_OBJECTIVE_UNITS" not in codes(spec) def test_objective_label_is_not_an_undeclared_symbol(): # beobachtet: Zielfunktion "Z = 12\,x" meldete Z als undeklariertes Symbol spec = make_spec(objective={"sense": "maximize", "latex_formula": "Z = 12\\,x_j", "description": "-"}) assert "UNDECLARED_SYMBOL" not in codes(spec) def test_symbol_right_of_the_equals_sign_is_still_checked(): spec = make_spec(objective={"sense": "maximize", "latex_formula": "Z = q \\cdot x_j", "description": "-"}) assert "UNDECLARED_SYMBOL" in codes(spec) def test_number_sets_are_not_symbols(): # beobachtet: Restriktion "x \in \mathbb{Z}" meldete Z als undeklariertes Symbol spec = make_spec(constraints=[ {"identifier": "ganzzahligkeit", "latex_formula": "x_j \\in \\mathbb{Z}", "description": "-"}, {"identifier": "vorzeichen", "latex_formula": "x_j \\in \\mathbb{R}_{\\ge 0}", "description": "-"}, ]) assert "UNDECLARED_SYMBOL" not in codes(spec) def test_implicit_products_are_not_undeclared(): # beobachtet: "\sin(cx)" meinte c*x, der Check las 'cx' als eigenen Namen (ebenso 'xy') spec = make_spec( parameters=[{"symbol": "c", "dimension": "Skalar", "unit": "1/K", "description": "-", "extracted_values": "3"}, {"symbol": "q", "dimension": "Skalar", "unit": "dimensionslos", "description": "-", "extracted_values": "0.5"}], variables=[{"name": "x", "var_type": "continuous", "domain": "[0, 4]", "description": "-"}, {"name": "y", "var_type": "continuous", "domain": "[0, 4]", "description": "-"}], objective={"sense": "maximize", "latex_formula": "\\max x\\sin(cx) + y\\sin(cy) + q\\,xy", "description": "-"}, constraints=[{"identifier": "Sicherheit", "latex_formula": "x + y \\le 6", "description": "-"}], ) assert "UNDECLARED_SYMBOL" not in codes(spec) def test_real_names_are_still_reported(): # Gegenprobe: 'Kapazitaet' besteht nicht nur aus deklarierten Einzelsymbolen spec = make_spec(constraints=[ {"identifier": "K1", "latex_formula": "x_j \\le Kapazitaet", "description": "-"}, ]) assert "UNDECLARED_SYMBOL" in codes(spec) # --- Fachbegriffe müssen erklärt sein (Block ERKLÄREN im Routing-Prompt) --- def test_unexplained_jargon_is_reported(): # beobachtet: Restriktion "Flusserhaltung_Jahr1", Zusammenfassung erwähnt "Fluss" nirgends spec = make_spec( executive_summary="Der Bauer entscheidet, wann er die Maschine ersetzt.", constraints=[{"identifier": "Flusserhaltung_Jahr1", "latex_formula": "\\sum_j x_j = 1", "description": "-"}, {"identifier": "Quelle_Startkauf", "latex_formula": "\\sum_j x_j = 1", "description": "-"}], ) meldungen = [i.message for i in check_spec(spec) if i.code == "UNEXPLAINED_JARGON"] assert meldungen and "Fluss" in meldungen[0] assert not has_errors(check_spec(spec)) # Warnung, kein Fehler: sonst gälte ein richtiges Ergebnis als unzuverlässig def test_explained_jargon_is_accepted(): spec = make_spec( executive_summary="Ein Bogen i → j bedeutet Kauf am Ende von Jahr i. Die Flusserhaltung sorgt dafür, " "dass die Kette nicht abreißt: zu jedem Knoten gehört genau ein Nachfolger.", constraints=[{"identifier": "Flusserhaltung_Jahr1", "latex_formula": "\\sum_j x_j = 1", "description": "-"}], indices=["Knoten j \\in J (Jahresenden)"], ) assert "UNEXPLAINED_JARGON" not in codes(spec) def test_everyday_words_are_not_jargon(): from checks.model_checks import unexplained_jargon assert unexplained_jargon("Menge je Produkt und Lager, Kosten in Euro", "Eine schlichte Zusammenfassung.") == [] # --- Im Aufgabentext verlangtes Verfahren (check_requested_method) --- DIJKSTRA_AUFGABE = "a) Formulieren Sie das Problem als Kürzeste-Wege-Problem!\nb) Lösen Sie es mit dem Dijkstra-Algorithmus!" def test_requested_dijkstra_with_flow_model_is_error(): # beobachtet: Fall graph_ersatz_dijkstra wurde als MILP-Flussmodell geloest - richtig gerechnet, Aufgabe verfehlt spec = make_spec(problem_class="Mixed-Integer Linear Programming (MILP)", target_stack="HiGHS", variables=[{"name": "x_{ij}", "var_type": "binary", "domain": "{0,1}", "description": "-"}]) issues = check_spec(spec, DIJKSTRA_AUFGABE) assert "REQUESTED_METHOD_IGNORED" in {i.code for i in issues} assert has_errors(issues) # loest eine Korrekturrunde aus, die neu routen kann def test_requested_dijkstra_with_networkx_is_accepted(): spec = make_spec(problem_class="Kürzeste Wege im Graphen (Shortest Path)", target_stack="NetworkX (Dijkstra)", variables=[{"name": "x_{ij}", "var_type": "binary", "domain": "{0,1}", "description": "-"}]) assert "REQUESTED_METHOD_IGNORED" not in {i.code for i in check_spec(spec, DIJKSTRA_AUFGABE)} def test_method_check_stays_quiet_without_a_named_method(): # ohne Aufgabentext und ohne genanntes Verfahren darf die Pruefung nichts melden spec = make_spec() assert "REQUESTED_METHOD_IGNORED" not in codes(spec) assert "REQUESTED_METHOD_IGNORED" not in {i.code for i in check_spec(spec, "Minimiere die Kosten.")} def test_every_checker_marker_in_the_prompts_exists(): """Jede [→ X]-Kennung in den Prompts muss einem echten Check-Code oder einer Prüffunktion entsprechen. Verhindert, dass eine Regel eine Prüfung behauptet, die es nicht (mehr) gibt. """ import re from pathlib import Path from checks import model_checks from solving import solver_stage quelle = Path(model_checks.__file__).read_text() + Path(solver_stage.__file__).read_text() bekannt = set(re.findall(r'code="([A-Z_]+)"', quelle)) | set(re.findall(r"^def (\w+)", quelle, re.M)) marker = set() projekt = Path(model_checks.__file__).parent.parent for datei in projekt.glob("*/*-Prompt.txt"): for treffer in re.findall(r"\[→ ([^\]]+)\]", datei.read_text(encoding="utf-8")): marker |= {teil.strip() for teil in treffer.split(",") if teil.strip() != "…"} # Legende ausnehmen assert marker, "keine Prüfer-Kennungen in den Prompts gefunden" assert marker <= bekannt, f"Kennung ohne zugehörige Prüfung: {sorted(marker - bekannt)}" # --- Verstehensstufe: Zahlen und Teilfragen des Aufgabentexts --- def make_understanding(**overrides): from core.model_schema import TaskUnderstanding daten = {"is_optimization_task": True, "reason": "-", "facts": [], "ignored_numbers": [], "sub_questions": [{"id": "1", "quote": "-", "kind": "rechnen", "needs_solver": True}]} daten.update(overrides) return TaskUnderstanding.model_validate(daten) def test_numbers_are_normalised_once(): from checks.model_checks import normalize_number assert normalize_number("1.234,56") == "1234.56" # deutsch: Punkt trennt Tausender assert normalize_number("3.8") == "3.8" # Aufgabennummer, kein Tausendertrenner assert normalize_number("1.234") == "1234" assert normalize_number("0,05") == "0.05" assert normalize_number("100.000") == "100000" def test_numbers_inside_words_are_not_numbers(): from checks.model_checks import numbers_in assert numbers_in("Aktie A02, Variable x1, Schicht 3") == {"3"} def test_uncaptured_numbers_are_reported(): from checks.model_checks import check_understanding # beobachtet: 0 von 14 Zahlen des Aufgabentexts landeten in der Spezifikation aufgabe = "Der Deckungsbeitrag ist 12 € je Stück. Mindestens 100 Stück sind abzusetzen." codes = {i.code for i in check_understanding(make_understanding(), aufgabe)} assert "UNCAPTURED_NUMBERS" in codes def test_numbers_quoted_in_a_fact_count_as_captured(): # eine Tabellenzeile als Zitat deckt alle Zahlen dieser Zeile ab from checks.model_checks import check_understanding aufgabe = "| A01 | 6,9 % | 0,68 % | 2,04 % |" verstanden = make_understanding(facts=[{ "value": "6,9 %", "unit": "%", "meaning": "Rendite von A01", "quote": "| A01 | 6,9 % | 0,68 % | 2,04 % |"}]) assert "UNCAPTURED_NUMBERS" not in {i.code for i in check_understanding(verstanden, aufgabe)} def test_explicitly_ignored_numbers_count_as_captured(): from checks.model_checks import check_understanding aufgabe = "## Aufgabe 2.7: Der Preis ist 8 €." verstanden = make_understanding( facts=[{"value": "8", "unit": "€", "meaning": "Preis", "quote": "Der Preis ist 8 €."}], ignored_numbers=[{"value": "2.7", "reason": "Aufgabennummer"}]) assert "UNCAPTURED_NUMBERS" not in {i.code for i in check_understanding(verstanden, aufgabe)} def test_missing_sub_questions_are_reported(): from checks.model_checks import check_understanding aufgabe = "- a) Formulieren Sie das Problem!\n- b) Lösen Sie es mit Dijkstra!" verstanden = make_understanding(sub_questions=[{"id": "a", "quote": "-", "kind": "formulieren", "needs_solver": False}]) meldungen = [i.message for i in check_understanding(verstanden, aufgabe) if i.code == "MISSING_SUB_QUESTIONS"] assert meldungen and "b" in meldungen[0] def test_complete_sub_questions_are_accepted(): from checks.model_checks import check_understanding aufgabe = "- a) Formulieren Sie das Problem!\n- b) Lösen Sie es mit Dijkstra!" verstanden = make_understanding(sub_questions=[ {"id": "a", "quote": "-", "kind": "formulieren", "needs_solver": False}, {"id": "b", "quote": "-", "kind": "rechnen", "needs_solver": True}]) assert "MISSING_SUB_QUESTIONS" not in {i.code for i in check_understanding(verstanden, aufgabe)} def test_split_sub_questions_cover_their_parent(): # beobachtet an "Portfolio Nr. 2": Teil 2 als 2a/2b erfasst, gemeldet wurde "es fehlen: 2" from checks.model_checks import check_understanding aufgabe = "* **Teil 1: Daten**\n* **Teil 2: Optimierung**\n* **Teil 3: Evaluation**" verstanden = make_understanding(sub_questions=[ {"id": i, "quote": "-", "kind": "rechnen", "needs_solver": True} for i in ("Teil 1", "Teil 2a", "Teil 2b", "Teil 3")]) assert "MISSING_SUB_QUESTIONS" not in {i.code for i in check_understanding(verstanden, aufgabe)} # "21" ist eine andere Teilfrage, keine Zerlegung von "2" verstanden = make_understanding(sub_questions=[ {"id": i, "quote": "-", "kind": "rechnen", "needs_solver": True} for i in ("1", "21", "3")]) assert "MISSING_SUB_QUESTIONS" in {i.code for i in check_understanding(verstanden, aufgabe)} def test_captured_facts_must_reach_the_specification(): # beobachtet: Verstehensstufe erfasste die ganze Kostenmatrix, extracted_values blieb None from checks.model_checks import check_data_transfer verstanden = make_understanding(facts=[ {"value": "8", "unit": "€", "meaning": "Kosten 0->1", "quote": "| 0 | 8 | 18 | 31 |"}]) ohne = make_spec(parameters=[{"symbol": "c_{ij}", "dimension": "Matrix", "unit": "€", "description": "Kosten"}]) meldungen = [i.message for i in check_data_transfer(ohne, verstanden)] assert meldungen and "8" in meldungen[0] mit = make_spec(parameters=[{"symbol": "c_{ij}", "dimension": "Matrix", "unit": "€", "description": "Kosten", "extracted_values": "8, 18, 31"}]) assert check_data_transfer(mit, verstanden) == [] def test_data_transfer_tolerates_partially_transferred_values(): # Indexwerte und Zeilenbeschriftungen muessen nicht in extracted_values stehen - kein Fehlalarm from checks.model_checks import check_data_transfer verstanden = make_understanding(facts=[ {"value": "0,1,2", "unit": "Jahr", "meaning": "moegliche Kaufzeitpunkte", "quote": "i = 0,1,2"}, {"value": "8", "unit": "€", "meaning": "Kosten", "quote": "| 0 | 8 |"}]) spec = make_spec(parameters=[{"symbol": "c", "dimension": "Matrix", "unit": "€", "description": "Kosten", "extracted_values": "8"}]) assert check_data_transfer(spec, verstanden) == [] def test_data_transfer_check_is_quiet_without_understanding(): from checks.model_checks import check_data_transfer assert check_data_transfer(make_spec(), None) == [] def test_sum_limits_are_not_an_objective_label(): # beobachtet: "\sum_{i=0}^{2}..." wurde am "=" der Summengrenze zerschnitten -> NONLINEAR_TERM spec = make_spec( problem_class="Kürzeste Wege im Graphen (Shortest Path)", target_stack="NetworkX (Dijkstra)", variables=[{"name": "x_{i,j}", "var_type": "binary", "domain": "{0,1}", "description": "-"}], parameters=[{"symbol": "c_{i,j}", "dimension": "Matrix", "unit": "€", "description": "Kosten", "extracted_values": "8"}], objective={"sense": "minimize", "description": "-", "latex_formula": "\\sum_{i=0}^{2}\\sum_{j=i+1}^{3} c_{i,j}\\, x_{i,j}"}, constraints=[{"identifier": "Weg", "latex_formula": "\\sum_j x_{0,j} = 1", "description": "-"}], ) assert "NONLINEAR_TERM" not in codes(spec) def test_objective_label_is_still_stripped(): from checks.model_checks import _objective_without_label assert _objective_without_label("Z = 12 x").strip() == "12 x" assert _objective_without_label("\\sum_{i=0}^{2} c_i x_i").startswith("\\sum") def test_everyday_words_neither_trigger_nor_explain_jargon(): from checks.model_checks import unexplained_jargon # beobachtet: „beeinflusst“ in der Zusammenfassung galt als Erklärung von „Flusserhaltung“ assert unexplained_jargon("Flusserhaltung", "Die Bestellmenge beeinflusst die Lagerkosten.") == ["Fluss"] assert unexplained_jargon("Der Einfluss des Preises", "Zusammenfassung") == [] assert unexplained_jargon("Flusserhaltung", "Der Fluss durch einen Knoten bleibt erhalten.") == [] def test_unambiguous_english_numbers_are_read_correctly(): from checks.model_checks import normalize_number assert normalize_number("1,234.56") == "1234.56" # vorher 1.23456 assert normalize_number("12,345,678") == "12345678" assert normalize_number("1,500") == "1.5" # mehrdeutig: bleibt deutsch gelesen assert normalize_number("1.234,56") == "1234.56" # deutsch unverändert def test_english_sub_question_markers_are_recognised(): from checks.model_checks import QUESTION_PATTERN for zeile in ("Part 2: Solve the model", "* **Question 1: How many units**", "- Task 3: Compare"): assert QUESTION_PATTERN.search(zeile), zeile assert not QUESTION_PATTERN.search("In part 2 of the text we see") # Fließtext bleibt Fließtext def test_bold_marker_with_colon_inside_is_recognised(): # vorher nur "**Teil 1: Daten**", nicht "**Teil 1:** Daten" (Doppelpunkt vor dem schließenden **) from checks.model_checks import QUESTION_PATTERN for zeile in ("**Teil 1:** Daten laden", "* **Question 2:** Solve", "**a)** Graph zeichnen"): assert QUESTION_PATTERN.search(zeile), zeile