This commit is contained in:
team3
2026-07-28 08:36:49 +02:00
parent 7912eae715
commit 58efba988d
16 changed files with 521 additions and 332 deletions

View File

@@ -1,23 +1,22 @@
"""Inhaltsprüfung: gleicht die Karten mit dem Quelltext ab.
"""Zwei Abgleiche zwischen Karten und Quelltext, in beide Richtungen.
Die Stilprüfung fragt „ist das gut geschrieben", diese hier fragt „stimmt das
überhaupt". Ziel ist, dass alle Karten zusammen alles abbilden, was das Programm
kann — und nichts, was es nicht kann.
**Feature im Code** — steht hinter jeder Funktion einer Karte auch Code?
**Code im Feature** — hat jede Stelle im Code auch eine Funktion auf einer Karte?
In drei Schritten, weil ein einzelner nicht funktioniert hat:
Beide arbeiten mit derselben Beweislast, nur andersherum.
1. **Inventar** — das Modell liest nur den Quelltext und listet die Fähigkeiten.
Ohne die Karten daneben, sonst bestätigt es bloß, was schon dasteht. Jede
Fähigkeit braucht eine wörtlich zitierte Codezeile; ohne Beleg fällt sie weg.
Mehrere Durchgänge werden vereinigt: hier zählt Vollständigkeit.
2. **Abdeckung** — je Fähigkeit eine kurze Frage: deckt eine bestehende Funktion
das ab? Eine Ja/Nein-Frage ist weit stabiler als offenes Suchen. Nur wenn alle
Stimmen „nein" sagen, entsteht ein Vorschlag.
Geprüft wird nur eine Richtung: was fehlt. Die Gegenrichtung — „diese Funktion hat
keinen Code, weg damit" — ist bewusst nicht eingebaut. Sie ließ sich nicht belegen
(ein Nichtvorhandensein lässt sich nicht zitieren), urteilte von Runde zu Runde
anders und schlug dabei vor, gültige Inhalte zu löschen. Karten dürfen außerdem
Dinge festhalten, die kein Knopf sind — Schreibregeln etwa.
Ein Vorhandensein lässt sich zitieren, ein Nichtvorhandensein nicht. Darum fragt
„Feature im Code" sechsmal unabhängig nach dem Beleg und meldet erst, wenn keine
einzige Stimme eine Codezeile findet, die auch wirklich dasteht. Ein geprüfter Fund
genügt zum Freispruch — sechs Fehlversuche sind die Anklage. Ein früherer Versuch
ließ das Modell direkt urteilen und schlug dabei vor, gültige Inhalte zu löschen.
„Code im Feature" fragt umgekehrt je Stelle, ob eine bestehende Funktion sie
abdeckt, und meldet ab fünf von sechs Nein-Stimmen. Einstimmigkeit war zu scharf:
dieselbe Frage kippt auch bei Temperatur 0 gelegentlich.
Was der Code selbst weiß, entscheidet der Code: die Liste der Bedienstellen kommt
aus `quellcode.interaktionen()`, jeder Beleg wird gegen die Datei geschlagen.
"""
import asyncio
@@ -28,8 +27,8 @@ import agents
import config
import quellcode
# Ohne Drossel liefen alle Punkte gleichzeitig los und der Provider antwortete
# mit 429. Die ausgefallenen Stimmen sahen dann aus wie „alles abgedeckt".
# Ohne Drossel liefen alle Anfragen gleichzeitig los und der Provider antwortete
# mit 429. Die ausgefallenen Stimmen sahen dann aus wie „nichts gefunden".
_schranke = None
@@ -47,14 +46,43 @@ async def _frage(system: str, nachricht: str, temperatur: float) -> str:
async def _urteil(system: str, nachricht: str, temperatur: float):
"""Eine Stimme als JSON. Unlesbares wird einmal neu gefragt, dann aufgegeben —
bei 240 Anfragen je Lauf ist ein Ausrutscher normal, ein Abbruch wäre es nicht."""
for versuch in range(2):
bei hunderten Anfragen je Lauf ist ein Ausrutscher normal, ein Abbruch nicht."""
for versuch in range(3):
try:
return _json(await _frage(system, nachricht, temperatur + 0.1 * versuch))
except ValueError:
continue
return None
# Achtung: diese Texte gehen unformatiert als System-Prompt raus. Doppelte Klammern
# wären hier keine Escapes, sondern landeten wörtlich in der Antwort.
SUCHE_SYSTEM = """Du suchst im Quelltext den Code zu einer beschriebenen Funktion.
Antworte ausschließlich mit JSON:
{"gefunden": true, "datei": "...", "beleg": "..."}
oder
{"gefunden": false, "grund": "..."}
- datei: der Pfad genau so, wie er über der Datei steht
- beleg: eine Zeile aus dieser Datei, Zeichen für Zeichen abgeschrieben
- grund: höchstens 8 Wörter, wenn es den Code nicht gibt
Suche gründlich und im ganzen Quelltext. Es zählt nicht nur, was einen Knopf hat:
Hintergrundlogik, Parser, Dateiverwaltung, Prüfregeln und Anzeigeverhalten sind
genauso Code. Ein erfundener Beleg ist wertlos — er wird nachgeschlagen."""
SUCHE_NACHRICHT = """{quelle}
Ende des Quelltextes.
Eine Karte beschreibt diese Funktion:
{funktion}
Finde die Stelle im Quelltext, die das umsetzt. Antworte ausschließlich mit dem
JSON-Objekt."""
ABDECKUNG_SYSTEM = """Du entscheidest, ob eine Stelle im Programm schon von einer
beschriebenen Funktion abgedeckt ist. Antworte ausschließlich mit JSON."""
@@ -66,7 +94,6 @@ ABDECKUNG = """An dieser Stelle kann eine Person etwas auslösen:
{umfeld}
```
{rumpf}
Die Karten des Projekts beschreiben bisher das hier:
{funktionen}
@@ -89,14 +116,14 @@ Rein technische Aufhänger gelten als abgedeckt: Speichern beim Verlassen eines
Feldes, Abbrechen mit Escape, Gesundheitsprüfungen, interne Weiterleitung.
Im Zweifel abgedeckt. Nur was einer Person wirklich fehlt, soll dazukommen."""
# Achtung: dieser Text geht unformatiert als System-Prompt raus. Doppelte Klammern
# wären hier keine Escapes, sondern landeten wörtlich in der Antwort.
# Sechs Stimmen, fünf müssen „fehlt" sagen.
#
# Einstimmigkeit war zu scharf: gemessen kippt dieselbe Frage auch bei Temperatur 0
# gelegentlich, und eine einzelne Ausreißerstimme löschte damit auch klare Funde.
# Eindeutige Stellen antworten 6:0 oder 0:6, Grenzfälle etwa 3:3 — die Schwelle bei
# fünf trennt beides und übersteht einen Ausrutscher.
# Sechs unabhängige Versuche, den Beleg zu finden. Eine einzige Stimme, die etwas
# findet, genügt zum Freispruch — die Anklage lautet auf Löschen, da wiegt ein
# Fehlurteil schwerer als ein übersehener Rest.
SUCHE_TEMPERATUREN = (0.0, 0.2, 0.4, 0.6, 0.8, 1.0)
# So viele Stimmen müssen überhaupt geantwortet haben, bevor angeklagt wird
ANKLAGE_AB = 4
# Sechs Stimmen, fünf müssen „fehlt" sagen. Eindeutige Stellen antworten 6:0 oder
# 0:6, Grenzfälle etwa 3:3 — die Schwelle bei fünf trennt beides.
URTEIL_TEMPERATUREN = (0.0, 0.2, 0.4, 0.6, 0.8, 1.0)
FEHLT_AB = 5
# Ab dieser Ähnlichkeit gelten zwei Titel als dieselbe Sache
@@ -117,12 +144,99 @@ def _json(antwort: str) -> dict:
return pruefung._json_lesen(antwort)
# ---------------------------------------------------------------- Feature im Code
def _funktionstext(karte: str, f: dict) -> str:
"""Alles, was die Funktion behauptet — danach wird im Code gesucht."""
zeilen = [f"Karte: {karte}", f'Funktion: {f["text"]}']
if f["ziel"]:
zeilen.append(f'Ziel: {f["ziel"]}')
for feld in ("regeln", "testen"):
if f[feld]:
eintraege = [z.strip("- ").strip() for z in f[feld].splitlines() if z.strip()]
zeilen.append(f'{feld.capitalize()}: {"; ".join(eintraege)}')
return "\n".join(zeilen)
async def feature_im_code(features: list, quelle: dict, quelltext: str,
zaehl=None) -> list:
"""Funktionen, für die keine Stimme einen nachprüfbaren Beleg findet."""
auftraege = [(f, x) for f in features for x in f["sektionen"]["funktionen"]]
async def eine(paar):
feature, x = paar
nachricht = SUCHE_NACHRICHT.format(
quelle=quelltext,
funktion=_funktionstext(feature["sektionen"]["titel"], x))
proben = await asyncio.gather(*(
_urteil(SUCHE_SYSTEM, nachricht, t) for t in SUCHE_TEMPERATUREN
), return_exceptions=True)
if zaehl:
zaehl()
gruende = []
brauchbar = belegt = behauptet = 0
for u in proben:
if not isinstance(u, dict):
continue
brauchbar += 1
if u.get("gefunden"):
# Auch ein misslungenes Zitat ist ein Widerspruch. Ein Modell
# verschreibt sich beim Abschreiben von Backticks — daraus eine
# Löschempfehlung zu machen, wäre grob falsch.
behauptet += 1
if quellcode.belegt(quelle, str(u.get("datei") or "").strip(),
str(u.get("beleg") or "").strip()):
belegt += 1
elif u.get("grund"):
gruende.append(str(u["grund"]).strip())
if belegt:
return None # geprüfter Beleg: die Funktion steht im Code
if behauptet:
return None # widersprüchliche Stimmen: nichts behaupten
if brauchbar < ANKLAGE_AB:
# Zu wenige Stimmen für eine Löschempfehlung. Lieber laut scheitern
# als eine gültige Funktion wegen eines Ausfalls anzuklagen.
raise RuntimeError(
f'{x["text"]}: nur {brauchbar} von '
f"{len(SUCHE_TEMPERATUREN)} Stimmen brauchbar")
return {"feature": feature["id"], "titel": x["text"],
"grund": gruende[0] if gruende else "Kein Beleg im Code gefunden"}
return [x for x in await asyncio.gather(*(eine(a) for a in auftraege)) if x]
def probleme_feature(features: list, ohne_code: list) -> list:
"""Ein Vorschlag je Karte: die betroffenen Funktionen aus der Liste nehmen."""
import pruefung
raus = []
for f in features:
ist = [x["text"] for x in f["sektionen"]["funktionen"]]
betroffen = [x for x in ohne_code if x["feature"] == f["id"]]
if not betroffen:
continue
weg = [x["titel"] for x in betroffen]
neu = [t for t in ist if t not in weg]
grund = (f"{len(weg)} Funktion ohne Code" if len(weg) == 1
else f"{len(weg)} Funktionen ohne Code")
beleg = " · ".join(f'{x["titel"]}: {x["grund"]}' for x in betroffen)
for p in pruefung._stellen("funktionen", "\n".join(ist), "\n".join(neu),
grund, beleg):
p["feature"] = f["id"]
p["funktion"] = ""
raus.append(p)
return raus
# ---------------------------------------------------------------- Code im Feature
def _funktionsliste(features: list) -> str:
"""Die Karten mit allem, was sie sagen.
Nur die Titel reichten nicht: der Richter musste raten, ob eine Funktion eine
Stelle abdeckt, und urteilte von Runde zu Runde anders. Ziel, Regeln und
Testschritte sagen es dagegen ausdrücklich.
Stelle abdeckt, und urteilte von Runde zu Runde anders.
"""
teile = []
for f in features:
@@ -154,40 +268,44 @@ async def _abdeckung(punkt: dict, liste: str) -> list:
return [p for p in proben if isinstance(p, dict)]
async def luecken(punkte: list, features: list) -> list:
"""Interaktionspunkte, die einstimmig von keiner Funktion abgedeckt sind."""
def _naechste_karte(name: str, karten: dict):
if not name:
return None
treffer = max(karten, key=lambda k: _aehnlich(name, k))
return karten[treffer] if _aehnlich(name, treffer) > 0.6 else None
async def code_im_feature(features: list, punkte: list, zaehl=None) -> list:
"""Bedienstellen, die von keiner Funktion abgedeckt sind."""
liste = _funktionsliste(features)
karten = {f["sektionen"]["titel"]: f for f in features}
getroffen = set() # Funktionen, die eine Stimme als Treffer genannt hat
async def eine(punkt):
urteile = await _abdeckung(punkt, liste)
for u in urteile:
if u.get("abgedeckt") and u.get("funktion"):
getroffen.add(str(u["funktion"]).strip())
if len(urteile) < len(URTEIL_TEMPERATUREN):
# Nicht verschweigen: eine ausgefallene Stimme sähe sonst aus wie
# „ist abgedeckt" und der Lauf meldete stillschweigend zu wenig.
if zaehl:
zaehl()
if len(urteile) < FEHLT_AB:
# Nicht verschweigen: ausgefallene Stimmen sähen sonst aus wie „ist
# abgedeckt" und der Lauf meldete stillschweigend zu wenig. Ein
# einzelner Ausfall darf den Lauf aber nicht abreißen — es zählt, ob
# noch genug Stimmen übrig sind, um die Schwelle zu erreichen.
raise RuntimeError(
f'{punkt["schluessel"]}: nur {len(urteile)} von '
f"{len(URTEIL_TEMPERATUREN)} Stimmen brauchbar")
if sum(u.get("abgedeckt") is False for u in urteile) < FEHLT_AB:
return None
ziele = [str(u.get("karte") or "").strip() for u in urteile]
karte = max(set(ziele), key=ziele.count)
treffer = karten.get(karte) or _naechste_karte(karte, karten)
if not treffer:
return None
name = max(set(ziele), key=ziele.count)
treffer = karten.get(name) or _naechste_karte(name, karten)
titel = [str(u.get("titel") or "").strip() for u in urteile if u.get("titel")]
if not titel:
if not treffer or not titel:
return None
return {"feature": treffer["id"],
"titel": max(titel, key=lambda t: sum(_aehnlich(t, x) for x in titel)),
"beleg": f'{punkt["datei"]}:{punkt["zeile"]}'}
gefunden = [x for x in await asyncio.gather(*(eine(p) for p in punkte)) if x]
luecken.getroffen = getroffen
# Zwei Punkte können auf dieselbe fehlende Funktion zeigen
# Zwei Stellen können auf dieselbe fehlende Funktion zeigen
raus = []
for x in gefunden:
gleich = next((y for y in raus if y["feature"] == x["feature"]
@@ -199,15 +317,8 @@ async def luecken(punkte: list, features: list) -> list:
return raus
def _naechste_karte(name: str, karten: dict):
if not name:
return None
treffer = max(karten, key=lambda k: _aehnlich(name, k))
return karten[treffer] if _aehnlich(name, treffer) > 0.6 else None
def probleme(features: list, fehlend: list) -> list:
"""Aus den Lücken die Ergänzungen an den Funktionslisten ableiten."""
def probleme_code(features: list, fehlend: list) -> list:
"""Ein Vorschlag je Karte: die fehlenden Funktionen anhängen."""
import pruefung
raus = []
@@ -216,28 +327,30 @@ def probleme(features: list, fehlend: list) -> list:
dazu = [x for x in fehlend if x["feature"] == f["id"]]
if not dazu:
continue
# Bestehende Reihenfolge behalten, Neues hinten anhängen: sortieren würde
# die halbe Liste verschieben und aus einer Ergänzung einen Umbau machen.
# Angezeigt wird ohnehin alphabetisch.
# Bestehende Reihenfolge behalten, Neues hinten anhängen — sortieren würde
# aus einer Ergänzung einen Umbau machen. Angezeigt wird ohnehin alphabetisch.
neu = list(dict.fromkeys(ist + [x["titel"] for x in dazu]))
belege = " · ".join(x["beleg"] for x in dazu)
grund = (f"{len(dazu)} Stelle ohne Funktion" if len(dazu) == 1
else f"{len(dazu)} Stellen ohne Funktion")
beleg = " · ".join(x["beleg"] for x in dazu)
for p in pruefung._stellen("funktionen", "\n".join(ist), "\n".join(neu),
_grund(dazu), belege):
grund, beleg):
p["feature"] = f["id"]
p["funktion"] = ""
raus.append(p)
return raus
def _grund(dazu: list) -> str:
return (f"{len(dazu)} Fähigkeit ohne Funktion" if len(dazu) == 1
else f"{len(dazu)} Fähigkeiten ohne Funktion")
# ---------------------------------------------------------------- Einstiege
async def pruefe_feature_im_code(features: list, quelle: dict, quelltext: str,
zaehl=None) -> list:
ohne = await feature_im_code(features, quelle, quelltext, zaehl)
return probleme_feature(features, ohne)
async def pruefe(features: list, quelle: dict, quelltext: str):
"""Der ganze Abgleich: Interaktionspunkte, Lücken, Überhang."""
async def pruefe_code_im_feature(features: list, quelle: dict, zaehl=None) -> list:
punkte = quellcode.interaktionen(quelle)
if not punkte:
raise RuntimeError("Keine Interaktionspunkte im Quelltext gefunden")
fehlend = await luecken(punkte, features)
return probleme(features, fehlend), punkte
raise RuntimeError("Keine Bedienstellen im Quelltext gefunden")
return probleme_code(features, await code_im_feature(features, punkte, zaehl))