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

@@ -77,40 +77,42 @@ einziger Vorschlag für das Feld — einzelne Zeilen daraus ergäben Dubletten o
Jede Anfrage bekommt nur ihren Ausschnitt der Karte und nur den Katalogteil, der für sie Jede Anfrage bekommt nur ihren Ausschnitt der Karte und nur den Katalogteil, der für sie
gilt. Die Herkunft eines Problems stammt aus dem Code, nicht aus der Modellantwort. gilt. Die Herkunft eines Problems stammt aus dem Code, nicht aus der Modellantwort.
## Mit dem Code abgleichen ## Abgleich mit dem Code
Das ⌗-Icon prüft die andere Frage: bilden alle Karten zusammen ab, was das Programm Zwei weitere Prüfungen, in beide Richtungen. Beide arbeiten mit derselben
kann? Gesucht werden Lücken — Fähigkeiten ohne Funktion auf einer Karte. Beweislast, nur andersherum: ein Vorhandensein lässt sich zitieren, ein
Nichtvorhandensein nicht.
``` ```
1 Inventar quellcode.py Bedienstellen aus dem Code, deterministisch ↧ Features im Code steht hinter jeder Funktion auch Code?
2 Abdecken 6 Stimmen deckt eine bestehende Funktion die Stelle ab? ↥ Code in Features hat jede Bedienstelle auch eine Funktion?
3 Ergänzen Code 5 von 6 sagen nein -> Vorschlag mit Fundort
``` ```
**Inventar.** `interaktionen()` liest die Stellen aus, an denen ein Mensch etwas **↧ Features im Code.** Je Funktion sechs unabhängige Versuche, die Codestelle zu
auslöst: Ereignisse im Markup und Einträge der Kontextmenüs. Zusammengefasst nach finden. Jeder Fund wird gegen die Datei geschlagen. Gemeldet wird erst, wenn keine
dem aufgerufenen Namen — `starte('ziel')` steht zwölfmal da und ist eine Fähigkeit. einzige Stimme etwas findet — auch ein misslungenes Zitat gilt als Widerspruch, denn
Ein Modell hatte die Liste anfangs frei aufgezählt: jede Runde eine andere. ein verschriebener Backtick darf keine Löschempfehlung auslösen. Unter vier
brauchbaren Stimmen wird gar nicht erst angeklagt.
Nicht im Inventar: Backend-Routen (die Verdrahtung hinter den Knöpfen), Zuweisungen, **↥ Code in Features.** Die Liste der Bedienstellen kommt aus dem Code, nicht aus
Speichern beim Verlassen, Abbrechen mit Escape. dem Modell: `quellcode.interaktionen()` liest Ereignisse im Markup, Kontextmenü-
Einträge und Fenster-Lauscher. Zusammengefasst nach dem aufgerufenen Namen —
`starte('ziel')` steht zwölfmal da und ist eine Fähigkeit. Je Stelle eine Ja/Nein-
Frage mit Code-Umfeld, Funktionsrumpf und dem vollen Text aller Karten; gemeldet ab
fünf von sechs Nein-Stimmen.
**Abdecken.** Je Stelle eine Ja/Nein-Frage, mit dem Code-Umfeld, dem Rumpf der Nicht im Inventar: Backend-Routen (jede hängt an einem Knopf, der schon dasteht),
Funktion dahinter und dem vollen Text aller Karten. Nur Titel reichten nicht — der Zuweisungen, Speichern beim Verlassen, Abbrechen mit Escape.
Richter musste raten und urteilte jede Runde anders.
**Schwelle.** Fünf von sechs Stimmen. Einstimmigkeit war zu scharf: dieselbe Frage **Warum diese Schwellen.** Einstimmigkeit war für ↥ zu scharf: dieselbe Frage kippt
kippt auch bei Temperatur 0 gelegentlich, und ein Ausreißer löschte klare Funde. auch bei Temperatur 0 gelegentlich, und ein Ausreißer löschte klare Funde.
Eindeutige Stellen antworten 6:0 oder 0:6, Grenzfälle etwa 3:3. Eindeutige Stellen antworten 6:0 oder 0:6, Grenzfälle etwa 3:3. Für ↧ gilt das
Gegenteil — dort ist die Anklage destruktiv, also blockiert schon eine
widersprechende Stimme.
Jeder Vorschlag nennt seinen Fundort als `datei:zeile`. Die Gegenrichtung — „diese Nachgemessen an diesem Projekt: auf gepflegten Karten melden beide nichts. Eine
Funktion hat keinen Code, weg damit" — ist bewusst nicht eingebaut: ein untergeschobene Funktion ohne Code fällt ↧ in jedem Lauf auf. Fehlende Funktionen
Nichtvorhandensein lässt sich nicht belegen, und die Vorschläge zielten wiederholt findet ↥, nach dem Übernehmen ist es still.
auf gültige Inhalte. Karten dürfen Dinge festhalten, die kein Knopf sind.
Gemessen an diesem Projekt: auf den unsynchronisierten Karten findet der Abgleich
die offenen Stellen, nach dem Übernehmen dreimal hintereinander nichts mehr.
Der Katalog liegt in `backend/stil/` — eine Datei je Feld, alphabetisch zusammengesetzt Der Katalog liegt in `backend/stil/` — eine Datei je Feld, alphabetisch zusammengesetzt
(`allgemein` landet dadurch vorn). Das Präfix entscheidet, wer sie zu sehen bekommt: (`allgemein` landet dadurch vorn). Das Präfix entscheidet, wer sie zu sehen bekommt:

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 **Feature im Code** — steht hinter jeder Funktion einer Karte auch Code?
überhaupt". Ziel ist, dass alle Karten zusammen alles abbilden, was das Programm **Code im Feature** — hat jede Stelle im Code auch eine Funktion auf einer Karte?
kann — und nichts, was es nicht kann.
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. Ein Vorhandensein lässt sich zitieren, ein Nichtvorhandensein nicht. Darum fragt
Ohne die Karten daneben, sonst bestätigt es bloß, was schon dasteht. Jede „Feature im Code" sechsmal unabhängig nach dem Beleg und meldet erst, wenn keine
Fähigkeit braucht eine wörtlich zitierte Codezeile; ohne Beleg fällt sie weg. einzige Stimme eine Codezeile findet, die auch wirklich dasteht. Ein geprüfter Fund
Mehrere Durchgänge werden vereinigt: hier zählt Vollständigkeit. genügt zum Freispruch — sechs Fehlversuche sind die Anklage. Ein früherer Versuch
2. **Abdeckung** — je Fähigkeit eine kurze Frage: deckt eine bestehende Funktion ließ das Modell direkt urteilen und schlug dabei vor, gültige Inhalte zu löschen.
das ab? Eine Ja/Nein-Frage ist weit stabiler als offenes Suchen. Nur wenn alle
Stimmen „nein" sagen, entsteht ein Vorschlag. „Code im Feature" fragt umgekehrt je Stelle, ob eine bestehende Funktion sie
Geprüft wird nur eine Richtung: was fehlt. Die Gegenrichtung — „diese Funktion hat abdeckt, und meldet ab fünf von sechs Nein-Stimmen. Einstimmigkeit war zu scharf:
keinen Code, weg damit" — ist bewusst nicht eingebaut. Sie ließ sich nicht belegen dieselbe Frage kippt auch bei Temperatur 0 gelegentlich.
(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 Was der Code selbst weiß, entscheidet der Code: die Liste der Bedienstellen kommt
Dinge festhalten, die kein Knopf sind — Schreibregeln etwa. aus `quellcode.interaktionen()`, jeder Beleg wird gegen die Datei geschlagen.
""" """
import asyncio import asyncio
@@ -28,8 +27,8 @@ import agents
import config import config
import quellcode import quellcode
# Ohne Drossel liefen alle Punkte gleichzeitig los und der Provider antwortete # Ohne Drossel liefen alle Anfragen gleichzeitig los und der Provider antwortete
# mit 429. Die ausgefallenen Stimmen sahen dann aus wie „alles abgedeckt". # mit 429. Die ausgefallenen Stimmen sahen dann aus wie „nichts gefunden".
_schranke = None _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): async def _urteil(system: str, nachricht: str, temperatur: float):
"""Eine Stimme als JSON. Unlesbares wird einmal neu gefragt, dann aufgegeben — """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.""" bei hunderten Anfragen je Lauf ist ein Ausrutscher normal, ein Abbruch nicht."""
for versuch in range(2): for versuch in range(3):
try: try:
return _json(await _frage(system, nachricht, temperatur + 0.1 * versuch)) return _json(await _frage(system, nachricht, temperatur + 0.1 * versuch))
except ValueError: except ValueError:
continue continue
return None 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 ABDECKUNG_SYSTEM = """Du entscheidest, ob eine Stelle im Programm schon von einer
beschriebenen Funktion abgedeckt ist. Antworte ausschließlich mit JSON.""" beschriebenen Funktion abgedeckt ist. Antworte ausschließlich mit JSON."""
@@ -66,7 +94,6 @@ ABDECKUNG = """An dieser Stelle kann eine Person etwas auslösen:
{umfeld} {umfeld}
``` ```
{rumpf} {rumpf}
Die Karten des Projekts beschreiben bisher das hier: Die Karten des Projekts beschreiben bisher das hier:
{funktionen} {funktionen}
@@ -89,14 +116,14 @@ Rein technische Aufhänger gelten als abgedeckt: Speichern beim Verlassen eines
Feldes, Abbrechen mit Escape, Gesundheitsprüfungen, interne Weiterleitung. Feldes, Abbrechen mit Escape, Gesundheitsprüfungen, interne Weiterleitung.
Im Zweifel abgedeckt. Nur was einer Person wirklich fehlt, soll dazukommen.""" Im Zweifel abgedeckt. Nur was einer Person wirklich fehlt, soll dazukommen."""
# Achtung: dieser Text geht unformatiert als System-Prompt raus. Doppelte Klammern # Sechs unabhängige Versuche, den Beleg zu finden. Eine einzige Stimme, die etwas
# wären hier keine Escapes, sondern landeten wörtlich in der Antwort. # findet, genügt zum Freispruch — die Anklage lautet auf Löschen, da wiegt ein
# Sechs Stimmen, fünf müssen „fehlt" sagen. # Fehlurteil schwerer als ein übersehener Rest.
# SUCHE_TEMPERATUREN = (0.0, 0.2, 0.4, 0.6, 0.8, 1.0)
# Einstimmigkeit war zu scharf: gemessen kippt dieselbe Frage auch bei Temperatur 0 # So viele Stimmen müssen überhaupt geantwortet haben, bevor angeklagt wird
# gelegentlich, und eine einzelne Ausreißerstimme löschte damit auch klare Funde. ANKLAGE_AB = 4
# Eindeutige Stellen antworten 6:0 oder 0:6, Grenzfälle etwa 3:3 — die Schwelle bei # Sechs Stimmen, fünf müssen „fehlt" sagen. Eindeutige Stellen antworten 6:0 oder
# fünf trennt beides und übersteht einen Ausrutscher. # 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) URTEIL_TEMPERATUREN = (0.0, 0.2, 0.4, 0.6, 0.8, 1.0)
FEHLT_AB = 5 FEHLT_AB = 5
# Ab dieser Ähnlichkeit gelten zwei Titel als dieselbe Sache # Ab dieser Ähnlichkeit gelten zwei Titel als dieselbe Sache
@@ -117,12 +144,99 @@ def _json(antwort: str) -> dict:
return pruefung._json_lesen(antwort) 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: def _funktionsliste(features: list) -> str:
"""Die Karten mit allem, was sie sagen. """Die Karten mit allem, was sie sagen.
Nur die Titel reichten nicht: der Richter musste raten, ob eine Funktion eine 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 Stelle abdeckt, und urteilte von Runde zu Runde anders.
Testschritte sagen es dagegen ausdrücklich.
""" """
teile = [] teile = []
for f in features: 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)] return [p for p in proben if isinstance(p, dict)]
async def luecken(punkte: list, features: list) -> list: def _naechste_karte(name: str, karten: dict):
"""Interaktionspunkte, die einstimmig von keiner Funktion abgedeckt sind.""" 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) liste = _funktionsliste(features)
karten = {f["sektionen"]["titel"]: f for f in features} karten = {f["sektionen"]["titel"]: f for f in features}
getroffen = set() # Funktionen, die eine Stimme als Treffer genannt hat
async def eine(punkt): async def eine(punkt):
urteile = await _abdeckung(punkt, liste) urteile = await _abdeckung(punkt, liste)
for u in urteile: if zaehl:
if u.get("abgedeckt") and u.get("funktion"): zaehl()
getroffen.add(str(u["funktion"]).strip()) if len(urteile) < FEHLT_AB:
if len(urteile) < len(URTEIL_TEMPERATUREN): # Nicht verschweigen: ausgefallene Stimmen sähen sonst aus wie „ist
# Nicht verschweigen: eine ausgefallene Stimme sähe sonst aus wie # abgedeckt" und der Lauf meldete stillschweigend zu wenig. Ein
# „ist abgedeckt" und der Lauf meldete stillschweigend zu wenig. # 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( raise RuntimeError(
f'{punkt["schluessel"]}: nur {len(urteile)} von ' f'{punkt["schluessel"]}: nur {len(urteile)} von '
f"{len(URTEIL_TEMPERATUREN)} Stimmen brauchbar") f"{len(URTEIL_TEMPERATUREN)} Stimmen brauchbar")
if sum(u.get("abgedeckt") is False for u in urteile) < FEHLT_AB: if sum(u.get("abgedeckt") is False for u in urteile) < FEHLT_AB:
return None return None
ziele = [str(u.get("karte") or "").strip() for u in urteile] ziele = [str(u.get("karte") or "").strip() for u in urteile]
karte = max(set(ziele), key=ziele.count) name = max(set(ziele), key=ziele.count)
treffer = karten.get(karte) or _naechste_karte(karte, karten) treffer = karten.get(name) or _naechste_karte(name, karten)
if not treffer:
return None
titel = [str(u.get("titel") or "").strip() for u in urteile if u.get("titel")] 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 None
return {"feature": treffer["id"], return {"feature": treffer["id"],
"titel": max(titel, key=lambda t: sum(_aehnlich(t, x) for x in titel)), "titel": max(titel, key=lambda t: sum(_aehnlich(t, x) for x in titel)),
"beleg": f'{punkt["datei"]}:{punkt["zeile"]}'} "beleg": f'{punkt["datei"]}:{punkt["zeile"]}'}
gefunden = [x for x in await asyncio.gather(*(eine(p) for p in punkte)) if x] gefunden = [x for x in await asyncio.gather(*(eine(p) for p in punkte)) if x]
luecken.getroffen = getroffen # Zwei Stellen können auf dieselbe fehlende Funktion zeigen
# Zwei Punkte können auf dieselbe fehlende Funktion zeigen
raus = [] raus = []
for x in gefunden: for x in gefunden:
gleich = next((y for y in raus if y["feature"] == x["feature"] 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 return raus
def _naechste_karte(name: str, karten: dict): def probleme_code(features: list, fehlend: list) -> list:
if not name: """Ein Vorschlag je Karte: die fehlenden Funktionen anhängen."""
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."""
import pruefung import pruefung
raus = [] raus = []
@@ -216,28 +327,30 @@ def probleme(features: list, fehlend: list) -> list:
dazu = [x for x in fehlend if x["feature"] == f["id"]] dazu = [x for x in fehlend if x["feature"] == f["id"]]
if not dazu: if not dazu:
continue continue
# Bestehende Reihenfolge behalten, Neues hinten anhängen: sortieren würde # Bestehende Reihenfolge behalten, Neues hinten anhängen — sortieren würde
# die halbe Liste verschieben und aus einer Ergänzung einen Umbau machen. # aus einer Ergänzung einen Umbau machen. Angezeigt wird ohnehin alphabetisch.
# Angezeigt wird ohnehin alphabetisch.
neu = list(dict.fromkeys(ist + [x["titel"] for x in dazu])) 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), for p in pruefung._stellen("funktionen", "\n".join(ist), "\n".join(neu),
_grund(dazu), belege): grund, beleg):
p["feature"] = f["id"] p["feature"] = f["id"]
p["funktion"] = "" p["funktion"] = ""
raus.append(p) raus.append(p)
return raus return raus
def _grund(dazu: list) -> str: # ---------------------------------------------------------------- Einstiege
return (f"{len(dazu)} Fähigkeit ohne Funktion" if len(dazu) == 1
else f"{len(dazu)} Fähigkeiten ohne Funktion") 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): async def pruefe_code_im_feature(features: list, quelle: dict, zaehl=None) -> list:
"""Der ganze Abgleich: Interaktionspunkte, Lücken, Überhang."""
punkte = quellcode.interaktionen(quelle) punkte = quellcode.interaktionen(quelle)
if not punkte: if not punkte:
raise RuntimeError("Keine Interaktionspunkte im Quelltext gefunden") raise RuntimeError("Keine Bedienstellen im Quelltext gefunden")
fehlend = await luecken(punkte, features) return probleme_code(features, await code_im_feature(features, punkte, zaehl))
return probleme(features, fehlend), punkte

View File

@@ -403,16 +403,21 @@ def _konsens(fassungen: list[dict]) -> dict:
return max(fassungen, key=naehe) return max(fassungen, key=naehe)
def _mehrheit(proben: list[dict]) -> dict: def _mehrheit(proben: list[dict], gemessen: tuple = ()) -> dict:
"""Nur was mehrere Stimmen unabhängig bemängeln, wird ein Vorschlag. """Nur was alle Stimmen unabhängig bemängeln, wird ein Vorschlag.
Bei Grenzfällen urteilt ein Modell mal so, mal so — einzelne Stimmen führten Bei Grenzfällen urteilt ein Modell mal so, mal so — einzelne Stimmen führten
dazu, dass jede Runde andere Felder aufschlugen und der Lauf nie zur Ruhe kam. dazu, dass jede Runde andere Felder aufschlugen und der Lauf nie zur Ruhe kam.
Gemessene Verstöße sind davon ausgenommen. Sie stehen fest, bevor gefragt wird;
zur Abstimmung steht nur noch der Wortlaut. Vorher verschwand ein gezählter
Verstoß, sobald eine einzige Stimme das Feld nicht anfasste.
""" """
zaehler = Counter(feld for p in proben for feld in p) zaehler = Counter(feld for p in proben for feld in p)
return { return {
feld: _konsens([p[feld] for p in proben if feld in p]) feld: _konsens([p[feld] for p in proben if feld in p])
for feld, treffer in zaehler.items() if treffer >= MEHRHEIT for feld, treffer in zaehler.items()
if treffer >= (1 if feld in gemessen else MEHRHEIT)
} }
@@ -426,7 +431,18 @@ async def _neue_felder(system: str, aufgabe: dict, offen: list, erlaubt: tuple,
gute = [p for p in proben if isinstance(p, dict)] gute = [p for p in proben if isinstance(p, dict)]
if not gute: if not gute:
raise next(p for p in proben if isinstance(p, BaseException)) raise next(p for p in proben if isinstance(p, BaseException))
return _mehrheit(gute) gemessen = tuple({b["feld"] for b in offen})
vorschlaege = _mehrheit(gute, gemessen)
# Bleibt ein gezählter Verstoß ohne Vorschlag, greift der eigene Ersatz des
# Messers. Sonst bliebe ein Fehler stehen, den der Code sicher kennt.
for b in offen:
if b["feld"] not in vorschlaege and b["vorschlag"]:
alt = struktur._zeilen(b["feld"], aufgabe["felder"].get(b["feld"], ""))
neu = [b["vorschlag"] if z == b["original"] else z for z in alt]
vorschlaege[b["feld"]] = {"neu": "\n".join(neu), "grund": b["problem"],
"regel": "Vom System gemessen"}
return vorschlaege
async def _pruefe(stil: dict, aufgabe: dict) -> list[dict]: async def _pruefe(stil: dict, aufgabe: dict) -> list[dict]:
@@ -457,38 +473,73 @@ async def _stil_aufgaben(projekt: str):
"""Prüfstellen der Stilprüfung samt der Kataloge, die sie brauchen.""" """Prüfstellen der Stilprüfung samt der Kataloge, die sie brauchen."""
stil = {b: _stil_lesen(b) for b in BEREICHE} stil = {b: _stil_lesen(b) for b in BEREICHE}
liste = [a for f in features.liste(projekt) for a in aufgaben(f)] liste = [a for f in features.liste(projekt) for a in aufgaben(f)]
return liste, lambda a: _pruefe(stil, a) return liste, lambda a, _zaehl: _pruefe(stil, a)
async def _inhalt_aufgaben(projekt: str): def _quelle(projekt: str):
"""Ein Abgleich über alle Karten — Vollständigkeit ist eine Eigenschaft der
Menge, nicht der einzelnen Karte."""
alle = features.liste(projekt) alle = features.liste(projekt)
quelle = quellcode.dateien() quelle = quellcode.dateien()
if not quelle: if not quelle:
raise ValueError("Kein Quelltext gefunden") raise ValueError("Kein Quelltext gefunden")
return alle, quelle
async def _feature_aufgaben(projekt: str):
"""Ein Lauf über alle Funktionen: steht hinter jeder auch Code?
Ein Aufruf statt vieler, weil das Ergebnis je Karte gebündelt wird — zwei
Vorschläge für dieselbe Funktionsliste wären nicht unabhängig anwendbar.
Der Zähler läuft trotzdem je Funktion mit.
"""
alle, quelle = _quelle(projekt)
text = quellcode.als_text(quelle) text = quellcode.als_text(quelle)
anzahl = sum(len(f["sektionen"]["funktionen"]) for f in alle)
async def abgleich(_): async def abgleich(_, zaehl):
probleme, _inv = await inhaltspruefung.pruefe(alle, quelle, text) return await inhaltspruefung.pruefe_feature_im_code(alle, quelle, text, zaehl)
return probleme
return [{"feature": "", "funktion": ""}], abgleich return _platzhalter(anzahl), abgleich
ARTEN = {"stil": _stil_aufgaben, "inhalt": _inhalt_aufgaben} async def _code_aufgaben(projekt: str):
"""Ein Lauf über alle Bedienstellen — Vollständigkeit ist eine Eigenschaft der
Menge, nicht der einzelnen Karte."""
alle, quelle = _quelle(projekt)
punkte = quellcode.interaktionen(quelle)
async def abgleich(_, zaehl):
return await inhaltspruefung.pruefe_code_im_feature(alle, quelle, zaehl)
return _platzhalter(len(punkte)), abgleich
def _platzhalter(anzahl: int) -> list:
"""Eine echte Aufgabe, der Rest nur für den Fortschrittsbalken."""
return [True] + [None] * max(0, anzahl - 1)
ARTEN = {
"stil": _stil_aufgaben,
"feature": _feature_aufgaben, # Feature im Code: was steht ohne Code da?
"code": _code_aufgaben, # Code im Feature: was fehlt auf den Karten?
}
async def _lauf(projekt: str, liste: list[dict], arbeite): async def _lauf(projekt: str, liste: list[dict], arbeite):
lauf = _laeufe[projekt] lauf = _laeufe[projekt]
schranke = asyncio.Semaphore(config.PRUEFUNG_PARALLEL) schranke = asyncio.Semaphore(config.PRUEFUNG_PARALLEL)
def zaehl():
lauf["fertig"] += 1
async def eine(aufgabe): async def eine(aufgabe):
if aufgabe is None:
return # Platzhalter: zählt der Sammellauf selbst hoch
async with schranke: async with schranke:
if _laeufe.get(projekt) is not lauf: if _laeufe.get(projekt) is not lauf:
return # verworfen (Panel geschlossen) — keine weitere Anfrage return # verworfen (Panel geschlossen) — keine weitere Anfrage
try: try:
lauf["probleme"].extend(await arbeite(aufgabe)) lauf["probleme"].extend(await arbeite(aufgabe, zaehl))
except Exception as e: except Exception as e:
# Zählt trotzdem als erledigt, sonst hängt der Fortschritt. # Zählt trotzdem als erledigt, sonst hängt der Fortschritt.
# Die Meldung nennt die Zahl, damit „100 %" nicht vollständig vortäuscht. # Die Meldung nennt die Zahl, damit „100 %" nicht vollständig vortäuscht.
@@ -496,7 +547,8 @@ async def _lauf(projekt: str, liste: list[dict], arbeite):
lauf["meldung"] = ( lauf["meldung"] = (
f"{lauf['fehler']} von {lauf['gesamt']} Prüfungen fehlgeschlagen: {e}" f"{lauf['fehler']} von {lauf['gesamt']} Prüfungen fehlgeschlagen: {e}"
) )
lauf["fertig"] += 1 if aufgabe is not True:
lauf["fertig"] += 1
try: try:
await asyncio.gather(*(eine(a) for a in liste)) await asyncio.gather(*(eine(a) for a in liste))

View File

@@ -71,6 +71,8 @@ def belegt(quelle: dict, pfad: str, schnipsel: str) -> bool:
_EREIGNIS = re.compile(r'@(click|auxclick|contextmenu|keyup|keydown|blur|submit|change)' _EREIGNIS = re.compile(r'@(click|auxclick|contextmenu|keyup|keydown|blur|submit|change)'
r'[\w.]*\s*=\s*"([^"]*)"') r'[\w.]*\s*=\s*"([^"]*)"')
_MENUEPUNKT = re.compile(r"""\{\s*text:\s*['"](.+?)['"]\s*,\s*aktion:""") _MENUEPUNKT = re.compile(r"""\{\s*text:\s*['"](.+?)['"]\s*,\s*aktion:""")
_ROUTE = re.compile(r'@router\.(get|post|put|delete)\("([^"]+)"\)')
_LAUSCHER = re.compile(r'(window|document)\.addEventListener\(\s*[\'"](\w+)[\'"]')
_NAME = re.compile(r"[A-Za-zÄÖÜäöü_$][\w$]*") _NAME = re.compile(r"[A-Za-zÄÖÜäöü_$][\w$]*")
# Rein technische Aufhänger: Speichern beim Verlassen, Abbrechen mit Escape. # Rein technische Aufhänger: Speichern beim Verlassen, Abbrechen mit Escape.
@@ -116,9 +118,11 @@ def interaktionen(quelle: dict = None) -> list[dict]:
Zusammengefasst nach dem aufgerufenen Namen: `@click="starte('ziel')"` steht Zusammengefasst nach dem aufgerufenen Namen: `@click="starte('ziel')"` steht
zwölfmal im Markup, ist aber eine Fähigkeit. zwölfmal im Markup, ist aber eine Fähigkeit.
Backend-Routen stehen bewusst nicht drin. Sie sind die Verdrahtung hinter den Dazu Fenster-Lauscher: ein Tastaturgriff ist eine Fähigkeit, auch ohne Knopf.
Knöpfen, nicht die Bedienung — als eigene Einträge erzeugten sie nur Meldungen
wie „Prüfungsstatus abrufen", die niemand auf einer Karte sehen will. Backend-Routen stehen bewusst nicht drin. Jede nutzbare Route hängt an einem
Knopf, der schon in der Liste steht — als eigene Einträge wurden sie doppelt
gemeldet und dabei der falschen Karte zugeordnet.
""" """
quelle = dateien() if quelle is None else quelle quelle = dateien() if quelle is None else quelle
raus, gesehen = [], set() raus, gesehen = [], set()
@@ -137,6 +141,9 @@ def interaktionen(quelle: dict = None) -> list[dict]:
for nr, zeile in enumerate(zeilen, 1): for nr, zeile in enumerate(zeilen, 1):
for m in _MENUEPUNKT.finditer(zeile): for m in _MENUEPUNKT.finditer(zeile):
merke(f"menue:{m.group(1)}", "Menüpunkt", pfad, nr, zeilen, zeile) merke(f"menue:{m.group(1)}", "Menüpunkt", pfad, nr, zeilen, zeile)
for m in _LAUSCHER.finditer(zeile):
merke(f"lauscher:{m.group(1)}.{m.group(2)}", "Tastatur oder Maus",
pfad, nr, zeilen, zeile)
if not pfad.endswith(".vue"): if not pfad.endswith(".vue"):
continue continue
for m in _EREIGNIS.finditer(zeile): for m in _EREIGNIS.finditer(zeile):

View File

@@ -44,7 +44,7 @@ def test_interaktionen_sind_deterministisch():
# Zwölf Aufrufe von starte() sind eine Fähigkeit, nicht zwölf # Zwölf Aufrufe von starte() sind eine Fähigkeit, nicht zwölf
assert schluessel.count("bedienen:starte") == 1 assert schluessel.count("bedienen:starte") == 1
assert "bedienen:menue" in schluessel assert "bedienen:menue" in schluessel
assert not any(s.startswith("route:") for s in schluessel) # Verdrahtung, keine Bedienung assert not any(s.startswith("schnittstelle:") for s in schluessel) # Verdrahtung
assert "menue:+ Projekt erstellen" in schluessel assert "menue:+ Projekt erstellen" in schluessel
# Technische Aufhänger und Zuweisungen sind keine Fähigkeiten # Technische Aufhänger und Zuweisungen sind keine Fähigkeiten
assert not any(s.startswith("bedienen:fertig") for s in schluessel) assert not any(s.startswith("bedienen:fertig") for s in schluessel)

View File

@@ -430,3 +430,12 @@ def test_konsens_nimmt_die_mittlere_fassung():
proben = _proben(f("Anmeldung starten"), f("Anmeldung startet"), proben = _proben(f("Anmeldung starten"), f("Anmeldung startet"),
f("Anmeldung beginnen"), f("Völlig anderer Text")) f("Anmeldung beginnen"), f("Völlig anderer Text"))
assert pruefung._mehrheit(proben)["titel"]["neu"].startswith("Anmeldung") assert pruefung._mehrheit(proben)["titel"]["neu"].startswith("Anmeldung")
def test_gemessenes_feld_braucht_keine_einstimmigkeit():
"""Ein gezählter Verstoß steht fest, bevor gefragt wird. Vorher verschwand er,
sobald eine einzige Stimme das Feld nicht anfasste."""
eins = {"titel": {"neu": "Kurz", "grund": "x"}}
proben = [eins, {}, {}, {}]
assert pruefung._mehrheit(proben) == {}
assert pruefung._mehrheit(proben, ("titel",)) == eins

View File

@@ -130,7 +130,9 @@ async def test_messung_ist_auftrag_und_tuersteher(datenordner, monkeypatch):
assert probleme[0]["funktion"] == "Anmelden" assert probleme[0]["funktion"] == "Anmelden"
async def test_unverbesserlicher_vorschlag_faellt_weg(datenordner, monkeypatch): async def test_gemessener_verstoss_wird_notfalls_selbst_behoben(datenordner, monkeypatch):
"""Ein gezählter Verstoß steht fest. Liefert keine Stimme einen brauchbaren
Ersatz, greift der Ersatz des Messers — sonst bliebe der Fehler stehen."""
import agents import agents
import features import features
import projekte import projekte
@@ -147,7 +149,8 @@ async def test_unverbesserlicher_vorschlag_faellt_weg(datenordner, monkeypatch):
monkeypatch.setattr(agents, "frage", sturr) monkeypatch.setattr(agents, "frage", sturr)
probleme = await pruefung._pruefe({"funktion": "<KATALOG>"}, pruefung.aufgaben(f)[1]) probleme = await pruefung._pruefe({"funktion": "<KATALOG>"}, pruefung.aufgaben(f)[1])
assert probleme == [] # lieber nichts als ein Vorschlag, der wieder auffällt assert [p["vorschlag"] for p in probleme] == ["Passwort prüfen"]
assert probleme[0]["regel"] == "Vom System gemessen"
def test_gemessene_befunde_haengen_am_ausschnitt(datenordner): def test_gemessene_befunde_haengen_am_ausschnitt(datenordner):

View File

@@ -5,10 +5,12 @@ Darkmode
optional optional
---------------------------------------- ----------------------------------------
<!-- ziel --> <!-- ziel -->
Zwischen Tag- und Nachtmodus wechseln Passt die Anwendung ans Umgebungslicht an
---------------------------------------- ----------------------------------------
<!-- regeln --> <!-- regeln -->
- Standard sind die Systemeinstellungen - Auto folgt der Systemeinstellung
- Zustand überlebt das Neuladen
- Kein Flackern beim Laden
---------------------------------------- ----------------------------------------
<!-- funktion --> <!-- funktion -->
Darkmode umschalten Darkmode umschalten
@@ -16,11 +18,6 @@ Darkmode umschalten
- Nacht-Icon: ☾ - Nacht-Icon: ☾
- Tag-Icon: ☀ - Tag-Icon: ☀
- Auto-Icon: A - Auto-Icon: A
- Zustand bleibt gespeichert
<!-- testen --> <!-- testen -->
- LMB auf Sidebar-Icon - LMB auf Sidebar-Icon
- Erster Modus ist Auto - Modus gewechselt
- LMB auf Icon
- Zweiter Modus ist Tag
- LMB auf Icon
- Dritter Modus ist Nacht

31
daten/Planer/design.md Normal file
View File

@@ -0,0 +1,31 @@
<!-- titel -->
Design
----------------------------------------
<!-- art -->
optional
----------------------------------------
<!-- ziel -->
Hält die Oberfläche lesbar und ruhig
----------------------------------------
<!-- regeln -->
- Karten stapeln sich lückenlos in Spalten
- Lesefeld ist in der Breite gedeckelt
- Leisten scrollen nicht mit
- Schmale Fenster stapeln die Bereiche
- Klickflächen sind mindestens 28 Pixel
- Textkontrast erfüllt die Lesbarkeitsschwelle
- Kontextmenü bleibt im Fenster
- Modal setzt den Fokus ins Eingabefeld
- Escape schließt das Modal
----------------------------------------
<!-- funktion -->
Kontextmenü schließen
<!-- ziel -->
Räumt ein offenes Menü aus dem Weg
<!-- regeln -->
- Klick daneben schließt es
- Ein neues Menü verdrängt das alte
<!-- testen -->
- RMB auf eine Karte
- LMB daneben
- Menü geschlossen

View File

@@ -5,40 +5,53 @@ Features
kern kern
---------------------------------------- ----------------------------------------
<!-- ziel --> <!-- ziel -->
Gruppiert Informationen zu ähnlichen Funktionen Hält fest, was ein Projekt können soll
---------------------------------------- ----------------------------------------
<!-- regeln --> <!-- regeln -->
- Eine Markdown-Datei je Feature
- Die Datei ist die Wahrheit
- Titel bestimmt den Dateinamen
- Namensgleichheit bekommt eine Nummer
- Fremde Marker bleiben erhalten
- Alphabetisch sortiert - Alphabetisch sortiert
- Unter daten als md gespeichert - Anpassen nur im Vollmodus
- Verlassen speichert - Verlassen speichert
- Escape verwirft - Escape verwirft
---------------------------------------- ----------------------------------------
<!-- funktion --> <!-- funktion -->
Anzeigemodus umschalten Anzeigemodus umschalten
<!-- ziel --> <!-- ziel -->
Erlaubt schnelles Lesen oder detaillierte Bearbeitung Kompaktmodus zum Lesen, Vollmodus zum Anpassen
<!-- regeln --> <!-- regeln -->
- Standard ist Kompaktmodus - Standard ist Kompaktmodus
- Vollmodus geht über responsive Spalten
- Vollmodus zeigt alle Felder - Vollmodus zeigt alle Felder
- Kompaktmodus zeigt Ziel und Funktionstitel als Liste
- Augen-Icon rechts oben - Augen-Icon rechts oben
<!-- testen --> <!-- testen -->
- MMB auf Karte - MMB auf Featurecard
- Modus der Karte wechselt - Modus umgeschaltet
- Alternativ Klick auf Augen-Icon
<!-- funktion --> <!-- funktion -->
Badge umschalten Feature anlegen
<!-- ziel -->
Unterscheidet die Kern- und optionalen Features
<!-- regeln --> <!-- regeln -->
- Kernfarbe blau - +-Icon
- Optionalfarbe grün - Feature öffnet im Vollmodus
- Verändert die Randfarbe vom Feature <!-- testen -->
- Prüfung meldet falsche Einstufung - RMB auf Hauptbereich
- Klick auf "Feature erstellen"
- Namen im Modal eingeben
- Bestätigen mit LMB
- Feature angelegt
<!-- funktion -->
Feature einstufen
<!-- ziel -->
Trennt tragende von verzichtbaren Features
<!-- regeln -->
- Kern blau umrandet
- Optional grün umrandet
- Badge zeigt die Einstufung
<!-- testen --> <!-- testen -->
- Vollmodus öffnen - Vollmodus öffnen
- Klick auf Badge mit LMB - LMB auf Badge klicken
- Einstufung gewechselt
<!-- funktion --> <!-- funktion -->
Feature entfernen Feature entfernen
<!-- regeln --> <!-- regeln -->
@@ -46,41 +59,28 @@ Feature entfernen
<!-- testen --> <!-- testen -->
- RMB auf Featurecard - RMB auf Featurecard
- Klick auf "Feature entfernen" - Klick auf "Feature entfernen"
- Bestätigen mit LMB im Modal - Bestätigen im Modal
- Feature entfernt - Feature entfernt
<!-- funktion --> <!-- funktion -->
Feature erstellen
<!-- regeln -->
- +-Icon
- Feature in Vollmodus geöffnet
<!-- testen -->
- RMB auf Hauptbereich
- Klick auf "Feature erstellen"
- Im Modal Namen eingeben
- Bestätigen mit LMB
<!-- funktion -->
Regeln anpassen Regeln anpassen
<!-- ziel -->
Spezifische Hinweise für Features
<!-- testen --> <!-- testen -->
- Vollmodus öffnen - Vollmodus öffnen
- Klick auf Regeln - LMB auf Regeln
- Inhalt angepasst - Text ändern
- Regeln angepasst
<!-- funktion --> <!-- funktion -->
Titel anpassen Titel anpassen
<!-- regeln --> <!-- regeln -->
- Aktualisiert den Dateinamen - Aktualisiert den Dateinamen
<!-- testen --> <!-- testen -->
- Vollmodus öffnen - Vollmodus öffnen
- Klick auf Titel - LMB auf Titel
- Inhalt ändern - Text ändern
- Titel aktualisiert - Titel angepasst
<!-- funktion --> <!-- funktion -->
Ziel anpassen Ziel anpassen
<!-- ziel -->
Beschreibt kurz das Kernziel vom Feature
<!-- testen --> <!-- testen -->
- Vollmodus öffnen - Vollmodus öffnen
- Klick auf Ziel - LMB auf Ziel
- Inhalt anpassen - Text ändern
- Ziel angepasst - Ziel angepasst

View File

@@ -5,15 +5,30 @@ Funktionen
kern kern
---------------------------------------- ----------------------------------------
<!-- ziel --> <!-- ziel -->
Konkrete Funktionen zum Feature Zerlegt ein Feature in prüfbare Stücke
---------------------------------------- ----------------------------------------
<!-- regeln --> <!-- regeln -->
- Gehört zu genau einem Feature
- Alphabetisch sortiert - Alphabetisch sortiert
- Ziel bleibt freiwillig
- Regeln bleiben freiwillig
- Testen bleibt freiwillig
- Anpassen nur im Vollmodus - Anpassen nur im Vollmodus
- Verlassen speichert - Verlassen speichert
- Escape verwirft - Escape verwirft
---------------------------------------- ----------------------------------------
<!-- funktion --> <!-- funktion -->
Funktion anlegen
<!-- regeln -->
- +-Icon
<!-- testen -->
- Vollmodus öffnen
- RMB auf Featurecard
- Klick auf "Funktion erstellen"
- Namen im Modal eingeben
- Bestätigen mit LMB
- Funktion angelegt
<!-- funktion -->
Funktion entfernen Funktion entfernen
<!-- regeln --> <!-- regeln -->
- x-Icon - x-Icon
@@ -24,48 +39,34 @@ Funktion entfernen
- Bestätigen im Modal - Bestätigen im Modal
- Funktion entfernt - Funktion entfernt
<!-- funktion --> <!-- funktion -->
Funktion erstellen Titel anpassen
<!-- regeln -->
- +-Icon
<!-- testen --> <!-- testen -->
- Vollmodus öffnen - Vollmodus öffnen
- RMB auf Featurecard - LMB auf Funktionstitel
- Klick auf "Funktion erstellen" - Text ändern
- Namen im Modal eingeben - Titel angepasst
- Funktion erstellt <!-- funktion -->
Ziel anpassen
<!-- testen -->
- Vollmodus öffnen
- LMB auf Ziel der Funktion
- Text ändern
- Ziel angepasst
<!-- funktion --> <!-- funktion -->
Regeln anpassen Regeln anpassen
<!-- ziel -->
Spezifische Hinweise für die Funktion
<!-- regeln --> <!-- regeln -->
- Eine Regel je Zeile - Eine Regel je Zeile
<!-- testen --> <!-- testen -->
- Vollmodus öffnen - Vollmodus öffnen
- Klick auf Regeln der Funktion - LMB auf Regeln der Funktion
- Text ändern
- Regeln angepasst - Regeln angepasst
<!-- funktion --> <!-- funktion -->
Testen anpassen Testschritte anpassen
<!-- ziel -->
Beschreibt die Schritte zum Prüfen der Funktion
<!-- regeln --> <!-- regeln -->
- Ein Schritt je Zeile - Ein Schritt je Zeile
<!-- testen --> <!-- testen -->
- Vollmodus öffnen - Vollmodus öffnen
- Klick auf Testen der Funktion - LMB auf Testen der Funktion
- Tests angepasst - Text ändern
<!-- funktion --> - Testschritte angepasst
Titel anpassen
<!-- ziel -->
Beschreibt die Funktion so kompakt wie möglich
<!-- testen -->
- Vollmodus öffnen
- Klick auf Titel der Funktion
- Titel angepasst
<!-- funktion -->
Ziel anpassen
<!-- ziel -->
Beschreibt kurz das Kernziel der Funktion
<!-- testen -->
- Vollmodus öffnen
- Klick auf Ziel der Funktion
- Ziel angepasst

View File

@@ -5,82 +5,98 @@ Karten prüfen
optional optional
---------------------------------------- ----------------------------------------
<!-- ziel --> <!-- ziel -->
Prüft alle Karten auf Stilvorgaben und schlägt Verbesserungen vor Meldet, wo eine Karte vom Soll abweicht
---------------------------------------- ----------------------------------------
<!-- regeln --> <!-- regeln -->
- Verwendet Minimax M2.7-HS per API - Ein Lauf je Projekt
- Alle Inhalte der Features haben globale Layouts und Stile - Ergebnis überlebt keinen Neustart
- API Zugänge stehen in .env - Zugang steht in .env
- Fehlender Zugang bricht sichtbar ab
- Jeder Vorschlag ist für sich anwendbar
- Jeder Vorschlag nennt seinen Beleg
- Eine Katalogdatei je Feld im Ordner stil
- Neue Katalogdatei wirkt ohne Codeänderung
---------------------------------------- ----------------------------------------
<!-- funktion --> <!-- funktion -->
Änderungsvorschläge anzeigen Alle Vorschläge annehmen
<!-- regeln --> <!-- regeln -->
- Annehmen als Haken-Icon: ✓ - Haken-Icon oben in der Karte
- Ablehnen als x-Icon: ✕ - Zahl nennt die Menge
- Icons stehen oben rechts <!-- testen -->
- Angenommener Vorschlag landet in der Datei - Prüfung durchlaufen lassen
- LMB auf Haken-Icon der Karte
- Alle Vorschläge übernommen
<!-- funktion -->
Alle Vorschläge verwerfen
<!-- regeln -->
- x-Icon oben in der Karte
<!-- testen -->
- Prüfung durchlaufen lassen
- LMB auf x-Icon der Karte
- Alle Vorschläge verworfen
<!-- funktion -->
Code in Features prüfen
<!-- ziel -->
Findet Bedienstellen ohne Funktion auf einer Karte
<!-- regeln -->
- Bedienstellen kommen aus dem Code
- Fünf von sechs Stimmen entscheiden
<!-- testen -->
- LMB auf Aufwärts-Icon
- Fehlende Funktionen gelistet
<!-- funktion -->
Features im Code prüfen
<!-- ziel -->
Findet Funktionen ohne Entsprechung im Code
<!-- regeln -->
- Sechs Stimmen suchen den Beleg
- Ein Fund genügt zum Freispruch
<!-- testen -->
- LMB auf Abwärts-Icon
- Funktionen ohne Code gelistet
<!-- funktion -->
Problemstelle anspringen
<!-- ziel -->
Führt vom Panel-Eintrag zur betroffenen Karte
<!-- regeln -->
- Karte blitzt kurz auf
<!-- testen -->
- Prüfung durchlaufen lassen
- LMB auf Eintrag im Panel
- Karte im Editor sichtbar
<!-- funktion -->
Prüfung beenden
<!-- regeln -->
- x-Icon oben im Panel
<!-- testen -->
- Prüfung durchlaufen lassen
- LMB auf x-Icon im Panel
- Panel geschlossen
<!-- funktion -->
Stil prüfen
<!-- ziel -->
Prüft die Form gegen den Katalog
<!-- regeln -->
- Prüf-Icon in der Sidebar
- Wortzahl und Satzzeichen misst der Code
<!-- testen -->
- LMB auf Prüf-Icon
- Fortschritt unter dem Icon
- Fehlerhafte Karten rot markiert
<!-- funktion -->
Vorschlag annehmen
<!-- regeln -->
- Haken-Icon: ✓
- Übrige Vorschläge bleiben stehen - Übrige Vorschläge bleiben stehen
<!-- testen --> <!-- testen -->
- Prüfung durchlaufen lassen - Prüfung durchlaufen lassen
- Unter den fehlerhaften Problemen werden Verbesserungsvorschläge angezeigt - LMB auf Haken-Icon
- Beide Icons angezeigt - Vorschlag in der Datei
<!-- funktion --> <!-- funktion -->
Badge prüfen Vorschlag verwerfen
<!-- ziel -->
Prüft die Einstufung als Kern oder Optional
<!-- regeln --> <!-- regeln -->
- Nur kern und optional erlaubt - x-Icon: ✕
- Einstufung muss zum Ziel passen
- Falscher Badge wird rot markiert
<!-- testen --> <!-- testen -->
- Prüfung durchlaufen lassen - Prüfung durchlaufen lassen
- Badge der Karte rot markiert - LMB auf x-Icon
- Vorschlag unter dem Titel angezeigt - Vorschlag verschwunden
<!-- funktion -->
Fehlerhafte Cards markieren
<!-- regeln -->
- Fehlerhafte Cards werden in Vollmodus angezeigt
<!-- testen -->
- Prüfung durchlaufen
- Fehlerhafte Cards werden rot markiert
- In den Cards werden die Probleme rot markiert
<!-- funktion -->
Prüfung ausführen
<!-- regeln -->
- Prüf-Icon in Sidebar Navigation
<!-- testen -->
- LMB auf Icon
- Prüfung gestartet
- Rechte Sidebar geöffnet
- Fortschritt unter Icon angezeigt
- Probleme live gelistet
<!-- funktion -->
Stellen einzeln prüfen
<!-- ziel -->
Jede Stelle bekommt eigene Vorgaben und einen eigenen Ausschnitt
<!-- regeln -->
- Eine Anfrage je Kartenebene
- Eine Anfrage je Funktion
- Kartenebene prüft Titel, Badge, Ziel, Regeln
- Funktion prüft Titel, Ziel, Regeln, Testen
- Prüfer sieht nur seinen Ausschnitt
- Herkunft eines Problems stammt aus dem Code
<!-- testen -->
- Prüfung starten
- Fortschritt beobachten
- Probleme mit Herkunft gelistet
<!-- funktion -->
Stil-Katalog ablegen
<!-- ziel -->
Trennt die Vorgaben je Feld voneinander
<!-- regeln -->
- Eine Datei je Feld im Ordner stil
- allgemein gilt für beide Prüfer
- feature-Präfix gilt für die Kartenebene
- funktion-Präfix gilt für die Funktionen
- Neue Datei wirkt ohne Codeänderung
- Leerer Ordner bricht die Prüfung ab
<!-- testen -->
- Datei im Ordner stil ergänzen
- Prüfung starten
- Neue Vorgabe wirkt

View File

@@ -1,49 +0,0 @@
<!-- titel -->
Karten Schreibstil
----------------------------------------
<!-- art -->
kern
----------------------------------------
<!-- ziel -->
----------------------------------------
<!-- regeln -->
- Gilt für alle Karten
- Feld leer lassen statt füllen
----------------------------------------
<!-- funktion -->
Badge einstufen
<!-- ziel -->
Stuft die Karte als Kern oder Optional ein
<!-- regeln -->
- Nur kern oder optional
- kern wenn das Projekt es braucht
- Im Zweifel kern
<!-- funktion -->
Regel schreiben
<!-- ziel -->
Spezifische Hinweise zum Titel
<!-- regeln -->
- Ein kurzer Satz je Regel
- Eine Aussage je Zeile
<!-- funktion -->
Testschritt schreiben
<!-- ziel -->
Beschreibt das Prüfen als Ablauf
<!-- regeln -->
- Ein Schritt je Zeile
- Letzte Zeile beschreibt Endzustand
<!-- funktion -->
Titel schreiben
<!-- ziel -->
Benennt die Sache so kompakt wie möglich
<!-- regeln -->
- Nur wenige Wörter
- Kurzsatz bei Funktionen
<!-- funktion -->
Ziel schreiben
<!-- ziel -->
Beschreibt kurz den Kern
<!-- regeln -->
- 1-2 simple Sätze
- Nur wenn der Titel nicht selbsterklärend ist

View File

@@ -8,44 +8,46 @@ kern
Gruppiert Features für ein Projekt Gruppiert Features für ein Projekt
---------------------------------------- ----------------------------------------
<!-- regeln --> <!-- regeln -->
- Ordner unter daten ist das Projekt
- Alphabetisch sortiert - Alphabetisch sortiert
- Aktives Projekt ist hervorgehoben - Aktives Projekt ist hervorgehoben
- Auswahl überlebt das Neuladen
- Entfernen nimmt die Features mit
- Verlassen speichert - Verlassen speichert
- Escape bricht ab - Escape bricht ab
---------------------------------------- ----------------------------------------
<!-- funktion --> <!-- funktion -->
Projekt anpassen Projekt anlegen
<!-- regeln -->
- +-Icon
- Projekt wird direkt aufgerufen
<!-- testen -->
- RMB auf Sidebar
- Klick auf "Projekt erstellen"
- Namen im Modal eingeben
- Bestätigen mit LMB
- Projekt angelegt
<!-- funktion -->
Projekt aufrufen
<!-- testen -->
- LMB auf Projektname
- Features angezeigt
<!-- funktion -->
Projekt umbenennen
<!-- regeln --> <!-- regeln -->
- Stift-Icon - Stift-Icon
<!-- testen --> <!-- testen -->
- RMB auf Projektname - RMB auf Projektname
- Klick auf "Projekt anpassen" - Klick auf "Projekt anpassen"
- Inhalt anpassen - Namen ändern
- Außerhalb klicken - Außerhalb klicken
- Name ist angepasst - Name angepasst
<!-- funktion -->
Projekt aufrufen
<!-- testen -->
- LMB auf Projektname
- Features werden angezeigt
<!-- funktion --> <!-- funktion -->
Projekt entfernen Projekt entfernen
<!-- regeln --> <!-- regeln -->
- x-Icon - x-Icon
- Features mit entfernen
<!-- testen --> <!-- testen -->
- RMB auf Projektname - RMB auf Projektname
- Klick auf "Projekt entfernen" - Klick auf "Projekt entfernen"
- Bestätigen im Modal - Bestätigen im Modal
- Projekt entfernt - Projekt entfernt
<!-- funktion -->
Projekt erstellen
<!-- regeln -->
- Projekt wird direkt aufgerufen
- +-Icon
<!-- testen -->
- RMB auf Sidebar
- Klick auf "Projekt erstellen"
- Im Modal Namen eingeben
- Bestätigen
- Projekt erstellt

View File

@@ -89,7 +89,8 @@ onMounted(async () => {
<span v-if="aktivesProjekt" class="pruefen"> <span v-if="aktivesProjekt" class="pruefen">
<span class="knoepfe"> <span class="knoepfe">
<button class="icon-knopf" title="Stil prüfen" @click="startePruefung(aktivesProjekt, 'stil')">⌾</button> <button class="icon-knopf" title="Stil prüfen" @click="startePruefung(aktivesProjekt, 'stil')">⌾</button>
<button class="icon-knopf" title="Mit Code abgleichen" @click="startePruefung(aktivesProjekt, 'inhalt')">⌗</button> <button class="icon-knopf" title="Features im Code: was steht ohne Code da?" @click="startePruefung(aktivesProjekt, 'feature')">↧</button>
<button class="icon-knopf" title="Code in Features: was fehlt auf den Karten?" @click="startePruefung(aktivesProjekt, 'code')">↥</button>
</span> </span>
<span v-if="pruefungLaeuft" class="fortschritt">{{ pruefungProzent }} %</span> <span v-if="pruefungLaeuft" class="fortschritt">{{ pruefungProzent }} %</span>
</span> </span>

View File

@@ -1,7 +1,11 @@
<script setup> <script setup>
import { art, laeuft, prozent, fertig, gesamt, meldung, probleme, schliesse } from '../composables/pruefung.js' import { art, laeuft, prozent, fertig, gesamt, meldung, probleme, schliesse } from '../composables/pruefung.js'
const UEBERSCHRIFT = { stil: 'Stilprüfung', inhalt: 'Abgleich mit Code' } const UEBERSCHRIFT = {
stil: 'Stilprüfung',
feature: 'Features im Code',
code: 'Code in Features',
}
// Nur die erste Zeile — mehrzeilige Stellen liefen hier sonst zu einem Klumpen zusammen // Nur die erste Zeile — mehrzeilige Stellen liefen hier sonst zu einem Klumpen zusammen
function erste(text) { function erste(text) {