This commit is contained in:
team3
2026-07-12 21:21:41 +02:00
parent cde1721d10
commit a9b2f5ab9e
28 changed files with 106 additions and 706 deletions

View File

@@ -55,16 +55,10 @@ _MARKER = re.compile(r"<!--\s*atom:\s*(\d+)\s*\|")
# den Match und die Marker-Zeile bliebe in den Text-Checks stehen (Marker sind
# einzeilig, also kein DOTALL). Muster spiegelt frontend/src/markdown.js.
_MARKER_VOLL = re.compile(r"<!--\s*atom:.*?-->")
_MARKER_DIAGRAMM = re.compile(r"<!--\s*diagramm:\s*(\d+)\s*-->")
_MARKER_BEISPIEL = re.compile(r"<!--\s*beispiel:\s*(\d+)\s*-->")
# Kurzbeschreibung je Diagramm-Typ (Writer-Angebot)
_DIAGRAMM_BESCHR = {"dependency": "Abhängigkeitsgraph der Begriffe dieses Bausteins",
"state": "Zustandsdiagramm/Automat", "flow": "Ablaufdiagramm",
"tree": "Baumstruktur"}
_BEISPIEL_BESCHR = {"code": "Code-Beispiel", "tabelle": "Tabelle/Vergleich",
"diagramm": "Diagramm-Beispiel"}
_VERBATIM_FORMEN = ("code", "tabelle", "diagramm")
_BEISPIEL_BESCHR = {"code": "Code-Beispiel", "tabelle": "Tabelle/Vergleich"}
_VERBATIM_FORMEN = ("code", "tabelle")
def _ohne_marker(text: str) -> str:
@@ -72,12 +66,12 @@ def _ohne_marker(text: str) -> str:
ALLE Text-Checks (Stil, Mathe, Länge, Vorwärtsverweise) laufen auf dem
gestrippten Text — sonst erzeugen Atom-TITEL wie „a_n" oder „Satz 6.26"
unfixierbare Aufträge (der Fix darf Marker nie ändern → Endlos-Loop).
Diagramm- und Beispiel-Marker gehören ebenso raus."""
return _MARKER_BEISPIEL.sub("", _MARKER_DIAGRAMM.sub("", _MARKER_VOLL.sub("", text)))
Beispiel-Marker gehören ebenso raus."""
return _MARKER_BEISPIEL.sub("", _MARKER_VOLL.sub("", text))
def _beispiel_verbatim(atom_id: int) -> list[tuple[int, dict]]:
"""Verifizierte Beispiele des Atoms in VERBATIM-Form (code/tabelle/diagramm) —
"""Verifizierte Beispiele des Atoms in VERBATIM-Form (code/tabelle) —
(artefakt_id, inhalt). Woven-Formen (text/mathe) gehen über `_beispiel`."""
out = []
for r in db.query("SELECT id, inhalt FROM artefakte WHERE atom_id=? AND typ='beispiel'"
@@ -89,23 +83,20 @@ def _beispiel_verbatim(atom_id: int) -> list[tuple[int, dict]]:
def _beispiel_render(inh: dict) -> str:
"""Verbatim-Beispiel → Markdown (Code-Fence / Tabelle / Mermaid-Fence)."""
"""Verbatim-Beispiel → Markdown (Code-Fence / Tabelle)."""
form = inh.get("form")
if form == "code" and inh.get("code"):
return f"```{inh.get('sprache', '')}\n{inh['code']}\n```"
if form == "tabelle" and inh.get("tabelle"):
return inh["tabelle"]
if form == "diagramm" and inh.get("mermaid"):
return f"```mermaid\n{inh['mermaid']}\n```"
return ""
def _marker_platzieren(b: dict, lang: str) -> str:
"""Verbatim-Marker DETERMINISTISCH setzen (der Writer tut es nicht mehr — er
ignorierte die Regel, 2/13 platziert). Beispiel-Marker hinter den Atom-Absatz
(atom-genau via Atom-Marker); Diagramme (baustein-scoped) ans Section-Ende.
Idempotent: alte Verbatim-Marker erst raus."""
lang = _MARKER_BEISPIEL.sub("", _MARKER_DIAGRAMM.sub("", lang))
(atom-genau via Atom-Marker). Idempotent: alte Verbatim-Marker erst raus."""
lang = _MARKER_BEISPIEL.sub("", lang)
je_atom = {}
for a in _atome_von(b["id"]):
vs = _beispiel_verbatim(a["id"])
@@ -124,10 +115,6 @@ def _marker_platzieren(b: dict, lang: str) -> str:
if pending is not None:
out += ["", f"<!-- beispiel: {pending} -->"]
lang = "\n".join(out)
diags = _diagramme_von(b["id"])
if diags:
block = "\n".join(f"<!-- diagramm: {d['id']} -->" for d in diags)
lang = lang.rstrip() + "\n\n" + block
return lang
@@ -145,24 +132,6 @@ def _beispiel_einsetzen(baustein_id: int, lang: str) -> str:
return _MARKER_BEISPIEL.sub(sub, lang)
def _diagramme_von(baustein_id: int) -> list[dict]:
return db.query("SELECT * FROM diagramme WHERE baustein_id=? AND status='verifiziert'"
" ORDER BY id", (baustein_id,))
def _diagramm_einsetzen(baustein_id: int, lang: str) -> str:
"""Assembly: `<!-- diagramm: id -->` → ```mermaid-Fence des verifizierten
Diagramms. Tote/unverifizierte Marker fallen ersatzlos weg."""
if "<!-- diagramm:" not in lang and "<!--diagramm:" not in lang:
return lang
je_id = {d["id"]: d for d in _diagramme_von(baustein_id)}
def sub(m: re.Match) -> str:
d = je_id.get(int(m.group(1)))
return f"```mermaid\n{d['mermaid']}\n```" if d and d["mermaid"] else ""
return _MARKER_DIAGRAMM.sub(sub, lang)
# Kritische Befund-Arten routen zurück auf den Prüfer, Stil-Arten nur in den Fix
# (vorwaerts ist Stil: ein Halbsatz-Fix, kein Fakten-Problem)
KRITISCH = ("marker_fehlend", "marker_fremd", "ziel_ohne_anker", "fachlich_falsch")
@@ -185,7 +154,7 @@ def _zitate(atom_id: int, max_n: int = 3) -> list[str]:
def _beispiel(atom_id: int) -> str:
"""Nur die WOVEN-Formen (text/mathe) als Prosa für den Writer. Verbatim-Formen
(code/tabelle/diagramm) kommen über den `<!-- beispiel: id -->`-Marker rein."""
(code/tabelle) kommen über den `<!-- beispiel: id -->`-Marker rein."""
row = db.one("SELECT inhalt FROM artefakte WHERE atom_id=? AND typ='beispiel'"
" AND status='verifiziert' LIMIT 1", (atom_id,))
if not row:
@@ -233,23 +202,35 @@ def _anknuepf_kontext(b: dict) -> str:
# ── Stages ────────────────────────────────────────────────────────────────────
def _writer_saeubern(s: str) -> str:
"""Geleakte Writer-Delimiter/Modelltokens entfernen. Der Writer beendet seine
Ausgabe mit `=====`; steht diese Zeile unter einem Bullet, ist sie eine
Setext-H1-Unterstreichung → marked macht daraus ein riesiges <h1>. Reine
`===…`-Zeilen und `<s>`/`</s>`-Tokens fallen daher ganz raus."""
s = re.sub(r"</?s>", "", s or "")
s = re.sub(r"(?m)^\s*={3,}\s*$", "", s)
return s.strip()
def _split_writer(text: str) -> tuple[str, str]:
"""Delimited-Ausgabe des Writers/Fix parsen: ===LANG===… / ===KOMPAKT===….
Grund: rohe LaTeX-Backslashes im Text sind invalides JSON (34 % Parse-Fehler).
Robust: Blöcke unabhängig von der Reihenfolge; fehlt der Delimiter, jsonx-Fallback
(alt-JSON), sonst ganzer Text = lang."""
(alt-JSON), sonst ganzer Text = lang. Beide Blöcke werden von geleakten
Terminatoren (`=====`/`</s>`) gesäubert."""
def block(name: str) -> str | None:
m = re.search(rf"===\s*{name}\s*===\s*(.*?)(?=\n===\s*[A-ZÄÖÜ]+\s*===|$)",
text or "", re.S)
return m.group(1).strip() if m else None
return _writer_saeubern(m.group(1)) if m else None
lang, kompakt = block("LANG"), block("KOMPAKT")
if lang is not None or kompakt is not None:
return kompakt or "", lang or ""
daten = jsonx.parse(text or "") # Fallback: altes JSON-Format
if isinstance(daten, dict):
return str(daten.get("kompakt", "")), str(daten.get("lang", ""))
return "", (text or "").strip()
return (_writer_saeubern(str(daten.get("kompakt", ""))),
_writer_saeubern(str(daten.get("lang", ""))))
return "", _writer_saeubern(text or "")
async def _stage_writer(ctx: llm.Kontext, b: dict) -> str:
@@ -275,7 +256,7 @@ async def _stage_writer(ctx: llm.Kontext, b: dict) -> str:
k2, l2 = _split_writer(res2)
if not _marker_fehlend(l2, atome):
kompakt, lang = k2, l2
lang = _marker_platzieren(b, lang) # Beispiel-/Diagramm-Marker deterministisch setzen
lang = _marker_platzieren(b, lang) # Beispiel-Marker deterministisch setzen
db.update("sections", "baustein_id", b["id"], text_kompakt=kompakt, text_lang=lang)
return "pruefer"
@@ -604,15 +585,17 @@ def kapitel_struktur(topic: str) -> list[dict]:
"titel": k.get("titel", "Weitere Themen"),
"intro": k.get("intro", ""),
"level": k.get("level", b["level"]), "sections": []})
# Diagramm- und Beispiel-Marker gegen ihre Fences/Tabellen tauschen (Assembly);
# Atom-Marker bleiben — die strippt erst das Frontend.
# Beispiel-Marker gegen ihre Fences/Tabellen tauschen (Assembly);
# Atom-Marker bleiben — die strippt erst das Frontend. _writer_saeubern hier
# AUCH beim Lesen: bestehende Sections mit geleaktem `=====`/`</s>` (Setext-H1
# → riesiger Bullet) werden ohne Neu-Generierung sauber ausgeliefert.
lang = ""
if sec:
lang = _beispiel_einsetzen(b["id"], _diagramm_einsetzen(b["id"], sec["text_lang"]))
lang = _beispiel_einsetzen(b["id"], _writer_saeubern(sec["text_lang"]))
kapitel[-1]["sections"].append({
"baustein": b["id"], "titel": b["titel"],
"ziel": ziele.get(b["ziel_id"], {}).get("text", ""),
"kompakt": sec["text_kompakt"] if sec else "",
"kompakt": _writer_saeubern(sec["text_kompakt"]) if sec else "",
"lang": lang})
# NULL-kapitel_id durch eindeutige synthetische Keys ersetzen — mehrere
# kapitellose Gruppen kollidierten sonst im Vue-:key (null == null).
@@ -658,12 +641,6 @@ def messen(ctx: llm.Kontext) -> list[dict]:
for i in _marker_fremd_ids(lang, atome):
befunde.append({"art": "marker_fremd", "item": str(b["id"]),
"detail": f"Atom {i}"})
gueltige_d = {d["id"] for d in _diagramme_von(b["id"])}
for m in _MARKER_DIAGRAMM.findall(lang):
if int(m) not in gueltige_d: # Marker ohne verifiziertes Diagramm → entfernen
befunde.append({"art": "diagramm_marker_tot", "item": str(b["id"]),
"detail": f"Stray Diagramm-Marker <!-- diagramm: {m} -->"
f" entfernen (kein solches verifiziertes Diagramm)"})
gueltige_b = {aid for a in atome for aid, _ in _beispiel_verbatim(a["id"])}
for m in _MARKER_BEISPIEL.findall(lang):
if int(m) not in gueltige_b: # Marker ohne verifiziertes Verbatim-Beispiel