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
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
kann? Gesucht werden Lücken — Fähigkeiten ohne Funktion auf einer Karte.
Zwei weitere Prüfungen, in beide Richtungen. Beide arbeiten mit derselben
Beweislast, nur andersherum: ein Vorhandensein lässt sich zitieren, ein
Nichtvorhandensein nicht.
```
1 Inventar quellcode.py Bedienstellen aus dem Code, deterministisch
2 Abdecken 6 Stimmen deckt eine bestehende Funktion die Stelle ab?
3 Ergänzen Code 5 von 6 sagen nein -> Vorschlag mit Fundort
↧ Features im Code steht hinter jeder Funktion auch Code?
↥ Code in Features hat jede Bedienstelle auch eine Funktion?
```
**Inventar.** `interaktionen()` liest die Stellen aus, an denen ein Mensch etwas
auslöst: Ereignisse im Markup und Einträge der Kontextmenüs. Zusammengefasst nach
dem aufgerufenen Namen — `starte('ziel')` steht zwölfmal da und ist eine Fähigkeit.
Ein Modell hatte die Liste anfangs frei aufgezählt: jede Runde eine andere.
**↧ Features im Code.** Je Funktion sechs unabhängige Versuche, die Codestelle zu
finden. Jeder Fund wird gegen die Datei geschlagen. Gemeldet wird erst, wenn keine
einzige Stimme etwas findet — auch ein misslungenes Zitat gilt als Widerspruch, denn
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,
Speichern beim Verlassen, Abbrechen mit Escape.
**↥ Code in Features.** Die Liste der Bedienstellen kommt aus dem Code, nicht aus
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
Funktion dahinter und dem vollen Text aller Karten. Nur Titel reichten nicht — der
Richter musste raten und urteilte jede Runde anders.
Nicht im Inventar: Backend-Routen (jede hängt an einem Knopf, der schon dasteht),
Zuweisungen, Speichern beim Verlassen, Abbrechen mit Escape.
**Schwelle.** Fünf von sechs Stimmen. Einstimmigkeit war zu scharf: dieselbe Frage
kippt 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.
**Warum diese Schwellen.** Einstimmigkeit war für ↥ zu scharf: dieselbe Frage kippt
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. 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
Funktion hat keinen Code, weg damit" — ist bewusst nicht eingebaut: ein
Nichtvorhandensein lässt sich nicht belegen, und die Vorschläge zielten wiederholt
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.
Nachgemessen an diesem Projekt: auf gepflegten Karten melden beide nichts. Eine
untergeschobene Funktion ohne Code fällt ↧ in jedem Lauf auf. Fehlende Funktionen
findet ↥, nach dem Übernehmen ist es still.
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:

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))

View File

@@ -403,16 +403,21 @@ def _konsens(fassungen: list[dict]) -> dict:
return max(fassungen, key=naehe)
def _mehrheit(proben: list[dict]) -> dict:
"""Nur was mehrere Stimmen unabhängig bemängeln, wird ein Vorschlag.
def _mehrheit(proben: list[dict], gemessen: tuple = ()) -> dict:
"""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
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)
return {
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)]
if not gute:
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]:
@@ -457,38 +473,73 @@ async def _stil_aufgaben(projekt: str):
"""Prüfstellen der Stilprüfung samt der Kataloge, die sie brauchen."""
stil = {b: _stil_lesen(b) for b in BEREICHE}
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):
"""Ein Abgleich über alle Karten — Vollständigkeit ist eine Eigenschaft der
Menge, nicht der einzelnen Karte."""
def _quelle(projekt: str):
alle = features.liste(projekt)
quelle = quellcode.dateien()
if not quelle:
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)
anzahl = sum(len(f["sektionen"]["funktionen"]) for f in alle)
async def abgleich(_):
probleme, _inv = await inhaltspruefung.pruefe(alle, quelle, text)
return probleme
async def abgleich(_, zaehl):
return await inhaltspruefung.pruefe_feature_im_code(alle, quelle, text, zaehl)
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):
lauf = _laeufe[projekt]
schranke = asyncio.Semaphore(config.PRUEFUNG_PARALLEL)
def zaehl():
lauf["fertig"] += 1
async def eine(aufgabe):
if aufgabe is None:
return # Platzhalter: zählt der Sammellauf selbst hoch
async with schranke:
if _laeufe.get(projekt) is not lauf:
return # verworfen (Panel geschlossen) — keine weitere Anfrage
try:
lauf["probleme"].extend(await arbeite(aufgabe))
lauf["probleme"].extend(await arbeite(aufgabe, zaehl))
except Exception as e:
# Zählt trotzdem als erledigt, sonst hängt der Fortschritt.
# 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"] = (
f"{lauf['fehler']} von {lauf['gesamt']} Prüfungen fehlgeschlagen: {e}"
)
lauf["fertig"] += 1
if aufgabe is not True:
lauf["fertig"] += 1
try:
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)'
r'[\w.]*\s*=\s*"([^"]*)"')
_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$]*")
# 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
zwölfmal im Markup, ist aber eine Fähigkeit.
Backend-Routen stehen bewusst nicht drin. Sie sind die Verdrahtung hinter den
Knöpfen, nicht die Bedienung — als eigene Einträge erzeugten sie nur Meldungen
wie „Prüfungsstatus abrufen", die niemand auf einer Karte sehen will.
Dazu Fenster-Lauscher: ein Tastaturgriff ist eine Fähigkeit, auch ohne Knopf.
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
raus, gesehen = [], set()
@@ -137,6 +141,9 @@ def interaktionen(quelle: dict = None) -> list[dict]:
for nr, zeile in enumerate(zeilen, 1):
for m in _MENUEPUNKT.finditer(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"):
continue
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
assert schluessel.count("bedienen:starte") == 1
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
# Technische Aufhänger und Zuweisungen sind keine Fähigkeiten
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"),
f("Anmeldung beginnen"), f("Völlig anderer Text"))
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"
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 features
import projekte
@@ -147,7 +149,8 @@ async def test_unverbesserlicher_vorschlag_faellt_weg(datenordner, monkeypatch):
monkeypatch.setattr(agents, "frage", sturr)
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):

View File

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

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
----------------------------------------
<!-- ziel -->
Gruppiert Informationen zu ähnlichen Funktionen
Hält fest, was ein Projekt können soll
----------------------------------------
<!-- 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
- Unter daten als md gespeichert
- Anpassen nur im Vollmodus
- Verlassen speichert
- Escape verwirft
----------------------------------------
<!-- funktion -->
Anzeigemodus umschalten
<!-- ziel -->
Erlaubt schnelles Lesen oder detaillierte Bearbeitung
Kompaktmodus zum Lesen, Vollmodus zum Anpassen
<!-- regeln -->
- Standard ist Kompaktmodus
- Vollmodus geht über responsive Spalten
- Vollmodus zeigt alle Felder
- Kompaktmodus zeigt Ziel und Funktionstitel als Liste
- Augen-Icon rechts oben
<!-- testen -->
- MMB auf Karte
- Modus der Karte wechselt
- Alternativ Klick auf Augen-Icon
- MMB auf Featurecard
- Modus umgeschaltet
<!-- funktion -->
Badge umschalten
<!-- ziel -->
Unterscheidet die Kern- und optionalen Features
Feature anlegen
<!-- regeln -->
- Kernfarbe blau
- Optionalfarbe grün
- Verändert die Randfarbe vom Feature
- Prüfung meldet falsche Einstufung
- +-Icon
- Feature öffnet im Vollmodus
<!-- testen -->
- 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 -->
- Vollmodus öffnen
- Klick auf Badge mit LMB
- LMB auf Badge klicken
- Einstufung gewechselt
<!-- funktion -->
Feature entfernen
<!-- regeln -->
@@ -46,41 +59,28 @@ Feature entfernen
<!-- testen -->
- RMB auf Featurecard
- Klick auf "Feature entfernen"
- Bestätigen mit LMB im Modal
- Bestätigen im Modal
- Feature entfernt
<!-- 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
<!-- ziel -->
Spezifische Hinweise für Features
<!-- testen -->
- Vollmodus öffnen
- Klick auf Regeln
- Inhalt angepasst
- LMB auf Regeln
- Text ändern
- Regeln angepasst
<!-- funktion -->
Titel anpassen
<!-- regeln -->
- Aktualisiert den Dateinamen
<!-- testen -->
- Vollmodus öffnen
- Klick auf Titel
- Inhalt ändern
- Titel aktualisiert
- LMB auf Titel
- Text ändern
- Titel angepasst
<!-- funktion -->
Ziel anpassen
<!-- ziel -->
Beschreibt kurz das Kernziel vom Feature
<!-- testen -->
- Vollmodus öffnen
- Klick auf Ziel
- Inhalt anpassen
- LMB auf Ziel
- Text ändern
- Ziel angepasst

View File

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

View File

@@ -5,82 +5,98 @@ Karten prüfen
optional
----------------------------------------
<!-- ziel -->
Prüft alle Karten auf Stilvorgaben und schlägt Verbesserungen vor
Meldet, wo eine Karte vom Soll abweicht
----------------------------------------
<!-- regeln -->
- Verwendet Minimax M2.7-HS per API
- Alle Inhalte der Features haben globale Layouts und Stile
- API Zugänge stehen in .env
- Ein Lauf je Projekt
- Ergebnis überlebt keinen Neustart
- 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 -->
Änderungsvorschläge anzeigen
Alle Vorschläge annehmen
<!-- regeln -->
- Annehmen als Haken-Icon: ✓
- Ablehnen als x-Icon: ✕
- Icons stehen oben rechts
- Angenommener Vorschlag landet in der Datei
- Haken-Icon oben in der Karte
- Zahl nennt die Menge
<!-- testen -->
- 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
<!-- testen -->
- Prüfung durchlaufen lassen
- Unter den fehlerhaften Problemen werden Verbesserungsvorschläge angezeigt
- Beide Icons angezeigt
- LMB auf Haken-Icon
- Vorschlag in der Datei
<!-- funktion -->
Badge prüfen
<!-- ziel -->
Prüft die Einstufung als Kern oder Optional
Vorschlag verwerfen
<!-- regeln -->
- Nur kern und optional erlaubt
- Einstufung muss zum Ziel passen
- Falscher Badge wird rot markiert
- x-Icon: ✕
<!-- testen -->
- Prüfung durchlaufen lassen
- Badge der Karte rot markiert
- Vorschlag unter dem Titel angezeigt
<!-- 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
- LMB auf x-Icon
- Vorschlag verschwunden

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
----------------------------------------
<!-- regeln -->
- Ordner unter daten ist das Projekt
- Alphabetisch sortiert
- Aktives Projekt ist hervorgehoben
- Auswahl überlebt das Neuladen
- Entfernen nimmt die Features mit
- Verlassen speichert
- Escape bricht ab
----------------------------------------
<!-- 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 -->
- Stift-Icon
<!-- testen -->
- RMB auf Projektname
- Klick auf "Projekt anpassen"
- Inhalt anpassen
- Namen ändern
- Außerhalb klicken
- Name ist angepasst
<!-- funktion -->
Projekt aufrufen
<!-- testen -->
- LMB auf Projektname
- Features werden angezeigt
- Name angepasst
<!-- funktion -->
Projekt entfernen
<!-- regeln -->
- x-Icon
- Features mit entfernen
<!-- testen -->
- RMB auf Projektname
- Klick auf "Projekt entfernen"
- Bestätigen im Modal
- 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 class="knoepfe">
<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 v-if="pruefungLaeuft" class="fortschritt">{{ pruefungProzent }} %</span>
</span>

View File

@@ -1,7 +1,11 @@
<script setup>
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
function erste(text) {