This commit is contained in:
team3
2026-06-27 00:58:46 +02:00
parent 2a2026c9ae
commit f018ca5048
44 changed files with 3070 additions and 379 deletions

View File

@@ -103,13 +103,15 @@ CREATE TABLE IF NOT EXISTS subbausteine (
nennungen INTEGER NOT NULL DEFAULT 1,
stufe TEXT,
relevanz TEXT,
fakten TEXT,
status TEXT NOT NULL DEFAULT 'kandidat',
updated_at TEXT NOT NULL,
PRIMARY KEY (topic, baustein_norm, sub_norm)
)
"""
# Ein Frage-Muster je (Baustein, Subbaustein, Typ).
# Ein Frage-Muster je (Baustein, Subbaustein). Schwierigkeit kommt erst bei der Prüfung
# aus dem Lerner-Niveau, nicht aus dem Muster — daher kein Typ-Kreuzprodukt mehr.
CREATE_FRAGE_MUSTER = """
CREATE TABLE IF NOT EXISTS frage_muster (
topic TEXT NOT NULL,
@@ -117,10 +119,9 @@ CREATE TABLE IF NOT EXISTS frage_muster (
sub_norm TEXT NOT NULL,
baustein TEXT NOT NULL,
sub_titel TEXT NOT NULL,
typ TEXT NOT NULL,
frage TEXT NOT NULL,
updated_at TEXT NOT NULL,
PRIMARY KEY (topic, baustein_norm, sub_norm, typ)
PRIMARY KEY (topic, baustein_norm, sub_norm)
)
"""
@@ -170,6 +171,33 @@ CREATE TABLE IF NOT EXISTS quelle (
)
"""
# Gliederung (Kapitel → Baustein-Nummern) je Thema als JSON. In der Bausteine-Phase erzeugt,
# vom Guide nur noch gelesen + je Format gefiltert. Format-agnostisch (alle Bausteine).
CREATE_GUIDE_GLIEDERUNG = """
CREATE TABLE IF NOT EXISTS guide_gliederung (
topic TEXT PRIMARY KEY,
json TEXT NOT NULL,
updated_at TEXT NOT NULL
)
"""
# Generischer Lern-Artefakt-Layer: je (Baustein, Subbaustein, Typ) ein Artefakt (JSON in `daten`).
# typ: karteikarte | beispiel | diagramm. sub_norm='' = Baustein-Ebene (z. B. Diagramm).
# In der Bausteine-Phase aus den Fakten erzeugt, vom Frontend präsentiert (Trennung Artefakt/Anzeige).
CREATE_SUB_ARTEFAKTE = """
CREATE TABLE IF NOT EXISTS sub_artefakte (
topic TEXT NOT NULL,
baustein_norm TEXT NOT NULL,
sub_norm TEXT NOT NULL DEFAULT '',
typ TEXT NOT NULL,
baustein TEXT NOT NULL DEFAULT '',
sub_titel TEXT NOT NULL DEFAULT '',
daten TEXT NOT NULL DEFAULT '{}',
updated_at TEXT NOT NULL,
PRIMARY KEY (topic, baustein_norm, sub_norm, typ)
)
"""
_db: aiosqlite.Connection | None = None
@@ -199,6 +227,8 @@ async def init_db():
await db.execute(CREATE_PIPELINE_STATE)
await db.execute(CREATE_GUIDE_CONTENT)
await db.execute(CREATE_QUELLE)
await db.execute(CREATE_GUIDE_GLIEDERUNG)
await db.execute(CREATE_SUB_ARTEFAKTE)
try: # Migration für Bestands-DBs ohne step-Spalte
await db.execute("ALTER TABLE guides ADD COLUMN step INTEGER")
except aiosqlite.OperationalError:
@@ -226,6 +256,16 @@ async def init_db():
await db.execute(f"ALTER TABLE baustein_progress ADD COLUMN {_spalte} {_typ}")
except aiosqlite.OperationalError:
pass
# Migration: frage_muster ohne typ-Spalte (1 Muster je Sub statt Sub×Typ-Kreuzprodukt).
# PK-Änderung → Tabelle einmalig neu bauen. Bestands-Muster gehen verloren (bewusst, kein Mapping).
cursor = await db.execute("PRAGMA table_info(frage_muster)")
if any(_r[1] == "typ" for _r in await cursor.fetchall()):
await db.execute("DROP TABLE frage_muster")
await db.execute(CREATE_FRAGE_MUSTER)
try: # Migration: subbausteine.fakten (Quell-Fakten je Sub, JSON-Blob) — Extract-once-Grounding.
await db.execute("ALTER TABLE subbausteine ADD COLUMN fakten TEXT") # nullable: COALESCE schützt Bestand
except aiosqlite.OperationalError:
pass
# Migration: alte vertiefungen-Tabelle → baustein_texte (Bestand = lange Form, art 'deepdive')
cursor = await db.execute("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'vertiefungen'")
if await cursor.fetchone():
@@ -482,6 +522,21 @@ async def set_baustein_score(topic: str, baustein: str, score: int) -> int:
return score
async def set_baustein_score_and_streak(topic: str, baustein: str, score: int, streak: int) -> tuple[int, int]:
"""Setzt Score + Streak atomar (vom Aufrufer geclampt). Liefert (score, streak)."""
db = await get_db()
await db.execute(
"""INSERT INTO baustein_progress (topic, baustein, gute_antworten, streak, updated_at)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT(topic, baustein) DO UPDATE SET
gute_antworten = excluded.gute_antworten, streak = excluded.streak,
updated_at = excluded.updated_at""",
(topic, baustein, score, streak, _now()),
)
await db.commit()
return score, streak
async def delete_baustein_progress(topic: str, baustein: str) -> None:
"""Fortschritt EINES Bausteins zurücksetzen: Zeile löschen (Score/Streak/Flags/offene Frage).
Fehlt die Zeile, liefert get_baustein_progress Defaults (0) — also voller Reset."""
@@ -574,6 +629,75 @@ async def subs_count_alle() -> dict[tuple[str, str], int]:
return {(t, bn): n for t, bn, n in await cursor.fetchall()}
# Sub-Ebene aus den zwei orthogonalen Spalten: rand → 4 (V), sonst Stufe (Lernpfad-Position):
# anfaenger/NULL → 1, fortgeschritten → 2, experte → 3. Alte Werte (einfach/mittel/schwer) werden
# abwärtskompatibel mitgemappt. Steuert Guide-Ansicht A/F/E/V + cap (freigeschaltete Subs × 25).
_EBENE_CASE = """CASE
WHEN relevanz = 'rand' THEN 4
WHEN stufe IN ('fortgeschritten', 'mittel') THEN 2
WHEN stufe IN ('experte', 'schwer') THEN 3
ELSE 1
END"""
def _leere_ebenen() -> dict[int, int]:
return {1: 0, 2: 0, 3: 0, 4: 0}
async def subs_je_ebene(topic: str, baustein: str) -> dict[int, int]:
"""Konsens-Subbausteine eines Bausteins je Ebene 14. roher Baustein-Titel rein."""
from textkit import _norm_titel
db = await get_db()
cursor = await db.execute(
f"SELECT {_EBENE_CASE} AS ebene, COUNT(*) FROM subbausteine "
"WHERE topic = ? AND baustein_norm = ? AND status = 'konsens' GROUP BY ebene",
(topic, _norm_titel(baustein)),
)
out = _leere_ebenen()
for ebene, n in await cursor.fetchall():
out[ebene] = n
return out
async def subs_je_ebene_roh(topic: str) -> dict[str, dict[int, int]]:
"""Subbausteine je Ebene, gruppiert nach ROHEM Baustein-Titel (= Guide-Section-Titel)."""
db = await get_db()
cursor = await db.execute(
f"SELECT baustein, {_EBENE_CASE} AS ebene, COUNT(*) FROM subbausteine "
"WHERE topic = ? AND status = 'konsens' GROUP BY baustein, ebene",
(topic,),
)
out: dict[str, dict[int, int]] = {}
for b, ebene, n in await cursor.fetchall():
out.setdefault(b, _leere_ebenen())[ebene] = n
return out
async def subs_je_ebene_alle() -> dict[tuple[str, str], dict[int, int]]:
"""Subbausteine je Ebene je (topic, baustein_norm) — für die themenweite Stufen-Ableitung."""
db = await get_db()
cursor = await db.execute(
f"SELECT topic, baustein_norm, {_EBENE_CASE} AS ebene, COUNT(*) FROM subbausteine "
"WHERE status = 'konsens' GROUP BY topic, baustein_norm, ebene"
)
out: dict[tuple[str, str], dict[int, int]] = {}
for t, bn, ebene, n in await cursor.fetchall():
out.setdefault((t, bn), _leere_ebenen())[ebene] = n
return out
async def subs_mit_ebene(topic: str, baustein: str) -> list[dict]:
"""Konsens-Subbausteine eines Bausteins mit Titel, sub_norm und Ebene 14."""
from textkit import _norm_titel
db = await get_db()
cursor = await db.execute(
f"SELECT sub_titel, sub_norm, {_EBENE_CASE} AS ebene FROM subbausteine "
"WHERE topic = ? AND baustein_norm = ? AND status = 'konsens'",
(topic, _norm_titel(baustein)),
)
return [{"titel": t, "norm": sn, "ebene": e} for t, sn, e in await cursor.fetchall()]
async def list_baustein_scores_all() -> list[tuple[str, str, int]]:
"""(topic, baustein, gute_antworten) je Baustein — Rohdaten für die Stufen-Ableitung."""
db = await get_db()
@@ -653,19 +777,21 @@ async def upsert_subbaustein(topic: str, baustein_norm: str, sub_norm: str, baus
async def put_subbaustein(topic: str, baustein_norm: str, sub_norm: str, baustein: str, sub_titel: str,
stufe: str | None = None, relevanz: str | None = None, status: str = "konsens") -> None:
"""Insert/Update OHNE Nennungszähler (Spiegel aus dem Sidecar). stufe/relevanz nur überschreiben,
wenn ein neuer Wert übergeben wird (COALESCE schützt Bestehendes)."""
stufe: str | None = None, relevanz: str | None = None,
fakten: str | None = None, status: str = "konsens") -> None:
"""Insert/Update OHNE Nennungszähler (Spiegel aus dem Sidecar). stufe/relevanz/fakten nur
überschreiben, wenn ein neuer Wert übergeben wird (COALESCE schützt Bestehendes)."""
db = await get_db()
await db.execute(
"""INSERT INTO subbausteine (topic, baustein_norm, sub_norm, baustein, sub_titel, nennungen, stufe, relevanz, status, updated_at)
VALUES (?, ?, ?, ?, ?, 1, ?, ?, ?, ?)
"""INSERT INTO subbausteine (topic, baustein_norm, sub_norm, baustein, sub_titel, nennungen, stufe, relevanz, fakten, status, updated_at)
VALUES (?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?)
ON CONFLICT(topic, baustein_norm, sub_norm) DO UPDATE SET
baustein = excluded.baustein, sub_titel = excluded.sub_titel,
stufe = COALESCE(excluded.stufe, subbausteine.stufe),
relevanz = COALESCE(excluded.relevanz, subbausteine.relevanz),
fakten = COALESCE(excluded.fakten, subbausteine.fakten),
status = excluded.status, updated_at = excluded.updated_at""",
(topic, baustein_norm, sub_norm, baustein, sub_titel, stufe, relevanz, status, _now()),
(topic, baustein_norm, sub_norm, baustein, sub_titel, stufe, relevanz, fakten, status, _now()),
)
await db.commit()
@@ -700,14 +826,14 @@ async def delete_subbausteine(topic: str) -> None:
await db.commit()
async def upsert_frage_muster(topic: str, baustein_norm: str, sub_norm: str, baustein: str, sub_titel: str, typ: str, frage: str) -> None:
async def upsert_frage_muster(topic: str, baustein_norm: str, sub_norm: str, baustein: str, sub_titel: str, frage: str) -> None:
db = await get_db()
await db.execute(
"""INSERT INTO frage_muster (topic, baustein_norm, sub_norm, baustein, sub_titel, typ, frage, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(topic, baustein_norm, sub_norm, typ) DO UPDATE SET
frage = excluded.frage, updated_at = excluded.updated_at""",
(topic, baustein_norm, sub_norm, baustein, sub_titel, typ, frage, _now()),
"""INSERT INTO frage_muster (topic, baustein_norm, sub_norm, baustein, sub_titel, frage, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(topic, baustein_norm, sub_norm) DO UPDATE SET
sub_titel = excluded.sub_titel, frage = excluded.frage, updated_at = excluded.updated_at""",
(topic, baustein_norm, sub_norm, baustein, sub_titel, frage, _now()),
)
await db.commit()
@@ -849,10 +975,90 @@ async def delete_guide_content(topic: str, format: str | None = None) -> None:
await db.commit()
async def set_gliederung(topic: str, gliederung_json: str) -> None:
"""Gliederung (Kapitel→Nummern, JSON) je Thema speichern — Bausteine-Artefakt für den Guide."""
db = await get_db()
await db.execute(
"""INSERT INTO guide_gliederung (topic, json, updated_at) VALUES (?, ?, ?)
ON CONFLICT(topic) DO UPDATE SET json = excluded.json, updated_at = excluded.updated_at""",
(topic, gliederung_json, _now()),
)
await db.commit()
async def get_gliederung(topic: str) -> str | None:
db = await get_db()
cursor = await db.execute("SELECT json FROM guide_gliederung WHERE topic = ?", (topic,))
row = await cursor.fetchone()
return row[0] if row else None
async def delete_gliederung(topic: str) -> None:
db = await get_db()
await db.execute("DELETE FROM guide_gliederung WHERE topic = ?", (topic,))
await db.commit()
async def put_sub_artefakt(topic: str, baustein_norm: str, sub_norm: str, typ: str,
daten: str, baustein: str = "", sub_titel: str = "") -> None:
"""Ein Lern-Artefakt (karteikarte/beispiel/diagramm) als JSON in `daten` speichern."""
db = await get_db()
await db.execute(
"""INSERT INTO sub_artefakte (topic, baustein_norm, sub_norm, typ, baustein, sub_titel, daten, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(topic, baustein_norm, sub_norm, typ) DO UPDATE SET
baustein = excluded.baustein, sub_titel = excluded.sub_titel,
daten = excluded.daten, updated_at = excluded.updated_at""",
(topic, baustein_norm, sub_norm, typ, baustein, sub_titel, daten, _now()),
)
await db.commit()
async def get_sub_artefakte(topic: str, typ: str | None = None) -> list[dict]:
db = await get_db()
if typ is None:
cursor = await db.execute("SELECT * FROM sub_artefakte WHERE topic = ? ORDER BY rowid", (topic,))
else:
cursor = await db.execute(
"SELECT * FROM sub_artefakte WHERE topic = ? AND typ = ? ORDER BY rowid", (topic, typ)
)
rows = await cursor.fetchall()
return [_row_to_dict(row, cursor) for row in rows]
async def delete_sub_artefakte(topic: str) -> None:
db = await get_db()
await db.execute("DELETE FROM sub_artefakte WHERE topic = ?", (topic,))
await db.commit()
async def get_baustein_huerden(topic: str, baustein_norm: str) -> list[str]:
"""Typische Irrtümer (huerden) aller Subs eines Bausteins — als Distraktor-Pool fürs Quiz.
Liest aus subbausteine.fakten (JSON-Blob); leere/fehlende werden übersprungen."""
db = await get_db()
cursor = await db.execute(
"SELECT fakten FROM subbausteine WHERE topic = ? AND baustein_norm = ?", (topic, baustein_norm)
)
rows = await cursor.fetchall()
huerden = []
for (fakten,) in rows:
if not fakten:
continue
try:
fk = json.loads(fakten)
except (ValueError, TypeError):
continue
h = (fk.get("huerden") or "").strip() if isinstance(fk, dict) else ""
if h:
huerden.append(h)
return huerden
async def delete_topic_pipeline(topic: str) -> None:
"""Bausteine-Bereich eines Themas verwerfen (Inventar/Subs/Muster/Coverage/State).
"""Bausteine-Bereich eines Themas verwerfen (Inventar/Subs/Muster/Coverage/State/Artefakte).
NICHT die Themen-Config `quelle` — die wird separat verwaltet (delete_quelle)."""
db = await get_db()
for tab in ("bausteine", "subbausteine", "frage_muster", "recherche_coverage", "pipeline_state"):
for tab in ("bausteine", "subbausteine", "frage_muster", "recherche_coverage",
"pipeline_state", "guide_gliederung", "sub_artefakte"):
await db.execute(f"DELETE FROM {tab} WHERE topic = ?", (topic,))
await db.commit()