diff --git a/backend/database.py b/backend/database.py index 7bb0d71..4538a0d 100644 --- a/backend/database.py +++ b/backend/database.py @@ -111,6 +111,13 @@ async def init_db(): await db.execute("ALTER TABLE baustein_progress ADD COLUMN streak INTEGER NOT NULL DEFAULT 0") except aiosqlite.OperationalError: pass + # Offene-Frage-Anker: Basis/Streak VOR der aktuell offenen Frage — macht die Bewertung + # serverseitig idempotent (Re-Bewertung) und driftfrei über Fragen hinweg. + for _spalte, _typ in (("offene_frage", "TEXT"), ("offene_basis", "INTEGER"), ("offene_streak", "INTEGER")): + try: + await db.execute(f"ALTER TABLE baustein_progress ADD COLUMN {_spalte} {_typ}") + 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(): @@ -321,6 +328,38 @@ async def list_baustein_progress(topic: str) -> list[dict]: return [{"baustein": b, "gute_antworten": n, "streak": s, "absolviert": a, "verstanden": v, "gemeistert": m} for b, n, s, a, v, m in rows] +async def get_baustein_progress(topic: str, baustein: str) -> dict: + """Eine Baustein-Zeile inkl. Offene-Frage-Anker. Defaults, falls noch keine existiert.""" + db = await get_db() + cursor = await db.execute( + "SELECT gute_antworten, streak, absolviert, verstanden, gemeistert, " + "offene_frage, offene_basis, offene_streak FROM baustein_progress " + "WHERE topic = ? AND baustein = ?", + (topic, baustein), + ) + row = await cursor.fetchone() + if row is None: + return {"gute_antworten": 0, "streak": 0, "absolviert": None, "verstanden": None, + "gemeistert": None, "offene_frage": None, "offene_basis": None, "offene_streak": None} + return {"gute_antworten": row[0], "streak": row[1], "absolviert": row[2], "verstanden": row[3], + "gemeistert": row[4], "offene_frage": row[5], "offene_basis": row[6], "offene_streak": row[7]} + + +async def set_offene_frage(topic: str, baustein: str, frage: str, basis: int, streak: int) -> None: + """Friert Basis + Streak VOR der jetzt offenen Frage ein (Anker für idempotente Re-Bewertung).""" + db = await get_db() + now = _now() + await db.execute( + """INSERT INTO baustein_progress (topic, baustein, offene_frage, offene_basis, offene_streak, updated_at) + VALUES (?, ?, ?, ?, ?, ?) + ON CONFLICT(topic, baustein) DO UPDATE SET + offene_frage = excluded.offene_frage, offene_basis = excluded.offene_basis, + offene_streak = excluded.offene_streak, updated_at = excluded.updated_at""", + (topic, baustein, frage, basis, streak, now), + ) + await db.commit() + + async def set_baustein_score(topic: str, baustein: str, score: int) -> int: """Setzt den Score absolut (vom Aufrufer geclampt) und liefert ihn zurück.""" db = await get_db() diff --git a/backend/lernen.py b/backend/lernen.py index c0fa38f..d5eaad3 100644 --- a/backend/lernen.py +++ b/backend/lernen.py @@ -173,7 +173,7 @@ def _doppelfrage_mangel(frage: str) -> str | None: async def _frage_mit_kritik( topic: str, baustein: str, section_block: str, kompakt_block: str, - transcript: str, provider: str, + transcript: str, vermeide_block: str, provider: str, ) -> str | None: """Frage generieren, vom Kritiker prüfen lassen, bei Mängeln neu (max KRITIK_MAX_RUNDEN).""" kritik_block = "(keine)" @@ -182,7 +182,8 @@ async def _frage_mit_kritik( data = await _gen_call( "Baustein-Frage", "fast", _frage_schema, provider, topic=topic, baustein=baustein, section_block=section_block, - kompakt_block=kompakt_block, transcript=transcript, kritik_block=kritik_block, + kompakt_block=kompakt_block, transcript=transcript, vermeide_block=vermeide_block, + kritik_block=kritik_block, ) if data is None: return None @@ -190,7 +191,7 @@ async def _frage_mit_kritik( probleme = await _kritik_call( "Baustein-Frage-Kritik", provider, role="guide", # starke KI prüft die Regeln topic=topic, baustein=baustein, section_block=section_block, - kompakt_block=kompakt_block, transcript=transcript, frage=frage, + kompakt_block=kompakt_block, transcript=transcript, vermeide_block=vermeide_block, frage=frage, ) hart = _doppelfrage_mangel(frage) # erzwingt Neugenerierung, auch wenn der KI-Kritiker es übersah if hart: @@ -242,15 +243,22 @@ def _bloecke(section: str, kompakt: str | None) -> tuple[str, str]: ) +def _vermeide_block(vermeide: list[str] | None) -> str: + eintraege = [f.strip() for f in (vermeide or []) if f and f.strip()] + return "\n".join(f"- {f}" for f in eintraege) or "(keine)" + + async def pruefung_frage( topic: str, baustein: str, section: str, kompakt: str | None, - messages: list[dict], provider: str = DEFAULT_PROVIDER, + messages: list[dict], vermeide: list[str] | None = None, provider: str = DEFAULT_PROVIDER, ) -> str | None: """Aktion 'frage': nächste Frage generieren (Generator + Kritiker) · None bei Fehler.""" try: section_block, kompakt_block = _bloecke(section, kompakt) transcript = _transcript(messages) if messages else "(leer)" - return await _frage_mit_kritik(topic, baustein, section_block, kompakt_block, transcript, provider) + return await _frage_mit_kritik( + topic, baustein, section_block, kompakt_block, transcript, _vermeide_block(vermeide), provider, + ) except Exception: log.warning("[%s] Frage fehlgeschlagen (%s)", topic, baustein, exc_info=True) return None diff --git a/backend/models.py b/backend/models.py index 1899120..4cc7209 100644 --- a/backend/models.py +++ b/backend/models.py @@ -215,10 +215,10 @@ class BausteinPruefungRequest(BaseModel): section: str = Field(default="", max_length=20000) # ausführliche Fassung section_kompakt: str = Field(default="", max_length=20000) # kompakte Fassung (Merksätze) aktion: Literal["frage", "diskussion", "antwort", "antwort_pruefen"] = "frage" - frage: str = Field(default="", max_length=2000) # aktuell geprüfte Frage (für diskussion/antwort) + frage: str = Field(default="", max_length=2000) # aktuell geprüfte Frage (für diskussion/antwort); Anker der Basis letzte_bewertung: str = Field(default="", max_length=2000) # Feedback der letzten Bewertung (Kontext für diskussion) - score_vor_frage: int = 0 # Score, als die Frage gestellt wurde → driftfreies (Re-)Bewerten - streak: int = 0 # Folge korrekter Antworten VOR dieser Frage (Streak-Bonus) + vermeide: list[str] = [] # schon gestellte + vorgemerkte Fragen — sinngemäß nicht wiederholen + # Basis/Streak werden serverseitig je Frage geführt (offene_frage-Anker) — nicht mehr vom Client. tier2: bool = False # ganzer Guide absolviert (alle ≥3) → −1 bei falsch, Deckel 10 tier3: bool = False # ganzer Guide verstanden (alle ≥10) → Meisterpfad, −2 bei falsch, Deckel 25 messages: list[ChatMessage] = [] # Dialog bisher; leer = erste Frage diff --git a/backend/routes.py b/backend/routes.py index c13af01..263a1fe 100644 --- a/backend/routes.py +++ b/backend/routes.py @@ -13,7 +13,8 @@ from database import ( create_topic, list_topics as db_list_topics, delete_topic, list_progress, set_progress, delete_progress, create_element, list_elements, get_element, update_element, delete_element, - list_baustein_progress, set_baustein_score, set_baustein_streak, set_baustein_absolviert, set_baustein_verstanden, set_baustein_gemeistert, delete_baustein_daten, + list_baustein_progress, get_baustein_progress, set_offene_frage, + set_baustein_score, set_baustein_streak, set_baustein_absolviert, set_baustein_verstanden, set_baustein_gemeistert, delete_baustein_daten, ) from bausteine import generate_bausteine, cancel_bausteine, bausteine_status, active_bausteine, reset_bausteine, lade_quelle, lade_uebersicht from elements import generate_element, chat_with_guide, chat_with_element, check_element, style_element, refine_suggestion @@ -233,12 +234,32 @@ async def baustein_chat_route(req: BausteinChatRequest): return {"reply": reply} +# Bewertungen je (topic, baustein) serialisieren — sonst überschreiben zwei +# gleichzeitige Bewertungen den absoluten Score mit veralteter Basis (Race). +_pruef_locks: dict[tuple[str, str], asyncio.Lock] = {} + + +def _pruef_lock(topic: str, baustein: str) -> asyncio.Lock: + key = (topic, baustein) + lock = _pruef_locks.get(key) + if lock is None: + lock = _pruef_locks[key] = asyncio.Lock() + return lock + + +def _basis(stand: dict, frage: str) -> tuple[int, int, bool]: + """Score-/Streak-Basis VOR der Frage. Gleiche offene Frage → Re-Bewertung auf derselben + Basis (idempotent); sonst neue Frage auf dem aktuellen Stand. → (basis, streak_basis, re_bewertung).""" + re_bewertung = stand["offene_frage"] == frage and stand["offene_basis"] is not None + if re_bewertung: + streak_basis = stand["offene_streak"] if stand["offene_streak"] is not None else stand["streak"] + return stand["offene_basis"], streak_basis, True + return stand["gute_antworten"], stand["streak"], False + + @router.post("/bausteine/pruefung", response_model=BausteinPruefungResponse) async def baustein_pruefung_route(req: BausteinPruefungRequest): - stand = next( - (p for p in await list_baustein_progress(req.topic) if p["baustein"] == req.baustein), - {"gute_antworten": 0, "absolviert": None, "verstanden": None, "gemeistert": None}, - ) + stand = await get_baustein_progress(req.topic, req.baustein) gute = stand["gute_antworten"] absolviert = stand["absolviert"] is not None verstanden = stand["verstanden"] is not None @@ -247,7 +268,7 @@ async def baustein_pruefung_route(req: BausteinPruefungRequest): msgs = [m.model_dump() for m in req.messages] if req.aktion == "frage": - frage = await pruefung_frage(req.topic, req.baustein, req.section, kompakt, msgs, provider=req.provider) + frage = await pruefung_frage(req.topic, req.baustein, req.section, kompakt, msgs, vermeide=req.vermeide, provider=req.provider) if frage is None: raise HTTPException(502, "Frage fehlgeschlagen — bitte erneut versuchen") return {"frage": frage, "gute_antworten": gute, "absolviert": absolviert, "verstanden": verstanden, "gemeistert": gemeistert} @@ -271,47 +292,58 @@ async def baustein_pruefung_route(req: BausteinPruefungRequest): raise HTTPException(400, "Antwort braucht eine laufende Frage") if req.aktion == "antwort": - # Vorschau: nur Bewerter, kein Kritiker, KEIN Persist, KEINE Meilensteine. + # Vorschau: nur Bewerter, kein Kritiker, KEIN Persist. Basis serverseitig (best effort). data = await pruefung_bewertung_schnell( req.topic, req.baustein, req.section, kompakt, req.frage, msgs, gute, provider=req.provider, ) if data is None: raise HTTPException(502, "Bewertung fehlgeschlagen — bitte erneut versuchen") - score = score_berechnen(req.score_vor_frage, data["bewertung"] == "gut", req.streak, req.tier2, req.tier3, absolviert, verstanden, gemeistert) + basis, streak_basis, _ = _basis(stand, req.frage) + score = score_berechnen(basis, data["bewertung"] == "gut", streak_basis, req.tier2, req.tier3, absolviert, verstanden, gemeistert) return {"feedback": data["feedback"], "bewertung": data["bewertung"], "gute_antworten": score, "absolviert": absolviert, "verstanden": verstanden, "gemeistert": gemeistert} - # aktion == "antwort_pruefen": verbindlich (Bewerter + Kritiker), persistiert Score + Meilensteine. - data = await pruefung_bewertung( - req.topic, req.baustein, req.section, kompakt, req.frage, msgs, gute, provider=req.provider, - role="guide" if req.gruendlich else "judge", - ) - if data is None: - raise HTTPException(502, "Bewertung fehlgeschlagen — bitte erneut versuchen") + # aktion == "antwort_pruefen": verbindlich, persistiert. Serialisiert je Baustein, damit + # die Basis immer den geprüften Vorgänger-Stand sieht (kein Stale-Overwrite). + async with _pruef_lock(req.topic, req.baustein): + stand = await get_baustein_progress(req.topic, req.baustein) # nach evtl. Vorgänger-Persist neu lesen + absolviert = stand["absolviert"] is not None + verstanden = stand["verstanden"] is not None + gemeistert = stand["gemeistert"] is not None + basis, streak_basis, re_bewertung = _basis(stand, req.frage) + if not re_bewertung: + # Neue Frage: Basis/Streak einfrieren (Anker für spätere Re-Bewertung dieser Frage). + await set_offene_frage(req.topic, req.baustein, req.frage, basis, streak_basis) - # Score driftfrei aus dem Basis-Score rechnen (Re-Bewertung ersetzt das vorige Ergebnis). - score = score_berechnen( - req.score_vor_frage, data["bewertung"] == "gut", req.streak, req.tier2, req.tier3, absolviert, verstanden, gemeistert, - ) - war_absolviert, war_verstanden = absolviert, verstanden - gute = await set_baustein_score(req.topic, req.baustein, score) - if score >= NOETIG and not absolviert: - absolviert = True - if await set_baustein_absolviert(req.topic, req.baustein): - asyncio.create_task(baustein_element_anlegen(req.topic, req.baustein, req.section, req.provider)) - if score >= MASTERY and not verstanden: - await set_baustein_verstanden(req.topic, req.baustein) - verstanden = True - if score >= MEISTERN and not gemeistert: - await set_baustein_gemeistert(req.topic, req.baustein) - gemeistert = True - # Streak persistieren — Reset nur am Deckel der gerade aktiven Stufe. - streak = streak_berechnen( - req.streak, data["bewertung"] == "gut", req.tier2, req.tier3, - absolviert and not war_absolviert, verstanden and not war_verstanden, - ) - await set_baustein_streak(req.topic, req.baustein, streak) - return {"feedback": data["feedback"], "bewertung": data["bewertung"], "gute_antworten": gute, "streak": streak, "absolviert": absolviert, "verstanden": verstanden, "gemeistert": gemeistert} + data = await pruefung_bewertung( + req.topic, req.baustein, req.section, kompakt, req.frage, msgs, basis, provider=req.provider, + role="guide" if req.gruendlich else "judge", + ) + if data is None: + raise HTTPException(502, "Bewertung fehlgeschlagen — bitte erneut versuchen") + + score = score_berechnen( + basis, data["bewertung"] == "gut", streak_basis, req.tier2, req.tier3, absolviert, verstanden, gemeistert, + ) + war_absolviert, war_verstanden = absolviert, verstanden + gute = await set_baustein_score(req.topic, req.baustein, score) + if score >= NOETIG and not absolviert: + absolviert = True + if await set_baustein_absolviert(req.topic, req.baustein): + asyncio.create_task(baustein_element_anlegen(req.topic, req.baustein, req.section, req.provider)) + if score >= MASTERY and not verstanden: + await set_baustein_verstanden(req.topic, req.baustein) + verstanden = True + if score >= MEISTERN and not gemeistert: + await set_baustein_gemeistert(req.topic, req.baustein) + gemeistert = True + # Streak persistieren — Reset nur am Deckel der gerade aktiven Stufe. + streak = streak_berechnen( + streak_basis, data["bewertung"] == "gut", req.tier2, req.tier3, + absolviert and not war_absolviert, verstanden and not war_verstanden, + ) + await set_baustein_streak(req.topic, req.baustein, streak) + return {"feedback": data["feedback"], "bewertung": data["bewertung"], "gute_antworten": gute, "streak": streak, "absolviert": absolviert, "verstanden": verstanden, "gemeistert": gemeistert} # --- Guides --- diff --git a/frontend/src/api.js b/frontend/src/api.js index df92325..9b355a5 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -77,13 +77,13 @@ export async function chatBaustein({ topic, baustein, section, section_kompakt = export async function pruefeBaustein({ topic, baustein, section, section_kompakt = '', provider, - aktion = 'frage', frage = '', letzte_bewertung = '', score_vor_frage = 0, streak = 0, + aktion = 'frage', frage = '', letzte_bewertung = '', vermeide = [], tier2 = false, tier3 = false, messages = [], gruendlich = false, }) { const res = await fetch(`${BASE}/bausteine/pruefung`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ topic, baustein, section, section_kompakt, aktion, frage, letzte_bewertung, score_vor_frage, streak, tier2, tier3, messages, provider, gruendlich }), + body: JSON.stringify({ topic, baustein, section, section_kompakt, aktion, frage, letzte_bewertung, vermeide, tier2, tier3, messages, provider, gruendlich }), }) return jsonOrThrow(res) } diff --git a/frontend/src/components/BausteinPanel.vue b/frontend/src/components/BausteinPanel.vue index 8e6da2c..b07f80d 100644 --- a/frontend/src/components/BausteinPanel.vue +++ b/frontend/src/components/BausteinPanel.vue @@ -53,8 +53,6 @@ const pruefMessages = slot.messages // {role, kind: 'frage'|'nachfrage'|'antwort const pruefPhase = slot.phase const aktuelleFrage = slot.aktuelleFrage // ankert Bewertung/Diskussion const letztesFeedback = slot.letztesFeedback // Kontext für die Diskussion über eine Bewertung -const scoreVorFrage = slot.scoreVorFrage // Score, als die Frage gestellt wurde → driftfreies (Re-)Bewerten -const streakVorFrage = slot.streakVorFrage // Streak, als die Frage gestellt wurde (driftfrei) // Transient (pro Instanz): Eingabe-Entwurf, Lade-Spinner. const pruefInput = ref('') const pruefLoading = ref(false) @@ -107,12 +105,10 @@ async function pruefSenden(payload, onOk) { } } -// Frage anzeigen (frisch oder vorgemerkt). scoreVorFrage = LIVE-Score zur Anzeigezeit (driftfrei). +// Frage anzeigen (frisch oder vorgemerkt). Basis/Streak führt der Server je Frage. function frageZeigen(text) { aktuelleFrage.value = text letztesFeedback.value = '' - scoreVorFrage.value = st.value.gute_antworten - streakVorFrage.value = st.value.streak pruefMessages.value.push({ role: 'assistant', kind: 'frage', content: text }) pruefPhase.value = 'frage_offen' } @@ -134,26 +130,34 @@ function istBekannt(text) { return !n || gestellteFragen().has(n) || pool.value.some((f) => normFrage(f) === n) } +// Schon gestellte + bereits vorgemerkte Fragen — der Generator/Kritiker soll sie +// (auch sinngemäß) meiden. Original-Texte, nicht normalisiert. +function vermeideListe() { + const gestellt = pruefMessages.value.filter((m) => m.kind === 'frage').map((m) => m.content) + return [...gestellt, ...pool.value] +} + async function frageEinzeln() { const res = await pruefeBaustein({ topic: props.topic, baustein: props.baustein, section: props.section, section_kompakt: props.sectionKompakt, - provider: props.provider, aktion: 'frage', messages: pruefDialog(), + provider: props.provider, aktion: 'frage', messages: pruefDialog(), vermeide: vermeideListe(), }) return res.frage } -// Pool parallel auf ZIEL auffüllen; jede Auflösung dedupt + füllt nach. +// Pool SEQUENZIELL auffüllen — je eine Generierung; jede neue sieht die vorigen +// (Verlauf + Pool) in der Vermeide-Liste und divergiert. Verhindert ähnliche Dubletten. function poolFuellen() { - while (pool.value.length + inflight.value < POOL_ZIEL && poolMiss < 6) { - inflight.value++ - frageEinzeln() - .then((text) => { - if (text && !istBekannt(text)) { pool.value.push(text); poolMiss = 0 } - else poolMiss++ - }) - .catch(() => { poolMiss++ }) - .finally(() => { inflight.value--; poolFuellen() }) - } + if (inflight.value > 0) return + if (pool.value.length >= POOL_ZIEL || poolMiss >= 6) return + inflight.value++ + frageEinzeln() + .then((text) => { + if (text && !istBekannt(text)) { pool.value.push(text); poolMiss = 0 } + else poolMiss++ + }) + .catch(() => { poolMiss++ }) + .finally(() => { inflight.value--; poolFuellen() }) } // Eine Frage aus dem Pool nehmen; ist er leer, auf die erste eintreffende warten. @@ -222,10 +226,8 @@ function bewerten(res) { } function antwortPayload() { - return { - aktion: 'antwort', frage: aktuelleFrage.value, score_vor_frage: scoreVorFrage.value, - streak: streakVorFrage.value, tier2: props.tier2, tier3: props.tier3, - } + // Basis/Streak führt der Server je Frage (frage = Anker). Frontend schickt sie nicht mehr. + return { aktion: 'antwort', frage: aktuelleFrage.value, tier2: props.tier2, tier3: props.tier3 } } // Hintergrund: voller Bewerter+Kritiker. Persistiert serverseitig; korrigiert die Anzeige bei Abweichung. @@ -239,8 +241,8 @@ async function verdictPruefen(gruendlich = false) { topic: props.topic, baustein: props.baustein, section: props.section, section_kompakt: props.sectionKompakt, provider: props.provider, messages: pruefDialog(), ...antwortPayload(), aktion: 'antwort_pruefen', gruendlich, }) - if (meine !== pruefenRun) return // eine neuere Bewertung läuft → diese verwerfen - // Streak kommt verbindlich vom Backend (persistiert) und fließt über applyPruefung in st. + // Server führt den Score (serialisiert, absolut) — IMMER übernehmen, auch wenn man + // schon bei der nächsten Frage ist. Sonst geht die Korrektur verloren. applyPruefung(res) if (ziel) { if (res.bewertung !== ziel.bewertung || (res.feedback && res.feedback !== ziel.content)) { @@ -250,7 +252,7 @@ async function verdictPruefen(gruendlich = false) { ziel.geprueft = true } } catch { - if (meine === pruefenRun && ziel) ziel.geprueft = true + if (ziel) ziel.geprueft = true } finally { if (gruendlich && meine === pruefenRun) pruefLoading.value = false } diff --git a/frontend/src/pruefungCache.js b/frontend/src/pruefungCache.js index 8f63028..9901fb6 100644 --- a/frontend/src/pruefungCache.js +++ b/frontend/src/pruefungCache.js @@ -15,8 +15,6 @@ export function usePruefSlot(key) { phase: ref('idle'), aktuelleFrage: ref(''), letztesFeedback: ref(''), - scoreVorFrage: ref(0), - streakVorFrage: ref(0), pool: ref([]), // vorgenerierte, geprüfte, unbeantwortete Fragen inflight: ref(0), // laufende Pool-Generierungen (instanzübergreifend koordiniert) }) diff --git a/templates/Format/Section.md b/templates/Format/Section.md index 0988003..48141aa 100644 --- a/templates/Format/Section.md +++ b/templates/Format/Section.md @@ -1,10 +1,10 @@ SECTION-AUFBAU -Jeder Baustein ist ein kleiner, eigenständiger Lern-Guide: er stellt EIN Konzept vor, erklärt es von Grund auf und macht es nutzbar. Der Leser bringt KEIN Vorwissen mit — du holst ihn ab und bringst ihm die Sache bei. Eine Section ist kein Stichwort-Zettel zum Nachschlagen. +Jeder Baustein ist ein kleiner, eigenständiger Lern-Guide: er stellt EIN Konzept vor, erklärt es von Grund auf und macht es nutzbar. Zielgruppe: ein Junior-Entwickler, der das Thema NEU lernt und KEIN Vorwissen mitbringt. Du holst ihn ab und bringst ihm die Sache bei. Eine Section ist kein Stichwort-Zettel zum Nachschlagen. Aufbau je Baustein — drei Beats, fließend ineinander, OHNE Zwischenüberschriften: -1. Einordnung — welche Frage beantwortet der Baustein, welches Problem löst er? Ein Satz, der den Leser abholt. Bei selbsterklärenden Bausteinen weglassen. -2. Erklärung — was es ist UND wie/warum es funktioniert. Alltagssprache, von der Intuition zum Detail. Fachbegriffe beim ersten Auftreten in einem Halbsatz auflösen. Eine Analogie oder ein Bild ist erlaubt und oft besser als eine Definition. +1. Einordnung (PFLICHT, der Ankerpunkt) — welches Problem löst der Baustein, wozu braucht man ihn? Knüpfe an etwas Bekanntes/Alltägliches an, bevor das Neue kommt. Ohne diesen Anker steht ein Neuling im Leeren. Nie weglassen. +2. Erklärung — was es ist UND wie/warum es funktioniert. Alltagssprache, von der Intuition zum Detail. JEDEN Fachbegriff beim ersten Auftreten in einem Halbsatz auflösen — auch Begriffe aus dem Section-Titel oder anderen Bausteinen NIE als bekannt voraussetzen. Eine Analogie oder ein Bild ist erlaubt und oft besser als eine Definition. „Wie"-Abläufe Schritt für Schritt zeigen (nicht nur das Ergebnis nennen). Kleine Beispiele/Mini-Snippets dürfen schon hier mitten im Text stehen, wo sie einen Punkt sofort greifbar machen. 3. Beispiel(e) — das Konzept konkret gemacht (siehe BEISPIELFORMAT). LESBARKEIT — wichtiger als Kürze: @@ -16,8 +16,8 @@ LESBARKEIT — wichtiger als Kürze: LÄNGE — so lang wie nötig: - KEIN festes Wortlimit. Die Länge richtet sich nach der Schwierigkeit des Konzepts. -- Verständnis-Test: Versteht ein Anfänger das Konzept allein aus dieser Section? Wenn nein → einfacher erklären, NICHT verdichten. -- Weglassen: Füllsätze, Einleitungsfloskeln („In diesem Abschnitt…"), Wiederholungen, Fazit. Nicht jeden Randfall nennen — das Übliche erklären; Varianten in die Beispiele, mehr Tiefe in die Vertiefung. +- Verständnis-Test: Versteht ein NEULING das Konzept allein aus dieser Section, ohne anderswo nachzulesen? Wenn nein → einen Schritt mehr erklären (Warum + Wie), NICHT verdichten. Diese Section trägt die volle Tiefe selbst — es gibt keine zweite Ausbaustufe mehr, die nachliefert. +- Weglassen: Füllsätze, Einleitungsfloskeln („In diesem Abschnitt…"), Wiederholungen, Fazit. Nicht jeden Randfall nennen — das Übliche erklären, seltene Varianten in die Beispiele. BEISPIELFORMAT — am Thema ausrichten, nicht pauschal an Code: - Code-/Tool-Thema (Sprache, Framework, CLI, Konfiguration): Codeblock mit Sprachangabe, wenige Zeilen, Minimalbeispiel. diff --git a/templates/Prompt/Baustein-Frage-Kritik.md b/templates/Prompt/Baustein-Frage-Kritik.md index 13f7988..791ebe4 100644 --- a/templates/Prompt/Baustein-Frage-Kritik.md +++ b/templates/Prompt/Baustein-Frage-Kritik.md @@ -9,13 +9,16 @@ KOMPAKTE FASSUNG (Merksätze, falls vorhanden): BISHERIGER PRÜFUNGS-VERLAUF (nur frühere Fragen und Antworten): {transcript} +BEREITS GESTELLT / SCHON VORGEMERKT (Frage darf keiner davon gleichen): +{vermeide_block} + ZU PRÜFENDE FRAGE: {frage} PRÜFE GEGEN DIESE KRITERIEN: - Stil: GENAU EINE Frage, ein einziges Fragezeichen, eine einzige Sache. Kein Mehrteiler ("und"/"sowie", "sowohl … als auch …", "nenne drei …"). - Kürze: maximal 1–2 Sätze, kein Szenario-Aufbau über mehrere Sätze, keine lange Vorrede. -- Keine Wiederholung einer Frage aus dem Verlauf. +- KEINE Wiederholung — beanstande, wenn die Frage einer aus Verlauf ODER Vermeide-Liste SINNGEMÄSS zu ähnlich ist (gleiche Kernsache, nur umformuliert). - Fachlich korrekt: Die Frage muss aus dem Material oben beantwortbar sein und darf der Referenz NICHT widersprechen. Keine erfundenen Zusatzannahmen. Beanstande NUR echte Verstöße. Ist die Frage knapp, einzeln und korrekt, ist sie in Ordnung — verlange nichts darüber hinaus. diff --git a/templates/Prompt/Baustein-Frage.md b/templates/Prompt/Baustein-Frage.md index c7b0d87..9c486fc 100644 --- a/templates/Prompt/Baustein-Frage.md +++ b/templates/Prompt/Baustein-Frage.md @@ -9,6 +9,9 @@ KOMPAKTE FASSUNG (Merksätze, falls vorhanden): BISHERIGER PRÜFUNGS-VERLAUF (nur frühere Fragen und Antworten): {transcript} +BEREITS GESTELLT / SCHON VORGEMERKT — auch SINNGEMÄSS NICHT wiederholen: +{vermeide_block} + HARTE REGELN FÜR DIE FRAGE — wichtiger als alles andere: - GENAU EINE Frage. Ein einziges Fragezeichen. Eine einzige Sache. - Maximal 1–2 Sätze. Kein Szenario-Aufbau, keine Vorrede, kein "Angenommen … und außerdem …". @@ -17,7 +20,7 @@ HARTE REGELN FÜR DIE FRAGE — wichtiger als alles andere: - KEIN Faktenabruf ("welche Daten…", "wie viele…", "was enthält…"): das prüft Auswendiglernen, nicht Verständnis. - Frag nur zu dem, was der Abschnitt wirklich erklärt — nicht zu am Rand Erwähntem (ein Stichwort), das er nicht ausführt. - Passt eine Transferfrage nicht in einen Satz, wähle eine einfachere Frage. -- Wiederhole keine Frage aus dem Verlauf. +- Wiederhole KEINE Frage aus Verlauf oder Vermeide-Liste — auch nicht sinngemäß/umformuliert. Frag eine ANDERE Sache (anderer Aspekt, anderer Subbaustein). FACHLICHE REFERENZ — WICHTIG: - Die Guide-Fassung und die Vertiefung oben sind die Referenz. Deine Frage darf ihr NIE widersprechen. diff --git a/templates/Prompt/Guide-Inhalt.md b/templates/Prompt/Guide-Inhalt.md index 1053820..81740b2 100644 --- a/templates/Prompt/Guide-Inhalt.md +++ b/templates/Prompt/Guide-Inhalt.md @@ -7,6 +7,8 @@ Dir zugeteilt sind folgende Kapitel und Bausteine — verbindlich: jeder zugetei Sammle pro Baustein: - Kernpunkte / Lernziele: was muss der Leser begreifen? Decke JEDEN gelisteten Subbaustein mit mindestens einem Kernpunkt ab. Hat ein Baustein keine Subbausteine, sammle 3–7 knappe Punkte. +- Voraussetzungen: welche Begriffe/Ideen muss man vorher kennen, um das hier zu verstehen? Jede in einem Halbsatz erklärbar — das sind die Ankerpunkte für Neulinge. +- Typische Hürden: wo verstehen Anfänger es erfahrungsgemäß falsch oder stolpern? - Belegte Fakten (Versionen, Namen, Werte) — nichts Unbelegtes; Unsicheres per Websuche prüfen. - Eine konkrete Beispiel-Idee: was soll das Beispiel zeigen? - Scope: nur was DIESER Baustein hergibt. Nicht mehr, nicht weniger. Am Rand Erwähntes nicht zum Thema aufblasen. @@ -16,6 +18,8 @@ Schreibe NUR die Datei {out_path} — pro Baustein ein section-Marker (Titel EXA - Kernpunkt … - Kernpunkt … +Voraussetzung: was vorher kurz erklärt werden muss (Anker) +Hürde: typischer Anfänger-Irrtum Beispiel: kurze Idee, was das Beispiel zeigt Die Marker-Zeile exakt so schreiben. Kein Text außerhalb der Sections, kein Fließtext-Guide. diff --git a/templates/Prompt/Guide-Writer.md b/templates/Prompt/Guide-Writer.md index fef1da5..dcca6d5 100644 --- a/templates/Prompt/Guide-Writer.md +++ b/templates/Prompt/Guide-Writer.md @@ -12,13 +12,14 @@ Jeder Baustein bekommt ZWEI Fassungen mit DENSELBEN Inhalten (dieselben Subbaust - **ausführlich**: der zusammenhängende Anfänger-Lerntext (siehe unten). Beide MÜSSEN dieselben Subbausteine abdecken. Nichts, das nur in einer Fassung steht. -SO SCHREIBST DU die ausführliche Fassung — EIN zusammenhängender Text, kein Stichwort-Stakkato: -- Schreibe pro Baustein EINEN fließenden Lern-Text laut SECTION-SPEZIFIKATION (Einordnung → Erklärung → Beispiel). Ein roter Faden, mit Übergängen. +SO SCHREIBST DU die ausführliche Fassung — EIN zusammenhängender Text für einen Junior, der das Thema NEU lernt: +- Beginne mit dem Anker (Einordnung): welches Problem/wozu, angeknüpft an etwas Bekanntes — erst dann das Neue. Pflicht, nie weglassen. +- Löse JEDEN Fachbegriff bei Erstnennung in einem Halbsatz auf. Setze nichts voraus — auch keine Begriffe aus dem Titel oder anderen Bausteinen. Nutze die „Voraussetzung"-Hinweise aus den Inhalten als Anker, die „Hürde"-Hinweise, um Missverständnisse vorweg auszuräumen. - Die Subbausteine sind deine INHALTS-CHECKLISTE, keine Überschriften: webe sie in den Fließtext ein. Behandle jeden, aber als Teil eines Ganzen — nicht als isolierten Mini-Absatz. - Reihenfolge: erst die `[einfach]`-Punkte (das Fundament), dann `[mittel]` (Details/Varianten), zuletzt `[schwer]` (Sonderfälle/Tücken — knapp, als fortgeschrittener Hinweis). -- Tiefe folgt der Zahl der Subbausteine: ein Baustein mit vielen Subbausteinen wird ein langer, gehaltvoller Text; einer mit wenigen bleibt kurz. Das ist gewollt. -- KEIN festes Längen-Limit. So lang wie nötig, damit ein Anfänger es ohne Vorwissen versteht. Lieber ein Satz mehr und klar. -- Beispiel großzügig einsetzen, wo es hilft — bei Code-/Tool-Themen fast immer ein kurzes Code-Snippet (Format laut Spezifikation). +- Diese Fassung trägt die volle Tiefe selbst — es gibt keine zweite Ausbaustufe. „Wie"-Abläufe Schritt für Schritt zeigen, nicht nur das Ergebnis. +- KEIN festes Längen-Limit. So lang wie nötig, damit ein Neuling es OHNE Vorwissen versteht. Im Zweifel ein Satz mehr und klar. +- Beispiele großzügig — auch kleine Mini-Beispiele mitten im Text, wo sie einen Punkt sofort greifbar machen, zusätzlich zum `### Beispiel` am Ende (Format laut Spezifikation). GEPRÜFTE INHALTE je Baustein — das ist verbindlich, was gelehrt werden muss: {inhalte}