diff --git a/README.md b/README.md index 187fe71..c41dfd7 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/backend/inhalt.py b/backend/inhalt.py index c0a5004..a83a65d 100644 --- a/backend/inhalt.py +++ b/backend/inhalt.py @@ -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)) diff --git a/backend/pruefung.py b/backend/pruefung.py index 001d1c4..0a9c262 100644 --- a/backend/pruefung.py +++ b/backend/pruefung.py @@ -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)) diff --git a/backend/quellcode.py b/backend/quellcode.py index cc878c3..c7a1d52 100644 --- a/backend/quellcode.py +++ b/backend/quellcode.py @@ -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): diff --git a/backend/tests/test_inhalt.py b/backend/tests/test_inhalt.py index 1338552..1a35e3d 100644 --- a/backend/tests/test_inhalt.py +++ b/backend/tests/test_inhalt.py @@ -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) diff --git a/backend/tests/test_pruefung.py b/backend/tests/test_pruefung.py index 761a5f6..b15ccc9 100644 --- a/backend/tests/test_pruefung.py +++ b/backend/tests/test_pruefung.py @@ -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 diff --git a/backend/tests/test_struktur.py b/backend/tests/test_struktur.py index 7b9cfd0..510b2ce 100644 --- a/backend/tests/test_struktur.py +++ b/backend/tests/test_struktur.py @@ -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": ""}, 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): diff --git a/daten/Planer/darkmode.md b/daten/Planer/darkmode.md index 2d0b51f..75fba55 100644 --- a/daten/Planer/darkmode.md +++ b/daten/Planer/darkmode.md @@ -5,10 +5,12 @@ Darkmode optional ---------------------------------------- -Zwischen Tag- und Nachtmodus wechseln +Passt die Anwendung ans Umgebungslicht an ---------------------------------------- -- Standard sind die Systemeinstellungen +- Auto folgt der Systemeinstellung +- Zustand überlebt das Neuladen +- Kein Flackern beim Laden ---------------------------------------- Darkmode umschalten @@ -16,11 +18,6 @@ Darkmode umschalten - Nacht-Icon: ☾ - Tag-Icon: ☀ - Auto-Icon: A -- Zustand bleibt gespeichert - LMB auf Sidebar-Icon -- Erster Modus ist Auto -- LMB auf Icon -- Zweiter Modus ist Tag -- LMB auf Icon -- Dritter Modus ist Nacht +- Modus gewechselt diff --git a/daten/Planer/design.md b/daten/Planer/design.md new file mode 100644 index 0000000..22282bb --- /dev/null +++ b/daten/Planer/design.md @@ -0,0 +1,31 @@ + +Design +---------------------------------------- + +optional +---------------------------------------- + +Hält die Oberfläche lesbar und ruhig +---------------------------------------- + +- 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 +---------------------------------------- + +Kontextmenü schließen + +Räumt ein offenes Menü aus dem Weg + +- Klick daneben schließt es +- Ein neues Menü verdrängt das alte + +- RMB auf eine Karte +- LMB daneben +- Menü geschlossen diff --git a/daten/Planer/features.md b/daten/Planer/features.md index 465e514..f446b77 100644 --- a/daten/Planer/features.md +++ b/daten/Planer/features.md @@ -5,40 +5,53 @@ Features kern ---------------------------------------- -Gruppiert Informationen zu ähnlichen Funktionen +Hält fest, was ein Projekt können soll ---------------------------------------- +- 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 ---------------------------------------- Anzeigemodus umschalten -Erlaubt schnelles Lesen oder detaillierte Bearbeitung +Kompaktmodus zum Lesen, Vollmodus zum Anpassen - Standard ist Kompaktmodus -- Vollmodus geht über responsive Spalten - Vollmodus zeigt alle Felder -- Kompaktmodus zeigt Ziel und Funktionstitel als Liste - Augen-Icon rechts oben -- MMB auf Karte -- Modus der Karte wechselt -- Alternativ Klick auf Augen-Icon +- MMB auf Featurecard +- Modus umgeschaltet -Badge umschalten - -Unterscheidet die Kern- und optionalen Features +Feature anlegen -- Kernfarbe blau -- Optionalfarbe grün -- Verändert die Randfarbe vom Feature -- Prüfung meldet falsche Einstufung +- +-Icon +- Feature öffnet im Vollmodus + +- RMB auf Hauptbereich +- Klick auf "Feature erstellen" +- Namen im Modal eingeben +- Bestätigen mit LMB +- Feature angelegt + +Feature einstufen + +Trennt tragende von verzichtbaren Features + +- Kern blau umrandet +- Optional grün umrandet +- Badge zeigt die Einstufung - Vollmodus öffnen -- Klick auf Badge mit LMB +- LMB auf Badge klicken +- Einstufung gewechselt Feature entfernen @@ -46,41 +59,28 @@ Feature entfernen - RMB auf Featurecard - Klick auf "Feature entfernen" -- Bestätigen mit LMB im Modal +- Bestätigen im Modal - Feature entfernt -Feature erstellen - -- +-Icon -- Feature in Vollmodus geöffnet - -- RMB auf Hauptbereich -- Klick auf "Feature erstellen" -- Im Modal Namen eingeben -- Bestätigen mit LMB - Regeln anpassen - -Spezifische Hinweise für Features - Vollmodus öffnen -- Klick auf Regeln -- Inhalt angepasst +- LMB auf Regeln +- Text ändern +- Regeln angepasst Titel anpassen - Aktualisiert den Dateinamen - Vollmodus öffnen -- Klick auf Titel -- Inhalt ändern -- Titel aktualisiert +- LMB auf Titel +- Text ändern +- Titel angepasst Ziel anpassen - -Beschreibt kurz das Kernziel vom Feature - Vollmodus öffnen -- Klick auf Ziel -- Inhalt anpassen +- LMB auf Ziel +- Text ändern - Ziel angepasst diff --git a/daten/Planer/funktionen.md b/daten/Planer/funktionen.md index a094216..9d7d0dd 100644 --- a/daten/Planer/funktionen.md +++ b/daten/Planer/funktionen.md @@ -5,15 +5,30 @@ Funktionen kern ---------------------------------------- -Konkrete Funktionen zum Feature +Zerlegt ein Feature in prüfbare Stücke ---------------------------------------- +- 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 anlegen + +- +-Icon + +- Vollmodus öffnen +- RMB auf Featurecard +- Klick auf "Funktion erstellen" +- Namen im Modal eingeben +- Bestätigen mit LMB +- Funktion angelegt + Funktion entfernen - x-Icon @@ -24,48 +39,34 @@ Funktion entfernen - Bestätigen im Modal - Funktion entfernt -Funktion erstellen - -- +-Icon +Titel anpassen - Vollmodus öffnen -- RMB auf Featurecard -- Klick auf "Funktion erstellen" -- Namen im Modal eingeben -- Funktion erstellt +- LMB auf Funktionstitel +- Text ändern +- Titel angepasst + +Ziel anpassen + +- Vollmodus öffnen +- LMB auf Ziel der Funktion +- Text ändern +- Ziel angepasst Regeln anpassen - -Spezifische Hinweise für die Funktion - Eine Regel je Zeile - Vollmodus öffnen -- Klick auf Regeln der Funktion +- LMB auf Regeln der Funktion +- Text ändern - Regeln angepasst -Testen anpassen - -Beschreibt die Schritte zum Prüfen der Funktion +Testschritte anpassen - Ein Schritt je Zeile - Vollmodus öffnen -- Klick auf Testen der Funktion -- Tests angepasst - -Titel anpassen - -Beschreibt die Funktion so kompakt wie möglich - -- Vollmodus öffnen -- Klick auf Titel der Funktion -- Titel angepasst - -Ziel anpassen - -Beschreibt kurz das Kernziel der Funktion - -- Vollmodus öffnen -- Klick auf Ziel der Funktion -- Ziel angepasst +- LMB auf Testen der Funktion +- Text ändern +- Testschritte angepasst diff --git a/daten/Planer/karten-pruefen.md b/daten/Planer/karten-pruefen.md index d9a7016..66e6e51 100644 --- a/daten/Planer/karten-pruefen.md +++ b/daten/Planer/karten-pruefen.md @@ -5,82 +5,98 @@ Karten prüfen optional ---------------------------------------- -Prüft alle Karten auf Stilvorgaben und schlägt Verbesserungen vor +Meldet, wo eine Karte vom Soll abweicht ---------------------------------------- -- 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 ---------------------------------------- -Änderungsvorschläge anzeigen +Alle Vorschläge annehmen -- 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 + +- Prüfung durchlaufen lassen +- LMB auf Haken-Icon der Karte +- Alle Vorschläge übernommen + +Alle Vorschläge verwerfen + +- x-Icon oben in der Karte + +- Prüfung durchlaufen lassen +- LMB auf x-Icon der Karte +- Alle Vorschläge verworfen + +Code in Features prüfen + +Findet Bedienstellen ohne Funktion auf einer Karte + +- Bedienstellen kommen aus dem Code +- Fünf von sechs Stimmen entscheiden + +- LMB auf Aufwärts-Icon +- Fehlende Funktionen gelistet + +Features im Code prüfen + +Findet Funktionen ohne Entsprechung im Code + +- Sechs Stimmen suchen den Beleg +- Ein Fund genügt zum Freispruch + +- LMB auf Abwärts-Icon +- Funktionen ohne Code gelistet + +Problemstelle anspringen + +Führt vom Panel-Eintrag zur betroffenen Karte + +- Karte blitzt kurz auf + +- Prüfung durchlaufen lassen +- LMB auf Eintrag im Panel +- Karte im Editor sichtbar + +Prüfung beenden + +- x-Icon oben im Panel + +- Prüfung durchlaufen lassen +- LMB auf x-Icon im Panel +- Panel geschlossen + +Stil prüfen + +Prüft die Form gegen den Katalog + +- Prüf-Icon in der Sidebar +- Wortzahl und Satzzeichen misst der Code + +- LMB auf Prüf-Icon +- Fortschritt unter dem Icon +- Fehlerhafte Karten rot markiert + +Vorschlag annehmen + +- Haken-Icon: ✓ - Übrige Vorschläge bleiben stehen - Prüfung durchlaufen lassen -- Unter den fehlerhaften Problemen werden Verbesserungsvorschläge angezeigt -- Beide Icons angezeigt +- LMB auf Haken-Icon +- Vorschlag in der Datei -Badge prüfen - -Prüft die Einstufung als Kern oder Optional +Vorschlag verwerfen -- Nur kern und optional erlaubt -- Einstufung muss zum Ziel passen -- Falscher Badge wird rot markiert +- x-Icon: ✕ - Prüfung durchlaufen lassen -- Badge der Karte rot markiert -- Vorschlag unter dem Titel angezeigt - -Fehlerhafte Cards markieren - -- Fehlerhafte Cards werden in Vollmodus angezeigt - -- Prüfung durchlaufen -- Fehlerhafte Cards werden rot markiert -- In den Cards werden die Probleme rot markiert - -Prüfung ausführen - -- Prüf-Icon in Sidebar Navigation - -- LMB auf Icon -- Prüfung gestartet -- Rechte Sidebar geöffnet -- Fortschritt unter Icon angezeigt -- Probleme live gelistet - -Stellen einzeln prüfen - -Jede Stelle bekommt eigene Vorgaben und einen eigenen Ausschnitt - -- 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 - -- Prüfung starten -- Fortschritt beobachten -- Probleme mit Herkunft gelistet - -Stil-Katalog ablegen - -Trennt die Vorgaben je Feld voneinander - -- 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 - -- Datei im Ordner stil ergänzen -- Prüfung starten -- Neue Vorgabe wirkt +- LMB auf x-Icon +- Vorschlag verschwunden diff --git a/daten/Planer/karten-schreibstil.md b/daten/Planer/karten-schreibstil.md deleted file mode 100644 index 0a3a928..0000000 --- a/daten/Planer/karten-schreibstil.md +++ /dev/null @@ -1,49 +0,0 @@ - -Karten Schreibstil ----------------------------------------- - -kern ----------------------------------------- - - ----------------------------------------- - -- Gilt für alle Karten -- Feld leer lassen statt füllen ----------------------------------------- - -Badge einstufen - -Stuft die Karte als Kern oder Optional ein - -- Nur kern oder optional -- kern wenn das Projekt es braucht -- Im Zweifel kern - -Regel schreiben - -Spezifische Hinweise zum Titel - -- Ein kurzer Satz je Regel -- Eine Aussage je Zeile - -Testschritt schreiben - -Beschreibt das Prüfen als Ablauf - -- Ein Schritt je Zeile -- Letzte Zeile beschreibt Endzustand - -Titel schreiben - -Benennt die Sache so kompakt wie möglich - -- Nur wenige Wörter -- Kurzsatz bei Funktionen - -Ziel schreiben - -Beschreibt kurz den Kern - -- 1-2 simple Sätze -- Nur wenn der Titel nicht selbsterklärend ist diff --git a/daten/Planer/projekte.md b/daten/Planer/projekte.md index 7abfadb..8faed7a 100644 --- a/daten/Planer/projekte.md +++ b/daten/Planer/projekte.md @@ -8,44 +8,46 @@ kern Gruppiert Features für ein Projekt ---------------------------------------- +- 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 ---------------------------------------- -Projekt anpassen +Projekt anlegen + +- +-Icon +- Projekt wird direkt aufgerufen + +- RMB auf Sidebar +- Klick auf "Projekt erstellen" +- Namen im Modal eingeben +- Bestätigen mit LMB +- Projekt angelegt + +Projekt aufrufen + +- LMB auf Projektname +- Features angezeigt + +Projekt umbenennen - Stift-Icon - RMB auf Projektname - Klick auf "Projekt anpassen" -- Inhalt anpassen +- Namen ändern - Außerhalb klicken -- Name ist angepasst - -Projekt aufrufen - -- LMB auf Projektname -- Features werden angezeigt +- Name angepasst Projekt entfernen - x-Icon -- Features mit entfernen - RMB auf Projektname - Klick auf "Projekt entfernen" - Bestätigen im Modal - Projekt entfernt - -Projekt erstellen - -- Projekt wird direkt aufgerufen -- +-Icon - -- RMB auf Sidebar -- Klick auf "Projekt erstellen" -- Im Modal Namen eingeben -- Bestätigen -- Projekt erstellt diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 789c5fc..0af5a25 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -89,7 +89,8 @@ onMounted(async () => { - + + {{ pruefungProzent }} % diff --git a/frontend/src/components/PruefPanel.vue b/frontend/src/components/PruefPanel.vue index a7a5334..af56c23 100644 --- a/frontend/src/components/PruefPanel.vue +++ b/frontend/src/components/PruefPanel.vue @@ -1,7 +1,11 @@