This commit is contained in:
team3
2026-07-06 14:44:20 +02:00
parent f4c5116abb
commit cc27e53b9e
20 changed files with 504 additions and 267 deletions

View File

@@ -324,8 +324,9 @@ async def test_panel_confirms_demote(board_env, tmp_path, monkeypatch):
assert (await db.kanban_get_card(TOPIC, B, "b-2"))["stage"] == "dedup"
async def test_panel_overrules_single_vote(board_env, tmp_path, monkeypatch):
"""Nur 1 von 3 Panel-Stimmen bestätigt den Judge-Demote → Karte überlebt (Journal)."""
async def test_single_recheck_vote_confirms_proposal(board_env, tmp_path, monkeypatch):
"""M3.2: ein pass-1-Vorschlag wird schon von EINER Recheck-Stimme bestätigt → rejected.
(15/17 mehrheitlich geretteten Karten waren später QA-Dubletten — Overturn nur einstimmig.)"""
db, ctx, files = board_env
monkeypatch.setattr(bi, "run_single_slot", _slot_router([
("-filter-recheck-", _confirm_votes({"1"}, {"fragments": {"1": 2}, "drop": []})),
@@ -335,6 +336,23 @@ async def test_panel_overrules_single_vote(board_env, tmp_path, monkeypatch):
("b-1", {"title": "Blockzitat", "description": "Zitat mit >"}),
("b-2", {"title": "Codeblock", "description": "Code mit Einrückung"}),
])
c1 = await db.kanban_get_card(TOPIC, B, "b-1")
assert c1["stage"] == "rejected"
assert c1["payload"]["reason"] == "fragment"
assert c1["payload"]["parent_norm"] == "codeblock"
async def test_unanimous_recheck_keep_overrules_proposal(board_env, tmp_path, monkeypatch):
"""M3.2: nur ein EINSTIMMIGES Recheck-Panel (0 Demote-Stimmen) hebt den pass-1-Vorschlag auf."""
db, ctx, files = board_env
monkeypatch.setattr(bi, "run_single_slot", _slot_router([
("-filter-recheck-", _confirm_votes(set(), {"fragments": {"1": 2}, "drop": []})),
("-filter-", {"fragments": {"1": 2}, "drop": []}),
]))
await _run_filter(db, ctx, tmp_path, [
("b-1", {"title": "Blockzitat", "description": "Zitat mit >"}),
("b-2", {"title": "Codeblock", "description": "Code mit Einrückung"}),
])
assert (await db.kanban_get_card(TOPIC, B, "b-1"))["stage"] == "dedup"
journal = json.loads(next(tmp_path.glob("inventar-filter-*.json")).read_text(encoding="utf-8"))
assert journal["ueberstimmt"] == ["Blockzitat"]
@@ -587,6 +605,45 @@ def test_canonical_key_camel_and_catalogue():
assert k("Section 2.1 Matching") == k("Matching")
def test_canonical_key_glued_problem_suffix():
"""Verklebtes Kompositum-Suffix „…problem" wird abgetrennt, damit „Cliquenproblem" und
„Clique" denselben Blocking-Key teilen (aak: sonst nie Dedup-Kandidat). Fugen-n/-s + Plural
inklusive; die generische Suffix-Regel darf keine echten Varianten über-mergen."""
from blocks import _canonical_key as k
assert k("Clique") == k("Cliquenproblem") == k("Cliquenprobleme") != ""
assert k("Set Cover") == k("SetCover-Problem") # bestehender Hyphen-Pfad bleibt
assert k("SAT") != k("3-SAT") # Varianten-Ziffer bleibt Signal
assert k("Problem") != k("Clique") # bloßes „Problem" wird nicht zum Stamm
def test_reference_strip_and_is_reference():
"""Katalog-Nummern werden gestrippt, der Konzeptname bleibt; reine Nummern → leer + Referenz.
Reale aak-Schadensfälle (Satz-/Bemerkungs-Titel liefen wörtlich bis done)."""
from blocks import _reference_strip as strip, _is_reference as isref
assert strip("Satz 7.13 (Christofides)") == "Christofides"
assert strip("Satz 7.6: Kriterium für Eulerschen Kreis") == "Kriterium für Eulerschen Kreis"
assert strip("N P via nicht-deterministische Turingmaschine (Definition 6.19)") \
== "N P via nicht-deterministische Turingmaschine"
assert strip("Bemerkung 7.22") == ""
assert strip("Vertex Cover") == "Vertex Cover" # kein Katalog-Gerüst → unverändert
assert isref("Bemerkung 7.22") and isref("Satz 7.18") and isref("Korollar 6.18")
assert isref("Bedingung (**)")
assert not isref("Satz 7.13 (Christofides)") # hat Konzept → keine Referenz
assert not isref("Satz 7.6: Kriterium für Eulerschen Kreis")
assert not isref("P⊆NP") and not isref("Σ*") # kurze Symbole bleiben echt
def test_is_named_statement_construction_suffix():
"""Reduktion mit Konstruktions-Suffix (: / = nach dem Ziel) ist ein Fragment, kein Statement —
darf nicht mehr vor Demotion geschützt sein (aak: „3-SAT ≤ K-COLOR: G=(V,E) Konstruktion")."""
from blocks import _is_named_statement as named
assert not named("3-SAT ≤ K-COLOR: G=(V,E) Konstruktion")
assert named("3-SAT ≤ Clique") # saubere Reduktion bleibt geschützt
assert named("Clique → Vertex Cover")
assert not named("X ist NP-vollständig") # unäre Aussage bleibt demotable
assert named("Satz von Cook/Levin: SAT ist NP-vollständig ⇔ …") # benanntes Ergebnis bleibt
def test_relation_guard_ignores_trailing_scaffolding():
"""Trailing „Reduktion/Transformation" ist kein Operand — sonst blockt der Guard
den korrekten Merge; Richtungs-Konflikte bleiben erkannt."""
@@ -1213,6 +1270,49 @@ async def test_namecheck_ok_behaelt_titel(testdb, tmp_path, monkeypatch):
assert block["payload"]["description"] == "Eigene Beschreibung"
async def test_namecheck_strips_source_numbering(testdb, tmp_path, monkeypatch):
"""M1.2: wählt der Check-Judge einen numerierten Titel, wird die Katalog-Nummer deterministisch
zum Konzept gestrippt ('Satz 7.13 (Christofides)''Christofides')."""
db = testdb
async def fake_members(topic, cid):
return [{"norm": "satz 7.13 (christofides)", "title": "Satz 7.13 (Christofides)",
"description": "3/2-Approximation für metrisches TSP", "readers": ["r1"], "sources": []},
{"norm": "b", "title": "B", "description": "db", "readers": ["r2"], "sources": []}]
async def fake_slot(ctx, label, *, key, prompt, role, capabilities, payload, timeout):
return "ok", payload((0, '{"best": 1}', "")) # Judge wählt den numerierten Member-Titel
monkeypatch.setattr(bi, "_member_rows", fake_members)
monkeypatch.setattr(bi, "run_single_slot", fake_slot)
payload = {"title": "Satz 7.13 (Christofides)", "description": "3/2-Approximation",
"main_norm": "satz 7.13 (christofides)"}
await db.kanban_upsert_card(TOPIC, B, "c7", "cluster", "naming_check", payload)
ctx = GenContext(topic=TOPIC, provider="claude", is_cancelled=lambda: False)
await bi._namecheck_one(ctx, _mk_flow(tmp_path), {"card_id": "c7", "payload": payload})
block = await db.kanban_get_card(TOPIC, B, "b-c7")
assert block["payload"]["title"] == "Christofides"
async def test_consensus_gate_single_reader_majority_quorum(testdb, tmp_path, monkeypatch):
"""M5.1: Einzel-Reader-Fund → clarify mit Mehrheits-Quorum statt Einstimmigkeit."""
db = testdb
monkeypatch.setattr(bi, "source_folder", lambda t: None) # thema: kein Korpus
async def fake_members(topic, cid):
return [{"title": "Seltenes Konzept", "description": "einmal erwähnt",
"readers": ["r1"], "supplement": False}]
monkeypatch.setattr(bi, "_member_rows", fake_members)
monkeypatch.setattr(bi, "_rep", lambda rows: rows[0])
await db.kanban_upsert_card(TOPIC, B, "cs", "cluster", "consensus_gate", {})
ctx = GenContext(topic=TOPIC, provider="claude", is_cancelled=lambda: False)
await bi._proc_consensus_gate(ctx, _mk_flow(tmp_path), [{"card_id": "cs", "payload": {}}])
card = await db.kanban_get_card(TOPIC, B, "cs")
assert card["stage"] == "clarify"
assert card["payload"]["quorum"] == "majority"
# ── Sanierung: Titel auf Korpus-Form, Beschreibungspflicht (QA: fremd/hygiene) ──────
def test_sanierung_schema_varianten():
@@ -1229,6 +1329,11 @@ def test_sanierung_noetig():
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
# M1.3: Referenz-Titel / unbalancierte Klammer / >80 Zeichen triggern auch ohne Korpus (Singletons)
assert bi._sanierung_noetig({"title": "Bemerkung 7.22", "description": "d"}, None)
assert bi._sanierung_noetig({"title": "N P (Definition 6.19", "description": "d"}, None)
assert bi._sanierung_noetig({"title": "A" * 81, "description": "d"}, None)
assert not bi._sanierung_noetig({"title": "Clique", "description": "d"}, None)
def _sanierung_env(tmp_path, monkeypatch, antwort):