This commit is contained in:
team3
2026-07-10 16:29:35 +02:00
parent 718c18523d
commit a50fb701b5
8 changed files with 115 additions and 41 deletions

View File

@@ -7,8 +7,12 @@ setzen (Lektion 57). Der Prüfer ist EIN verschmolzener Call (Fakten+Coverage,
Lektion 52); deterministische Checks laufen davor im Code."""
import asyncio
import json
import logging
import os
import re
import subprocess
from pathlib import Path
import db
import llm
@@ -17,6 +21,33 @@ from config import SECTION_WOERTER_PRO_ATOM
log = logging.getLogger("creator2.guide")
_KATEX_SKRIPT = Path(__file__).resolve().parent / "katex_check.mjs"
_KATEX_BASE = Path(__file__).resolve().parent.parent / "frontend"
_katex_gewarnt = False
def _katex_fehler(formeln: list[dict]) -> list[str]:
"""Parse-Gate: Formeln durch DENSELBEN Renderer wie das Frontend jagen.
Regex-Checks jagen eine offene Menge (heute \\textsc, morgen der nächste
unbekannte Befehl) — das Gate schließt die Klasse. Fake-Modus überspringt
(deterministisch + schnell); Unit-Test ruft direkt."""
global _katex_gewarnt
if not formeln or os.getenv("CREATOR_FAKE_AGENTS"):
return []
try:
res = subprocess.run(
["node", str(_KATEX_SKRIPT)], input=json.dumps(formeln),
capture_output=True, text=True, timeout=30,
env={**os.environ, "KATEX_BASE": str(_KATEX_BASE)})
if res.returncode:
raise RuntimeError(res.stderr.strip()[:200])
return json.loads(res.stdout)
except Exception as e: # nie fail-open OHNE Signal (Befund-Prinzip)
if not _katex_gewarnt:
_katex_gewarnt = True
log.warning("KaTeX-Gate nicht verfügbar (%s) — Formel-Validierung fällt aus", e)
return []
EBENE = "guide"
_MARKER = re.compile(r"<!--\s*atom:\s*(\d+)\s*\|")
@@ -125,6 +156,24 @@ def _mathe_auftraege(text: str, wo: str) -> list[str]:
if reste:
auftraege.append(f"{wo}: Formeln im Klartext ({', '.join(reste[:4])}…):"
f" auch Indizes/Potenzen gehören in $…$ (x_i → $x_i$).")
betraege = sorted(set(re.findall(r"\|[A-Za-z][^|\n]{0,8}\|\*?", ohne)))
if betraege: # |C*| im Klartext: Markdown deutet * als Kursiv-Start
auftraege.append(f"{wo}: Beträge im Klartext ({', '.join(betraege[:4])}…):"
f" in $…$ setzen ($|C^*|$) — sonst kollidiert * mit Markdown.")
for z in ohne.splitlines(): # einzelnes * = Kursiv-Unfall (nur **fett** erlaubt)
z = re.sub(r"^\s*[*-]\s", "", z)
if re.search(r"(?<!\*)\*(?!\*)", z):
auftraege.append(f"{wo}: einzelnes „*“ im Fließtext startet ungewollt"
f" Kursivschrift — Sterne nur als **fett** oder in $…$.")
break
if re.search(r"\\n(?![a-zA-Z])", text): # \neq etc. bleiben unberührt
auftraege.append(f"{wo}: literales „\\n“ im Text — durch echten"
f" Zeilenumbruch bzw. Leerzeile ersetzen.")
formeln = ([{"tex": m, "display": True} for m in re.findall(r"\$\$([\s\S]+?)\$\$", text)]
+ [{"tex": m, "display": False} for m in re.findall(r"\$([^$\n]+?)\$", text)])
for f in _katex_fehler(formeln):
auftraege.append(f"{wo}: Formel rendert nicht — {f}. In gültiges KaTeX"
f" umschreiben (Standardbefehle, kein LaTeX-Textsatz).")
return auftraege

28
backend/katex_check.mjs Normal file
View File

@@ -0,0 +1,28 @@
// KaTeX-Parse-Gate: prüft Formeln mit DEMSELBEN Renderer wie das Frontend.
// stdin: JSON [{tex, display}] → stdout: JSON [fehlermeldung, …]
// Ersetzt die Regex-Jagd auf einzelne Befehle: was hier durchgeht, rendert auch.
import { createRequire } from 'node:module'
import { readFileSync } from 'node:fs'
let katex
try {
katex = createRequire(import.meta.url)('katex') // Container: backend/node_modules
} catch {
const basis = process.env.KATEX_BASE // Dev: frontend/node_modules
katex = createRequire(basis.endsWith('/') ? basis : basis + '/')('katex')
}
// gleiche Makros wie frontend/src/markdown.js — das Gate darf nicht strenger
// sein als der Renderer
const macros = { '\\textsc': '\\text{#1}' }
const formeln = JSON.parse(readFileSync(0, 'utf8'))
const fehler = []
for (const f of formeln) {
try {
katex.renderToString(f.tex, { displayMode: !!f.display, macros: { ...macros } })
} catch (e) {
fehler.push(`$${f.tex.slice(0, 60)}$${String(e.message || e).slice(0, 120)}`)
}
}
process.stdout.write(JSON.stringify(fehler))