diff --git a/backend/blocks.py b/backend/blocks.py index 890c53f..e39eba6 100644 --- a/backend/blocks.py +++ b/backend/blocks.py @@ -597,6 +597,32 @@ def _sink_json(result, path: Path, schema): return val +def _sink_subs(result, path: Path): + """Finder reply as TEXT (marker format), persisted to `path` for audit/diagnosis. + File fallback: a tool-capable agent (thema web mode) that wrote the file despite the + text instruction still counts — same tolerance as _sink_or_file.""" + text = _reply_text(result).strip() + d = _parse_subblocks(text) + if d: + atomic_write_text(path, text) + return d + return _parse_subblocks(_read(path)) or None + + +def _finder_material(folder, sources: list[str] | None, queries: list[str]) -> tuple[str, str, str]: + """→ (material, backing, caps) for the Subblock-Research prompt. uni/projekt: corpus + excerpts INLINE — the finder had neither path nor excerpts and hunted the material per + call via glob/grep/bash (measured: 3–13 tool rounds, 52–141 s vs 15–35 s single-shot). + thema (or excerpt miss): web research as before, fail-open.""" + ev = _evidence_pack(folder, sources, queries) if folder else "" + if ev: + material = "\n" + _prompt("Blocks-Source-Inline", excerpts=ev) + "\n" + backing = ("Backed by the SOURCE EXCERPTS above, not invented — leave out any " + "sub-point the excerpts do not support.") + return material, backing, "none" + return "", "Backed, not invented. Verify uncertain points via web search.", ("files" if folder else "full") + + def _build_research_prompt(topic: str, out_path: Path, instructions: str, type: str, folder: Path | None, fokus: str = "", section: str = "") -> str: if section: # Section mode (uni/projekt): text directly in the prompt → small context, no file reading. @@ -760,15 +786,15 @@ async def _subblocks_block(ctx: GenContext, set_p, files: dict, entries: dict, i return ("\n\nBEREITS ERFASST — liste diese NICHT erneut. Finde nur, was FEHLT:\n" + "\n".join(known)) # ONE finder round (3 slots, quorum 2) → count of NEW sub norms; None = no result/cancel. - async def _one_round(label, subset, assignment, paths, keys, known, extra_instr): + async def _one_round(label, subset, assignment, paths, keys, known, extra_instr, material, backing, round_caps): chunk_idx = _title_index({num: title_by_num[num] for num in subset}) for p in paths: p.unlink(missing_ok=True) slots = [{ "key": k, - "prompt": _prompt("Subblock-Research", topic=topic, assignment=assignment, known=known, out_path=p, extra=_extra(extra_instr)), - "role": "quick", "capabilities": caps, - "payload": (lambda result, p=p: _parse_subblocks(_read(p)) or None), + "prompt": _prompt("Subblock-Research", topic=topic, assignment=assignment, known=known, material=material, backing=backing, extra=_extra(extra_instr)), + "role": "quick", "capabilities": round_caps, + "payload": (lambda result, p=p: _sink_subs(result, p)), } for k, p in zip(keys, paths)] agent_texts = await _race(topic, label, slots, 2, _timeout("subblock", len(subset)), provider, cancelled=is_cancelled, grace=CONSENSUS_GRACE) if is_cancelled() or not agent_texts: @@ -818,6 +844,10 @@ async def _subblocks_block(ctx: GenContext, set_p, files: dict, entries: dict, i # Phase "Subblocks find": per package loop until 0 new subs / time cap. async def _find(c, chunk): assignment = "\n".join(f"- {entries[num]}" for num in chunk) + # Material once per package (excerpts are round-invariant; one query per block + # keeps the coverage guarantee of _evidence_pack). + material, backing, round_caps = await asyncio.to_thread( + _finder_material, folder, sources, [str(entries[num]) for num in chunk]) start = time.monotonic() round_n = 0 while not is_cancelled(): @@ -825,7 +855,7 @@ async def _subblocks_block(ctx: GenContext, set_p, files: dict, entries: dict, i bekannt = await _known_block(chunk) if round_n > 1 else "" paths = [work_dir / f"subblock-c{c}-r{round_n}-{i}.md" for i in (1, 2, 3)] keys = [f"blocks-{topic}-{ns}subblock-c{c}-r{round_n}-{i}" for i in (1, 2, 3)] - new = await _one_round(f"{lbl}Subblocks package {c} R{round_n}", chunk, assignment, paths, keys, bekannt, instructions) + new = await _one_round(f"{lbl}Subblocks package {c} R{round_n}", chunk, assignment, paths, keys, bekannt, instructions, material, backing, round_caps) if new is None: if is_cancelled(): return False @@ -1067,9 +1097,11 @@ async def _subblocks_block(ctx: GenContext, set_p, files: dict, entries: dict, i focus = (instructions + "\n\nDieser Block hat bisher nur sehr wenige belegte " "Subbausteine. Suche gezielt nach WEITEREN belegbaren Kernaspekten, die " "oben fehlen. Nimm NUR auf, was die Quellen wirklich hergeben — nicht aufblähen.") + material, backing, round_caps = await asyncio.to_thread( + _finder_material, folder, sources, [str(entries[num]) for num in lacking]) paths = [work_dir / f"subblock-x{k}-c{c}-{i}.md" for i in (1, 2, 3)] keys = [f"blocks-{topic}-{ns}subblock-x{k}-c{c}-{i}" for i in (1, 2, 3)] - new = await _one_round(f"{lbl}Subblocks catch-up {c} X{k}", lacking, assignment, paths, keys, known, focus) + new = await _one_round(f"{lbl}Subblocks catch-up {c} X{k}", lacking, assignment, paths, keys, known, focus, material, backing, round_caps) if not new: return await _select(lacking, keep_consensus=True) @@ -1412,12 +1444,14 @@ async def _luecken_runde(ctx: GenContext, files: dict, title: str, luecken: list "Aspekten des Blocks — nichts anderes:\n" + "\n".join(f"- {l}" for l in luecken)) known = ("\n\nBEREITS ERFASST — liste diese NICHT erneut:\n" + "\n".join(f"- {s}" for s in have)) if have else "" + material, backing, caps = await asyncio.to_thread( + _finder_material, folder, sources, [title] + list(luecken)) paths = [work_dir / f"luecken-{ns}r1-{i}.md" for i in (1, 2, 3)] slots = [{ "key": f"blocks-{topic}-{ns}luecken-r1-{i}", - "prompt": _prompt("Subblock-Research", topic=topic, assignment=f"- {title}", known=known, out_path=p, extra=_extra(focus)), - "role": "quick", "capabilities": "files" if folder else "full", - "payload": (lambda result, p=p: _parse_subblocks(_read(p)) or None), + "prompt": _prompt("Subblock-Research", topic=topic, assignment=f"- {title}", known=known, material=material, backing=backing, extra=_extra(focus)), + "role": "quick", "capabilities": caps, + "payload": (lambda result, p=p: _sink_subs(result, p)), } for i, p in zip((1, 2, 3), paths)] agent_texts = await _race(topic, f"{lbl}Lücken-Nachfass", slots, 2, _timeout("subblock", 1), provider, cancelled=is_cancelled, grace=CONSENSUS_GRACE) diff --git a/backend/board_inventory.py b/backend/board_inventory.py index 62b0a46..3d47b82 100644 --- a/backend/board_inventory.py +++ b/backend/board_inventory.py @@ -104,6 +104,20 @@ def _naming_schema(data, count: int) -> tuple[int | None, str | None, bool] | No return n, name, False +def _sanierung_schema(data) -> tuple[str, str] | None: + """{"title": …, "description": …} → (title, description), beides getrimmt; Titel über + 80 Zeichen verfällt (leerer String = kein Vorschlag, Feld bleibt wie es ist).""" + if not isinstance(data, dict): + return None + t = str(data.get("title") or "").strip() + d = str(data.get("description") or "").strip() + if not t and not d: + return None + if len(t) > 80: + t = "" + return t, d + + def _name_verankert(name: str, rows: list[dict], ctoks: set[str] | None) -> bool: """Abstraction guard: a free-formed title must be anchored — in the corpus (uni/projekt) or in the members' own words (thema). Unanchored names drift to textbook canon @@ -470,6 +484,16 @@ def _hat_anker(title: str, ctoks: set[str]) -> bool: return False +def _sanierung_noetig(p: dict, ctoks: set[str] | None) -> bool: + """QA-messbare Befund-Formen am Entstehungsort: Titel ohne Korpus-Anker (QA: fremd — + misst Token-Anker, nicht Semantik) oder leere Beschreibung (QA: hygiene). Gilt auch für + Singleton-Cluster, die das Naming sonst überspringen — Reader-Rohtitel gingen wörtlich + bis done_block durch (gemessen: 'k-Coloring' statt Korpus-Form 'k-Color').""" + if not (p.get("description") or "").strip(): + return True + return bool(ctoks) and not _hat_anker(p.get("title", ""), ctoks) + + async def _anker_beleg(ctx: GenContext, flow: Flow, kandidaten: list[tuple[str, dict]]) -> set[str]: """Evidence judge for quorum titles WITHOUT any corpus anchor — two readers naming the same famous canon independently beat the quorum although the material never mentions it @@ -703,9 +727,59 @@ async def _name_one(ctx: GenContext, flow: Flow, c): p["title"] = custom or w["title"] p["description"] = w.get("description") or p.get("description", "") await db.kanban_set_payload(topic, BOARD, cid, p) + await _saniere_one(ctx, flow, cid, p) await db.kanban_advance(topic, BOARD, cid, "naming_check") +async def _saniere_one(ctx: GenContext, flow: Flow, cid: str, p: dict) -> None: + """Fremd-/Hygiene-Sanierung am Entstehungsort (nur Korpus-Quellen): Titel ohne + Korpus-Anker auf die Oberflächenform des Materials umschreiben, leere Beschreibung + belegt nachfassen. Läuft NACH der Member-Wahl auf dem finalen Payload — greift damit + auch für Singleton-Cluster und unverankerte Naming-Gewinner. Vorschläge werden + deterministisch validiert (_hat_anker, dieselbe Messlatte wie QA-fremd); fail-open: + unverankerter Vorschlag oder Judge-Ausfall lässt die Karte unverändert weiter.""" + from qa import _distinctive + topic = flow.topic + folder = source_folder(topic) + if folder is None: + return # thema: kein Korpus, keine Oberflächenform zum Verankern + ctoks = flow.state.get("korpus_tokens") + if ctoks is None: # Resume: das Konsens-Gate dieses Flows lief nie + ctoks = flow.state["korpus_tokens"] = await asyncio.to_thread(_korpus_tokens, folder) + if not _sanierung_noetig(p, ctoks): + return + titel, beschr = p.get("title", ""), (p.get("description") or "").strip() + toks = _distinctive(titel) | _distinctive(beschr) + ev = _evidence_pack(folder, p.get("sources") or None, [" ".join(sorted(toks))], + budget=4000) if toks else "" + if not ev: + return # ohne Auszüge kein Anker-Rewrite — Anker-Beleg hat Echtheit schon entschieden + h = _h(titel, beschr, "sanierung") + path = flow.work_dir / f"sanierung-{cid}-{h}.json" + verdict = _sanierung_schema(_json_file(path)) + if verdict is None: + status, verdict = await run_single_slot( + ctx, f"Sanierung {cid}", key=f"blocks-{topic}-sanierung-{cid}-{h}", + prompt=_prompt("Blocks-Sanierung", topic=topic, title=titel, + description=beschr or "(leer)", excerpts=ev), + role="judge", capabilities="none", + payload=lambda result, p2=path: _sink_json(result, p2, _sanierung_schema), + timeout=_timeout("selection_mapping", 1)) + if status != OK or verdict is None: + return + neu_titel, neu_beschr = verdict + changed = False + if (neu_titel and _norm_title(neu_titel) != _norm_title(titel) + and _hat_anker(neu_titel, ctoks)): + p["title"] = clean_title(neu_titel) + changed = True + if not beschr and neu_beschr: + p["description"] = neu_beschr + changed = True + if changed: + await db.kanban_set_payload(topic, BOARD, cid, p) + + async def _proc_naming_check(ctx: GenContext, flow: Flow, cards): results = await asyncio.gather(*[_namecheck_one(ctx, flow, c) for c in cards], return_exceptions=True) errs = [r for r in results if isinstance(r, Exception)] diff --git a/backend/fake_agents.py b/backend/fake_agents.py index b0be77a..0b06166 100644 --- a/backend/fake_agents.py +++ b/backend/fake_agents.py @@ -118,6 +118,13 @@ class Welt: return j({"keep": keep, "rest": []}) if "-naming-" in key: # deckt auch naming_check (gleicher Key) return j({"best": 1}) + if "-sanierung-" in key: # Titel/Beschreibung unverändert zurück (Fake-Welt ist sauber) + t = re.search(r"^Title: (.+)$", prompt, re.M) + d = re.search(r"^Description: (.+)$", prompt, re.M) + beschr = (d.group(1).strip() if d else "") + if beschr == "(leer)": + beschr = "Beschreibung aus dem Material." + return j({"title": t.group(1).strip() if t else "", "description": beschr}) if "-filter-" in key: # auch filter-recheck return j({"fragments": {}, "drop": []}) if "-gruppierung-completion-" in key: diff --git a/backend/tests/test_board_inventory.py b/backend/tests/test_board_inventory.py index 4df81ec..deb006b 100644 --- a/backend/tests/test_board_inventory.py +++ b/backend/tests/test_board_inventory.py @@ -1181,3 +1181,144 @@ async def test_namecheck_ok_behaelt_titel(testdb, tmp_path, monkeypatch): block = await db.kanban_get_card(TOPIC, B, "b-c9") assert block["payload"]["title"] == "Eigener Titel" assert block["payload"]["description"] == "Eigene Beschreibung" + + +# ── Sanierung: Titel auf Korpus-Form, Beschreibungspflicht (QA: fremd/hygiene) ────── + +def test_sanierung_schema_varianten(): + assert bi._sanierung_schema({"title": " k-Color ", "description": "d"}) == ("k-Color", "d") + assert bi._sanierung_schema({"title": "", "description": "nur Beschreibung"}) == ("", "nur Beschreibung") + assert bi._sanierung_schema({"title": "x" * 90, "description": "d"}) == ("", "d") # zu lang + assert bi._sanierung_schema({"title": "", "description": ""}) is None + assert bi._sanierung_schema("quatsch") is None + + +def test_sanierung_noetig(): + ctoks = {"color", "graph"} + assert bi._sanierung_noetig({"title": "k-Color", "description": ""}, ctoks) # leere Beschreibung + assert bi._sanierung_noetig({"title": "k-Coloring", "description": "d"}, ctoks) # kein Korpus-Anker + assert not bi._sanierung_noetig({"title": "k-Color", "description": "d"}, ctoks) + assert not bi._sanierung_noetig({"title": "k-Coloring", "description": "d"}, None) # thema: kein Korpus + + +def _sanierung_env(tmp_path, monkeypatch, antwort): + """Korpus mit 'k-Color'-Oberflächenform; Judge antwortet mit `antwort`.""" + (tmp_path / "korpus.txt").write_text( + "Das k-Color Problem: Kann der Graph mit k Farben gefärbt werden? NP-vollständig.", + encoding="utf-8") + monkeypatch.setattr(bi, "source_folder", lambda t: tmp_path) + seen = {} + + async def fake_slot(ctx, label, *, key, prompt, role, capabilities, payload, timeout): + seen[key] = prompt + return "ok", payload((0, json.dumps(antwort["val"]), "")) + + monkeypatch.setattr(bi, "run_single_slot", fake_slot) + return GenContext(topic=TOPIC, provider="claude", is_cancelled=lambda: False), seen + + +async def test_singleton_unverankert_wird_saniert(testdb, tmp_path, monkeypatch): + """Singleton-Cluster mit Reader-Titel ohne Korpus-Anker: Naming wird nicht mehr + übersprungen — der Titel wird auf die Korpus-Oberflächenform umgeschrieben + (gemessener aak-Fall 'k-Coloring' statt 'k-Color').""" + db = testdb + antwort = {"val": {"title": "k-Color", "description": "Kann der Graph mit k Farben gefärbt werden?"}} + ctx, seen = _sanierung_env(tmp_path, monkeypatch, antwort) + + async def fake_members(topic, cid): + return [{"norm": "k-coloring", "title": "k-Coloring", + "description": "Kann der Graph mit k Farben gefärbt werden?", + "readers": ["r1", "r2"], "sources": []}] + + monkeypatch.setattr(bi, "_member_rows", fake_members) + payload = {"title": "k-Coloring", "description": "Kann der Graph mit k Farben gefärbt werden?"} + await db.kanban_upsert_card(TOPIC, B, "c1", "cluster", "naming", payload) + await bi._name_one(ctx, _mk_flow(tmp_path), {"card_id": "c1", "payload": payload}) + card = await db.kanban_get_card(TOPIC, B, "c1") + assert card["stage"] == "naming_check" + assert card["payload"]["title"] == "k-Color" + assert any("-sanierung-" in k for k in seen) + + +async def test_leere_beschreibung_wird_gefuellt(testdb, tmp_path, monkeypatch): + """Verankerter Titel, leere Beschreibung (gemessener aak-Fall der SetCover-Fragmente): + Beschreibung wird belegt nachgefasst, Titel bleibt.""" + db = testdb + antwort = {"val": {"title": "k-Color", "description": "Färbbarkeit mit k Farben."}} + ctx, _seen = _sanierung_env(tmp_path, monkeypatch, antwort) + + async def fake_members(topic, cid): + return [{"norm": "k-color", "title": "k-Color", "description": "", + "readers": ["r1"], "sources": []}] + + monkeypatch.setattr(bi, "_member_rows", fake_members) + payload = {"title": "k-Color", "description": ""} + await db.kanban_upsert_card(TOPIC, B, "c5", "cluster", "naming", payload) + await bi._name_one(ctx, _mk_flow(tmp_path), {"card_id": "c5", "payload": payload}) + card = await db.kanban_get_card(TOPIC, B, "c5") + assert card["payload"]["title"] == "k-Color" + assert card["payload"]["description"] == "Färbbarkeit mit k Farben." + + +async def test_sanierung_unverankerter_vorschlag_verfaellt(testdb, tmp_path, monkeypatch): + """Judge-Vorschlag ohne Korpus-Anker wird verworfen (dieselbe Messlatte wie QA-fremd); + die Beschreibung wird trotzdem übernommen.""" + db = testdb + antwort = {"val": {"title": "Graphfärbungsproblem", "description": "Färbbarkeit mit k Farben."}} + ctx, _seen = _sanierung_env(tmp_path, monkeypatch, antwort) + + async def fake_members(topic, cid): + return [{"norm": "k-coloring", "title": "k-Coloring", + "description": "Kann der Graph mit k Farben gefärbt werden?", + "readers": ["r1"], "sources": []}] + + monkeypatch.setattr(bi, "_member_rows", fake_members) + payload = {"title": "k-Coloring", "description": "Kann der Graph mit k Farben gefärbt werden?"} + await db.kanban_upsert_card(TOPIC, B, "c2", "cluster", "naming", payload) + await bi._name_one(ctx, _mk_flow(tmp_path), {"card_id": "c2", "payload": payload}) + card = await db.kanban_get_card(TOPIC, B, "c2") + assert card["payload"]["title"] == "k-Coloring" # unverankert → verfällt + assert card["stage"] == "naming_check" + + +async def test_sanierung_fail_open(testdb, tmp_path, monkeypatch): + """Judge-Ausfall → Karte läuft unverändert weiter (kein Deadletter am Naming).""" + db = testdb + (tmp_path / "korpus.txt").write_text("Der Graph ist endlich.", encoding="utf-8") + monkeypatch.setattr(bi, "source_folder", lambda t: tmp_path) + + async def broken_slot(*a, **kw): + return "failed", None + + async def fake_members(topic, cid): + return [{"norm": "x", "title": "Graph", "description": "", "readers": ["r1"], "sources": []}] + + monkeypatch.setattr(bi, "run_single_slot", broken_slot) + monkeypatch.setattr(bi, "_member_rows", fake_members) + payload = {"title": "Graph", "description": ""} + await db.kanban_upsert_card(TOPIC, B, "c3", "cluster", "naming", payload) + ctx = GenContext(topic=TOPIC, provider="claude", is_cancelled=lambda: False) + await bi._name_one(ctx, _mk_flow(tmp_path), {"card_id": "c3", "payload": payload}) + card = await db.kanban_get_card(TOPIC, B, "c3") + assert card["stage"] == "naming_check" and card["payload"]["title"] == "Graph" + + +async def test_sanierung_anker_und_beschreibung_ok_kein_judge(testdb, tmp_path, monkeypatch): + """Verankerter Titel + Beschreibung vorhanden → kein Sanierungs-Call.""" + db = testdb + (tmp_path / "korpus.txt").write_text("Das k-Color Problem im Graph.", encoding="utf-8") + monkeypatch.setattr(bi, "source_folder", lambda t: tmp_path) + + async def never_slot(*a, **kw): + raise AssertionError("Sanierung darf ohne Befund-Form nicht laufen") + + async def fake_members(topic, cid): + return [{"norm": "k-color", "title": "k-Color", "description": "d", "readers": ["r1"], "sources": []}] + + monkeypatch.setattr(bi, "run_single_slot", never_slot) + monkeypatch.setattr(bi, "_member_rows", fake_members) + payload = {"title": "k-Color", "description": "d"} + await db.kanban_upsert_card(TOPIC, B, "c4", "cluster", "naming", payload) + ctx = GenContext(topic=TOPIC, provider="claude", is_cancelled=lambda: False) + await bi._name_one(ctx, _mk_flow(tmp_path), {"card_id": "c4", "payload": payload}) + assert (await db.kanban_get_card(TOPIC, B, "c4"))["stage"] == "naming_check" diff --git a/backend/tests/test_subblocks.py b/backend/tests/test_subblocks.py index 3dca326..c70daeb 100644 --- a/backend/tests/test_subblocks.py +++ b/backend/tests/test_subblocks.py @@ -75,11 +75,9 @@ def _mk_race(finder_by_agent): if "-r1-" in key: agent = int(key.rsplit("-", 1)[1]) subs = finder_by_agent.get(agent) or [] - if subs and (m := _MD_PATH.search(prompt)): + if subs: # Finder antworten als TEXT (Marker-Format), kein out_path mehr text = "\n" + "\n".join(f"- {s}" for s in subs) - with open(m.group(1), "w", encoding="utf-8") as f: - f.write(text) - outs.append(slot["payload"](None)) + outs.append(slot["payload"]((0, text, ""))) outs = [o for o in outs if o] return outs or None fake_race.slots_seen = [] @@ -177,11 +175,8 @@ async def test_catchup_adds_and_stops(sub_env, monkeypatch, tmp_path): if any("-subblock-x" in s["key"] for s in slots): hit["n"] += 1 if hit["n"] == 1: # first catch-up round: both agents agree on one new sub - for slot in slots[:2]: - m = _MD_PATH.search(slot["prompt"]) - with open(m.group(1), "w", encoding="utf-8") as f: - f.write("\n- Vertiefung der Konzepte") - return [slot["payload"](None) for slot in slots[:2]] + text = "\n- Vertiefung der Konzepte" + return [slot["payload"]((0, text, "")) for slot in slots[:2]] return None return await base(topic, label, slots, *a, **k) @@ -251,13 +246,8 @@ async def test_paraphrase_saturation_stops_early(sub_env, monkeypatch): async def with_r2(topic, label, slots, *a, **k): if any("-r2-" in s["key"] for s in slots): - outs = [] - for slot in slots[:2]: - m = _MD_PATH.search(slot["prompt"]) - with open(m.group(1), "w", encoding="utf-8") as f: - f.write("\n- Umbruch erfordert explizite Marker!") - outs.append(slot["payload"](None)) - return outs + text = "\n- Umbruch erfordert explizite Marker!" + return [slot["payload"]((0, text, "")) for slot in slots[:2]] return await base_fake(topic, label, slots, *a, **k) monkeypatch.setattr(blx, "_race", with_r2) @@ -282,11 +272,8 @@ async def test_round_cap_stops_endless_finders(sub_env, monkeypatch): rn = _re.search(r"-r(\d+)-", slots[0]["key"]) n = rn.group(1) if rn else "x" for slot in slots[:2]: - m = _MD_PATH.search(slot["prompt"]) - with open(m.group(1), "w", encoding="utf-8") as f: - f.write(f"\n- Konzept{n} ist eigenständig") prompts.append((slot["key"], slot["prompt"])) - outs.append(slot["payload"](None)) + outs.append(slot["payload"]((0, f"\n- Konzept{n} ist eigenständig", ""))) return outs monkeypatch.setattr(blx, "_race", endless) @@ -362,8 +349,9 @@ def test_sink_json_writes_only_valid(tmp_path): async def test_clarify_inline_evidence_no_tools(sub_env, monkeypatch, tmp_path): - """Mit Korpus: Judges bekommen Auszüge inline und laufen ohne Tools (Text-Antwort); - die j-Datei schreibt die Engine. Finder bleiben unverändert bei capabilities=files.""" + """Mit Korpus: Judges UND Finder bekommen Auszüge inline und laufen ohne Tools + (Text-Antwort); die Dateien schreibt die Engine. Der Finder verlor vorher 3–13 + Tool-Runden pro Call mit der Material-Suche via glob/grep/bash.""" db, ctx, files = sub_env d = _corpus(tmp_path) monkeypatch.setattr(blx, "source_folder", lambda t: d) @@ -378,7 +366,9 @@ async def test_clarify_inline_evidence_no_tools(sub_env, monkeypatch, tmp_path): assert judges and all(s["capabilities"] == "none" for s in judges) assert "── Skript.txt" in judges[0]["prompt"] assert "ls/find" not in judges[0]["prompt"] # keine Selbst-Recherche-Anweisung mehr - assert finders and all(s["capabilities"] == "files" for s in finders) + assert finders and all(s["capabilities"] == "none" for s in finders) + assert "── Skript.txt" in finders[0]["prompt"] # Auszüge inline statt Dateisystem-Suche + assert "web search" not in finders[0]["prompt"] assert list(tmp_path.glob("subblock-final-*-j*.md")) # Engine persistiert die Judge-Antwort diff --git a/templates/Prompt/Blocks-Sanierung.md b/templates/Prompt/Blocks-Sanierung.md new file mode 100644 index 0000000..d9598a9 --- /dev/null +++ b/templates/Prompt/Blocks-Sanierung.md @@ -0,0 +1,17 @@ +The block below was distilled from source material for the topic "{topic}", but its wording drifted from the material: the title may use terms that never literally appear in the sources (translated, expanded, or spelling-corrected), and the description may be missing. + +BLOCK: +Title: {title} +Description: {description} + +MATERIAL EXCERPTS: +{excerpts} + +Tasks: +- "title": If the title's terms do not literally appear in the excerpts, rewrite it using ONLY surface forms (exact spellings, even unusual ones) that appear in the excerpts — same meaning, same concreteness, max 8 words. If the title already matches the material wording, repeat it unchanged. Never broaden to a textbook term the excerpts don't use; NO catalog/reference brackets ("(Satz 6.33)", "(Kap. 4)"). +- "description": If the description is "(leer)", write ONE precise sentence grounded in the excerpts — no invented facts. Otherwise repeat it unchanged. + +Reply with ONLY the JSON — no code fences, no other text. + +Format: +{{"title": "…", "description": "…"}} diff --git a/templates/Prompt/Subblock-Research.md b/templates/Prompt/Subblock-Research.md index f0a362d..5b5487a 100644 --- a/templates/Prompt/Subblock-Research.md +++ b/templates/Prompt/Subblock-Research.md @@ -2,7 +2,7 @@ Break each assigned block of the topic "{topic}" into its SUBBLOCKS — the indi Assigned to you — binding: every block must appear, invent no additional ones: {assignment} - +{material} What a subblock is: - A single sub-point you must learn for the block. One statement, one aspect, one pitfall. - Examples: block `` → `src` (Bildquelle), `alt` (Alternativtext), empty `alt` for decorative images, `width`/`height` against layout shifts, void element without a closing tag. Block `

` → text paragraph as a block, allowed inline children, no nesting. @@ -15,15 +15,13 @@ DECISIVE — the count follows the difficulty: Rules: - Each subblock is atomic: exactly one sub-point. No two aspects in one point. - Only what THIS block yields (scope). Don't inflate anything mentioned only in passing. -- Backed, not invented. Verify uncertain points via web search. +- {backing} - Subblock titles in GERMAN (code identifiers stay original), max. ~10 words, one statement. -Write ONLY the file {out_path} — one block marker per block (title EXACTLY from the assignment), with the subblocks as a list below it: +Reply with ONLY the subblock lists in this exact marker format — one block marker per block (title EXACTLY from the assignment), the subblocks as a list below it. No code fences, no text outside the blocks: - First subblock - Second subblock - -Write the marker line exactly like this. No text outside the blocks. {known} {extra}