Files
claude-werkzeuge/.vault-tools/workflow-pruefung/docs/plans/2026-09-29-quorum/task-05-nachbesserung-gruppen.md
T
Pascal MichelandClaude Opus 5.5 fa1c26b407 Erste Fassung: Claude-Werkzeuge aus dem Vault
.vault-tools (ohne trading-plattform-pruefung und watermarks-remover),
.claude/agents, .claude/skills, .claude/workflows, .claude/settings.json.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-14 11:27:59 +02:00

25 KiB
Raw Blame History

type, title, status, summary, tags, created, updated, modell, effort, modell_begruendung
type title status summary tags created updated modell effort modell_begruendung
plan-task Task 05 — Nachbesserung: Teilgruppen nach Zeilen, --summe beim Ablegen, git ohne Index-Schreiben draft 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.
plan-task
vault-tools
codeworkflow
2026-09-30 2026-09-30 sonnet low 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

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:

    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:

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:

    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:

    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:

    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:

    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:

    ("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:

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

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:

        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:

    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:

// 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:

  // 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:

  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:

// 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:

// 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:

// 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:

    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:

// 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.