Compare commits

...

9 Commits

Author SHA1 Message Date
team3
c6bbed4049 update 2026-09-07 07:44:00 +02:00
team3
5d42b4cfc5 update 2026-07-29 15:19:41 +02:00
team3
2e86d682eb update 2026-07-28 12:31:42 +02:00
team3
58efba988d update 2026-07-28 08:36:49 +02:00
team3
7912eae715 update 2026-07-27 16:43:11 +02:00
team3
23c908534f update 2026-07-27 13:42:59 +02:00
team3
41185eaccc update 2026-07-27 12:47:32 +02:00
team3
1c8a4fd4fd update 2026-07-27 11:29:57 +02:00
team3
15ad6dfe8a update 2026-07-27 08:04:32 +02:00
245 changed files with 24648 additions and 692 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.

122
README.md
View File

@@ -9,13 +9,125 @@ Rein manuelle Pflege: Sidebar links = Projekte, Hauptbereich = Feature-Cards.
## Karten prüfen ## Karten prüfen
Das ⌾-Icon in der Sidebar prüft alle Karten des aktiven Projekts gegen Das ⌾-Icon in der Sidebar prüft alle Karten des aktiven Projekts gegen den Stil-Katalog.
`backend/stil.md` (eine Anfrage je Karte). Fehlerhafte Karten öffnen sich rot markiert Fehlerhafte Karten öffnen sich rot markiert und zeigen unter jeder Stelle einen Vorschlag
und zeigen unter jeder Stelle einen Vorschlag mit ✓/✕; ✓ schreibt ihn in die Datei, die mit ✓/✕; ✓ schreibt ihn in die Datei, die übrigen Vorschläge bleiben stehen. Unter dem
übrigen Vorschläge bleiben stehen. Rechts läuft ein Panel mit Fortschritt und Problemliste. Icon steht der Fortschritt, rechts läuft ein Panel mit Problemliste.
Die Prüfung soll ein Format erzwingen, nicht Geschmack durchsetzen. Darum vier
Sperren gegen Beliebigkeit — jede einzeln nachgemessen:
```
1 Messen struktur.py Wortzahl, Satzzeichen, Füllwörter, Klammern, Badge
2 Umschreiben 4 Stimmen ganze Felder neu, nicht einzelne Zeilen
3 Belegen Code Änderung nur mit wörtlich zitierter Katalogzeile
4 Nachmessen struktur.py Vorschlag, der selbst verstößt, wird abgelehnt
```
**Messen.** Was sich zählen lässt, zählt der Code. Was er wegstreichen kann, wird zum
fertigen Vorschlag. Der Rest hängt als Auftrag „Gemessen, bitte neu schreiben" am
Ausschnitt. Diese Zeilen stehen im Katalog unter `## Vom System gemessen` und sind
nicht zitierbar — sonst beruft sich das Modell auf die Füllwortliste und wendet sie
auf Wörter an, die nicht darin stehen.
**Umschreiben.** Das Modell zeigt auf keine Zeile, es schreibt das ganze Feld neu.
Adressierung war die größte Fehlerquelle: verschmolzene Zeilen, erfundene Zeilen,
Zeilen aus dem Kontextblock. Welche Zeile sich geändert hat, rechnet `_stellen` aus.
`original` ist damit immer echter Kartentext.
**Belegen.** Jede Änderung nennt die verletzte Katalogzeile, wörtlich. Der Code prüft
sie gegen den Katalog. Ohne Beleg keine Änderung. Ohne diese Sperre findet ein Modell
immer irgendetwas zu verbessern.
**Vier Stimmen, einstimmig.** Dieselbe Stelle wird viermal beurteilt, mit steigender
Temperatur. Nur was alle vier bemängeln, wird ein Vorschlag; die Formulierung kommt
aus der Fassung, die den anderen am ähnlichsten ist. Grenzfälle sehen mal so, mal so
aus — einzelne Stimmen ließen jede Runde andere Felder aufschlagen.
Gemessen an den Karten dieses Projekts, 35 Prüfstellen:
```
ohne diese Sperren 41 Vorschläge, 12 davon systematisch kaputt
mit 4 Vorschläge, 0 kaputt
nach dem Annehmen 2 -> 1 -> 2 Ruhezustand
```
Eine Anfrage je Prüfstelle und Stimme, nicht je Karte:
```
Karte ──┬── Kartenebene titel · badge · ziel · regeln
├── Funktion 1 titel · ziel · regeln · testen
└── Funktion 2 …
```
Ein Vorschlag ersetzt seine Stelle eins zu eins. Was das bewirkt, sagt der Prüfer
nicht — es ergibt sich aus dem Vergleich:
```
eine Zeile schreibt die Stelle um
mehrere Zeilen ersetzen die Stelle durch alle davon; steht die alte
wieder vorn, kommt nur etwas dazu
leer streicht die Stelle
```
Vorschläge werden einzeln und in beliebiger Reihenfolge angenommen. Jeder muss darum
für sich allein anwendbar sein. Baut das Modell eine ganze Liste um, wird daraus ein
einziger Vorschlag für das Feld — einzelne Zeilen daraus ergäben Dubletten oder Lücken.
Jede Anfrage bekommt nur ihren Ausschnitt der Karte und nur den Katalogteil, der für sie
gilt. Die Herkunft eines Problems stammt aus dem Code, nicht aus der Modellantwort.
## Abgleich mit dem Code
Zwei weitere Prüfungen, in beide Richtungen. Beide arbeiten mit derselben
Beweislast, nur andersherum: ein Vorhandensein lässt sich zitieren, ein
Nichtvorhandensein nicht.
```
↧ Features im Code steht hinter jeder Funktion auch Code?
↥ Code in Features hat jede Bedienstelle auch eine Funktion?
```
**↧ Features im Code.** Je Funktion sechs unabhängige Versuche, die Codestelle zu
finden. Jeder Fund wird gegen die Datei geschlagen. Gemeldet wird erst, wenn keine
einzige Stimme etwas findet — auch ein misslungenes Zitat gilt als Widerspruch, denn
ein verschriebener Backtick darf keine Löschempfehlung auslösen. Unter vier
brauchbaren Stimmen wird gar nicht erst angeklagt.
**↥ Code in Features.** Die Liste der Bedienstellen kommt aus dem Code, nicht aus
dem Modell: `quellcode.interaktionen()` liest Ereignisse im Markup, Kontextmenü-
Einträge und Fenster-Lauscher. Zusammengefasst nach dem aufgerufenen Namen —
`starte('ziel')` steht zwölfmal da und ist eine Fähigkeit. Je Stelle eine Ja/Nein-
Frage mit Code-Umfeld, Funktionsrumpf und dem vollen Text aller Karten; gemeldet ab
fünf von sechs Nein-Stimmen.
Nicht im Inventar: Backend-Routen (jede hängt an einem Knopf, der schon dasteht),
Zuweisungen, Speichern beim Verlassen, Abbrechen mit Escape.
**Warum diese Schwellen.** Einstimmigkeit war für ↥ zu scharf: dieselbe Frage kippt
auch bei Temperatur 0 gelegentlich, und ein Ausreißer löschte klare Funde.
Eindeutige Stellen antworten 6:0 oder 0:6, Grenzfälle etwa 3:3. Für ↧ gilt das
Gegenteil — dort ist die Anklage destruktiv, also blockiert schon eine
widersprechende Stimme.
Nachgemessen an diesem Projekt: auf gepflegten Karten melden beide nichts. Eine
untergeschobene Funktion ohne Code fällt ↧ in jedem Lauf auf. Fehlende Funktionen
findet ↥, nach dem Übernehmen ist es still.
Der Katalog liegt in `backend/stil/` — eine Datei je Feld, alphabetisch zusammengesetzt
(`allgemein` landet dadurch vorn). Das Präfix entscheidet, wer sie zu sehen bekommt:
```
allgemein.md beide Prüfer
feature-*.md nur die Kartenebene: titel, badge, ziel, regeln
funktion-*.md nur die Funktionen: titel, ziel, regeln, testen
```
Eine neue Datei wird ohne Codeänderung mitgelesen. Ist der Ordner leer, bricht die
Prüfung ab statt ohne Vorgaben zu prüfen.
Zugang über `.env` (Vorlage: `.env.example`) — `MINIMAX_API_KEY`, `MINIMAX_MODELL`, Zugang über `.env` (Vorlage: `.env.example`) — `MINIMAX_API_KEY`, `MINIMAX_MODELL`,
`MINIMAX_URL`, dazu `PRUEFUNG_PARALLEL` (gleichzeitig geprüfte Karten, Standard 10). `MINIMAX_URL`, dazu `PRUEFUNG_PARALLEL` (gleichzeitig laufende Anfragen, Standard 10).
Fehlt einer der Zugangswerte, bricht die Prüfung mit sichtbarer Meldung ab; Fehlt einer der Zugangswerte, bricht die Prüfung mit sichtbarer Meldung ab;
es wird nie still auf ein anderes Modell ausgewichen. Ergebnisse liegen nur im es wird nie still auf ein anderes Modell ausgewichen. Ergebnisse liegen nur im
Serverspeicher und überleben keinen Neustart. Serverspeicher und überleben keinen Neustart.

View File

@@ -1,6 +1,23 @@
"""Anbindung an MiniMax über das Anthropic-Messages-Format. Kein Fallback: fehlt die """Zwei Wege zum Modell, je nach Aufgabe.
Konfiguration oder antwortet der Provider nicht, bricht der Aufruf mit sichtbarer
Meldung ab.""" `frage` geht über MiniMax im Anthropic-Messages-Format: die Prüfungen sind hundert
Anfragen je Lauf und leben von der Temperaturstreuung — das muss billig und
parallelisierbar sein.
`frage_lokal` geht über die angemeldete Claude-CLI, dasselbe Abo wie die
Agenten-Übergabe. Die Gespräche sind einzelne Aufrufe und leben von Urteilskraft;
dort zählt Qualität, nicht Durchsatz.
Welche Werkzeuge dabei offenstehen, entscheidet die Betriebsart, nicht das Modell:
Planung liest den Projektcode, Recherche sucht im Web, und keine der beiden kann,
was die andere darf. Die Prüfungen bekommen gar nichts — dort ist der Katalog die
einzige zulässige Quelle.
Kein Fallback zwischen beiden: fehlt ein Weg, bricht der Aufruf mit sichtbarer
Meldung ab, statt still auf das schwächere Modell auszuweichen."""
import asyncio
import json
import httpx import httpx
@@ -10,6 +27,85 @@ ZEITLIMIT = 180.0
MAX_TOKENS = 8000 MAX_TOKENS = 8000
API_VERSION = "2023-06-01" API_VERSION = "2023-06-01"
GRUNDLAGE = (
"claude", "-p", "--model", "opus", "--output-format", "json",
# Spart ein Fünftel der Grundlast; was hier gilt, steht ohnehin im eigenen Prompt.
"--exclude-dynamic-system-prompt-sections",
# Ohne das erbt der Aufruf, was am Rechner sonst eingerichtet ist: MCP-Server,
# Workflows, Hooks, ein fremder Antwortstil. Ein Gespräch über Feature-Karten hat
# in keinem Postfach etwas zu suchen.
"--safe-mode",
)
# Was in keiner Betriebsart erlaubt ist. Beide Gespräche holen fremden Text in den
# Kontext — das eine Suchtreffer, das andere Quelltext — und fremder Text darf keinen
# Terminplan anlegen, keine Nachricht verschicken und keine Agenten starten. Was
# künftig an Werkzeugen dazukommt, steht hier nicht: beim Nachziehen der CLI gehört
# diese Liste geprüft.
GESPERRT = (
"Write", "Edit", "NotebookEdit", "Bash", "Task", "TodoWrite",
"Workflow", "Skill", "ReportFindings", "ScheduleWakeup", "DesignSync",
"CronCreate", "CronDelete", "CronList", "RemoteTrigger", "PushNotification",
"SendMessage", "Monitor", "EnterWorktree", "ExitWorktree",
"TaskCreate", "TaskGet", "TaskList", "TaskOutput", "TaskStop", "TaskUpdate",
)
def _kommando(erlaubt: tuple, gesperrt: tuple, zuege: int) -> tuple:
"""Ein Aufruf mit genau den Werkzeugen einer Betriebsart.
`--allowed-tools` ist nicht bloß die Umkehrung von `--disallowed-tools`: ohne die
Vorabfreigabe fragt die CLI je Suche und je Datei nach Erlaubnis und bekommt im
Stapelbetrieb keine Antwort. Der Versuch endet dann als Absage, nicht als Fehler.
`ToolSearch` steht nirgends: Suche, Abruf und Lesen sind nachladbar, ohne das
Werkzeug käme das Modell gar nicht an sie heran. Was es nachlädt, ist trotzdem
gesperrt — die Sperre wirkt beim Aufruf, nicht beim Suchen.
"""
return (*GRUNDLAGE,
"--allowed-tools", *erlaubt,
"--disallowed-tools", *GESPERRT, *gesperrt,
"--max-turns", str(zuege))
# Planen heißt: an den Karten arbeiten, mit dem Code als Beleg. Das Web bleibt zu —
# was das Projekt können soll, entscheidet die Person, nicht eine fremde Seite.
PLANUNG_KOMMANDO = _kommando(
("Read", "Glob", "Grep"), ("WebSearch", "WebFetch"), 20)
# Ohne eingestelltes Arbeitsverzeichnis gibt es nichts zu lesen. Lesen bliebe hier
# nicht folgenlos: ohne gesetzten Ordner liefe die Suche im Serververzeichnis, und
# das Modell beschriebe planer2 statt des Projekts.
PLANUNG_OHNE_CODE = _kommando(
(), ("Read", "Glob", "Grep", "WebSearch", "WebFetch"), 1)
# Recherche geht nach draußen und nirgendwo sonst: kein Blick in Dateien, damit
# nichts aus dem Projekt in einer Suchanfrage landet.
RECHERCHE_KOMMANDO = _kommando(
("WebSearch", "WebFetch"), ("Read", "Glob", "Grep"), 24)
# Opus denkt länger als MiniMax, der Prozessstart kostet extra, und Suchen wie
# Codelesen legen mehrere Abrufe obendrauf.
CHAT_ZEITLIMIT = 600.0
# Nur fürs Log: welcher Weg gerade lief. Ohne den Namen sagt eine Zeitangabe nichts,
# denn dieselbe Sekundenzahl ist bei einem Zug teuer und bei zwanzig billig.
WEGE = {
PLANUNG_KOMMANDO: "planung+code",
PLANUNG_OHNE_CODE: "planung",
RECHERCHE_KOMMANDO: "recherche",
}
# Der Provider drosselt bei vielen gleichzeitigen Anfragen
WIEDERHOLBAR = (429, 500, 502, 503, 504)
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):
"""Zugangsdaten fehlen. Eigener Typ, damit die Route das nicht mit
„Prüfung läuft bereits" verwechselt — beides wäre sonst ein ValueError."""
def pruefe_konfiguration(): def pruefe_konfiguration():
fehlend = [ fehlend = [
@@ -22,27 +118,106 @@ def pruefe_konfiguration():
if not wert if not wert
] ]
if fehlend: if fehlend:
raise ValueError(f"Nicht konfiguriert: {', '.join(fehlend)} in .env fehlt") raise NichtKonfiguriert(f"Nicht konfiguriert: {', '.join(fehlend)} in .env fehlt")
async def frage(system: str, inhalt: str) -> str: async def frage_lokal(system: str, inhalt: str,
kommando: tuple = PLANUNG_OHNE_CODE,
ordner: str | None = None) -> str:
"""Eine Runde über die Claude-CLI — der Text der Antwort.
Der Prompt geht über die Standardeingabe: Katalog und Kartenstand sind zu lang,
um sie als Kommandozeilenargument zu übergeben. Keine Temperatur — die CLI kennt
keine; für einen einzelnen Zug braucht es auch keine Streuung.
`ordner` ist das Verzeichnis, in dem der Aufruf läuft. Es entscheidet, was ein
erlaubtes Lesen erreicht; ohne Angabe erbt der Prozess das Serververzeichnis, und
das ist nur zulässig, solange gar nicht gelesen werden darf.
"""
try:
prozess = await asyncio.create_subprocess_exec(
*kommando, "--system-prompt", system,
cwd=ordner,
stdin=asyncio.subprocess.PIPE,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
except FileNotFoundError:
raise NichtKonfiguriert(
"Claude-CLI nicht gefunden — „claude“ muss im Pfad liegen und angemeldet sein"
)
try:
aus, err = await asyncio.wait_for(
prozess.communicate(inhalt.encode()), CHAT_ZEITLIMIT)
except asyncio.TimeoutError:
prozess.kill()
raise RuntimeError(f"Claude antwortet nicht innerhalb von {CHAT_ZEITLIMIT:.0f} s")
if prozess.returncode != 0:
raise RuntimeError(f"Claude bricht ab: {err.decode()[-300:].strip() or 'ohne Meldung'}")
try:
daten = json.loads(aus)
except ValueError:
raise RuntimeError("Antwort der Claude-CLI ist kein JSON")
if daten.get("is_error"):
raise RuntimeError(f"Claude meldet einen Fehler: {daten.get('result')}")
_notiere(kommando, daten)
text = str(daten.get("result") or "")
if not text.strip():
raise RuntimeError("Leere Antwort von Claude")
return text
def _notiere(kommando: tuple, daten: dict):
"""Wohin die Wartezeit ging — ins Serverlog, eine Zeile je Aufruf.
Die CLI legt es ohnehin bei; ungelesen bleibt nur die Frage „wieso dauert das so
lange" offen. Ein Zug ist ein Modellaufruf: jede Suche, jeder Abruf, jedes Lesen
zählt einzeln. Sind die Züge hoch, hat das Modell gearbeitet; ist einer teuer, war
der Prompt lang.
"""
zuege = daten.get("num_turns") or 0
api = (daten.get("duration_api_ms") or 0) / 1000
print(f"[claude] {WEGE.get(kommando, 'aufruf')}: {api:.0f}s, {zuege} Züge, "
f"{(api / zuege if zuege else api):.0f}s/Zug, "
f"${daten.get('total_cost_usd') or 0:.3f}", flush=True)
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. """Eine Anfrage, eine Antwort — der Text der Modellantwort.
Kalt geroutet: niedrige Temperatur, kein Thinking; die Prüfung soll nicht raten.""" Kalt geroutet: Temperatur 0, kein Thinking; die Prüfung soll nicht raten.
Für Mehrheitsentscheide wird sie bewusst angehoben, sonst wären die Stimmen gleich."""
pruefe_konfiguration() pruefe_konfiguration()
daten = { daten = {
"model": config.MINIMAX_MODELL, "model": config.MINIMAX_MODELL,
"max_tokens": MAX_TOKENS, "max_tokens": MAX_TOKENS,
"system": system, "system": system,
"messages": [{"role": "user", "content": inhalt}], "messages": [{"role": "user", "content": inhalt}],
"temperature": 0.2, "temperature": temperatur,
"thinking": {"type": "disabled"}, "thinking": {"type": "disabled"},
} }
async with httpx.AsyncClient(timeout=httpx.Timeout(ZEITLIMIT, connect=30)) as client: async with httpx.AsyncClient(timeout=httpx.Timeout(ZEITLIMIT, connect=30)) as client:
res = await client.post( for versuch in range(WIEDERHOLUNGEN):
config.MINIMAX_URL, res = await client.post(
headers={"x-api-key": config.MINIMAX_API_KEY, "anthropic-version": API_VERSION}, config.MINIMAX_URL,
json=daten, headers={"x-api-key": config.MINIMAX_API_KEY,
) "anthropic-version": API_VERSION},
json=daten,
)
if res.status_code not in WIEDERHOLBAR or versuch == WIEDERHOLUNGEN - 1:
break
# 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(res, versuch))
if res.status_code != 200: if res.status_code != 200:
raise RuntimeError(f"Provider antwortet mit HTTP {res.status_code}") raise RuntimeError(f"Provider antwortet mit HTTP {res.status_code}")
antwort = res.json() antwort = res.json()

319
backend/auftrag.py Normal file
View File

@@ -0,0 +1,319 @@
"""Die Planung an einen Agenten übergeben.
Claude implementiert die geplanten Änderungen im Arbeitsverzeichnis — lokal über
die Claude-CLI mit dem angemeldeten Konto. Solange er arbeitet, bleibt die
Planung stehen: die Übersicht sagt, was das Projekt kann, und das stimmt erst,
wenn der Code da ist. Erst nach Erfolg wird die Planung übernommen. Ein
Fehlschlag übernimmt nichts; die Planung steht noch und kann erneut los.
Der Agent meldet jeden erledigten Schritt als eigene Zeile; der Server liest den
Stream mit und hakt die Schritte ab. Zustand nur im Speicher — er überlebt keinen
Serverneustart, der laufende Prozess arbeitet trotzdem zu Ende, übernimmt dann
aber nichts mehr sichtbar.
"""
import asyncio
import difflib
import json
import re
import parser
import planung
import projekte
import verzeichnis
# stream-json liefert je Zeile ein Ereignis: sichtbar wird, woran der Agent
# gerade arbeitet, und die ERLEDIGT-Zeilen kommen an, sobald sie fallen.
KOMMANDO = ("claude", "-p", "--model", "opus", "--permission-mode", "acceptEdits",
"--output-format", "stream-json", "--verbose")
MARKE = "ERLEDIGT:"
# Einzelne Stream-Zeilen tragen ganze Dateien — die Standardgrenze ist zu klein
PUFFER = 10 * 1024 * 1024
# projektname -> {"laeuft", "fehler", "text", "schritte", "aktivitaet", "uebernommen"}
_laeufe = {}
_aufgaben = set()
PRUEFEN = "PRÜFEN"
# Eine neue Funktion ist immer unbewiesen — es gibt noch keinen Code, der sie leistet.
ART = {"weg": "GESTRICHEN", "geaendert": "GEÄNDERT", "neu": PRUEFEN}
AUFTRAG = """Du bekommst die Feature-Karten eines Projekts und die geplanten Änderungen.
Eine Karte ist ein Auftrag: Regeln sind Zusicherungen, eine Funktion ist etwas,
das man für sich testen kann. Setze den Code in diesem Verzeichnis so um, dass er
den geänderten Karten entspricht.
- Lies zuerst den bestehenden Code und folge seinen Konventionen.
- Ändere nur, was die Arbeitsschritte verlangen.
- {pruefen} heißt: die Funktion gilt als unbewiesen. Prüfe, ob der Code genau das
leistet, was ihre Testschritte beschreiben. Fehlt sie, baue sie. Weicht sie ab,
bring sie in Ordnung. Sie kann schon einmal gebaut worden sein und trotzdem falsch
arbeiten — glaube dem Code nicht, weil er existiert.
- GEÄNDERT heißt: gebaut, aber die Karte sagt jetzt etwas anderes.
- GESTRICHEN heißt: die Fähigkeit wird entfernt.
- Ein Kommando in Anführungszeichen muss danach genau so aufrufbar sein.
- Führe vorhandene Tests aus, wenn es welche gibt.
Die Arbeitsschritte:
{schritte}
Gib nach jedem abgeschlossenen Schritt sofort eine eigene Textzeile aus,
mit dem Schritt wörtlich wie oben:
{marke} <Schritt>
Hake nur ab, was du geprüft hast. Konntest du einen Ablauf nicht ausführen — weil
kein Fenster, kein Gerät oder kein Dienst da ist — dann melde ihn NICHT als erledigt.
Schreib am Ende in einer eigenen Zeile, was du nicht prüfen konntest und warum.
Die betroffenen Karten, wie sie danach gelten:
{karten}"""
def _kartentexte(projekt: str, titel: set) -> str:
"""Der volle Text der geplanten Karten — der Sollzustand, nicht der Diff."""
texte = []
for datei in sorted(projekte.wurzel(projekt).glob("*.md")):
roh = datei.read_text()
if parser.zerlege(roh)["titel"] in titel:
texte.append(roh.strip())
return "\n\n=====\n\n".join(texte) or "(keine — nur Streichungen)"
def _offene(projekt: str) -> set:
"""(Karte, Funktion) für alles, was noch nicht bewiesen ist.
Auch in Karten, die sich gegenüber dem Stand nicht geändert haben: eine Funktion
zurück auf „offen“ zu setzen ist die Art, eine kaputte Umsetzung zu melden. Sie
muss dann in die Arbeitsliste, sonst hätte der Klick keine Folge.
"""
raus = set()
for datei in projekte.wurzel(projekt).glob("*.md"):
s = parser.zerlege(datei.read_text())
if s["art"] == parser.VISION:
continue
for f in s["funktionen"]:
if f["stand"] == parser.OFFEN:
raus.add((s["titel"], f["text"]))
return raus
def _schritte(projekt: str, aenderungen: dict) -> list[dict]:
"""Die Arbeitsliste: ein Eintrag je Streichung, Kopfänderung und Funktion.
Karte und Funktion stehen daneben, nicht nur im Text. Beim Abhaken wird die
Funktion auf „gebaut“ gesetzt — den Namen aus dem Satz zurückzuparsen wäre
eine zweite Wahrheit über dieselbe Sache.
"""
offen = _offene(projekt)
raus = []
erledigt = set()
def funktionsschritt(titel: str, name: str, art: str):
erledigt.add((titel, name))
# Offen schlägt geändert: „prüfen und herstellen“ deckt beides ab, und der
# Agent soll dem vorhandenen Code nicht glauben.
marke = PRUEFEN if (titel, name) in offen else ART[art]
raus.append({"text": f"{titel} / {name}: {marke}", "karte": titel,
# Eine gestrichene Funktion gibt es hinterher nicht mehr
"funktion": "" if art == "weg" else name})
for titel in sorted(aenderungen):
e = aenderungen[titel]
if e["art"] == "weg":
raus.append({"text": f"Karte „{titel}“ streichen",
"karte": titel, "funktion": ""})
erledigt |= {(titel, n) for n in e.get("funktionen", {})}
continue
if e.get("kopf"):
raus.append({"text": f"Karte „{titel}“: Kartenkopf anpassen",
"karte": titel, "funktion": ""})
for name, art in sorted(e.get("funktionen", {}).items()):
funktionsschritt(titel, name, art)
for titel, name in sorted(offen - erledigt):
funktionsschritt(titel, name, "geaendert")
return raus
def _visionen(projekt: str) -> set:
"""Titel der Vision-Karten, in Stand und Arbeitskopie."""
titel = set()
for ordner in (projekte.stand(projekt), projekte.wurzel(projekt)):
for datei in ordner.glob("*.md"):
s = parser.zerlege(datei.read_text())
if s["art"] == parser.VISION:
titel.add(s["titel"])
return titel
def bau_auftrag(projekt: str) -> tuple[str, list[dict]]:
"""Prompt und Arbeitsliste, aus dem Stand der laufenden Planung.
Die Vision bleibt außen vor: sie beschreibt Absicht, keinen Auftrag —
eine geänderte Vision ist nichts, was ein Agent bauen soll.
"""
if not planung.laeuft(projekt):
return "", []
visionen = _visionen(projekt)
aenderungen = {t: e for t, e in planung.aenderungen(projekt).items()
if t not in visionen}
schritte = _schritte(projekt, aenderungen)
if not schritte:
return "", []
# Der Sollzustand jeder Karte, die Arbeit trägt — auch der unveränderten mit
# einer offenen Funktion. Ohne ihren Text wüsste der Agent nicht, wogegen er prüft.
bleibend = {s["karte"] for s in schritte if s["karte"] not in {
t for t, e in aenderungen.items() if e["art"] == "weg"}}
text = AUFTRAG.format(
schritte="\n".join(f'- {s["text"]}' for s in schritte),
marke=MARKE,
pruefen=PRUEFEN,
karten=_kartentexte(projekt, bleibend))
return text, schritte
def status(projekt: str) -> dict:
return _laeufe.get(projekt) or {
"laeuft": False, "fehler": "", "text": "",
"schritte": [], "aktivitaet": "", "uebernommen": False,
}
# „offen“ steht als eigener Block in der Datei; fehlt er, gilt die Funktion als
# gebaut. Umstellen heißt darum: den Block streichen. Ein Umbau über den Parser
# würde die Datei neu schreiben und dabei unbekannte Blöcke verlieren.
_FUNKTIONSMARKE = re.compile(r"^<!--\s*funktion\s*-->[ \t]*$", re.M | re.I)
_OFFENBLOCK = re.compile(
r"^<!--\s*stand\s*-->[ \t]*\n[ \t]*" + parser.OFFEN + r"[ \t]*\n", re.M | re.I)
def _als_gebaut(roh: str, funktion: str) -> str:
"""Streicht den offen-Vermerk genau dieser Funktion."""
marken = [m.start() for m in _FUNKTIONSMARKE.finditer(roh)]
for i, start in enumerate(marken):
ende = marken[i + 1] if i + 1 < len(marken) else len(roh)
block = roh[start:ende]
zeilen = [z.strip() for z in block.splitlines()[1:] if z.strip()]
titel = next((z for z in zeilen
if not z.startswith("<!--") and not z.startswith("---")), "")
if titel != funktion:
continue
return roh[:start] + _OFFENBLOCK.sub("", block, count=1) + roh[ende:]
return roh
def _setze_gebaut(projekt: str, schritt: dict):
"""Ein abgehakter Schritt heißt: der Code ist da, die Funktion ist gebaut.
Geschrieben wird in die Arbeitskopie; `planung.setze_um` trägt sie danach in den
Projektstand. Scheitert der Agent später, bleibt der Haken stehen — was gebaut
ist, ist gebaut, und die Planung läuft von dort weiter.
"""
if not schritt.get("funktion"):
return
for datei in projekte.wurzel(projekt).glob("*.md"):
roh = datei.read_text()
if parser.zerlege(roh)["titel"] != schritt.get("karte"):
continue
neu = _als_gebaut(roh, schritt["funktion"])
if neu != roh:
datei.write_text(neu)
return
def _hake_ab(projekt: str, lauf: dict, zeile: str):
"""Eine ERLEDIGT-Zeile gegen die offenen Schritte halten.
Wörtlich wäre schön, aber ein Modell zitiert nicht immer wörtlich —
deshalb genügt große Ähnlichkeit mit genau einem offenen Schritt.
"""
gemeldet = zeile.split(MARKE, 1)[1].strip()
offen = [s for s in lauf["schritte"] if not s["fertig"]]
for s in offen:
if s["text"] == gemeldet:
s["fertig"] = True
_setze_gebaut(projekt, s)
return
naechster = max(
offen,
key=lambda s: difflib.SequenceMatcher(None, s["text"], gemeldet).ratio(),
default=None)
if naechster and difflib.SequenceMatcher(
None, naechster["text"], gemeldet).ratio() >= 0.6:
naechster["fertig"] = True
_setze_gebaut(projekt, naechster)
def _verarbeite(projekt: str, lauf: dict, roh: bytes):
"""Ein Stream-Ereignis: Haken setzen, Aktivität zeigen, Ergebnis merken."""
try:
ereignis = json.loads(roh)
except ValueError:
return
if ereignis.get("type") == "result":
lauf["text"] = str(ereignis.get("result") or "").strip()
return
if ereignis.get("type") != "assistant":
return
for block in (ereignis.get("message") or {}).get("content") or []:
if block.get("type") == "text":
for zeile in str(block.get("text") or "").splitlines():
if MARKE in zeile:
_hake_ab(projekt, lauf, zeile)
elif block.get("type") == "tool_use":
eingabe = block.get("input") or {}
ziel = (eingabe.get("file_path") or eingabe.get("path")
or eingabe.get("command") or eingabe.get("pattern") or "")
lauf["aktivitaet"] = f'{block.get("name", "")} {str(ziel)[:80]}'.strip()
async def _fuehre_aus(projekt: str, auftrag: str, ordner: str):
lauf = _laeufe[projekt]
try:
prozess = await asyncio.create_subprocess_exec(
*KOMMANDO, cwd=ordner, limit=PUFFER,
stdin=asyncio.subprocess.PIPE,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE)
prozess.stdin.write(auftrag.encode())
await prozess.stdin.drain()
prozess.stdin.close()
while zeile := await prozess.stdout.readline():
_verarbeite(projekt, lauf, zeile)
fehler = (await prozess.stderr.read()).decode(errors="replace").strip()
if await prozess.wait() != 0:
lauf["fehler"] = fehler or f"Agent endete mit Code {prozess.returncode}"
return
# Erst jetzt wird die Planung wahr: der Code entspricht den Karten.
planung.setze_um(projekt)
lauf["uebernommen"] = True
except FileNotFoundError:
lauf["fehler"] = "claude-CLI nicht gefunden — ist Claude Code installiert?"
except Exception as e: # sichtbar statt still verschluckt
lauf["fehler"] = str(e)
finally:
lauf["aktivitaet"] = ""
lauf["laeuft"] = False
def starte(projekt: str, auftrag: str, schritte: list[dict]) -> bool:
"""Startet den Agenten im Hintergrund. False, wenn nichts zu tun ist."""
if not auftrag:
return False
ordner = verzeichnis.lies(projekt)
if not ordner:
return False
if status(projekt)["laeuft"]:
raise ValueError("Für dieses Projekt arbeitet schon ein Agent")
_laeufe[projekt] = {
"laeuft": True, "fehler": "", "text": "",
"schritte": [{**s, "fertig": False} for s in schritte],
"aktivitaet": "", "uebernommen": False,
}
aufgabe = asyncio.get_event_loop().create_task(
_fuehre_aus(projekt, auftrag, ordner))
_aufgaben.add(aufgabe)
aufgabe.add_done_callback(_aufgaben.discard)
return True

363
backend/chat.py Normal file
View File

@@ -0,0 +1,363 @@
"""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.
Gefragt wird über die Claude-CLI, nicht über MiniMax: das Gespräch soll planen helfen.
Ein Modell, das nur Kataloge abarbeitet, gibt hier die Frage zurück statt Vorschläge.
Zum Planen gehört der Quelltext, nicht das Web. Wer eine Karte entwirft, muss wissen,
was heute schon dasteht — das steht im Arbeitsverzeichnis und nirgends sonst. Fremdes
Wissen kommt aus der Recherche und ist ein eigener Raum (`recherche.py`): was dort
gefunden wird, bringt die Person selbst mit, statt dass es unbemerkt in einen Entwurf
sickert.
"""
import json
import agents
import features
import parser
import projekte
import pruefung
import verzeichnis
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. Dafür das starke Modell:
# hier wird geplant, nicht ein Katalog abgearbeitet.
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. Wie eine Sache gebaut ist, gehört auf keine Karte — wie man sie bedient, immer.
Bedienung ist nicht nur Maus und Menü. Einrichten, Installieren und Starten sind
Funktionen wie jede andere; ihr Auslöser ist ein Kommando. Setze es wörtlich in
Anführungszeichen, damit die Person es abtippen kann: `Befehl "make install"
ausführen`. Was das Kommando innen tut, bleibt draußen.
{code}
{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 nicht laufen kann, sonst „optional"
- ziel und regeln dürfen leer sein oder fehlen
- testen ist Pflicht bei jeder Funktion: der Ablauf, an dem man sie prüft. Weißt du
ihn nicht, gehört die Funktion nicht auf die Karte — dann frag danach
Ist nichts zu ändern, ist karten leer. Ändere nur, wonach gefragt wurde; Karten,
die niemand erwähnt hat, lässt du weg.
Zwei Arten von Wortmeldung, und sie werden verschieden behandelt.
Eine Frage will eine Antwort, keinen Entwurf. „Wie ist das heute?", „Muss ich noch
etwas tun?", „Wozu dient das?" — antworte in Prosa und lass karten leer. Ungefragt
eine Karte anzufassen ist ein Fehler, auch wenn dir dabei etwas auffällt. Sag es in
einem Satz und frag, ob du es aufnehmen sollst.
Ein Wunsch will einen Entwurf. Sobald etwas anders werden soll oder dazukommen soll,
leg die Karten daneben, wie sie danach aussehen. Fehlt dir dafür eine Angabe, frag
danach — aber leg trotzdem einen Entwurf daneben. An einem Entwurf sieht die Person
sofort, was du falsch verstanden hast, und er gilt erst, wenn sie ihn annimmt.
Bei einem Wunsch gibst du die Frage nie unbeantwortet zurück. Wer nach Namen fragt
— für Kommandos, Funktionen oder Karten — bekommt konkrete Namen, keine Gegenfrage.
Nenne das Übliche für diese Art von Anwendung, sag in einem Satz, worauf du dich
stützt, und frag danach, was daran nicht passt. „Das kann ich nicht vorschlagen" ist
keine zulässige Antwort.
Gehört etwas nicht auf eine Karte, sag in einem Satz, wohin es stattdessen gehört.
Beim Bestand bleibst du bei der Wahrheit: über das, was das Projekt heute kann,
erfindest du nichts. Ein Entwurf für etwas Neues kommt von dir."""
# Zwei Fassungen desselben Absatzes. Ohne eingestelltes Arbeitsverzeichnis gibt es
# nichts zu lesen, und ein Angebot, das ins Leere greift, wäre schlimmer als keines:
# das Modell behauptete sonst, nachgesehen zu haben.
CODE_MIT = """Der Quelltext des Projekts liegt in deinem Arbeitsverzeichnis. Du darfst
darin lesen und suchen, und du sollst es tun, sobald dein Entwurf davon abhängt, wie
etwas heute gebaut ist: ob es eine Funktion schon gibt, wie eine Bedienstelle heißt,
was ein Ablauf wirklich auslöst. Rate nicht, sieh nach.
Was du dort findest, ist ein Beleg, kein Kartentext. Wie eine Sache gebaut ist, gehört
auf keine Karte — dorthin kommt nur, was die Person davon merkt. Stützt du dich auf
eine Stelle, nenne im Antwortsatz die Datei.
Widerspricht der Code den Karten, schreib die Karte nicht still um. Sag in einem Satz,
was du gefunden hast, und frag, welche Seite gilt."""
CODE_OHNE = """Für dieses Projekt ist kein Arbeitsverzeichnis eingestellt. Du kannst
den Quelltext also nicht ansehen. Was das Projekt heute kann, weißt du nur aus den
Karten unten — behaupte nichts darüber hinaus."""
# 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 _arbeitsordner(projekt: str) -> str | None:
"""Wo der Quelltext liegt — oder nichts, wenn dort nichts zu holen ist."""
try:
return str(verzeichnis.pruefe(verzeichnis.lies(projekt)))
except (ValueError, FileNotFoundError, OSError):
return None
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.
# `sektionen` liegt daneben: nur am Titel lässt sich nicht entscheiden,
# ob die Karte taugt. Zerlegt wird hier, damit es keinen zweiten Parser gibt.
raus.append({"feld": "neue_karte", "original": "", "vorschlag": titel,
"roh": text, "sektionen": neu, "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")
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."
# Der Ordner entscheidet über beides zugleich: was im Prompt angeboten wird und
# was die CLI überhaupt zulässt. Ein toter Pfad zählt wie keiner — der Prozess
# ließe sich darin nicht einmal starten.
ordner = _arbeitsordner(projekt)
# 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(
code=CODE_MIT if ordner else CODE_OHNE,
stil="\n\n".join(pruefung._stil_lesen(b) for b in pruefung.BEREICHE),
karten=_kartenstand(projekt))
nachricht = NACHRICHT.format(verlauf=gespraech, text=text)
kommando = agents.PLANUNG_KOMMANDO if ordner else agents.PLANUNG_OHNE_CODE
# Ein Ausrutscher beim JSON kostet sonst die ganze Wortmeldung. Danach fliegt der
# Fehler nach oben und ist sichtbar.
daten = None
for versuch in range(3):
antwort = await agents.frage_lokal(system, nachricht, kommando, ordner)
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)}

BIN
backend/chatui.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

@@ -3,7 +3,9 @@ from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parent.parent PROJECT_ROOT = Path(__file__).resolve().parent.parent
DATEN_DIR = PROJECT_ROOT / "daten" DATEN_DIR = PROJECT_ROOT / "daten"
STIL_DATEI = Path(__file__).resolve().parent / "stil.md" # Ein Abschnitt je Datei; alphabetisch gelesen, „allgemein" landet dadurch vorn.
# Neue Datei anlegen genügt — der Katalog nimmt sie ohne Codeänderung auf.
STIL_DIR = Path(__file__).resolve().parent / "stil"
def _lade_env(): def _lade_env():
@@ -25,6 +27,9 @@ MINIMAX_API_KEY = os.environ.get("MINIMAX_API_KEY", "")
MINIMAX_MODELL = os.environ.get("MINIMAX_MODELL", "") MINIMAX_MODELL = os.environ.get("MINIMAX_MODELL", "")
MINIMAX_URL = os.environ.get("MINIMAX_URL", "") MINIMAX_URL = os.environ.get("MINIMAX_URL", "")
# Das Gespräch läuft nicht hierüber, sondern über die angemeldete Claude-CLI
# (`agents.frage_lokal`) — dasselbe Abo wie die Agenten-Übergabe, kein zweiter Zugang.
def _zahl(name: str, standard: int) -> int: def _zahl(name: str, standard: int) -> int:
try: try:

View File

@@ -6,12 +6,15 @@ import parser
import projekte import projekte
_UMLAUTE = str.maketrans({"ä": "ae", "ö": "oe", "ü": "ue", "ß": "ss"}) _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: def _slug(titel: str) -> str:
s = titel.lower().translate(_UMLAUTE) s = titel.lower().translate(_UMLAUTE)
s = re.sub(r"[^a-z0-9]+", "-", s).strip("-") s = re.sub(r"[^a-z0-9]+", "-", s).strip("-")
return s or "feature" return s[:NAMENSLAENGE].strip("-") or "feature"
def _eintrag(datei) -> dict: def _eintrag(datei) -> dict:
@@ -26,23 +29,39 @@ def _datei(projekt: str, id: str):
return datei return datei
def liste(projekt: str) -> list[dict]: def liste(projekt: str, nur_stand: bool = False) -> list[dict]:
dateien = projekte.wurzel(projekt).glob("*.md") """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)] eintraege = [_eintrag(d) for d in sorted(dateien)]
return sorted(eintraege, key=lambda e: e["sektionen"]["titel"].lower()) return sorted(eintraege, key=lambda e: e["sektionen"]["titel"].lower())
def anlegen(projekt: str, roh: str) -> dict: def pruefbar(projekt: str) -> list[dict]:
ordner = projekte.wurzel(projekt) """Alles außer der Vision. Die hält fest, wie es gedacht ist — sie ist die
titel = parser.zerlege(roh)["titel"] Vorlage, nicht der Prüfling, und keine Prüfung schlägt Änderungen daran vor."""
if not titel: return [f for f in liste(projekt) if f["sektionen"]["art"] != parser.VISION]
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" datei = ordner / f"{slug}.md"
n = 2 n = 2
while datei.exists(): while datei.exists():
datei = ordner / f"{slug}-{n}.md" datei = ordner / f"{slug}-{n}.md"
n += 1 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) datei.write_text(roh)
return _eintrag(datei) return _eintrag(datei)
@@ -52,11 +71,7 @@ def _folge_titel(datei, titel: str):
slug = _slug(titel) slug = _slug(titel)
if re.fullmatch(re.escape(slug) + r"(-\d+)?", datei.stem): if re.fullmatch(re.escape(slug) + r"(-\d+)?", datei.stem):
return datei # passt schon (inkl. Kollisions-Suffix) return datei # passt schon (inkl. Kollisions-Suffix)
ziel = datei.parent / f"{slug}.md" ziel = _frei(datei.parent, slug)
n = 2
while ziel.exists():
ziel = datei.parent / f"{slug}-{n}.md"
n += 1
datei.rename(ziel) datei.rename(ziel)
return ziel return ziel

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: 32 KiB

BIN
backend/komfort.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

95
backend/ledger.py Normal file
View File

@@ -0,0 +1,95 @@
"""Gedächtnis der Inhaltsprüfungen: was einmal beurteilt wurde, bleibt beurteilt.
Ein Urteil über eine Codestelle 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. Der nächste Lauf fragt
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 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
beurteilt.
Die Datei liegt im Projektordner. Sie wandert beim Umbenennen mit und verschwindet
beim Löschen — ohne Zutun, weil beides den Ordner bewegt. Wer von vorn anfangen
will, löscht sie; der nächste Lauf fragt dann wieder alles.
"""
import hashlib
import json
import projekte
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)
)
def _pfad(projekt: str):
return projekte.wurzel(projekt) / DATEI
def lade(projekt: str) -> dict:
"""Der gespeicherte Stand. Kaputte oder fehlende Datei heißt: nichts gemerkt."""
try:
daten = json.loads(_pfad(projekt).read_text())
except (OSError, ValueError):
daten = {}
return {fach: dict(daten.get(fach) or {}) for fach in FAECHER}
def sichere(projekt: str, stand: dict):
"""Sortiert und eingerückt — die Datei soll sich lesen und vergleichen lassen.
Erst daneben schreiben, dann umbenennen. Ein Absturz mitten im Schreiben ließe
sonst eine halbe Datei zurück, und das Gedächtnis eines ganzen Laufs wäre weg —
teuer bezahlt und nicht wiederherstellbar.
"""
text = json.dumps({fach: stand.get(fach) or {} for fach in FAECHER},
indent=1, ensure_ascii=False, sort_keys=True)
ziel = _pfad(projekt)
neben = ziel.with_suffix(".json.neu")
neben.write_text(text + "\n")
neben.replace(ziel)
def abdruck(*teile: str) -> str:
"""Kurzer Fingerabdruck über den Stand, der ein Urteil trägt."""
roh = json.dumps([t or "" for t in teile], ensure_ascii=False).encode()
return hashlib.sha1(roh).hexdigest()[:16]
# Schlüssel sind JSON-Listen. Ein Trennzeichen wäre einfacher, aber Kartennamen und
# Funktionstitel dürfen alles enthalten — und ein Schlüssel, der sich nicht wieder
# zerlegen lässt, ist keiner.
def schluessel(*teile: str) -> str:
return json.dumps(list(teile), ensure_ascii=False)
def paarschluessel(a: str, b: str) -> str:
return json.dumps(sorted((a, b)), ensure_ascii=False)
def teile(wert: str) -> list:
try:
gelesen = json.loads(wert)
except ValueError:
return []
return gelesen if isinstance(gelesen, list) else []
def behalte(fach: dict, gueltig: set) -> dict:
"""Einträge zu verschwundenen Stellen wegräumen — sonst wächst die Datei ewig."""
return {k: v for k, v in fach.items() if k in gueltig}

BIN
backend/offen.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -14,6 +14,24 @@ import re
MARKER = re.compile(r"^<!--\s*([a-zä-ü]+)\s*-->\s*$") MARKER = re.compile(r"^<!--\s*([a-zä-ü]+)\s*-->\s*$")
TRENNER = re.compile(r"^-{3,}\s*$") TRENNER = re.compile(r"^-{3,}\s*$")
# „vision" ist keine Einstufung, sondern eine eigene Kartenart: sie hält fest, wie
# es gedacht ist, und wird von keiner Prüfung angefasst. Unbekanntes ist „kern".
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 ""
return wert if wert in ARTEN else "kern"
def zerlege(roh: str) -> dict: def zerlege(roh: str) -> dict:
# Erst in Blöcke (schluessel, text) in Dateireihenfolge schneiden # Erst in Blöcke (schluessel, text) in Dateireihenfolge schneiden
@@ -51,7 +69,11 @@ def zerlege(roh: str) -> dict:
# vor der ersten Funktion: Karten-Ziel, danach: Ziel der Funktion # vor der ersten Funktion: Karten-Ziel, danach: Ziel der Funktion
(funktionen[-1]["ziel"] if funktionen else ziel).append(text) (funktionen[-1]["ziel"] if funktionen else ziel).append(text)
elif schluessel == "funktion": 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"): elif schluessel in ("regel", "regeln"):
# vor der ersten Funktion: Karten-Regeln, danach: Regeln der Funktion # vor der ersten Funktion: Karten-Regeln, danach: Regeln der Funktion
(funktionen[-1]["regeln"] if funktionen else regeln).append(text) (funktionen[-1]["regeln"] if funktionen else regeln).append(text)
@@ -70,7 +92,7 @@ def zerlege(roh: str) -> dict:
return { return {
"titel": titel_zeilen[0] if titel_zeilen else "", "titel": titel_zeilen[0] if titel_zeilen else "",
"art": "optional" if art and art[0].strip().lower() == "optional" else "kern", "art": _art(art),
"ziel": "\n\n".join(ziel), "ziel": "\n\n".join(ziel),
"regeln": "\n".join(regeln), "regeln": "\n".join(regeln),
"funktionen": funktionen, "funktionen": funktionen,

177
backend/planung.py Normal file
View File

@@ -0,0 +1,177 @@
"""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
import verzeichnis
# 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")
# Dateien, die dem Projekt gehören und nicht der Planung. Sie überleben das
# Umsetzen, auch wenn die Kopie sie nicht kennt. Das Gespräch und das
# Urteilsgedächtnis stehen bewusst nicht hier: die gehören zum geplanten Stand.
NUR_PROJEKT = (verzeichnis.DATEI,)
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.
Was dem Projekt gehört und nicht der Planung, bleibt stehen: das
Arbeitsverzeichnis wurde vielleicht erst nach dem Planungsstart festgelegt und
fehlt dann in der Kopie. Das Räumen hat es mitgenommen, und der nächste Lauf
fand kein Verzeichnis mehr.
"""
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():
if eintrag.name in NUR_PROJEKT and not (daneben / eintrag.name).exists():
eintrag.rename(daneben / eintrag.name)
continue
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 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): def _pruefe_name(name: str):
if not name or not name.strip(): if not name or not name.strip():
@@ -51,9 +55,24 @@ def loeschen(name: str):
shutil.rmtree(ordner) shutil.rmtree(ordner)
def wurzel(name: str): def stand(name: str):
"""Projektordner für Feature-Zugriffe; FileNotFoundError bei unbekanntem Projekt.""" """Der Projektordner selbst — was das Projekt heute kann.
Nur wer den umgesetzten Stand meint, ruft das. Alles andere nimmt `wurzel()`.
"""
ordner = _ordner(name) ordner = _ordner(name)
if not ordner.is_dir(): if not ordner.is_dir():
raise FileNotFoundError(f"Projekt „{name}“ nicht gefunden") raise FileNotFoundError(f"Projekt „{name}“ nicht gefunden")
return ordner 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

@@ -1,114 +1,573 @@
"""Prüft die Karten eines Projekts gegen den Stil-Katalog. """Prüft die Karten eines Projekts gegen den Stil-Katalog.
Ein Lauf je Projekt, Zustand nur im Speicher — er überlebt keinen Serverneustart. Ein Lauf je Projekt, Zustand nur im Speicher — er überlebt keinen Serverneustart.
Je Karte eine Anfrage, höchstens PRUEFUNG_PARALLEL gleichzeitig; Probleme erscheinen, Eine Anfrage je Prüfstelle: einmal die Kartenebene (titel, badge, ziel, regeln) und
sobald die jeweilige Karte geprüft ist. dazu je Funktion einmal (titel, ziel, regeln, testen). Jede Anfrage bekommt nur den
Katalogteil, der für sie gilt, und nur ihren Ausschnitt der Karte — das hält den
Prompt kurz und die Zuordnung eindeutig.
Höchstens PRUEFUNG_PARALLEL gleichzeitig; Probleme erscheinen, sobald ihre Stelle
geprüft ist.
""" """
import asyncio import asyncio
import difflib
import json import json
import re import re
from collections import Counter
import agents import agents
import config import config
import features import features
import ledger
import struktur
# projektname -> {"laeuft": bool, "fertig": int, "gesamt": int, 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} # "probleme": [...], "meldung": str}
_laeufe = {} _laeufe = {}
SYSTEM = """Du prüfst Karten einer Feature-Verwaltung gegen einen Stil-Katalog. # create_task gibt die Aufgabe nur schwach an die Schleife weiter — ohne eigene
# Referenz kann der Lauf mitten drin weggesammelt werden.
_aufgaben = set()
# Das Modell zeigt nicht mehr auf einzelne Zeilen, sondern schreibt ganze Felder neu.
# Adressierung war die Fehlerquelle: verschmolzene Zeilen, erfundene Zeilen, Zeilen aus
# dem Kontextblock. Welche Zeile sich geändert hat, rechnet der Code aus (_stellen).
SYSTEM = """Du prüfst {gegenstand} gegen einen Stil-Katalog.
{stil} {stil}
Du bekommst eine Karte als Markdown. Melde jede Abweichung vom Katalog. Du bekommst die Felder dieser Stelle. Schreibe jedes Feld neu, das dem Katalog nicht
genügt. Felder, die schon passen, lässt du weg.
Antworte ausschließlich mit JSON: Antworte ausschließlich mit JSON:
{{"probleme": [{{"feld": "...", "funktion": "...", "original": "...", {{"felder": {{"<feld>": {{"regel": "...", "neu": "...", "grund": "..."}}}}}}
"problem": "...", "vorschlag": "..."}}]}}
- feld: titel, ziel, regeln oder testen - <feld>: {felder}
- funktion: Titel der Funktion, zu der die Stelle gehört; "" für die Karte selbst - regel: die verletzte Katalogzeile, wörtlich abgeschrieben
- original: der fehlerhafte Text wortwörtlich; bei regeln und testen genau eine Zeile ohne führendes "- " - neu: der vollständige Inhalt des Feldes, so wie er dastehen soll
- problem: die verletzte Vorgabe in höchstens 8 Wörtern - grund: was am alten Inhalt falsch war, höchstens 8 Wörter
- vorschlag: der korrigierte Text, sonst nichts
Keine Probleme gefunden: {{"probleme": []}} regel muss Zeichen für Zeichen im Katalog oben stehen. Findest du dort keine Zeile,
Erfinde nichts und melde nur, was der Katalog verlangt.""" die der Inhalt verletzt, ändere nichts. Dein Geschmack ist keine Regel.
Bei regeln und testen ist neu eine Liste: eine Zeile je Eintrag, jede mit "- " davor.
Behalte dort jede Zeile, die für sich in Ordnung ist, wortgleich bei. Fehlt ein
Eintrag, ergänze ihn an der richtigen Stelle. Steht einer zu Unrecht da, lass ihn weg.
Ein Feld darf leer werden, wenn der Katalog das erlaubt.
Ändere so wenig wie möglich. Ein leeres oder fehlendes Feld ist kein Verstoß, solange
keine Katalogzeile es verlangt. Denk dir keine Inhalte aus, die es nicht gibt.
Was du schreibst, muss den Katalog erfüllen — es wird gemessen und sonst abgelehnt.
Steht unten „Gemessen", sind das Verstöße, die feststehen. Behebe sie; dort brauchst
du keine regel zu nennen.
{hinweis}
Nichts zu ändern: {{"felder": {{}}}}
Erfinde nichts und ändere nur, was der Katalog verlangt.
Der Katalog ist abschließend — leite keine weiteren Vorgaben daraus ab."""
NACHBESSERN = """Dein Vorschlag verstößt weiter gegen messbare Vorgaben:
{maengel}
Schreibe die betroffenen Felder erneut. Gleiches Format wie zuvor."""
# Eine Quelle für „was ist eine Liste" — sonst zerlegt der eine Teil, was der
# andere als Block behandelt, und aus einer Ergänzung wird ein Komplettersatz.
ZEILENFELDER = struktur.ZEILENFELDER
# Felder, die leer bleiben dürfen; ein leerer Vorschlag streicht die Stelle
LEERBAR = ZEILENFELDER + ("ziel",)
# Alles, was es überhaupt gibt; je Bereich gilt eine engere Auswahl
ALLE_FELDER = ("titel", "badge", "ziel", "regeln", "testen")
# Anfragen je Stimme, bis der Vorschlag die messbaren Vorgaben erfüllt
VERSUCHE = 2
# Unabhängige Stimmen je Prüfstelle. Verschiedene Temperaturen, damit sie sich
# unterscheiden — bei identischen Anfragen wäre die Mehrheit wertlos.
TEMPERATUREN = (0.0, 0.3, 0.6, 0.9)
# Einstimmig. Ein Verstoß, den nicht jede Stimme sieht, ist ein Grenzfall — und
# Grenzfälle sind genau das, was den Lauf sonst nie zur Ruhe kommen lässt.
MEHRHEIT = len(TEMPERATUREN)
# Was der jeweilige Prüfer sieht und melden darf. Der Bereichsname ist zugleich das
# Dateipräfix im Stil-Ordner — „feature" liest allgemein-* und feature-*.
BEREICHE = {
"feature": {
"gegenstand": "die Kartenebene einer Feature-Karte",
"felder": ("titel", "badge", "ziel", "regeln"),
# Ohne die Funktionstitel kann niemand beurteilen, ob die Karte kern ist
"hinweis": (
"Der Block funktionen ist nur Kontext für die Badge-Einstufung."
" Melde nichts daraus."
),
},
"funktion": {
"gegenstand": "eine einzelne Funktion einer Feature-Karte",
"felder": ("titel", "ziel", "regeln", "testen"),
"hinweis": "",
},
}
def _felder_text(felder: tuple) -> str:
return f"{', '.join(felder[:-1])} oder {felder[-1]}"
def _stil_lesen(bereich: str) -> str:
"""Katalog für einen Bereich: die allgemein-Dateien plus die des Bereichs.
Leerer Ordner heißt Fehlbedienung — dann lieber abbrechen als blind prüfen."""
def gehoert(datei, gruppe):
return datei.stem == gruppe or datei.stem.startswith(f"{gruppe}-")
dateien = sorted(
d for d in config.STIL_DIR.glob("*.md")
if gehoert(d, "allgemein") or gehoert(d, bereich)
)
if not dateien:
raise ValueError(f"Kein Stil-Katalog für „{bereich}“ in {config.STIL_DIR}")
return "\n\n".join(d.read_text().strip() for d in dateien)
def _block(marker: str, text: str) -> str:
"""Leere Felder gar nicht erst mitschicken — sonst meldet der Prüfer sie als fehlend."""
return f"<!-- {marker} -->\n{text}\n" if text.strip() else ""
def aufgaben(feature: dict) -> list[dict]:
"""Eine Prüfstelle für die Kartenebene, dazu je Funktion eine.
`felder` ist das, was geprüft wird; `kontext` geht nur zur Einordnung mit."""
s = feature["sektionen"]
liste = [{
"feature": feature["id"],
"funktion": "",
"bereich": "feature",
"felder": {
"titel": s["titel"],
"badge": s["art"],
"ziel": s["ziel"],
"regeln": s["regeln"],
},
"kontext": {"funktionen": "\n".join(f"- {f['text']}" for f in s["funktionen"])},
}]
for f in s["funktionen"]:
liste.append({
"feature": feature["id"],
"funktion": f["text"],
"bereich": "funktion",
"felder": {
"titel": f["text"],
"ziel": f["ziel"],
"regeln": f["regeln"],
"testen": f["testen"],
},
"kontext": {},
})
return liste
def inhalt(aufgabe: dict, offen: list[dict] = ()) -> str:
"""Der Ausschnitt für das Modell. `offen` sind gemessene Verstöße, für die der
Code keinen Ersatz kennt — sie hängen als Auftrag hinten dran."""
text = "".join(
_block(k, v) for k, v in {**aufgabe["felder"], **aufgabe["kontext"]}.items()
)
if offen:
text += "\nGemessen, bitte neu schreiben:\n"
text += "\n".join(struktur.auftrag(b) for b in offen) + "\n"
return text
def status(projekt: str) -> dict: def status(projekt: str) -> dict:
lauf = _laeufe.get(projekt) lauf = _laeufe.get(projekt)
if not lauf: if not lauf:
return {"laeuft": False, "fertig": 0, "gesamt": 0, "probleme": [], "meldung": ""} return {
"laeuft": False, "art": "", "fertig": 0, "gesamt": 0, "fehler": 0,
"probleme": [], "meldung": "",
}
return lauf return lauf
def _ohne_strich(text: str) -> str: def _ohne_strich(text: str) -> str:
return re.sub(r"^[-*]\s*", "", (text or "").strip()) """Listenzeilen kommen mal mit, mal ohne „- " zurück — je Zeile vereinheitlicht,
def _antwort_lesen(text: str) -> list[dict]:
"""Holt das JSON aus der Antwort, auch wenn es in ```-Zäunen steckt.
Listenzeilen kommen mal mit, mal ohne „- " zurück — hier vereinheitlicht,
damit die Anzeige die Stelle wiederfindet.""" damit die Anzeige die Stelle wiederfindet."""
treffer = re.search(r"\{.*\}", text, re.S) zeilen = (text or "").strip().splitlines()
if not treffer: return "\n".join(re.sub(r"^[-*]\s*", "", z.strip()) for z in zeilen)
raise ValueError("Antwort enthält kein JSON")
daten = json.loads(treffer.group(0))
probleme = [] _STRUKTUR = set(',:}]')
for p in daten.get("probleme") or []:
if not isinstance(p, dict) or not p.get("original"):
def _quotes_maskieren(roh: str) -> str:
"""Karteninhalt enthält Anführungszeichen (`Klick auf "Feature entfernen"`).
Das Modell gibt ihn wortwörtlich zurück und maskiert sie oft nicht — dann ist das
JSON kaputt. Ein Zeichen schließt den String nur, wenn danach JSON-Struktur folgt."""
aus = []
im_string = False
i = 0
while i < len(roh):
z = roh[i]
if im_string and z == "\\" and i + 1 < len(roh):
aus.append(roh[i:i + 2]) # bestehende Escape-Sequenz unverändert
i += 2
continue continue
p["original"] = _ohne_strich(p["original"]) if z == '"':
p["vorschlag"] = _ohne_strich(p.get("vorschlag")) if not im_string:
p["funktion"] = (p.get("funktion") or "").strip() im_string = True
probleme.append(p) elif roh[i + 1:].lstrip()[:1] in _STRUKTUR or not roh[i + 1:].strip():
return probleme im_string = False
else:
aus.append("\\")
aus.append(z)
i += 1
return "".join(aus)
async def _pruefe_karte(stil: str, feature: dict) -> list[dict]: def _json_lesen(text: str) -> dict:
text = await agents.frage(SYSTEM.format(stil=stil), feature["roh"]) """Holt das erste vollständige JSON-Objekt aus der Antwort.
probleme = _antwort_lesen(text)
raw_decode statt einer Regex: Antworten enden manchmal mit einem Nachsatz oder
einem zweiten Objekt — beides lief vorher in „Extra data".
"""
start = text.find("{")
if start < 0:
raise ValueError("Antwort enthält kein JSON")
roh = text[start:]
# strict=False lässt Zeilenumbrüche in Strings durch
leser = json.JSONDecoder(strict=False)
try:
return leser.raw_decode(roh)[0]
except json.JSONDecodeError:
return leser.raw_decode(_quotes_maskieren(roh))[0]
def _text(wert) -> str:
"""Listenfelder kommen mal als ein String mit Umbrüchen, mal als JSON-Liste."""
if isinstance(wert, list):
return "\n".join(str(z) for z in wert)
return str(wert) if wert is not None else ""
def _normal(zeile: str) -> str:
return re.sub(r"\s+", " ", zeile.strip().strip("-*·` ")).lower()
GEMESSEN = "## Vom System gemessen"
def katalogzeilen(stil: str) -> set:
"""Die zitierbaren Regeln: alle Aufzählungszeilen außer denen, die der Code
selbst misst. Sonst beruft sich das Modell auf die Füllwortliste und wendet sie
auf Wörter an, die gar nicht darin stehen."""
raus = set()
messbar = False
for zeile in stil.splitlines():
if zeile.startswith("## "):
messbar = zeile.strip() == GEMESSEN
elif zeile.strip().startswith("- ") and not messbar:
raus.add(_normal(zeile))
return raus
def _belegt(regel: str, zeilen: set) -> bool:
"""Steht die zitierte Regel wirklich im Katalog?
Ohne diesen Nachweis findet ein Modell immer irgendetwas zu verbessern und der
Lauf konvergiert nie. Kleine Zitierfehler lassen wir durch, freie Erfindungen nicht.
"""
regel = _normal(regel)
if not regel:
return False
if regel in zeilen:
return True
return any(difflib.SequenceMatcher(None, regel, z).ratio() >= 0.9 for z in zeilen)
def _felder_lesen(text: str, erlaubt: tuple, zeilen: set = None,
gemessen: tuple = ()) -> dict:
"""feld -> {"neu": str, "grund": str}. Unbekannte Felder fallen weg.
Ohne belegte Katalogzeile fällt die Änderung weg — außer der Code hat den
Verstoß selbst gemessen, dann steht er ohnehin fest."""
daten = _json_lesen(text).get("felder") or {}
raus = {}
for feld, wert in daten.items():
if feld not in erlaubt:
continue
if not isinstance(wert, dict): # manche Modelle kürzen auf den reinen Wert ab
wert = {"neu": wert, "grund": ""}
neu = _ohne_strich(_text(wert.get("neu")))
if "\n" in neu and feld not in ZEILENFELDER:
continue # nur Listen haben Zeilen
if not neu and feld not in LEERBAR:
continue # titel und badge dürfen nicht verschwinden
if zeilen is not None and feld not in gemessen:
if not _belegt(_text(wert.get("regel")), zeilen):
continue
raus[feld] = {"neu": neu, "grund": _text(wert.get("grund")).strip(),
"regel": _text(wert.get("regel")).strip()}
return raus
def _maengel(bereich: str, vorschlaege: dict) -> dict:
"""Was am Vorschlag selbst noch messbar falsch ist. Leer = brauchbar."""
raus = {}
for feld, wert in vorschlaege.items():
befunde = struktur.befunde(bereich, {feld: wert["neu"]})
if befunde:
raus[feld] = befunde
return raus
def _stellen(feld: str, alt: str, neu: str, grund: str, regel: str = "") -> list[dict]:
"""Aus alt und neu die einzelnen Änderungen ableiten.
`original` ist damit immer eine echte Zeile der Karte — das Modell kann sie
nicht mehr danebenlegen. Jede Stelle ist für sich anwendbar: Ersetzungen und
Streichungen greifen über den Zeilentext, Einfügungen hängen sich an die
Zeile davor und schreiben sie mit.
"""
if feld not in ZEILENFELDER:
return [] if alt.strip() == neu.strip() else [_stelle(feld, alt, neu, grund, regel)]
a = struktur._zeilen(feld, alt)
b = struktur._zeilen(feld, neu)
if a == b:
return []
if not a: # leeres Listenfeld — nichts, woran sich etwas festmachen ließe
return [_stelle(feld, "", "\n".join(b), grund, regel, ganz=True)]
raus = []
for tag, i1, i2, j1, j2 in difflib.SequenceMatcher(None, a, b).get_opcodes():
if tag == "equal":
continue
if tag == "insert":
# An die Zeile davor hängen; am Anfang an die dahinter
if i1 > 0:
anker, block = a[i1 - 1], [a[i1 - 1]] + b[j1:j2]
else:
anker, block = a[0], b[j1:j2] + [a[0]]
raus.append(_stelle(feld, anker, "\n".join(block), grund, regel))
continue
# replace und delete: die erste alte Zeile trägt den Ersatz, der Rest fällt weg
raus.append(_stelle(feld, a[i1], "\n".join(b[j1:j2]), grund, regel))
for zeile in a[i1 + 1:i2]:
raus.append(_stelle(feld, zeile, "", grund, regel))
# Mehrere Änderungen an einer Liste sind ein Umbau, kein Strauß von Einzelfixes.
# Einzeln angenommen ergäben sie Dubletten oder Lücken — also nur zusammen.
if len(raus) > 1:
return [_stelle(feld, "\n".join(a), "\n".join(b), grund, regel, ganz=True)]
return raus
def _stelle(feld: str, original: str, vorschlag: str, grund: str, regel: str = "",
ganz: bool = False) -> dict:
return {"feld": feld, "original": original, "vorschlag": vorschlag,
"ganzes_feld": ganz, "regel": regel, # belegte Katalogzeile, nachvollziehbar
"problem": grund or "Entspricht nicht dem Katalog"}
async def _stimme(system: str, aufgabe: dict, offen: list, erlaubt: tuple,
zeilen: set, temperatur: float) -> dict:
"""Eine Meinung, schon gegen die messbaren Vorgaben gefiltert. Was der Prüfer
selbst falsch schreibt, bekommt einen Versuch zur Nachbesserung."""
gemessen = tuple({b["feld"] for b in offen})
nachricht = inhalt(aufgabe, offen)
vorschlaege = {}
for versuch in range(VERSUCHE):
antwort = await agents.frage(system, nachricht, temperatur)
vorschlaege = _felder_lesen(antwort, erlaubt, zeilen, gemessen)
maengel = _maengel(aufgabe["bereich"], vorschlaege)
if not maengel:
return vorschlaege
if versuch == VERSUCHE - 1:
return {f: v for f, v in vorschlaege.items() if f not in maengel}
nachricht = inhalt(aufgabe, offen) + "\n" + NACHBESSERN.format(
maengel="\n".join(
f'- {feld} „{b["original"]}“: {b["problem"]}'
for feld, befunde in maengel.items() for b in befunde
)
)
return vorschlaege
def _konsens(fassungen: list[dict]) -> dict:
"""Die Fassung, die den anderen am ähnlichsten ist. Ein Ausreißer unter drei
zieht das Ergebnis so nicht mit."""
if len(fassungen) == 1:
return fassungen[0]
def naehe(a):
return sum(difflib.SequenceMatcher(None, a["neu"], b["neu"]).ratio()
for b in fassungen)
return max(fassungen, key=naehe)
def _mehrheit(proben: list[dict], gemessen: tuple = ()) -> dict:
"""Nur was alle Stimmen unabhängig bemängeln, wird ein Vorschlag.
Bei Grenzfällen urteilt ein Modell mal so, mal so — einzelne Stimmen führten
dazu, dass jede Runde andere Felder aufschlugen und der Lauf nie zur Ruhe kam.
Gemessene Verstöße sind davon ausgenommen. Sie stehen fest, bevor gefragt wird;
zur Abstimmung steht nur noch der Wortlaut. Vorher verschwand ein gezählter
Verstoß, sobald eine einzige Stimme das Feld nicht anfasste.
"""
zaehler = Counter(feld for p in proben for feld in p)
return {
feld: _konsens([p[feld] for p in proben if feld in p])
for feld, treffer in zaehler.items()
if treffer >= (1 if feld in gemessen else MEHRHEIT)
}
async def _neue_felder(system: str, aufgabe: dict, offen: list, erlaubt: tuple,
zeilen: set = None) -> dict:
"""Mehrere unabhängige Stimmen, dann die Mehrheit."""
proben = await asyncio.gather(*(
_stimme(system, aufgabe, offen, erlaubt, zeilen, t) for t in TEMPERATUREN
), return_exceptions=True)
# Eine kaputte Antwort kostet ihre Stimme, nicht die ganze Prüfstelle
gute = [p for p in proben if isinstance(p, dict)]
if not gute:
raise next(p for p in proben if isinstance(p, BaseException))
gemessen = tuple({b["feld"] for b in offen})
vorschlaege = _mehrheit(gute, gemessen)
# Bleibt ein gezählter Verstoß ohne Vorschlag, greift der eigene Ersatz des
# Messers. Sonst bliebe ein Fehler stehen, den der Code sicher kennt.
for b in offen:
if b["feld"] not in vorschlaege and b["vorschlag"]:
alt = struktur._zeilen(b["feld"], aufgabe["felder"].get(b["feld"], ""))
neu = [b["vorschlag"] if z == b["original"] else z for z in alt]
vorschlaege[b["feld"]] = {"neu": "\n".join(neu), "grund": b["problem"],
"regel": "Vom System gemessen"}
return vorschlaege
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(
stil=stil[aufgabe["bereich"]],
gegenstand=bereich["gegenstand"],
felder=_felder_text(bereich["felder"]),
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"]]))
probleme = []
for feld, wert in vorschlaege.items():
probleme += _stellen(feld, aufgabe["felder"].get(feld, ""), wert["neu"],
wert["grund"], wert.get("regel", ""))
# Herkunft stempelt der Code, nicht das Modell — es kennt nur seinen Ausschnitt
for p in probleme: for p in probleme:
p["feature"] = feature["id"] p["feature"] = aufgabe["feature"]
p["funktion"] = aufgabe["funktion"]
if stand is not None:
stand["stil"][schluessel] = {"abdruck": wie_gehabt, "probleme": probleme}
return probleme return probleme
async def _lauf(projekt: str, liste: list[dict]): async def _stil_aufgaben(projekt: str):
"""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)]
stand = ledger.lade(projekt)
stand["stil"] = ledger.behalte(stand["stil"], {_stilschluessel(a) for a in liste})
return (liste,
lambda a: _pruefe(stil, a, stand),
lambda: ledger.sichere(projekt, stand))
ARTEN = {
"stil": _stil_aufgaben,
}
async def _lauf(projekt: str, liste: list[dict], arbeite, abschluss=None):
lauf = _laeufe[projekt] lauf = _laeufe[projekt]
stil = config.STIL_DATEI.read_text()
schranke = asyncio.Semaphore(config.PRUEFUNG_PARALLEL) schranke = asyncio.Semaphore(config.PRUEFUNG_PARALLEL)
async def eine(feature): async def eine(aufgabe):
async with schranke: async with schranke:
if _laeufe.get(projekt) is not lauf: if _laeufe.get(projekt) is not lauf:
return # verworfen (Panel geschlossen) — keine weitere Anfrage return # verworfen (Panel geschlossen) — keine weitere Anfrage
try: try:
lauf["probleme"].extend(await _pruefe_karte(stil, feature)) lauf["probleme"].extend(await arbeite(aufgabe))
except Exception as e: except Exception as e:
lauf["meldung"] = str(e) # Zählt trotzdem als erledigt, sonst hängt der Fortschritt.
# Die Meldung nennt die Zahl, damit „100 %" nicht vollständig vortäuscht.
lauf["fehler"] += 1
lauf["meldung"] = (
f"{lauf['fehler']} von {lauf['gesamt']} Prüfungen fehlgeschlagen: {e}"
)
lauf["fertig"] += 1 lauf["fertig"] += 1
try: try:
await asyncio.gather(*(eine(f) for f in liste)) await asyncio.gather(*(eine(a) for a in liste))
finally: finally:
# Auch ein abgebrochener Lauf hat schon Urteile gefällt; die sind bezahlt
if abschluss:
abschluss()
lauf["laeuft"] = False lauf["laeuft"] = False
async def starte(projekt: str) -> dict: async def starte(projekt: str, art: str = "stil") -> dict:
"""Startet einen Lauf im Hintergrund; ein laufender wird nicht verdoppelt.""" """Startet einen Lauf im Hintergrund; ein laufender wird nicht verdoppelt."""
if art not in ARTEN:
raise ValueError(f"Unbekannte Prüfart „{art}“")
if status(projekt)["laeuft"]: 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 agents.pruefe_konfiguration() # früher Stopp statt halbem Lauf
liste = features.liste(projekt) # Vorbereiten vor dem Task: ein Fehler darin wäre sonst unsichtbar
liste, arbeite, *rest = await ARTEN[art](projekt)
_laeufe[projekt] = { _laeufe[projekt] = {
"laeuft": True, "laeuft": True,
"art": art,
"fertig": 0, "fertig": 0,
"gesamt": len(liste), "gesamt": len(liste),
"fehler": 0,
"probleme": [], "probleme": [],
"meldung": "", "meldung": "",
} }
asyncio.create_task(_lauf(projekt, liste)) aufgabe = asyncio.create_task(_lauf(projekt, liste, arbeite, *rest))
_aufgaben.add(aufgabe)
aufgabe.add_done_callback(_aufgaben.discard)
return _laeufe[projekt] return _laeufe[projekt]

208
backend/recherche.py Normal file
View File

@@ -0,0 +1,208 @@
"""Herausfinden, was gebaut werden soll — in mehreren Fäden nebeneinander.
Recherche ist kein Nachschlagewerk. Sie klärt, was draußen gilt, damit die Planung
daneben Karten schneiden kann: Zahlen, Grenzen, Namen, Belege. Was keine Entscheidung
trägt, gehört nicht in die Antwort.
Die Karten liegen dabei, aber nur zum Lesen. Ohne sie fragt man ins Leere — „welche
Funktionen fehlen noch" ist ohne den Bestand nicht zu beantworten, und das Modell
riete am Projekt vorbei. Vorschläge macht die Recherche trotzdem keine: sie schreibt
Prosa, keine Kartenfelder. Was aus einem Fund folgt, entscheidet die Person nebenan.
Mehrere Fäden, weil Themen sich sonst gegenseitig verdrecken. „Wie geht Upload zu S3"
und „welche Diagrammbibliothek" haben nichts miteinander zu tun; in einem Verlauf
zieht das Modell den einen Kontext in die andere Antwort. Ein Faden ist ein Thema,
und er bleibt stehen, bis ihn jemand schließt.
Die Antwort ist Markdown und wird als solches angezeigt. Kein JSON: hier wird nichts
in Felder zerlegt und nichts gegengerechnet, also gäbe es auch nichts zu prüfen — ein
Formatzwang kostete nur einen Aufruf mehr, wenn das Modell ihn verfehlt.
Die Fäden liegen im Projektstand, nicht in der Planungskopie: Nachgeschlagenes gilt
unabhängig davon, ob gerade geplant wird, und überlebt einen Planungsabbruch.
"""
import json
import uuid
import agents
import features
import parser
import projekte
import verzeichnis
DATEI = ".recherche.json"
# So viele Wortmeldungen behält ein Faden. Recherche geht in die Tiefe, deshalb mehr
# als im Planungsgespräch — dort steht das Ergebnis auf den Karten, hier nur im Faden.
TIEFE = 40
# Länger wird kein Reitertext lesbar; die ganze Frage steht ohnehin im Faden.
TITELLAENGE = 40
SYSTEM = """Du recherchierst für jemanden, der Software plant.
Was du herausfindest, wird eine Aufgabe für die Implementierung. Schreib deshalb so,
dass jemand danach loslegen kann, ohne deine Suche zu wiederholen: Zahlen, Grenzen,
Namen, Belege. Was keine Entscheidung trägt, lässt du weg — kein Hintergrund, keine
Geschichte, keine Alternative, die du schon verworfen hast.
Schlag im Web nach. Antworte nicht aus dem Gedächtnis, wenn es um Versionen, Grenzen,
Preise, Schnittstellen oder aktuelle Empfehlungen geht — da veraltet Wissen still.
Belege mit Links. Widersprechen sich zwei Quellen, sag es, statt dich zu entscheiden.
Findest du nichts, sag auch das — eine erfundene Zahl ist hier teurer als keine.
Antworte auf Deutsch, als Markdown. So ist eine Antwort gebaut:
Ein bis drei Sätze Prosa: was du gefunden hast und was daraus folgt.
## Festgelegt
Je Zeile eine Aussage mit ihrer Zahl, ihrem Namen, ihrer Grenze. Fett, was eine
Entscheidung ist. Keine Begründung — die steht oben in der Prosa.
## Offen
Was noch jemand entscheiden muss, als Frage. Weglassen, wenn nichts offen ist.
## Quellen
Je Zeile ein Link mit einem Halbsatz, wofür er steht.
Die Überschriften stehen nur da, wo sie etwas tragen. Auf eine Rückfrage antwortest du
in zwei Sätzen ohne Struktur; hast du nichts gefunden, sagst du das in einem.
Die Karten des Projekts stehen unten. Sie sagen, was gebaut werden soll — lies sie,
bevor du fragst, wovon die Rede ist. Wer wissen will, was noch fehlt, bekommt eine
Antwort, die sich auf diese Karten bezieht, nicht auf ein Projekt im Allgemeinen.
Du änderst die Karten nicht und schlägst auch keine Formulierung dafür vor. Was aus
deinem Fund folgt, entscheidet die Person nebenan. Den Quelltext siehst du nicht:
Was heute schon gebaut ist, steht als „gebaut" oder „offen" an den Funktionen.
Die Karten:
{karten}"""
def _punkte(text: str) -> list:
return [z.strip("- ").strip() for z in (text or "").splitlines() if z.strip()]
def _karten(projekt: str) -> str:
"""Der Kartenstand für den Prompt — mit „gebaut"/„offen", ohne Testabläufe.
Das Planungsgespräch blendet den Stand aus: dort entscheidet ihn der Code, und
ein Feld weniger ist ein Feld weniger zum Vergessen. Hier ist er das Wichtigste
am Bestand — „was fehlt noch" hängt an nichts anderem.
Die Testabläufe bleiben draußen. Sie sagen, wie man etwas prüft, und das trägt
keine Recherche; sie würden nur den Prompt füllen.
"""
vision, karten = "", []
for f in features.liste(projekt):
s = f["sektionen"]
if s["art"] == parser.VISION:
vision = s["ziel"]
continue
karten.append({
"titel": s["titel"], "art": s["art"], "ziel": s["ziel"],
"regeln": _punkte(s["regeln"]),
"funktionen": [{"titel": x["text"], "stand": x["stand"],
"ziel": x["ziel"], "regeln": _punkte(x["regeln"])}
for x in s["funktionen"]],
})
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 _pfad(projekt: str):
return projekte.stand(projekt) / DATEI
def _lade(projekt: str) -> list:
# Der Pfad wird vor dem try geholt: „Projekt gibt es nicht" ist ein
# FileNotFoundError und damit ein OSError. Im try gefangen, würde daraus ein
# leeres Ergebnis statt einer 404.
datei = _pfad(projekt)
try:
faeden = json.loads(datei.read_text())
except (OSError, ValueError):
return []
return faeden if isinstance(faeden, list) else []
def _sichere(projekt: str, faeden: list):
ziel = _pfad(projekt)
neben = ziel.with_suffix(".json.neu")
neben.write_text(json.dumps(faeden, indent=1, ensure_ascii=False) + "\n")
neben.replace(ziel)
def _finde(faeden: list, id: str) -> dict:
for f in faeden:
if f.get("id") == id:
return f
raise FileNotFoundError("Diesen Faden gibt es nicht mehr")
def liste(projekt: str) -> list:
"""Alle Fäden mit ihrem Verlauf — das Panel zeigt Reiter und Inhalt zugleich."""
return _lade(projekt)
def anlegen(projekt: str) -> dict:
"""Ein leerer Faden. Seinen Namen bekommt er von der ersten Frage."""
faeden = _lade(projekt)
faden = {"id": uuid.uuid4().hex[:8], "titel": "Neu", "verlauf": []}
faeden.append(faden)
_sichere(projekt, faeden)
return faden
def loeschen(projekt: str, id: str):
faeden = _lade(projekt)
_finde(faeden, id)
_sichere(projekt, [f for f in faeden if f.get("id") != id])
def _titel(text: str) -> str:
text = " ".join(text.split())
return text if len(text) <= TITELLAENGE else f"{text[:TITELLAENGE].rstrip()}…"
async def frage(projekt: str, id: str, text: str) -> dict:
"""Eine Runde in einem Faden — der Faden mit der neuen Antwort."""
text = (text or "").strip()
if not text:
raise ValueError("Nachricht ist leer")
faden = _finde(_lade(projekt), id)
gespraech = "\n\n".join(
f'{"Person" if e["rolle"] == "person" else "Du"}: {e["text"]}'
for e in faden["verlauf"]) or "Noch nichts besprochen."
nachricht = f"Bisher:\n{gespraech}\n\nNeue Frage:\n{text}"
# Ohne eingestelltes Arbeitsverzeichnis läuft der Prozess dort, wo der Server
# steht. Lesen ist ohnehin gesperrt; der Ordner sorgt nur dafür, dass ein
# gefundener Umweg im Projekt landet und nicht in planer2.
ordner = None
try:
ordner = str(verzeichnis.pruefe(verzeichnis.lies(projekt)))
except (ValueError, FileNotFoundError, OSError):
pass
antwort = (await agents.frage_lokal(
SYSTEM.format(karten=_karten(projekt)), nachricht,
agents.RECHERCHE_KOMMANDO, ordner)).strip()
# Erst jetzt neu laden: der Aufruf dauert Minuten, in denen ein anderer Faden
# angelegt oder geschlossen worden sein kann. Der Faden selbst kann weg sein —
# dann ist die Antwort verfallen, und das ist richtig so.
faeden = _lade(projekt)
faden = _finde(faeden, id)
if not faden["verlauf"]:
faden["titel"] = _titel(text)
faden["verlauf"] += [{"rolle": "person", "text": text},
{"rolle": "ki", "text": antwort}]
faden["verlauf"] = faden["verlauf"][-TIEFE:]
_sichere(projekt, faeden)
return faden

View File

@@ -3,9 +3,15 @@ hier wird auf 400/404/409 gemappt."""
from fastapi import APIRouter, HTTPException from fastapi import APIRouter, HTTPException
import agents
import auftrag
import chat
import features import features
import projekte import projekte
import planung
import pruefung import pruefung
import recherche
import verzeichnis
router = APIRouter(prefix="/api") router = APIRouter(prefix="/api")
@@ -54,9 +60,13 @@ async def projekt_loeschen(name: str):
@router.get("/projekte/{name}/features") @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: try:
return features.liste(name) return features.liste(name, nur_stand=sicht == "uebersicht")
except (FileNotFoundError, ValueError) as e: except (FileNotFoundError, ValueError) as e:
raise HTTPException(404, str(e)) raise HTTPException(404, str(e))
@@ -82,13 +92,16 @@ async def feature_speichern(name: str, id: str, body: dict):
@router.post("/projekte/{name}/pruefung") @router.post("/projekte/{name}/pruefung")
async def pruefung_starten(name: str): async def pruefung_starten(name: str, body: dict | None = None):
art = (body or {}).get("art") or "stil"
try: try:
return await pruefung.starte(name) return await pruefung.starte(name, art)
except FileNotFoundError as e: except FileNotFoundError as e:
raise HTTPException(404, str(e)) raise HTTPException(404, str(e))
except pruefung.LaeuftBereits as e:
raise HTTPException(409, str(e)) # 409 heißt genau das und sonst nichts
except ValueError as e: except ValueError as e:
raise HTTPException(409, str(e)) raise HTTPException(400, str(e))
@router.get("/projekte/{name}/pruefung") @router.get("/projekte/{name}/pruefung")
@@ -96,6 +109,105 @@ async def pruefung_status(name: str):
return pruefung.status(name) 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):
"""Mit Arbeitsverzeichnis implementiert ein Agent die Änderungen; die Planung
bleibt stehen und wird erst nach seinem Erfolg übernommen. Ohne Verzeichnis
oder ohne Änderungen wird sofort übernommen."""
try:
anweisung, schritte = auftrag.bau_auftrag(name)
agent = auftrag.starte(name, anweisung, schritte)
if not agent:
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, "agent": agent}
@router.get("/projekte/{name}/auftrag")
async def auftrag_status(name: str):
return auftrag.status(name)
@router.get("/system/ordner")
async def ordner_liste(pfad: str = ""):
"""Zum Durchblättern im Verzeichnis-Dialog — listet nur, schreibt nie."""
try:
return verzeichnis.ordner(pfad)
except FileNotFoundError as e:
raise HTTPException(404, str(e))
except ValueError as e:
raise HTTPException(400, str(e))
@router.get("/projekte/{name}/verzeichnis")
async def verzeichnis_lesen(name: str):
try:
return {"pfad": verzeichnis.lies(name)}
except FileNotFoundError as e:
raise HTTPException(404, str(e))
@router.put("/projekte/{name}/verzeichnis")
async def verzeichnis_setzen(name: str, body: dict):
pfad = (body.get("pfad") or "").strip()
try:
if not pfad:
verzeichnis.loesche(name)
return {"pfad": ""}
return {"pfad": verzeichnis.setze(name, pfad)}
except FileNotFoundError as e:
raise HTTPException(404, str(e))
except ValueError as e:
raise HTTPException(400, str(e))
@router.delete("/projekte/{name}/planung")
async def planung_abbrechen(name: str):
# Solange der Agent auf der Planung arbeitet, darf sie ihm niemand wegziehen
if auftrag.status(name)["laeuft"]:
raise HTTPException(409, "Der Agent arbeitet noch an dieser Planung")
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):
if auftrag.status(name)["laeuft"]:
raise HTTPException(409, "Der Agent arbeitet noch an dieser Planung")
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") @router.delete("/projekte/{name}/pruefung")
async def pruefung_beenden(name: str): async def pruefung_beenden(name: str):
pruefung.beende(name) pruefung.beende(name)
@@ -109,3 +221,69 @@ async def feature_loeschen(name: str, id: str):
except (FileNotFoundError, ValueError) as e: except (FileNotFoundError, ValueError) as e:
raise HTTPException(404, str(e)) raise HTTPException(404, str(e))
return {"ok": True} return {"ok": True}
@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}
@router.get("/projekte/{name}/recherche")
async def recherche_faeden(name: str):
try:
return {"faeden": recherche.liste(name)}
except FileNotFoundError as e:
raise HTTPException(404, str(e))
@router.post("/projekte/{name}/recherche")
async def recherche_anlegen(name: str):
try:
return recherche.anlegen(name)
except FileNotFoundError as e:
raise HTTPException(404, str(e))
@router.post("/projekte/{name}/recherche/{id}")
async def recherche_frage(name: str, id: str, body: dict):
try:
return await recherche.frage(name, id, 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}/recherche/{id}")
async def recherche_loeschen(name: str, id: str):
try:
recherche.loeschen(name, id)
except FileNotFoundError as e:
raise HTTPException(404, str(e))
return {"ok": True}

View File

@@ -1,68 +0,0 @@
# Stil-Katalog für Karten
Verbindliche Vorgaben für alle Inhalte einer Feature-Karte. Der Prüfer meldet jede
Abweichung als Problem und schlägt einen korrigierten Text vor.
## Allgemein
- Sprache: Deutsch, Präsens, keine Anrede
- Kein Satzzeichen am Zeilenende (kein Punkt, kein Semikolon)
- Keine Füllwörter (auch, noch, einfach, jeweils, ggf.)
- Keine Klammern und keine Schrägstriche außer in Icon-Angaben
- Icons heißen `<Name>-Icon`, etwa `x-Icon` oder `Stift-Icon`; ein Doppelpunkt mit dem
tatsächlichen Zeichen dahinter ist erlaubt (`Nacht-Icon: ☾`)
- Maustasten als `LMB`, `RMB`, `MMB`
## Titel der Karte
- Höchstens 3 Wörter
- Substantiv, kein Verb
- Benennt die Sache, nicht die Tätigkeit
Richtig: `Features` · `Karten prüfen` · `Darkmode`
Falsch: `Features verwalten und anzeigen` (zu lang, Tätigkeit)
## Titel einer Funktion
- Höchstens 5 Wörter
- Kurzsatz aus Objekt und Verb im Infinitiv
- Genau eine Tätigkeit
Richtig: `Feature erstellen` · `Anzeigemodus umschalten` · `Feature als Datei speichern`
Falsch: `Erstellung von Features` (Substantivierung) · `Feature erstellen und öffnen` (zwei Tätigkeiten)
## Ziel
- Höchstens 2 Sätze, je höchstens 12 Wörter
- Beschreibt den Zweck, nicht die Bedienung
- Entfällt, wenn der Titel selbsterklärend ist — dann bleibt das Feld leer
Richtig: `Gruppiert Informationen zu ähnlichen Funktionen`
Falsch: `Klick auf die Karte öffnet den Vollmodus` (Bedienung gehört zu Testen)
## Regeln
- Eine Zeile je Regel, höchstens 10 Wörter
- Genau eine Aussage je Zeile; zwei Aussagen werden zu zwei Regeln
- Beschreibt eine Eigenschaft oder Bedingung, keinen Ablaufschritt
- Keine Aufzählung mit Komma oder „und" zweier gleichrangiger Aussagen
Richtig: `Alphabetisch sortiert` · `Kern blau umrandet` · `x-Icon`
Falsch: `Kern blau umrandet, Optional grün umrandet` (zwei Aussagen)
Falsch: `Klick auf Badge` (Ablaufschritt, gehört zu Testen)
## Testen
- Eine Zeile je Schritt, höchstens 8 Wörter
- Schritte in Reihenfolge der Bedienung
- Letzte Zeile beschreibt den Endzustand, keine Handlung
- Handlungen als Nominalstil mit Maustaste oder Verb im Infinitiv
Richtig:
```
- Vollmodus öffnen
- RMB auf Funktion
- Klick auf "Funktion entfernen"
- Funktion entfernt
```
Falsch: letzte Zeile `Klick zum entfernen` (Handlung statt Endzustand)

52
backend/stil/allgemein.md Normal file
View File

@@ -0,0 +1,52 @@
# Stil-Katalog für Karten
Verbindliche Vorgaben für alle Inhalte einer Feature-Karte. Der Prüfer meldet jede
Abweichung als Problem und schlägt einen korrigierten Text vor.
## Allgemein
Gilt für jedes Feld.
- Sprache: Deutsch, Präsens, keine Anrede
- Maustasten als `LMB`, `RMB`, `MMB`
- Ein Menüeintrag wird wörtlich zitiert: `Klick auf "Feature entfernen"`
- Ein Kommando wird wörtlich zitiert: `Befehl "make install" ausführen`
- Ein Zitat zählt als ein Wort und darf Sonderzeichen tragen
- Jeder Gegenstand heißt überall gleich
## Bedienung, nicht Bauweise
Karten beschreiben, was gilt und wie man es auslöst. Wie es aussieht und wie es
innen gebaut ist, gehört nicht dazu.
- Keine Icons, keine Zeichen als Namen
- Keine Farben
- Keine Positionen wie `rechts oben` oder `unter dem Titel`
- Kein Layout: Rahmen, Spalten, Abstände, Breiten
- Erlaubt ist der Bereich: `Kopfzeile`, `Hauptbereich`, `Projektliste`
- Kein Aufbau eines Skripts, nur sein Aufruf
- Keine Dateinamen, Klassen oder Bibliotheken
Richtig: `LMB auf die Einstufung` · `Rückfrage vor dem Entfernen`
Richtig: `Befehl "make install" ausführen` · `Ein Aufruf genügt`
Falsch: `Augen-Icon rechts oben` · `Kern blau umrandet` · `Marke rechts vom Titel`
Falsch: `Das Skript legt einen Symlink in autostart` (Bauweise)
## Vom System gemessen
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 Testschritte — ohne sie ist sie keine Funktion
- Titel einer Funktion endet auf ein Verb im Infinitiv
- Titel einer Funktion beginnt nicht mit einem Auslöser
- 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 außerhalb eines Zitats
- Titel der Karte: höchstens 3 Wörter
- Titel einer Funktion: höchstens 5 Wörter
- Ziel: höchstens 2 Sätze, je höchstens 12 Wörter
- Regeln: höchstens 10 Wörter je Zeile
- Testen: höchstens 8 Wörter je Zeile
- Badge: genau `kern` oder `optional`

View File

@@ -0,0 +1,12 @@
## Badge der Karte
Feld `badge`.
- `kern`: fehlt die Karte, kann die Anwendung nicht laufen
- `optional`: fehlt die Karte, läuft die Anwendung weiter — mit weniger Funktionen
- Die Prüffrage ist immer dieselbe: Streiche die Karte. Läuft der Rest noch?
- Die Einstufung muss zum Ziel der Karte passen
Richtig: `Projekt` ist kern · `Prüfung` ist optional
Falsch: `Prüfung` als kern (ohne Prüfung läuft der Planer weiter)
Falsch: `Sicherung` als kern, weil sie wichtig ist (wichtig heißt nicht tragend)

View File

@@ -0,0 +1,16 @@
## Regeln der Karte
Feld `regeln`, wenn `funktion` leer ist. Gilt für die ganze Karte.
- Eine Zeile je Regel
- Genau eine Aussage je Zeile; zwei Aussagen werden zu zwei Regeln
- Sichert etwas zu, das man sonst nicht sieht
- Beschreibt eine Eigenschaft oder Bedingung, keinen Ablaufschritt
- Steht nicht schon in den Testschritten
- Steht nicht schon als Regel der Karte
- Keine Aufzählung mit Komma oder „und" zweier gleichrangiger Aussagen
Richtig: `Alphabetisch sortiert` · `Verlassen speichert` · `Escape verwirft`
Falsch: `Titel eingeben, dann bestätigen` (zwei Aussagen)
Falsch: `LMB auf Badge` (Ablaufschritt, gehört zu Testen)
Falsch: `Entsteht leer`, wenn die Testschritte keine Eingabe zeigen (schon sichtbar)

View File

@@ -0,0 +1,13 @@
## Titel der Karte
Feld `titel`, wenn `funktion` leer ist.
- Benennt einen Gegenstand, keine Tätigkeit
- Sammelbegriff für die Funktionen der Karte
- Jede Funktion der Karte handelt von diesem Gegenstand
- Ein Wort, wenn es reicht
Richtig: `Projekt` · `Feature` · `Vorschlag` · `Planung`
Falsch: `Karten prüfen` (Tätigkeit, das ist ein Funktionstitel)
Falsch: `Verwaltung und Anzeige` (zwei Gegenstände)
Falsch: `Gespräch` für eine Karte über Anweisungen (führt in die Irre)

View File

@@ -0,0 +1,14 @@
## Ziel der Karte
Feld `ziel`, wenn `funktion` leer ist.
- Ergänzt den Titel, bis er eindeutig ist
- Lässt erwarten, welche Funktionen auf der Karte stehen
- Beginnt mit einem Verb
- Erklärt den Titel nicht mit sich selbst
- Beschreibt den Zweck, nie die Bedienung
Richtig: `Bündelt die Features einer Anwendung samt Verzeichnis`
Falsch: `Gruppiert Features für ein Projekt` auf der Karte `Projekt` (zirkulär)
Falsch: `Eine Änderung, die auf Zustimmung wartet` (Definition ohne Verb)
Falsch: `LMB auf die Karte öffnet sie` (Bedienung gehört zu Testen)

View File

@@ -0,0 +1,16 @@
## Regeln einer Funktion
Feld `regeln`, wenn `funktion` gesetzt ist. Gilt nur für diese Funktion.
- Eine Zeile je Regel
- Genau eine Aussage je Zeile; zwei Aussagen werden zu zwei Regeln
- Sichert etwas zu, das man sonst nicht sieht
- Beschreibt eine Eigenschaft oder Bedingung, keinen Ablaufschritt
- Steht nicht schon in den Testschritten
- Steht nicht schon als Regel der Karte
- Keine Aufzählung mit Komma oder „und" zweier gleichrangiger Aussagen
Richtig: `Titel ist Pflicht` · `Rückfrage vor dem Entfernen`
Falsch: `Titel eingeben, dann bestätigen` (zwei Aussagen)
Falsch: `LMB auf Badge` (Ablaufschritt, gehört zu Testen)
Falsch: `Entsteht leer`, wenn die Testschritte keine Eingabe zeigen (schon sichtbar)

View File

@@ -0,0 +1,47 @@
## Testen einer Funktion
Feld `testen`. Gibt es nur bei Funktionen, nie auf Kartenebene.
Pflicht bei jeder Funktion. Eine Funktion ist etwas, das man für sich testen kann —
ohne Ablauf ist sie nur eine Behauptung. Ziel und Regeln dürfen fehlen, Testen nie.
Weiß man den Ablauf nicht, ist es keine Funktion, sondern eine offene Frage.
- Eine Zeile je Schritt
- Genau ein Ablauf: handeln, handeln, Ergebnis
- Nach dem Ergebnis folgt keine Handlung mehr
- Schritte in Reihenfolge der Bedienung
- Beginnt mit der Vorbedingung, wenn eine nötig ist
- Kein Schritt der Bedienung fehlt
- Jeder Schritt ist ohne Vorwissen ausführbar
- Letzte Zeile beschreibt den Endzustand, keine Handlung
- Handlungen als Nominalstil mit Maustaste, Kommando oder Verb im Infinitiv
- Ein Kommando steht wörtlich im Zitat und ist so aufrufbar
Richtig:
```
- Feature öffnen
- RMB auf die Funktion
- Klick auf "Funktion entfernen"
- Bestätigen
- Funktion entfernt
```
Auf der Kommandozeile gilt dasselbe — der Aufruf ist die Handlung:
```
- Befehl "make install" ausführen
- Abmelden und wieder anmelden
- Anwendung läuft automatisch
```
Falsch: `Installationsskript starten` (sagt nicht, wie)
Falsch: `Der Prüfschritt kopiert die Desktop-Datei` (Bauweise statt Bedienung)
Falsch: letzte Zeile `Klick zum Entfernen` (Handlung statt Endzustand)
Falsch: `Funktion entfernen wählen` (sagt nicht, wo)
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

@@ -0,0 +1,20 @@
## Titel einer Funktion
Feld `titel`, wenn `funktion` gesetzt ist.
- Kurzsatz aus Objekt und Verb im Infinitiv
- Beschreibt etwas, das man für sich testen kann
- Nennt den Gegenstand der Karte, wenn das nicht lügt
- Genau eine Tätigkeit
- Nennt eine Handlung, keinen Bereich, keinen Zustand
- Die Handlung löst ein Mensch aus, nicht das System
- Nennt die Wirkung, nie den Auslöser — der steht in den Testschritten
Richtig: `Feature anlegen` · `Vorschlag annehmen` · `Sicht wechseln`
Richtig: `Anwendung installieren` · `Autostart aktivieren` · `Tests ausführen`
Falsch: `Erstellung von Features` (Substantivierung)
Falsch: `Feature anlegen und öffnen` (zwei Tätigkeiten)
Falsch: `Galerie im Modal` (ein Bereich) · `Preis aktualisiert` (ein Zustand)
Falsch: `Änderung markieren` (das System markiert, gehört in die Regeln)
Falsch: `Befehl "make install" ausführen` (der Auslöser, gehört zu Testen)
Falsch: `LMB auf Speichern` (der Auslöser, gehört zu Testen)

View File

@@ -0,0 +1,13 @@
## Ziel einer Funktion
Feld `ziel`, wenn `funktion` gesetzt ist.
- Ergänzt den Titel um das Warum
- Beginnt mit einem Verb
- Entfällt, wenn der Titel selbsterklärend ist — dann bleibt das Feld leer
- Wiederholt den Titel nicht
- Beschreibt den Zweck, nie die Bedienung
Richtig: `Trennt tragende von verzichtbaren Features`
Falsch: `Legt die Vision an` auf `Vision anlegen` (wiederholt den Titel)
Falsch: `LMB auf Regeln der Funktion` (Bedienung gehört zu Testen)

203
backend/struktur.py Normal file
View File

@@ -0,0 +1,203 @@
"""Stufe 1 der Prüfung: alles, was sich zählen lässt, zählt der Code.
Das Modell erfand hier Regeln, die im Katalog nicht stehen — „Im" als Füllwort,
Satzzeichen, wo keins ist. Diese Klasse ist jetzt abschließend hier erledigt, und
der Prompt verbietet dem Modell, sie noch einmal zu melden.
Ein Befund mit Vorschlag geht ohne Anfrage durch. Ein Befund ohne Vorschlag heißt:
der Code weiß, dass die Stelle falsch ist, aber nicht, was richtig wäre — den
Auftrag bekommt das Modell.
"""
import re
# Höchstzahl Wörter je Zeile, nach (bereich, feld)
WORTGRENZEN = {
("feature", "titel"): 3,
("funktion", "titel"): 5,
("feature", "regeln"): 10,
("funktion", "regeln"): 10,
("funktion", "testen"): 8,
}
ZIEL_SAETZE = 2
ZIEL_WOERTER = 12
# Abschließend. Genau diese Wörter, keine sinnverwandten.
FUELLWOERTER = ("auch", "noch", "einfach", "jeweils", "ggf")
BADGE = ("kern", "optional")
# Felder, die als Liste geführt werden — je Zeile ein Eintrag.
# „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.
# „Befehl" und „Kommando" stehen dabei wie eine Taste: auf der Kommandozeile ist der
# Aufruf die Handlung, und ohne sie wäre ein Setup nicht bedienbar zu beschreiben.
_TASTE = re.compile(
r"^(?:LMB|RMB|MMB|Klick|Doppelklick|Enter|Escape|Strg|Tab|Befehl|Kommando)\b", re.I)
SATZENDE = ".;"
_KLAMMER = re.compile(r"[()/]")
# Ein Zitat ist eine Einheit: ein Menüeintrag oder ein Kommando. „Befehl "make install"
# ausführen" sind drei Wörter, nicht vier, und der Schrägstrich in „npm run build/dev"
# ist keiner von uns. Darum wird außerhalb der Zitate gemessen.
_ZITAT = 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*")
def _zeilen(feld: str, text: str) -> list[str]:
"""regeln und testen sind Listen, alles andere ist ein Block."""
if feld in ZEILENFELDER:
return [_STRICH.sub("", z.strip()) for z in text.splitlines() if z.strip()]
return [text.strip()] if text.strip() else []
def _problem(feld: str, original: str, problem: str, vorschlag: str = "") -> dict:
return {"feld": feld, "original": original, "problem": problem, "vorschlag": vorschlag}
def _messbar(zeile: str) -> str:
"""Die Zeile mit jedem Zitat als einem Wort — so wird gezählt und gesucht."""
return _ZITAT.sub("Zitat", zeile)
def _ausserhalb(zeile: str, wandel) -> str:
"""Wendet `wandel` nur auf die Teile außerhalb der Zitate an.
Ein Füllwort im Menüeintrag ist keins — dort steht der Text, den die Anwendung
zeigt. Würde er mitgestrichen, verwiese der Testschritt auf einen Knopf, den es
nicht gibt.
"""
teile, letzt = [], 0
for m in _ZITAT.finditer(zeile):
teile.append(wandel(zeile[letzt:m.start()]))
teile.append(m.group(0))
letzt = m.end()
teile.append(wandel(zeile[letzt:]))
return "".join(teile)
def _ohne_wort(zeile: str, wort: str) -> str:
weg = lambda t: re.sub(rf"\s*\b{re.escape(wort)}\b\.?", "", t, flags=re.I)
return re.sub(r"\s{2,}", " ", _ausserhalb(zeile, weg)).strip()
def _reparierbar(zeile: str) -> tuple[list[str], str]:
"""Verstöße, die sich wegstreichen lassen, samt der bereinigten Zeile."""
gruende, neu = [], zeile
if neu[-1] in SATZENDE:
gruende.append("Satzzeichen am Zeilenende")
neu = neu[:-1].rstrip()
for wort in FUELLWOERTER:
if re.search(rf"\b{wort}\b", _messbar(neu), re.I):
gruende.append(f"Füllwort „{wort}“")
neu = _ohne_wort(neu, wort)
return gruende, neu
def _umschreiben(bereich: str, feld: str, zeile: str) -> list[str]:
"""Verstöße, die der Code sieht, aber nicht beheben kann."""
gruende = []
gemessen = _messbar(zeile)
if _KLAMMER.search(gemessen):
gruende.append("Klammer oder Schrägstrich")
if _LUECKE.search(gemessen):
gruende.append("Platzhalter statt Inhalt")
grenze = WORTGRENZEN.get((bereich, feld))
if grenze and len(gemessen.split()) > grenze:
gruende.append(f"Mehr als {grenze} Wörter")
if bereich == "funktion" and feld == "titel":
woerter = gemessen.split()
if woerter and not _INFINITIV.search(woerter[-1]):
gruende.append("Kein Verb im Infinitiv am Ende")
# Der Titel nennt die Wirkung, nicht den Griff. „Befehl "make install"
# ausführen" ist ein Testschritt — als Titel sagt er nicht, was danach gilt.
if _TASTE.match(zeile):
gruende.append("Auslöser statt Wirkung im Titel")
if feld == "ziel":
saetze = [s.strip() for s in re.split(r"[.!?]+", gemessen) if s.strip()]
if len(saetze) > ZIEL_SAETZE:
gruende.append(f"Mehr als {ZIEL_SAETZE} Sätze")
if any(len(s.split()) > ZIEL_WOERTER for s in saetze):
gruende.append(f"Satz mit mehr als {ZIEL_WOERTER} Wörtern")
return gruende
def _handlung(zeile: str) -> bool:
woerter = _messbar(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 i, zeile in enumerate(zeilen):
if ergebnis and _handlung(zeile):
return zeile
# Die erste Zeile darf ein Zustand sein: das ist die Vorbedingung, nicht das
# Ergebnis. „Badge ist sichtbar" vor „RMB auf Badge" ist ein Ablauf, nicht zwei.
if i and 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."""
gruende, neu = _reparierbar(zeile)
offen = _umschreiben(bereich, feld, zeile)
if offen:
# Muss ohnehin neu geschrieben werden — dann alles in einem Zug, vom Modell
return [_problem(feld, zeile, ", ".join(gruende + offen))]
return [_problem(feld, zeile, ", ".join(gruende), neu)] if gruende else []
def befunde(bereich: str, felder: dict) -> list[dict]:
"""Alle messbaren Verstöße einer Prüfstelle, in Reihenfolge der Felder."""
raus = []
# Testschritte sind Pflicht. Eine Funktion ist definiert als etwas, das man für
# sich testen kann — ohne Ablauf ist die Behauptung unbelegt und der Titel lässt
# sich über alles stülpen. Ziel und Regeln bleiben freiwillig.
# 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 (felder.get("testen") or "").strip():
raus.append(_problem("testen", "", "Funktion ohne Testschritte"))
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
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
def auftrag(befund: dict) -> str:
"""Zeile für das Modell: der Code hat gemessen, den Ersatz schreibt das Modell."""
return f'- {befund["feld"]} „{befund["original"]}“: {befund["problem"]}'

View File

@@ -0,0 +1,418 @@
"""Agenten-Übergabe: der Auftrag beschreibt den Diff, der Start braucht ein Verzeichnis."""
import asyncio
import pytest
import auftrag
import features
import planung
import projekte
import verzeichnis
T = "-" * 40
@pytest.fixture
def projekt(datenordner):
projekte.anlegen("A")
features.anlegen("A", f"<!-- titel -->\nGalerie\n{T}\n<!-- funktion -->\nBild wählen\n")
features.anlegen("A", f"<!-- titel -->\nShop\n{T}\n<!-- funktion -->\nKarte kaufen\n")
yield "A"
auftrag._laeufe.clear()
def test_ohne_planung_kein_auftrag(projekt):
assert auftrag.bau_auftrag(projekt) == ("", [])
def test_leere_planung_kein_auftrag(projekt):
planung.starte(projekt)
assert auftrag.bau_auftrag(projekt) == ("", [])
def test_auftrag_nennt_diff_und_sollzustand(projekt):
planung.starte(projekt)
features.anlegen(projekt, f"<!-- titel -->\nMerkliste\n{T}\n<!-- funktion -->\nProdukt merken\n")
f = next(x for x in features.liste(projekt) if x["sektionen"]["titel"] == "Shop")
features.loeschen(projekt, f["id"])
text, schritte = auftrag.bau_auftrag(projekt)
texte = [s["text"] for s in schritte]
assert "Merkliste / Produkt merken: PRÜFEN" in texte
assert "Karte „Shop“ streichen" in texte
assert all(s in text for s in texte)
# Die Zuordnung steht daneben, nicht nur im Satz
assert {"text": "Merkliste / Produkt merken: PRÜFEN", "karte": "Merkliste",
"funktion": "Produkt merken"} in schritte
assert {"text": "Karte „Shop“ streichen", "karte": "Shop",
"funktion": ""} in schritte
# Sollzustand: die neue Karte steht voll drin, die gestrichene nicht
assert "<!-- funktion -->\nProdukt merken" in text
assert "Karte kaufen" not in text.split("Die betroffenen Karten")[1]
def test_start_braucht_verzeichnis(projekt):
assert auftrag.starte(projekt, "tu was", ["x"]) is False
def test_start_braucht_auftrag(projekt, tmp_path):
verzeichnis.setze(projekt, str(tmp_path))
assert auftrag.starte(projekt, "", []) is False
class Strom:
"""Attrappe des Claude-Prozesses: gibt vorbereitete Stream-Zeilen aus."""
def __init__(self, zeilen, rc=0, gesehen=None):
import json as _json
self._zeilen = [(_json.dumps(z) + "\n").encode() for z in zeilen]
self.returncode = rc
self._gesehen = gesehen if gesehen is not None else {}
class Stdin:
def write(s, daten): self._gesehen["eingabe"] = daten.decode()
async def drain(s): pass
def close(s): pass
self.stdin = Stdin()
class Stdout:
async def readline(s):
return self._zeilen.pop(0) if self._zeilen else b""
self.stdout = Stdout()
class Stderr:
async def read(s): return b""
self.stderr = Stderr()
async def wait(self):
return self.returncode
def text_ereignis(text):
return {"type": "assistant", "message": {"content": [{"type": "text", "text": text}]}}
def test_start_fuehrt_claude_im_verzeichnis_aus(projekt, tmp_path, monkeypatch):
verzeichnis.setze(projekt, str(tmp_path))
planung.starte(projekt)
gesehen = {}
async def unecht(*kommando, cwd, **k):
gesehen["kommando"] = kommando
gesehen["cwd"] = cwd
return Strom([{"type": "result", "result": "fertig"}], gesehen=gesehen)
monkeypatch.setattr(asyncio, "create_subprocess_exec", unecht)
async def lauf():
assert auftrag.starte(projekt, "Setze um: X",
[{"text": "S1", "karte": "", "funktion": ""}]) is True
assert auftrag.status(projekt)["laeuft"] is True
await asyncio.gather(*auftrag._aufgaben)
asyncio.run(lauf())
assert gesehen["kommando"][0] == "claude"
assert "--output-format" in gesehen["kommando"]
assert gesehen["cwd"] == str(tmp_path)
assert gesehen["eingabe"] == "Setze um: X"
stand = auftrag.status(projekt)
assert stand["text"] == "fertig"
assert stand["laeuft"] is False
def test_schritte_werden_abgehakt_und_planung_uebernommen(projekt, tmp_path, monkeypatch):
verzeichnis.setze(projekt, str(tmp_path))
planung.starte(projekt)
features.anlegen(projekt, f"<!-- titel -->\nMerkliste\n{T}\n<!-- funktion -->\nProdukt merken\n")
anweisung, schritte = auftrag.bau_auftrag(projekt)
async def unecht(*a, **k):
return Strom([
{"type": "assistant", "message": {"content": [
{"type": "tool_use", "name": "Edit", "input": {"file_path": "x.py"}}]}},
text_ereignis(f'ERLEDIGT: {schritte[0]["text"]}'),
{"type": "result", "result": "alles gebaut"},
])
monkeypatch.setattr(asyncio, "create_subprocess_exec", unecht)
async def lauf():
auftrag.starte(projekt, anweisung, schritte)
await asyncio.gather(*auftrag._aufgaben)
asyncio.run(lauf())
stand = auftrag.status(projekt)
assert stand["schritte"] == [{**schritte[0], "fertig": True}]
assert stand["uebernommen"] is True
assert planung.laeuft(projekt) is False
assert "Merkliste" in [f["sektionen"]["titel"] for f in features.liste(projekt)]
def test_fehlschlag_laesst_die_planung_stehen(projekt, tmp_path, monkeypatch):
verzeichnis.setze(projekt, str(tmp_path))
planung.starte(projekt)
features.anlegen(projekt, f"<!-- titel -->\nMerkliste\n{T}\n<!-- funktion -->\nProdukt merken\n")
anweisung, schritte = auftrag.bau_auftrag(projekt)
async def unecht(*a, **k):
return Strom([], rc=1)
monkeypatch.setattr(asyncio, "create_subprocess_exec", unecht)
async def lauf():
auftrag.starte(projekt, anweisung, schritte)
await asyncio.gather(*auftrag._aufgaben)
asyncio.run(lauf())
stand = auftrag.status(projekt)
assert stand["uebernommen"] is False
assert stand["fehler"]
assert planung.laeuft(projekt) is True
assert "Merkliste" not in [f["sektionen"]["titel"] for f in features.liste(projekt, nur_stand=True)]
def test_ungefaehre_meldung_hakt_den_naechsten_schritt_ab():
lauf = {"schritte": [{"text": "Galerie / Bild wählen: NEU", "fertig": False},
{"text": "Karte „Shop“ streichen", "fertig": False}]}
auftrag._hake_ab("Test", lauf, "ERLEDIGT: Galerie / Bild wählen (NEU)")
assert lauf["schritte"][0]["fertig"] is True
auftrag._hake_ab("Test", lauf, "ERLEDIGT: völlig anderes Zeug")
assert lauf["schritte"][1]["fertig"] is False
def test_fehlender_claude_wird_gemeldet(projekt, tmp_path, monkeypatch):
verzeichnis.setze(projekt, str(tmp_path))
async def kaputt(*a, **k):
raise FileNotFoundError("claude")
monkeypatch.setattr(asyncio, "create_subprocess_exec", kaputt)
async def lauf():
auftrag.starte(projekt, "X", [{"text": "S", "karte": "", "funktion": ""}])
await asyncio.gather(*auftrag._aufgaben)
asyncio.run(lauf())
assert "claude-CLI nicht gefunden" in auftrag.status(projekt)["fehler"]
assert auftrag.status(projekt)["laeuft"] is False
def test_zweiter_start_wird_abgelehnt(projekt, tmp_path):
verzeichnis.setze(projekt, str(tmp_path))
auftrag._laeufe[projekt] = {"laeuft": True, "fehler": "", "text": "",
"schritte": [], "aktivitaet": "", "uebernommen": False}
with pytest.raises(ValueError):
auftrag.starte(projekt, "X", [{"text": "S", "karte": "", "funktion": ""}])
# ------------------------------------------------------------- Verzeichnis
def test_verzeichnis_wird_gemerkt(projekt, tmp_path):
verzeichnis.setze(projekt, str(tmp_path))
assert verzeichnis.lies(projekt) == str(tmp_path)
verzeichnis.loesche(projekt)
assert verzeichnis.lies(projekt) == ""
def test_verzeichnis_lehnt_wurzel_und_relatives_ab(projekt):
with pytest.raises(ValueError):
verzeichnis.pruefe("/")
with pytest.raises(ValueError):
verzeichnis.pruefe("relativ/pfad")
with pytest.raises(FileNotFoundError):
verzeichnis.pruefe("/gibt/es/nicht")
def test_verzeichnis_liegt_im_stand_nicht_in_der_kopie(projekt, tmp_path):
planung.starte(projekt)
verzeichnis.setze(projekt, str(tmp_path))
planung.brich_ab(projekt)
assert verzeichnis.lies(projekt) == str(tmp_path)
# ------------------------------------------------------------- Durchblättern
def test_ordner_listet_nur_sichtbare_verzeichnisse(tmp_path):
(tmp_path / "b").mkdir()
(tmp_path / "A").mkdir()
(tmp_path / ".versteckt").mkdir()
(tmp_path / "datei.txt").write_text("x")
antwort = verzeichnis.ordner(str(tmp_path))
assert antwort["ordner"] == ["A", "b"]
assert antwort["pfad"] == str(tmp_path)
assert antwort["eltern"] == str(tmp_path.parent)
def test_ordner_ohne_pfad_beginnt_daheim():
from pathlib import Path
assert verzeichnis.ordner("")["pfad"] == str(Path.home())
def test_ordner_lehnt_datei_und_relatives_ab(tmp_path):
(tmp_path / "d.txt").write_text("x")
with pytest.raises(FileNotFoundError):
verzeichnis.ordner(str(tmp_path / "d.txt"))
with pytest.raises(ValueError):
verzeichnis.ordner("relativ")
def test_vision_gehoert_nie_zum_auftrag(projekt):
features.anlegen(projekt, "<!-- titel -->\nVision\n<!-- art -->\nvision\n<!-- ziel -->\nAlt\n")
planung.starte(projekt)
v = next(f for f in features.liste(projekt) if f["sektionen"]["art"] == "vision")
features.speichern(projekt, v["id"], v["roh"].replace("Alt", "Neu"))
assert auftrag.bau_auftrag(projekt) == ("", [])
features.anlegen(projekt, f"<!-- titel -->\nMerkliste\n{T}\n<!-- funktion -->\nProdukt merken\n")
text, schritte = auftrag.bau_auftrag(projekt)
assert [s["text"] for s in schritte] == ["Merkliste / Produkt merken: PRÜFEN"]
assert "Vision" not in text
# ------------------------------------------------- Stand: bestellt wird gebaut
def _stand(projekt, karte, funktion):
f = next(x for x in features.liste(projekt) if x["sektionen"]["titel"] == karte)
return next(x["stand"] for x in f["sektionen"]["funktionen"] if x["text"] == funktion)
def test_abgehakte_funktion_gilt_als_gebaut(projekt, tmp_path, monkeypatch):
"""Der Agent hat es gebaut — dann darf die Karte nicht „bestellt“ behaupten."""
verzeichnis.setze(projekt, str(tmp_path))
planung.starte(projekt)
features.anlegen(projekt, f"<!-- titel -->\nMerkliste\n{T}\n<!-- funktion -->\n"
f"Produkt merken\n<!-- stand -->\noffen\n{T}\n"
f"<!-- funktion -->\nProdukt entfernen\n<!-- stand -->\noffen\n")
anweisung, schritte = auftrag.bau_auftrag(projekt)
merken = next(s for s in schritte if s["funktion"] == "Produkt merken")
async def unecht(*a, **k):
return Strom([text_ereignis(f'ERLEDIGT: {merken["text"]}'),
{"type": "result", "result": "eins gebaut"}])
monkeypatch.setattr(asyncio, "create_subprocess_exec", unecht)
async def lauf():
auftrag.starte(projekt, anweisung, schritte)
await asyncio.gather(*auftrag._aufgaben)
asyncio.run(lauf())
# Nur der abgehakte Schritt wandert um; der andere bleibt bestellt
assert _stand(projekt, "Merkliste", "Produkt merken") == "gebaut"
assert _stand(projekt, "Merkliste", "Produkt entfernen") == "offen"
def test_gestrichene_funktion_wird_nicht_umgestellt(projekt, tmp_path):
"""Was weg ist, ist nicht gebaut — der Schritt trägt darum keinen Namen."""
planung.starte(projekt)
f = next(x for x in features.liste(projekt) if x["sektionen"]["titel"] == "Galerie")
features.speichern(projekt, f["id"], "<!-- titel -->\nGalerie\n")
_, schritte = auftrag.bau_auftrag(projekt)
weg = next(s for s in schritte if "Bild wählen" in s["text"])
assert weg["funktion"] == ""
def test_offen_vermerk_bleibt_bei_anderer_funktion(projekt):
roh = ("<!-- titel -->\nGalerie\n"
"<!-- funktion -->\nBild wählen\n<!-- stand -->\noffen\n"
"<!-- funktion -->\nBild schließen\n<!-- stand -->\noffen\n")
neu = auftrag._als_gebaut(roh, "Bild schließen")
assert neu.count("offen") == 1
assert "Bild wählen\n<!-- stand -->\noffen" in neu
def test_unbekannte_bloecke_bleiben_erhalten(projekt):
"""Textoperation statt Neuschreiben: der Rest der Datei bleibt Zeichen für Zeichen."""
roh = ("<!-- titel -->\nGalerie\n<!-- eigenes -->\nnicht anfassen\n"
"<!-- funktion -->\nBild wählen\n<!-- stand -->\noffen\n"
"<!-- regeln -->\n- Eine Regel\n")
neu = auftrag._als_gebaut(roh, "Bild wählen")
assert "<!-- eigenes -->\nnicht anfassen" in neu
assert "<!-- regeln -->\n- Eine Regel" in neu
assert "offen" not in neu
# ---------------------------------------- offen heißt: noch nicht bewiesen
def _auf_offen(projekt, karte, funktion):
"""Wie ein Klick auf den Stand: gebaut zurück auf offen."""
f = next(x for x in features.liste(projekt) if x["sektionen"]["titel"] == karte)
roh = f["roh"].replace(f"<!-- funktion -->\n{funktion}\n",
f"<!-- funktion -->\n{funktion}\n<!-- stand -->\noffen\n")
features.speichern(projekt, f["id"], roh)
def test_zurueckgesetzte_funktion_kommt_in_die_arbeitsliste(projekt):
"""Eine kaputte Umsetzung meldet man, indem man die Funktion auf offen setzt.
Die Karte ändert sich dadurch, aber sonst nichts — vorher blieb der Klick folgenlos.
"""
planung.starte(projekt)
_auf_offen(projekt, "Galerie", "Bild wählen")
text, schritte = auftrag.bau_auftrag(projekt)
assert [s["text"] for s in schritte] == ["Galerie / Bild wählen: PRÜFEN"]
# Der Sollzustand der Karte gehört dazu, sonst weiß der Agent nicht, wogegen er prüft
assert "<!-- funktion -->\nBild wählen" in text
assert auftrag.PRUEFEN in text
def test_offene_funktion_ohne_kartenaenderung(projekt, tmp_path):
"""Offen in der Kopie und im Stand: kein Diff, trotzdem Arbeit."""
f = next(x for x in features.liste(projekt) if x["sektionen"]["titel"] == "Shop")
features.speichern(projekt, f["id"], f["roh"].replace(
"<!-- funktion -->\nKarte kaufen\n",
"<!-- funktion -->\nKarte kaufen\n<!-- stand -->\noffen\n"))
planung.starte(projekt)
assert planung.aenderungen(projekt) == {}
_, schritte = auftrag.bau_auftrag(projekt)
assert [s["text"] for s in schritte] == ["Shop / Karte kaufen: PRÜFEN"]
def test_offen_schlaegt_geaendert(projekt):
"""Eine offene Funktion wird geprüft, nicht bloß angepasst."""
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(
"<!-- funktion -->\nBild wählen\n",
"<!-- funktion -->\nBild wählen\n<!-- stand -->\noffen\n"
"<!-- regeln -->\n- Eine neue Regel\n"))
_, schritte = auftrag.bau_auftrag(projekt)
assert [s["text"] for s in schritte] == ["Galerie / Bild wählen: PRÜFEN"]
def test_gebaut_und_geaendert_bleibt_geaendert(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(
"<!-- funktion -->\nBild wählen\n",
"<!-- funktion -->\nBild wählen\n<!-- regeln -->\n- Eine neue Regel\n"))
_, schritte = auftrag.bau_auftrag(projekt)
assert [s["text"] for s in schritte] == ["Galerie / Bild wählen: GEÄNDERT"]
def test_gestrichene_karte_zieht_ihre_offenen_nicht_nach(projekt):
"""Eine gestrichene Karte darf nicht zusätzlich als „prüfen“ auftauchen."""
f = next(x for x in features.liste(projekt) if x["sektionen"]["titel"] == "Shop")
features.speichern(projekt, f["id"], f["roh"].replace(
"<!-- funktion -->\nKarte kaufen\n",
"<!-- funktion -->\nKarte kaufen\n<!-- stand -->\noffen\n"))
planung.starte(projekt)
g = next(x for x in features.liste(projekt) if x["sektionen"]["titel"] == "Shop")
features.loeschen(projekt, g["id"])
_, schritte = auftrag.bau_auftrag(projekt)
assert [s["text"] for s in schritte] == ["Karte „Shop“ streichen"]
def test_ohne_planung_kein_lauf(projekt):
"""Der Agent arbeitet auf der Planung; ohne sie gibt es keinen Auftrag."""
_auf_offen(projekt, "Galerie", "Bild wählen")
assert auftrag.bau_auftrag(projekt) == ("", [])
def test_auftrag_verlangt_offenlegung_des_ungeprueften(projekt):
planung.starte(projekt)
_auf_offen(projekt, "Galerie", "Bild wählen")
text, _ = auftrag.bau_auftrag(projekt)
assert "nicht prüfen konntest" in text
assert "Hake nur ab, was du geprüft hast" in text

111
backend/tests/test_chat.py Normal file
View File

@@ -0,0 +1,111 @@
"""Was das Planungsgespräch darf — und was es nicht sieht.
Geprüft wird nicht die Antwort des Modells, sondern womit es losgeschickt wird:
Werkzeugsatz, Arbeitsverzeichnis und der Absatz im Systemprompt gehören zusammen.
Fällt eines auseinander, behauptet das Modell nachgesehen zu haben, ohne zu können.
"""
import json
import pytest
import agents
import chat
import projekte
import verzeichnis
KARTE = ("<!-- titel -->\nUpload\n<!-- art -->\nkern\n"
"<!-- ziel -->\nDateien landen im Speicher.\n"
"<!-- funktion -->\nDatei wählen\n<!-- testen -->\n- Dialog öffnen\n")
@pytest.fixture
def projekt(datenordner):
projekte.anlegen("P")
(datenordner / "P" / "upload.md").write_text(KARTE)
return "P"
@pytest.fixture
def mitschrift(monkeypatch):
"""Fängt den Aufruf ab und gibt eine gültige Antwort zurück."""
gesehen = {}
async def antworte(system, inhalt, kommando=None, ordner=None):
gesehen.update(system=system, inhalt=inhalt, kommando=kommando, ordner=ordner)
return json.dumps({"antwort": "Verstanden.", "karten": []})
monkeypatch.setattr(agents, "frage_lokal", antworte)
return gesehen
async def test_ohne_verzeichnis_kein_lesen(projekt, mitschrift):
await chat.frage(projekt, "Was kann das Projekt?")
assert mitschrift["kommando"] == agents.PLANUNG_OHNE_CODE
assert mitschrift["ordner"] is None
assert chat.CODE_OHNE in mitschrift["system"]
async def test_mit_verzeichnis_darf_lesen(projekt, mitschrift, tmp_path):
code = tmp_path / "quelltext"
code.mkdir()
verzeichnis.setze(projekt, str(code))
await chat.frage(projekt, "Gibt es den Upload schon?")
assert mitschrift["kommando"] == agents.PLANUNG_KOMMANDO
assert mitschrift["ordner"] == str(code.resolve())
assert chat.CODE_MIT in mitschrift["system"]
async def test_toter_pfad_zaehlt_wie_keiner(projekt, mitschrift, tmp_path):
"""Sonst startet der Prozess nicht, und die Meldung zeigt auf die CLI."""
code = tmp_path / "weg"
code.mkdir()
verzeichnis.setze(projekt, str(code))
code.rmdir()
await chat.frage(projekt, "Und jetzt?")
assert mitschrift["kommando"] == agents.PLANUNG_OHNE_CODE
assert mitschrift["ordner"] is None
async def test_planung_sucht_nicht_im_web(projekt, mitschrift, tmp_path):
"""Fremdes Wissen kommt aus der Recherche, nicht nebenbei in einen Entwurf."""
code = tmp_path / "quelltext"
code.mkdir()
verzeichnis.setze(projekt, str(code))
await chat.frage(projekt, "Wie macht man das üblicherweise?")
for satz in (agents.PLANUNG_KOMMANDO, agents.PLANUNG_OHNE_CODE):
assert "WebSearch" not in _erlaubt(satz)
assert "WebFetch" not in _erlaubt(satz)
def test_recherche_sieht_keinen_quelltext():
"""Umgekehrt: aus dem Projekt darf nichts in eine Suchanfrage geraten."""
erlaubt = _erlaubt(agents.RECHERCHE_KOMMANDO)
assert "WebSearch" in erlaubt and "WebFetch" in erlaubt
for werkzeug in ("Read", "Glob", "Grep"):
assert werkzeug not in erlaubt
@pytest.mark.parametrize("kommando", [
agents.PLANUNG_KOMMANDO, agents.PLANUNG_OHNE_CODE, agents.RECHERCHE_KOMMANDO])
def test_kein_satz_kann_etwas_anstossen(kommando):
"""Fremder Text im Kontext darf nichts auslösen, egal woher er kommt."""
gesperrt = _gesperrt(kommando)
for werkzeug in ("Write", "Edit", "Bash", "CronCreate", "PushNotification",
"SendMessage", "Workflow", "Task"):
assert werkzeug in gesperrt
assert "--safe-mode" in kommando
def _abschnitt(kommando: tuple, flagge: str) -> list:
rest = list(kommando[kommando.index(flagge) + 1:])
ende = next((i for i, w in enumerate(rest) if w.startswith("--")), len(rest))
return rest[:ende]
def _erlaubt(kommando: tuple) -> list:
return _abschnitt(kommando, "--allowed-tools")
def _gesperrt(kommando: tuple) -> list:
return _abschnitt(kommando, "--disallowed-tools")

View File

@@ -0,0 +1,168 @@
"""Das Gedächtnis der Stilprüfung: was gilt weiter, was wird neu gefragt.
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
import pytest
import agents
import features
import ledger
import projekte
@pytest.fixture
def projekt(datenordner):
projekte.anlegen("Test")
return "Test"
def karte(projekt, titel, *funktionen):
"""funktionen sind (name, regel) oder (name, regel, [testschritte]).
Ohne eigene Schritte bekommt die Funktion einen Platzhalter-Ablauf: Testschritte
sind Pflicht, und ohne sie bricht die Messung vor allem anderen ab.
"""
text = f"<!-- titel -->\n{titel}\n"
for eintrag in funktionen:
name, regel, schritte = (*eintrag, ["Karte öffnen", "Karte offen"])[:3]
text += f"<!-- funktion -->\n{name}\n<!-- regeln -->\n- {regel}\n"
text += "<!-- testen -->\n" + "".join(f"- {s}\n" for s in schritte)
return features.anlegen(projekt, text)
class Zaehler:
"""Ersetzt das Modell und zählt mit, wie oft überhaupt gefragt wurde."""
def __init__(self, antwort):
self.antwort = antwort
self.rufe = 0
async def __call__(self, system, nachricht, temperatur=0.0):
self.rufe += 1
wert = self.antwort
return wert(nachricht, self.rufe) if callable(wert) else wert
# ---------------------------------------------------------------- Datei
def test_leeres_gedaechtnis_hat_alle_faecher(projekt):
stand = ledger.lade(projekt)
assert set(stand) == set(ledger.FAECHER)
assert all(v == {} for v in stand.values())
def test_kaputte_datei_wird_wie_leer_behandelt(projekt):
(projekte.wurzel(projekt) / ledger.DATEI).write_text("{kein json")
assert ledger.lade(projekt)["belege"] == {}
def test_gemerktes_ueberlebt_den_lauf(projekt):
stand = ledger.lade(projekt)
stand["belege"][ledger.schluessel("a", "Tun")] = {"abdruck": "x", "datei": "a.py",
"beleg": "def tun():"}
ledger.sichere(projekt, stand)
assert ledger.lade(projekt)["belege"] == stand["belege"]
def test_schluessel_bleibt_zerlegbar(projekt):
"""Titel dürfen alles enthalten — auch das, was als Trennzeichen taugen würde."""
fies = 'Klick auf "x", dann | und \\ und ["a","b"]'
assert ledger.teile(ledger.schluessel("karte", fies)) == ["karte", fies]
def test_gedaechtnis_ist_kein_feature(projekt):
karte(projekt, "Eins")
ledger.sichere(projekt, ledger.lade(projekt))
assert [f["sektionen"]["titel"] for f in features.liste(projekt)] == ["Eins"]
# ---------------------------------------------------------------- Vision
VISION = "<!-- titel -->\nVision\n<!-- art -->\nvision\n<!-- ziel -->\nSo soll es werden\n"
def test_vision_ist_eine_eigene_kartenart(projekt):
f = features.anlegen(projekt, VISION)
assert f["sektionen"]["art"] == "vision"
assert f["sektionen"]["ziel"] == "So soll es werden"
def test_unbekannte_art_bleibt_kern(projekt):
f = features.anlegen(projekt, "<!-- titel -->\nX\n<!-- art -->\nwichtig\n")
assert f["sektionen"]["art"] == "kern"
def test_keine_pruefung_fasst_die_vision_an(projekt):
"""Die Vision ist die Vorlage, nicht der Prüfling."""
karte(projekt, "Eins", ("Etwas starten", "Knopf"))
features.anlegen(projekt, VISION)
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) == []
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) == []
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)
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)
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)]
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": "", "ziel": "",
"regeln": "- Passwort nie loggen\n- Sperre nach 5 Fehlversuchen", "regeln": "- Passwort nie loggen\n- Sperre nach 5 Fehlversuchen",
"testen": "- Formular öffnen\n- Daten eingeben", "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(): def test_titel_erste_nicht_leere_zeile():
s = zerlege("<!-- titel -->\n\nErste\nZweite\n") s = zerlege("<!-- titel -->\n\nErste\nZweite\n")
assert s["titel"] == "Erste" 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,231 @@
"""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()
def test_arbeitsverzeichnis_ueberlebt_das_umsetzen(projekt, tmp_path):
"""Erst nach dem Planungsstart festgelegt — dann fehlt es in der Kopie.
Vorher räumte das Umsetzen es mit dem alten Stand weg, und der nächste Lauf
fand kein Verzeichnis mehr.
"""
import verzeichnis
planung.starte(projekt)
verzeichnis.setze(projekt, str(tmp_path))
assert not (planung._kopie(projekt) / verzeichnis.DATEI).exists()
planung.setze_um(projekt)
assert verzeichnis.lies(projekt) == str(tmp_path)
def test_geplantes_verzeichnis_gewinnt(projekt, tmp_path):
"""Steht es in beiden, gilt die Kopie — sie ist der neuere Stand."""
import verzeichnis
alt = tmp_path / "alt"
alt.mkdir()
verzeichnis.setze(projekt, str(alt))
planung.starte(projekt)
neu = tmp_path / "neu"
neu.mkdir()
(planung._kopie(projekt) / verzeichnis.DATEI).write_text(str(neu))
planung.setze_um(projekt)
assert verzeichnis.lies(projekt) == str(neu)

View File

@@ -1,4 +1,5 @@
import asyncio import asyncio
import json
import pytest import pytest
@@ -6,10 +7,11 @@ import agents
import features import features
import projekte import projekte
import pruefung import pruefung
import struktur
ANTWORT = """```json ANTWORT = """```json
{"probleme": [{"feld": "regeln", "funktion": "Etwas tun", "original": "Zwei Aussagen, und noch eine", {"felder": {"titel": {"regel": "Sprache: Deutsch, Präsens, keine Anrede",
"problem": "Zwei Aussagen je Regel", "vorschlag": "Zwei Aussagen"}]} "neu": "Anmeldung", "grund": "Titel nennt Tätigkeit"}}}
```""" ```"""
@@ -38,7 +40,7 @@ async def warte_bis_fertig(projekt):
async def test_lauf_sammelt_probleme(projekt, monkeypatch): async def test_lauf_sammelt_probleme(projekt, monkeypatch):
monkeypatch.setattr(agents, "pruefe_konfiguration", lambda: None) monkeypatch.setattr(agents, "pruefe_konfiguration", lambda: None)
monkeypatch.setattr(agents, "frage", lambda system, inhalt: _antwort()) monkeypatch.setattr(agents, "frage", lambda system, inhalt, temperatur=0.0: _antwort())
await pruefung.starte(projekt) await pruefung.starte(projekt)
await warte_bis_fertig(projekt) await warte_bis_fertig(projekt)
@@ -48,7 +50,8 @@ async def test_lauf_sammelt_probleme(projekt, monkeypatch):
assert s["meldung"] == "" assert s["meldung"] == ""
assert len(s["probleme"]) == 2 assert len(s["probleme"]) == 2
assert s["probleme"][0]["feature"] in ("eins", "zwei") assert s["probleme"][0]["feature"] in ("eins", "zwei")
assert s["probleme"][0]["vorschlag"] == "Zwei Aussagen" assert s["probleme"][0]["vorschlag"] == "Anmeldung"
assert s["probleme"][0]["problem"] == "Titel nennt Tätigkeit"
async def _antwort(): async def _antwort():
@@ -65,13 +68,17 @@ async def test_ohne_konfiguration_kein_lauf(projekt, monkeypatch):
async def test_providerfehler_landet_in_meldung(projekt, monkeypatch): async def test_providerfehler_landet_in_meldung(projekt, monkeypatch):
monkeypatch.setattr(agents, "pruefe_konfiguration", lambda: None) monkeypatch.setattr(agents, "pruefe_konfiguration", lambda: None)
async def kaputt(system, inhalt): async def kaputt(system, inhalt, temperatur=0.0):
raise RuntimeError("Provider antwortet mit HTTP 500") raise RuntimeError("Provider antwortet mit HTTP 500")
monkeypatch.setattr(agents, "frage", kaputt) monkeypatch.setattr(agents, "frage", kaputt)
await pruefung.starte(projekt) await pruefung.starte(projekt)
await warte_bis_fertig(projekt) await warte_bis_fertig(projekt)
assert pruefung.status(projekt)["meldung"] == "Provider antwortet mit HTTP 500" s = pruefung.status(projekt)
assert s["fehler"] == 2
assert s["meldung"] == (
"2 von 2 Prüfungen fehlgeschlagen: Provider antwortet mit HTTP 500"
)
async def test_verworfener_lauf_stoppt(projekt, monkeypatch): async def test_verworfener_lauf_stoppt(projekt, monkeypatch):
@@ -79,16 +86,17 @@ async def test_verworfener_lauf_stoppt(projekt, monkeypatch):
monkeypatch.setattr(agents.config, "PRUEFUNG_PARALLEL", 1) monkeypatch.setattr(agents.config, "PRUEFUNG_PARALLEL", 1)
aufrufe = [] aufrufe = []
async def zaehle(system, inhalt): async def zaehle(system, inhalt, temperatur=0.0):
aufrufe.append(inhalt) aufrufe.append(inhalt)
pruefung.beende("Test") # Panel wird nach der ersten Karte geschlossen pruefung.beende("Test") # Panel wird nach der ersten Karte geschlossen
return '{"probleme": []}' return '{"felder": {}}'
monkeypatch.setattr(agents, "frage", zaehle) monkeypatch.setattr(agents, "frage", zaehle)
await pruefung.starte(projekt) await pruefung.starte(projekt)
for _ in range(50): for _ in range(50):
await asyncio.sleep(0.01) await asyncio.sleep(0.01)
assert len(aufrufe) == 1 # zweite Karte wird nicht mehr angefragt # Nur die erste Karte wird angefragt — mit ihren drei Stimmen
assert len(aufrufe) == len(pruefung.TEMPERATUREN)
async def test_parallel_begrenzt(projekt, monkeypatch): async def test_parallel_begrenzt(projekt, monkeypatch):
@@ -97,40 +105,338 @@ async def test_parallel_begrenzt(projekt, monkeypatch):
gleichzeitig = 0 gleichzeitig = 0
hoechststand = 0 hoechststand = 0
async def langsam(system, inhalt): async def langsam(system, inhalt, temperatur=0.0):
nonlocal gleichzeitig, hoechststand nonlocal gleichzeitig, hoechststand
gleichzeitig += 1 gleichzeitig += 1
hoechststand = max(hoechststand, gleichzeitig) hoechststand = max(hoechststand, gleichzeitig)
await asyncio.sleep(0.02) await asyncio.sleep(0.02)
gleichzeitig -= 1 gleichzeitig -= 1
return '{"probleme": []}' return '{"felder": {}}'
monkeypatch.setattr(agents, "frage", langsam) monkeypatch.setattr(agents, "frage", langsam)
await pruefung.starte(projekt) await pruefung.starte(projekt)
await warte_bis_fertig(projekt) await warte_bis_fertig(projekt)
assert hoechststand == 1 # Die Schranke gilt je Prüfstelle; deren Stimmen laufen gemeinsam
assert hoechststand == len(pruefung.TEMPERATUREN)
assert pruefung.status(projekt)["fertig"] == 2 assert pruefung.status(projekt)["fertig"] == 2
async def test_status_ohne_lauf(): async def test_status_ohne_lauf():
assert pruefung.status("Fremd") == { assert pruefung.status("Fremd") == {
"laeuft": False, "fertig": 0, "gesamt": 0, "probleme": [], "meldung": "" "laeuft": False, "art": "", "fertig": 0, "gesamt": 0, "fehler": 0,
"probleme": [], "meldung": "",
} }
def test_antwort_lesen_ohne_zaun(): KARTE = (
probleme = pruefung._antwort_lesen('{"probleme": [{"original": "x", "vorschlag": "y"}]}') "<!-- titel -->\nLogin\n<!-- art -->\noptional\n<!-- ziel -->\nAnmelden\n"
assert probleme == [{"original": "x", "vorschlag": "y", "funktion": ""}] "<!-- regeln -->\n- Kartenregel\n"
"<!-- funktion -->\nAnmelden\n<!-- testen -->\n- Formular öffnen\n"
"<!-- funktion -->\nAbmelden\n"
)
def test_listenstrich_wird_entfernt(): def test_aufgaben_eine_je_ebene(datenordner):
probleme = pruefung._antwort_lesen( projekte.anlegen("Test")
'{"probleme": [{"original": "- alt", "vorschlag": "- neu", "funktion": " F "}]}') f = features.anlegen("Test", KARTE)
assert probleme[0]["original"] == "alt" liste = pruefung.aufgaben(f)
assert probleme[0]["vorschlag"] == "neu"
assert probleme[0]["funktion"] == "F" # Dateireihenfolge, nicht alphabetisch — sortiert wird erst in der Anzeige
assert [a["funktion"] for a in liste] == ["", "Anmelden", "Abmelden"]
assert [a["bereich"] for a in liste] == ["feature", "funktion", "funktion"]
assert all(a["feature"] == "login" for a in liste)
def test_antwort_ohne_json(): def test_aufgabe_bekommt_nur_ihren_ausschnitt(datenordner):
projekte.anlegen("Test")
f = features.anlegen("Test", KARTE)
karte, anmelden, abmelden = pruefung.aufgaben(f)
inhalt = {a["funktion"]: pruefung.inhalt(a) for a in (karte, anmelden, abmelden)}
# Kartenebene: badge statt art, Funktionstitel nur als Kontext für den Badge
assert "<!-- badge -->\noptional" in inhalt[""]
assert "<!-- regeln -->\n- Kartenregel" in inhalt[""]
assert "<!-- funktionen -->\n- Anmelden\n- Abmelden" in inhalt[""]
assert "Formular öffnen" not in inhalt[""] # keine Details der Funktionen
# Funktion: eigener Titel, eigenes testen, nichts von der Karte
assert "<!-- titel -->\nAnmelden" in inhalt["Anmelden"]
assert "<!-- testen -->\n- Formular öffnen" in inhalt["Anmelden"]
assert "Kartenregel" not in inhalt["Anmelden"]
# Leere Felder werden gar nicht erst mitgeschickt
assert inhalt["Abmelden"] == "<!-- titel -->\nAbmelden\n"
async def test_lauf_zaehlt_alle_stellen(datenordner, monkeypatch):
projekte.anlegen("Test")
features.anlegen("Test", KARTE)
monkeypatch.setattr(agents, "pruefe_konfiguration", lambda: None)
gesehen = []
async def merke(system, inhalt, temperatur=0.0):
gesehen.append((system, inhalt))
return '{"felder": {}}'
monkeypatch.setattr(agents, "frage", merke)
await pruefung.starte("Test")
await warte_bis_fertig("Test")
assert pruefung.status("Test")["gesamt"] == 3 # 1 Karte + 2 Funktionen
assert len(gesehen) == 3 * len(pruefung.TEMPERATUREN)
# Der Kartenprüfer kennt die Badge-Regeln, der Funktionsprüfer nicht
karten_system = next(s for s, i in gesehen if "badge" in i)
assert "## Badge der Karte" in karten_system
assert "## Testen einer Funktion" not in karten_system
async def test_herkunft_kommt_vom_code_nicht_vom_modell(datenordner, monkeypatch):
projekte.anlegen("Test")
features.anlegen("Test", KARTE)
monkeypatch.setattr(agents, "pruefe_konfiguration", lambda: None)
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": "Einloggen", "grund": "x"},'
' "funktion": {"neu": "Erfunden", "grund": "y"}}}')
monkeypatch.setattr(agents, "frage", luegt)
await pruefung.starte("Test")
await warte_bis_fertig("Test")
zuordnung = sorted(p["funktion"] for p in pruefung.status("Test")["probleme"])
assert zuordnung == ["", "Abmelden", "Anmelden"]
def test_stil_katalog_je_bereich():
feature = pruefung._stil_lesen("feature")
funktion = pruefung._stil_lesen("funktion")
# allgemein steckt in beiden und steht vorn
for stil in (feature, funktion):
assert stil.startswith("# Stil-Katalog")
assert "## Allgemein" in stil
for stil in (feature, funktion):
assert pruefung.GEMESSEN in stil # messbare Vorgaben stehen beiden zur Verfügung
for ueberschrift in (
"## Titel der Karte", "## Badge der Karte",
"## Ziel der Karte", "## Regeln der Karte",
):
assert ueberschrift in feature
assert ueberschrift not in funktion # der Funktionsprüfer sieht das nicht
for ueberschrift in (
"## Titel einer Funktion", "## Ziel einer Funktion",
"## Regeln einer Funktion", "## Testen einer Funktion",
):
assert ueberschrift in funktion
assert ueberschrift not in feature
def test_neue_stil_datei_kommt_ohne_code_dazu(tmp_path, monkeypatch):
(tmp_path / "allgemein.md").write_text("## Allgemein\n")
(tmp_path / "feature-neu.md").write_text("## Neu\n")
monkeypatch.setattr(pruefung.config, "STIL_DIR", tmp_path)
stil = pruefung._stil_lesen("feature")
assert "## Neu" in stil
assert stil.index("## Allgemein") < stil.index("## Neu") # alphabetisch
assert "## Neu" not in pruefung._stil_lesen("funktion")
def test_stil_ordner_leer_bricht_ab(tmp_path, monkeypatch):
monkeypatch.setattr(pruefung.config, "STIL_DIR", tmp_path)
with pytest.raises(ValueError): with pytest.raises(ValueError):
pruefung._antwort_lesen("keine Ahnung") pruefung._stil_lesen("feature")
def _stellen(feld, alt, neu):
return [(s["original"], s["vorschlag"])
for s in pruefung._stellen(feld, alt, neu, "grund")]
TESTEN = "- RMB auf Hauptbereich\n- Klick auf \"Feature erstellen\"\n- Bestätigen mit LMB"
def test_ergaenzte_zeile_haengt_an_der_davor():
# Der Endzustand fehlt: die letzte Zeile bleibt, der neue Schritt kommt dahinter
assert _stellen("testen", TESTEN, TESTEN + "\n- Feature erstellt") == [
("Bestätigen mit LMB", "Bestätigen mit LMB\nFeature erstellt")]
def test_ergaenzung_am_anfang_haengt_an_der_dahinter():
assert _stellen("testen", TESTEN, "- Vollmodus öffnen\n" + TESTEN) == [
("RMB auf Hauptbereich", "Vollmodus öffnen\nRMB auf Hauptbereich")]
def test_ersetzte_zeile():
neu = TESTEN.replace("- Bestätigen mit LMB", "- Bestätigen")
assert _stellen("testen", TESTEN, neu) == [("Bestätigen mit LMB", "Bestätigen")]
def test_gestrichene_zeile():
neu = "- RMB auf Hauptbereich\n- Bestätigen mit LMB"
assert _stellen("testen", TESTEN, neu) == [('Klick auf "Feature erstellen"', "")]
def test_mehrere_aenderungen_werden_ein_umbau():
# Einzeln angenommen ergäben sie Dubletten oder Lücken — also nur zusammen
neu = "- RMB auf Hauptbereich\n- Feature anlegen"
stellen = pruefung._stellen("testen", TESTEN, neu, "grund")
assert len(stellen) == 1
assert stellen[0]["ganzes_feld"] is True
assert stellen[0]["original"] == "\n".join(struktur._zeilen("testen", TESTEN))
assert stellen[0]["vorschlag"] == "RMB auf Hauptbereich\nFeature anlegen"
def test_unveraendert_ergibt_keine_stelle():
assert _stellen("testen", TESTEN, TESTEN) == []
assert _stellen("titel", "Login", "Login") == []
assert _stellen("titel", "Login", " Login ") == []
def test_leeres_listenfeld_wird_ganz_gesetzt():
assert _stellen("testen", "", "- Eins\n- Zwei") == [("", "Eins\nZwei")]
def test_einzelfeld_wird_ganz_ersetzt():
assert _stellen("titel", "Login", "Anmeldung") == [("Login", "Anmeldung")]
assert _stellen("ziel", "Alt", "") == [("Alt", "")]
def test_einzelne_aenderung_zeigt_auf_eine_echte_zeile():
stellen = pruefung._stellen("testen", TESTEN, TESTEN + "\n- Fertig", "grund")
assert len(stellen) == 1
assert stellen[0]["ganzes_feld"] is False
assert stellen[0]["original"] in struktur._zeilen("testen", TESTEN)
def test_erfundenes_feld_wird_verworfen():
erlaubt = pruefung.BEREICHE["funktion"]["felder"]
antwort = ('{"felder": {"struktur": {"neu": "x", "grund": "y"},'
' "testen": {"neu": "- a", "grund": "z"}}}')
assert list(pruefung._felder_lesen(antwort, erlaubt)) == ["testen"]
def test_feld_darf_nicht_leer_werden_wenn_es_pflicht_ist():
erlaubt = pruefung.BEREICHE["feature"]["felder"]
leer = '{"felder": {"titel": {"neu": "", "grund": "x"}, "ziel": {"neu": "", "grund": "y"}}}'
assert list(pruefung._felder_lesen(leer, erlaubt)) == ["ziel"]
def test_mehrzeilig_nur_bei_listenfeldern():
erlaubt = pruefung.BEREICHE["funktion"]["felder"]
antwort = ('{"felder": {"titel": {"neu": "a\\nb", "grund": "x"},'
' "regeln": {"neu": "- a\\n- b", "grund": "y"}}}')
assert list(pruefung._felder_lesen(antwort, erlaubt)) == ["regeln"]
def test_neu_darf_liste_oder_string_sein():
erlaubt = pruefung.BEREICHE["funktion"]["felder"]
als_liste = '{"felder": {"testen": {"neu": ["- a", "- b"], "grund": "x"}}}'
als_string = '{"felder": {"testen": {"neu": "- a\\n- b", "grund": "x"}}}'
assert pruefung._felder_lesen(als_liste, erlaubt) == \
pruefung._felder_lesen(als_string, erlaubt)
def test_feld_ohne_huelle_wird_angenommen():
# Manche Antworten kürzen {"neu": ...} auf den reinen Wert ab
erlaubt = pruefung.BEREICHE["funktion"]["felder"]
gelesen = pruefung._felder_lesen('{"felder": {"titel": "Anmelden"}}', erlaubt)
assert gelesen == {"titel": {"neu": "Anmelden", "grund": "", "regel": ""}}
KATALOG = pruefung.katalogzeilen(pruefung._stil_lesen("funktion"))
def _mit_regel(regel):
antwort = ('{"felder": {"titel": {"regel": "%s", "neu": "Anmelden", "grund": "x"}}}'
% regel)
return pruefung._felder_lesen(
antwort, pruefung.BEREICHE["funktion"]["felder"], KATALOG)
def test_aenderung_braucht_eine_echte_katalogzeile():
assert _mit_regel("Genau eine Tätigkeit") != {} # steht so im Katalog
assert _mit_regel("genau eine tätigkeit") != {} # Groß-/Kleinschreibung egal
assert _mit_regel("- Genau eine Tätigkeit") != {} # Listenstrich egal
def test_gemessene_regeln_sind_nicht_zitierbar():
# Sonst beruft sich das Modell auf die Füllwortliste und wendet sie frei an
assert _mit_regel("Titel einer Funktion: höchstens 5 Wörter") == {}
assert _mit_regel("Keine Füllwörter. Die Liste ist abschließend:"
" auch, noch, einfach, jeweils, ggf.") == {}
assert "## Vom System gemessen" in pruefung._stil_lesen("funktion")
def test_erfundene_regel_wird_verworfen():
# Genau daran ist der Lauf vorher nicht konvergiert
assert _mit_regel("Platzhaltertext ist nicht erlaubt") == {}
assert _mit_regel("Ziel muss konkret sein") == {}
assert _mit_regel("") == {}
assert _mit_regel("Kein Geschmack") == {}
def test_gemessener_verstoss_braucht_keine_regel():
antwort = '{"felder": {"titel": {"neu": "Anmelden", "grund": "zu lang"}}}'
erlaubt = pruefung.BEREICHE["funktion"]["felder"]
assert pruefung._felder_lesen(antwort, erlaubt, KATALOG) == {}
assert pruefung._felder_lesen(antwort, erlaubt, KATALOG, ("titel",)) != {}
def test_katalogzeilen_sind_nur_die_aufzaehlung():
zeilen = pruefung.katalogzeilen("## Titel\n\nEinleitung\n\n- Erste Regel\n- Zweite\n")
assert zeilen == {"erste regel", "zweite"}
def test_json_mit_nachsatz_und_zaun():
for text in (
'{"felder": {}}\nDas war alles.',
'```json\n{"felder": {}}\n```',
'Hier: {"felder": {}} {"noch": "was"}',
):
assert pruefung._json_lesen(text) == {"felder": {}}
def test_json_ohne_objekt():
with pytest.raises(ValueError):
pruefung._json_lesen("keine Ahnung")
def _proben(*fassungen):
"""Eine Stimme je Eintrag; None heißt „diese Stimme sieht nichts"."""
return [f or {} for f in fassungen]
def test_uneinigkeit_faellt_weg():
# Grenzfälle beurteilt ein Modell mal so, mal so — die fliegen raus
eins = {"titel": {"neu": "A", "grund": "x"}}
assert pruefung._mehrheit(_proben(eins, eins, eins, None)) == {}
assert pruefung._mehrheit(_proben(eins, None, None, None)) == {}
def test_einstimmiges_bleibt():
eins = {"titel": {"neu": "A", "grund": "x"}}
assert pruefung._mehrheit(_proben(eins, eins, eins, eins)) == eins
def test_konsens_nimmt_die_mittlere_fassung():
# Der Ausreißer zieht das Ergebnis nicht mit
def f(text):
return {"titel": {"neu": text, "grund": "x"}}
proben = _proben(f("Anmeldung starten"), f("Anmeldung startet"),
f("Anmeldung beginnen"), f("Völlig anderer Text"))
assert pruefung._mehrheit(proben)["titel"]["neu"].startswith("Anmeldung")
def test_gemessenes_feld_braucht_keine_einstimmigkeit():
"""Ein gezählter Verstoß steht fest, bevor gefragt wird. Vorher verschwand er,
sobald eine einzige Stimme das Feld nicht anfasste."""
eins = {"titel": {"neu": "Kurz", "grund": "x"}}
proben = [eins, {}, {}, {}]
assert pruefung._mehrheit(proben) == {}
assert pruefung._mehrheit(proben, ("titel",)) == eins

View File

@@ -0,0 +1,193 @@
import pytest
import agents
import projekte
import recherche
@pytest.fixture
def projekt(datenordner):
projekte.anlegen("P")
return "P"
def test_leeres_projekt_hat_keine_faeden(projekt):
assert recherche.liste(projekt) == []
def test_anlegen_gibt_faden_mit_id(projekt):
faden = recherche.anlegen(projekt)
assert faden["id"] and faden["verlauf"] == []
assert [f["id"] for f in recherche.liste(projekt)] == [faden["id"]]
def test_faeden_haben_eigene_ids(projekt):
a, b = recherche.anlegen(projekt), recherche.anlegen(projekt)
assert a["id"] != b["id"]
assert len(recherche.liste(projekt)) == 2
def test_loeschen_trifft_nur_den_einen(projekt):
a, b = recherche.anlegen(projekt), recherche.anlegen(projekt)
recherche.loeschen(projekt, a["id"])
assert [f["id"] for f in recherche.liste(projekt)] == [b["id"]]
def test_loeschen_ohne_faden_ist_404(projekt):
with pytest.raises(FileNotFoundError):
recherche.loeschen(projekt, "gibtsnicht")
def test_faeden_liegen_im_projektstand_nicht_in_der_planung(projekt):
"""Nachgeschlagenes überlebt einen Planungsabbruch — es gehört zum Projekt."""
recherche.anlegen(projekt)
(projekte.stand(projekt) / projekte.PLANUNG).mkdir()
assert len(recherche.liste(projekt)) == 1
assert (projekte.stand(projekt) / recherche.DATEI).is_file()
def _antwort(satz="Laut Quelle X: ja."):
"""Eine Modellantwort — Markdown, nicht JSON."""
async def antworte(system, inhalt, kommando=None, ordner=None):
return satz
return antworte
async def test_frage_haengt_beide_zeilen_an(projekt, monkeypatch):
faden = recherche.anlegen(projekt)
monkeypatch.setattr(agents, "frage_lokal", _antwort(" Laut Quelle X: ja. "))
erg = await recherche.frage(projekt, faden["id"], "Wie geht das?")
assert [(e["rolle"], e["text"]) for e in erg["verlauf"]] == [
("person", "Wie geht das?"), ("ki", "Laut Quelle X: ja.")]
async def test_erste_frage_benennt_den_faden(projekt, monkeypatch):
faden = recherche.anlegen(projekt)
monkeypatch.setattr(agents, "frage_lokal", _antwort("ok"))
erg = await recherche.frage(projekt, faden["id"], "Wie lädt man\nzu S3 hoch?")
assert erg["titel"] == "Wie lädt man zu S3 hoch?"
# Die zweite Frage lässt den Namen stehen: der Reiter soll nicht wandern
erg = await recherche.frage(projekt, faden["id"], "Und die Grenzen?")
assert erg["titel"] == "Wie lädt man zu S3 hoch?"
async def test_langer_titel_wird_gekuerzt(projekt, monkeypatch):
faden = recherche.anlegen(projekt)
monkeypatch.setattr(agents, "frage_lokal", _antwort("ok"))
erg = await recherche.frage(projekt, faden["id"], "w" * 80)
assert len(erg["titel"]) == recherche.TITELLAENGE + 1
assert erg["titel"].endswith("…")
async def test_recherche_bekommt_die_websuche(projekt, monkeypatch):
"""Der Werkzeugsatz ist der ganze Unterschied zum Planungsgespräch."""
faden = recherche.anlegen(projekt)
gesehen = {}
async def antworte(system, inhalt, kommando=None, ordner=None):
gesehen.update(kommando=kommando, inhalt=inhalt)
return "ok"
monkeypatch.setattr(agents, "frage_lokal", antworte)
await recherche.frage(projekt, faden["id"], "Was gilt draußen?")
assert gesehen["kommando"] == agents.RECHERCHE_KOMMANDO
async def test_die_karten_liegen_im_prompt(projekt, monkeypatch):
"""Ohne Bestand ist „was fehlt noch" nicht zu beantworten — dann rät das Modell."""
(projekte.stand(projekt) / "gold.md").write_text(
"<!-- titel -->\nGold\n<!-- art -->\nkern\n"
"<!-- ziel -->\nSpieler verdient Gold pro Runde.\n"
"<!-- funktion -->\nZinsen erhalten\n<!-- stand -->\noffen\n"
"<!-- testen -->\n- Runde spielen\n")
faden = recherche.anlegen(projekt)
gesehen = {}
async def antworte(system, inhalt, kommando=None, ordner=None):
gesehen["system"] = system
return "ok"
monkeypatch.setattr(agents, "frage_lokal", antworte)
await recherche.frage(projekt, faden["id"], "Was fehlt noch?")
assert "Zinsen erhalten" in gesehen["system"]
# Der Stand ist der Punkt: er unterscheidet Gebautes von Bestelltem
assert '"stand": "offen"' in gesehen["system"]
# Testabläufe blähen den Prompt und tragen keine Recherche
assert "Runde spielen" not in gesehen["system"]
async def test_vision_kommt_mit(projekt, monkeypatch):
(projekte.stand(projekt) / "vision.md").write_text(
"<!-- titel -->\nVision\n<!-- art -->\nvision\n"
"<!-- ziel -->\nTFT im Browser nachbauen.\n")
faden = recherche.anlegen(projekt)
gesehen = {}
async def antworte(system, inhalt, kommando=None, ordner=None):
gesehen["system"] = system
return "ok"
monkeypatch.setattr(agents, "frage_lokal", antworte)
await recherche.frage(projekt, faden["id"], "Und nun?")
assert "TFT im Browser nachbauen." in gesehen["system"]
async def test_markdown_bleibt_wie_es_kam(projekt, monkeypatch):
"""Die Antwort wird als Markdown angezeigt — der Server fasst sie nicht an."""
roh = "Kurz gesagt.\n\n## Festgelegt\n- **5 Gold** pro Runde\n\n## Quellen\n- [Wiki](http://x)"
faden = recherche.anlegen(projekt)
monkeypatch.setattr(agents, "frage_lokal", _antwort(f"\n{roh}\n"))
erg = await recherche.frage(projekt, faden["id"], "Wie geht Gold?")
assert erg["verlauf"][-1]["text"] == roh
async def test_leere_frage_wird_abgelehnt(projekt):
faden = recherche.anlegen(projekt)
with pytest.raises(ValueError):
await recherche.frage(projekt, faden["id"], " ")
async def test_geschlossener_faden_verwirft_die_antwort(projekt, monkeypatch):
"""Antworten dauern Minuten. Wer inzwischen schließt, will das Ergebnis nicht."""
faden = recherche.anlegen(projekt)
async def antworte(system, inhalt, kommando=None, ordner=None):
recherche.loeschen(projekt, faden["id"])
return "zu spät"
monkeypatch.setattr(agents, "frage_lokal", antworte)
with pytest.raises(FileNotFoundError):
await recherche.frage(projekt, faden["id"], "Was gilt?")
assert recherche.liste(projekt) == []
async def test_paralleler_faden_bleibt_erhalten(projekt, monkeypatch):
"""Der zweite Faden entsteht, während der erste noch wartet."""
a = recherche.anlegen(projekt)
neu = {}
async def antworte(system, inhalt, kommando=None, ordner=None):
neu["b"] = recherche.anlegen(projekt)
return "fertig"
monkeypatch.setattr(agents, "frage_lokal", antworte)
await recherche.frage(projekt, a["id"], "Frage")
faeden = recherche.liste(projekt)
assert [f["id"] for f in faeden] == [a["id"], neu["b"]["id"]]
# Was der eine Faden erfährt, bleibt beim einen
assert len(faeden[0]["verlauf"]) == 2 and faeden[1]["verlauf"] == []
def test_kaputte_datei_gilt_als_leer(projekt):
(projekte.stand(projekt) / recherche.DATEI).write_text("{kein json")
assert recherche.liste(projekt) == []
async def test_tiefe_haelt_den_faden_kurz(projekt, monkeypatch):
faden = recherche.anlegen(projekt)
monkeypatch.setattr(agents, "frage_lokal", _antwort("ok"))
for i in range(recherche.TIEFE):
await recherche.frage(projekt, faden["id"], f"Frage {i}")
verlauf = recherche.liste(projekt)[0]["verlauf"]
assert len(verlauf) == recherche.TIEFE
assert verlauf[-2]["text"] == f"Frage {recherche.TIEFE - 1}"

View File

@@ -58,6 +58,55 @@ async def test_feature_crud(client):
assert (await client.delete("/api/projekte/Alpha/features/login")).status_code == 404 assert (await client.delete("/api/projekte/Alpha/features/login")).status_code == 404
async def test_pruefung_ohne_konfiguration(client, monkeypatch):
import agents
monkeypatch.setattr(agents.config, "MINIMAX_API_KEY", "")
await client.post("/api/projekte", json={"name": "Alpha"})
res = await client.post("/api/projekte/Alpha/pruefung")
# 400, nicht 409 — 409 heißt „läuft bereits" und würde die Meldung verschlucken
assert res.status_code == 400
assert "MINIMAX_API_KEY" in res.json()["detail"]
async def test_unbekanntes_projekt(client): async def test_unbekanntes_projekt(client):
assert (await client.get("/api/projekte/Fremd/features")).status_code == 404 assert (await client.get("/api/projekte/Fremd/features")).status_code == 404
assert (await client.post("/api/projekte/Fremd/features", json={"roh": ROH})).status_code == 404 assert (await client.post("/api/projekte/Fremd/features", json={"roh": ROH})).status_code == 404
assert (await client.get("/api/projekte/Fremd/recherche")).status_code == 404
async def test_recherche_faeden(client, monkeypatch):
import agents
await client.post("/api/projekte", json={"name": "Alpha"})
assert (await client.get("/api/projekte/Alpha/recherche")).json()["faeden"] == []
a = (await client.post("/api/projekte/Alpha/recherche")).json()
b = (await client.post("/api/projekte/Alpha/recherche")).json()
assert a["id"] != b["id"]
async def antworte(system, inhalt, kommando=None, ordner=None):
return "## Festgelegt\n- **5 GB** je Sendung"
monkeypatch.setattr(agents, "frage_lokal", antworte)
res = await client.post(f"/api/projekte/Alpha/recherche/{a['id']}",
json={"text": "Was gilt?"})
assert res.json()["titel"] == "Was gilt?"
assert len(res.json()["verlauf"]) == 2
assert res.json()["verlauf"][-1]["text"] == "## Festgelegt\n- **5 GB** je Sendung"
# Der zweite Faden bleibt leer — Fäden teilen sich nichts
faeden = (await client.get("/api/projekte/Alpha/recherche")).json()["faeden"]
assert [len(f["verlauf"]) for f in faeden] == [2, 0]
assert (await client.delete(f"/api/projekte/Alpha/recherche/{b['id']}")).status_code == 200
assert len((await client.get("/api/projekte/Alpha/recherche")).json()["faeden"]) == 1
assert (await client.delete(f"/api/projekte/Alpha/recherche/{b['id']}")).status_code == 404
async def test_recherche_leere_frage(client):
await client.post("/api/projekte", json={"name": "Alpha"})
faden = (await client.post("/api/projekte/Alpha/recherche")).json()
res = await client.post(f"/api/projekte/Alpha/recherche/{faden['id']}",
json={"text": " "})
assert res.status_code == 400

View File

@@ -0,0 +1,325 @@
import pruefung
import struktur
def _pruefe(bereich, feld, text):
return struktur.befunde(bereich, {feld: text})
def _probleme(befunde):
return [b["problem"] for b in befunde]
def test_satzzeichen_am_zeilenende():
b = _pruefe("funktion", "regeln", "- Passwort nie loggen.")[0]
assert b["problem"] == "Satzzeichen am Zeilenende"
assert b["vorschlag"] == "Passwort nie loggen"
def test_punkt_im_wort_ist_kein_satzende():
# Der Prüfer meldete hier „Punkt am Zeilenende" — die Zeile endet auf „v"
assert _pruefe("funktion", "regeln", "- API Zugänge stehen in .env") == []
assert _pruefe("funktion", "regeln", "- Nur kern und optional erlaubt") == []
def test_fuellwort_wird_entfernt():
b = _pruefe("funktion", "regeln", "- Auch Features entfernen")[0]
assert b["problem"] == "Füllwort „auch“"
assert b["vorschlag"] == "Features entfernen"
def test_liste_der_fuellwoerter_ist_abschliessend():
# Das Modell hielt „Im", „Nur" und „direkt" für Füllwörter — sind sie nicht
for zeile in ("- Im Modal Namen eingeben", "- Nur wenige Wörter",
"- Feature direkt in Vollmodus geöffnet"):
assert _pruefe("funktion", "regeln", zeile) == []
def test_wortgrenze_je_bereich_und_feld():
assert _probleme(_pruefe("feature", "titel", "Karten der Features prüfen")) \
== ["Mehr als 3 Wörter"]
assert _pruefe("funktion", "titel", "Karten der Features prüfen") == []
assert _probleme(_pruefe("funktion", "testen", "- Eins zwei drei vier fünf sechs sieben acht neun")) \
== ["Mehr als 8 Wörter"]
def test_ohne_vorschlag_wenn_der_code_nicht_kuerzen_kann():
b = _pruefe("funktion", "testen", "- Eins zwei drei vier fünf sechs sieben acht neun")[0]
assert b["vorschlag"] == "" # den Ersatz schreibt das Modell
def test_klammer_und_schraegstrich():
assert _probleme(_pruefe("funktion", "regeln", "- Rand blau (kern)")) \
== ["Klammer oder Schrägstrich"]
assert _probleme(_pruefe("funktion", "regeln", "- Kern/Optional getrennt")) \
== ["Klammer oder Schrägstrich"]
def test_ziel_saetze_und_laenge():
lang = "Dieses Ziel hat deutlich mehr als zwölf Wörter in einem einzigen langen Satz"
assert "Satz mit mehr als 12 Wörtern" in _probleme(_pruefe("feature", "ziel", lang))
assert _probleme(_pruefe("feature", "ziel", "Eins. Zwei. Drei")) == ["Mehr als 2 Sätze"]
def test_badge_nur_kern_oder_optional():
assert _pruefe("feature", "badge", "optional") == []
b = _pruefe("feature", "badge", "wichtig")[0]
assert b["vorschlag"] == "kern"
def test_jede_zeile_einzeln():
befunde = _pruefe("funktion", "regeln", "- Erste Regel.\n- Auch zweite Regel")
assert _probleme(befunde) == ["Satzzeichen am Zeilenende", "Füllwort „auch“"]
def test_ein_befund_je_zeile():
# Zwei Vorschläge für dieselbe Zeile wären nicht unabhängig anwendbar
befunde = _pruefe("funktion", "regeln", "- Auch Passwort prüfen.")
assert len(befunde) == 1
assert befunde[0]["problem"] == "Satzzeichen am Zeilenende, Füllwort „auch“"
assert befunde[0]["vorschlag"] == "Passwort prüfen"
def test_unreparierbares_zieht_den_ganzen_befund_zum_modell():
# Zu lang und Füllwort: der Code streicht nicht halb, das Modell schreibt neu
b = _pruefe("funktion", "testen", "- Auch eins zwei drei vier fünf sechs sieben acht")[0]
assert b["problem"] == "Füllwort „auch“, Mehr als 8 Wörter"
assert b["vorschlag"] == ""
def test_auftrag_nennt_feld_und_stelle():
b = _pruefe("feature", "titel", "Karten der Features prüfen")[0]
assert struktur.auftrag(b) == '- titel „Karten der Features prüfen“: Mehr als 3 Wörter'
async def test_messung_ist_auftrag_und_tuersteher(datenordner, monkeypatch):
"""Die Messung geht als Auftrag rein und prüft die Antwort wieder nach.
Ein Vorschlag, der weiter verstößt, wird zurückgewiesen — mit Begründung."""
import agents
import features
import projekte
projekte.anlegen("Test")
f = features.anlegen(
"Test",
"<!-- titel -->\nLogin\n<!-- funktion -->\nAnmelden\n"
"<!-- regeln -->\n- Auch Passwort prüfen.\n"
"<!-- testen -->\n- Anmelden\n- Angemeldet\n",
)
gesehen = []
# Jede Stimme liefert erst einen halbherzigen Vorschlag: Punkt weg, Füllwort bleibt
halb = '{"felder": {"regeln": {"neu": "- Auch Passwort prüfen", "grund": "Füllwort"}}}'
ganz = '{"felder": {"regeln": {"neu": "- Passwort prüfen", "grund": "Füllwort"}}}'
async def modell(system, inhalt, temperatur=0.0):
gesehen.append(inhalt)
return ganz if "Dein Vorschlag verstößt" in inhalt else halb
monkeypatch.setattr(agents, "frage", modell)
aufgabe = pruefung.aufgaben(f)[1]
probleme = await pruefung._pruefe({"funktion": "<KATALOG>"}, aufgabe)
# Eine Anfrage je Stimme, dazu je eine Nachbesserung
assert len(gesehen) == 2 * len(pruefung.TEMPERATUREN)
assert "Gemessen" in gesehen[0]
assert "Füllwort „auch“" in gesehen[0]
assert sum("Dein Vorschlag verstößt" in g for g in gesehen) == len(pruefung.TEMPERATUREN)
assert [p["original"] for p in probleme] == ["Auch Passwort prüfen."]
assert probleme[0]["vorschlag"] == "Passwort prüfen"
assert probleme[0]["feld"] == "regeln"
assert probleme[0]["funktion"] == "Anmelden"
async def test_gemessener_verstoss_wird_notfalls_selbst_behoben(datenordner, monkeypatch):
"""Ein gezählter Verstoß steht fest. Liefert keine Stimme einen brauchbaren
Ersatz, greift der Ersatz des Messers — sonst bliebe der Fehler stehen."""
import agents
import features
import projekte
projekte.anlegen("Test")
f = features.anlegen(
"Test",
"<!-- titel -->\nLogin\n<!-- funktion -->\nAnmelden\n"
"<!-- regeln -->\n- Auch Passwort prüfen.\n"
"<!-- testen -->\n- Anmelden\n- Angemeldet\n",
)
async def sturr(system, inhalt, temperatur=0.0):
return '{"felder": {"regeln": {"neu": "- Auch anders.", "grund": "x"}}}'
monkeypatch.setattr(agents, "frage", sturr)
probleme = await pruefung._pruefe({"funktion": "<KATALOG>"}, pruefung.aufgaben(f)[1])
assert [p["vorschlag"] for p in probleme] == ["Passwort prüfen"]
assert probleme[0]["regel"] == "Vom System gemessen"
def test_gemessene_befunde_haengen_am_ausschnitt(datenordner):
import features
import projekte
projekte.anlegen("Test")
f = features.anlegen("Test", "<!-- titel -->\nEin viel zu langer Kartentitel\n")
karte = pruefung.aufgaben(f)[0]
befunde = struktur.befunde("feature", karte["felder"])
assert "Gemessen" not in pruefung.inhalt(karte)
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_testschritte():
"""Ohne Ablauf ist der Titel eine Behauptung — der Prüfer stülpt ihn über alles."""
b = struktur.befunde("funktion", {"titel": "Modal steuern", "ziel": "",
"regeln": "", "testen": ""})
assert _probleme(b) == ["Funktion ohne Testschritte"]
def test_ziel_und_regeln_ersetzen_die_testschritte_nicht():
for feld in ("ziel", "regeln"):
felder = {"titel": "Modal steuern", "ziel": "", "regeln": "", "testen": ""}
felder[feld] = "Etwas tun" if feld == "ziel" else "- Etwas tun"
assert _probleme(struktur.befunde("funktion", felder)) \
== ["Funktion ohne Testschritte"], feld
def test_testschritte_genuegen():
assert struktur.befunde("funktion", {"titel": "Modal steuern", "ziel": "",
"regeln": "", "testen": "- Etwas tun"}) == []
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")
def test_kommando_als_testschritt():
"""Ein Setup wird über die Kommandozeile bedient — das muss zulässig sein."""
assert _pruefe("funktion", "testen", '- Befehl "make install" ausführen') == []
assert _pruefe("funktion", "titel", "Anwendung installieren") == []
def test_zitat_zaehlt_als_ein_wort():
"""Sonst sprengt jedes Kommando die Wortgrenze."""
lang = '- Befehl "uv run python -m tft.cli refresh" ausführen'
assert _pruefe("funktion", "testen", lang) == []
# Ohne Zitat greift die Grenze weiter
assert _probleme(_pruefe("funktion", "testen",
"- Ein sehr langer Schritt mit deutlich zu vielen Wörtern")) \
== ["Mehr als 8 Wörter"]
def test_schraegstrich_im_zitat_ist_erlaubt():
assert _pruefe("funktion", "testen", '- Befehl "npm run build/dev" ausführen') == []
assert _probleme(_pruefe("funktion", "regeln", "- Gilt für Bank und/oder Brett")) \
== ["Klammer oder Schrägstrich"]
def test_fuellwort_im_zitat_bleibt_stehen():
"""Der Menüeintrag heißt so — gestrichen zeigte der Schritt auf nichts."""
assert _pruefe("funktion", "testen", '- Klick auf "Auch Vorlagen zeigen"') == []
def test_kommandoschritt_ist_eine_handlung():
"""Nach dem Ergebnis darf keine Handlung mehr stehen — auch keine mit Kommando."""
zwei = ('- Befehl "make install" ausführen\n- Anwendung installiert\n'
'- Befehl "make test" ausführen')
assert _probleme(_pruefe("funktion", "testen", zwei)) == \
["Zweiter Ablauf in einer Funktion"]
def test_ausloeser_ist_kein_funktionstitel():
"""Der Titel nennt die Wirkung; der Griff steht in den Testschritten."""
for titel in ('Befehl "make install" ausführen', "LMB auf Speichern drücken",
"Klick auf Anlegen ausführen"):
assert "Auslöser statt Wirkung im Titel" in _probleme(
_pruefe("funktion", "titel", titel)), titel
assert _pruefe("funktion", "titel", "Anwendung installieren") == []
def test_vorbedingung_ist_kein_zweiter_ablauf():
"""Der Katalog erlaubt eine Vorbedingung zuerst — sie ist kein Endzustand."""
text = "- Badge ist sichtbar\n- RMB auf Badge\n- Text ändern\n- Neuer Text sichtbar"
assert _pruefe("funktion", "testen", text) == []
def test_zweiter_ablauf_faellt_weiter_auf():
text = "- Karte öffnen\n- Karte sichtbar\n- Karte schließen"
assert _probleme(_pruefe("funktion", "testen", text)) \
== ["Zweiter Ablauf in einer Funktion"]

75
backend/verzeichnis.py Normal file
View File

@@ -0,0 +1,75 @@
"""Das Arbeitsverzeichnis eines Projekts — dort implementiert der Agent.
Der Pfad liegt als eine Zeile im Projektordner und wandert beim Umbenennen mit.
Er ist eine Eigenschaft des Projekts, nicht der Planung: gelesen und geschrieben
wird immer im Projektstand, damit ein Planungsabbruch ihn nicht mitnimmt.
"""
import os
from pathlib import Path
import projekte
DATEI = ".verzeichnis"
def _pfad(projekt: str):
return projekte.stand(projekt) / DATEI
def lies(projekt: str) -> str:
"""Der eingestellte Pfad, oder leer."""
try:
return _pfad(projekt).read_text().strip()
except OSError:
return ""
def pruefe(pfad: str) -> Path:
"""Taugt der Pfad als Arbeitsverzeichnis? Gibt ihn aufgelöst zurück oder wirft."""
pfad = (pfad or "").strip()
if not pfad:
raise ValueError("Kein Pfad angegeben")
ziel = Path(os.path.expanduser(pfad))
if not ziel.is_absolute():
raise ValueError("Der Pfad muss absolut sein")
ziel = ziel.resolve()
if not ziel.is_dir():
raise FileNotFoundError(f"„{ziel}“ ist kein Verzeichnis")
if ziel == Path(ziel.anchor) or ziel == Path.home():
# Die Wurzel oder das ganze Heimatverzeichnis ist immer ein Fehlgriff
raise ValueError("Bitte ein Projektverzeichnis wählen, nicht die Wurzel")
return ziel
def setze(projekt: str, pfad: str) -> str:
ziel = pruefe(pfad)
_pfad(projekt).write_text(f"{ziel}\n")
return str(ziel)
def loesche(projekt: str):
_pfad(projekt).unlink(missing_ok=True)
def ordner(pfad: str = "") -> dict:
"""Ein Verzeichnis zum Durchblättern: Unterordner, ohne Verstecktes.
Kein Pfad heißt Heimatverzeichnis — dort beginnt die Suche. Die Auswahl
trifft der Mensch; hier wird nur gelistet, nie geschrieben.
"""
ziel = Path(os.path.expanduser(pfad.strip())) if pfad.strip() else Path.home()
if not ziel.is_absolute():
raise ValueError("Der Pfad muss absolut sein")
ziel = ziel.resolve()
if not ziel.is_dir():
raise FileNotFoundError(f"„{ziel}“ ist kein Verzeichnis")
try:
unter = sorted(
(e.name for e in ziel.iterdir()
if e.is_dir() and not e.name.startswith(".")),
key=str.lower)
except PermissionError:
raise ValueError(f"„{ziel}“ ist nicht lesbar")
eltern = str(ziel.parent) if ziel.parent != ziel else ""
return {"pfad": str(ziel), "eltern": eltern, "ordner": unter}

View File

@@ -0,0 +1,122 @@
[
{
"rolle": "person",
"text": "Wie wird die Anwendung auf dem PC ausgeführt?"
},
{
"rolle": "ki",
"text": "Ich weiß nicht, wie die Anwendung gestartet wird. Eine übliche Variante für Ubuntu wäre ein Icon im System Tray, das bei Klick auf das Icon die Topbar öffnet. Soll ich eine Karte dafür anlegen?"
},
{
"rolle": "person",
"text": "Nein es soll automatisch gestartet werden"
},
{
"rolle": "ki",
"text": "Entwurf für die Karte. Der Autostart ist kern, weil ohne ihn das Badge nie erscheint, wenn der Benutzer es erwartet."
},
{
"rolle": "person",
"text": "Braucht autostart nicht sowas wie ein Makefile oder so, dass einmal ausgeführt wird und die anwendung installiert?"
},
{
"rolle": "ki",
"text": "Guter Punkt. Autostart setzt voraus, dass die Anwendung einmal installiert wird. Das ist ein eigener Schritt, kein Teil des Badges. Hier ein Entwurf für eine Installationskarte. Klärungsbedarf: Muss der Benutzer die Installation selbst durchführen, oder reicht ein Skript?"
},
{
"rolle": "person",
"text": "ich brauche ein setup mit make commands um die anwendung; welche make commands würde sinn machen?"
},
{
"rolle": "ki",
"text": "Die Wahl der Make-Befehle ist eine Entscheidung zur Umsetzung, nicht zum Produkt. Ich kann keine passenden Befehle vorschlagen — das ist bei jeder Technologie anders und hängt davon ab, was die Installation umfasst. Wenn du weißt, welche Befehle du brauchst, nenne sie mir. Ich dokumentiere, wie man sie aufruft."
},
{
"rolle": "person",
"text": "ich brauche ein setup mit make commands um die anwendung; welche make commands würde sinn machen?"
},
{
"rolle": "ki",
"text": "Ich schlage zwei übliche Make-Befehle vor, die zu fast jedem Projekt passen: \"make install\" für die Erstinstallation und \"make uninstall\" zum Entfernen. \"make\" ohne Ziel zeigt eine Hilfe an. Das sind Konventionen — wenn etwas anderes besser passt, sag Bescheid."
},
{
"rolle": "person",
"text": "das badge soll eine weitere funktion bekommen; ich will mehrere kurze setze eingaben können; ein satz je zeile; dann kann ich bestimmen welcher satz angezeigt wird; oder ich kann einen idle setzen, dann wechselt der satz alle x Stunden"
},
{
"rolle": "ki",
"text": "Die Karte Badge wird um zwei Funktionen erweitert: Eintrag auswählen und Idle-Modus aktivieren. Einträge sind Zeilen in der Textarea, aus denen man einen zum Anzeigen wählt. Der Idle-Modus wechselt automatisch alle eingestellten Stunden. Die bestehende Funktion Text anpassen bleibt als RMB-Einstiegspunkt."
},
{
"rolle": "person",
"text": "die funktionen haben teilweise keine daten"
},
{
"rolle": "ki",
"text": "Testschritte ergänzt. Eintrag auswählen und Idle-Modus aktivieren haben jetzt Abläufe. Die Texte sind knapp — wenn etwas fehlt, sag Bescheid."
},
{
"rolle": "person",
"text": "prüfe die karten"
},
{
"rolle": "ki",
"text": "Vier klare Verstöße korrigiert: die Icon-Regel, die Position „Links in der Topbar\", „Makefile im Projektroot\" als Bauweise und „Bestätigen klicken\" ohne Maustaste. Dazu heißt „Textarea\" jetzt überall „Textfeld\", das Kartenziel ist nicht mehr zirkulär, „Idle-Modus\" wurde zu „Automatischen Wechsel aktivieren\" verdeutscht. Offene Frage: gilt jede Zeile als ein Eintrag, oder trennt wirklich eine Leerzeile die Einträge — beides stand widersprüchlich auf der Karte, ich habe „Jede Zeile ist ein Eintrag\" gesetzt. Zweite Frage: wie heißt der Schalter für den automatischen Wechsel, ich habe „Automatisch wechseln\" angenommen."
},
{
"rolle": "person",
"text": "einige funktionen haben keinen inhalt"
},
{
"rolle": "ki",
"text": "Zwei Funktionen hatten keine Testschritte: Einträge bearbeiten und Automatischen Wechsel aktivieren. Beide sind jetzt ausgefüllt. Beim automatischen Wechsel habe ich \"Automatisch wechseln\" als Schalter und ein Feld für Stunden angenommen — stimmt das? Bei Einträge bearbeiten nehme ich an, dass RMB direkt das Textfeld öffnet und \"Bestätigen\" speichert."
},
{
"rolle": "person",
"text": "prüfe die planung"
},
{
"rolle": "ki",
"text": "Vier Verstöße korrigiert: \"Abbrechen\" ist jetzt zitiert, die Regel \"Jede Zeile ist ein Eintrag\" stand doppelt in den Testschritten, dem Deinstallieren fehlte die Vorbedingung, und das Setup-Ziel nannte nur die Installation. Der Endzustand bei \"Einträge bearbeiten\" heißt jetzt \"Einträge gespeichert\", weil ein zuvor gewählter Eintrag sonst überschrieben würde. Neu als Entwurf, bitte prüfen: Einträge überleben das Abmelden, und die Auswahl eines Eintrags beendet den automatischen Wechsel — sonst gibt es keinen Weg zurück. Offener Widerspruch: \"Ohne Eintrag bleibt das Badge sichtbar\" widerspricht deiner Ansage \"sichtbar, wenn ein Text definiert ist\"; ich habe sie behalten, weil man nach der Installation sonst kein Ziel für RMB hat."
},
{
"rolle": "person",
"text": "Bleiben die Einträge persistent über den Neustart hinaus?"
},
{
"rolle": "ki",
"text": "Ja — die Regel \"Einträge bleiben nach dem Abmelden erhalten\" steht schon auf der Karte, sie deckt Abmelden und Neustart. Ich schärfe sie zu \"Einträge bleiben nach einem Neustart erhalten\", damit kein Zweifel bleibt. Offen: gilt das auch für den gewählten Eintrag und den automatischen Wechsel — laufen die nach dem Neustart weiter? Mein Entwurf nimmt Ja an und setzt je eine Regel dafür."
},
{
"rolle": "person",
"text": "okay fehlt noch etwas?"
},
{
"rolle": "ki",
"text": "Zwei Lücken sehe ich. Erstens: welcher Eintrag erscheint ohne eigene Auswahl — mein Entwurf setzt den ersten. Zweitens: überleben gewählter Eintrag und automatischer Wechsel einen Neustart — der Entwurf nimmt Ja an und schärft die Abmelde-Regel zum Neustart. Sonst ist die Planung vollständig, Setup bleibt unverändert."
},
{
"rolle": "person",
"text": "prüfe die features auf kompaktheit"
},
{
"rolle": "ki",
"text": "Drei Stellen waren doppelt: die Neustart-Zusage stand dreimal, ich fasse sie als Kartenregel \"Einstellungen bleiben nach einem Neustart erhalten\" zusammen. Beim automatischen Wechsel werden Reihenfolge und Umbruch zu einer Regel \"Wechsel läuft zyklisch durch die Einträge\". Die Regel zum ersten Eintrag ist kürzer formuliert, Setup bleibt unverändert. Sag Bescheid, falls die Sammelregel dir zu grob ist — dann trenne ich Einträge, Auswahl und Wechsel wieder."
},
{
"rolle": "person",
"text": "prüfe die karten"
},
{
"rolle": "ki",
"text": "Setup steht auf optional, trägt aber die Installation — ohne sie läuft nichts, also kern. \"Text eingeben\" sagt nicht, wohin — jetzt \"Text in das Textfeld eingeben\". Ebenso präzisiert: \"LMB auf die Zeile im Textfeld\". Offen: erfolgt die Auswahl wirklich im Bearbeitungs-Textfeld oder in einer eigenen Liste?"
},
{
"rolle": "person",
"text": "ich kann keinen text eintragen"
},
{
"rolle": "ki",
"text": "Mein Fehler in der Annahme: RMB öffnet kein Textfeld, sondern ein Menü. Entwurf: RMB auf Badge, dann Klick auf \"Einträge bearbeiten\" — erst dann kommt das Textfeld. Die Auswahl habe ich damit auf das Menü gelegt: LMB auf den Eintrag im Menü, ohne Bestätigen. Stimmen die Menüeinträge so, und ist es RMB oder öffnet LMB das Menü?"
}
]

View File

@@ -0,0 +1,122 @@
[
{
"rolle": "person",
"text": "Wie wird die Anwendung auf dem PC ausgeführt?"
},
{
"rolle": "ki",
"text": "Ich weiß nicht, wie die Anwendung gestartet wird. Eine übliche Variante für Ubuntu wäre ein Icon im System Tray, das bei Klick auf das Icon die Topbar öffnet. Soll ich eine Karte dafür anlegen?"
},
{
"rolle": "person",
"text": "Nein es soll automatisch gestartet werden"
},
{
"rolle": "ki",
"text": "Entwurf für die Karte. Der Autostart ist kern, weil ohne ihn das Badge nie erscheint, wenn der Benutzer es erwartet."
},
{
"rolle": "person",
"text": "Braucht autostart nicht sowas wie ein Makefile oder so, dass einmal ausgeführt wird und die anwendung installiert?"
},
{
"rolle": "ki",
"text": "Guter Punkt. Autostart setzt voraus, dass die Anwendung einmal installiert wird. Das ist ein eigener Schritt, kein Teil des Badges. Hier ein Entwurf für eine Installationskarte. Klärungsbedarf: Muss der Benutzer die Installation selbst durchführen, oder reicht ein Skript?"
},
{
"rolle": "person",
"text": "ich brauche ein setup mit make commands um die anwendung; welche make commands würde sinn machen?"
},
{
"rolle": "ki",
"text": "Die Wahl der Make-Befehle ist eine Entscheidung zur Umsetzung, nicht zum Produkt. Ich kann keine passenden Befehle vorschlagen — das ist bei jeder Technologie anders und hängt davon ab, was die Installation umfasst. Wenn du weißt, welche Befehle du brauchst, nenne sie mir. Ich dokumentiere, wie man sie aufruft."
},
{
"rolle": "person",
"text": "ich brauche ein setup mit make commands um die anwendung; welche make commands würde sinn machen?"
},
{
"rolle": "ki",
"text": "Ich schlage zwei übliche Make-Befehle vor, die zu fast jedem Projekt passen: \"make install\" für die Erstinstallation und \"make uninstall\" zum Entfernen. \"make\" ohne Ziel zeigt eine Hilfe an. Das sind Konventionen — wenn etwas anderes besser passt, sag Bescheid."
},
{
"rolle": "person",
"text": "das badge soll eine weitere funktion bekommen; ich will mehrere kurze setze eingaben können; ein satz je zeile; dann kann ich bestimmen welcher satz angezeigt wird; oder ich kann einen idle setzen, dann wechselt der satz alle x Stunden"
},
{
"rolle": "ki",
"text": "Die Karte Badge wird um zwei Funktionen erweitert: Eintrag auswählen und Idle-Modus aktivieren. Einträge sind Zeilen in der Textarea, aus denen man einen zum Anzeigen wählt. Der Idle-Modus wechselt automatisch alle eingestellten Stunden. Die bestehende Funktion Text anpassen bleibt als RMB-Einstiegspunkt."
},
{
"rolle": "person",
"text": "die funktionen haben teilweise keine daten"
},
{
"rolle": "ki",
"text": "Testschritte ergänzt. Eintrag auswählen und Idle-Modus aktivieren haben jetzt Abläufe. Die Texte sind knapp — wenn etwas fehlt, sag Bescheid."
},
{
"rolle": "person",
"text": "prüfe die karten"
},
{
"rolle": "ki",
"text": "Vier klare Verstöße korrigiert: die Icon-Regel, die Position „Links in der Topbar\", „Makefile im Projektroot\" als Bauweise und „Bestätigen klicken\" ohne Maustaste. Dazu heißt „Textarea\" jetzt überall „Textfeld\", das Kartenziel ist nicht mehr zirkulär, „Idle-Modus\" wurde zu „Automatischen Wechsel aktivieren\" verdeutscht. Offene Frage: gilt jede Zeile als ein Eintrag, oder trennt wirklich eine Leerzeile die Einträge — beides stand widersprüchlich auf der Karte, ich habe „Jede Zeile ist ein Eintrag\" gesetzt. Zweite Frage: wie heißt der Schalter für den automatischen Wechsel, ich habe „Automatisch wechseln\" angenommen."
},
{
"rolle": "person",
"text": "einige funktionen haben keinen inhalt"
},
{
"rolle": "ki",
"text": "Zwei Funktionen hatten keine Testschritte: Einträge bearbeiten und Automatischen Wechsel aktivieren. Beide sind jetzt ausgefüllt. Beim automatischen Wechsel habe ich \"Automatisch wechseln\" als Schalter und ein Feld für Stunden angenommen — stimmt das? Bei Einträge bearbeiten nehme ich an, dass RMB direkt das Textfeld öffnet und \"Bestätigen\" speichert."
},
{
"rolle": "person",
"text": "prüfe die planung"
},
{
"rolle": "ki",
"text": "Vier Verstöße korrigiert: \"Abbrechen\" ist jetzt zitiert, die Regel \"Jede Zeile ist ein Eintrag\" stand doppelt in den Testschritten, dem Deinstallieren fehlte die Vorbedingung, und das Setup-Ziel nannte nur die Installation. Der Endzustand bei \"Einträge bearbeiten\" heißt jetzt \"Einträge gespeichert\", weil ein zuvor gewählter Eintrag sonst überschrieben würde. Neu als Entwurf, bitte prüfen: Einträge überleben das Abmelden, und die Auswahl eines Eintrags beendet den automatischen Wechsel — sonst gibt es keinen Weg zurück. Offener Widerspruch: \"Ohne Eintrag bleibt das Badge sichtbar\" widerspricht deiner Ansage \"sichtbar, wenn ein Text definiert ist\"; ich habe sie behalten, weil man nach der Installation sonst kein Ziel für RMB hat."
},
{
"rolle": "person",
"text": "Bleiben die Einträge persistent über den Neustart hinaus?"
},
{
"rolle": "ki",
"text": "Ja — die Regel \"Einträge bleiben nach dem Abmelden erhalten\" steht schon auf der Karte, sie deckt Abmelden und Neustart. Ich schärfe sie zu \"Einträge bleiben nach einem Neustart erhalten\", damit kein Zweifel bleibt. Offen: gilt das auch für den gewählten Eintrag und den automatischen Wechsel — laufen die nach dem Neustart weiter? Mein Entwurf nimmt Ja an und setzt je eine Regel dafür."
},
{
"rolle": "person",
"text": "okay fehlt noch etwas?"
},
{
"rolle": "ki",
"text": "Zwei Lücken sehe ich. Erstens: welcher Eintrag erscheint ohne eigene Auswahl — mein Entwurf setzt den ersten. Zweitens: überleben gewählter Eintrag und automatischer Wechsel einen Neustart — der Entwurf nimmt Ja an und schärft die Abmelde-Regel zum Neustart. Sonst ist die Planung vollständig, Setup bleibt unverändert."
},
{
"rolle": "person",
"text": "prüfe die features auf kompaktheit"
},
{
"rolle": "ki",
"text": "Drei Stellen waren doppelt: die Neustart-Zusage stand dreimal, ich fasse sie als Kartenregel \"Einstellungen bleiben nach einem Neustart erhalten\" zusammen. Beim automatischen Wechsel werden Reihenfolge und Umbruch zu einer Regel \"Wechsel läuft zyklisch durch die Einträge\". Die Regel zum ersten Eintrag ist kürzer formuliert, Setup bleibt unverändert. Sag Bescheid, falls die Sammelregel dir zu grob ist — dann trenne ich Einträge, Auswahl und Wechsel wieder."
},
{
"rolle": "person",
"text": "prüfe die karten"
},
{
"rolle": "ki",
"text": "Setup steht auf optional, trägt aber die Installation — ohne sie läuft nichts, also kern. \"Text eingeben\" sagt nicht, wohin — jetzt \"Text in das Textfeld eingeben\". Ebenso präzisiert: \"LMB auf die Zeile im Textfeld\". Offen: erfolgt die Auswahl wirklich im Bearbeitungs-Textfeld oder in einer eigenen Liste?"
},
{
"rolle": "person",
"text": "ich kann keinen text eintragen"
},
{
"rolle": "ki",
"text": "Mein Fehler in der Annahme: RMB öffnet kein Textfeld, sondern ein Menü. Entwurf: RMB auf Badge, dann Klick auf \"Einträge bearbeiten\" — erst dann kommt das Textfeld. Die Auswahl habe ich damit auf das Menü gelegt: LMB auf den Eintrag im Menü, ohne Bestätigen. Stimmen die Menüeinträge so, und ist es RMB oder öffnet LMB das Menü?"
}
]

View File

@@ -0,0 +1,50 @@
<!-- titel -->
Badge
----------------------------------------
<!-- art -->
kern
----------------------------------------
<!-- ziel -->
Zeigt einen von mehreren Einträgen in der Topbar an
----------------------------------------
<!-- regeln -->
- Ohne Eintrag bleibt das Badge sichtbar
- Beliebige Schriftzeichen erlaubt
- Jede Zeile ist ein Eintrag
- Ohne Auswahl erscheint der erste Eintrag
- Einstellungen bleiben nach einem Neustart erhalten
----------------------------------------
<!-- funktion -->
Einträge bearbeiten
<!-- regeln -->
- Textfeld zeigt die vorhandenen Einträge
- "Abbrechen" verwirft die Änderung
<!-- testen -->
- RMB auf Badge
- Klick auf "Einträge bearbeiten"
- Text in das Textfeld eingeben
- LMB auf "Bestätigen"
- Einträge gespeichert
<!-- funktion -->
Eintrag auswählen
<!-- regeln -->
- Menü listet alle Einträge
<!-- testen -->
- Mehrere Einträge vorhanden
- RMB auf Badge
- LMB auf den Eintrag im Menü
- Gewählter Eintrag im Badge
<!-- funktion -->
Automatischen Wechsel aktivieren
<!-- ziel -->
Zeigt die Einträge ohne Zutun nacheinander
<!-- regeln -->
- Wechsel läuft zyklisch durch die Einträge
- Auswahl eines Eintrags beendet den Wechsel
<!-- testen -->
- Mehrere Einträge vorhanden
- RMB auf Badge
- Klick auf "Automatisch wechseln"
- Stunden bis zum Wechsel eingeben
- LMB auf "Bestätigen"
- Badge wechselt nach der eingestellten Zeit

View File

@@ -0,0 +1,25 @@
<!-- titel -->
Setup
----------------------------------------
<!-- art -->
kern
----------------------------------------
<!-- ziel -->
Bringt die Anwendung auf den Rechner und entfernt sie wieder
----------------------------------------
<!-- regeln -->
- Befehl "make" zeigt die Hilfe
----------------------------------------
<!-- funktion -->
Anwendung installieren
<!-- testen -->
- Befehl "make install" ausführen
- Abmelden und wieder anmelden
- Badge in der Topbar sichtbar
<!-- funktion -->
Anwendung deinstallieren
<!-- testen -->
- Anwendung installiert
- Befehl "make uninstall" ausführen
- Abmelden und wieder anmelden
- Badge fehlt in der Topbar

View File

@@ -0,0 +1,13 @@
<!-- titel -->
Vision
----------------------------------------
<!-- art -->
vision
----------------------------------------
<!-- ziel -->
Text in der Topbar vom Ubuntu Desktop anzeigen.
----------------------------------------
<!-- regeln -->
- Stop trying to get rich, start trying to follow the rules
- Stop act like a person, become that person
- Discipline is the game

View File

@@ -0,0 +1,50 @@
<!-- titel -->
Badge
----------------------------------------
<!-- art -->
kern
----------------------------------------
<!-- ziel -->
Zeigt einen von mehreren Einträgen in der Topbar an
----------------------------------------
<!-- regeln -->
- Ohne Eintrag bleibt das Badge sichtbar
- Beliebige Schriftzeichen erlaubt
- Jede Zeile ist ein Eintrag
- Ohne Auswahl erscheint der erste Eintrag
- Einstellungen bleiben nach einem Neustart erhalten
----------------------------------------
<!-- funktion -->
Einträge bearbeiten
<!-- regeln -->
- Textfeld zeigt die vorhandenen Einträge
- "Abbrechen" verwirft die Änderung
<!-- testen -->
- RMB auf Badge
- Klick auf "Einträge bearbeiten"
- Text in das Textfeld eingeben
- LMB auf "Bestätigen"
- Einträge gespeichert
<!-- funktion -->
Eintrag auswählen
<!-- regeln -->
- Menü listet alle Einträge
<!-- testen -->
- Mehrere Einträge vorhanden
- RMB auf Badge
- LMB auf den Eintrag im Menü
- Gewählter Eintrag im Badge
<!-- funktion -->
Automatischen Wechsel aktivieren
<!-- ziel -->
Zeigt die Einträge ohne Zutun nacheinander
<!-- regeln -->
- Wechsel läuft zyklisch durch die Einträge
- Auswahl eines Eintrags beendet den Wechsel
<!-- testen -->
- Mehrere Einträge vorhanden
- RMB auf Badge
- Klick auf "Automatisch wechseln"
- Stunden bis zum Wechsel eingeben
- LMB auf "Bestätigen"
- Badge wechselt nach der eingestellten Zeit

View File

@@ -0,0 +1,25 @@
<!-- titel -->
Setup
----------------------------------------
<!-- art -->
kern
----------------------------------------
<!-- ziel -->
Bringt die Anwendung auf den Rechner und entfernt sie wieder
----------------------------------------
<!-- regeln -->
- Befehl "make" zeigt die Hilfe
----------------------------------------
<!-- funktion -->
Anwendung installieren
<!-- testen -->
- Befehl "make install" ausführen
- Abmelden und wieder anmelden
- Badge in der Topbar sichtbar
<!-- funktion -->
Anwendung deinstallieren
<!-- testen -->
- Anwendung installiert
- Befehl "make uninstall" ausführen
- Abmelden und wieder anmelden
- Badge fehlt in der Topbar

View File

@@ -0,0 +1,13 @@
<!-- titel -->
Vision
----------------------------------------
<!-- art -->
vision
----------------------------------------
<!-- ziel -->
Text in der Topbar vom Ubuntu Desktop anzeigen.
----------------------------------------
<!-- regeln -->
- Stop trying to get rich, start trying to follow the rules
- Stop act like a person, become that person
- Discipline is the game

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
/home/arbeit/arbeit/MtoRagSystem

View File

@@ -0,0 +1,157 @@
<!-- titel -->
Anbindung
----------------------------------------
<!-- art -->
optional
----------------------------------------
<!-- ziel -->
Bindet einen externen Server kontrolliert an
----------------------------------------
<!-- regeln -->
- Ein Token wird nie gespeichert, nur sein Name
- Eine Anbindung startet eingeschaltet, ohne Chatfreigabe
- Eine Anbindung wirkt erst nach Freigabe
- Ein Import führt nie ein Werkzeug aus
- Ein Import liest keine Inhalte
- Anweisungen fremder Inhalte werden entfernt
- Ein Transport über Standardeingabe ist gesperrt
- Nur verschlüsselte Verbindungen sind erlaubt
- Interne Netze sind gesperrt
- Weiterleitungen werden nicht verfolgt
- Protokolle enthalten nur Metadaten
- Der Bereich braucht die höchste Rolle
----------------------------------------
<!-- funktion -->
Anbindung anlegen
<!-- regeln -->
- Speichern folgt einer bestandenen Prüfung
- Ein Speichern ändert keine Antwort
- Ein Formulartoken ist Pflicht
<!-- testen -->
- Menüeintrag MCP Quellen wählen
- Reiter Anbindung anlegen wählen
- Formular ausfüllen
- Anbindung speichern
- Anbindung in der Übersicht
----------------------------------------
<!-- funktion -->
Anbindung prüfen
<!-- ziel -->
Testet die Angaben gegen Erlaubnisliste und Regeln
<!-- regeln -->
- Ohne Onlinetest entstehen keine Abrufe
- Das Ergebnis nennt den Grund
- Die Prüfung speichert nicht
<!-- testen -->
- Formular ausfüllen
- LMB auf Anbindung prüfen
- Prüfergebnis mit Begründung
----------------------------------------
<!-- funktion -->
Anbindung bearbeiten
<!-- regeln -->
- Änderungen wirken erst nach dem Speichern
- Werkzeugdaten bleiben erhalten
- Zugewiesene Inhalte bleiben am Kurznamen gebunden
<!-- testen -->
- Übersicht öffnen
- LMB auf Bearbeiten
- Formular vorbefüllt
----------------------------------------
<!-- funktion -->
Anbindung umschalten
<!-- ziel -->
Legt eine Anbindung vorübergehend still
<!-- regeln -->
- Ausgewählte Inhalte bleiben gespeichert
- Eine stillgelegte Anbindung wirkt nicht
- Wiedereinschalten braucht keine neue Freigabe
<!-- testen -->
- Übersicht öffnen
- LMB auf Deaktivieren
- Zustandsmarke wechselt
----------------------------------------
<!-- funktion -->
Anbindung entfernen
<!-- regeln -->
- Der Raum wird zuvor abgeschaltet
- Eine Rückfrage geht voraus
<!-- testen -->
- Übersicht öffnen
- LMB auf Entfernen
- Bestätigen
- Anbindung verschwindet
----------------------------------------
<!-- funktion -->
Ressource übernehmen
<!-- ziel -->
Schreibt eine gefundene Ressource in die Anbindung
<!-- regeln -->
- Nur erkannte Ressourcen sind übernehmbar
- Die Übernahme schaltet den Chat nicht ein
<!-- testen -->
- Serverinhalte aktualisieren
- LMB auf Ressource übernehmen
- Ressource in der Anbindung
----------------------------------------
<!-- funktion -->
Serverinhalte aktualisieren
<!-- ziel -->
Erkennt Ressourcen, Werkzeuge und Vorlagen als Metadaten
<!-- regeln -->
- Kein Werkzeug wird ausgeführt
- Vorlagen werden erkannt, nie ausgeführt
- Ressourcen werden nicht gelesen
- Gefundene Werkzeuge sind zunächst gesperrt
<!-- testen -->
- Übersicht öffnen
- LMB auf Inhalte aktualisieren
- Gefundene Inhalte sichtbar
----------------------------------------
<!-- funktion -->
Anbindung online prüfen
<!-- ziel -->
Prüft eine gespeicherte Anbindung im Terminal
<!-- regeln -->
- Geprüft werden Freigabe und ein Inhalt
- Ohne prüfbaren Inhalt bricht der Lauf ab
- Ein fehlendes Geheimnis blockt vorher
- Anfragen werden bereinigt protokolliert
<!-- testen -->
- Kommando mit Servernamen aufrufen
- Prüfbericht sichtbar
----------------------------------------
<!-- funktion -->
Schutzregeln ansehen
<!-- ziel -->
Zeigt Grenzen und Systemprüfung der Anbindungen
<!-- regeln -->
- Die Anzeige ruft nichts extern ab
- Freie Werkzeugnutzung ist gesperrt
<!-- testen -->
- Menüeintrag Quellen wählen
- Reiter Sicherheit wählen
- Grenzen und Prüfstand sichtbar
----------------------------------------
<!-- funktion -->
Freigaben prüfen
<!-- ziel -->
Prüft die Serverliste der Grundeinstellung
<!-- regeln -->
- Kein Transport wird geöffnet
- Das Ergebnis nennt Server, Fehler und Warnungen
<!-- testen -->
- Kommando aufrufen
- Prüfstand je Regel sichtbar
----------------------------------------
<!-- funktion -->
Brücke trocken testen
<!-- ziel -->
Prüft Bereinigung und Belegbrücke ohne Serverkontakt
<!-- regeln -->
- Nur lokale Nutzlasten werden geprüft
- Kein Transport, keine Chatwirkung
- Fremde Anweisungsmarker fallen weg
<!-- testen -->
- Kommando aufrufen
- Bereinigte Nutzlast sichtbar

View File

@@ -0,0 +1,115 @@
<!-- titel -->
Antwort
----------------------------------------
<!-- art -->
kern
----------------------------------------
<!-- ziel -->
Beantwortet eine Frage aus geprüften Quellen
----------------------------------------
<!-- regeln -->
- Nur Aussagen aus den Quellen
- Fehlende Werte werden nicht erfunden
- Keine Schätzung ersetzt einen fehlenden Wert
- Fakten und Ableitungen bleiben getrennt
- Ableitungen sind als solche markiert
- Eignung gilt als belegpflichtige Aussage
- Negativaussagen brauchen einen Quellenbeleg
- Werbesprache ist untersagt
- Sprache der Frage gilt für die Antwort
- Interne Belegkennungen bleiben auf Nachfrage beschränkt
- Interne Denktexte erreichen niemanden
- Die genutzte Datenbasis steht an der Antwort
- Trefferzahl aus Wissen und Shop liegt bei
- Die Antwortdauer wird ausgewiesen
- Ein Vertrauensabzeichen steht an der Antwort
- Wiederholungsschleifen werden sichtbar abgeschnitten
- Transparente Unsicherheit schlägt sichere Falschaussage
----------------------------------------
<!-- funktion -->
Frage stellen
<!-- ziel -->
Sendet eine Frage und empfängt die Antwort laufend
<!-- regeln -->
- Leere Frage wird abgewiesen
- Höchstens 10000 Zeichen
- Warnung ab 7500 Zeichen
- Erneutes Senden bleibt bis zum Ende gesperrt
- Entwurf überlebt einen Neuladen
- Unterbrochener Empfang wird fortgesetzt
- Ein Lauf verfällt nach 900 Sekunden
- Ein stehender Lauf wird beendet
<!-- testen -->
- Text eingeben
- Enter drücken
- Frage im Verlauf
- Antwort läuft ein
----------------------------------------
<!-- funktion -->
Antwort abbrechen
<!-- ziel -->
Beendet eine laufende Antwort vorzeitig
<!-- regeln -->
- Nur während des Empfangs möglich
- Der Serverlauf arbeitet weiter
- Der Vermerk gilt nur in der laufenden Ansicht
- Nach dem Neuladen steht die volle Antwort
<!-- testen -->
- Frage stellen
- LMB auf Abbrechen
- Abbruchvermerk im Verlauf
----------------------------------------
<!-- funktion -->
Frage erneut senden
<!-- ziel -->
Wiederholt die Frage mit anderer Einstellung
<!-- regeln -->
- Angeboten sind nur ungenutzte Varianten
- Der Klick setzt die Einstellung passend
- Während des Empfangs gesperrt
<!-- testen -->
- Frage stellen
- LMB auf eine Variante
- Frage erneut im Verlauf
----------------------------------------
<!-- funktion -->
Antwortmodus wählen
<!-- ziel -->
Wählt zwischen gründlicher und schneller Antwort
<!-- regeln -->
- Zwei Werte: Standard und schnell
- Ungültiger Wert fällt auf Standard
- Der schnelle Modus nutzt ein anderes Modell
- Der schnelle Modus antwortet ungenauer
- Die Wahl gilt für die ganze Unterhaltung
<!-- testen -->
- Chat-Einstellungen öffnen
- Antwortmodus wählen
- Modus an der nächsten Antwort
----------------------------------------
<!-- funktion -->
Datenbasis wählen
<!-- ziel -->
Legt fest, ob Wissen oder Shopdaten zählen
<!-- regeln -->
- Vier Werte: automatisch, Wissen, Shop, beides
- Unbekannter Wert fällt auf automatisch
- Nur Wissen löst keine Shopsuche aus
- Die genutzte Basis steht in der Antwort
<!-- testen -->
- Chat-Einstellungen öffnen
- Datenbasis wählen
- Basis in der Zusammenfassung
----------------------------------------
<!-- funktion -->
Folgeaktion auslösen
<!-- ziel -->
Sendet eine vorgeschlagene Folgefrage sofort
<!-- regeln -->
- Der Fragetext kommt vom Server
- Angebot entfällt bei ablehnender Antwort
- Während des Empfangs passiert nichts
<!-- testen -->
- Frage stellen
- LMB auf eine Folgeaktion
- Folgefrage im Verlauf

View File

@@ -0,0 +1,85 @@
<!-- titel -->
Auftrag
----------------------------------------
<!-- art -->
kern
----------------------------------------
<!-- ziel -->
Führt Indexierung und Neuaufbau nachvollziehbar aus
----------------------------------------
<!-- regeln -->
- Jeder Lauf hinterlässt einen Auftrag
- Der Auftrag nennt Typ, Zustand, Zeiten und Person
- Fünf Zustände bis zum Abbruch
- Nur der Neuaufbau schreibt eine Protokolldatei
- Nur ein Indexlauf läuft gleichzeitig
- Ein Strukturwechsel erzwingt einen sichtbaren Neuaufbau
- Ein unvollständiger Aufbau bricht ab
----------------------------------------
<!-- funktion -->
Aufträge einsehen
<!-- ziel -->
Zeigt den Verlauf aller Indexläufe
<!-- regeln -->
- Neueste Aufträge stehen oben
- Der Fehlertext endet in der Liste bei 120 Zeichen
<!-- testen -->
- Menüeintrag Aufträge wählen
- Auftragsliste sichtbar
----------------------------------------
<!-- funktion -->
Auftrag verfolgen
<!-- ziel -->
Beobachtet einen einzelnen Lauf bis zum Ende
<!-- regeln -->
- Die Kennung muss dem Kennungsformat folgen
- Der Stand wird zweisekündlich nachgeladen
- Geliefert werden Typ, Zustand, Zeiten und Fehler
- Das Nachladen endet mit dem Auftrag
<!-- testen -->
- Auftragsliste öffnen
- LMB auf eine Kennung
- Zustandsmarke wechselt
----------------------------------------
<!-- funktion -->
Index neu aufbauen
<!-- ziel -->
Überführt vorgemerkte Änderungen in den Index
<!-- regeln -->
- Nur die höchste Rolle darf starten
- Der Auftrag beginnt wartend
- Die Arbeit läuft im Hintergrund
- Ein Fehlstart markiert den Auftrag als gescheitert
- Nach Erfolg wird der Zwischenspeicher geleert
<!-- testen -->
- Dokumentliste öffnen
- LMB auf Index neu aufbauen
- Bestätigen
- Auftragsseite sichtbar
----------------------------------------
<!-- funktion -->
Auftrag ausführen
<!-- ziel -->
Startet einen vorhandenen Auftrag im Terminal
<!-- regeln -->
- Unbekannte Kennung wird abgewiesen
- Abgeschlossene Aufträge laufen nicht erneut
- Ein Trockenlauf vermeidet schwere Schritte
<!-- testen -->
- Kommando mit Kennung aufrufen
- Zustandszeile sichtbar
----------------------------------------
<!-- funktion -->
Index hart neu aufbauen
<!-- ziel -->
Baut Chunks, Vektoren und Tags vollständig neu
<!-- regeln -->
- Ohne Erzwingen bricht der Lauf ab
- Vier Schritte laufen in fester Reihenfolge
- Einzelne Schritte lassen sich überspringen
- Ein gescheiterter Schritt beendet den Lauf
- Ein Trockenlauf schreibt keine Indexdatei
<!-- testen -->
- Kommando mit Erzwingen aufrufen
- Vier Schritte sichtbar
- Gesundheitsstand am Ende

View File

@@ -0,0 +1,118 @@
<!-- titel -->
Batch
----------------------------------------
<!-- art -->
optional
----------------------------------------
<!-- ziel -->
Importiert viele Dokumente in einem Lauf
----------------------------------------
<!-- regeln -->
- Höchstens 250 Dateien je Upload
- Erlaubt sind PDF, Text, Markdown und Word
- Geschützte PDF gelten als nicht verarbeitbar
- Doppelte Prüfsummen werden übersprungen
- Höchstens ein Neuaufbau je Lauf
- Ohne Indexwirkung entfällt der Neuaufbau
- Der Eingang wird nach dem Lauf geräumt
- Je gelungener Lauf entsteht ein Bericht
----------------------------------------
<!-- funktion -->
Batch hochladen
<!-- ziel -->
Lädt viele Dateien gesammelt und verarbeitet sie
<!-- regeln -->
- Ohne Datei bricht der Lauf ab
- Titel entstehen aus Dateinamen oder Manifest
- Schwache Titel werden markiert
- Wissenskarten entstehen nur auf Wunsch
- Aktivierung ohne Wissenskarte wird verworfen
<!-- testen -->
- Menüeintrag Batch wählen
- Dateien wählen
- Batch starten
- Ergebniszahlen sichtbar
----------------------------------------
<!-- funktion -->
Batch vorab prüfen
<!-- ziel -->
Prüft einen Eingang ohne jede Schreibwirkung
<!-- regeln -->
- Kein Dokument wird importiert
- Kein Modell wird aufgerufen
- Dateityp, Prüfsumme und Dubletten werden geprüft
- Zusatzschalter erzeugen eine Warnung
<!-- testen -->
- Kommando im Trockenlauf aufrufen
- Bericht mit Dateiliste sichtbar
----------------------------------------
<!-- funktion -->
Batch importieren
<!-- ziel -->
Importiert einen Eingang im Terminal
<!-- regeln -->
- Der Import ist über Prüfsummen wiederholbar
- Ohne eindeutigen Benutzer bricht der Lauf ab
- Der Kommandolauf hat keine Dateigrenze
- Erfolgreiche Quellen verlassen den Eingang
- Fehler und Übersprungenes landen getrennt
- Abgeleitete Versionen bleiben ohne Schalter inaktiv
<!-- testen -->
- Kommando mit Eingang aufrufen
- Bericht als Datei geschrieben
----------------------------------------
<!-- funktion -->
Berichte auflisten
<!-- regeln -->
- Höchstens 100 Berichte, neueste oben
- Defekte Berichte werden übersprungen
<!-- testen -->
- Menüeintrag Batch wählen
- Berichtsliste sichtbar
----------------------------------------
<!-- funktion -->
Bericht ansehen
<!-- ziel -->
Zeigt einen Lauf je Datei und Schritt
<!-- regeln -->
- Die Kennung darf keine Sonderzeichen tragen
- Ein unbekannter Bericht meldet einen Fehler
- Die Ansicht startet keine Verarbeitung
<!-- testen -->
- Berichtsliste öffnen
- LMB auf Anzeigen
- Schritte und Dateien sichtbar
----------------------------------------
<!-- funktion -->
Bericht herunterladen
<!-- regeln -->
- Zwei Formate: JSON und Markdown
- Eine fehlende Datei meldet einen Fehler
<!-- testen -->
- Berichtsliste öffnen
- LMB auf ein Format
- Datei im Download
----------------------------------------
<!-- funktion -->
Bericht löschen
<!-- regeln -->
- Beide Formate fallen zusammen weg
- Dokumente und Index bleiben unberührt
<!-- testen -->
- Berichtsliste öffnen
- LMB auf Löschen
- Bestätigen
- Bericht verschwindet
----------------------------------------
<!-- funktion -->
Eingang bereinigen
<!-- ziel -->
Räumt leere Ordner und falsche Ablagen auf
<!-- regeln -->
- Die Bereinigung wirkt nur im Importverzeichnis
- Namenskollisionen bekommen einen Zähler
- Kein Einfluss auf Suche oder Antwort
<!-- testen -->
- Menüeintrag Batch wählen
- LMB auf Eingang bereinigen
- Zahl geräumter Ordner sichtbar

View File

@@ -0,0 +1,71 @@
<!-- titel -->
Beleg
----------------------------------------
<!-- art -->
kern
----------------------------------------
<!-- ziel -->
Weist nach, worauf eine Aussage sich stützt
----------------------------------------
<!-- regeln -->
- Belegquellen sind Wissen, Shop und URL-Inhalte
- Frühere Antworten belegen keine Fachwerte
- Jede Antwort erhält genau einen Belegzustand
- Direkt belegt gilt nur bei einem tragenden Beleg
- Verstreute Anforderungen ergeben schwach belegt
- Fehlende Pflichtangabe ergibt nicht vollständig belegt
- Ohne Quelle gilt nicht geprüft
- Jeder Beleg trägt eine Stärke
- Fehlende Pflichtmerkmale werden einzeln benannt
- Direkte Dokumentquellen stehen unter der Antwort
- Höchstens vier Quellentitel erscheinen
- Eine genutzte Uploaddatei bleibt in der Quellenliste
----------------------------------------
<!-- funktion -->
Belegdetails aufklappen
<!-- ziel -->
Zeigt Prüfsignale hinter der Vertrauenslage
<!-- regeln -->
- Ohne echte Signale erscheint keine Belegkarte
- Konflikte werden gesondert benannt
- Die Anzeige gilt für alle sichtbaren Antworten
<!-- testen -->
- Frage stellen
- LMB auf technische Details
- Claims und Belege sichtbar
----------------------------------------
<!-- funktion -->
Zusatzinfos umschalten
<!-- ziel -->
Blendet Quellen und Folgeaktionen ein
<!-- regeln -->
- Nur genutzte Quellen werden gelistet
- Die Einstellung überlebt einen Neuladen
<!-- testen -->
- Chat-Einstellungen öffnen
- Zusatzinfos umschalten
- Quellenblock verschwindet
----------------------------------------
<!-- funktion -->
Statusprotokoll einschalten
<!-- ziel -->
Macht Prüf- und Quellenschritte sichtbar
<!-- regeln -->
- Der Bereich öffnet sich beim Einschalten
- Statusinfos stehen neben dem Verlauf
- Fehlermeldungen bleiben immer sichtbar
- Prompts und Geheimnisse erscheinen nie
<!-- testen -->
- Chat-Einstellungen öffnen
- Statusprotokoll einschalten
- Seitenbereich mit Zeitleiste sichtbar
----------------------------------------
<!-- funktion -->
Statusprotokoll schließen
<!-- regeln -->
- Schließen schaltet die Einstellung aus
- Der Zustand überlebt einen Neuladen
<!-- testen -->
- Statusprotokoll einschalten
- LMB auf Schließen im Bereich
- Bereich verschwindet

View File

@@ -0,0 +1,82 @@
<!-- titel -->
Benutzer
----------------------------------------
<!-- art -->
kern
----------------------------------------
<!-- ziel -->
Regelt Zugang und Rollen der Menschen
----------------------------------------
<!-- regeln -->
- Ein Passwort wird nur verschlüsselt gespeichert
- Eine E-Mail ist eindeutig
- Ein deaktiviertes Konto wird abgewiesen
- Niemand deaktiviert sich selbst
- Der letzte oberste Verwalter bleibt geschützt
- Die Verwaltung braucht die höchste Rolle
- Ein Konto lässt sich nicht löschen
- Deaktivierung beendet die laufende Sitzung
----------------------------------------
<!-- funktion -->
Benutzer filtern
<!-- regeln -->
- Filter nach Text, Zustand und Rolle
- Die Suche trifft Namen, E-Mail und Abteilung
- Ein ungültiger Wert fällt auf alle
- Die Gesamtzahlen bleiben ungefiltert
<!-- testen -->
- Menüeintrag Benutzer wählen
- Filter setzen
- LMB auf Filtern
- Trefferzahl sichtbar
----------------------------------------
<!-- funktion -->
Benutzer erstellen
<!-- regeln -->
- Das Passwort braucht acht Zeichen
- Die Wiederholung muss übereinstimmen
- Mindestens eine Rolle ist Pflicht
- Unbekannte Rollen werden abgewiesen
- Die Grundrolle ist nicht vergebbar
<!-- testen -->
- Benutzerliste öffnen
- Formular ausfüllen
- Rollen ankreuzen
- Erstellen
- Benutzer in der Liste
----------------------------------------
<!-- funktion -->
Benutzer bearbeiten
<!-- regeln -->
- Leeres Passwortfeld ändert nichts
- Die oberste Rolle entzieht sich niemand selbst
- Namen enden bei 100 Zeichen
- Abteilung endet bei 120 Zeichen
<!-- testen -->
- Benutzerliste öffnen
- LMB auf Bearbeiten
- Felder ändern
- Speichern
- Änderung in der Liste
----------------------------------------
<!-- funktion -->
Benutzer umschalten
<!-- ziel -->
Sperrt oder öffnet den Zugang sofort
<!-- regeln -->
- Bestehende Rollen bleiben erhalten
<!-- testen -->
- Benutzerliste öffnen
- LMB auf Zustand
- Bestätigen
- Zustandsspalte wechselt
----------------------------------------
<!-- funktion -->
Benutzer im Terminal anlegen
<!-- regeln -->
- Doppelte E-Mail wird abgewiesen
- Die Eingabe erfolgt abgefragt
<!-- testen -->
- Kommando aufrufen
- Angaben eintragen
- Benutzer angelegt

View File

@@ -0,0 +1,117 @@
<!-- titel -->
Betriebsprüfung
----------------------------------------
<!-- art -->
optional
----------------------------------------
<!-- ziel -->
Bewertet Betriebsreife vor der Abnahme
----------------------------------------
<!-- regeln -->
- Jede Prüfung liest nur lokale Angaben
- Kein Chat, kein Shop, kein Modell läuft
- Ein Fehler setzt den Gesamtstand
- Ein fehlender Nachweis wird zur Warnung
- Ein Nachweis wird nie stillschweigend angenommen
- Geheimnisse bleiben Verweise
- Der Restore-Nachweis bleibt Betreiberaufgabe
- Das System richtet keine Zeitsteuerung ein
- Jedes Ergebnis landet bereinigt im Protokoll
----------------------------------------
<!-- funktion -->
Betrieb überblicken
<!-- ziel -->
Fasst alle Prüfstände und die letzte Sicherung
<!-- regeln -->
- Die Seite bewertet bei jedem Aufruf neu
- Nur ausgelöste Prüfungen landen im Protokoll
- Der nächste sinnvolle Schritt steht dabei
<!-- testen -->
- Menüeintrag Betrieb wählen
- Gesamtstand und Teilstände sichtbar
----------------------------------------
<!-- funktion -->
Sicherheit prüfen
<!-- ziel -->
Prüft Rollen, Geheimnisse und Grenzen
<!-- regeln -->
- Die Prüfung ersetzt kein externes Audit
- Geheimniswerte werden nicht gelesen
<!-- testen -->
- Betriebsseite öffnen
- LMB auf Sicherheit prüfen
- Fallliste mit Ständen sichtbar
----------------------------------------
<!-- funktion -->
Sicherungsweg prüfen
<!-- ziel -->
Bewertet Pfad, Umfang und Restore-Nachweis
<!-- regeln -->
- Es entsteht keine Sicherung
- Ein Restore-Erfolg wird nicht behauptet
<!-- testen -->
- Betriebsseite öffnen
- LMB auf Sicherungsweg prüfen
- Prüfpunkte sichtbar
----------------------------------------
<!-- funktion -->
Zeitsteuerung prüfen
<!-- ziel -->
Bewertet Automatik, Turnus und Betreibernachweise
<!-- regeln -->
- Der Betreiber dokumentiert Automatik selbst
- Nicht beschreibbare Pfade erzeugen Hinweise
<!-- testen -->
- Betriebsseite öffnen
- LMB auf Zeitsteuerung prüfen
- Nachweistabelle sichtbar
----------------------------------------
<!-- funktion -->
Protokoll einsehen
<!-- ziel -->
Zeigt die zuletzt ausgelösten Prüfungen
<!-- regeln -->
- Keine Prompts und keine Rohantworten
- Keine Geheimnisse und keine Zugangsdaten
- Der Text endet bei 220 Zeichen
- Höchstens 50 Einträge bleiben erhalten
- Die Anzeige zeigt die letzten acht
<!-- testen -->
- Betriebsseite öffnen
- Protokollbereich lesen
- Letzte Prüfungen sichtbar
----------------------------------------
<!-- funktion -->
Abnahmebericht ausgeben
<!-- ziel -->
Lädt den bereinigten Bericht zur Abnahme
<!-- regeln -->
- Zwei Formate: JSON und Markdown
- Die Datengrenze steht im Bericht
- Erledigte Aufgaben fallen weg
- Der Export startet keine Prüfung
<!-- testen -->
- Betriebsseite öffnen
- Format wählen
- Bericht im Download
----------------------------------------
<!-- funktion -->
Betriebsreife im Terminal prüfen
<!-- regeln -->
- Nur ein grüner Stand endet ohne Fehler
- Warnungen ändern den Ausgang nicht
<!-- testen -->
- Kommando aufrufen
- Fallliste sichtbar
----------------------------------------
<!-- funktion -->
Unterlagen öffnen
<!-- ziel -->
Zeigt Runbook und Checklisten im Adminbereich
<!-- regeln -->
- Nur Markdown aus dem Projektverzeichnis
- Die Anzeige ist rein lesend
<!-- testen -->
- Menüeintrag Leitfäden wählen
- Dokument wählen
- Inhalt sichtbar

View File

@@ -0,0 +1,51 @@
<!-- titel -->
Claim
----------------------------------------
<!-- art -->
kern
----------------------------------------
<!-- ziel -->
Prüft einzelne Aussagen der Antwort gegen Belege
----------------------------------------
<!-- regeln -->
- Geprüft werden Werte, Preise und Artikelnummern
- Geprüft werden URLs und Rollencodes
- Geprüft werden Relationen und Kompatibilitätsaussagen
- Ein Claim ohne Quelle gilt als unbelegt
- Ein unbelegter Claim setzt ein Prüfsignal
- Unbelegte Claims werden benannt
- Höchstens fünf Beispiele erscheinen
- Rollencodes gelten nur bei genauer Übereinstimmung
- Relationen brauchen beide Partner in einem Dokument
- Eine leere Antwort gilt als ungeprüft
- Sieben Claim-Arten stehen fest
----------------------------------------
<!-- funktion -->
Antwort bewerten
<!-- ziel -->
Meldet falsche Zuordnung oder Quellenfehler zurück
<!-- regeln -->
- Zwei Werte: zustimmend und ablehnend
- Ablehnung erlaubt einen Kommentar
- Kommentar wird auf 2000 Zeichen gekürzt
- Bewertung braucht eine fertige Antwort
- Eine fremde Antwort wird abgewiesen
- Die Oberfläche sperrt nach dem Speichern
- Ein erneuter Aufruf überschreibt die Bewertung
- Frage und Antwort werden mitgespeichert
<!-- testen -->
- Frage stellen
- LMB auf eine Bewertung
- Bestätigung sichtbar
----------------------------------------
<!-- funktion -->
Prompt kopieren
<!-- ziel -->
Übernimmt die eigene Frage in die Zwischenablage
<!-- regeln -->
- Kopiert wird nur der Fragetext
- Erfolg wird kurz gemeldet
<!-- testen -->
- Frage stellen
- LMB auf Kopieren
- Rückmeldung an der Schaltfläche

View File

@@ -0,0 +1,84 @@
<!-- titel -->
Diagnose
----------------------------------------
<!-- art -->
optional
----------------------------------------
<!-- ziel -->
Untersucht einzelne Teile des Antwortwegs
----------------------------------------
<!-- regeln -->
- Diagnosen ändern Antwortgüte und Wahrheitssicherung nicht
- Prompts und Rohantworten werden nie ausgegeben
- Betriebsfunktionen beeinflussen die Antwort nicht
----------------------------------------
<!-- funktion -->
Im Terminal fragen
<!-- ziel -->
Stellt Fragen an das System ohne Oberfläche
<!-- regeln -->
- Die Antwort läuft in Stücken ein
- Statusmeldungen stehen dazwischen
- Der Lauf löst echte Modellaufrufe aus
<!-- testen -->
- Kommando aufrufen
- Frage eintragen
- Antwort läuft ein
----------------------------------------
<!-- funktion -->
Suchweg testen
<!-- ziel -->
Prüft den hybriden Suchweg zu einer Frage
<!-- regeln -->
- Leere Frage wird abgewiesen
- Die Ausgabe nennt Dauer und Trefferzahl
<!-- testen -->
- Kommando mit Frage aufrufen
- Trefferliste sichtbar
----------------------------------------
<!-- funktion -->
Faktenabdeckung prüfen
<!-- ziel -->
Prüft, wie gut Fakten eine Frage abdecken
<!-- regeln -->
- Eine leere Frage wird abgewiesen
- Eine Mindestgüte ist wählbar
<!-- testen -->
- Kommando mit Frage aufrufen
- Abdeckung mit Zusammenfassung sichtbar
----------------------------------------
<!-- funktion -->
Ankerauflösung testen
<!-- ziel -->
Prüft die Auflösung einer Folgefrage trocken
<!-- regeln -->
- Kein Eingriff in die Laufzeit
- Mehrdeutige Anker werden verworfen
- Eine genannte Artikelnummer schlägt jeden Anker
<!-- testen -->
- Kommando mit Folgefrage aufrufen
- Auflösungsbericht sichtbar
----------------------------------------
<!-- funktion -->
Verlaufsanker auslesen
<!-- ziel -->
Zieht Bezugsanker aus einem Gesprächsverlauf
<!-- regeln -->
- Anker belegen keine Fachwerte
- Eingabe per Argument, Datei oder Strom
- Ein eingebauter Rauchtest ist wählbar
<!-- testen -->
- Kommando mit Rauchtest aufrufen
- Ankerliste sichtbar
----------------------------------------
<!-- funktion -->
Fakten inspizieren
<!-- ziel -->
Untersucht gespeicherte Fakten der Chunkdatei
<!-- regeln -->
- Eine fehlende Chunkdatei meldet einen Fehler
- Nur lesender Zugriff auf den Index
- Filter nach Text, Typ und Anzahl
<!-- testen -->
- Kommando mit Suchtext aufrufen
- Trefferliste sichtbar

View File

@@ -0,0 +1,107 @@
<!-- titel -->
Dokument
----------------------------------------
<!-- art -->
kern
----------------------------------------
<!-- ziel -->
Führt eine Wissensquelle über ihre Versionen
----------------------------------------
<!-- regeln -->
- Ein Dokument hält mehrere Versionen
- Genau eine Version ist aktiv
- Tags hängen am Dokument, nicht an der Version
- Schreiben an Dokument oder Version ändert den Index nicht
----------------------------------------
<!-- funktion -->
Dokumente filtern
<!-- ziel -->
Grenzt die Liste nach Zustand und Pflege ein
<!-- regeln -->
- Sechs Filter: Suche, Status, Index, Version, Titel, Tags
- Unbekannter Filterwert fällt auf alle
- Die Suche trifft Titel, Kennung und Dateiname
- Kennzahlen zählen immer den Gesamtbestand
<!-- testen -->
- Dokumentliste öffnen
- Filter setzen
- LMB auf Filtern
- Trefferzahl sichtbar
----------------------------------------
<!-- funktion -->
Dokument hochladen
<!-- ziel -->
Legt eine Wissensquelle mit erster Version an
<!-- regeln -->
- Die Datei ist Pflicht
- Ohne Titel dient der Dateiname als Titel
- Ein schwacher Titel bekommt einen Prüfhinweis
- Version eins entsteht aktiv
- Je Version wird eine Prüfsumme gespeichert
- Der Index bleibt bis zum Neuaufbau unverändert
<!-- testen -->
- Dokumentliste öffnen
- LMB auf neues Dokument
- Datei wählen
- Speichern
- Dokumentseite sichtbar
----------------------------------------
<!-- funktion -->
Titel ändern
<!-- ziel -->
Ersetzt einen schwachen Titel fachlich
<!-- regeln -->
- Leerer Titel wird abgewiesen
- Höchstens 255 Zeichen
- Der Prüfhinweis entfällt danach
- Index und Antwortlogik bleiben unberührt
<!-- testen -->
- Dokumentseite öffnen
- Titel ändern
- Speichern
- Titel als geprüft markiert
----------------------------------------
<!-- funktion -->
Titel vorschlagen lassen
<!-- ziel -->
Erzeugt Titelkandidaten aus dem Dokumentanfang
<!-- regeln -->
- Der Ausschnitt endet bei 9000 Zeichen
- Höchstens sechs Vorschläge
- Höchstens 120 Zeichen je Vorschlag
- Zu kurze Kandidaten fallen weg
- Ein Klick füllt nur das Feld
- Dokument, Version und Index bleiben unberührt
<!-- testen -->
- Dokumentseite öffnen
- LMB auf Titel vorschlagen
- Kandidat wählen
- Titelfeld gefüllt
----------------------------------------
<!-- funktion -->
Indexbereitschaft ansehen
<!-- ziel -->
Zeigt, was den Index bisher nicht erreicht hat
<!-- regeln -->
- Offene Änderungen werden gezählt
- Gelöschte Dokumente im Index werden benannt
<!-- testen -->
- Dokumentliste öffnen
- Bereitschaftsblock lesen
- Zahl offener Änderungen sichtbar
----------------------------------------
<!-- funktion -->
Dokument löschen
<!-- ziel -->
Entfernt Dokument, Versionen und Dateien endgültig
<!-- regeln -->
- Nur die höchste Rolle darf löschen
- Ein laufender Neuaufbau verhindert das Löschen
- Versionsdateien werden mitgelöscht
- Ein Auftrag hält den Vorgang fest
- Der Index ändert sich beim nächsten Neuaufbau
<!-- testen -->
- Dokumentliste öffnen
- LMB auf Löschen
- Bestätigen
- Dokument verschwindet

View File

@@ -0,0 +1,77 @@
<!-- titel -->
Evalfall
----------------------------------------
<!-- art -->
optional
----------------------------------------
<!-- ziel -->
Beschreibt einen prüfbaren Fall für die Antwortgüte
----------------------------------------
<!-- regeln -->
- Neun Falltypen stehen im Adminbereich
- Ein zehnter Typ läuft nur im Terminal
- Eine Fallkennung ist projektweit eindeutig
- Pflichtfelder sind Kennung, Typ und Frage
- Fälle liegen als Zeilendatei je Typ und Bereich
- Schreiben erfolgt mit ausschließender Sperre
----------------------------------------
<!-- funktion -->
Bereich wählen
<!-- ziel -->
Schaltet zwischen Systembasis und Fachbereich
<!-- regeln -->
- Der Bereich prägt jede weitere Handlung
- Fachbereiche liegen in eigenen Ordnern
<!-- testen -->
- Menüeintrag Eval Suite wählen
- LMB auf einen Bereichsreiter
- Fälle des Bereichs sichtbar
----------------------------------------
<!-- funktion -->
Evalfall anlegen
<!-- regeln -->
- Doppelte Kennung wird abgewiesen
- Erwartung muss ein Objekt sein
- Verlauf muss eine Liste sein
- Ein Fehler verhindert jede Änderung
<!-- testen -->
- Menüeintrag Eval Suite wählen
- LMB auf neuer Test
- Formular ausfüllen
- Absenden
- Fall in der Datei
----------------------------------------
<!-- funktion -->
Evalfall geführt anlegen
<!-- ziel -->
Legt einen Fall über einen Assistenten an
<!-- regeln -->
- Eigener Entwurf mit eigener Fehlerprüfung
<!-- testen -->
- Menüeintrag Eval Suite wählen
- LMB auf Assistent
- Schritte ausfüllen
- Fall in der Datei
----------------------------------------
<!-- funktion -->
Evalfall ableiten
<!-- ziel -->
Übernimmt einen gescheiterten Lauf als Fall
<!-- regeln -->
- Kennung und Frage werden vorbelegt
<!-- testen -->
- Lauf mit Fehlschlag öffnen
- LMB auf Fall übernehmen
- Formular vorbefüllt
----------------------------------------
<!-- funktion -->
Evalfall löschen
<!-- regeln -->
- Eine defekte Zeile verhindert die Änderung
- Unbekannte Kennung meldet einen Fehler
- Eine Rückfrage geht voraus
<!-- testen -->
- Seite neuer Test öffnen
- LMB auf Löschen
- Bestätigen
- Fall verschwindet

View File

@@ -0,0 +1,100 @@
<!-- titel -->
Evallauf
----------------------------------------
<!-- art -->
optional
----------------------------------------
<!-- ziel -->
Prüft die Antwortpipeline gegen gespeicherte Fälle
----------------------------------------
<!-- regeln -->
- Ein gescheiterter Fall setzt den Gesamtstand
- Eine leere Auswahl gilt nicht als Fehlschlag
- Berichte entstehen unteilbar
- Evals messen Fälle, nicht jede mögliche Frage
- Fünf Falltypen rufen das Modell
- Vier Falltypen laufen ohne Modell
----------------------------------------
<!-- funktion -->
Evallauf starten
<!-- ziel -->
Führt Fälle aus dem Adminbereich aus
<!-- regeln -->
- Der Lauf braucht die Wissensrolle
- Der Bericht wird zusätzlich archiviert
- Der Lauf blockiert die Anfrage bis zum Ende
<!-- testen -->
- Menüeintrag Eval Suite wählen
- Auswahl treffen
- Lauf starten
- Ergebnis je Fall sichtbar
----------------------------------------
<!-- funktion -->
Einzelfall erneut testen
<!-- ziel -->
Wiederholt genau einen gescheiterten Fall
<!-- regeln -->
- Nur der gewählte Fall läuft
<!-- testen -->
- Lauf mit Fehlschlag öffnen
- LMB auf erneut testen
- Ergebnis des Falls sichtbar
----------------------------------------
<!-- funktion -->
Läufe vergleichen
<!-- ziel -->
Stellt zwei Berichte als Unterschied gegenüber
<!-- regeln -->
- Der Vergleich nennt behoben, neu und entfernt
<!-- testen -->
- Menüeintrag Eval Suite wählen
- Vergleichsbasis wählen
- Unterschied sichtbar
----------------------------------------
<!-- funktion -->
Evallauf im Terminal ausführen
<!-- regeln -->
- Die Auswahl folgt Typ und Bereich
- Ein Abbruch nach dem ersten Fehler ist wählbar
- Ein Lauf ohne Schreiben ist wählbar
- Fälle lassen sich nur auflisten
<!-- testen -->
- Kommando mit Typ aufrufen
- Ergebnis je Fall sichtbar
----------------------------------------
<!-- funktion -->
Evallauf parallelisieren
<!-- ziel -->
Verteilt Fälle auf mehrere Kindprozesse
<!-- regeln -->
- Zwischen einem und 32 Prozessen
- Vorgabe sind zwei Prozesse
- Ein Zeitlimit je Prozess ist wählbar
- Das Ergebnis ist ein Sammelbericht
<!-- testen -->
- Kommando mit Prozesszahl aufrufen
- Sammelbericht sichtbar
----------------------------------------
<!-- funktion -->
Berichte aufräumen
<!-- regeln -->
- Nur erzeugte Dateinamen fallen weg
- Aufräumen behält 20 Läufe je Bereich
- Die Liste zeigt zehn Läufe
<!-- testen -->
- Menüeintrag Eval Suite wählen
- LMB auf Aufräumen
- Berichtszahl sinkt
----------------------------------------
<!-- funktion -->
Berichte löschen
<!-- ziel -->
Entfernt alle Läufe eines Bereichs
<!-- regeln -->
- Der aktuelle Lauf fällt mit weg
- Eine Rückfrage geht voraus
<!-- testen -->
- Menüeintrag Eval Suite wählen
- LMB auf alle löschen
- Bestätigen
- Berichtsliste leer

View File

@@ -0,0 +1,138 @@
<!-- titel -->
Fremdinhalt
----------------------------------------
<!-- art -->
optional
----------------------------------------
<!-- ziel -->
Prüft externe Inhalte vor jeder Nutzung
----------------------------------------
<!-- regeln -->
- Geprüft wird der ausgewählte Inhalt
- Ein Lesen führt kein Werkzeug aus
- Ein Lesen ändert die Antwortpipeline nicht
- Vorschauen sind gekürzt und bereinigt
- Externe Inhalte heben ihren Rang nicht selbst
- Der Bereich braucht die höchste Rolle
- Keine Übernahme ins Standardwissen erfolgt automatisch
----------------------------------------
<!-- funktion -->
Inhalt live prüfen
<!-- ziel -->
Liest den ausgewählten Inhalt einmalig online
<!-- regeln -->
- Nur der gewählte Inhalt wird gelesen
- Ohne Zutun wird nichts gespeichert
<!-- testen -->
- Übersicht öffnen
- LMB auf Inhalt prüfen
- Bestätigen
- Vorschau mit Abrufzahl
----------------------------------------
<!-- funktion -->
Inhalt bereitstellen
<!-- ziel -->
Merkt einen gelesenen Inhalt als Kandidat vor
<!-- regeln -->
- Chat und Antwortnutzung bleiben aus
- Der Kandidat startet ohne Freigabe
- Eine Rückfrage geht voraus
<!-- testen -->
- Inhalt live prüfen
- LMB auf bereitstellen
- Bestätigen
- Kandidat in der Liste
----------------------------------------
<!-- funktion -->
Inhalt auswählen
<!-- ziel -->
Ordnet einen Inhalt einem Raum zur Prüfung zu
<!-- regeln -->
- Zwei Arten: eigener Raum und Ergänzung
- Die Auswahl gilt schon als geprüft
- Die Auswahl schaltet den Chat nicht ein
- Standardwissen wird dadurch nicht ergänzt
<!-- testen -->
- Räume überblicken
- LMB auf Inhalt auswählen
- Bestätigen
- Inhalt im Freigabebereich
----------------------------------------
<!-- funktion -->
Inhalt entscheiden
<!-- ziel -->
Gibt einen geprüften Inhalt frei oder stellt zurück
<!-- regeln -->
- Fünf Wege bis zum Zurücksetzen
- Eine unbekannte Entscheidung wird abgewiesen
- Eine Freigabe schaltet den Chat nicht ein
- Die Notiz endet bei 1000 Zeichen
<!-- testen -->
- Freigabebereich öffnen
- LMB auf Inhalt ansehen
- Entscheidung wählen
- Zustandsmarke wechselt
----------------------------------------
<!-- funktion -->
Inhalt im Chat einschalten
<!-- ziel -->
Fügt einen freigegebenen Inhalt dem Raum zu
<!-- regeln -->
- Nur geprüfte Inhalte eines eigenen Raums
- Nicht vorbereitete Inhalte werden abgewiesen
- Suche und Belege bleiben unberührt
- Es entsteht kein gespeichertes Wissen
<!-- testen -->
- Freigabebereich öffnen
- LMB auf im Chat einschalten
- Inhalt als aktiv markiert
----------------------------------------
<!-- funktion -->
Kandidaten filtern
<!-- ziel -->
Grenzt vorbereitete Inhalte nach Zustand ein
<!-- regeln -->
- Filter nach Zustand, Anbindung, Art und Zeit
- Das Filtern ruft nichts extern ab
<!-- testen -->
- Vorbereitete Inhalte öffnen
- Filter setzen
- Trefferliste eingegrenzt
----------------------------------------
<!-- funktion -->
Kandidat entfernen
<!-- ziel -->
Löscht einen vorbereiteten Inhalt endgültig
<!-- regeln -->
- Die Vorschau fällt mit weg
<!-- testen -->
- Vorbereitete Inhalte öffnen
- LMB auf Entfernen
- Kandidat verschwindet
----------------------------------------
<!-- funktion -->
Kandidaten ausgeben
<!-- ziel -->
Lädt vorbereitete Inhalte als Datei herunter
<!-- regeln -->
- Zwei Formate: Markdown und JSONL
- Der Export gibt die gefilterte Menge aus
- Der Export wirkt nicht als Freigabe
<!-- testen -->
- Vorbereitete Inhalte öffnen
- Format wählen
- Herunterladen
- Datei im Download
----------------------------------------
<!-- funktion -->
Ressource lesen
<!-- ziel -->
Liest eine freigegebene Ressource im Terminal
<!-- regeln -->
- Nur erlaubte Server und Adressen
- Der Inhalt wird bereinigt
- Ausgegeben werden nur Metadaten und Prüfsumme
- Eine Kürzung wird gekennzeichnet
<!-- testen -->
- Kommando mit Adresse aufrufen
- Metadaten und Prüfsumme sichtbar

View File

@@ -0,0 +1,64 @@
<!-- titel -->
Indexprofil
----------------------------------------
<!-- art -->
optional
----------------------------------------
<!-- ziel -->
Legt Struktur für Chunking und Einbettung fest
----------------------------------------
<!-- regeln -->
- Ein Profil ist unveränderlich
- Genau ein Profil ist aktiv
- Einsehen braucht die Wissensrolle
- Ändern braucht die höchste Rolle
- Die Version zählt beim Anlegen hoch
- Ein neues Profil entsteht inaktiv
----------------------------------------
<!-- funktion -->
Profile prüfen
<!-- ziel -->
Gleicht das aktive Profil gegen den Index ab
<!-- regeln -->
- Der Vergleich läuft gegen die Indexmetadaten
- Eine abweichende Zeile setzt den Gesamtstand
- Fehlende Metadaten gelten als Strukturabweichung
- Die Ansicht startet keinen Neuaufbau
<!-- testen -->
- Menüeintrag Indexprofile wählen
- Abweichungen je Zeile sichtbar
----------------------------------------
<!-- funktion -->
Profil anlegen
<!-- regeln -->
- Chunkgröße von 50 bis 1000 in Schritten von 25
- Überlappung von 25 bis 200 in Schritten von 25
- Das Einbettungsmodell ist Pflicht
- Fehlende Pflichtwerte nennen das Feld
- Nur die höchste Rolle darf anlegen
<!-- testen -->
- Indexprofile öffnen
- Werte eintragen
- Profil erstellen
- Inaktives Profil sichtbar
----------------------------------------
<!-- funktion -->
Profil aktivieren
<!-- regeln -->
- Das bisherige Profil wird deaktiviert
- Die Aktivierung startet keinen Neuaufbau
<!-- testen -->
- Indexprofile öffnen
- LMB auf Aktivieren
- Bestätigen
- Profil als aktiv markiert
----------------------------------------
<!-- funktion -->
Profil löschen
<!-- regeln -->
- Der Löschweg erscheint nur bei inaktiven Profilen
<!-- testen -->
- Indexprofile öffnen
- LMB auf Löschen
- Bestätigen
- Profil verschwindet

View File

@@ -0,0 +1,46 @@
<!-- titel -->
Konfiguration
----------------------------------------
<!-- art -->
optional
----------------------------------------
<!-- ziel -->
Prüft die wirksamen Einstellungen vor dem Betrieb
----------------------------------------
<!-- regeln -->
- Fachsemantik gehört in die Konfiguration
- Harte Sonderpfade im Antwortkern sind verboten
- Geänderter Kataloginhalt wirkt ohne Codeänderung
----------------------------------------
<!-- funktion -->
Konfiguration validieren
<!-- regeln -->
- Fehler und Warnungen erscheinen getrennt
- Warnungen ändern den Ausgang nicht
- Regressionswächter melden nur Warnungen
<!-- testen -->
- Kommando aufrufen
- Fehler- und Warnliste sichtbar
----------------------------------------
<!-- funktion -->
Konfiguration ausgeben
<!-- ziel -->
Zeigt die aufgelöste Einstellung zur Diagnose
<!-- regeln -->
- Die Ausgabe ist nicht bereinigt
- Pfade und Adressen stehen darin
- Nur der Shopschlüssel bleibt verborgen
<!-- testen -->
- Kommando aufrufen
- Vollständige Ausgabe sichtbar
----------------------------------------
<!-- funktion -->
Quellen auditieren
<!-- ziel -->
Prüft Herkunft, Muster und Fachneutralität
<!-- regeln -->
- Reine Quelltextanalyse ohne Laufzeitwirkung
- Ein Fehlerstand beendet den Lauf
<!-- testen -->
- Kommando aufrufen
- Kennzahlen und Funde sichtbar

View File

@@ -0,0 +1,68 @@
<!-- titel -->
Modellprofil
----------------------------------------
<!-- art -->
optional
----------------------------------------
<!-- ziel -->
Stellt Modell und Suchparameter der Antwort ein
----------------------------------------
<!-- regeln -->
- Höchstens ein Profil ist systemweit aktiv
- Ohne aktives Profil greifen die Vorgabewerte
- Die Version zählt je Modellname hoch
- Ein neues Profil entsteht inaktiv
- Ein aktives Profil lässt sich nicht löschen
- Nur fehlende Felder erben die Vorgabewerte
- Wertebereiche greifen erst beim Aktivieren
- Suchwerte werden still auf 1 bis 200 begrenzt
----------------------------------------
<!-- funktion -->
Profile auflisten
<!-- regeln -->
- Das aktive Profil steht oben
- Die Anzeige startet keine Erzeugung
<!-- testen -->
- Menüeintrag Modellsetup wählen
- Profilliste sichtbar
----------------------------------------
<!-- funktion -->
Profil erstellen
<!-- regeln -->
- Der Modellname ist Pflicht
- Nur die höchste Rolle darf anlegen
<!-- testen -->
- Profilliste öffnen
- Werte eintragen
- Absenden
- Inaktives Profil sichtbar
----------------------------------------
<!-- funktion -->
Profil aktivieren
<!-- testen -->
- Profilliste öffnen
- LMB auf Aktivieren
- Bestätigen
- Profil als aktiv markiert
----------------------------------------
<!-- funktion -->
Profil löschen
<!-- testen -->
- Profilliste öffnen
- LMB auf Löschen
- Bestätigen
- Profil verschwindet
----------------------------------------
<!-- funktion -->
Suche testen
<!-- ziel -->
Prüft das Trefferverhalten eines Profils
<!-- regeln -->
- Leere Frage liefert kein Ergebnis
- Der Test ändert das Profil nicht
- Der Test aktiviert das Profil nicht
<!-- testen -->
- Profilliste öffnen
- LMB auf Testen
- Frage eintragen
- Trefferliste sichtbar

View File

@@ -0,0 +1,43 @@
<!-- titel -->
Protokoll
----------------------------------------
<!-- art -->
optional
----------------------------------------
<!-- ziel -->
Zeigt Betriebsmeldungen der Dienste
----------------------------------------
<!-- regeln -->
- Nur die höchste Rolle darf lesen
- Die Anzeige ist rein lesend
- Kein Löschen ist vorgesehen
----------------------------------------
<!-- funktion -->
Protokolle auflisten
<!-- regeln -->
- Nur Dateien nach festem Namensmuster
- Neueste Änderung steht oben
<!-- testen -->
- Menüeintrag Protokolle wählen
- Dateiliste mit Größe sichtbar
----------------------------------------
<!-- funktion -->
Protokoll anzeigen
<!-- regeln -->
- Höchstens 1000 Zeilen
- Gelesen wird vom Dateiende
- Eine fehlende Datei meldet einen Fehler
<!-- testen -->
- Protokollliste öffnen
- LMB auf Anzeigen
- Letzte Zeilen sichtbar
----------------------------------------
<!-- funktion -->
Vektorprotokoll anzeigen
<!-- regeln -->
- Feste Quelle im Protokollverzeichnis
- Höchstens 500 Zeilen
- Eine fehlende Datei meldet einen Hinweis
<!-- testen -->
- Menüeintrag Vektorprotokoll wählen
- Letzte Zeilen sichtbar

View File

@@ -0,0 +1,99 @@
<!-- titel -->
Rückmeldung
----------------------------------------
<!-- art -->
optional
----------------------------------------
<!-- ziel -->
Sammelt Nutzerkritik für die Verbesserung
----------------------------------------
<!-- regeln -->
- Eine Rückmeldung hängt an einer Antwort
- Die Adminsicht braucht die Wissensrolle
- Ein Löschen ist endgültig
- Der Export enthält Frage und Antwort im Klartext
- Der Export folgt den gesetzten Filtern
----------------------------------------
<!-- funktion -->
Rückmeldungen filtern
<!-- ziel -->
Grenzt Fälle nach zehn Merkmalen ein
<!-- regeln -->
- Filter nach Bewertung und Bearbeitungsstand
- Filter nach Einordnung und Fehlerverdacht
- Filter nach Vorrang und Referenzwert
- Seitengröße wählbar mit 25, 50 oder 100
<!-- testen -->
- Menüeintrag Chat-Feedback wählen
- Filter setzen
- Trefferliste sichtbar
----------------------------------------
<!-- funktion -->
Rückmeldung ansehen
<!-- ziel -->
Zeigt einen Fall mit Frage und Antwort
<!-- testen -->
- Rückmeldungen filtern
- LMB auf einen Fall
- Frage und Antwort sichtbar
----------------------------------------
<!-- funktion -->
Rückmeldung erledigen
<!-- regeln -->
- Eine Notiz ist möglich
<!-- testen -->
- Rückmeldung ansehen
- LMB auf erledigt
- Zustand wechselt
----------------------------------------
<!-- funktion -->
Rückmeldung wieder öffnen
<!-- testen -->
- Erledigte Rückmeldung ansehen
- LMB auf wieder öffnen
- Zustand wechselt
----------------------------------------
<!-- funktion -->
Rückmeldungen gesammelt erledigen
<!-- ziel -->
Setzt den Stand für die ganze Auswahl
<!-- regeln -->
- Auswahl über Kästchen oder Filter
- Höchstens 5000 Fälle je Aktion
<!-- testen -->
- Rückmeldungen filtern
- Auswahl treffen
- LMB auf Auswahl erledigen
- Zustände wechseln
----------------------------------------
<!-- funktion -->
Rückmeldung löschen
<!-- testen -->
- Rückmeldung ansehen
- LMB auf Löschen
- Bestätigen
- Eintrag verschwindet
----------------------------------------
<!-- funktion -->
Rückmeldungen ausgeben
<!-- regeln -->
- Zwei Formate: Markdown und JSONL
- Der Export ist nicht bereinigt
- Vorgefertigte Auszüge je Vorrang
<!-- testen -->
- Rückmeldungen filtern
- Format wählen
- Datei im Download
----------------------------------------
<!-- funktion -->
Rückmeldungen einordnen
<!-- ziel -->
Bestimmt Typ, Risiko und Vorrang im Terminal
<!-- regeln -->
- Ohne Erzwingen nur ungeordnete Einträge
- Höchstens 5000 Einträge je Lauf
- Vorgabe sind 500 Einträge
- Zwischenstände werden gespeichert
<!-- testen -->
- Kommando aufrufen
- Zähler je Risiko sichtbar

View File

@@ -0,0 +1,76 @@
<!-- titel -->
Selbsttest
----------------------------------------
<!-- art -->
optional
----------------------------------------
<!-- ziel -->
Sichert Grenzen des Systems gegen Rückschritte
----------------------------------------
<!-- regeln -->
- 23 Selbsttests decken die Regressionsfläche
- Jeder Test hat sein eigenes Kommando
- Es gibt kein Sammelkommando
- Jeder Test liefert Fallliste und Ausgang
- Jeder Test kennt eine maschinenlesbare Ausgabe
- 15 Tests prüfen nur den Quelltext
- Sechs Tests prüfen gemischt
- Zwei Tests prüfen nur Verhalten
----------------------------------------
<!-- funktion -->
Einzeltest ausführen
<!-- ziel -->
Prüft eine benannte Grenze des Systems
<!-- regeln -->
- Kein Modell, kein Netz, keine Antwort
- Keine Datenbank und kein Import
- Ein gescheiterter Fall beendet den Lauf
<!-- testen -->
- Kommandonamen aus der Übersicht holen
- Kommando aufrufen
- Fallliste mit Ständen sichtbar
----------------------------------------
<!-- funktion -->
Ingestgrenzen prüfen
<!-- ziel -->
Sichert Markdown, Chunks und Batchgrenzen ab
<!-- regeln -->
- Zwölf Kommandos decken diesen Bereich
- Ein Test räumt sein Verzeichnis selbst auf
<!-- testen -->
- Kommando aus der Ingestgruppe aufrufen
- Fallliste mit Ständen sichtbar
----------------------------------------
<!-- funktion -->
Anbindungsgrenzen prüfen
<!-- ziel -->
Sichert Fremddaten und Werkzeuggrenzen ab
<!-- regeln -->
- Drei Kommandos decken diesen Bereich
- Verbotene Zeichenketten gelten als Fall
<!-- testen -->
- Kommando aus der Anbindungsgruppe aufrufen
- Fallliste mit Ständen sichtbar
----------------------------------------
<!-- funktion -->
Architekturgrenzen prüfen
<!-- ziel -->
Sichert Flussgrenzen und Produktregeln ab
<!-- regeln -->
- Fünf Kommandos decken diesen Bereich
- Ein Test hängt an einer Freigabeversion
<!-- testen -->
- Kommando aus der Architekturgruppe aufrufen
- Fallliste mit Ständen sichtbar
----------------------------------------
<!-- funktion -->
Antwortgrenzen prüfen
<!-- ziel -->
Sichert Anker, Prüfung und Raumauswahl ab
<!-- regeln -->
- Drei Kommandos decken diesen Bereich
- Keine Chatantwort und kein Shopzugriff
- Unbelegte Begriffe bleiben unrepariert
<!-- testen -->
- Kommando aus der Antwortgruppe aufrufen
- Fallliste mit Ständen sichtbar

View File

@@ -0,0 +1,75 @@
<!-- titel -->
Shoptreffer
----------------------------------------
<!-- art -->
optional
----------------------------------------
<!-- ziel -->
Liefert Preis und Verfügbarkeit aus dem Shop
----------------------------------------
<!-- regeln -->
- Der Preis stammt allein aus dem Shopdatensatz
- Ohne Shoppreis erscheint kein Preis
- Ein fehlender Preis wird nicht zu null
- Kostenlos gilt nur mit Quellenbeleg
- Verfügbarkeit stammt allein aus dem Shopfeld
- Die Produktadresse stammt aus dem Shoppfad
- Reine Wissensantworten tragen keine Shopadressen
- Ein Shoptreffer belegt keine technische Eignung
- Zubehör wird nicht als Hauptgerät ausgegeben
- Bündelangaben bleiben unzerlegt
- Gefilterte Treffer sind keine fehlenden Treffer
- Kein Treffer bedeutet nicht: gibt es nicht
- Ein Shopausfall wird gemeldet
- Bei automatischer Basis startet sie nur bei Kaufbezug
- Die Basis Shop erzwingt die Suche
- Die gesendete Suchanfrage wird offengelegt
- Bilder stammen aus dem Bildfeld desselben Datensatzes
----------------------------------------
<!-- funktion -->
Shopsuche auslösen
<!-- ziel -->
Sucht aus einer Wissensantwort passende Produkte
<!-- regeln -->
- Angebot entfällt bei gleicher Suche
- Angebot entfällt bei ablehnender Antwort
- Eine erweiterte Suche wird ausgewiesen
<!-- testen -->
- Frage stellen
- LMB auf Shopsuche
- Trefferliste sichtbar
----------------------------------------
<!-- funktion -->
Preis nachfragen
<!-- ziel -->
Ruft Handelsdaten zum genannten Produkt ab
<!-- regeln -->
- Angebot entfällt bei genanntem Preis
- Die Folgeantwort bleibt am Produkt
<!-- testen -->
- Frage nach einem Produkt stellen
- LMB auf Preis anzeigen
- Preis und Artikelnummer sichtbar
----------------------------------------
<!-- funktion -->
Treffer filtern
<!-- ziel -->
Grenzt die Liste auf Zubehör oder Geräte ein
<!-- regeln -->
- Angebot erscheint nur bei Wirkung
- Mehrdeutige Rollen gelten nicht als Gerät
<!-- testen -->
- Shopsuche auslösen
- LMB auf eine Rolle
- Liste eingegrenzt
----------------------------------------
<!-- funktion -->
Shopsuche diagnostizieren
<!-- ziel -->
Prüft eine Suchanfrage samt Reparatur im Terminal
<!-- regeln -->
- Der Zugriff läuft live gegen den Shop
- Die Reparatursuche bevorzugt den angefragten Code
<!-- testen -->
- Kommando mit Suchtext aufrufen
- Suchanfrage und Treffer sichtbar

View File

@@ -0,0 +1,90 @@
<!-- titel -->
Sicherung
----------------------------------------
<!-- art -->
optional
----------------------------------------
<!-- ziel -->
Sichert Anwendung und Datenbank nachvollziehbar
----------------------------------------
<!-- regeln -->
- Geheimnisse werden nie mitkopiert
- Fremdbibliotheken und Zwischenspeicher bleiben draußen
- Ein Datenbankabzug entsteht auf Anforderung oder Vorgabe
- Eine Sperrdatei verhindert parallele Läufe
- Sieben Stände bleiben rollierend erhalten
- Symlinks werden übersprungen
- Ein Verzeichnis nennt Umfang und Kennung
- Eine gelungene Sicherung beweist keinen Restore
----------------------------------------
<!-- funktion -->
Sicherung testen
<!-- ziel -->
Prüft den geplanten Umfang ohne Schreibvorgang
<!-- regeln -->
- Keine Sicherungsdatei entsteht
- Keine alte Sicherung fällt weg
- Fehlende Pflichtquellen erscheinen als Warnung
- Fehlende Zusatzquellen zählen getrennt
<!-- testen -->
- Betriebsseite öffnen
- LMB auf Sicherung testen
- Umfang und Warnungen sichtbar
----------------------------------------
<!-- funktion -->
Sicherung starten
<!-- regeln -->
- Ein Datenbankabzug braucht Schalter oder Vorgabe
- Das Verzeichnis nennt keinen Zugang
- Geräumte Sicherungen werden benannt
- Eine Rückfrage geht voraus
<!-- testen -->
- Betriebsseite öffnen
- LMB auf Sicherung starten
- Bestätigen
- Sicherung in der Liste
----------------------------------------
<!-- funktion -->
Sicherung herunterladen
<!-- ziel -->
Holt ein Archiv für den Restore-Test
<!-- regeln -->
- Nur die höchste Rolle darf herunterladen
- Nur gültige lokale Kennungen
- Der Abruf startet keine Sicherung
<!-- testen -->
- Betriebsseite öffnen
- LMB auf Herunterladen
- Archiv im Download
----------------------------------------
<!-- funktion -->
Restore prüfen
<!-- ziel -->
Bewertet eine Sicherung als Wiederherstellungsgrundlage
<!-- regeln -->
- Nur die höchste Rolle darf prüfen
- Es läuft keine Wiederherstellung
- Kein Datenbankimport erfolgt
- Gelesen werden Verzeichnis und Bestandteile
- Das Ergebnis liefert einen Plan
<!-- testen -->
- Betriebsseite öffnen
- LMB auf Restore prüfen
- Prüfpunkte und Plan sichtbar
----------------------------------------
<!-- funktion -->
Sicherung im Terminal erstellen
<!-- regeln -->
- Ein Trockenlauf schreibt keine Datei
- Die Aufbewahrung ist wählbar
<!-- testen -->
- Kommando aufrufen
- Kennung und Umfang sichtbar
----------------------------------------
<!-- funktion -->
Restore im Terminal prüfen
<!-- regeln -->
- Kein Netz, kein Modell, kein Chat
<!-- testen -->
- Kommando mit Kennung aufrufen
- Prüfpunkte und Plan sichtbar

View File

@@ -0,0 +1,83 @@
<!-- titel -->
Sitzung
----------------------------------------
<!-- art -->
kern
----------------------------------------
<!-- ziel -->
Hält Zugang und Verlauf einer Unterhaltung
----------------------------------------
<!-- regeln -->
- Chat und Adminbereich teilen eine Anmeldung
- Ein Abmelden beendet beide Bereiche
- Der Verlauf hängt an einem Browserkennzeichen
- Ein Abmelden löscht dieses Kennzeichen nicht
- Deaktivierte Konten werden abgewiesen
- Der Verlauf löst nur Bezüge auf
- Der Verlauf belegt keine Fachwerte
- Ein Hinweis nennt Fehlerrisiko und Datennutzung
----------------------------------------
<!-- funktion -->
Am Chat anmelden
<!-- regeln -->
- E-Mail und Passwort sind Pflicht
- Ohne Chatrolle erscheint eine Verbotsseite
- Falscher Zugang behält die E-Mail
- Angemeldete landen sofort im Chat
<!-- testen -->
- Anmeldeseite aufrufen
- E-Mail und Passwort eintragen
- Bestätigen
- Chat mit Verlauf sichtbar
----------------------------------------
<!-- funktion -->
Vom Chat abmelden
<!-- regeln -->
- Gemerkte Einstellungen bleiben erhalten
<!-- testen -->
- LMB auf Abmelden
- Anmeldeseite sichtbar
----------------------------------------
<!-- funktion -->
Am Adminbereich anmelden
<!-- regeln -->
- Ohne Adminrolle erscheint eine Verbotsseite
- Berechtigte landen auf der Systemübersicht
- Der letzte Benutzername ist vorbelegt
<!-- testen -->
- Adminanmeldung aufrufen
- E-Mail und Passwort eintragen
- Bestätigen
- Systemübersicht sichtbar
----------------------------------------
<!-- funktion -->
Vom Adminbereich abmelden
<!-- testen -->
- LMB auf Abmelden
- Anmeldeseite sichtbar
----------------------------------------
<!-- funktion -->
Verlauf löschen
<!-- ziel -->
Entfernt Gespräch, Produktbezüge und Uploads
<!-- regeln -->
- Während des Empfangs gesperrt
- Löscht alle temporären Uploads mit
- Keine Rückfrage, keine Wiederherstellung
- Danach fehlen alle Bezugsanker
<!-- testen -->
- LMB auf Verlauf löschen
- Verlauf leer
- Uploadliste leer
----------------------------------------
<!-- funktion -->
Hilfe öffnen
<!-- ziel -->
Zeigt das Bedienhandbuch im Chat
<!-- regeln -->
- Nur bei vorhandener Handbuchdatei
- Höchstens 512 KB werden gelesen
- Die Hilfe ändert keine Antwortlogik
<!-- testen -->
- LMB auf Hilfe
- Handbuch im Fenster sichtbar

View File

@@ -0,0 +1,46 @@
<!-- titel -->
Systemprompt
----------------------------------------
<!-- art -->
optional
----------------------------------------
<!-- ziel -->
Steuert die Grundanweisung an das Modell
----------------------------------------
<!-- regeln -->
- Genau eine Version ist aktiv
- Die Version zählt fortlaufend hoch
- Eine neue Version überschreibt keine alte
- Nur die höchste Rolle darf ändern
----------------------------------------
<!-- funktion -->
Version speichern
<!-- regeln -->
- Leerer Inhalt wird abgewiesen
- Die neue Version wird sofort aktiv
- Die bisherige Version wird abgeschaltet
<!-- testen -->
- Menüeintrag Systemprompt wählen
- Text eintragen
- Speichern
- Neue aktive Version oben
----------------------------------------
<!-- funktion -->
Version aktivieren
<!-- ziel -->
Schaltet auf eine frühere Version zurück
<!-- testen -->
- Systemprompt öffnen
- LMB auf Aktivieren
- Bestätigen
- Kennzeichnung wechselt
----------------------------------------
<!-- funktion -->
Version löschen
<!-- regeln -->
- Eine aktive Version bleibt geschützt
<!-- testen -->
- Systemprompt öffnen
- LMB auf Löschen
- Bestätigen
- Version verschwindet

View File

@@ -0,0 +1,92 @@
<!-- titel -->
Tag
----------------------------------------
<!-- art -->
optional
----------------------------------------
<!-- ziel -->
Lenkt die Suche über gepflegte Signale
----------------------------------------
<!-- regeln -->
- Drei Typen: allgemein, Katalogobjekt, Vertriebssignal
- Unbekannter Typ fällt auf allgemein
- Ein Slug ist eindeutig
- Jede Änderung stößt einen Neuaufbau an
- Ein laufender Neuaufbau verhindert einen zweiten
----------------------------------------
<!-- funktion -->
Tag anlegen
<!-- regeln -->
- Slug und Bezeichnung sind Pflicht
- Der Slug wird kleingeschrieben
- Leerzeichen werden zu Bindestrichen
- Ein doppelter Slug wird abgewiesen
<!-- testen -->
- Tagliste öffnen
- Bezeichnung und Slug eintragen
- Anlegen
- Tag in der Liste
----------------------------------------
<!-- funktion -->
Tag löschen
<!-- regeln -->
- Alle Zuweisungen fallen mit weg
<!-- testen -->
- Tagliste öffnen
- LMB auf Löschen
- Bestätigen
- Tag verschwindet
----------------------------------------
<!-- funktion -->
Dokumente zuweisen
<!-- ziel -->
Ordnet einem Tag mehrere Dokumente zu
<!-- regeln -->
- Nur aktive Dokumente stehen zur Wahl
- Doppelte Kennungen werden entfernt
- Nur Unterschiede werden geschrieben
<!-- testen -->
- Tagliste öffnen
- LMB auf Zuweisen
- Dokumente ankreuzen
- Speichern
- Zähler angepasst
----------------------------------------
<!-- funktion -->
Dokument-Tags bearbeiten
<!-- ziel -->
Setzt die Tags eines einzelnen Dokuments
<!-- regeln -->
- Tags erscheinen nach Typ sortiert
- Nicht angekreuzte Tags fallen weg
<!-- testen -->
- Dokumentliste öffnen
- LMB auf Tags
- Tags ankreuzen
- Speichern
- Neuaufbau angestoßen
----------------------------------------
<!-- funktion -->
Neuaufbau verfolgen
<!-- ziel -->
Zeigt den Fortschritt des Tag-Neuaufbaus laufend
<!-- regeln -->
- Der Stand wird zweisekündlich abgefragt
- Ein Lebenszeichen kommt zehnsekündlich
- Vier Zustände: wartend, laufend, fertig, gescheitert
- Das Zusehen stößt keinen Neuaufbau an
<!-- testen -->
- Tagliste öffnen
- Statuszeile beobachten
- Zustand wechselt bis fertig
----------------------------------------
<!-- funktion -->
Tagindex neu aufbauen
<!-- ziel -->
Baut Tagexport und Tagvektoren im Terminal neu
<!-- regeln -->
- Entweder Auftragskennung oder Neuanlage
- Eine Sperrdatei verhindert Parallelläufe
<!-- testen -->
- Kommando mit Neuanlage aufrufen
- Kennzahlen sichtbar

View File

@@ -0,0 +1,68 @@
<!-- titel -->
Upload
----------------------------------------
<!-- art -->
kern
----------------------------------------
<!-- ziel -->
Stellt eine Datei als Quelle der Sitzung bereit
----------------------------------------
<!-- regeln -->
- Erlaubt sind PDF, Markdown und Text
- Höchstens drei Dateien je Sitzung
- Höchstens 10 MB je Datei
- Ein Upload verfällt nach 60 Minuten
- Uploads ändern die Wissensbasis nie
- Ein Upload beginnt nachrangig
- Ein Upload lässt sich auf vorrangig stellen
- Der ausgewertete Text endet bei 60000 Zeichen
- Der Client prüft, der Server prüft erneut
----------------------------------------
<!-- funktion -->
Datei hochladen
<!-- regeln -->
- Eine leere Datei weist erst der Server ab
- Übergroße Datei wird abgewiesen
- Bei erreichtem Maximum ist der Zugang gesperrt
- Ein Upload beginnt nachrangig
<!-- testen -->
- LMB auf Hinzufügen
- Datei wählen
- Datei mit Größe sichtbar
----------------------------------------
<!-- funktion -->
Nutzung wählen
<!-- ziel -->
Legt fest, ob der Upload allein zählt
<!-- regeln -->
- Abfrage erscheint nach dem Hochladen
- Als eigener Raum zählt nur der Upload
- Als Ergänzung bleibt Standardwissen führend
- Ohne Freigabe bleibt Standardwissen wirksam
<!-- testen -->
- Datei hochladen
- Eine Nutzungsart wählen
- Wissensraum passt sich an
----------------------------------------
<!-- funktion -->
Upload priorisieren
<!-- ziel -->
Hebt den Upload gegenüber dem Wissen an
<!-- regeln -->
- Zwei Modi: nachrangig und vorrangig
- Als eigener Raum entfällt der Schalter
- Ein Widerspruch bleibt sichtbar ausgewiesen
<!-- testen -->
- Datei hochladen
- LMB auf den Rangvermerk
- Vermerk zeigt vorrangig
----------------------------------------
<!-- funktion -->
Upload entfernen
<!-- regeln -->
- Wirkt nur auf eigene Uploads
- Ohne Upload fällt der Raum zurück
<!-- testen -->
- Datei hochladen
- LMB auf Entfernen
- Datei verschwindet

View File

@@ -0,0 +1,135 @@
<!-- titel -->
Version
----------------------------------------
<!-- art -->
kern
----------------------------------------
<!-- ziel -->
Hält einen unveränderlichen Stand eines Dokuments
----------------------------------------
<!-- regeln -->
- Eine Version wird nie überschrieben
- Jede Version trägt eine Prüfsumme
- Eine neue Version entsteht inaktiv
- Nur die aktive Version speist den Index
----------------------------------------
<!-- funktion -->
Version hochladen
<!-- ziel -->
Legt einen Nachfolgestand als neue Version ab
<!-- regeln -->
- Die Datei ist Pflicht
- Die Nummer zählt vom höchsten Stand hoch
- Die aktive Version bleibt unverändert
<!-- testen -->
- Dokumentseite öffnen
- LMB auf neue Version
- Datei wählen
- Hochladen
- Inaktive Version sichtbar
----------------------------------------
<!-- funktion -->
Originaltext ansehen
<!-- ziel -->
Zeigt die Textbasis vor der Chunk-Erzeugung
<!-- regeln -->
- Die Anzeige ändert die Datei nicht
- Jede Seite liefert 40000 Zeichen
- Eine angeforderte Seitengröße endet bei 80000
- Weitere Seiten laden beim Blättern nach
- Nicht lesbare Formate melden einen Fehler
<!-- testen -->
- Dokumentseite öffnen
- LMB auf Original ansehen
- Text im Fenster sichtbar
----------------------------------------
<!-- funktion -->
Version ableiten
<!-- ziel -->
Speichert korrigierten Text als neue Version
<!-- regeln -->
- Die Arbeitskopie endet bei 180000 Zeichen
- Leerer Inhalt wird abgewiesen
- Der Dateiname nennt die Quellversion
- Quelle und aktive Version bleiben unberührt
- Die neue Version ist inaktiv
<!-- testen -->
- Originaltext ansehen
- Text anpassen
- Als neue Version speichern
- Inaktive Version sichtbar
----------------------------------------
<!-- funktion -->
Wissenskarte erzeugen
<!-- ziel -->
Erstellt eine strukturierte Markdown-Fassung der Version
<!-- regeln -->
- Die Quelle wird segmentiert verarbeitet
- Ein unvollständiger Plan bricht ab
- Der Zusammenbau erfolgt durch das System
- Der Nachweis nennt Modell und Segmentzahl
- Kein Index wird geschrieben
- Doppelklicks bleiben während des Laufs gesperrt
<!-- testen -->
- Dokumentseite öffnen
- LMB auf Wissenskarte erzeugen
- Bestätigen
- Neue inaktive Version sichtbar
----------------------------------------
<!-- funktion -->
Version aktivieren
<!-- ziel -->
Wählt eine Version als künftige Antwortbasis
<!-- regeln -->
- Genau eine Version bleibt aktiv
- Die Version wird zum Neuaufbau vorgemerkt
- Kein Auftrag und kein Neuaufbau startet
- Bis dahin gilt der letzte stabile Index
<!-- testen -->
- Dokumentseite öffnen
- LMB auf Version aktivieren
- Bestätigen
- Warnung zum Neuaufbau sichtbar
----------------------------------------
<!-- funktion -->
Version vormerken
<!-- ziel -->
Markiert die aktive Version für den Neuaufbau
<!-- regeln -->
- Nur die aktive Version lässt sich vormerken
- Es startet kein Einzellauf
<!-- testen -->
- Dokumentseite öffnen
- LMB auf vormerken
- Bestätigen
- Version als vorgemerkt markiert
----------------------------------------
<!-- funktion -->
Chunks lesen
<!-- ziel -->
Zeigt die Chunks einer Version aus dem Index
<!-- regeln -->
- Die Anzeige ändert keine Antwortlogik
- Höchstens 300 Chunks je Version
- Je Chunk höchstens 1400 Zeichen
- Je Chunk höchstens 30 Fakten
- Ohne Indexeinträge erscheint eine Warnung
<!-- testen -->
- Dokumentseite öffnen
- LMB auf Chunks lesen
- Chunkliste sichtbar
----------------------------------------
<!-- funktion -->
Chunkgüte vergleichen
<!-- ziel -->
Stellt Original und Wissenskarte gegenüber
<!-- regeln -->
- Die Prüfung läuft nur im Speicher
- Kein Index wird geschrieben
- Kein Modell wird aufgerufen
- Ein fehlendes Gegenstück meldet einen Hinweis
- Der Vergleich nennt Chunks, Tabellen und Fakten
<!-- testen -->
- Dokumentseite öffnen
- LMB auf Güte vergleichen
- Vergleich sichtbar

View File

@@ -0,0 +1,72 @@
<!-- titel -->
Vision
----------------------------------------
<!-- art -->
vision
----------------------------------------
<!-- ziel -->
Antworten sollen nicht nur plausibel klingen, sondern nachprüfbar sein.
----------------------------------------
<!-- funktion -->
Beleggeführt antworten
<!-- ziel -->
Jede Aussage bekommt einen nachvollziehbaren Belegzustand
- Was nicht in den Quellen steht, wird nicht behauptet
- Belegzustand und Abdeckung sind ablesbar
- Einzelne Aussagen werden gegen die Belege geprüft
----------------------------------------
<!-- funktion -->
Grenzen zeigen
<!-- ziel -->
Fehlendes, Schwaches und Widersprüchliches bleibt sichtbar
- Fehlende Quellen werden benannt, nicht ersetzt
- Quellenkonflikte werden gezeigt statt zusammengeführt
- Transparente Unsicherheit schlägt sichere Falschaussage
----------------------------------------
<!-- funktion -->
Quellen trennen
<!-- ziel -->
Fachwissen, Handelsdaten und Uploads bleiben unterscheidbar
- Ein Shoppreis belegt keine technische Eignung
- Wissensräume trennen die Bestände sichtbar
- Ein Upload bleibt temporär und nachrangig
----------------------------------------
<!-- funktion -->
Wissen pflegen
<!-- ziel -->
Dokumente werden antwortfähig, nicht nur abgelegt
- Versionen bleiben einzeln nachvollziehbar
- Struktur, Chunks und Fakten sind einsehbar
- Ein Neuaufbau ist eine bewusste Entscheidung
----------------------------------------
<!-- funktion -->
Fremddaten kontrollieren
<!-- ziel -->
Externe Quellen wirken nur nach bewusster Freigabe
- Nichts wird automatisch zu Standardwissen
- Nur lesende Werkzeuge, nie Schreibaktionen
- Jede Freigabe ist eine eigene Entscheidung
----------------------------------------
<!-- funktion -->
Fachfrei bleiben
<!-- ziel -->
Fachsemantik gehört in Konfiguration, nicht in den Kern
- Harte Sonderpfade im Antwortkern sind verboten
- Ein Fachpaket ist pflegbar ohne Codeänderung
- Diagnosen verändern die Antwortgüte nicht
----------------------------------------
<!-- funktion -->
Betrieb nachweisen
<!-- ziel -->
Der Betreiber kann belegen, dass das System trägt
- Prüfungen, Sicherungen und Berichte sind abrufbar
- Berichte enthalten keine Geheimnisse
- Ein Restore-Nachweis bleibt Aufgabe des Betreibers
----------------------------------------
<!-- funktion -->
Grenzen des Systems anerkennen
<!-- ziel -->
Das System ersetzt keine Prüfung durch Menschen
- Es gibt keine Rechteverwaltung im Wissen
- Kein Ersatz für Audit, Rechtsrat oder Abkommen
- Evals messen Fälle, nicht universelle Wahrheit

View File

@@ -0,0 +1,56 @@
<!-- titel -->
Werkzeug
----------------------------------------
<!-- art -->
optional
----------------------------------------
<!-- ziel -->
Erlaubt ein lesendes Werkzeug im Anbindungsraum
----------------------------------------
<!-- regeln -->
- Nur eindeutig lesende Werkzeuge sind erlaubt
- Werkzeuge mit Schreibrisiko bleiben gesperrt
- Eine Freigabe gilt nur für ihren Raum
- Das Modell schlägt vor, das System führt aus
- Ein freigegebenes Werkzeug läuft ohne Rückfrage
- Höchstens ein Werkzeug je Antwort
- Jede Nutzung erhält einen Nachweis
- Ein Werkzeugfehler wird sichtbar gemeldet
- Standardwissen und Shop bleiben ausgeschlossen
- Der Bereich braucht die höchste Rolle
----------------------------------------
<!-- funktion -->
Werkzeug freigeben
<!-- regeln -->
- Nicht eindeutig lesende Werkzeuge bleiben gesperrt
- Der Grund für eine Sperre steht dabei
- Die Freigabe führt kein Werkzeug aus
<!-- testen -->
- Räume überblicken
- Abschnitt Werkzeuge aufklappen
- LMB auf freigeben
- Werkzeug als freigegeben markiert
----------------------------------------
<!-- funktion -->
Werkzeugfreigabe zurücknehmen
<!-- regeln -->
- Die Rücknahme wirkt sofort
<!-- testen -->
- Räume überblicken
- Abschnitt Werkzeuge aufklappen
- LMB auf Freigabe entfernen
- Werkzeug wieder gesperrt
----------------------------------------
<!-- funktion -->
Werkzeugergebnis auswählen
<!-- ziel -->
Ordnet ein gespeichertes Ergebnis einem Raum zu
<!-- regeln -->
- Kein Werkzeug läuft dabei
- Ergebnisse bleiben gespeicherte Prüfinformationen
- Ergebnisse sind nicht live lesbar
<!-- testen -->
- Räume überblicken
- Abschnitt Ergebnisse aufklappen
- LMB auf auswählen
- Ergebnis im Freigabebereich

View File

@@ -0,0 +1,90 @@
<!-- titel -->
Wissensindex
----------------------------------------
<!-- art -->
kern
----------------------------------------
<!-- ziel -->
Hält die durchsuchbare Fassung des Wissens
----------------------------------------
<!-- regeln -->
- Treffer unter der Mindestgüte fallen weg
- Jeder Treffer trägt eine Chunkkennung
- Dokument und Abschnitt kommen mit, sofern vorhanden
- Eine nicht erreichbare Suche liefert keine Treffer
- Ein veralteter Index wird gemeldet
- Ein unvollständiger Aufbau bricht ab
----------------------------------------
<!-- funktion -->
Systemstand ansehen
<!-- ziel -->
Zeigt Chunkzahl und Gesundheit der Dienste
<!-- regeln -->
- Die Anzeige schreibt nichts
- Die harte Chunkgrenze liegt bei 25000
<!-- testen -->
- Menüeintrag Systemübersicht wählen
- Chunkzahl und Gesundheit sichtbar
----------------------------------------
<!-- funktion -->
Chunkindex einsehen
<!-- regeln -->
- Nur die höchste Rolle darf einsehen
- Vorgabe 50, höchstens 200 Einträge je Seite
- Die Anzeige startet keinen Neuaufbau
<!-- testen -->
- Menüeintrag Wissensbasis wählen
- Metadaten und Einträge sichtbar
----------------------------------------
<!-- funktion -->
Zwischenspeicher leeren
<!-- ziel -->
Verwirft kurzlebige Zwischenstände nach Wartung
<!-- regeln -->
- Der Dienst kennt keine Antwortlogik
- Ein nicht erreichbarer Vektordienst ergibt eine Warnung
- Eine Rückfrage geht voraus
<!-- testen -->
- Systemübersicht öffnen
- LMB auf Zwischenspeicher leeren
- Bestätigen
- Ergebnismeldung sichtbar
----------------------------------------
<!-- funktion -->
Gesundheit prüfen
<!-- ziel -->
Prüft Vektor- und Tagindex im Terminal
<!-- regeln -->
- Gesund ist jeder Zustand mit Vorsilbe OK
- Es entsteht kein Schreibzugriff
<!-- testen -->
- Kommando aufrufen
- Prüfbericht sichtbar
----------------------------------------
<!-- funktion -->
Vektordienst steuern
<!-- ziel -->
Startet, stoppt oder lädt den Vektordienst neu
<!-- regeln -->
- Die Steuerung läuft über das Kontrollskript
- Das Zeitlimit beträgt 600 Sekunden
<!-- testen -->
- Kommando mit Aktion aufrufen
- Ausgabe des Skripts sichtbar
----------------------------------------
<!-- funktion -->
Wissen zurücksetzen
<!-- ziel -->
Löscht Dokumente, Versionen, Tags und Indexdateien
<!-- regeln -->
- Nur die höchste Rolle darf ausführen
- Die Funktion ist standardmäßig gesperrt
- Der Bestätigungstext muss wörtlich stimmen
- Zuvor entsteht eine Sicherung
- Ein Fehlschlag der Sicherung bricht ab
<!-- testen -->
- Systemübersicht öffnen
- Bestätigungstext eintippen
- Absenden
- Bestätigen
- Sicherungspfad sichtbar

View File

@@ -0,0 +1,97 @@
<!-- titel -->
Wissensraum
----------------------------------------
<!-- art -->
kern
----------------------------------------
<!-- ziel -->
Bestimmt, welcher Bestand eine Antwort trägt
----------------------------------------
<!-- regeln -->
- Standardwissen ist immer verfügbar
- Standardwissen lässt sich nicht abschalten
- Ein Uploadraum antwortet nur aus dem Upload
- Ein Uploadraum kennt keinen stillen Rückfall
- Ein Anbindungsraum liest nur Freigegebenes
- Ein Anbindungsraum mischt kein Standardwissen
- Ein unbekannter Raum fällt auf Standardwissen
- Freigegebene Inhalte und Werkzeugergebnisse sind nachrangig
- Beide bleiben vom Rangvorteil ausgenommen
----------------------------------------
<!-- funktion -->
Wissensraum wählen
<!-- ziel -->
Legt die Antwortgrundlage der Unterhaltung fest
<!-- regeln -->
- Der Uploadraum braucht einen gültigen Upload
- Gesperrte Räume sind nicht wählbar
- Während des Empfangs gesperrt
<!-- testen -->
- Chat-Einstellungen öffnen
- Wissensraum wählen
- Raum in der Zusammenfassung
----------------------------------------
<!-- funktion -->
Räume überblicken
<!-- ziel -->
Zeigt, welche Räume arbeiten und angeboten werden
<!-- regeln -->
- Der Zugang braucht die höchste Rolle
- Anbindungen sind erst nach Freigabe wirksam
<!-- testen -->
- Menüeintrag Wissensräume wählen
- Räume mit Zählern sichtbar
----------------------------------------
<!-- funktion -->
Chatauswahl umschalten
<!-- ziel -->
Gibt einen Raum für die Chatauswahl frei
<!-- regeln -->
- Standardwissen bleibt gesperrt
- Ausschalten löscht keine Freigaben
- Wiedereinschalten braucht keine neue Freigabe
- Nicht vorgesehene Räume werden abgewiesen
<!-- testen -->
- Räume überblicken
- LMB auf Einschalten
- Raum als aktiv markiert
----------------------------------------
<!-- funktion -->
Freigegebenen Inhalt vorschauen
<!-- ziel -->
Liest einen freigegebenen Inhalt vor der Entscheidung
<!-- regeln -->
- Der Dialog liest nur auf Klick
- Kein Standardwissen und kein Werkzeug
- Es wird nichts gespeichert
<!-- testen -->
- Räume überblicken
- Freigabebereich öffnen
- LMB auf live lesen
- Textvorschau sichtbar
----------------------------------------
<!-- funktion -->
Herkunft ansehen
<!-- ziel -->
Zeigt, welche Inhalte welchem Raum gehören
<!-- regeln -->
- Reine Anzeige ohne Freigabe
- Kein externer Abruf entsteht
<!-- testen -->
- Räume überblicken
- Abschnitt Herkunft aufklappen
- Zuordnungen je Raum sichtbar
----------------------------------------
<!-- funktion -->
Raumgrenzen prüfen
<!-- ziel -->
Führt sechs Selbsttests zu den Raumgrenzen aus
<!-- regeln -->
- Es entsteht keine Chatantwort
- Kein Werkzeug wird ausgeführt
- Kein Inhalt wird live gelesen
- Netz und Speicher werden je Test ausgewiesen
<!-- testen -->
- Räume überblicken
- LMB auf Prüfungen ausführen
- Sammelergebnis sichtbar

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
/home/arbeit/arbeit/MtoRagSystem

View File

@@ -0,0 +1,157 @@
<!-- titel -->
Anbindung
----------------------------------------
<!-- art -->
optional
----------------------------------------
<!-- ziel -->
Bindet einen externen Server kontrolliert an
----------------------------------------
<!-- regeln -->
- Ein Token wird nie gespeichert, nur sein Name
- Eine Anbindung startet eingeschaltet, ohne Chatfreigabe
- Eine Anbindung wirkt erst nach Freigabe
- Ein Import führt nie ein Werkzeug aus
- Ein Import liest keine Inhalte
- Anweisungen fremder Inhalte werden entfernt
- Ein Transport über Standardeingabe ist gesperrt
- Nur verschlüsselte Verbindungen sind erlaubt
- Interne Netze sind gesperrt
- Weiterleitungen werden nicht verfolgt
- Protokolle enthalten nur Metadaten
- Der Bereich braucht die höchste Rolle
----------------------------------------
<!-- funktion -->
Anbindung anlegen
<!-- regeln -->
- Speichern folgt einer bestandenen Prüfung
- Ein Speichern ändert keine Antwort
- Ein Formulartoken ist Pflicht
<!-- testen -->
- Menüeintrag MCP Quellen wählen
- Reiter Anbindung anlegen wählen
- Formular ausfüllen
- Anbindung speichern
- Anbindung in der Übersicht
----------------------------------------
<!-- funktion -->
Anbindung prüfen
<!-- ziel -->
Testet die Angaben gegen Erlaubnisliste und Regeln
<!-- regeln -->
- Ohne Onlinetest entstehen keine Abrufe
- Das Ergebnis nennt den Grund
- Die Prüfung speichert nicht
<!-- testen -->
- Formular ausfüllen
- LMB auf Anbindung prüfen
- Prüfergebnis mit Begründung
----------------------------------------
<!-- funktion -->
Anbindung bearbeiten
<!-- regeln -->
- Änderungen wirken erst nach dem Speichern
- Werkzeugdaten bleiben erhalten
- Zugewiesene Inhalte bleiben am Kurznamen gebunden
<!-- testen -->
- Übersicht öffnen
- LMB auf Bearbeiten
- Formular vorbefüllt
----------------------------------------
<!-- funktion -->
Anbindung umschalten
<!-- ziel -->
Legt eine Anbindung vorübergehend still
<!-- regeln -->
- Ausgewählte Inhalte bleiben gespeichert
- Eine stillgelegte Anbindung wirkt nicht
- Wiedereinschalten braucht keine neue Freigabe
<!-- testen -->
- Übersicht öffnen
- LMB auf Deaktivieren
- Zustandsmarke wechselt
----------------------------------------
<!-- funktion -->
Anbindung entfernen
<!-- regeln -->
- Der Raum wird zuvor abgeschaltet
- Eine Rückfrage geht voraus
<!-- testen -->
- Übersicht öffnen
- LMB auf Entfernen
- Bestätigen
- Anbindung verschwindet
----------------------------------------
<!-- funktion -->
Ressource übernehmen
<!-- ziel -->
Schreibt eine gefundene Ressource in die Anbindung
<!-- regeln -->
- Nur erkannte Ressourcen sind übernehmbar
- Die Übernahme schaltet den Chat nicht ein
<!-- testen -->
- Serverinhalte aktualisieren
- LMB auf Ressource übernehmen
- Ressource in der Anbindung
----------------------------------------
<!-- funktion -->
Serverinhalte aktualisieren
<!-- ziel -->
Erkennt Ressourcen, Werkzeuge und Vorlagen als Metadaten
<!-- regeln -->
- Kein Werkzeug wird ausgeführt
- Vorlagen werden erkannt, nie ausgeführt
- Ressourcen werden nicht gelesen
- Gefundene Werkzeuge sind zunächst gesperrt
<!-- testen -->
- Übersicht öffnen
- LMB auf Inhalte aktualisieren
- Gefundene Inhalte sichtbar
----------------------------------------
<!-- funktion -->
Anbindung online prüfen
<!-- ziel -->
Prüft eine gespeicherte Anbindung im Terminal
<!-- regeln -->
- Geprüft werden Freigabe und ein Inhalt
- Ohne prüfbaren Inhalt bricht der Lauf ab
- Ein fehlendes Geheimnis blockt vorher
- Anfragen werden bereinigt protokolliert
<!-- testen -->
- Kommando mit Servernamen aufrufen
- Prüfbericht sichtbar
----------------------------------------
<!-- funktion -->
Schutzregeln ansehen
<!-- ziel -->
Zeigt Grenzen und Systemprüfung der Anbindungen
<!-- regeln -->
- Die Anzeige ruft nichts extern ab
- Freie Werkzeugnutzung ist gesperrt
<!-- testen -->
- Menüeintrag Quellen wählen
- Reiter Sicherheit wählen
- Grenzen und Prüfstand sichtbar
----------------------------------------
<!-- funktion -->
Freigaben prüfen
<!-- ziel -->
Prüft die Serverliste der Grundeinstellung
<!-- regeln -->
- Kein Transport wird geöffnet
- Das Ergebnis nennt Server, Fehler und Warnungen
<!-- testen -->
- Kommando aufrufen
- Prüfstand je Regel sichtbar
----------------------------------------
<!-- funktion -->
Brücke trocken testen
<!-- ziel -->
Prüft Bereinigung und Belegbrücke ohne Serverkontakt
<!-- regeln -->
- Nur lokale Nutzlasten werden geprüft
- Kein Transport, keine Chatwirkung
- Fremde Anweisungsmarker fallen weg
<!-- testen -->
- Kommando aufrufen
- Bereinigte Nutzlast sichtbar

View File

@@ -0,0 +1,115 @@
<!-- titel -->
Antwort
----------------------------------------
<!-- art -->
kern
----------------------------------------
<!-- ziel -->
Beantwortet eine Frage aus geprüften Quellen
----------------------------------------
<!-- regeln -->
- Nur Aussagen aus den Quellen
- Fehlende Werte werden nicht erfunden
- Keine Schätzung ersetzt einen fehlenden Wert
- Fakten und Ableitungen bleiben getrennt
- Ableitungen sind als solche markiert
- Eignung gilt als belegpflichtige Aussage
- Negativaussagen brauchen einen Quellenbeleg
- Werbesprache ist untersagt
- Sprache der Frage gilt für die Antwort
- Interne Belegkennungen bleiben auf Nachfrage beschränkt
- Interne Denktexte erreichen niemanden
- Die genutzte Datenbasis steht an der Antwort
- Trefferzahl aus Wissen und Shop liegt bei
- Die Antwortdauer wird ausgewiesen
- Ein Vertrauensabzeichen steht an der Antwort
- Wiederholungsschleifen werden sichtbar abgeschnitten
- Transparente Unsicherheit schlägt sichere Falschaussage
----------------------------------------
<!-- funktion -->
Frage stellen
<!-- ziel -->
Sendet eine Frage und empfängt die Antwort laufend
<!-- regeln -->
- Leere Frage wird abgewiesen
- Höchstens 10000 Zeichen
- Warnung ab 7500 Zeichen
- Erneutes Senden bleibt bis zum Ende gesperrt
- Entwurf überlebt einen Neuladen
- Unterbrochener Empfang wird fortgesetzt
- Ein Lauf verfällt nach 900 Sekunden
- Ein stehender Lauf wird beendet
<!-- testen -->
- Text eingeben
- Enter drücken
- Frage im Verlauf
- Antwort läuft ein
----------------------------------------
<!-- funktion -->
Antwort abbrechen
<!-- ziel -->
Beendet eine laufende Antwort vorzeitig
<!-- regeln -->
- Nur während des Empfangs möglich
- Der Serverlauf arbeitet weiter
- Der Vermerk gilt nur in der laufenden Ansicht
- Nach dem Neuladen steht die volle Antwort
<!-- testen -->
- Frage stellen
- LMB auf Abbrechen
- Abbruchvermerk im Verlauf
----------------------------------------
<!-- funktion -->
Frage erneut senden
<!-- ziel -->
Wiederholt die Frage mit anderer Einstellung
<!-- regeln -->
- Angeboten sind nur ungenutzte Varianten
- Der Klick setzt die Einstellung passend
- Während des Empfangs gesperrt
<!-- testen -->
- Frage stellen
- LMB auf eine Variante
- Frage erneut im Verlauf
----------------------------------------
<!-- funktion -->
Antwortmodus wählen
<!-- ziel -->
Wählt zwischen gründlicher und schneller Antwort
<!-- regeln -->
- Zwei Werte: Standard und schnell
- Ungültiger Wert fällt auf Standard
- Der schnelle Modus nutzt ein anderes Modell
- Der schnelle Modus antwortet ungenauer
- Die Wahl gilt für die ganze Unterhaltung
<!-- testen -->
- Chat-Einstellungen öffnen
- Antwortmodus wählen
- Modus an der nächsten Antwort
----------------------------------------
<!-- funktion -->
Datenbasis wählen
<!-- ziel -->
Legt fest, ob Wissen oder Shopdaten zählen
<!-- regeln -->
- Vier Werte: automatisch, Wissen, Shop, beides
- Unbekannter Wert fällt auf automatisch
- Nur Wissen löst keine Shopsuche aus
- Die genutzte Basis steht in der Antwort
<!-- testen -->
- Chat-Einstellungen öffnen
- Datenbasis wählen
- Basis in der Zusammenfassung
----------------------------------------
<!-- funktion -->
Folgeaktion auslösen
<!-- ziel -->
Sendet eine vorgeschlagene Folgefrage sofort
<!-- regeln -->
- Der Fragetext kommt vom Server
- Angebot entfällt bei ablehnender Antwort
- Während des Empfangs passiert nichts
<!-- testen -->
- Frage stellen
- LMB auf eine Folgeaktion
- Folgefrage im Verlauf

View File

@@ -0,0 +1,85 @@
<!-- titel -->
Auftrag
----------------------------------------
<!-- art -->
kern
----------------------------------------
<!-- ziel -->
Führt Indexierung und Neuaufbau nachvollziehbar aus
----------------------------------------
<!-- regeln -->
- Jeder Lauf hinterlässt einen Auftrag
- Der Auftrag nennt Typ, Zustand, Zeiten und Person
- Fünf Zustände bis zum Abbruch
- Nur der Neuaufbau schreibt eine Protokolldatei
- Nur ein Indexlauf läuft gleichzeitig
- Ein Strukturwechsel erzwingt einen sichtbaren Neuaufbau
- Ein unvollständiger Aufbau bricht ab
----------------------------------------
<!-- funktion -->
Aufträge einsehen
<!-- ziel -->
Zeigt den Verlauf aller Indexläufe
<!-- regeln -->
- Neueste Aufträge stehen oben
- Der Fehlertext endet in der Liste bei 120 Zeichen
<!-- testen -->
- Menüeintrag Aufträge wählen
- Auftragsliste sichtbar
----------------------------------------
<!-- funktion -->
Auftrag verfolgen
<!-- ziel -->
Beobachtet einen einzelnen Lauf bis zum Ende
<!-- regeln -->
- Die Kennung muss dem Kennungsformat folgen
- Der Stand wird zweisekündlich nachgeladen
- Geliefert werden Typ, Zustand, Zeiten und Fehler
- Das Nachladen endet mit dem Auftrag
<!-- testen -->
- Auftragsliste öffnen
- LMB auf eine Kennung
- Zustandsmarke wechselt
----------------------------------------
<!-- funktion -->
Index neu aufbauen
<!-- ziel -->
Überführt vorgemerkte Änderungen in den Index
<!-- regeln -->
- Nur die höchste Rolle darf starten
- Der Auftrag beginnt wartend
- Die Arbeit läuft im Hintergrund
- Ein Fehlstart markiert den Auftrag als gescheitert
- Nach Erfolg wird der Zwischenspeicher geleert
<!-- testen -->
- Dokumentliste öffnen
- LMB auf Index neu aufbauen
- Bestätigen
- Auftragsseite sichtbar
----------------------------------------
<!-- funktion -->
Auftrag ausführen
<!-- ziel -->
Startet einen vorhandenen Auftrag im Terminal
<!-- regeln -->
- Unbekannte Kennung wird abgewiesen
- Abgeschlossene Aufträge laufen nicht erneut
- Ein Trockenlauf vermeidet schwere Schritte
<!-- testen -->
- Kommando mit Kennung aufrufen
- Zustandszeile sichtbar
----------------------------------------
<!-- funktion -->
Index hart neu aufbauen
<!-- ziel -->
Baut Chunks, Vektoren und Tags vollständig neu
<!-- regeln -->
- Ohne Erzwingen bricht der Lauf ab
- Vier Schritte laufen in fester Reihenfolge
- Einzelne Schritte lassen sich überspringen
- Ein gescheiterter Schritt beendet den Lauf
- Ein Trockenlauf schreibt keine Indexdatei
<!-- testen -->
- Kommando mit Erzwingen aufrufen
- Vier Schritte sichtbar
- Gesundheitsstand am Ende

118
daten/MtoRagSystem/batch.md Normal file
View File

@@ -0,0 +1,118 @@
<!-- titel -->
Batch
----------------------------------------
<!-- art -->
optional
----------------------------------------
<!-- ziel -->
Importiert viele Dokumente in einem Lauf
----------------------------------------
<!-- regeln -->
- Höchstens 250 Dateien je Upload
- Erlaubt sind PDF, Text, Markdown und Word
- Geschützte PDF gelten als nicht verarbeitbar
- Doppelte Prüfsummen werden übersprungen
- Höchstens ein Neuaufbau je Lauf
- Ohne Indexwirkung entfällt der Neuaufbau
- Der Eingang wird nach dem Lauf geräumt
- Je gelungener Lauf entsteht ein Bericht
----------------------------------------
<!-- funktion -->
Batch hochladen
<!-- ziel -->
Lädt viele Dateien gesammelt und verarbeitet sie
<!-- regeln -->
- Ohne Datei bricht der Lauf ab
- Titel entstehen aus Dateinamen oder Manifest
- Schwache Titel werden markiert
- Wissenskarten entstehen nur auf Wunsch
- Aktivierung ohne Wissenskarte wird verworfen
<!-- testen -->
- Menüeintrag Batch wählen
- Dateien wählen
- Batch starten
- Ergebniszahlen sichtbar
----------------------------------------
<!-- funktion -->
Batch vorab prüfen
<!-- ziel -->
Prüft einen Eingang ohne jede Schreibwirkung
<!-- regeln -->
- Kein Dokument wird importiert
- Kein Modell wird aufgerufen
- Dateityp, Prüfsumme und Dubletten werden geprüft
- Zusatzschalter erzeugen eine Warnung
<!-- testen -->
- Kommando im Trockenlauf aufrufen
- Bericht mit Dateiliste sichtbar
----------------------------------------
<!-- funktion -->
Batch importieren
<!-- ziel -->
Importiert einen Eingang im Terminal
<!-- regeln -->
- Der Import ist über Prüfsummen wiederholbar
- Ohne eindeutigen Benutzer bricht der Lauf ab
- Der Kommandolauf hat keine Dateigrenze
- Erfolgreiche Quellen verlassen den Eingang
- Fehler und Übersprungenes landen getrennt
- Abgeleitete Versionen bleiben ohne Schalter inaktiv
<!-- testen -->
- Kommando mit Eingang aufrufen
- Bericht als Datei geschrieben
----------------------------------------
<!-- funktion -->
Berichte auflisten
<!-- regeln -->
- Höchstens 100 Berichte, neueste oben
- Defekte Berichte werden übersprungen
<!-- testen -->
- Menüeintrag Batch wählen
- Berichtsliste sichtbar
----------------------------------------
<!-- funktion -->
Bericht ansehen
<!-- ziel -->
Zeigt einen Lauf je Datei und Schritt
<!-- regeln -->
- Die Kennung darf keine Sonderzeichen tragen
- Ein unbekannter Bericht meldet einen Fehler
- Die Ansicht startet keine Verarbeitung
<!-- testen -->
- Berichtsliste öffnen
- LMB auf Anzeigen
- Schritte und Dateien sichtbar
----------------------------------------
<!-- funktion -->
Bericht herunterladen
<!-- regeln -->
- Zwei Formate: JSON und Markdown
- Eine fehlende Datei meldet einen Fehler
<!-- testen -->
- Berichtsliste öffnen
- LMB auf ein Format
- Datei im Download
----------------------------------------
<!-- funktion -->
Bericht löschen
<!-- regeln -->
- Beide Formate fallen zusammen weg
- Dokumente und Index bleiben unberührt
<!-- testen -->
- Berichtsliste öffnen
- LMB auf Löschen
- Bestätigen
- Bericht verschwindet
----------------------------------------
<!-- funktion -->
Eingang bereinigen
<!-- ziel -->
Räumt leere Ordner und falsche Ablagen auf
<!-- regeln -->
- Die Bereinigung wirkt nur im Importverzeichnis
- Namenskollisionen bekommen einen Zähler
- Kein Einfluss auf Suche oder Antwort
<!-- testen -->
- Menüeintrag Batch wählen
- LMB auf Eingang bereinigen
- Zahl geräumter Ordner sichtbar

View File

@@ -0,0 +1,71 @@
<!-- titel -->
Beleg
----------------------------------------
<!-- art -->
kern
----------------------------------------
<!-- ziel -->
Weist nach, worauf eine Aussage sich stützt
----------------------------------------
<!-- regeln -->
- Belegquellen sind Wissen, Shop und URL-Inhalte
- Frühere Antworten belegen keine Fachwerte
- Jede Antwort erhält genau einen Belegzustand
- Direkt belegt gilt nur bei einem tragenden Beleg
- Verstreute Anforderungen ergeben schwach belegt
- Fehlende Pflichtangabe ergibt nicht vollständig belegt
- Ohne Quelle gilt nicht geprüft
- Jeder Beleg trägt eine Stärke
- Fehlende Pflichtmerkmale werden einzeln benannt
- Direkte Dokumentquellen stehen unter der Antwort
- Höchstens vier Quellentitel erscheinen
- Eine genutzte Uploaddatei bleibt in der Quellenliste
----------------------------------------
<!-- funktion -->
Belegdetails aufklappen
<!-- ziel -->
Zeigt Prüfsignale hinter der Vertrauenslage
<!-- regeln -->
- Ohne echte Signale erscheint keine Belegkarte
- Konflikte werden gesondert benannt
- Die Anzeige gilt für alle sichtbaren Antworten
<!-- testen -->
- Frage stellen
- LMB auf technische Details
- Claims und Belege sichtbar
----------------------------------------
<!-- funktion -->
Zusatzinfos umschalten
<!-- ziel -->
Blendet Quellen und Folgeaktionen ein
<!-- regeln -->
- Nur genutzte Quellen werden gelistet
- Die Einstellung überlebt einen Neuladen
<!-- testen -->
- Chat-Einstellungen öffnen
- Zusatzinfos umschalten
- Quellenblock verschwindet
----------------------------------------
<!-- funktion -->
Statusprotokoll einschalten
<!-- ziel -->
Macht Prüf- und Quellenschritte sichtbar
<!-- regeln -->
- Der Bereich öffnet sich beim Einschalten
- Statusinfos stehen neben dem Verlauf
- Fehlermeldungen bleiben immer sichtbar
- Prompts und Geheimnisse erscheinen nie
<!-- testen -->
- Chat-Einstellungen öffnen
- Statusprotokoll einschalten
- Seitenbereich mit Zeitleiste sichtbar
----------------------------------------
<!-- funktion -->
Statusprotokoll schließen
<!-- regeln -->
- Schließen schaltet die Einstellung aus
- Der Zustand überlebt einen Neuladen
<!-- testen -->
- Statusprotokoll einschalten
- LMB auf Schließen im Bereich
- Bereich verschwindet

View File

@@ -0,0 +1,82 @@
<!-- titel -->
Benutzer
----------------------------------------
<!-- art -->
kern
----------------------------------------
<!-- ziel -->
Regelt Zugang und Rollen der Menschen
----------------------------------------
<!-- regeln -->
- Ein Passwort wird nur verschlüsselt gespeichert
- Eine E-Mail ist eindeutig
- Ein deaktiviertes Konto wird abgewiesen
- Niemand deaktiviert sich selbst
- Der letzte oberste Verwalter bleibt geschützt
- Die Verwaltung braucht die höchste Rolle
- Ein Konto lässt sich nicht löschen
- Deaktivierung beendet die laufende Sitzung
----------------------------------------
<!-- funktion -->
Benutzer filtern
<!-- regeln -->
- Filter nach Text, Zustand und Rolle
- Die Suche trifft Namen, E-Mail und Abteilung
- Ein ungültiger Wert fällt auf alle
- Die Gesamtzahlen bleiben ungefiltert
<!-- testen -->
- Menüeintrag Benutzer wählen
- Filter setzen
- LMB auf Filtern
- Trefferzahl sichtbar
----------------------------------------
<!-- funktion -->
Benutzer erstellen
<!-- regeln -->
- Das Passwort braucht acht Zeichen
- Die Wiederholung muss übereinstimmen
- Mindestens eine Rolle ist Pflicht
- Unbekannte Rollen werden abgewiesen
- Die Grundrolle ist nicht vergebbar
<!-- testen -->
- Benutzerliste öffnen
- Formular ausfüllen
- Rollen ankreuzen
- Erstellen
- Benutzer in der Liste
----------------------------------------
<!-- funktion -->
Benutzer bearbeiten
<!-- regeln -->
- Leeres Passwortfeld ändert nichts
- Die oberste Rolle entzieht sich niemand selbst
- Namen enden bei 100 Zeichen
- Abteilung endet bei 120 Zeichen
<!-- testen -->
- Benutzerliste öffnen
- LMB auf Bearbeiten
- Felder ändern
- Speichern
- Änderung in der Liste
----------------------------------------
<!-- funktion -->
Benutzer umschalten
<!-- ziel -->
Sperrt oder öffnet den Zugang sofort
<!-- regeln -->
- Bestehende Rollen bleiben erhalten
<!-- testen -->
- Benutzerliste öffnen
- LMB auf Zustand
- Bestätigen
- Zustandsspalte wechselt
----------------------------------------
<!-- funktion -->
Benutzer im Terminal anlegen
<!-- regeln -->
- Doppelte E-Mail wird abgewiesen
- Die Eingabe erfolgt abgefragt
<!-- testen -->
- Kommando aufrufen
- Angaben eintragen
- Benutzer angelegt

View File

@@ -0,0 +1,117 @@
<!-- titel -->
Betriebsprüfung
----------------------------------------
<!-- art -->
optional
----------------------------------------
<!-- ziel -->
Bewertet Betriebsreife vor der Abnahme
----------------------------------------
<!-- regeln -->
- Jede Prüfung liest nur lokale Angaben
- Kein Chat, kein Shop, kein Modell läuft
- Ein Fehler setzt den Gesamtstand
- Ein fehlender Nachweis wird zur Warnung
- Ein Nachweis wird nie stillschweigend angenommen
- Geheimnisse bleiben Verweise
- Der Restore-Nachweis bleibt Betreiberaufgabe
- Das System richtet keine Zeitsteuerung ein
- Jedes Ergebnis landet bereinigt im Protokoll
----------------------------------------
<!-- funktion -->
Betrieb überblicken
<!-- ziel -->
Fasst alle Prüfstände und die letzte Sicherung
<!-- regeln -->
- Die Seite bewertet bei jedem Aufruf neu
- Nur ausgelöste Prüfungen landen im Protokoll
- Der nächste sinnvolle Schritt steht dabei
<!-- testen -->
- Menüeintrag Betrieb wählen
- Gesamtstand und Teilstände sichtbar
----------------------------------------
<!-- funktion -->
Sicherheit prüfen
<!-- ziel -->
Prüft Rollen, Geheimnisse und Grenzen
<!-- regeln -->
- Die Prüfung ersetzt kein externes Audit
- Geheimniswerte werden nicht gelesen
<!-- testen -->
- Betriebsseite öffnen
- LMB auf Sicherheit prüfen
- Fallliste mit Ständen sichtbar
----------------------------------------
<!-- funktion -->
Sicherungsweg prüfen
<!-- ziel -->
Bewertet Pfad, Umfang und Restore-Nachweis
<!-- regeln -->
- Es entsteht keine Sicherung
- Ein Restore-Erfolg wird nicht behauptet
<!-- testen -->
- Betriebsseite öffnen
- LMB auf Sicherungsweg prüfen
- Prüfpunkte sichtbar
----------------------------------------
<!-- funktion -->
Zeitsteuerung prüfen
<!-- ziel -->
Bewertet Automatik, Turnus und Betreibernachweise
<!-- regeln -->
- Der Betreiber dokumentiert Automatik selbst
- Nicht beschreibbare Pfade erzeugen Hinweise
<!-- testen -->
- Betriebsseite öffnen
- LMB auf Zeitsteuerung prüfen
- Nachweistabelle sichtbar
----------------------------------------
<!-- funktion -->
Protokoll einsehen
<!-- ziel -->
Zeigt die zuletzt ausgelösten Prüfungen
<!-- regeln -->
- Keine Prompts und keine Rohantworten
- Keine Geheimnisse und keine Zugangsdaten
- Der Text endet bei 220 Zeichen
- Höchstens 50 Einträge bleiben erhalten
- Die Anzeige zeigt die letzten acht
<!-- testen -->
- Betriebsseite öffnen
- Protokollbereich lesen
- Letzte Prüfungen sichtbar
----------------------------------------
<!-- funktion -->
Abnahmebericht ausgeben
<!-- ziel -->
Lädt den bereinigten Bericht zur Abnahme
<!-- regeln -->
- Zwei Formate: JSON und Markdown
- Die Datengrenze steht im Bericht
- Erledigte Aufgaben fallen weg
- Der Export startet keine Prüfung
<!-- testen -->
- Betriebsseite öffnen
- Format wählen
- Bericht im Download
----------------------------------------
<!-- funktion -->
Betriebsreife im Terminal prüfen
<!-- regeln -->
- Nur ein grüner Stand endet ohne Fehler
- Warnungen ändern den Ausgang nicht
<!-- testen -->
- Kommando aufrufen
- Fallliste sichtbar
----------------------------------------
<!-- funktion -->
Unterlagen öffnen
<!-- ziel -->
Zeigt Runbook und Checklisten im Adminbereich
<!-- regeln -->
- Nur Markdown aus dem Projektverzeichnis
- Die Anzeige ist rein lesend
<!-- testen -->
- Menüeintrag Leitfäden wählen
- Dokument wählen
- Inhalt sichtbar

View File

@@ -0,0 +1,51 @@
<!-- titel -->
Claim
----------------------------------------
<!-- art -->
kern
----------------------------------------
<!-- ziel -->
Prüft einzelne Aussagen der Antwort gegen Belege
----------------------------------------
<!-- regeln -->
- Geprüft werden Werte, Preise und Artikelnummern
- Geprüft werden URLs und Rollencodes
- Geprüft werden Relationen und Kompatibilitätsaussagen
- Ein Claim ohne Quelle gilt als unbelegt
- Ein unbelegter Claim setzt ein Prüfsignal
- Unbelegte Claims werden benannt
- Höchstens fünf Beispiele erscheinen
- Rollencodes gelten nur bei genauer Übereinstimmung
- Relationen brauchen beide Partner in einem Dokument
- Eine leere Antwort gilt als ungeprüft
- Sieben Claim-Arten stehen fest
----------------------------------------
<!-- funktion -->
Antwort bewerten
<!-- ziel -->
Meldet falsche Zuordnung oder Quellenfehler zurück
<!-- regeln -->
- Zwei Werte: zustimmend und ablehnend
- Ablehnung erlaubt einen Kommentar
- Kommentar wird auf 2000 Zeichen gekürzt
- Bewertung braucht eine fertige Antwort
- Eine fremde Antwort wird abgewiesen
- Die Oberfläche sperrt nach dem Speichern
- Ein erneuter Aufruf überschreibt die Bewertung
- Frage und Antwort werden mitgespeichert
<!-- testen -->
- Frage stellen
- LMB auf eine Bewertung
- Bestätigung sichtbar
----------------------------------------
<!-- funktion -->
Prompt kopieren
<!-- ziel -->
Übernimmt die eigene Frage in die Zwischenablage
<!-- regeln -->
- Kopiert wird nur der Fragetext
- Erfolg wird kurz gemeldet
<!-- testen -->
- Frage stellen
- LMB auf Kopieren
- Rückmeldung an der Schaltfläche

View File

@@ -0,0 +1,84 @@
<!-- titel -->
Diagnose
----------------------------------------
<!-- art -->
optional
----------------------------------------
<!-- ziel -->
Untersucht einzelne Teile des Antwortwegs
----------------------------------------
<!-- regeln -->
- Diagnosen ändern Antwortgüte und Wahrheitssicherung nicht
- Prompts und Rohantworten werden nie ausgegeben
- Betriebsfunktionen beeinflussen die Antwort nicht
----------------------------------------
<!-- funktion -->
Im Terminal fragen
<!-- ziel -->
Stellt Fragen an das System ohne Oberfläche
<!-- regeln -->
- Die Antwort läuft in Stücken ein
- Statusmeldungen stehen dazwischen
- Der Lauf löst echte Modellaufrufe aus
<!-- testen -->
- Kommando aufrufen
- Frage eintragen
- Antwort läuft ein
----------------------------------------
<!-- funktion -->
Suchweg testen
<!-- ziel -->
Prüft den hybriden Suchweg zu einer Frage
<!-- regeln -->
- Leere Frage wird abgewiesen
- Die Ausgabe nennt Dauer und Trefferzahl
<!-- testen -->
- Kommando mit Frage aufrufen
- Trefferliste sichtbar
----------------------------------------
<!-- funktion -->
Faktenabdeckung prüfen
<!-- ziel -->
Prüft, wie gut Fakten eine Frage abdecken
<!-- regeln -->
- Eine leere Frage wird abgewiesen
- Eine Mindestgüte ist wählbar
<!-- testen -->
- Kommando mit Frage aufrufen
- Abdeckung mit Zusammenfassung sichtbar
----------------------------------------
<!-- funktion -->
Ankerauflösung testen
<!-- ziel -->
Prüft die Auflösung einer Folgefrage trocken
<!-- regeln -->
- Kein Eingriff in die Laufzeit
- Mehrdeutige Anker werden verworfen
- Eine genannte Artikelnummer schlägt jeden Anker
<!-- testen -->
- Kommando mit Folgefrage aufrufen
- Auflösungsbericht sichtbar
----------------------------------------
<!-- funktion -->
Verlaufsanker auslesen
<!-- ziel -->
Zieht Bezugsanker aus einem Gesprächsverlauf
<!-- regeln -->
- Anker belegen keine Fachwerte
- Eingabe per Argument, Datei oder Strom
- Ein eingebauter Rauchtest ist wählbar
<!-- testen -->
- Kommando mit Rauchtest aufrufen
- Ankerliste sichtbar
----------------------------------------
<!-- funktion -->
Fakten inspizieren
<!-- ziel -->
Untersucht gespeicherte Fakten der Chunkdatei
<!-- regeln -->
- Eine fehlende Chunkdatei meldet einen Fehler
- Nur lesender Zugriff auf den Index
- Filter nach Text, Typ und Anzahl
<!-- testen -->
- Kommando mit Suchtext aufrufen
- Trefferliste sichtbar

View File

@@ -0,0 +1,107 @@
<!-- titel -->
Dokument
----------------------------------------
<!-- art -->
kern
----------------------------------------
<!-- ziel -->
Führt eine Wissensquelle über ihre Versionen
----------------------------------------
<!-- regeln -->
- Ein Dokument hält mehrere Versionen
- Genau eine Version ist aktiv
- Tags hängen am Dokument, nicht an der Version
- Schreiben an Dokument oder Version ändert den Index nicht
----------------------------------------
<!-- funktion -->
Dokumente filtern
<!-- ziel -->
Grenzt die Liste nach Zustand und Pflege ein
<!-- regeln -->
- Sechs Filter: Suche, Status, Index, Version, Titel, Tags
- Unbekannter Filterwert fällt auf alle
- Die Suche trifft Titel, Kennung und Dateiname
- Kennzahlen zählen immer den Gesamtbestand
<!-- testen -->
- Dokumentliste öffnen
- Filter setzen
- LMB auf Filtern
- Trefferzahl sichtbar
----------------------------------------
<!-- funktion -->
Dokument hochladen
<!-- ziel -->
Legt eine Wissensquelle mit erster Version an
<!-- regeln -->
- Die Datei ist Pflicht
- Ohne Titel dient der Dateiname als Titel
- Ein schwacher Titel bekommt einen Prüfhinweis
- Version eins entsteht aktiv
- Je Version wird eine Prüfsumme gespeichert
- Der Index bleibt bis zum Neuaufbau unverändert
<!-- testen -->
- Dokumentliste öffnen
- LMB auf neues Dokument
- Datei wählen
- Speichern
- Dokumentseite sichtbar
----------------------------------------
<!-- funktion -->
Titel ändern
<!-- ziel -->
Ersetzt einen schwachen Titel fachlich
<!-- regeln -->
- Leerer Titel wird abgewiesen
- Höchstens 255 Zeichen
- Der Prüfhinweis entfällt danach
- Index und Antwortlogik bleiben unberührt
<!-- testen -->
- Dokumentseite öffnen
- Titel ändern
- Speichern
- Titel als geprüft markiert
----------------------------------------
<!-- funktion -->
Titel vorschlagen lassen
<!-- ziel -->
Erzeugt Titelkandidaten aus dem Dokumentanfang
<!-- regeln -->
- Der Ausschnitt endet bei 9000 Zeichen
- Höchstens sechs Vorschläge
- Höchstens 120 Zeichen je Vorschlag
- Zu kurze Kandidaten fallen weg
- Ein Klick füllt nur das Feld
- Dokument, Version und Index bleiben unberührt
<!-- testen -->
- Dokumentseite öffnen
- LMB auf Titel vorschlagen
- Kandidat wählen
- Titelfeld gefüllt
----------------------------------------
<!-- funktion -->
Indexbereitschaft ansehen
<!-- ziel -->
Zeigt, was den Index bisher nicht erreicht hat
<!-- regeln -->
- Offene Änderungen werden gezählt
- Gelöschte Dokumente im Index werden benannt
<!-- testen -->
- Dokumentliste öffnen
- Bereitschaftsblock lesen
- Zahl offener Änderungen sichtbar
----------------------------------------
<!-- funktion -->
Dokument löschen
<!-- ziel -->
Entfernt Dokument, Versionen und Dateien endgültig
<!-- regeln -->
- Nur die höchste Rolle darf löschen
- Ein laufender Neuaufbau verhindert das Löschen
- Versionsdateien werden mitgelöscht
- Ein Auftrag hält den Vorgang fest
- Der Index ändert sich beim nächsten Neuaufbau
<!-- testen -->
- Dokumentliste öffnen
- LMB auf Löschen
- Bestätigen
- Dokument verschwindet

View File

@@ -0,0 +1,77 @@
<!-- titel -->
Evalfall
----------------------------------------
<!-- art -->
optional
----------------------------------------
<!-- ziel -->
Beschreibt einen prüfbaren Fall für die Antwortgüte
----------------------------------------
<!-- regeln -->
- Neun Falltypen stehen im Adminbereich
- Ein zehnter Typ läuft nur im Terminal
- Eine Fallkennung ist projektweit eindeutig
- Pflichtfelder sind Kennung, Typ und Frage
- Fälle liegen als Zeilendatei je Typ und Bereich
- Schreiben erfolgt mit ausschließender Sperre
----------------------------------------
<!-- funktion -->
Bereich wählen
<!-- ziel -->
Schaltet zwischen Systembasis und Fachbereich
<!-- regeln -->
- Der Bereich prägt jede weitere Handlung
- Fachbereiche liegen in eigenen Ordnern
<!-- testen -->
- Menüeintrag Eval Suite wählen
- LMB auf einen Bereichsreiter
- Fälle des Bereichs sichtbar
----------------------------------------
<!-- funktion -->
Evalfall anlegen
<!-- regeln -->
- Doppelte Kennung wird abgewiesen
- Erwartung muss ein Objekt sein
- Verlauf muss eine Liste sein
- Ein Fehler verhindert jede Änderung
<!-- testen -->
- Menüeintrag Eval Suite wählen
- LMB auf neuer Test
- Formular ausfüllen
- Absenden
- Fall in der Datei
----------------------------------------
<!-- funktion -->
Evalfall geführt anlegen
<!-- ziel -->
Legt einen Fall über einen Assistenten an
<!-- regeln -->
- Eigener Entwurf mit eigener Fehlerprüfung
<!-- testen -->
- Menüeintrag Eval Suite wählen
- LMB auf Assistent
- Schritte ausfüllen
- Fall in der Datei
----------------------------------------
<!-- funktion -->
Evalfall ableiten
<!-- ziel -->
Übernimmt einen gescheiterten Lauf als Fall
<!-- regeln -->
- Kennung und Frage werden vorbelegt
<!-- testen -->
- Lauf mit Fehlschlag öffnen
- LMB auf Fall übernehmen
- Formular vorbefüllt
----------------------------------------
<!-- funktion -->
Evalfall löschen
<!-- regeln -->
- Eine defekte Zeile verhindert die Änderung
- Unbekannte Kennung meldet einen Fehler
- Eine Rückfrage geht voraus
<!-- testen -->
- Seite neuer Test öffnen
- LMB auf Löschen
- Bestätigen
- Fall verschwindet

View File

@@ -0,0 +1,100 @@
<!-- titel -->
Evallauf
----------------------------------------
<!-- art -->
optional
----------------------------------------
<!-- ziel -->
Prüft die Antwortpipeline gegen gespeicherte Fälle
----------------------------------------
<!-- regeln -->
- Ein gescheiterter Fall setzt den Gesamtstand
- Eine leere Auswahl gilt nicht als Fehlschlag
- Berichte entstehen unteilbar
- Evals messen Fälle, nicht jede mögliche Frage
- Fünf Falltypen rufen das Modell
- Vier Falltypen laufen ohne Modell
----------------------------------------
<!-- funktion -->
Evallauf starten
<!-- ziel -->
Führt Fälle aus dem Adminbereich aus
<!-- regeln -->
- Der Lauf braucht die Wissensrolle
- Der Bericht wird zusätzlich archiviert
- Der Lauf blockiert die Anfrage bis zum Ende
<!-- testen -->
- Menüeintrag Eval Suite wählen
- Auswahl treffen
- Lauf starten
- Ergebnis je Fall sichtbar
----------------------------------------
<!-- funktion -->
Einzelfall erneut testen
<!-- ziel -->
Wiederholt genau einen gescheiterten Fall
<!-- regeln -->
- Nur der gewählte Fall läuft
<!-- testen -->
- Lauf mit Fehlschlag öffnen
- LMB auf erneut testen
- Ergebnis des Falls sichtbar
----------------------------------------
<!-- funktion -->
Läufe vergleichen
<!-- ziel -->
Stellt zwei Berichte als Unterschied gegenüber
<!-- regeln -->
- Der Vergleich nennt behoben, neu und entfernt
<!-- testen -->
- Menüeintrag Eval Suite wählen
- Vergleichsbasis wählen
- Unterschied sichtbar
----------------------------------------
<!-- funktion -->
Evallauf im Terminal ausführen
<!-- regeln -->
- Die Auswahl folgt Typ und Bereich
- Ein Abbruch nach dem ersten Fehler ist wählbar
- Ein Lauf ohne Schreiben ist wählbar
- Fälle lassen sich nur auflisten
<!-- testen -->
- Kommando mit Typ aufrufen
- Ergebnis je Fall sichtbar
----------------------------------------
<!-- funktion -->
Evallauf parallelisieren
<!-- ziel -->
Verteilt Fälle auf mehrere Kindprozesse
<!-- regeln -->
- Zwischen einem und 32 Prozessen
- Vorgabe sind zwei Prozesse
- Ein Zeitlimit je Prozess ist wählbar
- Das Ergebnis ist ein Sammelbericht
<!-- testen -->
- Kommando mit Prozesszahl aufrufen
- Sammelbericht sichtbar
----------------------------------------
<!-- funktion -->
Berichte aufräumen
<!-- regeln -->
- Nur erzeugte Dateinamen fallen weg
- Aufräumen behält 20 Läufe je Bereich
- Die Liste zeigt zehn Läufe
<!-- testen -->
- Menüeintrag Eval Suite wählen
- LMB auf Aufräumen
- Berichtszahl sinkt
----------------------------------------
<!-- funktion -->
Berichte löschen
<!-- ziel -->
Entfernt alle Läufe eines Bereichs
<!-- regeln -->
- Der aktuelle Lauf fällt mit weg
- Eine Rückfrage geht voraus
<!-- testen -->
- Menüeintrag Eval Suite wählen
- LMB auf alle löschen
- Bestätigen
- Berichtsliste leer

View File

@@ -0,0 +1,138 @@
<!-- titel -->
Fremdinhalt
----------------------------------------
<!-- art -->
optional
----------------------------------------
<!-- ziel -->
Prüft externe Inhalte vor jeder Nutzung
----------------------------------------
<!-- regeln -->
- Geprüft wird der ausgewählte Inhalt
- Ein Lesen führt kein Werkzeug aus
- Ein Lesen ändert die Antwortpipeline nicht
- Vorschauen sind gekürzt und bereinigt
- Externe Inhalte heben ihren Rang nicht selbst
- Der Bereich braucht die höchste Rolle
- Keine Übernahme ins Standardwissen erfolgt automatisch
----------------------------------------
<!-- funktion -->
Inhalt live prüfen
<!-- ziel -->
Liest den ausgewählten Inhalt einmalig online
<!-- regeln -->
- Nur der gewählte Inhalt wird gelesen
- Ohne Zutun wird nichts gespeichert
<!-- testen -->
- Übersicht öffnen
- LMB auf Inhalt prüfen
- Bestätigen
- Vorschau mit Abrufzahl
----------------------------------------
<!-- funktion -->
Inhalt bereitstellen
<!-- ziel -->
Merkt einen gelesenen Inhalt als Kandidat vor
<!-- regeln -->
- Chat und Antwortnutzung bleiben aus
- Der Kandidat startet ohne Freigabe
- Eine Rückfrage geht voraus
<!-- testen -->
- Inhalt live prüfen
- LMB auf bereitstellen
- Bestätigen
- Kandidat in der Liste
----------------------------------------
<!-- funktion -->
Inhalt auswählen
<!-- ziel -->
Ordnet einen Inhalt einem Raum zur Prüfung zu
<!-- regeln -->
- Zwei Arten: eigener Raum und Ergänzung
- Die Auswahl gilt schon als geprüft
- Die Auswahl schaltet den Chat nicht ein
- Standardwissen wird dadurch nicht ergänzt
<!-- testen -->
- Räume überblicken
- LMB auf Inhalt auswählen
- Bestätigen
- Inhalt im Freigabebereich
----------------------------------------
<!-- funktion -->
Inhalt entscheiden
<!-- ziel -->
Gibt einen geprüften Inhalt frei oder stellt zurück
<!-- regeln -->
- Fünf Wege bis zum Zurücksetzen
- Eine unbekannte Entscheidung wird abgewiesen
- Eine Freigabe schaltet den Chat nicht ein
- Die Notiz endet bei 1000 Zeichen
<!-- testen -->
- Freigabebereich öffnen
- LMB auf Inhalt ansehen
- Entscheidung wählen
- Zustandsmarke wechselt
----------------------------------------
<!-- funktion -->
Inhalt im Chat einschalten
<!-- ziel -->
Fügt einen freigegebenen Inhalt dem Raum zu
<!-- regeln -->
- Nur geprüfte Inhalte eines eigenen Raums
- Nicht vorbereitete Inhalte werden abgewiesen
- Suche und Belege bleiben unberührt
- Es entsteht kein gespeichertes Wissen
<!-- testen -->
- Freigabebereich öffnen
- LMB auf im Chat einschalten
- Inhalt als aktiv markiert
----------------------------------------
<!-- funktion -->
Kandidaten filtern
<!-- ziel -->
Grenzt vorbereitete Inhalte nach Zustand ein
<!-- regeln -->
- Filter nach Zustand, Anbindung, Art und Zeit
- Das Filtern ruft nichts extern ab
<!-- testen -->
- Vorbereitete Inhalte öffnen
- Filter setzen
- Trefferliste eingegrenzt
----------------------------------------
<!-- funktion -->
Kandidat entfernen
<!-- ziel -->
Löscht einen vorbereiteten Inhalt endgültig
<!-- regeln -->
- Die Vorschau fällt mit weg
<!-- testen -->
- Vorbereitete Inhalte öffnen
- LMB auf Entfernen
- Kandidat verschwindet
----------------------------------------
<!-- funktion -->
Kandidaten ausgeben
<!-- ziel -->
Lädt vorbereitete Inhalte als Datei herunter
<!-- regeln -->
- Zwei Formate: Markdown und JSONL
- Der Export gibt die gefilterte Menge aus
- Der Export wirkt nicht als Freigabe
<!-- testen -->
- Vorbereitete Inhalte öffnen
- Format wählen
- Herunterladen
- Datei im Download
----------------------------------------
<!-- funktion -->
Ressource lesen
<!-- ziel -->
Liest eine freigegebene Ressource im Terminal
<!-- regeln -->
- Nur erlaubte Server und Adressen
- Der Inhalt wird bereinigt
- Ausgegeben werden nur Metadaten und Prüfsumme
- Eine Kürzung wird gekennzeichnet
<!-- testen -->
- Kommando mit Adresse aufrufen
- Metadaten und Prüfsumme sichtbar

View File

@@ -0,0 +1,64 @@
<!-- titel -->
Indexprofil
----------------------------------------
<!-- art -->
optional
----------------------------------------
<!-- ziel -->
Legt Struktur für Chunking und Einbettung fest
----------------------------------------
<!-- regeln -->
- Ein Profil ist unveränderlich
- Genau ein Profil ist aktiv
- Einsehen braucht die Wissensrolle
- Ändern braucht die höchste Rolle
- Die Version zählt beim Anlegen hoch
- Ein neues Profil entsteht inaktiv
----------------------------------------
<!-- funktion -->
Profile prüfen
<!-- ziel -->
Gleicht das aktive Profil gegen den Index ab
<!-- regeln -->
- Der Vergleich läuft gegen die Indexmetadaten
- Eine abweichende Zeile setzt den Gesamtstand
- Fehlende Metadaten gelten als Strukturabweichung
- Die Ansicht startet keinen Neuaufbau
<!-- testen -->
- Menüeintrag Indexprofile wählen
- Abweichungen je Zeile sichtbar
----------------------------------------
<!-- funktion -->
Profil anlegen
<!-- regeln -->
- Chunkgröße von 50 bis 1000 in Schritten von 25
- Überlappung von 25 bis 200 in Schritten von 25
- Das Einbettungsmodell ist Pflicht
- Fehlende Pflichtwerte nennen das Feld
- Nur die höchste Rolle darf anlegen
<!-- testen -->
- Indexprofile öffnen
- Werte eintragen
- Profil erstellen
- Inaktives Profil sichtbar
----------------------------------------
<!-- funktion -->
Profil aktivieren
<!-- regeln -->
- Das bisherige Profil wird deaktiviert
- Die Aktivierung startet keinen Neuaufbau
<!-- testen -->
- Indexprofile öffnen
- LMB auf Aktivieren
- Bestätigen
- Profil als aktiv markiert
----------------------------------------
<!-- funktion -->
Profil löschen
<!-- regeln -->
- Der Löschweg erscheint nur bei inaktiven Profilen
<!-- testen -->
- Indexprofile öffnen
- LMB auf Löschen
- Bestätigen
- Profil verschwindet

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