From 612cd7fb5468bd7872209a926f4e9f745a54839f Mon Sep 17 00:00:00 2001 From: Pascal Michel Date: Mon, 5 Oct 2026 06:36:34 +0200 Subject: [PATCH] Quorum: Task 06 (Freitext im Bericht entschaerft), Grund nennt blockierende ohne Beleg, Abschluss quorum.js: klartext() schreibt Wikilinks, Einbettungen und Tags aus Freitext als Klartext (Fund aus dem ersten echten Lauf); der Grund eines freien Urteils nennt blockierende Befunde ohne Beleg. faelle_workflows.mjs 248 Faelle gruen, mutanten_workflows.py 145 Mutanten, 0 ueberlebt. codeworkflow.md: Regel zum Befund ohne Beleg und Ablageort beim Kundendokument praezisiert, Vermerk 'noch nicht abgenommen' entfernt. LIESMICH.md fuer .vault-tools/quorum/. Co-Authored-By: Claude Opus 5.5 --- .claude/workflows/quorum.js | 33 +++++-- .vault-tools/quorum/LIESMICH.md | 59 +++++++++++++ .../plans/2026-09-29-quorum/00-uebersicht.md | 23 ++++- .../workflow-pruefung/faelle_workflows.mjs | 85 +++++++++++++++++++ .../workflow-pruefung/mutanten_workflows.py | 23 ++++- AI/Regelwerk/codeworkflow.md | 10 +-- 6 files changed, 212 insertions(+), 21 deletions(-) create mode 100644 .vault-tools/quorum/LIESMICH.md diff --git a/.claude/workflows/quorum.js b/.claude/workflows/quorum.js index 63a759a..d498248 100644 --- a/.claude/workflows/quorum.js +++ b/.claude/workflows/quorum.js @@ -41,10 +41,14 @@ export const meta = { // 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 +// Urteil: frei heißt kein bestätigter und kein ungeprüfter blockierender Befund mit Beleg; ein +// blockierender Befund ohne Beleg sperrt nicht (Plan, Offener Punkt 8), der Grund nennt ihn. Mit weniger als // zwei abgegebenen Stimmen gibt es kein Urteil und keinen Bericht, die Basis bleibt die alte. // Laufzeit: Date.now() und new Date() gibt es im Laufzeitsystem nicht. Die Codex-Laufzeit kommt // aus dem Job, die der Claude-Stimmen steht im Bericht als nicht gemessen. +// Bericht: Freitext aus Stimmen, Zusammenführen und Nachprüfung geht nur durch klartext() in +// bericht_md. Ein zitierter Wikilink, eine Einbettung oder ein Tag würde im Vault sonst lebendig; +// Hub-Zeile und Frontmatter baut der Workflow selbst, sie bleiben echte Links bzw. Felder. const ANLAESSE = ['einspielen', 'version', 'kundendokument'] const RANG = { blockierend: 0, wichtig: 1, hinweis: 2 } const KUERZEL = { opus: 'O', sonnet: 'S', codex: 'C', haiku: 'H' } @@ -482,6 +486,17 @@ function berichtLeer(a, p, urteil, grund) { return [kopf(a, p, urteil, `Urteil ${urteil}: ${grund}.`, 'nicht gelaufen'), '', `# Quorum ${p.nummer}: ${a.anlass}`, '', hubZeile(a), '', '## Urteil', '', `**${urteilText(urteil)}**: Seit dem letzten Quorum hat sich nichts geändert. Das Urteil stammt aus ${vorher}; keine Stimme ist gelaufen.`, ''].join('\n') } +// Freitext als Klartext für Obsidian: Jede öffnende eckige Klammer in einer Folge von zwei oder +// mehr bekommt einen Backslash, damit kein Wikilink und keine Einbettung entsteht. Eine Raute +// bekommt einen Backslash, wenn ihr kein Buchstabe, keine Ziffer, kein Unterstrich und kein +// Backslash vorausgeht und weder Leerraum noch eine zweite Raute folgt; so entsteht kein Tag. +// Die Leseansicht zeigt den Text wie zitiert. Zweimal angewendet ändert sich nichts mehr. +function klartext(s) { + return String(s) + .replace(/\[{2,}/g, folge => folge.replace(/\[/g, '\\[')) + .replace(/(^|[^\p{L}\p{N}_\\])#(?=[^\s#])/gu, '$1\\#') +} + 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' @@ -492,7 +507,7 @@ function berichtVoll(a, p, x) { const s = x.stimmen[n] const c = x.zaehlung[n] const name = n === 'haiku' ? 'Haiku (Ersatz für Codex)' : NAMEN[n] - z.push(`| ${name} | ${s.abgegeben ? 'ja' : 'nein: ' + s.grund} | ${c.gesamt} | ${c.belegt} | ${c.bestaetigt} | ${c.alleinfunde} | ${laufzeit(s.dauer_s)} |`) + z.push(`| ${name} | ${s.abgegeben ? 'ja' : 'nein: ' + klartext(s.grund)} | ${c.gesamt} | ${c.belegt} | ${c.bestaetigt} | ${c.alleinfunde} | ${laufzeit(s.dauer_s)} |`) } z.push('') if (x.stimmen.haiku) z.push('Haiku stand statt Codex im Quorum. Dieses Quorum war nur mit Claude besetzt.', '') @@ -501,20 +516,20 @@ function berichtVoll(a, p, x) { if (!x.gruppen.length) z.push('keine belegten Befunde', '') 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}, 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(`### G${g.nr}: ${g.schwere}, ${STATUS_TEXT[g.status]} (${g.stimmen.map(n => NAMEN[n]).join(', ')})`, '', `\`${erste.datei}\`, Stelle ${klartext(erste.stelle)}`, '') + for (const b of g.mitglieder) z.push(`- ${b.id} (${NAMEN[b.stimme]}, ${b.schwere}, Stelle ${klartext(b.stelle)}): ${klartext(b.aussage)}`, ` Beleg: ${klartext(b.beleg)}`) + if (g.nachpruefung) z.push('', `Nachprüfung: ${g.nachpruefung.urteil}, ${klartext(g.nachpruefung.fundstelle)}. ${klartext(g.nachpruefung.begruendung)}`) z.push('') } z.push('## Unbelegt', '') if (!x.unbelegt.length) z.push('keine') - for (const b of x.unbelegt) z.push(`- ${b.id} (${NAMEN[b.stimme]}, ${b.schwere}) \`${b.datei}\`, Stelle ${b.stelle}: ${b.aussage}`) + for (const b of x.unbelegt) z.push(`- ${b.id} (${NAMEN[b.stimme]}, ${b.schwere}) \`${b.datei}\`, Stelle ${klartext(b.stelle)}: ${klartext(b.aussage)}`) z.push('', '## Nicht prüfbar', '') - const offen = Object.keys(x.stimmen).flatMap(n => (x.stimmen[n].nicht_pruefbar || []).map(t => `- ${NAMEN[n]}: ${t}`)) + const offen = Object.keys(x.stimmen).flatMap(n => (x.stimmen[n].nicht_pruefbar || []).map(t => `- ${NAMEN[n]}: ${klartext(t)}`)) z.push(...(offen.length ? offen : ['keine'])) z.push('', '## Hinweise des Laufs', '') const alleHinweise = [...p.hinweise, ...laufHinweise] - z.push(...(alleHinweise.length ? alleHinweise.map(h => '- ' + h) : ['keine']), '') + z.push(...(alleHinweise.length ? alleHinweise.map(h => '- ' + klartext(h)) : ['keine']), '') return z.join('\n') } @@ -621,7 +636,7 @@ const sperrend = gruppen.filter(g => g.schwere === 'blockierend' && (gilt(g) || const urteil = sperrend.length ? 'nicht_frei' : 'frei' const grund = sperrend.length ? `${sperrend.filter(gilt).length} bestätigte und ${sperrend.filter(g => !gilt(g)).length} ungeprüfte blockierende Befunde` - : 'kein bestätigter und kein ungeprüfter blockierender Befund' + : 'kein bestätigter und kein ungeprüfter blockierender Befund mit Beleg' + (unbelegt.some(b => b.schwere === 'blockierend') ? `; ${unbelegt.filter(b => b.schwere === 'blockierend').length} blockierende ohne Beleg stehen unter Unbelegt und sperren nicht` : '') await ablesen('Ende', 0) const zaehlung = {} for (const n of Object.keys(stimmen)) { diff --git a/.vault-tools/quorum/LIESMICH.md b/.vault-tools/quorum/LIESMICH.md new file mode 100644 index 0000000..7dff640 --- /dev/null +++ b/.vault-tools/quorum/LIESMICH.md @@ -0,0 +1,59 @@ +--- +type: guide +title: "Quorum am Meilenstein – Werkzeug und Ablauf" +status: active +summary: "Wie das Quorum gestartet, der Bericht abgelegt und das Werkzeug geprüft wird, dazu was der erste echte Lauf belegt hat." +tags: [vault-tools, codeworkflow] +created: 2026-10-05 +updated: 2026-10-05 +--- + +# Quorum am Meilenstein + +Prüft vor dem Einspielen, vor einer neuen Minor- oder Major-Version auf master und vor der Abgabe eines Kundendokuments denselben Unterschied mit drei Stimmen: Opus, Sonnet und Codex, als Ersatz Haiku. Die Regel steht in `AI/Regelwerk/codeworkflow.md`, Abschnitt „Quorum am Meilenstein“. Hier steht nur die Bedienung. + +## Starten + +```text +# Code-Projekt, erstes Quorum dort: basis ist der Commit der letzten abgenommenen Version +workflow({ scriptPath: "D:/Claude/.claude/workflows/quorum.js" }, { anlass: "einspielen", projekt: "", basis: "", suite_datei: "" }) + +# Kundendokument: Ordner im Vault, Dateien relativ dazu, Übersicht des Projekts +workflow({ scriptPath: "D:/Claude/.claude/workflows/quorum.js" }, { anlass: "kundendokument", ordner: "D:/Claude/", dateien: ["", "..."], uebersicht: "<_MOC-datei>.md" }) +``` + +- Der Arbeitsbaum muss außerhalb von `docs/quorum/` sauber sein. +- `suite` nur für eine Suite, die in neun Minuten durchläuft und nichts Unversioniertes schreibt; sonst die Suite selbst fahren und `suite_datei` nennen. +- `codex: false` setzt sofort Haiku ein, etwa wenn das Codex-Kontingent bekannt aufgebraucht ist. + +## Bericht ablegen + +```bash +# bericht_md aus dem Ergebnis als bericht.md in den Ordner paket schreiben, dann: +PYTHONIOENCODING=utf-8 python -X utf8 .vault-tools/quorum/quorum_werkzeug.py ablegen --paket --bericht /bericht.md --ziel --summe +``` + +- Den fertigen Befehl liefert das Ergebnis als `ablegen_befehl`. Er setzt `stand` aus `paket.json` ein, damit kein Modell die Liste der Prüfsummen abschreibt. +- Danach den Bericht im Projekt committen. + +## Prüfen + +```bash +# Werkzeug: Fälle und Mutanten +python .vault-tools/quorum/faelle_quorum_werkzeug.py +python .vault-tools/quorum/mutanten_quorum_werkzeug.py +# Workflow: Fälle und Mutanten über die Attrappe (Mutantenlauf über 30 Minuten, im Hintergrund mit ausreichendem Zeitlimit starten) +node .vault-tools/workflow-pruefung/faelle_workflows.mjs +python .vault-tools/workflow-pruefung/mutanten_workflows.py +``` + +## Was belegt ist + +- **Erster echter Lauf am 2026-09-30, Anlass einspielen, in einem Code-Projekt des Vaults:** + - Alle drei Stimmen haben gültig abgegeben, Codex nach 1 min 52 s, mit Lesebeleg. + - Urteil frei. Bestätigte Befunde je Stimme: Opus 3, Sonnet 2, Codex 3. Alleinfunde: Opus 2, Sonnet 1, Codex 3 (Tabelle „Stimmen“ im Bericht). + - `ablegen` hat den Stand als Commit eingesetzt, der Bericht verlinkt sein README. + - Der Verbrauch blieb im Rahmen der Ablesung (5 Stunden 6 %, Woche 77 % vorher und nachher). +- **Fund aus diesem Lauf:** Freitext der Stimmen hat im Bericht lebendige Wikilinks erzeugt. Seit Task 06 schreibt `quorum.js` Wikilinks, Einbettungen und Tags aus Freitext als Klartext mit Backslash. +- **Bekannte Grenzen (Prüfung vom 2026-10-05, klein):** `zeilen()` liest bei Aufzählungen nur die erste Zahl („Zeilen 72 und 134“ wird 72) und kennt Formen wie „Zeile 10 bis Zeile 14“ oder einen Geviertstrich nicht; eine zu breite Gruppe des Zusammenführ-Agenten kann so eine Mehrheit kosten. `pfadnorm()` entfernt keinen führenden Schrägstrich. `klartext()` setzt den Backslash auch vor `#` in HTML-Entitäten und URL-Fragmenten und lässt `%%…%%`, `