update
This commit is contained in:
12
DESIGN.md
12
DESIGN.md
@@ -20,17 +20,14 @@ E1 Inventar Abschnitte (~12k Zeichen) → 2 Reader je Abschnitt → Atome mit
|
||||
Dedup: Anker-Overlap (auto) → Embedding-Kandidaten → 2er-Panel (einstimmig)
|
||||
Soll-Zuordnung je Atom; Soll-Punkt ohne Atom → gezielte Nachextraktion
|
||||
E2 Artefakte je Soll-Punkt-Gruppe: Flashcards + 1 typisiertes Beispiel pro Atom
|
||||
(Form: text|mathe|code|tabelle|diagramm, beste Passung) → Form-Gate
|
||||
(Form: text|mathe|code|tabelle, beste Passung) → Form-Gate
|
||||
(Syntax/Struktur parse-only, kein Ausführen) → Verify-Panel gegen Anker → Fix
|
||||
E3 Struktur Lernziele+Zuordnung aus Atomen (je Soll-Punkt) → Ziel-Check → Kanten auflösen
|
||||
→ Zyklen brechen (Judge) → Bausteine = Ziel-Gruppen im Band (split/merge)
|
||||
→ topologische Ordnung (Bausteine + Atome darin)
|
||||
E3.5 Diagramme je Baustein: Judge plant welche+Typ (state/flow/tree — Term-DAGs waren
|
||||
nutzlos, raus) → Spec-JSON (LLM, kein rohes Mermaid) → deterministisch →
|
||||
Mermaid → Parse-Gate → Grounding-Panel. Optional.
|
||||
E4 Guide je Baustein: Writer (Facts inline, Atom-Marker; Ausgabe DELIMITED, nicht JSON
|
||||
— LaTeX-Backslashes) → det. Checks → Prüfer → Fix (Fix-Cap gegen Churn).
|
||||
Beispiel-/Diagramm-Marker setzt der Code DETERMINISTISCH (nicht der Writer);
|
||||
Beispiel-Marker setzt der Code DETERMINISTISCH (nicht der Writer);
|
||||
Assembly tauscht Marker gegen ```-Fence/Tabelle.
|
||||
```
|
||||
|
||||
@@ -48,11 +45,10 @@ Stillstand oder 10 Iterationen. Erst dann startet die nächste Ebene.
|
||||
- `anker(atom_id, quelle_id, start, ende, zitat)`
|
||||
- `kanten(topic, von_atom, zu_atom, art braucht|verwandt, status)`
|
||||
- `artefakte(id, atom_id, typ flashcard|beispiel, inhalt JSON, status kandidat|verifiziert|verworfen)`
|
||||
— beispiel-`inhalt` trägt `form` + formspezifische Felder (text|code+sprache|tabelle|spec+mermaid);
|
||||
code/tabelle/diagramm kommen verbatim via `<!-- beispiel: id -->` in den Guide (Assembly-Swap)
|
||||
— beispiel-`inhalt` trägt `form` + formspezifische Felder (text|code+sprache|tabelle);
|
||||
code/tabelle kommen verbatim via `<!-- beispiel: id -->` in den Guide (Assembly-Swap)
|
||||
- `lernziele(id, topic, text, soll_id, status)`
|
||||
- `bausteine(id, topic, ziel_id, titel, ord, status)`
|
||||
- `diagramme(id, topic, baustein_id, typ, quelle dag|llm, spec JSON, mermaid, status geplant|kandidat|verifiziert|verworfen|kein, befunde JSON)`
|
||||
- `sections(baustein_id, stage, text_kompakt, text_lang, befunde JSON)`
|
||||
- `leitner(artefakt_id, box, faellig, historie)`
|
||||
- `befunde(run_id, ebene, art, item, detail, status offen|repariert|freigesprochen)`
|
||||
|
||||
@@ -29,9 +29,8 @@ RUN pip install --no-cache-dir torch --index-url https://download.pytorch.org/wh
|
||||
&& pip install --no-cache-dir -r /app/backend/requirements.txt
|
||||
|
||||
COPY --chown=app:app backend/ /app/backend/
|
||||
# katex + mermaid (+ jsdom fürs headless-DOM, das Mermaid zum Parsen braucht) fürs
|
||||
# Parse-Gate (backend/{katex,mermaid}_check.mjs prüfen mit dem echten Renderer)
|
||||
RUN cd /app/backend && npm install --no-audit --no-fund katex mermaid jsdom && chown -R app:app node_modules
|
||||
# katex fürs Parse-Gate (backend/katex_check.mjs prüft mit dem echten Renderer)
|
||||
RUN cd /app/backend && npm install --no-audit --no-fund katex && chown -R app:app node_modules
|
||||
COPY --chown=app:app templates/ /app/templates/
|
||||
COPY --chown=app:app --from=frontend /build/dist /app/frontend/dist
|
||||
COPY --chown=app:app dev-ops/opencode.json /home/app/.config/opencode/opencode.json
|
||||
|
||||
@@ -11,7 +11,6 @@ import subprocess
|
||||
import tempfile
|
||||
|
||||
import db
|
||||
import diagramme
|
||||
import llm
|
||||
from config import ARTEFAKT_CHUNK_ATOME, BEISPIEL_FORMEN, VERIFY_PANEL
|
||||
|
||||
@@ -30,8 +29,8 @@ _QUELLEN_REFERENZ = re.compile(
|
||||
|
||||
|
||||
def _prosa_teile(inhalt: dict) -> str:
|
||||
"""Nur menschenlesbare Prosa fürs Quellen-Referenz-Gate — NICHT Code/Mermaid/
|
||||
Tabelle (dort matchen Bezeichner/Kommentare den Regex falsch, z. B. „blatt 3")."""
|
||||
"""Nur menschenlesbare Prosa fürs Quellen-Referenz-Gate — NICHT Code/Tabelle
|
||||
(dort matchen Bezeichner/Kommentare den Regex falsch, z. B. „blatt 3")."""
|
||||
return " ".join(str(inhalt.get(k, "")) for k in ("frage", "antwort", "text"))
|
||||
|
||||
|
||||
@@ -80,9 +79,7 @@ def _beispiel_inhalt(e: dict) -> dict:
|
||||
"text": str(e.get("text", "")),
|
||||
"code": str(e.get("code", "")),
|
||||
"sprache": str(e.get("sprache", "")).strip().lower(),
|
||||
"tabelle": str(e.get("tabelle", "")),
|
||||
"spec": e.get("spec") if isinstance(e.get("spec"), dict) else {},
|
||||
"mermaid": ""}
|
||||
"tabelle": str(e.get("tabelle", ""))}
|
||||
|
||||
|
||||
async def _generieren(ctx: llm.Kontext, chunk: list[dict]) -> None:
|
||||
@@ -188,7 +185,7 @@ async def _fixen(ctx: llm.Kontext, k: dict, maengel: list[str], atom: dict) -> N
|
||||
status="verifiziert" if eintrag.get("ok") else "verworfen")
|
||||
|
||||
|
||||
_VERBATIM_FORMEN = ("code", "tabelle", "diagramm") # kommen verbatim in den Guide
|
||||
_VERBATIM_FORMEN = ("code", "tabelle") # kommen verbatim in den Guide
|
||||
|
||||
|
||||
def _tabelle_ok(md: str) -> bool:
|
||||
@@ -242,24 +239,12 @@ def _code_ok(sprache: str, code: str) -> bool:
|
||||
|
||||
def _beispiel_pruefen(inh: dict) -> tuple[bool, dict]:
|
||||
"""Form-spezifisches Gate (deterministisch, parse-only — nie Ausführung).
|
||||
→ (ok, inhalt); der Diagramm-Zweig füllt `mermaid`. Unbekannte/woven Formen
|
||||
passieren (der Judge prüft inhaltlich)."""
|
||||
→ (ok, inhalt). Unbekannte/woven Formen passieren (der Judge prüft inhaltlich)."""
|
||||
form = inh.get("form", "text")
|
||||
if form == "tabelle":
|
||||
return _tabelle_ok(inh.get("tabelle", "")), inh
|
||||
if form == "code":
|
||||
return _code_ok(inh.get("sprache", ""), inh.get("code", "")), inh
|
||||
if form == "diagramm":
|
||||
# Struktur-JSON → Mermaid (deterministisch) → Parse-Gate; wiederverwendet die
|
||||
# Diagramm-Ebene. Der LLM schreibt NIE rohes Mermaid — nur die Spec.
|
||||
spec = diagramme._spec_normalisieren(inh.get("spec") or {},
|
||||
(inh.get("spec") or {}).get("typ", "flow"))
|
||||
if not spec["knoten"]:
|
||||
return False, inh
|
||||
mermaid = diagramme._spec_zu_mermaid(spec)
|
||||
if not mermaid or 0 in diagramme._mermaid_fehler([{"id": 0, "mermaid": mermaid}]):
|
||||
return False, inh
|
||||
return True, {**inh, "spec": spec, "mermaid": mermaid}
|
||||
return True, inh
|
||||
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ LUECKEN_RUNDEN_MAX = 2 # gezielte Nachextraktion pro Soll-Lücke
|
||||
BRAUCHT_KANDIDATEN = 8 # Kandidaten-Cap je unaufgelöstem braucht-Titel (Lektion 34)
|
||||
|
||||
# ── Ebene 2: Artefakte ────────────────────────────────────────────────────────
|
||||
BEISPIEL_FORMEN = ("text", "mathe", "code", "tabelle", "diagramm") # Worked-Example-Formen
|
||||
BEISPIEL_FORMEN = ("text", "mathe", "code", "tabelle") # Worked-Example-Formen
|
||||
CODE_SPRACHEN = ("python", "javascript") # Syntax-Gate parse-only (Container: python3, node)
|
||||
ARTEFAKT_CHUNK_ATOME = 8 # Atome pro Generate-Call (ein Soll-Punkt, gestückelt)
|
||||
VERIFY_PANEL = 2 # unabhängige Prüfer, einstimmig = verifiziert
|
||||
@@ -107,9 +107,6 @@ BAUSTEIN_MAX_ATOME = 8
|
||||
ZIELE_CHUNK_ATOME = 30 # Atome pro Lernziel-Call
|
||||
KAPITEL_BAUSTEINE = 5 # Richtwert Bausteine je Kapitel (Kapitel-Schnitt-Judge)
|
||||
|
||||
# ── Ebene 3.5: Diagramme (nur LLM: Automat/Ablauf/Baum — Term-DAGs waren nutzlos) ──
|
||||
DIAGRAMM_TYPEN = ("state", "flow", "tree")
|
||||
|
||||
# ── Ebene 4: Guide ────────────────────────────────────────────────────────────
|
||||
SECTION_WOERTER_PRO_ATOM = (40, 220) # Längenband ausführlicher Teil je Atom
|
||||
|
||||
@@ -137,9 +134,6 @@ TIMEOUTS = {
|
||||
"writer": (450, 60),
|
||||
"pruefer": (600, 5),
|
||||
"qa_judge": (600, 0),
|
||||
"diagramm_plan": (200, 0),
|
||||
"diagramm_spec": (300, 0),
|
||||
"diagramm_verify": (200, 5),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -96,12 +96,6 @@ CREATE TABLE IF NOT EXISTS kapitel(
|
||||
id INTEGER PRIMARY KEY, topic TEXT NOT NULL, titel TEXT NOT NULL,
|
||||
intro TEXT NOT NULL DEFAULT '', ord INTEGER DEFAULT 0,
|
||||
art TEXT NOT NULL DEFAULT 'judge', level TEXT NOT NULL DEFAULT 'M');
|
||||
CREATE TABLE IF NOT EXISTS diagramme(
|
||||
id INTEGER PRIMARY KEY, topic TEXT NOT NULL, baustein_id INTEGER NOT NULL,
|
||||
typ TEXT NOT NULL, quelle TEXT NOT NULL DEFAULT 'dag',
|
||||
spec TEXT NOT NULL DEFAULT '{}', mermaid TEXT NOT NULL DEFAULT '',
|
||||
status TEXT NOT NULL DEFAULT 'geplant', befunde TEXT NOT NULL DEFAULT '[]');
|
||||
CREATE INDEX IF NOT EXISTS idx_diagramme_baustein ON diagramme(baustein_id);
|
||||
CREATE TABLE IF NOT EXISTS sections(
|
||||
baustein_id INTEGER PRIMARY KEY, stage TEXT NOT NULL DEFAULT 'writer',
|
||||
text_kompakt TEXT DEFAULT '', text_lang TEXT DEFAULT '', befunde TEXT DEFAULT '[]',
|
||||
@@ -116,7 +110,7 @@ CREATE TABLE IF NOT EXISTS befunde(
|
||||
|
||||
# Tabellen, deren Änderungen das Live-Board interessieren.
|
||||
_LIVE_TABELLEN = {"topics", "runs", "quellen", "soll", "atome", "artefakte",
|
||||
"lernziele", "bausteine", "kapitel", "sections", "befunde", "diagramme"}
|
||||
"lernziele", "bausteine", "kapitel", "sections", "befunde"}
|
||||
|
||||
|
||||
def _init_schema(con: sqlite3.Connection) -> None:
|
||||
|
||||
@@ -1,264 +0,0 @@
|
||||
"""Ebene 3.5: Diagramme. Pro Baustein ein Mermaid-Diagramm, wenn es Struktur oder
|
||||
Prozess sichtbar macht (Multimedia-Prinzip: instruktive Bilder heben Transfer,
|
||||
Deko schadet). Zuverlässigkeits-Prinzip: der LLM liefert NIE rohes Mermaid, nur
|
||||
Struktur-JSON; Python kompiliert deterministisch; ein Parse-Gate (mermaid_check.mjs)
|
||||
ist das Sicherheitsnetz. Abhängigkeitsdiagramme kommen direkt aus dem braucht-DAG,
|
||||
ganz ohne LLM — korrekt per Konstruktion, null Halluzination.
|
||||
|
||||
Phasen von bauen(): planen (welche + Typ) → Spec (nur LLM-Zweig) → kompilieren →
|
||||
Gate → verifizieren (Grounding). Platziert werden Diagramme erst im Guide-Writer
|
||||
(E4) über `<!-- diagramm: id -->`; das Assembly tauscht den Marker gegen den Fence."""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import db
|
||||
import llm
|
||||
from config import DIAGRAMM_TYPEN, DURCHGANG, VERIFY_PANEL
|
||||
|
||||
log = logging.getLogger("creator2.diagramme")
|
||||
|
||||
EBENE = "diagramme"
|
||||
|
||||
_MERMAID_SKRIPT = Path(__file__).resolve().parent / "mermaid_check.mjs"
|
||||
_MERMAID_BASE = Path(__file__).resolve().parent.parent / "frontend"
|
||||
_mermaid_gewarnt = False
|
||||
|
||||
|
||||
def _mermaid_fehler(diagramme: list[dict]) -> dict[int, str]:
|
||||
"""Parse-Gate: Diagramme durch DENSELBEN Parser wie das Frontend jagen (Zwilling
|
||||
zu guide._katex_fehler). → {id: fehlermeldung} nur der fehlerhaften. Fake-Modus
|
||||
überspringt (deterministisch); fail-open MIT Warnung, wenn node/mermaid fehlt."""
|
||||
global _mermaid_gewarnt
|
||||
if not diagramme or os.getenv("CREATOR_FAKE_AGENTS"):
|
||||
return {}
|
||||
try:
|
||||
res = subprocess.run(
|
||||
["node", str(_MERMAID_SKRIPT)],
|
||||
input=json.dumps([{"id": d["id"], "code": d["mermaid"]} for d in diagramme]),
|
||||
capture_output=True, text=True, timeout=60,
|
||||
env={**os.environ, "MERMAID_BASE": str(_MERMAID_BASE)})
|
||||
if res.returncode:
|
||||
raise RuntimeError(res.stderr.strip()[:200])
|
||||
return {e["id"]: e["fehler"] for e in json.loads(res.stdout)}
|
||||
except Exception as e: # nie fail-open OHNE Signal (Befund-Prinzip)
|
||||
if not _mermaid_gewarnt:
|
||||
_mermaid_gewarnt = True
|
||||
log.warning("Mermaid-Gate nicht verfügbar (%s) — Diagramm-Validierung fällt aus", e)
|
||||
return {}
|
||||
|
||||
|
||||
def _bausteine(topic: str) -> list[dict]:
|
||||
return db.query("SELECT * FROM bausteine WHERE topic=? ORDER BY ord", (topic,))
|
||||
|
||||
|
||||
def _atome_von(baustein_id: int) -> list[dict]:
|
||||
return db.query("SELECT * FROM atome WHERE baustein_id=? AND status NOT IN"
|
||||
" ('gemerged','verworfen') ORDER BY ord", (baustein_id,))
|
||||
|
||||
|
||||
# ── Kompilieren (deterministisch, kein LLM) ───────────────────────────────────
|
||||
|
||||
def _sid(raw) -> str:
|
||||
"""Mermaid-Knoten-ID: nur alnum/_ erlaubt, führender Buchstabe."""
|
||||
return "n" + re.sub(r"[^A-Za-z0-9]", "_", str(raw))
|
||||
|
||||
|
||||
def _lbl(text) -> str:
|
||||
"""Label für Mermaid entschärfen: keine Anführungszeichen/Klammern/Zeilenumbrüche,
|
||||
die die Syntax brechen. Gekürzt (Lesbarkeit)."""
|
||||
t = str(text).replace('"', "'").replace("\n", " ")
|
||||
t = t.replace("[", "(").replace("]", ")").replace("{", "(").replace("}", ")")
|
||||
return t.strip()[:80] or "?"
|
||||
|
||||
|
||||
def _spec_zu_mermaid(spec: dict) -> str:
|
||||
"""Struktur-JSON → Mermaid-Quelltext. state → stateDiagram-v2, sonst flowchart."""
|
||||
knoten = spec.get("knoten") or []
|
||||
kanten = spec.get("kanten") or []
|
||||
if not knoten:
|
||||
return ""
|
||||
if spec.get("typ") == "state":
|
||||
zeilen = ["stateDiagram-v2"]
|
||||
for k in kanten:
|
||||
lab = _lbl(k.get("label", ""))
|
||||
pfeil = f": {lab}" if lab and lab != "?" else ""
|
||||
zeilen.append(f" {_sid(k['von'])} --> {_sid(k['zu'])}{pfeil}")
|
||||
return "\n".join(zeilen)
|
||||
zeilen = ["flowchart TD"]
|
||||
for n in knoten:
|
||||
zeilen.append(f' {_sid(n["id"])}["{_lbl(n["label"])}"]')
|
||||
for k in kanten:
|
||||
lab = _lbl(k.get("label", ""))
|
||||
pfeil = f"-- {lab} -->" if lab and lab != "?" else "-->"
|
||||
zeilen.append(f' {_sid(k["von"])} {pfeil} {_sid(k["zu"])}')
|
||||
return "\n".join(zeilen)
|
||||
|
||||
|
||||
def _kompilieren(topic: str) -> None:
|
||||
for d in db.query("SELECT * FROM diagramme WHERE topic=? AND status='geplant'"
|
||||
" AND spec!='{}'", (topic,)):
|
||||
mermaid = _spec_zu_mermaid(db.uj(d["spec"], {}))
|
||||
if mermaid:
|
||||
db.update("diagramme", "id", d["id"], mermaid=mermaid, status="kandidat")
|
||||
|
||||
|
||||
# ── Planung (welche + Typ) ────────────────────────────────────────────────────
|
||||
|
||||
def _atom_liste(atome: list[dict]) -> str:
|
||||
return "\n".join(f"{a['id']}: {a['titel']} — {a['definition']}" for a in atome)
|
||||
|
||||
|
||||
async def _plan_judge(ctx: llm.Kontext) -> None:
|
||||
"""LLM-Zweig, Schritt „welche + Typ": je Baustein OHNE Diagramm-Zeile entscheidet
|
||||
ein Judge, ob ein Automaten-/Ablauf-/Baum-Diagramm nötig ist (Multimedia-Regel
|
||||
im Prompt). „noetig" → status='geplant'/quelle='llm'; sonst eine 'kein'-Zeile,
|
||||
damit der Judge nicht jede Repair-Iteration erneut fragt (idempotent)."""
|
||||
alle = _bausteine(ctx.topic)
|
||||
je_kap: dict = {}
|
||||
for b in alle:
|
||||
je_kap.setdefault(b["kapitel_id"], []).append(b["titel"])
|
||||
|
||||
async def einer(b: dict) -> None:
|
||||
if db.one("SELECT id FROM diagramme WHERE baustein_id=?", (b["id"],)):
|
||||
return
|
||||
atome = _atome_von(b["id"])
|
||||
if not atome:
|
||||
return
|
||||
nachbarn = "\n".join(f"- {t}" for t in je_kap.get(b["kapitel_id"], [])
|
||||
if t != b["titel"]) or "(keine)"
|
||||
res = await llm.call(ctx, stage="diagramm_plan", template="Diagramm-Plan",
|
||||
role="judge", n=len(atome), item=f"dp{b['id']}",
|
||||
werte={"titel": b["titel"], "atome": _atom_liste(atome),
|
||||
"nachbarn": nachbarn,
|
||||
"durchgang": DURCHGANG.get(b["level"], DURCHGANG["M"])},
|
||||
erwartet=dict)
|
||||
if res is None:
|
||||
return # Ausfall → nächste Runde erneut (kein 'kein'-Eintrag)
|
||||
typ = str(res.get("typ", "")).strip()
|
||||
if res.get("noetig") and typ in DIAGRAMM_TYPEN:
|
||||
db.insert("diagramme", topic=ctx.topic, baustein_id=b["id"], typ=typ,
|
||||
quelle="llm", status="geplant")
|
||||
else:
|
||||
db.insert("diagramme", topic=ctx.topic, baustein_id=b["id"], typ="",
|
||||
quelle="llm", status="kein") # kein Diagramm — nicht erneut fragen
|
||||
|
||||
await llm.alle(einer(b) for b in alle)
|
||||
|
||||
|
||||
def _spec_normalisieren(res: dict, typ: str) -> dict:
|
||||
"""LLM-Spec säubern: nur Knoten mit id+label, nur Kanten zwischen echten Knoten."""
|
||||
knoten = [{"id": str(n.get("id", "")).strip(), "label": str(n.get("label", "")).strip()}
|
||||
for n in (res.get("knoten") or []) if isinstance(n, dict) and n.get("id")]
|
||||
gueltig = {n["id"] for n in knoten}
|
||||
kanten = [{"von": str(k.get("von", "")).strip(), "zu": str(k.get("zu", "")).strip(),
|
||||
"label": str(k.get("label", "")).strip()}
|
||||
for k in (res.get("kanten") or [])
|
||||
if isinstance(k, dict) and str(k.get("von")) in gueltig
|
||||
and str(k.get("zu")) in gueltig]
|
||||
return {"typ": typ, "knoten": knoten, "kanten": kanten}
|
||||
|
||||
|
||||
async def _spec_bauen(ctx: llm.Kontext) -> None:
|
||||
"""LLM liefert NUR Struktur-JSON (kein Mermaid) — die Hauptfehlerquelle
|
||||
(roh geschriebenes Mermaid) fällt so weg. Kompiliert wird deterministisch."""
|
||||
offen = db.query("SELECT * FROM diagramme WHERE topic=? AND status='geplant'"
|
||||
" AND quelle='llm' AND spec='{}'", (ctx.topic,))
|
||||
|
||||
async def einer(d: dict) -> None:
|
||||
b = db.one("SELECT * FROM bausteine WHERE id=?", (d["baustein_id"],))
|
||||
atome = _atome_von(d["baustein_id"])
|
||||
res = await llm.call(ctx, stage="diagramm_spec", template="Diagramm-Spec",
|
||||
role="guide", n=len(atome), item=f"ds{d['id']}",
|
||||
werte={"titel": b["titel"] if b else "", "typ": d["typ"],
|
||||
"atome": _atom_liste(atome)}, erwartet=dict)
|
||||
if res is None:
|
||||
return # Ausfall → bleibt 'geplant', nächste Runde erneut
|
||||
spec = _spec_normalisieren(res, d["typ"])
|
||||
if spec["knoten"]:
|
||||
db.update("diagramme", "id", d["id"], spec=db.j(spec))
|
||||
|
||||
await llm.alle(einer(d) for d in offen)
|
||||
|
||||
|
||||
async def _verifizieren(ctx: llm.Kontext) -> None:
|
||||
"""Grounding-Panel für LLM-Diagramme: jeder Knoten muss aus einem Atom ableitbar
|
||||
sein, keine erfundene Struktur. Verwerfen (destruktiv) nur einstimmig
|
||||
(llm.einstimmig) — Panel-Ausfall lässt den Kandidaten stehen."""
|
||||
offen = db.query("SELECT * FROM diagramme WHERE topic=? AND status='kandidat'"
|
||||
" AND quelle='llm'", (ctx.topic,))
|
||||
|
||||
def urteil(stimme):
|
||||
if stimme and isinstance(stimme[0], dict) and "ok" in stimme[0]:
|
||||
return bool(stimme[0]["ok"])
|
||||
return None
|
||||
|
||||
async def einer(d: dict) -> None:
|
||||
atome = _atome_von(d["baustein_id"])
|
||||
stimmen = await llm.panel(ctx, VERIFY_PANEL, stage="diagramm_verify",
|
||||
template="Diagramm-Verify", schritt="diagramm_verify",
|
||||
role="judge", n=len(atome), item=f"dv{d['id']}",
|
||||
werte={"atome": _atom_liste(atome), "mermaid": d["mermaid"]},
|
||||
erwartet=list)
|
||||
if llm.einstimmig(stimmen, VERIFY_PANEL, urteil):
|
||||
db.update("diagramme", "id", d["id"], status="verifiziert")
|
||||
elif llm.einstimmig(stimmen, VERIFY_PANEL,
|
||||
lambda s: None if urteil(s) is None else not urteil(s)):
|
||||
db.update("diagramme", "id", d["id"], status="verworfen",
|
||||
befunde=db.j(["Grounding-Panel: nicht aus den Atomen ableitbar"]))
|
||||
# sonst: Kandidat bleibt (Panel-Ausfall / uneins) — nächste Runde erneut
|
||||
|
||||
await llm.alle(einer(d) for d in offen)
|
||||
|
||||
|
||||
# ── Ebenen-Vertrag ────────────────────────────────────────────────────────────
|
||||
|
||||
async def bauen(ctx: llm.Kontext) -> None:
|
||||
ctx.ebene = EBENE
|
||||
await _plan_judge(ctx) # welche/Typ: Judge (Automaten/Abläufe/Bäume) — kein DAG mehr
|
||||
await _spec_bauen(ctx) # LLM → Struktur-JSON (kein rohes Mermaid)
|
||||
_kompilieren(ctx.topic) # Spec → Mermaid (deterministisch)
|
||||
_gate_kandidaten(ctx.topic) # Parse-Gate: ungültige Mermaid-Quelle → verworfen
|
||||
await _verifizieren(ctx) # Grounding-Panel gegen die Atome
|
||||
|
||||
|
||||
def _gate_kandidaten(topic: str) -> None:
|
||||
"""Kandidaten durchs Parse-Gate: Syntaxfehler → verworfen (mit Befund). Ein
|
||||
kaputtes Diagramm darf nie in den Guide (fail-closed)."""
|
||||
kandidaten = db.query("SELECT * FROM diagramme WHERE topic=? AND status='kandidat'"
|
||||
" AND mermaid!=''", (topic,))
|
||||
fehler = _mermaid_fehler(kandidaten)
|
||||
for d in kandidaten:
|
||||
if d["id"] in fehler:
|
||||
db.update("diagramme", "id", d["id"], status="verworfen",
|
||||
befunde=db.j([f"Parse-Fehler: {fehler[d['id']]}"]))
|
||||
|
||||
|
||||
def messen(ctx: llm.Kontext) -> list[dict]:
|
||||
befunde = []
|
||||
for d in db.query("SELECT * FROM diagramme WHERE topic=? AND status='kandidat'",
|
||||
(ctx.topic,)):
|
||||
# hängengebliebener Kandidat (Spec/Verify offen) — sichtbar, kein stiller Verlust
|
||||
befunde.append({"art": "diagramm_unverifiziert", "item": str(d["id"]),
|
||||
"detail": f"Baustein {d['baustein_id']}"})
|
||||
return befunde
|
||||
|
||||
|
||||
async def reparieren(ctx: llm.Kontext, befunde: list[dict]) -> bool:
|
||||
ctx.ebene = EBENE
|
||||
if not befunde:
|
||||
return False
|
||||
await bauen(ctx)
|
||||
return True
|
||||
|
||||
|
||||
def gate(ctx: llm.Kontext) -> str | None:
|
||||
"""Diagramme sind optional — KEINE Diagramme blockiert den Lauf nicht. Backstop:
|
||||
ein verifiziertes Diagramm ohne Mermaid-Quelltext darf nie ausgeliefert werden."""
|
||||
tot = db.one("SELECT id FROM diagramme WHERE topic=? AND status='verifiziert'"
|
||||
" AND mermaid='' LIMIT 1", (ctx.topic,))
|
||||
return "verifiziertes Diagramm ohne Quelltext" if tot else None
|
||||
@@ -237,7 +237,7 @@ def _writer(prompt: str):
|
||||
absaetze = [f"Ziel dieser Einheit: {ziel.group(1) if ziel else 'Lernen'}. Formal: $n \\ge 0$."]
|
||||
fuellung = "Daran anknüpfend gehen wir das Schritt für Schritt am Beispiel durch. " * 5
|
||||
for (mk, titel), d in zip(marker, defs):
|
||||
# Beispiel-/Diagramm-Marker setzt jetzt _marker_platzieren deterministisch,
|
||||
# Beispiel-Marker setzt jetzt _marker_platzieren deterministisch,
|
||||
# nicht mehr der Writer.
|
||||
absaetze.append(f"#### {titel} verstehen\n{mk}\n{d} {fuellung}")
|
||||
lang = "\n\n".join(absaetze)
|
||||
@@ -247,23 +247,6 @@ def _writer(prompt: str):
|
||||
"lang": lang}
|
||||
|
||||
|
||||
def _diagramm_plan(prompt: str):
|
||||
# deterministisch: die Fake-Welt braucht kein Diagramm. Unit-Tests für den
|
||||
# LLM-Diagramm-Zweig monkeypatchen diesen Handler auf noetig=True.
|
||||
return {"noetig": False, "typ": ""}
|
||||
|
||||
|
||||
def _diagramm_spec(prompt: str):
|
||||
ids = [i for i in re.findall(r"^(\d+):", _text_nach(prompt, "ATOME:"), re.MULTILINE)]
|
||||
knoten = [{"id": i, "label": f"Konzept {i}"} for i in ids]
|
||||
kanten = [{"von": ids[k], "zu": ids[k + 1], "label": ""} for k in range(len(ids) - 1)]
|
||||
return {"typ": "flow", "knoten": knoten, "kanten": kanten}
|
||||
|
||||
|
||||
def _diagramm_verify(prompt: str):
|
||||
return [{"ok": True, "mangel": ""}]
|
||||
|
||||
|
||||
def _pruefer(prompt: str):
|
||||
return {"befunde": []}
|
||||
|
||||
@@ -298,6 +281,4 @@ _HANDLER = {
|
||||
"Kanten-Zyklus": _zyklus, "Braucht-Aufloesung": _braucht_aufloesung,
|
||||
"Guide-Writer": _writer, "Guide-Pruefer": _pruefer, "Guide-Fix": _fix,
|
||||
"QA-Guide-Falsch": _qa_falsch, "QA-Guide-Falsch-Check": _qa_falsch_check,
|
||||
"Diagramm-Plan": _diagramm_plan, "Diagramm-Spec": _diagramm_spec,
|
||||
"Diagramm-Verify": _diagramm_verify,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -153,10 +153,6 @@ def state(topic: str):
|
||||
"bausteine": db.query(
|
||||
"SELECT b.*, s.stage FROM bausteine b LEFT JOIN sections s ON s.baustein_id=b.id"
|
||||
" WHERE b.topic=? ORDER BY b.ord", (topic,)),
|
||||
"diagramme": db.query(
|
||||
"SELECT d.id, d.baustein_id, d.typ, d.quelle, d.status, b.titel, b.ord"
|
||||
" FROM diagramme d JOIN bausteine b ON b.id=d.baustein_id"
|
||||
" WHERE d.topic=? AND d.status!='kein' ORDER BY b.ord, d.id", (topic,)),
|
||||
# topic-scoped: offene Befunde früherer Läufe bleiben nach Resume sichtbar
|
||||
"befunde": db.query(
|
||||
"SELECT b.* FROM befunde b JOIN runs r ON r.id=b.run_id"
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
// Mermaid-Parse-Gate: prüft Diagramm-Quelltext mit DEMSELBEN Parser wie das Frontend.
|
||||
// stdin: JSON [{id, code}] → stdout: JSON [{id, fehler}, …] (nur die fehlerhaften).
|
||||
// Zwilling zu katex_check.mjs: was hier parst, rendert auch im Guide.
|
||||
//
|
||||
// Mermaid braucht zum Sanitisieren der Labels ein DOM — headless via jsdom, sonst
|
||||
// wirft parse() für JEDES Diagramm mit Label „DOMPurify.addHook is not a function"
|
||||
// (nicht ein Diagramm-Fehler, sondern ein Umgebungsfehler → alles fälschlich verworfen).
|
||||
import { createRequire } from 'node:module'
|
||||
import { readFileSync } from 'node:fs'
|
||||
|
||||
function laden(name) {
|
||||
try {
|
||||
return createRequire(import.meta.url)(name) // Container: backend/node_modules
|
||||
} catch {
|
||||
const basis = process.env.MERMAID_BASE // Dev: frontend/node_modules
|
||||
return createRequire(basis.endsWith('/') ? basis : basis + '/')(name)
|
||||
}
|
||||
}
|
||||
|
||||
const { JSDOM } = laden('jsdom')
|
||||
const dom = new JSDOM('<!DOCTYPE html><body></body>')
|
||||
globalThis.window = dom.window
|
||||
globalThis.document = dom.window.document
|
||||
|
||||
const mermaid = laden('mermaid').default
|
||||
mermaid.initialize({ startOnLoad: false, securityLevel: 'strict' })
|
||||
|
||||
const diagramme = JSON.parse(readFileSync(0, 'utf8'))
|
||||
const fehler = []
|
||||
for (const d of diagramme) {
|
||||
try {
|
||||
await mermaid.parse(d.code) // wirft bei Syntaxfehler
|
||||
} catch (e) {
|
||||
fehler.push({ id: d.id, fehler: String(e.message || e).slice(0, 160) })
|
||||
}
|
||||
}
|
||||
process.stdout.write(JSON.stringify(fehler))
|
||||
@@ -10,7 +10,6 @@ import agents
|
||||
import artefakte
|
||||
import auto_loop
|
||||
import db
|
||||
import diagramme
|
||||
import guide
|
||||
import inventar
|
||||
import korpus
|
||||
@@ -27,11 +26,10 @@ EBENEN = [
|
||||
("inventar", inventar, "inventar_fertig"),
|
||||
("artefakte", artefakte, "artefakte_fertig"),
|
||||
("struktur", struktur, "struktur_fertig"),
|
||||
("diagramme", diagramme, "diagramme_fertig"),
|
||||
("guide", guide, "fertig"),
|
||||
]
|
||||
_ORDNUNG = ["neu", "korpus", "korpus_fertig", "inventar_fertig",
|
||||
"artefakte_fertig", "struktur_fertig", "diagramme_fertig", "fertig"]
|
||||
"artefakte_fertig", "struktur_fertig", "fertig"]
|
||||
_laeufe: dict[str, asyncio.Task] = {}
|
||||
|
||||
|
||||
@@ -65,7 +63,7 @@ def lauf_stoppen(topic: str) -> None:
|
||||
db.update("runs", "id", run["id"], status="stopped", beendet=db.now())
|
||||
|
||||
|
||||
_ROLLBACK = {"guide": "diagramme_fertig", "diagramme": "struktur_fertig",
|
||||
_ROLLBACK = {"guide": "struktur_fertig",
|
||||
"struktur": "artefakte_fertig", "artefakte": "inventar_fertig",
|
||||
"inventar": "korpus_fertig", "korpus": "neu"}
|
||||
|
||||
@@ -77,25 +75,23 @@ def ebenen_entfernen(topic: str, ebene: str) -> None:
|
||||
if ebene not in _ROLLBACK:
|
||||
raise ValueError(f"unbekannte Ebene: {ebene}")
|
||||
lauf_stoppen(topic)
|
||||
stufe = ["guide", "diagramme", "struktur", "artefakte", "inventar", "korpus"].index(ebene)
|
||||
stufe = ["guide", "struktur", "artefakte", "inventar", "korpus"].index(ebene)
|
||||
db.execute("DELETE FROM sections WHERE baustein_id IN"
|
||||
" (SELECT id FROM bausteine WHERE topic=?)", (topic,))
|
||||
db.execute("UPDATE bausteine SET status='neu' WHERE topic=?", (topic,))
|
||||
db.execute("UPDATE kapitel SET intro='' WHERE topic=?", (topic,)) # Intro ist Guide-Text
|
||||
if stufe >= 1: # diagramme
|
||||
db.execute("DELETE FROM diagramme WHERE topic=?", (topic,))
|
||||
if stufe >= 2: # struktur
|
||||
if stufe >= 1: # struktur
|
||||
db.execute("DELETE FROM bausteine WHERE topic=?", (topic,))
|
||||
db.execute("DELETE FROM kapitel WHERE topic=?", (topic,))
|
||||
db.execute("DELETE FROM lernziele WHERE topic=?", (topic,))
|
||||
db.execute("UPDATE atome SET ziel_id=NULL, baustein_id=NULL, ord=0 WHERE topic=?",
|
||||
(topic,))
|
||||
if stufe >= 3: # artefakte
|
||||
if stufe >= 2: # artefakte
|
||||
db.execute("DELETE FROM leitner WHERE artefakt_id IN (SELECT ar.id FROM artefakte ar"
|
||||
" JOIN atome a ON ar.atom_id=a.id WHERE a.topic=?)", (topic,))
|
||||
db.execute("DELETE FROM artefakte WHERE atom_id IN"
|
||||
" (SELECT id FROM atome WHERE topic=?)", (topic,))
|
||||
if stufe >= 4: # inventar
|
||||
if stufe >= 3: # inventar
|
||||
db.execute("DELETE FROM anker WHERE atom_id IN (SELECT id FROM atome WHERE topic=?)",
|
||||
(topic,))
|
||||
db.execute("DELETE FROM kanten WHERE topic=?", (topic,))
|
||||
@@ -103,7 +99,7 @@ def ebenen_entfernen(topic: str, ebene: str) -> None:
|
||||
# atome_stand mit-nullen — sonst überspringt der Resume-Guard die Neu-Extraktion
|
||||
db.execute("UPDATE quellen SET status='extrahiert', atome_stand='' WHERE topic=?",
|
||||
(topic,))
|
||||
if stufe >= 5: # korpus
|
||||
if stufe >= 4: # korpus
|
||||
db.execute("DELETE FROM soll WHERE topic=?", (topic,))
|
||||
db.execute("DELETE FROM quellen WHERE topic=?", (topic,))
|
||||
import shutil
|
||||
@@ -118,7 +114,7 @@ def ebenen_entfernen(topic: str, ebene: str) -> None:
|
||||
# append-only), aber die Zählung beginnt hinter dem Reset-Marker neu.
|
||||
letzte = db.one("SELECT MAX(id) AS m FROM events")["m"] or 0
|
||||
resets = db.uj(db.one("SELECT resets FROM topics WHERE name=?", (topic,))["resets"], {})
|
||||
for name in ["guide", "diagramme", "struktur", "artefakte", "inventar", "korpus"][:stufe + 1]:
|
||||
for name in ["guide", "struktur", "artefakte", "inventar", "korpus"][:stufe + 1]:
|
||||
resets[name] = letzte
|
||||
db.update("topics", "name", topic, status=_ROLLBACK[ebene], resets=db.j(resets))
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import logging
|
||||
|
||||
import artefakte
|
||||
import db
|
||||
import diagramme
|
||||
import guide
|
||||
import inventar
|
||||
import korpus
|
||||
@@ -19,7 +18,7 @@ import struktur
|
||||
log = logging.getLogger("creator2.qa")
|
||||
|
||||
MODULE = {"korpus": korpus, "inventar": inventar, "artefakte": artefakte,
|
||||
"struktur": struktur, "diagramme": diagramme, "guide": guide}
|
||||
"struktur": struktur, "guide": guide}
|
||||
|
||||
# Gewichte je Befund-Art: kritisch 3.0 (verletzt Kernprinzip), mittel 1.5, stil 0.5.
|
||||
GEWICHTE = {
|
||||
@@ -34,8 +33,6 @@ GEWICHTE = {
|
||||
"section_fehlt": 3.0, "marker_fehlend": 3.0, "marker_fremd": 3.0,
|
||||
"ziel_ohne_anker": 3.0, "fachlich_falsch": 3.0, "laenge": 0.5, "vorwaerts": 0.5,
|
||||
"det_check": 0.5, "fix_offen": 1.5,
|
||||
"diagramm_parse_fehler": 3.0, "diagramm_ungegroundet": 3.0,
|
||||
"diagramm_unverifiziert": 0.5, "diagramm_marker_tot": 3.0,
|
||||
"beispiel_marker_tot": 3.0,
|
||||
}
|
||||
|
||||
@@ -49,7 +46,6 @@ def _basis(topic: str, ebene: str) -> int:
|
||||
" AND status NOT IN ('gemerged','verworfen')",
|
||||
"struktur": "SELECT COUNT(*) n FROM atome WHERE topic=?"
|
||||
" AND status NOT IN ('gemerged','verworfen')",
|
||||
"diagramme": "SELECT COUNT(*) n FROM bausteine WHERE topic=?",
|
||||
"guide": "SELECT COUNT(*) n FROM bausteine WHERE topic=?"}[ebene]
|
||||
row = db.one(z, (topic,))
|
||||
return max(1, int(row["n"]) if row else 1)
|
||||
|
||||
@@ -12,11 +12,9 @@
|
||||
"@fontsource-variable/inter": "^5.2.8",
|
||||
"dompurify": "^3.4.7",
|
||||
"highlight.js": "^11.11.1",
|
||||
"jsdom": "^29.1.1",
|
||||
"katex": "^0.17.0",
|
||||
"marked": "^18.0.4",
|
||||
"marked-highlight": "^2.2.4",
|
||||
"mermaid": "^11.4.1",
|
||||
"vue": "^3.5.32"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -7,9 +7,9 @@ import Ueben from './components/Ueben.vue'
|
||||
import Kennzahlen from './components/Kennzahlen.vue'
|
||||
|
||||
const topics = ref([])
|
||||
const topic = ref('')
|
||||
const topic = ref(localStorage.getItem('topic') || '') // Auswahl über Reload halten
|
||||
const state = ref(null)
|
||||
const tab = ref('board')
|
||||
const tab = ref(localStorage.getItem('tab') || 'board')
|
||||
const neuName = ref('')
|
||||
const neuArt = ref('thema')
|
||||
const fehler = ref('')
|
||||
@@ -24,6 +24,9 @@ function navToggle() {
|
||||
localStorage.setItem('navAuf', navAuf.value ? '1' : '0')
|
||||
}
|
||||
watch(navAuf, (a) => document.body.classList.toggle('nav-zu', !a), { immediate: true })
|
||||
// Ansicht über Reload halten: gewähltes Topic + Tab merken
|
||||
watch(topic, (t) => localStorage.setItem('topic', t))
|
||||
watch(tab, (t) => localStorage.setItem('tab', t))
|
||||
|
||||
let wsSchliessen = null
|
||||
let timer = null
|
||||
@@ -40,6 +43,8 @@ media.addEventListener('change', themeAnwenden)
|
||||
|
||||
async function topicsLaden() {
|
||||
topics.value = await api.topics()
|
||||
// gemerktes Topic gelöscht? → verwerfen, dann erst auf das erste zurückfallen
|
||||
if (topic.value && !topics.value.some((t) => t.name === topic.value)) topic.value = ''
|
||||
if (!topic.value && topics.value.length) topic.value = topics.value[0].name
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,6 @@ const EBENEN = [
|
||||
{ key: 'inventar', titel: 'Atome' },
|
||||
{ key: 'artefakte', titel: 'Artefakte' },
|
||||
{ key: 'struktur', titel: 'Bausteine' },
|
||||
{ key: 'diagramme', titel: 'Diagramme' },
|
||||
{ key: 'guide', titel: 'Guide' },
|
||||
]
|
||||
|
||||
@@ -80,10 +79,6 @@ function karten(key) {
|
||||
return s.bausteine.map((b) => ({ id: b.id, text: `${b.ord + 1}. ${b.titel}`,
|
||||
badges: [[b.status, statusFarbe(b.status)]] }))
|
||||
}
|
||||
if (key === 'diagramme') {
|
||||
return (s.diagramme || []).map((d) => ({ id: d.id, text: `${d.ord + 1}. ${d.titel}`,
|
||||
badges: [[d.status, statusFarbe(d.status)], [`${d.typ} · ${d.quelle}`, 'blau']] }))
|
||||
}
|
||||
return s.bausteine.map((b) => ({ id: b.id, text: `${b.ord + 1}. ${b.titel}`,
|
||||
badges: [[b.stage || 'offen', b.stage === 'done' ? 'gruen' : 'blau']] }))
|
||||
}
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
<script setup>
|
||||
import { computed, nextTick, onUnmounted, ref, watch } from 'vue'
|
||||
import mermaid from 'mermaid'
|
||||
import { api } from '../api.js'
|
||||
import { render, lesestat } from '../markdown.js'
|
||||
|
||||
// securityLevel:'strict' → kein foreignObject/HTML-Label (umgeht DOMPurify-Filter),
|
||||
// keine Klick-Handler. startOnLoad:false → wir rendern selbst nach jedem Einblenden.
|
||||
mermaid.initialize({ startOnLoad: false, securityLevel: 'strict', theme: 'neutral' })
|
||||
let mmId = 0
|
||||
|
||||
const props = defineProps({ topic: String, state: Object })
|
||||
const daten = ref(null)
|
||||
const level = ref('E') // Durchgang-Wahl: jede Stufe ist ein eigener Lese-Durchgang
|
||||
@@ -46,27 +40,6 @@ function html(s) {
|
||||
return htmlCache.get(key)
|
||||
}
|
||||
|
||||
// Mermaid rendert asynchron ins DOM — nach jedem Einblenden (Scroll/Level/Ansicht)
|
||||
// die noch nicht verarbeiteten ```mermaid-Blöcke im sichtbaren Bereich rendern.
|
||||
async function renderMermaid() {
|
||||
await nextTick()
|
||||
const wurzel = scrollEl.value
|
||||
if (!wurzel) return
|
||||
for (const code of wurzel.querySelectorAll('code.language-mermaid:not([data-mm])')) {
|
||||
code.setAttribute('data-mm', '1')
|
||||
const pre = code.closest('pre')
|
||||
try {
|
||||
const { svg } = await mermaid.render(`mm-${mmId++}`, code.textContent)
|
||||
const figur = document.createElement('div')
|
||||
figur.className = 'mermaid-figur'
|
||||
figur.innerHTML = svg
|
||||
pre?.replaceWith(figur)
|
||||
} catch {
|
||||
pre?.classList.add('mermaid-fehler') // kaputt: als Codeblock stehen lassen
|
||||
}
|
||||
}
|
||||
}
|
||||
watch([aktiv, ansicht, level, daten], renderMermaid, { flush: 'post' })
|
||||
|
||||
// Nur aktives Kapitel ±1 im DOM; der Rest ist ein Platzhalter mit Schätzhöhe
|
||||
const sichtbar = (i) => Math.abs(i - aktiv.value) <= 1
|
||||
|
||||
@@ -64,7 +64,7 @@ const MARKER = /<!--\s*atom:.*?-->/g
|
||||
export function lesestat(text) {
|
||||
let displayFormeln = 0
|
||||
const t = (text || '')
|
||||
.replace(/```[\s\S]*?```/g, ' ') // Code-/Diagramm-Fences zählen nicht als Text
|
||||
.replace(/```[\s\S]*?```/g, ' ') // Code-Fences zählen nicht als Text
|
||||
.replace(MARKER, ' ')
|
||||
.replace(/\$\$[\s\S]+?\$\$/g, () => { displayFormeln += 1; return ' ' })
|
||||
.replace(/\\\[[\s\S]+?\\\]/g, () => { displayFormeln += 1; return ' ' })
|
||||
|
||||
@@ -123,7 +123,7 @@ input, select {
|
||||
/* Board: Ebenen-Spalten links→rechts, Listen VOLLSTÄNDIG scrollbar */
|
||||
.board { display: flex; gap: 10px; padding: 12px; overflow-x: auto; flex: 1; min-height: 0; }
|
||||
.spalte {
|
||||
min-width: 265px; max-width: 320px; flex: 1; display: flex; flex-direction: column;
|
||||
min-width: 0; max-width: 320px; flex: 1; display: flex; flex-direction: column;
|
||||
background: var(--panel); border: 1px solid var(--rand); border-radius: 8px;
|
||||
}
|
||||
.spaltekopf { padding: 8px 10px; border-bottom: 1px solid var(--rand); display: grid; gap: 6px; }
|
||||
@@ -223,8 +223,6 @@ body.nav-zu .hauptbereich { padding-top: 38px; }
|
||||
}
|
||||
.guide-section .ziel { color: var(--akzent); font-size: 13px; }
|
||||
:is(.markdown, .kompakt) .katex-display { overflow-x: auto; overflow-y: hidden; padding: 4px 0; }
|
||||
.mermaid-figur { overflow-x: auto; text-align: center; margin: 16px 0; }
|
||||
.mermaid-figur svg { max-width: 100%; height: auto; }
|
||||
.markdown blockquote {
|
||||
margin: 12px 0; padding: 8px 14px; border-left: 3px solid var(--akzent);
|
||||
background: var(--karte); border-radius: 0 6px 6px 0; color: var(--text);
|
||||
|
||||
@@ -16,4 +16,3 @@ Für beispiel: BEHALTE die Form aus dem Artefakt oben und gib die passenden Feld
|
||||
- form "text"/"mathe": {{"text": "…"}}
|
||||
- form "code": {{"sprache": "python", "code": "…"}}
|
||||
- form "tabelle": {{"tabelle": "| … | … |\n|---|---|\n| … | … |"}}
|
||||
- form "diagramm": {{"spec": {{"knoten": […], "kanten": […]}}}}
|
||||
|
||||
@@ -15,8 +15,7 @@ Erzeuge pro Atom:
|
||||
"text" als durchgerechneter Schritt-für-Schritt-Ablauf.
|
||||
- [begriff] → NUR wenn abstrakt/schwer greifbar. Dann "text"/"mathe" (konkrete
|
||||
Instanz) oder "tabelle" (Fälle/Vergleich). Simpler, selbsterklärender Begriff → KEIN Beispiel.
|
||||
- [aussage] → NUR wenn nicht trivial. Dann "mathe"/"text" (Anwendung an Mini-Instanz)
|
||||
oder "diagramm" (Feld "spec": {{knoten:[…], kanten:[…]}}) für Automat/Ablauf/Baum.
|
||||
- [aussage] → NUR wenn nicht trivial. Dann "mathe"/"text" (Anwendung an Mini-Instanz).
|
||||
Nur EINE Form. Im Zweifel WEGLASSEN — ein schwaches/redundantes Beispiel schadet
|
||||
(Forschung: die Auswahl ist kritisch). Code selbsterklärend, ohne I/O-Tricks.
|
||||
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
<!-- template:Diagramm-Plan -->
|
||||
Entscheide, ob der Baustein „{titel}" ein Diagramm braucht.
|
||||
Durchgang: {durchgang}.
|
||||
|
||||
Ein Diagramm hilft NUR, wenn es Struktur oder Ablauf zeigt, den reiner Text teuer
|
||||
erklärt: Zustandsübergänge/Automaten, Abläufe/Verfahren, Baum-/Hierarchie-Strukturen.
|
||||
Es schadet, wenn es bloß wiederholt, was ein Satz schon sagt (Redundanz), oder nur
|
||||
schmückt. Im dritten Durchgang (Feinheiten) sparsam.
|
||||
|
||||
Nachbar-Bausteine (nicht dieselbe Relation doppeln):
|
||||
{nachbarn}
|
||||
|
||||
Wähle den Typ:
|
||||
- state: Automat / Zustandsübergänge.
|
||||
- flow: Ablauf / Verfahren / Schritte.
|
||||
- tree: Hierarchie / Baum / Zerlegung.
|
||||
|
||||
ATOME:
|
||||
{atome}
|
||||
|
||||
Antworte NUR mit JSON (keine Code-Fences):
|
||||
{{"noetig": true, "typ": "state"}}
|
||||
@@ -1,20 +0,0 @@
|
||||
<!-- template:Diagramm-Spec -->
|
||||
Beschreibe die STRUKTUR eines {typ}-Diagramms für den Baustein „{titel}".
|
||||
Schreibe KEIN Mermaid, KEINEN Code — nur die Struktur als JSON. Ein Programm
|
||||
kompiliert daraus das Diagramm.
|
||||
|
||||
Regeln (hart):
|
||||
- NUR Konzepte/Zustände/Schritte aus den ATOMEN unten. Nichts erfinden, nichts
|
||||
dazurecherchieren — jeder Knoten muss aus einem Atom ableitbar sein.
|
||||
- „knoten": kurze Labels (1–4 Wörter), auf Deutsch.
|
||||
- „kanten": „von"/„zu" verweisen auf „id"-Werte aus „knoten"; „label" optional
|
||||
(kurze Kantenbeschriftung, z. B. das auslösende Symbol beim Automaten).
|
||||
- Klein und lesbar halten: nur die tragende Struktur, keine Vollständigkeit um
|
||||
jeden Preis.
|
||||
|
||||
ATOME:
|
||||
{atome}
|
||||
|
||||
Antworte NUR mit JSON (keine Code-Fences):
|
||||
{{"typ": "{typ}", "knoten": [{{"id": "q0", "label": "Startzustand"}}],
|
||||
"kanten": [{{"von": "q0", "zu": "q1", "label": "a"}}]}}
|
||||
@@ -1,21 +0,0 @@
|
||||
<!-- template:Diagramm-Verify -->
|
||||
Prüfe das Diagramm GEGEN DIE ATOME. Ist es fachlich korrekt und vollständig aus
|
||||
den Atomen ableitbar?
|
||||
|
||||
ok=false, wenn:
|
||||
- ein Knoten/Zustand/Schritt vorkommt, der in keinem Atom steht (erfunden),
|
||||
- die dargestellten Beziehungen/Übergänge den Atomen widersprechen,
|
||||
- die tragende Struktur des Themas fehlt oder verdreht ist.
|
||||
|
||||
ok=true bei kleinen Vereinfachungen (Weglassen von Randfällen ist erlaubt).
|
||||
Layout/Ästhetik ist KEIN Mangel — nur inhaltliche Korrektheit zählt.
|
||||
Bei ok=false: „mangel" = ein konkreter, behebbarer Satz.
|
||||
|
||||
ATOME:
|
||||
{atome}
|
||||
|
||||
DIAGRAMM (Mermaid):
|
||||
{mermaid}
|
||||
|
||||
Antworte NUR mit JSON (keine Code-Fences):
|
||||
[{{"ok": true, "mangel": ""}}]
|
||||
@@ -67,8 +67,8 @@ Regeln (hart):
|
||||
7. Keine Vorgriffe auf Stoff, der nicht in den Atomen unten steht — auch nicht
|
||||
auf spätere Durchgänge. Unbekannte Begriffe beim ersten Auftreten kurz mit
|
||||
Bekanntem einordnen, nie vertrösten („dazu später mehr" ist VERBOTEN).
|
||||
(Code-/Tabellen-/Diagramm-Beispiele werden NACH dir automatisch beim passenden
|
||||
Atom eingefügt — du musst KEINE Beispiel-Marker setzen. Text-Beispiele unter den
|
||||
(Code-/Tabellen-Beispiele werden NACH dir automatisch beim passenden Atom
|
||||
eingefügt — du musst KEINE Beispiel-Marker setzen. Text-Beispiele unter den
|
||||
Atomen webst du wie gehabt in die Prosa ein.)
|
||||
|
||||
ATOME:
|
||||
|
||||
@@ -94,7 +94,7 @@ def test_tabelle_struktur_check():
|
||||
def test_referenziert_quelle_ignoriert_code():
|
||||
# „aufgabe 3" in Code/Kommentar darf NICHT als Quellen-Referenz matchen
|
||||
inhalt = {"form": "code", "text": "", "code": "# aufgabe 3: sum\nprint(sum([1,2]))",
|
||||
"sprache": "python", "tabelle": "", "mermaid": ""}
|
||||
"sprache": "python", "tabelle": ""}
|
||||
assert not artefakte._referenziert_quelle(inhalt)
|
||||
# in der Prosa (text) matcht es weiterhin
|
||||
assert artefakte._referenziert_quelle({"text": "siehe aufgabe 3", "frage": "", "antwort": ""})
|
||||
@@ -151,23 +151,6 @@ def test_formen_gate_verwirft_kaputten_code():
|
||||
assert db.one("SELECT status FROM artefakte WHERE id=?", (schlecht,))["status"] == "verworfen"
|
||||
|
||||
|
||||
def test_formen_gate_diagramm_beispiel():
|
||||
topic = topic_anlegen("diagbsp")
|
||||
a = db.insert("atome", topic=topic, titel="X", typ="begriff", definition="d",
|
||||
status="neu", braucht=db.j([]))
|
||||
spec = {"knoten": [{"id": "q0", "label": "Start"}, {"id": "q1", "label": "Ende"}],
|
||||
"kanten": [{"von": "q0", "zu": "q1", "label": "a"}]}
|
||||
d = db.insert("artefakte", atom_id=a, typ="beispiel", status="kandidat",
|
||||
inhalt=db.j({"form": "diagramm", "spec": spec}))
|
||||
leer = db.insert("artefakte", atom_id=a, typ="beispiel", status="kandidat",
|
||||
inhalt=db.j({"form": "diagramm", "spec": {"knoten": []}}))
|
||||
artefakte._formen_gate(topic)
|
||||
d_row = db.one("SELECT status, inhalt FROM artefakte WHERE id=?", (d,))
|
||||
assert d_row["status"] == "kandidat"
|
||||
assert db.uj(d_row["inhalt"])["mermaid"].startswith("flowchart") # kompiliert
|
||||
assert db.one("SELECT status FROM artefakte WHERE id=?", (leer,))["status"] == "verworfen"
|
||||
|
||||
|
||||
async def test_guide_messen_toter_beispiel_marker():
|
||||
import guide
|
||||
topic = topic_anlegen("bspqa")
|
||||
|
||||
@@ -313,7 +313,7 @@ async def test_reparieren_bewegt_false_bei_identischem_text(monkeypatch):
|
||||
b_id = db.insert("bausteine", topic=topic, ziel_id=ziel, titel="B", ord=0, status="neu")
|
||||
a = db.insert("atome", topic=topic, titel="A", typ="begriff", definition="d",
|
||||
status="neu", baustein_id=b_id, braucht=db.j([]))
|
||||
lang = f"<!-- atom: {a} | A -->\n" + "Wort " * 60
|
||||
lang = f"<!-- atom: {a} | A -->\n" + ("Wort " * 60).strip() # ohne Trailing-Space
|
||||
db.insert("sections", baustein_id=b_id, stage="fix", text_lang=lang,
|
||||
text_kompakt="- p", befunde=db.j(["Behebe (laenge): kürzen"]))
|
||||
monkeypatch.setitem(fake_agents._HANDLER, "Guide-Fix",
|
||||
@@ -366,6 +366,20 @@ def test_split_writer_delimited_und_fallback():
|
||||
assert guide._split_writer("nur text")[1] == "nur text"
|
||||
|
||||
|
||||
def test_writer_saeubern_terminator():
|
||||
import guide
|
||||
# geleakter `=====`-Terminator (Setext-H1 unter Bullet) + </s> raus
|
||||
roh = "- $M$ gilt\n\n$$L \\in NP$$\n- Für jedes $L$ gilt\n=====\n</s>"
|
||||
sauber = guide._writer_saeubern(roh)
|
||||
assert "=====" not in sauber and "</s>" not in sauber
|
||||
assert sauber.endswith("- Für jedes $L$ gilt")
|
||||
# _split_writer säubert den kompakt-Block (letzter Block fängt sonst das =====)
|
||||
k, l = guide._split_writer("===LANG===\nText\n===KOMPAKT===\n- a\n- b\n=====\n</s>")
|
||||
assert k == "- a\n- b" and l == "Text"
|
||||
# Tabellen-Trenner (---) bleibt unangetastet
|
||||
assert "---" in guide._writer_saeubern("| A | B |\n|---|---|\n| 1 | 2 |")
|
||||
|
||||
|
||||
async def test_fix_cap_friert_section_ein(monkeypatch):
|
||||
"""Nach FIX_MAX_VERSUCHE erfolglosen Fixes (Text unverändert) wird die Section
|
||||
für Stil-Befunde eingefroren → reparieren meldet bewegt=False (Stillstand)."""
|
||||
@@ -388,3 +402,25 @@ async def test_fix_cap_friert_section_ein(monkeypatch):
|
||||
bewegt = await guide.reparieren(
|
||||
ctx, [{"art": "det_check", "item": str(b), "detail": "Kürzen"}])
|
||||
assert bewegt is False # eingefroren → nicht geroutet → kein Fortschritt
|
||||
|
||||
|
||||
async def test_marker_platzieren_deterministisch():
|
||||
"""Der Writer setzt keine Beispiel-Marker mehr — _marker_platzieren fügt sie
|
||||
hinter den Atom-Absatz ein (atom-genau)."""
|
||||
import db
|
||||
import guide
|
||||
from conftest import topic_anlegen
|
||||
topic = topic_anlegen("platz")
|
||||
b = db.insert("bausteine", topic=topic, ziel_id=1, titel="B", ord=0, status="neu")
|
||||
a = db.insert("atome", topic=topic, titel="X", typ="begriff", definition="d",
|
||||
status="neu", baustein_id=b, ord=0, braucht=db.j([]))
|
||||
art = db.insert("artefakte", atom_id=a, typ="beispiel", status="verifiziert",
|
||||
inhalt=db.j({"form": "tabelle", "tabelle": "| A | B |\n|---|---|\n| 1 | 2 |"}))
|
||||
lang = f"#### X\n<!-- atom: {a} | X -->\nErklärung von X.\n\nWeiterer Absatz zum Thema."
|
||||
out = guide._marker_platzieren(db.one("SELECT * FROM bausteine WHERE id=?", (b,)), lang)
|
||||
assert f"<!-- beispiel: {art} -->" in out
|
||||
# der Marker steht NACH dem Atom-Absatz
|
||||
assert out.index("<!-- beispiel:") < out.index("Weiterer Absatz")
|
||||
# idempotent: zweiter Lauf dupliziert nicht
|
||||
assert guide._marker_platzieren(db.one("SELECT * FROM bausteine WHERE id=?", (b,)),
|
||||
out).count("<!-- beispiel:") == 1
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
"""Ebene 3.5 Diagramme: LLM-Diagramme (Automat/Ablauf/Baum), Spec→Mermaid, Assembly."""
|
||||
|
||||
import db
|
||||
import diagramme
|
||||
import llm
|
||||
from conftest import run_anlegen, topic_anlegen
|
||||
|
||||
|
||||
def _baustein_mit_kette(topic, n=4):
|
||||
"""Baustein mit n Atomen und einer braucht-Kette a0←a1←…←a(n-1) (n-1 Kanten)."""
|
||||
soll = db.insert("soll", topic=topic, punkt="P", status="bestaetigt", belege=db.j([]))
|
||||
ziel = db.insert("lernziele", topic=topic, text="Kann P", soll_id=soll, status="aktiv")
|
||||
b = db.insert("bausteine", topic=topic, ziel_id=ziel, titel="B", ord=0, status="neu")
|
||||
atome = [db.insert("atome", topic=topic, titel=f"Atom {i}", typ="begriff",
|
||||
definition="d", status="neu", baustein_id=b, ord=i, braucht=db.j([]))
|
||||
for i in range(n)]
|
||||
for i in range(1, n): # Atom i braucht Atom i-1
|
||||
db.execute("INSERT INTO kanten(topic, von_atom, zu_atom, art) VALUES(?,?,?,'braucht')",
|
||||
(topic, atome[i], atome[i - 1]))
|
||||
return b, atome
|
||||
|
||||
|
||||
def test_spec_zu_mermaid_flowchart():
|
||||
spec = {"typ": "flow",
|
||||
"knoten": [{"id": "1", "label": "A"}, {"id": "2", "label": 'B "x" [y]'}],
|
||||
"kanten": [{"von": "1", "zu": "2", "label": ""}]}
|
||||
out = diagramme._spec_zu_mermaid(spec)
|
||||
assert out.startswith("flowchart TD")
|
||||
assert 'n1["A"]' in out and "n1 --> n2" in out
|
||||
assert '"' not in out.split("\n", 1)[1].replace('["', "").replace('"]', "") # Label entschärft
|
||||
|
||||
|
||||
async def test_fake_judge_nein_keine_diagramme():
|
||||
topic = topic_anlegen("diag-leer")
|
||||
b, _ = _baustein_mit_kette(topic, n=2)
|
||||
ctx = llm.Kontext(run_anlegen(topic), topic, "minimax")
|
||||
await diagramme.bauen(ctx) # Fake-Judge sagt noetig=False → nur 'kein'-Merkzeile
|
||||
assert db.query("SELECT id FROM diagramme WHERE topic=? AND status='verifiziert'",
|
||||
(topic,)) == []
|
||||
assert db.one("SELECT status FROM diagramme WHERE baustein_id=?", (b,))["status"] == "kein"
|
||||
|
||||
|
||||
async def test_llm_diagramm_plan_spec_verify(monkeypatch):
|
||||
"""LLM-Zweig: Judge sagt nötig → Spec → Mermaid → Grounding-Panel verifiziert."""
|
||||
import fake_agents
|
||||
topic = topic_anlegen("diag-llm")
|
||||
# Baustein OHNE genug Kanten (kein DAG) → LLM-Zweig entscheidet
|
||||
soll = db.insert("soll", topic=topic, punkt="P", status="bestaetigt", belege=db.j([]))
|
||||
ziel = db.insert("lernziele", topic=topic, text="Kann P", soll_id=soll, status="aktiv")
|
||||
b = db.insert("bausteine", topic=topic, ziel_id=ziel, titel="Automat", ord=0, status="neu")
|
||||
for i in range(3):
|
||||
db.insert("atome", topic=topic, titel=f"Zustand {i}", typ="begriff", definition="d",
|
||||
status="neu", baustein_id=b, ord=i, braucht=db.j([]))
|
||||
monkeypatch.setitem(fake_agents._HANDLER, "Diagramm-Plan",
|
||||
lambda p: {"noetig": True, "typ": "state"})
|
||||
ctx = llm.Kontext(run_anlegen(topic), topic, "minimax")
|
||||
await diagramme.bauen(ctx)
|
||||
d = db.one("SELECT * FROM diagramme WHERE baustein_id=?", (b,))
|
||||
assert d["status"] == "verifiziert" and d["quelle"] == "llm" and d["typ"] == "state"
|
||||
assert d["mermaid"].startswith("stateDiagram-v2")
|
||||
|
||||
|
||||
async def test_llm_diagramm_grounding_verwirft(monkeypatch):
|
||||
"""Grounding-Panel einstimmig ok=false → Diagramm verworfen (fail-closed)."""
|
||||
import fake_agents
|
||||
topic = topic_anlegen("diag-verwirf")
|
||||
soll = db.insert("soll", topic=topic, punkt="P", status="bestaetigt", belege=db.j([]))
|
||||
ziel = db.insert("lernziele", topic=topic, text="Kann P", soll_id=soll, status="aktiv")
|
||||
b = db.insert("bausteine", topic=topic, ziel_id=ziel, titel="X", ord=0, status="neu")
|
||||
for i in range(3):
|
||||
db.insert("atome", topic=topic, titel=f"K{i}", typ="begriff", definition="d",
|
||||
status="neu", baustein_id=b, ord=i, braucht=db.j([]))
|
||||
monkeypatch.setitem(fake_agents._HANDLER, "Diagramm-Plan",
|
||||
lambda p: {"noetig": True, "typ": "flow"})
|
||||
monkeypatch.setitem(fake_agents._HANDLER, "Diagramm-Verify",
|
||||
lambda p: [{"ok": False, "mangel": "erfundener Knoten"}])
|
||||
ctx = llm.Kontext(run_anlegen(topic), topic, "minimax")
|
||||
await diagramme.bauen(ctx)
|
||||
assert db.one("SELECT status FROM diagramme WHERE baustein_id=?", (b,))["status"] == "verworfen"
|
||||
|
||||
|
||||
async def test_marker_wird_zu_fence():
|
||||
"""Assembly: platzierter <!-- diagramm: id --> → ```mermaid-Fence."""
|
||||
import guide
|
||||
topic = topic_anlegen("diag-place")
|
||||
b, _ = _baustein_mit_kette(topic, n=2)
|
||||
mermaid = "flowchart TD\n n1[\"A\"]\n n2[\"B\"]\n n1 --> n2"
|
||||
d = db.insert("diagramme", topic=topic, baustein_id=b, typ="flow", quelle="llm",
|
||||
spec=db.j({}), mermaid=mermaid, status="verifiziert")
|
||||
db.insert("sections", baustein_id=b, stage="done", text_kompakt="- k",
|
||||
text_lang=f"Text.\n\n<!-- diagramm: {d} -->\n\nMehr Text.")
|
||||
lang = guide.kapitel_struktur(topic)[0]["sections"][0]["lang"]
|
||||
assert "```mermaid" in lang and "flowchart TD" in lang
|
||||
assert "<!-- diagramm:" not in lang # Marker ersetzt
|
||||
|
||||
|
||||
async def test_toter_diagramm_marker_wird_entfernt():
|
||||
import guide
|
||||
topic = topic_anlegen("diag-tot")
|
||||
b, _ = _baustein_mit_kette(topic, n=2) # kein Diagramm gebaut
|
||||
db.insert("sections", baustein_id=b, stage="done", text_kompakt="- k",
|
||||
text_lang="Text.\n\n<!-- diagramm: 999 -->\n\nMehr.")
|
||||
lang = guide.kapitel_struktur(topic)[0]["sections"][0]["lang"]
|
||||
assert "<!-- diagramm:" not in lang and "```mermaid" not in lang
|
||||
|
||||
|
||||
async def test_guide_messen_meldet_toten_marker():
|
||||
"""Ein Marker ohne verifiziertes Diagramm → diagramm_marker_tot in guide.messen."""
|
||||
import guide
|
||||
topic = topic_anlegen("diag-qa")
|
||||
b, _ = _baustein_mit_kette(topic, n=4)
|
||||
db.insert("sections", baustein_id=b, stage="done", text_kompakt="- k",
|
||||
text_lang="Text.\n\n<!-- diagramm: 777 -->\n\n" + "Wort " * 60)
|
||||
ctx = llm.Kontext(run_anlegen(topic), topic, "minimax")
|
||||
ctx.ebene = "guide"
|
||||
assert "diagramm_marker_tot" in {x["art"] for x in guide.messen(ctx)}
|
||||
|
||||
|
||||
async def test_marker_platzieren_deterministisch():
|
||||
"""Der Writer setzt keine Beispiel-Marker mehr — _marker_platzieren fügt sie
|
||||
hinter den Atom-Absatz ein (atom-genau)."""
|
||||
import guide
|
||||
topic = topic_anlegen("platz")
|
||||
b = db.insert("bausteine", topic=topic, ziel_id=1, titel="B", ord=0, status="neu")
|
||||
a = db.insert("atome", topic=topic, titel="X", typ="begriff", definition="d",
|
||||
status="neu", baustein_id=b, ord=0, braucht=db.j([]))
|
||||
art = db.insert("artefakte", atom_id=a, typ="beispiel", status="verifiziert",
|
||||
inhalt=db.j({"form": "tabelle", "tabelle": "| A | B |\n|---|---|\n| 1 | 2 |"}))
|
||||
lang = f"#### X\n<!-- atom: {a} | X -->\nErklärung von X.\n\nWeiterer Absatz zum Thema."
|
||||
out = guide._marker_platzieren(db.one("SELECT * FROM bausteine WHERE id=?", (b,)), lang)
|
||||
assert f"<!-- beispiel: {art} -->" in out
|
||||
# der Marker steht NACH dem Atom-Absatz
|
||||
assert out.index("<!-- beispiel:") < out.index("Weiterer Absatz")
|
||||
# idempotent: zweiter Lauf dupliziert nicht
|
||||
assert guide._marker_platzieren(db.one("SELECT * FROM bausteine WHERE id=?", (b,)),
|
||||
out).count("<!-- beispiel:") == 1
|
||||
@@ -146,7 +146,7 @@ async def test_ebenen_entfernen_kaskade():
|
||||
assert db.one("SELECT status FROM topics WHERE name=?", (topic,))["status"] == "artefakte_fertig"
|
||||
# Anzeige-Reset: Token-Zählung der entfernten Ebenen beginnt neu (Events bleiben)
|
||||
resets = db.uj(db.one("SELECT resets FROM topics WHERE name=?", (topic,))["resets"], {})
|
||||
assert set(resets) == {"struktur", "diagramme", "guide"} # diagramme hängt an struktur
|
||||
assert set(resets) == {"struktur", "guide"} # guide hängt an struktur
|
||||
assert resets["struktur"] == db.one("SELECT MAX(id) AS m FROM events")["m"]
|
||||
run2 = await _lauf_komplett(topic)
|
||||
_pruefe_endzustand(topic, run2)
|
||||
|
||||
Reference in New Issue
Block a user