This commit is contained in:
team3
2026-07-29 15:19:41 +02:00
parent 2e86d682eb
commit 5d42b4cfc5
155 changed files with 14482 additions and 1736 deletions

451
LEARNINGS.md Normal file
View File

@@ -0,0 +1,451 @@
# Learnings
Was beim Bau von planer2 gemessen wurde. Nicht was plausibel klang — was sich
nachweisen ließ. Geschrieben, bevor Prüfung 3 entfernt wurde, damit ihr Wissen
bleibt.
Stand: 29.07.2026
## Der Aufbau in einem Absatz
Markdown-Dateien sind die Wahrheit. Eine Karte ist ein Auftrag, eine Funktion
etwas, das man für sich testen kann, eine Regel eine Zusicherung. Der Code eines
fremden Projekts ist Beleg, nie Quelle der Wahrheit. Drei Prüfungen glichen ab:
Stil, „hat jede Funktion Code" und „hat jede Codestelle eine Funktion".
## 1. Was funktioniert hat
### Beweislast statt Meinung
Jede Aussage über den Code muss eine wörtlich zitierte Zeile mitbringen.
`quellcode.belegt()` schlägt sie gegen die Datei. Ein misslungenes Zitat zählt
nicht als Fund.
Das ist der wichtigste Baustein des ganzen Systems. Ohne ihn schlug ein früher
Versuch vor, gültige Inhalte zu löschen — das Modell urteilte direkt und lag
falsch. Mit ihm ist ein Fund nachprüfbar und ein Nichtfund die einzige Aussage,
die überhaupt Stimmen braucht.
**Die Asymmetrie ist der Hebel.** Vorhandensein lässt sich zitieren,
Nichtvorhandensein nicht. Also: ein geprüfter Fund genügt zum Freispruch, sechs
Fehlversuche sind die Anklage.
### Das Urteilsgedächtnis
Ein Urteil kostet sechs Anfragen und fällt bei Grenzfällen mal so, mal so. Genau
daran scheiterte die Wiederholbarkeit: derselbe Stand lieferte in Runde 1 einen
Befund, in Runde 2 keinen, in Runde 3 wieder einen.
Gespeichert wird deshalb das Urteil, nicht nur das Ergebnis (`ledger.py`,
`.pruefung.json` im Projektordner). Ein Eintrag gilt weiter, solange seine
Voraussetzung nachweisbar dasteht:
- ein Beleg, solange die zitierte Zeile noch in der Datei steht
- eine Fehlmeldung, solange sich am Quelltext nichts geändert hat
Das machte gleichen Stand zu gleichem Ergebnis. Ohne dieses Gedächtnis gibt es
keine Konvergenz — der Loop würfelt jede Runde neu.
**Der Zuschnitt des Fingerabdrucks entscheidet über die Kosten.** Anfangs ging
der Abdruck über den ganzen Funktionstext. Eine Stilkorrektur an einer Regel
tötete damit auch das Inhaltsurteil. Gemessen: R1 code 384 Anfragen, R2 code 384
Anfragen — null Treffer im Gedächtnis. Nach der Kürzung auf Karten- und
Funktionstitel: R3 feature 30 statt 259, R3 code 21 statt 201. Rund 88 %
Trefferquote, sobald die Stiländerungen abgeklungen sind.
Regel daraus: **ein Urteil verfällt nur, wenn sich ändert, worauf es tatsächlich
beruht.** Nicht wenn sich der Text drumherum ändert.
### Frühabbruch statt fester Stimmenzahl
Sechs Stimmen je Einheit waren gemessen richtig für die *Schwelle*, aber
verschwenderisch für den *Normalfall*. Ein nachgeschlagener Beleg beendet die
Frage — die anderen fünf Antworten wurden bezahlt und weggeworfen.
Stufen (1, dann 2, dann 3) mit Abbruchprüfung nach jeder Stufe:
- Prüfung 2: von 6 auf ~1 Anfrage im Normalfall
- Prüfung 3: von 6 auf ~3
Gesamter Loop auf planer2: **2719 → 1354 Anfragen** bei gleicher Rundenzahl.
**Eine Falle dabei:** Eine ausgefallene Stimme ist keine Ja-Stimme. Der erste
Wurf brach ab, sobald die verbleibenden Stimmen die Schwelle rechnerisch nicht
mehr reißen konnten — und hielt damit einen HTTP-429 für „ist abgedeckt". Fix:
nur abbrechen, wenn alle gestellten Fragen auch beantwortet wurden.
### Schwellen misst man, man rät sie nicht
Alle Zahlen in `inhalt.py` stammen aus Messungen an gepflanzten Lücken:
- `FEHLT_AB = 4` von 6 — eine echte Lücke war immer einstimmig; strittige Stellen
lagen darunter und kippten zwischen Läufen
- `TITEL_KANAL = 0.85`, `INHALT_KANAL = 0.7` — darunter explodierten die
Kandidatenpaare
- `KERN_MIN = 3` — unter drei Wörtern kennt die Überlappung nur 0, 0,5 und 1
- `BUENDEL = 6` Stellen je Anfrage — größere Bündel verloren Stellen aus der Mitte
Der Versuch, Grenzfälle *lexikalisch* zu filtern, ist gescheitert: „Produkt
merken" und „Konfiguration merken" sehen identisch aus, einer ist eine echte
Lücke, der andere nicht. Die Lösung war eine Entscheidung, keine Formel:
empfindlich sein (4 von 6) und Ablehnungen dauerhaft merken.
### Der Musterkatalog als Daten
Was eine auslösbare Stelle ist, steht in `muster/*.toml`, nicht im Code. Ein neues
Rahmenwerk ist eine Datei, keine Codeänderung.
Auslöser dafür: Bei einem Shopware-Plugin fand das Inventar genau *eine* Stelle,
weil Twig, Symfony-Attribute und `element.addEventListener` als Regex im Programm
nicht vorkamen. Nach dem Katalog: 19 Stellen.
Zwei Nachbesserungen, die viel ausmachten:
- `store.reroll()` ist die Fähigkeit `reroll`, nicht `store`. Ohne diese Regel
fielen zwölf Knöpfe eines Stores auf einen einzigen Eintrag zusammen.
- Symfony schreibt Routen benannt (`path: '/x'`) **oder** positionell
(`Route('/x', ...)`). Nur die benannte Form zu kennen fand 5 von 127 Routen.
### Erzeugen aus dem Inventar, nicht aus dem Quelltext
Der Erzeuger geht von den Stellen aus, nicht vom Code als Ganzem. Damit ist
Vollständigkeit Bauart, nicht Hoffnung: jede Stelle bekommt eine Funktion, jede
Funktion stammt von mindestens einer Stelle.
Was das Modell frei aufzählt, war jede Runde anders, und niemand konnte sagen, ob
etwas fehlt.
Vier Schritte: benennen (je Stelle) → falten (gleiche Titel zusammenlegen) →
verdichten (alle Funktionen nebeneinander neu ordnen) → füllen (je Funktion Ziel,
Regeln, Testschritte aus dem Code).
**Verdichten ist der Schritt, an dem Minimalität entsteht.** Ohne ihn kamen aus
21 Stellen 17 Karten mit je einer Funktion. Mit ihm: 5 Karten, 19 Funktionen.
### Nachfassen statt Verlust
Fällt eine Anfrage aus, fehlen sechs Stellen auf einmal. Bei 19 Stellen ist das
fast die Hälfte — und die Karten waren still unvollständig. Gemessen: 8 von 19
Stellen blieben unbenannt.
Fix: kleinere Bündel nachfragen (6 → 2 → 1), und was auch einzeln keinen Namen
bekommt, aus dem Schlüssel benennen. Ein grober Titel fällt auf; eine fehlende
Stelle fällt niemandem auf.
Dasselbe Muster beim Verdichten: fiel eine Zuordnung aus, blieben ihre zwölf
Funktionen auf Einzelkarten stehen — aus 19 Fähigkeiten wurden wieder 16 Karten.
## 2. Was nicht funktioniert hat
### Prüfung 3 — drei Konstruktionsfehler
Die Prüfung „hat jede Codestelle eine Funktion" hat nie zuverlässig funktioniert.
Nicht wegen des Modells, sondern wegen der Grundmenge, gegen die sie zählt.
Gemessen an planer2 (64 Stellen, 45 Funktionen):
**1. Eingabefelder fehlen ganz.** `v-model="suche"` ist kein Muster. Vier solcher
Felder im Projekt, keins im Inventar. Die Feature-Suche existiert, wird aber nie
gezählt und nie gemeldet.
**2. Argumente fallen weg.** Der Schlüssel ist der Funktionsname. Alle vier
Prüfknöpfe rufen `startePruefung(projekt, art)` → eine Stelle statt vier. Vier
Fähigkeiten, ein Punkt.
**3. Gleichnamige Funktionen fallen zusammen.** `alleAnnehmen` gibt es in
`FeatureKarte.vue` und in `FeatureListe.vue` — zwei verschiedene Dinge, ein
Punkt. Der neue Sammelknopf „Alle N Karten anlegen" ist dadurch unsichtbar.
Ergebnis: Gegen das eigene Inventar meldete Prüfung 3 **62 von 64 = 97 %**. Gegen
das, was das Programm wirklich kann, waren es **rund 44 von 52 = 85 %**.
**Dazu zwei falsche Abgedeckt-Urteile.** `DELETE /pruefung` und `GET /chat`
tragen keine Funktion. Prüfung 3 meldete null Lücken — das Modell hielt sie für
gedeckt, weil eine ähnlich klingende Funktion danebenstand.
### Zusicherungen ohne Auslöser sind unsichtbar
Der größte Fund, und er gilt weit über Prüfung 3 hinaus.
Vieles, was ein Programm zusichert, hängt an keinem Auslöser: eine Beschriftung
im Markup, eine Vorbelegung beim ersten Aufruf, die Reihenfolge von Dingen, ein
Standardfall, eine Grenze.
Gemessen an MtoVariantConfigurator: Von acht Anforderungen fehlten **fünf** auf
den erzeugten Karten, obwohl der Code sie umsetzte. Transitivität der Dropdowns,
Reihenfolge aus dem Backend, Vorbelegung aus dem Elternartikel, Schritt-Badge,
Hinweistexte. Alle vorhanden, alle unsichtbar.
Die Vollständigkeitsprüfung meldete dabei **null Lücken** — sie zählt nur
Auslöser.
**Der Fix, der wirkte:** Karten bekamen eigenes Ziel und eigene Regeln, gefüllt
aus den *ganzen Dateien* statt aus fünf Zeilen um die Stelle. Dazu ein
Nachtrag-Schritt je Karte: „was steht im Code und fehlt hier?". Danach 8 von 8.
**Was nicht wirkte:** Der Prompt allein. „Nenne die Sache, nicht die Umsetzung"
stand von Anfang an drin und half nicht.
### Dubletten: Zeichenähnlichkeit fängt Typ 4 nicht
„Galerie vorwärts blättern" und „Galerie nächste anzeigen" sind dieselbe Sache.
Gemessene Titelähnlichkeit: **0,53**. Schwelle: **0,85**. Das Paar wurde nie
Kandidat — kein Modell hat es je gesehen.
Die Klon-Typologie erklärt es: Typ 1 (exakt) und Typ 2 (umbenannt) fängt
String-Matching. Typ 4 (gleiche Bedeutung, andere Worte) nicht.
**Der Fix:** ein vierter Kanal — alles auf derselben Karte wird Kandidat.
Bezahlbar, weil eine Dublette fast immer neben ihrer Zwillingsschwester steht.
Zehn Funktionen ergeben 45 Paare, nicht Tausende. Fand sofort zwei echte
Dubletten, die vier Runden lang durchgerutscht waren.
### Streuung: gemessen, nicht behoben
Gleicher Code, gleiche Einstellungen, andere Karten. Lange nur eine Beobachtung,
dann eine Zahl.
Adjusted Rand Index über die Zerlegung der *Codestellen* (Titel ändern sich
zwischen Läufen, Stellen nicht):
| | Mittel | Minimum |
|---|---|---|
| Karten | **0,34** | 0,28 |
| Funktionen | **0,53** | 0,51 |
Kartenzahl über drei Läufe: 9, 9, 13.
0,34 heißt: die Kartenzuordnung ist näher am Zufall als an einer Entscheidung.
Das ist der ungelöste Kern des Erzeugers.
Was die Streuung *senkte*, aber nicht beseitigte: die Codezeile als Anker beim
Umbenennen. Ohne sie wurde aus „Konfigurator öffnen" ein „Erweiterung
aktivieren" — das Modell sah nur den Titel.
### Der volle Stilkatalog verdirbt Struktur-Antworten
Nach Kartentiteln gefragt, antwortete das Modell mit ganzen Karten samt
Testschritten. Die Beispiele im Katalog sind stärker als die Formatangabe.
Fix: nur die Titelregeln mitgeben. Allgemeine Lehre — **gib einem Schritt nur
den Teil des Katalogs, den er braucht.**
### Zu viel Parallelität
Vier gleichzeitige Läufe → HTTP 429. Ausgefallene Stimmen sahen aus wie „nichts
gefunden". Nacheinander laufen lassen war die Lösung, nicht mehr Wiederholungen.
## 3. Was aus der Recherche belegt ist
Eine Recherche zu Verfahren für vollständige und minimale Extraktion lieferte
Belege für einiges, was hier gebaut wurde, und ein paar Warnungen.
**Bestätigt:**
- *Adaptive Consistency* (Aggarwal et al., EMNLP 2023): Frühabbruch senkt das
Stichprobenbudget „by up to 7.9 times with an average accuracy drop of less
than 0.1%". Deckt sich mit der hier gemessenen Halbierung.
- *Early cutoff* aus der Build-System-Forschung (Mokhov et al., ICFP 2018): ein
Urteil verfällt nur, wenn sich seine echten Abhängigkeiten ändern. Genau das
Problem des zu groben Fingerabdrucks.
- *Mutanten als Ersatz für echte Fehler* (Just et al., FSE 2014): 73 % Kopplung
über 357 echte Fehler. Rechtfertigt gepflanzte Lücken als Vollständigkeitsmaß —
mit der Einschränkung, dass rund ein Viertel ungekoppelt bleibt.
- *Extraktive statt abstraktive Erzeugung* senkt Halluzination messbar. Hier: das
Zitat, das gegen die Datei geschlagen wird.
**Gewarnt:**
- Chao1 und Capture-Recapture schätzen Vollständigkeit systematisch zu niedrig,
besonders bei kleinen Stichproben. Für 19 Einheiten wertlos.
- Testsuite-Minimierung erkauft Größenreduktion mit Erkennungsverlust: „over 70 %"
Reduktion, „around 12.5 %" weniger gefundene Fehler. Aggressive Minimalität hat
einen Preis.
- Self-Consistency hilft bei konvergenter richtiger Antwort und *schadet* bei
systematischem Bias. Mehr Stimmen ersetzen keine bessere Verankerung.
**Nicht anwendbar hier:** Dynamische Invariantenerkennung (Daikon) und
Coverage-Kriterien brauchen einen *laufenden* Zielprozess mit Tests. Das System
liest nur Dateien fremder Projekte.
## 4. Synchronisation zwischen Planung und Code
Zweite Recherche, gezielt auf die Frage: wie hält man eine fachliche Beschreibung
und den Code dauerhaft synchron, in beide Richtungen? 25 Behauptungen gegengeprüft,
13 überlebt, 12 verworfen.
### Beide Richtungen enden bei etwa der Hälfte
**Planung → Code: 53,5 %.** E2EDevBench (50 reale PyPI-Projekte, Anforderungsdokument
als einziger Eingang) misst 53,50 % umgesetzte Anforderungen in der besten
Konfiguration. Ein unabhängig gebauter Benchmark mit anderen Autoren und anderen
Agenten-Rahmenwerken kommt auf 53,75 %. „The other half of the requirements
remained unfulfilled."
Die Abnahme durch einen Menschen ist damit kein Sicherheitsgurt, sondern die
gemessene Notwendigkeit.
**Code → Planung: 59 %.** Polikarpova, Ciupa, Meyer (ISSTA 2009, 25 Klassen, 831
menschliche Klauseln, über 19.500 abgeleitete): Automatische Inferenz findet 59 %
aller von Programmierern geschriebenen Zusicherungen wieder — mit kleiner
Testsuite nur 50 %. Autorenfazit: „contract inference cannot completely replace
the manual work of writing assertions."
Und die Ausbeute ist verrauscht: 90 % der abgeleiteten Klauseln sind korrekt, aber
nur **64 % relevant**. Rund ein Drittel muss aussortiert werden.
### Der brauchbarste Einzelbefund
Der Engpass ist **nicht das Denkvermögen, sondern der Ausdrucksvorrat**.
Von der Abdeckungslücke gehen 31 Prozentpunkte auf „nicht ausdrückbar" und nur
etwa 10 auf „ausdrückbar, aber nicht gefunden" — Verhältnis rund 3:1. Und von den
nicht ausdrückbaren Klauseln scheiterten **54 % allein daran, dass sie eine
Funktion mit einem Argument aufrufen**, weitere 19 % an fehlenden
Implikations-Schablonen.
Die Autoren widersprechen ausdrücklich der naheliegenden Deutung: „It is tempting
to draw the conclusion that at present the tool cannot compete with humans on
higher levels of abstraction. However … 'abstract' assertions are mostly not
expressible by the tool."
Übertragen auf uns, als Hypothese ohne Messung: dem Extraktor das Fachvokabular
mitzugeben — Funktionsnamen, Domänenbegriffe — schlägt vermutlich ein besseres
Modell.
### Die Beleglücke ist der eigentliche Befund
Für drei Dinge liefert der ganze Korpus **keinen einzigen belegten Mechanismus**:
- **Inkrementelle Synchronisation** aus Commits, Diffs, Pull Requests. Alles
Gemessene ist Greenfield-Vollerzeugung.
- **Drift-Erkennung ohne Vollvergleich.** Die einzige geprüfte Behauptung dazu —
Invarianten als Verhaltens-Fingerabdruck — wurde 1:2 verworfen.
- **Zwei-Wege-Konflikte.** In dem untersuchten Spec-Driven-Rahmenwerk kommen die
Begriffe *traceability*, *drift*, *bidirectional* und *round-trip* **null mal**
vor. Diese Verfahren sind einbahnig.
Das heißt nicht, dass es nicht geht. Es heißt, dass niemand es gemessen hat.
### Was gemessen wurde und fast nichts brachte
Prozess- und Strukturarbeit auf der Spezifikationsseite:
- Kontext-Hooks in einem Vier-Phasen-Ablauf (Specify → Plan → Tasks → Implement):
**+3,0 %** der Skala
- Spezifikation nach Epics gliedern: **+1,4 Prozentpunkte** Abdeckung, gleichzeitig
**−4,0 Prozentpunkte** auf der lexikalischen Messgröße
Der einzige stabile Gewinn kommt aus der Erdung im echten Code und aus
ausführbaren Testsuiten. Das deckt sich mit unserem eigenen Fund: der Prompt allein
half nie, das Zeigen ganzer Dateien half sofort.
### Verworfen — und nicht durch die Hintertür zurückholen
Sechs Mechanismen, die für unsere Architektur naheliegend gewesen wären, haben die
Gegenprüfung nicht überstanden:
| Behauptung | Abstimmung |
|---|---|
| Invarianten-Vergleich als Drift-Fingerabdruck | 1:2 |
| LLM-Richter je Anforderung als referenzfreie Abdeckungsmessung | 0:3 |
| Spurious Scenario Rate als Überspezifikations-Melder | 0:3 |
| Einbettungs- statt Wortabdeckung als Messgröße | 0:3 |
| Daikon-Präzision über 95 % gegen ESC/Java | 0:3 |
| Ein Zwischenartefakt Plan/Design schade der Treue | 0:3 |
Besonders bitter: der Fingerabdruck-Gedanke. Er hätte genau unsere Drift-Frage
gelöst.
### Ein Widerspruch, der offen bleibt
Die Behauptung, Inferenz decke systematisch Zusicherungen auf, wo kein Mensch
etwas notiert hatte — im Mittel 4,9-fach so viele relevante Klauseln, 13,3-fach bei
Schleifeninvarianten — wurde als eigenständige Behauptung **0:3 verworfen**.
Dieselben Zahlen tauchen aber in der bestätigten Belegkette einer anderen
Behauptung wieder auf. Der Widerspruch ist ungelöst.
Die Zahlen gelten damit nicht als belegt. Die Richtung — Extraktion ergänzt den
Menschen, ersetzt ihn nicht — ist mehrfach gestützt.
### Grenzen dieser Recherche
- **Alter.** Die härtesten Zahlen (59 %, 64 %, 54 %) sind von 2009 und hängen am
damaligen Schablonensatz. Sie sind **keine Obergrenze für LLM-Extraktion** — dazu
existiert keine Messung.
- **Quellenlage.** Vier der sechs tragenden Quellen sind unbegutachtete Preprints.
Begutachtet sind zwei plus eine Industriefallstudie.
- **Transfergrenze.** Alle harten Zahlen messen maschinenprüfbare Artefakte —
ausführbare Vor- und Nachbedingungen, Eiffel-Kontrakte, Gherkin. Unsere Planung
ist Prosa für Projektmanager. Der Schluss von „59 % der formalen Zusicherungen"
auf „x % der fachlichen Beschreibung" ist Plausibilität, keine Messung.
- **Zirkularität.** Die 53,5 % beruhen auf einem Modell als Richter, das teilweise
eigene Ausgaben bewertet.
### Was das für planer2 heißt
- Prüfung 2 sitzt auf dem belegten Teil. Beleg gegen die Datei geschlagen, ein Fund
genügt, ein Nichtfund braucht Mehrheit.
- Prüfung 3 hätte auf dem unbelegten gesessen. Das Streichen war richtig — und
jetzt auch von außen gestützt.
- Der inkrementelle Normalfall ist Neuland. Kein Beleg dagegen, aber auch keiner
dafür. Wer ihn baut, misst selbst oder weiß nichts.
- Die Kostenlage stützt ihn trotzdem: Vollsynchronisation kostete im Benchmark
~7 $ und 5,24 Mio. gesendete Token je Projekt — für 53,5 %.
## 5. Handkarten gegen erzeugte Karten
Ein direkter Vergleich auf MtoVariantConfigurator.
| | Handkarten | Erzeuger |
|---|---|---|
| Runden bis still | 2 | 3 |
| Anfragen | 361 | 1044 |
| Karten | 7 | 4 |
| Funktionen | 26 | 17 |
| Anforderungen erfüllt | 8 von 8 | 5 von 8 |
Die Handkarten entstanden aus Issue, Wiki **und allen 47 Kommentaren** plus dem
Code. Die Prüfungen änderten daran nur drei Stilkleinigkeiten.
Der Unterschied liegt nicht bei den Funktionen — die findet der Erzeuger. Er
liegt bei den Kartenregeln, also bei den Zusicherungen ohne Auslöser.
**Ein Fehler dabei, der eine eigene Lehre ist:** Beim ersten Anlauf habe ich nur
Beschreibung und Wiki gelesen und „8 von 8 erfüllt" gemeldet. Die 47 Kommentare
standen in derselben Datei. Die Zahl war falsch, weil die Grundmenge falsch war.
Wer Vollständigkeit misst, muss zuerst prüfen, ob er die Quelle ganz gelesen hat.
## 6. Zahlen zum Nachschlagen
Vier Projekte, Loop aus Erzeugen und drei Prüfungen bis zur Ruhe.
| Projekt | Dateien | Stellen | Karten | Funktionen | Runden | Anfragen |
|---|---|---|---|---|---|---|
| MtoVariantConfigurator | 19 | 19 | 4–7 | 17–26 | 2–3 | 361–1044 |
| tft | 44 | 21 | 5 | 20 | 3 | 616 |
| planer2 | 34 | 64 | 8–13 | 46–51 | 4–5 | 1354–2719 |
| MtoRagSystem | 475 | 183 | 23–33 | 150–168 | 5+ | ~4000 |
MtoRagSystem: 6,6 Mio. Zeichen Quelltext. Prüfung 2 schickte anfangs alles je
Funktion und fiel für das ganze Projekt aus. Mit Vorauswahl (erst fragen, welche
Dateien, dann nur die zeigen): 149 von 150 Funktionen belegt, 0 Fehler.
## 7. Offene Fragen
- **Streuung.** ARI 0,34 bei den Karten. Verankerung half, löste es nicht.
Deterministische Gruppierung wäre reproduzierbar, aber nicht zwingend richtig.
- **Grundmenge für Zusicherungen.** Karten-Regeln plus Nachtrag-Schritt haben den
konkreten Fall gelöst. Ein *zählbares* Maß dafür gibt es nicht.
- **Belege je Funktion.** Prüfung 2 merkt sich nur einen Beleg — den ersten, der
sich nachschlagen ließ. Für eine spätere, deterministische Abdeckungsrechnung
bräuchte sie alle.
- **Körnung.** Wann sind zwei Dinge eine Funktion, wann zwei? Es gibt kein
belegtes absolutes Maß dafür ohne Referenzmodell.

View File

@@ -14,8 +14,10 @@ API_VERSION = "2023-06-01"
# Der Provider drosselt bei vielen gleichzeitigen Anfragen
WIEDERHOLBAR = (429, 500, 502, 503, 504)
WIEDERHOLUNGEN = 4
WIEDERHOLUNGEN = 6
PAUSE = 2.0
# Länger als das zu warten hilft nicht mehr; dann ist die Drossel zu weit offen
HOECHSTPAUSE = 60.0
class NichtKonfiguriert(ValueError):
@@ -37,6 +39,15 @@ def pruefe_konfiguration():
raise NichtKonfiguriert(f"Nicht konfiguriert: {', '.join(fehlend)} in .env fehlt")
def _pause(res, versuch: int) -> float:
"""Wie lange bis zum nächsten Versuch — der Provider darf es selbst sagen."""
gesagt = res.headers.get("retry-after")
try:
return min(float(gesagt), HOECHSTPAUSE)
except (TypeError, ValueError):
return min(PAUSE * (2 ** versuch), HOECHSTPAUSE)
async def frage(system: str, inhalt: str, temperatur: float = 0.0) -> str:
"""Eine Anfrage, eine Antwort — der Text der Modellantwort.
Kalt geroutet: Temperatur 0, kein Thinking; die Prüfung soll nicht raten.
@@ -63,7 +74,7 @@ async def frage(system: str, inhalt: str, temperatur: float = 0.0) -> str:
# Zu viele Anfragen oder Serverfehler: warten und noch einmal.
# Ohne das fielen bei großen Läufen ganze Stimmen aus — und ein
# fehlendes Urteil sah aus wie „nichts gefunden".
await asyncio.sleep(PAUSE * (2 ** versuch))
await asyncio.sleep(_pause(res, versuch))
if res.status_code != 200:
raise RuntimeError(f"Provider antwortet mit HTTP {res.status_code}")
antwort = res.json()

292
backend/chat.py Normal file
View File

@@ -0,0 +1,292 @@
"""Karten im Gespräch entwickeln.
Man schreibt, was man vorhat. Die KI antwortet und legt daneben, wie die Karten
danach aussähen. Übernommen wird einzeln — dasselbe ✓ und ✕ wie bei der Prüfung.
Warum kein eigener Anzeigeweg: Ein Vorschlag ist ein Vorschlag, egal ob ihn die
Prüfung oder das Gespräch gemacht hat. Der Chat liefert deshalb genau die Form, die
`pruefung._stellen` erzeugt — Karten zeigen ihn an derselben Stelle an, der Vergleich
funktioniert, Annehmen schreibt die Datei.
Das Modell schreibt nie selbst in eine Datei. Es beschreibt den Sollzustand einer
Karte; was sich dadurch ändert, rechnet der Code aus. Das ist dieselbe Beweislast wie
bei der Prüfung und der Grund, warum ein Missverständnis hier nichts kaputt macht.
"""
import json
import agents
import features
import parser
import projekte
import pruefung
DATEI = ".chat.json"
# So viele Wortmeldungen bleiben stehen. Mehr wäre teuer je Anfrage und hilft wenig:
# was vor zwanzig Schritten besprochen wurde, steht längst auf den Karten.
TIEFE = 30
# Antworten in einem Rutsch. Ein Gespräch verträgt keine sechs Stimmen — es soll
# schnell sein, und was falsch ist, sieht man am Vorschlag.
TEMPERATUR = 0.3
SYSTEM = """Du entwickelst mit einer Person zusammen die Feature-Karten eines Projekts.
Eine Karte ist ein Auftrag: die Person will ein Problem gelöst haben. Die Lösung
besteht aus Regeln und Funktionen. Eine Funktion ist etwas, das man für sich testen
kann. Was nur die Umsetzung betrifft, gehört auf keine Karte.
{stil}
Der aktuelle Stand:
{karten}
Antworte ausschließlich mit JSON:
{{"antwort": "...", "karten": [
{{"titel": "...", "art": "kern", "ziel": "...", "regeln": ["..."],
"funktionen": [{{"titel": "...", "ziel": "...", "regeln": ["..."],
"testen": ["..."]}}]}}
]}}
- antwort: was du der Person sagst, auf Deutsch, höchstens vier Sätze
- karten: nur die Karten, die sich ändern oder dazukommen sollen — jeweils
VOLLSTÄNDIG, so wie sie danach aussehen sollen. Der Stand oben hat dieselbe Form:
kopiere die Karte von dort und ändere nur, was sich ändern soll. Jede Zeile, die
du nicht wörtlich mit abschreibst, gilt als gestrichen.
- art: „kern", wenn die Anwendung ohne die Karte keinen Sinn hätte, sonst „optional"
- ziel, regeln und testen dürfen leer sein oder fehlen
Ist nichts zu ändern, ist karten leer. Ändere nur, wonach gefragt wurde; Karten,
die niemand erwähnt hat, lässt du weg.
Frag nach, was du nicht weißt — aber leg trotzdem einen Entwurf daneben. Eine Frage
ohne Vorschlag hilft niemandem: an einem Entwurf sieht die Person sofort, was du
falsch verstanden hast. Erfinde dabei nichts, was nicht gesagt wurde; lass eine
Funktion lieber knapp als ausgedacht."""
# Die Formatregel steht auch hier, nicht nur im System-Prompt. Bei langen Antworten
# vergaß das Modell sie sonst und schrieb reinen Fließtext — die ganze Wortmeldung
# war verloren.
NACHRICHT = """{verlauf}
Neue Nachricht der Person:
{text}
Antworte ausschließlich mit dem JSON-Objekt, ohne Text davor oder danach."""
NACHBESSERN = """Deine Antwort war kein JSON.
Schreibe sie noch einmal, ausschließlich als das JSON-Objekt aus der Vorgabe —
keine Einleitung, kein Nachsatz, keine Code-Zäune."""
def _pfad(projekt: str):
return projekte.wurzel(projekt) / DATEI
def verlauf(projekt: str) -> list:
try:
eintraege = json.loads(_pfad(projekt).read_text())
except (OSError, ValueError):
return []
return eintraege if isinstance(eintraege, list) else []
def _sichere(projekt: str, eintraege: list):
ziel = _pfad(projekt)
neben = ziel.with_suffix(".json.neu")
neben.write_text(json.dumps(eintraege[-TIEFE:], indent=1, ensure_ascii=False) + "\n")
neben.replace(ziel)
def leeren(projekt: str):
_pfad(projekt).unlink(missing_ok=True)
def _punkte(text: str) -> list:
return [z.strip("- ").strip() for z in (text or "").splitlines() if z.strip()]
def _als_spec(f: dict) -> dict:
"""Eine Karte in genau der Form, in der das Modell antworten soll."""
s = f["sektionen"]
return {"titel": s["titel"], "art": s["art"], "ziel": s["ziel"],
"regeln": _punkte(s["regeln"]),
"funktionen": [{"titel": x["text"], "ziel": x["ziel"],
"regeln": _punkte(x["regeln"]),
"testen": _punkte(x["testen"])}
for x in s["funktionen"]]}
def _kartenstand(projekt: str) -> str:
"""Der Stand als JSON — dieselbe Form wie die Antwort.
Vorher stand er als Fließtext da. Das Modell musste ihn beim Zurückschreiben
übersetzen und schob dabei Regeln zwischen Karten- und Funktionsebene hin und
her: aus „eine Funktion ergänzen" wurden vier Änderungen. Gleiche Form heißt,
dass Abschreiben nichts kostet.
"""
vision, karten = "", []
for f in features.liste(projekt):
if f["sektionen"]["art"] == parser.VISION:
vision = f["sektionen"]["ziel"]
else:
karten.append(_als_spec(f))
text = json.dumps(karten, indent=1, ensure_ascii=False) if karten \
else "Noch keine Karten."
if vision:
text = f"So ist das Projekt gedacht:\n\n{vision}\n\nDie Karten:\n\n{text}"
return text
def _zeilen(wert) -> str:
"""regeln und testen kommen als Liste oder als Block — hier wird eins daraus."""
if isinstance(wert, list):
eintraege = [str(z).strip("- ").strip() for z in wert]
else:
eintraege = [z.strip("- ").strip() for z in str(wert or "").splitlines()]
return "\n".join(f"- {z}" for z in eintraege if z)
TRENNER = "-" * 40
def roh(karte: dict) -> str:
"""Aus dem Sollzustand die Markdown-Datei — dieselbe Form wie im Frontend."""
art = str(karte.get("art") or "kern").strip().lower()
teile = [f'<!-- titel -->\n{str(karte.get("titel") or "").strip()}',
f'<!-- art -->\n{art if art in parser.ARTEN else "kern"}']
if str(karte.get("ziel") or "").strip():
teile.append(f'<!-- ziel -->\n{str(karte["ziel"]).strip()}')
if _zeilen(karte.get("regeln")):
teile.append(f'<!-- regeln -->\n{_zeilen(karte.get("regeln"))}')
for f in karte.get("funktionen") or []:
block = [f'<!-- funktion -->\n{str(f.get("titel") or "").strip()}']
if f.get("stand") == parser.OFFEN:
block.append(f'<!-- stand -->\n{parser.OFFEN}')
if str(f.get("ziel") or "").strip():
block.append(f'<!-- ziel -->\n{str(f["ziel"]).strip()}')
for feld in ("regeln", "testen"):
if _zeilen(f.get(feld)):
block.append(f'<!-- {feld} -->\n{_zeilen(f.get(feld))}')
teile.append("\n".join(block))
return f"\n{TRENNER}\n".join(teile) + "\n"
def _felder(sektionen: dict) -> dict:
return {"titel": sektionen["titel"], "badge": sektionen["art"],
"ziel": sektionen["ziel"], "regeln": sektionen["regeln"],
"funktionen": "\n".join(f["text"] for f in sektionen["funktionen"])}
def _vergleiche(alt: dict, neu: dict, feature_id: str, grund: str) -> list:
"""Was sich an einer bestehenden Karte ändert, Feld für Feld.
Auf Funktionsebene wird nur die Liste der Titel verglichen. Wer den Inhalt einer
Funktion ändern will, bekommt dafür eigene Stellen — sonst wäre ein Vorschlag ein
Klotz, den man nur ganz oder gar nicht nehmen kann.
"""
raus = []
for feld in ("titel", "badge", "ziel", "regeln", "funktionen"):
for p in pruefung._stellen(feld, alt.get(feld, ""), neu.get(feld, ""),
grund, "Aus dem Gespräch"):
p["feature"] = feature_id
p["funktion"] = ""
# Was das Gespräch an Funktionen erfindet, ist bestellt, nicht gebaut.
# Dieselbe Stelle aus Prüfung 3 hätte belegten Code — dort wäre „offen" falsch.
if feld == "funktionen":
p["neu_offen"] = True
raus.append(p)
return raus
def _funktionsstellen(alt: dict, neu: dict, feature_id: str, grund: str) -> list:
"""Änderungen innerhalb einer Funktion, je Feld eine eigene Stelle."""
vorher = {f["text"]: f for f in alt["funktionen"]}
raus = []
for f in neu["funktionen"]:
bestand = vorher.get(f["text"])
if not bestand:
continue # neue Funktion: steckt schon im Listenvergleich
for feld in ("ziel", "regeln", "testen"):
for p in pruefung._stellen(feld, bestand[feld], f[feld], grund,
"Aus dem Gespräch"):
p["feature"] = feature_id
p["funktion"] = f["text"]
raus.append(p)
return raus
def vorschlaege(projekt: str, karten: list) -> list:
"""Aus den Sollzuständen die einzelnen Stellen — bestehende Karten wie neue."""
nach_titel = {f["sektionen"]["titel"].lower(): f for f in features.liste(projekt)}
raus = []
for karte in karten:
if not isinstance(karte, dict) or not str(karte.get("titel") or "").strip():
continue
titel = str(karte["titel"]).strip()
# Den Stand entscheidet der Code, nicht das Modell: was schon gebaut war,
# bleibt gebaut; was das Gespräch neu erfindet, ist bestellt und noch offen.
# Das Modell sieht das Feld gar nicht — ein Feld weniger, das es vergessen kann.
bestand = nach_titel.get(titel.lower())
gebaut = {x["text"] for x in bestand["sektionen"]["funktionen"]
if x["stand"] == parser.GEBAUT} if bestand else set()
for f in karte.get("funktionen") or []:
f["stand"] = parser.GEBAUT if str(f.get("titel") or "").strip() in gebaut \
else parser.OFFEN
text = roh(karte)
neu = parser.zerlege(text)
if bestand is None:
# Neue Karte: es gibt keine Stelle, an der man ansetzen könnte —
# deshalb ein eigener Vorschlag, der die ganze Datei mitbringt.
raus.append({"feld": "neue_karte", "original": "", "vorschlag": titel,
"roh": text, "ganzes_feld": True, "feature": "",
"funktion": "", "regel": "Aus dem Gespräch",
"problem": "Neue Karte"})
continue
grund = f"Aus dem Gespräch: {titel}"
raus += _vergleiche(_felder(bestand["sektionen"]), _felder(neu),
bestand["id"], grund)
raus += _funktionsstellen(bestand["sektionen"], neu, bestand["id"], grund)
return raus
async def frage(projekt: str, text: str) -> dict:
"""Eine Runde: Frage rein, Antwort und Vorschläge raus."""
text = (text or "").strip()
if not text:
raise ValueError("Nachricht ist leer")
agents.pruefe_konfiguration()
bisher = verlauf(projekt)
gespraech = "\n\n".join(f'{"Person" if e["rolle"] == "person" else "Du"}: {e["text"]}'
for e in bisher) or "Noch nichts besprochen."
# Beide Kataloge: der Chat schreibt Karten UND Funktionen. Vorher kannte er nur
# die Kartenregeln und erfand Funktionstitel wie „Galerie im Modal" — ein
# Bereich statt einer Handlung, und die Prüfung merkte es erst hinterher.
system = SYSTEM.format(
stil="\n\n".join(pruefung._stil_lesen(b) for b in pruefung.BEREICHE),
karten=_kartenstand(projekt))
nachricht = NACHRICHT.format(verlauf=gespraech, text=text)
# Ein Ausrutscher beim JSON kostet sonst die ganze Wortmeldung. Zweiter Versuch
# mit etwas mehr Streuung; danach fliegt der Fehler nach oben und ist sichtbar.
daten = None
for versuch in range(3):
antwort = await agents.frage(system, nachricht, TEMPERATUR + 0.1 * versuch)
try:
daten = pruefung._json_lesen(antwort)
break
except ValueError:
if versuch == 2:
raise ValueError("Antwort war nicht lesbar — bitte noch einmal senden")
# Nachbessern statt blind wiederholen: das Modell sieht seinen eigenen
# Fehlversuch und weiß, was daran fehlte.
nachricht = (f"{nachricht}\n\nDein letzter Versuch war:\n"
f"{antwort[:2000]}\n\n{NACHBESSERN}")
satz = str(daten.get("antwort") or "").strip() or "Verstanden."
probleme = vorschlaege(projekt, daten.get("karten") or [])
bisher += [{"rolle": "person", "text": text}, {"rolle": "ki", "text": satz}]
_sichere(projekt, bisher)
return {"antwort": satz, "probleme": probleme, "verlauf": verlauf(projekt)}

View File

@@ -27,6 +27,13 @@ MINIMAX_API_KEY = os.environ.get("MINIMAX_API_KEY", "")
MINIMAX_MODELL = os.environ.get("MINIMAX_MODELL", "")
MINIMAX_URL = os.environ.get("MINIMAX_URL", "")
# Das Gespräch darf woanders laufen als die Prüfungen. Es ist ein einzelner Aufruf
# und lebt von Sprachgefühl; die Prüfungen sind hundert Stimmen und leben von der
# Temperaturstreuung. Nicht gesetzt heißt: alles über MiniMax.
CHAT_API_KEY = os.environ.get("CHAT_API_KEY", "")
CHAT_MODELL = os.environ.get("CHAT_MODELL", "")
CHAT_URL = os.environ.get("CHAT_URL", "https://api.anthropic.com/v1/messages")
def _zahl(name: str, standard: int) -> int:
try:

View File

@@ -6,12 +6,15 @@ import parser
import projekte
_UMLAUTE = str.maketrans({"ä": "ae", "ö": "oe", "ü": "ue", "ß": "ss"})
# Dateisysteme lassen 255 Zeichen je Name zu. Der Titel darf länger sein — er steht
# in der Datei, nicht im Namen. Ohne Deckel warf ein langer Titel einen Serverfehler.
NAMENSLAENGE = 80
def _slug(titel: str) -> str:
s = titel.lower().translate(_UMLAUTE)
s = re.sub(r"[^a-z0-9]+", "-", s).strip("-")
return s or "feature"
return s[:NAMENSLAENGE].strip("-") or "feature"
def _eintrag(datei) -> dict:
@@ -26,8 +29,14 @@ def _datei(projekt: str, id: str):
return datei
def liste(projekt: str) -> list[dict]:
dateien = projekte.wurzel(projekt).glob("*.md")
def liste(projekt: str, nur_stand: bool = False) -> list[dict]:
"""Die Karten. Standardmäßig die, an denen gerade gearbeitet wird.
`nur_stand` liest stattdessen den Projektordner — die Übersicht soll sagen,
was das Projekt heute kann, nicht was jemand gerade plant.
"""
ordner = projekte.stand(projekt) if nur_stand else projekte.wurzel(projekt)
dateien = ordner.glob("*.md")
eintraege = [_eintrag(d) for d in sorted(dateien)]
return sorted(eintraege, key=lambda e: e["sektionen"]["titel"].lower())
@@ -38,17 +47,21 @@ def pruefbar(projekt: str) -> list[dict]:
return [f for f in liste(projekt) if f["sektionen"]["art"] != parser.VISION]
def anlegen(projekt: str, roh: str) -> dict:
ordner = projekte.wurzel(projekt)
titel = parser.zerlege(roh)["titel"]
if not titel:
raise ValueError("Titel fehlt")
slug = _slug(titel)
def _frei(ordner, slug: str):
"""Der erste freie Dateiname zum Slug; gleiche Namen bekommen eine Nummer."""
datei = ordner / f"{slug}.md"
n = 2
while datei.exists():
datei = ordner / f"{slug}-{n}.md"
n += 1
return datei
def anlegen(projekt: str, roh: str) -> dict:
titel = parser.zerlege(roh)["titel"]
if not titel:
raise ValueError("Titel fehlt")
datei = _frei(projekte.wurzel(projekt), _slug(titel))
datei.write_text(roh)
return _eintrag(datei)
@@ -58,11 +71,7 @@ def _folge_titel(datei, titel: str):
slug = _slug(titel)
if re.fullmatch(re.escape(slug) + r"(-\d+)?", datei.stem):
return datei # passt schon (inkl. Kollisions-Suffix)
ziel = datei.parent / f"{slug}.md"
n = 2
while ziel.exists():
ziel = datei.parent / f"{slug}-{n}.md"
n += 1
ziel = _frei(datei.parent, slug)
datei.rename(ziel)
return ziel
@@ -72,9 +81,35 @@ def speichern(projekt: str, id: str, roh: str) -> dict:
titel = parser.zerlege(roh)["titel"]
if not titel:
raise ValueError("Titel fehlt")
vorher = datei.read_text()
datei.write_text(roh)
return _eintrag(_folge_titel(datei, titel))
neu = _folge_titel(datei, titel)
# Ein neuer Titel benennt die Datei um. Der Rückschritt muss beides kennen:
# die neue Datei fällt weg, die alte kommt mit ihrem alten Inhalt zurück.
schritt = {datei.stem: vorher}
if neu.stem != datei.stem:
schritt[neu.stem] = None
return _eintrag(neu)
def loeschen(projekt: str, id: str):
_datei(projekt, id).unlink()
datei = _datei(projekt, id)
titel = parser.zerlege(datei.read_text())["titel"] or id
datei.unlink()
def verschieben(projekt: str, id: str, ziel: str) -> dict:
"""Ein Feature in ein anderes Projekt umhängen.
Kein Kopieren: die Datei wandert, damit es sie nur einmal gibt. Der Verlauf
hält beide Seiten fest — sonst ließe sich der Schritt nicht zurücknehmen.
"""
if ziel == projekt:
raise ValueError("Feature liegt schon in diesem Projekt")
datei = _datei(projekt, id)
roh = datei.read_text()
titel = parser.zerlege(roh)["titel"] or id
# Erst das Ziel auflösen: gibt es das Projekt nicht, ist noch nichts passiert
neu = _frei(projekte.wurzel(ziel), _slug(titel))
datei.rename(neu)
return _eintrag(neu)

View File

@@ -1,748 +0,0 @@
"""Zwei Abgleiche zwischen Karten und Quelltext, in beide Richtungen.
**Feature im Code** — steht hinter jeder Funktion einer Karte auch Code? Dazu:
beschreiben zwei Funktionen dieselbe Fähigkeit?
**Code im Feature** — hat jede Stelle im Code auch eine Funktion auf einer Karte?
Beide arbeiten mit derselben Beweislast, nur andersherum.
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.
Jedes Urteil wandert in `ledger`. Der nächste Lauf fragt nur noch, was er nicht
schon weiß — dadurch liefert derselbe Stand dasselbe Ergebnis. Auch das unentschiedene
Urteil wird gemerkt: es galt als abgedeckt, und ein Münzwurf, der jede Runde neu
fällt, ist schlimmer als einer, der einmal fällt. Ändert sich die Stelle im Code,
wird sie ohnehin neu beurteilt.
"""
import asyncio
import difflib
import re
from collections import Counter
import agents
import config
import ledger
import quellcode
# Ohne Drossel liefen alle Anfragen gleichzeitig los und der Provider antwortete
# mit 429. Die ausgefallenen Stimmen sahen dann aus wie „nichts gefunden".
_schranke = None
def _drossel():
global _schranke
if _schranke is None:
_schranke = asyncio.Semaphore(config.PRUEFUNG_PARALLEL)
return _schranke
async def _frage(system: str, nachricht: str, temperatur: float) -> str:
async with _drossel():
return await agents.frage(system, nachricht, temperatur)
async def _urteil(system: str, nachricht: str, temperatur: float):
"""Eine Stimme als JSON. Unlesbares wird einmal neu gefragt, dann aufgegeben —
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."""
ABDECKUNG = """Diese Stelle im Programm lässt sich auslösen:
{art} in {datei}, Zeile {zeile}
```
{umfeld}
```
{rumpf}
Die Karten des Projekts beschreiben bisher das hier:
{funktionen}
Beschreibt eine dieser Funktionen, was an der Stelle oben passiert?
Wortwahl darf abweichen; entscheidend ist die Sache. Ziel, Regeln und Testschritte
einer Funktion gehören mit zu dem, was sie abdeckt, und die Regeln der Karte
ebenso: eine Zusicherung wie „alphabetisch sortiert" ist beschrieben, auch wenn
kein Knopf dazu gehört.
Antworte ausschließlich mit JSON:
{{"abgedeckt": true, "funktion": "..."}}
oder
{{"abgedeckt": false, "karte": "...", "titel": "..."}}
- funktion: der bestehende Funktionstitel, wörtlich, wenn abgedeckt
- karte: der Titel der Karte, in die die Fähigkeit thematisch gehört
- titel: wie die neue Funktion heißen soll — Objekt und Verb im Infinitiv,
höchstens 5 Wörter
Rein technische Aufhänger gelten als abgedeckt: Speichern beim Verlassen eines
Feldes, Abbrechen mit Escape, Gesundheitsprüfungen, interne Weiterleitung.
Fehlerbehandlung ebenso — eine Fehlermeldung ist kein eigener Auftrag, sondern
gehört zu der Fähigkeit, bei der etwas schiefging.
Im Zweifel abgedeckt. Nur was einer Person wirklich fehlt, soll dazukommen."""
DUBLETTE_SYSTEM = """Du entscheidest, ob zwei Funktionsbeschreibungen dieselbe
Fähigkeit meinen. Antworte ausschließlich mit JSON."""
DUBLETTE = """Zwei Funktionen aus den Karten eines Projekts:
A
{text_a}
B
{text_b}
Aufgefallen sind sie so: {anlass}
Beantworte der Reihe nach, allein aus Titel und Testschritten:
1. Woran arbeitet A? Der Gegenstand, an dem sich etwas ändert.
2. Woran arbeitet B?
3. Meinen 1 und 2 denselben Gegenstand?
4. Was ist anders, nachdem A ausgeführt wurde?
5. Was ist anders, nachdem B ausgeführt wurde?
6. Beschreiben 4 und 5 denselben Zustand? Es zählt das Ergebnis, nicht das Wort:
verschiedene Wörter für dasselbe Ergebnis sind gleich, ein Gegenstück nicht.
Antworte ausschließlich mit JSON:
{{"gegenstand_a": "...", "gegenstand_b": "...", "gegenstand_gleich": true,
"ergebnis_a": "...", "ergebnis_b": "...", "ergebnis_gleich": true}}
Urteile nicht selbst über die Dublette. Ob es eine ist, wird aus deinen Antworten
auf 3 und 6 errechnet."""
# 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
# So viele Stimmen müssen geantwortet haben, bevor überhaupt geurteilt wird. Das
# Urteil wird gemerkt und gilt dann dauerhaft — dafür reichen zwei Überlebende nicht.
URTEIL_AB = 4
# Ab dieser Ähnlichkeit gelten zwei Titel als dieselbe Sache
GLEICH = 0.75
# Drei Stimmen über ein Paar, einstimmig für die Dublette. Ein Löschvorschlag darf
# nicht an einer Einzelmeinung hängen.
DUBLETTE_TEMPERATUREN = (0.0, 0.3, 0.6)
# Kandidatenkanal „ähnlicher Titel". Höher als GLEICH: hier zahlt jedes Paar drei
# Anfragen, und „Feature anlegen"/„Feature entfernen" liegen schon bei 0,75.
TITEL_KANAL = 0.85
# Kandidatenkanal „ähnlicher Inhalt". Anteil der Wörter, die die kürzere der beiden
# Beschreibungen mit der längeren teilt. Überlappung statt Jaccard, weil eine knappe
# Dublette sonst an der Länge der ausführlicheren scheitert.
INHALT_KANAL = 0.7
# Wörter, die in so vielen Funktionen vorkommen, tragen nichts zur Unterscheidung
# bei. „Vollmodus öffnen", „Text ändern" stehen fast überall — ohne diesen Filter
# ähnelt jede Bearbeiten-Funktion jeder anderen zu 90 %.
FLOSKEL_ANTEIL = 0.2
# Deckel gegen Explosion bei vielen Funktionen; was darüber liegt, meldet der Lauf
DUBLETTEN_CAP = 40
# Ein gemerktes Urteil gehört zu der Frage, auf die es geantwortet hat. Wird die
# Frage umgeschrieben, ist das Urteil von gestern — deshalb hängt der Fingerabdruck
# am Wortlaut des Prompts. Sonst müsste man daran denken, eine Zahl hochzuzählen.
_SUCHE_STAND = ledger.abdruck(SUCHE_SYSTEM, SUCHE_NACHRICHT)
_ABDECKUNG_STAND = ledger.abdruck(ABDECKUNG_SYSTEM, ABDECKUNG)
_DUBLETTE_STAND = ledger.abdruck(DUBLETTE_SYSTEM, DUBLETTE)
def _normal(text: str) -> str:
return re.sub(r"[^a-zä-ü0-9 ]", "", (text or "").lower()).strip()
def _aehnlich(a: str, b: str) -> float:
return difflib.SequenceMatcher(None, _normal(a), _normal(b)).ratio()
def _json(antwort: str) -> dict:
import pruefung # zur Laufzeit, sonst Ringschluss
return pruefung._json_lesen(antwort)
async def _sammle(aufgaben: list, was: str, melde=None) -> list:
"""Alle Stellen beurteilen, auch wenn einzelne platzen.
Vorher riss die erste kaputte Stelle den ganzen Lauf ab. Zwei Folgen, beide
schlecht: die schon bezahlten Urteile der anderen waren weg, und das Gedächtnis
wurde gespeichert, während die übrigen Anfragen noch liefen — es blieb leer.
Jetzt wird gewartet, bis alle durch sind. Was ging, zählt; was nicht ging, steht
in der Meldung. Der nächste Lauf holt nur den Rest nach.
"""
ergebnisse = await asyncio.gather(*aufgaben, return_exceptions=True)
kaputt = [e for e in ergebnisse if isinstance(e, BaseException)]
if kaputt and melde:
melde(f"{len(kaputt)} von {len(ergebnisse)} {was} nicht beurteilt: "
f"{kaputt[0]}", len(kaputt))
return [x for x in ergebnisse if x and not isinstance(x, BaseException)]
# ---------------------------------------------------------------- Feature im Code
def _funktionstext(karte: str, f: dict) -> str:
"""Alles, was die Funktion behauptet — danach wird im Code gesucht.
Ohne Karte, wenn die anderswo schon dasteht: doppelt genannt zog sie beim
Dublettenurteil mehr Gewicht auf sich als Titel und Testschritte zusammen.
"""
zeilen = [f"Karte: {karte}"] if karte else []
zeilen.append(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)
def _quellabdruck(quelle: dict) -> str:
return ledger.abdruck(_SUCHE_STAND,
*(f"{p}\n{t}" for p, t in sorted(quelle.items())))
async def feature_im_code(features: list, quelle: dict, quelltext: str, stand: dict,
zaehl=None, melde=None) -> list:
"""Funktionen, für die keine Stimme einen nachprüfbaren Beleg findet.
Ein gemerkter Beleg zählt weiter, solange die zitierte Zeile noch dasteht —
dafür braucht es kein Modell, das prüft `quellcode.belegt()` nach. Eine gemerkte
Fehlanzeige gilt nur, solange sich am Quelltext nichts geändert hat: sie fordert
zum Löschen auf, und dafür muss der Befund frisch sein.
"""
quellstand = _quellabdruck(quelle)
auftraege = [(f, x) for f in features for x in f["sektionen"]["funktionen"]]
bekannt = {ledger.schluessel(f["id"], x["text"]) for f, x in auftraege}
stand["belege"] = ledger.behalte(stand["belege"], bekannt)
stand["ohne_code"] = ledger.behalte(stand["ohne_code"], bekannt)
async def eine(paar):
feature, x = paar
schluessel = ledger.schluessel(feature["id"], x["text"])
text = _funktionstext(feature["sektionen"]["titel"], x)
wie_gehabt = ledger.abdruck(text)
gemerkt = stand["belege"].get(schluessel)
if gemerkt and gemerkt["abdruck"] == wie_gehabt and quellcode.belegt(
quelle, gemerkt["datei"], gemerkt["beleg"]):
if zaehl:
zaehl()
return None
gemerkt = stand["ohne_code"].get(schluessel)
if gemerkt and gemerkt["abdruck"] == wie_gehabt \
and gemerkt["quelle"] == quellstand:
if zaehl:
zaehl()
return {"feature": feature["id"], "titel": x["text"],
"art": "ohne_code", "grund": gemerkt["grund"]}
nachricht = SUCHE_NACHRICHT.format(quelle=quelltext, funktion=text)
proben = await asyncio.gather(*(
_urteil(SUCHE_SYSTEM, nachricht, t) for t in SUCHE_TEMPERATUREN
), return_exceptions=True)
if zaehl:
zaehl()
gruende = []
brauchbar = behauptet = 0
beleg = None
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
datei = str(u.get("datei") or "").strip()
zitat = str(u.get("beleg") or "").strip()
if beleg is None and quellcode.belegt(quelle, datei, zitat):
beleg = {"abdruck": wie_gehabt, "datei": datei, "beleg": zitat}
elif u.get("grund"):
gruende.append(str(u["grund"]).strip())
stand["belege"].pop(schluessel, None)
stand["ohne_code"].pop(schluessel, None)
if beleg:
stand["belege"][schluessel] = beleg
return None # geprüfter Beleg: die Funktion steht im Code
if behauptet:
return None # widersprüchliche Stimmen: nichts behaupten und nichts merken
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")
grund = gruende[0] if gruende else "Kein Beleg im Code gefunden"
stand["ohne_code"][schluessel] = {"abdruck": wie_gehabt, "quelle": quellstand,
"grund": grund}
return {"feature": feature["id"], "titel": x["text"],
"art": "ohne_code", "grund": grund}
return await _sammle([eine(a) for a in auftraege], "Funktionen", melde)
# ---------------------------------------------------------------- Dubletten
def _funktionen(features: list) -> dict:
"""schluessel -> alles, was zur Funktion gehört."""
raus = {}
for f in features:
for x in f["sektionen"]["funktionen"]:
raus[ledger.schluessel(f["id"], x["text"])] = {
"feature": f["id"], "karte": f["sektionen"]["titel"],
"titel": x["text"], "eintrag": x,
"text": _funktionstext("", x),
# Ohne die Feldnamen: „Regeln:" steht über jedem Block, und eine
# Funktion namens „Regeln anpassen" verlöre sonst ihr eigenes Wort
"worte": " ".join((f["sektionen"]["titel"], x["text"], x["ziel"],
x["regeln"], x["testen"]))}
return raus
def _gewicht(f: dict) -> tuple:
"""Wer mehr sagt, bleibt stehen. Bei Gleichstand entscheidet der Name — nicht
schön, aber vorhersagbar; ein Zufallssieger machte den Vorschlag unstet."""
x = f["eintrag"]
zeilen = len([z for z in (x["regeln"] + "\n" + x["testen"]).splitlines() if z.strip()])
return (zeilen, len(x["ziel"]), f["titel"])
_WORT = re.compile(r"[a-zä-üß0-9]{4,}")
# Beugung abschneiden statt richtig stemmen: „entfernen" und „entfernt" sind
# dasselbe Wort, und ein Stemmer legte die Sprache im Code fest.
STAMM = 5
def _kerne(alle: dict) -> dict:
"""Je Funktion die Wörter, die sie von den anderen unterscheiden.
Was in jeder fünften Funktion steht, fliegt raus. Sonst misst der Vergleich nur
noch die gemeinsame Schrittfolge, und „Titel anpassen" und „Ziel anpassen"
stimmen zu hundert Prozent überein.
"""
mengen = {k: set(_WORT.findall(v["worte"].lower())) for k, v in alle.items()}
haeufig = Counter(wort for m in mengen.values() for wort in m)
grenze = max(3, int(len(mengen) * FLOSKEL_ANTEIL))
return {k: {w[:STAMM] for w in m if haeufig[w] < grenze} for k, m in mengen.items()}
def _ueberlappung(a: set, b: set) -> float:
return len(a & b) / min(len(a), len(b)) if a and b else 0.0
def _kandidaten(alle: dict, belege: dict) -> list[tuple]:
"""Paare, die überhaupt eine Dublette sein könnten, mit dem Anlass.
Drei Kanäle, weil eine Dublette auf drei Arten auffällt: dieselbe Codezeile als
Beleg, fast derselbe Titel, fast dieselben Wörter. Der letzte fängt, was die
ersten beiden durchlassen — „Funktion löschen" und „Funktion entfernen" haben
weder denselben Titel noch zwingend denselben Beleg.
Jedes Paar zu fragen wäre quadratisch und träfe fast nie. Erst billig
eingrenzen, dann einmal teuer urteilen.
"""
paare = {}
def merke(a, b, anlass):
if a != b:
paare.setdefault(ledger.paarschluessel(a, b), (a, b, anlass))
nach_stelle = {}
for name, eintrag in belege.items():
if name in alle:
stelle = (eintrag["datei"], re.sub(r"\s+", " ", eintrag["beleg"]).strip())
nach_stelle.setdefault(stelle, []).append(name)
for (datei, zeile), gruppe in nach_stelle.items():
gruppe = sorted(gruppe)
for i, a in enumerate(gruppe):
for b in gruppe[i + 1:]:
merke(a, b, f"Beide belegen dieselbe Zeile in {datei}: {zeile}")
kern = _kerne(alle)
namen = sorted(alle)
for i, a in enumerate(namen):
for b in namen[i + 1:]:
if _aehnlich(alle[a]["titel"], alle[b]["titel"]) >= TITEL_KANAL:
merke(a, b, "Die Titel sind fast gleich.")
elif _ueberlappung(kern[a], kern[b]) >= INHALT_KANAL:
merke(a, b, "Beide Beschreibungen bestehen fast aus denselben Wörtern.")
return [(k, *paare[k]) for k in sorted(paare)]
async def _ist_dublette(a: dict, b: dict, anlass: str) -> bool:
"""Einstimmig gleicher Gegenstand und gleiches Ergebnis — sonst keine Dublette.
Das Gesamturteil rechnet der Code aus den Teilantworten. Gefragt nach der
Dublette an sich, widersprach eine Stimme ihrer eigenen Analyse: derselbe
Gegenstand, dasselbe Tun, und trotzdem „nein". Zwei kleine Fragen beantwortet
sie verlässlich, die große nicht.
Gefragt wird nach dem Ergebnis, nicht nach dem Verb. „Löschen" und „entfernen"
hielten zwei von drei Stimmen für verschiedenes Tun — was danach anders ist,
beschrieben sie gleich.
"""
nachricht = DUBLETTE.format(text_a=a["text"], text_b=b["text"], anlass=anlass)
proben = await asyncio.gather(*(
_urteil(DUBLETTE_SYSTEM, nachricht, t) for t in DUBLETTE_TEMPERATUREN
), return_exceptions=True)
stimmen = [p for p in proben if isinstance(p, dict)]
if len(stimmen) < len(DUBLETTE_TEMPERATUREN):
raise RuntimeError(f'Dublette {a["titel"]}/{b["titel"]}: '
f"nur {len(stimmen)} von "
f"{len(DUBLETTE_TEMPERATUREN)} Stimmen brauchbar")
return all(u.get("gegenstand_gleich") is True and u.get("ergebnis_gleich") is True
for u in stimmen)
async def dubletten(features: list, stand: dict, melde=None) -> list:
"""Funktionen, die eine andere Funktion doppelt beschreiben.
Gemeldet wird immer die schwächere von beiden — die stärkere bleibt stehen.
Ein einmal gefälltes Urteil bleibt gemerkt, auch das ablehnende: sonst zahlt
jeder Lauf erneut für dieselbe Frage und antwortet womöglich anders.
"""
alle = _funktionen(features)
paare = _kandidaten(alle, stand["belege"])
stand["paare"] = ledger.behalte(stand["paare"], {p[0] for p in paare})
if len(paare) > DUBLETTEN_CAP:
if melde:
melde(f"{len(paare)} Kandidatenpaare, geprüft wurden {DUBLETTEN_CAP}")
paare = paare[:DUBLETTEN_CAP]
async def urteile(paar):
schluessel, a, b, anlass = paar
links, rechts = alle[a], alle[b]
wie_gehabt = ledger.abdruck(_DUBLETTE_STAND, links["text"], rechts["text"])
gemerkt = stand["paare"].get(schluessel)
if gemerkt and gemerkt["abdruck"] == wie_gehabt:
gleich = gemerkt["gleich"]
else:
gleich = await _ist_dublette(links, rechts, anlass)
stand["paare"][schluessel] = {"abdruck": wie_gehabt, "gleich": gleich}
if not gleich:
return None
schwach, stark = sorted((links, rechts), key=_gewicht)
return {"feature": schwach["feature"], "titel": schwach["titel"],
"art": "dublette",
"grund": f'Beschreibt „{stark["titel"]}“ '
f'(Karte {stark["karte"]}) ein zweites Mal'}
return await _sammle([urteile(p) for p in paare], "Funktionspaare", melde)
def _wegfall_grund(betroffen: list) -> str:
"""Was die Karte verliert, in einem Halbsatz. Ohne Trennung nach Anlass stand
über einer Dublette „ohne eigenen Code" — beides heißt streichen, aber aus
verschiedenen Gründen, und der Grund entscheidet, ob man zustimmt."""
teile = []
for art, eins, viele in (("dublette", "Funktion doppelt", "Funktionen doppelt"),
("ohne_code", "Funktion ohne eigenen Code",
"Funktionen ohne eigenen Code")):
n = sum(x["art"] == art for x in betroffen)
if n:
teile.append(f"{n} {eins if n == 1 else viele}")
return ", ".join(teile)
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, gesehen = [], set()
for x in ohne_code:
if x["feature"] == f["id"] and x["titel"] not in gesehen:
gesehen.add(x["titel"])
betroffen.append(x)
if not betroffen:
continue
weg = [x["titel"] for x in betroffen]
neu = [t for t in ist if t not in weg]
grund = _wegfall_grund(betroffen)
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.
Die Regeln der Karte gehören dazu. Vieles, was ein Kunde bestellt, ist keine
Bedienung, sondern eine Zusicherung — „eine Datei je Feature", „alphabetisch
sortiert". Ohne sie sah der Richter den Code dahinter als unbeschrieben an.
"""
teile = []
for f in features:
s = f["sektionen"]
block = [f'## Karte: {s["titel"]}']
if s["ziel"]:
block.append(f'Ziel: {s["ziel"]}')
if s["regeln"]:
zeilen = ", ".join(z.strip("- ").strip()
for z in s["regeln"].splitlines() if z.strip())
block.append(f"Regeln der Karte: {zeilen}")
for x in s["funktionen"]:
block.append(f'\n### {x["text"]} (Karte: {s["titel"]})')
if x["ziel"]:
block.append(f'Ziel: {x["ziel"]}')
for feld in ("regeln", "testen"):
if x[feld]:
zeilen = ", ".join(z.strip("- ").strip()
for z in x[feld].splitlines() if z.strip())
block.append(f'{feld.capitalize()}: {zeilen}')
teile.append("\n".join(block))
return "\n\n".join(teile) or "—"
async def _abdeckung(punkt: dict, liste: str) -> list:
rumpf = f'\nDahinter steht:\n\n```\n{punkt["rumpf"]}\n```\n' if punkt["rumpf"] else ""
nachricht = ABDECKUNG.format(funktionen=liste, art=punkt["art"],
datei=punkt["datei"], zeile=punkt["zeile"],
umfeld=punkt["umfeld"], rumpf=rumpf)
proben = await asyncio.gather(*(
_urteil(ABDECKUNG_SYSTEM, nachricht, t) for t in URTEIL_TEMPERATUREN
), return_exceptions=True)
return [p for p in proben if isinstance(p, dict)]
def _naechste_karte(name: str, karten: dict, schwelle: float = 0.6):
if not name or not karten:
return None
treffer = max(karten, key=lambda k: _aehnlich(name, k))
return karten[treffer] if _aehnlich(name, treffer) >= schwelle else None
def _zielkarte(urteile: list, karten: dict):
"""Auf welche Karte gehört die fehlende Funktion?
Die Stimmen schlagen oft Karten vor, die es nicht gibt („Export",
„Datenverwaltung"). Vorher fiel der Befund dann weg — eine Stelle, die
einstimmig fehlte, wurde verschwiegen, weil man sich über die Ablage nicht
einig war. Jetzt gewinnt der erste Vorschlag, der sich auflösen lässt, und
zur Not der ähnlichste. Falsch abgelegt kann man verschieben; verschwiegen nicht.
"""
namen = Counter(str(u.get("karte") or "").strip() for u in urteile
if u.get("abgedeckt") is False and u.get("karte"))
for name, _ in namen.most_common():
if treffer := karten.get(name) or _naechste_karte(name, karten):
return treffer
haeufigste = namen.most_common(1)
return _naechste_karte(haeufigste[0][0], karten, schwelle=0.0) if haeufigste else None
def _stellenabdruck(punkt: dict) -> str:
return ledger.abdruck(_ABDECKUNG_STAND, punkt["beleg"], punkt["umfeld"],
punkt["rumpf"])
async def code_im_feature(features: list, punkte: list, stand: dict,
zaehl=None, melde=None) -> list:
"""Bedienstellen, die von keiner Funktion abgedeckt sind.
Ein gemerktes Urteil gilt, solange es tragen kann: „abgedeckt durch X", solange
X noch auf einer Karte steht; „fehlt als Y", solange es kein Y gibt. Beides
fällt weg, sobald sich die Codestelle selbst ändert.
"""
liste = _funktionsliste(features)
karten = {f["sektionen"]["titel"]: f for f in features}
kennungen = {f["id"] for f in features}
titel_da = [x["text"] for f in features for x in f["sektionen"]["funktionen"]]
normal_da = {_normal(t) for t in titel_da}
stand["abdeckung"] = ledger.behalte(stand["abdeckung"],
{p["schluessel"] for p in punkte})
stand["ohne_karte"] = ledger.behalte(stand["ohne_karte"],
{p["schluessel"] for p in punkte})
async def eine(punkt):
schluessel = punkt["schluessel"]
wie_gehabt = _stellenabdruck(punkt)
gemerkt = stand["abdeckung"].get(schluessel)
if gemerkt and gemerkt["abdruck"] == wie_gehabt \
and (not gemerkt["funktion"] or _normal(gemerkt["funktion"]) in normal_da):
if zaehl:
zaehl()
return None
gemerkt = stand["ohne_karte"].get(schluessel)
if gemerkt and gemerkt["abdruck"] == wie_gehabt \
and gemerkt["feature"] in kennungen \
and not any(_aehnlich(gemerkt["titel"], t) > GLEICH for t in titel_da):
if zaehl:
zaehl()
return {"feature": gemerkt["feature"], "titel": gemerkt["titel"],
"beleg": gemerkt["beleg"]}
urteile = await _abdeckung(punkt, liste)
if zaehl:
zaehl()
nein = sum(u.get("abgedeckt") is False for u in urteile)
ausgefallen = len(URTEIL_TEMPERATUREN) - len(urteile)
if len(urteile) < URTEIL_AB or nein < FEHLT_AB <= nein + ausgefallen:
# Zu dünn oder offen. Dünn: zwei überlebende Stimmen genügen rechnerisch
# für „abgedeckt", weil die Schwelle unerreichbar wird — als Dauerurteil
# wäre das zu wenig. Offen: die ausgefallenen Stimmen hätten es kippen
# können. Beides nicht verschweigen; sonst sähe der Ausfall aus wie „ist
# abgedeckt" und der Lauf meldete stillschweigend zu wenig.
raise RuntimeError(
f'{punkt["schluessel"]}: nur {len(urteile)} von '
f"{len(URTEIL_TEMPERATUREN)} Stimmen brauchbar")
stand["abdeckung"].pop(schluessel, None)
stand["ohne_karte"].pop(schluessel, None)
# Steht das Ergebnis schon fest, zählen die fehlenden Stimmen nicht mehr.
# Vier Stimmen „abgedeckt" bei sechs möglichen können die Schwelle nicht
# mehr reißen — daraus einen Fehlschlag zu machen, wäre Verschwendung.
if nein < FEHLT_AB:
# Abgedeckt — oder unentschieden, was dasselbe Ergebnis hat. Gemerkt
# wird beides; der Name nur, wenn die Stimmen sich auf einen einigen.
namen = Counter(str(u.get("funktion") or "").strip()
for u in urteile if u.get("abgedeckt"))
name, treffer = namen.most_common(1)[0] if namen else ("", 0)
# Nur ein Name, den es wirklich gibt, taugt als Verfallsdatum. Deckt
# eine Regel die Stelle ab, nennt die Stimme deren Wortlaut — der
# stünde auf ewig ungültig in der Datei und würde jeden Lauf neu gefragt.
gilt = treffer >= len(urteile) / 2 and _normal(name) in normal_da
stand["abdeckung"][schluessel] = {"abdruck": wie_gehabt,
"funktion": name if gilt else ""}
return None
treffer = _zielkarte(urteile, karten)
titel = [str(u.get("titel") or "").strip() for u in urteile if u.get("titel")]
if not treffer or not titel:
return None
gefunden = {"feature": treffer["id"],
"titel": max(titel, key=lambda t: sum(_aehnlich(t, x) for x in titel)),
"beleg": f'{punkt["datei"]}:{punkt["zeile"]}'}
stand["ohne_karte"][schluessel] = {"abdruck": wie_gehabt, **gefunden}
return gefunden
gefunden = await _sammle([eine(p) for p in punkte], "Codestellen", melde)
# 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"]
and _aehnlich(y["titel"], x["titel"]) > GLEICH), None)
if gleich:
gleich["beleg"] += f', {x["beleg"]}'
else:
raus.append(dict(x))
return raus
def probleme_code(features: list, fehlend: list) -> list:
"""Ein Vorschlag je Karte: die fehlenden Funktionen anhängen."""
import pruefung
raus = []
for f in features:
ist = [x["text"] for x in f["sektionen"]["funktionen"]]
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
# aus einer Ergänzung einen Umbau machen. Angezeigt wird ohnehin alphabetisch.
neu = list(dict.fromkeys(ist + [x["titel"] 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, beleg):
p["feature"] = f["id"]
p["funktion"] = ""
raus.append(p)
return raus
# ---------------------------------------------------------------- Einstiege
async def pruefe_feature_im_code(projekt: str, features: list, quelle: dict,
quelltext: str, zaehl=None, melde=None) -> list:
stand = ledger.lade(projekt)
try:
ohne = await feature_im_code(features, quelle, quelltext, stand, zaehl, melde)
doppelt = await dubletten(features, stand, melde)
finally:
# Auch ein abgebrochener Lauf hat schon Urteile gefällt; die sind bezahlt
ledger.sichere(projekt, stand)
return probleme_feature(features, ohne + doppelt)
async def pruefe_code_im_feature(projekt: str, features: list, quelle: dict,
zaehl=None, melde=None) -> list:
punkte = quellcode.interaktionen(quelle)
if not punkte:
raise RuntimeError("Keine Bedienstellen im Quelltext gefunden")
stand = ledger.lade(projekt)
try:
fehlend = await code_im_feature(features, punkte, stand, zaehl, melde)
finally:
ledger.sichere(projekt, stand)
return probleme_code(features, fehlend)

BIN
backend/k1-1024.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
backend/k1-1280.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
backend/k1-1600.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

BIN
backend/k1-390.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
backend/k1-640.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
backend/k1-820.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
backend/komfort.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -9,8 +9,7 @@ nur noch, was er nicht schon weiß. Ein Eintrag gilt weiter, solange seine
Voraussetzung nachweisbar dasteht:
- ein Beleg, solange die zitierte Zeile noch in der Datei steht
- eine Abdeckung, solange die nennende Funktion noch auf einer Karte steht
- eine Fehlmeldung, solange keine passende Funktion dazugekommen ist
- eine Fehlanzeige, solange sich am Quelltext nichts geändert hat
Fällt die Voraussetzung weg, fällt der Eintrag weg und wird neu gefragt. Damit
bleibt gleicher Stand gleiches Ergebnis, und geänderter Stand wird wirklich neu
@@ -30,11 +29,10 @@ DATEI = ".pruefung.json"
# Ein Fach je Frage. Getrennt, weil sie unterschiedlich ungültig werden.
FAECHER = (
"stil", # Prüfstelle -> ihr Stilurteil (Prüfung 1)
"belege", # Funktion -> Codestelle, die sie umsetzt (Prüfung 2)
"ohne_code", # Funktion -> kein Code gefunden (Prüfung 2)
"paare", # Funktionspaar -> Dublette oder verschieden (Prüfung 2)
"abdeckung", # Codestelle -> Funktion, die sie beschreibt (Prüfung 3)
"ohne_karte", # Codestelle -> keine Funktion beschreibt sie (Prüfung 3)
)

BIN
backend/offen.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -19,6 +19,14 @@ TRENNER = re.compile(r"^-{3,}\s*$")
VISION = "vision"
ARTEN = ("kern", "optional", VISION)
# Eine Funktion ist entweder geliefert oder noch offen. „offen" heißt: die Karte
# beschreibt sie schon, den Code gibt es noch nicht — bestellt, nicht gebaut.
# Ohne diese Unterscheidung meldet die Vorwärtsprüfung jede geplante Funktion als
# Fehler und will sie streichen.
OFFEN = "offen"
GEBAUT = "gebaut"
STAENDE = (GEBAUT, OFFEN)
def _art(bloecke: list) -> str:
wert = bloecke[0].strip().lower() if bloecke else ""
@@ -61,7 +69,11 @@ def zerlege(roh: str) -> dict:
# vor der ersten Funktion: Karten-Ziel, danach: Ziel der Funktion
(funktionen[-1]["ziel"] if funktionen else ziel).append(text)
elif schluessel == "funktion":
funktionen.append({"text": text, "ziel": [], "regeln": [], "testen": []})
funktionen.append({"text": text, "ziel": [], "regeln": [], "testen": [],
"stand": GEBAUT})
elif schluessel == "stand" and funktionen:
wert = text.strip().lower()
funktionen[-1]["stand"] = wert if wert in STAENDE else GEBAUT
elif schluessel in ("regel", "regeln"):
# vor der ersten Funktion: Karten-Regeln, danach: Regeln der Funktion
(funktionen[-1]["regeln"] if funktionen else regeln).append(text)

163
backend/planung.py Normal file
View File

@@ -0,0 +1,163 @@
"""Eine Planung: der Kartensatz, wie er nach der nächsten Umsetzung aussehen soll.
Die Übersicht beantwortet „was kann mein Projekt". Eine geplante Funktion gehört
dort nicht hin — sie wäre gelogen. Deshalb liegt die Planung in einer eigenen
Kopie und der Projektordner bleibt unberührt, bis sie umgesetzt wird.
Kopiert wird der ganze Ordner, nicht nur die Karten. Gespräch und
Urteilsgedächtnis gehören zur Planung: was während des Planens besprochen und
beurteilt wurde, gilt für den geplanten Stand, nicht für den heutigen.
Solange eine Planung läuft, arbeitet `projekte.wurzel()` in der Kopie. Keine
Schreibfunktion muss davon wissen.
Es gibt höchstens eine Planung je Projekt. Sie endet auf zwei Arten: umgesetzt —
dann ersetzt die Kopie den Projektstand — oder abgebrochen, dann verschwindet sie
spurlos.
"""
import shutil
import parser
import projekte
# Was beim Vergleich zählt: der sichtbare Inhalt einer Funktion, nicht ihre Datei.
# Wird eine Karte umbenannt, wandert ihre Datei — der Vergleich hinge sonst an
# einem Dateinamen statt an dem, was dasteht.
FELDER = ("ziel", "regeln", "testen", "stand")
def laeuft(projekt: str) -> bool:
return (projekte.stand(projekt) / projekte.PLANUNG).is_dir()
def _kopie(projekt: str):
return projekte.stand(projekt) / projekte.PLANUNG
def starte(projekt: str):
"""Arbeitskopie anlegen. Ab hier zeigt `wurzel()` dorthin."""
ziel = _kopie(projekt)
if ziel.is_dir():
raise ValueError("Für dieses Projekt läuft schon eine Planung")
shutil.copytree(projekte.stand(projekt), ziel,
ignore=shutil.ignore_patterns(projekte.PLANUNG))
def brich_ab(projekt: str):
"""Planung verwerfen. Der Projektstand hat sie nie gesehen."""
shutil.rmtree(_kopie(projekt), ignore_errors=True)
def setze_um(projekt: str):
"""Die Kopie wird der neue Projektstand.
Erst die Kopie herausheben, dann den alten Stand räumen, dann die Kopie an
seine Stelle. Ein Absturz mitten im Räumen ließe sonst ein Projekt ohne Karten
zurück — und die Planung gleich mit.
"""
kopie = _kopie(projekt)
if not kopie.is_dir():
raise ValueError("Für dieses Projekt läuft keine Planung")
ordner = projekte.stand(projekt)
daneben = ordner.parent / f"{ordner.name}.neu"
shutil.rmtree(daneben, ignore_errors=True)
kopie.rename(daneben)
for eintrag in ordner.iterdir():
shutil.rmtree(eintrag) if eintrag.is_dir() else eintrag.unlink()
for eintrag in daneben.iterdir():
eintrag.rename(ordner / eintrag.name)
daneben.rmdir()
def _karten(ordner) -> dict:
"""titel -> Sektionen. Verglichen wird über den Titel, nicht über die Datei."""
raus = {}
for datei in sorted(ordner.glob("*.md")):
s = parser.zerlege(datei.read_text())
raus[s["titel"]] = s
return raus
def _funktionstext(f: dict) -> str:
return "\n".join(str(f.get(feld) or "") for feld in FELDER)
def _funktionen(karte: dict) -> dict:
return {f["text"]: _funktionstext(f) for f in karte["funktionen"]}
def _kartentext(karte: dict) -> str:
return "\n".join((karte.get("art") or "", karte.get("ziel") or "",
karte.get("regeln") or ""))
def aenderungen(projekt: str) -> dict:
"""Was die Planung am Projektstand ändert — je Karte und je Funktion.
Ohne laufende Planung ist das leer. Die Übersicht zeigt dann nichts an, und
die Karten sehen aus wie immer.
"""
if not laeuft(projekt):
return {}
alt = _karten(projekte.stand(projekt))
neu = _karten(_kopie(projekt))
raus = {}
for titel in set(alt) | set(neu):
a, b = alt.get(titel), neu.get(titel)
if a is None:
raus[titel] = {"art": "neu", "funktionen": {
f["text"]: "neu" for f in b["funktionen"]}}
continue
if b is None:
raus[titel] = {"art": "weg", "funktionen": {
f["text"]: "weg" for f in a["funktionen"]}}
continue
fa, fb = _funktionen(a), _funktionen(b)
funktionen = {}
for name in set(fa) | set(fb):
if name not in fa:
funktionen[name] = "neu"
elif name not in fb:
funktionen[name] = "weg"
elif fa[name] != fb[name]:
funktionen[name] = "geaendert"
kopf = _kartentext(a) != _kartentext(b)
if funktionen or kopf:
raus[titel] = {"art": "geaendert", "funktionen": funktionen,
"kopf": kopf}
return raus
def _datei_zu(ordner, titel: str):
for datei in ordner.glob("*.md"):
if parser.zerlege(datei.read_text())["titel"] == titel:
return datei
return None
def zuruecksetzen(projekt: str, titel: str):
"""Eine Karte auf den Projektstand zurückholen.
Drei Fälle: neu dazugekommen (löschen), weggefallen (zurückholen), geändert
(überschreiben). Der Rest der Planung bleibt stehen.
"""
if not laeuft(projekt):
raise ValueError("Für dieses Projekt läuft keine Planung")
ordner, kopie = projekte.stand(projekt), _kopie(projekt)
urspruenglich = _datei_zu(ordner, titel)
geplant = _datei_zu(kopie, titel)
if urspruenglich is None and geplant is None:
raise FileNotFoundError(f"Karte „{titel}“ nicht gefunden")
if urspruenglich is None:
geplant.unlink()
return
ziel = geplant or (kopie / urspruenglich.name)
ziel.write_text(urspruenglich.read_text())
def stand(projekt: str) -> dict:
return {"laeuft": laeuft(projekt), "aenderungen": aenderungen(projekt)}

View File

@@ -4,6 +4,10 @@ import shutil
import config
# Die Arbeitskopie einer laufenden Planung. Liegt im Projektordner und beginnt mit
# einem Punkt — damit taucht sie im Listing nicht als eigenes Projekt auf.
PLANUNG = ".planung"
def _pruefe_name(name: str):
if not name or not name.strip():
@@ -51,9 +55,24 @@ def loeschen(name: str):
shutil.rmtree(ordner)
def wurzel(name: str):
"""Projektordner für Feature-Zugriffe; FileNotFoundError bei unbekanntem Projekt."""
def stand(name: str):
"""Der Projektordner selbst — was das Projekt heute kann.
Nur wer den umgesetzten Stand meint, ruft das. Alles andere nimmt `wurzel()`.
"""
ordner = _ordner(name)
if not ordner.is_dir():
raise FileNotFoundError(f"Projekt „{name}“ nicht gefunden")
return ordner
def wurzel(name: str):
"""Der Ordner, in dem gerade gearbeitet wird.
Ohne offene Planung ist das der Projektordner. Läuft eine Planung, ist es ihre
Arbeitskopie — und zwar für alles: Karten, Gespräch, Gedächtnis. Ein
Schalter statt einer Fallunterscheidung in jeder Schreibfunktion.
"""
ordner = stand(name)
kopie = ordner / PLANUNG
return kopie if kopie.is_dir() else ordner

View File

@@ -18,10 +18,18 @@ from collections import Counter
import agents
import config
import features
import inhalt as inhaltspruefung
import quellcode
import ledger
import struktur
class LaeuftBereits(ValueError):
"""Eigener Typ, weil 409 genau eine Bedeutung haben muss.
Vorher war jeder Fehlstart ein 409 — auch ein fehlendes Quellverzeichnis. Das
Frontend liest 409 als „läuft schon" und hängt sich an einen Lauf, den es nicht
gibt. Ergebnis: „Keine Probleme gefunden" statt der Fehlermeldung.
"""
# projektname -> {"laeuft": bool, "fertig": int, "gesamt": int, "fehler": int,
# "probleme": [...], "meldung": str}
_laeufe = {}
@@ -445,7 +453,17 @@ async def _neue_felder(system: str, aufgabe: dict, offen: list, erlaubt: tuple,
return vorschlaege
async def _pruefe(stil: dict, aufgabe: dict) -> list[dict]:
def _stilschluessel(aufgabe: dict) -> str:
return ledger.schluessel(aufgabe["feature"], aufgabe["funktion"])
def _stilabdruck(aufgabe: dict, system: str) -> str:
"""Woran das Urteil hängt: der geprüfte Ausschnitt, der Katalog, die Frage.
Ändert sich eines davon, ist das gemerkte Urteil von gestern."""
return ledger.abdruck(system, inhalt(aufgabe))
async def _pruefe(stil: dict, aufgabe: dict, stand: dict = None) -> list[dict]:
"""Erst messen, dann umschreiben lassen, dann den Unterschied ausrechnen."""
bereich = BEREICHE[aufgabe["bereich"]]
system = SYSTEM.format(
@@ -455,6 +473,15 @@ async def _pruefe(stil: dict, aufgabe: dict) -> list[dict]:
hinweis=bereich["hinweis"],
)
offen = struktur.befunde(aufgabe["bereich"], aufgabe["felder"])
# Gemessene Verstöße stehen fest — die dürfen nie aus dem Gedächtnis kommen.
# Sonst bliebe eine neu hinzugekommene Messregel an alten Karten wirkungslos.
schluessel, wie_gehabt = _stilschluessel(aufgabe), _stilabdruck(aufgabe, system)
if stand is not None and not offen:
gemerkt = stand["stil"].get(schluessel)
if gemerkt and gemerkt["abdruck"] == wie_gehabt:
return [dict(p) for p in gemerkt["probleme"]]
vorschlaege = await _neue_felder(system, aufgabe, offen, bereich["felder"],
katalogzeilen(stil[aufgabe["bereich"]]))
@@ -466,83 +493,33 @@ async def _pruefe(stil: dict, aufgabe: dict) -> list[dict]:
for p in probleme:
p["feature"] = aufgabe["feature"]
p["funktion"] = aufgabe["funktion"]
if stand is not None:
stand["stil"][schluessel] = {"abdruck": wie_gehabt, "probleme": probleme}
return probleme
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.
Auch hier ein Gedächtnis: 176 Anfragen je Lauf brachten den Anbieter zum
Drosseln, und die ausgefallenen Stellen meldeten dann nichts. Unveränderter
Ausschnitt bei unverändertem Katalog heißt: das Urteil von vorhin gilt noch.
"""
stil = {b: _stil_lesen(b) for b in BEREICHE}
liste = [a for f in features.pruefbar(projekt) for a in aufgaben(f)]
return liste, lambda a, _zaehl: _pruefe(stil, a)
def _quelle(projekt: str):
alle = features.pruefbar(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? Und beschreibt
keine zwei Funktionen dieselbe Sache?
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(_, zaehl):
return await inhaltspruefung.pruefe_feature_im_code(
projekt, alle, quelle, text, zaehl, _melder(projekt))
return _platzhalter(anzahl), abgleich
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(
projekt, alle, quelle, zaehl, _melder(projekt))
return _platzhalter(len(punkte)), abgleich
def _melder(projekt: str):
"""Sagt dem Lauf, was schiefging, ohne ihn abzubrechen.
Ein Sammellauf ist eine einzige Aufgabe. Flöge er wegen einer Stelle, wären
alle anderen Befunde weg — und der Balken stünde trotzdem auf 100 %. So bleibt
das Ergebnis erhalten und der Fehlstand steht daneben.
"""
def melde(text: str, fehler: int = 0):
if lauf := _laeufe.get(projekt):
lauf["meldung"] = text
lauf["fehler"] += fehler
return melde
def _platzhalter(anzahl: int) -> list:
"""Eine echte Aufgabe, der Rest nur für den Fortschrittsbalken."""
return [True] + [None] * max(0, anzahl - 1)
stand = ledger.lade(projekt)
stand["stil"] = ledger.behalte(stand["stil"], {_stilschluessel(a) for a in liste})
return (liste,
lambda a, _zaehl: _pruefe(stil, a, stand),
lambda: ledger.sichere(projekt, stand))
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, abschluss=None):
lauf = _laeufe[projekt]
schranke = asyncio.Semaphore(config.PRUEFUNG_PARALLEL)
@@ -570,6 +547,9 @@ async def _lauf(projekt: str, liste: list[dict], arbeite):
try:
await asyncio.gather(*(eine(a) for a in liste))
finally:
# Auch ein abgebrochener Lauf hat schon Urteile gefällt; die sind bezahlt
if abschluss:
abschluss()
lauf["laeuft"] = False
@@ -578,10 +558,10 @@ async def starte(projekt: str, art: str = "stil") -> dict:
if art not in ARTEN:
raise ValueError(f"Unbekannte Prüfart „{art}“")
if status(projekt)["laeuft"]:
raise ValueError("Prüfung läuft bereits")
raise LaeuftBereits("Prüfung läuft bereits")
agents.pruefe_konfiguration() # früher Stopp statt halbem Lauf
# Vorbereiten vor dem Task: ein Fehler darin wäre sonst unsichtbar
liste, arbeite = await ARTEN[art](projekt)
liste, arbeite, *rest = await ARTEN[art](projekt)
_laeufe[projekt] = {
"laeuft": True,
"art": art,
@@ -591,7 +571,7 @@ async def starte(projekt: str, art: str = "stil") -> dict:
"probleme": [],
"meldung": "",
}
aufgabe = asyncio.create_task(_lauf(projekt, liste, arbeite))
aufgabe = asyncio.create_task(_lauf(projekt, liste, arbeite, *rest))
_aufgaben.add(aufgabe)
aufgabe.add_done_callback(_aufgaben.discard)
return _laeufe[projekt]

View File

@@ -1,189 +0,0 @@
"""Der Quelltext als Prüfgrundlage.
Die Inhaltsprüfung vergleicht Karten mit dem, was das Programm wirklich kann.
Damit das nicht auf Behauptungen hinausläuft, muss jede Aussage über den Code
eine wörtlich zitierte Zeile mitbringen — `belegt()` prüft sie nach.
"""
import re
import config
# Was zum lauffähigen Programm gehört. Tests beschreiben keine Nutzerfunktionen.
MUSTER = (
"backend/*.py",
"frontend/index.html",
"frontend/src/*.js",
"frontend/src/**/*.js",
"frontend/src/**/*.vue",
)
AUSGENOMMEN = ("tests/", "/tests/", "node_modules/", "dist/")
def _passt(pfad: str) -> bool:
return not any(a in pfad for a in AUSGENOMMEN)
def dateien() -> dict:
"""Pfad relativ zur Projektwurzel -> Inhalt, in fester Reihenfolge."""
raus = {}
for muster in MUSTER:
for datei in sorted(config.PROJECT_ROOT.glob(muster)):
pfad = str(datei.relative_to(config.PROJECT_ROOT))
if datei.is_file() and _passt(pfad):
raus[pfad] = datei.read_text()
return dict(sorted(raus.items()))
def als_text(quelle: dict = None) -> str:
"""Alle Dateien am Stück, jede mit Überschrift — so kann das Modell zitieren."""
quelle = dateien() if quelle is None else quelle
return "\n\n".join(f"### {pfad}\n{text.rstrip()}" for pfad, text in quelle.items())
def _normal(zeile: str) -> str:
return re.sub(r"\s+", " ", zeile).strip()
def belegt(quelle: dict, pfad: str, schnipsel: str) -> bool:
"""Steht der Schnipsel wörtlich in der Datei?
Verglichen wird ohne Rücksicht auf Einrückung — Modelle geben sie ungenau
wieder. Alles andere muss stimmen, sonst ist der Beleg wertlos.
"""
text = quelle.get(pfad)
if text is None or not schnipsel.strip():
return False
gesucht = [_normal(z) for z in schnipsel.splitlines() if z.strip()]
if not gesucht:
return False
vorhanden = [_normal(z) for z in text.splitlines()]
# Zusammenhängend: eine mehrzeilige Angabe muss auch mehrzeilig dastehen
for i in range(len(vorhanden) - len(gesucht) + 1):
if vorhanden[i:i + len(gesucht)] == gesucht:
return True
return False
# Die Stellen, an denen ein Mensch etwas auslöst. Vorher hat ein Modell die
# Fähigkeiten frei aufgezählt — jede Runde andere, jede Runde anders benannt.
# Hier steht die Liste fest, weil sie aus dem Code fällt.
_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.
# Sie tragen keine eigene Fähigkeit, sondern gehören zur Bedienung eines Feldes.
_UNINTERESSANT = {"fertig", "abbrechen", "blur", "waehle"}
# emit('auswaehlen') ist eine Fähigkeit, emit('abbrechen') nicht — es zählt das
# Ereignis, nicht der Aufruf. Vorher fiel mit „emit" auch das Auswählen weg.
_EMIT = re.compile(r"""emit\(\s*['"](\w+)['"]""")
_ZUWEISUNG = re.compile(r"^\s*[\w.]+\s*=[^=]")
def _umfeld(zeilen: list, nr: int, rand: int = 5) -> str:
von, bis = max(0, nr - rand - 1), min(len(zeilen), nr + rand)
return "\n".join(zeilen[von:bis]).strip()
def _pyrumpf(zeilen: list, nr: int, hoechstens: int = 14) -> str:
"""Der eingerückte Block unter einer Zeile — für Python statt geschweifter Klammern."""
if nr > len(zeilen):
return "" # Dekorator ohne Rumpf, etwa am Dateiende
block = [zeilen[nr - 1]]
for weiter in zeilen[nr:nr + hoechstens]:
if weiter.strip() and not weiter[:1].isspace() and not weiter.startswith("@"):
break
block.append(weiter)
return "\n".join(block).rstrip()
def _rumpf(zeilen: list, name: str, hoechstens: int = 14) -> str:
"""Die Umsetzung hinter einem Namen.
Das Markup allein sagt nur, dass ein Knopf etwas aufruft. Was er tut, steht im
Rumpf — ohne den musste der Prüfer raten und urteilte jede Runde anders.
"""
anfang = re.compile(rf"^\s*(async\s+)?function\s+{re.escape(name)}\s*\(|"
rf"^\s*const\s+{re.escape(name)}\s*=")
for i, zeile in enumerate(zeilen):
if not anfang.match(zeile):
continue
block = [zeile]
for weiter in zeilen[i + 1:i + hoechstens]:
block.append(weiter)
if weiter.startswith("}"):
break
return "\n".join(block)
return ""
def interaktionen(quelle: dict = None) -> list[dict]:
"""Auslösbare Stellen im Code, eine je Bedienhandlung.
Zusammengefasst nach dem aufgerufenen Namen: `@click="starte('ziel')"` steht
zwölfmal im Markup, ist aber eine Fähigkeit.
Dazu Fenster-Lauscher: ein Tastaturgriff ist eine Fähigkeit, auch ohne Knopf.
Dazu die Backend-Routen. Sie standen zwischenzeitlich nicht drin, weil sie
doppelt gemeldet und dabei der falschen Karte zugeordnet wurden. Beides ist
jetzt aufgefangen: das Gedächtnis hält die Zuordnung fest, und der
Dublettenabgleich fängt zwei Funktionen für dieselbe Sache. Was hinter einem
Knopf liegt, ist der einzige Weg an Parser, Dateiverwaltung und Prüflogik —
ohne die Routen bliebe der ganze Rücken des Programms ungeprüft.
Reine Hilfsfunktionen bleiben draußen. Ein Kunde bestellt keine Funktion namens
`_slug`; sie wird über die Route erreicht, die ihn interessiert. Jedes
öffentliche Symbol aufzunehmen ergäbe wieder ein Code-Inventar statt einer
Auftragsliste.
"""
quelle = dateien() if quelle is None else quelle
raus, gesehen = [], set()
def merke(schluessel, art, pfad, nr, zeilen, beleg):
if schluessel in gesehen:
return None
gesehen.add(schluessel)
punkt = {"schluessel": schluessel, "art": art, "datei": pfad, "zeile": nr,
"beleg": beleg.strip(), "umfeld": _umfeld(zeilen, nr), "rumpf": ""}
raus.append(punkt)
return punkt
for pfad, text in quelle.items():
zeilen = text.splitlines()
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)
for m in _ROUTE.finditer(zeile):
punkt = merke(f"route:{m.group(1)} {m.group(2)}", "Schnittstelle",
pfad, nr, zeilen, zeile)
if punkt is not None:
punkt["rumpf"] = _pyrumpf(zeilen, nr + 1)
if not pfad.endswith(".vue"):
continue
for m in _EREIGNIS.finditer(zeile):
ausdruck = m.group(2)
if _ZUWEISUNG.match(ausdruck) or ausdruck.startswith("$"):
continue # Zuweisung oder Ereignis-Objekt, keine Fähigkeit
treffer = _EMIT.search(ausdruck) or _NAME.search(ausdruck)
if not treffer or treffer.group(1 if _EMIT.search(ausdruck) else 0) \
in _UNINTERESSANT:
continue
name = treffer.group(1) if _EMIT.search(ausdruck) else treffer.group(0)
punkt = merke(f"bedienen:{name}", "Bedienung", pfad, nr, zeilen, zeile)
if punkt is not None:
punkt["rumpf"] = _rumpf(zeilen, name)
return raus
def uebersicht(quelle: dict = None) -> str:
"""Kurze Dateiliste mit Zeilenzahl — für Prompts, die den Code nicht brauchen."""
quelle = dateien() if quelle is None else quelle
return "\n".join(f"- {p} ({len(t.splitlines())} Zeilen)" for p, t in quelle.items())

View File

@@ -4,8 +4,10 @@ hier wird auf 400/404/409 gemappt."""
from fastapi import APIRouter, HTTPException
import agents
import chat
import features
import projekte
import planung
import pruefung
router = APIRouter(prefix="/api")
@@ -55,9 +57,13 @@ async def projekt_loeschen(name: str):
@router.get("/projekte/{name}/features")
async def features_liste(name: str):
async def features_liste(name: str, sicht: str = "planung"):
"""`sicht=uebersicht` liest den Projektstand, sonst die Arbeitskopie.
Ohne laufende Planung sind beide dasselbe Verzeichnis.
"""
try:
return features.liste(name)
return features.liste(name, nur_stand=sicht == "uebersicht")
except (FileNotFoundError, ValueError) as e:
raise HTTPException(404, str(e))
@@ -89,10 +95,10 @@ async def pruefung_starten(name: str, body: dict | None = None):
return await pruefung.starte(name, art)
except FileNotFoundError as e:
raise HTTPException(404, str(e))
except agents.NichtKonfiguriert as e:
raise HTTPException(400, str(e)) # 409 heißt „läuft bereits" — hier fehlt die .env
except pruefung.LaeuftBereits as e:
raise HTTPException(409, str(e)) # 409 heißt genau das und sonst nichts
except ValueError as e:
raise HTTPException(409, str(e))
raise HTTPException(400, str(e))
@router.get("/projekte/{name}/pruefung")
@@ -100,6 +106,56 @@ async def pruefung_status(name: str):
return pruefung.status(name)
@router.get("/projekte/{name}/planung")
async def planung_stand(name: str):
try:
return planung.stand(name)
except FileNotFoundError as e:
raise HTTPException(404, str(e))
@router.post("/projekte/{name}/planung")
async def planung_starten(name: str):
try:
planung.starte(name)
except FileNotFoundError as e:
raise HTTPException(404, str(e))
except ValueError as e:
raise HTTPException(409, str(e))
return planung.stand(name)
@router.post("/projekte/{name}/planung/umsetzen")
async def planung_umsetzen(name: str):
try:
planung.setze_um(name)
except FileNotFoundError as e:
raise HTTPException(404, str(e))
except ValueError as e:
raise HTTPException(409, str(e))
return {"ok": True}
@router.delete("/projekte/{name}/planung")
async def planung_abbrechen(name: str):
try:
planung.brich_ab(name)
except FileNotFoundError as e:
raise HTTPException(404, str(e))
return {"ok": True}
@router.post("/projekte/{name}/planung/zuruecksetzen")
async def planung_zuruecksetzen(name: str, body: dict):
try:
planung.zuruecksetzen(name, (body or {}).get("titel") or "")
except FileNotFoundError as e:
raise HTTPException(404, str(e))
except ValueError as e:
raise HTTPException(409, str(e))
return planung.stand(name)
@router.delete("/projekte/{name}/pruefung")
async def pruefung_beenden(name: str):
pruefung.beende(name)
@@ -113,3 +169,42 @@ async def feature_loeschen(name: str, id: str):
except (FileNotFoundError, ValueError) as e:
raise HTTPException(404, str(e))
return {"ok": True}
@router.put("/projekte/{name}/features/{id}/projekt")
async def feature_verschieben(name: str, id: str, body: dict):
try:
return features.verschieben(name, id, (body.get("ziel") or "").strip())
except FileNotFoundError as e:
raise HTTPException(404, str(e))
except ValueError as e:
raise HTTPException(400, str(e))
@router.get("/projekte/{name}/chat")
async def chat_verlauf(name: str):
try:
return {"verlauf": chat.verlauf(name)}
except FileNotFoundError as e:
raise HTTPException(404, str(e))
@router.post("/projekte/{name}/chat")
async def chat_frage(name: str, body: dict):
try:
return await chat.frage(name, body.get("text") or "")
except FileNotFoundError as e:
raise HTTPException(404, str(e))
except agents.NichtKonfiguriert as e:
raise HTTPException(400, str(e))
except ValueError as e:
raise HTTPException(400, str(e))
@router.delete("/projekte/{name}/chat")
async def chat_leeren(name: str):
try:
chat.leeren(name)
except FileNotFoundError as e:
raise HTTPException(404, str(e))
return {"ok": True}

View File

@@ -18,6 +18,9 @@ Gilt für jedes Feld.
Diese Vorgaben prüft der Code selbst, exakt und für jede Zeile. Halte dich beim
Schreiben daran — aber berufe dich nie darauf. Als `regel` sind sie nicht zulässig.
- Jede Funktion hat Ziel, Regeln oder Testschritte — nie nur einen Titel
- Titel einer Funktion endet auf ein Verb im Infinitiv
- Testschritte beschreiben genau einen Ablauf
- Kein Satzzeichen am Zeilenende, kein Punkt und kein Semikolon
- Keine Füllwörter. Die Liste ist abschließend: auch, noch, einfach, jeweils, ggf.
- Keine Klammern und keine Schrägstriche

View File

@@ -3,6 +3,8 @@
Feld `testen`. Gibt es nur bei Funktionen, nie auf Kartenebene.
- Eine Zeile je Schritt
- Genau ein Ablauf: handeln, handeln, Ergebnis
- Nach dem Ergebnis folgt keine Handlung mehr
- Schritte in Reihenfolge der Bedienung
- Kein Schritt der Bedienung fehlt
- Letzte Zeile beschreibt den Endzustand, keine Handlung
@@ -16,3 +18,12 @@ Richtig:
- Funktion entfernt
```
Falsch: letzte Zeile `Klick zum entfernen` (Handlung statt Endzustand)
Falsch, weil zwei Abläufe in einer Funktion stehen:
```
- Klick auf Miniaturbild
- Großansicht öffnet und schließt
- Option aus Großansicht auswählbar
```
Jede Zeile ist hier ein eigenes Ergebnis, kein Schritt eines Durchlaufs. Solche
Funktionen sind mehrere und gehören getrennt.

View File

@@ -4,6 +4,8 @@ Feld `titel`, wenn `funktion` gesetzt ist.
- Kurzsatz aus Objekt und Verb im Infinitiv
- Genau eine Tätigkeit
- Endet auf ein Verb im Infinitiv
- Nennt eine Handlung, keinen Bereich, keinen Zustand
Richtig: `Feature erstellen` · `Anzeigemodus umschalten` · `Feature als Datei speichern`
Falsch: `Erstellung von Features` (Substantivierung) · `Feature erstellen und öffnen` (zwei Tätigkeiten)
Falsch: `Erstellung von Features` (Substantivierung) · `Feature erstellen und öffnen` (zwei Tätigkeiten) · `Galerie im Modal` (ein Bereich) · `Preis aktualisiert` (ein Zustand) · `In den Warenkorb` (kein Verb)

View File

@@ -29,8 +29,21 @@ BADGE = ("kern", "optional")
# „funktionen" gehört der Inhaltsprüfung, hat aber dieselbe Gestalt.
ZEILENFELDER = ("regeln", "testen", "funktionen")
# Ein Funktionstitel nennt eine Handlung. Ohne Verb am Ende steht dort ein Bereich
# („Galerie im Modal") oder ein Zustand („Preis aktualisiert") — beides beschreibt
# nichts, was man für sich testen könnte. Deutsche Infinitive enden auf -en, -ln,
# -rn; wie Füllwörter und Wortgrenzen ist auch das an die Sprache gebunden.
_INFINITIV = re.compile(r"(?:e[lr]?n|ln|rn)$", re.I)
# Eine Handlung steht als Infinitiv am Anfang oder am Ende der Zeile („Bestätigen im
# Modal", „Text ändern") oder beginnt mit einer Taste. Alles andere ist ein Ergebnis.
# Nur Anfang und Ende, nicht irgendwo: deutsche Mehrzahl endet auch auf -en.
_TASTE = re.compile(r"^(?:LMB|RMB|MMB|Klick|Doppelklick|Enter|Escape|Strg|Tab)\b", re.I)
SATZENDE = ".;"
_KLAMMER = re.compile(r"[()/]")
# Eine Vorlage ist kein Inhalt. Das Modell schlug `<testschritt>` als Testschritt
# vor — formal richtig, gemeint war „hier fehlt noch etwas".
_LUECKE = re.compile(r"<[^<>]*>|\.\.\.|\bTODO\b|\bXXX\b", re.I)
_STRICH = re.compile(r"^[-*]\s*")
@@ -68,9 +81,15 @@ def _umschreiben(bereich: str, feld: str, zeile: str) -> list[str]:
gruende = []
if _KLAMMER.search(zeile):
gruende.append("Klammer oder Schrägstrich")
if _LUECKE.search(zeile):
gruende.append("Platzhalter statt Inhalt")
grenze = WORTGRENZEN.get((bereich, feld))
if grenze and len(zeile.split()) > grenze:
gruende.append(f"Mehr als {grenze} Wörter")
if bereich == "funktion" and feld == "titel":
woerter = zeile.split()
if woerter and not _INFINITIV.search(woerter[-1]):
gruende.append("Kein Verb im Infinitiv am Ende")
if feld == "ziel":
saetze = [s.strip() for s in re.split(r"[.!?]+", zeile) if s.strip()]
if len(saetze) > ZIEL_SAETZE:
@@ -80,6 +99,30 @@ def _umschreiben(bereich: str, feld: str, zeile: str) -> list[str]:
return gruende
def _handlung(zeile: str) -> bool:
woerter = zeile.split()
if not woerter:
return True
return bool(_TASTE.match(zeile) or _INFINITIV.search(woerter[-1])
or _INFINITIV.search(woerter[0]))
def _zweiter_ablauf(zeilen: list[str]) -> str:
"""Die Zeile, ab der ein zweiter Durchlauf beginnt — sonst leer.
Ein Ablauf ist: handeln, handeln, Ergebnis. Steht nach dem Ergebnis wieder eine
Handlung, sind es zwei Abläufe — und damit zwei Funktionen. Genau so entstand
„Bildergalerie bedienen" mit blättern, Miniatur wählen und Großansicht in einem.
"""
ergebnis = False
for zeile in zeilen:
if ergebnis and _handlung(zeile):
return zeile
if not _handlung(zeile):
ergebnis = True
return ""
def _zeile(bereich: str, feld: str, zeile: str) -> list[dict]:
"""Höchstens ein Befund je Zeile. Zwei Vorschläge für dieselbe Zeile wären nicht
unabhängig — der zweite fände seine Stelle nach dem ersten nicht mehr."""
@@ -94,13 +137,28 @@ def _zeile(bereich: str, feld: str, zeile: str) -> list[dict]:
def befunde(bereich: str, felder: dict) -> list[dict]:
"""Alle messbaren Verstöße einer Prüfstelle, in Reihenfolge der Felder."""
raus = []
# Ein bloßer Titel beschreibt nichts — und lässt sich über alles stülpen. Der
# Prüfer hielt damit einen Druckknopf für „Modal mit Tastatur steuern".
# Nur wenn die ganze Funktion vorliegt: bei einem einzelnen Feld sagt das Fehlen
# der anderen nichts.
inhaltsfelder = ("ziel", "regeln", "testen")
if bereich == "funktion" and all(f in felder for f in inhaltsfelder) \
and not any((felder.get(f) or "").strip() for f in inhaltsfelder):
raus.append(_problem("testen", "", "Funktion ohne Ziel, Regeln und Testen"))
return raus
for feld, text in felder.items():
if feld == "badge":
if text and text not in BADGE:
raus.append(_problem(feld, text, "Weder kern noch optional", "kern"))
continue
for zeile in _zeilen(feld, text or ""):
zeilen = _zeilen(feld, text or "")
for zeile in zeilen:
raus += _zeile(bereich, feld, zeile)
if feld == "testen" and not raus:
# Erst prüfen, wenn die Zeilen für sich in Ordnung sind: sonst hängen
# zwei Vorschläge am selben Feld und der zweite findet seine Stelle nicht.
if bruch := _zweiter_ablauf(zeilen):
raus.append(_problem(feld, bruch, "Zweiter Ablauf in einer Funktion"))
return raus

View File

@@ -1,60 +0,0 @@
import inhalt
import quellcode
def test_unformatierte_prompts_ohne_doppelklammern():
"""Was nicht durch .format() geht, darf keine Escape-Klammern tragen —
das Modell schrieb sie sonst wörtlich in die Antwort und das JSON war kaputt."""
for name in ("ABDECKUNG_SYSTEM",):
text = getattr(inhalt, name)
assert "{{" not in text and "}}" not in text, name
def test_formatierte_prompts_ueberstehen_format():
punkt = {"art": "Bedienung", "datei": "a.vue", "zeile": 1,
"umfeld": "x", "rumpf": ""}
fertig = inhalt.ABDECKUNG.format(funktionen="—", **punkt)
assert '{"abgedeckt": true' in fertig
assert "{{" not in fertig
def test_beleg_muss_woertlich_dastehen():
quelle = {"a.py": "def eins():\n return 1\n"}
assert quellcode.belegt(quelle, "a.py", "def eins():")
assert quellcode.belegt(quelle, "a.py", " def eins(): ") # Einrückung egal
assert quellcode.belegt(quelle, "a.py", "def eins():\nreturn 1")
assert not quellcode.belegt(quelle, "a.py", "def zwei():")
assert not quellcode.belegt(quelle, "b.py", "def eins():")
assert not quellcode.belegt(quelle, "a.py", "")
def test_interaktionen_sind_deterministisch():
quelle = {
"frontend/src/X.vue": (
'<button @click="starte(\'ziel\')">a</button>\n'
'<button @click="starte(\'regeln\')">b</button>\n'
'<div @contextmenu="menue">c</div>\n'
'<input @blur="fertig" @keydown.esc="abbrechen">\n'
'<span @click="offen = false">zu</span>\n'
),
"backend/routes.py": '@router.get("/projekte")\n@router.post("/projekte")\n',
"frontend/src/m.js": " { text: '+ Projekt erstellen', aktion: () => x() },\n",
}
schluessel = [p["schluessel"] for p in quellcode.interaktionen(quelle)]
# Zwölf Aufrufe von starte() sind eine Fähigkeit, nicht zwölf
assert schluessel.count("bedienen:starte") == 1
assert "bedienen:menue" in schluessel
# Routen: der einzige Weg an Parser, Dateiverwaltung und Prüflogik
assert "route:get /projekte" in schluessel
assert "route:post /projekte" in schluessel
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)
assert not any(s.startswith("bedienen:abbrechen") for s in schluessel)
assert not any(s.startswith("bedienen:offen") for s in schluessel)
# Zweiter Aufruf liefert exakt dasselbe
assert schluessel == [p["schluessel"] for p in quellcode.interaktionen(quelle)]
def test_tests_sind_kein_quelltext():
assert not any("tests/" in p for p in quellcode.dateien())

View File

@@ -1,8 +1,8 @@
"""Das Gedächtnis der Inhaltsprüfungen: was gilt weiter, was wird neu gefragt.
"""Das Gedächtnis der Stilprüfung: was gilt weiter, was wird neu gefragt.
Die Prüfungen 2 und 3 fragen ein Modell. Dieselbe Frage kippt bei Grenzfällen auch
bei Temperatur 0 — vorher meldete Runde 1 einen Befund, Runde 2 keinen, Runde 3
wieder einen. Hier steht, unter welchen Bedingungen ein Urteil stehen bleibt.
Die Prüfung fragt ein Modell. Dieselbe Frage kippt bei Grenzfällen auch bei
Temperatur 0 — vorher meldete Runde 1 einen Befund, Runde 2 keinen, Runde 3 wieder
einen. Hier steht, unter welchen Bedingungen ein Urteil stehen bleibt.
"""
import json
@@ -11,7 +11,6 @@ import pytest
import agents
import features
import inhalt
import ledger
import projekte
@@ -33,9 +32,6 @@ def karte(projekt, titel, *funktionen):
return features.anlegen(projekt, text)
QUELLE = {"frontend/src/App.vue": '<button @click="starte">los</button>\n'}
class Zaehler:
"""Ersetzt das Modell und zählt mit, wie oft überhaupt gefragt wurde."""
@@ -82,431 +78,6 @@ def test_gedaechtnis_ist_kein_feature(projekt):
assert [f["sektionen"]["titel"] for f in features.liste(projekt)] == ["Eins"]
# ---------------------------------------------------------------- Prüfung 2
GEFUNDEN = json.dumps({"gefunden": True, "datei": "frontend/src/App.vue",
"beleg": '<button @click="starte">los</button>'})
async def test_beleg_wird_gemerkt_und_beim_zweiten_lauf_nicht_gefragt(
projekt, monkeypatch):
karte(projekt, "Eins", ("Etwas starten", "Knopf"))
modell = Zaehler(GEFUNDEN)
monkeypatch.setattr(agents, "frage", modell)
alle = features.liste(projekt)
text = "### frontend/src/App.vue\n" + QUELLE["frontend/src/App.vue"]
assert await inhalt.pruefe_feature_im_code(projekt, alle, QUELLE, text) == []
erste_runde = modell.rufe
assert erste_runde >= len(inhalt.SUCHE_TEMPERATUREN)
assert await inhalt.pruefe_feature_im_code(projekt, alle, QUELLE, text) == []
assert modell.rufe == erste_runde # keine einzige neue Anfrage
async def test_beleg_verfaellt_wenn_die_zeile_verschwindet(projekt, monkeypatch):
karte(projekt, "Eins", ("Etwas starten", "Knopf"))
modell = Zaehler(GEFUNDEN)
monkeypatch.setattr(agents, "frage", modell)
alle = features.liste(projekt)
text = "### frontend/src/App.vue\n" + QUELLE["frontend/src/App.vue"]
await inhalt.pruefe_feature_im_code(projekt, alle, QUELLE, text)
vorher = modell.rufe
anders = {"frontend/src/App.vue": "<div>nichts mehr</div>\n"}
await inhalt.pruefe_feature_im_code(projekt, alle, anders, "### x\n<div>")
assert modell.rufe > vorher # der Beleg trägt nicht mehr, also neu fragen
async def test_geaenderte_funktion_wird_neu_gefragt(projekt, monkeypatch):
karte(projekt, "Eins", ("Etwas starten", "Knopf"))
modell = Zaehler(GEFUNDEN)
monkeypatch.setattr(agents, "frage", modell)
text = "### frontend/src/App.vue\n" + QUELLE["frontend/src/App.vue"]
await inhalt.pruefe_feature_im_code(projekt, features.liste(projekt), QUELLE, text)
vorher = modell.rufe
datei = projekte.wurzel(projekt) / f"{features.liste(projekt)[0]['id']}.md"
datei.write_text(datei.read_text().replace("- Knopf", "- Ganz anderer Inhalt"))
await inhalt.pruefe_feature_im_code(projekt, features.liste(projekt), QUELLE, text)
assert modell.rufe > vorher
async def test_fehlanzeige_gilt_nur_bei_unveraendertem_quelltext(projekt, monkeypatch):
karte(projekt, "Eins", ("Etwas starten", "Knopf"))
modell = Zaehler(json.dumps({"gefunden": False, "grund": "gibt es nicht"}))
monkeypatch.setattr(agents, "frage", modell)
alle = features.liste(projekt)
text = "### frontend/src/App.vue\n" + QUELLE["frontend/src/App.vue"]
erste = await inhalt.pruefe_feature_im_code(projekt, alle, QUELLE, text)
assert len(erste) == 1
vorher = modell.rufe
zweite = await inhalt.pruefe_feature_im_code(projekt, alle, QUELLE, text)
assert [p["vorschlag"] for p in zweite] == [p["vorschlag"] for p in erste]
assert modell.rufe == vorher
# Neuer Quelltext: eine Löschempfehlung muss frisch belegt werden
mehr = {**QUELLE, "backend/neu.py": "def starte():\n pass\n"}
await inhalt.pruefe_feature_im_code(projekt, alle, mehr, text)
assert modell.rufe > vorher
# ---------------------------------------------------------------- Dubletten
def punkt(schluessel="bedienen:starte", beleg='<button @click="starte">los</button>'):
return {"schluessel": schluessel, "art": "Bedienung",
"datei": "frontend/src/App.vue", "zeile": 1, "beleg": beleg,
"umfeld": beleg, "rumpf": ""}
async def test_gleicher_beleg_macht_zwei_funktionen_zu_kandidaten(projekt, monkeypatch):
karte(projekt, "Eins", ("Etwas starten", "Knopf"))
karte(projekt, "Zwei", ("Lauf beginnen", "Knopf"))
monkeypatch.setattr(agents, "frage", Zaehler(json.dumps({"gegenstand_gleich": True, "ergebnis_gleich": True})))
alle = features.liste(projekt)
stand = ledger.lade(projekt)
for f in alle:
stand["belege"][ledger.schluessel(f["id"], f["sektionen"]["funktionen"][0]["text"])] = {
"abdruck": "egal", "datei": "frontend/src/App.vue",
"beleg": '<button @click="starte">los</button>'}
gefunden = await inhalt.dubletten(alle, stand)
assert len(gefunden) == 1
# Gleich stark: der Titel entscheidet, damit der Vorschlag nicht springt
assert gefunden[0]["titel"] == "Etwas starten"
assert "Lauf beginnen" in gefunden[0]["grund"]
async def test_uneiniges_urteil_ist_keine_dublette_und_wird_gemerkt(
projekt, monkeypatch):
karte(projekt, "Eins", ("Etwas starten", "Knopf"))
karte(projekt, "Zwei", ("Lauf beginnen", "Knopf"))
def wechselhaft(nachricht, ruf):
return json.dumps({"gegenstand_gleich": True, "ergebnis_gleich": ruf == 1})
modell = Zaehler(wechselhaft)
monkeypatch.setattr(agents, "frage", modell)
alle = features.liste(projekt)
stand = ledger.lade(projekt)
for f in alle:
stand["belege"][ledger.schluessel(f["id"], f["sektionen"]["funktionen"][0]["text"])] = {
"abdruck": "egal", "datei": "a.vue", "beleg": "x"}
assert await inhalt.dubletten(alle, stand) == []
vorher = modell.rufe
assert await inhalt.dubletten(alle, stand) == []
assert modell.rufe == vorher # Ablehnung wird gemerkt, nicht neu bezahlt
def test_gegenstueck_faellt_nicht_ueber_den_titel():
"""anlegen/entfernen und öffnen/schließen dürfen den Titelkanal nicht auslösen —
sonst kostet jedes Gegensatzpaar drei Anfragen für nichts."""
for a, b in (("Feature anlegen", "Feature entfernen"),
("Panel öffnen", "Panel schließen")):
assert inhalt._aehnlich(a, b) < inhalt.TITEL_KANAL
ENTFERNEN = ["Vollmodus öffnen", "RMB auf die Funktion",
'Klick auf "Funktion entfernen"', "Funktion entfernt"]
def test_umschriebene_dublette_wird_kandidat(projekt):
"""Anderer Titel, andere Karte, dieselbe Sache. Weder Titel noch Beleg fangen
das — nur der Inhaltskanal."""
karte(projekt, "Funktionen",
("Funktion entfernen", "x-Icon", ENTFERNEN),
("Funktion anlegen", "+-Icon", ["Vollmodus öffnen", "RMB auf die Karte",
'Klick auf "Funktion erstellen"',
"Funktion angelegt"]),
("Testschritte anpassen", "Ein Schritt je Zeile",
["Vollmodus öffnen", "LMB auf Testen", "Text ändern", "Schritte angepasst"]))
karte(projekt, "Features", ("Funktion löschen", "x-Icon", ENTFERNEN))
alle = inhalt._funktionen(features.liste(projekt))
paare = {frozenset((alle[a]["titel"], alle[b]["titel"])) for _, a, b, _ in
inhalt._kandidaten(alle, {})}
assert frozenset(("Funktion entfernen", "Funktion löschen")) in paare
assert frozenset(("Funktion anlegen", "Funktion entfernen")) not in paare
def test_floskeln_zaehlen_nicht_mit(projekt):
"""Was fast überall steht, unterscheidet nichts und fliegt aus dem Vergleich."""
schritte = ["Vollmodus öffnen", "Text ändern", "Angepasst"]
karte(projekt, "Eins",
*[(f"{feld} anpassen", "eine Regel", schritte)
for feld in ("Titel", "Ziel", "Regeln", "Testen", "Badge")])
kerne = inhalt._kerne(inhalt._funktionen(features.liste(projekt)))
gemeinsam = set.intersection(*kerne.values())
assert gemeinsam == set() # „vollmodus", „ändern", „angepasst" sind raus
assert all(k for k in kerne.values()) # jede Funktion behält ihr eigenes Wort
# ---------------------------------------------------------------- Prüfung 3
ABGEDECKT = json.dumps({"abgedeckt": True, "funktion": "Etwas starten"})
FEHLT = json.dumps({"abgedeckt": False, "karte": "Eins", "titel": "Neu bedienen"})
async def test_abdeckung_wird_gemerkt_solange_die_funktion_dasteht(
projekt, monkeypatch):
karte(projekt, "Eins", ("Etwas starten", "Knopf"))
modell = Zaehler(ABGEDECKT)
monkeypatch.setattr(agents, "frage", modell)
stand = ledger.lade(projekt)
alle = features.liste(projekt)
assert await inhalt.code_im_feature(alle, [punkt()], stand) == []
vorher = modell.rufe
assert await inhalt.code_im_feature(alle, [punkt()], stand) == []
assert modell.rufe == vorher
# Funktion weg: die Abdeckung trägt nicht mehr
ohne = features.liste(projekt)
ohne[0]["sektionen"]["funktionen"] = []
await inhalt.code_im_feature(ohne, [punkt()], stand)
assert modell.rufe > vorher
async def test_geaenderte_codestelle_wird_neu_beurteilt(projekt, monkeypatch):
karte(projekt, "Eins", ("Etwas starten", "Knopf"))
modell = Zaehler(ABGEDECKT)
monkeypatch.setattr(agents, "frage", modell)
stand = ledger.lade(projekt)
alle = features.liste(projekt)
await inhalt.code_im_feature(alle, [punkt()], stand)
vorher = modell.rufe
await inhalt.code_im_feature(alle, [punkt(beleg="ganz anderer Code")], stand)
assert modell.rufe > vorher
async def test_fehlmeldung_bleibt_wortgleich_bis_die_funktion_da_ist(
projekt, monkeypatch):
karte(projekt, "Eins", ("Etwas starten", "Knopf"))
modell = Zaehler(FEHLT)
monkeypatch.setattr(agents, "frage", modell)
stand = ledger.lade(projekt)
alle = features.liste(projekt)
erste = await inhalt.code_im_feature(alle, [punkt()], stand)
assert [x["titel"] for x in erste] == ["Neu bedienen"]
vorher = modell.rufe
zweite = await inhalt.code_im_feature(alle, [punkt()], stand)
assert zweite == erste
assert modell.rufe == vorher # kein Flackern, keine neue Anfrage
async def test_angenommener_vorschlag_loest_die_fehlmeldung_ab(projekt, monkeypatch):
karte(projekt, "Eins", ("Etwas starten", "Knopf"))
modell = Zaehler(FEHLT)
monkeypatch.setattr(agents, "frage", modell)
stand = ledger.lade(projekt)
await inhalt.code_im_feature(features.liste(projekt), [punkt()], stand)
vorher = modell.rufe
# Der Nutzer nimmt den Vorschlag an — jetzt muss neu beurteilt werden
modell.antwort = ABGEDECKT
karte(projekt, "Zwei", ("Neu bedienen", "Knopf"))
assert await inhalt.code_im_feature(features.liste(projekt), [punkt()], stand) == []
assert modell.rufe > vorher
async def test_unentschiedenes_urteil_wird_festgehalten(projekt, monkeypatch):
"""Drei zu drei ist ein Münzwurf. Der darf einmal fallen, nicht jede Runde."""
karte(projekt, "Eins", ("Etwas starten", "Knopf"))
def haelfte(nachricht, ruf):
return ABGEDECKT if ruf % 2 else FEHLT
modell = Zaehler(haelfte)
monkeypatch.setattr(agents, "frage", modell)
stand = ledger.lade(projekt)
alle = features.liste(projekt)
assert await inhalt.code_im_feature(alle, [punkt()], stand) == []
vorher = modell.rufe
assert await inhalt.code_im_feature(alle, [punkt()], stand) == []
assert modell.rufe == vorher
async def test_verschwundene_stelle_raeumt_ihren_eintrag_weg(projekt, monkeypatch):
karte(projekt, "Eins", ("Etwas starten", "Knopf"))
monkeypatch.setattr(agents, "frage", Zaehler(ABGEDECKT))
stand = ledger.lade(projekt)
alle = features.liste(projekt)
await inhalt.code_im_feature(alle, [punkt()], stand)
assert stand["abdeckung"]
await inhalt.code_im_feature(alle, [punkt(schluessel="bedienen:anderes")], stand)
assert "bedienen:starte" not in stand["abdeckung"]
def test_fehlende_stelle_landet_auch_auf_erfundener_karte(projekt):
"""Sechs Stimmen sagen „fehlt", nennen aber Karten, die es nicht gibt. Vorher
fiel der Befund lautlos weg."""
karte(projekt, "Projekte", ("Projekt anlegen", "+-Icon"))
karten = {f["sektionen"]["titel"]: f for f in features.liste(projekt)}
urteile = [{"abgedeckt": False, "karte": "Datenverwaltung", "titel": "x"}] * 4
assert inhalt._zielkarte(urteile, karten) is karten["Projekte"]
def test_erster_aufloesbarer_kartenname_gewinnt(projekt):
karte(projekt, "Projekte", ("Projekt anlegen", "+-Icon"))
karte(projekt, "Darkmode", ("Darkmode umschalten", "Icon"))
karten = {f["sektionen"]["titel"]: f for f in features.liste(projekt)}
urteile = ([{"abgedeckt": False, "karte": "Export", "titel": "x"}] * 3
+ [{"abgedeckt": False, "karte": "Darkmode", "titel": "x"}] * 2)
assert inhalt._zielkarte(urteile, karten) is karten["Darkmode"]
def test_ohne_karten_kein_ziel(projekt):
assert inhalt._zielkarte([{"abgedeckt": False, "karte": "X"}], {}) is None
def test_grund_trennt_dublette_von_fehlendem_code():
"""Beides heißt streichen — aber aus verschiedenen Gründen, und der Grund
entscheidet, ob man zustimmt."""
assert inhalt._wegfall_grund([{"art": "dublette"}]) == "1 Funktion doppelt"
assert inhalt._wegfall_grund([{"art": "ohne_code"}] * 2) \
== "2 Funktionen ohne eigenen Code"
assert inhalt._wegfall_grund([{"art": "dublette"}, {"art": "ohne_code"}]) \
== "1 Funktion doppelt, 1 Funktion ohne eigenen Code"
def test_halbe_datei_ueberschreibt_nicht_die_ganze(projekt, monkeypatch):
"""Absturz beim Schreiben: die alte Datei muss stehen bleiben."""
stand = ledger.lade(projekt)
stand["belege"]["a"] = {"abdruck": "x", "datei": "a.py", "beleg": "z"}
ledger.sichere(projekt, stand)
echt = ledger.json.dumps
def platzt(*a, **k):
raise MemoryError("mittendrin")
monkeypatch.setattr(ledger.json, "dumps", platzt)
with pytest.raises(MemoryError):
ledger.sichere(projekt, {"belege": {"b": {}}})
monkeypatch.setattr(ledger.json, "dumps", echt)
assert ledger.lade(projekt)["belege"] == stand["belege"]
async def test_ausgefallene_stimmen_merken_nichts(projekt, monkeypatch):
"""Fällt der Anbieter aus, darf keine Löschempfehlung im Gedächtnis landen."""
karte(projekt, "Eins", ("Etwas starten", "Knopf"))
async def tot(system, nachricht, temperatur=0.0):
raise RuntimeError("429")
monkeypatch.setattr(agents, "frage", tot)
stand = ledger.lade(projekt)
gemeldet = []
assert await inhalt.feature_im_code(features.liste(projekt), QUELLE, "x", stand,
melde=lambda t, n=0: gemeldet.append(n)) == []
assert gemeldet == [1] # laut gescheitert, nicht still
assert stand["ohne_code"] == {}
assert stand["belege"] == {}
async def test_ein_ausfall_rettet_die_anderen_urteile(projekt, monkeypatch):
"""Eine kaputte Stelle darf die bezahlten Urteile der anderen nicht wegwerfen."""
karte(projekt, "Eins", ("Etwas starten", "Knopf"))
async def launisch(system, nachricht, temperatur=0.0):
if "bedienen:kaputt" in nachricht:
raise RuntimeError("429")
return ABGEDECKT
monkeypatch.setattr(agents, "frage", launisch)
stand = ledger.lade(projekt)
gemeldet = []
await inhalt.code_im_feature(
features.liste(projekt),
[punkt(), punkt(schluessel="bedienen:kaputt", beleg="bedienen:kaputt")],
stand, melde=lambda t, n=0: gemeldet.append(n))
assert gemeldet == [1]
assert "bedienen:starte" in stand["abdeckung"]
assert "bedienen:kaputt" not in stand["abdeckung"]
async def test_projekt_ohne_karten_stuerzt_nicht_ab(projekt, monkeypatch):
monkeypatch.setattr(agents, "frage", Zaehler(FEHLT))
stand = ledger.lade(projekt)
assert await inhalt.code_im_feature([], [punkt()], stand) == []
assert await inhalt.dubletten([], stand) == []
assert await inhalt.feature_im_code([], QUELLE, "x", stand) == []
async def test_feststehendes_urteil_braucht_keine_vollen_stimmen(projekt, monkeypatch):
"""Vier von sechs sagen „abgedeckt": die zwei fehlenden könnten die Schwelle
nicht mehr reißen. Daraus einen Fehlschlag zu machen wäre Verschwendung."""
karte(projekt, "Eins", ("Etwas starten", "Knopf"))
ausfaelle = {"n": 0}
async def zwei_platzen(system, nachricht, temperatur=0.0):
ausfaelle["n"] += 1
if ausfaelle["n"] <= 2:
raise RuntimeError("429")
return ABGEDECKT
monkeypatch.setattr(agents, "frage", zwei_platzen)
stand = ledger.lade(projekt)
gemeldet = []
assert await inhalt.code_im_feature(features.liste(projekt), [punkt()], stand,
melde=lambda t, n=0: gemeldet.append(n)) == []
assert gemeldet == [] # kein Fehler: das Ergebnis stand fest
assert "bedienen:starte" in stand["abdeckung"]
async def test_offenes_urteil_bei_ausfall_wird_gemeldet(projekt, monkeypatch):
"""Vier Stimmen sagen „fehlt", zwei fehlen — mit ihnen wäre die Schwelle
erreicht. Das ist offen und muss laut scheitern, nicht still durchgehen."""
karte(projekt, "Eins", ("Etwas starten", "Knopf"))
ausfaelle = {"n": 0}
async def zwei_platzen(system, nachricht, temperatur=0.0):
ausfaelle["n"] += 1
if ausfaelle["n"] <= 2:
raise RuntimeError("429")
return FEHLT
monkeypatch.setattr(agents, "frage", zwei_platzen)
stand = ledger.lade(projekt)
gemeldet = []
assert await inhalt.code_im_feature(features.liste(projekt), [punkt()], stand,
melde=lambda t, n=0: gemeldet.append(n)) == []
assert gemeldet == [1]
assert stand["abdeckung"] == {} and stand["ohne_karte"] == {}
async def test_zwei_ueberlebende_stimmen_urteilen_nicht(projekt, monkeypatch):
"""Rechnerisch könnten zwei Stimmen die Schwelle unerreichbar machen. Als
dauerhaft gemerktes Urteil ist das zu wenig."""
karte(projekt, "Eins", ("Etwas starten", "Knopf"))
ausfaelle = {"n": 0}
async def vier_platzen(system, nachricht, temperatur=0.0):
ausfaelle["n"] += 1
if ausfaelle["n"] <= 4:
raise RuntimeError("429")
return ABGEDECKT
monkeypatch.setattr(agents, "frage", vier_platzen)
stand = ledger.lade(projekt)
gemeldet = []
await inhalt.code_im_feature(features.liste(projekt), [punkt()], stand,
melde=lambda t, n=0: gemeldet.append(n))
assert gemeldet == [1]
assert stand["abdeckung"] == {}
# ---------------------------------------------------------------- Vision
VISION = "<!-- titel -->\nVision\n<!-- art -->\nvision\n<!-- ziel -->\nSo soll es werden\n"
@@ -530,3 +101,65 @@ def test_keine_pruefung_fasst_die_vision_an(projekt):
assert len(features.liste(projekt)) == 2
titel = [f["sektionen"]["titel"] for f in features.pruefbar(projekt)]
assert titel == ["Eins"]
# ---------------------------------------------------------------- Prüfung 1
async def test_stilurteil_wird_gemerkt_und_nicht_neu_gefragt(projekt, monkeypatch):
"""176 Anfragen je Lauf brachten den Anbieter zum Drosseln. Unveränderter
Ausschnitt bei unverändertem Katalog heißt: das Urteil von vorhin gilt."""
import pruefung
karte(projekt, "Eins", ("Etwas starten", "Knopf"))
modell = Zaehler('{"felder": {}}')
monkeypatch.setattr(agents, "frage", modell)
liste, arbeite, abschluss = await pruefung._stil_aufgaben(projekt)
for a in liste:
assert await arbeite(a, None) == []
abschluss()
erste = modell.rufe
assert erste >= len(pruefung.TEMPERATUREN)
liste, arbeite, abschluss = await pruefung._stil_aufgaben(projekt)
for a in liste:
assert await arbeite(a, None) == []
assert modell.rufe == erste # keine einzige neue Anfrage
async def test_geaenderter_text_wird_neu_gefragt(projekt, monkeypatch):
import pruefung
karte(projekt, "Eins", ("Etwas starten", "Knopf"))
modell = Zaehler('{"felder": {}}')
monkeypatch.setattr(agents, "frage", modell)
liste, arbeite, abschluss = await pruefung._stil_aufgaben(projekt)
for a in liste:
await arbeite(a, None)
abschluss()
vorher = modell.rufe
datei = projekte.wurzel(projekt) / f"{features.liste(projekt)[0]['id']}.md"
datei.write_text(datei.read_text().replace("- Knopf", "- Ganz anderer Text"))
liste, arbeite, _ = await pruefung._stil_aufgaben(projekt)
for a in liste:
await arbeite(a, None)
assert modell.rufe > vorher
async def test_gemessener_verstoss_kommt_nie_aus_dem_gedaechtnis(projekt, monkeypatch):
"""Sonst bliebe eine neu hinzugekommene Messregel an alten Karten wirkungslos."""
import pruefung
karte(projekt, "Eins", ("Etwas starten", "Auch Passwort prüfen."))
modell = Zaehler('{"felder": {"regeln": {"neu": "- Passwort prüfen",'
' "grund": "Füllwort", "regel": "x"}}}')
monkeypatch.setattr(agents, "frage", modell)
for lauf in range(2):
liste, arbeite, abschluss = await pruefung._stil_aufgaben(projekt)
gefunden = [p for a in liste for p in await arbeite(a, None)]
abschluss()
assert [p["vorschlag"] for p in gefunden] == ["Passwort prüfen"], lauf
assert modell.rufe > len(pruefung.TEMPERATUREN) # zweimal wirklich gefragt

View File

@@ -33,8 +33,10 @@ def test_alle_sektionen():
"ziel": "",
"regeln": "- Passwort nie loggen\n- Sperre nach 5 Fehlversuchen",
"testen": "- Formular öffnen\n- Daten eingeben",
"stand": "gebaut",
},
{"text": "Abmeldung", "ziel": "", "regeln": "", "testen": ""},
{"text": "Abmeldung", "ziel": "", "regeln": "", "testen": "",
"stand": "gebaut"},
]
@@ -114,3 +116,22 @@ def test_marker_mit_leerraum():
def test_titel_erste_nicht_leere_zeile():
s = zerlege("<!-- titel -->\n\nErste\nZweite\n")
assert s["titel"] == "Erste"
def test_stand_offen_wird_gelesen():
"""Bestellt, aber noch nicht gebaut — sonst klagt die Vorwärtsprüfung sie an."""
s = zerlege("<!-- titel -->\nX\n<!-- funktion -->\nA\n<!-- stand -->\noffen\n"
"<!-- funktion -->\nB\n")
assert [(f["text"], f["stand"]) for f in s["funktionen"]] == [("A", "offen"),
("B", "gebaut")]
assert s["sonstiges"] == "" # der Marker landet nicht im Rest
def test_unbekannter_stand_gilt_als_gebaut():
s = zerlege("<!-- titel -->\nX\n<!-- funktion -->\nA\n<!-- stand -->\nirgendwas\n")
assert s["funktionen"][0]["stand"] == "gebaut"
def test_stand_ohne_funktion_faellt_in_sonstiges():
s = zerlege("<!-- titel -->\nX\n<!-- stand -->\noffen\n")
assert s["sonstiges"] == "offen"

View File

@@ -0,0 +1,201 @@
"""Die Planung: eine Arbeitskopie, die den Projektstand nicht anfasst.
Geprüft wird die eine Zusage — solange die Planung läuft, bleibt die Übersicht,
was sie war. Und dass sie einen Neustart übersteht, weil sie auf der Platte liegt.
"""
import features
import parser
import planung
import projekte
import pytest
def karte(projekt: str, titel: str, *funktionen: str) -> str:
roh = f"<!-- titel -->\n{titel}\n" + "".join(
f"{'-' * 40}\n<!-- funktion -->\n{f}\n<!-- regeln -->\n- Etwas gilt\n"
for f in funktionen)
return features.anlegen(projekt, roh)["id"]
def titel(projekt: str) -> list:
return sorted(f["sektionen"]["titel"] for f in features.liste(projekt))
@pytest.fixture
def projekt(datenordner):
projekte.anlegen("P")
karte("P", "Galerie", "Bild wählen")
karte("P", "Shop", "Karte kaufen")
return "P"
def test_ohne_planung_arbeitet_alles_im_projektordner(projekt):
assert not planung.laeuft(projekt)
assert projekte.wurzel(projekt) == projekte.stand(projekt)
assert planung.aenderungen(projekt) == {}
def test_planung_leitet_das_schreiben_um(projekt):
planung.starte(projekt)
assert projekte.wurzel(projekt) != projekte.stand(projekt)
karte(projekt, "Neu", "Etwas tun")
# Die Karte steht in der Kopie …
assert "Neu" in titel(projekt)
# … und nicht im Projektstand
assert not (projekte.stand(projekt) / "neu.md").exists()
def test_zweite_planung_wird_abgelehnt(projekt):
planung.starte(projekt)
with pytest.raises(ValueError):
planung.starte(projekt)
def test_planung_ueberlebt_einen_neustart(projekt):
planung.starte(projekt)
karte(projekt, "Neu", "Etwas tun")
# Nichts im Speicher — der nächste Prozess sieht nur die Platte
assert planung.laeuft(projekt)
assert "Neu" in planung.aenderungen(projekt)
def test_abbruch_laesst_den_projektstand_unberuehrt(projekt):
vorher = titel(projekt)
planung.starte(projekt)
karte(projekt, "Neu", "Etwas tun")
planung.brich_ab(projekt)
assert not planung.laeuft(projekt)
assert titel(projekt) == vorher
def test_umsetzen_macht_die_kopie_zum_stand(projekt):
planung.starte(projekt)
karte(projekt, "Neu", "Etwas tun")
planung.setze_um(projekt)
assert not planung.laeuft(projekt)
assert "Neu" in titel(projekt)
assert not (projekte.stand(projekt) / projekte.PLANUNG).exists()
def test_umsetzen_ohne_planung_ist_ein_fehler(projekt):
with pytest.raises(ValueError):
planung.setze_um(projekt)
# ---------------------------------------------------------------- Änderungen
def test_neue_karte_ist_neu(projekt):
planung.starte(projekt)
karte(projekt, "Neu", "Etwas tun")
a = planung.aenderungen(projekt)
assert a["Neu"]["art"] == "neu"
assert a["Neu"]["funktionen"] == {"Etwas tun": "neu"}
assert "Galerie" not in a
def test_entfernte_karte_ist_weg(projekt):
planung.starte(projekt)
features.loeschen(projekt, "shop")
a = planung.aenderungen(projekt)
assert a["Shop"]["art"] == "weg"
assert a["Shop"]["funktionen"] == {"Karte kaufen": "weg"}
def test_neue_funktion_wird_markiert(projekt):
planung.starte(projekt)
f = next(x for x in features.liste(projekt) if x["sektionen"]["titel"] == "Galerie")
features.speichern(projekt, f["id"],
f["roh"] + f"{'-' * 40}\n<!-- funktion -->\nBild löschen\n")
a = planung.aenderungen(projekt)
assert a["Galerie"]["art"] == "geaendert"
assert a["Galerie"]["funktionen"] == {"Bild löschen": "neu"}
def test_geaenderte_regel_wird_markiert(projekt):
planung.starte(projekt)
f = next(x for x in features.liste(projekt) if x["sektionen"]["titel"] == "Galerie")
features.speichern(projekt, f["id"], f["roh"].replace("Etwas gilt", "Anderes gilt"))
assert planung.aenderungen(projekt)["Galerie"]["funktionen"] == {
"Bild wählen": "geaendert"}
def test_umbenannte_karte_ist_neu_und_weg(projekt):
"""Verglichen wird über den Titel. Eine Umbenennung ist zwei Änderungen."""
planung.starte(projekt)
f = next(x for x in features.liste(projekt) if x["sektionen"]["titel"] == "Shop")
features.speichern(projekt, f["id"], f["roh"].replace("Shop", "Laden", 1))
a = planung.aenderungen(projekt)
assert a["Shop"]["art"] == "weg" and a["Laden"]["art"] == "neu"
def test_gleicher_inhalt_ist_keine_aenderung(projekt):
planung.starte(projekt)
f = features.liste(projekt)[0]
features.speichern(projekt, f["id"], f["roh"])
assert planung.aenderungen(projekt) == {}
# ---------------------------------------------------------------- Zurücksetzen
def test_zuruecksetzen_loescht_eine_neue_karte(projekt):
planung.starte(projekt)
karte(projekt, "Neu", "Etwas tun")
planung.zuruecksetzen(projekt, "Neu")
assert planung.aenderungen(projekt) == {}
assert "Neu" not in titel(projekt)
def test_zuruecksetzen_holt_eine_geloeschte_karte_zurueck(projekt):
planung.starte(projekt)
features.loeschen(projekt, "shop")
planung.zuruecksetzen(projekt, "Shop")
assert planung.aenderungen(projekt) == {}
assert "Shop" in titel(projekt)
def test_zuruecksetzen_stellt_den_alten_inhalt_her(projekt):
planung.starte(projekt)
f = next(x for x in features.liste(projekt) if x["sektionen"]["titel"] == "Galerie")
features.speichern(projekt, f["id"], f["roh"].replace("Etwas gilt", "Anderes gilt"))
planung.zuruecksetzen(projekt, "Galerie")
assert planung.aenderungen(projekt) == {}
def test_zuruecksetzen_laesst_die_uebrigen_stehen(projekt):
planung.starte(projekt)
karte(projekt, "Eins", "A tun")
karte(projekt, "Zwei", "B tun")
planung.zuruecksetzen(projekt, "Eins")
assert set(planung.aenderungen(projekt)) == {"Zwei"}
def test_zuruecksetzen_ohne_planung_ist_ein_fehler(projekt):
with pytest.raises(ValueError):
planung.zuruecksetzen(projekt, "Galerie")
def test_unbekannte_karte_zuruecksetzen_meldet_es(projekt):
planung.starte(projekt)
with pytest.raises(FileNotFoundError):
planung.zuruecksetzen(projekt, "Gibt es nicht")
# ---------------------------------------------------------------- Nachbarschaft
def test_die_kopie_ist_kein_eigenes_projekt(projekt):
planung.starte(projekt)
assert [p["name"] for p in projekte.liste()] == ["P"]
def test_gespraech_und_gedaechtnis_wandern_mit(projekt):
"""Was während des Planens beurteilt wurde, gilt für den geplanten Stand."""
(projekte.stand(projekt) / ".pruefung.json").write_text('{"stil": {}}')
planung.starte(projekt)
assert (projekte.wurzel(projekt) / ".pruefung.json").is_file()
def test_die_kopie_kopiert_sich_nicht_selbst(projekt):
planung.starte(projekt)
assert not (projekte.wurzel(projekt) / projekte.PLANUNG).exists()

View File

@@ -197,8 +197,9 @@ async def test_herkunft_kommt_vom_code_nicht_vom_modell(datenordner, monkeypatch
async def luegt(system, inhalt, temperatur=0.0):
# Das Modell behauptet eine fremde Funktion — das muss ignoriert werden
# „Anmelden" ist selbst ein gültiger Titel — sonst weist die Messung ihn ab
return ('{"felder": {"titel": {"regel": "Sprache: Deutsch, Präsens, keine Anrede",'
' "neu": "Anders", "grund": "x"},'
' "neu": "Einloggen", "grund": "x"},'
' "funktion": {"neu": "Erfunden", "grund": "y"}}}')
monkeypatch.setattr(agents, "frage", luegt)

View File

@@ -166,3 +166,96 @@ def test_gemessene_befunde_haengen_am_ausschnitt(datenordner):
text = pruefung.inhalt(karte, befunde)
assert "Gemessen, bitte neu schreiben:" in text
assert "Mehr als 3 Wörter" in text
def test_funktionstitel_braucht_ein_verb():
"""Ohne Verb steht dort ein Bereich oder ein Zustand — nichts Testbares."""
for titel in ("Galerie im Modal", "Preis aktualisiert", "In den Warenkorb",
"Modal tastaturgesteuert"):
assert _probleme(_pruefe("funktion", "titel", titel)) \
== ["Kein Verb im Infinitiv am Ende"], titel
def test_gute_funktionstitel_bleiben_unberuehrt():
for titel in ("Feature anlegen", "Anzeigemodus umschalten", "Option wählen",
"Nächsten Rückschritt anzeigen", "Karten prüfen", "Stand umschalten",
"Projekt aufrufen", "Bildergalerie bedienen", "PDF hochladen"):
assert _pruefe("funktion", "titel", titel) == [], titel
def test_kartentitel_darf_ohne_verb_stehen():
"""Eine Karte ist ein Gegenstand, keine Handlung."""
assert _pruefe("feature", "titel", "Karten prüfen") == []
assert _pruefe("feature", "titel", "Darkmode") == []
ABLAUF = "- Vollmodus öffnen\n- LMB auf Regeln\n- Text ändern\n- Regeln angepasst"
def test_ein_ablauf_geht_durch():
assert _pruefe("funktion", "testen", ABLAUF) == []
def test_zweiter_ablauf_wird_gemeldet():
"""So entstand „Bildergalerie bedienen": blättern, Miniatur, Großansicht in einem."""
zwei = ("- Zurück-Button klicken\n- Vorheriges Bild erscheint\n"
"- Miniaturbild klicken\n- Zugehöriges Bild erscheint")
b = _pruefe("funktion", "testen", zwei)
assert _probleme(b) == ["Zweiter Ablauf in einer Funktion"]
assert b[0]["original"] == "Miniaturbild klicken"
def test_nominalstil_ist_eine_handlung():
"""„Bestätigen im Modal" hat das Verb vorn — sonst gälte es als Ergebnis."""
text = ("- RMB auf Featurecard\n- Klick auf \"Feature entfernen\"\n"
"- Bestätigen im Modal\n- Feature entfernt")
assert _pruefe("funktion", "testen", text) == []
def test_mehrzahl_auf_en_ist_kein_verb():
"""Deutsche Mehrzahl endet auch auf -en — nur Anfang und Ende zählen."""
text = "- LMB auf Icon\n- Fehlerhafte Karten rot markiert"
assert _pruefe("funktion", "testen", text) == []
def test_zeilenfehler_haben_vorrang():
"""Erst die Zeilen in Ordnung bringen; zwei Vorschläge je Feld wären nicht
unabhängig anwendbar."""
text = "- Auch klicken.\n- Ergebnis da\n- Weiter klicken"
assert _probleme(_pruefe("funktion", "testen", text)) \
== ["Satzzeichen am Zeilenende, Füllwort „auch“"]
def test_funktion_ohne_jedes_feld_ist_leer():
"""Ein bloßer Titel beschreibt nichts — der Prüfer stülpt ihn über alles."""
b = struktur.befunde("funktion", {"titel": "Modal steuern", "ziel": "",
"regeln": "", "testen": ""})
assert _probleme(b) == ["Funktion ohne Ziel, Regeln und Testen"]
def test_ein_einziges_feld_genuegt():
for feld in ("ziel", "regeln", "testen"):
felder = {"titel": "Modal steuern", "ziel": "", "regeln": "", "testen": ""}
felder[feld] = "Etwas tun" if feld == "ziel" else "- Etwas tun"
assert struktur.befunde("funktion", felder) == [], feld
def test_einzelnes_feld_loest_nichts_aus():
"""Beim Prüfen eines Feldes sagt das Fehlen der anderen nichts."""
assert struktur.befunde("funktion", {"titel": "Modal steuern"}) == []
def test_kartenebene_darf_leer_sein():
assert struktur.befunde("feature", {"titel": "Darkmode", "badge": "kern",
"ziel": "", "regeln": ""}) == []
def test_platzhalter_ist_kein_inhalt():
"""`<testschritt>` erfüllt jede Formregel und sagt trotzdem nichts."""
assert struktur._umschreiben("funktion", "testen", "<testschritt>")
assert struktur._umschreiben("funktion", "regeln", "Noch offen ...")
assert struktur._umschreiben("funktion", "regeln", "TODO klären")
def test_kleiner_als_ist_kein_platzhalter():
assert not struktur._umschreiben("funktion", "regeln", "Wert 3 < 5")

View File

@@ -0,0 +1,537 @@
{
"abdeckung": {
"einbinden:VariantConfiguratorPlugin": {
"abdruck": "823481507a44729d",
"funktion": "Konfigurationsdaten bereitstellen",
"trug": "5fbc8363b7cffe59"
},
"einbinden:erweitert @Storefront/storefront/component/buy-widget/configurator.html.twig": {
"abdruck": "c6d031d6bc598089",
"funktion": "Konfigurator aktivieren",
"trug": "6cf5a2b08ce2371a"
},
"einbinden:override sw-product-detail-variants": {
"abdruck": "2fcf518e7333458a",
"funktion": "",
"trug": ""
},
"einbinden:register mto-vc-variants-card": {
"abdruck": "c472feadaac5ac71",
"funktion": "",
"trug": ""
},
"hoeren:[data-mto-vc-gallery-next].click": {
"abdruck": "c402ad6f114e8c3d",
"funktion": "Galerie blättern",
"trug": "bacf45ebf9dd77b8"
},
"hoeren:[data-mto-vc-gallery-prev].click": {
"abdruck": "f50d4f7effc60a06",
"funktion": "Galerie blättern",
"trug": "bacf45ebf9dd77b8"
},
"hoeren:[data-mto-vc-lightbox-close].click": {
"abdruck": "89a22850af7694bb",
"funktion": "Galerie blättern",
"trug": "bacf45ebf9dd77b8"
},
"hoeren:document.keydown": {
"abdruck": "7c018ebbfa346bee",
"funktion": "Schritt öffnen",
"trug": "efe9634e3080c073"
},
"hoeren:el.click": {
"abdruck": "6a423a062267755c",
"funktion": "Nächsten Schritt anzeigen",
"trug": "f6b276e3fedf287d"
},
"hoeren:info.click": {
"abdruck": "7a5a70dfb792bf19",
"funktion": "Optionsdetail öffnen",
"trug": "39fed29af6734cd4"
},
"hoeren:info.keydown": {
"abdruck": "409cecf6fb3bbb4a",
"funktion": "Optionsdetail öffnen",
"trug": "39fed29af6734cd4"
},
"hoeren:lightboxSelect.click": {
"abdruck": "9add2d2c77b440b5",
"funktion": "Ansicht schließen",
"trug": "24d52cc6d249d4f5"
},
"hoeren:next.click": {
"abdruck": "d5b0cdcce40afcff",
"funktion": "Konfigurator schließen",
"trug": "1cc1a6b5fc943e03"
},
"hoeren:prev.click": {
"abdruck": "6b8db8d885c0d6f8",
"funktion": "Konfigurator schließen",
"trug": "1cc1a6b5fc943e03"
},
"hoeren:row.click": {
"abdruck": "b4ec6f63c613c35f",
"funktion": "Option wählen",
"trug": "80df89cf0c129387"
},
"hoeren:thumb.click": {
"abdruck": "b4aa2111971f6886",
"funktion": "Bild wählen",
"trug": "0604a0a6875489b8"
},
"hoeren:trigger.click": {
"abdruck": "e219933925adc81a",
"funktion": "Schritt öffnen",
"trug": "efe9634e3080c073"
},
"reaktion:ProductPageLoadedEvent": {
"abdruck": "6f808b91c1a0c4f5",
"funktion": "Konfigurationsdaten bereitstellen",
"trug": "5fbc8363b7cffe59"
},
"route:get /mto-variant-configurator/resolve/{productId}": {
"abdruck": "b14355d6007564a1",
"funktion": "Konfigurationsdaten bereitstellen",
"trug": "5fbc8363b7cffe59"
}
},
"belege": {
"[\"auswahl-anstossen\", \"Gewählte Option anzeigen\"]": {
"abdruck": "626af3ee300b610a",
"beleg": "<span class=\"mto-vc-select-trigger-label\" data-mto-vc-trigger-label>\n {{ mtoVcSelected ? mtoVcSelected.translated.name : 'mto.variantConfigurator.choose'|trans }}\n </span>",
"datei": "src/Resources/views/storefront/component/buy-widget/configurator.html.twig"
},
"[\"auswahl-anstossen\", \"Hinweistext anzeigen\"]": {
"abdruck": "b9baf394ca99d4e6",
"beleg": "{% if mtoVcBubble != mtoVcBubbleKey and mtoVcBubble is not empty %}\n <p class=\"mto-vc-bubble\">{{ mtoVcBubble|sw_sanitize }}</p>\n {% endif %}",
"datei": "src/Resources/views/storefront/component/buy-widget/configurator.html.twig"
},
"[\"auswahl-anstossen\", \"Schrittnummer anzeigen\"]": {
"abdruck": "b9e4deada2c3a6f1",
"beleg": "<span class=\"mto-vc-step-badge\">{{ 'mto.variantConfigurator.step'|trans({ '%step%': mtoVcStep })|sw_sanitize }}</span>",
"datei": "src/Resources/views/storefront/component/buy-widget/configurator.html.twig"
},
"[\"beratung-anbieten\", \"Beratung erreichen\"]": {
"abdruck": "fa513f8d265ad713",
"beleg": "{% if ctaPhone is not empty %}\n <span class=\"mto-vc-support-icon\" aria-hidden=\"true\"></span>\n <a href=\"tel:{{ ctaPhone|replace({' ': '', '/': '', '-': '', '(': '', ')': ''}) }}\"\n class=\"mto-vc-support-phone\">{{ ctaPhone|sw_sanitize }}</a>\n {% endif %}",
"datei": "src/Resources/views/storefront/component/buy-widget/mto-variant-configurator-modal.html.twig"
},
"[\"beratung-anbieten\", \"Optionsübersicht öffnen\"]": {
"abdruck": "8aba7fb9bf50281b",
"beleg": "{% if pdfUrl %}\n <a href=\"{{ pdfUrl }}\" target=\"_blank\" rel=\"noopener\"\n class=\"mto-vc-btn is-outline mto-vc-all-options\">\n {{ 'mto.variantConfigurator.allOptions'|trans|sw_sanitize }}\n </a>\n {% else %}\n <span class=\"mto-vc-actions-spacer\" aria-hidden=\"true\"></span>\n {% endif %}",
"datei": "src/Resources/views/storefront/component/buy-widget/mto-variant-configurator-modal.html.twig"
},
"[\"beratung-anbieten\", \"Vertrauen zeigen\"]": {
"abdruck": "5b204dff7de6f96d",
"beleg": "{% if trustLogo %}\n <img src=\"{{ trustLogo }}\" alt=\"\" class=\"mto-vc-trust-logo\">\n{% endif %}",
"datei": "src/Resources/views/storefront/component/buy-widget/mto-variant-configurator-modal.html.twig"
},
"[\"layout-zuweisen\", \"Konfigurator aktivieren\"]": {
"abdruck": "60a65511f06ae7fe",
"beleg": "<sw-switch-field\n v-model:value=\"configuratorActive\"\n class=\"mto-vc-variants-card__active\"\n :disabled=\"!editable\"\n :label=\"$tc('mto-variant-configurator.card.activeLabel')\"\n :help-text=\"$tc('mto-variant-configurator.card.activeHelp')\"\n />",
"datei": "src/Resources/app/administration/src/component/mto-vc-variants-card/mto-vc-variants-card.html.twig"
},
"[\"layout-zuweisen\", \"Optionsübersicht hinterlegen\"]": {
"abdruck": "97b421cf540a6cc2",
"beleg": "{% if pdfUrl %}\n <a href=\"{{ pdfUrl }}\" target=\"_blank\" rel=\"noopener\"\n class=\"mto-vc-btn is-outline mto-vc-all-options\">\n {{ 'mto.variantConfigurator.allOptions'|trans|sw_sanitize }}\n </a>\n {% else %}\n <span class=\"mto-vc-actions-spacer\" aria-hidden=\"true\"></span>\n {% endif %}",
"datei": "src/Resources/views/storefront/component/buy-widget/mto-variant-configurator-modal.html.twig"
},
"[\"layout-zuweisen\", \"Trust-Logo hinterlegen\"]": {
"abdruck": "1954311a4c3a9cc1",
"beleg": "$mediaId = $this->systemConfigService->get(\n MtoVariantConfigurator::CONFIG_TRUST_LOGO,\n $salesChannelId,\n );",
"datei": "src/Core/Content/Product/VariantResolveService.php"
},
"[\"option-ansehen\", \"Ansicht schließen\"]": {
"abdruck": "6eac493d9a481e30",
"beleg": "if (!this._nodes.lightbox.hidden) {\n this._closeLightbox();\n } else {\n this._close();\n }",
"datei": "src/Resources/app/storefront/src/js/variant-configurator.plugin.js"
},
"[\"option-ansehen\", \"Option aus Ansicht wählen\"]": {
"abdruck": "b4223f8cf3e2456e",
"beleg": "this._nodes.lightboxSelect?.addEventListener('click', () => {\n if (this._lightboxOption) {\n this._selectOption(this.options.groups[this._currentStep], this._lightboxOption.id);\n }\n this._closeLightbox();\n });",
"datei": "src/Resources/app/storefront/src/js/variant-configurator.plugin.js"
},
"[\"option-ansehen\", \"Optionsdetail öffnen\"]": {
"abdruck": "59faab581e0fac7f",
"beleg": "info.addEventListener('click', open);\n info.addEventListener('keydown', (event) => {\n if (event.key === 'Enter' || event.key === ' ') {\n open(event);\n }\n });",
"datei": "src/Resources/app/storefront/src/js/variant-configurator.plugin.js"
},
"[\"schritt-zeigen\", \"Eigenschaft benennen\"]": {
"abdruck": "dd54064a67689e2c",
"beleg": "this._nodes.title.textContent = group.name;",
"datei": "src/Resources/app/storefront/src/js/variant-configurator.plugin.js"
},
"[\"schritt-zeigen\", \"Optionen auflisten\"]": {
"abdruck": "a71af321f056608d",
"beleg": "if (option.media) {\n const visual = document.createElement('span');\n visual.className = 'mto-vc-option-media';\n const img = document.createElement('img');\n img.src = option.media;\n img.alt = '';\n img.loading = 'lazy';\n visual.appendChild(img);\n row.appendChild(visual);\n } else if (option.color) {\n const visual = document.createElement('span');\n visual.className = 'mto-vc-option-swatch';\n visual.style.backgroundColor = option.color;\n row.appendChild(visual);\n } else {\n row.classList.add('has-no-visual');\n }",
"datei": "src/Resources/app/storefront/src/js/variant-configurator.plugin.js"
},
"[\"schritt-zeigen\", \"Schrittstand anzeigen\"]": {
"abdruck": "aa7b8888103f8bd0",
"beleg": "this._nodes.counter.textContent = (this._snippets.stepCount || '__step__ / __total__')\n .replace('__step__', String(step))\n .replace('__total__', String(total));\n this._nodes.progress.style.width = `${(step / total) * 100}%`;",
"datei": "src/Resources/app/storefront/src/js/variant-configurator.plugin.js"
},
"[\"varianten-waehlen\", \"Auswahl übernehmen\"]": {
"abdruck": "21ccf5bc0286c95c",
"beleg": "if (optionId && select.value !== optionId) {\n select.value = optionId;\n lastChanged = select;\n }",
"datei": "src/Resources/app/storefront/src/js/variant-configurator.plugin.js"
},
"[\"varianten-waehlen\", \"Konfigurationsdaten bereitstellen\"]": {
"abdruck": "e6e5848381fce5aa",
"beleg": "$page->assign([",
"datei": "src/Subscriber/ProductPageSubscriber.php"
},
"[\"varianten-waehlen\", \"Konfigurator schließen\"]": {
"abdruck": "1b7d4aea18cdb896",
"beleg": "_close() {\n this._closeLightbox();\n this._modalEl.hidden = true;\n document.body.classList.remove('mto-vc-modal-open');\n document.removeEventListener('keydown', this._keydownHandler);\n this._lastFocus?.focus();\n }",
"datei": "src/Resources/app/storefront/src/js/variant-configurator.plugin.js"
},
"[\"varianten-waehlen\", \"Nächsten Schritt anzeigen\"]": {
"abdruck": "3deee97595984a3e",
"beleg": "const isLast = this._nextUsableStep(this._currentStep + 1, 1) === null;\n this._nodes.next.textContent = isLast\n ? this._nodes.next.dataset.finishLabel\n : this._nodes.next.dataset.nextLabel;",
"datei": "src/Resources/app/storefront/src/js/variant-configurator.plugin.js"
},
"[\"varianten-waehlen\", \"Option wählen\"]": {
"abdruck": "4b9f7dc844cf7aac",
"beleg": "this._selected[group.id] = optionId;",
"datei": "src/Resources/app/storefront/src/js/variant-configurator.plugin.js"
},
"[\"varianten-waehlen\", \"Schritt öffnen\"]": {
"abdruck": "94b3aa8d50c53fc8",
"beleg": "this._triggers.forEach((trigger) => {\n trigger.addEventListener('click', (event) => {\n event.preventDefault();\n this._open(parseInt(trigger.dataset.mtoVcStepIndex, 10));\n });\n });",
"datei": "src/Resources/app/storefront/src/js/variant-configurator.plugin.js"
},
"[\"varianten-waehlen\", \"Vorherigen Schritt anzeigen\"]": {
"abdruck": "df5e60fe148da5e6",
"beleg": "this._nodes.prev.disabled = this._nextUsableStep(this._currentStep - 1, -1) === null;",
"datei": "src/Resources/app/storefront/src/js/variant-configurator.plugin.js"
},
"[\"vorschau-zeigen\", \"Bild wählen\"]": {
"abdruck": "ab1e74251036541a",
"beleg": "thumb.className = `mto-vc-thumb${index === this._galleryIndex ? ' is-active' : ''}`;",
"datei": "src/Resources/app/storefront/src/js/variant-configurator.plugin.js"
},
"[\"vorschau-zeigen\", \"Galerie blättern\"]": {
"abdruck": "6b28a5866629b878",
"beleg": "_stepGallery(delta) {\n if (this._gallery.length < 2) {\n return;\n }\n const count = this._gallery.length;\n this._galleryIndex = (this._galleryIndex + delta + count) % count;\n this._renderGallery();\n }",
"datei": "src/Resources/app/storefront/src/js/variant-configurator.plugin.js"
},
"[\"vorschau-zeigen\", \"Vorschau laden\"]": {
"abdruck": "413024bc244f7770",
"beleg": "return new JsonResponse($this->variantResolveService->resolveVariant($variantId, $context));",
"datei": "src/Storefront/Controller/VariantConfiguratorController.php"
}
},
"ohne_code": {},
"ohne_karte": {},
"paare": {
"[\"[\\\"auswahl-anstossen\\\", \\\"Gewählte Option anzeigen\\\"]\", \"[\\\"auswahl-anstossen\\\", \\\"Hinweistext anzeigen\\\"]\"]": {
"abdruck": "450345e8a27f5695",
"gleich": false
},
"[\"[\\\"auswahl-anstossen\\\", \\\"Gewählte Option anzeigen\\\"]\", \"[\\\"auswahl-anstossen\\\", \\\"Schrittnummer anzeigen\\\"]\"]": {
"abdruck": "60008fdb6d69349b",
"gleich": false
},
"[\"[\\\"auswahl-anstossen\\\", \\\"Hinweistext anzeigen\\\"]\", \"[\\\"auswahl-anstossen\\\", \\\"Schrittnummer anzeigen\\\"]\"]": {
"abdruck": "3eaf19062d6d53a3",
"gleich": false
},
"[\"[\\\"beratung-anbieten\\\", \\\"Beratung erreichen\\\"]\", \"[\\\"beratung-anbieten\\\", \\\"Optionsübersicht öffnen\\\"]\"]": {
"abdruck": "9d3da3bf8c127ae1",
"gleich": false
},
"[\"[\\\"beratung-anbieten\\\", \\\"Beratung erreichen\\\"]\", \"[\\\"beratung-anbieten\\\", \\\"Vertrauen zeigen\\\"]\"]": {
"abdruck": "2c3c62cb4b935c56",
"gleich": false
},
"[\"[\\\"beratung-anbieten\\\", \\\"Optionsübersicht öffnen\\\"]\", \"[\\\"beratung-anbieten\\\", \\\"Vertrauen zeigen\\\"]\"]": {
"abdruck": "bf601d5dc7c6c507",
"gleich": false
},
"[\"[\\\"beratung-anbieten\\\", \\\"Optionsübersicht öffnen\\\"]\", \"[\\\"layout-zuweisen\\\", \\\"Optionsübersicht hinterlegen\\\"]\"]": {
"abdruck": "bc9dc09e54b78a75",
"gleich": false
},
"[\"[\\\"layout-zuweisen\\\", \\\"Konfigurator aktivieren\\\"]\", \"[\\\"layout-zuweisen\\\", \\\"Optionsbild am Datensatz pflegen\\\"]\"]": {
"abdruck": "5a4d802d668ddd55",
"gleich": false
},
"[\"[\\\"layout-zuweisen\\\", \\\"Konfigurator aktivieren\\\"]\", \"[\\\"layout-zuweisen\\\", \\\"Optionsübersicht hinterlegen\\\"]\"]": {
"abdruck": "eeed78a6284711f1",
"gleich": false
},
"[\"[\\\"layout-zuweisen\\\", \\\"Konfigurator aktivieren\\\"]\", \"[\\\"layout-zuweisen\\\", \\\"Trust-Logo hinterlegen\\\"]\"]": {
"abdruck": "b4944e5aa4df871a",
"gleich": false
},
"[\"[\\\"layout-zuweisen\\\", \\\"Optionsbild am Datensatz pflegen\\\"]\", \"[\\\"layout-zuweisen\\\", \\\"Optionsübersicht hinterlegen\\\"]\"]": {
"abdruck": "b91bdff82a120a8d",
"gleich": false
},
"[\"[\\\"layout-zuweisen\\\", \\\"Optionsbild am Datensatz pflegen\\\"]\", \"[\\\"layout-zuweisen\\\", \\\"Trust-Logo hinterlegen\\\"]\"]": {
"abdruck": "97268714e2b5d989",
"gleich": false
},
"[\"[\\\"layout-zuweisen\\\", \\\"Optionsübersicht hinterlegen\\\"]\", \"[\\\"layout-zuweisen\\\", \\\"Trust-Logo hinterlegen\\\"]\"]": {
"abdruck": "b52cedd4106ee837",
"gleich": false
},
"[\"[\\\"option-ansehen\\\", \\\"Ansicht schließen\\\"]\", \"[\\\"option-ansehen\\\", \\\"Option aus Ansicht wählen\\\"]\"]": {
"abdruck": "caa5ce4c5ff31774",
"gleich": false
},
"[\"[\\\"option-ansehen\\\", \\\"Ansicht schließen\\\"]\", \"[\\\"option-ansehen\\\", \\\"Optionsdetail öffnen\\\"]\"]": {
"abdruck": "462a46d7a2030874",
"gleich": false
},
"[\"[\\\"option-ansehen\\\", \\\"Option aus Ansicht wählen\\\"]\", \"[\\\"option-ansehen\\\", \\\"Optionsdetail öffnen\\\"]\"]": {
"abdruck": "8f695dfd90060204",
"gleich": false
},
"[\"[\\\"schritt-zeigen\\\", \\\"Eigenschaft benennen\\\"]\", \"[\\\"schritt-zeigen\\\", \\\"Optionen auflisten\\\"]\"]": {
"abdruck": "5372df6b482e9a5c",
"gleich": false
},
"[\"[\\\"schritt-zeigen\\\", \\\"Eigenschaft benennen\\\"]\", \"[\\\"schritt-zeigen\\\", \\\"Schrittstand anzeigen\\\"]\"]": {
"abdruck": "732d371c2135b53f",
"gleich": false
},
"[\"[\\\"schritt-zeigen\\\", \\\"Optionen auflisten\\\"]\", \"[\\\"schritt-zeigen\\\", \\\"Schrittstand anzeigen\\\"]\"]": {
"abdruck": "590d6974e948c69c",
"gleich": false
},
"[\"[\\\"varianten-waehlen\\\", \\\"Auswahl übernehmen\\\"]\", \"[\\\"varianten-waehlen\\\", \\\"Konfigurationsdaten bereitstellen\\\"]\"]": {
"abdruck": "34e23fca918e5dc0",
"gleich": false
},
"[\"[\\\"varianten-waehlen\\\", \\\"Auswahl übernehmen\\\"]\", \"[\\\"varianten-waehlen\\\", \\\"Konfigurator schließen\\\"]\"]": {
"abdruck": "eb521941a5dc979a",
"gleich": false
},
"[\"[\\\"varianten-waehlen\\\", \\\"Auswahl übernehmen\\\"]\", \"[\\\"varianten-waehlen\\\", \\\"Nächsten Schritt anzeigen\\\"]\"]": {
"abdruck": "271109c8b42cf5d5",
"gleich": false
},
"[\"[\\\"varianten-waehlen\\\", \\\"Auswahl übernehmen\\\"]\", \"[\\\"varianten-waehlen\\\", \\\"Option wählen\\\"]\"]": {
"abdruck": "618d3c213492555a",
"gleich": false
},
"[\"[\\\"varianten-waehlen\\\", \\\"Auswahl übernehmen\\\"]\", \"[\\\"varianten-waehlen\\\", \\\"Schritt öffnen\\\"]\"]": {
"abdruck": "b45252ce7cc39ed9",
"gleich": false
},
"[\"[\\\"varianten-waehlen\\\", \\\"Auswahl übernehmen\\\"]\", \"[\\\"varianten-waehlen\\\", \\\"Vorherigen Schritt anzeigen\\\"]\"]": {
"abdruck": "d7ade9bdd7d0c66e",
"gleich": false
},
"[\"[\\\"varianten-waehlen\\\", \\\"Konfigurationsdaten bereitstellen\\\"]\", \"[\\\"varianten-waehlen\\\", \\\"Konfigurator schließen\\\"]\"]": {
"abdruck": "97ceded85ca2035c",
"gleich": false
},
"[\"[\\\"varianten-waehlen\\\", \\\"Konfigurationsdaten bereitstellen\\\"]\", \"[\\\"varianten-waehlen\\\", \\\"Nächsten Schritt anzeigen\\\"]\"]": {
"abdruck": "ba83ab596b72af22",
"gleich": false
},
"[\"[\\\"varianten-waehlen\\\", \\\"Konfigurationsdaten bereitstellen\\\"]\", \"[\\\"varianten-waehlen\\\", \\\"Option wählen\\\"]\"]": {
"abdruck": "6f23248b5fbd7be4",
"gleich": false
},
"[\"[\\\"varianten-waehlen\\\", \\\"Konfigurationsdaten bereitstellen\\\"]\", \"[\\\"varianten-waehlen\\\", \\\"Schritt öffnen\\\"]\"]": {
"abdruck": "7350e93a2a368ac1",
"gleich": false
},
"[\"[\\\"varianten-waehlen\\\", \\\"Konfigurationsdaten bereitstellen\\\"]\", \"[\\\"varianten-waehlen\\\", \\\"Vorherigen Schritt anzeigen\\\"]\"]": {
"abdruck": "50753ef9f3d5c8b5",
"gleich": false
},
"[\"[\\\"varianten-waehlen\\\", \\\"Konfigurator schließen\\\"]\", \"[\\\"varianten-waehlen\\\", \\\"Nächsten Schritt anzeigen\\\"]\"]": {
"abdruck": "b49e65e1c60b5307",
"gleich": false
},
"[\"[\\\"varianten-waehlen\\\", \\\"Konfigurator schließen\\\"]\", \"[\\\"varianten-waehlen\\\", \\\"Option wählen\\\"]\"]": {
"abdruck": "b165f28b36fc3637",
"gleich": false
},
"[\"[\\\"varianten-waehlen\\\", \\\"Konfigurator schließen\\\"]\", \"[\\\"varianten-waehlen\\\", \\\"Schritt öffnen\\\"]\"]": {
"abdruck": "0bf509fed71f1c25",
"gleich": false
},
"[\"[\\\"varianten-waehlen\\\", \\\"Konfigurator schließen\\\"]\", \"[\\\"varianten-waehlen\\\", \\\"Vorherigen Schritt anzeigen\\\"]\"]": {
"abdruck": "de5acdbf92511e14",
"gleich": false
},
"[\"[\\\"varianten-waehlen\\\", \\\"Nächsten Schritt anzeigen\\\"]\", \"[\\\"varianten-waehlen\\\", \\\"Option wählen\\\"]\"]": {
"abdruck": "7c9a06867c701267",
"gleich": false
},
"[\"[\\\"varianten-waehlen\\\", \\\"Nächsten Schritt anzeigen\\\"]\", \"[\\\"varianten-waehlen\\\", \\\"Schritt öffnen\\\"]\"]": {
"abdruck": "f45378da83876bae",
"gleich": false
},
"[\"[\\\"varianten-waehlen\\\", \\\"Nächsten Schritt anzeigen\\\"]\", \"[\\\"varianten-waehlen\\\", \\\"Vorherigen Schritt anzeigen\\\"]\"]": {
"abdruck": "9fc4c9cf8ae1d7fb",
"gleich": false
},
"[\"[\\\"varianten-waehlen\\\", \\\"Option wählen\\\"]\", \"[\\\"varianten-waehlen\\\", \\\"Schritt öffnen\\\"]\"]": {
"abdruck": "a71f8b8b92d1025c",
"gleich": false
},
"[\"[\\\"varianten-waehlen\\\", \\\"Option wählen\\\"]\", \"[\\\"varianten-waehlen\\\", \\\"Vorherigen Schritt anzeigen\\\"]\"]": {
"abdruck": "e3270a7176c63620",
"gleich": false
},
"[\"[\\\"varianten-waehlen\\\", \\\"Schritt öffnen\\\"]\", \"[\\\"varianten-waehlen\\\", \\\"Vorherigen Schritt anzeigen\\\"]\"]": {
"abdruck": "44e775b8e8b106d1",
"gleich": false
},
"[\"[\\\"vorschau-zeigen\\\", \\\"Bild wählen\\\"]\", \"[\\\"vorschau-zeigen\\\", \\\"Galerie blättern\\\"]\"]": {
"abdruck": "19851d2901c309a1",
"gleich": false
},
"[\"[\\\"vorschau-zeigen\\\", \\\"Bild wählen\\\"]\", \"[\\\"vorschau-zeigen\\\", \\\"Vorschau laden\\\"]\"]": {
"abdruck": "508415e6500f9fca",
"gleich": false
},
"[\"[\\\"vorschau-zeigen\\\", \\\"Galerie blättern\\\"]\", \"[\\\"vorschau-zeigen\\\", \\\"Vorschau laden\\\"]\"]": {
"abdruck": "86aa1f412d5a5dcd",
"gleich": false
}
},
"stil": {
"[\"auswahl-anstossen\", \"\"]": {
"abdruck": "aeec25973dafe92f",
"probleme": []
},
"[\"auswahl-anstossen\", \"Gewählte Option anzeigen\"]": {
"abdruck": "59058982f7989af8",
"probleme": []
},
"[\"auswahl-anstossen\", \"Hinweistext anzeigen\"]": {
"abdruck": "b3568e0a2585d71c",
"probleme": []
},
"[\"auswahl-anstossen\", \"Schrittnummer anzeigen\"]": {
"abdruck": "117d58f0d4b21973",
"probleme": []
},
"[\"beratung-anbieten\", \"\"]": {
"abdruck": "4b0267b165360857",
"probleme": []
},
"[\"beratung-anbieten\", \"Beratung erreichen\"]": {
"abdruck": "cfdb65d682c3e068",
"probleme": []
},
"[\"beratung-anbieten\", \"Optionsübersicht öffnen\"]": {
"abdruck": "28d5587c78199c54",
"probleme": []
},
"[\"beratung-anbieten\", \"Vertrauen zeigen\"]": {
"abdruck": "8c00dec8d3ebf751",
"probleme": []
},
"[\"layout-zuweisen\", \"\"]": {
"abdruck": "1e0d2ca5296aabea",
"probleme": []
},
"[\"layout-zuweisen\", \"Konfigurator aktivieren\"]": {
"abdruck": "dca9bbf7361f5520",
"probleme": []
},
"[\"layout-zuweisen\", \"Optionsbild am Datensatz pflegen\"]": {
"abdruck": "cc0701aafd02ac3a",
"probleme": []
},
"[\"layout-zuweisen\", \"Optionsübersicht hinterlegen\"]": {
"abdruck": "de6c0e3e80d47496",
"probleme": []
},
"[\"layout-zuweisen\", \"Trust-Logo hinterlegen\"]": {
"abdruck": "ebd1a72ece157e69",
"probleme": []
},
"[\"option-ansehen\", \"\"]": {
"abdruck": "ace46c77101e26b3",
"probleme": []
},
"[\"option-ansehen\", \"Ansicht schließen\"]": {
"abdruck": "6bdaf5e60fb3baa9",
"probleme": []
},
"[\"option-ansehen\", \"Option aus Ansicht wählen\"]": {
"abdruck": "cc10af5dda4a7857",
"probleme": []
},
"[\"option-ansehen\", \"Optionsdetail öffnen\"]": {
"abdruck": "e00d07ce7b75d8fc",
"probleme": []
},
"[\"schritt-zeigen\", \"\"]": {
"abdruck": "f0d53ef421968759",
"probleme": []
},
"[\"schritt-zeigen\", \"Eigenschaft benennen\"]": {
"abdruck": "ec4828e2e9197083",
"probleme": []
},
"[\"schritt-zeigen\", \"Optionen auflisten\"]": {
"abdruck": "d8e6daed0f36098f",
"probleme": []
},
"[\"schritt-zeigen\", \"Schrittstand anzeigen\"]": {
"abdruck": "42a1c7f94c30f72d",
"probleme": []
},
"[\"varianten-waehlen\", \"\"]": {
"abdruck": "0835908894fa892c",
"probleme": []
},
"[\"varianten-waehlen\", \"Auswahl übernehmen\"]": {
"abdruck": "6683956c63370244",
"probleme": []
},
"[\"varianten-waehlen\", \"Konfigurationsdaten bereitstellen\"]": {
"abdruck": "498f3141e86f8db0",
"probleme": []
},
"[\"varianten-waehlen\", \"Konfigurator schließen\"]": {
"abdruck": "866918b327061449",
"probleme": []
},
"[\"varianten-waehlen\", \"Nächsten Schritt anzeigen\"]": {
"abdruck": "8d810a760dfec037",
"probleme": []
},
"[\"varianten-waehlen\", \"Option wählen\"]": {
"abdruck": "ac4eae399ac3a15e",
"probleme": []
},
"[\"varianten-waehlen\", \"Schritt öffnen\"]": {
"abdruck": "40f795456747214e",
"probleme": []
},
"[\"varianten-waehlen\", \"Vorherigen Schritt anzeigen\"]": {
"abdruck": "a63a26a8baea9c07",
"probleme": []
},
"[\"vorschau-zeigen\", \"\"]": {
"abdruck": "52976631fd09993a",
"probleme": []
},
"[\"vorschau-zeigen\", \"Bild wählen\"]": {
"abdruck": "a3e980ef7678a446",
"probleme": []
},
"[\"vorschau-zeigen\", \"Galerie blättern\"]": {
"abdruck": "47ccb441120c1470",
"probleme": []
},
"[\"vorschau-zeigen\", \"Vorschau laden\"]": {
"abdruck": "29e5951f170dba26",
"probleme": []
}
},
"verworfen": {}
}

View File

@@ -0,0 +1,122 @@
[
{
"titel": "„Lightbox“ geändert",
"dateien": {
"lightbox": "<!-- titel -->\nLightbox\n----------------------------------------\n<!-- art -->\nkern\n----------------------------------------\n<!-- ziel -->\nZeigt eine vergrößerte Ansicht der Optionsabbildung mit ihrem Namen\n----------------------------------------\n<!-- regeln -->\n- lightboxSelect bestätigt Auswahl und schließt\n- Escape und x-Icon schließen\n- Tab und Shift-Tab zirkulieren im Fokusring\n- Geschlossen kehrt Fokus zum Auslöser zurück\n- hidden-Attribut entfernt aus dem Barrierefreiheitsbaum\n----------------------------------------\n<!-- funktion -->\nAuswahl bestätigen\n<!-- ziel -->\nWählt eine Option im aktuellen Schritt und berechnet die gültigen Folgeschritte neu\n<!-- regeln -->\n- Bleibt im aktuellen Schritt\n- Invalidiert Folgeschritte mit ungültigen Kombinationen\n- Setzt Fokus auf die gewählte Option\n- Aktualisiert Galerie und Preis\n<!-- testen -->\n- Lightbox mit Option öffnen\n- LMB auf Auswahl bestätigen\n- Option ist markiert\n- Lightbox geschlossen\n----------------------------------------\n<!-- funktion -->\nKonfigurator schließen\n<!-- regeln -->\n- Schließt die Lightbox, ohne den Konfigurator zu schließen\n- x-Icon oder Escape beenden die Lightbox\n- Fokus kehrt zum Auslöser in der Optionsliste zurück\n<!-- testen -->\n- Option mit Bildauswahl öffnet die Lightbox\n- Escape drücken\n- Lightbox geschlossen, Modal offen\n----------------------------------------\n<!-- funktion -->\nLightbox schließen\n<!-- ziel -->\nEntfernt die vergrößerte Produktansicht wieder\n<!-- regeln -->\n- x-Icon oder Escape schließt\n- Zurück zum Dialog gibt Focus\n<!-- testen -->\n- Modal öffnen\n- Option mit Bild wählen\n- Leinwand-Icon klicken\n- Lightbox sichtbar\n- Escape drücken\n- Lightbox geschlossen, Dialog fokussiert\n----------------------------------------\n<!-- funktion -->\nLightbox öffnen\n<!-- ziel -->\nZeigt das Produktfoto einer Variante option in Originalgröße\n<!-- regeln -->\n- lightboxImage zeigt option.media\n- lightboxCaption zeigt option.name\n- lightboxSelect erhält Fokus\n<!-- testen -->\n- Modal öffnen\n- Auf Option mit Bild klicken\n- Info-Button der Option klicken\n- Lightbox zeigt das Foto in Originalgröße\n----------------------------------------\n<!-- funktion -->\nTastatur verwenden\n<!-- ziel -->\nBedient Modal und Lightbox per Tastatur\n<!-- regeln -->\n- Escape schließt Lightbox oder Modal\n- Tab zirkuliert im Fokusring vorwärts\n- Shift-Tab zirkuliert im Fokusring rückwärts\n<!-- testen -->\n- Modal öffnen\n- Lightbox öffnen\n- Escape drücken\n- Lightbox geschlossen\n- Escape drücken\n- Modal geschlossen\n"
}
},
{
"titel": "„Konfigurator“ geändert",
"dateien": {
"konfigurator": "<!-- titel -->\nKonfigurator\n----------------------------------------\n<!-- art -->\nkern\n----------------------------------------\n<!-- ziel -->\nFührt durch Variantenkonfiguration in Einzelschritten mit Live-Vorschau\n----------------------------------------\n<!-- regeln -->\n- Aktiv nur wenn Produkt mto_variant_configurator_active gesetzt hat\n- Optionen-Bilder nur bis zwei Schritte\n- Galerie und Preis aktualisieren live ohne Seitenreload\n- Body erhält CSS-Klasse mto-vc-modal-open solange Modal offen\n- Escape schließt Modal\n- Schließen stellt Fokus auf vorheriges Element\n- Registriert als mto-vc-variants-card in swProductDetail\n- Einstellungen in customFields des Produkts gespeichert\n- Falls inaktiv: Fallback auf Standard-Konfigurator von Shopware\n- Natives Select pro Gruppe im DOM\n- Schritte ohne gültige Optionen werden beim Navigieren übersprungen\n- Ungültige Kombinationen für Folgeschritte werden beim Wählen verworfen\n- Unbesuchte Schritte werden beim Bestätigen mit erster gültiger Option aufgefüllt\n- Deaktivierte Optionen sind nicht auswählbar\n- Gallery-Container erhält CSS-Klasse has-no-gallery wenn keine Bilder vorhanden\n- Schließen bei Lightbox: fokus auf Lightbox-Auswahl-Button\n----------------------------------------\n<!-- funktion -->\nGeführte Konfiguration aktivieren\n<!-- ziel -->\nFührt Kunden schrittweise durch die Variantenauswahl mit Hinweisen und Modal\n<!-- regeln -->\n- Aktiviert bei Produkten mit aktivem Konfigurator-Layout\n- Zeigt jeden Konfigurationsschritt mit Beschriftung\n- Blendentext je Gruppe über Modal aus\n- Inkrementelle Variantenauflösung über resolveUrl\n<!-- testen -->\n- Produktseite mit aktivem Konfigurator öffnen\n- LMB auf ersten Konfigurationsschritt\n- Option im Modal wählen\n- Modal schließt und nächster Schritt markiert\n----------------------------------------\n<!-- funktion -->\nKonfigurationsdaten abrufen\n<!-- ziel -->\nLiefert Variantendaten für eine Live-Aktualisierung im Konfigurator.\n<!-- regeln -->\n- GET /mto-variant-configurator/resolve/{productId}\n- Query-Parameter switched enthält die aktuell gewechselte Gruppe\n- Query-Parameter options enthält ein JSON-Array gewählter Optionen\n- Ungültiges JSON führt zu leerem options-Array\n- VariantNotFoundException hält productId als Fallback\n- Rückgabe enthält variantId, gallery media und Preis\n- Kein HTTP-Cache wegen kundenspezifischer Preise\n<!-- testen -->\n- GET /mto-variant-configurator/resolve/STRG-PRODUKT-ID?options=[{\"optionId\":\"farbe\",\"option\":\"rot\"}]\n- Antwort enthält variantId\n- Antwort enthält gallery media\n- Antwort enthält Preis\n----------------------------------------\n<!-- funktion -->\nKonfigurator öffnen\n<!-- ziel -->\nÖffnet den Konfigurator ab einem gewählten Schritt\n<!-- regeln -->\n- Erster Aufruf zeigt das Modal\n- Weiterer Aufruf wechselt den Schritt\n- Fokus vorheriges Element wird gespeichert\n- Body erhält CSS-Klasse mto-vc-modal-open\n- Escape-Schließen wird registriert\n- Galerie und Preis werden aktualisiert\n- Fokus springt auf den Next-Button\n<!-- testen -->\n- LMB auf Konfigurator-Trigger klicken\n- Modal erscheint im gewählten Schritt\n----------------------------------------\n<!-- funktion -->\nKonfigurator-Karte registrieren\n<!-- ziel -->\nMacht die Einstellungen des Varianten-Konfigurators auf der Produktdetail-Seite verfügbar\n<!-- regeln -->\n- Registriert als mto-vc-variants-card\n- Nutzt Produkt aus swProductDetail\n- Speichert Einstellungen in customFields des Produkts\n<!-- testen -->\n- Produktdetail-Seite eines Produkts öffnen\n- Tab Variants aufrufen\n- Registrierte mto-vc-variants-card wird angezeigt\n----------------------------------------\n<!-- funktion -->\nPlugin registrieren\n<!-- ziel -->\nMacht das Plugin für Elemente mit dem Selektor verfügbar\n<!-- regeln -->\n- Registriert unter VariantConfiguratorPlugin\n- Nutzt window.PluginManager\n- Bindet an [data-mto-variant-configurator]\n<!-- testen -->\n- PluginManager.register aufrufen\n- Element mit data-mto-variant-configurator erstellen\n- Plugin auf Element angewendet\n"
}
},
{
"titel": "„Konfigurator“ geändert",
"dateien": {
"konfigurator": "<!-- titel -->\nKonfigurator\n----------------------------------------\n<!-- art -->\nkern\n----------------------------------------\n<!-- ziel -->\nFührt durch Variantenkonfiguration in Einzelschritten mit Live-Vorschau\n----------------------------------------\n<!-- regeln -->\n- Aktiv nur wenn Produkt mto_variant_configurator_active gesetzt hat\n- Optionen-Bilder nur bis zwei Schritte\n- Galerie und Preis aktualisieren live ohne Seitenreload\n- Body erhält CSS-Klasse mto-vc-modal-open solange Modal offen\n- Escape schließt Modal\n- Schließen stellt Fokus auf vorheriges Element\n- Registriert als mto-vc-variants-card in swProductDetail\n- Einstellungen in customFields des Produkts gespeichert\n- Falls inaktiv: Fallback auf Standard-Konfigurator von Shopware\n- Natives Select pro Gruppe im DOM\n- Schritte ohne gültige Optionen werden beim Navigieren übersprungen\n- Ungültige Kombinationen für Folgeschritte werden beim Wählen verworfen\n- Unbesuchte Schritte werden beim Bestätigen mit erster gültiger Option aufgefüllt\n- Deaktivierte Optionen sind nicht auswählbar\n- Gallery-Container erhält CSS-Klasse has-no-gallery wenn keine Bilder vorhanden\n- Schließen bei Lightbox: fokus auf Lightbox-Auswahl-Button\n----------------------------------------\n<!-- funktion -->\nGeführte Konfiguration aktivieren\n<!-- ziel -->\nFührt Kunden schrittweise durch die Variantenauswahl mit Hinweisen und Modal\n<!-- regeln -->\n- Aktiviert bei Produkten mit aktivem Konfigurator-Layout\n- Zeigt jeden Konfigurationsschritt mit Beschriftung\n- Blendentext je Gruppe über Modal aus\n- Inkrementelle Variantenauflösung über resolveUrl\n<!-- testen -->\n- Produktseite mit aktivem Konfigurator öffnen\n- LMB auf ersten Konfigurationsschritt\n- Option im Modal wählen\n- Modal schließt und nächster Schritt markiert\n----------------------------------------\n<!-- funktion -->\nKonfigurationsdaten abrufen\n<!-- ziel -->\nLiefert Variantendaten für eine Live-Aktualisierung im Konfigurator\n<!-- regeln -->\n- GET /mto-variant-configurator/resolve/{productId}\n- Query-Parameter switched enthält die aktuell gewechselte Gruppe\n- Query-Parameter options enthält ein JSON-Array gewählter Optionen\n- Ungültiges JSON führt zu leerem options-Array\n- VariantNotFoundException hält productId als Fallback\n- Rückgabe enthält variantId, gallery media und Preis\n- Kein HTTP-Cache wegen kundenspezifischer Preise\n<!-- testen -->\n- GET /mto-variant-configurator/resolve/STRG-PRODUKT-ID?options=[{\"optionId\":\"farbe\",\"option\":\"rot\"}]\n- Antwort enthält variantId\n- Antwort enthält gallery media\n- Antwort enthält Preis\n----------------------------------------\n<!-- funktion -->\nKonfigurator öffnen\n<!-- ziel -->\nÖffnet den Konfigurator ab einem gewählten Schritt\n<!-- regeln -->\n- Erster Aufruf zeigt das Modal\n- Weiterer Aufruf wechselt den Schritt\n- Fokus vorheriges Element wird gespeichert\n- Body erhält CSS-Klasse mto-vc-modal-open\n- Escape-Schließen wird registriert\n- Galerie und Preis werden aktualisiert\n- Fokus springt auf den Next-Button\n<!-- testen -->\n- LMB auf Konfigurator-Trigger klicken\n- Modal erscheint im gewählten Schritt\n----------------------------------------\n<!-- funktion -->\nKonfigurator-Karte registrieren\n<!-- ziel -->\nMacht die Einstellungen des Varianten-Konfigurators auf der Produktdetail-Seite verfügbar\n<!-- regeln -->\n- Registriert als mto-vc-variants-card\n- Nutzt Produkt aus swProductDetail\n- Speichert Einstellungen in customFields des Produkts\n<!-- testen -->\n- Produktdetail-Seite eines Produkts öffnen\n- Tab Variants aufrufen\n- Registrierte mto-vc-variants-card wird angezeigt\n----------------------------------------\n<!-- funktion -->\nPlugin registrieren\n<!-- ziel -->\nMacht das Plugin für Elemente mit dem Selektor verfügbar\n<!-- regeln -->\n- Registriert unter VariantConfiguratorPlugin\n- Nutzt window.PluginManager\n- Bindet an [data-mto-variant-configurator]\n<!-- testen -->\n- PluginManager.register aufrufen\n- Element mit data-mto-variant-configurator erstellen\n- Plugin auf Element angewendet\n"
}
},
{
"titel": "„Konfigurator“ geändert",
"dateien": {
"konfigurator": "<!-- titel -->\nKonfigurator\n----------------------------------------\n<!-- art -->\nkern\n----------------------------------------\n<!-- ziel -->\nFührt durch Variantenkonfiguration in Einzelschritten mit Live-Vorschau\n----------------------------------------\n<!-- regeln -->\n- Aktiv nur wenn Produkt mto_variant_configurator_active gesetzt hat\n- Optionen-Bilder nur bis zwei Schritte\n- Galerie und Preis aktualisieren live ohne Seitenreload\n- Body erhält CSS-Klasse mto-vc-modal-open solange Modal offen\n- Escape schließt Modal\n- Schließen stellt Fokus auf vorheriges Element\n- Registriert als mto-vc-variants-card in swProductDetail\n- Einstellungen in customFields des Produkts gespeichert\n- Falls inaktiv: Fallback auf Standard-Konfigurator von Shopware\n- Natives Select pro Gruppe im DOM\n- Schritte ohne gültige Optionen werden beim Navigieren übersprungen\n- Ungültige Kombinationen für Folgeschritte werden beim Wählen verworfen\n- Unbesuchte Schritte werden beim Bestätigen mit erster gültiger Option aufgefüllt\n- Deaktivierte Optionen sind nicht auswählbar\n- Gallery-Container erhält CSS-Klasse has-no-gallery wenn keine Bilder vorhanden\n- Schließen bei Lightbox: fokus auf Lightbox-Auswahl-Button\n----------------------------------------\n<!-- funktion -->\nGeführte Konfiguration aktivieren\n<!-- ziel -->\nFührt Kunden schrittweise durch die Variantenauswahl mit Hinweisen und Modal\n<!-- regeln -->\n- Aktiviert bei Produkten mit aktivem Konfigurator-Layout\n- Zeigt jeden Konfigurationsschritt mit Beschriftung\n- Blendentext je Gruppe über Modal aus\n- Inkrementelle Variantenauflösung über resolveUrl\n<!-- testen -->\n- Produktseite mit aktivem Konfigurator öffnen\n- LMB auf ersten Konfigurationsschritt\n- Option im Modal wählen\n- Modal schließt und nächster Schritt markiert\n----------------------------------------\n<!-- funktion -->\nKonfigurationsdaten abrufen\n<!-- ziel -->\nLiefert Variantendaten für eine Live-Aktualisierung im Konfigurator\n<!-- regeln -->\n- Endpoint mto-variant-configurator resolve\n- Query-Parameter switched enthält die aktuell gewechselte Gruppe\n- Query-Parameter options enthält ein JSON-Array gewählter Optionen\n- Ungültiges JSON führt zu leerem options-Array\n- VariantNotFoundException hält productId als Fallback\n- Rückgabe enthält variantId, gallery media und Preis\n- Kein HTTP-Cache wegen kundenspezifischer Preise\n<!-- testen -->\n- GET /mto-variant-configurator/resolve/STRG-PRODUKT-ID?options=[{\"optionId\":\"farbe\",\"option\":\"rot\"}]\n- Antwort enthält variantId\n- Antwort enthält gallery media\n- Antwort enthält Preis\n----------------------------------------\n<!-- funktion -->\nKonfigurator öffnen\n<!-- ziel -->\nÖffnet den Konfigurator ab einem gewählten Schritt\n<!-- regeln -->\n- Erster Aufruf zeigt das Modal\n- Weiterer Aufruf wechselt den Schritt\n- Fokus vorheriges Element wird gespeichert\n- Body erhält CSS-Klasse mto-vc-modal-open\n- Escape-Schließen wird registriert\n- Galerie und Preis werden aktualisiert\n- Fokus springt auf den Next-Button\n<!-- testen -->\n- LMB auf Konfigurator-Trigger klicken\n- Modal erscheint im gewählten Schritt\n----------------------------------------\n<!-- funktion -->\nKonfigurator-Karte registrieren\n<!-- ziel -->\nMacht die Einstellungen des Varianten-Konfigurators auf der Produktdetail-Seite verfügbar\n<!-- regeln -->\n- Registriert als mto-vc-variants-card\n- Nutzt Produkt aus swProductDetail\n- Speichert Einstellungen in customFields des Produkts\n<!-- testen -->\n- Produktdetail-Seite eines Produkts öffnen\n- Tab Variants aufrufen\n- Registrierte mto-vc-variants-card wird angezeigt\n----------------------------------------\n<!-- funktion -->\nPlugin registrieren\n<!-- ziel -->\nMacht das Plugin für Elemente mit dem Selektor verfügbar\n<!-- regeln -->\n- Registriert unter VariantConfiguratorPlugin\n- Nutzt window.PluginManager\n- Bindet an [data-mto-variant-configurator]\n<!-- testen -->\n- PluginManager.register aufrufen\n- Element mit data-mto-variant-configurator erstellen\n- Plugin auf Element angewendet\n"
}
},
{
"titel": "„Galerie“ geändert",
"dateien": {
"galerie": "<!-- titel -->\nGalerie\n----------------------------------------\n<!-- art -->\nkern\n----------------------------------------\n<!-- ziel -->\nErmöglicht das Anzeigen und Durchblättern von Produktbildern im Konfigurator-Modal\n----------------------------------------\n<!-- regeln -->\n- Einzelne Galerie pro Modal\n- Navigiert zyklisch\n- Ein Bild deaktiviert Navigation\n- Hauptbild über thumbnails aktualisierbar\n----------------------------------------\n<!-- funktion -->\nGalerie nächste anzeigen\n<!-- ziel -->\nZeigt das nächste Bild der Galerie\n<!-- regeln -->\n- Mindestens 2 Bilder vorhanden\n- Zyklisch von letztem zu erstem Bild\n<!-- testen -->\n- Auf next-Button klicken\n- Nächstes Bild wird angezeigt\n----------------------------------------\n<!-- funktion -->\nGalerie rückwärts blättern\n<!-- ziel -->\nVorheriges Bild der Galerie anzeigen\n<!-- regeln -->\n- Nur mit mindestens zwei Bildern\n- Springt von erstem zum letzten Bild\n- Aktualisiert Miniaturansicht\n<!-- testen -->\n- Vollmodus öffnen\n- Klick auf Vorheriges-Bild-Icon\n- Erstes Bild zeigt\n----------------------------------------\n<!-- funktion -->\nGalerie vorherige anzeigen\n<!-- ziel -->\nBlendet das vorherige Bild der Galerie ein\n<!-- regeln -->\n- mto-vc-gallery-prev-Icon\n- Bei nur einem Bild keine Wirkung\n<!-- testen -->\n- Galerie mit mindestens zwei Bildern öffnen\n- LMB auf mto-vc-gallery-prev-Icon\n- Vorheriges Bild im Stage-Image angezeigt\n----------------------------------------\n<!-- funktion -->\nGalerie vorwärts blättern\n<!-- ziel -->\nZeigt das nächste Bild der Galerie\n<!-- regeln -->\n- Geht bei weniger als zwei Bildern nicht\n- Springt vom letzten zum ersten Bild\n<!-- testen -->\n- Modal öffnen\n- Klick auf gallery-next\n- Nächstes Bild wird angezeigt\n- Klick erneut bei letztem Bild\n- Erstes Bild wird angezeigt\n----------------------------------------\n<!-- funktion -->\nThumbnail auswählen\n<!-- ziel -->\nGreift direkt auf ein Produktbild zu, ohne durch die Galerie zu blättern\n<!-- regeln -->\n- Aktives Thumbnail zeigt x-Icon\n- Klick aktualisiert Hauptbild und Aktivzustand\n<!-- testen -->\n- Galerie mit mehreren Thumbnails öffnen\n- Klick auf Thumbnail\n- Hauptbild entspricht geklicktem Thumbnail\n"
}
},
{
"titel": "„Lightbox“ geändert",
"dateien": {
"lightbox": "<!-- titel -->\nLightbox\n----------------------------------------\n<!-- art -->\nkern\n----------------------------------------\n<!-- ziel -->\nZeigt eine vergrößerte Ansicht der Optionsabbildung mit ihrem Namen\n----------------------------------------\n<!-- regeln -->\n- lightboxSelect bestätigt Auswahl und schließt\n- Escape und x-Icon schließen\n- Tab und Shift-Tab zirkulieren im Fokusring\n- Geschlossen kehrt Fokus zum Auslöser zurück\n- hidden-Attribut entfernt aus dem Barrierefreiheitsbaum\n----------------------------------------\n<!-- funktion -->\nAuswahl bestätigen\n<!-- ziel -->\nWählt eine Option im aktuellen Schritt und berechnet die gültigen Folgeschritte neu\n<!-- regeln -->\n- Bleibt im aktuellen Schritt\n- Invalidiert Folgeschritte mit ungültigen Kombinationen\n- Setzt Fokus auf die gewählte Option\n- Aktualisiert Galerie und Preis\n<!-- testen -->\n- Lightbox mit Option öffnen\n- LMB auf Auswahl bestätigen\n- Option markiert\n----------------------------------------\n<!-- funktion -->\nKonfigurator schließen\n<!-- regeln -->\n- Schließt die Lightbox, ohne den Konfigurator zu schließen\n- x-Icon oder Escape beenden die Lightbox\n- Fokus kehrt zum Auslöser in der Optionsliste zurück\n<!-- testen -->\n- Option mit Bildauswahl öffnet die Lightbox\n- Escape drücken\n- Lightbox geschlossen, Modal offen\n----------------------------------------\n<!-- funktion -->\nLightbox schließen\n<!-- ziel -->\nEntfernt die vergrößerte Produktansicht wieder\n<!-- regeln -->\n- x-Icon oder Escape schließt\n- Zurück zum Dialog gibt Focus\n<!-- testen -->\n- Modal öffnen\n- Option mit Bild wählen\n- Leinwand-Icon klicken\n- Lightbox sichtbar\n- Escape drücken\n- Lightbox geschlossen, Dialog fokussiert\n----------------------------------------\n<!-- funktion -->\nLightbox öffnen\n<!-- ziel -->\nZeigt das Produktfoto einer Variante option in Originalgröße\n<!-- regeln -->\n- lightboxImage zeigt option.media\n- lightboxCaption zeigt option.name\n- lightboxSelect erhält Fokus\n<!-- testen -->\n- Modal öffnen\n- Auf Option mit Bild klicken\n- Info-Button der Option klicken\n- Lightbox zeigt das Foto in Originalgröße\n----------------------------------------\n<!-- funktion -->\nTastatur verwenden\n<!-- ziel -->\nBedient Modal und Lightbox per Tastatur\n<!-- regeln -->\n- Escape schließt Lightbox oder Modal\n- Tab zirkuliert im Fokusring vorwärts\n- Shift-Tab zirkuliert im Fokusring rückwärts\n<!-- testen -->\n- Modal öffnen\n- Lightbox öffnen\n- Escape drücken\n- Lightbox geschlossen\n- Escape drücken\n- Modal geschlossen\n"
}
},
{
"titel": "„Produkt“ geändert",
"dateien": {
"produkt": "<!-- titel -->\nProdukt\n----------------------------------------\n<!-- art -->\nkern\n----------------------------------------\n<!-- ziel -->\nStellt dem Produkttemplate Daten des Variantenkonfigurators bereit\n----------------------------------------\n<!-- regeln -->\n- Inaktiver Konfigurator setzt mtoVariantConfiguratorActive auf false\n- Aktiver Konfigurator weist alle Konfigurator-Variablen zu\n- sw-product-detail-variants wird mit eigener Template-Datei überschrieben\n- mtoVariantConfiguratorTrustLogo fehlt in Regeln und Funktion\n- Produktseite laden: Testschritte beschreiben Ergebnis statt Handlung\n- Variantenansicht erweitern: Funktion hat keine Testschritte, nur Regeln\n----------------------------------------\n<!-- funktion -->\nProduktseite laden\n<!-- ziel -->\nBereitet Variant-Konfigurationsdaten für die Produktseite vor\n<!-- regeln -->\n- mtoVariantConfiguratorActive false bei inaktivem Konfigurator\n- Alle Variablen bei aktivem Konfigurator zugewiesen\n- Initial-Variante für vorausgefülltes Modal geladen\n<!-- testen -->\n- Produkt mit aktivem Konfigurator öffnen\n- Seite vollständig laden\n- Template-Variablen prüfen\n- Seite ohne Konfigurator öffnen\n- mtoVariantConfiguratorActive ist false\n----------------------------------------\n<!-- funktion -->\nVariantenansicht erweitern\n<!-- ziel -->\nFügt der Produktdetail-Variantenansicht zusätzliche Felder oder Funktionen hinzu\n<!-- regeln -->\n- Überschreibt sw-product-detail-variants\n- Eigene Template-Datei wird geladen\n<!-- testen -->\n- Produktdetailseite öffnen\n- Varianten-Bereich aufrufen\n- Erweiterte Felder oder Funktionen sichtbar\n"
}
},
{
"titel": "„Lightbox“ geändert",
"dateien": {
"lightbox": "<!-- titel -->\nLightbox\n----------------------------------------\n<!-- art -->\nkern\n----------------------------------------\n<!-- ziel -->\nZeigt eine vergrößerte Ansicht der Optionsabbildung mit ihrem Namen\n----------------------------------------\n<!-- regeln -->\n- lightboxSelect bestätigt Auswahl und schließt\n- Escape und x-Icon schließen\n- Tab und Shift-Tab zirkulieren im Fokusring\n- Geschlossen kehrt Fokus zum Auslöser zurück\n- hidden-Attribut entfernt aus dem Barrierefreiheitsbaum\n----------------------------------------\n<!-- funktion -->\nAuswahl bestätigen\n<!-- ziel -->\nWählt eine Option im aktuellen Schritt und berechnet die gültigen Folgeschritte neu\n<!-- regeln -->\n- Bleibt im aktuellen Schritt\n- Invalidiert Folgeschritte mit ungültigen Kombinationen\n- Setzt Fokus auf die gewählte Option\n- Aktualisiert Galerie und Preis\n<!-- testen -->\n- Lightbox mit Option öffnen\n- LMB auf Auswahl bestätigen\n- Option markiert\n----------------------------------------\n<!-- funktion -->\nKonfigurator schließen\n<!-- regeln -->\n- Schließt die Lightbox, ohne den Konfigurator zu schließen\n- x-Icon oder Escape beenden die Lightbox\n- Fokus kehrt zum Auslöser in der Optionsliste zurück\n<!-- testen -->\n- Option mit Bildauswahl öffnet die Lightbox\n- Escape drücken\n- Lightbox geschlossen, Modal offen\n----------------------------------------\n<!-- funktion -->\nLightbox schließen\n<!-- ziel -->\nEntfernt die vergrößerte Produktansicht wieder\n<!-- regeln -->\n- x-Icon oder Escape schließt\n- Zurück zum Dialog gibt Focus\n<!-- testen -->\n- Lightbox öffnen\n- Escape drücken\n- Lightbox geschlossen, Dialog fokussiert\n----------------------------------------\n<!-- funktion -->\nLightbox öffnen\n<!-- ziel -->\nZeigt das Produktfoto einer Variante option in Originalgröße\n<!-- regeln -->\n- lightboxImage zeigt option.media\n- lightboxCaption zeigt option.name\n- lightboxSelect erhält Fokus\n<!-- testen -->\n- Modal öffnen\n- Auf Option mit Bild klicken\n- Info-Button der Option klicken\n- Lightbox zeigt das Foto in Originalgröße\n----------------------------------------\n<!-- funktion -->\nTastatur verwenden\n<!-- ziel -->\nBedient Modal und Lightbox per Tastatur\n<!-- regeln -->\n- Escape schließt Lightbox oder Modal\n- Tab zirkuliert im Fokusring vorwärts\n- Shift-Tab zirkuliert im Fokusring rückwärts\n<!-- testen -->\n- Modal öffnen\n- Lightbox öffnen\n- Escape drücken\n- Lightbox geschlossen\n- Escape drücken\n- Modal geschlossen\n"
}
},
{
"titel": "„Galerie“ geändert",
"dateien": {
"galerie": "<!-- titel -->\nGalerie\n----------------------------------------\n<!-- art -->\nkern\n----------------------------------------\n<!-- ziel -->\nErmöglicht das Anzeigen und Durchblättern von Produktbildern im Konfigurator-Modal\n----------------------------------------\n<!-- regeln -->\n- Einzelne Galerie pro Modal\n- Navigiert zyklisch\n- Ein Bild deaktiviert Navigation\n- Hauptbild über thumbnails aktualisierbar\n----------------------------------------\n<!-- funktion -->\nGalerie nächste anzeigen\n<!-- ziel -->\nZeigt das nächste Bild der Galerie\n<!-- regeln -->\n- Mindestens 2 Bilder vorhanden\n- Zyklisch von letztem zu erstem Bild\n<!-- testen -->\n- Auf next-Button klicken\n- Nächstes Bild wird angezeigt\n----------------------------------------\n<!-- funktion -->\nGalerie rückwärts blättern\n<!-- ziel -->\nVorheriges Bild der Galerie anzeigen\n<!-- regeln -->\n- Nur mit mindestens zwei Bildern\n- Springt von erstem zum letzten Bild\n- Aktualisiert Miniaturansicht\n<!-- testen -->\n- Vollmodus öffnen\n- Klick auf Vorheriges-Bild-Icon\n- Erstes Bild zeigt\n----------------------------------------\n<!-- funktion -->\nGalerie vorherige anzeigen\n<!-- ziel -->\nBlendet das vorherige Bild der Galerie ein\n<!-- regeln -->\n- mto-vc-gallery-prev-Icon\n- Bei nur einem Bild keine Wirkung\n<!-- testen -->\n- Galerie mit mindestens zwei Bildern öffnen\n- LMB auf mto-vc-gallery-prev-Icon\n- Vorheriges Bild im Stage-Image angezeigt\n----------------------------------------\n<!-- funktion -->\nGalerie vorwärts blättern\n<!-- ziel -->\nZeigt das nächste Bild der Galerie\n<!-- regeln -->\n- Geht bei weniger als zwei Bildern nicht\n- Springt vom letzten zum ersten Bild\n<!-- testen -->\n- Modal öffnen\n- Klick auf gallery-next\n- Nächstes Bild wird angezeigt\n----------------------------------------\n<!-- funktion -->\nThumbnail auswählen\n<!-- ziel -->\nGreift direkt auf ein Produktbild zu, ohne durch die Galerie zu blättern\n<!-- regeln -->\n- Aktives Thumbnail zeigt x-Icon\n- Klick aktualisiert Hauptbild und Aktivzustand\n<!-- testen -->\n- Galerie mit mehreren Thumbnails öffnen\n- Klick auf Thumbnail\n- Hauptbild entspricht geklicktem Thumbnail\n"
}
},
{
"titel": "„Lightbox“ geändert",
"dateien": {
"lightbox": "<!-- titel -->\nLightbox\n----------------------------------------\n<!-- art -->\nkern\n----------------------------------------\n<!-- ziel -->\nZeigt eine vergrößerte Ansicht der Optionsabbildung mit ihrem Namen\n----------------------------------------\n<!-- regeln -->\n- lightboxSelect bestätigt Auswahl und schließt\n- Escape und x-Icon schließen\n- Tab und Shift-Tab zirkulieren im Fokusring\n- Geschlossen kehrt Fokus zum Auslöser zurück\n- hidden-Attribut entfernt aus dem Barrierefreiheitsbaum\n----------------------------------------\n<!-- funktion -->\nAuswahl bestätigen\n<!-- ziel -->\nWählt eine Option im aktuellen Schritt und berechnet die gültigen Folgeschritte neu\n<!-- regeln -->\n- Bleibt im aktuellen Schritt\n- Invalidiert Folgeschritte mit ungültigen Kombinationen\n- Setzt Fokus auf die gewählte Option\n- Aktualisiert Galerie und Preis\n<!-- testen -->\n- Lightbox mit Option öffnen\n- LMB auf Auswahl bestätigen\n- Option markiert\n----------------------------------------\n<!-- funktion -->\nKonfigurator schließen\n<!-- regeln -->\n- Schließt die Lightbox, ohne den Konfigurator zu schließen\n- x-Icon oder Escape beenden die Lightbox\n- Fokus kehrt zum Auslöser in der Optionsliste zurück\n<!-- testen -->\n- LMB auf Option mit Bildauswahl\n- Escape drücken\n- Lightbox geschlossen, Modal offen\n----------------------------------------\n<!-- funktion -->\nLightbox schließen\n<!-- ziel -->\nEntfernt die vergrößerte Produktansicht wieder\n<!-- regeln -->\n- x-Icon oder Escape schließt\n- Zurück zum Dialog gibt Focus\n<!-- testen -->\n- Lightbox öffnen\n- Escape drücken\n- Lightbox geschlossen, Dialog fokussiert\n----------------------------------------\n<!-- funktion -->\nLightbox öffnen\n<!-- ziel -->\nZeigt das Produktfoto einer Variante option in Originalgröße\n<!-- regeln -->\n- lightboxImage zeigt option.media\n- lightboxCaption zeigt option.name\n- lightboxSelect erhält Fokus\n<!-- testen -->\n- Modal öffnen\n- Auf Option mit Bild klicken\n- Info-Button der Option klicken\n- Lightbox zeigt das Foto in Originalgröße\n----------------------------------------\n<!-- funktion -->\nTastatur verwenden\n<!-- ziel -->\nBedient Modal und Lightbox per Tastatur\n<!-- regeln -->\n- Escape schließt Lightbox oder Modal\n- Tab zirkuliert im Fokusring vorwärts\n- Shift-Tab zirkuliert im Fokusring rückwärts\n<!-- testen -->\n- Modal öffnen\n- Lightbox öffnen\n- Escape drücken\n- Lightbox geschlossen\n- Escape drücken\n- Modal geschlossen\n"
}
},
{
"titel": "„Lightbox“ geändert",
"dateien": {
"lightbox": "<!-- titel -->\nLightbox\n----------------------------------------\n<!-- art -->\nkern\n----------------------------------------\n<!-- ziel -->\nZeigt eine vergrößerte Ansicht der Optionsabbildung mit ihrem Namen\n----------------------------------------\n<!-- regeln -->\n- lightboxSelect bestätigt Auswahl\n- lightboxSelect schließt\n- Escape schließt\n- x-Icon schließt\n- Geschlossen kehrt Fokus zum Auslöser zurück\n- hidden-Attribut entfernt aus dem Barrierefreiheitsbaum\n----------------------------------------\n<!-- funktion -->\nAuswahl bestätigen\n<!-- ziel -->\nWählt eine Option im aktuellen Schritt und berechnet die gültigen Folgeschritte neu\n<!-- regeln -->\n- Bleibt im aktuellen Schritt\n- Invalidiert Folgeschritte mit ungültigen Kombinationen\n- Setzt Fokus auf die gewählte Option\n- Aktualisiert Galerie und Preis\n<!-- testen -->\n- Lightbox mit Option öffnen\n- LMB auf Auswahl bestätigen\n- Option markiert\n----------------------------------------\n<!-- funktion -->\nKonfigurator schließen\n<!-- regeln -->\n- Schließt die Lightbox, ohne den Konfigurator zu schließen\n- x-Icon oder Escape beenden die Lightbox\n- Fokus kehrt zum Auslöser in der Optionsliste zurück\n<!-- testen -->\n- LMB auf Option mit Bildauswahl\n- Escape drücken\n- Lightbox geschlossen, Modal offen\n----------------------------------------\n<!-- funktion -->\nLightbox schließen\n<!-- ziel -->\nEntfernt die vergrößerte Produktansicht wieder\n<!-- regeln -->\n- x-Icon oder Escape schließt\n- Zurück zum Dialog gibt Focus\n<!-- testen -->\n- Lightbox öffnen\n- Escape drücken\n- Lightbox geschlossen, Dialog fokussiert\n----------------------------------------\n<!-- funktion -->\nLightbox öffnen\n<!-- ziel -->\nZeigt das Produktfoto einer Variante option in Originalgröße\n<!-- regeln -->\n- lightboxImage zeigt option.media\n- lightboxCaption zeigt option.name\n- lightboxSelect erhält Fokus\n<!-- testen -->\n- Modal öffnen\n- Auf Option mit Bild klicken\n- Info-Button der Option klicken\n- Lightbox zeigt das Foto in Originalgröße\n----------------------------------------\n<!-- funktion -->\nTastatur verwenden\n<!-- ziel -->\nBedient Modal und Lightbox per Tastatur\n<!-- regeln -->\n- Escape schließt Lightbox oder Modal\n- Tab zirkuliert im Fokusring vorwärts\n- Shift-Tab zirkuliert im Fokusring rückwärts\n<!-- testen -->\n- Modal öffnen\n- Lightbox öffnen\n- Escape drücken\n- Lightbox geschlossen\n- Escape drücken\n- Modal geschlossen\n"
}
},
{
"titel": "„Konfigurator“ geändert",
"dateien": {
"konfigurator": "<!-- titel -->\nKonfigurator\n----------------------------------------\n<!-- art -->\nkern\n----------------------------------------\n<!-- ziel -->\nFührt durch Variantenkonfiguration in Einzelschritten mit Live-Vorschau\n----------------------------------------\n<!-- regeln -->\n- Aktiv nur wenn Produkt mto_variant_configurator_active gesetzt hat\n- Optionen-Bilder nur bis zwei Schritte\n- Galerie und Preis aktualisieren live ohne Seitenreload\n- Body erhält CSS-Klasse mto-vc-modal-open solange Modal offen\n- Escape schließt Modal\n- Schließen stellt Fokus auf vorheriges Element\n- Registriert als mto-vc-variants-card in swProductDetail\n- Einstellungen in customFields des Produkts gespeichert\n- Falls inaktiv: Fallback auf Standard-Konfigurator von Shopware\n- Natives Select pro Gruppe im DOM\n- Schritte ohne gültige Optionen werden beim Navigieren übersprungen\n- Ungültige Kombinationen für Folgeschritte werden beim Wählen verworfen\n- Unbesuchte Schritte werden beim Bestätigen mit erster gültiger Option aufgefüllt\n- Deaktivierte Optionen sind nicht auswählbar\n- Gallery-Container erhält CSS-Klasse has-no-gallery wenn keine Bilder vorhanden\n- Schließen bei Lightbox: fokus auf Lightbox-Auswahl-Button\n----------------------------------------\n<!-- funktion -->\nGeführte Konfiguration aktivieren\n<!-- ziel -->\nFührt Kunden schrittweise durch die Variantenauswahl mit Hinweisen und Modal\n<!-- regeln -->\n- Aktiviert bei Produkten mit aktivem Konfigurator-Layout\n- Zeigt jeden Konfigurationsschritt mit Beschriftung\n- Blendentext je Gruppe über Modal aus\n- Inkrementelle Variantenauflösung über resolveUrl\n<!-- testen -->\n- Produktseite mit aktivem Konfigurator öffnen\n- LMB auf ersten Konfigurationsschritt\n- Option im Modal wählen\n- Modal schließt und nächster Schritt markiert\n----------------------------------------\n<!-- funktion -->\nKonfigurationsdaten abrufen\n<!-- ziel -->\nLiefert Variantendaten für eine Live-Aktualisierung im Konfigurator\n<!-- regeln -->\n- Endpoint mto-variant-configurator resolve\n- Query-Parameter switched enthält die aktuell gewechselte Gruppe\n- Query-Parameter options enthält ein JSON-Array gewählter Optionen\n- Ungültiges JSON führt zu leerem options-Array\n- VariantNotFoundException hält productId als Fallback\n- Rückgabe enthält variantId, gallery media und Preis\n- Kein HTTP-Cache wegen kundenspezifischer Preise\n<!-- testen -->\n- Resolve-Endpunkt mit Produkt-ID und Option aufrufen\n- Antwort enthält variantId\n- Antwort enthält gallery media\n- Antwort enthält Preis\n----------------------------------------\n<!-- funktion -->\nKonfigurator öffnen\n<!-- ziel -->\nÖffnet den Konfigurator ab einem gewählten Schritt\n<!-- regeln -->\n- Erster Aufruf zeigt das Modal\n- Weiterer Aufruf wechselt den Schritt\n- Fokus vorheriges Element wird gespeichert\n- Body erhält CSS-Klasse mto-vc-modal-open\n- Escape-Schließen wird registriert\n- Galerie und Preis werden aktualisiert\n- Fokus springt auf den Next-Button\n<!-- testen -->\n- LMB auf Konfigurator-Trigger klicken\n- Modal erscheint im gewählten Schritt\n----------------------------------------\n<!-- funktion -->\nKonfigurator-Karte registrieren\n<!-- ziel -->\nMacht die Einstellungen des Varianten-Konfigurators auf der Produktdetail-Seite verfügbar\n<!-- regeln -->\n- Registriert als mto-vc-variants-card\n- Nutzt Produkt aus swProductDetail\n- Speichert Einstellungen in customFields des Produkts\n<!-- testen -->\n- Produktdetail-Seite eines Produkts öffnen\n- Tab Variants aufrufen\n- Registrierte mto-vc-variants-card wird angezeigt\n----------------------------------------\n<!-- funktion -->\nPlugin registrieren\n<!-- ziel -->\nMacht das Plugin für Elemente mit dem Selektor verfügbar\n<!-- regeln -->\n- Registriert unter VariantConfiguratorPlugin\n- Nutzt window.PluginManager\n- Bindet an [data-mto-variant-configurator]\n<!-- testen -->\n- PluginManager.register aufrufen\n- Element mit data-mto-variant-configurator erstellen\n- Plugin auf Element angewendet\n"
}
},
{
"titel": "„Lightbox“ geändert",
"dateien": {
"lightbox": "<!-- titel -->\nLightbox\n----------------------------------------\n<!-- art -->\nkern\n----------------------------------------\n<!-- ziel -->\nZeigt eine vergrößerte Ansicht der Optionsabbildung mit ihrem Namen\n----------------------------------------\n<!-- regeln -->\n- lightboxSelect bestätigt Auswahl\n- lightboxSelect schließt\n- Escape schließt\n- x-Icon schließt\n- Geschlossen kehrt Fokus zum Auslöser zurück\n- hidden-Attribut entfernt aus dem Barrierefreiheitsbaum\n----------------------------------------\n<!-- funktion -->\nAuswahl bestätigen\n<!-- ziel -->\nWählt eine Option im aktuellen Schritt und berechnet die gültigen Folgeschritte neu\n<!-- regeln -->\n- Bleibt im aktuellen Schritt\n- Invalidiert Folgeschritte mit ungültigen Kombinationen\n- Setzt Fokus auf die gewählte Option\n- Aktualisiert Galerie und Preis\n<!-- testen -->\n- Lightbox mit Option öffnen\n- LMB auf Auswahl bestätigen\n- Option markiert\n----------------------------------------\n<!-- funktion -->\nKonfigurator schließen\n<!-- regeln -->\n- Schließt die Lightbox, ohne den Konfigurator zu schließen\n- x-Icon oder Escape beenden die Lightbox\n- Fokus kehrt zum Auslöser in der Optionsliste zurück\n<!-- testen -->\n- LMB auf Option mit Bildauswahl\n- Escape drücken\n- Lightbox geschlossen, Modal offen\n----------------------------------------\n<!-- funktion -->\nLightbox schließen\n<!-- ziel -->\nEntfernt die vergrößerte Produktansicht wieder\n<!-- regeln -->\n- x-Icon schließt\n- Escape schließt\n- Zurück zum Dialog gibt Focus\n<!-- testen -->\n- Lightbox öffnen\n- Escape drücken\n- Lightbox geschlossen, Dialog fokussiert\n----------------------------------------\n<!-- funktion -->\nLightbox öffnen\n<!-- ziel -->\nZeigt das Produktfoto einer Variante option in Originalgröße\n<!-- regeln -->\n- lightboxImage zeigt option.media\n- lightboxCaption zeigt option.name\n- lightboxSelect erhält Fokus\n<!-- testen -->\n- Modal öffnen\n- Auf Option mit Bild klicken\n- Info-Button der Option klicken\n- Lightbox zeigt das Foto in Originalgröße\n----------------------------------------\n<!-- funktion -->\nTastatur verwenden\n<!-- ziel -->\nBedient Modal und Lightbox per Tastatur\n<!-- regeln -->\n- Escape schließt Lightbox oder Modal\n- Tab zirkuliert im Fokusring vorwärts\n- Shift-Tab zirkuliert im Fokusring rückwärts\n<!-- testen -->\n- Modal öffnen\n- Lightbox öffnen\n- Escape drücken\n- Lightbox geschlossen\n- Escape drücken\n- Modal geschlossen\n"
}
},
{
"titel": "„Option ansehen“ geändert",
"dateien": {
"option-ansehen": "<!-- titel -->\nOption ansehen\n----------------------------------------\n<!-- art -->\nkern\n----------------------------------------\n<!-- ziel -->\nDer Kunde prüft eine Option groß, bevor er sie nimmt\n----------------------------------------\n<!-- regeln -->\n- Nur Optionen mit Bild tragen den Hinweis\n- Optionsbilder erscheinen bis zwei Schritte\n- Ansicht liegt über dem Konfigurator\n----------------------------------------\n<!-- funktion -->\nOptionsdetail öffnen\n<!-- ziel -->\nZeigt Bild und Namen einer Option in voller Größe\n<!-- regeln -->\n- i-Icon öffnet die Ansicht\n- Enter öffnet die Ansicht\n- Leertaste öffnet die Ansicht\n<!-- testen -->\n- Konfigurator öffnen\n- LMB auf i-Icon einer Option\n- Große Ansicht erscheint\n----------------------------------------\n<!-- funktion -->\nOption aus Ansicht wählen\n<!-- ziel -->\nÜbernimmt die betrachtete Option in die Auswahl\n<!-- regeln -->\n- Knopf übernimmt und schließt die Ansicht\n- Schritt bleibt derselbe\n<!-- testen -->\n- Optionsdetail öffnen\n- LMB auf Wählen-Knopf\n- Option ist gewählt\n----------------------------------------\n<!-- funktion -->\nAnsicht schließen\n<!-- ziel -->\nKehrt zur Optionsliste zurück ohne Auswahl\n<!-- regeln -->\n- x-Icon schließt die Ansicht\n- Escape schließt die Ansicht\n- Konfigurator bleibt offen\n<!-- testen -->\n- Optionsdetail öffnen\n- Escape drücken\n- Optionsliste erscheint wieder\n"
}
},
{
"titel": "„Varianten wählen“ geändert",
"dateien": {
"varianten-waehlen": "<!-- titel -->\nVarianten wählen\n----------------------------------------\n<!-- art -->\nkern\n----------------------------------------\n<!-- ziel -->\nDer Kunde findet Schritt für Schritt eine bestellbare Kombination\n----------------------------------------\n<!-- regeln -->\n- Erster Schritt zeigt alle Optionen seiner Eigenschaft\n- Folgeschritte zeigen nur kombinierbare Optionen\n- Reihenfolge der Schritte kommt aus dem Backend\n- Ohne Auswahl greifen die Standardvarianten des Elternartikels\n- Schritte ohne gültige Option werden übersprungen\n- Eine Auswahl wechselt den Schritt nicht\n- Ungültige Folgeauswahl fällt beim Wechsel weg\n- Escape schließt den Konfigurator\n- Tabulator bleibt im Dialog\n----------------------------------------\n<!-- funktion -->\nSchritt öffnen\n<!-- ziel -->\nÖffnet den Konfigurator beim angeklickten Schritt\n<!-- regeln -->\n- LMB auf einen Schritt öffnet das Modal\n- Offener Konfigurator wechselt nur den Schritt\n- Modal hängt am Seitenkörper\n<!-- testen -->\n- Produktseite öffnen\n- LMB auf zweiten Schritt\n- Modal öffnet bei Schritt 2\n----------------------------------------\n<!-- funktion -->\nOption wählen\n<!-- ziel -->\nÜbernimmt eine Option in die laufende Auswahl\n<!-- regeln -->\n- Gewählte Option ist hervorgehoben\n- Nicht kombinierbare Optionen sind gesperrt\n- Auswahl verwirft ungültige Folgeschritte\n<!-- testen -->\n- Konfigurator öffnen\n- LMB auf eine Option\n- Option ist hervorgehoben\n----------------------------------------\n<!-- funktion -->\nNächsten Schritt anzeigen\n<!-- ziel -->\nFührt zum nächsten Schritt mit gültigen Optionen\n<!-- regeln -->\n- Weiter-Knopf heißt am letzten Schritt Fertig\n- Leere Schritte werden übersprungen\n<!-- testen -->\n- Konfigurator öffnen\n- LMB auf Weiter-Knopf\n- Nächster Schritt erscheint\n----------------------------------------\n<!-- funktion -->\nVorherigen Schritt anzeigen\n<!-- ziel -->\nFührt zurück zum letzten Schritt mit Optionen\n<!-- regeln -->\n- Zurück-Knopf ist am ersten Schritt gesperrt\n- Leere Schritte werden übersprungen\n<!-- testen -->\n- Zum zweiten Schritt gehen\n- LMB auf Zurück-Knopf\n- Erster Schritt erscheint\n----------------------------------------\n<!-- funktion -->\nAuswahl übernehmen\n<!-- ziel -->\nWechselt das Produkt auf die gewählte Variante\n<!-- regeln -->\n- Fertig schreibt die Auswahl in die Auswahlfelder\n- Unbesuchte Schritte bekommen die erste gültige Option\n- Der Variantenwechsel von Shopware führt den Wechsel aus\n<!-- testen -->\n- Alle Schritte durchgehen\n- LMB auf Fertig-Knopf\n- Seite zeigt die gewählte Variante\n----------------------------------------\n<!-- funktion -->\nKonfigurator schließen\n<!-- ziel -->\nBeendet die Auswahl ohne Wechsel der Variante\n<!-- regeln -->\n- x-Icon schließt das Modal\n- Escape schließt das Modal\n- Fokus kehrt zum Auslöser zurück\n<!-- testen -->\n- Konfigurator öffnen\n- Escape drücken\n- Modal ist geschlossen\n"
}
},
{
"titel": "„Vorschau zeigen“ geändert",
"dateien": {
"vorschau-zeigen": "<!-- titel -->\nVorschau zeigen\n----------------------------------------\n<!-- art -->\nkern\n----------------------------------------\n<!-- ziel -->\nDer Kunde sieht Bild und Preis der gewählten Kombination\n----------------------------------------\n<!-- regeln -->\n- Preis stimmt mit dem Preis der Produktseite überein\n- Antwort wird nicht zwischengespeichert\n- Späte Antworten überholter Anfragen werden verworfen\n- Ohne eigene Bilder greifen die Bilder des Elternartikels\n----------------------------------------\n<!-- funktion -->\nVorschau laden\n<!-- ziel -->\nHolt Bilder und Preis zur laufenden Auswahl\n<!-- regeln -->\n- Jede Auswahl löst eine neue Anfrage aus\n- Ohne Treffer bleibt die Produkt-ID als Rückfall\n- Erstes Bild ist das Titelbild der Variante\n<!-- testen -->\n- Konfigurator öffnen\n- Andere Option wählen\n- Bild und Preis ändern sich\n----------------------------------------\n<!-- funktion -->\nGalerie blättern\n<!-- ziel -->\nZeigt weitere Bilder der gewählten Variante\n<!-- regeln -->\n- Pfeil-Icons blättern vor und zurück\n- Nach dem letzten Bild folgt das erste\n- Bei einem Bild bleibt die Navigation aus\n<!-- testen -->\n- Konfigurator öffnen\n- LMB auf Vor-Icon\n- Nächstes Bild erscheint\n----------------------------------------\n<!-- funktion -->\nBild wählen\n<!-- ziel -->\nWechselt das große Bild über die Vorschaubilder\n<!-- regeln -->\n- Aktives Vorschaubild ist hervorgehoben\n- Großes Bild folgt der Wahl\n<!-- testen -->\n- Konfigurator öffnen\n- LMB auf drittes Vorschaubild\n- Großes Bild zeigt dieses Motiv\n"
}
},
{
"titel": "„Varianten wählen“ geändert",
"dateien": {
"varianten-waehlen": "<!-- titel -->\nVarianten wählen\n----------------------------------------\n<!-- art -->\nkern\n----------------------------------------\n<!-- ziel -->\nDer Kunde findet Schritt für Schritt eine bestellbare Kombination\n----------------------------------------\n<!-- regeln -->\n- Erster Schritt zeigt alle Optionen seiner Eigenschaft\n- Folgeschritte zeigen nur kombinierbare Optionen\n- Der Ablauf trägt beliebig viele Eigenschaften\n- Reihenfolge der Schritte kommt aus dem Backend\n- Ohne Auswahl greifen die Standardvarianten des Elternartikels\n- Schritte ohne gültige Option werden übersprungen\n- Eine Auswahl wechselt den Schritt nicht\n- Ungültige Folgeauswahl fällt beim Wechsel weg\n- Auf kleinen Bildschirmen bleibt der Ablauf derselbe\n- Escape schließt den Konfigurator\n- Tabulator bleibt im Dialog\n----------------------------------------\n<!-- funktion -->\nSchritt öffnen\n<!-- ziel -->\nÖffnet den Konfigurator beim angeklickten Schritt\n<!-- regeln -->\n- LMB auf einen Schritt öffnet das Modal\n- Offener Konfigurator wechselt nur den Schritt\n- Modal hängt am Seitenkörper\n<!-- testen -->\n- Produktseite öffnen\n- LMB auf zweiten Schritt\n- Modal öffnet bei Schritt 2\n----------------------------------------\n<!-- funktion -->\nOption wählen\n<!-- ziel -->\nÜbernimmt eine Option in die laufende Auswahl\n<!-- regeln -->\n- Gewählte Option ist hervorgehoben\n- Nicht kombinierbare Optionen sind gesperrt\n- Auswahl verwirft ungültige Folgeschritte\n- Schritt bleibt nach der Wahl derselbe\n<!-- testen -->\n- Konfigurator öffnen\n- LMB auf eine Option\n- Option ist hervorgehoben\n----------------------------------------\n<!-- funktion -->\nNächsten Schritt anzeigen\n<!-- ziel -->\nFührt zum nächsten Schritt mit gültigen Optionen\n<!-- regeln -->\n- Weiter-Knopf heißt am letzten Schritt Fertig\n- Leere Schritte werden übersprungen\n<!-- testen -->\n- Konfigurator öffnen\n- LMB auf Weiter-Knopf\n- Nächster Schritt erscheint\n----------------------------------------\n<!-- funktion -->\nVorherigen Schritt anzeigen\n<!-- ziel -->\nFührt zurück zum letzten Schritt mit Optionen\n<!-- regeln -->\n- Zurück-Knopf ist am ersten Schritt gesperrt\n- Leere Schritte werden übersprungen\n<!-- testen -->\n- Zum zweiten Schritt gehen\n- LMB auf Zurück-Knopf\n- Erster Schritt erscheint\n----------------------------------------\n<!-- funktion -->\nAuswahl übernehmen\n<!-- ziel -->\nWechselt das Produkt auf die gewählte Variante\n<!-- regeln -->\n- Fertig schreibt die Auswahl in die Auswahlfelder\n- Unbesuchte Schritte bekommen die erste gültige Option\n- Der Variantenwechsel von Shopware führt den Wechsel aus\n- Die Seite zeigt danach die Adresse der Variante\n<!-- testen -->\n- Alle Schritte durchgehen\n- LMB auf Fertig-Knopf\n- Produktseite zeigt die gewählte Variante\n----------------------------------------\n<!-- funktion -->\nKonfigurator schließen\n<!-- ziel -->\nBeendet die Auswahl ohne Wechsel der Variante\n<!-- regeln -->\n- x-Icon schließt das Modal\n- Escape schließt das Modal\n- Fokus kehrt zum Auslöser zurück\n<!-- testen -->\n- Konfigurator öffnen\n- Escape drücken\n- Modal ist geschlossen\n"
}
},
{
"titel": "„Layout zuweisen“ geändert",
"dateien": {
"layout-zuweisen": "<!-- titel -->\nLayout zuweisen\n----------------------------------------\n<!-- art -->\nkern\n----------------------------------------\n<!-- ziel -->\nEin Händler entscheidet je Produkt, ob die geführte Auswahl erscheint\n----------------------------------------\n<!-- regeln -->\n- Einstellungen liegen auf dem Varianten-Tab des Produkts\n- Werte stehen in den customFields des Produkts\n- Bearbeiten erfordert das Recht product.editor\n- Ohne Zuweisung bleibt das Standardlayout aktiv\n----------------------------------------\n<!-- funktion -->\nKonfigurator aktivieren\n<!-- ziel -->\nSchaltet die geführte Variantenauswahl für ein Produkt frei\n<!-- regeln -->\n- Schalter steht auf dem Varianten-Tab\n- Aktiv ersetzt die Buybox des Produkts\n- Inaktiv zeigt das Standardlayout von Shopware\n<!-- testen -->\n- Produkt im Admin öffnen\n- Varianten-Tab öffnen\n- Schalter aktivieren und speichern\n- Buybox zeigt die geführte Auswahl\n----------------------------------------\n<!-- funktion -->\nOptionsübersicht hinterlegen\n<!-- ziel -->\nHinterlegt ein PDF mit allen Varianten und Kombinationen\n<!-- regeln -->\n- Medienfeld steht auf dem Varianten-Tab\n- Feld hängt am Hauptartikel\n- Ohne PDF bleibt der Knopf unsichtbar\n<!-- testen -->\n- Varianten-Tab öffnen\n- PDF im Medienfeld wählen\n- Produkt speichern\n- Modal zeigt den Knopf für Optionen\n----------------------------------------\n<!-- funktion -->\nTrust-Logo hinterlegen\n<!-- ziel -->\nZeigt ein Vertrauenssiegel neben dem Gesamtpreis\n<!-- regeln -->\n- Logo steht in der Plugin-Konfiguration\n- Gilt für den ganzen Verkaufskanal\n- Ohne Logo erscheint nur der Text\n<!-- testen -->\n- Plugin-Konfiguration öffnen\n- Logo im Medienfeld wählen\n- Modal zeigt das Logo am Preis\n----------------------------------------\n<!-- funktion -->\nOptionsbild zuordnen\n<!-- stand -->\noffen\n<!-- ziel -->\nHinterlegt je Variante ein Farbbild oder Materialbild\n<!-- regeln -->\n- Zuordnung erfolgt auf Variantenebene\n- Bild erscheint neben der Option im Modal\n<!-- testen -->\n- Variante im Admin öffnen\n- Bild der Variante zuordnen\n- Modal zeigt das Bild an der Option\n"
}
},
{
"titel": "„Varianten wählen“ geändert",
"dateien": {
"varianten-waehlen": "<!-- titel -->\nVarianten wählen\n----------------------------------------\n<!-- art -->\nkern\n----------------------------------------\n<!-- ziel -->\nDer Kunde findet Schritt für Schritt eine bestellbare Kombination\n----------------------------------------\n<!-- regeln -->\n- Erster Schritt zeigt alle Optionen seiner Eigenschaft\n- Folgeschritte zeigen nur kombinierbare Optionen\n- Der Ablauf trägt beliebig viele Eigenschaften\n- Reihenfolge der Schritte kommt aus dem Backend\n- Ohne Auswahl greifen die Standardvarianten des Elternartikels\n- Schritte ohne gültige Option werden übersprungen\n- Eine Auswahl wechselt den Schritt nicht\n- Ungültige Folgeauswahl fällt beim Wechsel weg\n- Auf kleinen Bildschirmen bleibt der Ablauf derselbe\n- Escape schließt den Konfigurator\n- Tabulator bleibt im Dialog\n----------------------------------------\n<!-- funktion -->\nSchritt öffnen\n<!-- ziel -->\nÖffnet den Konfigurator beim angeklickten Schritt\n<!-- regeln -->\n- LMB auf einen Schritt öffnet das Modal\n- Offener Konfigurator wechselt nur den Schritt\n- Modal hängt am Seitenkörper\n<!-- testen -->\n- Produktseite öffnen\n- LMB auf zweiten Schritt\n- Modal öffnet bei Schritt 2\n----------------------------------------\n<!-- funktion -->\nOption wählen\n<!-- ziel -->\nÜbernimmt eine Option in die laufende Auswahl\n<!-- regeln -->\n- Gewählte Option ist hervorgehoben\n- Nicht kombinierbare Optionen sind gesperrt\n- Auswahl verwirft ungültige Folgeschritte\n- Schritt bleibt nach der Wahl derselbe\n<!-- testen -->\n- Konfigurator öffnen\n- LMB auf eine Option\n- Option ist hervorgehoben\n----------------------------------------\n<!-- funktion -->\nNächsten Schritt anzeigen\n<!-- ziel -->\nFührt zum nächsten Schritt mit gültigen Optionen\n<!-- regeln -->\n- Weiter-Knopf heißt am letzten Schritt Fertig\n- Leere Schritte werden übersprungen\n<!-- testen -->\n- Konfigurator öffnen\n- LMB auf Weiter-Knopf\n- Nächster Schritt erscheint\n----------------------------------------\n<!-- funktion -->\nVorherigen Schritt anzeigen\n<!-- ziel -->\nFührt zurück zum letzten Schritt mit Optionen\n<!-- regeln -->\n- Zurück-Knopf ist am ersten Schritt gesperrt\n- Leere Schritte werden übersprungen\n<!-- testen -->\n- Zum zweiten Schritt gehen\n- LMB auf Zurück-Knopf\n- Erster Schritt erscheint\n----------------------------------------\n<!-- funktion -->\nAuswahl übernehmen\n<!-- ziel -->\nWechselt das Produkt auf die gewählte Variante\n<!-- regeln -->\n- Fertig überträgt die Auswahl in die Auswahlfelder\n- Unbesuchte Schritte bekommen die erste gültige Option\n- Shopware führt den Variantenwechsel aus\n<!-- testen -->\n- Alle Schritte durchgehen\n- LMB auf Fertig-Knopf\n- Produktseite zeigt die gewählte Variante\n----------------------------------------\n<!-- funktion -->\nKonfigurator schließen\n<!-- ziel -->\nBeendet die Auswahl ohne Wechsel der Variante\n<!-- regeln -->\n- x-Icon schließt das Modal\n- Escape schließt das Modal\n- Fokus kehrt zum Auslöser zurück\n<!-- testen -->\n- Konfigurator öffnen\n- Escape drücken\n- Modal ist geschlossen\n"
}
},
{
"titel": "„Schritt zeigen“ geändert",
"dateien": {
"schritt-zeigen": "<!-- titel -->\nSchritt zeigen\n----------------------------------------\n<!-- art -->\nkern\n----------------------------------------\n<!-- ziel -->\nDer Kunde weiß im Modal, wo er steht und was er wählt\n----------------------------------------\n<!-- regeln -->\n- Optionen stehen rechts, die Bilder links\n- Lange Optionslisten lassen sich scrollen\n----------------------------------------\n<!-- funktion -->\nEigenschaft benennen\n<!-- ziel -->\nZeigt den Namen der Eigenschaft des aktuellen Schritts\n<!-- regeln -->\n- Name steht als Überschrift über der Liste\n- Name kommt aus der Eigenschaftsgruppe\n<!-- testen -->\n- Konfigurator öffnen\n- Überschrift nennt die erste Eigenschaft\n----------------------------------------\n<!-- funktion -->\nSchrittstand anzeigen\n<!-- ziel -->\nZeigt den aktuellen Schritt und die Gesamtzahl\n<!-- regeln -->\n- Zähler nennt Schritt und Gesamtzahl\n- Balken wächst mit dem Fortschritt\n- Text kommt aus einem Textbaustein\n<!-- testen -->\n- Konfigurator öffnen\n- Zähler zeigt 1 von 3\n----------------------------------------\n<!-- funktion -->\nOptionen auflisten\n<!-- ziel -->\nZeigt alle Optionen der Eigenschaft mit Namen und Bild\n<!-- regeln -->\n- Jede Option nennt ihren Namen\n- Bild erscheint nur bis zwei Schritte\n- Ohne Bild erscheint der Farbwert\n- Ohne Farbwert bleibt die Zeile ohne Zeichen\n<!-- testen -->\n- Konfigurator öffnen\n- Jede Option zeigt ihren Namen\n"
}
}
]

View File

@@ -0,0 +1,49 @@
<!-- titel -->
Auswahl anstoßen
----------------------------------------
<!-- art -->
kern
----------------------------------------
<!-- ziel -->
Der Kunde erkennt in der Buybox die Reihenfolge der Schritte
----------------------------------------
<!-- regeln -->
- Jede Eigenschaft bildet einen eigenen Schritt
- Natives Auswahlfeld bleibt für den Variantenwechsel erhalten
- Eigene Darstellung gilt unabhängig vom Anzeigetyp der Eigenschaft
- Ohne aktives Layout erscheint die Standard-Buybox
----------------------------------------
<!-- funktion -->
Schrittnummer anzeigen
<!-- ziel -->
Zeigt neben jeder Eigenschaft ihre Position im Ablauf
<!-- regeln -->
- Beschriftung nennt das Wort Schritt und die Nummer
- Nummer folgt der Reihenfolge aus dem Backend
<!-- testen -->
- Produktseite öffnen
- Erste Eigenschaft zeigt Schritt 1
----------------------------------------
<!-- funktion -->
Hinweistext anzeigen
<!-- ziel -->
Erklärt dem Kunden, worauf er im Schritt achtet
<!-- regeln -->
- Text kommt aus einem Textbaustein je Schritt
- Leerer Textbaustein blendet den Hinweis aus
- Hinweis hängt an der Nummer statt an der Eigenschaft
<!-- testen -->
- Textbaustein für Schritt 1 füllen
- Produktseite öffnen
- Hinweis steht unter der Überschrift
----------------------------------------
<!-- funktion -->
Gewählte Option anzeigen
<!-- ziel -->
Zeigt am Schritt, welche Option gerade gilt
<!-- regeln -->
- Ohne Auswahl steht dort Bitte wählen
- Vorbelegung erscheint ohne Zutun des Kunden
<!-- testen -->
- Hauptartikel ohne Variante öffnen
- Jeder Schritt nennt eine Option

View File

@@ -0,0 +1,47 @@
<!-- titel -->
Beratung anbieten
----------------------------------------
<!-- art -->
optional
----------------------------------------
<!-- ziel -->
Der Kunde findet im Konfigurator Vertrauen und einen Kontakt
----------------------------------------
<!-- regeln -->
- Alle Texte kommen aus Textbausteinen
- Leerer Textbaustein blendet den Bereich aus
----------------------------------------
<!-- funktion -->
Vertrauen zeigen
<!-- ziel -->
Zeigt Siegel und Zusage neben dem Gesamtpreis
<!-- regeln -->
- Logo stammt aus der Plugin-Konfiguration
- Ohne Logo bleibt der Text stehen
<!-- testen -->
- Konfigurator öffnen
- Siegel steht neben dem Preis
----------------------------------------
<!-- funktion -->
Beratung erreichen
<!-- ziel -->
Bietet eine Telefonnummer für Fragen zur Konfiguration
<!-- regeln -->
- Nummer ist als Telefonlink klickbar
- Leere Nummer blendet den Bereich aus
<!-- testen -->
- Konfigurator öffnen
- LMB auf die Nummer
- Telefon-App öffnet sich
----------------------------------------
<!-- funktion -->
Optionsübersicht öffnen
<!-- ziel -->
Öffnet das hinterlegte PDF mit allen Optionen
<!-- regeln -->
- Knopf erscheint nur mit hinterlegtem PDF
- PDF öffnet in einem neuen Tab
<!-- testen -->
- Konfigurator öffnen
- LMB auf Optionen-Knopf
- PDF öffnet in neuem Tab

View File

@@ -0,0 +1,69 @@
<!-- titel -->
Layout zuweisen
----------------------------------------
<!-- art -->
kern
----------------------------------------
<!-- ziel -->
Ein Händler entscheidet je Produkt, ob die geführte Auswahl erscheint
----------------------------------------
<!-- regeln -->
- Einstellungen liegen auf dem Varianten-Tab des Produkts
- Werte stehen in den customFields des Produkts
- Bearbeiten erfordert das Recht product.editor
- Ohne Zuweisung bleibt das Standardlayout aktiv
----------------------------------------
<!-- funktion -->
Konfigurator aktivieren
<!-- ziel -->
Schaltet die geführte Variantenauswahl für ein Produkt frei
<!-- regeln -->
- Schalter steht auf dem Varianten-Tab
- Aktiv ersetzt die Buybox des Produkts
- Inaktiv zeigt das Standardlayout von Shopware
<!-- testen -->
- Produkt im Admin öffnen
- Varianten-Tab öffnen
- Schalter aktivieren und speichern
- Buybox zeigt die geführte Auswahl
----------------------------------------
<!-- funktion -->
Optionsübersicht hinterlegen
<!-- ziel -->
Hinterlegt ein PDF mit allen Varianten und Kombinationen
<!-- regeln -->
- Medienfeld steht auf dem Varianten-Tab
- Feld hängt am Hauptartikel
- Ohne PDF bleibt der Knopf unsichtbar
<!-- testen -->
- Varianten-Tab öffnen
- PDF im Medienfeld wählen
- Produkt speichern
- Modal zeigt den Knopf für Optionen
----------------------------------------
<!-- funktion -->
Trust-Logo hinterlegen
<!-- ziel -->
Zeigt ein Vertrauenssiegel neben dem Gesamtpreis
<!-- regeln -->
- Logo steht in der Plugin-Konfiguration
- Gilt für den ganzen Verkaufskanal
- Ohne Logo erscheint nur der Text
<!-- testen -->
- Plugin-Konfiguration öffnen
- Logo im Medienfeld wählen
- Modal zeigt das Logo am Preis
----------------------------------------
<!-- funktion -->
Optionsbild am Datensatz pflegen
<!-- stand -->
offen
<!-- ziel -->
Hinterlegt das Bild einer Option am Datensatz der Variante
<!-- regeln -->
- Feld liegt im Formular der einzelnen Variante
- Heute stammt das Bild aus der Eigenschaftsgruppe
<!-- testen -->
- Variante im Admin öffnen
- Bildfeld der Variante füllen
- Variante speichern

View File

@@ -0,0 +1,51 @@
<!-- titel -->
Option ansehen
----------------------------------------
<!-- art -->
kern
----------------------------------------
<!-- ziel -->
Der Kunde prüft eine Option groß, bevor er sie nimmt
----------------------------------------
<!-- regeln -->
- Nur Optionen mit Bild tragen den Hinweis
- Ansicht liegt über dem Konfigurator
----------------------------------------
<!-- funktion -->
Optionsdetail öffnen
<!-- ziel -->
Zeigt Bild und Namen einer Option in voller Größe
<!-- regeln -->
- i-Icon öffnet die Ansicht
- Enter öffnet die Ansicht
- Leertaste öffnet die Ansicht
<!-- testen -->
- Konfigurator öffnen
- LMB auf i-Icon einer Option
- Große Ansicht erscheint
----------------------------------------
<!-- funktion -->
Option aus Ansicht wählen
<!-- ziel -->
Übernimmt die betrachtete Option in die Auswahl
<!-- regeln -->
- Knopf übernimmt die Option
- Knopf schließt die Ansicht
- Schritt bleibt derselbe
<!-- testen -->
- Optionsdetail öffnen
- LMB auf Wählen-Knopf
- Option ist gewählt
----------------------------------------
<!-- funktion -->
Ansicht schließen
<!-- ziel -->
Kehrt zur Optionsliste zurück ohne Auswahl
<!-- regeln -->
- x-Icon schließt die Ansicht
- Escape schließt die Ansicht
- Konfigurator bleibt offen
<!-- testen -->
- Optionsdetail öffnen
- Escape drücken
- Optionsliste erscheint wieder

View File

@@ -0,0 +1,49 @@
<!-- titel -->
Schritt zeigen
----------------------------------------
<!-- art -->
kern
----------------------------------------
<!-- ziel -->
Der Kunde weiß im Modal, wo er steht und was er wählt
----------------------------------------
<!-- regeln -->
- Optionen stehen rechts
- Bilder links
- Lange Optionslisten lassen sich scrollen
----------------------------------------
<!-- funktion -->
Eigenschaft benennen
<!-- ziel -->
Zeigt den Namen der Eigenschaft des aktuellen Schritts
<!-- regeln -->
- Name steht als Überschrift über der Liste
- Name kommt aus der Eigenschaftsgruppe
<!-- testen -->
- Konfigurator öffnen
- Überschrift nennt die erste Eigenschaft
----------------------------------------
<!-- funktion -->
Schrittstand anzeigen
<!-- ziel -->
Zeigt den aktuellen Schritt und die Gesamtzahl
<!-- regeln -->
- Zähler nennt Schritt und Gesamtzahl
- Balken wächst mit dem Fortschritt
- Text kommt aus einem Textbaustein
<!-- testen -->
- Konfigurator öffnen
- Zähler zeigt 1 von 3
----------------------------------------
<!-- funktion -->
Optionen auflisten
<!-- ziel -->
Zeigt alle Optionen der Eigenschaft mit Namen und Bild
<!-- regeln -->
- Jede Option nennt ihren Namen
- Bild erscheint nur bis zwei Schritte
- Ohne Bild erscheint der Farbwert
- Ohne Farbwert bleibt die Zeile ohne Zeichen
<!-- testen -->
- Konfigurator öffnen
- Jede Option zeigt ihren Namen

View File

@@ -0,0 +1,109 @@
<!-- titel -->
Varianten wählen
----------------------------------------
<!-- art -->
kern
----------------------------------------
<!-- ziel -->
Der Kunde findet Schritt für Schritt eine bestellbare Kombination
----------------------------------------
<!-- regeln -->
- Erster Schritt zeigt alle Optionen seiner Eigenschaft
- Folgeschritte zeigen nur kombinierbare Optionen
- Der Ablauf trägt beliebig viele Eigenschaften
- Reihenfolge der Schritte kommt aus dem Backend
- Ohne Auswahl greifen die Standardvarianten des Elternartikels
- Schritte ohne gültige Option werden übersprungen
- Eine Auswahl wechselt den Schritt nicht
- Ungültige Folgeauswahl fällt beim Wechsel weg
- Auf kleinen Bildschirmen bleibt der Ablauf derselbe
- Escape schließt den Konfigurator
- Tabulator bleibt im Dialog
----------------------------------------
<!-- funktion -->
Schritt öffnen
<!-- ziel -->
Öffnet den Konfigurator beim angeklickten Schritt
<!-- regeln -->
- LMB auf einen Schritt öffnet das Modal
- Offener Konfigurator wechselt nur den Schritt
- Modal hängt am Seitenkörper
<!-- testen -->
- Produktseite öffnen
- LMB auf zweiten Schritt
- Modal öffnet bei Schritt 2
----------------------------------------
<!-- funktion -->
Option wählen
<!-- ziel -->
Übernimmt eine Option in die laufende Auswahl
<!-- regeln -->
- Gewählte Option ist hervorgehoben
- Nicht kombinierbare Optionen sind gesperrt
- Auswahl verwirft ungültige Folgeschritte
- Schritt bleibt nach der Wahl derselbe
<!-- testen -->
- Konfigurator öffnen
- LMB auf eine Option
- Option ist hervorgehoben
----------------------------------------
<!-- funktion -->
Nächsten Schritt anzeigen
<!-- ziel -->
Führt zum nächsten Schritt mit gültigen Optionen
<!-- regeln -->
- Weiter-Knopf heißt am letzten Schritt Fertig
- Leere Schritte werden übersprungen
<!-- testen -->
- Konfigurator öffnen
- LMB auf Weiter-Knopf
- Nächster Schritt erscheint
----------------------------------------
<!-- funktion -->
Vorherigen Schritt anzeigen
<!-- ziel -->
Führt zurück zum letzten Schritt mit Optionen
<!-- regeln -->
- Zurück-Knopf ist am ersten Schritt gesperrt
- Leere Schritte werden übersprungen
<!-- testen -->
- Zum zweiten Schritt gehen
- LMB auf Zurück-Knopf
- Erster Schritt erscheint
----------------------------------------
<!-- funktion -->
Auswahl übernehmen
<!-- ziel -->
Wechselt das Produkt auf die gewählte Variante
<!-- regeln -->
- Fertig überträgt die Auswahl in die Auswahlfelder
- Unbesuchte Schritte bekommen die erste gültige Option
- Shopware führt den Variantenwechsel aus
<!-- testen -->
- Alle Schritte durchgehen
- LMB auf Fertig-Knopf
- Produktseite zeigt die gewählte Variante
----------------------------------------
<!-- funktion -->
Konfigurator schließen
<!-- ziel -->
Beendet die Auswahl ohne Wechsel der Variante
<!-- regeln -->
- x-Icon schließt das Modal
- Escape schließt das Modal
- Fokus kehrt zum Auslöser zurück
<!-- testen -->
- Konfigurator öffnen
- Escape drücken
- Modal ist geschlossen
----------------------------------------
<!-- funktion -->
Konfigurationsdaten bereitstellen
<!-- ziel -->
Legt der Produktseite alles bei, was der Konfigurator braucht
<!-- regeln -->
- Kombinationen stammen vom Hauptartikel
- Ohne aktives Layout bleibt die Beilage aus
<!-- testen -->
- Produktseite mit aktivem Layout öffnen
- Seite trägt Kombinationen und Vorbelegung

View File

@@ -0,0 +1,53 @@
<!-- titel -->
Vorschau zeigen
----------------------------------------
<!-- art -->
kern
----------------------------------------
<!-- ziel -->
Der Kunde sieht Bild und Preis der gewählten Kombination
----------------------------------------
<!-- regeln -->
- Preis stimmt mit dem Preis der Produktseite überein
- Antwort wird nicht zwischengespeichert
- Späte Antworten überholter Anfragen werden verworfen
- Ohne eigene Bilder greifen die Bilder des Elternartikels
----------------------------------------
<!-- funktion -->
Vorschau laden
<!-- ziel -->
Holt Bilder und Preis zur laufenden Auswahl
<!-- regeln -->
- Jede Auswahl löst eine neue Anfrage aus
- Titelbild der Variante steht an erster Stelle
- Ohne Treffer bleibt die Produkt-ID als Rückfall
<!-- testen -->
- Konfigurator öffnen
- Andere Option wählen
- Bild und Preis ändern sich
----------------------------------------
<!-- funktion -->
Galerie blättern
<!-- ziel -->
Zeigt weitere Bilder der gewählten Variante
<!-- regeln -->
- Pfeil-Icons blättern vor
- Pfeil-Icons blättern zurück
- Nach dem letzten Bild folgt das erste
- Bei einem Bild bleibt die Navigation aus
<!-- testen -->
- Konfigurator öffnen
- LMB auf Vor-Icon
- Nächstes Bild erscheint
----------------------------------------
<!-- funktion -->
Bild wählen
<!-- ziel -->
Wechselt das große Bild über die Vorschaubilder
<!-- regeln -->
- Aktives Vorschaubild ist hervorgehoben
- Großes Bild folgt der Wahl
<!-- testen -->
- Konfigurator öffnen
- LMB auf drittes Vorschaubild
- Großes Bild zeigt dieses Motiv

View File

@@ -0,0 +1,537 @@
{
"abdeckung": {
"einbinden:VariantConfiguratorPlugin": {
"abdruck": "823481507a44729d",
"funktion": "Konfigurationsdaten bereitstellen",
"trug": "5fbc8363b7cffe59"
},
"einbinden:erweitert @Storefront/storefront/component/buy-widget/configurator.html.twig": {
"abdruck": "c6d031d6bc598089",
"funktion": "Konfigurator aktivieren",
"trug": "6cf5a2b08ce2371a"
},
"einbinden:override sw-product-detail-variants": {
"abdruck": "2fcf518e7333458a",
"funktion": "",
"trug": ""
},
"einbinden:register mto-vc-variants-card": {
"abdruck": "c472feadaac5ac71",
"funktion": "",
"trug": ""
},
"hoeren:[data-mto-vc-gallery-next].click": {
"abdruck": "c402ad6f114e8c3d",
"funktion": "Galerie blättern",
"trug": "bacf45ebf9dd77b8"
},
"hoeren:[data-mto-vc-gallery-prev].click": {
"abdruck": "f50d4f7effc60a06",
"funktion": "Galerie blättern",
"trug": "bacf45ebf9dd77b8"
},
"hoeren:[data-mto-vc-lightbox-close].click": {
"abdruck": "89a22850af7694bb",
"funktion": "Galerie blättern",
"trug": "bacf45ebf9dd77b8"
},
"hoeren:document.keydown": {
"abdruck": "7c018ebbfa346bee",
"funktion": "Schritt öffnen",
"trug": "efe9634e3080c073"
},
"hoeren:el.click": {
"abdruck": "6a423a062267755c",
"funktion": "Nächsten Schritt anzeigen",
"trug": "f6b276e3fedf287d"
},
"hoeren:info.click": {
"abdruck": "7a5a70dfb792bf19",
"funktion": "Optionsdetail öffnen",
"trug": "39fed29af6734cd4"
},
"hoeren:info.keydown": {
"abdruck": "409cecf6fb3bbb4a",
"funktion": "Optionsdetail öffnen",
"trug": "39fed29af6734cd4"
},
"hoeren:lightboxSelect.click": {
"abdruck": "9add2d2c77b440b5",
"funktion": "Ansicht schließen",
"trug": "24d52cc6d249d4f5"
},
"hoeren:next.click": {
"abdruck": "d5b0cdcce40afcff",
"funktion": "Konfigurator schließen",
"trug": "1cc1a6b5fc943e03"
},
"hoeren:prev.click": {
"abdruck": "6b8db8d885c0d6f8",
"funktion": "Konfigurator schließen",
"trug": "1cc1a6b5fc943e03"
},
"hoeren:row.click": {
"abdruck": "b4ec6f63c613c35f",
"funktion": "Option wählen",
"trug": "80df89cf0c129387"
},
"hoeren:thumb.click": {
"abdruck": "b4aa2111971f6886",
"funktion": "Bild wählen",
"trug": "0604a0a6875489b8"
},
"hoeren:trigger.click": {
"abdruck": "e219933925adc81a",
"funktion": "Schritt öffnen",
"trug": "efe9634e3080c073"
},
"reaktion:ProductPageLoadedEvent": {
"abdruck": "6f808b91c1a0c4f5",
"funktion": "Konfigurationsdaten bereitstellen",
"trug": "5fbc8363b7cffe59"
},
"route:get /mto-variant-configurator/resolve/{productId}": {
"abdruck": "b14355d6007564a1",
"funktion": "Konfigurationsdaten bereitstellen",
"trug": "5fbc8363b7cffe59"
}
},
"belege": {
"[\"auswahl-anstossen\", \"Gewählte Option anzeigen\"]": {
"abdruck": "626af3ee300b610a",
"beleg": "<span class=\"mto-vc-select-trigger-label\" data-mto-vc-trigger-label>\n {{ mtoVcSelected ? mtoVcSelected.translated.name : 'mto.variantConfigurator.choose'|trans }}\n </span>",
"datei": "src/Resources/views/storefront/component/buy-widget/configurator.html.twig"
},
"[\"auswahl-anstossen\", \"Hinweistext anzeigen\"]": {
"abdruck": "b9baf394ca99d4e6",
"beleg": "{% if mtoVcBubble != mtoVcBubbleKey and mtoVcBubble is not empty %}\n <p class=\"mto-vc-bubble\">{{ mtoVcBubble|sw_sanitize }}</p>\n {% endif %}",
"datei": "src/Resources/views/storefront/component/buy-widget/configurator.html.twig"
},
"[\"auswahl-anstossen\", \"Schrittnummer anzeigen\"]": {
"abdruck": "b9e4deada2c3a6f1",
"beleg": "<span class=\"mto-vc-step-badge\">{{ 'mto.variantConfigurator.step'|trans({ '%step%': mtoVcStep })|sw_sanitize }}</span>",
"datei": "src/Resources/views/storefront/component/buy-widget/configurator.html.twig"
},
"[\"beratung-anbieten\", \"Beratung erreichen\"]": {
"abdruck": "fa513f8d265ad713",
"beleg": "{% if ctaPhone is not empty %}\n <span class=\"mto-vc-support-icon\" aria-hidden=\"true\"></span>\n <a href=\"tel:{{ ctaPhone|replace({' ': '', '/': '', '-': '', '(': '', ')': ''}) }}\"\n class=\"mto-vc-support-phone\">{{ ctaPhone|sw_sanitize }}</a>\n {% endif %}",
"datei": "src/Resources/views/storefront/component/buy-widget/mto-variant-configurator-modal.html.twig"
},
"[\"beratung-anbieten\", \"Optionsübersicht öffnen\"]": {
"abdruck": "8aba7fb9bf50281b",
"beleg": "{% if pdfUrl %}\n <a href=\"{{ pdfUrl }}\" target=\"_blank\" rel=\"noopener\"\n class=\"mto-vc-btn is-outline mto-vc-all-options\">\n {{ 'mto.variantConfigurator.allOptions'|trans|sw_sanitize }}\n </a>\n {% else %}\n <span class=\"mto-vc-actions-spacer\" aria-hidden=\"true\"></span>\n {% endif %}",
"datei": "src/Resources/views/storefront/component/buy-widget/mto-variant-configurator-modal.html.twig"
},
"[\"beratung-anbieten\", \"Vertrauen zeigen\"]": {
"abdruck": "5b204dff7de6f96d",
"beleg": "{% if trustLogo %}\n <img src=\"{{ trustLogo }}\" alt=\"\" class=\"mto-vc-trust-logo\">\n{% endif %}",
"datei": "src/Resources/views/storefront/component/buy-widget/mto-variant-configurator-modal.html.twig"
},
"[\"layout-zuweisen\", \"Konfigurator aktivieren\"]": {
"abdruck": "60a65511f06ae7fe",
"beleg": "<sw-switch-field\n v-model:value=\"configuratorActive\"\n class=\"mto-vc-variants-card__active\"\n :disabled=\"!editable\"\n :label=\"$tc('mto-variant-configurator.card.activeLabel')\"\n :help-text=\"$tc('mto-variant-configurator.card.activeHelp')\"\n />",
"datei": "src/Resources/app/administration/src/component/mto-vc-variants-card/mto-vc-variants-card.html.twig"
},
"[\"layout-zuweisen\", \"Optionsübersicht hinterlegen\"]": {
"abdruck": "97b421cf540a6cc2",
"beleg": "{% if pdfUrl %}\n <a href=\"{{ pdfUrl }}\" target=\"_blank\" rel=\"noopener\"\n class=\"mto-vc-btn is-outline mto-vc-all-options\">\n {{ 'mto.variantConfigurator.allOptions'|trans|sw_sanitize }}\n </a>\n {% else %}\n <span class=\"mto-vc-actions-spacer\" aria-hidden=\"true\"></span>\n {% endif %}",
"datei": "src/Resources/views/storefront/component/buy-widget/mto-variant-configurator-modal.html.twig"
},
"[\"layout-zuweisen\", \"Trust-Logo hinterlegen\"]": {
"abdruck": "1954311a4c3a9cc1",
"beleg": "$mediaId = $this->systemConfigService->get(\n MtoVariantConfigurator::CONFIG_TRUST_LOGO,\n $salesChannelId,\n );",
"datei": "src/Core/Content/Product/VariantResolveService.php"
},
"[\"option-ansehen\", \"Ansicht schließen\"]": {
"abdruck": "6eac493d9a481e30",
"beleg": "if (!this._nodes.lightbox.hidden) {\n this._closeLightbox();\n } else {\n this._close();\n }",
"datei": "src/Resources/app/storefront/src/js/variant-configurator.plugin.js"
},
"[\"option-ansehen\", \"Option aus Ansicht wählen\"]": {
"abdruck": "b4223f8cf3e2456e",
"beleg": "this._nodes.lightboxSelect?.addEventListener('click', () => {\n if (this._lightboxOption) {\n this._selectOption(this.options.groups[this._currentStep], this._lightboxOption.id);\n }\n this._closeLightbox();\n });",
"datei": "src/Resources/app/storefront/src/js/variant-configurator.plugin.js"
},
"[\"option-ansehen\", \"Optionsdetail öffnen\"]": {
"abdruck": "59faab581e0fac7f",
"beleg": "info.addEventListener('click', open);\n info.addEventListener('keydown', (event) => {\n if (event.key === 'Enter' || event.key === ' ') {\n open(event);\n }\n });",
"datei": "src/Resources/app/storefront/src/js/variant-configurator.plugin.js"
},
"[\"schritt-zeigen\", \"Eigenschaft benennen\"]": {
"abdruck": "dd54064a67689e2c",
"beleg": "this._nodes.title.textContent = group.name;",
"datei": "src/Resources/app/storefront/src/js/variant-configurator.plugin.js"
},
"[\"schritt-zeigen\", \"Optionen auflisten\"]": {
"abdruck": "a71af321f056608d",
"beleg": "if (option.media) {\n const visual = document.createElement('span');\n visual.className = 'mto-vc-option-media';\n const img = document.createElement('img');\n img.src = option.media;\n img.alt = '';\n img.loading = 'lazy';\n visual.appendChild(img);\n row.appendChild(visual);\n } else if (option.color) {\n const visual = document.createElement('span');\n visual.className = 'mto-vc-option-swatch';\n visual.style.backgroundColor = option.color;\n row.appendChild(visual);\n } else {\n row.classList.add('has-no-visual');\n }",
"datei": "src/Resources/app/storefront/src/js/variant-configurator.plugin.js"
},
"[\"schritt-zeigen\", \"Schrittstand anzeigen\"]": {
"abdruck": "aa7b8888103f8bd0",
"beleg": "this._nodes.counter.textContent = (this._snippets.stepCount || '__step__ / __total__')\n .replace('__step__', String(step))\n .replace('__total__', String(total));\n this._nodes.progress.style.width = `${(step / total) * 100}%`;",
"datei": "src/Resources/app/storefront/src/js/variant-configurator.plugin.js"
},
"[\"varianten-waehlen\", \"Auswahl übernehmen\"]": {
"abdruck": "21ccf5bc0286c95c",
"beleg": "if (optionId && select.value !== optionId) {\n select.value = optionId;\n lastChanged = select;\n }",
"datei": "src/Resources/app/storefront/src/js/variant-configurator.plugin.js"
},
"[\"varianten-waehlen\", \"Konfigurationsdaten bereitstellen\"]": {
"abdruck": "e6e5848381fce5aa",
"beleg": "$page->assign([",
"datei": "src/Subscriber/ProductPageSubscriber.php"
},
"[\"varianten-waehlen\", \"Konfigurator schließen\"]": {
"abdruck": "1b7d4aea18cdb896",
"beleg": "_close() {\n this._closeLightbox();\n this._modalEl.hidden = true;\n document.body.classList.remove('mto-vc-modal-open');\n document.removeEventListener('keydown', this._keydownHandler);\n this._lastFocus?.focus();\n }",
"datei": "src/Resources/app/storefront/src/js/variant-configurator.plugin.js"
},
"[\"varianten-waehlen\", \"Nächsten Schritt anzeigen\"]": {
"abdruck": "3deee97595984a3e",
"beleg": "const isLast = this._nextUsableStep(this._currentStep + 1, 1) === null;\n this._nodes.next.textContent = isLast\n ? this._nodes.next.dataset.finishLabel\n : this._nodes.next.dataset.nextLabel;",
"datei": "src/Resources/app/storefront/src/js/variant-configurator.plugin.js"
},
"[\"varianten-waehlen\", \"Option wählen\"]": {
"abdruck": "4b9f7dc844cf7aac",
"beleg": "this._selected[group.id] = optionId;",
"datei": "src/Resources/app/storefront/src/js/variant-configurator.plugin.js"
},
"[\"varianten-waehlen\", \"Schritt öffnen\"]": {
"abdruck": "94b3aa8d50c53fc8",
"beleg": "this._triggers.forEach((trigger) => {\n trigger.addEventListener('click', (event) => {\n event.preventDefault();\n this._open(parseInt(trigger.dataset.mtoVcStepIndex, 10));\n });\n });",
"datei": "src/Resources/app/storefront/src/js/variant-configurator.plugin.js"
},
"[\"varianten-waehlen\", \"Vorherigen Schritt anzeigen\"]": {
"abdruck": "df5e60fe148da5e6",
"beleg": "this._nodes.prev.disabled = this._nextUsableStep(this._currentStep - 1, -1) === null;",
"datei": "src/Resources/app/storefront/src/js/variant-configurator.plugin.js"
},
"[\"vorschau-zeigen\", \"Bild wählen\"]": {
"abdruck": "ab1e74251036541a",
"beleg": "thumb.className = `mto-vc-thumb${index === this._galleryIndex ? ' is-active' : ''}`;",
"datei": "src/Resources/app/storefront/src/js/variant-configurator.plugin.js"
},
"[\"vorschau-zeigen\", \"Galerie blättern\"]": {
"abdruck": "6b28a5866629b878",
"beleg": "_stepGallery(delta) {\n if (this._gallery.length < 2) {\n return;\n }\n const count = this._gallery.length;\n this._galleryIndex = (this._galleryIndex + delta + count) % count;\n this._renderGallery();\n }",
"datei": "src/Resources/app/storefront/src/js/variant-configurator.plugin.js"
},
"[\"vorschau-zeigen\", \"Vorschau laden\"]": {
"abdruck": "413024bc244f7770",
"beleg": "return new JsonResponse($this->variantResolveService->resolveVariant($variantId, $context));",
"datei": "src/Storefront/Controller/VariantConfiguratorController.php"
}
},
"ohne_code": {},
"ohne_karte": {},
"paare": {
"[\"[\\\"auswahl-anstossen\\\", \\\"Gewählte Option anzeigen\\\"]\", \"[\\\"auswahl-anstossen\\\", \\\"Hinweistext anzeigen\\\"]\"]": {
"abdruck": "450345e8a27f5695",
"gleich": false
},
"[\"[\\\"auswahl-anstossen\\\", \\\"Gewählte Option anzeigen\\\"]\", \"[\\\"auswahl-anstossen\\\", \\\"Schrittnummer anzeigen\\\"]\"]": {
"abdruck": "60008fdb6d69349b",
"gleich": false
},
"[\"[\\\"auswahl-anstossen\\\", \\\"Hinweistext anzeigen\\\"]\", \"[\\\"auswahl-anstossen\\\", \\\"Schrittnummer anzeigen\\\"]\"]": {
"abdruck": "3eaf19062d6d53a3",
"gleich": false
},
"[\"[\\\"beratung-anbieten\\\", \\\"Beratung erreichen\\\"]\", \"[\\\"beratung-anbieten\\\", \\\"Optionsübersicht öffnen\\\"]\"]": {
"abdruck": "9d3da3bf8c127ae1",
"gleich": false
},
"[\"[\\\"beratung-anbieten\\\", \\\"Beratung erreichen\\\"]\", \"[\\\"beratung-anbieten\\\", \\\"Vertrauen zeigen\\\"]\"]": {
"abdruck": "2c3c62cb4b935c56",
"gleich": false
},
"[\"[\\\"beratung-anbieten\\\", \\\"Optionsübersicht öffnen\\\"]\", \"[\\\"beratung-anbieten\\\", \\\"Vertrauen zeigen\\\"]\"]": {
"abdruck": "bf601d5dc7c6c507",
"gleich": false
},
"[\"[\\\"beratung-anbieten\\\", \\\"Optionsübersicht öffnen\\\"]\", \"[\\\"layout-zuweisen\\\", \\\"Optionsübersicht hinterlegen\\\"]\"]": {
"abdruck": "bc9dc09e54b78a75",
"gleich": false
},
"[\"[\\\"layout-zuweisen\\\", \\\"Konfigurator aktivieren\\\"]\", \"[\\\"layout-zuweisen\\\", \\\"Optionsbild am Datensatz pflegen\\\"]\"]": {
"abdruck": "5a4d802d668ddd55",
"gleich": false
},
"[\"[\\\"layout-zuweisen\\\", \\\"Konfigurator aktivieren\\\"]\", \"[\\\"layout-zuweisen\\\", \\\"Optionsübersicht hinterlegen\\\"]\"]": {
"abdruck": "eeed78a6284711f1",
"gleich": false
},
"[\"[\\\"layout-zuweisen\\\", \\\"Konfigurator aktivieren\\\"]\", \"[\\\"layout-zuweisen\\\", \\\"Trust-Logo hinterlegen\\\"]\"]": {
"abdruck": "b4944e5aa4df871a",
"gleich": false
},
"[\"[\\\"layout-zuweisen\\\", \\\"Optionsbild am Datensatz pflegen\\\"]\", \"[\\\"layout-zuweisen\\\", \\\"Optionsübersicht hinterlegen\\\"]\"]": {
"abdruck": "b91bdff82a120a8d",
"gleich": false
},
"[\"[\\\"layout-zuweisen\\\", \\\"Optionsbild am Datensatz pflegen\\\"]\", \"[\\\"layout-zuweisen\\\", \\\"Trust-Logo hinterlegen\\\"]\"]": {
"abdruck": "97268714e2b5d989",
"gleich": false
},
"[\"[\\\"layout-zuweisen\\\", \\\"Optionsübersicht hinterlegen\\\"]\", \"[\\\"layout-zuweisen\\\", \\\"Trust-Logo hinterlegen\\\"]\"]": {
"abdruck": "b52cedd4106ee837",
"gleich": false
},
"[\"[\\\"option-ansehen\\\", \\\"Ansicht schließen\\\"]\", \"[\\\"option-ansehen\\\", \\\"Option aus Ansicht wählen\\\"]\"]": {
"abdruck": "caa5ce4c5ff31774",
"gleich": false
},
"[\"[\\\"option-ansehen\\\", \\\"Ansicht schließen\\\"]\", \"[\\\"option-ansehen\\\", \\\"Optionsdetail öffnen\\\"]\"]": {
"abdruck": "462a46d7a2030874",
"gleich": false
},
"[\"[\\\"option-ansehen\\\", \\\"Option aus Ansicht wählen\\\"]\", \"[\\\"option-ansehen\\\", \\\"Optionsdetail öffnen\\\"]\"]": {
"abdruck": "8f695dfd90060204",
"gleich": false
},
"[\"[\\\"schritt-zeigen\\\", \\\"Eigenschaft benennen\\\"]\", \"[\\\"schritt-zeigen\\\", \\\"Optionen auflisten\\\"]\"]": {
"abdruck": "5372df6b482e9a5c",
"gleich": false
},
"[\"[\\\"schritt-zeigen\\\", \\\"Eigenschaft benennen\\\"]\", \"[\\\"schritt-zeigen\\\", \\\"Schrittstand anzeigen\\\"]\"]": {
"abdruck": "732d371c2135b53f",
"gleich": false
},
"[\"[\\\"schritt-zeigen\\\", \\\"Optionen auflisten\\\"]\", \"[\\\"schritt-zeigen\\\", \\\"Schrittstand anzeigen\\\"]\"]": {
"abdruck": "590d6974e948c69c",
"gleich": false
},
"[\"[\\\"varianten-waehlen\\\", \\\"Auswahl übernehmen\\\"]\", \"[\\\"varianten-waehlen\\\", \\\"Konfigurationsdaten bereitstellen\\\"]\"]": {
"abdruck": "34e23fca918e5dc0",
"gleich": false
},
"[\"[\\\"varianten-waehlen\\\", \\\"Auswahl übernehmen\\\"]\", \"[\\\"varianten-waehlen\\\", \\\"Konfigurator schließen\\\"]\"]": {
"abdruck": "eb521941a5dc979a",
"gleich": false
},
"[\"[\\\"varianten-waehlen\\\", \\\"Auswahl übernehmen\\\"]\", \"[\\\"varianten-waehlen\\\", \\\"Nächsten Schritt anzeigen\\\"]\"]": {
"abdruck": "271109c8b42cf5d5",
"gleich": false
},
"[\"[\\\"varianten-waehlen\\\", \\\"Auswahl übernehmen\\\"]\", \"[\\\"varianten-waehlen\\\", \\\"Option wählen\\\"]\"]": {
"abdruck": "618d3c213492555a",
"gleich": false
},
"[\"[\\\"varianten-waehlen\\\", \\\"Auswahl übernehmen\\\"]\", \"[\\\"varianten-waehlen\\\", \\\"Schritt öffnen\\\"]\"]": {
"abdruck": "b45252ce7cc39ed9",
"gleich": false
},
"[\"[\\\"varianten-waehlen\\\", \\\"Auswahl übernehmen\\\"]\", \"[\\\"varianten-waehlen\\\", \\\"Vorherigen Schritt anzeigen\\\"]\"]": {
"abdruck": "d7ade9bdd7d0c66e",
"gleich": false
},
"[\"[\\\"varianten-waehlen\\\", \\\"Konfigurationsdaten bereitstellen\\\"]\", \"[\\\"varianten-waehlen\\\", \\\"Konfigurator schließen\\\"]\"]": {
"abdruck": "97ceded85ca2035c",
"gleich": false
},
"[\"[\\\"varianten-waehlen\\\", \\\"Konfigurationsdaten bereitstellen\\\"]\", \"[\\\"varianten-waehlen\\\", \\\"Nächsten Schritt anzeigen\\\"]\"]": {
"abdruck": "ba83ab596b72af22",
"gleich": false
},
"[\"[\\\"varianten-waehlen\\\", \\\"Konfigurationsdaten bereitstellen\\\"]\", \"[\\\"varianten-waehlen\\\", \\\"Option wählen\\\"]\"]": {
"abdruck": "6f23248b5fbd7be4",
"gleich": false
},
"[\"[\\\"varianten-waehlen\\\", \\\"Konfigurationsdaten bereitstellen\\\"]\", \"[\\\"varianten-waehlen\\\", \\\"Schritt öffnen\\\"]\"]": {
"abdruck": "7350e93a2a368ac1",
"gleich": false
},
"[\"[\\\"varianten-waehlen\\\", \\\"Konfigurationsdaten bereitstellen\\\"]\", \"[\\\"varianten-waehlen\\\", \\\"Vorherigen Schritt anzeigen\\\"]\"]": {
"abdruck": "50753ef9f3d5c8b5",
"gleich": false
},
"[\"[\\\"varianten-waehlen\\\", \\\"Konfigurator schließen\\\"]\", \"[\\\"varianten-waehlen\\\", \\\"Nächsten Schritt anzeigen\\\"]\"]": {
"abdruck": "b49e65e1c60b5307",
"gleich": false
},
"[\"[\\\"varianten-waehlen\\\", \\\"Konfigurator schließen\\\"]\", \"[\\\"varianten-waehlen\\\", \\\"Option wählen\\\"]\"]": {
"abdruck": "b165f28b36fc3637",
"gleich": false
},
"[\"[\\\"varianten-waehlen\\\", \\\"Konfigurator schließen\\\"]\", \"[\\\"varianten-waehlen\\\", \\\"Schritt öffnen\\\"]\"]": {
"abdruck": "0bf509fed71f1c25",
"gleich": false
},
"[\"[\\\"varianten-waehlen\\\", \\\"Konfigurator schließen\\\"]\", \"[\\\"varianten-waehlen\\\", \\\"Vorherigen Schritt anzeigen\\\"]\"]": {
"abdruck": "de5acdbf92511e14",
"gleich": false
},
"[\"[\\\"varianten-waehlen\\\", \\\"Nächsten Schritt anzeigen\\\"]\", \"[\\\"varianten-waehlen\\\", \\\"Option wählen\\\"]\"]": {
"abdruck": "7c9a06867c701267",
"gleich": false
},
"[\"[\\\"varianten-waehlen\\\", \\\"Nächsten Schritt anzeigen\\\"]\", \"[\\\"varianten-waehlen\\\", \\\"Schritt öffnen\\\"]\"]": {
"abdruck": "f45378da83876bae",
"gleich": false
},
"[\"[\\\"varianten-waehlen\\\", \\\"Nächsten Schritt anzeigen\\\"]\", \"[\\\"varianten-waehlen\\\", \\\"Vorherigen Schritt anzeigen\\\"]\"]": {
"abdruck": "9fc4c9cf8ae1d7fb",
"gleich": false
},
"[\"[\\\"varianten-waehlen\\\", \\\"Option wählen\\\"]\", \"[\\\"varianten-waehlen\\\", \\\"Schritt öffnen\\\"]\"]": {
"abdruck": "a71f8b8b92d1025c",
"gleich": false
},
"[\"[\\\"varianten-waehlen\\\", \\\"Option wählen\\\"]\", \"[\\\"varianten-waehlen\\\", \\\"Vorherigen Schritt anzeigen\\\"]\"]": {
"abdruck": "e3270a7176c63620",
"gleich": false
},
"[\"[\\\"varianten-waehlen\\\", \\\"Schritt öffnen\\\"]\", \"[\\\"varianten-waehlen\\\", \\\"Vorherigen Schritt anzeigen\\\"]\"]": {
"abdruck": "44e775b8e8b106d1",
"gleich": false
},
"[\"[\\\"vorschau-zeigen\\\", \\\"Bild wählen\\\"]\", \"[\\\"vorschau-zeigen\\\", \\\"Galerie blättern\\\"]\"]": {
"abdruck": "19851d2901c309a1",
"gleich": false
},
"[\"[\\\"vorschau-zeigen\\\", \\\"Bild wählen\\\"]\", \"[\\\"vorschau-zeigen\\\", \\\"Vorschau laden\\\"]\"]": {
"abdruck": "508415e6500f9fca",
"gleich": false
},
"[\"[\\\"vorschau-zeigen\\\", \\\"Galerie blättern\\\"]\", \"[\\\"vorschau-zeigen\\\", \\\"Vorschau laden\\\"]\"]": {
"abdruck": "86aa1f412d5a5dcd",
"gleich": false
}
},
"stil": {
"[\"auswahl-anstossen\", \"\"]": {
"abdruck": "aeec25973dafe92f",
"probleme": []
},
"[\"auswahl-anstossen\", \"Gewählte Option anzeigen\"]": {
"abdruck": "59058982f7989af8",
"probleme": []
},
"[\"auswahl-anstossen\", \"Hinweistext anzeigen\"]": {
"abdruck": "b3568e0a2585d71c",
"probleme": []
},
"[\"auswahl-anstossen\", \"Schrittnummer anzeigen\"]": {
"abdruck": "117d58f0d4b21973",
"probleme": []
},
"[\"beratung-anbieten\", \"\"]": {
"abdruck": "4b0267b165360857",
"probleme": []
},
"[\"beratung-anbieten\", \"Beratung erreichen\"]": {
"abdruck": "cfdb65d682c3e068",
"probleme": []
},
"[\"beratung-anbieten\", \"Optionsübersicht öffnen\"]": {
"abdruck": "28d5587c78199c54",
"probleme": []
},
"[\"beratung-anbieten\", \"Vertrauen zeigen\"]": {
"abdruck": "8c00dec8d3ebf751",
"probleme": []
},
"[\"layout-zuweisen\", \"\"]": {
"abdruck": "1e0d2ca5296aabea",
"probleme": []
},
"[\"layout-zuweisen\", \"Konfigurator aktivieren\"]": {
"abdruck": "dca9bbf7361f5520",
"probleme": []
},
"[\"layout-zuweisen\", \"Optionsbild am Datensatz pflegen\"]": {
"abdruck": "cc0701aafd02ac3a",
"probleme": []
},
"[\"layout-zuweisen\", \"Optionsübersicht hinterlegen\"]": {
"abdruck": "de6c0e3e80d47496",
"probleme": []
},
"[\"layout-zuweisen\", \"Trust-Logo hinterlegen\"]": {
"abdruck": "ebd1a72ece157e69",
"probleme": []
},
"[\"option-ansehen\", \"\"]": {
"abdruck": "ace46c77101e26b3",
"probleme": []
},
"[\"option-ansehen\", \"Ansicht schließen\"]": {
"abdruck": "6bdaf5e60fb3baa9",
"probleme": []
},
"[\"option-ansehen\", \"Option aus Ansicht wählen\"]": {
"abdruck": "cc10af5dda4a7857",
"probleme": []
},
"[\"option-ansehen\", \"Optionsdetail öffnen\"]": {
"abdruck": "e00d07ce7b75d8fc",
"probleme": []
},
"[\"schritt-zeigen\", \"\"]": {
"abdruck": "f0d53ef421968759",
"probleme": []
},
"[\"schritt-zeigen\", \"Eigenschaft benennen\"]": {
"abdruck": "ec4828e2e9197083",
"probleme": []
},
"[\"schritt-zeigen\", \"Optionen auflisten\"]": {
"abdruck": "d8e6daed0f36098f",
"probleme": []
},
"[\"schritt-zeigen\", \"Schrittstand anzeigen\"]": {
"abdruck": "42a1c7f94c30f72d",
"probleme": []
},
"[\"varianten-waehlen\", \"\"]": {
"abdruck": "0835908894fa892c",
"probleme": []
},
"[\"varianten-waehlen\", \"Auswahl übernehmen\"]": {
"abdruck": "6683956c63370244",
"probleme": []
},
"[\"varianten-waehlen\", \"Konfigurationsdaten bereitstellen\"]": {
"abdruck": "498f3141e86f8db0",
"probleme": []
},
"[\"varianten-waehlen\", \"Konfigurator schließen\"]": {
"abdruck": "866918b327061449",
"probleme": []
},
"[\"varianten-waehlen\", \"Nächsten Schritt anzeigen\"]": {
"abdruck": "8d810a760dfec037",
"probleme": []
},
"[\"varianten-waehlen\", \"Option wählen\"]": {
"abdruck": "ac4eae399ac3a15e",
"probleme": []
},
"[\"varianten-waehlen\", \"Schritt öffnen\"]": {
"abdruck": "40f795456747214e",
"probleme": []
},
"[\"varianten-waehlen\", \"Vorherigen Schritt anzeigen\"]": {
"abdruck": "a63a26a8baea9c07",
"probleme": []
},
"[\"vorschau-zeigen\", \"\"]": {
"abdruck": "52976631fd09993a",
"probleme": []
},
"[\"vorschau-zeigen\", \"Bild wählen\"]": {
"abdruck": "a3e980ef7678a446",
"probleme": []
},
"[\"vorschau-zeigen\", \"Galerie blättern\"]": {
"abdruck": "47ccb441120c1470",
"probleme": []
},
"[\"vorschau-zeigen\", \"Vorschau laden\"]": {
"abdruck": "29e5951f170dba26",
"probleme": []
}
},
"verworfen": {}
}

View File

@@ -0,0 +1,49 @@
<!-- titel -->
Auswahl anstoßen
----------------------------------------
<!-- art -->
kern
----------------------------------------
<!-- ziel -->
Der Kunde erkennt in der Buybox die Reihenfolge der Schritte
----------------------------------------
<!-- regeln -->
- Jede Eigenschaft bildet einen eigenen Schritt
- Natives Auswahlfeld bleibt für den Variantenwechsel erhalten
- Eigene Darstellung gilt unabhängig vom Anzeigetyp der Eigenschaft
- Ohne aktives Layout erscheint die Standard-Buybox
----------------------------------------
<!-- funktion -->
Schrittnummer anzeigen
<!-- ziel -->
Zeigt neben jeder Eigenschaft ihre Position im Ablauf
<!-- regeln -->
- Beschriftung nennt das Wort Schritt und die Nummer
- Nummer folgt der Reihenfolge aus dem Backend
<!-- testen -->
- Produktseite öffnen
- Erste Eigenschaft zeigt Schritt 1
----------------------------------------
<!-- funktion -->
Hinweistext anzeigen
<!-- ziel -->
Erklärt dem Kunden, worauf er im Schritt achtet
<!-- regeln -->
- Text kommt aus einem Textbaustein je Schritt
- Leerer Textbaustein blendet den Hinweis aus
- Hinweis hängt an der Nummer statt an der Eigenschaft
<!-- testen -->
- Textbaustein für Schritt 1 füllen
- Produktseite öffnen
- Hinweis steht unter der Überschrift
----------------------------------------
<!-- funktion -->
Gewählte Option anzeigen
<!-- ziel -->
Zeigt am Schritt, welche Option gerade gilt
<!-- regeln -->
- Ohne Auswahl steht dort Bitte wählen
- Vorbelegung erscheint ohne Zutun des Kunden
<!-- testen -->
- Hauptartikel ohne Variante öffnen
- Jeder Schritt nennt eine Option

View File

@@ -0,0 +1,47 @@
<!-- titel -->
Beratung anbieten
----------------------------------------
<!-- art -->
optional
----------------------------------------
<!-- ziel -->
Der Kunde findet im Konfigurator Vertrauen und einen Kontakt
----------------------------------------
<!-- regeln -->
- Alle Texte kommen aus Textbausteinen
- Leerer Textbaustein blendet den Bereich aus
----------------------------------------
<!-- funktion -->
Vertrauen zeigen
<!-- ziel -->
Zeigt Siegel und Zusage neben dem Gesamtpreis
<!-- regeln -->
- Logo stammt aus der Plugin-Konfiguration
- Ohne Logo bleibt der Text stehen
<!-- testen -->
- Konfigurator öffnen
- Siegel steht neben dem Preis
----------------------------------------
<!-- funktion -->
Beratung erreichen
<!-- ziel -->
Bietet eine Telefonnummer für Fragen zur Konfiguration
<!-- regeln -->
- Nummer ist als Telefonlink klickbar
- Leere Nummer blendet den Bereich aus
<!-- testen -->
- Konfigurator öffnen
- LMB auf die Nummer
- Telefon-App öffnet sich
----------------------------------------
<!-- funktion -->
Optionsübersicht öffnen
<!-- ziel -->
Öffnet das hinterlegte PDF mit allen Optionen
<!-- regeln -->
- Knopf erscheint nur mit hinterlegtem PDF
- PDF öffnet in einem neuen Tab
<!-- testen -->
- Konfigurator öffnen
- LMB auf Optionen-Knopf
- PDF öffnet in neuem Tab

View File

@@ -0,0 +1,69 @@
<!-- titel -->
Layout zuweisen
----------------------------------------
<!-- art -->
kern
----------------------------------------
<!-- ziel -->
Ein Händler entscheidet je Produkt, ob die geführte Auswahl erscheint
----------------------------------------
<!-- regeln -->
- Einstellungen liegen auf dem Varianten-Tab des Produkts
- Werte stehen in den customFields des Produkts
- Bearbeiten erfordert das Recht product.editor
- Ohne Zuweisung bleibt das Standardlayout aktiv
----------------------------------------
<!-- funktion -->
Konfigurator aktivieren
<!-- ziel -->
Schaltet die geführte Variantenauswahl für ein Produkt frei
<!-- regeln -->
- Schalter steht auf dem Varianten-Tab
- Aktiv ersetzt die Buybox des Produkts
- Inaktiv zeigt das Standardlayout von Shopware
<!-- testen -->
- Produkt im Admin öffnen
- Varianten-Tab öffnen
- Schalter aktivieren und speichern
- Buybox zeigt die geführte Auswahl
----------------------------------------
<!-- funktion -->
Optionsübersicht hinterlegen
<!-- ziel -->
Hinterlegt ein PDF mit allen Varianten und Kombinationen
<!-- regeln -->
- Medienfeld steht auf dem Varianten-Tab
- Feld hängt am Hauptartikel
- Ohne PDF bleibt der Knopf unsichtbar
<!-- testen -->
- Varianten-Tab öffnen
- PDF im Medienfeld wählen
- Produkt speichern
- Modal zeigt den Knopf für Optionen
----------------------------------------
<!-- funktion -->
Trust-Logo hinterlegen
<!-- ziel -->
Zeigt ein Vertrauenssiegel neben dem Gesamtpreis
<!-- regeln -->
- Logo steht in der Plugin-Konfiguration
- Gilt für den ganzen Verkaufskanal
- Ohne Logo erscheint nur der Text
<!-- testen -->
- Plugin-Konfiguration öffnen
- Logo im Medienfeld wählen
- Modal zeigt das Logo am Preis
----------------------------------------
<!-- funktion -->
Optionsbild am Datensatz pflegen
<!-- stand -->
offen
<!-- ziel -->
Hinterlegt das Bild einer Option am Datensatz der Variante
<!-- regeln -->
- Feld liegt im Formular der einzelnen Variante
- Heute stammt das Bild aus der Eigenschaftsgruppe
<!-- testen -->
- Variante im Admin öffnen
- Bildfeld der Variante füllen
- Variante speichern

View File

@@ -0,0 +1,51 @@
<!-- titel -->
Option ansehen
----------------------------------------
<!-- art -->
kern
----------------------------------------
<!-- ziel -->
Der Kunde prüft eine Option groß, bevor er sie nimmt
----------------------------------------
<!-- regeln -->
- Nur Optionen mit Bild tragen den Hinweis
- Ansicht liegt über dem Konfigurator
----------------------------------------
<!-- funktion -->
Optionsdetail öffnen
<!-- ziel -->
Zeigt Bild und Namen einer Option in voller Größe
<!-- regeln -->
- i-Icon öffnet die Ansicht
- Enter öffnet die Ansicht
- Leertaste öffnet die Ansicht
<!-- testen -->
- Konfigurator öffnen
- LMB auf i-Icon einer Option
- Große Ansicht erscheint
----------------------------------------
<!-- funktion -->
Option aus Ansicht wählen
<!-- ziel -->
Übernimmt die betrachtete Option in die Auswahl
<!-- regeln -->
- Knopf übernimmt die Option
- Knopf schließt die Ansicht
- Schritt bleibt derselbe
<!-- testen -->
- Optionsdetail öffnen
- LMB auf Wählen-Knopf
- Option ist gewählt
----------------------------------------
<!-- funktion -->
Ansicht schließen
<!-- ziel -->
Kehrt zur Optionsliste zurück ohne Auswahl
<!-- regeln -->
- x-Icon schließt die Ansicht
- Escape schließt die Ansicht
- Konfigurator bleibt offen
<!-- testen -->
- Optionsdetail öffnen
- Escape drücken
- Optionsliste erscheint wieder

View File

@@ -0,0 +1,49 @@
<!-- titel -->
Schritt zeigen
----------------------------------------
<!-- art -->
kern
----------------------------------------
<!-- ziel -->
Der Kunde weiß im Modal, wo er steht und was er wählt
----------------------------------------
<!-- regeln -->
- Optionen stehen rechts
- Bilder links
- Lange Optionslisten lassen sich scrollen
----------------------------------------
<!-- funktion -->
Eigenschaft benennen
<!-- ziel -->
Zeigt den Namen der Eigenschaft des aktuellen Schritts
<!-- regeln -->
- Name steht als Überschrift über der Liste
- Name kommt aus der Eigenschaftsgruppe
<!-- testen -->
- Konfigurator öffnen
- Überschrift nennt die erste Eigenschaft
----------------------------------------
<!-- funktion -->
Schrittstand anzeigen
<!-- ziel -->
Zeigt den aktuellen Schritt und die Gesamtzahl
<!-- regeln -->
- Zähler nennt Schritt und Gesamtzahl
- Balken wächst mit dem Fortschritt
- Text kommt aus einem Textbaustein
<!-- testen -->
- Konfigurator öffnen
- Zähler zeigt 1 von 3
----------------------------------------
<!-- funktion -->
Optionen auflisten
<!-- ziel -->
Zeigt alle Optionen der Eigenschaft mit Namen und Bild
<!-- regeln -->
- Jede Option nennt ihren Namen
- Bild erscheint nur bis zwei Schritte
- Ohne Bild erscheint der Farbwert
- Ohne Farbwert bleibt die Zeile ohne Zeichen
<!-- testen -->
- Konfigurator öffnen
- Jede Option zeigt ihren Namen

View File

@@ -0,0 +1,109 @@
<!-- titel -->
Varianten wählen
----------------------------------------
<!-- art -->
kern
----------------------------------------
<!-- ziel -->
Der Kunde findet Schritt für Schritt eine bestellbare Kombination
----------------------------------------
<!-- regeln -->
- Erster Schritt zeigt alle Optionen seiner Eigenschaft
- Folgeschritte zeigen nur kombinierbare Optionen
- Der Ablauf trägt beliebig viele Eigenschaften
- Reihenfolge der Schritte kommt aus dem Backend
- Ohne Auswahl greifen die Standardvarianten des Elternartikels
- Schritte ohne gültige Option werden übersprungen
- Eine Auswahl wechselt den Schritt nicht
- Ungültige Folgeauswahl fällt beim Wechsel weg
- Auf kleinen Bildschirmen bleibt der Ablauf derselbe
- Escape schließt den Konfigurator
- Tabulator bleibt im Dialog
----------------------------------------
<!-- funktion -->
Schritt öffnen
<!-- ziel -->
Öffnet den Konfigurator beim angeklickten Schritt
<!-- regeln -->
- LMB auf einen Schritt öffnet das Modal
- Offener Konfigurator wechselt nur den Schritt
- Modal hängt am Seitenkörper
<!-- testen -->
- Produktseite öffnen
- LMB auf zweiten Schritt
- Modal öffnet bei Schritt 2
----------------------------------------
<!-- funktion -->
Option wählen
<!-- ziel -->
Übernimmt eine Option in die laufende Auswahl
<!-- regeln -->
- Gewählte Option ist hervorgehoben
- Nicht kombinierbare Optionen sind gesperrt
- Auswahl verwirft ungültige Folgeschritte
- Schritt bleibt nach der Wahl derselbe
<!-- testen -->
- Konfigurator öffnen
- LMB auf eine Option
- Option ist hervorgehoben
----------------------------------------
<!-- funktion -->
Nächsten Schritt anzeigen
<!-- ziel -->
Führt zum nächsten Schritt mit gültigen Optionen
<!-- regeln -->
- Weiter-Knopf heißt am letzten Schritt Fertig
- Leere Schritte werden übersprungen
<!-- testen -->
- Konfigurator öffnen
- LMB auf Weiter-Knopf
- Nächster Schritt erscheint
----------------------------------------
<!-- funktion -->
Vorherigen Schritt anzeigen
<!-- ziel -->
Führt zurück zum letzten Schritt mit Optionen
<!-- regeln -->
- Zurück-Knopf ist am ersten Schritt gesperrt
- Leere Schritte werden übersprungen
<!-- testen -->
- Zum zweiten Schritt gehen
- LMB auf Zurück-Knopf
- Erster Schritt erscheint
----------------------------------------
<!-- funktion -->
Auswahl übernehmen
<!-- ziel -->
Wechselt das Produkt auf die gewählte Variante
<!-- regeln -->
- Fertig überträgt die Auswahl in die Auswahlfelder
- Unbesuchte Schritte bekommen die erste gültige Option
- Shopware führt den Variantenwechsel aus
<!-- testen -->
- Alle Schritte durchgehen
- LMB auf Fertig-Knopf
- Produktseite zeigt die gewählte Variante
----------------------------------------
<!-- funktion -->
Konfigurator schließen
<!-- ziel -->
Beendet die Auswahl ohne Wechsel der Variante
<!-- regeln -->
- x-Icon schließt das Modal
- Escape schließt das Modal
- Fokus kehrt zum Auslöser zurück
<!-- testen -->
- Konfigurator öffnen
- Escape drücken
- Modal ist geschlossen
----------------------------------------
<!-- funktion -->
Konfigurationsdaten bereitstellen
<!-- ziel -->
Legt der Produktseite alles bei, was der Konfigurator braucht
<!-- regeln -->
- Kombinationen stammen vom Hauptartikel
- Ohne aktives Layout bleibt die Beilage aus
<!-- testen -->
- Produktseite mit aktivem Layout öffnen
- Seite trägt Kombinationen und Vorbelegung

View File

@@ -0,0 +1,53 @@
<!-- titel -->
Vorschau zeigen
----------------------------------------
<!-- art -->
kern
----------------------------------------
<!-- ziel -->
Der Kunde sieht Bild und Preis der gewählten Kombination
----------------------------------------
<!-- regeln -->
- Preis stimmt mit dem Preis der Produktseite überein
- Antwort wird nicht zwischengespeichert
- Späte Antworten überholter Anfragen werden verworfen
- Ohne eigene Bilder greifen die Bilder des Elternartikels
----------------------------------------
<!-- funktion -->
Vorschau laden
<!-- ziel -->
Holt Bilder und Preis zur laufenden Auswahl
<!-- regeln -->
- Jede Auswahl löst eine neue Anfrage aus
- Titelbild der Variante steht an erster Stelle
- Ohne Treffer bleibt die Produkt-ID als Rückfall
<!-- testen -->
- Konfigurator öffnen
- Andere Option wählen
- Bild und Preis ändern sich
----------------------------------------
<!-- funktion -->
Galerie blättern
<!-- ziel -->
Zeigt weitere Bilder der gewählten Variante
<!-- regeln -->
- Pfeil-Icons blättern vor
- Pfeil-Icons blättern zurück
- Nach dem letzten Bild folgt das erste
- Bei einem Bild bleibt die Navigation aus
<!-- testen -->
- Konfigurator öffnen
- LMB auf Vor-Icon
- Nächstes Bild erscheint
----------------------------------------
<!-- funktion -->
Bild wählen
<!-- ziel -->
Wechselt das große Bild über die Vorschaubilder
<!-- regeln -->
- Aktives Vorschaubild ist hervorgehoben
- Großes Bild folgt der Wahl
<!-- testen -->
- Konfigurator öffnen
- LMB auf drittes Vorschaubild
- Großes Bild zeigt dieses Motiv

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,53 @@
<!-- titel -->
Ansicht
----------------------------------------
<!-- art -->
kern
----------------------------------------
<!-- ziel -->
Umschalten zwischen Ansichtsmodi und Farbschema der Anwendung
----------------------------------------
<!-- regeln -->
- Systemänderungen wirken sofort im Auto-Modus
- Kompaktmodus zeigt Zusammenfassung
- Vollmodus zeigt alle Details
- Panel schließen entfernt es aus dem DOM
- Auge-Icon nur im Vollmodus
- MMB in beiden Modi wechselt die Ansicht
----------------------------------------
<!-- funktion -->
Ansicht wechseln
<!-- testen -->
- MMB auf Karte drücken
- Kompaktmodus wechselt zu Vollmodus
----------------------------------------
<!-- funktion -->
Darkmode umschalten
<!-- ziel -->
Passt die Farbgebung der App an die bevorzugte Umgebung an
<!-- regeln -->
- Zyklus: Auto → Hell → Dunkel → Auto
- Auto folgt der Systemeinstellung
- Auswahl bleibt nach Neuladen erhalten
<!-- testen -->
- Klick auf Darkmode-Icon
- Modus wechselt zum nächsten im Zyklus
- Seite zeigt den gewählten Modus
----------------------------------------
<!-- funktion -->
Panel schließen
<!-- regeln -->
- schliesse entfernt das Panel aus dem DOM
<!-- testen -->
- LMB auf ✕-Icon
- Panel nicht mehr sichtbar
----------------------------------------
<!-- funktion -->
Systemfarben nutzen
<!-- regeln -->
- Nur wirksam wenn Modus auf Auto steht
- Systemänderungen werden sofort übernommen
<!-- testen -->
- Auto-Modus wählen
- Systemfarbschema wechseln
- App-Oberfläche passt sich an

83
daten/PLANER2GEN/chat.md Normal file
View File

@@ -0,0 +1,83 @@
<!-- titel -->
Chat
----------------------------------------
<!-- art -->
kern
----------------------------------------
<!-- ziel -->
Beschreibt ein Projekt im Gespräch und erhält Karten als Vorschlag zurück
----------------------------------------
<!-- regeln -->
- Unbekanntes Projekt gibt 404
- x-Icon schließt Chat
- Prüfung wird geschlossen falls offen
- Chat verwerfen nur bei vorhandenem Verlauf
- Chat verwerfen ohne Backend-Aufruf
- body enthält Pflichtfeld text
----------------------------------------
<!-- funktion -->
Chat leeren
<!-- ziel -->
Entfernt alle Nachrichten aus dem Chatverlauf
<!-- regeln -->
- 404 bei unbekanntem Projekt
- chat.leeren aufgerufen
<!-- testen -->
- Chat öffnen
- Nachricht senden
- Löschanfrage an Chat senden
- Antwort {"ok": true}
- Chatverlauf leer
----------------------------------------
<!-- funktion -->
Chat schließen
<!-- ziel -->
Schließt das Chat-Panel und kehrt zur Projektansicht zurück
<!-- regeln -->
- x-Icon
- Schließen ist immer möglich
<!-- testen -->
- LMB auf x-Icon
- Panel entfernt
----------------------------------------
<!-- funktion -->
Chat öffnen
<!-- ziel -->
Tauscht die rechte Spalte zwischen Chat und Prüfung aus
<!-- regeln -->
- Schließt Prüfung, falls offen
<!-- testen -->
- Prüfung öffnen
- LMB auf Chat-Button
- ChatPanel sichtbar, PrüfPanel geschlossen
----------------------------------------
<!-- funktion -->
Chat-Nachricht senden
<!-- ziel -->
Holt eine Antwort des KI-Agenten zum aktuellen Projekt
<!-- regeln -->
- body enthält Pflichtfeld text
- Leere Nachricht wird abgelehnt
- Projekt nicht gefunden gibt 404
- Agent nicht konfiguriert gibt 400
- Antwort enthält gesamten Verlauf
<!-- testen -->
- POST an Chat-Endpunkt mit Text Frage
- Antwort mit Verlauf enthält Nachricht und Antwort
- POST mit leerem Text gibt 400
----------------------------------------
<!-- funktion -->
Nachricht senden
<!-- ziel -->
Schickt eine Nachricht und zeigt die Antwort der KI
<!-- regeln -->
- Leere Nachrichten nicht senden
- Anfragen werden nacheinander verarbeitet
- Eigene Nachricht erscheint sofort vor der Antwort
- Fehlgeschlagene Nachrichten werden wiederhergestellt
<!-- testen -->
- Text ins Eingabefeld eingeben
- Enter drücken
- Eigene Nachricht erscheint im Verlauf
- Ladeanzeige erscheint
- Antwort der KI erscheint im Verlauf

View File

@@ -0,0 +1,260 @@
<!-- titel -->
Features
----------------------------------------
<!-- art -->
kern
----------------------------------------
<!-- ziel -->
Verwaltet Features und Projekte einer Software, einschließlich Bearbeitung, Suche und Überprüfung
----------------------------------------
<!-- regeln -->
- Seitenleiste zeigt alle Projekte
- Features stehen in Spalten unter dem aktiven Projekt
- MMB oder Augen-Icon wechselt Kompakt- und Vollmodus
- Fehlerhafte Karte öffnet sich im Vollmodus
- Suchergebnis blendet nicht passende Karten aus
- Vorgeschlagene Karte als Umriss dargestellt
- Feature verschieben nur bei mehreren Projekten
- Kontextmenü der Karte nur im Vollmodus
- Problemanzahl als Badge auf der Karte
- Sammelaktion ab drei vorgeschlagenen Karten
- Stand wechselt zwischen offen und gebaut
- Funktionen sind über Kontextmenü nach oben und unten verschiebbar
- Einzelne Felder lassen sich zu einer Funktion hinzufügen
- Verschieben unbekannter Features wirft 404
----------------------------------------
<!-- funktion -->
Badge umschalten
<!-- ziel -->
Markiert ein Feature als kern oder optional
<!-- regeln -->
- Toggelt zwischen kern und optional
- Nur im Vollmodus verfügbar
<!-- testen -->
- Vollmodus öffnen
- LMB auf Badge
- Badge zeigt optional statt kern
----------------------------------------
<!-- funktion -->
Feature anlegen
<!-- ziel -->
Legt ein neues Feature im aktuellen Projekt an
<!-- regeln -->
- Titel ist Pflichtfeld
- Erstellt leere Feature-Datei mit Titel
- Neue Karte öffnet im Vollmodus
- Alphabetisch einsortiert
<!-- testen -->
- RMB auf den Bereich
- Klick auf "+ Feature erstellen"
- Titel eingeben und bestätigen
- Karte im Vollmodus sichtbar
----------------------------------------
<!-- funktion -->
Feature bearbeiten
<!-- ziel -->
Ändert Titel, Ziel, Regeln, Funktion oder Testschritte direkt in der Karte
<!-- regeln -->
- Titel-Klick öffnet ein Eingabefeld
- Nur im Vollmodus verfügbar
- Escape verwirft die Änderung
- Enter oder Klick außerhalb speichert
- Leerer Titel wird verworfen
<!-- testen -->
- Karte im Vollmodus öffnen
- Klick auf Titel
- Titel-Feld ist fokussiert und markiert
- Text ändern und Enter drücken
- Titel aktualisiert
----------------------------------------
<!-- funktion -->
Feature löschen
<!-- ziel -->
Entfernt ein Feature dauerhaft aus dem Projekt
<!-- regeln -->
- Wird über das Kontextmenü der Karte aufgerufen
- Erfragt eine Bestätigung vor dem Entfernen
- Löst das Entfernen über ein Event aus
<!-- testen -->
- RMB auf die Feature-Karte
- Klick auf "Feature entfernen"
- Bestätigung im Dialog
- Feature entfernt
----------------------------------------
<!-- funktion -->
Feature sichern
<!-- ziel -->
Speichert Änderungen an einer Feature-Karte dauerhaft
<!-- regeln -->
- Akzeptiert rohen Kartentext als Text
- Gibt 404 bei unbekanntem Projekt
- Gibt 400 bei ungültigem Kartentext
<!-- testen -->
- PUT api.projekte.name.features.id mit body {"roh": "Neuer Text"}
- Antwort 200 mit aktualisierter Karte
- Karte enthält neuen Text
----------------------------------------
<!-- funktion -->
Feature verschieben
<!-- ziel -->
Verschiebt ein Feature in ein anderes Projekt
<!-- regeln -->
- Nur möglich wenn mehrere Projekte existieren
- Zielprojekt muss existieren
- Feature bleibt unter neuer Projektzugehörigkeit erhalten
<!-- testen -->
- RMB auf Karte
- Klick auf "→ Feature verschieben"
- Zielprojekt auswählen
- Feature im Zielprojekt
----------------------------------------
<!-- funktion -->
Projekt auswählen
<!-- ziel -->
Zeigt die Features eines anderen Projekts an
<!-- regeln -->
- Aktives Projekt ist hervorgehoben
- Klick auf aktives Projekt bleibt aktiv
<!-- testen -->
- Klick auf Projekt in der Liste
- Projekt ist hervorgehoben
- Vorher aktives Projekt ist nicht mehr hervorgehoben
----------------------------------------
<!-- funktion -->
Projekt entfernen
<!-- ziel -->
Löscht ein nicht mehr benötigtes Projekt mitsamt aller seiner Features
<!-- regeln -->
- Fragt zur Bestätigung nach dem Projektnamen
- Löscht das Projekt und alle zugehörigen Features
- x-Icon bricht ab
- Entfernen-Button bestätigt die Löschung
<!-- testen -->
- RMB auf Projekt in der Seitenleiste
- Klick auf ✕ Projekt entfernen
- Klick auf Entfernen
- Projekt nicht mehr in der Liste
----------------------------------------
<!-- funktion -->
Projekt erstellen
<!-- ziel -->
Ermöglicht das Anlegen eines neuen Projekts über die Seitenleiste
<!-- regeln -->
- Projektname ist erforderlich
- Leerzeichen am Rand werden entfernt
- Projektname darf nicht existieren
- Erstelltes Projekt wird automatisch ausgewählt
<!-- testen -->
- Seitenleiste mit RMB öffnen
- Klick auf "+ Projekt"
- Projektname eingeben
- Klick auf "Erstellen"
- Modal geschlossen, Projekt ausgewählt
----------------------------------------
<!-- funktion -->
Projekt löschen
<!-- regeln -->
- 404 bei unbekanntem Projekt
- 400 bei ungültigem Wert
- Erfolg gibt {"ok": true} zurück
<!-- testen -->
- api-projekte name per GET aufrufen
- Antwort pruefen: 404
----------------------------------------
<!-- funktion -->
Projekt umbenennen
<!-- ziel -->
Ermöglicht das Umbenennen bestehender Projekte direkt in der Seitenleiste
<!-- regeln -->
- Umbenennung über Kontextmenü
- Umbenennung über Enter-Taste
- Escape bricht die Umbenennung ab
- Leere Eingabe verwirft die Änderung
- Gleicher Name wie zuvor verwirft die Änderung
- Projekt mit gleichem Namen erzeugt Fehler
<!-- testen -->
- RMB auf Projekt in der Seitenleiste
- Klick auf "Projekt anpassen"
- Neuen Namen eingeben
- Enter-Taste drücken
- Projektname in der Liste aktualisiert
----------------------------------------
<!-- funktion -->
Projekte anzeigen
<!-- testen -->
- Projekte-Endpunkt aufrufen
- Rückgabe prüfen
- Liste aller Projekte zurück
----------------------------------------
<!-- funktion -->
Projektfeatures anzeigen
<!-- ziel -->
Zeigt alle Features eines Projekts im Rohformat
<!-- regeln -->
- Rohformat enthält Karte und Funktionen
- Fehlendes Projekt wirft 404
<!-- testen -->
- GET api projekte MeinProjekt features
- Antwort enthält Liste der Features
- 404 bei unbekanntem Projekt
----------------------------------------
<!-- funktion -->
Projektmenü öffnen
<!-- ziel -->
Bietet Zugriff auf Anpassung, Quelle und Löschung einzelner Projekte
<!-- regeln -->
- RMB auf Projektname
- Drei Optionen verfügbar
<!-- testen -->
- RMB auf Projekt in Seitenleiste
- Menü erscheint mit drei Einträgen
- ✎ Projekt anpassen startet Umbenennung
- ◇ Quelle festlegen öffnet Modal zur Pfadwahl
- ✕ Projekt entfernen öffnet Bestätigungsdialog
----------------------------------------
<!-- funktion -->
Quelldaten laden
<!-- ziel -->
Legt fest, woher ein Projekt seine Daten bezieht
<!-- regeln -->
- GET gibt den aktuellen Pfad zurück
- PUT setzt einen neuen Pfad
- Leerer Pfad löscht die Zuordnung
<!-- testen -->
- PUT quelle mit projektname und pfad data input
- GET quelle mit projektname
- Rueckgabe enthaelt projektname und pfad data input
----------------------------------------
<!-- funktion -->
Vision entfernen
<!-- ziel -->
Ermöglicht das Löschen einer fehlerhaften oder überflüssigen Vision
<!-- regeln -->
- x-Icon
- RMB auf Karte öffnet Menü
- Klick außerhalb schließt Menü
<!-- testen -->
- RMB auf Vision-Karte
- Klick auf "Vision entfernen"
- Vision-Karte entfernt
----------------------------------------
<!-- funktion -->
Vision erstellen
<!-- ziel -->
Erstellt eine einzelne Vision-Karte für das Projekt
<!-- regeln -->
- Nur eine Vision je Projekt
- Vision steht über den Features
- Karte vom Typ vision
<!-- testen -->
- RMB auf den Bereich
- Klick auf + Vision erstellen
- Vision-Karte sichtbar
----------------------------------------
<!-- funktion -->
Doppeltes Projekt verhindern
<!-- testen -->
- Projekt mit Namen erstellen
- Projekt mit gleichem Namen erstellen
- Meldung erscheint
- Projekt nicht erstellt

View File

@@ -0,0 +1,102 @@
<!-- titel -->
Funktionen
----------------------------------------
<!-- art -->
kern
----------------------------------------
<!-- ziel -->
Beschreibt einzelne Schritte einer Feature-Karte
----------------------------------------
<!-- regeln -->
- Leere Titel werden verworfen
- Bearbeiten nur im Vollmodus
- Stand wechselt zwischen offen und gebaut
- Jede Änderung speichert sofort
- Kein Verschieben bei erster oder letzter Funktion
- Kontextmenü schließt bei Klick außerhalb
- Funktionsmenü in Kompaktmodus öffnet Kartenmenü
- Ziel, Regeln und Testen ohne Bearbeitung im Kompaktmodus
----------------------------------------
<!-- funktion -->
Funktion anlegen
<!-- ziel -->
Beschreibt eine neue Funktion der Karte
<!-- regeln -->
- Funktion enthält Ziel, Regeln und Testen
- Neue Funktion beginnt mit Stand gebaut
<!-- testen -->
- RMB auf Feature-Karte
- Klick auf "+ Funktion erstellen"
- Titel im Modal eingeben
- Klick auf "Erstellen"
- Funktion erscheint in der Liste
----------------------------------------
<!-- funktion -->
Funktion löschen
<!-- ziel -->
Entfernt eine nicht mehr benötigte Funktion aus der Liste
<!-- regeln -->
- Nur im Vollmodus verfügbar
- x-Icon im Kontextmenü
- Modale Rückfrage vor dem Entfernen
- Speichert die Änderung
<!-- testen -->
- Vollmodus öffnen
- RMB auf Funktion
- x-Icon betätigen
- Funktion entfernt
----------------------------------------
<!-- funktion -->
Funktion nach oben bewegen
<!-- ziel -->
Ändert die Reihenfolge der Funktionen
<!-- regeln -->
- Nur verfügbar bei nicht erster Funktion
- Vertauscht die Funktionsposition mit der darüberliegenden
- Speichert automatisch
<!-- testen -->
- RMB auf Funktion
- ▲ Nach oben wählen
- Funktion eine Position höher
----------------------------------------
<!-- funktion -->
Funktionsstand wechseln
<!-- ziel -->
Markiert eine Funktion als bestellt oder gebaut
<!-- regeln -->
- Nur im Vollmodus verfügbar
- @click.stop verhindert das Kartenmenü
- Toggle zwischen offen und gebaut
- Speichert sofort
<!-- testen -->
- Vollmodus öffnen
- LMB auf Stand-Button einer Funktion
- Stand wechselt von offen zu ✓ oder zurück
- Änderung gespeichert
----------------------------------------
<!-- funktion -->
Kontextmenü anzeigen
<!-- ziel -->
Bietet Zugriff auf kontextbezogene Aktionen einer Karte oder Funktion
<!-- regeln -->
- Kartenmenü im Kompaktmodus verfügbar
<!-- testen -->
- Vollmodus öffnen
- RMB auf Funktionszeile
- Menü mit Verschieben und Entfernen sichtbar
----------------------------------------
<!-- funktion -->
Kontextmenü verbergen
<!-- ziel -->
Schließt das Kontextmenü, wenn die Person woanders klickt oder Escape drückt
<!-- regeln -->
- Klick außerhalb des Menüs schließt es
- Klick auf Menüeintrag führt Aktion aus
- Escape-Taste schließt das Menü
<!-- testen -->
- Kontextmenü öffnen
- LMB außerhalb des Menüs klicken
- Menü geschlossen
- Kontextmenü erneut öffnen
- Escape-Taste drücken
- Menü geschlossen

View File

@@ -0,0 +1,92 @@
<!-- titel -->
Prüfung
----------------------------------------
<!-- art -->
kern
----------------------------------------
<!-- ziel -->
Prüft Feature-Karten auf formale Fehler und zeigt Stellen mit Problemen an
----------------------------------------
<!-- regeln -->
- Prüfung beenden gibt {"ok": true} zurück
- Fehlerstelle wird 1200ms lang hervorgehoben
- PruefPanel zeigt eine von vier Prüfarten: stil, feature, code, erzeugen
- Leerer Status ohne aktive Prüfung
- Eingabe bestätigen erfordert Bereitschaft des Systems
- Klick auf ✕ schließt das Panel
- Chat öffnen schließt das Prüfungspanel
----------------------------------------
<!-- funktion -->
Eingabe bestätigen
<!-- ziel -->
Ermöglicht schnelle Bestätigung der Eingabe per Tastatur
<!-- regeln -->
- Nur auslösbar wenn bereit true ist
- Emittiert bestaetigt-Event mit getrimmtem Wert
<!-- testen -->
- Eingabefeld fokussieren
- Text eingeben
- Enter drücken
- bestaetigt-Event mit Wert
----------------------------------------
<!-- funktion -->
Fehlerstelle anzeigen
<!-- ziel -->
Navigiert zur Feature-Karte mit dem Problem
<!-- regeln -->
- Scrollt zur Feature-Karte
- Markiert die Karte mit gesucht
- Highlight endet nach 1200ms
<!-- testen -->
- Prüfung mit Problemen öffnen
- LMB auf Problemeintrag
- Karte scrollt in die Ansicht und zeigt Highlight
----------------------------------------
<!-- funktion -->
Prüfung beenden
<!-- ziel -->
Bricht die laufende Prüfung ab, ohne Ergebnisse zu übernehmen
<!-- regeln -->
- Löschaufruf an Endpunkt
- Gibt ok true zurück
- FileNotFoundError als 404 weitergereicht
<!-- testen -->
- Prüfung im Projekt starten
- Warten auf Status running
- Löschaufruf an Prüfungsendpunkt senden
- Prüfung beendet
----------------------------------------
<!-- funktion -->
Prüfung starten
<!-- ziel -->
Prüft das Projekt auf Probleme und zeigt gefundene Stellen
<!-- regeln -->
- Vier Prüfarten verfügbar: erzeugen, stil, feature, code
- Progress in Prozent während der Prüfung
- PruefPanel zeigt Ergebnisse nach Abschluss
<!-- testen -->
- Prüfung starten mit Art "stil"
- Fortschritt in Prozent sichtbar
- PrüfPanel zeigt Ergebnisse
----------------------------------------
<!-- funktion -->
Prüfungsstatus anzeigen
<!-- ziel -->
Zeigt den aktuellen Zustand einer laufenden Prüfung
<!-- regeln -->
- Ohne aktive Prüfung wird ein leerer Status zurückgegeben
<!-- testen -->
- GET-Anfrage an Prüfungsendpunkt senden
- Status der Prüfung im Response
----------------------------------------
<!-- funktion -->
Systemstatus prüfen
<!-- ziel -->
Prüft ob der Backend-Server erreichbar ist
<!-- regeln -->
- GET api health
- Antwortet immer 200 mit {"ok": true}
<!-- testen -->
- GET api health
- Statuscode 200
- {"ok": true} im Body

View File

@@ -0,0 +1,62 @@
<!-- titel -->
Verlauf
----------------------------------------
<!-- art -->
kern
----------------------------------------
<!-- ziel -->
Verfolgt Änderungen und macht sie rückgängig
----------------------------------------
<!-- regeln -->
- ↺-Icon zeigt letzten Schritt als Tooltip
- Leerer Verlauf ist kein Fehlerzustand
- ↩-Icon ist ↺-Icon im Code
----------------------------------------
<!-- funktion -->
Aktion rückgängig machen
<!-- ziel -->
Nimmt den letzten Schritt im aktiven Projekt zurück
<!-- regeln -->
- ↺-Schaltfläche bei fehlendem letzterSchritt deaktiviert
- Strg+Z gilt nicht in Eingabefeldern
- Fehlgeschlagener Aufruf zeigt Meldung
<!-- testen -->
- Projekt mit Verlauf öffnen
- ↺-Schaltfläche anklicken
- Letzter Schritt aus Verlauf entfernt
- Meldung bleibt leer
----------------------------------------
<!-- funktion -->
Chat-Verlauf abrufen
<!-- ziel -->
Ermöglicht das Lesen vergangener Chat-Nachrichten eines Projekts
<!-- regeln -->
- Gibt den gesamten Verlauf als Liste zurück
- 404 bei unbekanntem Projekt
<!-- testen -->
- GET-Anfrage an Chat-Endpunkt senden
- Verlauf als JSON-Array mit Nachrichten
- Unbekanntes Projekt gibt 404
----------------------------------------
<!-- funktion -->
Projektverlauf anzeigen
<!-- ziel -->
Zeigt den letzten Stand im Projektverlauf
<!-- regeln -->
- 404 bei unbekanntem Projekt
- Feld "letzter" enthält letzten Verlaufseintrag
<!-- testen -->
- Endpunkt für Projektverlauf aufrufen
- Antwort mit Feld letzter prüfen
----------------------------------------
<!-- funktion -->
Änderung verwerfen
<!-- regeln -->
- LMB auf ↩-Icon verwirft den letzten Schritt
- Strg+Z außerhalb von Eingabefeldern verwirft
- Ohne vorherigen Schritt ist das Icon deaktiviert
- Nach dem Verwerfen aktualisiert sich die Feature-Liste
<!-- testen -->
- Projekt mit einer Änderung öffnen
- LMB auf ↩-Icon
- Änderung in der Liste zurückgesetzt

View File

@@ -0,0 +1,55 @@
<!-- titel -->
Vorschläge
----------------------------------------
<!-- art -->
kern
----------------------------------------
<!-- ziel -->
Zeigt Katalogprüfungsergebnisse und erlaubt Übernahme oder Verwerfung
----------------------------------------
<!-- regeln -->
- Fehlerhafte Karten öffnen sich im Vollmodus
- Nicht anwendbarer Vorschlag bleibt stehen
- Verwerfen entfernt das Problem dauerhaft
- Vor dem Übernehmen ist der Unterschied sichtbar
----------------------------------------
<!-- funktion -->
Alle Vorschläge annehmen
<!-- ziel -->
Nimmt alle Vorschläge auf einmal an statt einzeln
<!-- regeln -->
- Kartenentfernung hat Priorität
- Nicht anwendbarer Vorschlag wird übersprungen
<!-- testen -->
- Karte mit N Problemen öffnen
- LMB auf Übernehmen-Button
- Vorschläge angewendet
- Karte gespeichert
----------------------------------------
<!-- funktion -->
Vorschlag verwerfen
<!-- ziel -->
Entfernt einen Vorschlag aus der Anzeige, ohne ihn anzuwenden
<!-- regeln -->
- Entfernt das Problem aus der Liste
- Die Karte bleibt unverändert
<!-- testen -->
- Karte mit Vorschlag öffnen
- Klick auf ✕-Icon
- Vorschlag verschwindet, Karte ist unverändert
----------------------------------------
<!-- funktion -->
Vorschlag übernehmen
<!-- ziel -->
Nimmt einen Vorschlag als neue Feature-Karte ins Projekt auf
<!-- testen -->
- Feature mit Vorschlag öffnen
- Klick auf ✓-Icon
- Neue Karte im Gitter
----------------------------------------
<!-- funktion -->
Alle Vorschläge verwerfen
<!-- testen -->
- Vorschläge erstellen
- "Alle verwerfen" betätigen
- Alle Vorschläge verworfen

View File

@@ -0,0 +1,928 @@
{
"abdeckung": {
"bedienen:ablehnen": {
"abdruck": "e9cce25d92b81ff6",
"funktion": "Vorschlag annehmen",
"trug": "089e774256dcce89"
},
"bedienen:alleAblehnen": {
"abdruck": "ac9262c7cafe9537",
"funktion": "Alle Vorschläge verwerfen",
"trug": "6869d68fdbd6d0a7"
},
"bedienen:alleAnnehmen": {
"abdruck": "94f23cae75fdc531",
"funktion": "Alle Vorschläge annehmen",
"trug": "94ebeec7b03abb70"
},
"bedienen:annehmen": {
"abdruck": "2742ad031b3d3cca",
"funktion": "Vorschlag annehmen",
"trug": "089e774256dcce89"
},
"bedienen:auswaehlen": {
"abdruck": "d061bf62466085ce",
"funktion": "Projekt aufrufen",
"trug": "e24c2ab0a27ace24"
},
"bedienen:bereichsMenue": {
"abdruck": "4277536d43c7bcd5",
"funktion": "Projekt aufrufen",
"trug": "e24c2ab0a27ace24"
},
"bedienen:bestaetigen": {
"abdruck": "db1f954b9ad01749",
"funktion": "Eingabe mit Enter bestätigen",
"trug": "43fca3b0e83b3701"
},
"bedienen:erledige": {
"abdruck": "1b80ae13958bb11a",
"funktion": "Vorgeschlagene Karte anlegen",
"trug": "de2b733c447db595"
},
"bedienen:funktionsMenue": {
"abdruck": "bacdd0a7cc9d0f92",
"funktion": "Funktion verschieben",
"trug": "6a33d1b7b5a7f28c"
},
"bedienen:karteAnnehmen": {
"abdruck": "bf348251763e402a",
"funktion": "Vorgeschlagene Karte anlegen",
"trug": "de2b733c447db595"
},
"bedienen:kartenMenue": {
"abdruck": "d3ba136a562eb1e9",
"funktion": "Feature entfernen",
"trug": "f486bf09c64ebb3a"
},
"bedienen:leeren": {
"abdruck": "f2af432a5ae3b255",
"funktion": "Gespräch verwerfen",
"trug": "1f848fba8b2a5e56"
},
"bedienen:leistenMenue": {
"abdruck": "ed951fbeaaa9bc70",
"funktion": "Projekt anlegen",
"trug": "44b8a59e1dcb20d4"
},
"bedienen:oeffneChat": {
"abdruck": "9bf171761edca141",
"funktion": "Gespräch öffnen",
"trug": "c71610a339bb298e"
},
"bedienen:projektMenue": {
"abdruck": "ea73f8c2597c0443",
"funktion": "Projekt umbenennen",
"trug": "40d6954a44fa3240"
},
"bedienen:schliesse": {
"abdruck": "919b104c012c034d",
"funktion": "",
"trug": ""
},
"bedienen:schliessen": {
"abdruck": "1e12f9ae001e93e1",
"funktion": "Gespräch verwerfen",
"trug": "1f848fba8b2a5e56"
},
"bedienen:senden": {
"abdruck": "8df3402ed3373acc",
"funktion": "Nachricht senden",
"trug": "e61022551a98250f"
},
"bedienen:springe": {
"abdruck": "0b134efa50c03bff",
"funktion": "Problemstelle anspringen",
"trug": "7486526f6867c436"
},
"bedienen:starte": {
"abdruck": "761c2f62c917a3db",
"funktion": "Titel anpassen",
"trug": "d0b7a430e96e2599"
},
"bedienen:startePruefung": {
"abdruck": "31c3fef23c6319d8",
"funktion": "Gespräch öffnen",
"trug": "c71610a339bb298e"
},
"bedienen:umbenennen": {
"abdruck": "6185b6b39708bf02",
"funktion": "Projekt umbenennen",
"trug": "40d6954a44fa3240"
},
"bedienen:wechsleAnsicht": {
"abdruck": "e3193fce50eaa58e",
"funktion": "Anzeigemodus umschalten",
"trug": "fccea553715951de"
},
"bedienen:wechsleArt": {
"abdruck": "67250a1e8d82e960",
"funktion": "Feature einstufen",
"trug": "90b6c77ff0c412e6"
},
"bedienen:wechsleDarkMode": {
"abdruck": "b83524eb0e12a7d7",
"funktion": "Darkmode umschalten",
"trug": "4033fc55c753db3e"
},
"bedienen:wechsleStand": {
"abdruck": "8c2e3bf3be58ec00",
"funktion": "Stand umschalten",
"trug": "37a3835d1d2a3e6c"
},
"bedienen:zurueck": {
"abdruck": "e378014fbce96d10",
"funktion": "Schritt zurücknehmen",
"trug": "10e46c458040a5e3"
},
"hoeren:systemDunkel.change": {
"abdruck": "d3ef681b987557dc",
"funktion": "Darkmode umschalten",
"trug": "4033fc55c753db3e"
},
"hoeren:window.click": {
"abdruck": "620812522ed2d900",
"funktion": "Kontextmenü schließen",
"trug": "87216235bbe42a1a"
},
"hoeren:window.keydown": {
"abdruck": "c3e1df2d60fcc067",
"funktion": "Schritt zurücknehmen",
"trug": "10e46c458040a5e3"
},
"menue:+ Feature erstellen": {
"abdruck": "82c40072137d5757",
"funktion": "Feature anlegen",
"trug": "df7a1452a32da410"
},
"menue:+ Funktion erstellen": {
"abdruck": "b44457d64a5ec9d1",
"funktion": "Feature entfernen",
"trug": "f486bf09c64ebb3a"
},
"menue:+ Projekt erstellen": {
"abdruck": "2be770390433a2ce",
"funktion": "Projekt anlegen",
"trug": "44b8a59e1dcb20d4"
},
"menue:+ Vision erstellen": {
"abdruck": "cb22a654070e58b4",
"funktion": "Feature anlegen",
"trug": "df7a1452a32da410"
},
"menue:→ Feature verschieben": {
"abdruck": "38e6b244668ee164",
"funktion": "Feature entfernen",
"trug": "f486bf09c64ebb3a"
},
"menue:▲ Nach oben": {
"abdruck": "4e6fd1617d817357",
"funktion": "Funktion verschieben",
"trug": "6a33d1b7b5a7f28c"
},
"menue:▼ Nach unten": {
"abdruck": "183651450e546c4f",
"funktion": "Funktion entfernen",
"trug": "885e16a93460f706"
},
"menue:◇ Quelle festlegen": {
"abdruck": "e4bb1781bc375d6d",
"funktion": "Projekt umbenennen",
"trug": "40d6954a44fa3240"
},
"menue:✎ Projekt anpassen": {
"abdruck": "ad505addca45b06c",
"funktion": "Projekt umbenennen",
"trug": "40d6954a44fa3240"
},
"menue:✕ Feature entfernen": {
"abdruck": "bc968582260c45b9",
"funktion": "Funktion anlegen",
"trug": "78d0329cf6eacca9"
},
"menue:✕ Funktion entfernen": {
"abdruck": "d29458b150c55777",
"funktion": "Funktion verschieben",
"trug": "6a33d1b7b5a7f28c"
},
"menue:✕ Projekt entfernen": {
"abdruck": "aca25c0b5feaf752",
"funktion": "Projekt umbenennen",
"trug": "40d6954a44fa3240"
},
"menue:✕ Vision entfernen": {
"abdruck": "88520b1f105d3fb5",
"funktion": "Anzeigemodus umschalten",
"trug": "fccea553715951de"
},
"route:delete /projekte/{name}": {
"abdruck": "b4f871be35d10e5f",
"funktion": "Projekt entfernen",
"trug": "e91a89b8d78dbd2e"
},
"route:delete /projekte/{name}/features/{id}": {
"abdruck": "c1db81de98835c39",
"funktion": "Feature entfernen",
"trug": "f486bf09c64ebb3a"
},
"route:delete /projekte/{name}/pruefung": {
"abdruck": "7a480f33ddf55165",
"funktion": "Prüfung beenden",
"trug": "83435eb3e1394e49"
},
"route:get /health": {
"abdruck": "65257319741edf35",
"funktion": "",
"trug": ""
},
"route:get /projekte": {
"abdruck": "276556054437c303",
"funktion": "",
"trug": ""
},
"route:get /projekte/{name}/chat": {
"abdruck": "fd2a22c36ea85545",
"funktion": "Gespräch öffnen",
"trug": "c71610a339bb298e"
},
"route:get /projekte/{name}/features": {
"abdruck": "122f93cbd0d07881",
"funktion": "Projekt aufrufen",
"trug": "e24c2ab0a27ace24"
},
"route:get /projekte/{name}/pruefung": {
"abdruck": "e651ac90b7b2c520",
"funktion": "",
"trug": ""
},
"route:get /projekte/{name}/quelle": {
"abdruck": "834d3af3203ca3a8",
"funktion": "Quelle festlegen",
"trug": "b8b70dd0ff9817cb"
},
"route:get /projekte/{name}/verlauf": {
"abdruck": "5a3dcc678733fad8",
"funktion": "",
"trug": ""
},
"route:post /projekte": {
"abdruck": "fa1628d7e469885d",
"funktion": "Projekt anlegen",
"trug": "44b8a59e1dcb20d4"
},
"route:post /projekte/{name}/chat": {
"abdruck": "c3edc89cec5d2f6f",
"funktion": "Nachricht senden",
"trug": "e61022551a98250f"
},
"route:post /projekte/{name}/features": {
"abdruck": "00eb9c16560bac73",
"funktion": "Feature anlegen",
"trug": "df7a1452a32da410"
},
"route:post /projekte/{name}/pruefung": {
"abdruck": "6d7444a9dfad7d54",
"funktion": "Stil prüfen",
"trug": "d5019cdafd70efb7"
},
"route:post /projekte/{name}/pruefung/verworfen": {
"abdruck": "a43f7221e01c386f",
"funktion": "Alle Vorschläge verwerfen",
"trug": "6869d68fdbd6d0a7"
},
"route:post /projekte/{name}/verlauf/zurueck": {
"abdruck": "308fec6d21b01d60",
"funktion": "Schritt zurücknehmen",
"trug": "10e46c458040a5e3"
},
"route:put /projekte/{name}": {
"abdruck": "75383ac009961cad",
"funktion": "Projekt umbenennen",
"trug": "40d6954a44fa3240"
},
"route:put /projekte/{name}/features/{id}": {
"abdruck": "b24c7000fd2ec94c",
"funktion": "",
"trug": ""
},
"route:put /projekte/{name}/features/{id}/projekt": {
"abdruck": "fafc7a8a1656f93b",
"funktion": "Feature verschieben",
"trug": "cd2cd705298831e0"
},
"route:put /projekte/{name}/quelle": {
"abdruck": "d342055e3163548a",
"funktion": "Quelle festlegen",
"trug": "b8b70dd0ff9817cb"
}
},
"belege": {
"[\"darkmode\", \"Darkmode umschalten\"]": {
"abdruck": "653c08d0647a7f7b",
"beleg": "function wechsleDarkMode() {\n modus.value = MODI[(MODI.indexOf(modus.value) + 1) % MODI.length]\n localStorage.setItem('darkModus', modus.value)\n wendeAn()\n}",
"datei": "frontend/src/App.vue"
},
"[\"design\", \"Eingabe mit Enter bestätigen\"]": {
"abdruck": "3bace3e9c9f16023",
"beleg": "@keydown.enter.exact.prevent=\"senden\"",
"datei": "frontend/src/components/ChatPanel.vue"
},
"[\"design\", \"Kontextmenü schließen\"]": {
"abdruck": "671fa86bccb2c2c6",
"beleg": "export function schliesseMenue() {\n kontextMenue.value = null\n}",
"datei": "frontend/src/composables/kontextmenue.js"
},
"[\"features\", \"Anzeigemodus umschalten\"]": {
"abdruck": "b22ba2dd122887fa",
"beleg": "function wechsleAnsicht() {\n gross.value = !gross.value\n}",
"datei": "frontend/src/components/FeatureKarte.vue"
},
"[\"features\", \"Feature anlegen\"]": {
"abdruck": "d2968bea16a06b65",
"beleg": "async function erstellen(titel) {",
"datei": "frontend/src/components/FeatureListe.vue"
},
"[\"features\", \"Feature durchsuchen\"]": {
"abdruck": "4f249c42d46d25b5",
"beleg": "function passt(f) {\n const wort = suche.value.trim().toLowerCase()\n if (!wort) return true\n const s = f.sektionen\n const text = [s.titel, s.ziel, s.regeln,\n ...s.funktionen.map(x => `${x.text} ${x.ziel} ${x.regeln} ${x.testen}`)]\n return text.join(' ').toLowerCase().includes(wort)\n}",
"datei": "frontend/src/components/FeatureListe.vue"
},
"[\"features\", \"Feature einstufen\"]": {
"abdruck": "a0ee6590c24456bb",
"beleg": "function wechsleArt() {\n s.value.art = s.value.art === 'kern' ? 'optional' : 'kern'\n speichern()\n}",
"datei": "frontend/src/components/FeatureKarte.vue"
},
"[\"features\", \"Feature entfernen\"]": {
"abdruck": "c6f27381a41a63d7",
"beleg": "async function entfernen() {\n const id = modal.value.feature.id\n try {\n await featureLoeschen(props.projekt, id)\n features.value = features.value.filter(f => f.id !== id)\n modal.value = null\n emit('geaendert')\n } catch (e) {\n meldung.value = e.message\n }\n}",
"datei": "frontend/src/components/FeatureListe.vue"
},
"[\"features\", \"Feature verschieben\"]": {
"abdruck": "a82b97e3d4a1b533",
"beleg": "def verschieben(projekt: str, id: str, ziel: str) -> dict:",
"datei": "backend/features.py"
},
"[\"features\", \"Regeln anpassen\"]": {
"abdruck": "0af0e4eadcaae560",
"beleg": "<div v-else-if=\"s.regeln\" class=\"klickbar\" @click=\"starte('regeln')\">",
"datei": "frontend/src/components/FeatureKarte.vue"
},
"[\"features\", \"Titel anpassen\"]": {
"abdruck": "7f727f71dc555492",
"beleg": "<h3 v-else :title=\"gross ? 'Klicken zum Bearbeiten' : null\" @click=\"starte('titel')\">{{ s.titel }}</h3>",
"datei": "frontend/src/components/FeatureKarte.vue"
},
"[\"features\", \"Ziel anpassen\"]": {
"abdruck": "965b52779fa6aa35",
"beleg": "<div v-else-if=\"s.ziel\" class=\"inhalt klickbar\" v-html=\"md(s.ziel)\" @click=\"starte('ziel')\" />",
"datei": "frontend/src/components/FeatureKarte.vue"
},
"[\"funktionen\", \"Funktion anlegen\"]": {
"abdruck": "6250215b920cf0f0",
"beleg": "async function funktionErstellen(titel) {",
"datei": "frontend/src/components/FeatureKarte.vue"
},
"[\"funktionen\", \"Funktion entfernen\"]": {
"abdruck": "20e8d8e507ee6310",
"beleg": "eintraege.push({ text: '✕ Funktion entfernen', aktion: () => (modal.value = { art: 'entfernen', i }) })",
"datei": "frontend/src/components/FeatureKarte.vue"
},
"[\"funktionen\", \"Funktion verschieben\"]": {
"abdruck": "8435119f0dd5b891",
"beleg": "async function schiebe(i, richtung) {",
"datei": "frontend/src/components/FeatureKarte.vue"
},
"[\"funktionen\", \"Regeln anpassen\"]": {
"abdruck": "303d6988603e3d61",
"beleg": "async function starte(feld, i, j) {\n if (!gross.value) return // Bearbeiten nur in der ausführlichen Ansicht\n const original = wert(feld, i, j)\n aktiv.value = { feld, i, j, original }\n entwurf.value = original\n await nextTick()\n const el = Array.isArray(feldRef.value) ? feldRef.value[0] : feldRef.value\n el?.focus()\n if (feld === 'titel') el?.select()\n}",
"datei": "frontend/src/components/FeatureKarte.vue"
},
"[\"funktionen\", \"Stand umschalten\"]": {
"abdruck": "b66ea5f488899d8f",
"beleg": "@click.stop=\"wechsleStand(i)\"",
"datei": "frontend/src/components/FeatureKarte.vue"
},
"[\"funktionen\", \"Testschritte anpassen\"]": {
"abdruck": "e303ebeaece423de",
"beleg": "<div v-else class=\"klickbar\" @click=\"starte('testen', i)\">",
"datei": "frontend/src/components/FeatureKarte.vue"
},
"[\"funktionen\", \"Titel anpassen\"]": {
"abdruck": "3ceaa19290c793b4",
"beleg": "s.value.funktionen[a.i].text = neu",
"datei": "frontend/src/components/FeatureKarte.vue"
},
"[\"funktionen\", \"Ziel anpassen\"]": {
"abdruck": "4cb617031cbf6ef4",
"beleg": "<div v-else class=\"inhalt klickbar\" v-html=\"md(f.ziel)\" @click=\"starte('ziel', i)\" />",
"datei": "frontend/src/components/FeatureKarte.vue"
},
"[\"karten-besprechen\", \"Gespräch schließen\"]": {
"abdruck": "d6de2c46300fef26",
"beleg": "<button class=\"icon-knopf\" title=\"Gespräch schließen\" @click=\"emit('schliessen')\">✕</button>",
"datei": "frontend/src/components/ChatPanel.vue"
},
"[\"karten-besprechen\", \"Gespräch verwerfen\"]": {
"abdruck": "9e74be976a4d0e15",
"beleg": "@router.delete(\"/projekte/{name}/chat\")\nasync def chat_leeren(name: str):\n try:\n chat.leeren(name)\n except FileNotFoundError as e:\n raise HTTPException(404, str(e))\n return {\"ok\": True}",
"datei": "backend/routes.py"
},
"[\"karten-besprechen\", \"Gespräch öffnen\"]": {
"abdruck": "c84f9a14428a019e",
"beleg": "<button\n class=\"icon-knopf\"\n :class=\"{ an: chatOffen }\"\n title=\"Karten im Gespräch entwickeln\"\n @click=\"oeffneChat\"\n >✎</button>",
"datei": "frontend/src/App.vue"
},
"[\"karten-besprechen\", \"Nachricht senden\"]": {
"abdruck": "308218d682fe3dd9",
"beleg": "async def frage(projekt: str, text: str) -> dict:",
"datei": "backend/chat.py"
},
"[\"karten-besprechen\", \"Vorgeschlagene Karte anlegen\"]": {
"abdruck": "15aa862d2e0f6f6e",
"beleg": "async function karteAnnehmen(p) {",
"datei": "frontend/src/components/FeatureListe.vue"
},
"[\"karten-pruefen\", \"Alle Vorschläge annehmen\"]": {
"abdruck": "52fb2acd3d6945fa",
"beleg": "async function alleAnnehmen() {\n props.probleme.forEach(p => { if (anwenden(p)) erledige(p) })\n await speichern()\n}",
"datei": "frontend/src/components/FeatureKarte.vue"
},
"[\"karten-pruefen\", \"Alle Vorschläge verwerfen\"]": {
"abdruck": "f98fd3fc005b9e56",
"beleg": "<button\n class=\"icon-knopf rot\"\n :title=\"`Alle ${probleme.length} Vorschläge verwerfen`\"\n @click=\"alleAblehnen\"\n >✕</button>",
"datei": "frontend/src/components/FeatureKarte.vue"
},
"[\"karten-pruefen\", \"Code in Features prüfen\"]": {
"abdruck": "daf5820ada5ee059",
"beleg": "async def code_im_feature(features: list, punkte: list, stand: dict,",
"datei": "backend/inhalt.py"
},
"[\"karten-pruefen\", \"Features im Code prüfen\"]": {
"abdruck": "acc652c172671790",
"beleg": "ANKLAGE_AB = 4",
"datei": "backend/inhalt.py"
},
"[\"karten-pruefen\", \"Funktion abnehmen\"]": {
"abdruck": "21095461d6190dec",
"beleg": "if beleg:\n stand[\"belege\"][schluessel] = beleg\n if x.get(\"stand\") == parser.OFFEN:\n # Abnahme: die Karte hat es bestellt, jetzt steht es im Code\n return {\"feature\": feature[\"id\"], \"titel\": x[\"text\"], \"art\": \"gebaut\",\n \"grund\": f'{beleg[\"datei\"]}: {beleg[\"beleg\"].strip()}'}",
"datei": "backend/inhalt.py"
},
"[\"karten-pruefen\", \"Problemstelle anspringen\"]": {
"abdruck": "340a32321fd23834",
"beleg": "karte.classList.add('gesucht')",
"datei": "frontend/src/components/PruefPanel.vue"
},
"[\"karten-pruefen\", \"Prüfung beenden\"]": {
"abdruck": "7400070de29ce762",
"beleg": "<button class=\"icon-knopf\" title=\"Schließen\" @click=\"schliesse\">✕</button>",
"datei": "frontend/src/components/PruefPanel.vue"
},
"[\"karten-pruefen\", \"Stil prüfen\"]": {
"abdruck": "86c4530f5dd0bd52",
"beleg": "async def starte(projekt: str, art: str = \"stil\") -> dict:",
"datei": "backend/pruefung.py"
},
"[\"karten-pruefen\", \"Vorschlag annehmen\"]": {
"abdruck": "e4b81d800fd23fec",
"beleg": "async function annehmen(p) {\n if (anwenden(p)) erledige(p)\n await speichern()\n}",
"datei": "frontend/src/components/FeatureKarte.vue"
},
"[\"karten-pruefen\", \"Vorschlag verwerfen\"]": {
"abdruck": "b37b74ea369ed66c",
"beleg": "export function erledige(problem) {\n probleme.value = probleme.value.filter(p => p !== problem)\n}",
"datei": "frontend/src/composables/pruefung.js"
},
"[\"projekte\", \"Projekt anlegen\"]": {
"abdruck": "9b3598c9081155b2",
"beleg": "def anlegen(name: str):",
"datei": "backend/projekte.py"
},
"[\"projekte\", \"Projekt aufrufen\"]": {
"abdruck": "1071109545496535",
"beleg": "@click=\"emit('auswaehlen', p.name)\"",
"datei": "frontend/src/components/SeitenLeiste.vue"
},
"[\"projekte\", \"Projekt entfernen\"]": {
"abdruck": "e413063557fa0f13",
"beleg": "shutil.rmtree(ordner)",
"datei": "backend/projekte.py"
},
"[\"projekte\", \"Projekt umbenennen\"]": {
"abdruck": "d4e71f8ef907393c",
"beleg": "function projektMenue(e, name) {\n oeffneMenue(e, [\n { text: '✎ Projekt anpassen', aktion: () => starteUmbenennen(name) },\n { text: '◇ Quelle festlegen', aktion: () => starteQuelle(name) },\n { text: '✕ Projekt entfernen', aktion: () => oeffneModal({ art: 'entfernen', name }) },\n ])\n}",
"datei": "frontend/src/components/SeitenLeiste.vue"
},
"[\"projekte\", \"Quelle festlegen\"]": {
"abdruck": "fc6c20f4e8d74fa6",
"beleg": "if ziel == Path(ziel.anchor) or ziel == Path.home():",
"datei": "backend/quelle.py"
},
"[\"schritt-zuruecknehmen\", \"Nächsten Rückschritt anzeigen\"]": {
"abdruck": "6768d8a0b83537a2",
"beleg": "letzterSchritt.value = (await fetchVerlauf(aktivesProjekt.value)).letzter",
"datei": "frontend/src/App.vue"
},
"[\"schritt-zuruecknehmen\", \"Schritt zurücknehmen\"]": {
"abdruck": "c49332e7a679dfda",
"beleg": "def zurueck(projekt: str) -> str:\n \"\"\"Den letzten Schritt zurücknehmen. Gibt zurück, was zurückgenommen wurde.\"\"\"\n schritte = lade(projekt)\n if not schritte:\n raise ValueError(\"Nichts zum Zurücknehmen\")\n schritt = schritte.pop()",
"datei": "backend/verlauf.py"
},
"[\"vision-festhalten\", \"Anzeigemodus umschalten\"]": {
"abdruck": "b983b82c02343560",
"beleg": "const gross = ref(false)",
"datei": "frontend/src/components/VisionKarte.vue"
},
"[\"vision-festhalten\", \"Vision anlegen\"]": {
"abdruck": "58ceb9502d1754bf",
"beleg": "<div class=\"bereich\" @contextmenu.prevent=\"bereichsMenue\">",
"datei": "frontend/src/components/FeatureListe.vue"
},
"[\"vision-festhalten\", \"Vision anpassen\"]": {
"abdruck": "dd527b57b4c9685e",
"beleg": "async function fertig() {\n if (!aktiv.value) return\n aktiv.value = false\n const neu = entwurf.value.trim()\n if (neu === text.value) return\n text.value = neu\n await speichern()\n}",
"datei": "frontend/src/components/VisionKarte.vue"
},
"[\"vision-festhalten\", \"Vision entfernen\"]": {
"abdruck": "0aeb156d13b32b7e",
"beleg": "function kartenMenue(e) {\n oeffneMenue(e, [{ text: '✕ Vision entfernen', aktion: () => emit('entfernen') }])\n}",
"datei": "frontend/src/components/VisionKarte.vue"
}
},
"ohne_code": {},
"ohne_karte": {
"route:delete /projekte/{name}/chat": {
"abdruck": "4ba7a240985a83b4",
"beleg": "backend/routes.py:182",
"feature": "karten-besprechen",
"karten": "41f55589fcff4959",
"stelle": "route:delete /projekte/{name}/chat",
"titel": "Chatverlauf leeren"
}
},
"paare": {
"[\"[\\\"features\\\", \\\"Anzeigemodus umschalten\\\"]\", \"[\\\"vision-festhalten\\\", \\\"Anzeigemodus umschalten\\\"]\"]": {
"abdruck": "d23706be97d6369a",
"gleich": false
},
"[\"[\\\"features\\\", \\\"Feature anlegen\\\"]\", \"[\\\"features\\\", \\\"Feature entfernen\\\"]\"]": {
"abdruck": "586059671bbfa75b",
"gleich": false
},
"[\"[\\\"features\\\", \\\"Feature anlegen\\\"]\", \"[\\\"funktionen\\\", \\\"Funktion anlegen\\\"]\"]": {
"abdruck": "3eb9956887005cc4",
"gleich": false
},
"[\"[\\\"features\\\", \\\"Feature anlegen\\\"]\", \"[\\\"projekte\\\", \\\"Projekt anlegen\\\"]\"]": {
"abdruck": "66a7175c58b703c4",
"gleich": false
},
"[\"[\\\"features\\\", \\\"Feature entfernen\\\"]\", \"[\\\"funktionen\\\", \\\"Funktion anlegen\\\"]\"]": {
"abdruck": "97a9dde3e968b1eb",
"gleich": false
},
"[\"[\\\"features\\\", \\\"Feature entfernen\\\"]\", \"[\\\"funktionen\\\", \\\"Funktion entfernen\\\"]\"]": {
"abdruck": "9717a5c6eab69f22",
"gleich": false
},
"[\"[\\\"features\\\", \\\"Feature entfernen\\\"]\", \"[\\\"projekte\\\", \\\"Projekt entfernen\\\"]\"]": {
"abdruck": "65c43ee10d0d8b4a",
"gleich": false
},
"[\"[\\\"features\\\", \\\"Feature entfernen\\\"]\", \"[\\\"vision-festhalten\\\", \\\"Vision entfernen\\\"]\"]": {
"abdruck": "60434d422bbd2ca3",
"gleich": false
},
"[\"[\\\"features\\\", \\\"Regeln anpassen\\\"]\", \"[\\\"features\\\", \\\"Titel anpassen\\\"]\"]": {
"abdruck": "75e90ff6d5453c2d",
"gleich": false
},
"[\"[\\\"features\\\", \\\"Regeln anpassen\\\"]\", \"[\\\"features\\\", \\\"Ziel anpassen\\\"]\"]": {
"abdruck": "a9e849a044a7ee41",
"gleich": false
},
"[\"[\\\"features\\\", \\\"Regeln anpassen\\\"]\", \"[\\\"funktionen\\\", \\\"Regeln anpassen\\\"]\"]": {
"abdruck": "827215ea68dcbada",
"gleich": false
},
"[\"[\\\"features\\\", \\\"Regeln anpassen\\\"]\", \"[\\\"funktionen\\\", \\\"Titel anpassen\\\"]\"]": {
"abdruck": "ea7185c230fbdd2f",
"gleich": false
},
"[\"[\\\"features\\\", \\\"Regeln anpassen\\\"]\", \"[\\\"funktionen\\\", \\\"Ziel anpassen\\\"]\"]": {
"abdruck": "bc79858745a7d262",
"gleich": false
},
"[\"[\\\"features\\\", \\\"Titel anpassen\\\"]\", \"[\\\"features\\\", \\\"Ziel anpassen\\\"]\"]": {
"abdruck": "69360dc1502aa01f",
"gleich": false
},
"[\"[\\\"features\\\", \\\"Titel anpassen\\\"]\", \"[\\\"funktionen\\\", \\\"Titel anpassen\\\"]\"]": {
"abdruck": "01fbbfc1ca91d32a",
"gleich": false
},
"[\"[\\\"features\\\", \\\"Titel anpassen\\\"]\", \"[\\\"funktionen\\\", \\\"Ziel anpassen\\\"]\"]": {
"abdruck": "414ddf4e7bff9896",
"gleich": false
},
"[\"[\\\"features\\\", \\\"Ziel anpassen\\\"]\", \"[\\\"funktionen\\\", \\\"Titel anpassen\\\"]\"]": {
"abdruck": "83a0a87e978c859a",
"gleich": false
},
"[\"[\\\"features\\\", \\\"Ziel anpassen\\\"]\", \"[\\\"funktionen\\\", \\\"Ziel anpassen\\\"]\"]": {
"abdruck": "5e97e3c6fe9df287",
"gleich": false
},
"[\"[\\\"funktionen\\\", \\\"Funktion anlegen\\\"]\", \"[\\\"funktionen\\\", \\\"Funktion entfernen\\\"]\"]": {
"abdruck": "3e58148f974b6b34",
"gleich": false
},
"[\"[\\\"funktionen\\\", \\\"Funktion anlegen\\\"]\", \"[\\\"projekte\\\", \\\"Projekt anlegen\\\"]\"]": {
"abdruck": "202f51c53a73b452",
"gleich": false
},
"[\"[\\\"funktionen\\\", \\\"Funktion entfernen\\\"]\", \"[\\\"projekte\\\", \\\"Projekt entfernen\\\"]\"]": {
"abdruck": "a9d2c4dd5e70fa2c",
"gleich": false
},
"[\"[\\\"funktionen\\\", \\\"Funktion entfernen\\\"]\", \"[\\\"vision-festhalten\\\", \\\"Vision entfernen\\\"]\"]": {
"abdruck": "4090ee6826466fa3",
"gleich": false
},
"[\"[\\\"funktionen\\\", \\\"Regeln anpassen\\\"]\", \"[\\\"funktionen\\\", \\\"Titel anpassen\\\"]\"]": {
"abdruck": "c8321ad2b769d77f",
"gleich": false
},
"[\"[\\\"funktionen\\\", \\\"Regeln anpassen\\\"]\", \"[\\\"funktionen\\\", \\\"Ziel anpassen\\\"]\"]": {
"abdruck": "7ce003da82080259",
"gleich": false
},
"[\"[\\\"funktionen\\\", \\\"Regeln anpassen\\\"]\", \"[\\\"karten-pruefen\\\", \\\"Funktion abnehmen\\\"]\"]": {
"abdruck": "d2f80684e1be6287",
"gleich": false
},
"[\"[\\\"funktionen\\\", \\\"Testschritte anpassen\\\"]\", \"[\\\"funktionen\\\", \\\"Titel anpassen\\\"]\"]": {
"abdruck": "7a51922f327ba28b",
"gleich": false
},
"[\"[\\\"funktionen\\\", \\\"Testschritte anpassen\\\"]\", \"[\\\"funktionen\\\", \\\"Ziel anpassen\\\"]\"]": {
"abdruck": "97a70c7ecfbb9f30",
"gleich": false
},
"[\"[\\\"funktionen\\\", \\\"Titel anpassen\\\"]\", \"[\\\"funktionen\\\", \\\"Ziel anpassen\\\"]\"]": {
"abdruck": "17b00db94f54b62d",
"gleich": false
},
"[\"[\\\"funktionen\\\", \\\"Titel anpassen\\\"]\", \"[\\\"vision-festhalten\\\", \\\"Vision anpassen\\\"]\"]": {
"abdruck": "d17a7b7bad8d947a",
"gleich": false
},
"[\"[\\\"funktionen\\\", \\\"Ziel anpassen\\\"]\", \"[\\\"vision-festhalten\\\", \\\"Vision anpassen\\\"]\"]": {
"abdruck": "e25b21ca4183d372",
"gleich": false
},
"[\"[\\\"karten-pruefen\\\", \\\"Alle Vorschläge annehmen\\\"]\", \"[\\\"karten-pruefen\\\", \\\"Alle Vorschläge verwerfen\\\"]\"]": {
"abdruck": "b9c707879cffb7cd",
"gleich": false
},
"[\"[\\\"karten-pruefen\\\", \\\"Alle Vorschläge verwerfen\\\"]\", \"[\\\"karten-pruefen\\\", \\\"Vorschlag verwerfen\\\"]\"]": {
"abdruck": "aed348a776637c4a",
"gleich": false
},
"[\"[\\\"projekte\\\", \\\"Projekt anlegen\\\"]\", \"[\\\"projekte\\\", \\\"Projekt entfernen\\\"]\"]": {
"abdruck": "957ebec8e3096ce6",
"gleich": false
},
"[\"[\\\"projekte\\\", \\\"Projekt entfernen\\\"]\", \"[\\\"vision-festhalten\\\", \\\"Vision entfernen\\\"]\"]": {
"abdruck": "8757fb8b0f89d5a6",
"gleich": false
}
},
"stil": {
"[\"darkmode\", \"\"]": {
"abdruck": "a5b0fd59f25c55ae",
"probleme": []
},
"[\"darkmode\", \"Darkmode umschalten\"]": {
"abdruck": "0dbcca6954cc4d16",
"probleme": []
},
"[\"design\", \"\"]": {
"abdruck": "1ae7985ebcfe6c6a",
"probleme": []
},
"[\"design\", \"Eingabe mit Enter bestätigen\"]": {
"abdruck": "b961f168017fb01e",
"probleme": []
},
"[\"design\", \"Kontextmenü schließen\"]": {
"abdruck": "a3717ea52a012784",
"probleme": []
},
"[\"features\", \"\"]": {
"abdruck": "b4f6fe4cd065e9e9",
"probleme": []
},
"[\"features\", \"Anzeigemodus umschalten\"]": {
"abdruck": "8d468a73bcec5521",
"probleme": []
},
"[\"features\", \"Feature anlegen\"]": {
"abdruck": "7d4d6e36b4f03318",
"probleme": []
},
"[\"features\", \"Feature durchsuchen\"]": {
"abdruck": "3acde68ab76b942c",
"probleme": []
},
"[\"features\", \"Feature einstufen\"]": {
"abdruck": "10e74d934dafacc9",
"probleme": []
},
"[\"features\", \"Feature entfernen\"]": {
"abdruck": "509a1153b2e6382f",
"probleme": []
},
"[\"features\", \"Feature verschieben\"]": {
"abdruck": "5169bc85a796b6a9",
"probleme": []
},
"[\"features\", \"Regeln anpassen\"]": {
"abdruck": "6b4eeed76c631333",
"probleme": []
},
"[\"features\", \"Titel anpassen\"]": {
"abdruck": "de6e2788e24aa230",
"probleme": []
},
"[\"features\", \"Ziel anpassen\"]": {
"abdruck": "f26e4426663b8265",
"probleme": []
},
"[\"funktionen\", \"\"]": {
"abdruck": "adcc38efdb393cf6",
"probleme": []
},
"[\"funktionen\", \"Funktion anlegen\"]": {
"abdruck": "11c59974712619a0",
"probleme": []
},
"[\"funktionen\", \"Funktion entfernen\"]": {
"abdruck": "e55d43e078f1020e",
"probleme": []
},
"[\"funktionen\", \"Funktion verschieben\"]": {
"abdruck": "1bdf3be9b53fdb99",
"probleme": []
},
"[\"funktionen\", \"Regeln anpassen\"]": {
"abdruck": "e354caf2701ee79c",
"probleme": []
},
"[\"funktionen\", \"Stand umschalten\"]": {
"abdruck": "f39b25d34cc6e4c4",
"probleme": []
},
"[\"funktionen\", \"Testschritte anpassen\"]": {
"abdruck": "8fb282300c19c3c2",
"probleme": []
},
"[\"funktionen\", \"Titel anpassen\"]": {
"abdruck": "63e9a78a02d890fe",
"probleme": []
},
"[\"funktionen\", \"Ziel anpassen\"]": {
"abdruck": "a72ce3656c787475",
"probleme": []
},
"[\"karten-besprechen\", \"\"]": {
"abdruck": "8d330c5a71e25e0c",
"probleme": []
},
"[\"karten-besprechen\", \"Gespräch schließen\"]": {
"abdruck": "d1c043cd26a05d69",
"probleme": []
},
"[\"karten-besprechen\", \"Gespräch verwerfen\"]": {
"abdruck": "e6d601409eb48f7a",
"probleme": []
},
"[\"karten-besprechen\", \"Gespräch öffnen\"]": {
"abdruck": "b9a3f8340aa9e266",
"probleme": []
},
"[\"karten-besprechen\", \"Nachricht senden\"]": {
"abdruck": "c3ec1453a9aff541",
"probleme": []
},
"[\"karten-besprechen\", \"Vorgeschlagene Karte anlegen\"]": {
"abdruck": "ca0a5b02f10bbd1e",
"probleme": []
},
"[\"karten-pruefen\", \"\"]": {
"abdruck": "43f92e591142fa95",
"probleme": []
},
"[\"karten-pruefen\", \"Alle Vorschläge annehmen\"]": {
"abdruck": "5afe1e8ec40d8b82",
"probleme": []
},
"[\"karten-pruefen\", \"Alle Vorschläge verwerfen\"]": {
"abdruck": "c44bbe11908637e9",
"probleme": []
},
"[\"karten-pruefen\", \"Code in Features prüfen\"]": {
"abdruck": "ed5da2461b99ddf3",
"probleme": []
},
"[\"karten-pruefen\", \"Features im Code prüfen\"]": {
"abdruck": "c31fcfa6dbc75232",
"probleme": []
},
"[\"karten-pruefen\", \"Funktion abnehmen\"]": {
"abdruck": "dd3cf177204bcf25",
"probleme": []
},
"[\"karten-pruefen\", \"Problemstelle anspringen\"]": {
"abdruck": "0656bbf0d6554c85",
"probleme": []
},
"[\"karten-pruefen\", \"Prüfung beenden\"]": {
"abdruck": "af40481c06d2fd97",
"probleme": []
},
"[\"karten-pruefen\", \"Stil prüfen\"]": {
"abdruck": "8326e792e8383b53",
"probleme": []
},
"[\"karten-pruefen\", \"Vorschlag annehmen\"]": {
"abdruck": "6f8d17b80787b7aa",
"probleme": []
},
"[\"karten-pruefen\", \"Vorschlag verwerfen\"]": {
"abdruck": "cf5bda968428df9f",
"probleme": []
},
"[\"projekte\", \"\"]": {
"abdruck": "a59f6fdfa58188fa",
"probleme": []
},
"[\"projekte\", \"Projekt anlegen\"]": {
"abdruck": "2c3ab4452996ffaf",
"probleme": []
},
"[\"projekte\", \"Projekt aufrufen\"]": {
"abdruck": "2ca7a3ea9e79fc6e",
"probleme": []
},
"[\"projekte\", \"Projekt entfernen\"]": {
"abdruck": "a0c52ceb9605a805",
"probleme": []
},
"[\"projekte\", \"Projekt umbenennen\"]": {
"abdruck": "d41292ff010e7fd3",
"probleme": []
},
"[\"projekte\", \"Quelle festlegen\"]": {
"abdruck": "990ee354f1aa70fa",
"probleme": []
},
"[\"schritt-zuruecknehmen\", \"\"]": {
"abdruck": "f4ecff18e05ff058",
"probleme": []
},
"[\"schritt-zuruecknehmen\", \"Nächsten Rückschritt anzeigen\"]": {
"abdruck": "2d944c3424b6f851",
"probleme": []
},
"[\"schritt-zuruecknehmen\", \"Schritt zurücknehmen\"]": {
"abdruck": "28825333acb1e107",
"probleme": []
},
"[\"vision-festhalten\", \"\"]": {
"abdruck": "e83e9ad5470aa4b1",
"probleme": []
},
"[\"vision-festhalten\", \"Anzeigemodus umschalten\"]": {
"abdruck": "87c494e1e1b0a7b2",
"probleme": []
},
"[\"vision-festhalten\", \"Vision anlegen\"]": {
"abdruck": "7826b26956ba51d2",
"probleme": []
},
"[\"vision-festhalten\", \"Vision anpassen\"]": {
"abdruck": "378ea30c154f7994",
"probleme": []
},
"[\"vision-festhalten\", \"Vision entfernen\"]": {
"abdruck": "340343e51583e86a",
"probleme": []
}
},
"verworfen": {
"route:delete /projekte/{name}/chat": {
"abdruck": "4ba7a240985a83b4",
"titel": "Gespräch schließen\nChatverlauf leeren"
}
}
}

View File

@@ -0,0 +1,23 @@
<!-- titel -->
Darkmode
----------------------------------------
<!-- art -->
optional
----------------------------------------
<!-- ziel -->
Passt die Anwendung ans Umgebungslicht an
----------------------------------------
<!-- regeln -->
- Auto folgt der Systemeinstellung
- Zustand überlebt das Neuladen
- Kein Flackern beim Laden
----------------------------------------
<!-- funktion -->
Darkmode umschalten
<!-- regeln -->
- Nacht-Icon: ☾
- Tag-Icon: ☀
- Auto-Icon: A
<!-- testen -->
- LMB auf Icon in der Kopfzeile
- Modus gewechselt

View File

@@ -0,0 +1,44 @@
<!-- 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 -->
Eingabe mit Enter bestätigen
<!-- ziel -->
Spart den Weg zum Knopf
<!-- regeln -->
- Gilt in einzeiligen Feldern
- Escape verwirft stattdessen
<!-- testen -->
- Modal öffnen
- Text eingeben
- Enter drücken
- Eingabe übernommen
<!-- funktion -->
Kontextmenü schließen
<!-- ziel -->
Räumt ein offenes Menü aus dem Weg
<!-- regeln -->
- Klick daneben schließt es
- Escape schließt es
- Ein neues Menü verdrängt das alte
<!-- testen -->
- RMB auf eine Karte
- LMB daneben
- Menü geschlossen

View File

@@ -0,0 +1,109 @@
<!-- titel -->
Features
----------------------------------------
<!-- art -->
kern
----------------------------------------
<!-- ziel -->
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
- Anpassen nur im Vollmodus
- Verlassen speichert
- Escape verwirft
----------------------------------------
<!-- funktion -->
Anzeigemodus umschalten
<!-- ziel -->
Kompaktmodus zum Lesen, Vollmodus zum Anpassen
<!-- regeln -->
- Standard ist Kompaktmodus
- Vollmodus zeigt alle Felder
- Augen-Icon rechts oben
<!-- testen -->
- MMB auf Featurecard
- Modus umgeschaltet
<!-- funktion -->
Feature anlegen
<!-- regeln -->
- +-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
- LMB auf Badge
- Einstufung gewechselt
<!-- funktion -->
Feature durchsuchen
<!-- ziel -->
Findet die Karte, ohne alle zu lesen
<!-- regeln -->
- Sucht in allen Feldern
- Zählt Ausgeblendete mit
<!-- testen -->
- Suchwort eingeben
- Nur passende Karten bleiben
<!-- funktion -->
Feature entfernen
<!-- regeln -->
- x-Icon
<!-- testen -->
- RMB auf Featurecard
- Klick auf "Feature entfernen"
- Bestätigen im Modal
- Feature entfernt
<!-- funktion -->
Feature verschieben
<!-- ziel -->
Hängt ein Feature in ein anderes Projekt um
<!-- regeln -->
- Datei wandert
- Datei wird nicht kopiert
- Nur bei mehreren Projekten
<!-- testen -->
- RMB auf Featurecard
- Klick auf "Feature verschieben"
- Zielprojekt wählen
- Feature verschoben
<!-- funktion -->
Regeln anpassen
<!-- testen -->
- Vollmodus öffnen
- LMB auf Regeln der Karte
- Text ändern
- Regeln angepasst
<!-- funktion -->
Titel anpassen
<!-- regeln -->
- Aktualisiert den Dateinamen
<!-- testen -->
- Vollmodus öffnen
- LMB auf Titel der Karte
- Text ändern
- Titel angepasst
<!-- funktion -->
Ziel anpassen
<!-- testen -->
- Vollmodus öffnen
- LMB auf Ziel der Karte
- Text ändern
- Ziel angepasst

View File

@@ -0,0 +1,98 @@
<!-- titel -->
Funktionen
----------------------------------------
<!-- art -->
kern
----------------------------------------
<!-- ziel -->
Zerlegt ein Feature in prüfbare Stücke
----------------------------------------
<!-- regeln -->
- Gehört zu genau einem Feature
- Offen heißt bestellt, nicht gebaut
- Neue Funktionen von Hand gelten als gebaut
- Reihenfolge frei wählbar
- 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
<!-- testen -->
- Vollmodus öffnen
- RMB auf Funktion
- Klick auf "Funktion entfernen"
- Bestätigen im Modal
- Funktion entfernt
<!-- funktion -->
Stand umschalten
<!-- ziel -->
Trennt Bestelltes von Gebautem
<!-- regeln -->
- Marke rechts vom Titel
- Offen ist gelb
- Gebaut ist ein Haken
- Kompakt erscheint nur die offene Marke
<!-- testen -->
- Vollmodus öffnen
- LMB auf die Marke
- Stand gewechselt
<!-- funktion -->
Funktion verschieben
<!-- ziel -->
Bringt die Schritte in ihre natürliche Folge
<!-- regeln -->
- Reihenfolge steht in der Datei
<!-- testen -->
- Vollmodus öffnen
- RMB auf Funktion
- Klick auf "Nach oben"
- Funktion verschoben
<!-- funktion -->
Titel anpassen
<!-- testen -->
- Vollmodus öffnen
- 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
<!-- regeln -->
- Eine Regel je Zeile
<!-- testen -->
- Vollmodus öffnen
- LMB auf Regeln der Funktion
- Text ändern
- Regeln angepasst
<!-- funktion -->
Testschritte anpassen
<!-- regeln -->
- Ein Schritt je Zeile
<!-- testen -->
- Vollmodus öffnen
- LMB auf Testen der Funktion
- Text ändern
- Testschritte angepasst

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View File

@@ -0,0 +1,61 @@
<!-- titel -->
Karten besprechen
----------------------------------------
<!-- art -->
optional
----------------------------------------
<!-- ziel -->
Entwickelt Karten im Gespräch mit der KI
----------------------------------------
<!-- regeln -->
- Die KI schreibt nie selbst in eine Karte
- Sie beschreibt den Sollzustand
- Was sich ändert, rechnet der Code aus
- Vorschläge sehen aus wie bei der Prüfung
- Neue Funktionen gelten als offen
- Das Gespräch überlebt den Neustart
- Die letzten 30 Wortmeldungen bleiben
- Gespräch und Prüfung teilen eine Spalte
----------------------------------------
<!-- funktion -->
Gespräch öffnen
<!-- regeln -->
- Stift-Icon in der Kopfzeile
- Icon leuchtet bei offenem Gespräch
<!-- testen -->
- LMB auf Stift-Icon
- Gespräch geöffnet
<!-- funktion -->
Nachricht senden
<!-- ziel -->
Sagt der KI, was das Projekt können soll
<!-- regeln -->
- Enter sendet
- Eigene Zeile erscheint sofort
- Text bleibt bei einem Fehler stehen
<!-- testen -->
- Text eingeben
- Enter drücken
- Antwort und Vorschläge erscheinen
<!-- funktion -->
Vorgeschlagene Karte anlegen
<!-- ziel -->
Macht aus einem Umriss eine echte Karte
<!-- regeln -->
- Gestrichelter Rand vor dem Annehmen
<!-- testen -->
- LMB auf Haken am Umriss
- Karte angelegt
<!-- funktion -->
Gespräch verwerfen
<!-- regeln -->
- Löscht nur das Gespräch
- Karten bleiben unberührt
<!-- testen -->
- LMB auf Papierkorb-Icon
- Gespräch geleert
<!-- funktion -->
Gespräch schließen
<!-- testen -->
- LMB auf x-Icon
- Spalte wieder frei

View File

@@ -0,0 +1,82 @@
<!-- titel -->
Karten prüfen
----------------------------------------
<!-- art -->
optional
----------------------------------------
<!-- ziel -->
Meldet, wo eine Karte vom Soll abweicht
----------------------------------------
<!-- regeln -->
- 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 -->
Alle Vorschläge annehmen
<!-- regeln -->
- 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 -->
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 Kopfzeile
- 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
- LMB auf Haken-Icon
- Vorschlag in der Datei
<!-- funktion -->
Vorschlag verwerfen
<!-- regeln -->
- x-Icon: ✕
<!-- testen -->
- Prüfung durchlaufen lassen
- LMB auf x-Icon
- Vorschlag verschwunden

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -0,0 +1,53 @@
<!-- titel -->
Projekte
----------------------------------------
<!-- art -->
kern
----------------------------------------
<!-- ziel -->
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 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"
- Namen ändern
- Außerhalb klicken
- Name angepasst
<!-- funktion -->
Projekt entfernen
<!-- regeln -->
- x-Icon
<!-- testen -->
- RMB auf Projektname
- Klick auf "Projekt entfernen"
- Bestätigen im Modal
- Projekt entfernt

View File

@@ -0,0 +1,53 @@
<!-- titel -->
Vision festhalten
----------------------------------------
<!-- art -->
optional
----------------------------------------
<!-- ziel -->
Hält fest, wie es gedacht ist
----------------------------------------
<!-- regeln -->
- Höchstens eine je Projekt
- Goldener Rand
- Steht als erste Karte
- Nur Titel und ein Textfeld
- Text wird als Markdown gezeigt
- Keine Prüfung ändert sie
----------------------------------------
<!-- funktion -->
Anzeigemodus umschalten
<!-- ziel -->
Kompaktmodus zum Lesen, Vollmodus zum Anpassen
<!-- regeln -->
- Standard ist Kompaktmodus
- Kompakt zeigt nur die obersten Punkte
- Augen-Icon rechts oben
- Vollmodus geht über alle Spalten
<!-- testen -->
- MMB auf die Vision
- Modus umgeschaltet
<!-- funktion -->
Vision anlegen
<!-- testen -->
- RMB auf Hauptbereich
- Klick auf "Vision erstellen"
- Vision angelegt
<!-- funktion -->
Vision anpassen
<!-- regeln -->
- Anpassen nur im Vollmodus
- Verlassen speichert
- Escape verwirft
<!-- testen -->
- Vollmodus öffnen
- LMB auf den Text
- Text ändern
- Vision angepasst
<!-- funktion -->
Vision entfernen
<!-- testen -->
- RMB auf die Vision
- Klick auf "Vision entfernen"
- Bestätigen im Modal
- Vision entfernt

View File

@@ -0,0 +1,19 @@
<!-- titel -->
Vision
----------------------------------------
<!-- art -->
vision
----------------------------------------
<!-- ziel -->
- (Basis) Projekte und ihre Features manuell verwalten
- Mit Hilfe von KI-Chat Live Projekte entwickeln und Features erweitern
- Ich schreibe mit der KI und definiere meine Idee
- Die KI zeigt mir Live wie sich die Features ändern
- Ich verbessere und die KI passt Live an
- Wenn ich zufrieden bin, dann lasse ich die Features von der KI implementieren
- (später) Zu einem ausgewählten Projekt analysiert die KI alle kompakten Informationen dazu
- Ich wähle ein Verzeichnis das als Quelle dient
- Die Quelle wird durch ein System geschickt, dass kompakte Informationen extrahiert
- Die kompakten Informationen enthalten das Wissen zu dem WIE vom Projekt
- Also ich brauche die Funktionen und Regeln zum Projekt, nicht wie es Implementiert wird
- Es werden Features zum Projekt generiert, die es komplett abbilden

View File

@@ -1,172 +1,319 @@
{
"abdeckung": {
"bedienen:ablehnen": {
"abdruck": "61a0e2e5f8a4c439",
"funktion": "Vorschlag annehmen"
"abdruck": "e9cce25d92b81ff6",
"funktion": "Vorschlag annehmen",
"trug": "089e774256dcce89"
},
"bedienen:alleAblehnen": {
"abdruck": "9d613f6b111b9b67",
"funktion": "Alle Vorschläge verwerfen"
"abdruck": "ac9262c7cafe9537",
"funktion": "Alle Vorschläge verwerfen",
"trug": "6869d68fdbd6d0a7"
},
"bedienen:alleAnnehmen": {
"abdruck": "9456d726b83144c9",
"funktion": "Alle Vorschläge annehmen"
"abdruck": "94f23cae75fdc531",
"funktion": "Alle Vorschläge annehmen",
"trug": "94ebeec7b03abb70"
},
"bedienen:annehmen": {
"abdruck": "97dc5220725e77ee",
"funktion": "Vorschlag annehmen"
"abdruck": "2742ad031b3d3cca",
"funktion": "Vorschlag annehmen",
"trug": "089e774256dcce89"
},
"bedienen:auswaehlen": {
"abdruck": "31f34c41cbd1bd93",
"funktion": "Projekt aufrufen"
"abdruck": "d061bf62466085ce",
"funktion": "Projekt aufrufen",
"trug": "e24c2ab0a27ace24"
},
"bedienen:bereichsMenue": {
"abdruck": "421627feeb92867c",
"funktion": "Projekt aufrufen"
"abdruck": "4277536d43c7bcd5",
"funktion": "Projekt aufrufen",
"trug": "e24c2ab0a27ace24"
},
"bedienen:bestaetigen": {
"abdruck": "b1ce0152c68722c9",
"funktion": "Feature anlegen"
"abdruck": "db1f954b9ad01749",
"funktion": "Eingabe mit Enter bestätigen",
"trug": "43fca3b0e83b3701"
},
"bedienen:erledige": {
"abdruck": "1b80ae13958bb11a",
"funktion": "Vorgeschlagene Karte anlegen",
"trug": "de2b733c447db595"
},
"bedienen:funktionsMenue": {
"abdruck": "67bf62e64b2af11f",
"funktion": "Funktion entfernen"
"abdruck": "bacdd0a7cc9d0f92",
"funktion": "Funktion verschieben",
"trug": "6a33d1b7b5a7f28c"
},
"bedienen:karteAnnehmen": {
"abdruck": "bf348251763e402a",
"funktion": "Vorgeschlagene Karte anlegen",
"trug": "de2b733c447db595"
},
"bedienen:kartenMenue": {
"abdruck": "bc47fd3a97b2027c",
"funktion": "Feature entfernen"
"abdruck": "d3ba136a562eb1e9",
"funktion": "Feature entfernen",
"trug": "f486bf09c64ebb3a"
},
"bedienen:leeren": {
"abdruck": "f2af432a5ae3b255",
"funktion": "Gespräch verwerfen",
"trug": "1f848fba8b2a5e56"
},
"bedienen:leistenMenue": {
"abdruck": "90a66ccbe71ddc74",
"funktion": "Projekt anlegen"
"abdruck": "ed951fbeaaa9bc70",
"funktion": "Projekt anlegen",
"trug": "44b8a59e1dcb20d4"
},
"bedienen:oeffneChat": {
"abdruck": "9bf171761edca141",
"funktion": "Gespräch öffnen",
"trug": "c71610a339bb298e"
},
"bedienen:projektMenue": {
"abdruck": "58ca0dac4286c61d",
"funktion": "Projekt umbenennen"
"abdruck": "ea73f8c2597c0443",
"funktion": "Projekt umbenennen",
"trug": "40d6954a44fa3240"
},
"bedienen:schliesse": {
"abdruck": "f78739ce74d876f5",
"funktion": ""
"abdruck": "919b104c012c034d",
"funktion": "",
"trug": ""
},
"bedienen:schliessen": {
"abdruck": "1e12f9ae001e93e1",
"funktion": "Gespräch verwerfen",
"trug": "1f848fba8b2a5e56"
},
"bedienen:senden": {
"abdruck": "8df3402ed3373acc",
"funktion": "Nachricht senden",
"trug": "e61022551a98250f"
},
"bedienen:springe": {
"abdruck": "67b827c9bba0c05a",
"funktion": "Problemstelle anspringen"
"abdruck": "0b134efa50c03bff",
"funktion": "Problemstelle anspringen",
"trug": "7486526f6867c436"
},
"bedienen:starte": {
"abdruck": "06e7ff1e70efc603",
"funktion": "Titel anpassen"
"abdruck": "761c2f62c917a3db",
"funktion": "Titel anpassen",
"trug": "d0b7a430e96e2599"
},
"bedienen:startePruefung": {
"abdruck": "1f9ac6d1bb70cda8",
"funktion": "Stil prüfen"
"abdruck": "31c3fef23c6319d8",
"funktion": "Gespräch öffnen",
"trug": "c71610a339bb298e"
},
"bedienen:umbenennen": {
"abdruck": "af404032e5814872",
"funktion": "Projekt umbenennen"
"abdruck": "6185b6b39708bf02",
"funktion": "Projekt umbenennen",
"trug": "40d6954a44fa3240"
},
"bedienen:wechsleAnsicht": {
"abdruck": "ee50f2dbfa66f779",
"funktion": "Anzeigemodus umschalten"
"abdruck": "e3193fce50eaa58e",
"funktion": "Anzeigemodus umschalten",
"trug": "fccea553715951de"
},
"bedienen:wechsleArt": {
"abdruck": "3fcd27f68677ed3a",
"funktion": "Feature einstufen"
"abdruck": "67250a1e8d82e960",
"funktion": "Feature einstufen",
"trug": "90b6c77ff0c412e6"
},
"bedienen:wechsleDarkMode": {
"abdruck": "51e5efb1e9196f3b",
"funktion": "Darkmode umschalten"
"abdruck": "b83524eb0e12a7d7",
"funktion": "Darkmode umschalten",
"trug": "4033fc55c753db3e"
},
"lauscher:window.click": {
"abdruck": "5dd03db00120c133",
"funktion": "Kontextmenü schließen"
"bedienen:wechsleStand": {
"abdruck": "8c2e3bf3be58ec00",
"funktion": "Stand umschalten",
"trug": "37a3835d1d2a3e6c"
},
"lauscher:window.keydown": {
"abdruck": "70b3f36549535bfc",
"funktion": ""
"bedienen:zurueck": {
"abdruck": "e378014fbce96d10",
"funktion": "Schritt zurücknehmen",
"trug": "10e46c458040a5e3"
},
"hoeren:systemDunkel.change": {
"abdruck": "d3ef681b987557dc",
"funktion": "Darkmode umschalten",
"trug": "4033fc55c753db3e"
},
"hoeren:window.click": {
"abdruck": "620812522ed2d900",
"funktion": "Kontextmenü schließen",
"trug": "87216235bbe42a1a"
},
"hoeren:window.keydown": {
"abdruck": "c3e1df2d60fcc067",
"funktion": "Schritt zurücknehmen",
"trug": "10e46c458040a5e3"
},
"menue:+ Feature erstellen": {
"abdruck": "a9a3fefd653555c7",
"funktion": "Feature anlegen"
"abdruck": "82c40072137d5757",
"funktion": "Feature anlegen",
"trug": "df7a1452a32da410"
},
"menue:+ Funktion erstellen": {
"abdruck": "0f968068851c2f7a",
"funktion": "Feature entfernen"
"abdruck": "b44457d64a5ec9d1",
"funktion": "Feature entfernen",
"trug": "f486bf09c64ebb3a"
},
"menue:+ Projekt erstellen": {
"abdruck": "dbbe1573ebe97251",
"funktion": "Projekt entfernen"
"abdruck": "2be770390433a2ce",
"funktion": "Projekt anlegen",
"trug": "44b8a59e1dcb20d4"
},
"menue:+ Vision erstellen": {
"abdruck": "1d814205e510b035",
"funktion": "Feature anlegen"
"abdruck": "cb22a654070e58b4",
"funktion": "Feature anlegen",
"trug": "df7a1452a32da410"
},
"menue:→ Feature verschieben": {
"abdruck": "38e6b244668ee164",
"funktion": "Feature entfernen",
"trug": "f486bf09c64ebb3a"
},
"menue:▲ Nach oben": {
"abdruck": "4e6fd1617d817357",
"funktion": "Funktion verschieben",
"trug": "6a33d1b7b5a7f28c"
},
"menue:▼ Nach unten": {
"abdruck": "183651450e546c4f",
"funktion": "Funktion entfernen",
"trug": "885e16a93460f706"
},
"menue:◇ Quelle festlegen": {
"abdruck": "e4bb1781bc375d6d",
"funktion": "Projekt umbenennen",
"trug": "40d6954a44fa3240"
},
"menue:✎ Projekt anpassen": {
"abdruck": "cd7390c2f6852257",
"funktion": "Projekt umbenennen"
"abdruck": "ad505addca45b06c",
"funktion": "Projekt umbenennen",
"trug": "40d6954a44fa3240"
},
"menue:✕ Feature entfernen": {
"abdruck": "e773a996f84a8d7b",
"funktion": "Feature entfernen"
"abdruck": "bc968582260c45b9",
"funktion": "Funktion anlegen",
"trug": "78d0329cf6eacca9"
},
"menue:✕ Funktion entfernen": {
"abdruck": "99495f37aa040550",
"funktion": "Funktion entfernen"
"abdruck": "d29458b150c55777",
"funktion": "Funktion verschieben",
"trug": "6a33d1b7b5a7f28c"
},
"menue:✕ Projekt entfernen": {
"abdruck": "bc7b8c360ac3c403",
"funktion": "Projekt anlegen"
"abdruck": "aca25c0b5feaf752",
"funktion": "Projekt umbenennen",
"trug": "40d6954a44fa3240"
},
"menue:✕ Vision entfernen": {
"abdruck": "ec65c57f36ff14d0",
"funktion": "Anzeigemodus umschalten"
"abdruck": "88520b1f105d3fb5",
"funktion": "Anzeigemodus umschalten",
"trug": "fccea553715951de"
},
"route:delete /projekte/{name}": {
"abdruck": "9014a4b884cca27a",
"funktion": "Projekt entfernen"
"abdruck": "b4f871be35d10e5f",
"funktion": "Projekt entfernen",
"trug": "e91a89b8d78dbd2e"
},
"route:delete /projekte/{name}/features/{id}": {
"abdruck": "ff8d0773ef483606",
"funktion": "Feature entfernen"
"abdruck": "c1db81de98835c39",
"funktion": "Feature entfernen",
"trug": "f486bf09c64ebb3a"
},
"route:delete /projekte/{name}/pruefung": {
"abdruck": "79f2bdc0413ea88a",
"funktion": "Prüfung beenden"
"abdruck": "7a480f33ddf55165",
"funktion": "Prüfung beenden",
"trug": "83435eb3e1394e49"
},
"route:get /health": {
"abdruck": "3214361386c64911",
"funktion": ""
"abdruck": "65257319741edf35",
"funktion": "",
"trug": ""
},
"route:get /projekte": {
"abdruck": "2f6756caafbe76b8",
"funktion": ""
"abdruck": "276556054437c303",
"funktion": "",
"trug": ""
},
"route:get /projekte/{name}/chat": {
"abdruck": "fd2a22c36ea85545",
"funktion": "Gespräch öffnen",
"trug": "c71610a339bb298e"
},
"route:get /projekte/{name}/features": {
"abdruck": "509119bcecfd63e6",
"funktion": "Projekt aufrufen"
"abdruck": "122f93cbd0d07881",
"funktion": "Projekt aufrufen",
"trug": "e24c2ab0a27ace24"
},
"route:get /projekte/{name}/pruefung": {
"abdruck": "f7d46d8a5be26212",
"funktion": "Stil prüfen"
"abdruck": "e651ac90b7b2c520",
"funktion": "",
"trug": ""
},
"route:get /projekte/{name}/quelle": {
"abdruck": "834d3af3203ca3a8",
"funktion": "Quelle festlegen",
"trug": "b8b70dd0ff9817cb"
},
"route:get /projekte/{name}/verlauf": {
"abdruck": "5a3dcc678733fad8",
"funktion": "",
"trug": ""
},
"route:post /projekte": {
"abdruck": "c3e70befc5eb772b",
"funktion": "Projekt anlegen"
"abdruck": "fa1628d7e469885d",
"funktion": "Projekt anlegen",
"trug": "44b8a59e1dcb20d4"
},
"route:post /projekte/{name}/chat": {
"abdruck": "c3edc89cec5d2f6f",
"funktion": "Nachricht senden",
"trug": "e61022551a98250f"
},
"route:post /projekte/{name}/features": {
"abdruck": "df08920d8f5e5d4d",
"funktion": "Feature anlegen"
"abdruck": "00eb9c16560bac73",
"funktion": "Feature anlegen",
"trug": "df7a1452a32da410"
},
"route:post /projekte/{name}/pruefung": {
"abdruck": "9fb8d6c9260477c7",
"funktion": "Stil prüfen"
"abdruck": "6d7444a9dfad7d54",
"funktion": "Stil prüfen",
"trug": "d5019cdafd70efb7"
},
"route:post /projekte/{name}/pruefung/verworfen": {
"abdruck": "a43f7221e01c386f",
"funktion": "Alle Vorschläge verwerfen",
"trug": "6869d68fdbd6d0a7"
},
"route:post /projekte/{name}/verlauf/zurueck": {
"abdruck": "308fec6d21b01d60",
"funktion": "Schritt zurücknehmen",
"trug": "10e46c458040a5e3"
},
"route:put /projekte/{name}": {
"abdruck": "6030731add729040",
"funktion": "Projekt umbenennen"
"abdruck": "75383ac009961cad",
"funktion": "Projekt umbenennen",
"trug": "40d6954a44fa3240"
},
"route:put /projekte/{name}/features/{id}": {
"abdruck": "e95aea276bf795cd",
"funktion": ""
"abdruck": "b24c7000fd2ec94c",
"funktion": "",
"trug": ""
},
"route:put /projekte/{name}/features/{id}/projekt": {
"abdruck": "fafc7a8a1656f93b",
"funktion": "Feature verschieben",
"trug": "cd2cd705298831e0"
},
"route:put /projekte/{name}/quelle": {
"abdruck": "d342055e3163548a",
"funktion": "Quelle festlegen",
"trug": "b8b70dd0ff9817cb"
}
},
"belege": {
@@ -175,9 +322,14 @@
"beleg": "function wechsleDarkMode() {\n modus.value = MODI[(MODI.indexOf(modus.value) + 1) % MODI.length]\n localStorage.setItem('darkModus', modus.value)\n wendeAn()\n}",
"datei": "frontend/src/App.vue"
},
"[\"design\", \"Eingabe mit Enter bestätigen\"]": {
"abdruck": "3bace3e9c9f16023",
"beleg": "@keydown.enter.exact.prevent=\"senden\"",
"datei": "frontend/src/components/ChatPanel.vue"
},
"[\"design\", \"Kontextmenü schließen\"]": {
"abdruck": "519c5a56bf4b9810",
"beleg": "schliesseMenue()",
"abdruck": "671fa86bccb2c2c6",
"beleg": "export function schliesseMenue() {\n kontextMenue.value = null\n}",
"datei": "frontend/src/composables/kontextmenue.js"
},
"[\"features\", \"Anzeigemodus umschalten\"]": {
@@ -190,14 +342,24 @@
"beleg": "async function erstellen(titel) {",
"datei": "frontend/src/components/FeatureListe.vue"
},
"[\"features\", \"Feature durchsuchen\"]": {
"abdruck": "4f249c42d46d25b5",
"beleg": "function passt(f) {\n const wort = suche.value.trim().toLowerCase()\n if (!wort) return true\n const s = f.sektionen\n const text = [s.titel, s.ziel, s.regeln,\n ...s.funktionen.map(x => `${x.text} ${x.ziel} ${x.regeln} ${x.testen}`)]\n return text.join(' ').toLowerCase().includes(wort)\n}",
"datei": "frontend/src/components/FeatureListe.vue"
},
"[\"features\", \"Feature einstufen\"]": {
"abdruck": "de473e39c7232637",
"abdruck": "a0ee6590c24456bb",
"beleg": "function wechsleArt() {\n s.value.art = s.value.art === 'kern' ? 'optional' : 'kern'\n speichern()\n}",
"datei": "frontend/src/components/FeatureKarte.vue"
},
"[\"features\", \"Feature entfernen\"]": {
"abdruck": "c6f27381a41a63d7",
"beleg": "def loeschen(projekt: str, id: str):\n _datei(projekt, id).unlink()",
"beleg": "async function entfernen() {\n const id = modal.value.feature.id\n try {\n await featureLoeschen(props.projekt, id)\n features.value = features.value.filter(f => f.id !== id)\n modal.value = null\n emit('geaendert')\n } catch (e) {\n meldung.value = e.message\n }\n}",
"datei": "frontend/src/components/FeatureListe.vue"
},
"[\"features\", \"Feature verschieben\"]": {
"abdruck": "a82b97e3d4a1b533",
"beleg": "def verschieben(projekt: str, id: str, ziel: str) -> dict:",
"datei": "backend/features.py"
},
"[\"features\", \"Regeln anpassen\"]": {
@@ -222,7 +384,12 @@
},
"[\"funktionen\", \"Funktion entfernen\"]": {
"abdruck": "20e8d8e507ee6310",
"beleg": "{ text: '✕ Funktion entfernen', aktion: () => (modal.value = { art: 'entfernen', i }) },",
"beleg": "eintraege.push({ text: '✕ Funktion entfernen', aktion: () => (modal.value = { art: 'entfernen', i }) })",
"datei": "frontend/src/components/FeatureKarte.vue"
},
"[\"funktionen\", \"Funktion verschieben\"]": {
"abdruck": "8435119f0dd5b891",
"beleg": "async function schiebe(i, richtung) {",
"datei": "frontend/src/components/FeatureKarte.vue"
},
"[\"funktionen\", \"Regeln anpassen\"]": {
@@ -230,6 +397,11 @@
"beleg": "async function starte(feld, i, j) {\n if (!gross.value) return // Bearbeiten nur in der ausführlichen Ansicht\n const original = wert(feld, i, j)\n aktiv.value = { feld, i, j, original }\n entwurf.value = original\n await nextTick()\n const el = Array.isArray(feldRef.value) ? feldRef.value[0] : feldRef.value\n el?.focus()\n if (feld === 'titel') el?.select()\n}",
"datei": "frontend/src/components/FeatureKarte.vue"
},
"[\"funktionen\", \"Stand umschalten\"]": {
"abdruck": "b66ea5f488899d8f",
"beleg": "@click.stop=\"wechsleStand(i)\"",
"datei": "frontend/src/components/FeatureKarte.vue"
},
"[\"funktionen\", \"Testschritte anpassen\"]": {
"abdruck": "e303ebeaece423de",
"beleg": "<div v-else class=\"klickbar\" @click=\"starte('testen', i)\">",
@@ -245,6 +417,31 @@
"beleg": "<div v-else class=\"inhalt klickbar\" v-html=\"md(f.ziel)\" @click=\"starte('ziel', i)\" />",
"datei": "frontend/src/components/FeatureKarte.vue"
},
"[\"karten-besprechen\", \"Gespräch schließen\"]": {
"abdruck": "d6de2c46300fef26",
"beleg": "<button class=\"icon-knopf\" title=\"Gespräch schließen\" @click=\"emit('schliessen')\">✕</button>",
"datei": "frontend/src/components/ChatPanel.vue"
},
"[\"karten-besprechen\", \"Gespräch verwerfen\"]": {
"abdruck": "9e74be976a4d0e15",
"beleg": "@router.delete(\"/projekte/{name}/chat\")\nasync def chat_leeren(name: str):\n try:\n chat.leeren(name)\n except FileNotFoundError as e:\n raise HTTPException(404, str(e))\n return {\"ok\": True}",
"datei": "backend/routes.py"
},
"[\"karten-besprechen\", \"Gespräch öffnen\"]": {
"abdruck": "c84f9a14428a019e",
"beleg": "<button\n class=\"icon-knopf\"\n :class=\"{ an: chatOffen }\"\n title=\"Karten im Gespräch entwickeln\"\n @click=\"oeffneChat\"\n >✎</button>",
"datei": "frontend/src/App.vue"
},
"[\"karten-besprechen\", \"Nachricht senden\"]": {
"abdruck": "308218d682fe3dd9",
"beleg": "async def frage(projekt: str, text: str) -> dict:",
"datei": "backend/chat.py"
},
"[\"karten-besprechen\", \"Vorgeschlagene Karte anlegen\"]": {
"abdruck": "15aa862d2e0f6f6e",
"beleg": "async function karteAnnehmen(p) {",
"datei": "frontend/src/components/FeatureListe.vue"
},
"[\"karten-pruefen\", \"Alle Vorschläge annehmen\"]": {
"abdruck": "52fb2acd3d6945fa",
"beleg": "async function alleAnnehmen() {\n props.probleme.forEach(p => { if (anwenden(p)) erledige(p) })\n await speichern()\n}",
@@ -252,7 +449,7 @@
},
"[\"karten-pruefen\", \"Alle Vorschläge verwerfen\"]": {
"abdruck": "f98fd3fc005b9e56",
"beleg": "function alleAblehnen() {\n props.probleme.forEach(erledige)\n}",
"beleg": "<button\n class=\"icon-knopf rot\"\n :title=\"`Alle ${probleme.length} Vorschläge verwerfen`\"\n @click=\"alleAblehnen\"\n >✕</button>",
"datei": "frontend/src/components/FeatureKarte.vue"
},
"[\"karten-pruefen\", \"Code in Features prüfen\"]": {
@@ -265,6 +462,11 @@
"beleg": "ANKLAGE_AB = 4",
"datei": "backend/inhalt.py"
},
"[\"karten-pruefen\", \"Funktion abnehmen\"]": {
"abdruck": "21095461d6190dec",
"beleg": "if beleg:\n stand[\"belege\"][schluessel] = beleg\n if x.get(\"stand\") == parser.OFFEN:\n # Abnahme: die Karte hat es bestellt, jetzt steht es im Code\n return {\"feature\": feature[\"id\"], \"titel\": x[\"text\"], \"art\": \"gebaut\",\n \"grund\": f'{beleg[\"datei\"]}: {beleg[\"beleg\"].strip()}'}",
"datei": "backend/inhalt.py"
},
"[\"karten-pruefen\", \"Problemstelle anspringen\"]": {
"abdruck": "340a32321fd23834",
"beleg": "karte.classList.add('gesucht')",
@@ -307,9 +509,24 @@
},
"[\"projekte\", \"Projekt umbenennen\"]": {
"abdruck": "d4e71f8ef907393c",
"beleg": "function projektMenue(e, name) {\n oeffneMenue(e, [\n { text: '✎ Projekt anpassen', aktion: () => starteUmbenennen(name) },\n { text: '✕ Projekt entfernen', aktion: () => oeffneModal({ art: 'entfernen', name }) },\n ])\n}",
"beleg": "function projektMenue(e, name) {\n oeffneMenue(e, [\n { text: '✎ Projekt anpassen', aktion: () => starteUmbenennen(name) },\n { text: '◇ Quelle festlegen', aktion: () => starteQuelle(name) },\n { text: '✕ Projekt entfernen', aktion: () => oeffneModal({ art: 'entfernen', name }) },\n ])\n}",
"datei": "frontend/src/components/SeitenLeiste.vue"
},
"[\"projekte\", \"Quelle festlegen\"]": {
"abdruck": "fc6c20f4e8d74fa6",
"beleg": "if ziel == Path(ziel.anchor) or ziel == Path.home():",
"datei": "backend/quelle.py"
},
"[\"schritt-zuruecknehmen\", \"Nächsten Rückschritt anzeigen\"]": {
"abdruck": "6768d8a0b83537a2",
"beleg": "letzterSchritt.value = (await fetchVerlauf(aktivesProjekt.value)).letzter",
"datei": "frontend/src/App.vue"
},
"[\"schritt-zuruecknehmen\", \"Schritt zurücknehmen\"]": {
"abdruck": "c49332e7a679dfda",
"beleg": "def zurueck(projekt: str) -> str:\n \"\"\"Den letzten Schritt zurücknehmen. Gibt zurück, was zurückgenommen wurde.\"\"\"\n schritte = lade(projekt)\n if not schritte:\n raise ValueError(\"Nichts zum Zurücknehmen\")\n schritt = schritte.pop()",
"datei": "backend/verlauf.py"
},
"[\"vision-festhalten\", \"Anzeigemodus umschalten\"]": {
"abdruck": "b983b82c02343560",
"beleg": "const gross = ref(false)",
@@ -332,24 +549,69 @@
}
},
"ohne_code": {},
"ohne_karte": {},
"ohne_karte": {
"route:delete /projekte/{name}/chat": {
"abdruck": "4ba7a240985a83b4",
"beleg": "backend/routes.py:182",
"feature": "karten-besprechen",
"karten": "41f55589fcff4959",
"stelle": "route:delete /projekte/{name}/chat",
"titel": "Chatverlauf leeren"
}
},
"paare": {
"[\"[\\\"features\\\", \\\"Anzeigemodus umschalten\\\"]\", \"[\\\"vision-festhalten\\\", \\\"Anzeigemodus umschalten\\\"]\"]": {
"abdruck": "d23706be97d6369a",
"gleich": false
},
"[\"[\\\"features\\\", \\\"Feature anlegen\\\"]\", \"[\\\"features\\\", \\\"Feature entfernen\\\"]\"]": {
"abdruck": "586059671bbfa75b",
"gleich": false
},
"[\"[\\\"features\\\", \\\"Feature anlegen\\\"]\", \"[\\\"funktionen\\\", \\\"Funktion anlegen\\\"]\"]": {
"abdruck": "3eb9956887005cc4",
"gleich": false
},
"[\"[\\\"features\\\", \\\"Feature anlegen\\\"]\", \"[\\\"projekte\\\", \\\"Projekt anlegen\\\"]\"]": {
"abdruck": "66a7175c58b703c4",
"gleich": false
},
"[\"[\\\"features\\\", \\\"Feature entfernen\\\"]\", \"[\\\"funktionen\\\", \\\"Funktion anlegen\\\"]\"]": {
"abdruck": "97a9dde3e968b1eb",
"gleich": false
},
"[\"[\\\"features\\\", \\\"Feature entfernen\\\"]\", \"[\\\"funktionen\\\", \\\"Funktion entfernen\\\"]\"]": {
"abdruck": "9717a5c6eab69f22",
"gleich": false
},
"[\"[\\\"features\\\", \\\"Feature entfernen\\\"]\", \"[\\\"projekte\\\", \\\"Projekt entfernen\\\"]\"]": {
"abdruck": "65c43ee10d0d8b4a",
"gleich": false
},
"[\"[\\\"features\\\", \\\"Feature entfernen\\\"]\", \"[\\\"vision-festhalten\\\", \\\"Vision entfernen\\\"]\"]": {
"abdruck": "60434d422bbd2ca3",
"gleich": false
},
"[\"[\\\"features\\\", \\\"Regeln anpassen\\\"]\", \"[\\\"features\\\", \\\"Titel anpassen\\\"]\"]": {
"abdruck": "75e90ff6d5453c2d",
"gleich": false
},
"[\"[\\\"features\\\", \\\"Regeln anpassen\\\"]\", \"[\\\"features\\\", \\\"Ziel anpassen\\\"]\"]": {
"abdruck": "a9e849a044a7ee41",
"gleich": false
},
"[\"[\\\"features\\\", \\\"Regeln anpassen\\\"]\", \"[\\\"funktionen\\\", \\\"Regeln anpassen\\\"]\"]": {
"abdruck": "827215ea68dcbada",
"gleich": false
},
"[\"[\\\"features\\\", \\\"Regeln anpassen\\\"]\", \"[\\\"funktionen\\\", \\\"Titel anpassen\\\"]\"]": {
"abdruck": "ea7185c230fbdd2f",
"gleich": false
},
"[\"[\\\"features\\\", \\\"Regeln anpassen\\\"]\", \"[\\\"funktionen\\\", \\\"Ziel anpassen\\\"]\"]": {
"abdruck": "bc79858745a7d262",
"gleich": false
},
"[\"[\\\"features\\\", \\\"Titel anpassen\\\"]\", \"[\\\"features\\\", \\\"Ziel anpassen\\\"]\"]": {
"abdruck": "69360dc1502aa01f",
"gleich": false
@@ -370,6 +632,10 @@
"abdruck": "5e97e3c6fe9df287",
"gleich": false
},
"[\"[\\\"funktionen\\\", \\\"Funktion anlegen\\\"]\", \"[\\\"funktionen\\\", \\\"Funktion entfernen\\\"]\"]": {
"abdruck": "3e58148f974b6b34",
"gleich": false
},
"[\"[\\\"funktionen\\\", \\\"Funktion anlegen\\\"]\", \"[\\\"projekte\\\", \\\"Projekt anlegen\\\"]\"]": {
"abdruck": "202f51c53a73b452",
"gleich": false
@@ -382,9 +648,281 @@
"abdruck": "4090ee6826466fa3",
"gleich": false
},
"[\"[\\\"funktionen\\\", \\\"Regeln anpassen\\\"]\", \"[\\\"funktionen\\\", \\\"Titel anpassen\\\"]\"]": {
"abdruck": "c8321ad2b769d77f",
"gleich": false
},
"[\"[\\\"funktionen\\\", \\\"Regeln anpassen\\\"]\", \"[\\\"funktionen\\\", \\\"Ziel anpassen\\\"]\"]": {
"abdruck": "7ce003da82080259",
"gleich": false
},
"[\"[\\\"funktionen\\\", \\\"Regeln anpassen\\\"]\", \"[\\\"karten-pruefen\\\", \\\"Funktion abnehmen\\\"]\"]": {
"abdruck": "d2f80684e1be6287",
"gleich": false
},
"[\"[\\\"funktionen\\\", \\\"Testschritte anpassen\\\"]\", \"[\\\"funktionen\\\", \\\"Titel anpassen\\\"]\"]": {
"abdruck": "7a51922f327ba28b",
"gleich": false
},
"[\"[\\\"funktionen\\\", \\\"Testschritte anpassen\\\"]\", \"[\\\"funktionen\\\", \\\"Ziel anpassen\\\"]\"]": {
"abdruck": "97a70c7ecfbb9f30",
"gleich": false
},
"[\"[\\\"funktionen\\\", \\\"Titel anpassen\\\"]\", \"[\\\"funktionen\\\", \\\"Ziel anpassen\\\"]\"]": {
"abdruck": "17b00db94f54b62d",
"gleich": false
},
"[\"[\\\"funktionen\\\", \\\"Titel anpassen\\\"]\", \"[\\\"vision-festhalten\\\", \\\"Vision anpassen\\\"]\"]": {
"abdruck": "d17a7b7bad8d947a",
"gleich": false
},
"[\"[\\\"funktionen\\\", \\\"Ziel anpassen\\\"]\", \"[\\\"vision-festhalten\\\", \\\"Vision anpassen\\\"]\"]": {
"abdruck": "e25b21ca4183d372",
"gleich": false
},
"[\"[\\\"karten-pruefen\\\", \\\"Alle Vorschläge annehmen\\\"]\", \"[\\\"karten-pruefen\\\", \\\"Alle Vorschläge verwerfen\\\"]\"]": {
"abdruck": "b9c707879cffb7cd",
"gleich": false
},
"[\"[\\\"karten-pruefen\\\", \\\"Alle Vorschläge verwerfen\\\"]\", \"[\\\"karten-pruefen\\\", \\\"Vorschlag verwerfen\\\"]\"]": {
"abdruck": "aed348a776637c4a",
"gleich": false
},
"[\"[\\\"projekte\\\", \\\"Projekt anlegen\\\"]\", \"[\\\"projekte\\\", \\\"Projekt entfernen\\\"]\"]": {
"abdruck": "957ebec8e3096ce6",
"gleich": false
},
"[\"[\\\"projekte\\\", \\\"Projekt entfernen\\\"]\", \"[\\\"vision-festhalten\\\", \\\"Vision entfernen\\\"]\"]": {
"abdruck": "8757fb8b0f89d5a6",
"gleich": false
}
},
"stil": {
"[\"darkmode\", \"\"]": {
"abdruck": "a5b0fd59f25c55ae",
"probleme": []
},
"[\"darkmode\", \"Darkmode umschalten\"]": {
"abdruck": "0dbcca6954cc4d16",
"probleme": []
},
"[\"design\", \"\"]": {
"abdruck": "1ae7985ebcfe6c6a",
"probleme": []
},
"[\"design\", \"Eingabe mit Enter bestätigen\"]": {
"abdruck": "b961f168017fb01e",
"probleme": []
},
"[\"design\", \"Kontextmenü schließen\"]": {
"abdruck": "a3717ea52a012784",
"probleme": []
},
"[\"features\", \"\"]": {
"abdruck": "b4f6fe4cd065e9e9",
"probleme": []
},
"[\"features\", \"Anzeigemodus umschalten\"]": {
"abdruck": "8d468a73bcec5521",
"probleme": []
},
"[\"features\", \"Feature anlegen\"]": {
"abdruck": "7d4d6e36b4f03318",
"probleme": []
},
"[\"features\", \"Feature durchsuchen\"]": {
"abdruck": "3acde68ab76b942c",
"probleme": []
},
"[\"features\", \"Feature einstufen\"]": {
"abdruck": "10e74d934dafacc9",
"probleme": []
},
"[\"features\", \"Feature entfernen\"]": {
"abdruck": "509a1153b2e6382f",
"probleme": []
},
"[\"features\", \"Feature verschieben\"]": {
"abdruck": "5169bc85a796b6a9",
"probleme": []
},
"[\"features\", \"Regeln anpassen\"]": {
"abdruck": "6b4eeed76c631333",
"probleme": []
},
"[\"features\", \"Titel anpassen\"]": {
"abdruck": "de6e2788e24aa230",
"probleme": []
},
"[\"features\", \"Ziel anpassen\"]": {
"abdruck": "f26e4426663b8265",
"probleme": []
},
"[\"funktionen\", \"\"]": {
"abdruck": "adcc38efdb393cf6",
"probleme": []
},
"[\"funktionen\", \"Funktion anlegen\"]": {
"abdruck": "11c59974712619a0",
"probleme": []
},
"[\"funktionen\", \"Funktion entfernen\"]": {
"abdruck": "e55d43e078f1020e",
"probleme": []
},
"[\"funktionen\", \"Funktion verschieben\"]": {
"abdruck": "1bdf3be9b53fdb99",
"probleme": []
},
"[\"funktionen\", \"Regeln anpassen\"]": {
"abdruck": "e354caf2701ee79c",
"probleme": []
},
"[\"funktionen\", \"Stand umschalten\"]": {
"abdruck": "f39b25d34cc6e4c4",
"probleme": []
},
"[\"funktionen\", \"Testschritte anpassen\"]": {
"abdruck": "8fb282300c19c3c2",
"probleme": []
},
"[\"funktionen\", \"Titel anpassen\"]": {
"abdruck": "63e9a78a02d890fe",
"probleme": []
},
"[\"funktionen\", \"Ziel anpassen\"]": {
"abdruck": "a72ce3656c787475",
"probleme": []
},
"[\"karten-besprechen\", \"\"]": {
"abdruck": "8d330c5a71e25e0c",
"probleme": []
},
"[\"karten-besprechen\", \"Gespräch schließen\"]": {
"abdruck": "d1c043cd26a05d69",
"probleme": []
},
"[\"karten-besprechen\", \"Gespräch verwerfen\"]": {
"abdruck": "e6d601409eb48f7a",
"probleme": []
},
"[\"karten-besprechen\", \"Gespräch öffnen\"]": {
"abdruck": "b9a3f8340aa9e266",
"probleme": []
},
"[\"karten-besprechen\", \"Nachricht senden\"]": {
"abdruck": "c3ec1453a9aff541",
"probleme": []
},
"[\"karten-besprechen\", \"Vorgeschlagene Karte anlegen\"]": {
"abdruck": "ca0a5b02f10bbd1e",
"probleme": []
},
"[\"karten-pruefen\", \"\"]": {
"abdruck": "43f92e591142fa95",
"probleme": []
},
"[\"karten-pruefen\", \"Alle Vorschläge annehmen\"]": {
"abdruck": "5afe1e8ec40d8b82",
"probleme": []
},
"[\"karten-pruefen\", \"Alle Vorschläge verwerfen\"]": {
"abdruck": "c44bbe11908637e9",
"probleme": []
},
"[\"karten-pruefen\", \"Code in Features prüfen\"]": {
"abdruck": "ed5da2461b99ddf3",
"probleme": []
},
"[\"karten-pruefen\", \"Features im Code prüfen\"]": {
"abdruck": "c31fcfa6dbc75232",
"probleme": []
},
"[\"karten-pruefen\", \"Funktion abnehmen\"]": {
"abdruck": "dd3cf177204bcf25",
"probleme": []
},
"[\"karten-pruefen\", \"Problemstelle anspringen\"]": {
"abdruck": "0656bbf0d6554c85",
"probleme": []
},
"[\"karten-pruefen\", \"Prüfung beenden\"]": {
"abdruck": "af40481c06d2fd97",
"probleme": []
},
"[\"karten-pruefen\", \"Stil prüfen\"]": {
"abdruck": "8326e792e8383b53",
"probleme": []
},
"[\"karten-pruefen\", \"Vorschlag annehmen\"]": {
"abdruck": "6f8d17b80787b7aa",
"probleme": []
},
"[\"karten-pruefen\", \"Vorschlag verwerfen\"]": {
"abdruck": "cf5bda968428df9f",
"probleme": []
},
"[\"projekte\", \"\"]": {
"abdruck": "a59f6fdfa58188fa",
"probleme": []
},
"[\"projekte\", \"Projekt anlegen\"]": {
"abdruck": "2c3ab4452996ffaf",
"probleme": []
},
"[\"projekte\", \"Projekt aufrufen\"]": {
"abdruck": "2ca7a3ea9e79fc6e",
"probleme": []
},
"[\"projekte\", \"Projekt entfernen\"]": {
"abdruck": "a0c52ceb9605a805",
"probleme": []
},
"[\"projekte\", \"Projekt umbenennen\"]": {
"abdruck": "d41292ff010e7fd3",
"probleme": []
},
"[\"projekte\", \"Quelle festlegen\"]": {
"abdruck": "990ee354f1aa70fa",
"probleme": []
},
"[\"schritt-zuruecknehmen\", \"\"]": {
"abdruck": "f4ecff18e05ff058",
"probleme": []
},
"[\"schritt-zuruecknehmen\", \"Nächsten Rückschritt anzeigen\"]": {
"abdruck": "2d944c3424b6f851",
"probleme": []
},
"[\"schritt-zuruecknehmen\", \"Schritt zurücknehmen\"]": {
"abdruck": "28825333acb1e107",
"probleme": []
},
"[\"vision-festhalten\", \"\"]": {
"abdruck": "e83e9ad5470aa4b1",
"probleme": []
},
"[\"vision-festhalten\", \"Anzeigemodus umschalten\"]": {
"abdruck": "87c494e1e1b0a7b2",
"probleme": []
},
"[\"vision-festhalten\", \"Vision anlegen\"]": {
"abdruck": "7826b26956ba51d2",
"probleme": []
},
"[\"vision-festhalten\", \"Vision anpassen\"]": {
"abdruck": "378ea30c154f7994",
"probleme": []
},
"[\"vision-festhalten\", \"Vision entfernen\"]": {
"abdruck": "340343e51583e86a",
"probleme": []
}
},
"verworfen": {
"route:delete /projekte/{name}/chat": {
"abdruck": "4ba7a240985a83b4",
"titel": "Gespräch schließen\nChatverlauf leeren"
}
}
}

View File

@@ -19,5 +19,5 @@ Darkmode umschalten
- Tag-Icon: ☀
- Auto-Icon: A
<!-- testen -->
- LMB auf Sidebar-Icon
- LMB auf Icon in der Kopfzeile
- Modus gewechselt

View File

@@ -19,11 +19,24 @@ Hält die Oberfläche lesbar und ruhig
- Escape schließt das Modal
----------------------------------------
<!-- funktion -->
Eingabe mit Enter bestätigen
<!-- ziel -->
Spart den Weg zum Knopf
<!-- regeln -->
- Gilt in einzeiligen Feldern
- Escape verwirft stattdessen
<!-- testen -->
- Modal öffnen
- Text eingeben
- Enter drücken
- Eingabe übernommen
<!-- funktion -->
Kontextmenü schließen
<!-- ziel -->
Räumt ein offenes Menü aus dem Weg
<!-- regeln -->
- Klick daneben schließt es
- Escape schließt es
- Ein neues Menü verdrängt das alte
<!-- testen -->
- RMB auf eine Karte

View File

@@ -50,9 +50,19 @@ Trennt tragende von verzichtbaren Features
- Badge zeigt die Einstufung
<!-- testen -->
- Vollmodus öffnen
- LMB auf Badge klicken
- LMB auf Badge
- Einstufung gewechselt
<!-- funktion -->
Feature durchsuchen
<!-- ziel -->
Findet die Karte, ohne alle zu lesen
<!-- regeln -->
- Sucht in allen Feldern
- Zählt Ausgeblendete mit
<!-- testen -->
- Suchwort eingeben
- Nur passende Karten bleiben
<!-- funktion -->
Feature entfernen
<!-- regeln -->
- x-Icon
@@ -62,6 +72,19 @@ Feature entfernen
- Bestätigen im Modal
- Feature entfernt
<!-- funktion -->
Feature verschieben
<!-- ziel -->
Hängt ein Feature in ein anderes Projekt um
<!-- regeln -->
- Datei wandert
- Datei wird nicht kopiert
- Nur bei mehreren Projekten
<!-- testen -->
- RMB auf Featurecard
- Klick auf "Feature verschieben"
- Zielprojekt wählen
- Feature verschoben
<!-- funktion -->
Regeln anpassen
<!-- testen -->
- Vollmodus öffnen

View File

@@ -9,7 +9,9 @@ Zerlegt ein Feature in prüfbare Stücke
----------------------------------------
<!-- regeln -->
- Gehört zu genau einem Feature
- Alphabetisch sortiert
- Offen heißt bestellt, nicht gebaut
- Neue Funktionen von Hand gelten als gebaut
- Reihenfolge frei wählbar
- Ziel bleibt freiwillig
- Regeln bleiben freiwillig
- Testen bleibt freiwillig
@@ -39,6 +41,30 @@ Funktion entfernen
- Bestätigen im Modal
- Funktion entfernt
<!-- funktion -->
Stand umschalten
<!-- ziel -->
Trennt Bestelltes von Gebautem
<!-- regeln -->
- Marke rechts vom Titel
- Offen ist gelb
- Gebaut ist ein Haken
- Kompakt erscheint nur die offene Marke
<!-- testen -->
- Vollmodus öffnen
- LMB auf die Marke
- Stand gewechselt
<!-- funktion -->
Funktion verschieben
<!-- ziel -->
Bringt die Schritte in ihre natürliche Folge
<!-- regeln -->
- Reihenfolge steht in der Datei
<!-- testen -->
- Vollmodus öffnen
- RMB auf Funktion
- Klick auf "Nach oben"
- Funktion verschoben
<!-- funktion -->
Titel anpassen
<!-- testen -->
- Vollmodus öffnen

BIN
daten/Planer/k1-1024.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
daten/Planer/k1-1280.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
daten/Planer/k1-1600.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

BIN
daten/Planer/k1-390.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
daten/Planer/k1-640.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
daten/Planer/k1-820.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View File

@@ -0,0 +1,61 @@
<!-- titel -->
Karten besprechen
----------------------------------------
<!-- art -->
optional
----------------------------------------
<!-- ziel -->
Entwickelt Karten im Gespräch mit der KI
----------------------------------------
<!-- regeln -->
- Die KI schreibt nie selbst in eine Karte
- Sie beschreibt den Sollzustand
- Was sich ändert, rechnet der Code aus
- Vorschläge sehen aus wie bei der Prüfung
- Neue Funktionen gelten als offen
- Das Gespräch überlebt den Neustart
- Die letzten 30 Wortmeldungen bleiben
- Gespräch und Prüfung teilen eine Spalte
----------------------------------------
<!-- funktion -->
Gespräch öffnen
<!-- regeln -->
- Stift-Icon in der Kopfzeile
- Icon leuchtet bei offenem Gespräch
<!-- testen -->
- LMB auf Stift-Icon
- Gespräch geöffnet
<!-- funktion -->
Nachricht senden
<!-- ziel -->
Sagt der KI, was das Projekt können soll
<!-- regeln -->
- Enter sendet
- Eigene Zeile erscheint sofort
- Text bleibt bei einem Fehler stehen
<!-- testen -->
- Text eingeben
- Enter drücken
- Antwort und Vorschläge erscheinen
<!-- funktion -->
Vorgeschlagene Karte anlegen
<!-- ziel -->
Macht aus einem Umriss eine echte Karte
<!-- regeln -->
- Gestrichelter Rand vor dem Annehmen
<!-- testen -->
- LMB auf Haken am Umriss
- Karte angelegt
<!-- funktion -->
Gespräch verwerfen
<!-- regeln -->
- Löscht nur das Gespräch
- Karten bleiben unberührt
<!-- testen -->
- LMB auf Papierkorb-Icon
- Gespräch geleert
<!-- funktion -->
Gespräch schließen
<!-- testen -->
- LMB auf x-Icon
- Spalte wieder frei

View File

@@ -35,26 +35,6 @@ Alle Vorschläge verwerfen
- 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
@@ -77,7 +57,7 @@ Stil prüfen
<!-- ziel -->
Prüft die Form gegen den Katalog
<!-- regeln -->
- Prüf-Icon in der Sidebar
- Prüf-Icon in der Kopfzeile
- Wortzahl und Satzzeichen misst der Code
<!-- testen -->
- LMB auf Prüf-Icon

BIN
daten/Planer/komfort.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
daten/Planer/offen.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -11,7 +11,7 @@ vision
- Die KI zeigt mir Live wie sich die Features ändern
- Ich verbessere und die KI passt Live an
- Wenn ich zufrieden bin, dann lasse ich die Features von der KI implementieren
- Zu einem ausgewählten Projekt analysiert die KI alle kompakten Informationen dazu
- (später) Zu einem ausgewählten Projekt analysiert die KI alle kompakten Informationen dazu
- Ich wähle ein Verzeichnis das als Quelle dient
- Die Quelle wird durch ein System geschickt, dass kompakte Informationen extrahiert
- Die kompakten Informationen enthalten das Wissen zu dem WIE vom Projekt

2735
daten/RAG/.pruefung.json Normal file

File diff suppressed because it is too large Load Diff

93
daten/RAG/admin.md Normal file
View File

@@ -0,0 +1,93 @@
<!-- titel -->
Admin
----------------------------------------
<!-- art -->
kern
----------------------------------------
<!-- ziel -->
Bietet Zugriff auf Enterprise-Betriebsdaten und Systemüberwachung für Administratoren
----------------------------------------
<!-- regeln -->
- POST-Operationen erfordern gültiges CSRF-Token
- Wiederherstellung erfordert ROLE_SUPER_ADMIN
- Ergebnis jeder Operation als Flash-Meldung
- Backup-Download erfordert ROLE_SUPER_ADMIN
----------------------------------------
<!-- funktion -->
Betrieb einsehen
<!-- ziel -->
Verschafft Admins einen Überblick über den Betriebszustand aller Systeme
<!-- regeln -->
- Backup- und Restore-Bereitschaft zeigt Status
- Sicherheit und Datenschutz zeigt Bereitschaft
- Deployment-Scheduler zeigt Bereitschaft
- Backup-Übersicht zeigt vorhandene Sicherungen
- Operator-Prüfprotokoll zeigt letzte 8 Prüfungen
- Übersicht kombiniert alle Berichte
<!-- testen -->
- Admin-Dashboard öffnen
- Backup- und Restore-Bereitschaft anzeigen
- Sicherheit und Datenschutz anzeigen
- Deployment-Scheduler anzeigen
- Backup-Übersicht mit vorhandenen Sicherungen
- Operator-Prüfprotokoll mit letzten 8 Einträgen
- Kombinierte Übersicht aller Berichte
----------------------------------------
<!-- funktion -->
Dashboard anzeigen
<!-- ziel -->
Zeigt Systemzustand und Konfiguration im Admin-Bereich
<!-- regeln -->
- Rendered admin dashboard index html twig
- Enthält Chunk-Zähler und Chunk-Limit
- Enthält Vektor- und Tag-Gesundheitsdaten
- Enthält Reset-Konfiguration
<!-- testen -->
- Admin-Dashboard aufrufen
- HTTP 200 zurückerhalten
- admin-dashboard-index-html-twigt Template prüfen
- Chunk-Zähler im Response vorhanden
- Vektor-Gesundheitsdaten im Response vorhanden
----------------------------------------
<!-- funktion -->
Logout einleiten
<!-- ziel -->
Beendet die aktive Adminsession sicher
<!-- regeln -->
- GET oder POST an Admin-Logout startet Logout
- Wird vom Framework-Sicherheitssystem abgefangen
- Leere Methoden-Signatur
- Löscht Session und Authentifizierungsdaten
<!-- testen -->
- Klick auf Logout-Button im Admin
- Weiterleitung zur Login-Seite
- Session beendet
----------------------------------------
<!-- funktion -->
Wiederherstellung testen
<!-- ziel -->
Prueft ob ein Backup sich vollständig wiederherstellen laesst
<!-- regeln -->
- Benoetigt ROLE_SUPER_ADMIN
- POST-Request mit CSRF-Token
- Prueft angegebene Backup-ID gegen Muster retriex-backup-YYYYMMDD-HHMMSS
- Trockenlauf ohne tatsaechliche Wiederherstellung
- Ergebnis als Flash-Meldung nach Startseite
<!-- testen -->
- Admin-Enterprise-Seite aufrufen
- Dry-Run-Wiederherstellung mit gueltigem CSRF-Token absenden
- Rueckleitung auf Admin-Enterprise mit Flash-Meldung pruefen
- Flash-Meldung zeigt Warnungen und Fehler oder Erfolg
----------------------------------------
<!-- funktion -->
Zum Dashboard weiterleiten
<!-- ziel -->
Leitet admin und admin- auf das Dashboard um
<!-- regeln -->
- Admin leitet zum Dashboard um
- Verhindert doppelten Inhalt
<!-- testen -->
- admin aufrufen
- Weiterleitung zu dashboard prüfen
- admin-Abschluss aufrufen
- Weiterleitung zu dashboard prüfen

286
daten/RAG/agenten.md Normal file
View File

@@ -0,0 +1,286 @@
<!-- titel -->
Agenten
----------------------------------------
<!-- art -->
kern
----------------------------------------
<!-- ziel -->
Steuert Agentenaktivitaet und prueft deren Komponenten automatisiert
----------------------------------------
<!-- regeln -->
- JSON-Ausgabe bei gesetztem --json Flag
- Self-TestsFuehrenKeineMCPTransportAufrufeAus
- AgentenaktivitaetSeiteAusQueryParameterPage
- LimitZwischen1Und200Standardmaessig50
- StreamingGibtJedesTokenSofortAus
- Inspector liefert paths, meta und ndjson
- paths zeigen konfigurierte Agentenlogikdateien
- meta enthält Statistiken zur Agentenaktivität
- ndjson enthält paginierte Aktivitätseinträge
----------------------------------------
<!-- funktion -->
Agentenaktivität anzeigen
<!-- ziel -->
Zeigt eine liste der agentenaktivitäten
<!-- regeln -->
- Seite aus Query-Parameter page
- Limit zwischen 1 und 200
- Standardlimit 50
<!-- testen -->
- Admin-System-Agent abrufen
- Antwort ist 200
- Debug-Count zeigt Itemanzahl
----------------------------------------
<!-- funktion -->
Chat-Auswahl-Guard testen
<!-- ziel -->
Validiert dass der Chat-Auswahl-Guard korrekt funktioniert
<!-- regeln -->
- Command mto:agent:knowledge-space:chat-selection-guard-self-test
- json gibt strukturierte Daten aus
- Exit-Code 0 bei Status OK, sonst 1
<!-- testen -->
- Command mit --json Flag ausführen
- JSON mit status summary cases enthält
- Exit-Code entspricht Status
----------------------------------------
<!-- funktion -->
Fähigkeiten ermitteln
<!-- ziel -->
Listet verfügbare Resources, Tools und Prompts eines Admin-MCP-Servers auf
<!-- regeln -->
- Benötigt gespeicherten Admin-MCP-Server
- read liest Resource-URIs metadaten
- json maschinelles Format
- Status ready bei erfolgreicher Erkennung
- Runtime-Calls gezählt
<!-- testen -->
- Befehl mit gespeichertem Servernamen ausführen
- Ausgabe auf Status ready prüfen
- Resources, Tools und Prompts in Ausgabe
----------------------------------------
<!-- funktion -->
Hybrid-Tool-Auswahl testen
<!-- ziel -->
Stellt sicher, dass LLM-Vorschlag und RetrieX-Validierung bei der Tool-Auswahl zusammenarbeiten
<!-- regeln -->
- Planner besteht eigenen Self-Test
- AgentRunner nutzt LLM für Vorschlag vor RetrieX-Validierung
- Loader zeigt Status beim Tool-Auswählen
- Model-Config enthält dediziertes mcp_tool_selection-Modell
<!-- testen -->
- mto:agent:mcp:hybrid-tool-selection-planner-self-test --json ausführen
- JSON-Ausgabe auf status OK prüfen
- Alle vier Cases mit Status PASS vorhanden
----------------------------------------
<!-- funktion -->
MCP-Gateway trocken ausführen
<!-- ziel -->
Prüft ob eine Resource die Gateway-Policy erfüllt und welches Evidence erzeugt wird
<!-- regeln -->
- Kein MCP-Transport wird geöffnet
- fixture nutzt eingebaute Testdaten
- Exit-Code 0 bei Status ready
- json gibt Ergebnis als JSON aus
<!-- testen -->
- bin console mto agent mcp gateway-dry-run --fixture ausführen
- Ausgabe zeigt status ready
----------------------------------------
<!-- funktion -->
MCP-Laufzeit-Gate prüfen
<!-- ziel -->
Prüft ob ein MCP-Read-Only-Zugriff erlaubt ist, ohne Transport zu öffnen
<!-- regeln -->
- Gate ohne Transport öffnen
- fixture nutzt eingebaute Testdaten
- assume-enabled simuliert aktivierte MCP-Konfiguration
- JSON-Ausgabe verfügbar
<!-- testen -->
- Console-Befehl mit --json ausführen
- Exit-Code auf SUCCESS prüfen
- JSON auf Status ready und Fehler prüfen
----------------------------------------
<!-- funktion -->
MCP-Laufzeitgrenze testen
<!-- ziel -->
Validiert, dass MCP-Server nur Admin-gesteuert und begrenzt in der Antwort-Runtime verwendet werden
<!-- regeln -->
- McpDataRoomStore nur in Admin, CLI, ReadModels referenziert
- Keine externen MCP-Abrufe in Runtime-Boundary
- Keine Chat-, Retrieval- oder Evidence-Beruehrung
- Tool-Nachweise unterscheiden sichtbare Tool-Ausfuehrung
- Tool-first-Inhalts-Fallback bleibt im Statusprotokoll sichtbar
- Live-Inhalts-Fallback buendelt MCP-JSON-RPC-Resource-Reads
<!-- testen -->
- Rueckgabewert pruefen: Exitcode 0 bestaetigt OK
- Rueckgabewert pruefen: Exitcode 1 bestaetigt FAIL
----------------------------------------
<!-- funktion -->
MCP-Quelle prüfen
<!-- ziel -->
Prüft eine gespeicherte Admin-MCP-Quelle als registrierungskompatible Richtlinie
<!-- regeln -->
- Lokale Prüfung ohne MCP-Transport
- server wählt die gespeicherte Quelle
- audit-all prüft alle Quellen
- online führt einen Nur-Lese-Netzwerktest durch
- json gibt nur Metadaten aus
<!-- testen -->
- mto:agent:mcp:admin-source-check --server=my_source
- Ergebnis zeigt Status, Label und Fehler
----------------------------------------
<!-- funktion -->
Mit Agent chatten
<!-- ziel -->
Stellt Fragen an den KI-Agenten und empfängt Antworten in Echtzeit
<!-- regeln -->
- Streaming-Ausgabe jedes Tokens sofort
- Beendet auf Eingabe von exit
- Beendet auf Strg+C
- Leerer Prompt beendet
- user-id Standardwert ist cli
<!-- testen -->
- mto:agent:chat ausführen
- Frage eingeben
- Antwort tokenweise empfangen
- exit eingeben
- CLI beendet
----------------------------------------
<!-- funktion -->
Performance-Cache leeren
<!-- ziel -->
Leert alle RetrieX Runtime-Caches und den Python Vector-Service-Query-Cache
<!-- regeln -->
- Löscht den PHP Runtime-Cache
- Löscht optional den Vector-Service-Query-Cache
- skip-vector-service überspringt den Vector-Service-Cache
- json gibt maschinenlesbare Ausgabe aus
<!-- testen -->
- php bin console mto:agent:performance:cache:clear json
----------------------------------------
<!-- funktion -->
Produktlabel-Formatierer testen
<!-- ziel -->
Validiert die p465-Formatter-Extraktion ohne Laufzeiteffekte
<!-- regeln -->
- Liest nur lokale Quelldateien
- Ruft keine Chat-, Retrieval-, Shop-, MCP- oder LLM-Dienste auf
- Prüft AgentRunner-Delegation an ProductLabelFormatter
- Prüft Formatter-Besitz reiner Anzeige-Methoden
- Prüft Formatter-Besitz reiner Label-Methoden
- Prüft Abwesenheit von Runtime-Dependencies im Formatter
- Prüft Abwesenheit von p465-Methoden im AgentRunner
- Prüft Dokumentationsabdeckung in allen Referenzdateien
<!-- testen -->
- Produktlabel-Formatter-Selbsttest mit --json ausführen
- JSON zeigt status OK und 11 PASS
----------------------------------------
<!-- funktion -->
Release-Selbsttest bereitstellen
<!-- ziel -->
Validiert den v1.10.8 Release-Lock ohne Seiteneffekte im Chat-Pipeline, Agent-Runtime, Retrieval, Ranking, Shop, Followup, Evidence oder Truth Layer.
<!-- regeln -->
- Prüft 22 Release-Artefakte
- Prüft 8 zentrale Produktdokumente auf v1.10.8
- Prüft 16 Required-Release-Phrasen
- Liest nur lokale Dateien
- Führt keine Chat-, LLM- oder MCP-Live-Aufrufe aus
- Führt keine Retrieval-, Shop- oder Netzaufrufe aus
<!-- testen -->
- Ausgabe zeigt Status PASS
- JSON zeigt status OK und runtime_calls 0
----------------------------------------
<!-- funktion -->
Ressource abrufen
<!-- ziel -->
Liest eine erlaubte MCP-Ressource über die CLI ab
<!-- regeln -->
- Nur allowlisted Ressourcen abrufbar
- MIME-Typ geprüft
- Ergebnis enthält Status, Metadaten und Fehler
<!-- testen -->
- Befehl mit Optionen ausführen
- Kurzformat prüfen
- json Flag verwenden
- Status ready und Ressource vorhanden
----------------------------------------
<!-- funktion -->
Ressource validieren
<!-- ziel -->
Ermöglicht das Prüfen, ob eine MCP-Ressource_URI auf der Allowlist steht
<!-- regeln -->
- Prüft URI gegen konfigurierten Allowlist-Eintrag
- Prüft MIME-Type gegen erlaubte Typen des Eintrags
- Gibt Fehler und Warnungen aus
- CLI-Exit-Code spiegelt Entscheidung wider
<!-- testen -->
- CLI-Befehl mit --server und --resource-uri ausführen
- Option --mime-type ergänzen
- Exit-Code zeigt Ergebnis an
----------------------------------------
<!-- funktion -->
Rollencode-Zuweisung prüfen
<!-- ziel -->
Validiert die p465b-Rollencode-Zuweisungs-Grenzen ohne Laufzeiteffekte
<!-- regeln -->
- Prüft statische Artefakte nur
- Keine Chat-, Retrieval- oder LLM-Laufzeit
- Keine Shop-Suche
- Keine MCP-Aufrufe
- ProductLabelFormatter bleibt frei von Shop, Retrieval, Evidence und LLM
- AgentRunner bleibt Orchestrator
<!-- testen -->
- Befehl ausführen: mto:agent:architecture:product-role-code-assignment-guard-self-test
- Ausgabe auf Status OK prüfen
- Ergebnis: status = OK oder FAIL mit Details
----------------------------------------
<!-- funktion -->
Stabilitäts-Selbsttest bereitstellen
<!-- ziel -->
Validierung der Shop- und Follow-up-Ankerwächter vor dem Deployment
<!-- regeln -->
- Prüft REQUIRED_METHODS in AgentRunner.php
- Prüft keine FORBIDDEN_GUARD_MARKERS in Ankerwächter-Methoden
- Liest PATCH_MANIFEST.md und patch_history-Dateien
- Erzeugt Report ohne Runtime-Seiteneffekte
- json gibt maschinenlesbares Format aus
<!-- testen -->
- Binconsole mto:agent:shop-followup-anchor-stability-self-test
- Status OK prüfen
- passed größer 0 prüfen
- failed gleich 0 prüfen
- Binconsole mit --json
- JSON-Status OK prüfen
----------------------------------------
<!-- funktion -->
System rebuild bereitstellen
----------------------------------------
<!-- funktion -->
Vektordienst steuern
<!-- ziel -->
Steuert den Vektorsuchdienst über die Kommandozeile
<!-- regeln -->
- Rufzeit 300 Sekunden
- Standard-Host 0.0.0.0
- Standard-Port 8090
- Ruft Vector-Kontrollskript auf
<!-- testen -->
- Status wird angezeigt
----------------------------------------
<!-- funktion -->
Wissensbatch finalisieren testen
<!-- ziel -->
Prüft die Verknüpfung von Batch-Abschluss und Bericht ohne Seiteneffekte
<!-- regeln -->
- Prüft ohne Import, LLM-Aufruf oder Index-Rebuild
- Neun Prüfungen für Routing, Report und Abhängigkeiten
- Gibt FAIL bei einer einzigen fehlgeschlagenen Prüfung
- JSON-Ausgabe enthält status, scope, checks und failed
<!-- testen -->
- Command ausführen: mto:agent:knowledge:batch-finalize-self-test
- Ausgabe zeigt status OK oder FAIL
- JSON-Flag setzen: --json
- JSON enthält status und failed-Liste
----------------------------------------
<!-- funktion -->
MCP-Allowlist prüfen

43
daten/RAG/anleitungen.md Normal file
View File

@@ -0,0 +1,43 @@
<!-- titel -->
Anleitungen
----------------------------------------
<!-- art -->
kern
----------------------------------------
<!-- ziel -->
Zeigt Anleitungen als gerendertes Markdown im Admin-Bereich
----------------------------------------
<!-- regeln -->
- Erste H1 als Titel
- Ohne H1 wird Slug als Titel verwendet
- H1 erscheint doppelt: als Titel und im gerenderten Inhalt
----------------------------------------
<!-- funktion -->
Anleitung anzeigen
<!-- ziel -->
Liest eine Markdown-Datei und zeigt sie als HTML gerendert an
<!-- regeln -->
- Ungültiger Slug wirft 404
- Fehlende Datei wirft 404
- Titel aus erster H1 oder aus Slug
- Markdown als HTML gerendert
<!-- testen -->
- admin guides beispiel-seite aufrufen
- Titel und HTML-Inhalt sehen
- admin guides ungueltig aufrufen
- 404-Fehlerseite
----------------------------------------
<!-- funktion -->
Anleitungsübersicht öffnen
<!-- ziel -->
Listet alle verfügbaren Anleitungen zur schnellen Auswahl auf
<!-- regeln -->
- Markdown-Dateien aus bestimmtem Ordner
- Erste Überschrift als Titel
- Ohne Ordner wird leere Liste angezeigt
- Alphabetisch nach Titel sortiert
- Ungültige Dateinamen ignoriert
<!-- testen -->
- Admin-Bereich öffnen
- Klick auf Anleitungen
- Anleitungen alphabetisch aufgelistet

94
daten/RAG/auftraege.md Normal file
View File

@@ -0,0 +1,94 @@
<!-- titel -->
Aufträge
----------------------------------------
<!-- art -->
kern
----------------------------------------
<!-- ziel -->
Verarbeitet asynchrone Aufgaben mit Verfolgung von Status und Ereignissen
----------------------------------------
<!-- regeln -->
- ROLE_ADMIN_AREA für Admin-Statusabruf erforderlich
- Admin-Bereich für Ereignisse gilt ohne Events-Endpunkt
- Antwort enthält lastEventId, updatedAt, startedAt, completedAt, runningStaleAfterSeconds
- Vollständiger Kontext muss explizit angefordert werden, Standard ist eingeschränkter Kontext
- Abgeschlossene, fehlgeschlagene und abgebrochene Ingest-Jobs werden mit Exitcode SUCCESS beendet
- Dry-Run gibt im Output einen Hinweis zum Zwischenspeicher-Status aus
- Ingest-Job-Verarbeitung leert PHP-Laufzeit- und Vector-Service-Cache nach Abschluss
----------------------------------------
<!-- funktion -->
Auftrag starten
<!-- ziel -->
Übermittelt einen Prompt zur asynchronen Verarbeitung im Hintergrund
<!-- regeln -->
- Leere Prompts werden abgelehnt
- Prompts über dem Zeichenlimit werden abgelehnt
- Ungültige Upload-IDs werden abgelehnt
- Job-ID besteht aus 48 Hexadezimalzeichen
- Erstellt Job mit Status pending
<!-- testen -->
- POST ask-jobs mit gültigem Prompt
- Antwort enthält jobId
- Statuscode ist 200
- POST ask-jobs mit leerem Prompt
- Statuscode ist 400
- POST ask-jobs mit zu langem Prompt
- Statuscode ist 413
----------------------------------------
<!-- funktion -->
Auftragsereignisse abrufen
<!-- ziel -->
Ermöglicht dem Client, Auftragsereignisse zu pollen
<!-- regeln -->
- Unbekannte Auftrags-ID gibt HTTP 404 zurück
- Veraltete laufende Aufträge werden als fehlgeschlagen markiert
- Ereignisse nach after werden zurückgegeben
<!-- testen -->
- GET ask-jobs jobId events after 0
- Antwort enthält status, events, lastEventId
- Unbekannte jobId gibt HTTP 404
----------------------------------------
<!-- funktion -->
Auftragsstatus anzeigen
<!-- ziel -->
Ermöglicht dem Admin, den aktuellen Status eines Auftrags ohne Seitenreload zu prüfen
<!-- regeln -->
- ROLE_ADMIN_AREA erforderlich
- JSON mit id, type, status, startedAt, finishedAt, errorMessage, logPath
- 404 bei unbekannter Auftrags-ID
<!-- testen -->
- Detailseite eines Auftrags öffnen
- Statusabfrage per AJAX auslösen
- JSON mit status und Zeitstempeln empfangen
----------------------------------------
<!-- funktion -->
Auftragsstatus prüfen
<!-- ziel -->
Ermöglicht das Prüfen des aktuellen Zustands eines Auftrags
<!-- regeln -->
- jobId ist eine 48-stellige Hexadezimalzahl
- Ungültige jobId liefert Status missing
- Abgelaufene Aufträge werden als failed markiert
- Antwort enthält status, message, timestamps und serverTime
<!-- testen -->
- ask-jobs-Endpunkt mit jobId aufrufen
- JSON-Antwort mit Status prüfen
- Status pending, running, completed, interrupted oder failed zurückgegeben
----------------------------------------
<!-- funktion -->
Ingest-Job starten
<!-- ziel -->
Startet einen bereitgestellten Ingest-Job über seine UUID
<!-- regeln -->
- Job-ID als Pflichtargument erforderlich
- Job-Status muss ausführbar sein
- Abgeschlossene Jobs werden nicht erneut ausgeführt
- Dry-Run vermeidet Seiteneffekte
- Fehler führen zu FAILURE-Exitcode
<!-- testen -->
- Befehl mit gültiger Job-ID ausführen
- Job wird verarbeitet
- Erfolgsmeldung erscheint
----------------------------------------
<!-- funktion -->
Ingest-Jobs auflisten

97
daten/RAG/backup.md Normal file
View File

@@ -0,0 +1,97 @@
<!-- titel -->
Backup
----------------------------------------
<!-- art -->
kern
----------------------------------------
<!-- ziel -->
Erstellt, prüft und stellt Backups bereit
----------------------------------------
<!-- regeln -->
- POST-Routen erfordern CSRF-Token
- Super Admin darf Restore-Prüfungen ausführen
- Flash-Meldung zeigt Status nach jeder Aktion
- Alle Prüfungen werden protokolliert
- Backup-IDs folgen dem Muster retriex-backup-Datum-Uhrzeit
- json gibt maschinenlesbare Ausgabe zurück
- Ungueltiges CSRF-Token zeigt Flash-Meldung
- Backup herunterladen mit "Neueste" waehlt aktuelles Backup
- Operator-Report als JSON exportieren
- Deployment-Scheduler-Pruefung mit Beschreibung in Flash-Meldung
- Sicherheit-und-Datenschutz-Pruefung mit Beschreibung in Flash-Meldung
- Backup-Dry-Run erwähnt fehlende Dateischreibweise in Flash-Meldung
----------------------------------------
<!-- funktion -->
Backup herunterladen
<!-- ziel -->
Ermöglicht dem Super Admin das Herunterladen einer Sicherungsdatei
<!-- regeln -->
- Nur Super Admin
- Backup-ID aus Liste oder "Neueste"
- Fehlgeschlagener Download zeigt Flash-Meldung
- Erfolgreicher Download protokolliert OperatorCheckProtocol
<!-- testen -->
- admin_enterprise_operations öffnen
- Backup-ID aus Liste auswählen
- Klick auf Download-Button
- Datei im Browser heruntergeladen
----------------------------------------
<!-- funktion -->
Backup starten
<!-- ziel -->
Erstellt eine Sicherungskopie mit allen konfigurierten Quellen
<!-- regeln -->
- CSRF-Token erforderlich
- Ablauf im Operator-Protokoll
- Flash-Meldung je nach Status
- Database-Dump als Option
<!-- testen -->
- Backup-Endpunkt per POST aufrufen
- _token und include_database_dump senden
- Flash-Meldung zeigt Status
- Backup erstellt
----------------------------------------
<!-- funktion -->
Backup-Tauglichkeit prüfen
<!-- ziel -->
Prüft ob das System alle Voraussetzungen für Backup und Restore erfüllt
<!-- regeln -->
- JSON-Ausgabe mit --json
- Tabelle zeigt alle Testfälle
- Rückgabe SUCCESS bei Status OK
<!-- testen -->
- Befehl mto:agent:enterprise:backup-restore-readiness ausführen
- Tabelle mit Testfällen prüfen
- Rückgabe SUCCESS bei Status OK
----------------------------------------
<!-- funktion -->
Backup-Trockenlauf prüfen
<!-- ziel -->
Prüft die Wiederherstellbarkeit eines Backups ohne tatsächlichen Restore
<!-- regeln -->
- Nur Super-Admin darf Funktion ausführen
- CSRF-Token wird geprüft
- Report wird im Prüfprotokoll aufgezeichnet
- Flash-Nachricht zeigt Status, Backup-ID und Anzahl Warnungen sowie Fehler
<!-- testen -->
- POST an admin enterprise restore dry-run latest
- Erfolg: Redirect zur Übersicht mit Flash-Nachricht
- Fehler: Redirect zur Übersicht mit Fehlermeldung
----------------------------------------
<!-- funktion -->
Backup-Wiederherstellung prüfen
<!-- ziel -->
Prüft ob Backup-Pfad, Restore-Nachweis und Betreiberhinweise vorhanden sind
<!-- regeln -->
- CSRF-Token erforderlich
- Flash-Meldung zeigt Zusammenfassung
- Protokolliert den Check
<!-- testen -->
- POST an /admin/enterprise/check/backup-restore
- ungültiges CSRF-Token senden
- Weiterleitung mit Fehlermeldung
- gültiges CSRF-Token senden
- Weiterleitung mit Zusammenfassung
----------------------------------------
<!-- funktion -->
Operatorbericht als JSON exportieren

89
daten/RAG/benutzer.md Normal file
View File

@@ -0,0 +1,89 @@
<!-- titel -->
Benutzer
----------------------------------------
<!-- art -->
kern
----------------------------------------
<!-- ziel -->
Ermöglicht Super-Admins die Verwaltung aller Benutzer des Systems
----------------------------------------
<!-- regeln -->
- POST-Requests erfordern gültiges CSRF-Token
- Mindestens eine Rolle erforderlich
- Statusfilter akzeptiert nur all, active, inactive
- Statusfilter default bei ungueltigem Wert ist all
- Rollefilter default bei ungueltiger Rolle ist all
- Vorname ist optional
- Abteilung ist optional
- Passwort ist erforderlich
- is_active ist standardmaessig inaktiv
- Rollen werden als Array uebergeben
- Bearbeiten erfordert aktuellen Benutzer
- Benutzer-ID muss UUID-Format haben
----------------------------------------
<!-- funktion -->
Benutzer aktivieren
<!-- ziel -->
Aktiviert oder deaktiviert einen Benutzer sofort
<!-- regeln -->
- Nur ROLE_SUPER_ADMIN hat Zugriff
- POST mit gültigem CSRF-Token nötig
- Umschalten zwischen Aktiv und Inaktiv
- Redirect zur Übersicht nach Umschalten
<!-- testen -->
- Admin-Übersicht öffnen
- LMB auf Toggle-Button eines aktiven Benutzers
- Benutzer ist inaktiv
----------------------------------------
<!-- funktion -->
Benutzer anlegen
<!-- ziel -->
Legt einen Anwendungsbenutzer über die Konsole an
<!-- regeln -->
- E-Mail gültig und einmalig
- Passwort mindestens 8 Zeichen
- Passwort zweimal identisch eingegeben
- Mindestens eine Rolle zugewiesen
<!-- testen -->
- mto-agent-user-create aufrufen
- Ungültige E-Mail eingeben
- Validierungsfehler erwarten
- Gültige E-Mail eingeben
- Zu kurzes Passwort eingeben
- Validierungsfehler erwarten
- Passwort eingeben
- Anderes Passwort wiederholen
- Validierungsfehler erwarten
- Passwort wiederholen
- Rollen auswählen
- Benutzeraktivierung bestätigen
- Ausgabe zeigt E-Mail Rolle Aktiv
----------------------------------------
<!-- funktion -->
Benutzer auflisten
<!-- ziel -->
Zeigt admin-seitig alle Benutzer mit Filteroptionen an
<!-- regeln -->
- Nur Super-Admin darf zugreifen
- Filterung nach Suchbegriff, Status und Rolle
- Standardfilterung ohne Parameter zeigt alle
<!-- testen -->
- Seite /admin/users aufrufen
- Seite zeigt Benutzerliste mit Rollen und Status
- Filter auf Status "aktiv" setzen
- Liste zeigt nur aktive Benutzer
----------------------------------------
<!-- funktion -->
Benutzer bearbeiten
<!-- ziel -->
Änderungen an Benutzerdaten speichern
<!-- regeln -->
- Super Admin erforderlich
- CSRF-Token erforderlich
- E-Mail muss gültig sein
- Neues Passwort optional
<!-- testen -->
- Bearbeitungsseite für Benutzer öffnen
- Formular mit geänderten Daten absenden
- Erfolgsmeldung anzeigen
- Zur Übersicht redirect

90
daten/RAG/berichte.md Normal file
View File

@@ -0,0 +1,90 @@
<!-- titel -->
Berichte
----------------------------------------
<!-- art -->
kern
----------------------------------------
<!-- ziel -->
Berichte aus Enterprise-Diensten generieren, herunterladen und löschen
----------------------------------------
<!-- regeln -->
- Rolle SUPER_ADMIN statt EDITOR für Bericht als JSON herunterladen erforderlich
- Akzeptanzbericht generieren fehlt Ausgabe als Markdown für Nachweisprotokoll
- FAIL-Status: Exitcode 1 nur im JSON-Modus
- Operatorbericht als Markdown herunterladen fehlt als Funktion
----------------------------------------
<!-- funktion -->
Akzeptanzbericht generieren
<!-- ziel -->
Zeigt zusammengefasste Ergebnisse der Operator-Prüfungen im Terminal
<!-- regeln -->
- markdown-Option gibt Markdown aus
- json-Option gibt JSON aus
- Standardausgabe formatiert als Tabelle
<!-- testen -->
- Kommando mit markdown-Option ausführen
- Markdown-Tabelle erscheint
----------------------------------------
<!-- funktion -->
Bericht als JSON herunterladen
<!-- ziel -->
Exportiert einen Batch-Bericht als JSON-Datei zum Speichern
<!-- regeln -->
- Rolle EDITOR erforderlich
- Format muss json oder md sein
- Datei muss existieren
<!-- testen -->
- Batch-Übersicht öffnen
- LMB auf JSON-Download-Link eines Batch-Berichts
- Datei wird heruntergeladen
----------------------------------------
<!-- funktion -->
Bericht löschen
<!-- ziel -->
Entfernt einen nicht mehr benötigten Batch-Bericht dauerhaft
<!-- regeln -->
- POST-Methode erforderlich
- CSRF-Token obligatorisch
- Nur ROLE_EDITOR hat Zugriff
- Erfolgs- oder Warnmeldung als Flash
- Zur Berichte-Übersicht weitergeleitet
<!-- testen -->
- Batch-Bericht öffnen
- Löschen-Schaltfläche klicken
- Bestätigungsdialog bestätigen
- Zur Übersicht weitergeleitet
- Flash-Meldung 'gelöscht' sichtbar
----------------------------------------
<!-- funktion -->
Berichte auflisten
<!-- ziel -->
Zeigt alle Batch-Importe zur Übersicht
<!-- regeln -->
- listReports liefert alle Berichte
- defaultReportDir wird übergeben
<!-- testen -->
- GET admin knowledge-batches aufrufen
- Seite mit Berichtsliste wird angezeigt
----------------------------------------
<!-- funktion -->
Übersicht rendern
<!-- ziel -->
Zeigt den Betriebszustand aller Enterprise-Komponenten auf einen Blick
<!-- regeln -->
- Baut Berichte aller Enterprise-Dienste
- json gibt maschinenlesbares JSON aus
- Ohne Flag formatiert SymfonyStyle-Ausgabe
- FAIL-Status gibt Exitcode 1 zurück
<!-- testen -->
- php bin console mto:agent:enterprise:operations-overview --json
- JSON-Ausgabe enthält Felder status, progress, components, primary_action
- Exitcode ist 1 bei Status FAIL
----------------------------------------
<!-- funktion -->
Operatorbericht als Markdown herunterladen
----------------------------------------
<!-- funktion -->
Batch-Bericht hochladen
----------------------------------------
<!-- funktion -->
Batch-Bericht anzeigen

53
daten/RAG/chat.md Normal file
View File

@@ -0,0 +1,53 @@
<!-- titel -->
Chat
----------------------------------------
<!-- art -->
kern
----------------------------------------
<!-- ziel -->
Ermöglicht authentifizierten Nutzern, Prompts zu senden und Antworten als Event-Stream zu empfangen
----------------------------------------
<!-- regeln -->
- Eigener Login-Bereich
- Upload-IDs werden gegen Client-ID validiert
- CHAT_HANDBOOK.md wird aus Projektverzeichnis gelesen
- Fehlende oder leere CHAT_HANDBOOK.md als nicht verfügbar
- Anonym bei fehlendem Namen
- Handbook maximal 512 KB
----------------------------------------
<!-- funktion -->
Abmelden ermöglichen
<!-- testen -->
- Abmelden wählen
- Anmeldung beendet
----------------------------------------
<!-- funktion -->
Chat streamen
<!-- ziel -->
Liefert eine Chat-Antwort als kontinuierlichen Datenstrom im Browser
<!-- regeln -->
- POST mit Prompt im Body
- Prompt zu lang: Stream mit error-Event
- Prompt leer: Stream mit error-Event
- Upload-IDs werden validiert
- Antwort im Format text event-stream
- retry: 3000 bei Verbindungsproblemen
<!-- testen -->
- Ask-SSE-Endpunkt mit gültigem Prompt ansprechen
- Daten-Chunks aus Stream empfangen
- event: done als Abschluss
----------------------------------------
<!-- funktion -->
Chatseite anzeigen
<!-- ziel -->
Zeigt die Chatseite mit Prompt-Benutzername und Wissensraum-Auswahl
<!-- regeln -->
- Liest CHAT_HANDBOOK.md aus dem Projektverzeichnis
- Fehlende oder leere Datei wird als nicht verfügbar behandelt
- Eigene Vorlage: chat index.html.twig
<!-- testen -->
- GET chat aufrufen
- Seite rendert ohne Fehler
- Prompt-Benutzername ist Anonym
- Wissensraum-Auswahl wird angezeigt
- Handbuch-Verfügbarkeit wird angezeigt

386
daten/RAG/diagnose.md Normal file
View File

@@ -0,0 +1,386 @@
<!-- titel -->
Diagnose
----------------------------------------
<!-- art -->
kern
----------------------------------------
<!-- ziel -->
Ermöglicht Betreibern die Überprüfung von Systemzustand, Konfiguration und Datenqualität ohne produktive Änderungen
----------------------------------------
<!-- regeln -->
- Alle Diagnosekommandos bieten --json-Ausgabe
- Exit-Code 0 spiegelt Status OK wider
- Exit-Code 1 zeigt Fehler oder Befunde an
- Diagnoseoperationen verändern keine Daten
- Kein Diagnosebefehl ruft LLM während der Prüfung auf
- MAX_LINE_BYTES = 2097152
- limit-Option: Standardwert 10
- Diagnostic-only-Hinweis im Footer
- Patchline-Pruefung bei Architecture Release Lock
- Genau ein AgentRunner-Boundary-Call sichtbar
----------------------------------------
<!-- funktion -->
Abdeckung prüfen
<!-- ziel -->
Prüft ob gespeicherte Ingest-Facts eine Anfrage abdecken
<!-- regeln -->
- Query muss nicht leer sein
- index.ndjson muss existieren
- Treffer nach State-Rang und Score sortiert
- json gibt maschinenlesbare Daten zurück
<!-- testen -->
- Command mit Query ausführen
- Diagnoseausgabe mit Abdeckungsdaten prüfen
- Command mit --json ausführen
- JSON-Struktur mit rows und summary prüfen
- Command mit --min-score und --limit ausführen
- Gefilterte Treffer nach Score geordnet
----------------------------------------
<!-- funktion -->
Abrufgrenzen prüfen
<!-- ziel -->
Prüft ob Retrieval-Architekturgrenzen eingehalten werden
<!-- regeln -->
- Required-Dateien sind vorhanden
- AgentRunner besitzt Retrieval-Orchestrierung
- Genau ein Boundary-Call in AgentRunner
- Boundary-Service bleibt dünn
- Flow bleibt trace-only
- ReadModel bleibt beschränkt
- PostCallFocus erweitert keinen Constructor
- RetrieverInterface bleibt minimal
- HybridRetriever bleibt in Retrieval-Layer
- Dokumentation sichert Architecture Release Lock
<!-- testen -->
- Command ausführen mit --json
- Ausgabe zeigt Status OK
----------------------------------------
<!-- funktion -->
Bereitschaftsbericht ausgeben
<!-- ziel -->
Prüft Bereitschaft von Deployment und Scheduler und zeigt Ergebnisse an
<!-- regeln -->
- json-Option gibt maschinenlesbares JSON aus
- Standardausgabe zeigt DefinitionList und Tabelle
- Exit-Code 0 bei Status OK
<!-- testen -->
- Befehl mit --json ausführen
- JSON-Ausgabe validieren
- Befehl ohne Option ausführen
- Tabelle mit Status OK prüfen
----------------------------------------
<!-- funktion -->
Datenqualität prüfen
<!-- ziel -->
Prüft die Qualität von Text-Chunks im Original und nach Markdown-Umsetzung auf Datenverluste
<!-- regeln -->
- Liest ausschließlich ohne Index-Schreibzugriff
- Kein Ranking und keine Antwortlogik
- Argument document-id ist erforderlich
- Option version wählt eine bestimmte Dokumentversion
- Option json liefert maschinenlesbare Ausgabe
<!-- testen -->
- php bin/console mto:agent:ingest:chunk-quality {uuid}
- Ausgabe zeigt Titel und ausgewählte Version
- Abschnitt Original zeigt Chunk-Profil
- Abschnitt Markdown zeigt Chunk-Profil
- Abschnitt Vergleich zeigt Kennzahlen und Delta
- Abschnitt Interpretation zeigt strukturierte Hinweise
----------------------------------------
<!-- funktion -->
Diagnose anzeigen
<!-- ziel -->
Ermöglicht dem Administrator die MCP-Anbindungen und Wissensraum-Inhalte zu prüfen und zu verwalten
<!-- regeln -->
- Nur Super-Administratoren haben Zugriff
- Vier Ansichten: Übersicht, Anbindung, Wissensräume, System
- Vorgangsvarianten werden als Flash-Meldungen zurückgemeldet
- Formularaktionen erfordern gültiges CSRF-Token
- Transienter Zustand wird über POST-GET-Redirect erhalten
<!-- testen -->
- Admin-MCP-Seite aufrufen
- Route antwortet mit Status 200
- Übersicht der Anbindungen sichtbar
----------------------------------------
<!-- funktion -->
Genre-Hotspots prüfen
<!-- ziel -->
Entdeckt genrespezifische Begriffe im Kerncode, die das Sprachmodell beeinflussen können
<!-- regeln -->
- Ergebnis ist schreibgeschützt
- json gibt maschinenlesbare Daten aus
- strict gibt Nicht-Null-Exit-Code bei Befunden aus
- details zeigt alle Ergebniszeilen
<!-- testen -->
- Terminal öffnen
- Befehl ohne Parameter ausführen
- Zusammenfassung mit Befunden erscheint
----------------------------------------
<!-- funktion -->
Guard-Mechanismus prüfen
<!-- ziel -->
Prüft die Guard-Implementierung ohne Seiteneffekte
<!-- regeln -->
- Liest nur lokale Dateien
- Keine Runtime-Aufrufe
- Prüft 8 Testfälle
<!-- testen -->
- Befehl ausführen: mto:agent:proofing:unsupported-tool-term-guard-self-test
- Status OK in JSON-Ausgabe prüfen
- Alle 8 Testfälle bestanden
----------------------------------------
<!-- funktion -->
Hygieneregeln prüfen
<!-- ziel -->
Prüft ob Batch-Hygieneregeln korrekt implementiert und nicht in die Antwortlaufzeit verdrahtet sind
<!-- regeln -->
- source_noise_cleaner_entfernt_PDF_Restzeichen
- validator_entfernt_QUELLSEGMENT_Echo
- directory_cleanup_entfernt_leere_Admin-Upload_Ordner
- directory_cleanup_verschiebt_Legacy_failed_Eintraege
- batch_reports_sind_loeschbar_mit_CSRF_Schutz
- document_delete_bereinigt_physische_Dateien
- failed_skipped_resolvt_ausserhalb_inbox
- keine_Hygiene_Dienste_in_Antwortlaufzeit_verdrahtet
<!-- testen -->
- Befehl `mto:agent:knowledge:batch-hygiene-self-test` ausfuehren
- JSON-Ausgabe pruefen
- Alle Faelle Status OK
----------------------------------------
<!-- funktion -->
Indizierungszustand prüfen
<!-- ziel -->
Prüft automatisch, ob der verzögerte Indizierungsvertrag in allen Dateien korrekt umgesetzt ist
<!-- regeln -->
- Liest Controller, Services, Repositories und Templates ein
- Prüft 19 Vertragsfälle zum verzögerten Indizierungsworkflow
- Gibt Zusammenfassung oder strukturiertes JSON aus
- Gibt Erfolgscode bei allen bestandenen Fällen zurück
<!-- testen -->
- Befehl mit --json ausführen
- Ausgabe auf "OK" oder "FAILED" prüfen
- FEHLGESCHLAGEN: Case entfernt ungültigen startJob-Aufruf in activateVersion
- FEHLGESCHLAGEN: Case entfernt verzögertes setCurrentVersion in documentService
- Alle Cases bestanden: Rückgabewert Command::SUCCESS
----------------------------------------
<!-- funktion -->
Konfiguration prüfen
<!-- ziel -->
Prüft ob alle RetrieX-Einstellungen gültig sind und zeigt Fehler oder Warnungen an
<!-- regeln -->
- mto:agent:config:validate als Konsolenbefehl verfügbar
- Gibt Fehler und Warnungen aus
- json gibt maschinenlesbares Ergebnis aus
- Rückgabewert 0 bei gültiger Konfiguration
- Rückgabewert 1 bei ungültiger Konfiguration
- Regression Guardrails werden geprüft
<!-- testen -->
- php bin/console mto:agent:config:validate ausführen
- Ergebnis auf Fehler und Warnungen prüfen
- Exitcode prüfen
- Konfiguration gültig: Exitcode 0
- php bin/console mto:agent:config:validate --json ausführen
- JSON-Ausgabe enthält status, errors, warnings
----------------------------------------
<!-- funktion -->
Provenienz prüfen
<!-- ziel -->
Prüft ob Provenienzdiagnostik ohne Ranking- oder Antwortwirkung funktioniert
<!-- regeln -->
- Liest Ingest-Service, Admin-Inspektor, Template und Styles
- Prüft Provenienz-Metadaten in NDJSON
- Prüft Diagnostik-Metadaten in NDJSON
- Prüft Sidecar-Lesen ohne LLM-Aufruf
- Prüft Admin-Ansicht mit Provenienz und Heading-Pfad
- Prüft Read-only-Label in UI
- Prüft Kein Antwort-Runtime-Einfluss
- Prüft Genre-Neutralität der Diagnostik
<!-- testen -->
- mto agent ingest markdown-derived-chunk-diagnostics-self-test ausführen
- Kommando endet erfolgreich
- Ausgabe zeigt status OK
- Alle 8 Cases zeigen status OK
----------------------------------------
<!-- funktion -->
Retrieval-Pipeline testen
<!-- ziel -->
Ermöglicht die Überprüfung der Retrieval-Pipeline mit detailliertem Debug-Output
<!-- regeln -->
- Prompt als Argument erforderlich
- Leerer Prompt erzeugt Fehler
- json gibt Rohdaten ohne Formatierung aus
- show-text zeigt ungekürzte Chunk-Texte
<!-- testen -->
- mto:agent:retrieval:test mit Prompt ausführen
- Ausgabe auf Pipeline-Summary prüfen
- mto:agent:retrieval:test --json ausführen
- JSON-Struktur mit duration_ms und results vorhanden
----------------------------------------
<!-- funktion -->
Retrieval-Qualität testen
<!-- ziel -->
Prüft ob die Konfiguration passende Inhalte findet
<!-- regeln -->
- Erfordert ROLE_KNOWLEDGE_ADMIN
- GET zeigt leere Maske
- POST führt TestRetrieval aus
<!-- testen -->
- Seite test-retrieval öffnen
- Prompt eingeben und absenden
- Ergebnisse werden angezeigt
----------------------------------------
<!-- funktion -->
Segmentierung prüfen
<!-- ziel -->
Prüft ob Segmentierung korrekt ausgeführt wird
<!-- regeln -->
- Liest alle relevanten Dateien ein
- Prüft required_files_present
- Prüft execution_uses_complete_segment_plan
- Prüft segment_prompt_is_fragment_only
- Prüft llm_called_per_segment_with_long_transport
- Prüft sidecar_records_segment_processing_without_raw_payloads
- Prüft generated_version_remains_inactive
- Prüft no_index_or_answer_runtime_wiring
- Prüft no_genre_runtime_gate_markers
- Report enthält Status und Zusammenfassung
<!-- testen -->
- mto:agent:ingest:segmented-markdown-normalization-self-test ausführen
- Status OK oder FAIL prüfen
- Testläufe als Zusammenfassung sehen
----------------------------------------
<!-- funktion -->
Shop-Suche testen
<!-- ziel -->
Prüft die Shop-Suche mit detaillierter Ausgabe von Suchvorschau, Ergebnissen und optionalem Repair
<!-- regeln -->
- Standard-Query: 'zeige mir produkte unter 5000 euro'
- repair aktiviert zusätzliche Search-Repair-Suche
- json gibt maschinenlesbare Ausgabe aus
- Primärsuche wird immer ausgeführt
<!-- testen -->
- mto:agent:test:shop-search ausführen
- Suchvorschau und Primärergebnisse prüfen
- mto:agent:test:shop-search laptop --repair ausführen
- Repair-Auswertung mit Ergebnissen prüfen
- mto:agent:test:shop-search laptop --json ausführen
- JSON-Ausgabe mit query, preview und repair prüfen
----------------------------------------
<!-- funktion -->
Sicherheitsstatus prüfen
<!-- ziel -->
Prüft den Security- und Datenschutz-Bereitschaftsgrad ohne Seiteneffekte
<!-- regeln -->
- Kommandoname: mto:agent:enterprise:security-data-protection-readiness
- json gibt maschinenlesbares JSON aus
- Erfolg bei Status OK
- Fehler bei Status nicht OK
<!-- testen -->
- php bin/console mto:agent:enterprise:security-data-protection-readiness
- Tabelle mit Cases wird ausgegeben
- php bin/console mto:agent:enterprise:security-data-protection-readiness --json
- JSON-Objekt wird ausgegeben
----------------------------------------
<!-- funktion -->
Stapel-Upload verifizieren
<!-- ziel -->
Prüft die Integrität des Batch-Upload-Systems ohne Dokumente zu importieren
<!-- regeln -->
- Trockenlauf ohne Dokumentenimport
- Ruft kein LLM auf
- Ändert keine Daten im Wissensspeicher
- Gibt maschinenlesbares JSON aus
- Prüft CSRF-Schutz, Dienstintegration und Fortschrittsanzeige
- Prüft PDF-Schutz, Speichergrenzen und Leerraum-Bereinigung
- Prüft CLI-Progress und keine Agent-Verschaltung
<!-- testen -->
- PHP-Befehl aus src Command KnowledgeBatchAdminUploadSelfTestCommand.php ausführen
- Alle 10 Cases in der Tabelle prüfen
- Jeder Case zeigt Status OK oder FAIL
----------------------------------------
<!-- funktion -->
Systemzustand prüfen
<!-- ziel -->
Prüft NDJSON und FAISS-Konsistenz
<!-- regeln -->
- Gibt JSON aus
- OK-Status gibt SUCCESS zurück
- Fehler-Status gibt FAILURE zurück
<!-- testen -->
- php bin console mto:agent:vector:health
- Konsistente Indices: OK-Status in JSON
- Fehler: FAILURE und Fehlermeldung in JSON
----------------------------------------
<!-- funktion -->
Tag-Index-Gesundheit prüfen
<!-- ziel -->
Prüft die Konsistenz zwischen Tag-Daten und FAISS-Index
<!-- regeln -->
- summary zeigt Zusammenfassung statt JSON
- Exitcode 0 bei Status OK oder OK_EMPTY
- Exitcode 1 bei allen anderen Status
- Prüft tags_ndjson_exists, vector_exists, meta_valid
<!-- testen -->
- bin console mto:agent:tag:health ausführen
- LMB auf --summary
- Zusammenfassung mit Status, Zählern und Gültigkeit
- Befehl mit Exitcode 0 beendet
----------------------------------------
<!-- funktion -->
Tags neu indizieren
<!-- ziel -->
Echtzeitaktualisierung des Neuindizierungsfortschritts im Browser
<!-- regeln -->
- Lädt alle 2 Sekunden neuen Status
- Sendet Ereignis nur bei Hash-Änderung
- Sendet Keepalive alle 10 Sekunden
- Endet bei Verbindungsabbruch
<!-- testen -->
- Stream-Endpunkt aufrufen
- MIME-Typ text event stream prüfen
- Antwortet mit Stream
- Event-Stream antwortet
----------------------------------------
<!-- funktion -->
Verankerung testen
<!-- ziel -->
Diagnostiziert die Verankerung von Nachfolgedialogen in der Historie
<!-- regeln -->
- Kein Laufzeitverhalten geändert
- Report als Text oder JSON ausgebbar
- sample führt integrierten Smoke-Test aus
- Historie aus Datei, STDIN oder Option lesbar
<!-- testen -->
- Sample-Dry-Run ausführen
- Text-Report auf erkennbaren Anker prüfen
- Kernprodukt und Nachfolgedialog identifizieren
----------------------------------------
<!-- funktion -->
Vergleichsgrenzen validieren
<!-- ziel -->
Prueft, ob alle Dateien und Muster fuer die Chunk-Qualitaetsvalidierung vorhanden sind
<!-- regeln -->
- Liest src Service Admin DocumentChunkQualityComparisonService php
- Liest src Command IngestChunkQualityCommand php
- Liest src Controller Admin DocumentController php
- Liest templates admin document show html twig
- Liest public assets styles base css
- Liest DocumentMarkdownSegmentationPlanService php
- Liest AgentRunner php und KnowledgeIngestService php
- Prueft Dateien auf vorhandene Strings mit str_contains
- Prueft Dateien auf abwesende Strings mit !str_contains
- Gibt FAIL zurueck sobald ein Fall fehlschlaegt
- x-Icon
<!-- testen -->
- php bin console mto agent ingest chunk-quality-self-test ausfuehren
- Rueckgabe auf vollstaendige Faelle pruefen
- Status OK bei allen gueltigen Implementierungen
----------------------------------------
<!-- funktion -->
Titelvorschläge-Selbsttest bereitstellen
----------------------------------------
<!-- funktion -->
Konfigurationsquellen prüfen
----------------------------------------
<!-- funktion -->
Batch-Markdown-Normalisierung testen

Some files were not shown because too many files have changed in this diff Show More