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)
|
Dedup: Anker-Overlap (auto) → Embedding-Kandidaten → 2er-Panel (einstimmig)
|
||||||
Soll-Zuordnung je Atom; Soll-Punkt ohne Atom → gezielte Nachextraktion
|
Soll-Zuordnung je Atom; Soll-Punkt ohne Atom → gezielte Nachextraktion
|
||||||
E2 Artefakte je Soll-Punkt-Gruppe: Flashcards + 1 typisiertes Beispiel pro Atom
|
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
|
(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
|
E3 Struktur Lernziele+Zuordnung aus Atomen (je Soll-Punkt) → Ziel-Check → Kanten auflösen
|
||||||
→ Zyklen brechen (Judge) → Bausteine = Ziel-Gruppen im Band (split/merge)
|
→ Zyklen brechen (Judge) → Bausteine = Ziel-Gruppen im Band (split/merge)
|
||||||
→ topologische Ordnung (Bausteine + Atome darin)
|
→ 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
|
E4 Guide je Baustein: Writer (Facts inline, Atom-Marker; Ausgabe DELIMITED, nicht JSON
|
||||||
— LaTeX-Backslashes) → det. Checks → Prüfer → Fix (Fix-Cap gegen Churn).
|
— 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.
|
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)`
|
- `anker(atom_id, quelle_id, start, ende, zitat)`
|
||||||
- `kanten(topic, von_atom, zu_atom, art braucht|verwandt, status)`
|
- `kanten(topic, von_atom, zu_atom, art braucht|verwandt, status)`
|
||||||
- `artefakte(id, atom_id, typ flashcard|beispiel, inhalt JSON, status kandidat|verifiziert|verworfen)`
|
- `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);
|
— beispiel-`inhalt` trägt `form` + formspezifische Felder (text|code+sprache|tabelle);
|
||||||
code/tabelle/diagramm kommen verbatim via `<!-- beispiel: id -->` in den Guide (Assembly-Swap)
|
code/tabelle kommen verbatim via `<!-- beispiel: id -->` in den Guide (Assembly-Swap)
|
||||||
- `lernziele(id, topic, text, soll_id, status)`
|
- `lernziele(id, topic, text, soll_id, status)`
|
||||||
- `bausteine(id, topic, ziel_id, titel, ord, 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)`
|
- `sections(baustein_id, stage, text_kompakt, text_lang, befunde JSON)`
|
||||||
- `leitner(artefakt_id, box, faellig, historie)`
|
- `leitner(artefakt_id, box, faellig, historie)`
|
||||||
- `befunde(run_id, ebene, art, item, detail, status offen|repariert|freigesprochen)`
|
- `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
|
&& pip install --no-cache-dir -r /app/backend/requirements.txt
|
||||||
|
|
||||||
COPY --chown=app:app backend/ /app/backend/
|
COPY --chown=app:app backend/ /app/backend/
|
||||||
# katex + mermaid (+ jsdom fürs headless-DOM, das Mermaid zum Parsen braucht) fürs
|
# katex fürs Parse-Gate (backend/katex_check.mjs prüft mit dem echten Renderer)
|
||||||
# Parse-Gate (backend/{katex,mermaid}_check.mjs prüfen mit dem echten Renderer)
|
RUN cd /app/backend && npm install --no-audit --no-fund katex && chown -R app:app node_modules
|
||||||
RUN cd /app/backend && npm install --no-audit --no-fund katex mermaid jsdom && chown -R app:app node_modules
|
|
||||||
COPY --chown=app:app templates/ /app/templates/
|
COPY --chown=app:app templates/ /app/templates/
|
||||||
COPY --chown=app:app --from=frontend /build/dist /app/frontend/dist
|
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
|
COPY --chown=app:app dev-ops/opencode.json /home/app/.config/opencode/opencode.json
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import subprocess
|
|||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
import db
|
import db
|
||||||
import diagramme
|
|
||||||
import llm
|
import llm
|
||||||
from config import ARTEFAKT_CHUNK_ATOME, BEISPIEL_FORMEN, VERIFY_PANEL
|
from config import ARTEFAKT_CHUNK_ATOME, BEISPIEL_FORMEN, VERIFY_PANEL
|
||||||
|
|
||||||
@@ -30,8 +29,8 @@ _QUELLEN_REFERENZ = re.compile(
|
|||||||
|
|
||||||
|
|
||||||
def _prosa_teile(inhalt: dict) -> str:
|
def _prosa_teile(inhalt: dict) -> str:
|
||||||
"""Nur menschenlesbare Prosa fürs Quellen-Referenz-Gate — NICHT Code/Mermaid/
|
"""Nur menschenlesbare Prosa fürs Quellen-Referenz-Gate — NICHT Code/Tabelle
|
||||||
Tabelle (dort matchen Bezeichner/Kommentare den Regex falsch, z. B. „blatt 3")."""
|
(dort matchen Bezeichner/Kommentare den Regex falsch, z. B. „blatt 3")."""
|
||||||
return " ".join(str(inhalt.get(k, "")) for k in ("frage", "antwort", "text"))
|
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", "")),
|
"text": str(e.get("text", "")),
|
||||||
"code": str(e.get("code", "")),
|
"code": str(e.get("code", "")),
|
||||||
"sprache": str(e.get("sprache", "")).strip().lower(),
|
"sprache": str(e.get("sprache", "")).strip().lower(),
|
||||||
"tabelle": str(e.get("tabelle", "")),
|
"tabelle": str(e.get("tabelle", ""))}
|
||||||
"spec": e.get("spec") if isinstance(e.get("spec"), dict) else {},
|
|
||||||
"mermaid": ""}
|
|
||||||
|
|
||||||
|
|
||||||
async def _generieren(ctx: llm.Kontext, chunk: list[dict]) -> None:
|
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")
|
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:
|
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]:
|
def _beispiel_pruefen(inh: dict) -> tuple[bool, dict]:
|
||||||
"""Form-spezifisches Gate (deterministisch, parse-only — nie Ausführung).
|
"""Form-spezifisches Gate (deterministisch, parse-only — nie Ausführung).
|
||||||
→ (ok, inhalt); der Diagramm-Zweig füllt `mermaid`. Unbekannte/woven Formen
|
→ (ok, inhalt). Unbekannte/woven Formen passieren (der Judge prüft inhaltlich)."""
|
||||||
passieren (der Judge prüft inhaltlich)."""
|
|
||||||
form = inh.get("form", "text")
|
form = inh.get("form", "text")
|
||||||
if form == "tabelle":
|
if form == "tabelle":
|
||||||
return _tabelle_ok(inh.get("tabelle", "")), inh
|
return _tabelle_ok(inh.get("tabelle", "")), inh
|
||||||
if form == "code":
|
if form == "code":
|
||||||
return _code_ok(inh.get("sprache", ""), inh.get("code", "")), inh
|
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
|
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)
|
BRAUCHT_KANDIDATEN = 8 # Kandidaten-Cap je unaufgelöstem braucht-Titel (Lektion 34)
|
||||||
|
|
||||||
# ── Ebene 2: Artefakte ────────────────────────────────────────────────────────
|
# ── 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)
|
CODE_SPRACHEN = ("python", "javascript") # Syntax-Gate parse-only (Container: python3, node)
|
||||||
ARTEFAKT_CHUNK_ATOME = 8 # Atome pro Generate-Call (ein Soll-Punkt, gestückelt)
|
ARTEFAKT_CHUNK_ATOME = 8 # Atome pro Generate-Call (ein Soll-Punkt, gestückelt)
|
||||||
VERIFY_PANEL = 2 # unabhängige Prüfer, einstimmig = verifiziert
|
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
|
ZIELE_CHUNK_ATOME = 30 # Atome pro Lernziel-Call
|
||||||
KAPITEL_BAUSTEINE = 5 # Richtwert Bausteine je Kapitel (Kapitel-Schnitt-Judge)
|
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 ────────────────────────────────────────────────────────────
|
# ── Ebene 4: Guide ────────────────────────────────────────────────────────────
|
||||||
SECTION_WOERTER_PRO_ATOM = (40, 220) # Längenband ausführlicher Teil je Atom
|
SECTION_WOERTER_PRO_ATOM = (40, 220) # Längenband ausführlicher Teil je Atom
|
||||||
|
|
||||||
@@ -137,9 +134,6 @@ TIMEOUTS = {
|
|||||||
"writer": (450, 60),
|
"writer": (450, 60),
|
||||||
"pruefer": (600, 5),
|
"pruefer": (600, 5),
|
||||||
"qa_judge": (600, 0),
|
"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,
|
id INTEGER PRIMARY KEY, topic TEXT NOT NULL, titel TEXT NOT NULL,
|
||||||
intro TEXT NOT NULL DEFAULT '', ord INTEGER DEFAULT 0,
|
intro TEXT NOT NULL DEFAULT '', ord INTEGER DEFAULT 0,
|
||||||
art TEXT NOT NULL DEFAULT 'judge', level TEXT NOT NULL DEFAULT 'M');
|
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(
|
CREATE TABLE IF NOT EXISTS sections(
|
||||||
baustein_id INTEGER PRIMARY KEY, stage TEXT NOT NULL DEFAULT 'writer',
|
baustein_id INTEGER PRIMARY KEY, stage TEXT NOT NULL DEFAULT 'writer',
|
||||||
text_kompakt TEXT DEFAULT '', text_lang TEXT DEFAULT '', befunde TEXT DEFAULT '[]',
|
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.
|
# Tabellen, deren Änderungen das Live-Board interessieren.
|
||||||
_LIVE_TABELLEN = {"topics", "runs", "quellen", "soll", "atome", "artefakte",
|
_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:
|
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$."]
|
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
|
fuellung = "Daran anknüpfend gehen wir das Schritt für Schritt am Beispiel durch. " * 5
|
||||||
for (mk, titel), d in zip(marker, defs):
|
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.
|
# nicht mehr der Writer.
|
||||||
absaetze.append(f"#### {titel} verstehen\n{mk}\n{d} {fuellung}")
|
absaetze.append(f"#### {titel} verstehen\n{mk}\n{d} {fuellung}")
|
||||||
lang = "\n\n".join(absaetze)
|
lang = "\n\n".join(absaetze)
|
||||||
@@ -247,23 +247,6 @@ def _writer(prompt: str):
|
|||||||
"lang": lang}
|
"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):
|
def _pruefer(prompt: str):
|
||||||
return {"befunde": []}
|
return {"befunde": []}
|
||||||
|
|
||||||
@@ -298,6 +281,4 @@ _HANDLER = {
|
|||||||
"Kanten-Zyklus": _zyklus, "Braucht-Aufloesung": _braucht_aufloesung,
|
"Kanten-Zyklus": _zyklus, "Braucht-Aufloesung": _braucht_aufloesung,
|
||||||
"Guide-Writer": _writer, "Guide-Pruefer": _pruefer, "Guide-Fix": _fix,
|
"Guide-Writer": _writer, "Guide-Pruefer": _pruefer, "Guide-Fix": _fix,
|
||||||
"QA-Guide-Falsch": _qa_falsch, "QA-Guide-Falsch-Check": _qa_falsch_check,
|
"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
|
# den Match und die Marker-Zeile bliebe in den Text-Checks stehen (Marker sind
|
||||||
# einzeilig, also kein DOTALL). Muster spiegelt frontend/src/markdown.js.
|
# einzeilig, also kein DOTALL). Muster spiegelt frontend/src/markdown.js.
|
||||||
_MARKER_VOLL = re.compile(r"<!--\s*atom:.*?-->")
|
_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*-->")
|
_MARKER_BEISPIEL = re.compile(r"<!--\s*beispiel:\s*(\d+)\s*-->")
|
||||||
|
|
||||||
# Kurzbeschreibung je Diagramm-Typ (Writer-Angebot)
|
_BEISPIEL_BESCHR = {"code": "Code-Beispiel", "tabelle": "Tabelle/Vergleich"}
|
||||||
_DIAGRAMM_BESCHR = {"dependency": "Abhängigkeitsgraph der Begriffe dieses Bausteins",
|
_VERBATIM_FORMEN = ("code", "tabelle")
|
||||||
"state": "Zustandsdiagramm/Automat", "flow": "Ablaufdiagramm",
|
|
||||||
"tree": "Baumstruktur"}
|
|
||||||
_BEISPIEL_BESCHR = {"code": "Code-Beispiel", "tabelle": "Tabelle/Vergleich",
|
|
||||||
"diagramm": "Diagramm-Beispiel"}
|
|
||||||
_VERBATIM_FORMEN = ("code", "tabelle", "diagramm")
|
|
||||||
|
|
||||||
|
|
||||||
def _ohne_marker(text: str) -> str:
|
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
|
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"
|
gestrippten Text — sonst erzeugen Atom-TITEL wie „a_n" oder „Satz 6.26"
|
||||||
unfixierbare Aufträge (der Fix darf Marker nie ändern → Endlos-Loop).
|
unfixierbare Aufträge (der Fix darf Marker nie ändern → Endlos-Loop).
|
||||||
Diagramm- und Beispiel-Marker gehören ebenso raus."""
|
Beispiel-Marker gehören ebenso raus."""
|
||||||
return _MARKER_BEISPIEL.sub("", _MARKER_DIAGRAMM.sub("", _MARKER_VOLL.sub("", text)))
|
return _MARKER_BEISPIEL.sub("", _MARKER_VOLL.sub("", text))
|
||||||
|
|
||||||
|
|
||||||
def _beispiel_verbatim(atom_id: int) -> list[tuple[int, dict]]:
|
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`."""
|
(artefakt_id, inhalt). Woven-Formen (text/mathe) gehen über `_beispiel`."""
|
||||||
out = []
|
out = []
|
||||||
for r in db.query("SELECT id, inhalt FROM artefakte WHERE atom_id=? AND typ='beispiel'"
|
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:
|
def _beispiel_render(inh: dict) -> str:
|
||||||
"""Verbatim-Beispiel → Markdown (Code-Fence / Tabelle / Mermaid-Fence)."""
|
"""Verbatim-Beispiel → Markdown (Code-Fence / Tabelle)."""
|
||||||
form = inh.get("form")
|
form = inh.get("form")
|
||||||
if form == "code" and inh.get("code"):
|
if form == "code" and inh.get("code"):
|
||||||
return f"```{inh.get('sprache', '')}\n{inh['code']}\n```"
|
return f"```{inh.get('sprache', '')}\n{inh['code']}\n```"
|
||||||
if form == "tabelle" and inh.get("tabelle"):
|
if form == "tabelle" and inh.get("tabelle"):
|
||||||
return inh["tabelle"]
|
return inh["tabelle"]
|
||||||
if form == "diagramm" and inh.get("mermaid"):
|
|
||||||
return f"```mermaid\n{inh['mermaid']}\n```"
|
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def _marker_platzieren(b: dict, lang: str) -> str:
|
def _marker_platzieren(b: dict, lang: str) -> str:
|
||||||
"""Verbatim-Marker DETERMINISTISCH setzen (der Writer tut es nicht mehr — er
|
"""Verbatim-Marker DETERMINISTISCH setzen (der Writer tut es nicht mehr — er
|
||||||
ignorierte die Regel, 2/13 platziert). Beispiel-Marker hinter den Atom-Absatz
|
ignorierte die Regel, 2/13 platziert). Beispiel-Marker hinter den Atom-Absatz
|
||||||
(atom-genau via Atom-Marker); Diagramme (baustein-scoped) ans Section-Ende.
|
(atom-genau via Atom-Marker). Idempotent: alte Verbatim-Marker erst raus."""
|
||||||
Idempotent: alte Verbatim-Marker erst raus."""
|
lang = _MARKER_BEISPIEL.sub("", lang)
|
||||||
lang = _MARKER_BEISPIEL.sub("", _MARKER_DIAGRAMM.sub("", lang))
|
|
||||||
je_atom = {}
|
je_atom = {}
|
||||||
for a in _atome_von(b["id"]):
|
for a in _atome_von(b["id"]):
|
||||||
vs = _beispiel_verbatim(a["id"])
|
vs = _beispiel_verbatim(a["id"])
|
||||||
@@ -124,10 +115,6 @@ def _marker_platzieren(b: dict, lang: str) -> str:
|
|||||||
if pending is not None:
|
if pending is not None:
|
||||||
out += ["", f"<!-- beispiel: {pending} -->"]
|
out += ["", f"<!-- beispiel: {pending} -->"]
|
||||||
lang = "\n".join(out)
|
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
|
return lang
|
||||||
|
|
||||||
|
|
||||||
@@ -145,24 +132,6 @@ def _beispiel_einsetzen(baustein_id: int, lang: str) -> str:
|
|||||||
return _MARKER_BEISPIEL.sub(sub, lang)
|
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
|
# 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)
|
# (vorwaerts ist Stil: ein Halbsatz-Fix, kein Fakten-Problem)
|
||||||
KRITISCH = ("marker_fehlend", "marker_fremd", "ziel_ohne_anker", "fachlich_falsch")
|
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:
|
def _beispiel(atom_id: int) -> str:
|
||||||
"""Nur die WOVEN-Formen (text/mathe) als Prosa für den Writer. Verbatim-Formen
|
"""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'"
|
row = db.one("SELECT inhalt FROM artefakte WHERE atom_id=? AND typ='beispiel'"
|
||||||
" AND status='verifiziert' LIMIT 1", (atom_id,))
|
" AND status='verifiziert' LIMIT 1", (atom_id,))
|
||||||
if not row:
|
if not row:
|
||||||
@@ -233,23 +202,35 @@ def _anknuepf_kontext(b: dict) -> str:
|
|||||||
|
|
||||||
# ── Stages ────────────────────────────────────────────────────────────────────
|
# ── 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]:
|
def _split_writer(text: str) -> tuple[str, str]:
|
||||||
"""Delimited-Ausgabe des Writers/Fix parsen: ===LANG===… / ===KOMPAKT===….
|
"""Delimited-Ausgabe des Writers/Fix parsen: ===LANG===… / ===KOMPAKT===….
|
||||||
Grund: rohe LaTeX-Backslashes im Text sind invalides JSON (34 % Parse-Fehler).
|
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
|
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:
|
def block(name: str) -> str | None:
|
||||||
m = re.search(rf"===\s*{name}\s*===\s*(.*?)(?=\n===\s*[A-ZÄÖÜ]+\s*===|$)",
|
m = re.search(rf"===\s*{name}\s*===\s*(.*?)(?=\n===\s*[A-ZÄÖÜ]+\s*===|$)",
|
||||||
text or "", re.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")
|
lang, kompakt = block("LANG"), block("KOMPAKT")
|
||||||
if lang is not None or kompakt is not None:
|
if lang is not None or kompakt is not None:
|
||||||
return kompakt or "", lang or ""
|
return kompakt or "", lang or ""
|
||||||
daten = jsonx.parse(text or "") # Fallback: altes JSON-Format
|
daten = jsonx.parse(text or "") # Fallback: altes JSON-Format
|
||||||
if isinstance(daten, dict):
|
if isinstance(daten, dict):
|
||||||
return str(daten.get("kompakt", "")), str(daten.get("lang", ""))
|
return (_writer_saeubern(str(daten.get("kompakt", ""))),
|
||||||
return "", (text or "").strip()
|
_writer_saeubern(str(daten.get("lang", ""))))
|
||||||
|
return "", _writer_saeubern(text or "")
|
||||||
|
|
||||||
|
|
||||||
async def _stage_writer(ctx: llm.Kontext, b: dict) -> str:
|
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)
|
k2, l2 = _split_writer(res2)
|
||||||
if not _marker_fehlend(l2, atome):
|
if not _marker_fehlend(l2, atome):
|
||||||
kompakt, lang = k2, l2
|
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)
|
db.update("sections", "baustein_id", b["id"], text_kompakt=kompakt, text_lang=lang)
|
||||||
return "pruefer"
|
return "pruefer"
|
||||||
|
|
||||||
@@ -604,15 +585,17 @@ def kapitel_struktur(topic: str) -> list[dict]:
|
|||||||
"titel": k.get("titel", "Weitere Themen"),
|
"titel": k.get("titel", "Weitere Themen"),
|
||||||
"intro": k.get("intro", ""),
|
"intro": k.get("intro", ""),
|
||||||
"level": k.get("level", b["level"]), "sections": []})
|
"level": k.get("level", b["level"]), "sections": []})
|
||||||
# Diagramm- und Beispiel-Marker gegen ihre Fences/Tabellen tauschen (Assembly);
|
# Beispiel-Marker gegen ihre Fences/Tabellen tauschen (Assembly);
|
||||||
# Atom-Marker bleiben — die strippt erst das Frontend.
|
# 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 = ""
|
lang = ""
|
||||||
if sec:
|
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({
|
kapitel[-1]["sections"].append({
|
||||||
"baustein": b["id"], "titel": b["titel"],
|
"baustein": b["id"], "titel": b["titel"],
|
||||||
"ziel": ziele.get(b["ziel_id"], {}).get("text", ""),
|
"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})
|
"lang": lang})
|
||||||
# NULL-kapitel_id durch eindeutige synthetische Keys ersetzen — mehrere
|
# NULL-kapitel_id durch eindeutige synthetische Keys ersetzen — mehrere
|
||||||
# kapitellose Gruppen kollidierten sonst im Vue-:key (null == null).
|
# 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):
|
for i in _marker_fremd_ids(lang, atome):
|
||||||
befunde.append({"art": "marker_fremd", "item": str(b["id"]),
|
befunde.append({"art": "marker_fremd", "item": str(b["id"]),
|
||||||
"detail": f"Atom {i}"})
|
"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"])}
|
gueltige_b = {aid for a in atome for aid, _ in _beispiel_verbatim(a["id"])}
|
||||||
for m in _MARKER_BEISPIEL.findall(lang):
|
for m in _MARKER_BEISPIEL.findall(lang):
|
||||||
if int(m) not in gueltige_b: # Marker ohne verifiziertes Verbatim-Beispiel
|
if int(m) not in gueltige_b: # Marker ohne verifiziertes Verbatim-Beispiel
|
||||||
|
|||||||
@@ -153,10 +153,6 @@ def state(topic: str):
|
|||||||
"bausteine": db.query(
|
"bausteine": db.query(
|
||||||
"SELECT b.*, s.stage FROM bausteine b LEFT JOIN sections s ON s.baustein_id=b.id"
|
"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,)),
|
" 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
|
# topic-scoped: offene Befunde früherer Läufe bleiben nach Resume sichtbar
|
||||||
"befunde": db.query(
|
"befunde": db.query(
|
||||||
"SELECT b.* FROM befunde b JOIN runs r ON r.id=b.run_id"
|
"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 artefakte
|
||||||
import auto_loop
|
import auto_loop
|
||||||
import db
|
import db
|
||||||
import diagramme
|
|
||||||
import guide
|
import guide
|
||||||
import inventar
|
import inventar
|
||||||
import korpus
|
import korpus
|
||||||
@@ -27,11 +26,10 @@ EBENEN = [
|
|||||||
("inventar", inventar, "inventar_fertig"),
|
("inventar", inventar, "inventar_fertig"),
|
||||||
("artefakte", artefakte, "artefakte_fertig"),
|
("artefakte", artefakte, "artefakte_fertig"),
|
||||||
("struktur", struktur, "struktur_fertig"),
|
("struktur", struktur, "struktur_fertig"),
|
||||||
("diagramme", diagramme, "diagramme_fertig"),
|
|
||||||
("guide", guide, "fertig"),
|
("guide", guide, "fertig"),
|
||||||
]
|
]
|
||||||
_ORDNUNG = ["neu", "korpus", "korpus_fertig", "inventar_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] = {}
|
_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())
|
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",
|
"struktur": "artefakte_fertig", "artefakte": "inventar_fertig",
|
||||||
"inventar": "korpus_fertig", "korpus": "neu"}
|
"inventar": "korpus_fertig", "korpus": "neu"}
|
||||||
|
|
||||||
@@ -77,25 +75,23 @@ def ebenen_entfernen(topic: str, ebene: str) -> None:
|
|||||||
if ebene not in _ROLLBACK:
|
if ebene not in _ROLLBACK:
|
||||||
raise ValueError(f"unbekannte Ebene: {ebene}")
|
raise ValueError(f"unbekannte Ebene: {ebene}")
|
||||||
lauf_stoppen(topic)
|
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"
|
db.execute("DELETE FROM sections WHERE baustein_id IN"
|
||||||
" (SELECT id FROM bausteine WHERE topic=?)", (topic,))
|
" (SELECT id FROM bausteine WHERE topic=?)", (topic,))
|
||||||
db.execute("UPDATE bausteine SET status='neu' 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
|
db.execute("UPDATE kapitel SET intro='' WHERE topic=?", (topic,)) # Intro ist Guide-Text
|
||||||
if stufe >= 1: # diagramme
|
if stufe >= 1: # struktur
|
||||||
db.execute("DELETE FROM diagramme WHERE topic=?", (topic,))
|
|
||||||
if stufe >= 2: # struktur
|
|
||||||
db.execute("DELETE FROM bausteine WHERE topic=?", (topic,))
|
db.execute("DELETE FROM bausteine WHERE topic=?", (topic,))
|
||||||
db.execute("DELETE FROM kapitel WHERE topic=?", (topic,))
|
db.execute("DELETE FROM kapitel WHERE topic=?", (topic,))
|
||||||
db.execute("DELETE FROM lernziele 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=?",
|
db.execute("UPDATE atome SET ziel_id=NULL, baustein_id=NULL, ord=0 WHERE topic=?",
|
||||||
(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"
|
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,))
|
" JOIN atome a ON ar.atom_id=a.id WHERE a.topic=?)", (topic,))
|
||||||
db.execute("DELETE FROM artefakte WHERE atom_id IN"
|
db.execute("DELETE FROM artefakte WHERE atom_id IN"
|
||||||
" (SELECT id FROM atome WHERE topic=?)", (topic,))
|
" (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=?)",
|
db.execute("DELETE FROM anker WHERE atom_id IN (SELECT id FROM atome WHERE topic=?)",
|
||||||
(topic,))
|
(topic,))
|
||||||
db.execute("DELETE FROM kanten 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
|
# atome_stand mit-nullen — sonst überspringt der Resume-Guard die Neu-Extraktion
|
||||||
db.execute("UPDATE quellen SET status='extrahiert', atome_stand='' WHERE topic=?",
|
db.execute("UPDATE quellen SET status='extrahiert', atome_stand='' WHERE topic=?",
|
||||||
(topic,))
|
(topic,))
|
||||||
if stufe >= 5: # korpus
|
if stufe >= 4: # korpus
|
||||||
db.execute("DELETE FROM soll WHERE topic=?", (topic,))
|
db.execute("DELETE FROM soll WHERE topic=?", (topic,))
|
||||||
db.execute("DELETE FROM quellen WHERE topic=?", (topic,))
|
db.execute("DELETE FROM quellen WHERE topic=?", (topic,))
|
||||||
import shutil
|
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.
|
# 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
|
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"], {})
|
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
|
resets[name] = letzte
|
||||||
db.update("topics", "name", topic, status=_ROLLBACK[ebene], resets=db.j(resets))
|
db.update("topics", "name", topic, status=_ROLLBACK[ebene], resets=db.j(resets))
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import logging
|
|||||||
|
|
||||||
import artefakte
|
import artefakte
|
||||||
import db
|
import db
|
||||||
import diagramme
|
|
||||||
import guide
|
import guide
|
||||||
import inventar
|
import inventar
|
||||||
import korpus
|
import korpus
|
||||||
@@ -19,7 +18,7 @@ import struktur
|
|||||||
log = logging.getLogger("creator2.qa")
|
log = logging.getLogger("creator2.qa")
|
||||||
|
|
||||||
MODULE = {"korpus": korpus, "inventar": inventar, "artefakte": artefakte,
|
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 je Befund-Art: kritisch 3.0 (verletzt Kernprinzip), mittel 1.5, stil 0.5.
|
||||||
GEWICHTE = {
|
GEWICHTE = {
|
||||||
@@ -34,8 +33,6 @@ GEWICHTE = {
|
|||||||
"section_fehlt": 3.0, "marker_fehlend": 3.0, "marker_fremd": 3.0,
|
"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,
|
"ziel_ohne_anker": 3.0, "fachlich_falsch": 3.0, "laenge": 0.5, "vorwaerts": 0.5,
|
||||||
"det_check": 0.5, "fix_offen": 1.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,
|
"beispiel_marker_tot": 3.0,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,7 +46,6 @@ def _basis(topic: str, ebene: str) -> int:
|
|||||||
" AND status NOT IN ('gemerged','verworfen')",
|
" AND status NOT IN ('gemerged','verworfen')",
|
||||||
"struktur": "SELECT COUNT(*) n FROM atome WHERE topic=?"
|
"struktur": "SELECT COUNT(*) n FROM atome WHERE topic=?"
|
||||||
" AND status NOT IN ('gemerged','verworfen')",
|
" AND status NOT IN ('gemerged','verworfen')",
|
||||||
"diagramme": "SELECT COUNT(*) n FROM bausteine WHERE topic=?",
|
|
||||||
"guide": "SELECT COUNT(*) n FROM bausteine WHERE topic=?"}[ebene]
|
"guide": "SELECT COUNT(*) n FROM bausteine WHERE topic=?"}[ebene]
|
||||||
row = db.one(z, (topic,))
|
row = db.one(z, (topic,))
|
||||||
return max(1, int(row["n"]) if row else 1)
|
return max(1, int(row["n"]) if row else 1)
|
||||||
|
|||||||
@@ -12,11 +12,9 @@
|
|||||||
"@fontsource-variable/inter": "^5.2.8",
|
"@fontsource-variable/inter": "^5.2.8",
|
||||||
"dompurify": "^3.4.7",
|
"dompurify": "^3.4.7",
|
||||||
"highlight.js": "^11.11.1",
|
"highlight.js": "^11.11.1",
|
||||||
"jsdom": "^29.1.1",
|
|
||||||
"katex": "^0.17.0",
|
"katex": "^0.17.0",
|
||||||
"marked": "^18.0.4",
|
"marked": "^18.0.4",
|
||||||
"marked-highlight": "^2.2.4",
|
"marked-highlight": "^2.2.4",
|
||||||
"mermaid": "^11.4.1",
|
|
||||||
"vue": "^3.5.32"
|
"vue": "^3.5.32"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import Ueben from './components/Ueben.vue'
|
|||||||
import Kennzahlen from './components/Kennzahlen.vue'
|
import Kennzahlen from './components/Kennzahlen.vue'
|
||||||
|
|
||||||
const topics = ref([])
|
const topics = ref([])
|
||||||
const topic = ref('')
|
const topic = ref(localStorage.getItem('topic') || '') // Auswahl über Reload halten
|
||||||
const state = ref(null)
|
const state = ref(null)
|
||||||
const tab = ref('board')
|
const tab = ref(localStorage.getItem('tab') || 'board')
|
||||||
const neuName = ref('')
|
const neuName = ref('')
|
||||||
const neuArt = ref('thema')
|
const neuArt = ref('thema')
|
||||||
const fehler = ref('')
|
const fehler = ref('')
|
||||||
@@ -24,6 +24,9 @@ function navToggle() {
|
|||||||
localStorage.setItem('navAuf', navAuf.value ? '1' : '0')
|
localStorage.setItem('navAuf', navAuf.value ? '1' : '0')
|
||||||
}
|
}
|
||||||
watch(navAuf, (a) => document.body.classList.toggle('nav-zu', !a), { immediate: true })
|
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 wsSchliessen = null
|
||||||
let timer = null
|
let timer = null
|
||||||
@@ -40,6 +43,8 @@ media.addEventListener('change', themeAnwenden)
|
|||||||
|
|
||||||
async function topicsLaden() {
|
async function topicsLaden() {
|
||||||
topics.value = await api.topics()
|
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
|
if (!topic.value && topics.value.length) topic.value = topics.value[0].name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ const EBENEN = [
|
|||||||
{ key: 'inventar', titel: 'Atome' },
|
{ key: 'inventar', titel: 'Atome' },
|
||||||
{ key: 'artefakte', titel: 'Artefakte' },
|
{ key: 'artefakte', titel: 'Artefakte' },
|
||||||
{ key: 'struktur', titel: 'Bausteine' },
|
{ key: 'struktur', titel: 'Bausteine' },
|
||||||
{ key: 'diagramme', titel: 'Diagramme' },
|
|
||||||
{ key: 'guide', titel: 'Guide' },
|
{ 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}`,
|
return s.bausteine.map((b) => ({ id: b.id, text: `${b.ord + 1}. ${b.titel}`,
|
||||||
badges: [[b.status, statusFarbe(b.status)]] }))
|
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}`,
|
return s.bausteine.map((b) => ({ id: b.id, text: `${b.ord + 1}. ${b.titel}`,
|
||||||
badges: [[b.stage || 'offen', b.stage === 'done' ? 'gruen' : 'blau']] }))
|
badges: [[b.stage || 'offen', b.stage === 'done' ? 'gruen' : 'blau']] }))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,8 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, nextTick, onUnmounted, ref, watch } from 'vue'
|
import { computed, nextTick, onUnmounted, ref, watch } from 'vue'
|
||||||
import mermaid from 'mermaid'
|
|
||||||
import { api } from '../api.js'
|
import { api } from '../api.js'
|
||||||
import { render, lesestat } from '../markdown.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 props = defineProps({ topic: String, state: Object })
|
||||||
const daten = ref(null)
|
const daten = ref(null)
|
||||||
const level = ref('E') // Durchgang-Wahl: jede Stufe ist ein eigener Lese-Durchgang
|
const level = ref('E') // Durchgang-Wahl: jede Stufe ist ein eigener Lese-Durchgang
|
||||||
@@ -46,27 +40,6 @@ function html(s) {
|
|||||||
return htmlCache.get(key)
|
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
|
// Nur aktives Kapitel ±1 im DOM; der Rest ist ein Platzhalter mit Schätzhöhe
|
||||||
const sichtbar = (i) => Math.abs(i - aktiv.value) <= 1
|
const sichtbar = (i) => Math.abs(i - aktiv.value) <= 1
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ const MARKER = /<!--\s*atom:.*?-->/g
|
|||||||
export function lesestat(text) {
|
export function lesestat(text) {
|
||||||
let displayFormeln = 0
|
let displayFormeln = 0
|
||||||
const t = (text || '')
|
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(MARKER, ' ')
|
||||||
.replace(/\$\$[\s\S]+?\$\$/g, () => { displayFormeln += 1; return ' ' })
|
.replace(/\$\$[\s\S]+?\$\$/g, () => { displayFormeln += 1; return ' ' })
|
||||||
.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: Ebenen-Spalten links→rechts, Listen VOLLSTÄNDIG scrollbar */
|
||||||
.board { display: flex; gap: 10px; padding: 12px; overflow-x: auto; flex: 1; min-height: 0; }
|
.board { display: flex; gap: 10px; padding: 12px; overflow-x: auto; flex: 1; min-height: 0; }
|
||||||
.spalte {
|
.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;
|
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; }
|
.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; }
|
.guide-section .ziel { color: var(--akzent); font-size: 13px; }
|
||||||
:is(.markdown, .kompakt) .katex-display { overflow-x: auto; overflow-y: hidden; padding: 4px 0; }
|
: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 {
|
.markdown blockquote {
|
||||||
margin: 12px 0; padding: 8px 14px; border-left: 3px solid var(--akzent);
|
margin: 12px 0; padding: 8px 14px; border-left: 3px solid var(--akzent);
|
||||||
background: var(--karte); border-radius: 0 6px 6px 0; color: var(--text);
|
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 "text"/"mathe": {{"text": "…"}}
|
||||||
- form "code": {{"sprache": "python", "code": "…"}}
|
- form "code": {{"sprache": "python", "code": "…"}}
|
||||||
- form "tabelle": {{"tabelle": "| … | … |\n|---|---|\n| … | … |"}}
|
- 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.
|
"text" als durchgerechneter Schritt-für-Schritt-Ablauf.
|
||||||
- [begriff] → NUR wenn abstrakt/schwer greifbar. Dann "text"/"mathe" (konkrete
|
- [begriff] → NUR wenn abstrakt/schwer greifbar. Dann "text"/"mathe" (konkrete
|
||||||
Instanz) oder "tabelle" (Fälle/Vergleich). Simpler, selbsterklärender Begriff → KEIN Beispiel.
|
Instanz) oder "tabelle" (Fälle/Vergleich). Simpler, selbsterklärender Begriff → KEIN Beispiel.
|
||||||
- [aussage] → NUR wenn nicht trivial. Dann "mathe"/"text" (Anwendung an Mini-Instanz)
|
- [aussage] → NUR wenn nicht trivial. Dann "mathe"/"text" (Anwendung an Mini-Instanz).
|
||||||
oder "diagramm" (Feld "spec": {{knoten:[…], kanten:[…]}}) für Automat/Ablauf/Baum.
|
|
||||||
Nur EINE Form. Im Zweifel WEGLASSEN — ein schwaches/redundantes Beispiel schadet
|
Nur EINE Form. Im Zweifel WEGLASSEN — ein schwaches/redundantes Beispiel schadet
|
||||||
(Forschung: die Auswahl ist kritisch). Code selbsterklärend, ohne I/O-Tricks.
|
(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
|
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
|
auf spätere Durchgänge. Unbekannte Begriffe beim ersten Auftreten kurz mit
|
||||||
Bekanntem einordnen, nie vertrösten („dazu später mehr" ist VERBOTEN).
|
Bekanntem einordnen, nie vertrösten („dazu später mehr" ist VERBOTEN).
|
||||||
(Code-/Tabellen-/Diagramm-Beispiele werden NACH dir automatisch beim passenden
|
(Code-/Tabellen-Beispiele werden NACH dir automatisch beim passenden Atom
|
||||||
Atom eingefügt — du musst KEINE Beispiel-Marker setzen. Text-Beispiele unter den
|
eingefügt — du musst KEINE Beispiel-Marker setzen. Text-Beispiele unter den
|
||||||
Atomen webst du wie gehabt in die Prosa ein.)
|
Atomen webst du wie gehabt in die Prosa ein.)
|
||||||
|
|
||||||
ATOME:
|
ATOME:
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ def test_tabelle_struktur_check():
|
|||||||
def test_referenziert_quelle_ignoriert_code():
|
def test_referenziert_quelle_ignoriert_code():
|
||||||
# „aufgabe 3" in Code/Kommentar darf NICHT als Quellen-Referenz matchen
|
# „aufgabe 3" in Code/Kommentar darf NICHT als Quellen-Referenz matchen
|
||||||
inhalt = {"form": "code", "text": "", "code": "# aufgabe 3: sum\nprint(sum([1,2]))",
|
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)
|
assert not artefakte._referenziert_quelle(inhalt)
|
||||||
# in der Prosa (text) matcht es weiterhin
|
# in der Prosa (text) matcht es weiterhin
|
||||||
assert artefakte._referenziert_quelle({"text": "siehe aufgabe 3", "frage": "", "antwort": ""})
|
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"
|
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():
|
async def test_guide_messen_toter_beispiel_marker():
|
||||||
import guide
|
import guide
|
||||||
topic = topic_anlegen("bspqa")
|
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")
|
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",
|
a = db.insert("atome", topic=topic, titel="A", typ="begriff", definition="d",
|
||||||
status="neu", baustein_id=b_id, braucht=db.j([]))
|
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,
|
db.insert("sections", baustein_id=b_id, stage="fix", text_lang=lang,
|
||||||
text_kompakt="- p", befunde=db.j(["Behebe (laenge): kürzen"]))
|
text_kompakt="- p", befunde=db.j(["Behebe (laenge): kürzen"]))
|
||||||
monkeypatch.setitem(fake_agents._HANDLER, "Guide-Fix",
|
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"
|
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):
|
async def test_fix_cap_friert_section_ein(monkeypatch):
|
||||||
"""Nach FIX_MAX_VERSUCHE erfolglosen Fixes (Text unverändert) wird die Section
|
"""Nach FIX_MAX_VERSUCHE erfolglosen Fixes (Text unverändert) wird die Section
|
||||||
für Stil-Befunde eingefroren → reparieren meldet bewegt=False (Stillstand)."""
|
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(
|
bewegt = await guide.reparieren(
|
||||||
ctx, [{"art": "det_check", "item": str(b), "detail": "Kürzen"}])
|
ctx, [{"art": "det_check", "item": str(b), "detail": "Kürzen"}])
|
||||||
assert bewegt is False # eingefroren → nicht geroutet → kein Fortschritt
|
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"
|
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)
|
# 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"], {})
|
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"]
|
assert resets["struktur"] == db.one("SELECT MAX(id) AS m FROM events")["m"]
|
||||||
run2 = await _lauf_komplett(topic)
|
run2 = await _lauf_komplett(topic)
|
||||||
_pruefe_endzustand(topic, run2)
|
_pruefe_endzustand(topic, run2)
|
||||||
|
|||||||
Reference in New Issue
Block a user