update
This commit is contained in:
@@ -1,15 +1,13 @@
|
||||
"""Ebene 3: Struktur. Backward Design: Lernziele aus den Atomen je Soll-Punkt
|
||||
(Zuordnung im selben Call — Lektion 52), dann wird Struktur RECHENBAR:
|
||||
Baustein-Schnitt = Partition der Ziel-Gruppen ins Größenband, Reihenfolge =
|
||||
topologische Sortierung des braucht-Graphen. Zyklen bricht ein Judge an der
|
||||
schwächsten Kante (deterministischer Fallback)."""
|
||||
(Zuordnung im selben Call — Lektion 52), Baustein-Schnitt = Partition der
|
||||
Ziel-Gruppen ins Größenband. Reihenfolge = Judge je Level (Dozenten-Blick)
|
||||
mit Quellpositions-Prior — der braucht-Graph war zu dünn (aak: 77 wirksame
|
||||
von 385 Kanten) und ordnete faktisch per Zufall; die Maschinerie ist raus."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
|
||||
import db
|
||||
import inventar
|
||||
import llm
|
||||
from config import (BAUSTEIN_MAX_ATOME, BAUSTEIN_MIN_ATOME, DURCHGANG,
|
||||
KAPITEL_BAUSTEINE, LEVEL_RANG, ZIELE_CHUNK_ATOME)
|
||||
@@ -24,11 +22,6 @@ def _atome(topic: str) -> list[dict]:
|
||||
" ('gemerged','verworfen') ORDER BY id", (topic,))
|
||||
|
||||
|
||||
def _braucht_kanten(topic: str) -> list[dict]:
|
||||
return db.query("SELECT * FROM kanten WHERE topic=? AND art='braucht' AND status='aktiv'",
|
||||
(topic,))
|
||||
|
||||
|
||||
# ── Lernziele ─────────────────────────────────────────────────────────────────
|
||||
|
||||
async def _ziele_bilden(ctx: llm.Kontext) -> None:
|
||||
@@ -78,119 +71,41 @@ async def _ziele_bilden(ctx: llm.Kontext) -> None:
|
||||
await llm.alle(eine_gruppe(s, g) for s, g in gruppen.items())
|
||||
|
||||
|
||||
# ── Zyklen brechen ────────────────────────────────────────────────────────────
|
||||
|
||||
def _finde_zyklus(knoten: list[int], kanten: list[tuple[int, int]]) -> list[tuple[int, int]] | None:
|
||||
"""Ein Zyklus als Kantenliste, oder None (iterative DFS mit Farben)."""
|
||||
nach = defaultdict(list)
|
||||
for v, z in kanten:
|
||||
nach[v].append(z)
|
||||
farbe = {k: 0 for k in knoten} # 0 weiß, 1 grau, 2 schwarz
|
||||
eltern: dict[int, int] = {}
|
||||
for start in knoten:
|
||||
if farbe[start]:
|
||||
continue
|
||||
stapel = [(start, iter(nach[start]))]
|
||||
farbe[start] = 1
|
||||
while stapel:
|
||||
v, it = stapel[-1]
|
||||
fortschritt = False
|
||||
for z in it:
|
||||
if z not in farbe:
|
||||
continue
|
||||
if farbe[z] == 0:
|
||||
farbe[z] = 1
|
||||
eltern[z] = v
|
||||
stapel.append((z, iter(nach[z])))
|
||||
fortschritt = True
|
||||
break
|
||||
if farbe[z] == 1: # Rückkante → Zyklus rekonstruieren
|
||||
pfad = [(v, z)]
|
||||
k = v
|
||||
while k != z:
|
||||
pfad.append((eltern[k], k))
|
||||
k = eltern[k]
|
||||
return pfad
|
||||
if not fortschritt:
|
||||
farbe[v] = 2
|
||||
stapel.pop()
|
||||
return None
|
||||
|
||||
|
||||
async def _zyklen_brechen(ctx: llm.Kontext) -> None:
|
||||
while True:
|
||||
atome = _atome(ctx.topic)
|
||||
ids = [a["id"] for a in atome]
|
||||
kanten = [(k["von_atom"], k["zu_atom"]) for k in _braucht_kanten(ctx.topic)
|
||||
if k["von_atom"] in ids and k["zu_atom"] in ids]
|
||||
zyklus = _finde_zyklus(ids, kanten)
|
||||
if not zyklus:
|
||||
return
|
||||
je_id = {a["id"]: a for a in atome}
|
||||
liste = "\n".join(f"{v}→{z}: {je_id[v]['titel']} braucht {je_id[z]['titel']}"
|
||||
for v, z in zyklus)
|
||||
res = await llm.call(ctx, stage="zyklus", template="Kanten-Zyklus",
|
||||
werte={"kanten": liste}, role="judge",
|
||||
item=f"z{zyklus[0][0]}", erwartet=dict)
|
||||
wahl = None
|
||||
if res:
|
||||
paar = (res.get("von"), res.get("zu"))
|
||||
if paar in zyklus:
|
||||
wahl = paar
|
||||
if wahl is None: # deterministischer Fallback: kleinste Kante
|
||||
wahl = min(zyklus)
|
||||
db.execute("UPDATE kanten SET status='gebrochen' WHERE topic=? AND von_atom=?"
|
||||
" AND zu_atom=? AND art='braucht'", (ctx.topic, wahl[0], wahl[1]))
|
||||
log.info("Zyklus gebrochen: %s→%s", wahl[0], wahl[1])
|
||||
|
||||
|
||||
# ── Bausteine schneiden + ordnen ──────────────────────────────────────────────
|
||||
|
||||
def _topo(knoten: list[int], kanten: list[tuple[int, int]],
|
||||
rang: dict[int, tuple]) -> list[int]:
|
||||
"""Kahn; kanten (v, z) = v braucht z ⇒ z kommt vor v. Ties nach rang (stabil).
|
||||
Kanten STRIKT auf die Knotenmenge filtern — Kanten zu fremden Atomen zogen
|
||||
sonst fremde Knoten in die Ausgabe und verdrängten Gruppenmitglieder
|
||||
(gemessen aak: 22 Atome ohne Baustein)."""
|
||||
kn = set(knoten)
|
||||
vorher = defaultdict(set)
|
||||
nachher = defaultdict(set)
|
||||
for v, z in kanten:
|
||||
if v in kn and z in kn:
|
||||
vorher[v].add(z)
|
||||
nachher[z].add(v)
|
||||
offen = sorted([k for k in knoten if not vorher[k]], key=lambda k: rang[k])
|
||||
out = []
|
||||
erledigt: set[int] = set()
|
||||
while offen:
|
||||
k = offen.pop(0)
|
||||
out.append(k)
|
||||
erledigt.add(k)
|
||||
neu = []
|
||||
for n in nachher[k]:
|
||||
if not (vorher[n] - erledigt) and n not in erledigt and n not in offen and n not in neu:
|
||||
neu.append(n)
|
||||
offen = sorted(offen + neu, key=lambda kk: rang[kk])
|
||||
out += sorted([k for k in knoten if k not in erledigt], key=lambda k: rang[k])
|
||||
return out
|
||||
|
||||
|
||||
def _anker_rang(topic: str) -> dict[int, tuple]:
|
||||
"""Natürliche Quellreihenfolge als Tie-Break: (quelle_id, start) des ersten Ankers."""
|
||||
"""Natürliche Quellreihenfolge: (quelle_id, start) des ersten Ankers,
|
||||
stoff-Quellen bevorzugt — Aufgabensammlungen ordnen nicht didaktisch."""
|
||||
out = {}
|
||||
for a in _atome(topic):
|
||||
row = db.one("SELECT quelle_id, start FROM anker WHERE atom_id=? AND start>=0"
|
||||
" ORDER BY quelle_id, start LIMIT 1", (a["id"],))
|
||||
row = db.one("SELECT k.quelle_id, k.start FROM anker k"
|
||||
" JOIN quellen q ON q.id=k.quelle_id"
|
||||
" WHERE k.atom_id=? AND k.start>=0"
|
||||
" ORDER BY (q.rolle != 'stoff'), k.quelle_id, k.start LIMIT 1",
|
||||
(a["id"],))
|
||||
out[a["id"]] = (row["quelle_id"], row["start"]) if row else (999_999, a["id"])
|
||||
return out
|
||||
|
||||
|
||||
def _bausteine_schneiden(ctx: llm.Kontext) -> None:
|
||||
def _baustein_rang(bausteine: list[dict], atome: list[dict],
|
||||
rang: dict[int, tuple]) -> dict[int, tuple]:
|
||||
"""Quellpositions-Prior je Baustein: MEDIAN der Atom-Ränge — robust gegen
|
||||
einzelne Merge-Importe mit fremdem Rang (aak: ein „Komplexitätstheorie"-Atom
|
||||
von Zeichen 464 zog den ETH-Baustein per min() an Position 0)."""
|
||||
b_rang = {}
|
||||
for b in bausteine:
|
||||
raenge = sorted(rang.get(a["id"], (9, 9)) for a in atome
|
||||
if a["baustein_id"] == b["id"])
|
||||
b_rang[b["id"]] = raenge[len(raenge) // 2] if raenge else (9, 9)
|
||||
return b_rang
|
||||
|
||||
|
||||
async def _bausteine_schneiden(ctx: llm.Kontext) -> None:
|
||||
"""Deterministisch, idempotent: alte Zuordnung verwerfen, neu schneiden.
|
||||
Gruppen sind (Ziel, Level) — je Level entstehen GETRENNTE Bausteine
|
||||
(E/M/S-Durchgänge). Kleine Gruppen level-intern mergen (das Atom wechselt
|
||||
sein Ziel, Baustein bleibt EIN Ziel + EIN Level), große entlang der
|
||||
Topo-Reihenfolge splitten ("Teil n")."""
|
||||
Quellreihenfolge splitten ("Teil n")."""
|
||||
topic = ctx.topic
|
||||
# Aufträge hängen an Baustein-IDs — beim Neuschnitt mitlöschen, sonst Waisen
|
||||
db.execute("DELETE FROM auftraege WHERE baustein_id IN"
|
||||
@@ -198,23 +113,14 @@ def _bausteine_schneiden(ctx: llm.Kontext) -> None:
|
||||
db.execute("DELETE FROM bausteine WHERE topic=?", (topic,))
|
||||
atome = _atome(topic)
|
||||
rang = _anker_rang(topic)
|
||||
kanten = [(k["von_atom"], k["zu_atom"]) for k in _braucht_kanten(topic)]
|
||||
|
||||
gruppen: dict = defaultdict(list)
|
||||
for a in atome:
|
||||
gruppen[(a["ziel_id"], a["level"])].append(a)
|
||||
|
||||
# kleine Gruppen mergen (kleinste zuerst), nur innerhalb des Levels.
|
||||
# Partnerwahl thematisch statt per Soll-Punkt — das Soll ist seit der
|
||||
# Entkopplung reine Checkliste (152 feine Punkte hätten sonst keine
|
||||
# Merge-Partner): meiste braucht-Kanten zwischen den Gruppen, sonst
|
||||
# Nachbar in der Quellreihenfolge.
|
||||
def kanten_score(g1: list[dict], g2: list[dict]) -> int:
|
||||
ids1 = {a["id"] for a in g1}
|
||||
ids2 = {a["id"] for a in g2}
|
||||
return sum(1 for v, z in kanten
|
||||
if (v in ids1 and z in ids2) or (v in ids2 and z in ids1))
|
||||
|
||||
# Partnerwahl über Quell-Nähe — das Soll ist seit der Entkopplung reine
|
||||
# Checkliste (152 feine Punkte hätten sonst keine Merge-Partner).
|
||||
def gruppen_rang(g: list[dict]) -> tuple:
|
||||
return min(rang[a["id"]] for a in g)
|
||||
|
||||
@@ -235,8 +141,7 @@ def _bausteine_schneiden(ctx: llm.Kontext) -> None:
|
||||
|
||||
def naehe(pp: tuple) -> tuple:
|
||||
r = gruppen_rang(gruppen[pp])
|
||||
return (-kanten_score(gruppen[k], gruppen[pp]),
|
||||
(abs(r[0] - eigener[0]), abs(r[1] - eigener[1])),
|
||||
return ((abs(r[0] - eigener[0]), abs(r[1] - eigener[1])),
|
||||
len(gruppen[pp]))
|
||||
|
||||
p = min(passende, key=naehe)
|
||||
@@ -252,7 +157,7 @@ def _bausteine_schneiden(ctx: llm.Kontext) -> None:
|
||||
# Bausteine anlegen (große Gruppen splitten), Atome zuordnen
|
||||
ziele = {z["id"]: z for z in db.query("SELECT * FROM lernziele WHERE topic=?", (topic,))}
|
||||
for (ziel_id, level), gruppe in gruppen.items():
|
||||
ordnung = _topo([a["id"] for a in gruppe], kanten, rang)
|
||||
ordnung = sorted((a["id"] for a in gruppe), key=lambda i: rang[i])
|
||||
z = ziele.get(ziel_id, {})
|
||||
titel = z.get("titel") or z.get("text", "Baustein") # Kurztitel; Zieltext nur Fallback
|
||||
n_teile = -(-len(gruppe) // BAUSTEIN_MAX_ATOME)
|
||||
@@ -273,55 +178,54 @@ def _bausteine_schneiden(ctx: llm.Kontext) -> None:
|
||||
for pos, atom_id in enumerate(teil_ids):
|
||||
db.update("atome", "id", atom_id, baustein_id=b_id, ord=pos)
|
||||
|
||||
_bausteine_ordnen(topic, kanten, rang)
|
||||
await _bausteine_ordnen(ctx)
|
||||
|
||||
|
||||
def _bausteine_ordnen(topic: str, kanten: list[tuple[int, int]], rang: dict) -> None:
|
||||
"""Baustein-DAG aus aggregierten Atom-Kanten, JE LEVEL getrennt geordnet;
|
||||
ord läuft global fortlaufend E→M→S (eine Achse — Vorwärts-Logik und Kapitel-
|
||||
Kontiguität bleiben level-blind korrekt). Cross-Level-Kanten zeigen nach der
|
||||
Konfliktlösung immer auf tiefere Level und sind durch die Blockordnung
|
||||
automatisch erfüllt. Zyklen deterministisch an der schwächsten Aggregat-Kante
|
||||
gebrochen (wenigste Atom-Kanten)."""
|
||||
async def _bausteine_ordnen(ctx: llm.Kontext) -> None:
|
||||
"""Didaktische Ordnung je Level per Judge (Dozenten-Blick); ord läuft global
|
||||
fortlaufend E→M→S (eine Achse — Vorwärts-Logik und Kapitel-Kontiguität
|
||||
bleiben level-blind korrekt). Eingabe ist nach Quellpositions-Prior
|
||||
vorsortiert (das Skript IST vom Dozenten geordnet); der Code erzwingt eine
|
||||
exakte Permutation — ungültig → 1 Retry → Prior-Ordnung mit Befund
|
||||
(ordnung_fallback, Muster wie kapitel_fallback)."""
|
||||
topic = ctx.topic
|
||||
bausteine = db.query("SELECT * FROM bausteine WHERE topic=?", (topic,))
|
||||
atome = _atome(topic)
|
||||
je_atom = {a["id"]: a["baustein_id"] for a in atome}
|
||||
b_level = {b["id"]: b["level"] for b in bausteine}
|
||||
gewicht: dict = defaultdict(int)
|
||||
for v, z in kanten: # v braucht z ⇒ Baustein(z) vor Baustein(v)
|
||||
bv, bz = je_atom.get(v), je_atom.get(z)
|
||||
if bv and bz and bv != bz and b_level[bv] == b_level[bz]:
|
||||
gewicht[(bv, bz)] += 1
|
||||
# Tie-Break: Quellposition des Bausteins (früheste Belegstelle); Kanten gewinnen
|
||||
# weiter. Kapitel entstehen erst NACH der Ordnung (Entkopplung vom Soll).
|
||||
b_rang = {}
|
||||
for b in bausteine:
|
||||
b_rang[b["id"]] = min((rang.get(a["id"], (9, 9)) for a in atome
|
||||
if a["baustein_id"] == b["id"]), default=(9, 9))
|
||||
b_rang = _baustein_rang(bausteine, atome, _anker_rang(topic))
|
||||
ziele = {z["id"]: z for z in db.query("SELECT * FROM lernziele WHERE topic=?", (topic,))}
|
||||
offset = 0
|
||||
for level in LEVEL_RANG:
|
||||
b_ids = [b["id"] for b in bausteine if b["level"] == level]
|
||||
if not b_ids:
|
||||
folge = sorted((b for b in bausteine if b["level"] == level),
|
||||
key=lambda b: b_rang[b["id"]])
|
||||
if not folge:
|
||||
continue
|
||||
b_kanten = {k for k in gewicht if b_level[k[0]] == level}
|
||||
while True:
|
||||
zyklus = _finde_zyklus(b_ids, list(b_kanten))
|
||||
if not zyklus:
|
||||
break
|
||||
schwach = min(zyklus, key=lambda k: (gewicht[k], k))
|
||||
b_kanten.discard(schwach)
|
||||
# Die Atom-Kanten hinter der geopferten Aggregat-Kante stilllegen — sonst
|
||||
# meldet die QA sie ewig als Vorwärts-Kante (Baustein-Zyklen kann keine
|
||||
# Ordnung vollständig rückwärts auflösen).
|
||||
bv, bz = schwach
|
||||
db.execute("UPDATE kanten SET status='geopfert' WHERE topic=? AND art='braucht'"
|
||||
" AND status='aktiv'"
|
||||
" AND von_atom IN (SELECT id FROM atome WHERE baustein_id=?)"
|
||||
" AND zu_atom IN (SELECT id FROM atome WHERE baustein_id=?)",
|
||||
(topic, bv, bz))
|
||||
for pos, b_id in enumerate(_topo(b_ids, list(b_kanten), b_rang)):
|
||||
db.update("bausteine", "id", b_id, ord=offset + pos)
|
||||
offset += len(b_ids)
|
||||
liste = "\n".join(f"{i + 1}: {b['titel']} — "
|
||||
f"{ziele.get(b['ziel_id'], {}).get('text', '')}"
|
||||
for i, b in enumerate(folge))
|
||||
soll = list(range(1, len(folge) + 1))
|
||||
|
||||
async def ordnen(hinweis: str, tag: str) -> list[int] | None:
|
||||
res = await llm.call(ctx, stage="ordnung", template="Baustein-Ordnung",
|
||||
role="judge", n=len(folge), item=tag, erwartet=list,
|
||||
werte={"bausteine": liste, "hinweis": hinweis,
|
||||
"durchgang": DURCHGANG[level]})
|
||||
perm = [x for x in res or [] if isinstance(x, int)]
|
||||
return perm if sorted(perm) == soll else None
|
||||
|
||||
perm = await ordnen("", f"ordnung-{level}")
|
||||
if perm is None:
|
||||
perm = await ordnen(
|
||||
"ACHTUNG: Der letzte Versuch war ungültig. Antworte mit GENAU"
|
||||
f" den Nummern 1–{len(folge)}, jede genau einmal.",
|
||||
f"ordnung-{level}-2")
|
||||
art = "judge"
|
||||
if perm is None: # Prior-Ordnung ist der ehrliche Fallback — mit Befund
|
||||
art = "fallback"
|
||||
perm = soll
|
||||
for pos, nr in enumerate(perm):
|
||||
db.update("bausteine", "id", folge[nr - 1]["id"],
|
||||
ord=offset + pos, ordnung=art)
|
||||
offset += len(folge)
|
||||
|
||||
|
||||
async def _level_kalibrieren(ctx: llm.Kontext) -> None:
|
||||
@@ -361,29 +265,6 @@ async def _level_kalibrieren(ctx: llm.Kontext) -> None:
|
||||
await llm.alle(einer(z, g) for z, g in gruppen.items())
|
||||
|
||||
|
||||
def _level_konflikte_loesen(topic: str) -> None:
|
||||
"""Deterministisch: Eine Voraussetzung darf nie über dem Level ihres Nutzers
|
||||
liegen — sonst fehlt dem Durchgang sein Fundament. Auflösung durch ABSENKEN
|
||||
der Voraussetzung (sie gehört ins Grundgerüst des Abhängigen); Anheben würde
|
||||
Kerninhalte aus dem E-Durchgang verdrängen. Fixpunkt: Level fallen monoton,
|
||||
terminiert nach max. 2 Stufen je Atom."""
|
||||
level = {a["id"]: a["level"] for a in _atome(topic)}
|
||||
kanten = [(k["von_atom"], k["zu_atom"]) for k in _braucht_kanten(topic)
|
||||
if k["von_atom"] in level and k["zu_atom"] in level]
|
||||
gesenkt = 0
|
||||
geaendert = True
|
||||
while geaendert:
|
||||
geaendert = False
|
||||
for v, z in kanten: # v braucht z ⇒ level(z) ≤ level(v)
|
||||
if LEVEL_RANG[level[z]] > LEVEL_RANG[level[v]]:
|
||||
level[z] = level[v]
|
||||
db.update("atome", "id", z, level=level[v])
|
||||
gesenkt += 1
|
||||
geaendert = True
|
||||
if gesenkt:
|
||||
log.info("Level-Konflikte: %d Voraussetzungen abgesenkt", gesenkt)
|
||||
|
||||
|
||||
async def _kapitel_bilden(ctx: llm.Kontext) -> None:
|
||||
"""Kapitel = kontiguierliche Segmente der geordneten Baustein-Folge, JE
|
||||
DURCHGANG (Level) geschnitten; kapitel.ord läuft global E→M→S weiter. Das
|
||||
@@ -469,12 +350,8 @@ async def _kapitel_bilden(ctx: llm.Kontext) -> None:
|
||||
async def bauen(ctx: llm.Kontext) -> None:
|
||||
ctx.ebene = EBENE
|
||||
await _ziele_bilden(ctx)
|
||||
await inventar._braucht_fallback(ctx) # Kompositum↔Expansion-braucht-Titel nachziehen,
|
||||
# bevor Zyklen/Level auf dem Graph rechnen
|
||||
await _zyklen_brechen(ctx)
|
||||
await _level_kalibrieren(ctx) # VOR dem Schnitt: Level bestimmt die Bausteine
|
||||
_level_konflikte_loesen(ctx.topic)
|
||||
_bausteine_schneiden(ctx)
|
||||
await _bausteine_schneiden(ctx)
|
||||
await _kapitel_bilden(ctx)
|
||||
|
||||
|
||||
@@ -503,13 +380,6 @@ def messen(ctx: llm.Kontext) -> list[dict]:
|
||||
befunde.append({"art": "level_mix", "item": str(a["id"]),
|
||||
"detail": f"Atom {a['level']} in Baustein {b['level']}"
|
||||
f" ({b['titel']})"})
|
||||
level_von = {a["id"]: a["level"] for a in atome}
|
||||
for k in _braucht_kanten(ctx.topic): # Voraussetzung nie über dem Nutzer-Level
|
||||
lv, lz = level_von.get(k["von_atom"]), level_von.get(k["zu_atom"])
|
||||
if lv and lz and LEVEL_RANG[lz] > LEVEL_RANG[lv]:
|
||||
befunde.append({"art": "level_konflikt", "item": str(k["id"]),
|
||||
"detail": f"Atom {k['von_atom']} ({lv}) braucht"
|
||||
f" {k['zu_atom']} ({lz})"})
|
||||
kaps = db.query("SELECT * FROM kapitel WHERE topic=?", (ctx.topic,))
|
||||
kap_ids = {k["id"] for k in kaps}
|
||||
kap_level = {k["id"]: k["level"] for k in kaps}
|
||||
@@ -541,18 +411,11 @@ def messen(ctx: llm.Kontext) -> list[dict]:
|
||||
if k["ord"] != letzte:
|
||||
befunde.append({"art": "kapitel_zerstueckelt", "item": str(k["id"]),
|
||||
"detail": k["titel"]})
|
||||
ord_von = {b["id"]: b["ord"] for b in bausteine}
|
||||
for k in _braucht_kanten(ctx.topic):
|
||||
bv = next((a["baustein_id"] for a in atome if a["id"] == k["von_atom"]), None)
|
||||
bz = next((a["baustein_id"] for a in atome if a["id"] == k["zu_atom"]), None)
|
||||
if bv and bz and bv != bz and ord_von.get(bz, 0) > ord_von.get(bv, 0):
|
||||
befunde.append({"art": "vorwaerts_kante", "item": str(k["id"]),
|
||||
"detail": f"Atom {k['von_atom']} braucht {k['zu_atom']} (später)"})
|
||||
ids = [a["id"] for a in atome]
|
||||
kanten = [(k["von_atom"], k["zu_atom"]) for k in _braucht_kanten(ctx.topic)
|
||||
if k["von_atom"] in ids and k["zu_atom"] in ids]
|
||||
if _finde_zyklus(ids, kanten):
|
||||
befunde.append({"art": "zyklus", "item": "", "detail": "braucht-Graph hat Zyklus"})
|
||||
# stiller Ordnungs-Fallback wäre unsichtbarer Qualitätsverlust (wie kapitel_fallback)
|
||||
for lv in sorted({b["level"] for b in bausteine if b["ordnung"] == "fallback"}):
|
||||
befunde.append({"art": "ordnung_fallback", "item": lv,
|
||||
"detail": f"Durchgang {lv}: Judge lieferte keine gültige"
|
||||
f" Permutation — Quellreihenfolge übernommen"})
|
||||
return befunde
|
||||
|
||||
|
||||
@@ -561,9 +424,7 @@ async def reparieren(ctx: llm.Kontext, befunde: list[dict]) -> bool:
|
||||
if not befunde:
|
||||
return False
|
||||
await _ziele_bilden(ctx) # fängt Partition-Lücken
|
||||
await _zyklen_brechen(ctx) # fängt Zyklen
|
||||
await _level_kalibrieren(ctx)
|
||||
_level_konflikte_loesen(ctx.topic) # fängt level_konflikt
|
||||
_bausteine_schneiden(ctx) # Neu-Schnitt fängt Band, Vorwärts-Kanten, level_mix
|
||||
await _bausteine_schneiden(ctx) # Neu-Schnitt fängt Band, level_mix, ordnung_fallback
|
||||
await _kapitel_bilden(ctx) # fängt Kapitel-Befunde
|
||||
return True
|
||||
|
||||
Reference in New Issue
Block a user