--- 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 `, `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 --bericht --ziel ``` wird: ```python python quorum_werkzeug.py ablegen --paket --bericht --ziel --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.