Files
claude-werkzeuge/.vault-tools/workflow-pruefung/faelle_workflows.mjs
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

1639 lines
104 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Fälle für die Workflow-Skripte unter .claude/workflows/, gefahren mit attrappe.mjs.
// Aufruf: node faelle_workflows.mjs [<ordner mit den Workflows>]
// Ohne Argument gilt ../../.claude/workflows relativ zu dieser Datei. Je Workflow läuft zuerst
// die Grundlinie; scheitert sie, messen die übrigen Fälle dieses Workflows nichts.
// Exit 0, wenn alle Fälle bestehen, sonst 1.
import { mkdtempSync, writeFileSync, rmSync, cpSync } from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { fuehreAus } from './attrappe.mjs'
const hier = path.dirname(fileURLToPath(import.meta.url))
const ORDNER = path.resolve(process.argv[2] || path.join(hier, '..', '..', '.claude', 'workflows'))
const WF = name => path.join(ORDNER, name + '.js')
const ok = (wert, dauer = 5) => ({ wert, dauer })
const fenster = (fuenf, woche, extra = []) => [
{ label: '5-hour limit', percentUsed: fuenf, resetsAt: '2026-09-24T18:30:00.000Z' },
{ label: 'Weekly · all models', percentUsed: woche, resetsAt: '2026-09-28T00:00:00.000Z' },
...extra,
]
const ablesung = (fuenf, woche, extra) => ({ verfuegbar: true, status: 'ok', plan: 'Pro', fenster: fenster(fuenf, woche, extra), fehler: '' })
const OHNE = 'ohne'
// Antworten für alle Agenten, die die Workflows starten. Unbekannte Labels werfen, damit ein
// falsches Label im Protokoll auffällt.
function antworten(k = {}) {
let n = 0
return async a => {
const label = a.label || ''
if (label.startsWith('verbrauch:')) {
const x = (k.ablesungen || [])[n++]
if (x === OHNE) return undefined
return ok(x === undefined ? ablesung(10, 10) : x)
}
if (label === 'werkzeug:planwerkzeuge') return ok({ laeufe: [{ befehl: 'planwerkzeuge', exit: 0, ausgabe: 'keine Befunde' }] })
if (label === 'werkzeug:kopie') return ok({ laeufe: [{ befehl: 'kopie', exit: 0, ausgabe: "KOPIE=C:/tmp/k/mat\n{'neu': 2}" }] })
if (label === 'werkzeug:vergleich') return ok({ laeufe: [{ befehl: 'vergleich', exit: 0, ausgabe: 'VERGLEICH-OK\nEXIT suite: 0' }] })
if (label === 'werkzeug:mutanten') {
if (k.mutanten === OHNE) return undefined
const ausgabe = k.mutantenAusgabe !== undefined ? k.mutantenAusgabe : 'wurzel: /tmp/x\nM1 OK a=GRUEN(x)\nergebnis: 1 Mutanten, 1 OK, 0 LUECKE, 0 UNKLAR, 0 ZEIT, 0 MUSTER\nEXIT mutanten_lauf: 0'
return ok({ laeufe: [{ befehl: 'mutanten', exit: 0, ausgabe }] })
}
if (label.startsWith('plankritik:')) {
const linse = label.slice('plankritik:'.length)
if (k.linse) { const x = k.linse(linse); if (x === OHNE) return undefined; if (x !== undefined) return ok(x, k.dauer || 30) }
const basis = { arithmetik: 10, bezeichner: 20, abdeckung: 30 }[linse] || 40
return ok({ befunde: [0, 1].map(j => ({ schweregrad: 'wichtig', datei: 'task-01.md', zeile: basis + j, beschreibung: linse + ' ' + j, reproduktion: 'x' })), selbst_ausgefuehrt: [], nicht_pruefbar: [] }, k.dauer || 30)
}
if (label.startsWith('widerlegung:')) return ok({ urteil: 'bestaetigt', schweregrad: 'wichtig', bedingung: '', begruendung: 'nachgestellt', beleg: 'befehl' }, k.dauer || 30)
if (label.startsWith('pruefung')) {
if (k.pruefer === OHNE) return undefined
return ok({ selbst_ausgefuehrt: [], eigener_lauf: [], befunde: [{ schweregrad: 'klein', datei: 'a.py', zeile: 1, beschreibung: 'x', reproduktion: 'y' }], plan_vs_umsetzung: [], geprueft_und_in_ordnung: [], nicht_pruefbar: [] }, 20)
}
if (label.startsWith('executer:')) {
if (k.executer === OHNE) return undefined
return ok({ erledigt: ['alles'], geaenderte_dateien: [], verifikation: [], abweichungen: ['keine'], annahmen: [], beobachtet: [] }, 10)
}
if (label.startsWith('planer:task-')) {
const nr = Number(label.slice('planer:task-'.length))
const x = k.planer ? k.planer(nr) : undefined
if (x === OHNE) return undefined
const ziel = /Schreib genau eine Datei: (\S+\.md)/.exec(a.prompt)
const datei = k.planerDatei ? k.planerDatei(nr, ziel && ziel[1]) : ziel && ziel[1]
return ok({ datei, zieldateien: [], testdateien: [], abweichungen_von_uebersicht: [], offene_punkte: [] }, k.planerDauer ? k.planerDauer(nr) : 20 + nr * 3)
}
throw new Error('unerwarteter Agent ' + label)
}
}
function pruefe() {
const fehler = []
return { fehler, soll: (bedingung, text) => { if (!bedingung) fehler.push(text) } }
}
const mitLabel = (p, anfang) => p.agents.filter(x => (x.label || '').startsWith(anfang))
const spitze = (p, phase, modell) => p.spitzeJePhase[phase + '|' + modell] || 0
// Wie viele andere Agenten mit diesem Labelanfang liefen, als a startete.
const laufendBeiStart = (p, a, anfang) => mitLabel(p, anfang).filter(b => b !== a && b.start < a.start && (b.ende === null || b.ende > a.start)).length
function ohneVerstoss(p, soll) { soll(p.verstoesse.length === 0, 'Verstöße: ' + p.verstoesse.join('; ')) }
const faelle = []
const fall = (workflow, name, lauf) => faelle.push({ workflow, name, lauf })
// ---------------------------------------------------------------- attrappe
// Die Attrappe muss so streng sein wie das Ziel, sonst misst jeder grüne Fall nur sie selbst.
const TMP = mkdtempSync(path.join(os.tmpdir(), 'attrappe-'))
function skript(name, rumpf) {
const datei = path.join(TMP, name + '.js')
writeFileSync(datei, `export const meta = { name: '${name}', description: 'Selbsttest' }\n${rumpf}\n`, 'utf8')
return datei
}
const A = (datei, k) => fuehreAus(datei, {}, antworten(k), { ordner: TMP })
for (const [i, [name, rumpf, muster]] of [
['Date.now() wirft', 'return Date.now()', /Date\.now\(\)/],
['new Date() ohne Argument wirft', 'return new Date()', /new Date\(\)/],
['Math.random() wirft', 'return Math.random()', /Math\.random\(\)/],
['unbekannter Workflow wirft', "return await workflow('gibt-es-nicht', {})", /nicht lesbar/],
['Schema ohne properties wirft', "return await agent('x', { label: 'verbrauch:t', model: 'haiku', schema: { type: 'object' } })", /Schema braucht/],
].entries()) {
fall('attrappe', name, async () => {
const { fehler, soll } = pruefe()
const { fehler: f } = await A(skript('wirft-' + i, rumpf))
soll(f && muster.test(f), 'Fehler: ' + f)
return fehler
})
}
fall('attrappe', 'Grundlinie: Math.min und new Date(0) gehen, Globals sind da', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, fehler: f } = await A(skript('grund', "phase('P'); log('x'); return [Math.min(1, 2), new Date(0).getTime(), typeof parallel, typeof pipeline, args !== undefined]"))
soll(f === null && JSON.stringify(e) === '[1,0,"function","function",true]', 'Fehler/Ergebnis: ' + f + ' ' + JSON.stringify(e))
return fehler
})
fall('attrappe', 'workflow() im Kind wirft', async () => {
const { fehler, soll } = pruefe()
const enkel = skript('enkel', 'return 1')
const kind = skript('kind', `return await workflow({ scriptPath: ${JSON.stringify(enkel)} }, {})`)
const { fehler: f } = await A(skript('eltern', `return await workflow({ scriptPath: ${JSON.stringify(kind)} }, {})`))
soll(f && /nesting is limited to one level/.test(f), 'Fehler: ' + f)
return fehler
})
fall('attrappe', 'im Kind tragen Agenten die Phase ▸ <name>, phase() wirkt dort nicht', async () => {
const { fehler, soll } = pruefe()
const kind = skript('kindphase', "phase('Eigen'); return await agent('x', { label: 'werkzeug:planwerkzeuge', model: 'sonnet', phase: 'Eigen' })")
const { protokoll: p } = await A(skript('elternphase', `phase('Oben'); return await workflow({ scriptPath: ${JSON.stringify(kind)} }, {})`))
soll(p.agents.length === 1 && p.agents[0].phase === '▸ kindphase', 'Phase: ' + (p.agents[0] && p.agents[0].phase))
soll(JSON.stringify(p.phasen) === '["Oben"]', 'phase() im Kind hat gewirkt: ' + JSON.stringify(p.phasen))
return fehler
})
fall('attrappe', 'dasselbe Kind zweimal: ▸ name und ▸ name #2', async () => {
const { fehler, soll } = pruefe()
const kind = skript('kindzwei', "return await agent('x', { label: 'werkzeug:planwerkzeuge', model: 'sonnet' })")
const { protokoll: p } = await A(skript('elternzwei', `await workflow({ scriptPath: ${JSON.stringify(kind)} }, {}); return await workflow({ scriptPath: ${JSON.stringify(kind)} }, {})`))
soll(JSON.stringify(p.agents.map(a => a.phase)) === '["▸ kindzwei","▸ kindzwei #2"]', 'Phasen: ' + JSON.stringify(p.agents.map(a => a.phase)))
return fehler
})
fall('attrappe', 'agent() ohne model ist ein Verstoß', async () => {
const { fehler, soll } = pruefe()
const { protokoll: p } = await A(skript('ohne-modell', "return await agent('x', { label: 'verbrauch:t' })"))
soll(p.verstoesse.some(v => /ohne model/.test(v)), 'Verstöße: ' + JSON.stringify(p.verstoesse))
return fehler
})
fall('attrappe', 'Ergebnis ohne required-Feld wird null und ist ein Verstoß', async () => {
const { fehler, soll } = pruefe()
const schema = "{ type: 'object', properties: { a: { type: 'string' }, b: { type: 'string' } }, required: ['a', 'b'] }"
const { ergebnis: e, protokoll: p } = await A(skript('required', `return await agent('x', { label: 'werkzeug:planwerkzeuge', model: 'sonnet', schema: ${schema} })`))
soll(e === null && p.verstoesse.some(v => /Ergebnis ohne/.test(v)), 'Ergebnis/Verstöße: ' + JSON.stringify([e, p.verstoesse]))
return fehler
})
fall('attrappe', 'Spitze zählt gleichzeitige Agenten je Phase und Modell', async () => {
const { fehler, soll } = pruefe()
const rumpf = "phase('P'); await parallel([1, 2, 3].map(() => () => agent('x', { label: 'widerlegung:t', model: 'opus', phase: 'P' }))); await agent('x', { label: 'widerlegung:t', model: 'opus', phase: 'P' }); return 1"
const { protokoll: p } = await A(skript('spitze', rumpf))
soll(spitze(p, 'P', 'opus') === 3, 'Spitze ' + spitze(p, 'P', 'opus'))
return fehler
})
// ---------------------------------------------------------------- verbrauch
const V =(a, k) => fuehreAus(WF('verbrauch'), a, antworten(k), { ordner: ORDNER })
fall('verbrauch', 'Grundlinie: Agent liest 10/10, wunsch 3 ergibt 3 Plätze', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, fehler: f, protokoll: p } = await V({ anlass: 'test', wunsch: 3 }, {})
soll(f === null, 'Fehler: ' + f)
soll(e && e.status === 'ok' && e.plaetze === 3 && e.gebremst === false && e.quelle === 'agent', 'Ergebnis: ' + JSON.stringify(e))
soll(p.agents.length === 1 && p.agents[0].label === 'verbrauch:test' && p.agents[0].modell === 'haiku', 'Agent: ' + JSON.stringify(p.agents.map(x => [x.label, x.modell])))
ohneVerstoss(p, soll)
return fehler
})
fall('verbrauch', 'probe ersetzt den Agenten', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, protokoll: p } = await V({ anlass: 't', wunsch: 2, probe: ablesung(10, 10) })
soll(p.agents.length === 0, 'Agent gestartet trotz probe')
soll(e && e.quelle === 'probe' && e.plaetze === 2, JSON.stringify(e))
return fehler
})
for (const [fuenf, woche, wunsch, plaetze] of [
[49.9, 10, 4, 4], [50, 10, 4, 2], [69.9, 10, 4, 2], [70, 10, 4, 1], [84.9, 10, 4, 1], [85, 10, 4, 0], [100, 10, 4, 0], [0, 0, 4, 4],
[10, 69.9, 4, 4], [10, 70, 4, 2], [10, 84.9, 4, 2], [10, 85, 4, 1], [10, 94.9, 4, 1], [10, 95, 4, 0], [10, 100, 4, 0],
[101, 10, 4, 0], [10, 150, 4, 0],
]) {
fall('verbrauch', `Stufe 5 h ${fuenf} %, Woche ${woche} % ergibt ${plaetze}`, async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e } = await V({ anlass: 't', wunsch, probe: ablesung(fuenf, woche) })
soll(e && e.plaetze === plaetze, `plaetze ${e && e.plaetze}, erwartet ${plaetze}`)
soll(e && e.gebremst === (plaetze === 0), 'gebremst ' + (e && e.gebremst))
return fehler
})
}
fall('verbrauch', 'wunsch 0 bei 95 % misst nur und bremst nicht', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e } = await V({ anlass: 't', wunsch: 0, probe: ablesung(95, 10) })
soll(e && e.plaetze === 0 && e.gebremst === false && e.status === 'ok', JSON.stringify(e))
return fehler
})
fall('verbrauch', 'wunsch 7 wird auf die harte Obergrenze 4 gesenkt', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e } = await V({ anlass: 't', wunsch: 7, probe: ablesung(10, 10) })
soll(e && e.plaetze === 4 && /harte Obergrenze/.test(e.grund), JSON.stringify(e))
return fehler
})
for (const wunsch of [-1, 1.5, '3', null, undefined]) {
fall('verbrauch', `wunsch ${JSON.stringify(wunsch)} wirft vor dem Agenten`, async () => {
const { fehler, soll } = pruefe()
const { fehler: f, protokoll: p } = await V({ anlass: 't', wunsch })
soll(f && /wunsch muss/.test(f), 'Fehler: ' + f)
soll(p.agents.length === 0, 'Agent gestartet')
return fehler
})
}
for (const anlass of [undefined, '', ' ', 3]) {
fall('verbrauch', `anlass ${JSON.stringify(anlass)} wirft`, async () => {
const { fehler, soll } = pruefe()
const { fehler: f } = await V({ anlass, wunsch: 1 })
soll(f && /anlass fehlt/.test(f), 'Fehler: ' + f)
return fehler
})
}
for (const probe of [null, [], 'x']) {
fall('verbrauch', `probe ${JSON.stringify(probe)} wirft`, async () => {
const { fehler, soll } = pruefe()
const { fehler: f } = await V({ anlass: 't', wunsch: 1, probe })
soll(f && /probe muss/.test(f), 'Fehler: ' + f)
return fehler
})
}
fall('verbrauch', 'Agent ohne Ergebnis: nicht_gelesen, zwei Plätze, bei wunsch 1 einer', async () => {
const { fehler, soll } = pruefe()
const a = await V({ anlass: 't', wunsch: 3 }, { ablesungen: [OHNE] })
soll(a.ergebnis && a.ergebnis.status === 'nicht_gelesen' && a.ergebnis.plaetze === 2 && a.ergebnis.gebremst === false, JSON.stringify(a.ergebnis))
const b = await V({ anlass: 't', wunsch: 1 }, { ablesungen: [OHNE] })
soll(b.ergebnis && b.ergebnis.plaetze === 1, JSON.stringify(b.ergebnis))
return fehler
})
for (const [name, probe] of [
['status unavailable', { verfuegbar: true, status: 'unavailable', plan: '', fenster: [], fehler: '' }],
['verfuegbar false', { verfuegbar: false, status: 'fehler', plan: '', fenster: [], fehler: 'Werkzeug fehlt' }],
['verfuegbar als Text', { verfuegbar: 'true', status: 'ok', plan: 'Pro', fenster: fenster(10, 10), fehler: '' }],
]) {
fall('verbrauch', `${name}: nicht_gelesen`, async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e } = await V({ anlass: 't', wunsch: 3, probe })
soll(e && e.status === 'nicht_gelesen' && e.plaetze === 2, JSON.stringify(e))
return fehler
})
}
const ok5 = { label: '5-hour limit', percentUsed: 10, resetsAt: 'x' }
const okW = { label: 'Weekly · all models', percentUsed: 10, resetsAt: 'y' }
for (const [name, liste] of [
['fenster leer', []],
['fenster kein Array', { a: 1 }],
['nur Woche', [okW]],
['nur 5 Stunden', [ok5]],
['Eintrag null an erster Stelle', [null, ok5, okW]],
['Eintrag null an letzter Stelle', [ok5, okW, null]],
['label fehlt', [ok5, { percentUsed: 1, resetsAt: 'z' }, okW]],
['percentUsed null vorn', [{ ...ok5, percentUsed: null }, okW]],
['percentUsed null hinten', [ok5, { ...okW, percentUsed: null }]],
['percentUsed als Text', [{ ...ok5, percentUsed: '12' }, okW]],
['percentUsed NaN', [{ ...ok5, percentUsed: NaN }, okW]],
['percentUsed -1', [ok5, { ...okW, percentUsed: -1 }]],
['percentUsed Infinity', [{ ...ok5, percentUsed: Infinity }, okW]],
['Opus-Woche ohne Zahl', [ok5, okW, { label: 'Weekly · Opus', percentUsed: null, resetsAt: 'z' }]],
]) {
fall('verbrauch', `${name}: unbekannte_fenster, alte Obergrenze`, async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e } = await V({ anlass: 't', wunsch: 4, probe: { verfuegbar: true, status: 'ok', plan: 'Pro', fenster: liste, fehler: '' } })
soll(e && e.status === 'unbekannte_fenster' && e.plaetze === 2 && e.gebremst === false, JSON.stringify(e))
return fehler
})
}
for (const [name, liste] of [
['doppeltes 5-Stunden-Fenster, hohes zuerst', [{ ...ok5, percentUsed: 90 }, ok5, okW]],
['doppeltes 5-Stunden-Fenster, hohes zuletzt', [ok5, okW, { ...ok5, percentUsed: 90 }]],
]) {
fall('verbrauch', `${name}: das höhere zählt`, async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e } = await V({ anlass: 't', wunsch: 4, probe: { verfuegbar: true, status: 'ok', plan: 'Pro', fenster: liste, fehler: '' } })
soll(e && e.plaetze === 0 && e.gebremst === true && e.fuenf_stunden.prozent === 90, JSON.stringify(e))
return fehler
})
}
fall('verbrauch', 'Opus-Woche 90 % zählt wie die Woche', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e } = await V({ anlass: 't', wunsch: 4, probe: ablesung(10, 10, [{ label: 'Weekly · Opus', percentUsed: 90, resetsAt: 'z' }]) })
soll(e && e.plaetze === 1 && e.woche_opus && e.woche_opus.prozent === 90, JSON.stringify(e))
return fehler
})
fall('verbrauch', 'ein Label mit "hour" in der Mitte ist kein 5-Stunden-Fenster', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e } = await V({ anlass: 't', wunsch: 4, probe: ablesung(10, 10, [{ label: 'Weekly · Opus hours', percentUsed: 90, resetsAt: 'z' }]) })
soll(e && e.plaetze === 1 && e.fuenf_stunden.prozent === 10, JSON.stringify(e))
return fehler
})
fall('verbrauch', 'Woche aller Modelle 90 % zählt auch neben einer niedrigen Opus-Woche', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e } = await V({ anlass: 't', wunsch: 4, probe: ablesung(10, 90, [{ label: 'Weekly · Opus', percentUsed: 10, resetsAt: 'z' }]) })
soll(e && e.plaetze === 1, JSON.stringify(e))
return fehler
})
for (const [name, fremd] of [['Fable 99 %', 99], ['Fable ohne Zahl', null], ['Fable als Text', 'viel']]) {
fall('verbrauch', `fremdes Fenster (${name}) wird übergangen`, async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e } = await V({ anlass: 't', wunsch: 4, probe: ablesung(10, 10, [{ label: 'Weekly · Fable', percentUsed: fremd, resetsAt: 'z' }]) })
soll(e && e.status === 'ok' && e.plaetze === 4, JSON.stringify(e))
return fehler
})
}
for (const [a, b] of [['5-Hour Limit', 'Weekly · All Models'], ['5 hour limit', 'weekly all models'], ['5hour', 'WEEKLY - ALL MODELS']]) {
fall('verbrauch', `Schreibweise "${a}" / "${b}" wird erkannt`, async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e } = await V({ anlass: 't', wunsch: 4, probe: { verfuegbar: true, status: 'ok', plan: 'Pro', fenster: [{ label: a, percentUsed: 60, resetsAt: 'x' }, { label: b, percentUsed: 10, resetsAt: 'y' }], fehler: '' } })
soll(e && e.status === 'ok' && e.plaetze === 2, JSON.stringify(e))
return fehler
})
}
// ---------------------------------------------------------------- plankritik
const PK_ARGS = { projekt: 'D:/x', planordner: 'D:/x/plan', kopie: 'C:/tmp/k/mat' }
const PK = (a, k, o = {}) => fuehreAus(WF('plankritik'), a, antworten(k), { ordner: ORDNER, ...o })
fall('plankritik', 'Grundlinie: drei Linsen gleichzeitig, Widerlegung zu zweit, drei Ablesungen', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, fehler: f, protokoll: p } = await PK(PK_ARGS, {})
soll(f === null, 'Fehler: ' + f)
soll(e && e.status === 'fertig', 'status ' + (e && e.status))
soll(spitze(p, 'Plankritik', 'opus') === 3, 'Linsen gleichzeitig: ' + spitze(p, 'Plankritik', 'opus'))
soll(spitze(p, 'Widerlegung', 'opus') === 2, 'Widerlegung gleichzeitig: ' + spitze(p, 'Widerlegung', 'opus'))
const ab = mitLabel(p, 'verbrauch:').map(x => x.label)
soll(JSON.stringify(ab) === JSON.stringify(['verbrauch:plankritik:vor Linsen', 'verbrauch:plankritik:vor Widerlegung', 'verbrauch:plankritik:Ende']), 'Ablesungen: ' + JSON.stringify(ab))
soll(mitLabel(p, 'verbrauch:').every(x => x.modell === 'haiku'), 'Ablesung nicht auf haiku')
soll(e && e.verbrauch.length === 3 && e.plaetze.linsen === 3 && e.plaetze.widerlegung === 2, 'verbrauch/plaetze: ' + JSON.stringify(e && e.plaetze))
soll(e && e.zaehlung.bestaetigt === 6 && e.zaehlung.nicht_geprueft === 0, 'zaehlung ' + JSON.stringify(e && e.zaehlung))
for (const k of ['zaehlung', 'kopie', 'temp_ordner', 'befunde', 'selbst_ausgefuehrt', 'nicht_pruefbar', 'vorbereitung', 'ohne_ergebnis']) soll(e && k in e, 'Feld fehlt: ' + k)
ohneVerstoss(p, soll)
return fehler
})
fall('plankritik', '5 Stunden bei 60 %: Linsen zu zweit', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, protokoll: p } = await PK(PK_ARGS, { ablesungen: [ablesung(60, 10)] })
soll(spitze(p, 'Plankritik', 'opus') === 2 && e.plaetze.linsen === 2, 'Linsen gleichzeitig: ' + spitze(p, 'Plankritik', 'opus'))
soll(e.status === 'fertig', 'status ' + e.status)
return fehler
})
fall('plankritik', 'gebremst vor den Linsen: keine Linse, keine Widerlegung', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, fehler: f, protokoll: p } = await PK(PK_ARGS, { ablesungen: [ablesung(90, 10)] })
soll(f === null, 'Fehler: ' + f)
soll(e && e.status === 'gebremst' && e.gebremst_vor === 'Linsen', JSON.stringify(e && [e.status, e.gebremst_vor]))
soll(mitLabel(p, 'plankritik:').length === 0 && mitLabel(p, 'widerlegung:').length === 0, 'Opus gestartet trotz Bremse')
soll(e && e.verbrauch.length === 1 && e.ohne_ergebnis.length === 3, 'verbrauch/ohne_ergebnis ' + JSON.stringify(e && [e.verbrauch.length, e.ohne_ergebnis]))
soll(e && e.zaehlung && e.zaehlung.bestaetigt === 0 && e.zaehlung.nicht_geprueft === 0, 'zaehlung ' + JSON.stringify(e && e.zaehlung))
soll(e && /neu starten/.test(e.naechster_schritt), 'naechster_schritt ' + (e && e.naechster_schritt))
return fehler
})
fall('plankritik', 'gebremst vor der Widerlegung: nur die Befunde über der Schwelle heißen gebremst', async () => {
const { fehler, soll } = pruefe()
const zeile = { arithmetik: 11, bezeichner: 12, abdeckung: 13 }
const mitKlein = linse => ({ befunde: [
{ schweregrad: 'wichtig', datei: 'task-01.md', zeile: zeile[linse], beschreibung: 'w', reproduktion: 'x' },
{ schweregrad: 'klein', datei: 'task-02.md', zeile: zeile[linse], beschreibung: 'k', reproduktion: 'x' },
], selbst_ausgefuehrt: [], nicht_pruefbar: [] })
const { ergebnis: e, protokoll: p } = await PK(PK_ARGS, { ablesungen: [ablesung(10, 10), ablesung(90, 10)], linse: mitKlein })
soll(mitLabel(p, 'plankritik:').length === 3, 'Linsen: ' + mitLabel(p, 'plankritik:').length)
soll(mitLabel(p, 'widerlegung:').length === 0, 'Widerlegung trotz Bremse')
soll(e.status === 'gebremst' && e.gebremst_vor === 'Widerlegung', JSON.stringify([e.status, e.gebremst_vor]))
const urteile = e.befunde.map(b => b.schweregrad + ':' + b.urteil).sort()
soll(urteile.filter(u => u === 'wichtig:nicht geprüft (gebremst)').length === 3 && urteile.filter(u => u === 'klein:nicht geprüft').length === 3, 'Urteile: ' + JSON.stringify(urteile))
soll(e.zaehlung.nicht_geprueft === 6 && e.zaehlung.bestaetigt === 0, JSON.stringify(e.zaehlung))
soll(mitLabel(p, 'verbrauch:').length === 3, 'Ablesung am Ende fehlt')
soll(/nachholen/.test(e.naechster_schritt), 'naechster_schritt ' + e.naechster_schritt)
return fehler
})
fall('plankritik', 'vor der Widerlegung 75 %: Widerlegung einzeln', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, protokoll: p } = await PK(PK_ARGS, { ablesungen: [ablesung(10, 10), ablesung(75, 10)] })
soll(spitze(p, 'Widerlegung', 'opus') === 1 && e.plaetze.widerlegung === 1, 'Widerlegung gleichzeitig: ' + spitze(p, 'Widerlegung', 'opus'))
soll(mitLabel(p, 'widerlegung:').length === 6 && e.status === 'fertig', JSON.stringify([mitLabel(p, 'widerlegung:').length, e.status]))
return fehler
})
fall('plankritik', 'Ablesung ohne Ergebnis: Linsen zu zweit (alte Obergrenze)', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, protokoll: p } = await PK(PK_ARGS, { ablesungen: [OHNE] })
soll(spitze(p, 'Plankritik', 'opus') === 2, 'Linsen gleichzeitig: ' + spitze(p, 'Plankritik', 'opus'))
soll(e.status === 'fertig' && e.verbrauch[0].status === 'nicht_gelesen', JSON.stringify(e.verbrauch[0]))
return fehler
})
fall('plankritik', 'als Kind: verbrauch.js wirft, Rückfall auf zwei', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, fehler: f, protokoll: p } = await PK(PK_ARGS, {}, { tiefe: 1 })
soll(f === null, 'Fehler: ' + f)
soll(spitze(p, '▸ plankritik', 'opus') === 2, 'Linsen gleichzeitig: ' + spitze(p, '▸ plankritik', 'opus'))
soll(mitLabel(p, 'verbrauch:').length === 0, 'Ableseagent trotz Kind')
soll(e && e.verbrauch.every(v => v.status === 'nicht_gelesen' && /nicht ausführbar/.test(v.grund)), JSON.stringify(e && e.verbrauch))
soll(e && e.status === 'fertig', 'status ' + (e && e.status))
return fehler
})
fall('plankritik', 'Aufträge nennen den Testort', async () => {
const { fehler, soll } = pruefe()
const mit = await PK(PK_ARGS, {})
const satz = 'Tests fährst du nur in der Kopie C:/tmp/k/mat, nie im echten Projektbaum'
for (const a of [...mitLabel(mit.protokoll, 'plankritik:'), ...mitLabel(mit.protokoll, 'widerlegung:')]) {
soll(a.prompt.includes(satz) && /eigenen Kopie dieser Kopie/.test(a.prompt), a.label + ' ohne Testort')
}
const ohne = await PK({ projekt: 'D:/x', planordner: 'D:/x/plan' }, {})
for (const a of mitLabel(ohne.protokoll, 'plankritik:')) {
soll(/Tests fährst du in dieser Plankritik nicht/.test(a.prompt), a.label + ': ohne Kopie kein Testverbot')
}
return fehler
})
fall('plankritik', 'plaetze 1 begrenzt die Linsen auf eine', async () => {
const { fehler, soll } = pruefe()
const { protokoll: p } = await PK({ ...PK_ARGS, plaetze: 1 }, {})
soll(spitze(p, 'Plankritik', 'opus') === 1, 'Linsen gleichzeitig: ' + spitze(p, 'Plankritik', 'opus'))
return fehler
})
for (const plaetze of [0, 5, 2.5, '3']) {
fall('plankritik', `plaetze ${JSON.stringify(plaetze)} wirft vor dem ersten Agenten`, async () => {
const { fehler, soll } = pruefe()
const { fehler: f, protokoll: p } = await PK({ ...PK_ARGS, plaetze }, {})
soll(f && /plaetze muss/.test(f), 'Fehler: ' + f)
soll(p.agents.length === 0, 'Agent gestartet')
return fehler
})
}
fall('plankritik', 'doppelte Linse wirft vor dem ersten Agenten', async () => {
const { fehler, soll } = pruefe()
const { fehler: f, protokoll: p } = await PK({ ...PK_ARGS, linsen: ['arithmetik', 'bezeichner', 'arithmetik'] }, {})
soll(f && /doppelt in linsen/.test(f), 'Fehler: ' + f)
soll(p.agents.length === 0, 'Agent gestartet')
return fehler
})
fall('plankritik', 'Linse ohne Ergebnis steht in ohne_ergebnis', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e } = await PK(PK_ARGS, { linse: l => (l === 'arithmetik' ? OHNE : undefined) })
soll(e.status === 'fertig' && JSON.stringify(e.ohne_ergebnis) === '["arithmetik"]', JSON.stringify(e.ohne_ergebnis))
return fehler
})
fall('plankritik', 'verifizieren false: keine Ablesung vor der Widerlegung', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, protokoll: p } = await PK({ ...PK_ARGS, verifizieren: false }, {})
soll(mitLabel(p, 'widerlegung:').length === 0, 'Widerlegung trotz verifizieren false')
soll(mitLabel(p, 'verbrauch:').length === 2, 'Ablesungen: ' + mitLabel(p, 'verbrauch:').length)
soll(e.status === 'fertig' && e.zaehlung.nicht_geprueft === 6, JSON.stringify(e.zaehlung))
return fehler
})
fall('plankritik', 'mit kopie, mutanten in Vorgabe: ein Agent werkzeug:mutanten, Rohausgabe geht an die Linse arithmetik', async () => {
const { fehler, soll } = pruefe()
const marke = 'MUTANT-MARKE-007'
const { protokoll: p } = await PK(PK_ARGS, { mutantenAusgabe: `wurzel: /tmp/x\n${marke}\nEXIT mutanten_lauf: 0` })
const agenten = mitLabel(p, 'werkzeug:mutanten')
soll(agenten.length === 1, 'Agenten werkzeug:mutanten: ' + agenten.length)
soll(agenten[0] && /MSYS_NO_PATHCONV=1/.test(agenten[0].prompt), 'MSYS_NO_PATHCONV fehlt im Befehl: ' + (agenten[0] && agenten[0].prompt))
soll(agenten[0] && /cygpath -m -l/.test(agenten[0].prompt), 'cygpath -m -l fehlt im Befehl: ' + (agenten[0] && agenten[0].prompt))
const arith = mitLabel(p, 'plankritik:arithmetik')[0]
soll(arith && arith.prompt.includes(marke) && /<mutantenlauf>/.test(arith.prompt), 'Rohausgabe fehlt im Auftrag der Linse arithmetik')
return fehler
})
fall('plankritik', 'mutanten "aus": kein Agent werkzeug:mutanten', async () => {
const { fehler, soll } = pruefe()
const { protokoll: p } = await PK({ ...PK_ARGS, mutanten: 'aus' }, {})
soll(mitLabel(p, 'werkzeug:mutanten').length === 0, 'Agent trotz mutanten aus')
return fehler
})
fall('plankritik', 'ohne kopie und ohne materialisieren: kein Agent werkzeug:mutanten', async () => {
const { fehler, soll } = pruefe()
const { protokoll: p } = await PK({ projekt: 'D:/x', planordner: 'D:/x/plan' }, {})
soll(mitLabel(p, 'werkzeug:mutanten').length === 0, 'Agent ohne Kopie gestartet')
return fehler
})
fall('plankritik', 'mutanten "halb" wirft vor dem ersten Agenten', async () => {
const { fehler, soll } = pruefe()
const { fehler: f, protokoll: p } = await PK({ ...PK_ARGS, mutanten: 'halb' }, {})
soll(f && /mutanten muss/.test(f), 'Fehler: ' + f)
soll(p.agents.length === 0, 'Agent gestartet')
return fehler
})
fall('plankritik', 'Hilfsagent ohne EXIT mutanten_lauf und ohne KEINE_MUTANTEN_TOML: kein Block <mutantenlauf>', async () => {
const { fehler, soll } = pruefe()
const { protokoll: p } = await PK(PK_ARGS, { mutantenAusgabe: 'irgendeine unbrauchbare Ausgabe ohne Marker' })
const arith = mitLabel(p, 'plankritik:arithmetik')[0]
soll(arith && !/<mutantenlauf>/.test(arith.prompt), 'Block trotz unbrauchbarer Ausgabe vorhanden')
return fehler
})
fall('plankritik', 'KEINE_MUTANTEN_TOML: Linse bekommt den Hinweissatz statt der Rohausgabe', async () => {
const { fehler, soll } = pruefe()
const { protokoll: p } = await PK(PK_ARGS, { mutantenAusgabe: 'KEINE_MUTANTEN_TOML' })
const arith = mitLabel(p, 'plankritik:arithmetik')[0]
soll(arith && /keine mutanten\.toml/.test(arith.prompt) && /Schwere wichtig/.test(arith.prompt), 'Hinweissatz fehlt: ' + (arith && arith.prompt))
soll(arith && !/<mutantenlauf>/.test(arith.prompt), 'Block trotz KEINE_MUTANTEN_TOML vorhanden')
return fehler
})
fall('plankritik', 'ZEITGRENZE im Mutantenlauf: Linse erfährt es, Hilfsagent fährt mit zehn Minuten', async () => {
const { fehler, soll } = pruefe()
const { protokoll: p } = await PK(PK_ARGS, { mutantenAusgabe: 'M1 OK a=ROT(x)\nZEITGRENZE: Mutantenlauf nach 540 s abgebrochen\nEXIT mutanten_lauf: 124' })
const hilfe = mitLabel(p, 'werkzeug:mutanten')[0]
soll(hilfe && /timeout auf 600000/.test(hilfe.prompt), 'Zeitvorgabe fehlt im Auftrag des Hilfsagenten')
const arith = mitLabel(p, 'plankritik:arithmetik')[0]
soll(arith && /Zeitgrenze erreicht/.test(arith.prompt) && /<mutantenlauf>/.test(arith.prompt), 'Hinweis auf die Zeitgrenze fehlt: ' + (arith && arith.prompt.slice(-400)))
return fehler
})
// Fälle für die Nachbesserung vom 2026-09-28 (N5, N7, N8, N10): fünf Mutanten aus
// eigene_mutanten.py töten, dazu Modus muster, Exit 2/124 und mutanten_status/mutanten_modus.
fall('plankritik', 'Modus muster wird als muster an den Wrapper übergeben, nicht als voll', async () => {
const { fehler, soll } = pruefe()
const { protokoll: p } = await PK({ ...PK_ARGS, mutanten: 'muster' }, {})
const hilfe = mitLabel(p, 'werkzeug:mutanten')[0]
const zeile = (hilfe ? hilfe.prompt.split('\n').find(z => z.includes('mutanten_lauf_kopie.sh')) : '') || ''
soll(/ muster$/.test(zeile), 'Befehl endet nicht auf muster: ' + zeile)
return fehler
})
fall('plankritik', 'Mutantenstufe läuft auch mit materialisierter Kopie ohne args.kopie', async () => {
const { fehler, soll } = pruefe()
const args = { projekt: 'D:/x', planordner: 'D:/x/plan', materialisieren: { ohne: ['config/secrets.toml'] } }
const { protokoll: p } = await PK(args, {})
soll(mitLabel(p, 'werkzeug:mutanten').length === 1, 'Mutantenlauf lief nicht mit materialisierter Kopie')
return fehler
})
fall('plankritik', 'arithmetik bekommt mehr als die letzten fünf Zeilen des Mutantenlaufs', async () => {
const { fehler, soll } = pruefe()
const marke = 'MARKE-VOR-DEM-ENDE'
const fuellzeilen = Array.from({ length: 50 }, (_, i) => 'zeile ' + i).join('\n')
const ausgabe = `wurzel: /tmp/x\n${marke}\n${fuellzeilen}\nEXIT mutanten_lauf: 0`
const { protokoll: p } = await PK(PK_ARGS, { mutantenAusgabe: ausgabe })
const arith = mitLabel(p, 'plankritik:arithmetik')[0]
soll(arith && arith.prompt.includes(marke), 'Marke außerhalb der letzten fünf Zeilen fehlt im Auftrag')
return fehler
})
fall('plankritik', 'mutanten_lauf in der Rückgabe ist bei einem Lauf nicht null', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e } = await PK(PK_ARGS, {})
soll(e && e.mutanten_lauf !== null && /EXIT mutanten_lauf/.test(e.mutanten_lauf), 'mutanten_lauf: ' + JSON.stringify(e && e.mutanten_lauf))
return fehler
})
fall('plankritik', 'Wurzel im Mutanten-Befehl ist ctx.kopie, nie args.projekt', async () => {
const { fehler, soll } = pruefe()
const { protokoll: p } = await PK({ ...PK_ARGS, projekt: 'D:/anderes-projekt' }, {})
const hilfe = mitLabel(p, 'werkzeug:mutanten')[0]
soll(hilfe && hilfe.prompt.includes(PK_ARGS.kopie) && !hilfe.prompt.includes('anderes-projekt'), 'Befehl nennt args.projekt statt ctx.kopie: ' + (hilfe && hilfe.prompt))
return fehler
})
fall('plankritik', 'modus muster: Linse erfährt, dass kein Mutant gefahren ist', async () => {
const { fehler, soll } = pruefe()
const ausgabe = 'wurzel: /tmp/x\nmodus: muster (nur Musterpruefung, kein Mutant gefahren)\nM1 OK Trefferzahl=1\nEXIT mutanten_lauf: 0'
const { protokoll: p } = await PK({ ...PK_ARGS, mutanten: 'muster' }, { mutantenAusgabe: ausgabe })
const arith = mitLabel(p, 'plankritik:arithmetik')[0]
soll(arith && /Kein Mutant ist gefahren/.test(arith.prompt), 'Hinweis auf Musterlauf fehlt')
soll(arith && !/prüfe bei OK an der Fehlermeldung/.test(arith.prompt), 'Satz "prüfe bei OK" steht trotz Modus muster noch da')
return fehler
})
fall('plankritik', 'Exit 2 des Mutantenlaufs: Linse erfährt, dass er vor oder während der Mutanten gescheitert ist', async () => {
const { fehler, soll } = pruefe()
const ausgabe = 'wurzel: /tmp/x\nbefunde:\n - x\nEXIT mutanten_lauf: 2'
const { protokoll: p } = await PK(PK_ARGS, { mutantenAusgabe: ausgabe })
const arith = mitLabel(p, 'plankritik:arithmetik')[0]
soll(arith && /vor oder während der Mutanten gescheitert \(Exit 2\)/.test(arith.prompt) && /Schwere wichtig/.test(arith.prompt), 'Hinweis auf Exit 2 fehlt: ' + (arith && arith.prompt.slice(-400)))
return fehler
})
fall('plankritik', 'mutanten_status und mutanten_modus in allen Zuständen', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: eAus } = await PK({ ...PK_ARGS, mutanten: 'aus' }, {})
soll(eAus && eAus.mutanten_status === 'aus' && eAus.mutanten_modus === 'aus', 'aus: ' + JSON.stringify(eAus && [eAus.mutanten_status, eAus.mutanten_modus]))
const { ergebnis: eOhneKopie } = await PK({ projekt: 'D:/x', planordner: 'D:/x/plan' }, {})
soll(eOhneKopie && eOhneKopie.mutanten_status === 'ohne_kopie' && eOhneKopie.mutanten_modus === 'voll', 'ohne_kopie: ' + JSON.stringify(eOhneKopie && [eOhneKopie.mutanten_status, eOhneKopie.mutanten_modus]))
const { ergebnis: eKeineToml } = await PK(PK_ARGS, { mutantenAusgabe: 'KEINE_MUTANTEN_TOML' })
soll(eKeineToml && eKeineToml.mutanten_status === 'keine_toml', 'keine_toml: ' + JSON.stringify(eKeineToml && eKeineToml.mutanten_status))
const { ergebnis: eOhneErgebnis } = await PK(PK_ARGS, { mutantenAusgabe: 'irgendeine unbrauchbare Ausgabe ohne Marker' })
soll(eOhneErgebnis && eOhneErgebnis.mutanten_status === 'ohne_ergebnis', 'ohne_ergebnis: ' + JSON.stringify(eOhneErgebnis && eOhneErgebnis.mutanten_status))
const { ergebnis: eExit2 } = await PK(PK_ARGS, { mutantenAusgabe: 'EXIT mutanten_lauf: 2' })
soll(eExit2 && eExit2.mutanten_status === 'exit_2', 'exit_2: ' + JSON.stringify(eExit2 && eExit2.mutanten_status))
const { ergebnis: eExitDefault } = await PK(PK_ARGS, {})
soll(eExitDefault && eExitDefault.mutanten_status === 'exit_0' && eExitDefault.mutanten_modus === 'voll', 'exit_0: ' + JSON.stringify(eExitDefault && [eExitDefault.mutanten_status, eExitDefault.mutanten_modus]))
const { ergebnis: eMuster } = await PK({ ...PK_ARGS, mutanten: 'muster' }, {})
soll(eMuster && eMuster.mutanten_modus === 'muster', 'muster: ' + JSON.stringify(eMuster && eMuster.mutanten_modus))
return fehler
})
// ---------------------------------------------------------------- pruefung
const PR_ARGS = { projekt: 'D:/x', plan: 'D:/x/plan' }
const PR = (a, k, o = {}) => fuehreAus(WF('pruefung'), a, antworten(k), { ordner: ORDNER, ...o })
fall('pruefung', 'Grundlinie: Ablesung vor und nach dem pruefer', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, fehler: f, protokoll: p } = await PR(PR_ARGS, {})
soll(f === null, 'Fehler: ' + f)
const folge = p.agents.slice().sort((x, y) => x.start - y.start).map(x => x.label)
soll(JSON.stringify(folge) === JSON.stringify(['verbrauch:pruefung:vor Prüfung', 'pruefung', 'verbrauch:pruefung:nach Prüfung']), 'Folge: ' + JSON.stringify(folge))
soll(e && e.verbrauch.length === 2 && e.verbrauch.every(v => v.status === 'ok' && v.plaetze === 0 && v.gebremst === false), JSON.stringify(e && e.verbrauch))
soll(e && e.effort === '(Frontmatter)', 'effort ' + (e && e.effort))
soll(mitLabel(p, 'pruefung')[0].modell === 'opus', 'pruefer nicht auf opus')
ohneVerstoss(p, soll)
return fehler
})
fall('pruefung', 'kritisch hebt auf xhigh', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, protokoll: p } = await PR({ ...PR_ARGS, kritisch: true }, {})
soll(e.effort === 'xhigh' && mitLabel(p, 'pruefung:kritisch')[0].effort === 'xhigh', 'effort ' + e.effort)
return fehler
})
fall('pruefung', 'verbrauch false: keine Ablesung', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, protokoll: p } = await PR({ ...PR_ARGS, verbrauch: false }, {})
soll(mitLabel(p, 'verbrauch:').length === 0 && e.verbrauch.length === 0, 'Ablesung trotz verbrauch false')
return fehler
})
fall('pruefung', 'als Kind ohne verbrauch false: Ablesung scheitert, Prüfung läuft', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, fehler: f, protokoll: p } = await PR(PR_ARGS, {}, { tiefe: 1 })
soll(f === null, 'Fehler: ' + f)
soll(mitLabel(p, 'pruefung').length === 1, 'pruefer nicht gelaufen')
soll(e && e.verbrauch.length === 2 && e.verbrauch.every(v => v.status === 'nicht_gelesen'), JSON.stringify(e && e.verbrauch))
return fehler
})
fall('pruefung', 'pruefer ohne Bericht: Fehler, beide Ablesungen gelaufen', async () => {
const { fehler, soll } = pruefe()
const { fehler: f, protokoll: p } = await PR(PR_ARGS, { pruefer: OHNE })
soll(f === 'pruefung: der pruefer hat keinen Bericht geliefert', 'Fehler: ' + f)
soll(mitLabel(p, 'verbrauch:').length === 2, 'Ablesungen: ' + mitLabel(p, 'verbrauch:').length)
return fehler
})
for (const verbrauch of ['nein', 0, null]) {
fall('pruefung', `verbrauch ${JSON.stringify(verbrauch)} wirft`, async () => {
const { fehler, soll } = pruefe()
const { fehler: f, protokoll: p } = await PR({ ...PR_ARGS, verbrauch }, {})
soll(f && /verbrauch muss/.test(f), 'Fehler: ' + f)
soll(p.agents.length === 0, 'Agent gestartet')
return fehler
})
}
// ---------------------------------------------------------------- welle-mit-pruefung
const WM_ARGS = { projekt: 'D:/x', planordner: 'D:/x/plan', tasks: [{ datei: 'task-01.md', modell: 'haiku' }], kopie: 'C:/tmp/k/mat', suite: 'pytest -q' }
const WM = (a, k) => fuehreAus(WF('welle-mit-pruefung'), a, antworten(k), { ordner: ORDNER })
fall('welle-mit-pruefung', 'Grundlinie: Welle, Vergleich, Ablesung um die Prüfung, Kind liest nicht selbst', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, fehler: f, protokoll: p } = await WM(WM_ARGS, {})
soll(f === null, 'Fehler: ' + f)
soll(e && e.status === 'fertig', 'status ' + (e && e.status))
soll(mitLabel(p, 'executer:').length === 1 && mitLabel(p, 'werkzeug:vergleich').length === 1 && mitLabel(p, 'pruefung').length === 1, 'Agenten: ' + JSON.stringify(p.agents.map(x => x.label)))
const ab = mitLabel(p, 'verbrauch:').map(x => x.label)
soll(JSON.stringify(ab) === JSON.stringify(['verbrauch:welle-mit-pruefung:vor Prüfung', 'verbrauch:welle-mit-pruefung:nach Prüfung']), 'Ablesungen: ' + JSON.stringify(ab))
const kind = p.workflows.find(w => w.ref === 'pruefung')
soll(kind && kind.args.verbrauch === false, 'Kind pruefung ohne verbrauch false')
soll(e.pruefung.verbrauch.length === 0 && mitLabel(p, 'verbrauch:pruefung:').length === 0, 'Kind hat selbst abgelesen')
soll(e.verbrauch.length === 2, 'verbrauch ' + e.verbrauch.length)
ohneVerstoss(p, soll)
return fehler
})
fall('welle-mit-pruefung', 'bis vergleich: keine Prüfung, pruefung_args mit Rohbelegen', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, fehler: f, protokoll: p } = await WM({ ...WM_ARGS, bis: 'vergleich' }, {})
soll(f === null, 'Fehler: ' + f)
soll(e && e.status === 'bis_vergleich', 'status ' + (e && e.status))
soll(mitLabel(p, 'pruefung').length === 0 && mitLabel(p, 'verbrauch:').length === 0, 'Prüfung oder Ablesung trotz bis')
const pa = e && e.pruefung_args
soll(pa && /VERGLEICH-OK/.test(pa.schwerpunkt) && pa.plan === 'D:/x/plan' && JSON.stringify(pa.tasks) === '["task-01.md"]', 'pruefung_args ' + JSON.stringify(pa))
soll(pa && !('verbrauch' in pa), 'pruefung_args trägt verbrauch')
soll(e && /nur lesend/.test(e.naechster_schritt), 'naechster_schritt ohne Lesevorgabe')
return fehler
})
fall('welle-mit-pruefung', 'bis vergleich bei unvollständiger Welle: keine pruefung_args', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e } = await WM({ ...WM_ARGS, bis: 'vergleich' }, { executer: OHNE })
soll(e && e.status === 'bis_vergleich' && e.pruefung_args === null && /unvollständig/.test(e.pruefung_grund), JSON.stringify(e && [e.status, e.pruefung_args, e.pruefung_grund]))
return fehler
})
fall('welle-mit-pruefung', 'unvollständige Welle: Prüfung übersprungen, keine Ablesung', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, protokoll: p } = await WM(WM_ARGS, { executer: OHNE })
soll(e.status === 'ohne_pruefung' && /unvollständig/.test(e.pruefung.uebersprungen), JSON.stringify([e.status, e.pruefung]))
soll(mitLabel(p, 'verbrauch:').length === 0 && mitLabel(p, 'pruefung').length === 0, 'Ablesung oder Prüfung trotz Lücke')
return fehler
})
fall('welle-mit-pruefung', 'verbrauch.js fehlt: Ablesungen nicht_gelesen, Welle und Prüfung bleiben', async () => {
const { fehler, soll } = pruefe()
const ohneVerbrauch = mkdtempSync(path.join(os.tmpdir(), 'ohne-verbrauch-'))
cpSync(ORDNER, ohneVerbrauch, { recursive: true })
rmSync(path.join(ohneVerbrauch, 'verbrauch.js'))
const { ergebnis: e, fehler: f } = await fuehreAus(path.join(ohneVerbrauch, 'welle-mit-pruefung.js'), WM_ARGS, antworten({}), { ordner: ohneVerbrauch })
rmSync(ohneVerbrauch, { recursive: true, force: true })
soll(f === null && e.status === 'fertig' && e.welle && e.pruefung.zaehlung, 'Fehler/Ergebnis: ' + f + ' ' + JSON.stringify(e && e.status))
soll(e && e.verbrauch.length === 2 && e.verbrauch.every(v => v.status === 'nicht_gelesen'), JSON.stringify(e && e.verbrauch))
return fehler
})
for (const bis of ['alles', '', 1]) {
fall('welle-mit-pruefung', `bis ${JSON.stringify(bis)} wirft`, async () => {
const { fehler, soll } = pruefe()
const { fehler: f, protokoll: p } = await WM({ ...WM_ARGS, bis }, {})
soll(f && /bis kennt nur/.test(f), 'Fehler: ' + f)
soll(p.agents.length === 0, 'Agent gestartet')
return fehler
})
}
// ---------------------------------------------------------------- planung
const T = (nr, voraussetzungen, welle) => ({ nr, thema: 'Thema ' + nr, datei: `task-${String(nr).padStart(2, '0')}-t.md`, modell: 'haiku', voraussetzungen, welle })
const DAG = [T(1, [], 1), T(2, [1], 2), T(3, [1], 2), T(4, [1], 2), T(5, [2, 3], 3), T(6, [4], 3), T(7, [], 1)]
const PL_ARGS = { projekt: 'D:/x', planordner: 'D:/x/plan', gemeinsam: 'GEMEINSAM', taskauftrag: 'Task {NR} {THEMA} in {DATEI}, Modell {MODELL}, Welle {WELLE}, vor: {VORAUSSETZUNGEN}', tasks: DAG }
const PL = (a, k, o = {}) => fuehreAus(WF('planung'), a, antworten(k), { ordner: ORDNER, ...o })
const planerVon = (p, nr) => p.agents.find(x => x.label === `planer:task-${String(nr).padStart(2, '0')}`)
function voraussetzungenEingehalten(p, tasks, soll) {
for (const t of tasks) {
const a = planerVon(p, t.nr)
if (!a) continue
for (const n of t.voraussetzungen) {
const v = planerVon(p, n)
soll(v && v.ende !== null && v.ende < a.start, `Task ${t.nr} startete vor dem Ende von Task ${n}`)
}
}
}
fall('planung', 'Grundlinie: Start nach den Voraussetzungen, ohne Wunsch zwei gleichzeitig', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, fehler: f, protokoll: p } = await PL(PL_ARGS, {})
soll(f === null, 'Fehler: ' + f)
soll(e && e.status === 'fertig' && e.geschrieben.length === 7, 'status ' + (e && e.status))
voraussetzungenEingehalten(p, DAG, soll)
soll(spitze(p, 'Taskdateien', 'opus') === 2, 'gleichzeitig: ' + spitze(p, 'Taskdateien', 'opus'))
const ab = mitLabel(p, 'verbrauch:').map(x => x.label)
soll(ab[0] === 'verbrauch:planung:Anfang' && ab[ab.length - 1] === 'verbrauch:planung:Ende' && ab.includes('verbrauch:planung:nach 4 Tasks'), 'Ablesungen: ' + JSON.stringify(ab))
soll(e && e.plaetze_verlauf[0] === 2, 'plaetze_verlauf ' + JSON.stringify(e && e.plaetze_verlauf))
soll(p.agents.filter(x => x.label.startsWith('planer:')).every(x => x.modell === 'opus' && x.agentType === 'planer'), 'planer nicht auf opus/planer')
ohneVerstoss(p, soll)
return fehler
})
fall('planung', 'Wunsch vier: drei bis vier gleichzeitig, Voraussetzungen halten', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, protokoll: p } = await PL({ ...PL_ARGS, plaetze: 4 }, {})
const s = spitze(p, 'Taskdateien', 'opus')
soll(s >= 3 && s <= 4, 'gleichzeitig: ' + s)
soll(e.status === 'fertig' && e.plaetze_verlauf[0] === 4, JSON.stringify([e.status, e.plaetze_verlauf]))
voraussetzungenEingehalten(p, DAG, soll)
return fehler
})
const FREI = [1, 2, 3, 4, 5, 6, 7, 8].map(n => T(n, [], 1))
fall('planung', 'Nachlesen senkt die Plätze: danach startet keiner neben zwei anderen', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, protokoll: p } = await PL({ ...PL_ARGS, tasks: FREI, plaetze: 4, nachlesen_alle: 2 }, { ablesungen: [ablesung(10, 10), ablesung(60, 10), ablesung(60, 10), ablesung(60, 10), ablesung(60, 10)] })
const zweite = mitLabel(p, 'verbrauch:')[1]
const danach = mitLabel(p, 'planer:').filter(a => a.start > zweite.start)
soll(danach.length > 0, 'nach der zweiten Ablesung startete kein Planer')
soll(danach.every(a => laufendBeiStart(p, a, 'planer:') <= 1), 'Start neben zwei oder mehr: ' + JSON.stringify(danach.map(a => [a.label, laufendBeiStart(p, a, 'planer:')])))
soll(e.status === 'fertig' && JSON.stringify(e.plaetze_verlauf.slice(0, 2)) === '[4,2]', JSON.stringify([e.status, e.plaetze_verlauf]))
return fehler
})
fall('planung', 'Nachlesen hebt die Plätze: danach laufen mehr als zwei', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, protokoll: p } = await PL({ ...PL_ARGS, tasks: FREI, plaetze: 4, nachlesen_alle: 2 }, { ablesungen: [ablesung(60, 10), ablesung(10, 10), ablesung(10, 10), ablesung(10, 10)] })
const zweite = mitLabel(p, 'verbrauch:')[1]
const danach = mitLabel(p, 'planer:').filter(a => a.start > zweite.start)
soll(danach.some(a => laufendBeiStart(p, a, 'planer:') >= 2), 'nach dem Heben nie mehr als zwei gleichzeitig')
soll(e.status === 'fertig' && JSON.stringify(e.plaetze_verlauf.slice(0, 2)) === '[2,4]', JSON.stringify([e.status, e.plaetze_verlauf]))
return fehler
})
fall('planung', 'gebremst schon am Anfang: kein Planer, Status gebremst', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, protokoll: p } = await PL(PL_ARGS, { ablesungen: [ablesung(90, 10)] })
soll(mitLabel(p, 'planer:').length === 0, 'Planer trotz Bremse')
soll(e.status === 'gebremst' && e.offen.length === 7, JSON.stringify([e.status, e.offen]))
soll(/nicht mit resumeFromRunId/.test(e.naechster_schritt), 'naechster_schritt ' + e.naechster_schritt)
return fehler
})
fall('planung', 'Startreihenfolge folgt Welle und Nummer, nicht der Liste', async () => {
const { fehler, soll } = pruefe()
const { protokoll: p } = await PL({ ...PL_ARGS, tasks: [T(3, [], 2), T(2, [], 1), T(1, [], 1)], plaetze: 1 }, {})
const folge = mitLabel(p, 'planer:').sort((x, y) => x.start - y.start).map(a => a.label)
soll(JSON.stringify(folge) === '["planer:task-01","planer:task-02","planer:task-03"]', 'Folge ' + JSON.stringify(folge))
return fehler
})
fall('planung', 'kein Nachlesen, wenn kein Task mehr offen ist', async () => {
const { fehler, soll } = pruefe()
const { protokoll: p } = await PL({ ...PL_ARGS, tasks: FREI.slice(0, 3), plaetze: 4, nachlesen_alle: 1 }, {})
const ab = mitLabel(p, 'verbrauch:').map(x => x.label)
soll(JSON.stringify(ab) === '["verbrauch:planung:Anfang","verbrauch:planung:Ende"]', 'Ablesungen: ' + JSON.stringify(ab))
return fehler
})
fall('planung', 'Ergebnis mit fremder Datei gilt als gescheitert', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, protokoll: p } = await PL(PL_ARGS, { planerDatei: (nr, richtig) => (nr === 2 ? 'D:/x/plan/task-99-t.md' : richtig) })
soll(e.gescheitert.includes(2) && e.blockiert.includes(5) && !planerVon(p, 5), JSON.stringify([e.gescheitert, e.blockiert]))
soll(p.logs.some(l => /statt task-02-t\.md/.test(l)), 'kein Hinweis auf die fremde Datei')
return fehler
})
fall('planung', 'Ergebnis mit ähnlichem Dateinamen gilt als gescheitert', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e } = await PL(PL_ARGS, { planerDatei: (nr, richtig) => (nr === 7 ? 'D:/x/plan/xtask-07-t.md' : richtig) })
soll(JSON.stringify(e.gescheitert) === '[7]', JSON.stringify(e.gescheitert))
return fehler
})
fall('planung', 'Nachlesen im Takt von nachlesen_alle, solange Tasks offen sind', async () => {
const { fehler, soll } = pruefe()
const { protokoll: p } = await PL({ ...PL_ARGS, tasks: FREI, plaetze: 2, nachlesen_alle: 2 }, { planerDauer: () => 100 })
const ab = mitLabel(p, 'verbrauch:').map(x => x.label.slice('verbrauch:planung:'.length))
soll(JSON.stringify(ab) === '["Anfang","nach 2 Tasks","nach 4 Tasks","nach 6 Tasks","Ende"]', 'Ablesungen: ' + JSON.stringify(ab))
return fehler
})
fall('planung', '5 Stunden bei 60 %: höchstens zwei gleichzeitig', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, protokoll: p } = await PL({ ...PL_ARGS, plaetze: 4 }, { ablesungen: [ablesung(60, 10), ablesung(60, 10)] })
soll(spitze(p, 'Taskdateien', 'opus') === 2, 'gleichzeitig: ' + spitze(p, 'Taskdateien', 'opus'))
soll(e.status === 'fertig', 'status ' + e.status)
voraussetzungenEingehalten(p, DAG, soll)
return fehler
})
fall('planung', 'Bremse beim Nachlesen: kein Start mehr, laufende enden', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, protokoll: p } = await PL({ ...PL_ARGS, nachlesen_alle: 2 }, { ablesungen: [ablesung(10, 10), ablesung(90, 10)] })
soll(e.status === 'gebremst' && e.offen.length > 0, JSON.stringify([e.status, e.offen]))
const zweite = mitLabel(p, 'verbrauch:')[1]
soll(zweite && p.agents.filter(x => x.label.startsWith('planer:') && x.start > zweite.start).length === 0, 'Planer nach der Bremse gestartet')
soll(p.agents.filter(x => x.label.startsWith('planer:')).every(x => x.ende !== null), 'laufender Planer nicht beendet')
return fehler
})
fall('planung', 'erste Voraussetzung scheitert: abhängige blockiert, unabhängige laufen', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, protokoll: p } = await PL(PL_ARGS, { planer: nr => (nr === 1 ? OHNE : undefined) })
soll(e.status === 'unvollstaendig', 'status ' + e.status)
soll(JSON.stringify(e.gescheitert) === '[1]' && JSON.stringify(e.blockiert) === '[2,3,4,5,6]', JSON.stringify([e.gescheitert, e.blockiert]))
soll(e.geschrieben.map(g => g.nr).join() === '7', 'geschrieben ' + e.geschrieben.map(g => g.nr))
soll([2, 3, 4, 5, 6].every(n => !planerVon(p, n)), 'blockierter Task gestartet')
return fehler
})
fall('planung', 'letzte Tasks scheitern: nichts blockiert', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e } = await PL(PL_ARGS, { planer: nr => (nr === 6 || nr === 7 ? OHNE : undefined) })
soll(e.status === 'unvollstaendig' && JSON.stringify(e.gescheitert.slice().sort()) === '[6,7]' && e.blockiert.length === 0, JSON.stringify([e.status, e.gescheitert, e.blockiert]))
soll(e.geschrieben.length === 5, 'geschrieben ' + e.geschrieben.length)
return fehler
})
fall('planung', 'erledigt: Task 1 startet nicht, die abhängigen schon', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, protokoll: p } = await PL({ ...PL_ARGS, erledigt: [1] }, {})
soll(!planerVon(p, 1), 'Task 1 gestartet')
soll(e.status === 'fertig' && e.geschrieben.length === 6, JSON.stringify([e.status, e.geschrieben.length]))
return fehler
})
fall('planung', 'drei Planer ohne Ergebnis: Abbruch, kein weiterer Start', async () => {
const { fehler, soll } = pruefe()
const frei = [1, 2, 3, 4, 5, 6].map(n => T(n, [], 1))
const { ergebnis: e, protokoll: p } = await PL({ ...PL_ARGS, tasks: frei, plaetze: 2 }, { planer: nr => (nr <= 3 ? OHNE : undefined), planerDauer: nr => (nr <= 3 ? 5 : 60) })
soll(e.status === 'abgebrochen', 'status ' + e.status)
soll(!planerVon(p, 5) && !planerVon(p, 6) && JSON.stringify(e.offen) === '[5,6]', 'offen ' + JSON.stringify(e.offen))
return fehler
})
for (const [name, aendern, text] of [
['Voraussetzung ins Leere', a => ({ ...a, tasks: [T(1, [], 1), T(2, [99], 2)] }), /Voraussetzung 99 gibt es nicht/],
['Zyklus', a => ({ ...a, tasks: [T(1, [2], 1), T(2, [1], 2)] }), /Zyklus/],
['Selbstbezug', a => ({ ...a, tasks: [T(1, [1], 1)] }), /Zyklus/],
['doppelte Nummer', a => ({ ...a, tasks: [T(1, [], 1), T(1, [], 1)] }), /doppelt in tasks/],
['doppelte Datei', a => ({ ...a, tasks: [T(1, [], 1), { ...T(2, [], 1), datei: T(1, [], 1).datei }] }), /bei zwei Tasks/],
['doppelte Voraussetzung', a => ({ ...a, tasks: [T(1, [], 1), T(2, [1, 1], 2)] }), /Voraussetzung steht doppelt/],
['modell unbekannt', a => ({ ...a, tasks: [{ ...T(1, [], 1), modell: 'gpt' }] }), /modell "gpt"/],
['erledigt ins Leere', a => ({ ...a, erledigt: [99] }), /erledigt muss/],
['plaetze 5', a => ({ ...a, plaetze: 5 }), /plaetze muss/],
['nachlesen_alle 0', a => ({ ...a, nachlesen_alle: 0 }), /nachlesen_alle muss/],
['tasks leer', a => ({ ...a, tasks: [] }), /nicht leere Liste/],
['nr als Text', a => ({ ...a, tasks: [{ ...T(1, [], 1), nr: '1' }] }), /nr muss/],
['voraussetzungen fehlen', a => ({ ...a, tasks: [{ ...T(1, [], 1), voraussetzungen: undefined }] }), /voraussetzungen muss/],
['gemeinsam fehlt', a => ({ ...a, gemeinsam: '' }), /args fehlen: gemeinsam/],
]) {
fall('planung', `${name} wirft vor dem ersten Agenten`, async () => {
const { fehler, soll } = pruefe()
const { fehler: f, protokoll: p } = await PL(aendern(PL_ARGS), {})
soll(f && text.test(f), 'Fehler: ' + f)
soll(p.agents.length === 0, 'Agent gestartet')
return fehler
})
}
fall('planung', 'Auftrag nennt die Voraussetzungen und ersetzt alle Platzhalter', async () => {
const { fehler, soll } = pruefe()
const { protokoll: p } = await PL(PL_ARGS, {})
const t5 = planerVon(p, 5).prompt
soll(t5.includes('GEMEINSAM') && t5.includes('task-02-t.md') && t5.includes('task-03-t.md') && t5.includes('D:/x/plan/task-05-t.md'), 'Auftrag Task 5 unvollständig')
soll(!/\{(NR|THEMA|DATEI|MODELL|WELLE|VORAUSSETZUNGEN)\}/.test(t5), 'Platzhalter übrig')
soll(/Lies sie, bevor du einen Namen, eine Signatur oder einen Import daraus übernimmst/.test(t5), 'Lesehinweis fehlt')
soll(!/Lies sie/.test(planerVon(p, 1).prompt), 'Lesehinweis ohne Voraussetzungen')
soll(/vor: \(keine\)/.test(planerVon(p, 1).prompt), 'Task 1 ohne "(keine)"')
return fehler
})
fall('planung', 'alles erledigt: kein Agent, keine Ablesung', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, protokoll: p } = await PL({ ...PL_ARGS, erledigt: DAG.map(t => t.nr) }, {})
soll(p.agents.length === 0 && e.status === 'fertig' && e.verbrauch.length === 0, JSON.stringify([p.agents.length, e.status]))
return fehler
})
fall('planung', 'Ablesung ohne Ergebnis: höchstens zwei gleichzeitig', async () => {
const { fehler, soll } = pruefe()
const { protokoll: p } = await PL({ ...PL_ARGS, plaetze: 4 }, { ablesungen: [OHNE, OHNE] })
soll(spitze(p, 'Taskdateien', 'opus') === 2, 'gleichzeitig: ' + spitze(p, 'Taskdateien', 'opus'))
return fehler
})
fall('planung', 'als Kind: Rückfall auf zwei, Planung läuft durch', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, fehler: f, protokoll: p } = await PL({ ...PL_ARGS, plaetze: 4 }, {}, { tiefe: 1 })
soll(f === null && e.status === 'fertig', 'Fehler/status: ' + f + ' ' + (e && e.status))
soll(spitze(p, '▸ planung', 'opus') === 2, 'gleichzeitig: ' + spitze(p, '▸ planung', 'opus'))
return fehler
})
// ---------------------------------------------------------------- quorum
// Fälle für quorum.js (Plan .vault-tools/workflow-pruefung/docs/plans/2026-09-29-quorum/). Eigene
// Antworten, weil die Labels der Stimmen und der Codex-Hilfsagenten nur hier vorkommen. Vorgabe:
// Opus, Sonnet und Codex melden denselben Befund an src/a.py Zeile 10 (Mehrheit, blockierend),
// 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), 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')],
sonnet: [qB('src/a.py', '10', 'wichtig'), qB('src/b.py', '5', 'hinweis')],
codex: [qB('src/a.py', '10', 'blockierend')],
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'
// Gruppiert wie ein sorgfältiger Sonnet-Agent: gleiche Datei und gleiche Stelle.
function qGruppiere(prompt) {
const jeStelle = new Map()
for (const m of prompt.matchAll(/^\[([A-Z]\d+)\] Stimme \S+ \| (.+?) \| (.+?) \| \S+$/gm)) {
const k = m[2] + '|' + m[3]
if (!jeStelle.has(k)) jeStelle.set(k, [])
jeStelle.get(k).push(m[1])
}
return { gruppen: [...jeStelle.values()].map(ids => ({ ids, begruendung: 'gleiche Stelle' })) }
}
function qUrteile(prompt, urteil = () => 'bestaetigt') {
return { urteile: [...prompt.matchAll(/^Befund ([A-Z]\d+):$/gm)].map(m => ({ id: m[1], urteil: urteil(m[1]), fundstelle: 'src/a.py:1, zitiert', begruendung: 'nachgestellt' })) }
}
function antwortenQuorum(k = {}) {
let n = 0
const stimmen = k.stimmen || Q_STIMMEN
const ohne = k.ohne || []
const lauf = (label, text) => ok({ laeufe: [{ befehl: label, exit: 0, ausgabe: text }] })
return async aufruf => {
const label = aufruf.label || ''
if (ohne.includes(label)) return undefined
if (label.startsWith('verbrauch:')) {
const x = (k.ablesungen || [])[n++]
if (x === OHNE) return undefined
return ok(x === undefined ? ablesung(10, 10) : x)
}
if (label === 'werkzeug:paket') return lauf(label, k.paketAusgabe !== undefined ? k.paketAusgabe : 'Paket in C:/tmp/q\nQUORUM_PAKET ' + JSON.stringify(qPaket(k.paket)) + '\nEXIT paket: 0')
if (label === 'codex:start') return lauf(label, k.codexStart !== undefined ? k.codexStart : Q_START)
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:')) {
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)
}
}
const QR = (a, k, o = {}) => fuehreAus(WF('quorum'), a, antwortenQuorum(k), { ordner: ORDNER, ...o })
const qGruppe = (e, status) => (e && Array.isArray(e.gruppen) ? e.gruppen.filter(g => g.status === status) : [])
// Die IDs, die der Agent mit diesem Label unter "Befund <ID>:" zur Nachprüfung bekam.
function qNachgeprueft(p) {
const a = mitLabel(p, 'nachpruefung')[0]
return a ? [...a.prompt.matchAll(/^Befund ([A-Z]\d+):$/gm)].map(m => m[1]) : null
}
fall('quorum', 'Grundlinie: Paket, drei Stimmen gleichzeitig, Mehrheit sperrt, Einzelbefund nachgeprüft, Bericht', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, fehler: f, protokoll: p } = await QR(Q_ARGS, {})
soll(f === null, 'Fehler: ' + f)
soll(e && e.status === 'fertig' && e.urteil === 'nicht_frei', 'status/urteil ' + JSON.stringify(e && [e.status, e.urteil, e.grund]))
const erst = p.agents.slice().sort((x, y) => x.start - y.start)[0]
soll(erst && erst.label === 'werkzeug:paket' && erst.modell === 'sonnet' && erst.effort === 'low', 'erster Agent ' + JSON.stringify(erst && [erst.label, erst.modell, erst.effort]))
const ab = mitLabel(p, 'verbrauch:').map(x => x.label)
soll(JSON.stringify(ab) === JSON.stringify(['verbrauch:quorum:vor Stimmen', 'verbrauch:quorum:Ende']), 'Ablesungen: ' + JSON.stringify(ab))
const opus = mitLabel(p, 'stimme:opus')[0]
const sonnet = mitLabel(p, 'stimme:sonnet')[0]
soll(opus && opus.modell === 'opus' && opus.agentType === 'quorum-stimme' && opus.effort === undefined, 'Opus-Stimme ' + JSON.stringify(opus && [opus.modell, opus.agentType, opus.effort]))
soll(sonnet && sonnet.modell === 'sonnet' && sonnet.agentType === 'quorum-stimme' && sonnet.effort === undefined, 'Sonnet-Stimme ' + JSON.stringify(sonnet && [sonnet.modell, sonnet.agentType, sonnet.effort]))
soll(opus && /^Auftragsart: Stimme\.$/m.test(opus.prompt) && opus.prompt.includes('Prüfpaket: C:/tmp/q/paket.md') && opus.prompt.includes('Wurzel: D:/x'), 'Auftrag der Stimme ohne Auftragsart, Paket oder Wurzel')
soll(mitLabel(p, 'stimme:haiku').length === 0, 'Haiku trotz gültiger Codex-Stimme')
const start = mitLabel(p, 'codex:start')[0]
soll(start && laufendBeiStart(p, start, 'stimme:') === 2, 'Codex startete nicht neben Opus und Sonnet')
soll(mitLabel(p, 'codex:').length === 3 && mitLabel(p, 'codex:').every(x => x.modell === 'sonnet' && x.effort === 'low' && !x.agentType), 'Codex-Hilfsagenten: ' + JSON.stringify(mitLabel(p, 'codex:').map(x => [x.label, x.modell, x.effort])))
const zf = mitLabel(p, 'zusammenfuehren')[0]
const np = mitLabel(p, 'nachpruefung')[0]
soll(zf && zf.modell === 'sonnet' && zf.effort === 'medium' && !zf.agentType, 'Zusammenführen ' + JSON.stringify(zf && [zf.modell, zf.effort, zf.agentType]))
soll(np && np.modell === 'sonnet' && np.agentType === 'quorum-stimme' && /^Auftragsart: Nachprüfung\.$/m.test(np.prompt), 'Nachprüfung ' + JSON.stringify(np && [np.modell, np.agentType]))
soll(JSON.stringify(qNachgeprueft(p)) === '["O2"]', 'nachgeprüft: ' + JSON.stringify(qNachgeprueft(p)))
const g = qGruppe(e, 'mehrheit')[0]
soll(g && g.schwere === 'blockierend' && JSON.stringify(g.stimmen.slice().sort()) === '["codex","opus","sonnet"]', 'Mehrheitsgruppe ' + JSON.stringify(g && [g.schwere, g.stimmen]))
soll(qGruppe(e, 'hinweis_ungeprueft').length === 1 && qGruppe(e, 'bestaetigt').length === 1, 'Status der Gruppen ' + JSON.stringify(e && e.gruppen && e.gruppen.map(x => x.status)))
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: PAKET\n', '\ncodex: abgegeben\n---\n', 'Projekt: [README](../../README.md)']) soll(md.includes(z), 'Bericht ohne ' + JSON.stringify(z))
ohneVerstoss(p, soll)
return fehler
})
fall('quorum', 'Bericht: Stimmentabelle mit Codex-Laufzeit, Mehrheit zuerst, Verbrauch vor und nach dem Lauf', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e } = await QR(Q_ARGS, {})
const md = (e && e.bericht_md) || ''
for (const z of [
'| Opus | ja | 2 | 2 | 2 | 1 | nicht gemessen |',
'| Codex | ja | 1 | 1 | 1 | 0 | 3 min 12 s |',
'### G1: blockierend, Mehrheit (Opus, Sonnet, Codex)',
'Verbrauch vor den Stimmen: 5 Stunden 10 %, Woche 10 %; am Ende: 5 Stunden 10 %, Woche 10 %.',
]) soll(md.includes(z), 'Bericht ohne ' + JSON.stringify(z))
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()
const stimmen = { opus: [qB('src/a.py', '10', 'blockierend')], sonnet: [qB('src/b.py', '5', 'hinweis')], codex: [] }
const { ergebnis: e } = await QR(Q_ARGS, { stimmen, urteile: pr => qUrteile(pr, () => urteil) })
soll(e && e.status === 'fertig' && e.urteil === erwartet, JSON.stringify(e && [e.status, e.urteil, e.grund]))
soll(e && e.zaehlung.opus.alleinfunde === (urteil === 'bestaetigt' ? 1 : 0), 'alleinfunde ' + JSON.stringify(e && e.zaehlung && e.zaehlung.opus))
return fehler
})
}
fall('quorum', 'Nachprüfung ohne Ergebnis: ungeprüfter blockierender Einzelbefund sperrt, ungeprüfter wichtiger nicht', async () => {
const { fehler, soll } = pruefe()
const a = await QR(Q_ARGS, { stimmen: { opus: [qB('src/a.py', '10', 'blockierend')], sonnet: [qB('src/b.py', '3', 'wichtig')], codex: [] }, ohne: ['nachpruefung'] })
soll(a.ergebnis && a.ergebnis.urteil === 'nicht_frei' && qGruppe(a.ergebnis, 'nicht_geprueft').length === 2, 'blockierend: ' + JSON.stringify(a.ergebnis && [a.ergebnis.urteil, a.ergebnis.gruppen.map(g => g.status)]))
soll(a.ergebnis && a.ergebnis.hinweise.some(h => /Nachprüfung ohne Ergebnis/.test(h)), 'kein Hinweis auf die fehlende Nachprüfung')
const b = await QR(Q_ARGS, { stimmen: { opus: [qB('src/a.py', '10', 'wichtig')], sonnet: [qB('src/b.py', '3', 'wichtig')], codex: [] }, ohne: ['nachpruefung'] })
soll(b.ergebnis && b.ergebnis.urteil === 'frei', 'wichtig: ' + JSON.stringify(b.ergebnis && [b.ergebnis.urteil, b.ergebnis.grund]))
return fehler
})
fall('quorum', 'nur Hinweise mit je einer Stimme: keine Nachprüfung, frei', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, protokoll: p } = await QR(Q_ARGS, { stimmen: { opus: [qB('src/a.py', '1', 'hinweis')], sonnet: [qB('src/b.py', '2', 'hinweis')], codex: [] } })
soll(mitLabel(p, 'nachpruefung').length === 0, 'Nachprüfung für Hinweise gestartet')
soll(e && e.urteil === 'frei' && qGruppe(e, 'hinweis_ungeprueft').length === 2, JSON.stringify(e && [e.urteil, e.gruppen.map(g => g.status)]))
return fehler
})
fall('quorum', 'Befund ohne Beleg zählt nicht für die Mehrheit und geht nicht ins Zusammenführen', async () => {
const { fehler, soll } = pruefe()
const stimmen = { opus: [qB('src/a.py', '10', 'blockierend', ' '), qB('src/b.py', '1', 'wichtig')], sonnet: [qB('src/a.py', '10', 'blockierend')], codex: [] }
const { ergebnis: e, protokoll: p } = await QR(Q_ARGS, { stimmen })
const zf = mitLabel(p, 'zusammenfuehren')[0]
soll(zf && !zf.prompt.includes('[O1]') && zf.prompt.includes('[O2]') && zf.prompt.includes('[S1]'), 'Zusammenführen: ' + (zf && zf.prompt.slice(0, 400)))
soll(e && JSON.stringify(e.unbelegt.map(b => b.id)) === '["O1"]', 'unbelegt ' + JSON.stringify(e && e.unbelegt))
soll(qGruppe(e, 'mehrheit').length === 0, 'Mehrheit trotz unbelegtem Befund')
soll(JSON.stringify(qNachgeprueft(p)) === '["O2","S1"]', 'nachgeprüft: ' + JSON.stringify(qNachgeprueft(p)))
soll(e && e.zaehlung.opus.gesamt === 2 && e.zaehlung.opus.belegt === 1, JSON.stringify(e && e.zaehlung && e.zaehlung.opus))
return fehler
})
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 => /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: [] }
const { ergebnis: e, protokoll: p } = await QR(Q_ARGS, { stimmen, gruppen: () => ({ gruppen: [{ ids: ['O1', 'X9'], begruendung: 'a' }, { ids: ['O1', 'S1'], begruendung: 'b' }] }) })
soll(qGruppe(e, 'mehrheit').length === 0, 'Mehrheit aus einer doppelt vergebenen ID')
soll(e && e.hinweise.some(h => /unbekannte ID X9/.test(h)) && e.hinweise.some(h => /ID O1 steht in zwei Gruppen/.test(h)), 'Hinweise: ' + JSON.stringify(e && e.hinweise))
soll(JSON.stringify(qNachgeprueft(p)) === '["O1","S1"]', 'nachgeprüft: ' + JSON.stringify(qNachgeprueft(p)))
return fehler
})
fall('quorum', 'Zusammenführen ohne Ergebnis: jeder belegte Befund eine Gruppe, alle Einzelbefunde über Hinweis nachgeprüft', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, protokoll: p } = await QR(Q_ARGS, { ohne: ['zusammenfuehren'] })
soll(qGruppe(e, 'mehrheit').length === 0, 'Mehrheit ohne Zusammenführen')
soll(JSON.stringify(qNachgeprueft(p)) === '["O1","O2","S1","C1"]', 'nachgeprüft: ' + JSON.stringify(qNachgeprueft(p)))
soll(e && e.hinweise.some(h => /Zusammenführen ohne Ergebnis/.test(h)), 'kein Hinweis')
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()
const stimmen = { opus: [qB('src/a.py', '10', erst)], sonnet: [qB('src/a.py', '10', zweit)], codex: [] }
const { ergebnis: e, protokoll: p } = await QR(Q_ARGS, { stimmen })
const g = qGruppe(e, 'mehrheit')[0]
soll(g && g.schwere === 'blockierend' && e.urteil === 'nicht_frei', JSON.stringify(e && [e.urteil, g && g.schwere]))
soll(mitLabel(p, 'nachpruefung').length === 0, 'Mehrheitsgruppe nachgeprüft')
return fehler
})
}
fall('quorum', 'Reihenfolge: blockierender Einzelbefund an erster und letzter Stelle einer Stimme wird nachgeprüft', async () => {
const { fehler, soll } = pruefe()
const stimmen = { opus: [qB('src/a.py', '1', 'blockierend'), qB('src/x.py', '2', 'hinweis'), qB('src/y.py', '3', 'hinweis'), qB('src/a.py', '99', 'blockierend')], sonnet: [qB('src/z.py', '1', 'hinweis')], codex: [] }
const { protokoll: p } = await QR(Q_ARGS, { stimmen })
soll(JSON.stringify(qNachgeprueft(p)) === '["O1","O4"]', 'nachgeprüft: ' + JSON.stringify(qNachgeprueft(p)))
return fehler
})
for (const [name, k, grund] of [
['Status vor dem Lauf nicht gelesen', { codexStart: 'Fehler beim Lesen\nEXIT status: 1' }, /Status vor dem Lauf/],
['Auftrag ohne jobId', { codexStart: '{\n "running": []\n}\nEXIT status: 0\nFehler\nEXIT task: 1' }, /nicht gestartet/],
['usage limit im Log', { codexEnde: { usage_limit_treffer: 1 } }, /usage limit/],
['Lesebeleg mit fremder Lesemarke', { codexEnde: { lesebeleg: 'Lesemarke: ffffffffffffffff' } }, /Lesebeleg/],
['Job endet failed', { codexEnde: { job_status: 'failed' } }, /failed/],
['Log nicht gelesen', { codexEnde: { log_gelesen: false } }, /Log nicht gelesen/],
['keine Antwort im Format', { codexEnde: { antwort_gefunden: false } }, /Format/],
['Auswertung mit Exit 2', { codexEnde: { exit_codex: 2 } }, /Exit 2/],
['Auswertung ohne Ergebnis', { ohne: ['codex:ende'] }, /ohne Ergebnis/],
]) {
fall('quorum', `Codex ungültig (${name}): Haiku springt nach den übrigen Stimmen ein, Bericht sagt es`, async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, protokoll: p } = await QR(Q_ARGS, k)
const haiku = mitLabel(p, 'stimme:haiku')[0]
soll(haiku && haiku.modell === 'haiku' && haiku.agentType === 'quorum-stimme', 'Haiku nicht eingesprungen')
soll(e && e.stimmen.codex.abgegeben === false && grund.test(e.stimmen.codex.grund), 'grund ' + JSON.stringify(e && e.stimmen.codex.grund))
soll(e && e.bericht_md && e.bericht_md.includes('\ncodex: ersetzt\n') && e.bericht_md.includes('| Haiku (Ersatz für Codex) | ja |'), 'Bericht ohne Ersatz')
const vorher = p.agents.filter(x => /^(stimme:opus|stimme:sonnet|codex:)/.test(x.label || ''))
soll(haiku && vorher.every(x => x.ende !== null && x.ende < haiku.start), 'Haiku startete vor dem Ende der übrigen Stimmen')
soll(!/Status|jobId/.test(name) || mitLabel(p, 'codex:warten').length === 0, 'gewartet ohne gestarteten Auftrag')
return fehler
})
}
fall('quorum', 'Codex nie fertig: so viele Warterunden wie codex_runden, dann Abbruch und Haiku', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, protokoll: p } = await QR({ ...Q_ARGS, codex_runden: 2 }, { codexWarten: () => Q_WARTEN_OFFEN })
soll(mitLabel(p, 'codex:warten:').length === 2, 'Warterunden: ' + mitLabel(p, 'codex:warten:').length)
const ab = mitLabel(p, 'codex:abbruch')[0]
soll(ab && ab.prompt.includes("cancel 'task-q1' --json"), 'Abbruch fehlt')
soll(mitLabel(p, 'codex:ende').length === 0 && mitLabel(p, 'stimme:haiku').length === 1, 'Ende oder Haiku: ' + JSON.stringify(p.agents.map(x => x.label)))
soll(e && /nach 2 Warterunden/.test(e.stimmen.codex.grund), 'grund ' + JSON.stringify(e && e.stimmen.codex.grund))
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' })
soll(mitLabel(p, 'codex:warten:').length === 1 && mitLabel(p, 'codex:abbruch').length === 1, 'Agenten: ' + JSON.stringify(p.agents.map(x => x.label)))
soll(e && /Warten auf Codex gescheitert/.test(e.stimmen.codex.grund) && mitLabel(p, 'stimme:haiku').length === 1, 'grund ' + JSON.stringify(e && e.stimmen.codex.grund))
return fehler
})
fall('quorum', 'codex false: kein Codex-Agent, Haiku springt ein', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, protokoll: p } = await QR({ ...Q_ARGS, codex: false }, {})
soll(mitLabel(p, 'codex:').length === 0 && mitLabel(p, 'stimme:haiku').length === 1, 'Agenten: ' + JSON.stringify(p.agents.map(x => x.label)))
soll(e && /abgeschaltet/.test(e.stimmen.codex.grund) && e.bericht_md.includes('\ncodex: ersetzt\n'), 'grund oder Bericht: ' + JSON.stringify(e && e.stimmen.codex.grund))
return fehler
})
fall('quorum', 'Codex-Befehle: Plugin 1.0.6, nur lesend, Auftrag aus dem Paket, Warten mit zehn Minuten', async () => {
const { fehler, soll } = pruefe()
const { protokoll: p } = await QR(Q_ARGS, {})
const start = mitLabel(p, 'codex:start')[0]
const warten = mitLabel(p, 'codex:warten:1')[0]
const ende = mitLabel(p, 'codex:ende')[0]
for (const z of ['openai-codex/codex/1.0.6/scripts/codex-companion.mjs', `status --json --cwd 'D:/x'`, `task --background --json --cwd 'D:/x' --prompt-file 'C:/tmp/q/codex-auftrag.md'`]) soll(start && start.prompt.includes(z), 'Start ohne ' + z)
soll(p.agents.every(x => !String(x.prompt).includes('--write')), 'ein Auftrag enthält --write')
soll(warten && warten.prompt.includes(`status 'task-q1' --wait --timeout-ms 540000 --json --cwd 'D:/x'`) && /timeout auf 600000/.test(warten.prompt), 'Warten: ' + (warten && warten.prompt))
for (const z of [`result 'task-q1' --json --cwd 'D:/x' > 'C:/tmp/q/codex-ergebnis.json'`, `quorum_werkzeug.py' codex --ergebnis 'C:/tmp/q/codex-ergebnis.json'`]) soll(ende && ende.prompt.includes(z), 'Ende ohne ' + z)
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 && 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
})
fall('quorum', 'Ablesung ohne Ergebnis: Opus-Stimme läuft nach der alten Obergrenze', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, protokoll: p } = await QR(Q_ARGS, { ablesungen: [OHNE] })
soll(mitLabel(p, 'stimme:opus').length === 1 && e && e.status === 'fertig' && e.verbrauch[0].status === 'nicht_gelesen', JSON.stringify(e && [e.status, e.verbrauch[0]]))
return fehler
})
fall('quorum', 'als Kind: verbrauch.js wirft, Rückfall, Stimmen laufen', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, fehler: f, protokoll: p } = await QR(Q_ARGS, {}, { tiefe: 1 })
soll(f === null && e && e.status === 'fertig', 'Fehler/status: ' + f + ' ' + (e && e.status))
soll(mitLabel(p, 'stimme:opus').length === 1 && mitLabel(p, 'verbrauch:').length === 0, 'Agenten: ' + JSON.stringify(p.agents.map(x => x.label)))
soll(e && e.verbrauch.length === 2 && e.verbrauch.every(v => v.status === 'nicht_gelesen'), JSON.stringify(e && e.verbrauch))
return fehler
})
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, 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`, '\nstand: PAKET\n', '2026-09-20-version.md']) soll(md.includes(z), 'Bericht ohne ' + JSON.stringify(z))
return fehler
})
}
fall('quorum', 'Paket nicht gebaut: Fehler mit der BEFUND-Zeile, keine Stimme', async () => {
const { fehler, soll } = pruefe()
const { fehler: f, protokoll: p } = await QR(Q_ARGS, { paketAusgabe: 'BEFUND: Arbeitsbaum nicht sauber, die Stimmen lesen ihn statt HEAD: M src/a.py\nEXIT paket: 2' })
soll(f && /Paket nicht gebaut/.test(f) && /Arbeitsbaum nicht sauber/.test(f), 'Fehler: ' + f)
soll(p.agents.length === 1, 'Agenten: ' + JSON.stringify(p.agents.map(x => x.label)))
return fehler
})
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()
const { fehler: f, protokoll: p } = await QR(Q_ARGS, k)
soll(f && text.test(f), 'Fehler: ' + f)
soll(p.agents.length === 1, 'Agenten: ' + JSON.stringify(p.agents.map(x => x.label)))
return fehler
})
}
fall('quorum', 'weniger als zwei Stimmen: kein Urteil, kein Bericht, Ablesung am Ende', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e, protokoll: p } = await QR(Q_ARGS, { ohne: ['stimme:sonnet', 'stimme:haiku'], codexEnde: { usage_limit_treffer: 1 } })
soll(e && e.status === 'unvollstaendig' && e.urteil === 'kein_urteil' && e.bericht_md === null && e.bericht_pfad === null, JSON.stringify(e && [e.status, e.urteil, e.bericht_pfad]))
soll(mitLabel(p, 'zusammenfuehren').length === 0 && mitLabel(p, 'nachpruefung').length === 0, 'Zusammenführen oder Nachprüfung trotz einer Stimme')
soll(mitLabel(p, 'verbrauch:').length === 2, 'Ablesungen: ' + mitLabel(p, 'verbrauch:').length)
return fehler
})
fall('quorum', 'zwei Stimmen reichen: Opus ohne Ergebnis, Bericht nennt es', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e } = await QR(Q_ARGS, { ohne: ['stimme:opus'] })
soll(e && e.status === 'fertig' && e.zaehlung.opus.abgegeben === false, JSON.stringify(e && [e.status, e.zaehlung]))
soll(e && e.bericht_md.includes('| Opus | nein: ohne Ergebnis |'), 'Bericht ohne Zeile für Opus')
return fehler
})
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)")`,
`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 PAKET und Wikilink auf die Übersicht', async () => {
const { fehler, soll } = pruefe()
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 '${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: ' + K) && start && start.prompt.includes(`--cwd '${K}'`), 'Wurzel nicht der Ordner')
const md = (e && e.bericht_md) || ''
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/],
['projekt fehlt', { anlass: 'einspielen' }, /args fehlen: projekt/],
['projekt leer', { anlass: 'version', projekt: ' ' }, /projekt muss ein nicht leerer Text/],
['basis leer', { ...Q_ARGS, basis: '' }, /basis muss ein nicht leerer Text/],
['dateien bei Code', { ...Q_ARGS, dateien: ['a.md'] }, /dateien gilt nur für kundendokument/],
['dateien fehlen', { anlass: 'kundendokument', ordner: 'D:/k', uebersicht: 'u.md' }, /dateien muss/],
['dateien leer', { anlass: 'kundendokument', ordner: 'D:/k', uebersicht: 'u.md', dateien: [] }, /dateien muss/],
['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/],
['codex_runden 0', { ...Q_ARGS, codex_runden: 0 }, /codex_runden muss/],
['codex_runden 7', { ...Q_ARGS, codex_runden: 7 }, /codex_runden muss/],
['codex_runden 1.5', { ...Q_ARGS, codex_runden: 1.5 }, /codex_runden muss/],
]) {
fall('quorum', `${name} wirft vor dem ersten Agenten`, async () => {
const { fehler, soll } = pruefe()
const { fehler: f, protokoll: p } = await QR(a, {})
soll(f && text.test(f), 'Fehler: ' + f)
soll(p.agents.length === 0, 'Agent gestartet')
return fehler
})
}
// Klartext im Bericht (Nachbesserung Task 06): Stimmen, Nachprüfung und Hilfsagenten zitieren
// Wikilink, Einbettung, Tag und Abschnittslink. Q_KLAR ist derselbe Text, wie er im Bericht
// stehen muss: lesbar, aber ohne Link, Einbettung und Tag.
const Q_ZITATE = '[[a/b]] ![[c]] #needs-review [[#x]]'
const Q_KLAR = '\\[\\[a/b]] !\\[\\[c]] \\#needs-review \\[\\[\\#x]]'
const qZ = schwere => ({ datei: 'src/a.py', stelle: Q_ZITATE, schwere, aussage: Q_ZITATE, beleg: Q_ZITATE })
// Was Obsidian als Wikilink, Einbettung oder Tag lesen könnte. Der Tag zählt hier schon hinter
// jedem Zeichen außer Buchstabe, Ziffer, Unterstrich und Backslash, nicht erst nach Leerraum.
function qObsidian(text) {
const funde = []
if (/(^|[^!])\[\[/.test(text)) funde.push('Wikilink')
if (text.includes('![[')) funde.push('Einbettung')
if (/(^|[^\p{L}\p{N}_\\])#[^\s#]/u.test(text)) funde.push('Tag')
return funde
}
const qSauber = (soll, text) => { const f = qObsidian(text); soll(f.length === 0, 'Obsidian-Syntax im Bericht: ' + f.join(', ')) }
fall('quorum', 'Klartext im Bericht: Stelle, Aussage und Beleg einer Gruppe ohne Wikilink, Einbettung und Tag, README-Link bleibt', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e } = await QR(Q_ARGS, { stimmen: { opus: [qZ('blockierend')], sonnet: [qZ('blockierend')], codex: [] } })
const md = (e && e.bericht_md) || ''
qSauber(soll, md)
for (const z of [
'### G1: blockierend, Mehrheit (Opus, Sonnet)\n\n`src/a.py`, Stelle ' + Q_KLAR + '\n',
'- O1 (Opus, blockierend, Stelle ' + Q_KLAR + '): ' + Q_KLAR + '\n Beleg: ' + Q_KLAR + '\n',
'\n\nProjekt: [README](../../README.md)\n\n',
]) soll(md.includes(z), 'Bericht ohne ' + JSON.stringify(z))
return fehler
})
fall('quorum', 'Klartext im Bericht: Fundstelle und Begründung der Nachprüfung ohne Wikilink, Einbettung und Tag', async () => {
const { fehler, soll } = pruefe()
const stimmen = { opus: [qB('src/a.py', '10', 'blockierend')], sonnet: [qB('src/b.py', '5', 'hinweis')], codex: [] }
const urteile = pr => ({ urteile: [...pr.matchAll(/^Befund ([A-Z]\d+):$/gm)].map(m => ({ id: m[1], urteil: 'bestaetigt', fundstelle: Q_ZITATE, begruendung: Q_ZITATE })) })
const { ergebnis: e } = await QR(Q_ARGS, { stimmen, urteile })
const md = (e && e.bericht_md) || ''
qSauber(soll, md)
const z = '\nNachprüfung: bestaetigt, ' + Q_KLAR + '. ' + Q_KLAR + '\n'
soll(md.includes(z), 'Bericht ohne ' + JSON.stringify(z))
return fehler
})
fall('quorum', 'Klartext im Bericht: Unbelegt und Nicht prüfbar ohne Wikilink, Einbettung und Tag', async () => {
const { fehler, soll } = pruefe()
const stimmen = { opus: [{ ...qZ('wichtig'), beleg: '' }], sonnet: [qB('src/b.py', '5', 'hinweis')], codex: [] }
const { ergebnis: e } = await QR(Q_ARGS, { stimmen, nichtPruefbar: { sonnet: [Q_ZITATE] } })
const md = (e && e.bericht_md) || ''
qSauber(soll, md)
for (const z of [
'## Unbelegt\n\n- O1 (Opus, wichtig) `src/a.py`, Stelle ' + Q_KLAR + ': ' + Q_KLAR + '\n',
'## Nicht prüfbar\n\n- Sonnet: ' + Q_KLAR + '\n',
]) soll(md.includes(z), 'Bericht ohne ' + JSON.stringify(z))
return fehler
})
fall('quorum', 'Klartext im Bericht: Hinweise des Laufs und Grund der Codex-Stimme ohne Wikilink, Einbettung und Tag', async () => {
const { fehler, soll } = pruefe()
const stimmen = { opus: [qB('src/a.py', '10', 'hinweis')], sonnet: [qB('src/b.py', '5', 'hinweis')] }
const k = { stimmen, paket: { hinweise: [Q_ZITATE] }, codexEnde: { job_status: Q_ZITATE }, gruppen: () => ({ gruppen: [{ ids: [Q_ZITATE], begruendung: 'zitiert' }] }) }
const { ergebnis: e } = await QR(Q_ARGS, k)
const md = (e && e.bericht_md) || ''
qSauber(soll, md)
for (const z of [
'| Codex | nein: Codex-Auftrag endete mit ' + Q_KLAR + ' |',
'## Hinweise des Laufs\n\n- ' + Q_KLAR + '\n',
'\n- Codex ohne gültige Stimme (Codex-Auftrag endete mit ' + Q_KLAR + '); Haiku ist eingesprungen\n',
'\n- Zusammenführen: unbekannte ID ' + Q_KLAR + ' verworfen\n',
]) soll(md.includes(z), 'Bericht ohne ' + JSON.stringify(z))
return fehler
})
fall('quorum', 'Klartext im Bericht eines Kundendokuments: Hub-Zeile bleibt Wikilink, Frontmatter bleibt, zitierte Syntax nicht', async () => {
const { fehler, soll } = pruefe()
const K = 'D:/Claude/IT Workspace/Beispiel/abgabe'
const HUB = 'Übersicht: [[IT Workspace/Beispiel/abgabe/00 Übersicht|00 Übersicht.md]]'
const args = { anlass: 'kundendokument', ordner: K, dateien: ['kapitel/01.md'], uebersicht: '00 Übersicht.md' }
const paket = { anlass: 'kundendokument', art: 'doku', wurzel: K, basis: 'erstes Quorum', stand: 'sha256:' + 'c'.repeat(64), vorher: null, nummer: 1 }
const { ergebnis: e } = await QR(args, { paket, stimmen: { opus: [qZ('blockierend')], sonnet: [qZ('blockierend')], codex: [] } })
const md = (e && e.bericht_md) || ''
qSauber(soll, md.replace(HUB, ''))
soll(md.includes('\n\n' + HUB + '\n\n') && md.split('[[').length === 2, 'Hub-Zeile nicht genau einmal als Wikilink: ' + JSON.stringify(md.split('\n').filter(x => x.includes('Übersicht'))))
for (const z of ['---\ntype: note\n', '\ntags: [quorum]\n', '- O1 (Opus, blockierend, Stelle ' + Q_KLAR + '): ' + Q_KLAR + '\n']) soll(md.includes(z), 'Bericht ohne ' + JSON.stringify(z))
return fehler
})
// ---------------------------------------------------------------- Lauf
let gescheitert = 0
const grundlinieGescheitert = new Set()
for (const f of faelle) {
let fehler
try { fehler = await f.lauf() } catch (e) { fehler = ['Absturz im Fall: ' + (e && e.stack || e)] }
if (f.name.startsWith('Grundlinie') && fehler.length) grundlinieGescheitert.add(f.workflow)
if (fehler.length) {
gescheitert++
console.log(`FEHLER ${f.workflow}: ${f.name}\n ${fehler.join('\n ')}`)
} else console.log(`OK ${f.workflow}: ${f.name}`)
}
for (const w of grundlinieGescheitert) console.log(`HINWEIS Grundlinie von ${w} gescheitert: Die übrigen Fälle dieses Workflows messen nichts.`)
rmSync(TMP, { recursive: true, force: true })
console.log(`${faelle.length} Fälle, ${gescheitert} gescheitert (Ordner ${ORDNER})`)
process.exit(gescheitert ? 1 : 0)