update
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 = "<!-- block: Alpha -->\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("<!-- block: Alpha -->\n- Vertiefung der Konzepte")
|
||||
return [slot["payload"](None) for slot in slots[:2]]
|
||||
text = "<!-- block: Alpha -->\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("<!-- block: Alpha -->\n- Umbruch erfordert explizite Marker!")
|
||||
outs.append(slot["payload"](None))
|
||||
return outs
|
||||
text = "<!-- block: Alpha -->\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"<!-- block: Alpha -->\n- Konzept{n} ist eigenständig")
|
||||
prompts.append((slot["key"], slot["prompt"]))
|
||||
outs.append(slot["payload"](None))
|
||||
outs.append(slot["payload"]((0, f"<!-- block: Alpha -->\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
|
||||
|
||||
|
||||
|
||||
17
templates/Prompt/Blocks-Sanierung.md
Normal file
17
templates/Prompt/Blocks-Sanierung.md
Normal file
@@ -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": "…"}}
|
||||
@@ -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 `<img>` → `src` (Bildquelle), `alt` (Alternativtext), empty `alt` for decorative images, `width`/`height` against layout shifts, void element without a closing tag. Block `<p>` → 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:
|
||||
|
||||
<!-- block: Exact block title -->
|
||||
- First subblock
|
||||
- Second subblock
|
||||
|
||||
Write the marker line exactly like this. No text outside the blocks.
|
||||
{known}
|
||||
{extra}
|
||||
|
||||
Reference in New Issue
Block a user