Automatischer Push: 45 Datei(en) geändert
- .claude/agents/planer.md - .claude/agents/quorum-stimme.md - .claude/skills/bookstack-upload/SKILL.md - .claude/skills/bookstack-upload/scripts/bookstack/archiv.py - .claude/skills/bookstack-upload/scripts/bookstack/auswahl.py - .claude/skills/bookstack-upload/scripts/bookstack/beschreibung.py - .claude/skills/bookstack-upload/scripts/bookstack/bilder.py - .claude/skills/bookstack-upload/scripts/bookstack/ergebnis.py - .claude/skills/bookstack-upload/scripts/bookstack/hochladen.py - .claude/skills/bookstack-upload/scripts/bookstack/konvertierung.py - .claude/skills/bookstack-upload/scripts/bookstack/struktur.py - .claude/skills/bookstack-upload/scripts/bookstack/verweise.py - … und 33 weitere Erzeugt vom PostToolUse-Hook .vault-tools/auto-push/auto_push.py. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
1 parent
e8472409e9
commit
eb48f8822d
45 files changed
+8645
-262
No files matched your search
@@ -5,7 +5,7 @@ status: draft
|
||||
summary: "Nach dem Plan prüft ein neuer Workflow quorum vor Einspielen, neuer Version auf master und Abgabe eines Kundendokuments denselben Unterschied seit dem letzten Quorum mit Opus, Sonnet und Codex (Ersatz Haiku), zählt Mehrheiten, lässt Einzelbefunde von Sonnet nachprüfen und liefert Urteil und Bericht."
|
||||
tags: [plan, vault-tools, codeworkflow]
|
||||
created: 2026-09-29
|
||||
updated: 2026-09-29
|
||||
updated: 2026-09-30
|
||||
---
|
||||
|
||||
# Quorum am Meilenstein
|
||||
@@ -15,7 +15,7 @@ updated: 2026-09-29
|
||||
Die Anforderung ist die Spec `Inbox/2026-09-29-spec-quorum.md`; dieser Plan zitiert sie nicht, er setzt sie um. Nach dem Plan gibt es:
|
||||
|
||||
- **`.claude/workflows/quorum.js`:** baut über einen Hilfsagenten ein Prüfpaket, startet Opus und Sonnet als `quorum-stimme` und Codex über das Plugin gleichzeitig, setzt Haiku ein, wenn Codex keine gültige Stimme abgibt, lässt einen Sonnet-Agenten gleiche Befunde gruppieren, zählt die Stimmen, lässt Einzelbefunde der Schwere blockierend und wichtig nachprüfen und liefert Urteil, Bericht und Berichtspfad.
|
||||
- **`.vault-tools/quorum/quorum_werkzeug.py`:** tut für den Workflow, was nur mit Dateisystem und Shell geht: Paket bauen, leeren Unterschied erkennen, Basis und Berichtsnummer bestimmen, das Ergebnis eines Codex-Jobs auswerten. Mit eigenen Fällen und Mutanten.
|
||||
- **`.vault-tools/quorum/quorum_werkzeug.py`:** tut für den Workflow, was nur mit Dateisystem und Shell geht: Paket bauen, leeren Unterschied erkennen, Basis und Berichtsnummer bestimmen, das Ergebnis eines Codex-Jobs auswerten und den Bericht ablegen (`ablegen`, setzt `stand` aus `paket.json` ein). Mit eigenen Fällen und Mutanten.
|
||||
- **`.claude/agents/quorum-stimme.md`:** die nur lesende Stimme mit drei Auftragsarten (Stimme, Nachprüfung, Nachkontrolle), dazu eine Zeile in der Rollentabelle des Stamms.
|
||||
- **`AI/Regelwerk/codeworkflow.md`:** der Abschnitt „Quorum am Meilenstein“ als der eine Ort der Regel, Schritt 6 verweist darauf.
|
||||
- **`.vault-tools/workflow-pruefung/`:** Grundlinie, Fälle und Mutanten für `quorum.js` wie für die übrigen Workflows.
|
||||
@@ -33,7 +33,7 @@ Die Anforderung ist die Spec `Inbox/2026-09-29-spec-quorum.md`; dieser Plan ziti
|
||||
```text
|
||||
quorum.js (Args: anlass, projekt | ordner+dateien+uebersicht, basis?, kontext?, suite? | suite_datei?, codex?, codex_runden?)
|
||||
├─ Paket werkzeug:paket (sonnet, low) ── python quorum_werkzeug.py paket ... --ziel $T
|
||||
│ → QUORUM_PAKET {ordner, leer, basis, stand, dateien, lesemarke, bericht_pfad, nummer, vorher, ...}
|
||||
│ → QUORUM_PAKET {ordner, leer, basis, stand, anzahl_dateien, lesemarke, bericht_pfad, nummer, vorher, paket_summe, ...}; Listen nur in paket.json
|
||||
│ leer → Urteil des letzten Berichts übernehmen, keine Stimme, kein verbrauch
|
||||
├─ Stimmen verbrauch.js (wunsch 1) → gebremst: kein Urteil
|
||||
│ gleichzeitig: stimme:opus (quorum-stimme, opus)
|
||||
@@ -60,14 +60,14 @@ Nachgelesen im Quelltext des Plugins `openai-codex/codex/1.0.6` (unter `~/.claud
|
||||
| Rein lesende Prüfung eines Git-Diffs | `review` fährt den eingebauten Reviewer mit Sandbox `read-only` (`scripts/lib/codex.mjs` 1010-1013), kennt aber nur Arbeitsbaum oder Branch als Ziel (cc 259-269) und nimmt keinen eigenen Auftragstext (cc 271-276). `adversarial-review --base <ref>` fährt `read-only` (cc 414) mit festem Ausgabeschema (cc 415, `schemas/review-output.schema.json`, `additionalProperties: false`, kein Feld für einen Lesebeleg). Beide verlangen Git (cc 360). |
|
||||
| Dokumente ohne Git | nur `task`: ohne `--write` Sandbox `read-only` (cc 491), Auftrag aus `--prompt-file` (cc 643-645), kein `ensureGitRepository` in `executeTaskRun` (cc 461-495). Die Wurzel ist das Git-Repo oder, ohne Git, das Arbeitsverzeichnis (`scripts/lib/workspace.mjs` 3-9). |
|
||||
| Warten über zehn Minuten | `task --background` startet einen abgekoppelten Prozess (cc 671-682, 788-805) und gibt `jobId` und `logFile` zurück (cc 700-707); `status <job> --wait --timeout-ms` wartet höchstens so lange (cc 318-334, 891-897) und meldet `waitTimedOut`. `result <job> --json` liefert Job und gespeichertes Ergebnis, nur für beendete Jobs (cc 910-926, `scripts/lib/job-control.mjs` 256-263). |
|
||||
| Kontingentabbruch | Ein Fehler des Codex-Servers geht als Zeile `Codex error: <Meldung>` ins Log (`scripts/lib/codex.mjs` 537-540, geschrieben über `scripts/lib/tracked-jobs.mjs` 122-131); die Schlussausgabe steht ebenfalls im Log (`tracked-jobs.mjs` 179). Exit 0 gibt es nur bei Turn-Status `completed` (`codex.mjs` 754-756, cc 665-667). Die Meldung selbst („usage limit“) ist aus dem Vorfall vom 2026-09-05 bekannt (Harness-Gedächtnis `codex-kontingent-begrenzt`), ihre Form im Log ist nicht gemessen. |
|
||||
| Kontingentabbruch | Ein Fehler des Codex-Servers geht als Zeile `Codex error: <Meldung>` ins Log (`scripts/lib/codex.mjs` 537-540, geschrieben über `scripts/lib/tracked-jobs.mjs` 122-131); die Schlussausgabe steht ebenfalls im Log (`tracked-jobs.mjs` 179). Exit 0 gibt es nur bei Turn-Status `completed` (`codex.mjs` 754-756, cc 665-667). Die Form im Log ist belegt: Die Logs vom 2026-09-05 im Plugin-Datenordner zeigen `[<Zeitstempel>] Codex error: You've hit your usage limit. …` (Plankritik 2026-09-30). Gezählt wird deshalb nur eine Zeile dieser Form, nicht jedes Vorkommen im Log, denn dort stehen auch die Antwort von Codex und jede ausgeführte Befehlszeile. |
|
||||
| Status und Logs | Zustand unter `$CLAUDE_PLUGIN_DATA/state/<ordnername>-<hash>/`, ohne die Variable im System-Temp unter `codex-companion/` (`scripts/lib/state.mjs` 29-44); darin `state.json` und `jobs/<job>.json` und `jobs/<job>.log` (`state.mjs` 183-191). `status` ohne Job-Kennung zeigt nur Jobs der eigenen Sitzungs-ID (`job-control.mjs` 15-25), mit Kennung alle (242-254). `status` startet keinen Codex-Lauf (cc 883-908, `job-control.mjs` 213-240, `codex.mjs` 906-923). |
|
||||
|
||||
**Entscheidung:** `task` ohne `--write`, im Hintergrund, für Code und Dokumente gleich. Nur `task` erreicht Dokumente ohne Git und erlaubt einen eigenen Auftrag mit Antwortformat und Lesebeleg; alle drei Stimmen lesen dann dasselbe Paket. Das Warten übernehmen kurze Hilfsagenten mit `status --wait`, weil das Bash-Werkzeug eines Agenten nach zehn Minuten endet.
|
||||
|
||||
**Wie der Workflow Codex erreicht:** über Hilfsagenten ohne `agentType` auf **`sonnet` mit `effort: 'low'`**, die je genau einen im Workflow zusammengesetzten Befehl fahren und die Ausgabe übertragen. Das Urteil über die Gültigkeit rechnet der Workflow aus den übertragenen Feldern, nicht der Agent. `low`, weil mechanische Arbeit auf `low` gemessen gleich treu war wie auf `high` (codeworkflow.md, „Effort je Rolle“). Nicht Haiku, weil `codex:ende` die Befundtexte von Codex überträgt und Haiku typografische Zeichen still ersetzt (ebenda, „Haiku und typografische Zeichen“).
|
||||
|
||||
**Gültige Codex-Stimme** (Spec, Abschnitt Codex): `status` vor dem Lauf mit Exit 0; Job endet `completed`; Log gelesen und null Treffer für „usage limit“; Antwort im vereinbarten Block; der Lesebeleg zitiert die Zeile `Lesemarke: <16 Hex>` aus `paket.md`. Die Lesemarke steht nicht im Codex-Auftrag, also kann sie nur zitieren, wer die Datei gelesen hat.
|
||||
**Gültige Codex-Stimme** (Spec, Abschnitt Codex): `status` vor dem Lauf mit Exit 0; Job endet `completed`; Log gelesen und keine Zeile der Form `[<Zeitstempel>] Codex error: …usage limit…`; Antwort im vereinbarten Block; der Lesebeleg zitiert die Zeile `Lesemarke: <16 Hex>` aus `paket.md`. Die Lesemarke steht nicht im Codex-Auftrag, also kann sie nur zitieren, wer die Datei gelesen hat.
|
||||
|
||||
## Entscheidungsliste (pruefwerkzeuge.md, Abschnitt 1)
|
||||
|
||||
@@ -79,7 +79,7 @@ Nachgelesen im Quelltext des Plugins `openai-codex/codex/1.0.6` (unter `~/.claud
|
||||
| Wann ist der Unterschied leer? | Spec | Code: kein Unterschied außerhalb von `docs/quorum/`. Dokumente: keine Datei neu, geändert oder entfernt nach SHA-256. |
|
||||
| Was gilt bei leerem Unterschied? | Spec | Das Urteil des letzten Berichts, ohne Stimme und ohne `verbrauch`. |
|
||||
| Wann zählt ein Befund für die Mehrheit? | Spec | Mit nicht leerem Beleg (nach `trim`). |
|
||||
| Wann sind Befunde gleich? | Spec, Projektvorgabe | Der Sonnet-Agent gruppiert; das Skript löst jede Gruppe über verschiedene Dateien auf. Mehrheit heißt zwei verschiedene Stimmen. |
|
||||
| Wann sind Befunde gleich? | Spec, Projektvorgabe (Nachbesserung 2026-09-30) | Der Sonnet-Agent gruppiert; das Skript teilt jede Gruppe in Teilgruppen nach Datei und, wo die Stellen Zeilennummern tragen, nach Zeilenbereichen, die sich mit einer Toleranz von drei Zeilen überschneiden (transitive Hülle). Eine Stelle ohne Zeilennummer bleibt bei ihrer Gruppe, solange die Zeilen dieser Datei genau eine Teilgruppe bilden. Mehrheit heißt zwei verschiedene Stimmen in einer Teilgruppe. |
|
||||
| Schwere einer Mehrheitsgruppe? | Annahme (offen) | Die höchste ihrer Mitglieder. |
|
||||
| Wer wird nachgeprüft? | Spec | Gruppen ohne Mehrheit mit Schwere blockierend oder wichtig. Hinweise gehen ungeprüft in den Bericht. |
|
||||
| Urteil frei? | Spec, verschärft (offen) | Kein bestätigter blockierender Befund und kein blockierender, der ungeprüft blieb. |
|
||||
@@ -103,8 +103,9 @@ Nachgelesen im Quelltext des Plugins `openai-codex/codex/1.0.6` (unter `~/.claud
|
||||
|---|---|---|---|---|
|
||||
| 01 | `.vault-tools/quorum/quorum_werkzeug.py`, `faelle_quorum_werkzeug.py`, `mutanten_quorum_werkzeug.py` | neu | haiku | 1 |
|
||||
| 02 | `.claude/workflows/quorum.js` | neu | haiku | 1 |
|
||||
| 03 | `.vault-tools/workflow-pruefung/faelle_workflows.mjs`, `mutanten_workflows.py` | geändert | haiku | 2 |
|
||||
| 03 | `.vault-tools/workflow-pruefung/faelle_workflows.mjs`, `mutanten_workflows.py` | geändert | sonnet, low | 3 |
|
||||
| 04 | `.claude/agents/quorum-stimme.md` (neu), `CLAUDE.md`, `AI/Regelwerk/codeworkflow.md` (geändert) | neu und geändert | sonnet, low | 1 |
|
||||
| 05 | `.vault-tools/quorum/quorum_werkzeug.py`, `faelle_quorum_werkzeug.py`, `mutanten_quorum_werkzeug.py`, `.claude/workflows/quorum.js` | Nachbesserung nach der Prüfung von Welle 1 (2026-09-30): Teilgruppen nach Datei und Zeilen mit Toleranz drei, `GIT_OPTIONAL_LOCKS=0`, `--summe` in `ablegen`; Delta gegen den Baum | sonnet, low | 2 |
|
||||
|
||||
Die Zieldateien sind disjunkt. **Welle 1** (Task 01, 02, 04) läuft parallel. **Welle 2** (Task 03) folgt, weil ihre Fälle und Mutanten gegen `quorum.js` aus Task 02 laufen.
|
||||
|
||||
@@ -113,7 +114,7 @@ Die Zieldateien sind disjunkt. **Welle 1** (Task 01, 02, 04) läuft parallel. **
|
||||
- **M1, kostenlos, vor Welle 1** (Sonnet-Executer oder Koordinator, im Bash-Werkzeug eines Subagenten, denn so ruft der Workflow das Plugin):
|
||||
`node "$HOME/.claude/plugins/cache/openai-codex/codex/1.0.6/scripts/codex-companion.mjs" status --json --cwd D:/Claude; echo "EXIT $?"; [ -n "$CLAUDE_PLUGIN_DATA" ] && echo "CLAUDE_PLUGIN_DATA gesetzt" || echo "CLAUDE_PLUGIN_DATA leer"`
|
||||
Erwartet: Exit 0 und ein JSON mit `workspaceRoot`. Die Zeile zu `CLAUDE_PLUGIN_DATA` sagt, ob Jobs aus dem Workflow im Plugin-Datenordner oder im System-Temp landen. `status` startet keinen Codex-Lauf (Belege oben).
|
||||
- **M2, kostet Codex-Kontingent, im Lauf mit echten Daten:** Überlebt der Hintergrundjob das Ende des Bash-Aufrufs von `codex:start`? Wie lange braucht Codex für ein Paket? Wie sieht die Log-Zeile bei „usage limit“ aus, falls sie auftritt?
|
||||
- **M2, kostet Codex-Kontingent, im Lauf mit echten Daten:** Überlebt der Hintergrundjob das Ende des Bash-Aufrufs von `codex:start`? Wie lange braucht Codex für ein Paket? (Die Form der Log-Zeile bei „usage limit“ ist aus den Logs vom 2026-09-05 belegt.)
|
||||
- **M3, im Lauf mit echten Daten:** Verbrauch je Lauf (Ablesungen vorher und am Ende stehen im Bericht).
|
||||
|
||||
## Fertig heißt
|
||||
@@ -127,7 +128,7 @@ Die Zieldateien sind disjunkt. **Welle 1** (Task 01, 02, 04) läuft parallel. **
|
||||
7. `python .vault-tools/regelwerk-check/regelwerk_check.py --vault .`: keine Befundzeile, die `quorum-stimme` oder `Rollentabelle` enthält, und nicht mehr Befunde als vor Task 04.
|
||||
8. Byte-Vergleich: `welle_vergleich.py` je Welle gegen die Kopie mit angewendetem Plan, dazu `plan_bloecke.py nach` für Task 03 und 04 ohne Befund.
|
||||
9. M1 wie oben: Exit 0 und JSON.
|
||||
10. Lauf mit echten Daten wie unten: `status: fertig` mit Bericht, danach ein Leerlauf mit `status: leer`.
|
||||
10. Lauf mit echten Daten wie unten: `status: fertig` mit Bericht, und die eingebaute Macke steht darin als Mehrheit oder als bestätigter Einzelbefund; danach ein Leerlauf mit `status: leer`.
|
||||
|
||||
## Lauf mit echten Daten
|
||||
|
||||
@@ -136,7 +137,7 @@ Nach Welle 2, in einer frischen Sitzung oder über `scriptPath` (ein neuer Workf
|
||||
1. Ein Probe-Repo im Scratchpad: zwei Commits, im zweiten eine eingebaute Macke mit klarer Schwere (etwa eine vertauschte Bedingung in einer kleinen Funktion, dazu ein Test, der sie nicht fängt, und ein Suite-Ergebnis in einer Datei).
|
||||
2. `workflow({ scriptPath: "D:/Claude/.claude/workflows/quorum.js" }, { anlass: "einspielen", projekt: "<probe>", basis: "<erster commit>", suite_datei: "<datei>" })`.
|
||||
3. Auffallen müsste: Die Macke steht als Mehrheit oder als bestätigter Einzelbefund im Bericht. Codex ist gültig oder der Grund steht im Bericht (M2). Die Lesemarken der Claude-Stimmen passen (sonst Hinweis im Bericht). Verbrauch vorher und am Ende stehen drin (M3). Das Journal zeigt Opus, Sonnet und `codex:start` gleichzeitig.
|
||||
4. `bericht_md` unter `bericht_pfad` ablegen, dann denselben Aufruf ohne `basis` noch einmal: `status: leer`, das Urteil ist übernommen, genau ein Agent (`werkzeug:paket`) lief.
|
||||
4. `bericht_md` als `bericht.md` in den Paketordner schreiben und den `ablegen_befehl` aus dem Ergebnis fahren, dann denselben Aufruf ohne `basis` noch einmal: `status: leer`, das Urteil ist übernommen, genau ein Agent (`werkzeug:paket`) lief.
|
||||
5. Das Temp-Paket (`paket` im Ergebnis) danach löschen.
|
||||
|
||||
Der erste echte Meilenstein eines Projekts ist der zweite Lauf mit echten Daten; die Auswertung nach fünf Quoren steht in der Regel.
|
||||
@@ -150,11 +151,11 @@ Der erste echte Meilenstein eines Projekts ist der zweite Lauf mit echten Daten;
|
||||
- **Laufzeit je Claude-Stimme** aus dem Workflow-Journal, nach dem Muster von `verbrauch_reihe.py` (Labels `stimme:*`, Zeitstempel aus `agent-<id>.jsonl`), als kleines Skript unter `.vault-tools/quorum/`.
|
||||
- **Auswertung nach fünf Quoren:** eine Tabelle über alle Berichte eines Projekts (Stimme, Befunde, bestätigt, Alleinfunde, wie oft Haiku statt Codex), als Grundlage für den Dump.
|
||||
- **`LIESMICH.md`** für `.vault-tools/quorum/` mit den Belegen der ersten Läufe.
|
||||
- Falls Pascal es will: ein Workflow-Modus für die Nachkontrolle behobener Befunde statt des Agent-Werkzeugs, und die Stamm-Zeile für Kundendokumente (Offene Punkte 3 und 9).
|
||||
- Falls Pascal es will: ein Workflow-Modus für die Nachkontrolle behobener Befunde statt des Agent-Werkzeugs (Offener Punkt 3).
|
||||
|
||||
## Risiken
|
||||
|
||||
- **Fremde Änderungen im Baum.** `CLAUDE.md` und `AI/Regelwerk/codeworkflow.md` tragen am 2026-09-29 nicht committete Änderungen einer anderen Sitzung (Sonnet 5.5). Die Aus-Blöcke von Task 04 zitieren diesen Stand. Vor Welle 1 `plan_bloecke.py vor` fahren; bei einem Befund den Plan nachziehen, nicht den Baum.
|
||||
- **Parallele Sitzungen im Baum.** `CLAUDE.md` und `AI/Regelwerk/codeworkflow.md` sind seit dem Auto-Push vom 2026-09-30, 04:00, committet, `plan_bloecke.py vor` war danach ohne Befund. Andere Sitzungen können beide Dateien weiter ändern. Vor Welle 1 deshalb erneut `plan_bloecke.py vor` fahren; bei einem Befund den Plan nachziehen, nicht den Baum. `CLAUDE.md` liegt im Arbeitsbaum mit CRLF, `plan_materialisiere.py` schreibt sie ganz als LF zurück; das entspricht `.gitattributes` (`eol=lf`).
|
||||
- **Hintergrundjob stirbt** mit dem Bash-Aufruf von `codex:start` (M2). Dann endet `status --wait` mit einem Job, der nie fertig wird, oder mit `failed`; die Codex-Stimme ist ungültig, Haiku springt ein, und der Bericht nennt den Grund.
|
||||
- **Codex braucht länger** als `codex_runden` mal neun Minuten. Dann bricht der Workflow den Job ab und setzt Haiku ein.
|
||||
- **Falsche Gruppen** erzeugen eine falsche Mehrheit. Dagegen stehen die Originaltexte je Gruppe im Bericht; gemessen ist die Fehlerquote nicht.
|
||||
@@ -164,15 +165,19 @@ Der erste echte Meilenstein eines Projekts ist der zweite Lauf mit echten Daten;
|
||||
|
||||
## Offene Punkte für Pascal
|
||||
|
||||
**Entschieden am 2026-09-30:** Pascal hat die Spec abgenommen und alle Vorschläge dieser Liste angenommen. Punkt 9 ist als Block in Task 04 umgesetzt (Zeile im Regelwerk-Verzeichnis des Stamms), aus Paket 2 entfällt er damit.
|
||||
|
||||
1. **„Prüfpaket bauen ohne Agent“ geht nicht wörtlich.** Ein Workflow-Skript hat weder Dateisystem noch Shell; das Laufzeitsystem reicht nur `agent`, `parallel`, `pipeline`, `phase`, `log`, `workflow`, `args` und `budget` hinein (nachgebildet in `attrappe.mjs` 45). Deshalb fährt ein Hilfsagent (Sonnet, `low`) genau einen Skriptbefehl. Bei leerem Unterschied läuft also genau dieser Hilfsagent, keine Stimme.
|
||||
2. **Den Bericht schreibt der Koordinator**, nicht der Workflow: Der liefert `bericht_md` und `bericht_pfad`, der Koordinator legt ab, verlinkt und committet. Ein Hilfsagent, der den Text abschreibt, wäre eine Fehlerquelle mehr.
|
||||
2. **Den Bericht schreibt der Koordinator**, nicht der Workflow: Der liefert `bericht_md`, `bericht_pfad` und `ablegen_befehl`, der Koordinator legt über `quorum_werkzeug.py ablegen` ab und committet; den Stand setzt das Skript aus `paket.json` ein (Plankritik 2026-09-30, Q8). Ein Hilfsagent, der den Text abschreibt, wäre eine Fehlerquelle mehr.
|
||||
|
||||
**Seit der Nachbesserung vom 2026-09-30:** Ein Kundendokument muss unter `D:/Claude/` liegen, weil der Bericht die Übersicht per Wikilink mit vollem Vault-Pfad verlinkt; `pruefeArgs` lehnt einen Ordner außerhalb ab.
|
||||
3. **Nachkontrolle behobener Befunde** läuft über das Agent-Werkzeug (`quorum-stimme` auf Sonnet, Auftragsart Nachkontrolle), nicht als Workflow-Modus. Reicht das?
|
||||
4. **Laufzeit je Claude-Stimme** steht im Bericht als „nicht gemessen“, bis Paket 2 sie aus dem Journal nachträgt. Codex-Laufzeit steht drin.
|
||||
5. **Schwere einer Mehrheitsgruppe** bei abweichender Einstufung: Der Plan nimmt die höchste.
|
||||
6. **Ungeprüft blockierend sperrt:** Liefert die Nachprüfung kein Urteil, sperrt ein blockierender Einzelbefund wie ein bestätigter. Die Spec nennt nur „bestätigt“.
|
||||
7. **Weniger als zwei Stimmen:** kein Urteil, kein Bericht, die Basis bleibt die alte. Die Spec regelt den Fall nicht.
|
||||
8. **Unbelegte Einzelbefunde** werden nicht nachgeprüft, auch blockierende nicht. Die Spec sagt nur, dass sie nicht für die Mehrheit zählen.
|
||||
9. **Kundendokumente lösen die Regel nie aus:** `codeworkflow.md` wird nur bei Code-Projekten geladen. Vorschlag: eine Zeile im Regelwerk-Verzeichnis des Stamms („Einspielen, neue Version auf master oder Kundendokument vor der Abgabe → codeworkflow.md, Quorum am Meilenstein“). Nicht umgesetzt.
|
||||
9. **Kundendokumente lösen die Regel nie aus:** `codeworkflow.md` wird nur bei Code-Projekten geladen. Vorschlag: eine Zeile im Regelwerk-Verzeichnis des Stamms („Einspielen, neue Version auf master oder Kundendokument vor der Abgabe → codeworkflow.md, Quorum am Meilenstein“). Umgesetzt am 2026-09-30 in Task 04.
|
||||
10. **Erstes Quorum eines Code-Projekts** braucht eine Basis. Welche, entscheidet der Koordinator je Projekt (etwa der Commit der letzten abgenommenen Version).
|
||||
11. **Welche Spec- und Planübersichten ins Paket kommen**, nennt der Koordinator über `kontext`. Eine automatische Erkennung gibt es nicht.
|
||||
12. **Effort der Sonnet-Stimme:** keine Abweichung, `high` aus dem Frontmatter. Abdeckung ist der Zweck, und bei Sonnet treibt das Lesen die Kosten, nicht der Effort (codeworkflow.md, „Effort je Rolle“). Gemessen ist Sonnet als Prüfer nicht.
|
||||
|
||||
+303
-35
@@ -2,10 +2,10 @@
|
||||
type: plan-task
|
||||
title: "Task 01 — quorum_werkzeug.py: Prüfpaket bauen und Codex-Ergebnis auswerten"
|
||||
status: draft
|
||||
summary: "Neues Skript unter .vault-tools/quorum/ mit den Befehlen paket und codex, dazu ein Fall-Treiber ohne Netz und ein Mutantenlauf."
|
||||
summary: "Neues Skript unter .vault-tools/quorum/ mit den Befehlen paket, codex und ablegen, dazu ein Fall-Treiber ohne Netz und ein Mutantenlauf."
|
||||
tags: [plan-task, vault-tools, codeworkflow]
|
||||
created: 2026-09-29
|
||||
updated: 2026-09-29
|
||||
updated: 2026-09-30
|
||||
modell: haiku
|
||||
modell_begruendung: "Abschrift: Alle drei Dateien stehen wörtlich im Plan und werden per plan_materialisiere.py angewendet; Zieldateien und Verbote sind genannt, kein Codeblock trägt typografische Anführungszeichen, Striche oder Auslassungspunkte."
|
||||
---
|
||||
@@ -14,7 +14,7 @@ modell_begruendung: "Abschrift: Alle drei Dateien stehen wörtlich im Plan und w
|
||||
|
||||
Die globalen Vorgaben stehen in `00-uebersicht.md` im selben Ordner, Abschnitt „Globale Vorgaben“. Arbeitsverzeichnis ist `D:/Claude`.
|
||||
|
||||
Was danach anders ist: Der Workflow `quorum` (Task 02) kann über einen Hilfsagenten ein Prüfpaket bauen lassen und das Ergebnis eines Codex-Jobs auswerten. Das Skript entscheidet, ob der Unterschied leer ist, bestimmt Basis, Stand, Berichtsnummer und Berichtspfad und schreibt die Lesemarke ins Paket, aber nicht in den Codex-Auftrag.
|
||||
Was danach anders ist: Der Workflow `quorum` (Task 02) kann über einen Hilfsagenten ein Prüfpaket bauen lassen und das Ergebnis eines Codex-Jobs auswerten. Das Skript entscheidet, ob der Unterschied leer ist, bestimmt Basis, Stand, Berichtsnummer und Berichtspfad und schreibt die Lesemarke ins Paket, aber nicht in den Codex-Auftrag. Mit `ablegen` legt der Koordinator den Bericht aus dem Workflow ab; den Stand setzt das Skript aus `paket.json` ein, kein Agent schreibt ihn ab.
|
||||
|
||||
## Zieldateien
|
||||
|
||||
@@ -52,10 +52,19 @@ Abschnitt "Quorum am Meilenstein".
|
||||
paket baut das Pruefpaket in --ziel: paket.md (Lesemarke, Basis, Stand, Dateiliste,
|
||||
CHANGELOG-Zugaenge, Kontext, Suite-Ergebnis), diff.patch (nur Code), codex-auftrag.md
|
||||
und paket.json. Es entscheidet, ob der Unterschied leer ist, und nennt Pfad und Nummer
|
||||
des naechsten Berichts. Die letzte Zeile der Ausgabe ist QUORUM_PAKET <json>.
|
||||
des naechsten Berichts. Die letzte Zeile der Ausgabe ist QUORUM_PAKET <json>, nur mit
|
||||
Feldern fester Groesse, weil ein Hilfsagent sie abschreibt: statt der Dateiliste
|
||||
anzahl_dateien, beim Kundendokument als stand "sha256:" plus die SHA-256 ueber die
|
||||
sortierte Stand-Liste, dazu paket_summe, die SHA-256 der Bytes von paket.json. Die
|
||||
Dateiliste und die Stand-Liste stehen nur in paket.json (dateien, stand_liste).
|
||||
codex liest die Ausgabe von "codex-companion.mjs result <job> --json" aus einer Datei, zaehlt
|
||||
"usage limit" im Log des Jobs und zieht die Antwort zwischen den Marken heraus. Ob die
|
||||
Codex-Stimme gilt, entscheidet der Workflow, nicht dieses Skript.
|
||||
die Zeilen "[<Zeitstempel>] Codex error: ...usage limit..." im Log des Jobs und zieht die
|
||||
Antwort zwischen den Marken heraus. Ob die Codex-Stimme gilt, entscheidet der Workflow,
|
||||
nicht dieses Skript.
|
||||
ablegen legt den Bericht aus dem Workflow ab: prueft nummer und basis im Frontmatter gegen
|
||||
paket.json und ersetzt die Frontmatter-Zeile "stand: PAKET" durch den Stand aus
|
||||
paket.json (Code: der Commit, Kundendokument: die Stand-Liste). So schreibt kein Agent
|
||||
den Stand ab, an dem der naechste Lauf ansetzt.
|
||||
|
||||
Woher die Antworten kommen (AI/Regelwerk/pruefwerkzeuge.md, Abschnitt 1):
|
||||
- Letztes Quorum: der Bericht mit der hoechsten nummer im Frontmatter unter docs/quorum/ (Code)
|
||||
@@ -64,25 +73,30 @@ Woher die Antworten kommen (AI/Regelwerk/pruefwerkzeuge.md, Abschnitt 1):
|
||||
- Basis bei Code: --basis, sonst stand des letzten Berichts; ohne beides ist das ein Befund.
|
||||
Stand ist HEAD, und der Arbeitsbaum muss ausserhalb von docs/quorum/ sauber sein, weil die
|
||||
Stimmen die Dateien im Arbeitsbaum lesen. Unversionierte Dateien zaehlen einzeln
|
||||
(--untracked-files=all); sonst fasst git einen ganz unversionierten Ordner docs/ zu einer
|
||||
Zeile zusammen, und der Ausschluss docs/quorum greift nicht. Projektvorgabe.
|
||||
(--untracked-files=all); das uebersteuert auch status.showUntrackedFiles=no in der
|
||||
Git-Konfiguration, das unversionierte Dateien sonst aus der Ausgabe nimmt. Projektvorgabe.
|
||||
- Leer: kein Unterschied ausserhalb von docs/quorum/ (Code) bzw. keine Datei neu, geaendert oder
|
||||
entfernt nach SHA-256 (Kundendokument). Spec, "Kein Diff wird zweimal geprueft".
|
||||
- Job und Ergebnis von Codex: Felder status, logFile, startedAt und completedAt im Job,
|
||||
rawOutput im gespeicherten Ergebnis. Quelle: Plugin codex 1.0.6, scripts/lib/tracked-jobs.mjs
|
||||
Zeilen 142-179 und scripts/codex-companion.mjs Zeilen 497-529 und 910-926.
|
||||
- "usage limit": Gross- und Kleinschreibung egal, jede Fundstelle im Log zaehlt. Quelle: Meldung
|
||||
vom 2026-09-05 (Harness-Gedaechtnis codex-kontingent-begrenzt); die genaue Form im Log ist
|
||||
nicht gemessen.
|
||||
- "usage limit": gezaehlt wird nur eine Log-Zeile der Form "[<Zeitstempel>] Codex error:
|
||||
...usage limit...", Gross- und Kleinschreibung egal. Quelle: Plugin codex 1.0.6,
|
||||
scripts/lib/codex.mjs Zeile 539 und scripts/lib/tracked-jobs.mjs Zeilen 36-41; die Form ist
|
||||
an den Logs vom 2026-09-05 im Plugin-Datenordner belegt. Nicht jede Fundstelle, denn im Log
|
||||
stehen auch jede Befehlszeile, jede Nachricht von Codex und die Schlussantwort (codex.mjs 247
|
||||
und 441-442, tracked-jobs.mjs 179).
|
||||
Was das Skript nicht sieht: ob die Stimmen das Paket wirklich lesen (dafuer die Lesemarke), ob
|
||||
ein Bericht von Hand geaendert wurde, und Dateien, die .gitignore ausblendet. Es ist ein
|
||||
Werkzeug, kein Nachweis.
|
||||
ein Bericht von Hand geaendert wurde, Dateien, die .gitignore ausblendet, und eine Nachricht von
|
||||
Codex, die eine Fehlerzeile samt Zeitstempel am Zeilenanfang zitiert (sie zaehlt als Treffer).
|
||||
Es ist ein Werkzeug, kein Nachweis.
|
||||
Aufruf:
|
||||
python quorum_werkzeug.py paket --anlass einspielen|version --projekt <repo> [--basis <ref>]
|
||||
--ziel <ordner> [--kontext <pfad> ...] [--suite-datei <pfad>]
|
||||
python quorum_werkzeug.py paket --anlass kundendokument --ordner <ordner> --uebersicht <relpfad>
|
||||
--ziel <ordner> --dateien <relpfad> ... [--kontext <pfad> ...]
|
||||
python quorum_werkzeug.py codex --ergebnis <datei>
|
||||
python quorum_werkzeug.py ablegen --paket <ordner> --bericht <md-datei> --ziel <pfad>
|
||||
Exit 0 mit Ergebnis, 2 bei einem Befund (Zeile BEFUND), 3 bei einem unerwarteten Abbruch (Zeile
|
||||
ABBRUCH).
|
||||
"""
|
||||
@@ -106,9 +120,11 @@ AUSSCHLUSS = ":(exclude)docs/quorum"
|
||||
SUITE_ZEILEN = 80
|
||||
MARKE_ANFANG = "QUORUM-ANTWORT-ANFANG"
|
||||
MARKE_ENDE = "QUORUM-ANTWORT-ENDE"
|
||||
USAGE_LIMIT = re.compile(r"usage limit", re.I)
|
||||
USAGE_LIMIT = re.compile(r"^\[[^\]]+\] Codex error: .*usage limit", re.I | re.M)
|
||||
SHA256 = re.compile(r"[0-9a-f]{64}")
|
||||
ZAHL = re.compile(r"[0-9]+")
|
||||
PAKET_ZEILE = ("ordner", "anlass", "art", "wurzel", "leer", "basis", "stand", "anzahl_dateien", "lesemarke",
|
||||
"bericht_pfad", "nummer", "datum", "vorher", "suite", "hinweise")
|
||||
|
||||
CODEX_AUFTRAG = Template("""Du bist eine von drei unabhaengigen Pruefstimmen eines Quorums am Meilenstein. Die anderen Stimmen kennst du nicht. Du aenderst nichts: keine Datei schreiben, keinen Befehl, der etwas veraendert.
|
||||
|
||||
@@ -215,6 +231,8 @@ def lies_suite(pfad_text):
|
||||
except OSError as fehler:
|
||||
raise Befund(f"suite-datei nicht lesbar: {pfad_text} ({type(fehler).__name__})")
|
||||
zeilen = text.replace("\r\n", "\n").split("\n")
|
||||
if zeilen[-1] == "":
|
||||
zeilen.pop()
|
||||
exits = [z for z in zeilen if z.startswith("EXIT suite: ")]
|
||||
kurz = "exit " + exits[-1][len("EXIT suite: "):].strip() if exits else "ohne EXIT-Zeile"
|
||||
return kurz, zeilen[-SUITE_ZEILEN:]
|
||||
@@ -372,18 +390,64 @@ def paket(a):
|
||||
auftrag = CODEX_AUFTRAG.substitute(paket=(ziel / "paket.md").as_posix(), wurzel=teil["wurzel"].as_posix(), anfang=MARKE_ANFANG, ende=MARKE_ENDE)
|
||||
with open(ziel / "codex-auftrag.md", "w", encoding="utf-8", newline="\n") as datei:
|
||||
datei.write(auftrag)
|
||||
stand_liste = teil["stand"] if teil["art"] == "doku" else None
|
||||
stand = teil["stand"] if stand_liste is None else "sha256:" + hashlib.sha256("\n".join(sorted(stand_liste)).encode("utf-8")).hexdigest()
|
||||
ergebnis = {
|
||||
"ordner": ziel.as_posix(), "anlass": a.anlass, "art": teil["art"], "wurzel": teil["wurzel"].as_posix(),
|
||||
"leer": not teil["dateien"], "basis": teil["basis"], "stand": teil["stand"], "dateien": teil["dateien"],
|
||||
"leer": not teil["dateien"], "basis": teil["basis"], "stand": stand, "anzahl_dateien": len(teil["dateien"]),
|
||||
"lesemarke": marke, "bericht_pfad": bericht.as_posix(), "nummer": (vorher["nummer"] if vorher else 0) + 1,
|
||||
"datum": datum,
|
||||
"vorher": {"pfad": vorher["pfad"], "nummer": vorher["nummer"], "urteil": vorher["urteil"]} if vorher else None,
|
||||
"suite": suite_kurz, "hinweise": teil["hinweise"],
|
||||
"suite": suite_kurz, "hinweise": teil["hinweise"], "dateien": teil["dateien"], "stand_liste": stand_liste,
|
||||
}
|
||||
with open(ziel / "paket.json", "w", encoding="utf-8", newline="\n") as datei:
|
||||
datei.write(json.dumps(ergebnis, ensure_ascii=False, indent=2) + "\n")
|
||||
zeile = {k: ergebnis[k] for k in PAKET_ZEILE}
|
||||
zeile["paket_summe"] = hashlib.sha256((ziel / "paket.json").read_bytes()).hexdigest()
|
||||
print(f"Paket in {ziel.as_posix()}: {len(teil['dateien'])} Datei(en) im Unterschied, leer: {'ja' if ergebnis['leer'] else 'nein'}")
|
||||
print("QUORUM_PAKET " + json.dumps(ergebnis, ensure_ascii=True))
|
||||
print("QUORUM_PAKET " + json.dumps(zeile, ensure_ascii=True))
|
||||
return 0
|
||||
|
||||
|
||||
def ablegen(a):
|
||||
ordner = Path(a.paket)
|
||||
try:
|
||||
daten = json.loads((ordner / "paket.json").read_text(encoding="utf-8"))
|
||||
except (OSError, ValueError) as fehler:
|
||||
raise Befund(f"paket.json nicht lesbar in {ordner.as_posix()} ({type(fehler).__name__})")
|
||||
ziel = Path(a.ziel)
|
||||
if ziel.exists():
|
||||
raise Befund(f"ziel gibt es schon: {ziel.as_posix()}")
|
||||
try:
|
||||
text = Path(a.bericht).read_text(encoding="utf-8")
|
||||
except (OSError, UnicodeDecodeError) as fehler:
|
||||
raise Befund(f"bericht nicht lesbar: {a.bericht} ({type(fehler).__name__})")
|
||||
zeilen = kopf(text)
|
||||
if zeilen is None:
|
||||
raise Befund("bericht ohne Frontmatter")
|
||||
werte = {}
|
||||
for zeile in zeilen:
|
||||
schluessel, trenner, wert = zeile.partition(":")
|
||||
if trenner and not zeile.startswith((" ", "-")):
|
||||
werte[schluessel.strip()] = wert.strip().strip('"')
|
||||
if werte.get("nummer") != str(daten["nummer"]):
|
||||
raise Befund(f"nummer im Bericht ({werte.get('nummer')}) passt nicht zu paket.json ({daten['nummer']})")
|
||||
if werte.get("basis") != str(daten["basis"]):
|
||||
raise Befund(f"basis im Bericht ({werte.get('basis')}) passt nicht zu paket.json ({daten['basis']})")
|
||||
treffer = [i for i, z in enumerate(zeilen) if z == "stand: PAKET"]
|
||||
if len(treffer) != 1:
|
||||
raise Befund(f"die Zeile stand: PAKET steht {len(treffer)}-mal im Frontmatter, erwartet genau einmal")
|
||||
if daten["art"] == "doku":
|
||||
neu = ["stand:"] + [f' - "{s}"' for s in daten["stand_liste"]]
|
||||
else:
|
||||
neu = [f"stand: {daten['stand']}"]
|
||||
alle = text.split("\n")
|
||||
stelle = 1 + treffer[0]
|
||||
alle[stelle:stelle + 1] = neu
|
||||
ziel.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(ziel, "x", encoding="utf-8", newline="\n") as datei:
|
||||
datei.write("\n".join(alle))
|
||||
print(f"Bericht abgelegt: {ziel.as_posix()}")
|
||||
return 0
|
||||
|
||||
|
||||
@@ -478,9 +542,13 @@ def main(argv=None):
|
||||
p.add_argument("--suite-datei", dest="suite_datei")
|
||||
c = unter.add_parser("codex")
|
||||
c.add_argument("--ergebnis", required=True)
|
||||
b = unter.add_parser("ablegen")
|
||||
b.add_argument("--paket", required=True)
|
||||
b.add_argument("--bericht", required=True)
|
||||
b.add_argument("--ziel", required=True)
|
||||
a = zerleger.parse_args(argv)
|
||||
try:
|
||||
return paket(a) if a.befehl == "paket" else codex(a)
|
||||
return {"paket": paket, "codex": codex, "ablegen": ablegen}[a.befehl](a)
|
||||
except Befund as befund:
|
||||
print(f"BEFUND: {befund}")
|
||||
return 2
|
||||
@@ -524,6 +592,8 @@ SKRIPT = Path(sys.argv[1]) if len(sys.argv) > 1 else Path(__file__).with_name("q
|
||||
HEUTE = date.today().isoformat()
|
||||
MARKE = "0123456789abcdef"
|
||||
FAELLE = []
|
||||
ZEILE_FELDER = ["ordner", "anlass", "art", "wurzel", "leer", "basis", "stand", "anzahl_dateien", "lesemarke",
|
||||
"bericht_pfad", "nummer", "datum", "vorher", "suite", "hinweise", "paket_summe"]
|
||||
|
||||
|
||||
def fall(name):
|
||||
@@ -588,6 +658,18 @@ def bericht(ordner, name, nummer, stand, urteil="frei"):
|
||||
f"{stand_text}codex: abgegeben\n---\n\n# Quorum {nummer}: einspielen\n")
|
||||
|
||||
|
||||
def workflow_bericht(pfad, nummer, basis, stand_zeilen=("stand: PAKET",), rumpf="", zeilenende="\n"):
|
||||
"""Ein Bericht, wie ihn quorum.js als bericht_md liefert: stand steht als Platzhalter PAKET."""
|
||||
zeilen = ["---", "type: note", f'title: "Quorum {nummer}: einspielen"', "status: done",
|
||||
'summary: "Urteil frei."', "tags: [quorum]", f"created: {HEUTE}", f"updated: {HEUTE}",
|
||||
f"nummer: {nummer}", "anlass: einspielen", "urteil: frei", f'basis: "{basis}"',
|
||||
*stand_zeilen, "codex: abgegeben", "---", "", f"# Quorum {nummer}: einspielen", "", rumpf]
|
||||
pfad.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(pfad, "w", encoding="utf-8", newline="") as datei:
|
||||
datei.write(zeilenende.join(zeilen) + zeilenende)
|
||||
return pfad
|
||||
|
||||
|
||||
def fahre(*argumente):
|
||||
umgebung = dict(os.environ)
|
||||
umgebung["PYTHONIOENCODING"] = "utf-8"
|
||||
@@ -603,6 +685,12 @@ def paket_json(ausgabe):
|
||||
return None
|
||||
|
||||
|
||||
def voll(ziel):
|
||||
"""Der Inhalt von paket.json, oder None."""
|
||||
pfad = ziel / "paket.json"
|
||||
return json.loads(pfad.read_text(encoding="utf-8")) if pfad.exists() else None
|
||||
|
||||
|
||||
def code_paket(tmp, w, *zusatz):
|
||||
ziel = Path(tempfile.mkdtemp(dir=tmp))
|
||||
code, aus, err = fahre("paket", "--anlass", "einspielen", "--projekt", w, "--ziel", ziel, *zusatz)
|
||||
@@ -623,7 +711,21 @@ def doku_paket(tmp, o, *dateien, uebersicht="00-uebersicht.md"):
|
||||
return code, aus + err, paket_json(aus), ziel
|
||||
|
||||
|
||||
def ablegen(paket_ordner, bericht_datei, ziel):
|
||||
code, aus, err = fahre("ablegen", "--paket", paket_ordner, "--bericht", bericht_datei, "--ziel", ziel)
|
||||
return code, aus + err
|
||||
|
||||
|
||||
ANTWORT = {"lesebeleg": f"Lesemarke: {MARKE}", "befunde": [{"datei": "a.py", "stelle": "1", "schwere": "Wichtig", "aussage": "x ist falsch", "beleg": "x = 2"}], "nicht_pruefbar": []}
|
||||
ANTWORT_LIMIT = {**ANTWORT, "befunde": [{**ANTWORT["befunde"][0], "aussage": "zaehlt jedes usage limit im Log", "beleg": 'USAGE_LIMIT = re.compile(r"usage limit", re.I)'}]}
|
||||
LOG_FEHLALARM = (
|
||||
"[2026-09-30T10:00:00.000Z] Starting Codex task.\n"
|
||||
'[2026-09-30T10:00:05.000Z] Running command: rg -n "usage limit" .vault-tools/quorum\n'
|
||||
"[2026-09-30T10:01:00.000Z] Assistant message captured: Der Zaehler fuer usage limit liest das ganze Log.\n"
|
||||
"\n[2026-09-30T10:01:00.000Z] Assistant message\n"
|
||||
"Der Zaehler fuer usage limit liest das ganze Log.\n"
|
||||
"Codex error: You've hit your usage limit. (Zitat aus einem alten Log)\n"
|
||||
)
|
||||
|
||||
|
||||
def block(antwort):
|
||||
@@ -662,8 +764,9 @@ def f_grundlinie(tmp):
|
||||
f = soll(code == 0 and p is not None, f"Exit {code}: {text}")
|
||||
if not p:
|
||||
return f
|
||||
v = voll(ziel) or {}
|
||||
f += soll(p["basis"] == a and p["stand"] == git(w, "rev-parse", "HEAD") and p["leer"] is False, f"basis/stand/leer {p['basis']} {p['stand']} {p['leer']}")
|
||||
f += soll(p["dateien"] == [{"pfad": "CHANGELOG.md", "art": "geaendert"}, {"pfad": "a.py", "art": "geaendert"}], f"dateien {p['dateien']}")
|
||||
f += soll(v.get("dateien") == [{"pfad": "CHANGELOG.md", "art": "geaendert"}, {"pfad": "a.py", "art": "geaendert"}] and p["anzahl_dateien"] == 2, f"dateien {v.get('dateien')} {p['anzahl_dateien']}")
|
||||
f += soll(p["nummer"] == 2 and p["vorher"]["nummer"] == 1 and p["vorher"]["urteil"] == "frei" and p["art"] == "code", f"nummer/vorher {p['nummer']} {p['vorher']}")
|
||||
f += soll(p["bericht_pfad"] == (w / "docs" / "quorum" / f"{HEUTE}-einspielen.md").as_posix(), f"bericht_pfad {p['bericht_pfad']}")
|
||||
md = (ziel / "paket.md").read_text(encoding="utf-8")
|
||||
@@ -674,7 +777,9 @@ def f_grundlinie(tmp):
|
||||
f += soll((ziel / "paket.md").as_posix() in auftrag and "QUORUM-ANTWORT-ANFANG" in auftrag and "QUORUM-ANTWORT-ENDE" in auftrag, "Codex-Auftrag ohne Paketpfad oder Marken")
|
||||
f += soll("- neue Zeile im Log" in md and "- erste Fassung, korrigiert" in md and "\n- erste Fassung\n" not in md, "CHANGELOG-Zeilen falsch")
|
||||
f += soll("+x = 2" in diff and "docs/quorum" not in diff, "diff.patch falsch")
|
||||
f += soll(json.loads((ziel / "paket.json").read_text(encoding="utf-8")) == p, "paket.json weicht von QUORUM_PAKET ab")
|
||||
f += soll(sorted(p) == sorted(ZEILE_FELDER), f"Felder der Zeile {sorted(p)}")
|
||||
f += soll({k: x for k, x in v.items() if k not in ("dateien", "stand_liste")} == {k: x for k, x in p.items() if k != "paket_summe"} and v.get("stand_liste", 0) is None, "paket.json weicht von QUORUM_PAKET ab")
|
||||
f += soll(p.get("paket_summe") == hashlib.sha256((ziel / "paket.json").read_bytes()).hexdigest(), f"paket_summe {p.get('paket_summe')} passt nicht zu paket.json")
|
||||
return f
|
||||
|
||||
|
||||
@@ -689,7 +794,7 @@ def f_erstes_ohne_basis(tmp):
|
||||
def f_erstes_mit_basis(tmp):
|
||||
w, a, b = repo(tmp)
|
||||
code, text, p, ziel = code_paket(tmp, w, "--basis", a)
|
||||
return soll(code == 0 and p is not None and p["nummer"] == 1 and p["vorher"] is None and p["basis"] == a and len(p["dateien"]) == 2, f"Exit {code}: {text}")
|
||||
return soll(code == 0 and p is not None and p["nummer"] == 1 and p["vorher"] is None and p["basis"] == a and p["anzahl_dateien"] == 2, f"Exit {code}: {text}")
|
||||
|
||||
|
||||
@fall("Erstes Quorum mit --basis HEAD: leerer Unterschied ist ein Befund")
|
||||
@@ -703,7 +808,25 @@ def f_erstes_leer(tmp):
|
||||
def f_basis_unbekannt(tmp):
|
||||
w, a, b = repo(tmp)
|
||||
code, text, p, ziel = code_paket(tmp, w, "--basis", "gibt-es-nicht")
|
||||
return soll(code == 2 and "BEFUND" in text and "Traceback" not in text, f"Exit {code}: {text}")
|
||||
return soll(code == 2 and "BEFUND" in text and "rev-parse" in text and "gibt-es-nicht" in text and "Traceback" not in text, f"Exit {code}: {text}")
|
||||
|
||||
|
||||
@fall("Hinweise: basis weicht vom letzten Stand ab, basis ist kein Vorfahr; gleiche basis ohne Hinweis")
|
||||
def f_hinweise(tmp):
|
||||
w, a, b = repo(tmp)
|
||||
git(w, "checkout", "-q", "-b", "seite", a)
|
||||
schreibe(w / "s.py", "s = 1\n")
|
||||
git(w, "add", "-A")
|
||||
git(w, "commit", "-q", "-m", "S")
|
||||
s = git(w, "rev-parse", "HEAD")
|
||||
git(w, "checkout", "-q", "-")
|
||||
bericht(w / "docs" / "quorum", "2026-09-20-einspielen.md", 1, a)
|
||||
f = []
|
||||
for basis, weicht, vorfahr in (("HEAD~1", False, False), (s, True, True)):
|
||||
code, text, p, ziel = code_paket(tmp, w, "--basis", basis)
|
||||
h = " ".join(p["hinweise"]) if p else ""
|
||||
f += soll(code == 0 and p is not None and ("basis weicht" in h) == weicht and ("kein Vorfahr" in h) == vorfahr, f"basis {basis}: Exit {code}: {h} {text}")
|
||||
return f
|
||||
|
||||
|
||||
@fall("Leerer Unterschied mit unversioniertem Bericht: leer, Urteil des Vorberichts reicht durch")
|
||||
@@ -711,7 +834,7 @@ def f_leer(tmp):
|
||||
w, a, b = repo(tmp)
|
||||
bericht(w / "docs" / "quorum", "2026-09-20-einspielen.md", 1, b, urteil="nicht_frei")
|
||||
code, text, p, ziel = code_paket(tmp, w)
|
||||
return soll(code == 0 and p is not None and p["leer"] is True and p["dateien"] == [] and p["vorher"]["urteil"] == "nicht_frei" and p["nummer"] == 2, f"Exit {code}: {text}")
|
||||
return soll(code == 0 and p is not None and p["leer"] is True and p["anzahl_dateien"] == 0 and p["vorher"]["urteil"] == "nicht_frei" and p["nummer"] == 2, f"Exit {code}: {text}")
|
||||
|
||||
|
||||
@fall("Seit der Basis nur docs/quorum geaendert: leer")
|
||||
@@ -740,6 +863,15 @@ def f_unsauber_neu(tmp):
|
||||
return soll(code == 2 and "nicht sauber" in text and "neu.py" in text, f"Exit {code}: {text}")
|
||||
|
||||
|
||||
@fall("Unversionierte Datei bei status.showUntrackedFiles=no: trotzdem Befund")
|
||||
def f_unsauber_verborgen(tmp):
|
||||
w, a, b = repo(tmp)
|
||||
git(w, "config", "status.showUntrackedFiles", "no")
|
||||
schreibe(w / "neu.py", "z = 1\n")
|
||||
code, text, p, ziel = code_paket(tmp, w, "--basis", a)
|
||||
return soll(code == 2 and "nicht sauber" in text and "neu.py" in text, f"Exit {code}: {text}")
|
||||
|
||||
|
||||
@fall("Hoechste nummer gewinnt, an erster und an letzter Stelle der Dateiliste")
|
||||
def f_hoechste_nummer(tmp):
|
||||
f = []
|
||||
@@ -796,10 +928,21 @@ def f_arten(tmp):
|
||||
git(w, "add", "-A")
|
||||
git(w, "commit", "-q", "-m", "C")
|
||||
code, text, p, ziel = code_paket(tmp, w, "--basis", a)
|
||||
arten = {d["pfad"]: d["art"] for d in (p["dateien"] if p else [])}
|
||||
arten = {d["pfad"]: d["art"] for d in (voll(ziel) or {}).get("dateien", [])}
|
||||
return soll(code == 0 and arten == {"CHANGELOG.md": "geaendert", "a.py": "geaendert", "b.py": "entfernt", "c.py": "neu"}, f"Exit {code}: {arten} {text}")
|
||||
|
||||
|
||||
@fall("Umbenannte Datei: alter Pfad entfernt, neuer Pfad neu")
|
||||
def f_umbenannt(tmp):
|
||||
w, a, b = repo(tmp)
|
||||
git(w, "config", "diff.renames", "true")
|
||||
git(w, "mv", "b.py", "umbenannt.py")
|
||||
git(w, "commit", "-q", "-m", "C")
|
||||
code, text, p, ziel = code_paket(tmp, w, "--basis", b)
|
||||
arten = {d["pfad"]: d["art"] for d in (voll(ziel) or {}).get("dateien", [])}
|
||||
return soll(code == 0 and arten == {"b.py": "entfernt", "umbenannt.py": "neu"}, f"Exit {code}: {arten} {text}")
|
||||
|
||||
|
||||
@fall("Berichtspfad schon belegt: Zaehler -2")
|
||||
def f_pfad_belegt(tmp):
|
||||
w, a, b = repo(tmp)
|
||||
@@ -808,14 +951,15 @@ def f_pfad_belegt(tmp):
|
||||
return soll(code == 0 and p is not None and p["bericht_pfad"].endswith(f"/docs/quorum/{HEUTE}-einspielen-2.md"), f"Exit {code}: {text}")
|
||||
|
||||
|
||||
@fall("Suite-Datei: Exit und die letzten Zeilen stehen im Paket")
|
||||
@fall("Suite-Datei: letzte EXIT-Zeile und genau die letzten 80 Zeilen stehen im Paket")
|
||||
def f_suite(tmp):
|
||||
w, a, b = repo(tmp)
|
||||
suite = tmp / "suite.txt"
|
||||
schreibe(suite, "".join(f"zeile {n}\n" for n in range(1, 101)) + "EXIT suite: 1\n")
|
||||
schreibe(suite, "EXIT suite: 0\n" + "".join(f"zeile {n}\n" for n in range(1, 120)) + "EXIT suite: 1\n")
|
||||
code, text, p, ziel = code_paket(tmp, w, "--basis", a, "--suite-datei", suite)
|
||||
md = (ziel / "paket.md").read_text(encoding="utf-8") if (ziel / "paket.md").exists() else ""
|
||||
return soll(code == 0 and p is not None and p["suite"] == "exit 1" and "\nzeile 100\n" in md and "\nzeile 1\n" not in md and "EXIT suite: 1" in md, f"Exit {code}: {text}")
|
||||
f = soll(code == 0 and p is not None and p["suite"] == "exit 1", f"Exit {code}: {text}")
|
||||
return f + soll("Die letzten 80 Zeilen der Ausgabe:" in md and "\nzeile 41\n" in md and "\nzeile 40\n" not in md and "\nEXIT suite: 1\n```\n" in md, f"Zeilen im Paket: {md[-400:]}")
|
||||
|
||||
|
||||
@fall("Suite-Datei fehlt: Befund")
|
||||
@@ -865,13 +1009,33 @@ def f_doku_erstes(tmp):
|
||||
code, text, p, ziel = doku_paket(tmp, o, "kapitel/02.md", "kapitel/01.md")
|
||||
if not p:
|
||||
return [f"Exit {code}: {text}"]
|
||||
v = voll(ziel) or {}
|
||||
liste = [f"{summe(o / 'kapitel' / '01.md')} kapitel/01.md", f"{summe(o / 'kapitel' / '02.md')} kapitel/02.md"]
|
||||
f = soll(code == 0 and p["art"] == "doku" and p["basis"] == "erstes Quorum" and p["nummer"] == 1 and p["vorher"] is None, f"Kopf {p}")
|
||||
f += soll(p["stand"] == [f"{summe(o / 'kapitel' / '01.md')} kapitel/01.md", f"{summe(o / 'kapitel' / '02.md')} kapitel/02.md"], f"stand {p['stand']}")
|
||||
f += soll(sorted(d["art"] for d in p["dateien"]) == ["neu", "neu"] and p["leer"] is False, f"dateien {p['dateien']}")
|
||||
f += soll(v.get("stand_liste") == liste and p["stand"] == "sha256:" + hashlib.sha256("\n".join(sorted(liste)).encode("utf-8")).hexdigest(), f"stand {p['stand']} {v.get('stand_liste')}")
|
||||
f += soll(sorted(d["art"] for d in v.get("dateien", [])) == ["neu", "neu"] and p["anzahl_dateien"] == 2 and p["leer"] is False, f"dateien {v.get('dateien')}")
|
||||
f += soll(p["bericht_pfad"] == (o / "quorum" / f"{HEUTE}-kundendokument.md").as_posix() and not (ziel / "diff.patch").exists(), f"bericht_pfad {p['bericht_pfad']}")
|
||||
return f
|
||||
|
||||
|
||||
@fall("Kundendokument: stand in der Zeile ist die Pruefsumme ueber die sortierte Stand-Liste")
|
||||
def f_doku_stand_summe(tmp):
|
||||
o = dokusatz(tmp)
|
||||
eins = summe(o / "kapitel" / "01.md")
|
||||
for n in range(200):
|
||||
schreibe(o / "kapitel" / "02.md", f"# Zwei {n}\n")
|
||||
if summe(o / "kapitel" / "02.md") < eins:
|
||||
break
|
||||
liste = [f"{eins} kapitel/01.md", f"{summe(o / 'kapitel' / '02.md')} kapitel/02.md"]
|
||||
richtig = "sha256:" + hashlib.sha256("\n".join(sorted(liste)).encode("utf-8")).hexdigest()
|
||||
falsch = "sha256:" + hashlib.sha256("\n".join(liste).encode("utf-8")).hexdigest()
|
||||
f = soll(richtig != falsch, "Fixture unterscheidet sortiert und unsortiert nicht")
|
||||
code, text, p, ziel = doku_paket(tmp, o, "kapitel/01.md", "kapitel/02.md")
|
||||
v = voll(ziel) or {}
|
||||
f += soll(code == 0 and p is not None and p["stand"] == richtig, f"Exit {code}: stand {p['stand'] if p else None} {text}")
|
||||
return f + soll(v.get("stand_liste") == liste, f"stand_liste {v.get('stand_liste')}")
|
||||
|
||||
|
||||
@fall("Kundendokument, zweites Quorum: geaendert, neu und entfernt, unveraendert fehlt")
|
||||
def f_doku_zweites(tmp):
|
||||
o = dokusatz(tmp)
|
||||
@@ -881,7 +1045,7 @@ def f_doku_zweites(tmp):
|
||||
schreibe(o / "kapitel" / "01.md", "# Eins, neu gefasst\n")
|
||||
schreibe(o / "kapitel" / "04.md", "# Vier\n")
|
||||
code, text, p, ziel = doku_paket(tmp, o, "kapitel/01.md", "kapitel/02.md", "kapitel/04.md")
|
||||
arten = {d["pfad"]: d["art"] for d in (p["dateien"] if p else [])}
|
||||
arten = {d["pfad"]: d["art"] for d in (voll(ziel) or {}).get("dateien", [])}
|
||||
return soll(code == 0 and arten == {"kapitel/01.md": "geaendert", "kapitel/04.md": "neu", "kapitel/03.md": "entfernt"} and p["basis"] == "2026-09-20-kundendokument.md" and p["nummer"] == 2, f"Exit {code}: {arten} {text}")
|
||||
|
||||
|
||||
@@ -891,7 +1055,7 @@ def f_doku_leer(tmp):
|
||||
stand = [f"{summe(o / 'kapitel' / n)} kapitel/{n}" for n in ("01.md", "02.md")]
|
||||
bericht(o / "quorum", "2026-09-20-kundendokument.md", 1, stand)
|
||||
code, text, p, ziel = doku_paket(tmp, o, "kapitel/01.md", "kapitel/02.md")
|
||||
return soll(code == 0 and p is not None and p["leer"] is True and p["dateien"] == [], f"Exit {code}: {text}")
|
||||
return soll(code == 0 and p is not None and p["leer"] is True and p["anzahl_dateien"] == 0, f"Exit {code}: {text}")
|
||||
|
||||
|
||||
@fall("Kundendokument: Pfad nach aussen, absoluter Pfad, fehlende und doppelte Datei sind Befunde")
|
||||
@@ -933,6 +1097,69 @@ def f_doku_stand_kaputt(tmp):
|
||||
return f
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- ablegen
|
||||
|
||||
@fall("ablegen, Code: stand PAKET wird der Commit, CRLF wird LF, Ordner angelegt, naechster Lauf ist leer")
|
||||
def f_ablegen_code(tmp):
|
||||
w, a, b = repo(tmp)
|
||||
code, text, p, ziel = code_paket(tmp, w, "--basis", a)
|
||||
if not p:
|
||||
return [f"paket: Exit {code}: {text}"]
|
||||
quelle = workflow_bericht(tmp / "bericht.md", p["nummer"], p["basis"], zeilenende="\r\n")
|
||||
code, text = ablegen(ziel, quelle, p["bericht_pfad"])
|
||||
abgelegt = Path(p["bericht_pfad"])
|
||||
roh = abgelegt.read_bytes() if abgelegt.exists() else b""
|
||||
erwartet = quelle.read_bytes().replace(b"\r\n", b"\n").replace(b"\nstand: PAKET\n", f"\nstand: {b}\n".encode("ascii"))
|
||||
f = soll(code == 0 and roh == erwartet, f"Exit {code}: {text} {roh[:400]!r}")
|
||||
code, text, p2, ziel2 = code_paket(tmp, w)
|
||||
return f + soll(code == 0 and p2 is not None and p2["leer"] is True and p2["basis"] == b and p2["nummer"] == 2, f"zweiter Lauf: Exit {code}: {text}")
|
||||
|
||||
|
||||
@fall("ablegen, Kundendokument: stand PAKET wird die Liste, nur im Frontmatter, naechster Lauf ist leer")
|
||||
def f_ablegen_doku(tmp):
|
||||
o = dokusatz(tmp)
|
||||
code, text, p, ziel = doku_paket(tmp, o, "kapitel/01.md", "kapitel/02.md")
|
||||
if not p:
|
||||
return [f"paket: Exit {code}: {text}"]
|
||||
quelle = workflow_bericht(tmp / "bericht.md", p["nummer"], p["basis"], rumpf="stand: PAKET")
|
||||
code, text = ablegen(ziel, quelle, p["bericht_pfad"])
|
||||
abgelegt = Path(p["bericht_pfad"])
|
||||
inhalt = abgelegt.read_text(encoding="utf-8") if abgelegt.exists() else ""
|
||||
liste = "".join(f' - "{summe(o / "kapitel" / n)} kapitel/{n}"\n' for n in ("01.md", "02.md"))
|
||||
f = soll(code == 0 and f'basis: "erstes Quorum"\nstand:\n{liste}codex: abgegeben\n' in inhalt and inhalt.rstrip("\n").endswith("\nstand: PAKET"), f"Exit {code}: {text} {inhalt[:600]}")
|
||||
code, text, p2, ziel2 = doku_paket(tmp, o, "kapitel/01.md", "kapitel/02.md")
|
||||
return f + soll(code == 0 and p2 is not None and p2["leer"] is True and p2["nummer"] == 2, f"zweiter Lauf: Exit {code}: {text}")
|
||||
|
||||
|
||||
@fall("ablegen: falsche nummer, falsche basis, stand PAKET fehlt oder doppelt, Ziel vorhanden, ohne Frontmatter, ohne paket.json sind Befunde; nichts geschrieben")
|
||||
def f_ablegen_befunde(tmp):
|
||||
w, a, b = repo(tmp)
|
||||
code, text, p, ziel = code_paket(tmp, w, "--basis", a)
|
||||
if not p:
|
||||
return [f"paket: Exit {code}: {text}"]
|
||||
vorhanden = tmp / "vorhanden.md"
|
||||
schreibe(vorhanden, "alt\n")
|
||||
f = []
|
||||
for name, nummer, basis, stand_zeilen, nach, erwartet in (
|
||||
("nummer", p["nummer"] + 1, p["basis"], ("stand: PAKET",), tmp / "n" / "x.md", "nummer"),
|
||||
("basis", p["nummer"], b, ("stand: PAKET",), tmp / "b" / "x.md", "basis"),
|
||||
("ohne-stand", p["nummer"], p["basis"], ("stand: x",), tmp / "o" / "x.md", "stand: PAKET"),
|
||||
("stand-doppelt", p["nummer"], p["basis"], ("stand: PAKET", "stand: PAKET"), tmp / "d" / "x.md", "stand: PAKET"),
|
||||
("ziel-da", p["nummer"], p["basis"], ("stand: PAKET",), vorhanden, "gibt es schon"),
|
||||
):
|
||||
quelle = workflow_bericht(tmp / f"{name}.md", nummer, basis, stand_zeilen)
|
||||
code, text = ablegen(ziel, quelle, nach)
|
||||
unberuehrt = nach.read_text(encoding="utf-8") == "alt\n" if nach == vorhanden else not nach.parent.exists()
|
||||
f += soll(code == 2 and "BEFUND" in text and erwartet in text and unberuehrt and "Traceback" not in text, f"{name}: Exit {code}: {text}")
|
||||
ohne_kopf = tmp / "ohne-kopf.md"
|
||||
schreibe(ohne_kopf, "# nur Text\nstand: PAKET\n")
|
||||
code, text = ablegen(ziel, ohne_kopf, tmp / "k" / "x.md")
|
||||
f += soll(code == 2 and "ohne Frontmatter" in text and not (tmp / "k").exists(), f"ohne-kopf: Exit {code}: {text}")
|
||||
leer = Path(tempfile.mkdtemp(dir=tmp))
|
||||
code, text = ablegen(leer, tmp / "nummer.md", tmp / "p" / "x.md")
|
||||
return f + soll(code == 2 and "paket.json nicht lesbar" in text and not (tmp / "p").exists(), f"ohne-paket: Exit {code}: {text}")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- codex
|
||||
|
||||
@fall("Codex: completed, Log ohne usage limit, Antwort gelesen, Schwere klein geschrieben")
|
||||
@@ -949,6 +1176,25 @@ def f_codex_limit(tmp):
|
||||
return soll(code == 0 and z.get("usage_limit_treffer") == "1", f"Exit {code}: {text}")
|
||||
|
||||
|
||||
@fall("Codex: usage limit nur in Befehl, Nachricht und Final output zaehlt nicht")
|
||||
def f_codex_limit_fehlalarm(tmp):
|
||||
roh = block(ANTWORT_LIMIT)
|
||||
log = LOG_FEHLALARM + "\n[2026-09-30T10:02:00.000Z] Final output\n" + roh + "[2026-09-30T10:02:01.000Z] Turn completed.\n"
|
||||
code, text, z, antwort = auswerten(ergebnisdatei(tmp, roh, log=log))
|
||||
return soll(code == 0 and z.get("job_status") == "completed" and z.get("usage_limit_treffer") == "0" and z.get("antwort_gefunden") == "ja", f"Exit {code}: {text}")
|
||||
|
||||
|
||||
@fall("Codex: echte Fehlerzeile nach anderen Zeilen, im Final output wiederholt, zaehlt einmal")
|
||||
def f_codex_limit_echt(tmp):
|
||||
log = ("[2026-09-05T11:37:00.000Z] Starting Codex task.\n"
|
||||
'[2026-09-05T11:37:10.000Z] Running command: rg -n "TODO" .\n'
|
||||
"[2026-09-05T11:37:32.037Z] Codex error: You've hit your usage limit. Upgrade to Plus or try again later.\n"
|
||||
"\n[2026-09-05T11:37:32.100Z] Final output\n"
|
||||
"Codex error: You've hit your usage limit. Upgrade to Plus or try again later.\n")
|
||||
code, text, z, antwort = auswerten(ergebnisdatei(tmp, "", status="failed", log=log))
|
||||
return soll(code == 0 and z.get("usage_limit_treffer") == "1", f"Exit {code}: {text}")
|
||||
|
||||
|
||||
@fall("Codex: Log fehlt")
|
||||
def f_codex_ohne_log(tmp):
|
||||
code, text, z, antwort = auswerten(ergebnisdatei(tmp, block(ANTWORT), mit_log=False))
|
||||
@@ -1067,10 +1313,32 @@ MUTANTEN = [
|
||||
("entfernte Datei uebersehen", ' dateien.append({"pfad": rel, "art": "entfernt"})', " pass"),
|
||||
("Pfad ausserhalb des Ordners erlaubt", ' if rel.anchor or ".." in rel.parts:', " if False:"),
|
||||
("Lesemarke im Codex-Auftrag", 'wurzel=teil["wurzel"].as_posix(), anfang=', 'wurzel=teil["wurzel"].as_posix() + " " + marke, anfang='),
|
||||
("usage limit nur klein geschrieben", 'USAGE_LIMIT = re.compile(r"usage limit", re.I)', 'USAGE_LIMIT = re.compile(r"usage limit")'),
|
||||
("usage limit nur klein geschrieben", r'USAGE_LIMIT = re.compile(r"^\[[^\]]+\] Codex error: .*usage limit", re.I | re.M)', r'USAGE_LIMIT = re.compile(r"^\[[^\]]+\] Codex error: .*usage limit", re.M)'),
|
||||
("usage limit ueberall im Log gezaehlt", r'USAGE_LIMIT = re.compile(r"^\[[^\]]+\] Codex error: .*usage limit", re.I | re.M)', 'USAGE_LIMIT = re.compile(r"usage limit", re.I)'),
|
||||
("usage limit ohne Zeitstempel am Zeilenanfang", r'USAGE_LIMIT = re.compile(r"^\[[^\]]+\] Codex error: .*usage limit", re.I | re.M)', 'USAGE_LIMIT = re.compile(r"Codex error: .*usage limit", re.I | re.M)'),
|
||||
("usage limit nur in der ersten Zeile", r'USAGE_LIMIT = re.compile(r"^\[[^\]]+\] Codex error: .*usage limit", re.I | re.M)', r'USAGE_LIMIT = re.compile(r"^\[[^\]]+\] Codex error: .*usage limit", re.I)'),
|
||||
("erster statt letzter Block", "roh.rfind(MARKE_ANFANG)", "roh.find(MARKE_ANFANG)"),
|
||||
("Schwere ungeprueft", ' if felder["schwere"] not in SCHWEREN:\n', " if False:\n"),
|
||||
("Befund ohne beleg-Feld gilt", " leer = [k for k, v in felder.items() if not isinstance(v, str)]", " leer = []"),
|
||||
("Suite: erste statt letzte EXIT-Zeile", "exits[-1][len(", "exits[0][len("),
|
||||
("Suite: 81 statt 80 Zeilen", "SUITE_ZEILEN = 80", "SUITE_ZEILEN = 81"),
|
||||
("Suite: Leerzeile am Ende mitgezaehlt", ' if zeilen[-1] == "":\n', " if False:\n"),
|
||||
("Umbenennung als geaendert", '"diff", "--name-status", "--no-renames", basis', '"diff", "--name-status", basis'),
|
||||
("Hinweis basis weicht fehlt", ' if vorher and vorher["stand"] != basis:\n', " if False:\n"),
|
||||
("Hinweis kein Vorfahr fehlt", ' if git(projekt, "merge-base", "--is-ancestor", basis, stand, pruefen=False).returncode != 0:\n', " if False:\n"),
|
||||
("Stand-Liste unsortiert in der Pruefsumme", r'"\n".join(sorted(stand_liste))', r'"\n".join(stand_liste)'),
|
||||
("dateien in der Zeile", " zeile = {k: ergebnis[k] for k in PAKET_ZEILE}", " zeile = dict(ergebnis)"),
|
||||
("paket_summe ueber paket.md", 'hashlib.sha256((ziel / "paket.json").read_bytes())', 'hashlib.sha256((ziel / "paket.md").read_bytes())'),
|
||||
("ablegen: paket.json unlesbar ohne Befund", 'raise Befund(f"paket.json nicht lesbar in {ordner.as_posix()} ({type(fehler).__name__})")', "raise"),
|
||||
("ablegen: Ziel ueberschrieben", " if ziel.exists():\n", " if False:\n"),
|
||||
("ablegen: Frontmatter ungeprueft", ' if zeilen is None:\n raise Befund("bericht ohne Frontmatter")\n', ' if False:\n raise Befund("bericht ohne Frontmatter")\n'),
|
||||
("ablegen: nummer ungeprueft", ' if werte.get("nummer") != str(daten["nummer"]):\n', " if False:\n"),
|
||||
("ablegen: basis ungeprueft", ' if werte.get("basis") != str(daten["basis"]):\n', " if False:\n"),
|
||||
("ablegen: stand PAKET im Rumpf mitgezaehlt", ' treffer = [i for i, z in enumerate(zeilen) if z == "stand: PAKET"]', r' treffer = [i - 1 for i, z in enumerate(text.split("\n")) if z == "stand: PAKET"]'),
|
||||
("ablegen: stand PAKET zweimal erlaubt", " if len(treffer) != 1:\n", " if not treffer:\n"),
|
||||
("ablegen: fehlendes stand PAKET erlaubt", " if len(treffer) != 1:\n", " if len(treffer) > 1:\n"),
|
||||
("ablegen: Kundendokument bekommt die Pruefsumme", ' if daten["art"] == "doku":\n', " if False:\n"),
|
||||
("ablegen: Ordner nicht angelegt", " ziel.parent.mkdir(parents=True, exist_ok=True)\n", ""),
|
||||
]
|
||||
|
||||
# Mutanten, die bei heutigem git kein Verhalten aendern koennten, stehen hier mit Grund; ein
|
||||
@@ -1135,12 +1403,12 @@ Erwartet: Jede Fallzeile beginnt mit `OK`, die Schlusszeile endet auf `0 geschei
|
||||
python D:/Claude/.vault-tools/quorum/mutanten_quorum_werkzeug.py; echo "EXIT $?"
|
||||
```
|
||||
|
||||
Erwartet: Jede Mutantenzeile beginnt mit `GEFANGEN`, die Schlusszeile endet auf `0 ueberlebt, 0 veraltet`, `EXIT 0`. Der Lauf dauert einige Minuten. Überlebt der Mutant „unversionierte Dateien zusammengefasst“, fasst das installierte git einen unversionierten Ordner trotz Ausschluss nicht zusammen; dann nichts ändern, sondern mit der Ausgabe von `git --version` melden. Der Koordinator trägt ihn mit Grund in `WIRKUNGSLOS` ein.
|
||||
Erwartet: Jede Mutantenzeile beginnt mit `GEFANGEN`, die Schlusszeile endet auf `0 ueberlebt, 0 veraltet`, `EXIT 0`. Der Lauf dauert einige Minuten. Überlebt der Mutant „unversionierte Dateien zusammengefasst“, dann nichts ändern, sondern mit der Ausgabe von `git --version` und `git config --show-origin --get-all status.showUntrackedFiles; echo "EXIT $?"` melden; der Fall „Unversionierte Datei bei status.showUntrackedFiles=no“ soll ihn fangen.
|
||||
|
||||
```
|
||||
git -C D:/Claude status --porcelain -- .vault-tools/quorum/
|
||||
git -C D:/Claude status --porcelain --untracked-files=all -- .vault-tools/quorum/
|
||||
```
|
||||
|
||||
Erwartet: genau die drei Zieldateien als `??`.
|
||||
Erwartet: genau drei Zeilen, `?? .vault-tools/quorum/faelle_quorum_werkzeug.py`, `?? .vault-tools/quorum/mutanten_quorum_werkzeug.py` und `?? .vault-tools/quorum/quorum_werkzeug.py`.
|
||||
|
||||
Melde alle Ausgaben vollständig. Läuft etwas anders als erwartet: nichts anpassen, melden, mit der ersten `FEHLER`-Zeile und ihrem Text.
|
||||
+70
-20
@@ -5,7 +5,7 @@ status: draft
|
||||
summary: "Neuer gespeicherter Workflow quorum: Paket über einen Hilfsagenten, Opus und Sonnet als quorum-stimme und Codex über das Plugin gleichzeitig, Ersatz Haiku, Gruppieren, Nachprüfen, Urteil und Berichtstext."
|
||||
tags: [plan-task, vault-tools, codeworkflow]
|
||||
created: 2026-09-29
|
||||
updated: 2026-09-29
|
||||
updated: 2026-09-30
|
||||
modell: haiku
|
||||
modell_begruendung: "Abschrift: Die Datei steht wörtlich im Plan und wird per plan_materialisiere.py angewendet; Zieldatei und Verbote sind genannt, kein Codeblock trägt typografische Anführungszeichen, Striche oder Auslassungspunkte."
|
||||
---
|
||||
@@ -16,7 +16,7 @@ Die globalen Vorgaben stehen in `00-uebersicht.md` im selben Ordner, Abschnitt
|
||||
|
||||
Was danach anders ist:
|
||||
|
||||
- **Ein Aufruf reicht:** `workflow({ scriptPath: "D:/Claude/.claude/workflows/quorum.js" }, { anlass, projekt, ... })` baut das Paket, lässt drei Stimmen prüfen, gruppiert, prüft nach und gibt Urteil, `bericht_md` und `bericht_pfad` zurück.
|
||||
- **Ein Aufruf reicht:** `workflow({ scriptPath: "D:/Claude/.claude/workflows/quorum.js" }, { anlass, projekt, ... })` baut das Paket, lässt drei Stimmen prüfen, gruppiert, prüft nach und gibt Urteil, `bericht_md`, `bericht_pfad`, `paket` und `ablegen_befehl` zurück.
|
||||
- **Kein Unterschied wird zweimal geprüft:** Ist das Paket leer, übernimmt der Lauf das Urteil des letzten Berichts, ohne eine Stimme zu starten.
|
||||
- **Codex zählt nur mit Beleg:** Status vorher gelesen, Job `completed`, Log ohne „usage limit“, Lesemarke zitiert. Sonst springt Haiku ein, und der Bericht sagt es.
|
||||
|
||||
@@ -49,7 +49,7 @@ Die ganze Datei ist neu:
|
||||
export const meta = {
|
||||
name: 'quorum',
|
||||
description: 'Quorum am Meilenstein: Opus, Sonnet und Codex (Ersatz Haiku) prüfen unabhängig denselben Unterschied seit dem letzten Quorum; was zwei melden, gilt, Einzelbefunde prüft Sonnet nach',
|
||||
whenToUse: 'Vor dem Einspielen, vor Merge und Push einer neuen Minor- oder Major-Version und vor der Übergabe eines Kundendokuments (AI/Regelwerk/codeworkflow.md, "Quorum am Meilenstein"). Args: {anlass: "einspielen"|"version"|"kundendokument", projekt?, basis?, ordner?, dateien?, uebersicht?, kontext?: [pfade], suite?, suite_datei?, codex?: false, codex_runden?: 1..6}. Einspielen und version brauchen projekt (Wurzel eines Git-Repos, sauberer Arbeitsbaum), das erste Quorum dort zusätzlich basis. Kundendokument braucht ordner, dateien (Pfade relativ zu ordner) und uebersicht. Rückgabe mit status fertig, leer, gebremst oder unvollstaendig, urteil frei, nicht_frei oder kein_urteil, dazu bericht_pfad und bericht_md; den Bericht legt der Koordinator ab. Einzelheiten im Kommentar unter meta.',
|
||||
whenToUse: 'Vor dem Einspielen, vor Merge und Push einer neuen Minor- oder Major-Version und vor der Übergabe eines Kundendokuments (AI/Regelwerk/codeworkflow.md, "Quorum am Meilenstein"). Args: {anlass: "einspielen"|"version"|"kundendokument", projekt?, basis?, ordner?, dateien?, uebersicht?, kontext?: [pfade], suite?, suite_datei?, codex?: false, codex_runden?: 1..6}. Einspielen und version brauchen projekt (Wurzel eines Git-Repos, sauberer Arbeitsbaum), das erste Quorum dort zusätzlich basis. suite nur für eine Suite, die in neun Minuten durchläuft und keine unversionierten Dateien schreibt; sonst die Suite selbst fahren und suite_datei nennen. Kundendokument braucht ordner (im Vault unter D:/Claude/), dateien (Pfade relativ zu ordner) und uebersicht. Rückgabe mit status fertig, leer, gebremst oder unvollstaendig, urteil frei, nicht_frei oder kein_urteil, dazu bericht_pfad, bericht_md, paket und ablegen_befehl: bericht_md als bericht.md im Ordner paket speichern und ablegen_befehl fahren, der den Stand aus paket.json einsetzt. Einzelheiten im Kommentar unter meta.',
|
||||
phases: [
|
||||
{ title: 'Paket', detail: 'Hilfsagent baut das Prüfpaket mit quorum_werkzeug.py' },
|
||||
{ title: 'Stimmen', detail: 'Opus und Sonnet als quorum-stimme, Codex über Hilfsagenten, Ersatz Haiku' },
|
||||
@@ -65,6 +65,9 @@ export const meta = {
|
||||
// Paket: Ein Workflow-Skript hat weder Dateisystem noch Shell. Das Prüfpaket baut deshalb ein
|
||||
// Hilfsagent (sonnet, low), der genau einen Befehl fährt: quorum_werkzeug.py paket. Das Skript
|
||||
// entscheidet, ob der Unterschied leer ist; der Hilfsagent überträgt nur die Ausgabe.
|
||||
// Die Zeile QUORUM_PAKET trägt nur Felder fester Größe. Dateiliste und Stand-Liste bleiben in
|
||||
// paket.json; den Stand setzt quorum_werkzeug.py ablegen beim Ablegen des Berichts aus paket.json
|
||||
// ein, der Bericht trägt bis dahin die Zeile "stand: PAKET". So schreibt kein Agent den Stand ab.
|
||||
// Stimmen: Opus und Sonnet laufen als agentType quorum-stimme, Effort aus deren Frontmatter.
|
||||
// Vor der Opus-Stimme liest verbrauch.js mit wunsch 1 ab; gebremst heißt, keine Stimme startet.
|
||||
// Codex über das Plugin codex 1.0.6, nur lesend; cc heißt scripts/codex-companion.mjs:
|
||||
@@ -80,8 +83,10 @@ export const meta = {
|
||||
// - Die Codex-Hilfsagenten laufen auf sonnet mit low: feste Befehle, Ausgaben übertragen. Haiku
|
||||
// nicht, weil Befundtexte typografische Zeichen tragen können, die Haiku still ersetzt.
|
||||
// Zusammenführen: Ein Sonnet-Agent ohne agentType gruppiert nur belegte Befunde; das Skript löst
|
||||
// Gruppen über verschiedene Dateien auf und zählt die Stimmen. Mehrheit heißt zwei verschiedene
|
||||
// Stimmen, die Schwere der Gruppe ist die höchste ihrer Mitglieder.
|
||||
// Gruppen über verschiedene Dateien und über Zeilenangaben, die sich nicht überschneiden, auf und
|
||||
// zählt die Stimmen. Mehrheit heißt zwei verschiedene Stimmen, die Schwere der Gruppe ist die
|
||||
// höchste ihrer Mitglieder. quorum-stimme passt hier nicht, weil dessen Auftragsarten den Auftrag
|
||||
// verfälschen; dass der Agent keine Datei liest, sagt nur sein Auftrag, ein Fall sichert den Satz.
|
||||
// Nachprüfen: quorum-stimme auf sonnet für Gruppen ohne Mehrheit mit Schwere blockierend oder
|
||||
// wichtig. Ein blockierender Einzelbefund ohne Urteil sperrt wie ein bestätigter.
|
||||
// Urteil: frei heißt kein bestätigter und kein ungeprüfter blockierender Befund. Mit weniger als
|
||||
@@ -94,13 +99,14 @@ const KUERZEL = { opus: 'O', sonnet: 'S', codex: 'C', haiku: 'H' }
|
||||
const NAMEN = { opus: 'Opus', sonnet: 'Sonnet', codex: 'Codex', haiku: 'Haiku' }
|
||||
const VERBRAUCH = 'D:/Claude/.claude/workflows/verbrauch.js'
|
||||
const WERKZEUG = 'D:/Claude/.vault-tools/quorum/quorum_werkzeug.py'
|
||||
const VAULT = 'D:/Claude/'
|
||||
const COMPANION = '"$HOME/.claude/plugins/cache/openai-codex/codex/1.0.6/scripts/codex-companion.mjs"'
|
||||
const CODEX_RUNDEN = 3
|
||||
const WARTEN_MS = 540000
|
||||
const OHNE_ABLESUNG = 2
|
||||
const PY = 'PYTHONIOENCODING=utf-8 python -X utf8'
|
||||
const LANGE = 'Setze im Bash-Werkzeug den Parameter timeout auf 600000; der Befehl darf bis zu zehn Minuten laufen.'
|
||||
const PAKET_FELDER = ['ordner', 'anlass', 'art', 'wurzel', 'leer', 'basis', 'stand', 'dateien', 'lesemarke', 'bericht_pfad', 'nummer', 'datum', 'vorher', 'suite', 'hinweise']
|
||||
const PAKET_FELDER = ['ordner', 'anlass', 'art', 'wurzel', 'leer', 'basis', 'stand', 'anzahl_dateien', 'lesemarke', 'bericht_pfad', 'nummer', 'datum', 'vorher', 'suite', 'hinweise', 'paket_summe']
|
||||
const STATUS_TEXT = { mehrheit: 'Mehrheit', bestaetigt: 'bestätigt nach Nachprüfung', widerlegt: 'widerlegt nach Nachprüfung', nicht_geprueft: 'nicht geprüft', hinweis_ungeprueft: 'Hinweis, ungeprüft' }
|
||||
// Einfache Anführungszeichen: In Bash bleiben darin $, Backtick und Backslash wörtlich.
|
||||
const q = s => "'" + String(s).replace(/'/g, "'\\''") + "'"
|
||||
@@ -180,6 +186,7 @@ function pruefeArgs(a) {
|
||||
if (new Set(a.dateien).size !== a.dateien.length) throw new Error('quorum: eine Datei steht doppelt in dateien')
|
||||
const fremd = ['projekt', 'basis', 'suite', 'suite_datei'].filter(k => a[k] !== undefined)
|
||||
if (fremd.length) throw new Error('quorum: ' + fremd.join(', ') + ' gilt nur für einspielen und version')
|
||||
if (vaultPfad(a.ordner) === null) throw new Error('quorum: ordner muss im Vault unter ' + VAULT + ' liegen; der Bericht verlinkt seine Übersicht mit vollem Vault-Pfad')
|
||||
} else {
|
||||
if (a.projekt === undefined) throw new Error('quorum: args fehlen: projekt')
|
||||
const fremd = ['ordner', 'dateien', 'uebersicht'].filter(k => a[k] !== undefined)
|
||||
@@ -209,6 +216,12 @@ function wurzel(a) {
|
||||
return a.anlass === 'kundendokument' ? a.ordner : a.projekt
|
||||
}
|
||||
|
||||
// Pfad relativ zur Vault-Wurzel, ohne Schrägstrich am Ende; null außerhalb des Vaults.
|
||||
function vaultPfad(ordner) {
|
||||
const o = String(ordner).replace(/\\/g, '/').replace(/\/+$/, '')
|
||||
return o.toLowerCase().startsWith(VAULT.toLowerCase()) ? o.slice(VAULT.length) : null
|
||||
}
|
||||
|
||||
const laufHinweise = []
|
||||
function hinweis(text) {
|
||||
laufHinweise.push(text)
|
||||
@@ -222,7 +235,10 @@ function paketBefehl(a) {
|
||||
if (a.kontext && a.kontext.length) teile.push(`--kontext ${a.kontext.map(q).join(' ')}`)
|
||||
if (a.suite_datei) teile.push(`--suite-datei ${q(a.suite_datei)}`)
|
||||
if (a.suite) teile.push('--suite-datei "$T/suite.txt"')
|
||||
const suite = a.suite ? `(cd ${q(a.projekt)} && ${a.suite}) > "$T/suite.txt" 2>&1; echo "EXIT suite: $?" >> "$T/suite.txt"; ` : ''
|
||||
// Schreibt die Suite Dateien in den Arbeitsbaum, scheitert das Paket an "nicht sauber"; die
|
||||
// BEFUND-Zeile davor nennt dann die Suite als Ursache.
|
||||
const baum = `git -C ${q(a.projekt)} status --porcelain --untracked-files=all -- . ':(exclude)docs/quorum'`
|
||||
const suite = a.suite ? `V=$(${baum}); (cd ${q(a.projekt)} && ${a.suite}) > "$T/suite.txt" 2>&1; echo "EXIT suite: $?" >> "$T/suite.txt"; [ "$V" = "$(${baum})" ] || echo "BEFUND: Die Suite hat den Arbeitsbaum verändert; suite_datei nutzen oder ihre Ausgaben in .gitignore aufnehmen"; ` : ''
|
||||
return `T=$(cygpath -m -l "$(mktemp -d)") && ${suite}${teile.join(' ')}; echo "EXIT paket: $?"`
|
||||
}
|
||||
|
||||
@@ -242,7 +258,8 @@ function lesePaket(r) {
|
||||
const fehlt = PAKET_FELDER.filter(k => !p || typeof p !== 'object' || !(k in p))
|
||||
if (fehlt.length) return { fehler: 'QUORUM_PAKET ohne ' + fehlt.join(', ') }
|
||||
if (!/^[0-9a-f]{16}$/.test(p.lesemarke)) return { fehler: 'QUORUM_PAKET mit unbrauchbarer Lesemarke ' + JSON.stringify(p.lesemarke) }
|
||||
if (!Array.isArray(p.dateien) || !Array.isArray(p.hinweise)) return { fehler: 'QUORUM_PAKET: dateien und hinweise müssen Listen sein' }
|
||||
if (!Number.isInteger(p.anzahl_dateien) || p.anzahl_dateien < 0) return { fehler: 'QUORUM_PAKET: anzahl_dateien muss eine ganze Zahl ab 0 sein, ist ' + JSON.stringify(p.anzahl_dateien) }
|
||||
if (!Array.isArray(p.hinweise)) return { fehler: 'QUORUM_PAKET: hinweise muss eine Liste sein' }
|
||||
return { paket: p }
|
||||
}
|
||||
|
||||
@@ -322,7 +339,9 @@ async function codexStimme(a, p) {
|
||||
z.grund = 'Status vor dem Lauf nicht gelesen'
|
||||
return z
|
||||
}
|
||||
const job = /"jobId": "([^"]+)"/.exec(start)
|
||||
// Nur hinter "EXIT status:" suchen: status --json listet frühere Jobs derselben Sitzung samt
|
||||
// request.jobId (job-control.mjs 161-181); die erste Fundstelle wäre dann ein alter Job.
|
||||
const job = /"jobId": "([^"]+)"/.exec(start.slice(start.lastIndexOf('EXIT status:')))
|
||||
if (!/EXIT task: 0(?!\d)/.test(start) || !job) {
|
||||
z.grund = 'Codex-Auftrag nicht gestartet'
|
||||
return z
|
||||
@@ -376,9 +395,25 @@ function pfadnorm(p) {
|
||||
return String(p).replace(/\\/g, '/').replace(/\/+/g, '/').replace(/^\.\//, '').toLowerCase()
|
||||
}
|
||||
|
||||
// Stelle als Zeilenbereich [von, bis], wenn sie nur aus Zeilennummern besteht ("10", "Zeile 10",
|
||||
// "Zeilen 8-12", "L10"); sonst null, dann gilt das Urteil des Zusammenführ-Agenten.
|
||||
function zeilen(stelle) {
|
||||
const m = /^\s*(?:(?:zeilen?|z\.|lines?|l)\s*)?(\d+)(?:\s*(?:bis|to|\W)\s*(\d+))?\s*$/i.exec(String(stelle))
|
||||
if (!m) return null
|
||||
const von = Number(m[1])
|
||||
const bis = m[2] === undefined ? von : Number(m[2])
|
||||
return von <= bis ? [von, bis] : [bis, von]
|
||||
}
|
||||
|
||||
// Jedes Paar von Zeilenbereichen überschneidet sich; Stellen ohne Zeilennummern zählen nicht mit.
|
||||
function ueberschneiden(mitglieder) {
|
||||
const bereiche = mitglieder.map(b => zeilen(b.stelle)).filter(Boolean)
|
||||
return bereiche.every(x => bereiche.every(y => x[0] <= y[1] && y[0] <= x[1]))
|
||||
}
|
||||
|
||||
// Gruppen des Agenten prüfen: unbekannte IDs fallen weg, eine ID zählt nur in ihrer ersten
|
||||
// Gruppe, eine Gruppe über verschiedene Dateien wird aufgelöst. Was keiner Gruppe angehört,
|
||||
// bleibt eine Gruppe für sich.
|
||||
// Gruppe, eine Gruppe über verschiedene Dateien wird aufgelöst, ebenso eine, deren Zeilenangaben
|
||||
// sich nicht überschneiden. Was keiner Gruppe angehört, bleibt eine Gruppe für sich.
|
||||
function gruppenBilden(roh, befunde) {
|
||||
const jeId = new Map(befunde.map(b => [b.id, b]))
|
||||
const vergeben = new Set()
|
||||
@@ -399,6 +434,9 @@ function gruppenBilden(roh, befunde) {
|
||||
if (new Set(mitglieder.map(b => pfadnorm(b.datei))).size > 1) {
|
||||
meldungen.push(`Zusammenführen: Gruppe ${mitglieder.map(b => b.id).join(', ')} aufgelöst: verschiedene Dateien`)
|
||||
mitglieder.forEach(b => gruppen.push([b]))
|
||||
} else if (!ueberschneiden(mitglieder)) {
|
||||
meldungen.push(`Zusammenführen: Gruppe ${mitglieder.map(b => b.id).join(', ')} aufgelöst: Zeilen überschneiden sich nicht (${mitglieder.map(b => b.stelle).join('; ')})`)
|
||||
mitglieder.forEach(b => gruppen.push([b]))
|
||||
} else gruppen.push(mitglieder)
|
||||
}
|
||||
for (const b of befunde) if (!vergeben.has(b.id)) gruppen.push([b])
|
||||
@@ -433,15 +471,21 @@ function urteilText(urteil) {
|
||||
return urteil === 'frei' ? 'frei' : urteil === 'nicht_frei' ? 'nicht frei' : 'kein Urteil'
|
||||
}
|
||||
|
||||
// Die Zeile stand: PAKET ersetzt quorum_werkzeug.py ablegen durch den Stand aus paket.json.
|
||||
function kopf(a, p, urteil, summary, codexFeld) {
|
||||
const stand = Array.isArray(p.stand) ? 'stand:\n' + p.stand.map(s => ` - "${s}"`).join('\n') : 'stand: ' + p.stand
|
||||
return ['---', 'type: note', `title: "Quorum ${p.nummer}: ${a.anlass}"`, 'status: done', `summary: "${summary}"`, 'tags: [quorum]', `created: ${p.datum}`, `updated: ${p.datum}`, `nummer: ${p.nummer}`, `anlass: ${a.anlass}`, `urteil: ${urteil}`, `basis: "${p.basis}"`, stand, `codex: ${codexFeld}`, '---'].join('\n')
|
||||
return ['---', 'type: note', `title: "Quorum ${p.nummer}: ${a.anlass}"`, 'status: done', `summary: "${summary}"`, 'tags: [quorum]', `created: ${p.datum}`, `updated: ${p.datum}`, `nummer: ${p.nummer}`, `anlass: ${a.anlass}`, `urteil: ${urteil}`, `basis: "${p.basis}"`, 'stand: PAKET', `codex: ${codexFeld}`, '---'].join('\n')
|
||||
}
|
||||
|
||||
// Hub-Zeile nach der Hub-Regel (AI/Regelwerk/navigation.md, "Links schreiben und Dateien
|
||||
// verschieben"): im Code-Projekt ein relativer Markdown-Link aufs README, sonst ein Wikilink mit
|
||||
// vollem Vault-Pfad auf die Übersicht des Kundendokuments. Der Bericht liegt in <ordner>/quorum/.
|
||||
function hubZeile(a) {
|
||||
if (a.anlass !== 'kundendokument') return 'Projekt: [README](../../README.md)'
|
||||
const rel = vaultPfad(a.ordner)
|
||||
const teile = rel.split('/')
|
||||
if (teile[0].toLowerCase() === 'code projekte' && teile.length >= 2) return `Projekt: [README](${'../'.repeat(teile.length - 1)}README.md)`
|
||||
const u = a.uebersicht.replace(/\\/g, '/')
|
||||
return `Übersicht: [${u}](<../${u}>)`
|
||||
return `Übersicht: [[${rel}/${u.replace(/\.md$/i, '')}|${u.split('/').pop()}]]`
|
||||
}
|
||||
|
||||
function berichtLeer(a, p, urteil, grund) {
|
||||
@@ -453,7 +497,7 @@ function berichtVoll(a, p, x) {
|
||||
const namen = Object.keys(x.stimmen).filter(n => x.stimmen[n].abgegeben).map(n => NAMEN[n]).join(', ')
|
||||
const codexFeld = x.stimmen.codex.abgegeben ? 'abgegeben' : 'ersetzt'
|
||||
const z = [kopf(a, p, x.urteil, `Urteil ${x.urteil}: ${x.grund}; Stimmen ${namen}.`, codexFeld), '', `# Quorum ${p.nummer}: ${a.anlass}`, '', hubZeile(a), '']
|
||||
z.push('## Urteil', '', `**${urteilText(x.urteil)}**: ${x.grund}.`, '', `Basis ${p.basis}, Stand ${Array.isArray(p.stand) ? 'siehe Frontmatter' : p.stand}; ${p.dateien.length} Datei(en) im Unterschied.`, '')
|
||||
z.push('## Urteil', '', `**${urteilText(x.urteil)}**: ${x.grund}.`, '', `Basis ${p.basis}, Stand ${p.stand}; ${p.anzahl_dateien} Datei(en) im Unterschied.`, '')
|
||||
z.push('## Stimmen', '', '| Stimme | abgegeben | Befunde | belegt | bestätigt | Alleinfunde | Laufzeit |', '|---|---|---|---|---|---|---|')
|
||||
for (const n of Object.keys(x.stimmen)) {
|
||||
const s = x.stimmen[n]
|
||||
@@ -469,7 +513,7 @@ function berichtVoll(a, p, x) {
|
||||
for (const g of x.gruppen) {
|
||||
const erste = g.mitglieder[0]
|
||||
z.push(`### G${g.nr}: ${g.schwere}, ${STATUS_TEXT[g.status]} (${g.stimmen.map(n => NAMEN[n]).join(', ')})`, '', `\`${erste.datei}\`, Stelle ${erste.stelle}`, '')
|
||||
for (const b of g.mitglieder) z.push(`- ${b.id} (${NAMEN[b.stimme]}, ${b.schwere}): ${b.aussage}`, ` Beleg: ${b.beleg}`)
|
||||
for (const b of g.mitglieder) z.push(`- ${b.id} (${NAMEN[b.stimme]}, ${b.schwere}, Stelle ${b.stelle}): ${b.aussage}`, ` Beleg: ${b.beleg}`)
|
||||
if (g.nachpruefung) z.push('', `Nachprüfung: ${g.nachpruefung.urteil}, ${g.nachpruefung.fundstelle}. ${g.nachpruefung.begruendung}`)
|
||||
z.push('')
|
||||
}
|
||||
@@ -485,8 +529,14 @@ function berichtVoll(a, p, x) {
|
||||
return z.join('\n')
|
||||
}
|
||||
|
||||
// Der fertige Befehl für den Koordinator, nachdem er bericht_md als bericht.md im Paketordner
|
||||
// gespeichert hat.
|
||||
function ablegenBefehl(p) {
|
||||
return `${PY} ${q(WERKZEUG)} ablegen --paket ${q(p.ordner)} --bericht ${q(p.ordner + '/bericht.md')} --ziel ${q(p.bericht_pfad)}`
|
||||
}
|
||||
|
||||
function ergebnis(status, urteil, grund, md, stimmen, gruppen, unbelegt, zaehlung, naechster) {
|
||||
return { status, urteil, grund, bericht_pfad: md ? paket.bericht_pfad : null, bericht_md: md, paket: paket.ordner, stimmen, gruppen, unbelegt, zaehlung, verbrauch: ablesungen, hinweise: [...paket.hinweise, ...laufHinweise], naechster_schritt: naechster }
|
||||
return { status, urteil, grund, bericht_pfad: md ? paket.bericht_pfad : null, bericht_md: md, paket: paket.ordner, ablegen_befehl: md ? ablegenBefehl(paket) : null, stimmen, gruppen, unbelegt, zaehlung, verbrauch: ablesungen, hinweise: [...paket.hinweise, ...laufHinweise], naechster_schritt: naechster }
|
||||
}
|
||||
|
||||
pruefeArgs(args)
|
||||
@@ -500,7 +550,7 @@ if (paket.leer) {
|
||||
const urteil = paket.vorher && paket.vorher.urteil === 'frei' ? 'frei' : 'nicht_frei'
|
||||
const grund = `Unterschied leer; Urteil aus Quorum ${paket.vorher ? paket.vorher.nummer : '?'} übernommen, keine Stimme gestartet`
|
||||
log(grund)
|
||||
return ergebnis('leer', urteil, grund, berichtLeer(args, paket, urteil, grund), {}, [], [], null, 'Bericht ablegen und committen; das übernommene Urteil gilt.')
|
||||
return ergebnis('leer', urteil, grund, berichtLeer(args, paket, urteil, grund), {}, [], [], null, 'bericht_md als bericht.md im Ordner paket speichern, ablegen_befehl fahren und committen; das übernommene Urteil gilt.')
|
||||
}
|
||||
|
||||
phase('Stimmen')
|
||||
@@ -599,8 +649,8 @@ gruppen.sort((x, y) => (ORDNUNG[x.status] - ORDNUNG[y.status]) || (RANG[x.schwer
|
||||
gruppen.forEach((g, i) => { g.nr = i + 1 })
|
||||
const md = berichtVoll(args, paket, { urteil, grund, stimmen, zaehlung, gruppen, unbelegt })
|
||||
return ergebnis('fertig', urteil, grund, md, stimmen, gruppen, unbelegt, zaehlung, urteil === 'frei'
|
||||
? 'Bericht unter bericht_pfad ablegen, im README bzw. in der Übersicht verlinken und committen; der Meilenstein ist frei.'
|
||||
: 'Bericht ablegen und committen. Die Korrekturen entscheidet der Koordinator, umgesetzt über die Standardschleife; danach bestätigt eine quorum-stimme auf Sonnet (Auftragsart Nachkontrolle) je Befund, dass er behoben ist.')
|
||||
? 'bericht_md als bericht.md im Ordner paket speichern, ablegen_befehl fahren, den Bericht im README bzw. in der Übersicht verlinken und committen; der Meilenstein ist frei.'
|
||||
: 'bericht_md als bericht.md im Ordner paket speichern, ablegen_befehl fahren und committen. Die Korrekturen entscheidet der Koordinator, umgesetzt über die Standardschleife; danach bestätigt eine quorum-stimme auf Sonnet (Auftragsart Nachkontrolle) je Befund, dass er behoben ist.')
|
||||
```
|
||||
|
||||
## Schritt 2 — Verifikation
|
||||
|
||||
+244
-22
@@ -5,16 +5,17 @@ status: draft
|
||||
summary: "faelle_workflows.mjs bekommt einen Abschnitt quorum mit eigener Antwort-Attrappe, Grundlinie und Angriffsliste; mutanten_workflows.py bekommt die Mutanten, die diese Fälle fangen sollen."
|
||||
tags: [plan-task, vault-tools, codeworkflow]
|
||||
created: 2026-09-29
|
||||
updated: 2026-09-29
|
||||
modell: haiku
|
||||
modell_begruendung: "Abschrift: Beide Blöcke stehen wörtlich im Plan und werden per plan_materialisiere.py angewendet; Zieldateien und Verbote sind genannt, kein Codeblock trägt typografische Anführungszeichen, Striche oder Auslassungspunkte."
|
||||
updated: 2026-09-30
|
||||
modell: sonnet
|
||||
effort: low
|
||||
modell_begruendung: "Abschrift: Beide Blöcke stehen wörtlich im Plan und werden per plan_materialisiere.py angewendet, Zieldateien und Verbote sind genannt; sonnet mit effort low statt haiku, weil ein Fall und ein Mutant einen Gedankenstrich (U+2013) tragen, den Haiku still ersetzt."
|
||||
---
|
||||
|
||||
# Task 03 — Grundlinie, Fälle und Mutanten für `quorum.js`
|
||||
|
||||
Die globalen Vorgaben stehen in `00-uebersicht.md` im selben Ordner, Abschnitt „Globale Vorgaben“. Arbeitsverzeichnis ist `D:/Claude`.
|
||||
|
||||
**Voraussetzung:** Task 02 ist angewendet, `.claude/workflows/quorum.js` liegt im Baum. Deshalb Welle 2.
|
||||
**Voraussetzung:** Task 02 und die Nachbesserung Task 05 sind angewendet: `.claude/workflows/quorum.js` liegt im Baum und trägt Teilgruppen nach Datei und Zeilen, `--summe` im `ablegen_befehl` und `GIT_OPTIONAL_LOCKS=0` im Arbeitsbaum-Vergleich. Deshalb nach Task 05. Schritt 2 braucht außerdem die alte `quorum.js`, die Task 05 in Schritt 2 unter `/tmp/quorum-task05-alt` (Git-Bash) gesichert hat.
|
||||
|
||||
Was danach anders ist: `node .vault-tools/workflow-pruefung/faelle_workflows.mjs` fährt `quorum.js` mit Grundlinie und Angriffsliste gegen die Attrappe, und `mutanten_workflows.py` baut in `quorum.js` die Fehler ein, die diese Fälle fangen sollen. Die Antwort-Attrappe für das Zusammenführen gruppiert wie ein sorgfältiger Agent nach Datei und Stelle; die für die Nachprüfung urteilt, wie der jeweilige Fall es vorgibt.
|
||||
|
||||
@@ -26,6 +27,7 @@ Was danach anders ist: `node .vault-tools/workflow-pruefung/faelle_workflows.mjs
|
||||
## Verbote
|
||||
|
||||
- Keine andere Datei, auch nicht `attrappe.mjs` und keine Datei unter `.claude/workflows/`.
|
||||
- Den Sicherungsordner `/tmp/quorum-task05-alt` nur lesen, nicht ändern und nicht löschen.
|
||||
- Kein Lauf eines echten Workflows, kein Aufruf des Codex-Plugins.
|
||||
- Nichts von Hand schreiben oder umformulieren; ein Widerspruch wird gemeldet.
|
||||
- Kein Commit, kein `git add`.
|
||||
@@ -62,7 +64,7 @@ wird:
|
||||
// Opus zusätzlich einen wichtigen Einzelbefund, Sonnet einen Hinweis.
|
||||
const Q_ARGS = { anlass: 'einspielen', projekt: 'D:/x' }
|
||||
const Q_MARKE = '0123456789abcdef'
|
||||
const qPaket = (x = {}) => ({ ordner: 'C:/tmp/q', anlass: 'einspielen', art: 'code', wurzel: 'D:/x', leer: false, basis: 'a'.repeat(40), stand: 'b'.repeat(40), dateien: [{ pfad: 'src/a.py', art: 'geaendert' }], lesemarke: Q_MARKE, bericht_pfad: 'D:/x/docs/quorum/2026-09-30-einspielen.md', nummer: 2, datum: '2026-09-30', vorher: { pfad: 'D:/x/docs/quorum/2026-09-20-version.md', nummer: 1, urteil: 'frei' }, suite: 'ohne', hinweise: [], ...x })
|
||||
const qPaket = (x = {}) => ({ ordner: 'C:/tmp/q', anlass: 'einspielen', art: 'code', wurzel: 'D:/x', leer: false, basis: 'a'.repeat(40), stand: 'b'.repeat(40), anzahl_dateien: 1, lesemarke: Q_MARKE, bericht_pfad: 'D:/x/docs/quorum/2026-09-30-einspielen.md', nummer: 2, datum: '2026-09-30', vorher: { pfad: 'D:/x/docs/quorum/2026-09-20-version.md', nummer: 1, urteil: 'frei' }, suite: 'ohne', hinweise: [], paket_summe: 'e'.repeat(64), ...x })
|
||||
const qB = (datei, stelle, schwere, beleg = 'zitierte Zeile') => ({ datei, stelle, schwere, aussage: `${schwere} in ${datei} ${stelle}`, beleg })
|
||||
const Q_STIMMEN = {
|
||||
opus: [qB('src/a.py', '10', 'blockierend'), qB('src/a.py', '30', 'wichtig')],
|
||||
@@ -71,6 +73,9 @@ const Q_STIMMEN = {
|
||||
haiku: [qB('src/c.py', '1', 'wichtig')],
|
||||
}
|
||||
const Q_START = '{\n "running": []\n}\nEXIT status: 0\n{\n "jobId": "task-q1",\n "status": "queued"\n}\nEXIT task: 0'
|
||||
// status --json mit einem früheren Hintergrundjob derselben Sitzung: enrichJob gibt dessen request
|
||||
// samt jobId mit aus (Plugin codex 1.0.6, scripts/lib/job-control.mjs 161-181).
|
||||
const Q_START_ALT = '{\n "running": [],\n "latestFinished": {\n "id": "task-alt",\n "status": "completed",\n "request": {\n "jobId": "task-alt"\n }\n }\n}\nEXIT status: 0\n{\n "jobId": "task-q1",\n "status": "queued"\n}\nEXIT task: 0'
|
||||
const Q_WARTEN_FERTIG = '{\n "waitTimedOut": false\n}\nEXIT warten: 0'
|
||||
const Q_WARTEN_OFFEN = '{\n "waitTimedOut": true\n}\nEXIT warten: 0'
|
||||
|
||||
@@ -107,7 +112,10 @@ function antwortenQuorum(k = {}) {
|
||||
if (label.startsWith('codex:warten:')) return lauf(label, k.codexWarten ? k.codexWarten(Number(label.slice('codex:warten:'.length))) : Q_WARTEN_FERTIG)
|
||||
if (label === 'codex:abbruch') return lauf(label, '{\n "status": "cancelled"\n}\nEXIT cancel: 0')
|
||||
if (label === 'codex:ende') return ok({ exit_codex: 0, job_status: 'completed', log_gelesen: true, usage_limit_treffer: 0, antwort_gefunden: true, dauer_s: 192, lesebeleg: 'Lesemarke: ' + Q_MARKE, befunde: stimmen.codex || [], nicht_pruefbar: [], ...(k.codexEnde || {}) })
|
||||
if (label.startsWith('stimme:')) return ok({ lesemarke: Q_MARKE, befunde: stimmen[label.slice('stimme:'.length)] || [], nicht_pruefbar: [] }, 30)
|
||||
if (label.startsWith('stimme:')) {
|
||||
const name = label.slice('stimme:'.length)
|
||||
return ok({ lesemarke: (k.lesemarken || {})[name] || Q_MARKE, befunde: stimmen[name] || [], nicht_pruefbar: (k.nichtPruefbar || {})[name] || [] }, 30)
|
||||
}
|
||||
if (label === 'zusammenfuehren') return ok(k.gruppen ? k.gruppen(aufruf.prompt) : qGruppiere(aufruf.prompt))
|
||||
if (label === 'nachpruefung') return ok(k.urteile ? k.urteile(aufruf.prompt) : qUrteile(aufruf.prompt))
|
||||
throw new Error('unerwarteter Agent ' + label)
|
||||
@@ -150,8 +158,9 @@ fall('quorum', 'Grundlinie: Paket, drei Stimmen gleichzeitig, Mehrheit sperrt, E
|
||||
soll(e && JSON.stringify(e.zaehlung.opus) === JSON.stringify({ abgegeben: true, gesamt: 2, belegt: 2, bestaetigt: 2, alleinfunde: 1 }), 'zaehlung opus ' + JSON.stringify(e && e.zaehlung && e.zaehlung.opus))
|
||||
soll(e && e.zaehlung.sonnet.bestaetigt === 1 && e.zaehlung.sonnet.alleinfunde === 0 && e.zaehlung.codex.bestaetigt === 1 && !('haiku' in e.zaehlung), 'zaehlung ' + JSON.stringify(e && e.zaehlung))
|
||||
soll(e && e.bericht_pfad === 'D:/x/docs/quorum/2026-09-30-einspielen.md', 'bericht_pfad ' + (e && e.bericht_pfad))
|
||||
soll(e && e.paket === 'C:/tmp/q' && e.ablegen_befehl === "PYTHONIOENCODING=utf-8 python -X utf8 'D:/Claude/.vault-tools/quorum/quorum_werkzeug.py' ablegen --paket 'C:/tmp/q' --bericht 'C:/tmp/q/bericht.md' --ziel 'D:/x/docs/quorum/2026-09-30-einspielen.md' --summe '" + 'e'.repeat(64) + "'", 'paket/ablegen_befehl ' + JSON.stringify(e && [e.paket, e.ablegen_befehl]))
|
||||
const md = (e && e.bericht_md) || ''
|
||||
for (const z of ['---\ntype: note\n', '\nnummer: 2\n', '\nanlass: einspielen\n', '\nurteil: nicht_frei\n', `\nbasis: "${'a'.repeat(40)}"\n`, `\nstand: ${'b'.repeat(40)}\n`, '\ncodex: abgegeben\n---\n', 'Projekt: [README](../../README.md)']) soll(md.includes(z), 'Bericht ohne ' + JSON.stringify(z))
|
||||
for (const z of ['---\ntype: note\n', '\nnummer: 2\n', '\nanlass: einspielen\n', '\nurteil: nicht_frei\n', `\nbasis: "${'a'.repeat(40)}"\n`, '\nstand: PAKET\n', '\ncodex: abgegeben\n---\n', 'Projekt: [README](../../README.md)']) soll(md.includes(z), 'Bericht ohne ' + JSON.stringify(z))
|
||||
ohneVerstoss(p, soll)
|
||||
return fehler
|
||||
})
|
||||
@@ -169,6 +178,25 @@ fall('quorum', 'Bericht: Stimmentabelle mit Codex-Laufzeit, Mehrheit zuerst, Ver
|
||||
return fehler
|
||||
})
|
||||
|
||||
fall('quorum', 'Bericht: Befundzeilen mit Stelle und Beleg, Nachprüfung mit Fundstelle, Unbelegt, Nicht prüfbar, Hinweise und Lesemarke', async () => {
|
||||
const { fehler, soll } = pruefe()
|
||||
const stimmen = { opus: [qB('src/a.py', '10', 'blockierend', 'if x > 0:'), qB('src/a.py', '30', 'wichtig', 'return y'), qB('src/d.py', '7', 'blockierend', '')], sonnet: [qB('src/a.py', '10', 'wichtig', 'if x > 0: # S')], codex: [] }
|
||||
const k = { stimmen, paket: { hinweise: ['basis weicht vom Stand des letzten Quorums ab'] }, nichtPruefbar: { sonnet: ['tests/ nicht lesbar'] }, lesemarken: { opus: 'ffffffffffffffff', sonnet: 'Lesemarke: ' + Q_MARKE } }
|
||||
const { ergebnis: e } = await QR(Q_ARGS, k)
|
||||
const md = (e && e.bericht_md) || ''
|
||||
for (const z of [
|
||||
'### G1: blockierend, Mehrheit (Opus, Sonnet)\n\n`src/a.py`, Stelle 10\n',
|
||||
'- O1 (Opus, blockierend, Stelle 10): blockierend in src/a.py 10\n Beleg: if x > 0:\n',
|
||||
'- S1 (Sonnet, wichtig, Stelle 10): wichtig in src/a.py 10\n Beleg: if x > 0: # S\n',
|
||||
'- O2 (Opus, wichtig, Stelle 30): wichtig in src/a.py 30\n Beleg: return y\n\nNachprüfung: bestaetigt, src/a.py:1, zitiert. nachgestellt\n',
|
||||
'## Unbelegt\n\n- O3 (Opus, blockierend) `src/d.py`, Stelle 7: blockierend in src/d.py 7\n',
|
||||
'## Nicht prüfbar\n\n- Sonnet: tests/ nicht lesbar\n',
|
||||
'## Hinweise des Laufs\n\n- basis weicht vom Stand des letzten Quorums ab\n- Opus: Lesemarke fehlt oder weicht ab',
|
||||
]) soll(md.includes(z), 'Bericht ohne ' + JSON.stringify(z))
|
||||
soll(e && !e.hinweise.some(h => h.startsWith('Sonnet:')), 'Sonnet zitiert die Lesemarke samt Zeilenanfang und gilt trotzdem als abweichend: ' + JSON.stringify(e && e.hinweise))
|
||||
return fehler
|
||||
})
|
||||
|
||||
for (const [urteil, erwartet] of [['widerlegt', 'frei'], ['bestaetigt', 'nicht_frei']]) {
|
||||
fall('quorum', `blockierender Einzelbefund ${urteil}: ${erwartet}`, async () => {
|
||||
const { fehler, soll } = pruefe()
|
||||
@@ -211,15 +239,65 @@ fall('quorum', 'Befund ohne Beleg zählt nicht für die Mehrheit und geht nicht
|
||||
return fehler
|
||||
})
|
||||
|
||||
fall('quorum', 'Gruppe über verschiedene Dateien wird aufgelöst: keine Mehrheit', async () => {
|
||||
fall('quorum', 'Gruppe über verschiedene Dateien wird aufgeteilt: keine Mehrheit', async () => {
|
||||
const { fehler, soll } = pruefe()
|
||||
const stimmen = { opus: [qB('src/a.py', '10', 'blockierend')], sonnet: [qB('src/b.py', '10', 'blockierend')], codex: [] }
|
||||
const { ergebnis: e } = await QR(Q_ARGS, { stimmen, gruppen: () => ({ gruppen: [{ ids: ['O1', 'S1'], begruendung: 'zusammen' }] }), urteile: pr => qUrteile(pr, () => 'widerlegt') })
|
||||
soll(e && qGruppe(e, 'mehrheit').length === 0 && e.urteil === 'frei', JSON.stringify(e && [e.urteil, e.gruppen.map(g => g.status)]))
|
||||
soll(e && e.hinweise.some(h => /aufgelöst: verschiedene Dateien/.test(h)), 'kein Hinweis auf die aufgelöste Gruppe: ' + JSON.stringify(e && e.hinweise))
|
||||
soll(e && e.hinweise.some(h => /Gruppe O1, S1 aufgeteilt in O1 \| S1 \(/.test(h)), 'kein Hinweis auf die aufgeteilte Gruppe: ' + JSON.stringify(e && e.hinweise))
|
||||
return fehler
|
||||
})
|
||||
|
||||
// Zwei Stellen mit Zeilennummern gehören zusammen, wenn sich ihre Zeilenbereiche mit einer
|
||||
// Toleranz von drei Zeilen überschneiden; ohne Zeilennummer gilt das Urteil des Agenten.
|
||||
for (const [name, stelleO, stelleS, mehrheit] of [
|
||||
['Zeile 10 und Zeile 250', '10', '250', false],
|
||||
['Zeile 42 und Zeile 43', 'Zeile 42', 'Zeile 43', true],
|
||||
['Zeile 10 und Zeile 13, Toleranz drei', '10', '13', true],
|
||||
['Zeile 10 und Zeile 14, jenseits der Toleranz', '10', '14', false],
|
||||
['Zeilen 10-20 und Zeile 22', 'Zeilen 10-20', 'Zeile 22', true],
|
||||
['Zeilen 10–20 und Z. 22', 'Zeilen 10–20', 'Z. 22', true],
|
||||
['L12 und line 200', 'L12', 'line 200', false],
|
||||
['a.py:42 und a.py:250', 'a.py:42', 'a.py:250', false],
|
||||
['Zeile 42 (Funktion f) und Funktion g, Zeile 250', 'Zeile 42 (Funktion f)', 'Funktion g, Zeile 250', false],
|
||||
['Zeile 10 und ein Abschnitt', '10', 'Abschnitt Einleitung', true],
|
||||
['zwei Abschnitte ohne Zeilennummer', 'Abschnitt Einleitung', 'Abschnitt Einleitung, Satz 2', true],
|
||||
]) {
|
||||
fall('quorum', `Gruppe in derselben Datei, ${name}: ${mehrheit ? 'eine Gruppe, Mehrheit' : 'aufgeteilt, beide nachgeprüft'}`, async () => {
|
||||
const { fehler, soll } = pruefe()
|
||||
const stimmen = { opus: [qB('src/a.py', stelleO, 'blockierend')], sonnet: [qB('src/a.py', stelleS, 'wichtig')], codex: [] }
|
||||
const { ergebnis: e, protokoll: p } = await QR(Q_ARGS, { stimmen, gruppen: () => ({ gruppen: [{ ids: ['O1', 'S1'], begruendung: 'zusammen' }] }), urteile: pr => qUrteile(pr, () => 'widerlegt') })
|
||||
soll(e && qGruppe(e, 'mehrheit').length === (mehrheit ? 1 : 0) && e.urteil === (mehrheit ? 'nicht_frei' : 'frei'), JSON.stringify(e && [e.urteil, e.gruppen.map(g => g.status)]))
|
||||
soll(JSON.stringify(qNachgeprueft(p)) === (mehrheit ? 'null' : '["O1","S1"]'), 'nachgeprüft: ' + JSON.stringify(qNachgeprueft(p)))
|
||||
soll(e && e.hinweise.some(h => /Gruppe O1, S1 aufgeteilt in O1 \| S1 \(/.test(h)) === !mehrheit, 'Hinweise: ' + JSON.stringify(e && e.hinweise))
|
||||
const md = (e && e.bericht_md) || ''
|
||||
for (const z of [`(Opus, blockierend, Stelle ${stelleO}): `, `(Sonnet, wichtig, Stelle ${stelleS}): `]) soll(md.includes(z), 'Bericht ohne ' + JSON.stringify(z))
|
||||
return fehler
|
||||
})
|
||||
}
|
||||
|
||||
// Drei Stimmen, eine Gruppe des Agenten: Sie zerfällt in zusammenhängende Teilgruppen, jede mit
|
||||
// zwei Stimmen ist eine Mehrheit, der Rest geht in die Nachprüfung, jede Aufteilung ist ein Hinweis.
|
||||
for (const [name, stellen, mehrheiten, nachgeprueft, hinweis] of [
|
||||
['Zeilen 10-14, Zeile 12 und 11 bilden eine Mehrheit', [['src/a.py', 'Zeilen 10-14'], ['src/a.py', 'Zeile 12'], ['src/a.py', '11']], ['codex,opus,sonnet'], null, null],
|
||||
['Zeile 42 und 43 sind eine Mehrheit, Zeile 250 steht für sich', [['src/a.py', 'Zeile 42'], ['src/a.py', 'Zeile 43'], ['src/a.py', 'Zeile 250']], ['opus,sonnet'], '["C1"]', /Gruppe O1, S1, C1 aufgeteilt in O1, S1 \| C1 \(/],
|
||||
['Zeile 10, 13 und 16 hängen über die transitive Hülle zusammen', [['src/a.py', 'Zeile 10'], ['src/a.py', 'Zeile 13'], ['src/a.py', 'Zeile 16']], ['codex,opus,sonnet'], null, null],
|
||||
['Stelle ohne Zeilennummer neben getrennten Zeilen steht für sich', [['src/a.py', 'Zeile 10'], ['src/a.py', 'Funktion f'], ['src/a.py', 'Zeile 250']], [], '["O1","S1","C1"]', /Gruppe O1, S1, C1 aufgeteilt in O1 \| S1 \| C1 \(/],
|
||||
['zwei Stellen in a.py sind eine Mehrheit, b.py steht für sich', [['src/a.py', 'Zeile 10'], ['src/a.py', 'Zeile 11'], ['src/b.py', 'Zeile 10']], ['opus,sonnet'], '["C1"]', /Gruppe O1, S1, C1 aufgeteilt in O1, S1 \| C1 \(/],
|
||||
]) {
|
||||
fall('quorum', `Teilgruppen einer Gruppe des Agenten: ${name}`, async () => {
|
||||
const { fehler, soll } = pruefe()
|
||||
const [opus, sonnet, codex] = stellen.map(([datei, stelle]) => [qB(datei, stelle, 'blockierend')])
|
||||
const { ergebnis: e, protokoll: p } = await QR(Q_ARGS, { stimmen: { opus, sonnet, codex }, gruppen: () => ({ gruppen: [{ ids: ['O1', 'S1', 'C1'], begruendung: 'zusammen' }] }), urteile: pr => qUrteile(pr, () => 'widerlegt') })
|
||||
const m = qGruppe(e, 'mehrheit').map(g => g.stimmen.slice().sort().join(','))
|
||||
soll(JSON.stringify(m) === JSON.stringify(mehrheiten), 'Mehrheiten: ' + JSON.stringify(m))
|
||||
soll(JSON.stringify(qNachgeprueft(p)) === String(nachgeprueft), 'nachgeprüft: ' + JSON.stringify(qNachgeprueft(p)))
|
||||
const h = e ? e.hinweise.filter(x => /aufgeteilt in/.test(x)) : []
|
||||
soll(hinweis ? h.length === 1 && hinweis.test(h[0]) : h.length === 0, 'Hinweise: ' + JSON.stringify(h))
|
||||
return fehler
|
||||
})
|
||||
}
|
||||
|
||||
fall('quorum', 'Zusammenführen mit unbekannter und doppelter ID: beide verworfen, keine Mehrheit', async () => {
|
||||
const { fehler, soll } = pruefe()
|
||||
const stimmen = { opus: [qB('src/a.py', '10', 'blockierend')], sonnet: [qB('src/a.py', '10', 'blockierend')], codex: [] }
|
||||
@@ -239,6 +317,17 @@ fall('quorum', 'Zusammenführen ohne Ergebnis: jeder belegte Befund eine Gruppe,
|
||||
return fehler
|
||||
})
|
||||
|
||||
fall('quorum', 'Unabhängigkeit: Stimmen kennen einander nicht, Zusammenführen liest keine Dateien', async () => {
|
||||
const { fehler, soll } = pruefe()
|
||||
const { protokoll: p } = await QR(Q_ARGS, { codexEnde: { job_status: 'failed' } })
|
||||
const stimmen = mitLabel(p, 'stimme:')
|
||||
soll(stimmen.length === 3, 'Stimmen: ' + JSON.stringify(stimmen.map(x => x.label)))
|
||||
for (const s of stimmen) soll(s.prompt.includes('Die anderen Stimmen kennst du nicht.') && !/Opus|Sonnet|Codex|Haiku/.test(s.prompt), s.label + ': ' + s.prompt)
|
||||
const zf = mitLabel(p, 'zusammenfuehren')[0]
|
||||
soll(zf && !zf.agentType && zf.prompt.includes('Lies keine Dateien und fahre keine Befehle; du arbeitest nur mit dieser Liste.'), 'Zusammenführen: ' + (zf && zf.prompt.slice(0, 300)))
|
||||
return fehler
|
||||
})
|
||||
|
||||
for (const [erst, zweit] of [['wichtig', 'blockierend'], ['blockierend', 'wichtig']]) {
|
||||
fall('quorum', `Mehrheitsgruppe aus ${erst} und ${zweit} zählt als blockierend`, async () => {
|
||||
const { fehler, soll } = pruefe()
|
||||
@@ -295,6 +384,20 @@ fall('quorum', 'Codex nie fertig: so viele Warterunden wie codex_runden, dann Ab
|
||||
return fehler
|
||||
})
|
||||
|
||||
fall('quorum', 'Codex nie fertig ohne codex_runden: Vorgabe drei Warterunden', async () => {
|
||||
const { fehler, soll } = pruefe()
|
||||
const { ergebnis: e, protokoll: p } = await QR(Q_ARGS, { codexWarten: () => Q_WARTEN_OFFEN })
|
||||
soll(mitLabel(p, 'codex:warten:').length === 3 && e && /nach 3 Warterunden/.test(e.stimmen.codex.grund), 'Warterunden: ' + mitLabel(p, 'codex:warten:').length + ', grund ' + JSON.stringify(e && e.stimmen.codex.grund))
|
||||
return fehler
|
||||
})
|
||||
|
||||
fall('quorum', 'codex_runden 6 ist zugelassen: sechs Warterunden', async () => {
|
||||
const { fehler, soll } = pruefe()
|
||||
const { fehler: f, protokoll: p } = await QR({ ...Q_ARGS, codex_runden: 6 }, { codexWarten: () => Q_WARTEN_OFFEN })
|
||||
soll(f === null && mitLabel(p, 'codex:warten:').length === 6, 'Fehler ' + f + ', Warterunden: ' + mitLabel(p, 'codex:warten:').length)
|
||||
return fehler
|
||||
})
|
||||
|
||||
fall('quorum', 'Warten scheitert: Abbruch nach der ersten Runde und Haiku', async () => {
|
||||
const { fehler, soll } = pruefe()
|
||||
const { ergebnis: e, protokoll: p } = await QR(Q_ARGS, { codexWarten: () => 'kein Job\nEXIT warten: 1' })
|
||||
@@ -324,12 +427,23 @@ fall('quorum', 'Codex-Befehle: Plugin 1.0.6, nur lesend, Auftrag aus dem Paket,
|
||||
return fehler
|
||||
})
|
||||
|
||||
fall('quorum', 'Codex-Start: jobId nur aus der Ausgabe von task, nicht aus einem früheren Job im Status', async () => {
|
||||
const { fehler, soll } = pruefe()
|
||||
const { ergebnis: e, protokoll: p } = await QR(Q_ARGS, { codexStart: Q_START_ALT })
|
||||
soll(e && e.stimmen.codex.job === 'task-q1' && e.stimmen.codex.abgegeben === true, 'Codex-Stimme ' + JSON.stringify(e && [e.stimmen.codex.job, e.stimmen.codex.grund]))
|
||||
const warten = mitLabel(p, 'codex:warten:1')[0]
|
||||
const ende = mitLabel(p, 'codex:ende')[0]
|
||||
soll(warten && warten.prompt.includes("status 'task-q1' --wait") && ende && ende.prompt.includes("result 'task-q1' --json"), 'Warten oder Ende nicht auf task-q1')
|
||||
soll(p.agents.every(x => !String(x.prompt).includes('task-alt')), 'ein Auftrag nennt den früheren Job')
|
||||
return fehler
|
||||
})
|
||||
|
||||
fall('quorum', 'gebremst vor den Stimmen: keine Stimme, kein Urteil, kein Bericht', async () => {
|
||||
const { fehler, soll } = pruefe()
|
||||
const { ergebnis: e, fehler: f, protokoll: p } = await QR(Q_ARGS, { ablesungen: [ablesung(90, 10)] })
|
||||
soll(f === null, 'Fehler: ' + f)
|
||||
soll(mitLabel(p, 'stimme:').length === 0 && mitLabel(p, 'codex:').length === 0, 'Stimme trotz Bremse: ' + JSON.stringify(p.agents.map(x => x.label)))
|
||||
soll(e && e.status === 'gebremst' && e.urteil === 'kein_urteil' && e.bericht_md === null && e.bericht_pfad === null, JSON.stringify(e && [e.status, e.urteil, e.bericht_pfad]))
|
||||
soll(e && e.status === 'gebremst' && e.urteil === 'kein_urteil' && e.bericht_md === null && e.bericht_pfad === null && e.ablegen_befehl === null, JSON.stringify(e && [e.status, e.urteil, e.bericht_pfad, e.ablegen_befehl]))
|
||||
soll(e && /neu starten/.test(e.naechster_schritt), 'naechster_schritt ' + (e && e.naechster_schritt))
|
||||
return fehler
|
||||
})
|
||||
@@ -353,13 +467,14 @@ fall('quorum', 'als Kind: verbrauch.js wirft, Rückfall, Stimmen laufen', async
|
||||
for (const vorUrteil of ['frei', 'nicht_frei']) {
|
||||
fall('quorum', `leerer Unterschied: nur der Paket-Agent, Urteil ${vorUrteil} übernommen`, async () => {
|
||||
const { fehler, soll } = pruefe()
|
||||
const paket = { leer: true, dateien: [], stand: 'a'.repeat(40), vorher: { pfad: 'D:/x/docs/quorum/2026-09-20-version.md', nummer: 1, urteil: vorUrteil } }
|
||||
const paket = { leer: true, anzahl_dateien: 0, stand: 'a'.repeat(40), vorher: { pfad: 'D:/x/docs/quorum/2026-09-20-version.md', nummer: 1, urteil: vorUrteil } }
|
||||
const { ergebnis: e, fehler: f, protokoll: p } = await QR(Q_ARGS, { paket })
|
||||
soll(f === null, 'Fehler: ' + f)
|
||||
soll(p.agents.length === 1 && p.agents[0].label === 'werkzeug:paket', 'Agenten ' + JSON.stringify(p.agents.map(x => x.label)))
|
||||
soll(e && e.status === 'leer' && e.urteil === vorUrteil && e.verbrauch.length === 0, JSON.stringify(e && [e.status, e.urteil, e.verbrauch]))
|
||||
soll(e && typeof e.ablegen_befehl === 'string' && e.ablegen_befehl.endsWith(" ablegen --paket 'C:/tmp/q' --bericht 'C:/tmp/q/bericht.md' --ziel 'D:/x/docs/quorum/2026-09-30-einspielen.md' --summe '" + 'e'.repeat(64) + "'"), 'ablegen_befehl ' + (e && e.ablegen_befehl))
|
||||
const md = (e && e.bericht_md) || ''
|
||||
for (const z of ['\ncodex: nicht gelaufen\n', `\nurteil: ${vorUrteil}\n`, '2026-09-20-version.md']) soll(md.includes(z), 'Bericht ohne ' + JSON.stringify(z))
|
||||
for (const z of ['\ncodex: nicht gelaufen\n', `\nurteil: ${vorUrteil}\n`, '\nstand: PAKET\n', '2026-09-20-version.md']) soll(md.includes(z), 'Bericht ohne ' + JSON.stringify(z))
|
||||
return fehler
|
||||
})
|
||||
}
|
||||
@@ -376,6 +491,12 @@ for (const [name, k, text] of [
|
||||
['Lesemarke unbrauchbar', { paket: { lesemarke: 'xyz' } }, /unbrauchbarer Lesemarke/],
|
||||
['Feld fehlt', { paketAusgabe: 'QUORUM_PAKET {"ordner":"C:/tmp/q"}\nEXIT paket: 0' }, /QUORUM_PAKET ohne/],
|
||||
['kein JSON', { paketAusgabe: 'QUORUM_PAKET {kaputt}\nEXIT paket: 0' }, /kein JSON/],
|
||||
['paket_summe fehlt', { paket: { paket_summe: undefined } }, /QUORUM_PAKET ohne paket_summe$/],
|
||||
['paket_summe kein Hex', { paket: { paket_summe: 'xyz' } }, /unbrauchbarer paket_summe/],
|
||||
['anzahl_dateien negativ', { paket: { anzahl_dateien: -1 } }, /anzahl_dateien muss eine ganze Zahl ab 0/],
|
||||
['anzahl_dateien als Text', { paket: { anzahl_dateien: '1' } }, /anzahl_dateien muss eine ganze Zahl ab 0/],
|
||||
['hinweise keine Liste', { paket: { hinweise: 'x' } }, /hinweise muss eine Liste/],
|
||||
['Exit 2 trotz Paketzeile', { paketAusgabe: 'QUORUM_PAKET ' + JSON.stringify(qPaket()) + '\nBEFUND: nachgestellt\nEXIT paket: 2' }, /Paket nicht gebaut: BEFUND: nachgestellt/],
|
||||
]) {
|
||||
fall('quorum', `QUORUM_PAKET kaputt (${name}): Fehler, keine Stimme`, async () => {
|
||||
const { fehler, soll } = pruefe()
|
||||
@@ -403,37 +524,57 @@ fall('quorum', 'zwei Stimmen reichen: Opus ohne Ergebnis, Bericht nennt es', asy
|
||||
return fehler
|
||||
})
|
||||
|
||||
fall('quorum', 'Paketbefehl für Code: basis, kontext, suite mit zehn Minuten', async () => {
|
||||
fall('quorum', 'Paketbefehl für Code: basis, kontext, suite mit zehn Minuten, Arbeitsbaum vor und nach der Suite', async () => {
|
||||
const { fehler, soll } = pruefe()
|
||||
const { protokoll: p } = await QR({ ...Q_ARGS, basis: 'v1.2.0', suite: 'pytest -q', kontext: ['D:/x/docs/plans/p/00-uebersicht.md'] }, {})
|
||||
const pk = mitLabel(p, 'werkzeug:paket')[0]
|
||||
for (const z of [
|
||||
`T=$(cygpath -m -l "$(mktemp -d)")`,
|
||||
`(cd 'D:/x' && pytest -q) > "$T/suite.txt" 2>&1; echo "EXIT suite: $?" >> "$T/suite.txt"`,
|
||||
`V=$(GIT_OPTIONAL_LOCKS=0 git -C 'D:/x' status --porcelain --untracked-files=all -- . ':(exclude)docs/quorum'); (cd 'D:/x' && pytest -q) > "$T/suite.txt" 2>&1; echo "EXIT suite: $?" >> "$T/suite.txt"`,
|
||||
`[ "$V" = "$(GIT_OPTIONAL_LOCKS=0 git -C 'D:/x' status --porcelain --untracked-files=all -- . ':(exclude)docs/quorum')" ] || echo "BEFUND: Die Suite hat den Arbeitsbaum verändert`,
|
||||
`paket --anlass einspielen --ziel "$T" --projekt 'D:/x' --basis 'v1.2.0' --kontext 'D:/x/docs/plans/p/00-uebersicht.md' --suite-datei "$T/suite.txt"`,
|
||||
'timeout auf 600000',
|
||||
]) soll(pk && pk.prompt.includes(z), 'Paketbefehl ohne ' + z)
|
||||
return fehler
|
||||
})
|
||||
|
||||
fall('quorum', 'Kundendokument: Paketbefehl, Wurzel für Stimmen und Codex, Stand-Liste und Link auf die Übersicht', async () => {
|
||||
fall('quorum', 'Kundendokument: Paketbefehl, Wurzel für Stimmen und Codex, Stand PAKET und Wikilink auf die Übersicht', async () => {
|
||||
const { fehler, soll } = pruefe()
|
||||
const args = { anlass: 'kundendokument', ordner: 'D:/k', dateien: ['kapitel/01.md', 'kapitel/02 teil.md'], uebersicht: '00 Übersicht.md' }
|
||||
const stand = ['c'.repeat(64) + ' kapitel/01.md', 'd'.repeat(64) + ' kapitel/02 teil.md']
|
||||
const paket = { anlass: 'kundendokument', art: 'doku', wurzel: 'D:/k', basis: 'erstes Quorum', stand, vorher: null, nummer: 1, bericht_pfad: 'D:/k/quorum/2026-09-30-kundendokument.md', dateien: [{ pfad: 'kapitel/01.md', art: 'neu' }, { pfad: 'kapitel/02 teil.md', art: 'neu' }] }
|
||||
const K = 'D:/Claude/IT Workspace/Beispiel/abgabe'
|
||||
const args = { anlass: 'kundendokument', ordner: K, dateien: ['kapitel/01.md', 'kapitel/02 teil.md'], uebersicht: '00 Übersicht.md' }
|
||||
const stand = 'sha256:' + 'c'.repeat(64)
|
||||
const paket = { anlass: 'kundendokument', art: 'doku', wurzel: K, basis: 'erstes Quorum', stand, vorher: null, nummer: 1, bericht_pfad: K + '/quorum/2026-09-30-kundendokument.md', anzahl_dateien: 2 }
|
||||
const { ergebnis: e, fehler: f, protokoll: p } = await QR(args, { paket })
|
||||
soll(f === null && e && e.status === 'fertig', 'Fehler/status: ' + f + ' ' + (e && e.status))
|
||||
const pk = mitLabel(p, 'werkzeug:paket')[0]
|
||||
soll(pk && pk.prompt.includes(`paket --anlass kundendokument --ziel "$T" --ordner 'D:/k' --uebersicht '00 Übersicht.md' --dateien 'kapitel/01.md' 'kapitel/02 teil.md'`) && !pk.prompt.includes('--projekt'), 'Paketbefehl: ' + (pk && pk.prompt))
|
||||
soll(pk && pk.prompt.includes(`paket --anlass kundendokument --ziel "$T" --ordner '${K}' --uebersicht '00 Übersicht.md' --dateien 'kapitel/01.md' 'kapitel/02 teil.md'`) && !pk.prompt.includes('--projekt'), 'Paketbefehl: ' + (pk && pk.prompt))
|
||||
const opus = mitLabel(p, 'stimme:opus')[0]
|
||||
const start = mitLabel(p, 'codex:start')[0]
|
||||
soll(opus && opus.prompt.includes('Wurzel: D:/k') && start && start.prompt.includes(`--cwd 'D:/k'`), 'Wurzel nicht der Ordner')
|
||||
soll(opus && opus.prompt.includes('Wurzel: ' + K) && start && start.prompt.includes(`--cwd '${K}'`), 'Wurzel nicht der Ordner')
|
||||
const md = (e && e.bericht_md) || ''
|
||||
for (const z of [`\nstand:\n - "${stand[0]}"\n - "${stand[1]}"\ncodex: `, '\nbasis: "erstes Quorum"\n', 'Übersicht: [00 Übersicht.md](<../00 Übersicht.md>)']) soll(md.includes(z), 'Bericht ohne ' + JSON.stringify(z))
|
||||
for (const z of ['\nstand: PAKET\ncodex: ', '\nbasis: "erstes Quorum"\n', `Basis erstes Quorum, Stand ${stand}; 2 Datei(en) im Unterschied.`, 'Übersicht: [[IT Workspace/Beispiel/abgabe/00 Übersicht|00 Übersicht.md]]']) soll(md.includes(z), 'Bericht ohne ' + JSON.stringify(z))
|
||||
soll(!md.includes('README'), 'Bericht eines Kundendokuments verlinkt ein README')
|
||||
return fehler
|
||||
})
|
||||
|
||||
for (const [name, ordner, uebersicht, zeile] of [
|
||||
['IT Workspace, Übersicht im Unterordner', 'D:/Claude/IT Workspace/Beispiel/abgabe', 'teil/00 Übersicht.md', 'Übersicht: [[IT Workspace/Beispiel/abgabe/teil/00 Übersicht|00 Übersicht.md]]'],
|
||||
['Backslashes und Schrägstrich am Ende', 'D:\\Claude\\Security - Compliance Projekte\\active - Beispiel\\', '00-uebersicht.md', 'Übersicht: [[Security - Compliance Projekte/active - Beispiel/00-uebersicht|00-uebersicht.md]]'],
|
||||
['Code-Projekt, zwei Ebenen unter der Wurzel', 'D:/Claude/Code Projekte/active - Beispiel/doku/kunde', '00-uebersicht.md', 'Projekt: [README](../../../README.md)'],
|
||||
['Code-Projekt, Wurzel', 'D:/Claude/Code Projekte/active - Beispiel', '00-uebersicht.md', 'Projekt: [README](../README.md)'],
|
||||
]) {
|
||||
fall('quorum', `Kundendokument, Hub-Zeile (${name})`, async () => {
|
||||
const { fehler, soll } = pruefe()
|
||||
const args = { anlass: 'kundendokument', ordner, dateien: ['kapitel/01.md'], uebersicht }
|
||||
const paket = { anlass: 'kundendokument', art: 'doku', wurzel: ordner, basis: 'erstes Quorum', stand: 'sha256:' + 'c'.repeat(64), vorher: null, nummer: 1 }
|
||||
const { ergebnis: e, fehler: f } = await QR(args, { paket })
|
||||
const md = (e && e.bericht_md) || ''
|
||||
soll(f === null && md.includes('\n\n' + zeile + '\n\n'), 'Fehler ' + f + ', Bericht ohne ' + JSON.stringify(zeile))
|
||||
soll(md.split('\n').filter(z => /^(Projekt|Übersicht): /.test(z)).length === 1, 'nicht genau eine Hub-Zeile')
|
||||
return fehler
|
||||
})
|
||||
}
|
||||
|
||||
for (const [name, a, text] of [
|
||||
['anlass fehlt', { projekt: 'D:/x' }, /anlass muss/],
|
||||
['anlass unbekannt', { anlass: 'release', projekt: 'D:/x' }, /anlass muss/],
|
||||
@@ -446,6 +587,7 @@ for (const [name, a, text] of [
|
||||
['Datei doppelt', { anlass: 'kundendokument', ordner: 'D:/k', uebersicht: 'u.md', dateien: ['a.md', 'a.md'] }, /doppelt in dateien/],
|
||||
['uebersicht fehlt', { anlass: 'kundendokument', ordner: 'D:/k', dateien: ['a.md'] }, /args fehlen: uebersicht/],
|
||||
['projekt bei Kundendokument', { anlass: 'kundendokument', ordner: 'D:/k', uebersicht: 'u.md', dateien: ['a.md'], projekt: 'D:/x' }, /projekt gilt nur für einspielen und version/],
|
||||
['ordner außerhalb des Vaults', { anlass: 'kundendokument', ordner: 'D:/k', uebersicht: 'u.md', dateien: ['a.md'] }, /ordner muss im Vault/],
|
||||
['suite und suite_datei', { ...Q_ARGS, suite: 'pytest', suite_datei: 'D:/s.txt' }, /schließen sich aus/],
|
||||
['kontext als Text', { ...Q_ARGS, kontext: 'D:/a.md' }, /kontext muss/],
|
||||
['codex als Text', { ...Q_ARGS, codex: 'nein' }, /codex muss/],
|
||||
@@ -494,13 +636,60 @@ wird:
|
||||
("quorum", "leerer Unterschied startet Stimmen", "if (paket.leer) {", "if (false) {"),
|
||||
("quorum", "unbelegter Befund zaehlt", "belegt: String(b.beleg).trim() !== ''", "belegt: true"),
|
||||
("quorum", "Zusammenfuehren sieht unbelegte Befunde", "zusammenfuehrenAuftrag(belegt)", "zusammenfuehrenAuftrag(alle)"),
|
||||
("quorum", "Gruppe ueber verschiedene Dateien bleibt", "if (new Set(mitglieder.map(b => pfadnorm(b.datei))).size > 1) {", "if (false) {"),
|
||||
("quorum", "Dateien nicht getrennt", "if (datei[i] === datei[j] && bereich[i] && bereich[j]", "if (bereich[i] && bereich[j]"),
|
||||
("quorum", "Schwere der Gruppe ist die niedrigste", ".sort((x, y) => RANG[x] - RANG[y])[0]", ".sort((x, y) => RANG[y] - RANG[x])[0]"),
|
||||
("quorum", "Hinweis wird nachgeprueft", "gruppen.filter(g => !g.mehrheit && g.schwere !== 'hinweis')", "gruppen.filter(g => !g.mehrheit)"),
|
||||
("quorum", "Mehrheitsgruppe wird nachgeprueft", "gruppen.filter(g => !g.mehrheit && g.schwere !== 'hinweis')", "gruppen.filter(g => g.schwere !== 'hinweis')"),
|
||||
("quorum", "ungepruefter blockierender Befund sperrt nicht", "(gilt(g) || g.status === 'nicht_geprueft')", "gilt(g)"),
|
||||
("quorum", "Urteil schon mit einer Stimme", "if (abgegeben.length < 2) {", "if (abgegeben.length < 1) {"),
|
||||
("quorum", "Bericht ohne Urteil", "return ergebnis('unvollstaendig', 'kein_urteil', grund, null,", "return ergebnis('unvollstaendig', 'kein_urteil', grund, 'x',"),
|
||||
("quorum", "jobId aus der ganzen Ausgabe samt Status", "start.slice(start.lastIndexOf('EXIT status:'))", "start"),
|
||||
("quorum", "Teilgruppen ignoriert, Gruppe bleibt ganz", "teile.forEach(t => gruppen.push(t))", "gruppen.push(mitglieder)"),
|
||||
("quorum", "Gruppe bei Aufteilung ganz aufgeloest", "teile.forEach(t => gruppen.push(t))", "(teile.length > 1 ? mitglieder.map(b => [b]) : teile).forEach(t => gruppen.push(t))"),
|
||||
("quorum", "Aufteilung ohne Hinweis", "if (teile.length > 1) meldungen.push(", "if (false) meldungen.push("),
|
||||
("quorum", "Toleranz null", "const TOLERANZ = 3", "const TOLERANZ = 0"),
|
||||
("quorum", "Toleranz zwei", "const TOLERANZ = 3", "const TOLERANZ = 2"),
|
||||
("quorum", "Toleranz vier", "const TOLERANZ = 3", "const TOLERANZ = 4"),
|
||||
("quorum", "Toleranz nur nach unten", "y[0] <= x[1] + TOLERANZ", "y[0] <= x[1]"),
|
||||
("quorum", "keine transitive Huelle", "if (datei[i] === datei[j] && bereich[i]", "if (i === 0 && datei[i] === datei[j] && bereich[i]"),
|
||||
("quorum", "Stelle ohne Zeilennummer bleibt nie bei ihrer Teilgruppe", "if (stuecke.size === 1) verbinde(", "if (false) verbinde("),
|
||||
("quorum", "Stelle ohne Zeilennummer schliesst sich der ersten Teilgruppe an", "if (stuecke.size === 1) verbinde(", "if (stuecke.size >= 1) verbinde("),
|
||||
("quorum", "Stellen ohne Zeilennummer getrennt", "else if (stuecke.size === 0 && gleich.length) verbinde(", "else if (false) verbinde("),
|
||||
("quorum", "a.py:42 nicht gelesen", " || /\\.[a-z0-9]+:(\\d+)(?:\\s*(?:-|–)\\s*(\\d+))?/i.exec(s)\n", ""),
|
||||
("quorum", "Zeilennummer nur am Anfang gelesen", "/(?:^|[^\\p{L}\\d])(?:zeilen?", "/^\\s*(?:zeilen?"),
|
||||
("quorum", "L12 nicht gelesen", "|z\\.|lines?|l)\\s*(\\d+)", "|z\\.|lines?)\\s*(\\d+)"),
|
||||
("quorum", "Gedankenstrich im Bereich nicht gelesen", "(?:-|–|bis|to)\\s*l?", "(?:-|bis|to)\\s*l?"),
|
||||
("quorum", "Zeilenbereich nur erste Zeile", "const bis = m[2] === undefined ? von : Number(m[2])", "const bis = von"),
|
||||
("quorum", "Stelle ohne Zeilen zaehlt als Zeile 0", "if (!m) return null", "if (!m) return [0, 0]"),
|
||||
("quorum", "Befundzeilen der Gruppe fehlen", "for (const b of g.mitglieder) z.push(", "for (const b of []) z.push("),
|
||||
("quorum", "Stelle fehlt in der Befundzeile", ", ${b.schwere}, Stelle ${b.stelle}): ${b.aussage}", ", ${b.schwere}): ${b.aussage}"),
|
||||
("quorum", "Nachpruefungszeile fehlt", "if (g.nachpruefung) z.push(''", "if (false) z.push(''"),
|
||||
("quorum", "Nachpruefung ohne Fundstelle", "${g.nachpruefung.urteil}, ${g.nachpruefung.fundstelle}.", "${g.nachpruefung.urteil}."),
|
||||
("quorum", "Unbelegt-Liste leer", "for (const b of x.unbelegt)", "for (const b of [])"),
|
||||
("quorum", "Nicht pruefbar leer", "(x.stimmen[n].nicht_pruefbar || [])", "([])"),
|
||||
("quorum", "Hinweise des Laufs fehlen im Bericht", "const alleHinweise = [...p.hinweise, ...laufHinweise]", "const alleHinweise = []"),
|
||||
("quorum", "Lesemarke der Claude-Stimmen nicht geprueft", "lesemarke_ok: String(e.lesemarke).includes(p.lesemarke)", "lesemarke_ok: true"),
|
||||
("quorum", "Lesemarke exakt verglichen", "lesemarke_ok: String(e.lesemarke).includes(p.lesemarke)", "lesemarke_ok: String(e.lesemarke) === p.lesemarke"),
|
||||
("quorum", "Stelle fehlt in der Gruppenueberschrift", ", Stelle ${erste.stelle}", ""),
|
||||
("quorum", "Stand statt PAKET im Frontmatter", "'stand: PAKET'", "'stand: ' + p.stand"),
|
||||
("quorum", "ablegen_befehl fehlt", "ablegen_befehl: md ? ablegenBefehl(paket) : null", "ablegen_befehl: null"),
|
||||
("quorum", "ablegen_befehl auch ohne Bericht", "ablegen_befehl: md ? ablegenBefehl(paket) : null", "ablegen_befehl: ablegenBefehl(paket)"),
|
||||
("quorum", "anzahl_dateien nicht geprueft", "if (!Number.isInteger(p.anzahl_dateien) || p.anzahl_dateien < 0) return", "if (false) return"),
|
||||
("quorum", "paket_summe nicht verlangt", "'hinweise', 'paket_summe']", "'hinweise']"),
|
||||
("quorum", "paket_summe nicht geprueft", "if (!/^[0-9a-f]{64}$/.test(p.paket_summe)) return", "if (false) return"),
|
||||
("quorum", "ablegen_befehl ohne summe", " --summe ${q(p.paket_summe)}", ""),
|
||||
("quorum", "Exit des Pakets nicht geprueft", "if (!/EXIT paket: 0(?!\\d)/.test(aus) || !m) {", "if (!m) {"),
|
||||
("quorum", "Vorgabe sechs Warterunden", "const CODEX_RUNDEN = 3", "const CODEX_RUNDEN = 6"),
|
||||
("quorum", "codex_runden 6 abgelehnt", "a.codex_runden <= 6", "a.codex_runden < 6"),
|
||||
("quorum", "Suite ohne Vergleich des Arbeitsbaums", '" ] || echo "BEFUND: Die Suite hat', '" ] && echo "BEFUND: Die Suite hat'),
|
||||
("quorum", "Arbeitsbaum-Vergleich frischt den Index auf", "const baum = `GIT_OPTIONAL_LOCKS=0 git -C", "const baum = `git -C"),
|
||||
("quorum", "ordner ausserhalb des Vaults erlaubt", "if (vaultPfad(a.ordner) === null) throw", "if (false) throw"),
|
||||
("quorum", "Kundendokument im Code-Projekt ohne README-Link", "if (teile[0].toLowerCase() === 'code projekte' && teile.length >= 2) return", "if (false) return"),
|
||||
("quorum", "README-Tiefe um eins daneben", "'../'.repeat(teile.length - 1)", "'../'.repeat(teile.length)"),
|
||||
("quorum", ".md bleibt im Wikilink", "u.replace(/\\.md$/i, '')", "u"),
|
||||
("quorum", "Backslash im Ordner bleibt", "String(ordner).replace(/\\\\/g, '/')", "String(ordner)"),
|
||||
("quorum", "Stimmen kennen einander", "Die anderen Stimmen kennst du nicht.", "Die anderen Stimmen sind Opus, Sonnet und Codex."),
|
||||
("quorum", "Zusammenfuehren darf Dateien lesen", "Lies keine Dateien und fahre keine Befehle; du arbeitest nur mit dieser Liste.", "Lies die Dateien, wenn du sie brauchst."),
|
||||
]
|
||||
```
|
||||
|
||||
@@ -514,6 +703,39 @@ T=$(mktemp -d) && cp D:/Claude/.claude/workflows/*.js "$T"/ && rm "$T/quorum.js"
|
||||
|
||||
Erwartet: `EXIT 1`, dann eine Zahl größer als null (die gescheiterten Quorum-Fälle), dann direkt `ENDE`: Keine andere `FEHLER`-Zeile als die des Quorums.
|
||||
|
||||
Dann die Fälle gegen eine Kopie der Workflows, in der `quorum.js` die Fassung vor Task 05 ist (Sicherung aus Task 05, Schritt 2):
|
||||
|
||||
```
|
||||
ALT="$(cygpath -m -l /tmp)/quorum-task05-alt" && T=$(mktemp -d) && cp D:/Claude/.claude/workflows/*.js "$T"/ && cp "$ALT/quorum.js" "$T/quorum.js" && node D:/Claude/.vault-tools/workflow-pruefung/faelle_workflows.mjs "$(cygpath -m "$T")" > "$T/aus.txt"; echo "EXIT $?"; grep '^FEHLER' "$T/aus.txt"; echo "ENDE"
|
||||
```
|
||||
|
||||
Erwartet: `EXIT 1`, dann genau diese `FEHLER`-Zeilen in dieser Reihenfolge, dann `ENDE`. Jede steht für eine Entscheidung, die Task 05 geändert hat (Teilgruppen, `--summe`, `paket_summe`, `GIT_OPTIONAL_LOCKS=0`):
|
||||
|
||||
```
|
||||
FEHLER quorum: Grundlinie: Paket, drei Stimmen gleichzeitig, Mehrheit sperrt, Einzelbefund nachgeprüft, Bericht
|
||||
FEHLER quorum: Gruppe über verschiedene Dateien wird aufgeteilt: keine Mehrheit
|
||||
FEHLER quorum: Gruppe in derselben Datei, Zeile 10 und Zeile 250: aufgeteilt, beide nachgeprüft
|
||||
FEHLER quorum: Gruppe in derselben Datei, Zeile 42 und Zeile 43: eine Gruppe, Mehrheit
|
||||
FEHLER quorum: Gruppe in derselben Datei, Zeile 10 und Zeile 13, Toleranz drei: eine Gruppe, Mehrheit
|
||||
FEHLER quorum: Gruppe in derselben Datei, Zeile 10 und Zeile 14, jenseits der Toleranz: aufgeteilt, beide nachgeprüft
|
||||
FEHLER quorum: Gruppe in derselben Datei, Zeilen 10-20 und Zeile 22: eine Gruppe, Mehrheit
|
||||
FEHLER quorum: Gruppe in derselben Datei, Zeilen 10–20 und Z. 22: eine Gruppe, Mehrheit
|
||||
FEHLER quorum: Gruppe in derselben Datei, L12 und line 200: aufgeteilt, beide nachgeprüft
|
||||
FEHLER quorum: Gruppe in derselben Datei, a.py:42 und a.py:250: aufgeteilt, beide nachgeprüft
|
||||
FEHLER quorum: Gruppe in derselben Datei, Zeile 42 (Funktion f) und Funktion g, Zeile 250: aufgeteilt, beide nachgeprüft
|
||||
FEHLER quorum: Teilgruppen einer Gruppe des Agenten: Zeilen 10-14, Zeile 12 und 11 bilden eine Mehrheit
|
||||
FEHLER quorum: Teilgruppen einer Gruppe des Agenten: Zeile 42 und 43 sind eine Mehrheit, Zeile 250 steht für sich
|
||||
FEHLER quorum: Teilgruppen einer Gruppe des Agenten: Zeile 10, 13 und 16 hängen über die transitive Hülle zusammen
|
||||
FEHLER quorum: Teilgruppen einer Gruppe des Agenten: Stelle ohne Zeilennummer neben getrennten Zeilen steht für sich
|
||||
FEHLER quorum: Teilgruppen einer Gruppe des Agenten: zwei Stellen in a.py sind eine Mehrheit, b.py steht für sich
|
||||
FEHLER quorum: leerer Unterschied: nur der Paket-Agent, Urteil frei übernommen
|
||||
FEHLER quorum: leerer Unterschied: nur der Paket-Agent, Urteil nicht_frei übernommen
|
||||
FEHLER quorum: QUORUM_PAKET kaputt (paket_summe kein Hex): Fehler, keine Stimme
|
||||
FEHLER quorum: Paketbefehl für Code: basis, kontext, suite mit zehn Minuten, Arbeitsbaum vor und nach der Suite
|
||||
```
|
||||
|
||||
Die beiden Fälle „Zeile 10 und ein Abschnitt“ und „zwei Abschnitte ohne Zeilennummer“ sind auch gegen die alte Fassung grün; sie sichern, dass Stellen ohne Zeilennummer beim Urteil des Agenten bleiben. Scheitert `cp "$ALT/quorum.js"`, fehlt die Sicherung aus Task 05: anhalten und melden.
|
||||
|
||||
## Schritt 3 — Verifikation
|
||||
|
||||
```
|
||||
|
||||
+51
-6
@@ -20,8 +20,9 @@ Was danach anders ist:
|
||||
- **`quorum-stimme`** gibt es als Agenten: nur lesend (Read, Grep, Glob), Opus mit `high` im Frontmatter, im Workflow auch auf Sonnet und Haiku. Die Agentendatei trägt die drei Auftragsarten, die der Workflow und der Koordinator verwenden: Stimme, Nachprüfung, Nachkontrolle.
|
||||
- **Der Stamm** führt die Stimme in der Rollentabelle, damit `regelwerk_check.py` Modell und Effort prüfen kann (erste Backtick-Angabe der Modellspalte `opus`, der Effortspalte `high`), und nennt `quorum` bei den gespeicherten Workflows.
|
||||
- **`codeworkflow.md`** hat den Abschnitt „Quorum am Meilenstein“ als den einen Ort der Regel; Schritt 6 und die Workflow-Liste unter „Effort je Rolle“ verweisen darauf.
|
||||
- **Der Stamm** bekommt außerdem eine Zeile im Regelwerk-Verzeichnis, die das Quorum auch außerhalb von Code-Projekten auslöst, vor allem vor der Abgabe eines Kundendokuments. Pascal hat sie am 2026-09-30 angeordnet, weil `codeworkflow.md` sonst nur bei Code-Arbeit geladen wird.
|
||||
|
||||
`CLAUDE.md` und `codeworkflow.md` tragen im Arbeitsbaum Änderungen einer anderen Sitzung, die noch nicht committet sind. Die Aus-Blöcke zitieren diesen Stand vom 2026-09-29. Steht ein Aus-Block nicht genau einmal im Baum, bricht Schritt 1 ab; dann nichts von Hand ändern, sondern melden.
|
||||
Die Aus-Blöcke zitieren den Stand vom 2026-09-29; der Auto-Push hat ihn am 2026-09-30 um 04:00 committet, und `plan_bloecke.py vor` war danach ohne Befund. Parallele Sitzungen können `CLAUDE.md` und `codeworkflow.md` weiter ändern. Steht ein Aus-Block nicht genau einmal im Baum, bricht Schritt 1 ab; dann nichts von Hand ändern, sondern melden.
|
||||
|
||||
## Zieldateien
|
||||
|
||||
@@ -54,7 +55,7 @@ Erwartet für den zweiten Befehl: `0 Befund(e).` Sonst abbrechen und melden.
|
||||
python D:/Claude/.vault-tools/plan-kopie/plan_materialisiere.py D:/Claude/.vault-tools/workflow-pruefung/docs/plans/2026-09-29-quorum D:/Claude task-04-stimme-und-regel.md
|
||||
```
|
||||
|
||||
Erwartet: `'neu': 1`, `'ersetzt': 5` und `0 Befund(e).`
|
||||
Erwartet: `'neu': 1`, `'ersetzt': 8` und `0 Befund(e).`
|
||||
|
||||
### `.claude/agents/quorum-stimme.md`
|
||||
|
||||
@@ -194,9 +195,9 @@ Regelwerk-Änderungen im Vault lösen kein Quorum aus.
|
||||
|
||||
**Ersatz.** Gibt Codex keine gültige Stimme ab, springt Haiku ein. Gültig heißt: Der Status des Plugins ist vor dem Lauf gelesen, der Job endet `completed`, sein Log ist gelesen und trägt kein „usage limit“, und die Antwort zitiert die Lesemarke aus dem Paket. Mit `codex: false` springt Haiku sofort ein, etwa wenn das Kontingent bekannt aufgebraucht ist. Der Bericht sagt, wenn Haiku statt Codex drinsteht. Steht das oft da, ist das Quorum faktisch nur mit Claude besetzt.
|
||||
|
||||
**Sperre.** Solange ein bestätigter blockierender Befund offen ist oder der Lauf kein Urteil hat, gibt es kein Merge, keinen Push, kein Einspielen und keine Abgabe. Die Korrekturen entscheidest du, umgesetzt werden sie über die Standardschleife. Danach läuft kein neues Quorum: Eine `quorum-stimme` auf Sonnet, über das Agent-Werkzeug mit der Auftragsart Nachkontrolle gestartet, bestätigt je behobenem Befund, dass er behoben ist. Den Korrektur-Diff prüft das nächste Quorum mit, weil dessen Basis der Stand dieses Laufs ist.
|
||||
**Sperre.** Solange das Urteil nicht frei ist, also ein bestätigter oder ungeprüft gebliebener blockierender Befund offen ist oder der Lauf kein Urteil hat, gibt es kein Merge, keinen Push, kein Einspielen und keine Abgabe. Die Korrekturen entscheidest du, umgesetzt werden sie über die Standardschleife. Danach läuft kein neues Quorum: Eine `quorum-stimme` auf Sonnet, über das Agent-Werkzeug mit der Auftragsart Nachkontrolle gestartet, bestätigt je behobenem Befund, dass er behoben ist. Den Korrektur-Diff prüft das nächste Quorum mit, weil dessen Basis der Stand dieses Laufs ist.
|
||||
|
||||
**Bericht.** Der Workflow liefert `bericht_md` und `bericht_pfad`, du legst die Datei ab und committest sie: im Code-Projekt unter `docs/quorum/`, beim Kundendokument unter `quorum/` im Projektordner, Dateiname `JJJJ-MM-TT-<anlass>.md`. Der Bericht verlinkt README bzw. Übersicht. Sein Frontmatter trägt `nummer`, `basis` und `stand`, und daran setzt der nächste Lauf an. Ein Lauf ohne Urteil liefert keinen Bericht, die Basis bleibt dann die alte. Je Stimme stehen darin Befunde, bestätigte und Alleinfunde, also bestätigte Befunde, die nur diese Stimme hatte, dazu die Codex-Laufzeit und der Verbrauch vor und nach dem Lauf.
|
||||
**Bericht.** Der Workflow liefert `bericht_md`, `bericht_pfad` und `ablegen_befehl`. Du schreibst `bericht_md` in eine Datei und legst sie mit dem Befehl ab. Er setzt `stand` aus `paket.json` ein, damit die Liste der Prüfsummen durch kein Modell abgeschrieben werden muss. Danach committest du den Bericht: im Code-Projekt unter `docs/quorum/`, beim Kundendokument unter `quorum/` im Projektordner, Dateiname `JJJJ-MM-TT-<anlass>.md`. Der Bericht verlinkt README bzw. Übersicht. Sein Frontmatter trägt `nummer`, `basis` und `stand`, und daran setzt der nächste Lauf an. Ein Lauf ohne Urteil liefert keinen Bericht, die Basis bleibt dann die alte. Je Stimme stehen darin Befunde, bestätigte und Alleinfunde, also bestätigte Befunde, die nur diese Stimme hatte, dazu die Codex-Laufzeit und der Verbrauch vor und nach dem Lauf.
|
||||
|
||||
**Was bleibt:** die Echtgeld-Regeln und die volle Schleife für Geld-, Sicherheits- und Netzpfade. Gekürzt wird erst, wenn die Ertragskurve es trägt. Nach fünf Quoren liest du die Berichte, schreibst die Auswertung als Dump und legst Pascal einen Vorschlag hin. Die Frage dabei: Findet das Quorum, was die Wellenprüfung übersieht, und welche Stimme trägt die Alleinfunde? Über Kürzungen entscheidet Pascal.
|
||||
|
||||
@@ -217,6 +218,50 @@ wird:
|
||||
`planung` für die Erstplanung, `verbrauch` für die Ablesung des Planverbrauchs und `quorum` für die Prüfung am Meilenstein (siehe „Quorum am Meilenstein“ oben).
|
||||
```
|
||||
|
||||
### Auslöser im Regelwerk-Verzeichnis von `CLAUDE.md`
|
||||
|
||||
Aus:
|
||||
|
||||
```
|
||||
- Produktivdatei anlegen oder anfassen → [[AI/Regelwerk/codedoku|codedoku.md]]
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```
|
||||
- Meilenstein: vor dem Einspielen, vor einer neuen Minor- oder Major-Version auf master oder vor der Abgabe eines Kundendokuments → Quorum nach [[AI/Regelwerk/codeworkflow|codeworkflow.md]], Abschnitt „Quorum am Meilenstein“ — auch außerhalb von Code-Projekten.
|
||||
- Produktivdatei anlegen oder anfassen → [[AI/Regelwerk/codedoku|codedoku.md]]
|
||||
```
|
||||
|
||||
### agentType-Liste unter „Effort je Rolle“ in `AI/Regelwerk/codeworkflow.md`
|
||||
|
||||
Aus:
|
||||
|
||||
```
|
||||
Den Effort liefert mit `agentType` (`planer`, `pruefer`, `executer`, `recherche`) dessen Frontmatter
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```
|
||||
Den Effort liefert mit `agentType` (`planer`, `pruefer`, `executer`, `recherche`, `quorum-stimme`) dessen Frontmatter
|
||||
```
|
||||
|
||||
### „Wo das gilt“ unter „Verbrauch und Opus-Plätze“ in `AI/Regelwerk/codeworkflow.md`
|
||||
|
||||
Aus:
|
||||
|
||||
```
|
||||
- **`planung`:** Vorgegeben sind zwei Plätze, nach Verbrauch gesenkt und nach je vier fertigen Planern neu gelesen.
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```
|
||||
- **`planung`:** Vorgegeben sind zwei Plätze, nach Verbrauch gesenkt und nach je vier fertigen Planern neu gelesen.
|
||||
- **`quorum`:** Liest vor der Opus-Stimme mit `wunsch` eins ab. Gebremst heißt: keine Stimme, kein Urteil, Neustart nach dem Fenster.
|
||||
```
|
||||
|
||||
## Schritt 2 — Verifikation
|
||||
|
||||
```
|
||||
@@ -229,7 +274,7 @@ Erwartet: `0 Befund(e).`
|
||||
python D:/Claude/.vault-tools/regelwerk-check/regelwerk_check.py --vault D:/Claude > "$TEMP/regelwerk-nachher.txt"; echo "EXIT $?"; diff "$TEMP/regelwerk-vorher.txt" "$TEMP/regelwerk-nachher.txt" && echo "GLEICH"; grep -n -e 'quorum-stimme' -e 'Rollentabelle' "$TEMP/regelwerk-nachher.txt"; echo "ENDE"
|
||||
```
|
||||
|
||||
Erwartet: derselbe Exit wie in Schritt 0, `GLEICH`, danach direkt `ENDE`: Die Ausgabe hat sich nicht verändert, und keine Befundzeile nennt `quorum-stimme` oder die Rollentabelle.
|
||||
Erwartet: derselbe Exit wie in Schritt 0, `GLEICH`, danach direkt `ENDE`: Die Ausgabe hat sich nicht verändert, und keine Befundzeile nennt `quorum-stimme` oder die Rollentabelle. Zwei andere Sitzungen können parallel an `CLAUDE.md` und `AI/Regelwerk/` arbeiten. Zeigt `diff` Unterschiede, meldest du sie wörtlich; ein Befund dieses Tasks ist nur eine neue Zeile, die `quorum-stimme`, die Rollentabelle, `CLAUDE.md` oder `codeworkflow.md` nennt. Das entscheidet der Koordinator.
|
||||
|
||||
```
|
||||
python -X utf8 -c "import sys; t=open(sys.argv[1],encoding='utf-8').read(); print(t.count('\r'), t.startswith('---\nname: quorum-stimme\n'))" D:/Claude/.claude/agents/quorum-stimme.md
|
||||
@@ -241,6 +286,6 @@ Erwartet: `0 True`.
|
||||
git -C D:/Claude status --porcelain -- .claude/agents/quorum-stimme.md CLAUDE.md AI/Regelwerk/codeworkflow.md
|
||||
```
|
||||
|
||||
Erwartet: `?? .claude/agents/quorum-stimme.md`, dazu ` M CLAUDE.md` und ` M AI/Regelwerk/codeworkflow.md` (beide waren schon vorher geändert).
|
||||
Erwartet: `?? .claude/agents/quorum-stimme.md`, dazu ` M CLAUDE.md` und ` M AI/Regelwerk/codeworkflow.md`.
|
||||
|
||||
Melde alle Ausgaben vollständig. Läuft etwas anders als erwartet: nichts anpassen, melden.
|
||||
+568
@@ -0,0 +1,568 @@
|
||||
---
|
||||
type: plan-task
|
||||
title: "Task 05 — Nachbesserung: Teilgruppen nach Zeilen, --summe beim Ablegen, git ohne Index-Schreiben"
|
||||
status: draft
|
||||
summary: "Nachbesserung nach Welle 1: quorum.js teilt jede Gruppe des Zusammenführ-Agenten in Teilgruppen nach Datei und Zeilen (Toleranz drei Zeilen), ablegen prüft --summe gegen paket.json, und jeder git-Aufruf des Paketbaus läuft mit GIT_OPTIONAL_LOCKS=0."
|
||||
tags: [plan-task, vault-tools, codeworkflow]
|
||||
created: 2026-09-30
|
||||
updated: 2026-09-30
|
||||
modell: sonnet
|
||||
effort: low
|
||||
modell_begruendung: "Abschrift per plan_materialisiere.py mit genannten Zieldateien und Verboten; sonnet mit effort low statt haiku, weil ein Codeblock in quorum.js einen Gedankenstrich (U+2013) in einer Regex trägt, den Haiku still ersetzt."
|
||||
---
|
||||
|
||||
Übersicht: [00-uebersicht.md](00-uebersicht.md)
|
||||
|
||||
# Task 05 — Nachbesserung: Teilgruppen nach Zeilen, `--summe` beim Ablegen, git ohne Index-Schreiben
|
||||
|
||||
Die globalen Vorgaben stehen in `00-uebersicht.md` im selben Ordner, Abschnitt „Globale Vorgaben“. Arbeitsverzeichnis ist `D:/Claude`.
|
||||
|
||||
**Voraussetzung:** Task 01 und Task 02 sind angewendet, ihre Dateien liegen im Baum. Task 03 läuft erst nach diesem Task: Seine Fälle und Mutanten setzen die Änderungen an `quorum.js` voraus, und sein Rot-Nachweis nimmt die alte `quorum.js`, die Schritt 2 hier sichert.
|
||||
|
||||
## Anlass
|
||||
|
||||
Die Prüfung nach Welle 1 hat drei Befunde gemeldet. Die Umsetzung war plangetreu; die Befunde richten sich an den Plan.
|
||||
|
||||
| Befund | Schwere | Änderung in diesem Task |
|
||||
|---|---|---|
|
||||
| Mehrheit geht verloren: `ueberschneiden()` verlangt, dass sich jedes Paar von Zeilenbereichen überschneidet, `gruppenBilden()` löst eine Gruppe sonst ganz auf; „Zeile 42“ gegen „Zeile 43“ wird aufgelöst, Stellen in Prosa bleiben ungeprüft gruppiert | wichtig | `zeilen()` liest jede übliche Form; neue Funktion `teilgruppen()` teilt jede Gruppe in zusammenhängende Teilgruppen nach Datei und Zeilen mit einer Toleranz von drei Zeilen; jede Aufteilung steht als Hinweis im Bericht |
|
||||
| Paketbau schreibt `.git/index` (git status frischt die Stat-Daten auf) | klein | `git()` in `quorum_werkzeug.py` setzt `GIT_OPTIONAL_LOCKS=0`; ebenso der Arbeitsbaum-Vergleich um die Suite in `quorum.js` |
|
||||
| `paket_summe` ist Pflicht, prüft aber niemand | klein | `ablegen_befehl` trägt `--summe <paket_summe>`, `ablegen` vergleicht sie mit der SHA-256 von `paket.json` und endet sonst mit `BEFUND`, Exit 2; `quorum.js` verlangt 64 Hex-Zeichen |
|
||||
|
||||
Die Regeln der Teilgruppen, wie sie die Blöcke unten umsetzen:
|
||||
|
||||
- Zwei Stellen gehören nur zusammen, wenn sie dieselbe Datei nennen.
|
||||
- Zwei Stellen mit Zeilennummern gehören zusammen, wenn sich ihre Zeilenbereiche mit einer Toleranz von drei Zeilen überschneiden („Zeile 42“ und „Zeile 45“ ja, „Zeile 42“ und „Zeile 46“ nicht). Die Gruppe zerfällt in die transitive Hülle dieser Beziehung.
|
||||
- Zeilennummern werden gelesen aus „12“, „12-14“, „Zeile 12“, „Zeilen 10-14“, „Zeilen 10–14“, „Z. 10 bis 14“, „L12“, „line 12“, „a.py:42“ und aus denselben Formen mitten im Text wie „Zeile 42 (Funktion f)“.
|
||||
- Eine Stelle ohne Zeilennummer folgt dem Urteil des Agenten: Sie bleibt bei den Stellen ihrer Datei, solange deren Zeilen eine einzige Teilgruppe bilden oder die Datei gar keine Zeilennummer nennt. Zerfallen die Zeilen in mehrere Teilgruppen, steht sie für sich.
|
||||
- Jede Teilgruppe mit zwei verschiedenen Stimmen ist eine Mehrheit; eine Teilgruppe mit nur einer Stimme geht wie bisher in die Nachprüfung.
|
||||
- Jede Aufteilung steht als Hinweis im Bericht: `Zusammenführen: Gruppe O1, S1, C1 aufgeteilt in O1, S1 | C1 (O1 src/a.py Zeile 42; …)`.
|
||||
|
||||
Die Fälle und Mutanten für `quorum.js` stehen in Task 03 (`faelle_workflows.mjs`, `mutanten_workflows.py`), der noch nicht ausgeführt ist. Deshalb sieht dieser Task das Rot nur für `quorum_werkzeug.py`; das Rot für `quorum.js` beobachtet Task 03, Schritt 2, an der hier gesicherten alten Fassung.
|
||||
|
||||
## Zieldateien
|
||||
|
||||
- `D:\Claude\.vault-tools\quorum\faelle_quorum_werkzeug.py` (geändert)
|
||||
- `D:\Claude\.vault-tools\quorum\mutanten_quorum_werkzeug.py` (geändert)
|
||||
- `D:\Claude\.vault-tools\quorum\quorum_werkzeug.py` (geändert)
|
||||
- `D:\Claude\.claude\workflows\quorum.js` (geändert)
|
||||
|
||||
## Verbote
|
||||
|
||||
- Keine andere Datei im Vault, auch nicht `faelle_workflows.mjs`, `mutanten_workflows.py`, `attrappe.mjs` und die übrigen Workflows unter `.claude/workflows/`.
|
||||
- Außerhalb des Vaults nur der Sicherungsordner aus Schritt 2 (`/tmp/quorum-task05-alt` in der Git-Bash). Ihn nicht löschen: Task 03 braucht ihn.
|
||||
- Kein Lauf eines echten Workflows, kein Aufruf des Codex-Plugins.
|
||||
- Nichts von Hand schreiben oder umformulieren; ein Widerspruch zwischen Plan und Baum wird gemeldet.
|
||||
- Kein Commit, kein `git add`.
|
||||
|
||||
## Schritt 1 — Vorprüfung
|
||||
|
||||
```
|
||||
python D:/Claude/.vault-tools/plan-kopie/plan_bloecke.py vor D:/Claude/.vault-tools/workflow-pruefung/docs/plans/2026-09-29-quorum/task-05-nachbesserung-gruppen.md --projekt D:/Claude
|
||||
```
|
||||
|
||||
Erwartet: `0 Befund(e).` Bei einem Befund anhalten und melden.
|
||||
|
||||
## Schritt 2 — Alte Fassung sichern
|
||||
|
||||
```
|
||||
ALT="$(cygpath -m -l /tmp)/quorum-task05-alt" && mkdir "$ALT" && cp D:/Claude/.claude/workflows/quorum.js D:/Claude/.vault-tools/quorum/quorum_werkzeug.py "$ALT"/ && ls "$ALT"; echo "EXIT $?"; echo "GIT_OPTIONAL_LOCKS=${GIT_OPTIONAL_LOCKS-ungesetzt}"
|
||||
```
|
||||
|
||||
Erwartet: `quorum.js`, `quorum_werkzeug.py`, `EXIT 0` und eine Zeile `GIT_OPTIONAL_LOCKS=…`; ihren Wert meldest du mit. Scheitert `mkdir`, weil der Ordner schon da ist: anhalten und melden, nichts löschen.
|
||||
|
||||
## Schritt 3 — Blöcke anwenden
|
||||
|
||||
```
|
||||
python D:/Claude/.vault-tools/plan-kopie/plan_materialisiere.py D:/Claude/.vault-tools/workflow-pruefung/docs/plans/2026-09-29-quorum D:/Claude task-05-nachbesserung-gruppen.md
|
||||
```
|
||||
|
||||
Erwartet: `'ersetzt': 20` und `0 Befund(e).`
|
||||
|
||||
### Fälle in `.vault-tools/quorum/faelle_quorum_werkzeug.py`
|
||||
|
||||
Die Fälle starten das Skript ohne ein `GIT_OPTIONAL_LOCKS` aus der aufrufenden Shell; sonst sähe der Index-Fall auch bei einem Skript ohne die Variable grün aus.
|
||||
|
||||
Aus:
|
||||
|
||||
```
|
||||
umgebung = dict(os.environ)
|
||||
umgebung["PYTHONIOENCODING"] = "utf-8"
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```python
|
||||
umgebung = dict(os.environ)
|
||||
umgebung["PYTHONIOENCODING"] = "utf-8"
|
||||
# Das Skript muss GIT_OPTIONAL_LOCKS selbst setzen; ein Wert aus der aufrufenden Shell verdeckte das.
|
||||
umgebung.pop("GIT_OPTIONAL_LOCKS", None)
|
||||
```
|
||||
|
||||
Der Helfer für `ablegen` übergibt `--summe`; ohne Angabe die SHA-256 des `paket.json` im Paketordner.
|
||||
|
||||
Aus:
|
||||
|
||||
```
|
||||
def ablegen(paket_ordner, bericht_datei, ziel):
|
||||
code, aus, err = fahre("ablegen", "--paket", paket_ordner, "--bericht", bericht_datei, "--ziel", ziel)
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```python
|
||||
def ablegen(paket_ordner, bericht_datei, ziel, paket_summe=None):
|
||||
"""ablegen mit --summe; ohne Angabe die SHA-256 von paket.json im Paketordner, Nullen, wenn es fehlt."""
|
||||
if paket_summe is None:
|
||||
pfad = Path(paket_ordner) / "paket.json"
|
||||
paket_summe = summe(pfad) if pfad.exists() else "0" * 64
|
||||
code, aus, err = fahre("ablegen", "--paket", paket_ordner, "--bericht", bericht_datei, "--ziel", ziel, "--summe", paket_summe)
|
||||
```
|
||||
|
||||
Neuer Fall am Ende des Abschnitts „paket, Code“: Der Paketbau lässt `.git/index` unberührt, auch wo ein gewöhnliches `git status` ihn auffrischen würde. Die Gegenprobe im selben Fall sichert zu, dass die Fixture das überhaupt zeigen kann.
|
||||
|
||||
Aus:
|
||||
|
||||
```
|
||||
return soll(code == 2 and "schon ein paket.md" in aus and (ziel / "paket.md").read_text(encoding="utf-8") == "alt\n", f"Exit {code}: {aus} {err}")
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```python
|
||||
return soll(code == 2 and "schon ein paket.md" in aus and (ziel / "paket.md").read_text(encoding="utf-8") == "alt\n", f"Exit {code}: {aus} {err}")
|
||||
|
||||
|
||||
@fall("paket laesst .git/index unberuehrt, auch wo git status ihn auffrischen wuerde (GIT_OPTIONAL_LOCKS=0)")
|
||||
def f_index_unberuehrt(tmp):
|
||||
"""Zwei gleiche Repos mit veralteten Stat-Daten fuer a.py. Die Gegenprobe zeigt, dass ein
|
||||
gewoehnliches git status dort .git/index schreibt; ohne sie misst der zweite Teil nichts."""
|
||||
f = []
|
||||
for teil in ("gegenprobe", "paket"):
|
||||
(tmp / teil).mkdir()
|
||||
w, a, b = repo(tmp / teil)
|
||||
zeit = (w / "a.py").stat().st_mtime - 1000
|
||||
os.utime(w / "a.py", (zeit, zeit))
|
||||
index = w / ".git" / "index"
|
||||
vorher = summe(index)
|
||||
if teil == "gegenprobe":
|
||||
umgebung = {k: v for k, v in os.environ.items() if k != "GIT_OPTIONAL_LOCKS"}
|
||||
subprocess.run(["git", "-C", str(w), "status", "--porcelain"], check=True, capture_output=True, env=umgebung)
|
||||
f += soll(summe(index) != vorher, "Gegenprobe: git status ohne GIT_OPTIONAL_LOCKS=0 laesst .git/index unveraendert; der Fall misst so nichts")
|
||||
else:
|
||||
code, text, p, ziel = code_paket(tmp / teil, w, "--basis", a)
|
||||
f += soll(code == 0 and p is not None, f"Exit {code}: {text}")
|
||||
f += soll(summe(index) == vorher, "paket hat .git/index geschrieben")
|
||||
return f
|
||||
```
|
||||
|
||||
Die beiden Ablege-Fälle übergeben `paket_summe` aus der Zeile `QUORUM_PAKET`; das bindet das Feld an die Prüfung in `ablegen`.
|
||||
|
||||
Aus:
|
||||
|
||||
```
|
||||
quelle = workflow_bericht(tmp / "bericht.md", p["nummer"], p["basis"], zeilenende="\r\n")
|
||||
code, text = ablegen(ziel, quelle, p["bericht_pfad"])
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```python
|
||||
quelle = workflow_bericht(tmp / "bericht.md", p["nummer"], p["basis"], zeilenende="\r\n")
|
||||
code, text = ablegen(ziel, quelle, p["bericht_pfad"], p["paket_summe"])
|
||||
```
|
||||
|
||||
Aus:
|
||||
|
||||
```
|
||||
quelle = workflow_bericht(tmp / "bericht.md", p["nummer"], p["basis"], rumpf="stand: PAKET")
|
||||
code, text = ablegen(ziel, quelle, p["bericht_pfad"])
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```python
|
||||
quelle = workflow_bericht(tmp / "bericht.md", p["nummer"], p["basis"], rumpf="stand: PAKET")
|
||||
code, text = ablegen(ziel, quelle, p["bericht_pfad"], p["paket_summe"])
|
||||
```
|
||||
|
||||
Neuer Fall am Ende des Abschnitts „ablegen“. Die beiden Pakete gleichen sich in `nummer` und `basis`; nur `--summe` unterscheidet einen vertauschten Paketordner vom richtigen.
|
||||
|
||||
Aus:
|
||||
|
||||
```
|
||||
return f + soll(code == 2 and "paket.json nicht lesbar" in text and not (tmp / "p").exists(), f"ohne-paket: Exit {code}: {text}")
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```python
|
||||
return f + soll(code == 2 and "paket.json nicht lesbar" in text and not (tmp / "p").exists(), f"ohne-paket: Exit {code}: {text}")
|
||||
|
||||
|
||||
@fall("ablegen: --summe eines anderen Pakets, umformatiertes paket.json oder fehlendes --summe: Exit 2, nichts geschrieben")
|
||||
def f_ablegen_summe(tmp):
|
||||
w, a, b = repo(tmp)
|
||||
code, text, p, ziel = code_paket(tmp, w, "--basis", a)
|
||||
code2, text2, p2, ziel2 = code_paket(tmp, w, "--basis", a)
|
||||
if not p or not p2:
|
||||
return [f"paket: Exit {code}/{code2}: {text} {text2}"]
|
||||
f = soll(p["nummer"] == p2["nummer"] and p["basis"] == p2["basis"] and p["paket_summe"] != p2["paket_summe"], "Fixture: Die beiden Pakete sollen sich in paket_summe unterscheiden, nicht in nummer und basis")
|
||||
quelle = workflow_bericht(tmp / "bericht.md", p["nummer"], p["basis"])
|
||||
code, text = ablegen(ziel2, quelle, tmp / "fremd" / "x.md", p["paket_summe"])
|
||||
f += soll(code == 2 and "BEFUND" in text and "weicht von der SHA-256" in text and not (tmp / "fremd").exists(), f"fremdes Paket: Exit {code}: {text}")
|
||||
schreibe(ziel / "paket.json", (ziel / "paket.json").read_text(encoding="utf-8") + "\n")
|
||||
code, text = ablegen(ziel, quelle, tmp / "umformatiert" / "x.md", p["paket_summe"])
|
||||
f += soll(code == 2 and "weicht von der SHA-256" in text and not (tmp / "umformatiert").exists(), f"paket.json umformatiert: Exit {code}: {text}")
|
||||
code, aus, err = fahre("ablegen", "--paket", ziel2, "--bericht", quelle, "--ziel", tmp / "ohne" / "x.md")
|
||||
return f + soll(code == 2 and "--summe" in aus + err and not (tmp / "ohne").exists(), f"ohne --summe: Exit {code}: {aus} {err}")
|
||||
```
|
||||
|
||||
### Mutanten in `.vault-tools/quorum/mutanten_quorum_werkzeug.py`
|
||||
|
||||
Aus:
|
||||
|
||||
```
|
||||
("ablegen: Ordner nicht angelegt", " ziel.parent.mkdir(parents=True, exist_ok=True)\n", ""),
|
||||
]
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```python
|
||||
("ablegen: Ordner nicht angelegt", " ziel.parent.mkdir(parents=True, exist_ok=True)\n", ""),
|
||||
("git ohne GIT_OPTIONAL_LOCKS=0", "*argumente], env=umgebung,", "*argumente],"),
|
||||
("ablegen: --summe ungeprueft", " if gemessen != a.summe:\n", " if False:\n"),
|
||||
("ablegen: --summe ueber paket.md", 'gemessen = hashlib.sha256((ordner / "paket.json").read_bytes())', 'gemessen = hashlib.sha256((ordner / "paket.md").read_bytes())'),
|
||||
]
|
||||
```
|
||||
|
||||
### Umsetzung in `.vault-tools/quorum/quorum_werkzeug.py`
|
||||
|
||||
Docstring, Abschnitt `ablegen`:
|
||||
|
||||
Aus:
|
||||
|
||||
```
|
||||
ablegen legt den Bericht aus dem Workflow ab: prueft nummer und basis im Frontmatter gegen
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```python
|
||||
ablegen legt den Bericht aus dem Workflow ab: prueft --summe gegen die SHA-256 von paket.json
|
||||
(paket_summe aus QUORUM_PAKET; ein vertauschter Paketordner oder ein seit dem Paketbau
|
||||
geaendertes paket.json ist ein Befund), dann nummer und basis im Frontmatter gegen
|
||||
```
|
||||
|
||||
Docstring, Aufruf:
|
||||
|
||||
Aus:
|
||||
|
||||
```
|
||||
python quorum_werkzeug.py ablegen --paket <ordner> --bericht <md-datei> --ziel <pfad>
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```python
|
||||
python quorum_werkzeug.py ablegen --paket <ordner> --bericht <md-datei> --ziel <pfad> --summe <paket_summe>
|
||||
```
|
||||
|
||||
Jeder git-Aufruf des Skripts läuft über `git()`:
|
||||
|
||||
Aus:
|
||||
|
||||
```
|
||||
def git(wurzel, *argumente, pruefen=True):
|
||||
try:
|
||||
lauf = subprocess.run(["git", "-C", str(wurzel), "-c", "core.quotepath=false", *argumente],
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```python
|
||||
def git(wurzel, *argumente, pruefen=True):
|
||||
"""git im Projekt, nur lesend. GIT_OPTIONAL_LOCKS=0 haelt git status davon ab, .git/index
|
||||
aufzufrischen und dafuer index.lock zu nehmen (git(1), Abschnitt Umgebungsvariablen); sonst
|
||||
kollidiert der Paketbau mit einem git-Befehl, der gleichzeitig im Projekt laeuft."""
|
||||
umgebung = {**os.environ, "GIT_OPTIONAL_LOCKS": "0"}
|
||||
try:
|
||||
lauf = subprocess.run(["git", "-C", str(wurzel), "-c", "core.quotepath=false", *argumente], env=umgebung,
|
||||
```
|
||||
|
||||
`ablegen` vergleicht `--summe` mit den Bytes von `paket.json`, bevor es etwas anderes prüft:
|
||||
|
||||
Aus:
|
||||
|
||||
```
|
||||
raise Befund(f"paket.json nicht lesbar in {ordner.as_posix()} ({type(fehler).__name__})")
|
||||
ziel = Path(a.ziel)
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```python
|
||||
raise Befund(f"paket.json nicht lesbar in {ordner.as_posix()} ({type(fehler).__name__})")
|
||||
gemessen = hashlib.sha256((ordner / "paket.json").read_bytes()).hexdigest()
|
||||
if gemessen != a.summe:
|
||||
raise Befund(f"--summe {a.summe} weicht von der SHA-256 von paket.json in {ordner.as_posix()} ab ({gemessen}): falscher Paketordner, oder paket.json wurde seit dem Paketbau geaendert")
|
||||
ziel = Path(a.ziel)
|
||||
```
|
||||
|
||||
Aus:
|
||||
|
||||
```
|
||||
b.add_argument("--ziel", required=True)
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```python
|
||||
b.add_argument("--ziel", required=True)
|
||||
b.add_argument("--summe", required=True)
|
||||
```
|
||||
|
||||
### Umsetzung in `.claude/workflows/quorum.js`
|
||||
|
||||
Kopfkommentar, Absatz „Zusammenführen“:
|
||||
|
||||
Aus:
|
||||
|
||||
```
|
||||
// Zusammenführen: Ein Sonnet-Agent ohne agentType gruppiert nur belegte Befunde; das Skript löst
|
||||
// Gruppen über verschiedene Dateien und über Zeilenangaben, die sich nicht überschneiden, auf und
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```javascript
|
||||
// Zusammenführen: Ein Sonnet-Agent ohne agentType gruppiert nur belegte Befunde; das Skript teilt
|
||||
// jede Gruppe nach Datei und Zeilen in zusammenhängende Teilgruppen (Funktion teilgruppen) und
|
||||
```
|
||||
|
||||
Arbeitsbaum-Vergleich um die Suite ohne Index-Schreiben:
|
||||
|
||||
Aus:
|
||||
|
||||
```
|
||||
// Schreibt die Suite Dateien in den Arbeitsbaum, scheitert das Paket an "nicht sauber"; die
|
||||
// BEFUND-Zeile davor nennt dann die Suite als Ursache.
|
||||
const baum = `git -C ${q(a.projekt)} status --porcelain --untracked-files=all -- . ':(exclude)docs/quorum'`
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```javascript
|
||||
// Schreibt die Suite Dateien in den Arbeitsbaum, scheitert das Paket an "nicht sauber"; die
|
||||
// BEFUND-Zeile davor nennt dann die Suite als Ursache. GIT_OPTIONAL_LOCKS=0, damit status
|
||||
// .git/index nicht auffrischt und keinem git-Befehl des Koordinators das index.lock nimmt.
|
||||
const baum = `GIT_OPTIONAL_LOCKS=0 git -C ${q(a.projekt)} status --porcelain --untracked-files=all -- . ':(exclude)docs/quorum'`
|
||||
```
|
||||
|
||||
`lesePaket` verlangt eine brauchbare `paket_summe`:
|
||||
|
||||
Aus:
|
||||
|
||||
```
|
||||
if (!/^[0-9a-f]{16}$/.test(p.lesemarke)) return { fehler: 'QUORUM_PAKET mit unbrauchbarer Lesemarke ' + JSON.stringify(p.lesemarke) }
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```javascript
|
||||
if (!/^[0-9a-f]{16}$/.test(p.lesemarke)) return { fehler: 'QUORUM_PAKET mit unbrauchbarer Lesemarke ' + JSON.stringify(p.lesemarke) }
|
||||
if (!/^[0-9a-f]{64}$/.test(p.paket_summe)) return { fehler: 'QUORUM_PAKET mit unbrauchbarer paket_summe ' + JSON.stringify(p.paket_summe) }
|
||||
```
|
||||
|
||||
`zeilen()` liest jede übliche Form; die letzten vier Zeilen der Funktion bleiben:
|
||||
|
||||
Aus:
|
||||
|
||||
```
|
||||
// Stelle als Zeilenbereich [von, bis], wenn sie nur aus Zeilennummern besteht ("10", "Zeile 10",
|
||||
// "Zeilen 8-12", "L10"); sonst null, dann gilt das Urteil des Zusammenführ-Agenten.
|
||||
function zeilen(stelle) {
|
||||
const m = /^\s*(?:(?:zeilen?|z\.|lines?|l)\s*)?(\d+)(?:\s*(?:bis|to|\W)\s*(\d+))?\s*$/i.exec(String(stelle))
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```javascript
|
||||
// Stelle als Zeilenbereich [von, bis], oder null ohne Zeilennummer; dann gilt das Urteil des
|
||||
// Zusammenführ-Agenten. Gelesen werden "12", "12-14", "Zeile 12", "Zeilen 10-14", "Z. 10 bis 14",
|
||||
// "L12", "line 12" und "a.py:42", auch mitten im Text wie "Zeile 42 (Funktion f)"; ein Bereich
|
||||
// darf Bindestrich, Gedankenstrich, "bis" oder "to" tragen. Zuerst zählt eine Angabe mit Wort,
|
||||
// dann eine hinter Dateiendung und Doppelpunkt, zuletzt die reine Zahl.
|
||||
function zeilen(stelle) {
|
||||
const s = String(stelle)
|
||||
const m = /(?:^|[^\p{L}\d])(?:zeilen?|z\.|lines?|l)\s*(\d+)(?:\s*(?:-|–|bis|to)\s*l?\s*(\d+))?/iu.exec(s)
|
||||
|| /\.[a-z0-9]+:(\d+)(?:\s*(?:-|–)\s*(\d+))?/i.exec(s)
|
||||
|| /^\s*(\d+)(?:\s*(?:-|–|bis|to)\s*(\d+))?\s*$/i.exec(s)
|
||||
```
|
||||
|
||||
`ueberschneiden()` weicht `nahe()` und `teilgruppen()`:
|
||||
|
||||
Aus:
|
||||
|
||||
```
|
||||
// Jedes Paar von Zeilenbereichen überschneidet sich; Stellen ohne Zeilennummern zählen nicht mit.
|
||||
function ueberschneiden(mitglieder) {
|
||||
const bereiche = mitglieder.map(b => zeilen(b.stelle)).filter(Boolean)
|
||||
return bereiche.every(x => bereiche.every(y => x[0] <= y[1] && y[0] <= x[1]))
|
||||
}
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```javascript
|
||||
// Zwei Zeilenbereiche gehören zusammen, wenn sie sich mit einer Toleranz von TOLERANZ Zeilen
|
||||
// überschneiden: "Zeile 42" und "Zeile 45" ja, "Zeile 42" und "Zeile 46" nicht.
|
||||
const TOLERANZ = 3
|
||||
function nahe(x, y) {
|
||||
return x[0] <= y[1] + TOLERANZ && y[0] <= x[1] + TOLERANZ
|
||||
}
|
||||
|
||||
// Teilt die Mitglieder einer Gruppe des Agenten in zusammenhängende Teilgruppen (transitive Hülle):
|
||||
// Zusammen gehören nur Stellen derselben Datei, zwei Stellen mit Zeilennummern nur, wenn sie nahe
|
||||
// liegen. Eine Stelle ohne Zeilennummer folgt dem Urteil des Agenten: Sie bleibt bei den Stellen
|
||||
// ihrer Datei, solange deren Zeilen eine einzige Teilgruppe bilden oder die Datei gar keine Zeilen
|
||||
// nennt. Zerfallen die Zeilen, steht sie für sich, denn eine falsche Gruppe erzeugt eine falsche
|
||||
// Mehrheit. Mitglieder und Teilgruppen bleiben in der Reihenfolge des Agenten.
|
||||
function teilgruppen(mitglieder) {
|
||||
const bereich = mitglieder.map(b => zeilen(b.stelle))
|
||||
const datei = mitglieder.map(b => pfadnorm(b.datei))
|
||||
const eltern = mitglieder.map((b, i) => i)
|
||||
const finde = i => (eltern[i] === i ? i : finde(eltern[i]))
|
||||
const verbinde = (i, j) => { eltern[finde(j)] = finde(i) }
|
||||
for (let i = 0; i < mitglieder.length; i++) {
|
||||
for (let j = i + 1; j < mitglieder.length; j++) {
|
||||
if (datei[i] === datei[j] && bereich[i] && bereich[j] && nahe(bereich[i], bereich[j])) verbinde(i, j)
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < mitglieder.length; i++) {
|
||||
if (bereich[i]) continue
|
||||
const gleich = mitglieder.map((b, j) => j).filter(j => j !== i && datei[j] === datei[i])
|
||||
const stuecke = new Set(gleich.filter(j => bereich[j]).map(finde))
|
||||
if (stuecke.size === 1) verbinde([...stuecke][0], i)
|
||||
else if (stuecke.size === 0 && gleich.length) verbinde(gleich[0], i)
|
||||
}
|
||||
const teile = new Map()
|
||||
mitglieder.forEach((b, i) => {
|
||||
if (!teile.has(finde(i))) teile.set(finde(i), [])
|
||||
teile.get(finde(i)).push(b)
|
||||
})
|
||||
return [...teile.values()]
|
||||
}
|
||||
```
|
||||
|
||||
Kommentar über `gruppenBilden()`:
|
||||
|
||||
Aus:
|
||||
|
||||
```
|
||||
// Gruppen des Agenten prüfen: unbekannte IDs fallen weg, eine ID zählt nur in ihrer ersten
|
||||
// Gruppe, eine Gruppe über verschiedene Dateien wird aufgelöst, ebenso eine, deren Zeilenangaben
|
||||
// sich nicht überschneiden. Was keiner Gruppe angehört, bleibt eine Gruppe für sich.
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```javascript
|
||||
// Gruppen des Agenten prüfen: unbekannte IDs fallen weg, eine ID zählt nur in ihrer ersten
|
||||
// Gruppe, und jede Gruppe zerfällt in ihre Teilgruppen (Funktion teilgruppen); jede Aufteilung
|
||||
// steht als Hinweis im Bericht. Was keiner Gruppe angehört, bleibt eine Gruppe für sich.
|
||||
```
|
||||
|
||||
`gruppenBilden()` teilt statt aufzulösen:
|
||||
|
||||
Aus:
|
||||
|
||||
```
|
||||
if (new Set(mitglieder.map(b => pfadnorm(b.datei))).size > 1) {
|
||||
meldungen.push(`Zusammenführen: Gruppe ${mitglieder.map(b => b.id).join(', ')} aufgelöst: verschiedene Dateien`)
|
||||
mitglieder.forEach(b => gruppen.push([b]))
|
||||
} else if (!ueberschneiden(mitglieder)) {
|
||||
meldungen.push(`Zusammenführen: Gruppe ${mitglieder.map(b => b.id).join(', ')} aufgelöst: Zeilen überschneiden sich nicht (${mitglieder.map(b => b.stelle).join('; ')})`)
|
||||
mitglieder.forEach(b => gruppen.push([b]))
|
||||
} else gruppen.push(mitglieder)
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```javascript
|
||||
const teile = teilgruppen(mitglieder)
|
||||
if (teile.length > 1) meldungen.push(`Zusammenführen: Gruppe ${mitglieder.map(b => b.id).join(', ')} aufgeteilt in ${teile.map(t => t.map(b => b.id).join(', ')).join(' | ')} (${mitglieder.map(b => `${b.id} ${b.datei} ${b.stelle}`).join('; ')})`)
|
||||
teile.forEach(t => gruppen.push(t))
|
||||
```
|
||||
|
||||
`ablegen_befehl` trägt die Summe:
|
||||
|
||||
Aus:
|
||||
|
||||
```
|
||||
// Der fertige Befehl für den Koordinator, nachdem er bericht_md als bericht.md im Paketordner
|
||||
// gespeichert hat.
|
||||
function ablegenBefehl(p) {
|
||||
return `${PY} ${q(WERKZEUG)} ablegen --paket ${q(p.ordner)} --bericht ${q(p.ordner + '/bericht.md')} --ziel ${q(p.bericht_pfad)}`
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```javascript
|
||||
// Der fertige Befehl für den Koordinator, nachdem er bericht_md als bericht.md im Paketordner
|
||||
// gespeichert hat. --summe ist paket_summe aus QUORUM_PAKET: ablegen setzt den Stand nur aus dem
|
||||
// paket.json ein, dessen SHA-256 der Paketbau gemeldet hat, und endet sonst mit BEFUND.
|
||||
function ablegenBefehl(p) {
|
||||
return `${PY} ${q(WERKZEUG)} ablegen --paket ${q(p.ordner)} --bericht ${q(p.ordner + '/bericht.md')} --ziel ${q(p.bericht_pfad)} --summe ${q(p.paket_summe)}`
|
||||
```
|
||||
|
||||
## Schritt 4 — Rot-Nachweis für `quorum_werkzeug.py`
|
||||
|
||||
Die neuen Fälle gegen die in Schritt 2 gesicherte alte Fassung:
|
||||
|
||||
```
|
||||
ALT="$(cygpath -m -l /tmp)/quorum-task05-alt" && python D:/Claude/.vault-tools/quorum/faelle_quorum_werkzeug.py "$ALT/quorum_werkzeug.py"; echo "EXIT $?"
|
||||
```
|
||||
|
||||
Erwartet: `EXIT 1`. Genau diese Fallzeilen beginnen mit `FEHLER`, in dieser Reihenfolge; jede andere Fallzeile beginnt mit `OK`:
|
||||
|
||||
```
|
||||
FEHLER paket laesst .git/index unberuehrt, auch wo git status ihn auffrischen wuerde (GIT_OPTIONAL_LOCKS=0)
|
||||
FEHLER ablegen, Code: stand PAKET wird der Commit, CRLF wird LF, Ordner angelegt, naechster Lauf ist leer
|
||||
FEHLER ablegen, Kundendokument: stand PAKET wird die Liste, nur im Frontmatter, naechster Lauf ist leer
|
||||
FEHLER ablegen: falsche nummer, falsche basis, stand PAKET fehlt oder doppelt, Ziel vorhanden, ohne Frontmatter, ohne paket.json sind Befunde; nichts geschrieben
|
||||
FEHLER ablegen: --summe eines anderen Pakets, umformatiertes paket.json oder fehlendes --summe: Exit 2, nichts geschrieben
|
||||
```
|
||||
|
||||
- Unter dem Index-Fall steht `paket hat .git/index geschrieben` und keine Zeile mit `Gegenprobe`. Steht dort die Gegenprobe, schreibt `git status` in dieser Umgebung den Index nicht, und der Fall misst nichts: anhalten und mit der Ausgabe von `git --version` melden.
|
||||
- Unter den vier `ablegen`-Fällen steht der Grund der alten Fassung: Sie kennt `--summe` nicht (`unrecognized arguments`).
|
||||
- Weicht die Liste ab: nichts anpassen, melden.
|
||||
|
||||
## Schritt 5 — Verifikation
|
||||
|
||||
```
|
||||
python D:/Claude/.vault-tools/quorum/faelle_quorum_werkzeug.py; echo "EXIT $?"
|
||||
```
|
||||
|
||||
Erwartet: Jede Fallzeile beginnt mit `OK`, kein `FEHLER`, die letzte Zeile endet auf `0 gescheitert`, `EXIT 0`.
|
||||
|
||||
```
|
||||
python D:/Claude/.vault-tools/quorum/mutanten_quorum_werkzeug.py; echo "EXIT $?"
|
||||
```
|
||||
|
||||
Erwartet: Jede Mutantenzeile beginnt mit `GEFANGEN`, die letzte Zeile endet auf `0 ueberlebt, 0 veraltet`, `EXIT 0`. Der Lauf dauert einige Minuten.
|
||||
|
||||
```
|
||||
node -e "const AF = Object.getPrototypeOf(async function () {}).constructor; const t = require('fs').readFileSync('D:/Claude/.claude/workflows/quorum.js', 'utf8'); new AF('agent', 'parallel', 'pipeline', 'phase', 'log', 'workflow', 'args', 'budget', 'Date', 'Math', t.replace(/^export const meta = /m, 'const meta = ')); console.log('SYNTAX OK')"; echo "EXIT $?"
|
||||
```
|
||||
|
||||
Erwartet: `SYNTAX OK`, `EXIT 0`. Das prüft nur, dass `quorum.js` sich übersetzen lässt, wie `attrappe.mjs` es lädt; das Verhalten prüfen erst die Fälle aus Task 03.
|
||||
|
||||
```
|
||||
python D:/Claude/.vault-tools/plan-kopie/plan_bloecke.py nach D:/Claude/.vault-tools/workflow-pruefung/docs/plans/2026-09-29-quorum/task-05-nachbesserung-gruppen.md --projekt D:/Claude
|
||||
```
|
||||
|
||||
Erwartet: `0 Befund(e).`
|
||||
|
||||
Melde alle Ausgaben vollständig, dazu den Wert der Zeile `GIT_OPTIONAL_LOCKS=` aus Schritt 2. Läuft etwas anders als erwartet: nichts anpassen, melden, mit jeder `FEHLER`- oder `UEBERLEBT`-Zeile und ihrem Text.
|
||||
Reference in new issue
Block a user