This commit is contained in:
team3
2026-07-12 16:13:50 +02:00
parent a284e03225
commit 31a42bbf1d
48 changed files with 3625 additions and 253 deletions

View File

@@ -74,7 +74,7 @@ async def _ziele_bilden(ctx: llm.Kontext) -> None:
for a in rest:
db.update("atome", "id", a["id"], ziel_id=ziel_id)
await asyncio.gather(*(eine_gruppe(s, g) for s, g in gruppen.items()))
await llm.alle(eine_gruppe(s, g) for s, g in gruppen.items())
# ── Zyklen brechen ────────────────────────────────────────────────────────────
@@ -220,8 +220,11 @@ def _bausteine_schneiden(ctx: llm.Kontext) -> None:
kleine = sorted([k for k, g in gruppen.items() if len(g) < BAUSTEIN_MIN_ATOME],
key=lambda k: len(gruppen[k]))
for k in kleine:
passende = [p for p in gruppen if p != k and p[1] == k[1]
and len(gruppen[p]) + len(gruppen[k]) <= BAUSTEIN_MAX_ATOME]
# nur Level-Gleichheit als Schranke — die MAX-Grenze fiel weg, weil der
# Split große Gruppen ohnehin bandkonform zerlegt. Sonst blieb eine
# kleine Gruppe ohne passenden Partner als Unter-Band-Baustein liegen,
# den messen dann als unreparierbaren band-Befund meldete.
passende = [p for p in gruppen if p != k and p[1] == k[1]]
if not passende:
continue
eigener = gruppen_rang(gruppen[k])
@@ -249,9 +252,15 @@ def _bausteine_schneiden(ctx: llm.Kontext) -> None:
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)
groesse = -(-len(gruppe) // n_teile)
# gleichverteilt statt ceil-Scheiben: der letzte Teil war sonst der Rest
# (43→8,8,8,8,8,3 verletzt das Band). divmod → 43→8,7,7,7,7,7; mit
# MIN≤MAX/2 liegt jeder Teil bei n_teile≥2 beweisbar im Band.
basis, rest_teile = divmod(len(gruppe), n_teile)
start = 0
for teil in range(n_teile):
teil_ids = ordnung[teil * groesse:(teil + 1) * groesse]
gr = basis + (1 if teil < rest_teile else 0)
teil_ids = ordnung[start:start + gr]
start += gr
if not teil_ids:
continue
b_titel = titel if n_teile == 1 else f"{titel} (Teil {teil + 1})"
@@ -473,14 +482,13 @@ def messen(ctx: llm.Kontext) -> list[dict]:
je_baustein: dict = defaultdict(list)
for a in atome:
je_baustein[a["baustein_id"]].append(a)
# Band-Unterschreitung nur, wo ein Merge MÖGLICH gewesen wäre (kombiniert
# ≤ Band) — dieselbe Bedingung wie im Schnitt; eine unerfüllbare Messlatte
# pendelt nur die Note (Lektion 75).
# Band-Unterschreitung nur, wo der Schnitt einen Merge-Partner HÄTTE (gleiches
# Level, anderer Baustein) — der Schnitt mergt jetzt ohne MAX-Schranke und
# re-splittet bandkonform, also ist genau das die erfüllbare Messlatte. Eine
# einsame Unter-Band-Gruppe ohne Level-Partner meldet nichts (Lektion 75).
for b in bausteine:
n = len(je_baustein[b["id"]])
partner = any(bb["id"] != b["id"] and bb["level"] == b["level"]
and len(je_baustein[bb["id"]]) + n <= BAUSTEIN_MAX_ATOME
for bb in bausteine)
partner = any(bb["id"] != b["id"] and bb["level"] == b["level"] for bb in bausteine)
if n > BAUSTEIN_MAX_ATOME or (n < BAUSTEIN_MIN_ATOME and partner):
befunde.append({"art": "band", "item": str(b["id"]),
"detail": f"{b['titel']}: {n} Atome"})