Abgleich mit dem Vault-Stand 22761eb
11 Datei(en) geändert. Erzeugt von .vault-tools/auto-push/werkzeuge_spiegel.py.
This commit is contained in:
1 parent
9e506ff588
commit
ae3ec3ca89
11 files changed
+1520
-65
No files matched your search
@@ -18,6 +18,7 @@ Du denkst, du tippst nicht. Dein Ergebnis ist ein Plan, den ein Executer ohne R
|
||||
- **Jede Taskdatei trägt im Frontmatter `modell:` und `modell_begruendung:`.** `title`, `summary` und `modell_begruendung` stehen in Anführungszeichen — ein `: ` im Text macht den Block sonst zu ungültigem YAML (Blöcke und Regel: `AI/Regelwerk/frontmatter.md`). Das Kriterium ist eine einzige Frage: Braucht der Executer für diesen Task Ermessen, oder steht alles wörtlich im Plan? Steht der Code wörtlich da, sind die Zieldateien namentlich genannt und die Verbote ausdrücklich, dann ist die Rolle Abschrift und `haiku` genügt, auch wenn ein Codeblock typografische Zeichen wie „ “ – — … enthält (Haiku 5.5, Pascals Entscheidung vom 2026-10-09; die Ausnahme für Haiku 4.5 steht in `AI/Regelwerk/codeworkflow.md`, Schritt 3). Bleibt Ermessen offen, ist es Fehlersuche, oder muss Fließtext formuliert statt abgeschrieben werden, dann `sonnet`.
|
||||
- **Erst lesen, dann planen.** Bestehende Struktur, Namensgebung, Testkonventionen und Abhängigkeiten des Zielprojekts lesen. Der Plan passt sich der vorhandenen Umgebung an, nicht umgekehrt. Nennt der Auftrag eine Leseliste mit Dateien und Zeilenbereichen, liest du sie zuerst. Sie ist nie abschließend: Jede Datei, deren Pfad, Bezeichner oder Inhalt du in den Plan schreibst, hast du selbst gelesen. Grund: Planer ohne eigenes Lesen schrieben Pfade aus dem Gedächtnis (Pascal, 2026-10-30).
|
||||
- **Erwartete pytest-Fehlerzeilen nur mit breiter Ausgabe.** Ohne Terminal kürzt pytest (gemessen an 9.1.1) jede `FAILED`-Zeile der Kurzzusammenfassung auf 80 Zeichen, die Fehlerart ist dann abgeschnitten („Failed: D..."). Nennt der Plan eine erwartete Fehlerzeile wörtlich, steht im Befehl `COLUMNS=300` davor (Git-Bash) bzw. `$env:COLUMNS=300;` (PowerShell).
|
||||
- **Ein Sammelfehler hat in der Kurzzusammenfassung keine Ursache.** Scheitert der Import einer Testdatei, schreibt pytest 9.1.1 dort nur `ERROR tests/test_x.py`, ohne ` - ModuleNotFoundError: …`; die Ursache steht nur im Block `ERRORS` als `E ModuleNotFoundError: No module named '…'`. Ein Rot-Nachweis über ein fehlendes Modul nennt deshalb beide Zeilen getrennt. Zweimal falsch vorhergesagt (2026-10-20 BookStack Kundendoku, 2026-11-02 Testfeld Proxmox), beide Male hätte ein wörtlich arbeitender Executer abgebrochen.
|
||||
- **Rot vor der Umsetzung bei neuen Modulen** (Pascal, 2026-10-26). Testet ein Task ein Modul, eine Datei oder ein Skript, das es vor dem Task nicht gibt, darf der Test vor der Umsetzung am Import (`ModuleNotFoundError`), an der fehlenden Datei oder an einem Gerüst-Stub (`NichtBereit`) scheitern. Die erwartete Fehlerzeile nennt genau diese erste Zeile. Dass die Zusicherungen greifen, belegen dann die Mutanten: Jeder neue Verhaltenszweig, auch jeder Fehler- und Sperrzweig, bekommt einen Mutanten mit `rot_bei`. Bei einem bestehenden Modul bleibt Rot an einer Zusicherung Pflicht.
|
||||
- **Doku-Tasks verankern nie auf Plandaten** (Pascal, 2026-10-26). `Aus:`-Blöcke in `CHANGELOG.md`, `README.md`, Anleitung und Codedoku verankern an einem Titel oder einer stabilen Abschnittsüberschrift (`# Änderungen`, `## Stand`), nie an einem Versionskopf, einer `updated:`-Zeile oder Text eines anderen Pakets. Ein neuer Versionskopf trägt „Datum bei Freigabe“. Kein Planblock ändert `updated:`; das zieht der Koordinator am Umsetzungstag nach. Beleg: UI-876 und KN-883 brachen in der Nacht zum 2026-10-26 dreimal, sobald ein Vorgängerpaket korrigiert wurde.
|
||||
- **Mutanten stehen in einer Datei, nicht als Prosa.** Verlangt der Auftrag Mutanten, schreibst du sie als `mutanten.toml` in den Planordner, Format 1 nach `D:\Claude\.vault-tools\plan-kopie\mutanten_lauf_ENTWURF.md`, Abschnitt 2; lies den Abschnitt, bevor du die Datei schreibst. Die Plankritik fährt die Datei mit `mutanten_lauf.py` auf ihrer Kopie. In „Fertig heißt“ steht danach nur der Aufruf mit der Erwartung „Exit 0, jeder Mutant `OK`“, keine Zählung. Drei Fallen, alle belegt:
|
||||
|
||||
@@ -26,6 +26,9 @@ export const meta = {
|
||||
// und der Woche.
|
||||
// - Rückgabe mit status fertig oder gebremst; ein gebremster Lauf wird neu gestartet, nicht
|
||||
// fortgesetzt.
|
||||
// - werkzeug_befunde (seit 2026-11-02) listet die Befundzeilen der Planwerkzeuge und die von
|
||||
// plan_materialisiere.py aus materialisieren, je mit dem Werkzeug. Die Plankritik hält dabei
|
||||
// nicht an; die Linsen laufen wie sonst.
|
||||
//
|
||||
// Rollenparameter: model wird immer gesetzt (Stamm). Der Effort der pruefer kommt aus dem
|
||||
// Frontmatter von .claude/agents/pruefer.md; args.effort überschreibt ihn nur bewusst.
|
||||
@@ -45,6 +48,36 @@ const VERBRAUCH = 'D:/Claude/.claude/workflows/verbrauch.js'
|
||||
const RANG = { blockierend: 0, wichtig: 1, klein: 2 }
|
||||
// Einfache Anführungszeichen: In Bash bleiben darin $, Backtick und Backslash wörtlich.
|
||||
const q = s => "'" + String(s).replace(/'/g, "'\\''") + "'"
|
||||
// Befundzeile der Planwerkzeuge: "BEFUND <datei>:<zeile>:", bei plan_zeichen "<datei>:<zeile>:<spalte>:".
|
||||
// kopie_bauen.py schreibt "BEFUND Projekt ..." ohne Zeilennummer; die Form trennt beide.
|
||||
const BEFUNDZEILE = /^BEFUND [^\s:]+:\d+:/
|
||||
const befundZeilen = text => String(text || '').split('\n').map(z => z.replace(/\r$/, '')).filter(z => BEFUNDZEILE.test(z))
|
||||
// Ein Hilfsagent darf seine Ausgabe auf mehrere laeufe verteilen: Im Echtlauf wf_9ab7fd8e-ad1
|
||||
// (2026-11-02) gab werkzeug:planwerkzeuge je Werkzeug einen Lauf zurück, und laeufe[0] allein
|
||||
// enthielt plan_bloecke nicht. Jede Ausgabe eines Hilfsagenten wird deshalb über alle laeufe gelesen,
|
||||
// in ihrer Reihenfolge und mit Zeilenumbruch verbunden: Befund- und EXIT-Zeilen, KOPIE-Zeile und
|
||||
// angewendete Blöcke, Mutantenlauf.
|
||||
const ausgabeAller = r => (r && Array.isArray(r.laeufe) ? r.laeufe : []).map(l => String((l && l.ausgabe) || '')).join('\n')
|
||||
// Exit über alle laeufe, für die Kopie aus materialisieren: Ihr Befehl ist eine Kette mit &&, sie
|
||||
// endet nur mit 0, wenn jedes Glied mit 0 endet. Verwendbar ist die Kopie deshalb nur, wenn jeder Lauf
|
||||
// Exit 0 hat; ein Lauf ohne Exit zählt nicht als 0 und steht im Log als undefined. Planwerkzeuge und
|
||||
// Mutantenlauf lesen stattdessen die EXIT-Zeilen, die ihr Befehl selbst schreibt.
|
||||
const exitsAller = r => (r && Array.isArray(r.laeufe) ? r.laeufe : []).map(l => l && l.exit)
|
||||
const alleExitNull = r => exitsAller(r).length > 0 && exitsAller(r).every(x => x === 0)
|
||||
|
||||
// Stand eines Planwerkzeugs aus seiner EXIT-Zeile und dem Teil der Ausgabe, der zu ihm gehört.
|
||||
// Gelaufen ist es mit Exit 0, mit Exit 2 und mindestens einer Befundzeile, plan_bloecke auch mit
|
||||
// Exit 3 und "Nichts geprueft:" (kein Aus-/wird-Block). Exit 2 ohne Befundzeile ist ein
|
||||
// gescheitertes Werkzeug: python endet auch bei "can't open file" mit 2 (2026-09-24 ging so ein
|
||||
// Errno 2 als Rohausgabe an die Linse).
|
||||
function werkzeugStand(aus, name, abschnitt) {
|
||||
const m = new RegExp('EXIT ' + name + ': (\\d+)').exec(aus)
|
||||
const befunde = befundZeilen(abschnitt)
|
||||
if (!m) return { name, gelaufen: false, grund: 'ohne EXIT-Zeile', befunde }
|
||||
const exit = Number(m[1])
|
||||
const gelaufen = exit === 0 || (exit === 2 && befunde.length > 0) || (name === 'plan_bloecke' && exit === 3 && /^Nichts geprueft:/m.test(abschnitt))
|
||||
return { name, gelaufen, grund: 'Exit ' + exit + (exit === 2 && !befunde.length ? ' ohne Befundzeile' : ''), befunde }
|
||||
}
|
||||
|
||||
// Text für die Linse arithmetik, wenn der Mutantenlauf (werkzeug:mutanten) etwas geliefert hat.
|
||||
// Seit der Nachbesserung 2026-09-28 (N5, N7): modus: muster in der Rohausgabe heißt, dass kein
|
||||
@@ -231,19 +264,29 @@ Gilt der Befund nur unter einer Bedingung, nenn sie. Gib deine eigene Einschätz
|
||||
pruefeArgs(args)
|
||||
const W = args.werkzeuge || 'D:/Claude/.vault-tools/plan-kopie'
|
||||
const PY = 'PYTHONIOENCODING=utf-8 python -X utf8'
|
||||
const ctx = { kopie: args.kopie || null, werkzeuge: null, temp: null, mutantenLauf: null, mutantenStatus: null }
|
||||
const ctx = { kopie: args.kopie || null, werkzeuge: null, temp: null, mutantenLauf: null, mutantenStatus: null, befunde: [] }
|
||||
const MUTANTEN_MODUS = args.mutanten || 'voll'
|
||||
const vorbereitung = []
|
||||
|
||||
// Eine Kopie gilt nur als verwendbar, wenn der Lauf sauber endet, den Pfad bis zum
|
||||
// Zeilenende nennt (Pfade mit Leerzeichen) und das Planwerkzeug mindestens einen Block
|
||||
// angewendet hat. Sonst rechnet Linse 1 am Text, und das steht im Log.
|
||||
// Eine Kopie gilt nur als verwendbar, wenn jeder Lauf sauber endet (alleExitNull), die Ausgabe
|
||||
// den Pfad bis zum Zeilenende nennt (Pfade mit Leerzeichen) und das Planwerkzeug mindestens einen
|
||||
// Block angewendet hat. Sonst rechnet Linse 1 am Text, und das steht im Log. Meldet
|
||||
// plan_materialisiere.py einen Befund, ist das kein gescheitertes Werkzeug: Die Zeilen gehen
|
||||
// nach werkzeug_befunde, und der Temp-Ordner wird genannt.
|
||||
function kopieAusLauf(r, wozu) {
|
||||
const lauf = r && r.laeufe && r.laeufe[0]
|
||||
if (!lauf) { log(`${wozu}: Hilfsagent ohne Ergebnis, keine Kopie`); return null }
|
||||
const m = /^KOPIE=(.+?)\s*$/m.exec(lauf.ausgabe)
|
||||
const angewendet = /'(neu|ersetzt|angehaengt)': [1-9]/.test(lauf.ausgabe)
|
||||
if (lauf.exit !== 0 || !m) { log(`${wozu}: Kopie nicht verwendbar (Exit ${lauf.exit}${m ? '' : ', kein KOPIE-Pfad'})`); return null }
|
||||
const exits = exitsAller(r)
|
||||
if (!exits.length) { log(`${wozu}: Hilfsagent ohne Ergebnis, keine Kopie`); return null }
|
||||
const aus = ausgabeAller(r)
|
||||
const m = /^KOPIE=(.+?)\s*$/m.exec(aus)
|
||||
const angewendet = /'(neu|ersetzt|angehaengt)': [1-9]/.test(aus)
|
||||
const befunde = befundZeilen(aus)
|
||||
if (befunde.length) {
|
||||
befunde.forEach(befund => ctx.befunde.push({ werkzeug: 'plan_materialisiere', befund }))
|
||||
if (m) ctx.temp = m[1].replace(/\/mat$/, '')
|
||||
log(`${wozu}: plan_materialisiere.py meldet ${befunde.length} Befund(e) (Exit ${exits.map(String).join('/')}), keine Kopie; die Zeilen stehen unter werkzeug_befunde`)
|
||||
return null
|
||||
}
|
||||
if (!alleExitNull(r) || !m) { log(`${wozu}: Kopie nicht verwendbar (Exit ${exits.map(String).join('/')}${m ? '' : ', kein KOPIE-Pfad'})`); return null }
|
||||
ctx.temp = m[1].replace(/\/mat$/, '')
|
||||
if (!angewendet) { log(`${wozu}: das Planwerkzeug hat keinen Block angewendet; die Kopie ist der unveränderte Stand und wird nicht verwendet`); return null }
|
||||
return m[1]
|
||||
@@ -268,13 +311,22 @@ if (!ctx.kopie && args.materialisieren) {
|
||||
{
|
||||
const befehl = `${PY} ${q(W + '/plan_zeichen.py')} ${q(args.planordner)}; echo "EXIT plan_zeichen: $?"; ls ${q(args.planordner)}/task-*.md >/dev/null 2>&1 && { ${PY} ${q(W + '/plan_bloecke.py')} vor ${q(args.planordner)}/task-*.md --projekt ${q(args.projekt)}; echo "EXIT plan_bloecke: $?"; } || echo "plan_bloecke: keine task-*.md im Planordner, nicht gefahren"`
|
||||
const r = await werkzeugAgent('werkzeug:planwerkzeuge', 'Vorbereitung', befehl, 'Codeblöcke auf unsichtbare Zeichen und Aus-Blöcke gegen den Baum prüfen')
|
||||
// Nur wenn beide Werkzeuge mit 0 endeten, gilt die Ausgabe als gelaufen (2026-09-24: ein Protokollsatz statt
|
||||
// Pfad in args.werkzeuge endete mit Errno 2 und ging trotzdem als Rohausgabe an die Linse). Der Exit des
|
||||
// ganzen Befehls hilft dabei nicht, er ist der des letzten echo; gelesen werden die EXIT-Zeilen.
|
||||
const aus = r && r.laeufe && r.laeufe[0] ? String(r.laeufe[0].ausgabe || '') : ''
|
||||
const gelaufen = /EXIT plan_zeichen: 0(?!\d)/.test(aus) && (/EXIT plan_bloecke: 0(?!\d)/.test(aus) || /plan_bloecke: keine task-/.test(aus))
|
||||
if (gelaufen) ctx.werkzeuge = aus
|
||||
else log('Planwerkzeuge: Hilfsagent ohne Ergebnis; Linse bezeichner fährt sie selbst')
|
||||
// Die Ausgabe geht an die Linse, wenn beide Werkzeuge gelaufen sind, auch mit Befund (werkzeugStand oben).
|
||||
// Der Exit des ganzen Befehls hilft dabei nicht, er ist der des letzten echo; gelesen werden die
|
||||
// EXIT-Zeilen. Was vor "EXIT plan_zeichen:" steht, gehört zu plan_zeichen, der Rest zu plan_bloecke.
|
||||
const lauf = r && r.laeufe && r.laeufe[0]
|
||||
if (!lauf) log('Planwerkzeuge: Hilfsagent ohne Ergebnis; Linse bezeichner fährt sie selbst')
|
||||
else {
|
||||
const aus = ausgabeAller(r)
|
||||
const grenze = aus.indexOf('EXIT plan_zeichen:')
|
||||
const zeichen = werkzeugStand(aus, 'plan_zeichen', grenze >= 0 ? aus.slice(0, grenze) : aus)
|
||||
const bloecke = /plan_bloecke: keine task-/.test(aus)
|
||||
? { name: 'plan_bloecke', gelaufen: true, befunde: [] }
|
||||
: werkzeugStand(aus, 'plan_bloecke', grenze >= 0 ? aus.slice(grenze) : '')
|
||||
for (const w of [zeichen, bloecke]) w.befunde.forEach(befund => ctx.befunde.push({ werkzeug: w.name, befund }))
|
||||
if (zeichen.gelaufen && bloecke.gelaufen) ctx.werkzeuge = aus
|
||||
else log('Planwerkzeuge: ' + [zeichen, bloecke].filter(w => !w.gelaufen).map(w => w.name + ' ' + w.grund).join(', ') + '; Linse bezeichner fährt sie selbst')
|
||||
}
|
||||
vorbereitung.push(r)
|
||||
}
|
||||
// Mutantenlauf (seit 2026-09-27). mutanten_lauf.py ändert Dateien in der Wurzel, die es bekommt,
|
||||
@@ -298,9 +350,13 @@ else if (!ctx.kopie) ctx.mutantenStatus = 'ohne_kopie'
|
||||
if (ctx.kopie && MUTANTEN_MODUS !== 'aus') {
|
||||
const befehl = `MSYS_NO_PATHCONV=1 wsl -d Debian -- bash /mnt/d/Claude/.vault-tools/plan-kopie/mutanten_lauf_kopie.sh "$(cygpath -m -l ${q(ctx.kopie)})" "$(cygpath -m -l ${q(args.planordner)})" ${MUTANTEN_MODUS === 'muster' ? 'muster' : 'voll'}`
|
||||
const r = await werkzeugAgent('werkzeug:mutanten', 'Vorbereitung', befehl, 'Mutanten aus mutanten.toml auf einer Kopie der Kopie fahren', 'Setze im Bash-Werkzeug den Parameter timeout auf 600000; der Befehl darf bis zu zehn Minuten laufen.')
|
||||
const aus = r && r.laeufe && r.laeufe[0] ? String(r.laeufe[0].ausgabe || '') : ''
|
||||
// Alle laeufe, etwa Anfang und Ende einer gekürzten Ausgabe: Die EXIT-Zeile steht am Schluss
|
||||
// (mutanten_lauf_kopie.sh schreibt höchstens eine); bei mehreren gilt wie bisher die erste.
|
||||
// KEINE_MUTANTEN_TOML zählt nur als ganze Zeile, wie das Skript sie schreibt: Ein zusätzlicher Lauf,
|
||||
// der etwa den Skripttext zeigt, machte sonst aus einem gefahrenen Lauf keine_toml.
|
||||
const aus = ausgabeAller(r)
|
||||
const exitMatch = /EXIT mutanten_lauf: (\d+)/.exec(aus)
|
||||
if (/KEINE_MUTANTEN_TOML/.test(aus)) { ctx.mutantenLauf = { keineToml: true, rohausgabe: aus }; ctx.mutantenStatus = 'keine_toml' }
|
||||
if (/^KEINE_MUTANTEN_TOML\s*$/m.test(aus)) { ctx.mutantenLauf = { keineToml: true, rohausgabe: aus }; ctx.mutantenStatus = 'keine_toml' }
|
||||
else if (exitMatch) { ctx.mutantenLauf = { keineToml: false, rohausgabe: aus }; ctx.mutantenStatus = 'exit_' + exitMatch[1] }
|
||||
else { log('Mutantenlauf: Hilfsagent ohne verwertbares Ergebnis (kein KEINE_MUTANTEN_TOML, kein EXIT mutanten_lauf); Linsen laufen ohne Mutantenlauf weiter'); ctx.mutantenStatus = 'ohne_ergebnis' }
|
||||
vorbereitung.push(r)
|
||||
@@ -324,6 +380,7 @@ if (vorLinsen.gebremst) {
|
||||
selbst_ausgefuehrt: [],
|
||||
nicht_pruefbar: [],
|
||||
vorbereitung,
|
||||
werkzeug_befunde: ctx.befunde,
|
||||
ohne_ergebnis: gewaehlt,
|
||||
verbrauch: ablesungen,
|
||||
plaetze: { linsen: 0, widerlegung: 0 },
|
||||
@@ -388,6 +445,7 @@ return {
|
||||
selbst_ausgefuehrt: ergebnisse.map(e => ({ linse: e.linse, befehle: e.ergebnis.selbst_ausgefuehrt })),
|
||||
nicht_pruefbar: ergebnisse.flatMap(e => e.ergebnis.nicht_pruefbar.map(x => e.linse + ': ' + x)),
|
||||
vorbereitung,
|
||||
werkzeug_befunde: ctx.befunde,
|
||||
ohne_ergebnis: ohneErgebnis,
|
||||
verbrauch: ablesungen,
|
||||
plaetze: { linsen: vorLinsen.plaetze, widerlegung: plaetzeWiderlegung },
|
||||
|
||||
@@ -165,8 +165,16 @@ ${zusatz ? '\n' + zusatz + '\n' : ''}
|
||||
{ label, phase, schema: LAUF, model: 'haiku', effort: 'low' })
|
||||
}
|
||||
|
||||
// Ausgabe aller laeufe in ihrer Reihenfolge, mit Zeilenumbruch verbunden (seit 2026-11-02, wie
|
||||
// ausgabeAller in plankritik.js): Ein Hilfsagent darf die Ausgabe eines Befehls auf mehrere laeufe
|
||||
// verteilen (Echtlauf wf_9ab7fd8e-ad1). Mit laeufe[0] allein fehlte dann etwa "EXIT paket: 0" oder
|
||||
// "EXIT task: 0", und ein gestarteter Codex-Auftrag lief ungesehen weiter. Bewertet werden nur die
|
||||
// EXIT-Zeilen, die jeder Befehl selbst schreibt, nie das Feld exit.
|
||||
function ausgabe(r) {
|
||||
return r && r.laeufe && r.laeufe[0] ? String(r.laeufe[0].ausgabe || '') : ''
|
||||
return laufTexte(r).join('\n')
|
||||
}
|
||||
function laufTexte(r) {
|
||||
return (r && Array.isArray(r.laeufe) ? r.laeufe : []).map(l => String((l && l.ausgabe) || ''))
|
||||
}
|
||||
|
||||
function wurzel(a) {
|
||||
@@ -291,20 +299,45 @@ async function codexStimme(a, p) {
|
||||
return z
|
||||
}
|
||||
const P = q(wurzel(a))
|
||||
const start = ausgabe(await werkzeugAgent('codex:start', 'Stimmen',
|
||||
const startAntwort = await werkzeugAgent('codex:start', 'Stimmen',
|
||||
`node ${COMPANION} status --json --cwd ${P}; S=$?; echo "EXIT status: $S"; if [ "$S" -eq 0 ]; then node ${COMPANION} task --background --json --cwd ${P} --prompt-file ${q(p.ordner + '/codex-auftrag.md')}; echo "EXIT task: $?"; fi`,
|
||||
'den Codex-Status vor dem Lauf lesen und die Prüfung nur lesend im Hintergrund starten'))
|
||||
'den Codex-Status vor dem Lauf lesen und die Prüfung nur lesend im Hintergrund starten')
|
||||
const start = ausgabe(startAntwort)
|
||||
// Task beginnt mit jobId, Komma, status: queued (codex-companion.mjs 701-706; JSON: 91-100).
|
||||
// Nur innerhalb eines Laufs: Im Status folgt auf { nie jobId (job-control.mjs 161-181, 213-239;
|
||||
// lib/state.mjs 129-147; codex-companion.mjs 567-578); request hat kein status (604-613).
|
||||
// Einrückung allein belegt keine Herkunft; gleiche ID zählt einmal, die erste Fundstelle gilt.
|
||||
// echo setzt EXIT an den Zeilenanfang; Vorsätze zählen nicht für die Reihenfolgeprüfung.
|
||||
// Freie Umstellung innerhalb eines Laufs oder erfundene/geänderte Inhalte sind nicht abgesichert;
|
||||
// codexUngueltig() weist dann eine fremde Stimme über den Lesebeleg zurück.
|
||||
const taskOK = /EXIT task: 0(?!\d)/.test(start)
|
||||
if (!/EXIT status: 0(?!\d)/.test(start)) {
|
||||
z.grund = 'Status vor dem Lauf nicht gelesen'
|
||||
z.grund = 'Status vor dem Lauf nicht gelesen' + (taskOK ? '; Codex-Auftrag läuft womöglich ungesehen weiter' : '')
|
||||
return z
|
||||
}
|
||||
// Nur hinter "EXIT status:" suchen: status --json listet frühere Jobs derselben Sitzung samt
|
||||
// request.jobId (job-control.mjs 161-181); die erste Fundstelle wäre dann ein alter Job.
|
||||
const job = /"jobId": "([^"]+)"/.exec(start.slice(start.lastIndexOf('EXIT status:')))
|
||||
if (!/EXIT task: 0(?!\d)/.test(start) || !job) {
|
||||
z.grund = 'Codex-Auftrag nicht gestartet'
|
||||
const taskJobs = []
|
||||
let startIndex = 0
|
||||
for (const laufText of laufTexte(startAntwort)) {
|
||||
const laufJobs = [...laufText.matchAll(/\{\s*"jobId"\s*:\s*"([^"\\\r\n]+)"\s*,\s*"status"\s*:\s*"queued"/g)]
|
||||
for (const treffer of laufJobs) {
|
||||
treffer.index += startIndex
|
||||
taskJobs.push(treffer)
|
||||
}
|
||||
startIndex += laufText.length + 1
|
||||
}
|
||||
const taskIds = new Set(taskJobs.map(treffer => treffer[1]))
|
||||
const job = taskIds.size === 1 ? taskJobs[0] : null
|
||||
if (!taskOK || !job) {
|
||||
z.grund = taskOK
|
||||
? 'jobId nicht sicher bestimmbar; Codex-Auftrag läuft womöglich ungesehen weiter'
|
||||
: 'Codex-Auftrag nicht gestartet'
|
||||
return z
|
||||
}
|
||||
const statusEnde = start.search(/^EXIT status: 0(?!\d)/m)
|
||||
const taskEnde = start.search(/^EXIT task: 0(?!\d)/m)
|
||||
if (!(statusEnde < job.index && job.index < taskEnde)) {
|
||||
hinweis('Codex-Start: Ausgabe nicht in Befehlsreihenfolge; jobId aus der task-Ausgabe (' + job[1] + ')')
|
||||
}
|
||||
z.job = job[1]
|
||||
const runden = a.codex_runden || CODEX_RUNDEN
|
||||
let fertig = false
|
||||
|
||||
@@ -19,12 +19,35 @@ export const meta = {
|
||||
// Verbrauch: Vor und nach der Prüfung liest dieser Workflow den Planverbrauch über verbrauch.js
|
||||
// ab (nur messen); das Kind pruefung bekommt verbrauch: false, weil ein Kind keinen weiteren
|
||||
// Workflow aufrufen kann.
|
||||
// Planbefund (seit 2026-11-02): Meldet plan_materialisiere.py in der Phase Kopie einen Befund, also
|
||||
// eine Zeile "BEFUND <taskdatei>:<zeile>:" (Exit 2, nach einem Absturz auch 1), startet keine Welle.
|
||||
// Der Lauf endet vor executer-welle mit status plan_befund und den Zeilen in plan_befunde, ohne
|
||||
// Vergleich, Prüfung und Ablesung. Vorher lief die Welle trotzdem, nur ohne Byte-Vergleich. Exit 2
|
||||
// ohne eine solche Zeile ist ein gescheitertes Werkzeug (python endet auch bei "can't open file"
|
||||
// mit 2) und nimmt den alten Weg, ebenso die BEFUND-Zeilen von kopie_bauen.py: Sie tragen keine
|
||||
// Zeilennummer.
|
||||
const MODELLE = ['haiku', 'sonnet', 'opus']
|
||||
const EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max']
|
||||
const VERBRAUCH = 'D:/Claude/.claude/workflows/verbrauch.js'
|
||||
// Einfache Anführungszeichen: In Bash bleiben darin $, Backtick und Backslash wörtlich.
|
||||
const q = s => "'" + String(s).replace(/'/g, "'\\''") + "'"
|
||||
const PY = 'PYTHONIOENCODING=utf-8 python -X utf8'
|
||||
// Befundzeile der Planwerkzeuge: "BEFUND <datei>:<zeile>:", bei plan_zeichen "<datei>:<zeile>:<spalte>:".
|
||||
const BEFUNDZEILE = /^BEFUND [^\s:]+:\d+:/
|
||||
const befundZeilen = text => String(text || '').split('\n').map(z => z.replace(/\r$/, '')).filter(z => BEFUNDZEILE.test(z))
|
||||
// Ein Hilfsagent darf seine Ausgabe auf mehrere laeufe verteilen (Echtlauf wf_9ab7fd8e-ad1 am
|
||||
// 2026-11-02: je Werkzeug ein Lauf). Jede Ausgabe eines Hilfsagenten wird deshalb über alle laeufe
|
||||
// gelesen, in ihrer Reihenfolge und mit Zeilenumbruch verbunden: Befundzeilen, KOPIE-Zeile und
|
||||
// angewendete Blöcke der Kopie, die Rohausgabe des Vergleichs. Sonst stünde ein Befund in laeufe[1],
|
||||
// und die Welle liefe trotzdem.
|
||||
const ausgabeAller = r => (r && Array.isArray(r.laeufe) ? r.laeufe : []).map(l => String((l && l.ausgabe) || '')).join('\n')
|
||||
// Exit über alle laeufe: Der Befehl der Kopie ist eine Kette mit &&, sie endet nur mit 0, wenn jedes
|
||||
// Glied mit 0 endet. Verwendbar ist die Kopie deshalb nur, wenn jeder Lauf Exit 0 hat; ein Lauf ohne
|
||||
// Exit zählt nicht als 0 und steht im Log als undefined. Mit laeufe[0] allein galt ein erster Lauf
|
||||
// mit 0 vor einem Absturz im zweiten als fertige Kopie.
|
||||
const exitsAller = r => (r && Array.isArray(r.laeufe) ? r.laeufe : []).map(l => l && l.exit)
|
||||
const alleExitNull = r => exitsAller(r).length > 0 && exitsAller(r).every(x => x === 0)
|
||||
const tempHinweis = t => t ? `${t} bleibt liegen (Projektkopie ohne die Ausschlüsse); löschen, wenn der Diff nicht mehr gebraucht wird` : null
|
||||
|
||||
const LAUF = {
|
||||
type: 'object',
|
||||
@@ -98,12 +121,32 @@ if (!kopie) {
|
||||
const befehl = `T=$(mktemp -d) && echo "KOPIE=$(cygpath -m "$T/mat")" && ${PY} ${q(W + '/kopie_bauen.py')} ${q(args.projekt)} "$T/mat" --ohne ${args.ohne.map(q).join(' ')} && ${PY} ${q(W + '/plan_materialisiere.py')} ${q(args.planordner)} "$T/mat" ${taskdateien.map(q).join(' ')}`
|
||||
const r = await werkzeugAgent('werkzeug:kopie', 'Kopie', befehl, 'Stand vor der Welle kopieren und die Taskdateien der Welle darauf anwenden', 'haiku')
|
||||
belege.kopie = r
|
||||
const lauf = r && r.laeufe && r.laeufe[0]
|
||||
const m = lauf && /^KOPIE=(.+?)\s*$/m.exec(lauf.ausgabe)
|
||||
const aus = ausgabeAller(r)
|
||||
const m = /^KOPIE=(.+?)\s*$/m.exec(aus)
|
||||
if (m) temp = m[1].replace(/\/mat$/, '')
|
||||
const angewendet = lauf && /'(neu|ersetzt|angehaengt)': [1-9]/.test(lauf.ausgabe)
|
||||
if (lauf && lauf.exit === 0 && m && angewendet) kopie = m[1]
|
||||
else log('Kopie nicht verwendbar (' + (!lauf ? 'Hilfsagent ohne Ergebnis' : lauf.exit !== 0 ? 'Exit ' + lauf.exit : !m ? 'kein KOPIE-Pfad' : 'kein Block angewendet') + '); der Byte-Vergleich entfällt, die Welle läuft trotzdem')
|
||||
const planBefunde = befundZeilen(aus)
|
||||
if (planBefunde.length) {
|
||||
const grund = `plan_materialisiere.py meldet ${planBefunde.length} Befund(e) (Exit ${exitsAller(r).map(String).join('/')}); die Welle startet nicht`
|
||||
log('Kopie: ' + grund)
|
||||
return {
|
||||
status: 'plan_befund',
|
||||
plan_befunde: planBefunde,
|
||||
kopie: null,
|
||||
temp_ordner: tempHinweis(temp),
|
||||
welle: null,
|
||||
vergleich: null,
|
||||
kopie_lauf: r,
|
||||
pruefung: { uebersprungen: grund },
|
||||
pruefung_args: null,
|
||||
pruefung_grund: grund,
|
||||
fehler,
|
||||
verbrauch: ablesungen,
|
||||
naechster_schritt: 'Den Plan an den Zeilen in plan_befunde korrigieren und welle-mit-pruefung neu starten. Kein Executer ist gelaufen.' + (temp ? ' Die Kopie unter temp_ordner löschen.' : ''),
|
||||
}
|
||||
}
|
||||
const angewendet = /'(neu|ersetzt|angehaengt)': [1-9]/.test(aus)
|
||||
if (alleExitNull(r) && m && angewendet) kopie = m[1]
|
||||
else log('Kopie nicht verwendbar (' + (!exitsAller(r).length ? 'Hilfsagent ohne Ergebnis' : !alleExitNull(r) ? 'Exit ' + exitsAller(r).map(String).join('/') :!m ? 'kein KOPIE-Pfad' : 'kein Block angewendet') + '); der Byte-Vergleich entfällt, die Welle läuft trotzdem')
|
||||
}
|
||||
|
||||
phase('Welle')
|
||||
@@ -136,8 +179,9 @@ if (!welle) pruefungGrund = 'keine Welle'
|
||||
else if (welle.ohne_bericht && welle.ohne_bericht.length) pruefungGrund = 'Welle unvollständig, ohne Bericht: ' + welle.ohne_bericht.join(', ')
|
||||
let pruefungArgs = null
|
||||
if (!pruefungGrund) {
|
||||
const vl = belege.vergleich && belege.vergleich.laeufe && belege.vergleich.laeufe[0]
|
||||
const vergleichstext = vl ? kopfUndEnde(vl.ausgabe, 1500, 2500) : ''
|
||||
// Anfang und Ende des verbundenen Texts wie bei einem Lauf. Liefert der Hilfsagent die Suite vor
|
||||
// einem langen Vergleich, fällt ihr Ergebnis heraus (benannte Lücke; der pruefer fährt die Suite selbst).
|
||||
const vergleichstext = kopfUndEnde(ausgabeAller(belege.vergleich), 1500, 2500)
|
||||
const schwerpunkt = [
|
||||
args.schwerpunkt || '',
|
||||
welle.mit_abweichung.length ? `Diese Tasks melden Abweichungen vom Plan: ${welle.mit_abweichung.join(', ')}.` : '',
|
||||
@@ -149,7 +193,7 @@ if (!pruefungGrund) {
|
||||
}
|
||||
}
|
||||
|
||||
const tempText = temp ? `${temp} bleibt liegen (Projektkopie ohne die Ausschlüsse); löschen, wenn der Diff nicht mehr gebraucht wird` : null
|
||||
const tempText = tempHinweis(temp)
|
||||
|
||||
if (args.bis === 'vergleich') {
|
||||
if (pruefungGrund) log('Prüfung wäre übersprungen worden: ' + pruefungGrund)
|
||||
|
||||
@@ -5,7 +5,7 @@ title: "Planwerkzeuge: Plan auf eine Kopie anwenden und jede Welle dagegen vergl
|
||||
summary: "Skripte für die Standardschleife bei Plänen mit ausgeschriebenem Code — schlanke Kopie bauen, Plan auf die Kopie anwenden, je Welle Menge und Bytes vergleichen, Aus-wird-Blöcke prüfen, Codeblöcke auf unsichtbare Zeichen absuchen, Escape-Folgen per Codepunkt setzen. Entstanden 2026-09-17 in der Trading Plattform (Teil 3b)."
|
||||
tags: [vault-tools, codeworkflow]
|
||||
created: 2026-09-17
|
||||
updated: 2026-10-17
|
||||
updated: 2026-11-02
|
||||
---
|
||||
|
||||
# Planwerkzeuge für die Kopie mit angewendetem Plan
|
||||
@@ -14,8 +14,8 @@ Die Regel dazu steht in [[AI/Regelwerk/codeworkflow|codeworkflow.md]] („Die Ko
|
||||
|
||||
## Ablauf
|
||||
|
||||
1. **Kopie bauen — erst, wenn der Planer fertig gemeldet hat.** Den Projektbaum ohne `.git`, ohne Geheimnisdateien und ohne `node_modules` in einen Wegwerfordner kopieren. **Ausschlüsse als Pfade vom Wurzelordner angeben, nicht als Ordnernamen:** Ein Ausschluss von „jedem Ordner namens `data`" hat am 2026-09-17 auch das Paket `trading/data` getroffen, und die Suite der Kopie brach mit Sammelfehlern ab. Nach dem Kopieren die Paketliste gegen das Original halten. **Beides erledigt `kopie_bauen.py <projekt> <ziel> --ohne <relpfad> …`:** Es nimmt Ausschlüsse nur als Pfade vom Wurzelordner, meldet einen Ausschluss ohne Gegenstück („ein Name statt eines Pfads?"), überspringt Verknüpfungen und Junctions, lässt `__pycache__` weg, weigert sich bei einem vorhandenen Ziel und vergleicht am Ende die Python-Pakete beider Bäume. Für die Trading Plattform: `--ohne .git dashboard/node_modules dashboard/dist data artifacts config/secrets.toml docs/plans`. Braucht die Kopie `node_modules`, eine Junction setzen (`mklink /J`) — **und sie vor dem Löschen der Kopie mit `cmd //c rmdir <pfad>` wieder lösen**, sonst trifft `rm -rf` das echte `node_modules`. Nie `tsc -b` durch die Junction fahren (schreibt nach `node_modules/.tmp`); `npx tsc -p tsconfig.app.json --noEmit` nehmen.
|
||||
2. **Plan anwenden:** `plan_materialisiere.py <planordner> <kopie> task-01.md task-02.md …` in Wellenreihenfolge. Erkennt die Marken `Aus:` / `wird:`, „Die ganze Datei ist neu", „vollständig ersetzt" und „Häng diesen Block ans Ende"; Dateien, die `## Zieldateien` als „(gelöscht)" führt, löscht es in der Kopie. Prosa-Tasks („wird: ein neuer Abschnitt, er muss sagen …") lassen sich nicht anwenden. Eine Datei ganz ohne Endung (`host/logrotate/edge-traefik`, `Dockerfile`, `.gitignore`) ist Ziel, wenn derselbe Pfad unter `## Zieldateien` steht. Fehlt der Eintrag, ist ihr Block ein Befund mit Exit 2, außer bei einer neuen Datei ohne Schrägstrich (unten, „Seit 2026-10-17").
|
||||
1. **Kopie bauen — erst, wenn der Planer fertig gemeldet hat.** Den Projektbaum ohne `.git`, ohne Geheimnisdateien und ohne `node_modules` in einen Wegwerfordner kopieren. **Ausschlüsse als Pfade vom Wurzelordner angeben, nicht als Ordnernamen:** Ein Ausschluss von „jedem Ordner namens `data`" hat am 2026-09-17 auch das Paket `trading/data` getroffen, und die Suite der Kopie brach mit Sammelfehlern ab. Nach dem Kopieren die Paketliste gegen das Original halten. **Beides erledigt `kopie_bauen.py <projekt> <ziel> --ohne <relpfad> …`:** Es nimmt Ausschlüsse nur als Pfade vom Wurzelordner, meldet einen Ausschluss ohne Gegenstück („ein Name statt eines Pfads?"), überspringt Verknüpfungen und Junctions, lässt `__pycache__` weg, weigert sich bei einem vorhandenen Ziel und vergleicht am Ende die Python-Pakete beider Bäume. Für die Trading Plattform: `--ohne .git dashboard/node_modules dashboard/dist data artifacts config/secrets.toml docs/plans`. Braucht die Kopie `node_modules`, eine Junction setzen (`mklink /J`) — **und sie vor dem Löschen der Kopie mit `cmd //c rmdir <pfad>` wieder lösen**, sonst trifft `rm -rf` das echte `node_modules`. Nie `tsc -b` durch die Junction fahren (schreibt nach `node_modules/.tmp`); `npx tsc -p tsconfig.app.json --noEmit` nehmen. Auch Vitest schreibt hinter die Junction: Vite legt beim Laden der Konfiguration `node_modules/.vite-temp` an und löscht die Datei danach, unabhängig von `cacheDir` (gemessen 2026-11-02 im Nexhorizons Hub, Vite 8.3.0); `cacheDir` in einer Wegwerf-Konfiguration hält nur den eigentlichen Cache aus dem fremden `node_modules` heraus.
|
||||
2. **Plan anwenden:** `plan_materialisiere.py <planordner> <kopie> task-01.md task-02.md …` in Wellenreihenfolge. Erkennt die Marken `Aus:` / `wird:`, „Die ganze Datei ist neu", „vollständig ersetzt" und „Häng diesen Block ans Ende"; Dateien, die `## Zieldateien` als „(gelöscht)" führt, löscht es in der Kopie. Prosa-Tasks („wird: ein neuer Abschnitt, er muss sagen …") lassen sich nicht anwenden. Vor dem ersten Schreibvorgang prüft es die Zäune aller übergebenen Taskdateien; ein kaputter Zaun ist ein Befund mit Exit 2, und der Lauf schreibt nichts (unten, „Seit 2026-11-02“). Eine Datei ganz ohne Endung (`host/logrotate/edge-traefik`, `Dockerfile`, `.gitignore`) ist Ziel, wenn derselbe Pfad unter `## Zieldateien` steht. Fehlt der Eintrag, ist ihr Block ein Befund mit Exit 2, außer bei einer neuen Datei ohne Schrägstrich (unten, „Seit 2026-10-17").
|
||||
3. **Auf der Kopie fahren:** Suite, Typprüfung, Mutanten. Ein Mutantenlauf gibt je Mutant die Fehlerzeile aus, nicht nur den Exitcode.
|
||||
4. **Vor jeder Welle und nach jeder Plankorrektur:** `plan_bloecke.py vor <tasks…> --projekt <projekt>` (jeder Aus-Block steht genau einmal im echten Baum) und `plan_zeichen.py <planordner>` (keine Zeilen- und Absatztrenner, Format- und Steuerzeichen in Codeblöcken).
|
||||
5. **Nach jeder Welle:** `welle_vergleich.py --projekt <projekt> --mat <kopie> [--prosa <rel> …] -- <taskdateien der Welle>`. Prüft, ob genau die Zieldateien geändert sind und ob jede byte-gleich mit der Kopie ist. Es zeigt je Datei nur die erste Abweichung — danach `diff` auf die ganze Datei. Eigene Plankorrekturen im Baum meldet es zu Recht als „außerhalb der Zieldateien". Eine Zieldatei, die git ignoriert (etwa Prüfdateien unter `tests/.work/`), vergleicht es seit 2026-10-17 ebenfalls byte-genau mit der Kopie und zählt sie in der Schlusszeile als „von git ignoriert"; vorher meldete es sie als „unveraendert gegenueber HEAD" und verglich sie nicht.
|
||||
@@ -62,6 +62,30 @@ Nachbesserung vom 2026-10-09 (Paket RB3 der Trading Plattform, Plan `docs/plans/
|
||||
|
||||
Seit 2026-10-11 schreibt `plan_materialisiere.py` nur noch innerhalb der Kopie. Vorher schlug ein absoluter Pfad in einer Überschrift unter Windows die Kopie (`kopie / "D:/…"` ist der absolute Pfad): Das Skript ersetzte dann die echte Datei, mit Exit 0 und „0 Befund(e)“. Gemessen an einer Probe-Datei im Scratchpad. Entdeckt beim Planen von Paket E9 der Trading Plattform, dessen Tasks 725 und 726 Dateien unter `D:/Claude/.vault-tools/` ändern. Jetzt ist ein Ziel außerhalb der Kopie (absolut oder mit `..`) ein Befund mit Exit 2, und nichts wird geschrieben. Belegt am selben Tag mit drei Fällen (Aus/wird absolut, ganze Datei absolut, `../`): je Befund, die Dateien draußen unverändert; und mit einer Gegenprobe: Die Tasks 723 und 724 von E9 ergeben in einer frischen Kopie dieselben Zählungen und byte-gleiche Dateien. Pläne für Dateien außerhalb des Projekts nennen sie deshalb relativ zu einem Wurzelordner (etwa `.vault-tools/trading-plattform-pruefung/abendkontrolle.py` gegen eine Kopie, die diesen Ordner enthält).
|
||||
|
||||
Seit 2026-11-02 prüfen `plan_materialisiere.py` und `plan_bloecke.py` die Zäune einer Taskdatei, bevor sie einen Block anfassen. Anlass war Auftrag 13 der Kette 050 (Trading Plattform) am 2026-11-01: In `task-1020` klebte der schließende Zaun zweimal an `])`. Der Block lief bis in den nächsten Task, das erste Aus/wird-Paar dort ging ohne eigene Meldung verloren, und das Werkzeug sagte nur „Aus-Block steht 0-mal“. Das sah nach einem falschen Anker aus. Unter „Die ganze Datei ist neu“ schrieb derselbe Fehler Planprosa in die Zieldatei, mit Exit 0 und „0 Befund(e)“. Die Regel jetzt, `n` ist die Zaunlänge des Blocks:
|
||||
- Ein Block schließt mit mindestens `n` Backticks in einer Zeile ohne weiteren Text, höchstens drei Spalten tiefer als der Öffner (CommonMark 0.31.2). Tiefer ist es Inhalt. Vorher schloss jeder Einzug, auch ein Zaun in einer eingerückten Zeichenkette, und auch zwei Zäune auf einer Zeile.
|
||||
- Befund ist eine Zeile im Block, die auf mindestens `n` Backticks endet und nicht schließt (klebender Zaun, auch ein zu tief eingerückter).
|
||||
- Befund ist eine Zeile im Block, die mit mindestens `n` Backticks beginnt und nicht schließt: Der Schlusszaun fehlt, oder der Außenzaun ist zu kurz.
|
||||
- Befund ist ein Block, der bis zum Dateiende nicht schließt, und eine Taskdatei, die sich nicht lesen lässt.
|
||||
- `plan_materialisiere.py` prüft alle übergebenen Taskdateien vor dem ersten Schreibvorgang. Bei einem Befund schreibt und löscht der ganze Lauf nichts und endet mit Exit 2, auch die heilen Taskdateien davor bleiben ungeschrieben. Die Zählzeile je Task entfällt dann.
|
||||
- `plan_bloecke.py` meldet denselben Befund und prüft die Blöcke dieser Taskdatei nicht, die übrigen Taskdateien prüft es weiter. Ein Zaunbefund endet immer mit Exit 2, auch wenn sonst nichts geprüft wurde. Die Blocksuche ist jetzt dieselbe wie in `plan_materialisiere.py` (`zaun()` und `schliesst()`), vorher sah es nur Zäune in Spalte 0.
|
||||
- Eine Zeile mit weiterem Backtick hinter dem Zaun öffnet nichts, sie ist Inline-Code (CommonMark 0.31.2: „Info strings for backtick code blocks cannot contain backticks“). Vorher schluckte so eine Zeile den nächsten Block.
|
||||
- Bewusst entschieden: Ein Text im Block, etwa eine Zeichenkette in einem Test, der auf `n` Backticks endet oder mit ihnen beginnt, ist derselbe Befund. Von einem klebenden Zaun ist er nicht zu unterscheiden. Abhilfe ist ein längerer Zaun um den Block. Beim Text am Zeilenende steht oft ein Folgebefund weiter hinten, weil der echte Schlusszaun für die Suche einen Block öffnet; der längere Zaun löst beide.
|
||||
- Nicht entscheidbar: Ein nackter Zaun als eigene Zeile im Text, höchstens drei Spalten tiefer als der Öffner, schließt den Block. Geht die Zahl der Zäune auf, wird der Rest Prosa, und es kommt kein Befund. Text mit Zäunen braucht deshalb immer einen längeren Zaun um den Block. Der Treiberfall „Luecke: nackter Zaun als Zeile im Text“ hält das fest.
|
||||
- Kein Befund sind Markdown-Vorlagen in längeren Zäunen mit `##`-Überschriften und kürzeren Zäunen darin, eingerückte Blöcke, Inline-Code und Zaunfolgen mitten in der Zeile oder in Anführungszeichen, Prosa außerhalb von Blöcken.
|
||||
- `welle_vergleich.py` liest keine Blöcke, nur `## Zieldateien`, und bleibt unverändert. `plan_zeichen.py` und `plan-diff/plan_diff.py` haben je eine eigene Blocksuche mit derselben Lücke. Sie schreiben nichts und bleiben unverändert; was ein kaputter Zaun dort bewirkt, ist nicht gefahren, nur am Quelltext gelesen.
|
||||
|
||||
Belegt am 2026-11-02 (Rohdaten und Messskripte: `AI/secondbrain/regelwerk-memory/2026-11-01-zaunpruefung/`):
|
||||
- `plan_zaun_treiber.py [--werkzeuge <ordner>] [--mutanten]` fährt 114 Fälle, die Grundlinie zuerst, und prüft je Fall Exit, jede Befundzeile samt Zeilennummer, die Zahl der Befunde, die Summenzeile und jede Datei der Kopie. Die Angriffe aus `paar()` haben ein geheiltes Gegenstück, das schreiben muss; sonst wäre „nichts geschrieben“ auch grün, wenn die Fixture nichts anwendet. Gefahren unter Windows und in WSL Debian.
|
||||
- Alte Fassung 65 von 114 Fällen rot, neue 0. `plan_ziel_treiber.py` bleibt bei 33 von 33.
|
||||
- `--mutanten` baut 39 Fehler ein, jeder ist an seinem Fall gefangen.
|
||||
- `task-1020` vor der Korrektur: genau zwei Befunde an den Zeilen 1678 und 1729, die Kopie bleibt unverändert. Die alte Fassung meldete dort 80 Folgefehler und schrieb fünf Dateien.
|
||||
- Alle Taskdateien im Vault und in den Jobordnern (2 253 verschiedene, 59 790 Blöcke): `bloecke()` alt und neu liefern dasselbe. Die Zaunprüfung meldet acht Zeilen in drei alten Doku-Tasks (`task-89` und `task-94` der Trading Plattform, `task-08` von After Install). Alle acht sind echt: Der Außenzaun war zu kurz, der Block endete zu früh. `plan_bloecke.py vor` weicht in genau diesen drei von 188 Planordnern ab.
|
||||
- Kette 050: alle 50 Lieferungen, angewendet wie in `kette_050.py` (Basis plus transitive Vorgänger), mit alter und neuer Fassung: gleiche Meldungen, gleiche Bäume. Die eingesetzte Fassung meldet in den 52 Taskdateien der Lieferungen keinen Zaunbefund, `zaun_scan.py` kein `KLEBT` und kein `OFFEN`. Seine `STRUKTUR`-Treffer sind Markdown-Überschriften als Inhalt geschlossener `markdown`- und `text`-Blöcke und bleiben ohne Befund.
|
||||
- Zustandssuche über die echten Funktionen (`messung/zaun_suche.py`, 40 000 zufällige heile Dateien, je ein Zaun beschädigt): keine heile Datei mit Befund, wobei der Generator in Dreierblöcken keine inneren Zäune erzeugt; alle 4 915 klebenden Zäune gefunden.
|
||||
- Ein `pruefer` fand keinen blockierenden Befund. Eingearbeitet sind der Einzug, die zwei Zäune auf einer Zeile, die weitere Regel für den Zeilenanfang und die Lücken seiner Treiberfälle.
|
||||
- Lücken: Ein nackter Zaun als Öffner ist von einem Schlusszaun nicht zu unterscheiden. Fehlt der Schlusszaun und kommt als Nächstes ein nackter Zaun derselben oder größerer Länge, schließt der den Block, und die Rollen tauschen bis zum nächsten Öffner mit Sprache oder bis zum Dateiende; zwei solche Fehler heben sich auf. In der Zufallssuche blieben rund 2 % der fehlenden Schlusszäune still, rund 13 % der fehlenden oder klebenden Öffner. Dasselbe gilt für einen Backtick in der Sprachangabe, für Zäune hinter Listenpunkt oder Zitatzeichen und für Backtick-Zäune in Tilde-Blöcken.
|
||||
|
||||
Seit 2026-10-17 geht ein Block nicht mehr still in die falsche Datei, wenn seine Überschrift eine Datei ohne Endung nennt. Beide Werkzeuge nutzen dafür `ordne()` in `plan_materialisiere.py`; `plan_bloecke.py` importiert die Funktion. Anlass war die Plankritik von Paket R1 im Edge-Stack (Befunde 1 und 5): Die Überschrift „Stelle 30 — `host/logrotate/edge-traefik`" setzte kein Ziel, das Ziel blieb bei `tests/systemd/test-units.sh` aus Stelle 29, und der Block „Die ganze Datei ist neu" überschrieb diese Datei, mit Exit 0 und „0 Befund(e)". Die Regel jetzt:
|
||||
- Ziel ist der letzte PFAD-Treffer der Überschrift. Ohne ihn ist Ziel der letzte Token, der (ohne `./`) genau so unter `## Zieldateien` derselben Taskdatei steht. So wird eine Datei ohne Endung planbar: `host/logrotate/edge-traefik`, `Dockerfile`, `.gitignore`.
|
||||
- Ein absoluter Eintrag unter Zieldateien zählt relativ zum Projekt. Das ist in `plan_bloecke.py` `--projekt`, wie in `welle_vergleich.py`, und in `plan_materialisiere.py` der Ordner vor `docs/plans` im Pfad des Planordners. Ordner, Muster mit `*` und Pfade mit `..` unter Zieldateien sind kein Ziel.
|
||||
@@ -96,6 +120,6 @@ Offen seit 2026-09-23: `plan_materialisiere.py` stirbt mit `FileNotFoundError`,
|
||||
|
||||
## Was die Skripte nicht sehen
|
||||
|
||||
Dateien, die `.gitignore` ausblendet und die keine Zieldatei sind; bei einer ignorierten Zieldatei, ob sie vor der Welle schon so aussah; ob die Kopie selbst sauber ist (eine verschmutzte Kopie erzeugt Fehlalarm, kein falsches Grün); **ob die Kopie älter ist als der Plan** — wird eine Taskdatei nach dem Anwenden noch überarbeitet, meldet `welle_vergleich.py` eine Abweichung, die keine ist (2026-09-17, Teil 4c: der Planer lief noch, als die Kopie entstand). Dann entscheidet `plan_bloecke.py nach` gegen den echten Baum, und die betroffenen Dateien der Kopie werden aus `git show HEAD:<pfad>` neu aufgebaut und der Task erneut angewendet; verwechselbare sichtbare Zeichen (kyrillisches „с"); eine neue Zieldatei ohne Endung und ohne Schrägstrich, die nicht unter `## Zieldateien` steht — ihr Block geht still ins vorige Ziel. Sie sind ein Fangnetz, kein Nachweis — den liefert der Lauf mit echten Daten.
|
||||
Dateien, die `.gitignore` ausblendet und die keine Zieldatei sind; bei einer ignorierten Zieldatei, ob sie vor der Welle schon so aussah; ob die Kopie selbst sauber ist (eine verschmutzte Kopie erzeugt Fehlalarm, kein falsches Grün); **ob die Kopie älter ist als der Plan** — wird eine Taskdatei nach dem Anwenden noch überarbeitet, meldet `welle_vergleich.py` eine Abweichung, die keine ist (2026-09-17, Teil 4c: der Planer lief noch, als die Kopie entstand). Dann entscheidet `plan_bloecke.py nach` gegen den echten Baum, und die betroffenen Dateien der Kopie werden aus `git show HEAD:<pfad>` neu aufgebaut und der Task erneut angewendet; verwechselbare sichtbare Zeichen (kyrillisches „с"); eine neue Zieldatei ohne Endung und ohne Schrägstrich, die nicht unter `## Zieldateien` steht — ihr Block geht still ins vorige Ziel; ein nackter Zaun als Zeile im Text, ein fehlender Schlusszaun vor einem nackten Zaun und ein fehlender Öffner. Sie sind ein Fangnetz, kein Nachweis — den liefert der Lauf mit echten Daten.
|
||||
|
||||
Das ältere `.vault-tools/plan-diff/plan_diff.py` vergleicht Ganzdatei-Blöcke einer früheren Plankonvention und bleibt dafür in Gebrauch.
|
||||
@@ -13,8 +13,14 @@ Koordinator-Werkzeug im Scratchpad. Geltungsbereich (Fangnetz, kein Nachweis):
|
||||
Schraegstrich; Name, der direkt in --projekt als Datei liegt oder Dateiname eines Eintrags ist), ist
|
||||
jeder Aus- und wird-Block danach ein Befund, bis eine Ueberschrift mit Ziel kommt; vorher wurde still
|
||||
gegen das vorige Ziel geprueft. Luecken wie in plan_materialisiere.py; Nachweis: plan_ziel_treiber.py.
|
||||
- Zaunpruefung (seit 2026-11-02, Regel und Luecken im Kopf von plan_materialisiere.py, Nachweis: plan_zaun_treiber.py):
|
||||
Klebt ein Zaun, fehlt einer oder schliesst ein Block nie, ist das ein Befund, und die Bloecke dieser Taskdatei
|
||||
werden nicht geprueft: Hinter dem Zaun stimmt die Datei nicht mehr, und ein still geschluckter Aus-Block fiele
|
||||
sonst nur als "steht nicht in ..." auf. Vorher lief hier eine eigene Blocksuche nur in Spalte 0; seitdem gilt
|
||||
dieselbe wie in plan_materialisiere.py (zaun() und schliesst()), Aus- und wird-Bloecke zaehlen weiter nur mit
|
||||
Zaun in Spalte 0.
|
||||
- Exit 3 hat Vorrang: Wurde in keiner Taskdatei ein Block gegen eine Datei geprueft, endet es mit 3,
|
||||
auch wenn Befunde ausgegeben sind.
|
||||
auch wenn Befunde ausgegeben sind. Ausnahme ist ein Befund der Zaunpruefung, dort bleibt es bei 2.
|
||||
- Modus vor: jeder Aus-Block steht genau einmal in seiner Zieldatei (0 = Befund, >1 = Befund
|
||||
"mehrdeutig"). Belegt, dass der Plan den heutigen Stand byte-genau zitiert.
|
||||
- Modus nach: jeder wird-Block steht in der Zieldatei; jeder Aus-Block steht nicht mehr darin,
|
||||
@@ -30,12 +36,11 @@ import re
|
||||
import sys
|
||||
from pathlib import Path, PureWindowsPath
|
||||
|
||||
# Die Zielregel (PFAD, Zieldateien, offene Token) steht einmal, in plan_materialisiere.py im selben Ordner;
|
||||
# Zielregel und Zaunregel stehen einmal, in plan_materialisiere.py im selben Ordner;
|
||||
# der Ordner kommt ausdruecklich in den Suchpfad, sonst scheitert der Import unter "python -I"
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from plan_materialisiere import ordne, zieldateien # noqa: E402
|
||||
from plan_materialisiere import ordne, schliesst, zaun, zaunbefunde, zieldateien # noqa: E402
|
||||
|
||||
ZAUN = re.compile(r"^```")
|
||||
UEBERSCHRIFT = re.compile(r"^#{2,4} ")
|
||||
|
||||
|
||||
@@ -60,16 +65,15 @@ def lies_bloecke(taskdatei: Path, projekt: Path) -> list[dict]:
|
||||
ziel, ungeklaert = neu, None
|
||||
elif offen:
|
||||
ungeklaert = (i + 1, offen)
|
||||
if ZAUN.match(zeile):
|
||||
laenge = zaun(zeile)
|
||||
if laenge:
|
||||
start = i
|
||||
einzug = len(zeile) - len(zeile.lstrip())
|
||||
# Ein Zaun aus n Backticks schliesst erst mit mindestens n Backticks: Ein
|
||||
# Vier-Backtick-Block darf einen Drei-Backtick-Block enthalten (Doku-Tasks).
|
||||
laenge = len(zeile) - len(zeile.lstrip("`"))
|
||||
i += 1
|
||||
inhalt: list[str] = []
|
||||
while i < len(zeilen) and not (
|
||||
zeilen[i].startswith("`" * laenge) and not zeilen[i].strip("`").strip()
|
||||
):
|
||||
while i < len(zeilen) and not schliesst(zeilen[i], laenge, einzug):
|
||||
inhalt.append(zeilen[i])
|
||||
i += 1
|
||||
marke = ""
|
||||
@@ -78,7 +82,7 @@ def lies_bloecke(taskdatei: Path, projekt: Path) -> list[dict]:
|
||||
k -= 1
|
||||
if k >= 0:
|
||||
marke = zeilen[k].strip()
|
||||
if marke in ("Aus:", "wird:"):
|
||||
if marke in ("Aus:", "wird:") and zeile.startswith("`"):
|
||||
bloecke.append({"art": marke[:-1], "ziel": ziel, "zeile": start + 1, "text": "\n".join(inhalt),
|
||||
"ungeklaert": ungeklaert})
|
||||
i += 1
|
||||
@@ -95,8 +99,16 @@ def main() -> int:
|
||||
|
||||
befunde = 0
|
||||
geprueft = 0
|
||||
zaunfehler = False
|
||||
for name in argumente.taskdateien:
|
||||
taskdatei = Path(name)
|
||||
kaputt = zaunbefunde(taskdatei)
|
||||
if kaputt:
|
||||
for zeile, meldung in kaputt:
|
||||
print(f"BEFUND {taskdatei.name}:{zeile}: {meldung}; Bloecke dieser Taskdatei nicht geprueft")
|
||||
befunde += len(kaputt)
|
||||
zaunfehler = True
|
||||
continue
|
||||
bloecke = lies_bloecke(taskdatei, projekt)
|
||||
paare = 0
|
||||
for nummer, block in enumerate(bloecke):
|
||||
@@ -143,7 +155,7 @@ def main() -> int:
|
||||
print(f"BEFUND {taskdatei.name}:{block['zeile']}: Aus-Block steht noch in {block['ziel']}")
|
||||
befunde += 1
|
||||
print(f"{taskdatei.name}: {len(bloecke)} Aus-/wird-Bloecke, im Modus {argumente.modus} geprueft: {paare}")
|
||||
if geprueft == 0:
|
||||
if geprueft == 0 and not zaunfehler:
|
||||
print("Nichts geprueft: kein Aus-/wird-Block gefunden.")
|
||||
return 3
|
||||
print(f"{befunde} Befund(e).")
|
||||
|
||||
@@ -30,7 +30,40 @@ Geltungsbereich:
|
||||
enthaelt "Die ganze Datei ist neu" -> schreibt die Datei
|
||||
beginnt mit "Häng diesen Block ans Ende" -> haengt an
|
||||
Alles andere (Befehle, erwartete Ausgaben, Prosa-"wird: ...") wird uebergangen und gezaehlt.
|
||||
- Zaeune aus n Backticks schliessen erst mit n Backticks.
|
||||
- Zaeune aus n Backticks schliessen erst mit n Backticks, in einer Zeile ohne weiteren Text und hoechstens drei
|
||||
Spalten tiefer als der Oeffner; tiefer ist es Inhalt. Eine Zeile mit weiterem Backtick hinter dem Zaun oeffnet
|
||||
nichts, sie ist Inline-Code. Beides nach CommonMark 0.31.2, Abschnitt Fenced code blocks, nachgelesen 2026-11-01
|
||||
("may be preceded by up to three spaces of indentation", "Info strings for backtick code blocks cannot contain
|
||||
backticks"). Abweichend davon oeffnet ein Zaun bei jeder Einrueckung, und Tilde-Zaeune kennt das Werkzeug nicht.
|
||||
- Zaunpruefung (seit 2026-11-02, Anlass in zaunbefunde()): Vor dem ersten Schreibvorgang liest zaunbefunde() alle
|
||||
uebergebenen Taskdateien. Ein Befund in einer davon, und der ganze Lauf schreibt und loescht nichts, auch nicht
|
||||
die heilen Dateien davor. Befund sind, mit n als Zaunlaenge des Blocks:
|
||||
1. eine Zeile im Block, die auf mindestens n Backticks endet, ohne zu schliessen (klebender Zaun, auch ein
|
||||
Zaun mehr als drei Spalten tiefer als der Oeffner),
|
||||
2. eine Zeile im Block, die mit mindestens n Backticks beginnt, ohne zu schliessen (Schlusszaun fehlt,
|
||||
Aussenzaun zu kurz),
|
||||
3. ein Block, der bis zum Dateiende nicht schliesst,
|
||||
4. eine Taskdatei, die sich nicht lesen laesst.
|
||||
Herkunft: 1 und 3 stehen im Auftrag zur Haertung. 2 ist Projektvorgabe, denselben stillen Schaden richtet ein
|
||||
fehlender Zaun an. 4 ist Projektvorgabe nach AI/Regelwerk/pruefwerkzeuge.md, vorher stuerzte der Lauf mittendrin ab.
|
||||
Bewusst entschieden: Ein Text im Block, etwa eine Zeichenkette in einem Test, die am Zeilenende auf n Backticks
|
||||
endet oder mit ihnen beginnt, ist derselbe Befund. Von einem klebenden Zaun ist er nicht zu unterscheiden, und die
|
||||
Folgen sind ungleich: Der Fehlalarm kostet einen laengeren Zaun um den Block, der uebersehene Zaun schreibt
|
||||
Planprosa in die Zieldatei. Gemessen am 2026-11-02 ueber alle verschiedenen Taskdateien im Vault und in den
|
||||
Jobordnern: 1 und 3 ohne Treffer, 2 acht Zeilen in drei alten Doku-Tasks, alle echt (Aussenzaun zu kurz, der
|
||||
Block endete zu frueh).
|
||||
Nicht entscheidbar ist ein nackter Zaun als eigene Zeile im Text, hoechstens drei Spalten tiefer als der Oeffner:
|
||||
Er schliesst den Block. Geht die Zahl der Zaeune auf, wird der Rest Prosa, der Block endet dort, und es kommt
|
||||
kein Befund (Treiberfall "Luecke:"). Text mit Zaeunen braucht deshalb immer einen laengeren Zaun um den Block.
|
||||
Kein Befund: laengere Zaeune um Markdown-Vorlagen mit ##-Ueberschriften und kuerzeren Zaeunen darin, Inline-Code
|
||||
und Zaunfolgen mitten in der Zeile oder in Anfuehrungszeichen, Prosa ausserhalb von Bloecken.
|
||||
Nicht gesehen (Fangnetz, kein Nachweis): Ein nackter Zaun als Oeffner ist von einem Schlusszaun nicht zu
|
||||
unterscheiden. Fehlt der Schlusszaun und kommt als naechstes ein nackter Zaun derselben oder groesserer Laenge,
|
||||
schliesst der den Block, und die Rollen tauschen bis zum naechsten Oeffner mit Sprache (das meldet 2) oder bis
|
||||
zum Dateiende (das meldet 3 am letzten Zaun, zwei solche Fehler heben sich auf). Ebenso bei einem Oeffner, der
|
||||
fehlt, an der Zeile davor klebt oder einen Backtick in der Sprachangabe traegt, bei Zaeunen hinter Listenpunkt
|
||||
oder Zitatzeichen und bei Backtick-Zaeunen in Tilde-Bloecken. Nachweis: plan_zaun_treiber.py. plan_bloecke.py
|
||||
nutzt dieselben Funktionen.
|
||||
- Geschrieben wird immer mit LF.
|
||||
Aufruf: python plan_materialisiere.py <planordner> <kopie> <taskdatei> [<taskdatei> ...]
|
||||
Exit 0 ohne Befund, 2 mit Befund.
|
||||
@@ -133,6 +166,65 @@ def normal(text: str) -> str:
|
||||
return text.replace("\r\n", "\n").replace("\r", "\n")
|
||||
|
||||
|
||||
def zaun(zeile: str) -> int:
|
||||
"""Laenge des Zauns, der in dieser Zeile einen Block oeffnet, sonst 0."""
|
||||
kern = zeile.lstrip()
|
||||
laenge = len(kern) - len(kern.lstrip("`"))
|
||||
return laenge if laenge >= 3 and "`" not in kern[laenge:] else 0
|
||||
|
||||
|
||||
def schliesst(zeile: str, laenge: int, einzug: int) -> bool:
|
||||
"""Schliesst die Zeile einen Block mit Zaun der Laenge laenge und Einzug einzug?
|
||||
|
||||
Mindestens so viele Backticks, sonst nichts in der Zeile, hoechstens drei Spalten tiefer als der Oeffner.
|
||||
Nachbesserung 2026-11-02 (Pruefung der Zaunpruefung): Vorher schloss jeder Einzug und "``` ```". Ein eingerueckter
|
||||
Zaun in einer Zeichenkette kappte den Block mit 0 Befunden, und plan_bloecke.py hatte das bis dahin gesehen.
|
||||
"""
|
||||
kern = zeile.lstrip()
|
||||
return kern.startswith("`" * laenge) and not kern.rstrip().strip("`") and len(zeile) - len(kern) - einzug <= 3
|
||||
|
||||
|
||||
def zaunbefunde(taskdatei: Path) -> list[tuple[int, str]]:
|
||||
"""(Zeile, Meldung) je Stelle, an der die Bloecke der Taskdatei nicht stimmen; leer heisst: bloecke() liest sie richtig.
|
||||
|
||||
Nachbesserung 2026-11-01 (Kette 050 der Trading Plattform, Auftrag 13): In task-1020 klebte der Zaun an `])`,
|
||||
der Block lief bis in den naechsten Task, es kam nur "Aus-Block steht 0-mal", und ein Aus/wird-Paar ging ohne
|
||||
Meldung verloren. Unter "Die ganze Datei ist neu" schrieb derselbe Fehler Planprosa mit 0 Befunden in die Datei.
|
||||
Fuer die Meldung endet ein Block am klebenden Zaun, und ein Zaun am Zeilenanfang im Block beginnt einen neuen: So
|
||||
steht jeder Fehler einmal da, und die Bloecke dahinter stimmen wieder. War es doch ein Text mit Zaunfolge, oeffnet
|
||||
der echte Schlusszaun fuer diese Suche einen Block, und ein Folgebefund steht weiter hinten; der laengere Zaun um
|
||||
den Block loest beide.
|
||||
"""
|
||||
try:
|
||||
zeilen = normal(taskdatei.read_text(encoding="utf-8")).split("\n")
|
||||
except (OSError, UnicodeDecodeError) as fehler:
|
||||
return [(0, f"Taskdatei nicht lesbar ({fehler})")]
|
||||
befunde = []
|
||||
i = 0
|
||||
while i < len(zeilen):
|
||||
laenge = zaun(zeilen[i])
|
||||
start = i
|
||||
i += 1
|
||||
if not laenge:
|
||||
continue
|
||||
spalte = len(zeilen[start]) - len(zeilen[start].lstrip())
|
||||
while i < len(zeilen) and not schliesst(zeilen[i], laenge, spalte):
|
||||
if zeilen[i].rstrip().endswith("`" * laenge):
|
||||
befunde.append((i + 1, f"Zaun klebt an der Codezeile, Block ab Zeile {start + 1}; der Schlusszaun "
|
||||
"gehoert auf eine eigene Zeile, ein Text mit Zaunfolge in einen laengeren Zaun"))
|
||||
break
|
||||
kern = zeilen[i].lstrip()
|
||||
if kern.startswith("`" * laenge):
|
||||
befunde.append((i + 1, f"hier oeffnet ein Block, der Block ab Zeile {start + 1} ist aber nicht geschlossen; "
|
||||
"es fehlt sein Schlusszaun, oder der Text braucht einen laengeren Zaun"))
|
||||
start, laenge, spalte = i, len(kern) - len(kern.lstrip("`")), len(zeilen[i]) - len(kern)
|
||||
i += 1
|
||||
if i >= len(zeilen):
|
||||
befunde.append((start + 1, f"Block schliesst nie (Zaun aus {laenge} Backticks)"))
|
||||
i += 1
|
||||
return befunde
|
||||
|
||||
|
||||
def bloecke(taskdatei: Path, kopie: Path | None = None):
|
||||
zeilen = normal(taskdatei.read_text(encoding="utf-8")).split("\n")
|
||||
geplant = zieldateien(zeilen, wurzeln(taskdatei.parent))
|
||||
@@ -149,17 +241,13 @@ def bloecke(taskdatei: Path, kopie: Path | None = None):
|
||||
ziel, ungeklaert = neu, None
|
||||
elif offen:
|
||||
ungeklaert = (i + 1, offen)
|
||||
if zeile.lstrip().startswith("```"):
|
||||
laenge = zaun(zeile)
|
||||
if laenge:
|
||||
einzug = len(zeile) - len(zeile.lstrip())
|
||||
kern = zeile.lstrip()
|
||||
laenge = len(kern) - len(kern.lstrip("`"))
|
||||
start = i
|
||||
i += 1
|
||||
inhalt = []
|
||||
while i < len(zeilen):
|
||||
kandidat = zeilen[i].lstrip()
|
||||
if kandidat.startswith("`" * laenge) and not kandidat.strip("`").strip():
|
||||
break
|
||||
while i < len(zeilen) and not schliesst(zeilen[i], laenge, einzug):
|
||||
inhalt.append(zeilen[i][einzug:] if zeilen[i][:einzug].strip() == "" else zeilen[i])
|
||||
i += 1
|
||||
k = start - 1
|
||||
@@ -219,6 +307,14 @@ def loesche(taskdatei: Path, kopie: Path) -> int:
|
||||
def main() -> int:
|
||||
plan, kopie = Path(sys.argv[1]), Path(sys.argv[2])
|
||||
befunde = 0
|
||||
# Erst alle Zaeune aller Taskdateien, dann der erste Schreibvorgang: Hinter einem kaputten Zaun stimmt die Datei nicht mehr
|
||||
for name in sys.argv[3:]:
|
||||
for zeile, meldung in zaunbefunde(plan / name):
|
||||
print(f"BEFUND {name}:{zeile}: {meldung}; nichts geschrieben")
|
||||
befunde += 1
|
||||
if befunde:
|
||||
print(f"{befunde} Befund(e).")
|
||||
return 2
|
||||
for name in sys.argv[3:]:
|
||||
taskdatei = plan / name
|
||||
alt = None
|
||||
|
||||
@@ -0,0 +1,377 @@
|
||||
"""Regressionsnachweis fuer die Zaunpruefung von plan_materialisiere.py und plan_bloecke.py.
|
||||
|
||||
Anlass 2026-11-01 (Kette 050 der Trading Plattform, Auftrag 13): In task-1020 klebte der schliessende Zaun
|
||||
zweimal an der Codezeile. Der Block lief weiter, schluckte das naechste Aus/wird-Paar ohne eigene Meldung, und
|
||||
das Werkzeug sagte nur "Aus-Block steht 0-mal". Unter "Die ganze Datei ist neu" schrieb es Planprosa in die
|
||||
Zieldatei und meldete 0 Befunde. Die Faelle unten bauen diese Form nach, an erster und an letzter Stelle, je
|
||||
schreibender Marke, im eingerueckten und im laengeren Block, mit CRLF, ueber zwei Taskdateien, dazu den Block,
|
||||
der nie schliesst, und den fehlenden Zaun vor dem naechsten Block.
|
||||
Die Angriffe aus paar() haben ein geheiltes Gegenstueck, das sich nur im Zaun unterscheidet und schreiben muss:
|
||||
Sonst waere "nichts geschrieben" auch dann gruen, wenn die Fixture von vornherein nichts anwendet. Die Gegenfaelle
|
||||
ohne Befund sind echte Inhalte: Markdown-Vorlagen in laengeren Zaeunen mit ##-Ueberschriften, eingerueckte Bloecke,
|
||||
Inline-Code, Zeichenketten mit Zaunfolge. Faelle mit "Luecke:" im Namen halten fest, was das Werkzeug bewusst nicht
|
||||
sieht: Wird einer rot, ist die Luecke zu und der Kopfkommentar von plan_materialisiere.py nachzuziehen.
|
||||
Grundlinie zuerst, dann jeder Fall einzeln in einem frischen Wegwerfordner. Je Fall werden Exit-Code, jede
|
||||
erwartete Befundzeile samt Zeilennummer (aus dem Text der Taskdatei gerechnet), die Zahl der BEFUND-Zeilen, die
|
||||
Summenzeile und bei plan_materialisiere.py jede Datei der Kopie geprueft.
|
||||
--mutanten baut die Fehler aus MUTANTEN je in eine Kopie der Werkzeuge ein und verlangt, dass mindestens ein Fall
|
||||
rot wird; ein Mutant, dessen Zeile nicht genau einmal dasteht, ist selbst ein Fehler.
|
||||
Aufruf: python plan_zaun_treiber.py [--werkzeuge <ordner>] [--mutanten] Vorgabe: der Ordner dieses Skripts.
|
||||
Exit 0, wenn jeder Fall so ausgeht wie erwartet und jeder Mutant gefangen ist, sonst 2.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
BASIS = {
|
||||
"tests/a.sh": "eins\nzwei\ndrei\n",
|
||||
"tests/b.sh": "vier\nfuenf\n",
|
||||
"tests/c.py": "@pytest.mark.parametrize(\"pfad\", [\n pytest.param(pfad)\n])\n",
|
||||
"README.md": "# Projekt\n",
|
||||
}
|
||||
Z = "```"
|
||||
KLEBT = "Zaun klebt"
|
||||
FEHLT = "nicht geschlossen"
|
||||
OFFEN = "schliesst nie"
|
||||
LESEN = "nicht lesbar"
|
||||
KOPF = "# Task 01\n\n## Prüfstand\n\n"
|
||||
A = "#### Stelle 1 — `tests/a.sh`"
|
||||
B = "#### Stelle 2 — `tests/b.sh`"
|
||||
C = "#### Stelle 1 — `tests/c.py`"
|
||||
N = "#### Stelle 1 — `docs/neu.md`"
|
||||
NICHTS = {}
|
||||
GANZ_A = {"tests/a.sh": "eins\nZWEI\ndrei\n"}
|
||||
GANZ_B = {"tests/b.sh": "VIER\nfuenf\n"}
|
||||
|
||||
|
||||
def b(wort: str, wortlaut: str | None, welche: int = 1, datei: str = "task-01.md") -> tuple:
|
||||
"""Erwarteter Befund: Wort, Text der Zeile (welche-tes Vorkommen, -1 das letzte), Taskdatei."""
|
||||
return wort, wortlaut, welche, datei
|
||||
|
||||
|
||||
def aus_wird(ueberschrift: str, alt: str, neu: str, schluss_aus: str = "\n" + Z, schluss_wird: str = "\n" + Z) -> str:
|
||||
return f"{ueberschrift}\n\nAus:\n\n{Z}text\n{alt}{schluss_aus}\n\nwird:\n\n{Z}text\n{neu}{schluss_wird}\n\n"
|
||||
|
||||
|
||||
def ganz(ueberschrift: str, text: str, schluss: str = "\n" + Z) -> str:
|
||||
return f"{ueberschrift}\n\nDie ganze Datei ist neu:\n\n{Z}text\n{text}{schluss}\n\n"
|
||||
|
||||
|
||||
def anhaengen(ueberschrift: str, text: str, schluss: str = "\n" + Z) -> str:
|
||||
return f"{ueberschrift}\n\nHäng diesen Block ans Ende der Datei:\n\n{Z}text\n{text}{schluss}\n\n"
|
||||
|
||||
|
||||
def befehl(text: str, schluss: str = "\n" + Z) -> str:
|
||||
return f"Aufruf:\n\n{Z}bash\n{text}{schluss}\n\n"
|
||||
|
||||
|
||||
STELLE_A = aus_wird(A, "zwei", "ZWEI")
|
||||
STELLE_B = aus_wird(B, "vier", "VIER")
|
||||
|
||||
# Name, Art (mat, vor, nach), Taskdatei(en), Exit, erwartete Befunde (Wort, Text der Zeile), Dateien der Kopie
|
||||
# (nur mat; nur Abweichungen von BASIS, None heisst geloescht)
|
||||
FAELLE = [
|
||||
("Grundlinie", "mat", KOPF + STELLE_A + STELLE_B, 0, [], {**GANZ_A, **GANZ_B}),
|
||||
("bloecke: Grundlinie", "vor", KOPF + STELLE_A + STELLE_B, 0, [], None),
|
||||
]
|
||||
|
||||
|
||||
def paar(name: str, vorlage: str, klebt: str, heil: str, erwartet: list, dateien: dict, bloecke: bool = True) -> None:
|
||||
"""Haengt den Angriff und sein Gegenstueck an; {S} in der Vorlage ist die Stelle des Zauns."""
|
||||
FAELLE.append((name, "mat", vorlage.replace("{S}", klebt), 2, erwartet, NICHTS))
|
||||
FAELLE.append((f"{name}: geheilt", "mat", vorlage.replace("{S}", heil), 0, [], dateien))
|
||||
if bloecke:
|
||||
FAELLE.append((f"bloecke vor: {name}", "vor", vorlage.replace("{S}", klebt), 2, erwartet, None))
|
||||
FAELLE.append((f"bloecke nach: {name}", "nach", vorlage.replace("{S}", klebt), 2, erwartet, None))
|
||||
|
||||
|
||||
NAH = "\n" + Z
|
||||
# Die Form aus task-1020: beide Zaeune kleben an "])", das naechste Paar waere geschluckt
|
||||
paar("T27-Form: Aus und wird kleben", KOPF + aus_wird(C, " pytest.param(pfad)\n])", " pytest.param(PFAD)\n])", "{S}", "{S}") + STELLE_B,
|
||||
Z, NAH, [b(KLEBT, "])" + Z), b(KLEBT, "])" + Z, 2)], {"tests/c.py": BASIS["tests/c.py"].replace("(pfad)", "(PFAD)"), **GANZ_B})
|
||||
paar("nur Aus klebt, erste Stelle", KOPF + aus_wird(A, "zwei", "ZWEI", "{S}") + STELLE_B, Z, NAH, [b(KLEBT, "zwei" + Z)], {**GANZ_A, **GANZ_B})
|
||||
paar("nur wird klebt, Folgepaar", KOPF + aus_wird(A, "zwei", "ZWEI", NAH, "{S}") + STELLE_B, Z, NAH, [b(KLEBT, "ZWEI" + Z)], {**GANZ_A, **GANZ_B})
|
||||
paar("nur wird klebt, letzte Stelle", KOPF + STELLE_B + aus_wird(A, "zwei", "ZWEI", NAH, "{S}"), Z, NAH, [b(KLEBT, "ZWEI" + Z)], {**GANZ_A, **GANZ_B})
|
||||
paar("ganze Datei klebt, Folgepaar", KOPF + ganz(N, "neu", "{S}") + STELLE_A, Z, NAH, [b(KLEBT, "neu" + Z)], {"docs/neu.md": "neu\n", **GANZ_A}, bloecke=False)
|
||||
paar("ganze Datei klebt, letzte Stelle", KOPF + STELLE_A + ganz(N, "neu", "{S}"), Z, NAH, [b(KLEBT, "neu" + Z)], {"docs/neu.md": "neu\n", **GANZ_A}, bloecke=False)
|
||||
paar("anhaengen klebt", KOPF + STELLE_A + anhaengen(B, "sechs", "{S}") + "Schluss.\n", Z, NAH, [b(KLEBT, "sechs" + Z)],
|
||||
{**GANZ_A, "tests/b.sh": "vier\nfuenf\nsechs\n"}, bloecke=False)
|
||||
paar("Befehlsblock klebt, Folgepaar", KOPF + befehl("ls", "{S}") + STELLE_A, Z, NAH, [b(KLEBT, "ls" + Z)], GANZ_A)
|
||||
paar("eingerueckter Block klebt", KOPF + f"1. Schritt:\n\n {Z}bash\n ls{{S}}\n\n" + STELLE_A, Z, "\n " + Z, [b(KLEBT, " ls" + Z)], GANZ_A)
|
||||
paar("Leerzeichen hinter dem klebenden Zaun", KOPF + STELLE_B + ganz(N, "neu", "{S}"), Z + " ", NAH, [b(KLEBT, "neu" + Z + " ")], {"docs/neu.md": "neu\n", **GANZ_B}, bloecke=False)
|
||||
paar("klebender Zaun laenger als der Block", KOPF + aus_wird(A, "zwei", "ZWEI", "{S}") + STELLE_B, Z + "``", NAH, [b(KLEBT, "zwei" + Z + "``")], {**GANZ_A, **GANZ_B})
|
||||
VIER = Z + "`"
|
||||
paar("Vierer-Zaun klebt", KOPF + f"{N}\n\nDie ganze Datei ist neu:\n\n{VIER}markdown\nneu{{S}}\n\n" + STELLE_A, VIER, "\n" + VIER, [b(KLEBT, "neu" + VIER)],
|
||||
{"docs/neu.md": "neu\n", **GANZ_A}, bloecke=False)
|
||||
# Bewusst entschieden: Auch ein Text, der auf einen Zaun endet, ist ein Befund; Abhilfe ist der laengere Zaun (Fall unten)
|
||||
FAELLE.append(("Zeichenkette im Test endet auf den Zaun", "mat",
|
||||
KOPF + ganz(N, f'TEXT = """Aus:\nzwei{Z}\n"""') + STELLE_A, 2, [b(KLEBT, "zwei" + Z), b(FEHLT, Z + "text", 2)], NICHTS))
|
||||
# Block, der nie schliesst: letzte, einzige und allerletzte Zeile, Schlusszaun zu kurz; die heile Form schreibt
|
||||
paar("Block schliesst nie, letzte Stelle", KOPF + STELLE_A + f"{N}\n\nDie ganze Datei ist neu:\n\n{Z}text\nneu\n{{S}}", "", Z + "\n",
|
||||
[b(OFFEN, Z + "text", -1)], {"docs/neu.md": "neu\n", **GANZ_A}, bloecke=False)
|
||||
paar("Block schliesst nie, einziger Block", KOPF + f"{N}\n\nDie ganze Datei ist neu:\n\n{Z}text\nneu\n\nProsa.\n{{S}}", "", Z + "\n",
|
||||
[b(OFFEN, Z + "text")], {"docs/neu.md": "neu\n\nProsa.\n"}, bloecke=False)
|
||||
paar("Aus-Block schliesst nie", KOPF + f"{A}\n\nAus:\n\n{Z}text\nzwei\n{{S}}", "", Z + "\n\nwird:\n\n" + Z + "text\nZWEI\n" + Z + "\n", [b(OFFEN, Z + "text")], GANZ_A)
|
||||
paar("Zaun auf der allerletzten Zeile", KOPF + STELLE_A + "Aufruf:\n\n" + Z + "{S}", "", "bash\nls\n" + Z, [b(OFFEN, Z, -1)], GANZ_A)
|
||||
paar("Schlusszaun kuerzer als der Oeffner", KOPF + f"{N}\n\nDie ganze Datei ist neu:\n\n{VIER}text\nneu\n{{S}}\n\nProsa.\n", Z, VIER, [b(OFFEN, VIER + "text")], {"docs/neu.md": "neu\n"}, bloecke=False)
|
||||
# Zaun fehlt, der naechste Block oeffnet: Der Block schloesse mit dem Zaun des naechsten
|
||||
paar("Zaun fehlt, Folgepaar", KOPF + ganz(N, "neu", "{S}") + STELLE_A, "", NAH, [b(FEHLT, Z + "text", 2)], {"docs/neu.md": "neu\n", **GANZ_A}, bloecke=False)
|
||||
paar("Zaun fehlt im Aus-Block", KOPF + aus_wird(A, "zwei", "ZWEI", "{S}") + STELLE_B, "", NAH, [b(FEHLT, Z + "text", 2)], {**GANZ_A, **GANZ_B})
|
||||
paar("Zaun fehlt, naechster Block ist laenger", KOPF + ganz(N, "neu", "{S}") + f"{A}\n\nDie ganze Datei ist neu:\n\n{VIER}markdown\n{Z}text\nx\n{Z}\n{VIER}\n", "", NAH,
|
||||
[b(FEHLT, VIER + "markdown")], {"docs/neu.md": "neu\n", "tests/a.sh": f"{Z}text\nx\n{Z}\n"}, bloecke=False)
|
||||
# Zwei Fehler in einer Datei: beide stehen da, keiner verdeckt den anderen
|
||||
FAELLE.append(("klebt und schliesst nie", "mat", KOPF + aus_wird(A, "zwei", "ZWEI", Z) + ganz(N, "neu", ""), 2,
|
||||
[b(KLEBT, "zwei" + Z), b(OFFEN, Z + "text", -1)], NICHTS))
|
||||
paar("CRLF: beide kleben", (KOPF + aus_wird(A, "zwei", "ZWEI", "{S}", "{S}") + STELLE_B).replace("\n", "\r\n"), Z, "\r\n" + Z,
|
||||
[b(KLEBT, "zwei" + Z), b(KLEBT, "ZWEI" + Z)], {**GANZ_A, **GANZ_B})
|
||||
# Mehrere Taskdateien: Der ganze Lauf schreibt nichts, auch die heile Datei davor nicht
|
||||
ZWEI_DATEIEN = [("zweite Taskdatei klebt", {"task-01.md": KOPF + STELLE_A, "task-02.md": KOPF + ganz(N, "neu", Z)}, [b(KLEBT, "neu" + Z, 1, "task-02.md")]),
|
||||
("erste Taskdatei klebt", {"task-01.md": KOPF + ganz(N, "neu", Z), "task-02.md": KOPF + STELLE_A}, [b(KLEBT, "neu" + Z)])]
|
||||
for name, dateien, erwartet in ZWEI_DATEIEN:
|
||||
FAELLE.append((name, "mat", dateien, 2, erwartet, NICHTS))
|
||||
FAELLE.append(("zwei Taskdateien: geheilt", "mat", {"task-01.md": KOPF + STELLE_A, "task-02.md": KOPF + ganz(N, "neu")}, 0, [], {"docs/neu.md": "neu\n", **GANZ_A}))
|
||||
# Loeschung unter Zieldateien bleibt aus, solange die Taskdatei einen Zaunbefund traegt
|
||||
LOESCHEN = "# Task 01\n\n## Zieldateien\n\n- `tests/b.sh` (gelöscht)\n\n## Prüfstand\n\n" + aus_wird(A, "zwei", "ZWEI", "{S}")
|
||||
paar("Loeschung unter Zieldateien", LOESCHEN, Z, NAH, [b(KLEBT, "zwei" + Z)], {**GANZ_A, "tests/b.sh": None}, bloecke=False)
|
||||
# Unlesbar: ein Befund, kein Traceback, auch die heile Datei davor bleibt ungeschrieben
|
||||
FAELLE.append(("Taskdatei fehlt", "mat", {"task-01.md": KOPF + STELLE_A, "task-02.md": None}, 2, [b(LESEN, None, 1, "task-02.md")], NICHTS))
|
||||
FAELLE.append(("Taskdatei ist kein UTF-8", "mat", {"task-01.md": KOPF + STELLE_A, "task-02.md": b"\xff\xfe\x00kaputt"}, 2, [b(LESEN, None, 1, "task-02.md")], NICHTS))
|
||||
FAELLE.append(("bloecke: Taskdatei fehlt", "vor", {"task-01.md": KOPF + STELLE_A, "task-02.md": None}, 2, [b(LESEN, None, 1, "task-02.md")], None))
|
||||
|
||||
# Nachbesserung nach der Pruefung vom 2026-11-02: Einzug, zwei Zaeune auf einer Zeile, Zeile beginnt mit dem Zaun
|
||||
TIEF = " " + Z
|
||||
ZEICHENKETTE = f'def test():\n t = """\n{TIEF}\n code\n{TIEF}\n """'
|
||||
FAELLE.extend([
|
||||
("Zaun vier Spalten tiefer ist Inhalt, kein Schluss", "mat", KOPF + ganz(N, ZEICHENKETTE) + STELLE_A, 2, [b(KLEBT, TIEF)], NICHTS),
|
||||
("Zaun vier Spalten tiefer im laengeren Zaun", "mat", KOPF + f"{N}\n\nDie ganze Datei ist neu:\n\n{VIER}python\n{ZEICHENKETTE}\n{VIER}\n\n" + STELLE_A, 0, [],
|
||||
{"docs/neu.md": ZEICHENKETTE + "\n", **GANZ_A}),
|
||||
("bloecke: Zaun vier Spalten tiefer im Aus-Block", "vor", KOPF + aus_wird(A, f"eins\n{TIEF}\n zwei\n{TIEF}\ndrei", "X") + STELLE_B, 2, [b(KLEBT, TIEF)], None),
|
||||
("Zaun drei Spalten tiefer schliesst", "mat", KOPF + ganz(N, "neu", "\n " + Z) + STELLE_A, 0, [], {"docs/neu.md": "neu\n", **GANZ_A}),
|
||||
("eingerueckter Block: Schlusszaun drei Spalten tiefer", "mat", KOPF + f"1. Schritt:\n\n {Z}bash\n ls\n {Z}\n\n" + STELLE_A, 0, [], GANZ_A),
|
||||
("eingerueckter Block: Zaun vier Spalten tiefer", "mat", KOPF + f"1. Schritt:\n\n {Z}bash\n ls\n {Z}\n {Z}\n\n" + STELLE_A, 2,
|
||||
[b(KLEBT, " " + Z), b(FEHLT, Z + "text")], NICHTS),
|
||||
("zwei Zaeune auf einer Zeile schliessen nicht", "mat", KOPF + ganz(N, "neu", f"\n{Z} {Z}") + STELLE_A, 2, [b(KLEBT, f"{Z} {Z}")], NICHTS),
|
||||
("Blockzeile beginnt mit dem Zaun und endet anders", "mat", KOPF + ganz(N, f"neu\n{Z} {Z}text\nzwei") + STELLE_A, 2, [b(FEHLT, f"{Z} {Z}text")], NICHTS),
|
||||
("eingerueckter Oeffner im Block", "mat", KOPF + f"1. Schritt:\n\n {Z}bash\n ls\n\n2. Danach:\n\n {Z}text\n pwd\n {Z}\n\n" + STELLE_A, 2, [b(FEHLT, " " + Z + "text")], NICHTS),
|
||||
("zwei Bloecke ohne Schluss", "mat", KOPF + ganz(N, "neu", "") + ganz(A, "x", ""), 2, [b(FEHLT, Z + "text", 2), b(OFFEN, Z + "text", 2)], NICHTS),
|
||||
("Meldung nennt den Block des klebenden Zauns", "mat", f"Prosa.\n\n{Z}text\nzwei{Z}\n", 2, [b("Block ab Zeile 3", "zwei" + Z)], NICHTS),
|
||||
("Meldung nennt je den offenen Block", "mat", f"{Z}text\na\n{Z}text\nb\n{Z}text\nc\n{Z}\n", 2, [b("Block ab Zeile 1", Z + "text", 2), b("Block ab Zeile 3", Z + "text", 3)], NICHTS),
|
||||
("Prosa beginnt mit zwei Backticks", "mat", KOPF + "``nur zwei\n\n" + STELLE_A, 0, [], GANZ_A),
|
||||
("Zaun fehlt, naechster Block tiefer eingerueckt", "mat", KOPF + ganz(N, "neu", "") + f"1. Schritt:\n\n {Z}bash\n ls\n {Z}\n\n" + STELLE_A, 2,
|
||||
[b(FEHLT, " " + Z + "bash")], NICHTS),
|
||||
# plan_bloecke.py: Der Schlusszaun des eingerueckten Blocks muss schliessen, sonst ginge das Aus danach still verloren
|
||||
("bloecke: eingerueckter Block, Schlusszaun drei Spalten tiefer", "vor", KOPF + f"1. Schritt:\n\n {Z}bash\n ls\n {Z}\n\n" + aus_wird(A, "gibt es nicht", "x"), 2,
|
||||
[b("Aus-Block steht nicht", Z + "text")], None),
|
||||
# Bewusst nicht gesehen: Ein nackter Zaun in Spalte 0 schliesst, die Zahl der Zaeune geht auf, der Rest ist Prosa
|
||||
("Luecke: nackter Zaun als Zeile im Text", "mat", KOPF + ganz(N, f't = """\n{Z}\ncode\n{Z}\n"""') + STELLE_A, 0, [], {"docs/neu.md": 't = """\n', **GANZ_A}),
|
||||
])
|
||||
|
||||
# Echte Inhalte: kein Befund, und der Text kommt unveraendert an
|
||||
VORLAGE =f"## Abschnitt\n\n### `tests/b.sh`\n\nProsa mit `Code`.\n\n{Z}text\nx\n{Z}\n\n{Z}bash\ny\n{Z}"
|
||||
FAELLE.extend([
|
||||
("Markdown-Vorlage im Vierer-Zaun, ##-Ueberschriften aendern kein Ziel", "mat",
|
||||
KOPF + f"{N}\n\nDie ganze Datei ist neu:\n\n{VIER}markdown\n{VORLAGE}\n{VIER}\n\n" + STELLE_A, 0, [], {"docs/neu.md": VORLAGE + "\n", **GANZ_A}),
|
||||
("Vorlage: letzte Stelle", "mat", KOPF + STELLE_A + f"{N}\n\nDie ganze Datei ist neu:\n\n{VIER}markdown\n{VORLAGE}\n{VIER}\n", 0, [], {"docs/neu.md": VORLAGE + "\n", **GANZ_A}),
|
||||
("Eingerueckter Befehl in einer Liste", "mat", KOPF + f"1. Schritt:\n\n {Z}bash\n ls\n {Z}\n\n2. Danach:\n\n {Z}\n pwd\n {Z}\n\n" + STELLE_A, 0, [], GANZ_A),
|
||||
("Spalte-0-Block mit eingeruecktem Schlusszaun", "mat", KOPF + ganz(N, "neu", "\n " + Z) + STELLE_A, 0, [], {"docs/neu.md": "neu\n", **GANZ_A}),
|
||||
("Inline-Code am Ende einer Blockzeile", "mat", KOPF + ganz(N, "- `pfad`\n- ``doppelt``\n- ende``") + STELLE_A, 0, [],
|
||||
{"docs/neu.md": "- `pfad`\n- ``doppelt``\n- ende``\n", **GANZ_A}),
|
||||
("Zaunfolge am Zeilenende im Vierer-Zaun", "mat", KOPF + f"{N}\n\nDie ganze Datei ist neu:\n\n{VIER}text\nzwei{Z}\n{VIER}\n\n" + STELLE_A, 0, [],
|
||||
{"docs/neu.md": f"zwei{Z}\n", **GANZ_A}),
|
||||
("Zeichenkette im Test im laengeren Zaun", "mat", KOPF + f'{N}\n\nDie ganze Datei ist neu:\n\n{VIER}python\nTEXT = """Aus:\nzwei{Z}\n{Z}text\n"""\n{VIER}\n\n' + STELLE_A, 0, [],
|
||||
{"docs/neu.md": f'TEXT = """Aus:\nzwei{Z}\n{Z}text\n"""\n', **GANZ_A}),
|
||||
("Zaunfolge in Anfuehrungszeichen", "mat", KOPF + ganz(N, f'Z = "{Z}"\nzaun = f"{{Z}}text"\nassert x == "{Z}"') + STELLE_A, 0, [],
|
||||
{"docs/neu.md": f'Z = "{Z}"\nzaun = f"{{Z}}text"\nassert x == "{Z}"\n', **GANZ_A}),
|
||||
("Zaunfolge mitten in der Zeile", "mat", KOPF + ganz(N, f"a {Z} b\n{Z[:2]}x") + STELLE_A, 0, [], {"docs/neu.md": f"a {Z} b\n{Z[:2]}x\n", **GANZ_A}),
|
||||
("Prosa endet auf Zaunfolge", "mat", KOPF + f"Der Zaun besteht aus {Z}\n\n" + STELLE_A, 0, [], GANZ_A),
|
||||
("Inline-Code am Zeilenanfang oeffnet nichts", "mat", KOPF + f"{Z}foo{Z} ist Inline-Code.\n\n" + STELLE_A + STELLE_B, 0, [], {**GANZ_A, **GANZ_B}),
|
||||
("Inline-Code am Zeilenanfang, letzte Zeile", "mat", KOPF + STELLE_A + f"{Z}foo{Z} ist Inline-Code.\n", 0, [], GANZ_A),
|
||||
("Schlusszaun mit Leerzeichen dahinter", "mat", KOPF + ganz(N, "neu", "\n" + Z + " ") + STELLE_A, 0, [], {"docs/neu.md": "neu\n", **GANZ_A}),
|
||||
("Schlusszaun laenger als der Oeffner", "mat", KOPF + ganz(N, "neu", "\n" + VIER) + STELLE_A, 0, [], {"docs/neu.md": "neu\n", **GANZ_A}),
|
||||
("leerer Block", "mat", KOPF + f"Aufruf:\n\n{Z}\n{Z}\n\n" + STELLE_A, 0, [], GANZ_A),
|
||||
("Taskdatei ohne Block", "mat", KOPF + "Nur Prosa.\n", 0, [], NICHTS),
|
||||
("leere Taskdatei", "mat", "", 0, [], NICHTS),
|
||||
# plan_bloecke.py: dieselbe Blockstruktur wie plan_materialisiere.py
|
||||
("bloecke: Vorlage als Aus-Block im Vierer-Zaun", "vor", KOPF + STELLE_A + f"{A}\n\nAus:\n\n{VIER}text\nzwei\ndrei\n{VIER}\n\nwird:\n\n{VIER}markdown\n{VORLAGE}\n{VIER}\n\n", 0, [], None),
|
||||
("bloecke: Inline-Code am Zeilenanfang oeffnet nichts", "vor", KOPF + f"{Z}foo{Z} ist Inline-Code.\n\n" + STELLE_A + STELLE_B, 0, [], None),
|
||||
("bloecke: eingerueckter Block nach Aus zaehlt nicht", "vor", KOPF + f"{A}\n\nAus:\n\n {Z}text\n gibt es nicht\n {Z}\n\n" + STELLE_A, 0, [], None),
|
||||
("bloecke: eingerueckter Schlusszaun schliesst", "vor", KOPF + aus_wird(A, "zwei", "ZWEI", "\n " + Z) + STELLE_B, 0, [], None),
|
||||
# Die heile Datei daneben wird weiter geprueft; ihr Befund steht neben dem Zaunbefund
|
||||
("bloecke: zwei Taskdateien, die heile wird geprueft", "vor",
|
||||
{"task-01.md": KOPF + aus_wird(A, "zwei", "ZWEI", Z), "task-02.md": KOPF + aus_wird(A, "gibt es nicht", "x")}, 2,
|
||||
[b(KLEBT, "zwei" + Z), b("Aus-Block steht nicht", Z + "text", 1, "task-02.md")], None),
|
||||
])
|
||||
|
||||
MAT, BLO = "plan_materialisiere.py", "plan_bloecke.py"
|
||||
FEHLT_ZEILE = ' if kern.startswith("`" * laenge):'
|
||||
NEUBEGINN = ' start, laenge, spalte = i, len(kern) - len(kern.lstrip("`")), len(zeilen[i]) - len(kern)'
|
||||
SCHLUSS = ' return kern.startswith("`" * laenge) and not kern.rstrip().strip("`") and len(zeile) - len(kern) - einzug <= 3'
|
||||
# Name, Datei, Text (ganze Zeilen, genau einmal in der Datei), Ersatz, Fall, an dem der Mutant rot werden muss
|
||||
MUTANTEN = [
|
||||
("klebt: Pruefung aus", MAT, ' if zeilen[i].rstrip().endswith("`" * laenge):', " if False:", "T27-Form: Aus und wird kleben"),
|
||||
("klebt: Leerzeichen dahinter uebersehen", MAT, ' if zeilen[i].rstrip().endswith("`" * laenge):', ' if zeilen[i].endswith("`" * laenge):', "Leerzeichen hinter dem klebenden Zaun"),
|
||||
("klebt: feste Laenge drei", MAT, ' if zeilen[i].rstrip().endswith("`" * laenge):', f' if zeilen[i].rstrip().endswith("{Z}"):', "Zaunfolge am Zeilenende im Vierer-Zaun"),
|
||||
("klebt: Block endet nicht am Zaun", MAT, 'in einen laengeren Zaun"))\n break\n', 'in einen laengeren Zaun"))\n', "T27-Form: Aus und wird kleben"),
|
||||
("offen: Pruefung aus", MAT, " if i >= len(zeilen):", " if False:", "Block schliesst nie, letzte Stelle"),
|
||||
("fehlt: Pruefung aus", MAT, FEHLT_ZEILE, " if False:", "Zaun fehlt, Folgepaar"),
|
||||
("fehlt: nur laengere Zaeune", MAT, FEHLT_ZEILE, ' if kern.startswith("`" * (laenge + 1)):', "Zaun fehlt, Folgepaar"),
|
||||
("fehlt: nur gleich lange Zaeune", MAT, FEHLT_ZEILE, ' if len(kern) - len(kern.lstrip("`")) == laenge:', "Zaun fehlt, naechster Block ist laenger"),
|
||||
("fehlt: nur Zaeune mit Sprache", MAT, FEHLT_ZEILE, " if zaun(zeilen[i]) >= laenge:", "Blockzeile beginnt mit dem Zaun und endet anders"),
|
||||
("fehlt: kein neuer Block", MAT, NEUBEGINN, " pass", "Zaun fehlt, naechster Block ist laenger"),
|
||||
("fehlt: Zeile des Blocks bleibt", MAT, NEUBEGINN, ' laenge, spalte = len(kern) - len(kern.lstrip("`")), len(zeilen[i]) - len(kern)', "zwei Bloecke ohne Schluss"),
|
||||
("fehlt: Zaunlaenge bleibt", MAT, NEUBEGINN, " start, spalte = i, len(zeilen[i]) - len(kern)", "Zaun fehlt, naechster Block ist laenger"),
|
||||
("fehlt: Einzug bleibt", MAT, NEUBEGINN, ' start, laenge = i, len(kern) - len(kern.lstrip("`"))', "Zaun fehlt, naechster Block tiefer eingerueckt"),
|
||||
("main: Vorpruefung aus", MAT, " for zeile, meldung in zaunbefunde(plan / name):", " for zeile, meldung in []:", "ganze Datei klebt, Folgepaar"),
|
||||
("main: Befund ohne Abbruch", MAT, ' if befunde:\n print(f"{befunde} Befund(e).")\n return 2\n', "", "ganze Datei klebt, Folgepaar"),
|
||||
("main: nur die erste Taskdatei", MAT, " for name in sys.argv[3:]:\n for zeile, meldung", " for name in sys.argv[3:4]:\n for zeile, meldung", "zweite Taskdatei klebt"),
|
||||
("Inline-Code oeffnet einen Block", MAT, ' return laenge if laenge >= 3 and "`" not in kern[laenge:] else 0', " return laenge if laenge >= 3 else 0", "Inline-Code am Zeilenanfang oeffnet nichts"),
|
||||
("Zaun schon ab zwei Backticks", MAT, ' return laenge if laenge >= 3 and "`" not in kern[laenge:] else 0', ' return laenge if laenge >= 2 and "`" not in kern[laenge:] else 0', "Prosa beginnt mit zwei Backticks"),
|
||||
("Zaun erst ab vier Backticks", MAT, ' return laenge if laenge >= 3 and "`" not in kern[laenge:] else 0', ' return laenge if laenge >= 4 and "`" not in kern[laenge:] else 0', "Grundlinie"),
|
||||
("Schlusszaun nur in Spalte 0", MAT, ' kern = zeile.lstrip()\n return kern.startswith("`" * laenge)', ' kern = zeile\n return kern.startswith("`" * laenge)', "Eingerueckter Befehl in einer Liste"),
|
||||
("Schlusszaun mit Text dahinter", MAT, SCHLUSS, ' return kern.startswith("`" * laenge) and len(zeile) - len(kern) - einzug <= 3', "Zaun fehlt, Folgepaar"),
|
||||
("Schlusszaun: Leerzeichen dahinter schliessen nicht", MAT, SCHLUSS, ' return kern.startswith("`" * laenge) and not kern.strip("`") and len(zeile) - len(kern) - einzug <= 3', "Schlusszaun mit Leerzeichen dahinter"),
|
||||
("Schlusszaun: zwei Zaeune auf einer Zeile", MAT, SCHLUSS, ' return kern.startswith("`" * laenge) and not kern.strip("`").strip() and len(zeile) - len(kern) - einzug <= 3', "zwei Zaeune auf einer Zeile schliessen nicht"),
|
||||
("Schlusszaun: vier Spalten tiefer schliesst", MAT, SCHLUSS, ' return kern.startswith("`" * laenge) and not kern.rstrip().strip("`") and len(zeile) - len(kern) - einzug <= 4', "Zaun vier Spalten tiefer ist Inhalt, kein Schluss"),
|
||||
("Schlusszaun: drei Spalten tiefer schliesst nicht", MAT, SCHLUSS, ' return kern.startswith("`" * laenge) and not kern.rstrip().strip("`") and len(zeile) - len(kern) - einzug <= 2', "Zaun drei Spalten tiefer schliesst"),
|
||||
("Schlusszaun: Einzug des Oeffners zaehlt nicht", MAT, SCHLUSS, ' return kern.startswith("`" * laenge) and not kern.rstrip().strip("`") and len(zeile) - len(kern) <= 3', "eingerueckter Block: Schlusszaun drei Spalten tiefer"),
|
||||
("Suche: Einzug des Blocks fehlt", MAT, " spalte = len(zeilen[start]) - len(zeilen[start].lstrip())", " spalte = 0", "eingerueckter Block: Schlusszaun drei Spalten tiefer"),
|
||||
("bloecke(): Einzug des Blocks fehlt", MAT, " while i < len(zeilen) and not schliesst(zeilen[i], laenge, einzug):\n inhalt.append(zeilen[i][einzug:]", " while i < len(zeilen) and not schliesst(zeilen[i], laenge, 0):\n inhalt.append(zeilen[i][einzug:]", "eingerueckter Block: Schlusszaun drei Spalten tiefer"),
|
||||
("lesen: UTF-8-Fehler nicht abgefangen", MAT, " except (OSError, UnicodeDecodeError) as fehler:", " except OSError as fehler:", "Taskdatei ist kein UTF-8"),
|
||||
("lesen: fehlende Datei nicht abgefangen", MAT, " except (OSError, UnicodeDecodeError) as fehler:", " except UnicodeDecodeError as fehler:", "Taskdatei fehlt"),
|
||||
("bloecke(): eigene Suche nach dem Oeffner", MAT, " laenge = zaun(zeile)\n if laenge:\n einzug", ' laenge = len(zeile.lstrip()) - len(zeile.lstrip().lstrip("`"))\n if laenge >= 3:\n einzug', "Inline-Code am Zeilenanfang oeffnet nichts"),
|
||||
("plan_bloecke: Pruefung aus", BLO, " kaputt = zaunbefunde(taskdatei)", " kaputt = []", "bloecke vor: T27-Form: Aus und wird kleben"),
|
||||
("plan_bloecke: Summe zaehlt je Datei", BLO, " befunde += len(kaputt)", " befunde += 1", "bloecke vor: T27-Form: Aus und wird kleben"),
|
||||
("plan_bloecke: Einzug des Blocks fehlt", BLO, " while i < len(zeilen) and not schliesst(zeilen[i], laenge, einzug):", " while i < len(zeilen) and not schliesst(zeilen[i], laenge, 0):", "bloecke: eingerueckter Block, Schlusszaun drei Spalten tiefer"),
|
||||
("plan_bloecke: prueft trotz Befund", BLO, " zaunfehler = True\n continue\n", " zaunfehler = True\n", "bloecke vor: T27-Form: Aus und wird kleben"),
|
||||
("plan_bloecke: Exit 3 trotz Zaunbefund", BLO, " if geprueft == 0 and not zaunfehler:", " if geprueft == 0:", "bloecke vor: T27-Form: Aus und wird kleben"),
|
||||
("plan_bloecke: eingerueckter Block zaehlt", BLO, ' if marke in ("Aus:", "wird:") and zeile.startswith("`"):', ' if marke in ("Aus:", "wird:"):', "bloecke: eingerueckter Block nach Aus zaehlt nicht"),
|
||||
("plan_bloecke: eigene Suche nach dem Oeffner", BLO, " laenge = zaun(zeile)\n if laenge:\n start = i", f' laenge = len(zeile) - len(zeile.lstrip("`"))\n if zeile.startswith("{Z}"):\n start = i', "bloecke: Inline-Code am Zeilenanfang oeffnet nichts"),
|
||||
("plan_bloecke: Schlusszaun nur in Spalte 0", BLO, " while i < len(zeilen) and not schliesst(zeilen[i], laenge, einzug):", ' while i < len(zeilen) and not (zeilen[i].startswith("`" * laenge) and not zeilen[i].strip("`").strip()):', "bloecke: eingerueckter Schlusszaun schliesst"),
|
||||
]
|
||||
|
||||
|
||||
def schreibe(pfad: Path, text) -> None:
|
||||
pfad.parent.mkdir(parents=True, exist_ok=True)
|
||||
pfad.write_bytes(text if isinstance(text, bytes) else text.encode("utf-8"))
|
||||
|
||||
|
||||
def zeile_von(text, wortlaut: str, welche: int = 1) -> int:
|
||||
"""Zeilennummer des welche-ten Vorkommens von wortlaut als ganze Zeile; -1 zaehlt von hinten, wie bei Listen."""
|
||||
zeilen = text.replace("\r\n", "\n").split("\n")
|
||||
treffer = [nr for nr, zeile in enumerate(zeilen, start=1) if zeile == wortlaut]
|
||||
return treffer[welche - 1 if welche > 0 else welche]
|
||||
|
||||
|
||||
def fahre(werkzeuge: Path, art: str, wurzel: Path, plan: Path, namen: list[str]) -> subprocess.CompletedProcess:
|
||||
if art == "mat":
|
||||
befehl_ = [str(werkzeuge / "plan_materialisiere.py"), str(plan), str(wurzel / "kopie"), *namen]
|
||||
else:
|
||||
befehl_ = [str(werkzeuge / "plan_bloecke.py"), art, *[str(plan / n) for n in namen], "--projekt", str(wurzel / "kopie")]
|
||||
return subprocess.run([sys.executable, "-X", "utf8", "-B", *befehl_], capture_output=True, text=True,
|
||||
encoding="utf-8", errors="replace", stdin=subprocess.DEVNULL)
|
||||
|
||||
|
||||
def alle_dateien(ordner: Path) -> dict[str, bytes]:
|
||||
return {p.relative_to(ordner).as_posix(): p.read_bytes() for p in ordner.rglob("*") if p.is_file()}
|
||||
|
||||
|
||||
def fall(werkzeuge: Path, art: str, task, exit_erwartet: int, erwartet: list, dateien: dict | None) -> tuple[list[str], str]:
|
||||
"""Faehrt einen Fall in einem frischen Wegwerfordner; Rueckgabe (Gruende fuer Rot, Ausgabe)."""
|
||||
wurzel = Path(tempfile.mkdtemp(prefix="pz_"))
|
||||
try:
|
||||
for rel, text in BASIS.items():
|
||||
schreibe(wurzel / "kopie" / rel, text)
|
||||
tasks = task if isinstance(task, dict) else {"task-01.md": task}
|
||||
for name, text in tasks.items():
|
||||
if text is not None:
|
||||
schreibe(wurzel / "plan" / name, text)
|
||||
lauf = fahre(werkzeuge, art, wurzel, wurzel / "plan", list(tasks))
|
||||
ausgabe = lauf.stdout + lauf.stderr
|
||||
gruende = []
|
||||
if lauf.returncode != exit_erwartet:
|
||||
gruende.append(f"Exit {lauf.returncode}, erwartet {exit_erwartet}")
|
||||
if "Traceback" in ausgabe:
|
||||
gruende.append("Traceback in der Ausgabe")
|
||||
for wort, wortlaut, welche, datei in erwartet:
|
||||
kopf = f"BEFUND {datei}:"
|
||||
if wortlaut is not None:
|
||||
kopf += f"{zeile_von(tasks[datei], wortlaut, welche)}:"
|
||||
if not any(z.startswith(kopf) and wort in z for z in ausgabe.split("\n")):
|
||||
gruende.append(f"Meldung fehlt: {kopf} ... {wort}")
|
||||
if ausgabe.count("BEFUND ") != len(erwartet):
|
||||
gruende.append(f"{ausgabe.count('BEFUND ')} BEFUND-Zeile(n), erwartet {len(erwartet)}")
|
||||
if f"{len(erwartet)} Befund(e)." not in ausgabe.split("\n"):
|
||||
gruende.append(f"Summenzeile fehlt: {len(erwartet)} Befund(e).")
|
||||
if dateien is not None:
|
||||
soll = {rel: text.encode("utf-8") for rel, text in {**BASIS, **dateien}.items() if text is not None}
|
||||
ist = alle_dateien(wurzel / "kopie")
|
||||
for rel in sorted(set(soll) | set(ist)):
|
||||
if soll.get(rel) != ist.get(rel):
|
||||
gruende.append(f"{rel}: {ist.get(rel)!r}, erwartet {soll.get(rel)!r}")
|
||||
return gruende, ausgabe
|
||||
finally:
|
||||
shutil.rmtree(wurzel, ignore_errors=True)
|
||||
|
||||
|
||||
def alle_faelle(werkzeuge: Path, laut: bool) -> list[str]:
|
||||
"""Faehrt alle Faelle, Grundlinie zuerst; Rueckgabe: Namen der roten Faelle."""
|
||||
rot = []
|
||||
for name, art, task, exit_erwartet, erwartet, dateien in FAELLE:
|
||||
gruende, ausgabe = fall(werkzeuge, art, task, exit_erwartet, erwartet, dateien)
|
||||
if laut:
|
||||
print(f"{'ok ' if not gruende else 'FEHLER'} {name}")
|
||||
if gruende:
|
||||
rot.append(name)
|
||||
if laut:
|
||||
for grund in gruende:
|
||||
print(" " + grund[:300])
|
||||
print(" " + ausgabe.strip().replace("\n", "\n ")[:900])
|
||||
if name == "Grundlinie":
|
||||
if laut:
|
||||
print("Die Grundlinie ist nicht sauber -- der Rest misst nichts.")
|
||||
return rot
|
||||
return rot
|
||||
|
||||
|
||||
def mutanten_lauf(werkzeuge: Path) -> int:
|
||||
"""Baut je Mutant einen Fehler in eine Kopie der Werkzeuge ein; Rueckgabe: Mutanten ohne Rot an ihrem Fall."""
|
||||
fehler = 0
|
||||
for name, datei, alt, neu, soll in MUTANTEN:
|
||||
ordner = Path(tempfile.mkdtemp(prefix="pm_"))
|
||||
try:
|
||||
shutil.copy(werkzeuge / MAT, ordner / MAT)
|
||||
shutil.copy(werkzeuge / BLO, ordner / BLO)
|
||||
text = (ordner / datei).read_text(encoding="utf-8")
|
||||
if text.count(alt) != 1:
|
||||
print(f"FEHLER {name}: der Text steht {text.count(alt)}-mal in {datei}")
|
||||
fehler += 1
|
||||
continue
|
||||
(ordner / datei).write_text(text.replace(alt, neu), encoding="utf-8", newline="\n")
|
||||
rot = alle_faelle(ordner, False)
|
||||
print(f"{'gefangen ' if soll in rot else 'UEBERLEBT'} {name}: {len(rot)} rot, erwartet an: {soll}")
|
||||
fehler += 0 if soll in rot else 1
|
||||
finally:
|
||||
shutil.rmtree(ordner, ignore_errors=True)
|
||||
print(f"{fehler} Mutant(en) nicht an ihrem Fall gefangen, von {len(MUTANTEN)}.")
|
||||
return fehler
|
||||
|
||||
|
||||
def main() -> int:
|
||||
zerleger = argparse.ArgumentParser()
|
||||
zerleger.add_argument("--werkzeuge", default=str(Path(__file__).resolve().parent))
|
||||
zerleger.add_argument("--mutanten", action="store_true")
|
||||
argumente = zerleger.parse_args()
|
||||
werkzeuge = Path(argumente.werkzeuge)
|
||||
rot = alle_faelle(werkzeuge, True)
|
||||
print(f"{len(rot)} Fall/Faelle mit falschem Ausgang von {len(FAELLE)}.")
|
||||
if rot:
|
||||
return 2
|
||||
return 2 if argumente.mutanten and mutanten_lauf(werkzeuge) else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -5,7 +5,7 @@ title: "Workflow-Prüfung: Attrappe, Fälle und Messreihe zum Verbrauch"
|
||||
summary: "Node-Attrappe für die gespeicherten Workflows des Vaults mit Fällen je Workflow, dazu verbrauch_reihe.py, das aus den Workflow-Protokollen rechnet, wie viel vom 5-Stunden-Fenster und von der Woche zwischen zwei Verbrauchsablesungen verbraucht wurde. Entstanden 2026-09-24."
|
||||
tags: [vault-tools, codeworkflow]
|
||||
created: 2026-09-24
|
||||
updated: 2026-10-30
|
||||
updated: 2026-11-02
|
||||
---
|
||||
|
||||
# Workflow-Prüfung
|
||||
@@ -43,8 +43,11 @@ Stand 2026-09-24 nach Plankritik, Prüfung und Nachbesserung. Die Zahlen gelten
|
||||
- **Ableser als Agententyp** (2026-10-17, Task 02 desselben Plans): Der Ableser läuft als `agentType: verbrauch-ableser` (Haiku, `effort: low`, nur ToolSearch und `get_usage`). Ein Startfehler, etwa ein in der Sitzung noch unbekannter Typ, kommt als `nicht_gelesen` mit `WARNUNG` zurück, nie als Rückfall auf einen Agenten mit allen Werkzeugen. Echtläufe: Vor der Registrierung warf `agent()` „agent type 'verbrauch-ableser' not found“ ohne Agent (`wf_63cb007d-71b`); danach lasen vier von vier Ablesern richtig, mit Effort `low` und nur ToolSearch, `get_usage` und StructuredOutput (`wf_1901f51f-dd3`, `wf_4d8a69f9-51a`). Die Sitzung kannte die neue Agentendatei nach etwa zehn Minuten. 261 Fälle grün, drei neue gegen den alten Stand rot; 157 Mutanten, alle gefangen oder als wirkungslos geführt. **Ungemessen:** ein Ableser, der eine abweichende Nutzeranfrage sieht. Alle vier Echtläufe starteten in Zügen ohne Nutzernachricht, der Harness reichte keine Anfrage durch.
|
||||
- **Eichung der Stufen** (2026-10-30, Workflow-Review S-11): `verbrauch_reihe.py` über den ganzen Speicher ergab 1.245 Ablesungen und 647 Paare. Getrennt wurde mit einem Wegwerfskript, das die Funktionen von `verbrauch_reihe.py` importiert: Plan aus dem Feld `plan` der Ablesung, Konto aus Wochentag und Stunde des Wochen-`resetsAt`, auf die volle Stunde gerundet, weil der Wert über die Stundengrenze zittert. Zwei Konten: Reset montags 00:00 UTC (Pro bis 2026-09-30, Max ab 2026-10-05) und mittwochs 20:00 UTC (Max ab 2026-10-09). 340 Ablesungen ohne Plan waren gescheiterte oder abgewichene Ablesungen. Max: 9,2 bzw. 10,1 % des 5-Stunden-Fensters und 2,5 bzw. 2,6 % der Woche je Opus-Agentenstunde (187 und 205 Paare, 107 und 125 Opus-Stunden), Median je Paar ab 15 Opus-Minuten 8,6 bzw. 9,0, P90 rund 19. Pro: 14 Paare, zu wenig. Anstieg des Fensters je Paar nach gleichzeitig laufenden Opus-Agenten: einer Median 3, P90 13, Spitze 33 Punkte (211 Paare); zwei 4, 14, 42 (134); drei 5, 9, 18 (45); vier nur 2 Paare. Pascal hat die Stufen daraufhin unverändert bestätigt und Stufe 3 freigegeben. **Ungemessen:** Max zeigt je Opus-Stunde mehr Prozent als Pro; als reiner Planeffekt ist das unplausibel, wahrscheinlich liefen im Max-Zeitraum mehr Sitzungen parallel. Die Kontentrennung kann `verbrauch_reihe.py` selbst nicht.
|
||||
- **`planung` mit `effort`** (2026-10-30, für die Messung A2): Das Argument wird geprüft (`low` bis `max`), an jeden Planer neben `agentType` weitergereicht und in der Rückgabe genannt, ohne Argument `(Frontmatter)`. Drei neue Fälle, alle drei gegen den alten Stand rot; drei neue Mutanten, einzeln gefahren und alle gefangen. Der volle Mutantenlauf ist dafür nicht gelaufen.
|
||||
- **Exit 2 der Planwerkzeuge als Befund** (2026-11-02, Auftrag der Startsitzung der Zaunprüfung, Pascal: „Ja, nachziehen“): `welle-mit-pruefung` hält vor `executer-welle`, wenn `plan_materialisiere.py` beim Bau der Kopie eine Zeile `BEFUND <taskdatei>:<zeile>:` meldet (`status: plan_befund`, die Zeilen in `plan_befunde`). `plankritik` zählt ein Planwerkzeug mit Exit 2 und Befundzeile als gelaufen, `plan_bloecke` auch mit Exit 3 und „Nichts geprueft:“. Die Ausgabe geht dann an die Linse `bezeichner`, die Zeilen stehen unter `werkzeug_befunde`. Exit 2 ohne Befundzeile nennt das Log mit Werkzeug und Exit statt „Hilfsagent ohne Ergebnis“. Beide lesen dafür alle `laeufe` des Hilfsagenten: Im Echtlauf `wf_9ab7fd8e-ad1` gab `werkzeug:planwerkzeuge` je Werkzeug einen Lauf zurück, und `laeufe[0]` allein enthielt `plan_bloecke` nicht. 24 neue Fälle, gegen den alten Stand 19 rot; die 5 Gegenproben für den alten Weg sind auf beiden Ständen grün. 292 Fälle grün. 22 neue Mutanten, je an ihrem Pflichtfall gefangen; alle 188 Mutanten, parallel über ein Wegwerfskript: 187 gefangen, 1 wirkungslos wie bisher. Echtläufe auf einem Wegwerfprojekt mit klebendem Zaun: `wf_63d99146-e89` hält mit `plan_befund`, ein Agent, kein Executer; die Mutante ohne Halt (`wf_2b979130-800`) endet mit `bis_vergleich`, ihr Executer schrieb den kaputten Plan ins Projekt; `plankritik` (`wf_f1867091-dda`) führt beide Befundzeilen unter `werkzeug_befunde`, und die Linse bekam die Werkzeugausgabe. **Ungemessen:** ein Hilfsagent, der Befundzeilen umformuliert oder weglässt; dann hält nichts.
|
||||
- **Alle `laeufe` an fünf weiteren Stellen** (2026-11-02, Folgeauftrag der Zaunprüfung): Der Erfolgspfad der Kopie in `welle-mit-pruefung` und in `kopieAusLauf()` von `plankritik`, der Vergleich für den `pruefer`, der Mutantenlauf in `plankritik` und `ausgabe()` in `quorum` lesen die Ausgabe aller `laeufe`, in ihrer Reihenfolge und mit Zeilenumbruch verbunden. Verwendbar ist eine Kopie nur, wenn jeder Lauf Exit 0 hat: Ihr Befehl ist eine Kette mit `&&`. Mit `laeufe[0]` allein galt ein erster Lauf mit 0 vor einem Absturz im zweiten als fertige Kopie, und im Quorum lief ein gestarteter Codex-Auftrag ungesehen weiter, während Haiku einsprang. `KEINE_MUTANTEN_TOML` zählt nur noch als ganze Zeile; sonst machte ein zusätzlicher Lauf mit dem Skripttext aus einem gefahrenen Mutantenlauf `keine_toml` (Befund der Gegenprüfung durch den `pruefer`). Die Antworten in `faelle_workflows.mjs` nehmen jetzt auch für `werkzeug:vergleich`, `werkzeug:mutanten` und die Quorum-Labels Listen von Läufen. 18 neue Fälle, gegen den alten Stand 16 rot; die 2 übrigen sichern Code, den erst dieser Auftrag braucht, und sind dort grün. 310 Fälle grün. 16 neue Mutanten und 3 alte mit angepasstem Muster, je an ihrem Pflichtfall gefangen; alle 204 Mutanten, parallel über ein Wegwerfskript: 203 gefangen, 1 wirkungslos wie bisher. **Ungemessen:** ein Echtlauf, in dem ein Hilfsagent an einer dieser Stellen seine Ausgabe verteilt; `laeufe` in anderer Reihenfolge als ausgeführt. An der Reihenfolge hängt bei einem langen Vergleich das Ergebnis der Suite für den `pruefer`; die jobId-Suche in `quorum.js` hängt seit dem Folgeauftrag zum Codex-Start nicht mehr daran.
|
||||
- **Codex-Start unabhängig von Reihenfolge und Form der `laeufe`** (2026-11-02, Folgeauftrag, Pascal: „Ja, hier bauen“; geplant mit Codex Sol `gpt-6.1-sol`, in vier Runden kritisiert von Codex Astra `gpt-6-astra`): `codexStimme()` in `quorum.js` nimmt die jobId nur noch am Objektanfang der Task-Antwort, `{` gefolgt von `"jobId": "<id>"`, Komma und `"status": "queued"` bei beliebigem Leerraum, und nur innerhalb eines Laufs. Verfolgt wird ein Auftrag nur bei genau einer verschiedenen ID. Die alte Regel nahm die erste jobId hinter dem letzten `EXIT status:`. In 42 echten Rückgaben von `codex:start` aus Workflow-Transkripten (2026-09-30 bis 2026-10-31) wählte sie einmal einen früheren Job, weil der Hilfsagent `EXIT status: 0` vor das Status-JSON gestellt hatte, und fand zweimal keine jobId, weil er die Task-Antwort kompakt neu serialisiert hatte. Zwei weitere Rückgaben verteilten die Ausgabe auf zwei `laeufe`, je Befehl einen; der Code vor dem Folgeauftrag „alle laeufe“ las nur den ersten. In allen fünf Läufen sprang Haiku ein, und der Codex-Auftrag lief ungesehen. Über den echten Code und die Attrappe gefahren, verfolgt der neue Stand in allen 42 die richtige ID. Kommt die Ausgabe nicht in Befehlsreihenfolge, steht ein Hinweis im Bericht; fehlt trotz `EXIT task: 0` eine sichere ID, warnt der Grund vor einem womöglich ungesehen weiterlaufenden Auftrag. Abgesichert ist das Bedrohungsmodell M1 bis M4: Verteilung und beliebige Reihenfolge der `laeufe`, Leerraum und kompakte Form, Kürzung nach der Vorgabe im Hilfsagentenauftrag, verschobene EXIT-Zeilen mit kurzen Vorsätzen. Frei umgestellte Zeilen innerhalb eines Laufs oder erfundene Inhalte fängt nur der Lesebeleg in `codexUngueltig()`. 31 neue Fälle, gegen den alten Stand 27 rot; die 4 übrigen sichern die Umrechnung der Fundstellen und zwei Gründe ohne Warnung. 341 Fälle grün. 25 neue oder angepasste Mutanten an ihrem Pflichtfall gefangen; alle 227 Mutanten, parallel über ein Wegwerfskript: 226 gefangen, 1 wirkungslos wie bisher. Die Breitensuche des `pruefer` über die echte Funktion fand in 1.944.937 erzeugten Lagen keine alte ID, gegen den alten Stand 537.307. **Ungemessen:** ein Echtlauf mit vertauschten `laeufe`; unter den 42 war keiner.
|
||||
|
||||
**Offen:** Die Trading-Sitzung hat `plankritik.js` am 2026-09-24 nach einem echten Lauf nachgebessert (Ausgabe der Planwerkzeuge nur bei `EXIT 0`). Für diese Stelle gibt es noch keinen Fall.
|
||||
**Offen:** Für `ausgabe()` im Quorum liegen echte Rückgaben mit zwei `laeufe` vor (2026-10-13, 2026-10-22); für die übrigen vier Stellen ist nur die Attrappe belegt, ein Echtlauf mit verteilten `laeufe` steht aus. Nicht gebaut: `kopieAusLauf()` in `plankritik.js` nennt den Temp-Ordner nicht, wenn die Kopie an einem Exit ungleich 0 scheitert. Am Codex-Start lehnt `quorum.js` mit Warnung ab, wenn die Task-Antwort zwischen `{` und `"jobId"` oder zwischen `"jobId"` und `"status"` auf zwei `laeufe` geteilt ist; ohne EXIT-Zeile am Zeilenanfang kommt der Reihenfolgehinweis auch bei richtiger Reihenfolge. Statuseinträge, die eine ältere Pluginversion in `state.json` geschrieben hat, sind nicht an Quellen geprüft; in den 42 Echtläufen trug keiner das Merkmal. `codex:warten` erkennt das Ende nur an `"waitTimedOut": false` mit Leerzeichen; so stand es in allen 54 echten Rückgaben, ob ein Hilfsagent mit kompaktem Start auch die Warte-Ausgabe kompakt liefert, ist ungemessen. Den Inhalt eines Hilfsagenten liest kein Workflow mehr nur aus `laeufe[0]`; `plankritik.js` fragt `laeufe[0]` bei den Planwerkzeugen nur ab, um „Hilfsagent ohne Ergebnis“ zu erkennen. Die Stelle „Ausgabe der Planwerkzeuge nur bei `EXIT 0`“ (Trading-Sitzung, 2026-09-24) hat seit 2026-11-02 Fälle.
|
||||
|
||||
## Was die Skripte nicht sehen
|
||||
|
||||
|
||||
@@ -33,11 +33,23 @@ function antworten(k = {}) {
|
||||
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:planwerkzeuge') {
|
||||
if (k.planwerkzeuge === OHNE) return undefined
|
||||
if (Array.isArray(k.planwerkzeuge)) return ok({ laeufe: k.planwerkzeuge })
|
||||
return ok({ laeufe: [{ befehl: 'planwerkzeuge', exit: 0, ausgabe: k.planwerkzeuge !== undefined ? k.planwerkzeuge : 'keine Befunde' }] })
|
||||
}
|
||||
if (label === 'werkzeug:kopie') {
|
||||
if (k.kopie === OHNE) return undefined
|
||||
if (Array.isArray(k.kopie)) return ok({ laeufe: k.kopie })
|
||||
return ok({ laeufe: [k.kopie || { befehl: 'kopie', exit: 0, ausgabe: "KOPIE=C:/tmp/k/mat\n{'neu': 2}" }] })
|
||||
}
|
||||
if (label === 'werkzeug:vergleich') {
|
||||
if (Array.isArray(k.vergleich)) return ok({ laeufe: k.vergleich })
|
||||
return ok({ laeufe: [{ befehl: 'vergleich', exit: 0, ausgabe: 'VERGLEICH-OK\nEXIT suite: 0' }] })
|
||||
}
|
||||
if (label === 'werkzeug:mutanten') {
|
||||
if (k.mutanten === OHNE) return undefined
|
||||
if (Array.isArray(k.mutanten)) return ok({ laeufe: k.mutanten })
|
||||
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 }] })
|
||||
}
|
||||
@@ -766,6 +778,267 @@ fall('plankritik', 'mutanten_status und mutanten_modus in allen Zuständen', asy
|
||||
return fehler
|
||||
})
|
||||
|
||||
// Planwerkzeuge mit Befund (2026-11-02, Auftrag der Startsitzung, Pascal: "Ja, nachziehen"): Ein
|
||||
// Planwerkzeug mit Exit 2 und mindestens einer Zeile "BEFUND <datei>:<zeile>:" ist gelaufen. Seine
|
||||
// Rohausgabe geht an die Linse bezeichner, die Zeilen nach werkzeug_befunde. Exit 2 ohne Befundzeile ist
|
||||
// ein gescheitertes Werkzeug; "Hilfsagent ohne Ergebnis" heißt nur noch: kein Lauf.
|
||||
const PK_TEXT = { projekt: 'D:/x', planordner: 'D:/x/plan' }
|
||||
const Z_OK = ['0 Befund(e).', 'EXIT plan_zeichen: 0']
|
||||
const B_OK = ['task-01.md: 2 Aus-/wird-Bloecke, im Modus vor geprueft: 1', '0 Befund(e).', 'EXIT plan_bloecke: 0']
|
||||
const B_ZAUN = 'BEFUND task-01.md:9: Zaun klebt an der Codezeile; Bloecke dieser Taskdatei nicht geprueft'
|
||||
const Z_BEFUND = 'BEFUND task-02.md:4:12: U+200B ZERO WIDTH SPACE (Cf) in: x'
|
||||
const ERRNO = "python: can't open file 'D:/x/werkzeuge/plan_bloecke.py': [Errno 2] No such file or directory"
|
||||
const werkzeuge = (...zeilen) => zeilen.flat().join('\n')
|
||||
const bezeichner = p => (mitLabel(p, 'plankritik:bezeichner')[0] || {}).prompt || ''
|
||||
const mitAusgabe = (p, text) => { const a = bezeichner(p); const i = a.indexOf('<werkzeugausgabe>'); return i >= 0 && a.indexOf(text, i) > i }
|
||||
const hilfsagentLog = p => p.logs.filter(l => /Hilfsagent ohne Ergebnis/.test(l))
|
||||
const kopieLauf = (exit, ...zeilen) => ({ befehl: 'kopie', exit, ausgabe: ['KOPIE=C:/tmp/k/mat', '12 Dateien kopiert, 1 Pfade ausgeschlossen, 0 Verknuepfungen uebersprungen, 0 Pakete in der Kopie.', '0 Befund(e).', ...zeilen].join('\n') })
|
||||
|
||||
fall('plankritik', 'Planwerkzeuge mit Exit 0: Rohausgabe geht an die Linse bezeichner, werkzeug_befunde leer', async () => {
|
||||
const { fehler, soll } = pruefe()
|
||||
const { ergebnis: e, fehler: f, protokoll: p } = await PK(PK_TEXT, { planwerkzeuge: werkzeuge(Z_OK, B_OK) })
|
||||
soll(f === null, 'Fehler: ' + f)
|
||||
soll(mitAusgabe(p, 'EXIT plan_bloecke: 0'), 'Rohausgabe fehlt im Auftrag der Linse bezeichner')
|
||||
soll(e && Array.isArray(e.werkzeug_befunde) && e.werkzeug_befunde.length === 0, 'werkzeug_befunde ' + JSON.stringify(e && e.werkzeug_befunde))
|
||||
soll(!p.logs.some(l => l.startsWith('Planwerkzeuge:')), 'Log: ' + p.logs.join(' | '))
|
||||
return fehler
|
||||
})
|
||||
|
||||
fall('plankritik', 'plan_bloecke Exit 2 mit Zaunbefund: gelaufen, Rohausgabe an die Linse, Zeile unter werkzeug_befunde', async () => {
|
||||
const { fehler, soll } = pruefe()
|
||||
const { ergebnis: e, fehler: f, protokoll: p } = await PK(PK_TEXT, { planwerkzeuge: werkzeuge(Z_OK, B_ZAUN, '1 Befund(e).', 'EXIT plan_bloecke: 2') })
|
||||
soll(f === null, 'Fehler: ' + f)
|
||||
soll(mitAusgabe(p, B_ZAUN), 'Befundzeile fehlt im Auftrag der Linse bezeichner')
|
||||
soll(e && JSON.stringify(e.werkzeug_befunde) === JSON.stringify([{ werkzeug: 'plan_bloecke', befund: B_ZAUN }]), 'werkzeug_befunde ' + JSON.stringify(e && e.werkzeug_befunde))
|
||||
soll(!p.logs.some(l => l.startsWith('Planwerkzeuge:')), 'Log: ' + p.logs.join(' | '))
|
||||
return fehler
|
||||
})
|
||||
|
||||
fall('plankritik', 'plan_zeichen Exit 2 mit Befund (erstes Werkzeug): gelaufen und plan_zeichen zugeordnet', async () => {
|
||||
const { fehler, soll } = pruefe()
|
||||
const { ergebnis: e, protokoll: p } = await PK(PK_TEXT, { planwerkzeuge: werkzeuge(Z_BEFUND, '1 Befund(e).', 'EXIT plan_zeichen: 2', B_OK) })
|
||||
soll(mitAusgabe(p, Z_BEFUND), 'Befundzeile fehlt im Auftrag der Linse bezeichner')
|
||||
soll(e && JSON.stringify(e.werkzeug_befunde) === JSON.stringify([{ werkzeug: 'plan_zeichen', befund: Z_BEFUND }]), 'werkzeug_befunde ' + JSON.stringify(e && e.werkzeug_befunde))
|
||||
return fehler
|
||||
})
|
||||
|
||||
fall('plankritik', 'Befunde beider Werkzeuge mit CRLF: je Werkzeug zugeordnet, ohne CR', async () => {
|
||||
const { fehler, soll } = pruefe()
|
||||
const aus = [Z_BEFUND, '1 Befund(e).', 'EXIT plan_zeichen: 2', B_ZAUN, '1 Befund(e).', 'EXIT plan_bloecke: 2'].join('\r\n')
|
||||
const { ergebnis: e, protokoll: p } = await PK(PK_TEXT, { planwerkzeuge: aus })
|
||||
soll(e && JSON.stringify(e.werkzeug_befunde) === JSON.stringify([{ werkzeug: 'plan_zeichen', befund: Z_BEFUND }, { werkzeug: 'plan_bloecke', befund: B_ZAUN }]), 'werkzeug_befunde ' + JSON.stringify(e && e.werkzeug_befunde))
|
||||
soll(mitAusgabe(p, B_ZAUN), 'Rohausgabe fehlt im Auftrag der Linse bezeichner')
|
||||
return fehler
|
||||
})
|
||||
|
||||
for (const [name, aus, werkzeug] of [
|
||||
['plan_bloecke Exit 2 ohne Befundzeile (Errno 2)', werkzeuge(Z_OK, ERRNO, 'EXIT plan_bloecke: 2'), 'plan_bloecke Exit 2 ohne Befundzeile'],
|
||||
['plan_zeichen Exit 2 ohne Befundzeile (erstes Werkzeug)', werkzeuge(ERRNO.replace('plan_bloecke', 'plan_zeichen'), 'EXIT plan_zeichen: 2', B_OK), 'plan_zeichen Exit 2 ohne Befundzeile'],
|
||||
['plan_bloecke Exit 3 ohne "Nichts geprueft"', werkzeuge(Z_OK, 'EXIT plan_bloecke: 3'), 'plan_bloecke Exit 3'],
|
||||
['plan_bloecke ohne EXIT-Zeile', werkzeuge(Z_OK, B_ZAUN), 'plan_bloecke ohne EXIT-Zeile'],
|
||||
]) {
|
||||
fall('plankritik', `${name}: gescheitert, Linse fährt selbst, Log nennt das Werkzeug statt des Hilfsagenten`, async () => {
|
||||
const { fehler, soll } = pruefe()
|
||||
const { fehler: f, protokoll: p } = await PK(PK_TEXT, { planwerkzeuge: aus })
|
||||
soll(f === null, 'Fehler: ' + f)
|
||||
soll(!/<werkzeugausgabe>/.test(bezeichner(p)) && /Fahre dazu plan_bloecke\.py vor/.test(bezeichner(p)), 'Linse bekommt die Ausgabe eines gescheiterten Werkzeugs')
|
||||
soll(p.logs.some(l => l.startsWith('Planwerkzeuge: ') && l.includes(werkzeug)), 'Log nennt nicht ' + werkzeug + ': ' + p.logs.join(' | '))
|
||||
soll(hilfsagentLog(p).length === 0, 'Hilfsagent-Meldung trotz Lauf: ' + hilfsagentLog(p).join(' | '))
|
||||
return fehler
|
||||
})
|
||||
}
|
||||
|
||||
fall('plankritik', 'plan_bloecke Exit 3 mit "Nichts geprueft": gelaufen, Rohausgabe an die Linse', async () => {
|
||||
const { fehler, soll } = pruefe()
|
||||
const { protokoll: p } = await PK(PK_TEXT, { planwerkzeuge: werkzeuge(Z_OK, 'task-01.md: 0 Aus-/wird-Bloecke, im Modus vor geprueft: 0', 'Nichts geprueft: kein Aus-/wird-Block gefunden.', 'EXIT plan_bloecke: 3') })
|
||||
soll(mitAusgabe(p, 'Nichts geprueft'), 'Rohausgabe fehlt im Auftrag der Linse bezeichner')
|
||||
soll(!p.logs.some(l => l.startsWith('Planwerkzeuge:')), 'Log: ' + p.logs.join(' | '))
|
||||
return fehler
|
||||
})
|
||||
|
||||
fall('plankritik', 'Hilfsagent ohne Lauf: nur dann "Hilfsagent ohne Ergebnis"', async () => {
|
||||
const { fehler, soll } = pruefe()
|
||||
const { protokoll: p } = await PK(PK_TEXT, { planwerkzeuge: OHNE })
|
||||
soll(hilfsagentLog(p).some(l => l.startsWith('Planwerkzeuge: Hilfsagent ohne Ergebnis')), 'Log: ' + p.logs.join(' | '))
|
||||
soll(!/<werkzeugausgabe>/.test(bezeichner(p)), 'Linse bekommt eine Ausgabe ohne Lauf')
|
||||
return fehler
|
||||
})
|
||||
|
||||
const M_ARGS = { projekt: 'D:/x', planordner: 'D:/x/plan', materialisieren: { ohne: ['config/secrets.toml'] } }
|
||||
const MAT_BEFUND = 'BEFUND task-01.md:7: Zaun klebt an der Codezeile; nichts geschrieben'
|
||||
|
||||
fall('plankritik', 'materialisieren mit Planbefund: keine Kopie, Zeile unter werkzeug_befunde, temp_ordner genannt', async () => {
|
||||
const { fehler, soll } = pruefe()
|
||||
const { ergebnis: e, fehler: f, protokoll: p } = await PK(M_ARGS, { kopie: kopieLauf(2, MAT_BEFUND, '1 Befund(e).') })
|
||||
soll(f === null, 'Fehler: ' + f)
|
||||
soll(e && e.kopie === null && e.mutanten_status === 'ohne_kopie', JSON.stringify(e && [e.kopie, e.mutanten_status]))
|
||||
soll(e && JSON.stringify(e.werkzeug_befunde) === JSON.stringify([{ werkzeug: 'plan_materialisiere', befund: MAT_BEFUND }]), 'werkzeug_befunde ' + JSON.stringify(e && e.werkzeug_befunde))
|
||||
soll(e && /C:\/tmp\/k bleibt liegen/.test(e.temp_ordner || ''), 'temp_ordner ' + JSON.stringify(e && e.temp_ordner))
|
||||
soll(p.logs.some(l => /plan_materialisiere\.py meldet 1 Befund/.test(l)) && !p.logs.some(l => /Kopie nicht verwendbar/.test(l)), 'Log: ' + p.logs.join(' | '))
|
||||
return fehler
|
||||
})
|
||||
|
||||
fall('plankritik', 'materialisieren mit BEFUND von kopie_bauen.py: kein Planbefund, Kopie nicht verwendbar', async () => {
|
||||
const { fehler, soll } = pruefe()
|
||||
const { ergebnis: e, protokoll: p } = await PK(M_ARGS, { kopie: { befehl: 'kopie', exit: 2, ausgabe: 'KOPIE=C:/tmp/k/mat\nBEFUND Projekt D:/x ist kein Ordner' } })
|
||||
soll(e && e.kopie === null && Array.isArray(e.werkzeug_befunde) && e.werkzeug_befunde.length === 0, JSON.stringify(e && [e.kopie, e.werkzeug_befunde]))
|
||||
soll(p.logs.some(l => /Kopie nicht verwendbar \(Exit 2/.test(l)), 'Log: ' + p.logs.join(' | '))
|
||||
return fehler
|
||||
})
|
||||
|
||||
fall('plankritik', 'gebremst vor den Linsen: werkzeug_befunde steht auch dann in der Rückgabe', async () => {
|
||||
const { fehler, soll } = pruefe()
|
||||
const { ergebnis: e } = await PK(PK_TEXT, { planwerkzeuge: werkzeuge(Z_OK, B_ZAUN, '1 Befund(e).', 'EXIT plan_bloecke: 2'), ablesungen: [ablesung(90, 10)] })
|
||||
soll(e && e.status === 'gebremst', 'status ' + (e && e.status))
|
||||
soll(e && JSON.stringify(e.werkzeug_befunde) === JSON.stringify([{ werkzeug: 'plan_bloecke', befund: B_ZAUN }]), 'werkzeug_befunde ' + JSON.stringify(e && e.werkzeug_befunde))
|
||||
return fehler
|
||||
})
|
||||
|
||||
// Ein Hilfsagent darf seine Ausgabe auf mehrere laeufe verteilen. Im Echtlauf wf_9ab7fd8e-ad1 (2026-11-02)
|
||||
// gab werkzeug:planwerkzeuge je Werkzeug einen Lauf zurück; die zwei laeufe unten sind seine Rückgabe wörtlich.
|
||||
const ECHT_ZAUN = 'BEFUND task-01-probe.md:22: Zaun klebt an der Codezeile, Block ab Zeile 20; der Schlusszaun gehoert auf eine eigene Zeile, ein Text mit Zaunfolge in einen laengeren Zaun; Bloecke dieser Taskdatei nicht geprueft'
|
||||
const ECHT_LAEUFE = [
|
||||
{ befehl: 'plan_zeichen.py auf probe/plan', exit: 0, ausgabe: '1 Dateien, 2 Codebloecke gelesen.\nSichtbare Nicht-ASCII-Zeichen in Codebloecken: \n0 Befund(e).\nEXIT plan_zeichen: 0' },
|
||||
{ befehl: 'plan_bloecke.py vor probe/plan/task-*.md --projekt probe/projekt', exit: 2, ausgabe: ECHT_ZAUN + '\n1 Befund(e).\nEXIT plan_bloecke: 2' },
|
||||
]
|
||||
|
||||
fall('plankritik', 'Planwerkzeuge auf zwei laeufe verteilt (Echtlauf wf_9ab7fd8e-ad1): beide gelesen, Befund an die Linse', async () => {
|
||||
const { fehler, soll } = pruefe()
|
||||
const { ergebnis: e, protokoll: p } = await PK(PK_TEXT, { planwerkzeuge: ECHT_LAEUFE })
|
||||
soll(mitAusgabe(p, ECHT_ZAUN), 'Befundzeile aus dem zweiten Lauf fehlt im Auftrag der Linse bezeichner')
|
||||
soll(e && JSON.stringify(e.werkzeug_befunde) === JSON.stringify([{ werkzeug: 'plan_bloecke', befund: ECHT_ZAUN }]), 'werkzeug_befunde ' + JSON.stringify(e && e.werkzeug_befunde))
|
||||
soll(!p.logs.some(l => l.startsWith('Planwerkzeuge:')), 'Log: ' + p.logs.join(' | '))
|
||||
return fehler
|
||||
})
|
||||
|
||||
fall('plankritik', 'materialisieren auf zwei laeufe verteilt, Befundzeile im zweiten: Zeile unter werkzeug_befunde', async () => {
|
||||
const { fehler, soll } = pruefe()
|
||||
const kopie = [
|
||||
{ befehl: 'kopie_bauen.py', exit: 0, ausgabe: 'KOPIE=C:/tmp/k/mat\n12 Dateien kopiert, 1 Pfade ausgeschlossen, 0 Verknuepfungen uebersprungen, 0 Pakete in der Kopie.\n0 Befund(e).' },
|
||||
{ befehl: 'plan_materialisiere.py', exit: 2, ausgabe: MAT_BEFUND + '\n1 Befund(e).' },
|
||||
]
|
||||
const { ergebnis: e, protokoll: p } = await PK(M_ARGS, { kopie })
|
||||
soll(e && e.kopie === null && JSON.stringify(e.werkzeug_befunde) === JSON.stringify([{ werkzeug: 'plan_materialisiere', befund: MAT_BEFUND }]), JSON.stringify(e && [e.kopie, e.werkzeug_befunde]))
|
||||
soll(p.logs.some(l => /plan_materialisiere\.py meldet 1 Befund\(e\) \(Exit 0\/2\)/.test(l)), 'Log: ' + p.logs.join(' | '))
|
||||
return fehler
|
||||
})
|
||||
|
||||
// Alle laeufe auch im Erfolgspfad der Kopie und im Mutantenlauf (Folgeauftrag 2026-11-02). KOPIE_GLIEDER ist die
|
||||
// Ausgabe einer Ausführung, je Glied der Kette mktemp, echo, kopie_bauen.py, plan_materialisiere.py auf einen Lauf
|
||||
// verteilt, wie im Echtlauf je Werkzeug: KOPIE-Zeile im zweiten, angewendeter Block im letzten. Verwendbar ist die
|
||||
// Kopie nur, wenn jeder Lauf Exit 0 hat; die Kette endet nur mit 0, wenn jedes Glied mit 0 endet. KOPIE_HALB: Der
|
||||
// erste Lauf ist vollständig und endet mit 0, danach stürzt plan_materialisiere.py an der zweiten Taskdatei ab;
|
||||
// laeufe[0] allein sah wie eine Kopie aus.
|
||||
const MAT_ZEILE = "task-01.md: {'neu': 0, 'ersetzt': 1, 'angehaengt': 0, 'uebergangen': 0, 'geloescht': 0}"
|
||||
const KOPIE_GLIEDER = (exits = [0, 0, 0, 0]) => [
|
||||
{ befehl: 'T=$(mktemp -d)', exit: exits[0], ausgabe: '' },
|
||||
{ befehl: 'echo "KOPIE=$(cygpath -m "$T/mat")"', exit: exits[1], ausgabe: 'KOPIE=C:/tmp/k/mat' },
|
||||
{ befehl: 'kopie_bauen.py', exit: exits[2], ausgabe: '12 Dateien kopiert, 1 Pfade ausgeschlossen, 0 Verknuepfungen uebersprungen, 0 Pakete in der Kopie.\n0 Befund(e).' },
|
||||
{ befehl: 'plan_materialisiere.py', exit: exits[3], ausgabe: MAT_ZEILE + '\n0 Befund(e).' },
|
||||
]
|
||||
const KOPIE_HALB = [
|
||||
{ befehl: 'kopie_bauen.py && plan_materialisiere.py', exit: 0, ausgabe: ['KOPIE=C:/tmp/k/mat', '12 Dateien kopiert, 1 Pfade ausgeschlossen, 0 Verknuepfungen uebersprungen, 0 Pakete in der Kopie.', '0 Befund(e).', MAT_ZEILE].join('\n') },
|
||||
{ befehl: 'plan_materialisiere.py, task-02.md', exit: 1, ausgabe: "Traceback (most recent call last):\nUnicodeDecodeError: 'utf-8' codec can't decode byte 0xff in position 0: invalid start byte" },
|
||||
]
|
||||
// Exit 1 an erster bzw. letzter Stelle der vier Glieder, sonst 0.
|
||||
const exitsMitFehler = stelle => [0, 0, 0, 0].map((x, i) => (i === stelle ? 1 : x))
|
||||
// Der zweite Lauf ohne Feld exit: Er zählt nicht als 0 und steht im Log als undefined; das Schema LAUF verlangt
|
||||
// das Feld, die Attrappe prüft nur die oberste Ebene.
|
||||
const ohneExit = laeufe => laeufe.map((l, i) => (i === 1 ? { befehl: l.befehl, ausgabe: l.ausgabe } : l))
|
||||
|
||||
fall('plankritik', 'materialisieren auf vier laeufe verteilt, alle Exit 0: Kopie verwendet, Mutantenlauf und Linse arithmetik darauf', async () => {
|
||||
const { fehler, soll } = pruefe()
|
||||
const { ergebnis: e, fehler: f, protokoll: p } = await PK(M_ARGS, { kopie: KOPIE_GLIEDER() })
|
||||
soll(f === null, 'Fehler: ' + f)
|
||||
soll(e && e.kopie === 'C:/tmp/k/mat', 'kopie ' + JSON.stringify(e && e.kopie))
|
||||
soll(mitLabel(p, 'werkzeug:mutanten').length === 1, 'Mutantenlauf ohne Kopie: ' + mitLabel(p, 'werkzeug:mutanten').length)
|
||||
const arith = mitLabel(p, 'plankritik:arithmetik')[0]
|
||||
soll(arith && arith.prompt.includes('Der Plan ist auf die Kopie C:/tmp/k/mat angewendet'), 'Linse arithmetik rechnet ohne Kopie')
|
||||
soll(!p.logs.some(l => l.startsWith('Plankritik: ')), 'Log: ' + p.logs.join(' | '))
|
||||
return fehler
|
||||
})
|
||||
|
||||
fall('plankritik', 'materialisieren: erster Lauf vollständig mit Exit 0, Absturz im zweiten: keine Kopie, Log nennt beide Exits', async () => {
|
||||
const { fehler, soll } = pruefe()
|
||||
const { ergebnis: e, protokoll: p } = await PK({ ...M_ARGS, materialisieren: { ...M_ARGS.materialisieren, tasks: ['task-01.md', 'task-02.md'] } }, { kopie: KOPIE_HALB })
|
||||
soll(e && e.kopie === null && e.mutanten_status === 'ohne_kopie', JSON.stringify(e && [e.kopie, e.mutanten_status]))
|
||||
soll(p.logs.some(l => l.includes('Kopie nicht verwendbar (Exit 0/1)')), 'Log: ' + p.logs.join(' | '))
|
||||
return fehler
|
||||
})
|
||||
|
||||
for (const stelle of [0, 3]) {
|
||||
fall('plankritik', `materialisieren auf vier laeufe verteilt, Exit 1 im ${stelle ? 'letzten' : 'ersten'}: keine Kopie, Log nennt alle Exits`, async () => {
|
||||
const { fehler, soll } = pruefe()
|
||||
const exits = exitsMitFehler(stelle)
|
||||
const { ergebnis: e, protokoll: p } = await PK(M_ARGS, { kopie: KOPIE_GLIEDER(exits) })
|
||||
soll(e && e.kopie === null && e.mutanten_status === 'ohne_kopie', JSON.stringify(e && [e.kopie, e.mutanten_status]))
|
||||
soll(p.logs.some(l => l.includes(`Kopie nicht verwendbar (Exit ${exits.join('/')})`)), 'Log: ' + p.logs.join(' | '))
|
||||
return fehler
|
||||
})
|
||||
}
|
||||
|
||||
fall('plankritik', 'materialisieren auf vier laeufe verteilt, ein Lauf ohne exit: keine Kopie, Log nennt undefined', async () => {
|
||||
const { fehler, soll } = pruefe()
|
||||
const { ergebnis: e, protokoll: p } = await PK(M_ARGS, { kopie: ohneExit(KOPIE_GLIEDER()) })
|
||||
soll(e && e.kopie === null && e.mutanten_status === 'ohne_kopie', JSON.stringify(e && [e.kopie, e.mutanten_status]))
|
||||
soll(p.logs.some(l => l.includes('Kopie nicht verwendbar (Exit 0/undefined/0/0)')), 'Log: ' + p.logs.join(' | '))
|
||||
return fehler
|
||||
})
|
||||
|
||||
fall('plankritik', 'materialisieren, Hilfsagent ohne Ergebnis: keine Kopie, Log nennt den Hilfsagenten', async () => {
|
||||
const { fehler, soll } = pruefe()
|
||||
const { ergebnis: e, protokoll: p } = await PK(M_ARGS, { kopie: OHNE })
|
||||
soll(e && e.kopie === null && e.mutanten_status === 'ohne_kopie', JSON.stringify(e && [e.kopie, e.mutanten_status]))
|
||||
soll(p.logs.includes('Plankritik: Hilfsagent ohne Ergebnis, keine Kopie'), 'Log: ' + p.logs.join(' | '))
|
||||
return fehler
|
||||
})
|
||||
|
||||
// Mutantenlauf auf zwei laeufe verteilt, etwa Anfang und Ende einer gekürzten Ausgabe: Die EXIT-Zeile und
|
||||
// KEINE_MUTANTEN_TOML stehen im zweiten.
|
||||
fall('plankritik', 'Mutantenlauf auf zwei laeufe verteilt, EXIT-Zeile im zweiten: exit_0, beide Teile an die Linse arithmetik', async () => {
|
||||
const { fehler, soll } = pruefe()
|
||||
const mutanten = [
|
||||
{ befehl: 'mutanten_lauf_kopie.sh, Anfang', exit: 0, ausgabe: 'wurzel: /tmp/x\nmodus: voll\nM1 OK a=ROT(MUTANT-ANFANG-01)' },
|
||||
{ befehl: 'mutanten_lauf_kopie.sh, Ende', exit: 0, ausgabe: 'M2 OK b=ROT(MUTANT-ENDE-02)\nergebnis: 2 Mutanten, 2 OK, 0 LUECKE, 0 UNKLAR, 0 ZEIT, 0 MUSTER\nEXIT mutanten_lauf: 0' },
|
||||
]
|
||||
const { ergebnis: e, protokoll: p } = await PK(PK_ARGS, { mutanten })
|
||||
soll(e && e.mutanten_status === 'exit_0', 'mutanten_status ' + JSON.stringify(e && e.mutanten_status))
|
||||
const arith = (mitLabel(p, 'plankritik:arithmetik')[0] || {}).prompt || ''
|
||||
const block = arith.includes('<mutantenlauf>') ? arith.slice(arith.indexOf('<mutantenlauf>')) : ''
|
||||
soll(block.includes('MUTANT-ANFANG-01') && block.includes('MUTANT-ENDE-02'), 'Block <mutantenlauf> ohne beide Teile: ' + JSON.stringify(block.slice(0, 300)))
|
||||
soll(e && typeof e.mutanten_lauf === 'string' && e.mutanten_lauf.includes('MUTANT-ANFANG-01') && e.mutanten_lauf.includes('EXIT mutanten_lauf: 0'), 'mutanten_lauf ' + JSON.stringify(e && e.mutanten_lauf))
|
||||
soll(!p.logs.some(l => l.startsWith('Mutantenlauf:')), 'Log: ' + p.logs.join(' | '))
|
||||
return fehler
|
||||
})
|
||||
|
||||
fall('plankritik', 'Mutantenlauf auf zwei laeufe verteilt, KEINE_MUTANTEN_TOML im zweiten: keine_toml, Hinweissatz an die Linse', async () => {
|
||||
const { fehler, soll } = pruefe()
|
||||
const mutanten = [
|
||||
{ befehl: 'wsl -d Debian', exit: 0, ausgabe: 'wsl: Hinweis vor dem Skript' },
|
||||
{ befehl: 'mutanten_lauf_kopie.sh', exit: 0, ausgabe: 'KEINE_MUTANTEN_TOML' },
|
||||
]
|
||||
const { ergebnis: e, protokoll: p } = await PK(PK_ARGS, { mutanten })
|
||||
soll(e && e.mutanten_status === 'keine_toml', 'mutanten_status ' + JSON.stringify(e && e.mutanten_status))
|
||||
const arith = (mitLabel(p, 'plankritik:arithmetik')[0] || {}).prompt || ''
|
||||
soll(/keine mutanten\.toml/.test(arith) && !/<mutantenlauf>/.test(arith), 'Hinweissatz fehlt oder Block trotz KEINE_MUTANTEN_TOML')
|
||||
return fehler
|
||||
})
|
||||
|
||||
// Ein zusätzlicher Lauf des Hilfsagenten zeigt den Skripttext von mutanten_lauf_kopie.sh um die Suche nach
|
||||
// mutanten.toml: KEINE_MUTANTEN_TOML steht dort nur in einem echo, nicht als eigene Zeile der Ausgabe.
|
||||
fall('plankritik', 'Mutantenlauf mit zusätzlichem Lauf, der das Skript zeigt: exit_0 bleibt, kein Satz zur fehlenden mutanten.toml', async () => {
|
||||
const { fehler, soll } = pruefe()
|
||||
const mutanten = [
|
||||
{ befehl: 'mutanten_lauf_kopie.sh', exit: 0, ausgabe: 'wurzel: /tmp/x\nmodus: voll\nM1 OK a=ROT(MUTANT-SKRIPT-03)\nergebnis: 1 Mutanten, 1 OK, 0 LUECKE, 0 UNKLAR, 0 ZEIT, 0 MUSTER\nEXIT mutanten_lauf: 0' },
|
||||
{ befehl: 'cat mutanten_lauf_kopie.sh', exit: 0, ausgabe: 'MUTANTEN_TOML="$PLANORDNER/mutanten.toml"\nif [ ! -f "$MUTANTEN_TOML" ]; then\n echo "KEINE_MUTANTEN_TOML"\n exit 0\nfi' },
|
||||
]
|
||||
const { ergebnis: e, protokoll: p } = await PK(PK_ARGS, { mutanten })
|
||||
soll(e && e.mutanten_status === 'exit_0', 'mutanten_status ' + JSON.stringify(e && e.mutanten_status))
|
||||
const arith = (mitLabel(p, 'plankritik:arithmetik')[0] || {}).prompt || ''
|
||||
soll(arith.includes('<mutantenlauf>') && arith.includes('MUTANT-SKRIPT-03') && !/keine mutanten\.toml/.test(arith), 'Linse arithmetik ohne Mutantenlauf oder mit dem Satz zur fehlenden mutanten.toml')
|
||||
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 })
|
||||
@@ -906,6 +1179,155 @@ for (const bis of ['alles', '', 1]) {
|
||||
})
|
||||
}
|
||||
|
||||
// Planbefund (2026-11-02, Auftrag der Startsitzung, Pascal: "Ja, nachziehen"): Meldet plan_materialisiere.py
|
||||
// in der Phase Kopie eine Zeile "BEFUND <taskdatei>:<zeile>:", endet der Lauf vor executer-welle. Exit 2
|
||||
// ohne Befundzeile und die BEFUND-Zeilen von kopie_bauen.py nehmen den alten Weg (Welle ohne Byte-Vergleich).
|
||||
const { kopie: _wmKopie, ...WM_OHNE_KOPIE } = WM_ARGS
|
||||
const WM_BAU = { ...WM_OHNE_KOPIE, ohne: ['config/secrets.toml'] }
|
||||
const ZAUN1 = 'BEFUND task-01.md:7: Zaun klebt an der Codezeile; nichts geschrieben'
|
||||
const ZAUN2 = 'BEFUND task-02.md:31: Block schliesst nie; nichts geschrieben'
|
||||
function ohneWelle(p, soll) {
|
||||
soll(!p.workflows.some(w => w.ref === 'executer-welle'), 'executer-welle aufgerufen')
|
||||
const danach = p.agents.filter(x => x.label !== 'werkzeug:kopie').map(x => x.label)
|
||||
soll(danach.length === 0, 'Agenten nach dem Halt: ' + JSON.stringify(danach))
|
||||
}
|
||||
|
||||
fall('welle-mit-pruefung', 'Planbefund: hält vor executer-welle, Befundzeile in plan_befunde', async () => {
|
||||
const { fehler, soll } = pruefe()
|
||||
const { ergebnis: e, fehler: f, protokoll: p } = await WM(WM_BAU, { kopie: kopieLauf(2, ZAUN1, '1 Befund(e).') })
|
||||
soll(f === null, 'Fehler: ' + f)
|
||||
soll(e && e.status === 'plan_befund', 'status ' + (e && e.status))
|
||||
soll(e && JSON.stringify(e.plan_befunde) === JSON.stringify([ZAUN1]), 'plan_befunde ' + JSON.stringify(e && e.plan_befunde))
|
||||
soll(e && e.welle === null && e.kopie === null && e.pruefung_args === null && /C:\/tmp\/k bleibt liegen/.test(e.temp_ordner || ''), JSON.stringify(e && [e.welle, e.kopie, e.pruefung_args, e.temp_ordner]))
|
||||
soll(e && e.kopie_lauf && e.kopie_lauf.laeufe[0].exit === 2, 'kopie_lauf ' + JSON.stringify(e && e.kopie_lauf))
|
||||
soll(p.logs.some(l => /meldet 1 Befund/.test(l)), 'Log: ' + p.logs.join(' | '))
|
||||
ohneWelle(p, soll)
|
||||
ohneVerstoss(p, soll)
|
||||
return fehler
|
||||
})
|
||||
|
||||
fall('welle-mit-pruefung', 'Planbefund mit bis vergleich: hält ebenso, keine pruefung_args', async () => {
|
||||
const { fehler, soll } = pruefe()
|
||||
const { ergebnis: e, protokoll: p } = await WM({ ...WM_BAU, bis: 'vergleich' }, { kopie: kopieLauf(2, ZAUN1, '1 Befund(e).') })
|
||||
soll(e && e.status === 'plan_befund' && e.pruefung_args === null && /Befund/.test(e.pruefung_grund || ''), JSON.stringify(e && [e.status, e.pruefung_args, e.pruefung_grund]))
|
||||
ohneWelle(p, soll)
|
||||
return fehler
|
||||
})
|
||||
|
||||
fall('welle-mit-pruefung', 'Planbefund an erster und letzter Stelle, CRLF: beide Zeilen ohne CR, in Reihenfolge', async () => {
|
||||
const { fehler, soll } = pruefe()
|
||||
const tasks = [{ datei: 'task-01.md', modell: 'haiku' }, { datei: 'task-02.md', modell: 'haiku' }]
|
||||
const ausgabe = ['KOPIE=C:/tmp/k/mat', ZAUN1, 'zwischen', ZAUN2, '2 Befund(e).'].join('\r\n')
|
||||
const { ergebnis: e, protokoll: p } = await WM({ ...WM_BAU, tasks }, { kopie: { befehl: 'kopie', exit: 2, ausgabe } })
|
||||
soll(e && e.status === 'plan_befund' && JSON.stringify(e.plan_befunde) === JSON.stringify([ZAUN1, ZAUN2]), JSON.stringify(e && [e.status, e.plan_befunde]))
|
||||
soll(e && /C:\/tmp\/k bleibt liegen/.test(e.temp_ordner || ''), 'temp_ordner ' + JSON.stringify(e && e.temp_ordner))
|
||||
ohneWelle(p, soll)
|
||||
return fehler
|
||||
})
|
||||
|
||||
fall('welle-mit-pruefung', 'Absturz nach einer Befundzeile (Exit 1): hält ebenso', async () => {
|
||||
const { fehler, soll } = pruefe()
|
||||
const { ergebnis: e, protokoll: p } = await WM(WM_BAU, { kopie: kopieLauf(1, ZAUN1, 'Traceback (most recent call last):', 'OSError: x') })
|
||||
soll(e && e.status === 'plan_befund' && JSON.stringify(e.plan_befunde) === JSON.stringify([ZAUN1]), JSON.stringify(e && [e.status, e.plan_befunde]))
|
||||
ohneWelle(p, soll)
|
||||
return fehler
|
||||
})
|
||||
|
||||
for (const [name, lauf] of [
|
||||
['Exit 2 ohne Befundzeile (Errno 2)', { befehl: 'kopie', exit: 2, ausgabe: "KOPIE=C:/tmp/k/mat\n12 Dateien kopiert.\n0 Befund(e).\npython: can't open file 'D:/x/plan_materialisiere.py': [Errno 2] No such file or directory" }],
|
||||
['BEFUND von kopie_bauen.py ohne Zeilennummer', { befehl: 'kopie', exit: 2, ausgabe: 'KOPIE=C:/tmp/k/mat\nBEFUND Projekt D:/x ist kein Ordner' }],
|
||||
['BEFUND von kopie_bauen.py zu einem Ausschluss', { befehl: 'kopie', exit: 1, ausgabe: "KOPIE=C:/tmp/k/mat\n12 Dateien kopiert.\nBEFUND Ausschluss 'config/secrets.toml' gibt es nicht\n1 Befund(e)." }],
|
||||
]) {
|
||||
fall('welle-mit-pruefung', `${name}: kein Planbefund, Welle läuft wie bisher ohne Byte-Vergleich`, async () => {
|
||||
const { fehler, soll } = pruefe()
|
||||
const { ergebnis: e, fehler: f, protokoll: p } = await WM(WM_BAU, { kopie: lauf })
|
||||
soll(f === null, 'Fehler: ' + f)
|
||||
soll(e && e.status === 'fertig' && e.kopie === null && !('plan_befunde' in e), JSON.stringify(e && [e.status, e.kopie, e.plan_befunde]))
|
||||
soll(mitLabel(p, 'executer:').length === 1, 'executer ' + mitLabel(p, 'executer:').length)
|
||||
soll(p.logs.some(l => /Kopie nicht verwendbar \(Exit [12]\)/.test(l)), 'Log: ' + p.logs.join(' | '))
|
||||
return fehler
|
||||
})
|
||||
}
|
||||
|
||||
fall('welle-mit-pruefung', 'Hilfsagent der Kopie ohne Ergebnis: Welle läuft wie bisher', async () => {
|
||||
const { fehler, soll } = pruefe()
|
||||
const { ergebnis: e, protokoll: p } = await WM(WM_BAU, { kopie: OHNE })
|
||||
soll(e && e.status === 'fertig' && mitLabel(p, 'executer:').length === 1, JSON.stringify(e && e.status))
|
||||
soll(p.logs.some(l => /Hilfsagent ohne Ergebnis/.test(l)), 'Log: ' + p.logs.join(' | '))
|
||||
return fehler
|
||||
})
|
||||
|
||||
fall('welle-mit-pruefung', 'Kopie auf zwei laeufe verteilt, Befundzeile im zweiten: hält ebenso', async () => {
|
||||
const { fehler, soll } = pruefe()
|
||||
const kopie = [
|
||||
{ befehl: 'kopie_bauen.py', exit: 0, ausgabe: 'KOPIE=C:/tmp/k/mat\n12 Dateien kopiert, 1 Pfade ausgeschlossen, 0 Verknuepfungen uebersprungen, 0 Pakete in der Kopie.\n0 Befund(e).' },
|
||||
{ befehl: 'plan_materialisiere.py', exit: 2, ausgabe: ZAUN1 + '\n1 Befund(e).' },
|
||||
]
|
||||
const { ergebnis: e, protokoll: p } = await WM(WM_BAU, { kopie })
|
||||
soll(e && e.status === 'plan_befund' && JSON.stringify(e.plan_befunde) === JSON.stringify([ZAUN1]), JSON.stringify(e && [e.status, e.plan_befunde]))
|
||||
soll(p.logs.some(l => /meldet 1 Befund\(e\) \(Exit 0\/2\)/.test(l)), 'Log: ' + p.logs.join(' | '))
|
||||
ohneWelle(p, soll)
|
||||
return fehler
|
||||
})
|
||||
|
||||
// Alle laeufe auch im Erfolgspfad der Kopie und im Vergleich (Folgeauftrag 2026-11-02); KOPIE_GLIEDER, KOPIE_HALB
|
||||
// und exitsMitFehler stehen bei plankritik.
|
||||
const vergleichAuftrag = p => (mitLabel(p, 'werkzeug:vergleich')[0] || {}).prompt || ''
|
||||
|
||||
fall('welle-mit-pruefung', 'Kopie auf vier laeufe verteilt, alle Exit 0: Kopie verwendet, Byte-Vergleich gegen sie', async () => {
|
||||
const { fehler, soll } = pruefe()
|
||||
const { ergebnis: e, fehler: f, protokoll: p } = await WM(WM_BAU, { kopie: KOPIE_GLIEDER() })
|
||||
soll(f === null, 'Fehler: ' + f)
|
||||
soll(e && e.status === 'fertig' && e.kopie === 'C:/tmp/k/mat', JSON.stringify(e && [e.status, e.kopie]))
|
||||
soll(vergleichAuftrag(p).includes("--mat 'C:/tmp/k/mat'"), 'Byte-Vergleich ohne die Kopie: ' + vergleichAuftrag(p).slice(0, 300))
|
||||
soll(!p.logs.some(l => /Kopie nicht verwendbar/.test(l)), 'Log: ' + p.logs.join(' | '))
|
||||
return fehler
|
||||
})
|
||||
|
||||
fall('welle-mit-pruefung', 'Kopie: erster Lauf vollständig mit Exit 0, Absturz im zweiten: Kopie nicht verwendet, Log nennt beide Exits', async () => {
|
||||
const { fehler, soll } = pruefe()
|
||||
const tasks = [{ datei: 'task-01.md', modell: 'haiku' }, { datei: 'task-02.md', modell: 'haiku' }]
|
||||
const { ergebnis: e, protokoll: p } = await WM({ ...WM_BAU, tasks }, { kopie: KOPIE_HALB })
|
||||
soll(e && e.status === 'fertig' && e.kopie === null && !('plan_befunde' in e), JSON.stringify(e && [e.status, e.kopie]))
|
||||
soll(!vergleichAuftrag(p).includes('--mat'), 'Byte-Vergleich gegen eine halbe Kopie: ' + vergleichAuftrag(p).slice(0, 300))
|
||||
soll(p.logs.some(l => l.includes('Kopie nicht verwendbar (Exit 0/1)')), 'Log: ' + p.logs.join(' | '))
|
||||
return fehler
|
||||
})
|
||||
|
||||
for (const stelle of [0, 3]) {
|
||||
fall('welle-mit-pruefung', `Kopie auf vier laeufe verteilt, Exit 1 im ${stelle ? 'letzten' : 'ersten'}: Kopie nicht verwendet, Log nennt alle Exits`, async () => {
|
||||
const { fehler, soll } = pruefe()
|
||||
const exits = exitsMitFehler(stelle)
|
||||
const { ergebnis: e, protokoll: p } = await WM(WM_BAU, { kopie: KOPIE_GLIEDER(exits) })
|
||||
soll(e && e.status === 'fertig' && e.kopie === null, JSON.stringify(e && [e.status, e.kopie]))
|
||||
soll(p.logs.some(l => l.includes(`Kopie nicht verwendbar (Exit ${exits.join('/')})`)), 'Log: ' + p.logs.join(' | '))
|
||||
return fehler
|
||||
})
|
||||
}
|
||||
|
||||
fall('welle-mit-pruefung', 'Kopie auf vier laeufe verteilt, ein Lauf ohne exit: Kopie nicht verwendet, Log nennt undefined', async () => {
|
||||
const { fehler, soll } = pruefe()
|
||||
const { ergebnis: e, protokoll: p } = await WM(WM_BAU, { kopie: ohneExit(KOPIE_GLIEDER()) })
|
||||
soll(e && e.status === 'fertig' && e.kopie === null, JSON.stringify(e && [e.status, e.kopie]))
|
||||
soll(p.logs.some(l => l.includes('Kopie nicht verwendbar (Exit 0/undefined/0/0)')), 'Log: ' + p.logs.join(' | '))
|
||||
return fehler
|
||||
})
|
||||
|
||||
fall('welle-mit-pruefung', 'Vergleich auf zwei laeufe verteilt: Byte-Vergleich und Suite erreichen den pruefer, in ihrer Reihenfolge', async () => {
|
||||
const { fehler, soll } = pruefe()
|
||||
const vergleich = [
|
||||
{ befehl: 'welle_vergleich.py', exit: 0, ausgabe: 'VERGLEICH-MARKE-01: task-01.md, 1 Datei, Bytes gleich\nEXIT welle_vergleich: 0' },
|
||||
{ befehl: 'pytest -q', exit: 1, ausgabe: 'SUITE-MARKE-02: 1 failed, 3 passed\nEXIT suite: 1' },
|
||||
]
|
||||
const { ergebnis: e, protokoll: p } = await WM(WM_ARGS, { vergleich })
|
||||
soll(e && e.status === 'fertig', 'status ' + (e && e.status))
|
||||
const auftrag = (mitLabel(p, 'pruefung')[0] || {}).prompt || ''
|
||||
const ab = auftrag.indexOf('Schwerpunkt:')
|
||||
soll(ab >= 0 && ['VERGLEICH-MARKE-01', 'EXIT welle_vergleich: 0', 'SUITE-MARKE-02', 'EXIT suite: 1'].every(t => auftrag.indexOf(t, ab) > ab), 'Auftrag des pruefer ohne beide laeufe: ' + JSON.stringify(auftrag.slice(ab, ab + 400)))
|
||||
soll(auftrag.indexOf('VERGLEICH-MARKE-01', ab) < auftrag.indexOf('SUITE-MARKE-02', ab), 'Reihenfolge der laeufe vertauscht: ' + JSON.stringify(auftrag.slice(ab, ab + 400)))
|
||||
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)]
|
||||
@@ -1194,7 +1616,8 @@ 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 }] })
|
||||
// Eine Liste statt eines Textes sind die laeufe selbst, etwa auf mehrere laeufe verteilt.
|
||||
const lauf = (label, text) => ok({ laeufe: Array.isArray(text) ? text : [{ befehl: label, exit: 0, ausgabe: text }] })
|
||||
return async aufruf => {
|
||||
const label = aufruf.label || ''
|
||||
if (ohne.includes(label)) return undefined
|
||||
@@ -1447,6 +1870,8 @@ fall('quorum', 'Reihenfolge: blockierender Einzelbefund an erster und letzter St
|
||||
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/],
|
||||
['Task-Antwort mit jobId, aber EXIT task 1', { codexStart: Q_START.replace('EXIT task: 0', 'EXIT task: 1') }, /^Codex-Auftrag nicht gestartet$/],
|
||||
['Status gescheitert, task nicht gelaufen: Grund ohne Warnung', { codexStart: 'Fehler beim Lesen\nEXIT status: 1' }, /^Status vor dem Lauf nicht gelesen$/],
|
||||
['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/],
|
||||
@@ -1544,6 +1969,281 @@ fall('quorum', 'Codex-Befehle: Plugin 1.0.6, nur lesend, Auftrag aus dem Paket,
|
||||
return fehler
|
||||
})
|
||||
|
||||
const qStartLaeufeVertauscht = text => {
|
||||
const [status, task] = text.split('\nEXIT status: 0\n')
|
||||
return [
|
||||
{ befehl: 'Startbefehl, Ausgabe von task', exit: 0, ausgabe: task },
|
||||
{ befehl: 'Startbefehl, Ausgabe von status', exit: 0, ausgabe: status + '\nEXIT status: 0' },
|
||||
]
|
||||
}
|
||||
const qStartZeilenLaeufe = zeilen => zeilen.map((ausgabe, i) => ({
|
||||
befehl: 'Startbefehl, Ausgabe Teil ' + (i + 1), exit: 0, ausgabe,
|
||||
}))
|
||||
const qStartNeueIdZeile = ' "jobId": "task-q1",'
|
||||
const qStartZeilen = Q_START_ALT.split('\n')
|
||||
const qStartOhneNeueId = Q_START_ALT.replace(qStartNeueIdZeile + '\n', '')
|
||||
const qStartStatusRoh = Q_START_ALT.split('\nEXIT status: 0\n')[0]
|
||||
const qStartTaskBlock = Q_START.split('\nEXIT status: 0\n')[1].replace('\nEXIT task: 0', '')
|
||||
const qStartTaskRoh = qStartTaskBlock + '\nEXIT task: 0'
|
||||
const qStartTaskKompakt = '{"jobId":"task-q1","status":"queued","title":"Codex Task","summary":".","logFile":"C:/tmp/q/codex.log"}'
|
||||
const qStartReihenfolgeHinweis = 'Codex-Start: Ausgabe nicht in Befehlsreihenfolge; jobId aus der task-Ausgabe (task-q1)'
|
||||
const qStartUnsicherGrund = 'jobId nicht sicher bestimmbar; Codex-Auftrag läuft womöglich ungesehen weiter'
|
||||
const qStartTaskGeteilt = [
|
||||
{ befehl: 'Startbefehl, Status und Task-Anfang', exit: 0, ausgabe: qStartStatusRoh + '\nEXIT status: 0\n{' },
|
||||
{ befehl: 'Startbefehl, Task-Fortsetzung', exit: 0, ausgabe: qStartTaskRoh.slice(2) },
|
||||
]
|
||||
const qStartAlterRest = ausgabe => [
|
||||
...qStartLaeufeVertauscht(qStartOhneNeueId.replace(' "jobId": "task-alt"\n', '')),
|
||||
{ befehl: 'Startbefehl, verschobener Statusrest', exit: 0, ausgabe },
|
||||
]
|
||||
const qStartAlterJob = (id, status) => ({
|
||||
createdAt: '2026-10-01T00:00:00.000Z',
|
||||
updatedAt: '2026-10-01T00:01:00.000Z',
|
||||
id,
|
||||
status,
|
||||
request: {
|
||||
cwd: 'D:/x',
|
||||
model: 'codex',
|
||||
effort: 'low',
|
||||
prompt: 'früherer Auftrag',
|
||||
write: false,
|
||||
resumeLast: false,
|
||||
jobId: id
|
||||
},
|
||||
})
|
||||
const qStartStatusKompakt = JSON.stringify({
|
||||
workspaceRoot: 'D:/x',
|
||||
config: { stopReviewGate: false },
|
||||
sessionRuntime: {},
|
||||
running: [qStartAlterJob('task-alt-queued', 'queued')],
|
||||
latestFinished: {
|
||||
createdAt: '2026-10-01T00:00:00.000Z',
|
||||
updatedAt: '2026-10-01T00:01:00.000Z',
|
||||
id: 'task-alt-ohne-request',
|
||||
status: 'completed',
|
||||
},
|
||||
recent: [],
|
||||
needsReview: false,
|
||||
})
|
||||
const qStartStatusUmformatiert = qStartStatusKompakt.replace(
|
||||
'"jobId":"task-alt-queued"',
|
||||
'\n "jobId": "task-alt-queued"\n'
|
||||
)
|
||||
const qStartK4Status = 'EXIT status: 0\n' + qStartStatusUmformatiert
|
||||
const qStartK7StatusZeilen = qStartStatusKompakt
|
||||
.replace(/^\{/, '{\n')
|
||||
.replace('"id":"task-alt-queued","status":"queued",', '"id":"task-alt-queued"\n,\n"status": "queued",\n')
|
||||
.replace('"jobId":"task-alt-queued"', '\n "jobId": "task-alt-queued"\n')
|
||||
.split('\n')
|
||||
const qStartK7Laeufe = qStartZeilenLaeufe([
|
||||
qStartK7StatusZeilen[0],
|
||||
qStartK7StatusZeilen[5],
|
||||
qStartK7StatusZeilen[2],
|
||||
qStartK7StatusZeilen[3],
|
||||
qStartK7StatusZeilen[1],
|
||||
qStartK7StatusZeilen[4],
|
||||
qStartK7StatusZeilen[6],
|
||||
'EXIT status: 0',
|
||||
'EXIT task: 0',
|
||||
])
|
||||
const qStartStatusLang = JSON.stringify({
|
||||
workspaceRoot: 'D:/x',
|
||||
config: { stopReviewGate: false },
|
||||
sessionRuntime: {},
|
||||
running: Array.from({ length: 24 }, (_, i) => qStartAlterJob('task-alt-' + (i + 1), 'queued')),
|
||||
latestFinished: qStartAlterJob('task-alt-fertig', 'completed'),
|
||||
recent: [],
|
||||
needsReview: false,
|
||||
}, null, 2)
|
||||
const qStartLangeZeilen = (qStartStatusLang + '\nEXIT status: 0\n' + qStartTaskRoh).split('\n')
|
||||
const qStartGekuerzteZeilen = [
|
||||
...qStartLangeZeilen.slice(0, 60),
|
||||
'[... ' + (qStartLangeZeilen.length - 180) + ' Zeilen gekürzt ...]',
|
||||
...qStartLangeZeilen.slice(-120),
|
||||
]
|
||||
const qStartGekuerzt = qStartGekuerzteZeilen.join('\n')
|
||||
const qStartB = 'STATUS (EXIT status: 0): workspaceRoot D:/x; config.stopReviewGate=false; running=[]; latestFinished: id task-alt\nTASK (EXIT task: 0): ' + qStartTaskKompakt + '\nEXIT task: 0'
|
||||
const qStartC = 'EXIT status: 0\nTASK-START: ' + qStartTaskKompakt + '\nEXIT task: 0'
|
||||
const qStartKompaktMitStatus = qStartStatusKompakt + '\nEXIT status: 0\nTASK-START: ' + qStartTaskKompakt + '\nEXIT task: 0'
|
||||
|
||||
for (const [name, codexStart, hinweisErwartet] of [
|
||||
['task vor status, ohne früheren Job', qStartLaeufeVertauscht(Q_START), true],
|
||||
['task vor status, mit früherem Job', qStartLaeufeVertauscht(Q_START_ALT), true],
|
||||
['Task-Block vor Status-JSON, EXIT-Zeilen in Befehlsreihenfolge', [
|
||||
{ befehl: 'Startbefehl, Task-Block', exit: 0, ausgabe: qStartTaskBlock },
|
||||
{ befehl: 'Startbefehl, Status mit EXIT', exit: 0, ausgabe: qStartStatusRoh + '\nEXIT status: 0' },
|
||||
{ befehl: 'Startbefehl, Task-Exit', exit: 0, ausgabe: 'EXIT task: 0' },
|
||||
], true],
|
||||
['Task-Block hinter EXIT task', [
|
||||
{ befehl: 'Startbefehl, Status mit EXIT', exit: 0, ausgabe: qStartStatusRoh + '\nEXIT status: 0' },
|
||||
{ befehl: 'Startbefehl, Task-Exit', exit: 0, ausgabe: 'EXIT task: 0' },
|
||||
{ befehl: 'Startbefehl, Task-Block', exit: 0, ausgabe: qStartTaskBlock },
|
||||
], true],
|
||||
['Mittelkürzung nach 60/120-Regel, danach task vor status', qStartLaeufeVertauscht(qStartGekuerzt), true],
|
||||
['Echtlauf B, kompakt mit TASK-Vorsatz und Status als Prosa', qStartB, false],
|
||||
['Echtlauf C, kompakt mit TASK-START und ohne Status-JSON', qStartC, false],
|
||||
['Echtlauf A, EXIT status vor Status-JSON', 'EXIT status: 0\n' + Q_START_ALT.replace('\nEXIT status: 0', ''), false],
|
||||
['dieselbe ID kompakt vor status und formatiert danach', [
|
||||
{ befehl: 'Startbefehl, kompakter Task-Treffer', exit: 0, ausgabe: qStartTaskKompakt },
|
||||
{ befehl: 'Startbefehl, Status und formatierter Task-Treffer', exit: 0, ausgabe: Q_START_ALT },
|
||||
], true],
|
||||
['K4: neuformatierter Status mit alter Request-Zeile und Task-Antwort', qStartK4Status + '\n' + qStartTaskRoh, false],
|
||||
['synthetisch: K7-Statusreste mit intakter Task-Antwort in eigenem Lauf', [
|
||||
...qStartK7Laeufe,
|
||||
{ befehl: 'Startbefehl, intakte Task-Antwort', exit: 0, ausgabe: qStartTaskBlock },
|
||||
], true],
|
||||
['geordnet: Status mit EXIT in einem Lauf, Task mit EXIT im nächsten', [
|
||||
{ befehl: 'Startbefehl, Status mit EXIT', exit: 0, ausgabe: qStartStatusRoh + '\nEXIT status: 0' },
|
||||
{ befehl: 'Startbefehl, Task mit EXIT', exit: 0, ausgabe: qStartTaskRoh },
|
||||
], false],
|
||||
['synthetisch: 15 leere Läufe vor Status und Task', [
|
||||
...Array.from({ length: 15 }, (_, i) => ({ befehl: 'Startbefehl, leerer Lauf ' + (i + 1), exit: 0, ausgabe: '' })),
|
||||
{ befehl: 'Startbefehl, Status mit EXIT', exit: 0, ausgabe: qStartStatusRoh + '\nEXIT status: 0' },
|
||||
{ befehl: 'Startbefehl, Task mit EXIT', exit: 0, ausgabe: qStartTaskRoh },
|
||||
], false],
|
||||
['synthetisch: Vorsatz mit EXIT status vor dem Task-Block, Status-JSON danach', [
|
||||
{ befehl: 'Startbefehl, Vorsatz und Task-Block', exit: 0, ausgabe: 'Vorab (EXIT status: 0): ' + qStartTaskBlock },
|
||||
{ befehl: 'Startbefehl, Status mit EXIT', exit: 0, ausgabe: qStartStatusRoh + '\nEXIT status: 0' },
|
||||
{ befehl: 'Startbefehl, Task-Exit', exit: 0, ausgabe: 'EXIT task: 0' },
|
||||
], true],
|
||||
]) {
|
||||
fall('quorum', 'Codex-Start (' + name + '): neue Stimme und passende Hinweise', async () => {
|
||||
const { fehler, soll } = pruefe()
|
||||
if (name === 'Mittelkürzung nach 60/120-Regel, danach task vor status') {
|
||||
soll(qStartStatusLang.split('\n').length > 200, 'Statusvorlage hat nicht mehr als 200 Zeilen')
|
||||
soll(qStartGekuerzteZeilen.length === 181, 'Kürzung enthält nicht 60 + 1 + 120 Zeilen')
|
||||
soll(qStartGekuerzteZeilen[60] === '[... ' + (qStartLangeZeilen.length - 180) + ' Zeilen gekürzt ...]', 'Kürzungsmarker oder Zeilenzahl falsch')
|
||||
soll(qStartGekuerzteZeilen.includes(qStartNeueIdZeile), 'neue Task-ID ging bei der Kürzung verloren')
|
||||
}
|
||||
const { ergebnis: e, fehler: f, protokoll: p } = await QR(Q_ARGS, { codexStart })
|
||||
soll(f === null, 'Fehler: ' + f)
|
||||
soll(e && e.stimmen.codex.abgegeben === true && e.stimmen.codex.job === 'task-q1' && e.stimmen.codex.grund === '', 'Codex-Stimme ' + JSON.stringify(e && e.stimmen.codex))
|
||||
const warten = mitLabel(p, 'codex:warten:')
|
||||
const ende = mitLabel(p, 'codex:ende')
|
||||
soll(warten.length === 1 && warten[0].prompt.includes("status 'task-q1' --wait"), 'Warten nicht genau einmal auf task-q1')
|
||||
soll(ende.length === 1 && ende[0].prompt.includes("result 'task-q1' --json"), 'Auswertung nicht genau einmal für task-q1')
|
||||
soll(mitLabel(p, 'stimme:haiku').length === 0 && mitLabel(p, 'codex:abbruch').length === 0, 'Haiku oder Abbruch trotz fertigem neuen Auftrag')
|
||||
soll(p.agents.every(x => !String(x.prompt).includes('task-alt')), 'ein Auftrag nennt einen früheren Job')
|
||||
soll(p.logs.filter(text => text === qStartReihenfolgeHinweis).length === Number(hinweisErwartet), 'Reihenfolgehinweis im Protokoll falsch')
|
||||
soll(e && e.hinweise.includes(qStartReihenfolgeHinweis) === hinweisErwartet && e.bericht_md.includes(qStartReihenfolgeHinweis) === hinweisErwartet, 'Reihenfolgehinweis in Hinweisen oder Bericht falsch')
|
||||
soll(e && !e.hinweise.some(text => text.includes(qStartUnsicherGrund)) && !e.bericht_md.includes(qStartUnsicherGrund), 'Warnung trotz eindeutiger neuer ID')
|
||||
return fehler
|
||||
})
|
||||
}
|
||||
|
||||
for (const [name, codexStart, hinweisErwartet] of [
|
||||
['vertauscht', qStartLaeufeVertauscht(Q_START_ALT), true],
|
||||
['kompakt mit früherem queued-Job', qStartKompaktMitStatus, false],
|
||||
]) {
|
||||
fall('quorum', 'Codex-Start (' + name + '): neuer Auftrag wird nach zwei Warterunden abgebrochen', async () => {
|
||||
const { fehler, soll } = pruefe()
|
||||
const { ergebnis: e, fehler: f, protokoll: p } = await QR({ ...Q_ARGS, codex_runden: 2 }, {
|
||||
codexStart,
|
||||
codexWarten: () => Q_WARTEN_OFFEN,
|
||||
})
|
||||
soll(f === null, 'Fehler: ' + f)
|
||||
const warten = mitLabel(p, 'codex:warten:')
|
||||
const abbruch = mitLabel(p, 'codex:abbruch')
|
||||
soll(warten.length === 2 && warten.every(x => x.prompt.includes("status 'task-q1' --wait")), 'nicht zweimal auf den neuen Auftrag gewartet')
|
||||
soll(abbruch.length === 1 && abbruch[0].prompt.includes("cancel 'task-q1' --json"), 'neuer Auftrag nicht genau einmal abgebrochen')
|
||||
soll(e && e.stimmen.codex.job === 'task-q1' && e.stimmen.codex.abgegeben === false && /nach 2 Warterunden/.test(e.stimmen.codex.grund), 'Codex-Ergebnis ' + JSON.stringify(e && e.stimmen.codex))
|
||||
soll(mitLabel(p, 'stimme:haiku').length === 1 && mitLabel(p, 'codex:ende').length === 0, 'Ersatz oder Auswertung nach Zeitüberschreitung falsch')
|
||||
soll(p.agents.every(x => !String(x.prompt).includes('task-alt')), 'Warten oder Abbruch nennt einen früheren Job')
|
||||
soll(p.logs.filter(text => text === qStartReihenfolgeHinweis).length === Number(hinweisErwartet), 'Reihenfolgehinweis vor dem Warten falsch')
|
||||
soll(e && e.hinweise.includes(qStartReihenfolgeHinweis) === hinweisErwartet && e.bericht_md.includes(qStartReihenfolgeHinweis) === hinweisErwartet, 'Reihenfolgehinweis im Ergebnis falsch')
|
||||
return fehler
|
||||
})
|
||||
}
|
||||
|
||||
for (const [name, codexStart, grund] of [
|
||||
[
|
||||
'Task-Nutzlast in umgekehrte Einzelzeilen zerlegt',
|
||||
qStartZeilenLaeufe(qStartZeilen.slice().reverse()),
|
||||
qStartUnsicherGrund,
|
||||
],
|
||||
[
|
||||
'keine task-ID',
|
||||
qStartLaeufeVertauscht(Q_START.replace(qStartNeueIdZeile + '\n', '')),
|
||||
qStartUnsicherGrund,
|
||||
],
|
||||
[
|
||||
'nur alte Status-ID',
|
||||
qStartLaeufeVertauscht(qStartOhneNeueId),
|
||||
qStartUnsicherGrund,
|
||||
],
|
||||
[
|
||||
'alte Status-ID hinter EXIT status',
|
||||
qStartAlterRest(' "jobId": "task-alt"'),
|
||||
qStartUnsicherGrund,
|
||||
],
|
||||
[
|
||||
'unvollständige Rückgabe ohne Status und task-ID',
|
||||
[
|
||||
{ befehl: 'Startbefehl, Ausgaberest', exit: 0, ausgabe: '[... 250 Zeilen gekürzt ...]\nEXIT task: 0' },
|
||||
],
|
||||
'Status vor dem Lauf nicht gelesen; Codex-Auftrag läuft womöglich ungesehen weiter',
|
||||
],
|
||||
[
|
||||
'kompakter Status mit früherem queued-Job, ohne Task-Antwort',
|
||||
qStartStatusKompakt + '\nEXIT status: 0\nEXIT task: 0',
|
||||
qStartUnsicherGrund,
|
||||
],
|
||||
[
|
||||
'synthetisch: alte Request-ID mit Komma vor queued, ohne Objektöffnung',
|
||||
qStartAlterRest(' "jobId": "task-alt",\n "status": "queued"'),
|
||||
qStartUnsicherGrund,
|
||||
],
|
||||
[
|
||||
'synthetisch: alte Request-ID ohne Komma vor queued, mit Objektöffnung',
|
||||
qStartAlterRest('{\n "jobId": "task-alt"\n "status": "queued"\n}'),
|
||||
qStartUnsicherGrund,
|
||||
],
|
||||
[
|
||||
'zwei verschiedene IDs aus formatiertem und kompaktem Task-Objekt',
|
||||
qStartLaeufeVertauscht(Q_START_ALT + '\n' + qStartTaskKompakt.replace('task-q1', 'task-q2')),
|
||||
qStartUnsicherGrund,
|
||||
],
|
||||
[
|
||||
'K4: neuformatierter Status mit alter Request-Zeile, ohne Task-Antwort',
|
||||
qStartK4Status + '\nEXIT task: 0',
|
||||
qStartUnsicherGrund,
|
||||
],
|
||||
[
|
||||
'synthetisch: Task-Objektanfang mit completed statt queued',
|
||||
'{\n "running": []\n}\nEXIT status: 0\n{"jobId":"task-alt","status":"completed","title":"synthetisch"}\nEXIT task: 0',
|
||||
qStartUnsicherGrund,
|
||||
],
|
||||
[
|
||||
'synthetisch: K7-Merkmal aus vier einzeiligen Läufen, nur Statusreste',
|
||||
qStartK7Laeufe,
|
||||
qStartUnsicherGrund,
|
||||
],
|
||||
[
|
||||
'echte Task-Antwort am Objektanfang über zwei Läufe zerrissen',
|
||||
qStartTaskGeteilt,
|
||||
qStartUnsicherGrund,
|
||||
],
|
||||
]) {
|
||||
fall('quorum', 'Codex-Start unsicher (' + name + '): Haiku und Warnung vor ungesehenem Auftrag', async () => {
|
||||
const { fehler, soll } = pruefe()
|
||||
if (name === 'synthetisch: K7-Merkmal aus vier einzeiligen Läufen, nur Statusreste') {
|
||||
soll(qStartK7StatusZeilen.length === 7 && JSON.stringify(JSON.parse(qStartK7StatusZeilen.join('\n'))) === qStartStatusKompakt, 'K7-Statusvorlage ändert JSON-Bestandteile oder ihre Reihenfolge')
|
||||
soll(qStartK7Laeufe.every(l => !l.ausgabe.includes('\n')), 'K7-Statusfragmente sind nicht einzeilige Läufe')
|
||||
}
|
||||
if (name === 'echte Task-Antwort am Objektanfang über zwei Läufe zerrissen') {
|
||||
soll(qStartTaskGeteilt.map(l => l.ausgabe).join('\n') === Q_START_ALT, 'geteilte echte Task-Antwort weicht von Q_START_ALT ab')
|
||||
}
|
||||
const { ergebnis: e, fehler: f, protokoll: p } = await QR(Q_ARGS, { codexStart })
|
||||
soll(f === null, 'Fehler: ' + f)
|
||||
soll(e && e.stimmen.codex.abgegeben === false && e.stimmen.codex.job === null && e.stimmen.codex.grund === grund, 'Codex-Ergebnis ' + JSON.stringify(e && e.stimmen.codex))
|
||||
soll(mitLabel(p, 'codex:warten:').length === 0 && mitLabel(p, 'codex:abbruch').length === 0 && mitLabel(p, 'codex:ende').length === 0, 'Auftrag ohne sichere jobId verfolgt')
|
||||
soll(mitLabel(p, 'stimme:haiku').length === 1, 'Haiku nicht genau einmal eingesprungen')
|
||||
soll(p.agents.every(x => !String(x.prompt).includes('task-alt')), 'ein Auftrag nennt einen früheren Job')
|
||||
soll(e && e.hinweise.some(text => text.includes(grund)) && e.bericht_md.includes(grund), 'Warnung vor ungesehenem Auftrag fehlt in Hinweisen oder Bericht')
|
||||
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 })
|
||||
@@ -1555,6 +2255,50 @@ fall('quorum', 'Codex-Start: jobId nur aus der Ausgabe von task, nicht aus einem
|
||||
return fehler
|
||||
})
|
||||
|
||||
// Alle laeufe in ausgabe() (Folgeauftrag 2026-11-02): Paket, Codex-Start und Warten, je eine Ausführung, deren
|
||||
// Ausgabe der Hilfsagent auf zwei laeufe verteilt; die EXIT-Zeile steht jeweils im zweiten. Mit laeufe[0] allein
|
||||
// galt das Paket als nicht gebaut und ein gestarteter Codex-Auftrag als nicht gestartet: Haiku sprang ein, der
|
||||
// Auftrag lief ungesehen weiter.
|
||||
fall('quorum', 'Paket auf zwei laeufe verteilt, EXIT paket im zweiten: Paket gebaut, Stimmen laufen', async () => {
|
||||
const { fehler, soll } = pruefe()
|
||||
const paketAusgabe = [
|
||||
{ befehl: 'Paketbefehl, Ausgabe Teil 1', exit: 0, ausgabe: 'Paket in C:/tmp/q\nQUORUM_PAKET ' + JSON.stringify(qPaket()) },
|
||||
{ befehl: 'Paketbefehl, Ausgabe Teil 2', exit: 0, ausgabe: 'EXIT paket: 0' },
|
||||
]
|
||||
const { ergebnis: e, fehler: f, protokoll: p } = await QR(Q_ARGS, { paketAusgabe })
|
||||
soll(f === null, 'Fehler: ' + f)
|
||||
soll(e && e.status === 'fertig' && e.paket === 'C:/tmp/q', JSON.stringify(e && [e.status, e.paket]))
|
||||
soll(mitLabel(p, 'stimme:opus').length === 1 && mitLabel(p, 'stimme:sonnet').length === 1, 'Stimmen: ' + JSON.stringify(p.agents.map(x => x.label)))
|
||||
return fehler
|
||||
})
|
||||
|
||||
fall('quorum', 'Codex-Start auf zwei laeufe verteilt, Status mit früherem Job: jobId aus task, Codex-Stimme gültig', async () => {
|
||||
const { fehler, soll } = pruefe()
|
||||
const codexStart = [
|
||||
{ befehl: 'Startbefehl, Ausgabe von status', exit: 0, ausgabe: '{\n "running": [],\n "latestFinished": {\n "id": "task-alt",\n "status": "completed",\n "request": {\n "jobId": "task-alt"\n }\n }\n}\nEXIT status: 0' },
|
||||
{ befehl: 'Startbefehl, Ausgabe von task', exit: 0, ausgabe: '{\n "jobId": "task-q1",\n "status": "queued"\n}\nEXIT task: 0' },
|
||||
]
|
||||
soll(codexStart.map(l => l.ausgabe).join('\n') === Q_START_ALT, 'Vorlage: die zwei laeufe sind nicht Q_START_ALT')
|
||||
const { ergebnis: e, protokoll: p } = await QR(Q_ARGS, { codexStart })
|
||||
soll(e && e.stimmen.codex.abgegeben === true && e.stimmen.codex.job === 'task-q1', 'Codex-Stimme ' + JSON.stringify(e && [e.stimmen.codex.job, e.stimmen.codex.grund]))
|
||||
soll(mitLabel(p, 'stimme:haiku').length === 0 && mitLabel(p, 'codex:abbruch').length === 0, 'Agenten: ' + JSON.stringify(p.agents.map(x => x.label)))
|
||||
const warten = mitLabel(p, 'codex:warten:1')[0]
|
||||
soll(warten && warten.prompt.includes("status 'task-q1' --wait"), 'Warten nicht auf task-q1')
|
||||
return fehler
|
||||
})
|
||||
|
||||
fall('quorum', 'Warten auf zwei laeufe verteilt, EXIT warten im zweiten: fertig, kein Abbruch, Codex-Stimme gültig', async () => {
|
||||
const { fehler, soll } = pruefe()
|
||||
const codexWarten = () => [
|
||||
{ befehl: 'Wartebefehl, Ausgabe Teil 1', exit: 0, ausgabe: '{\n "waitTimedOut": false\n}' },
|
||||
{ befehl: 'Wartebefehl, Ausgabe Teil 2', exit: 0, ausgabe: 'EXIT warten: 0' },
|
||||
]
|
||||
const { ergebnis: e, protokoll: p } = await QR(Q_ARGS, { codexWarten })
|
||||
soll(mitLabel(p, 'codex:warten:').length === 1 && mitLabel(p, 'codex:abbruch').length === 0 && mitLabel(p, 'codex:ende').length === 1, 'Agenten: ' + JSON.stringify(p.agents.map(x => x.label)))
|
||||
soll(e && e.stimmen.codex.abgegeben === true && mitLabel(p, 'stimme:haiku').length === 0, 'Codex-Stimme ' + JSON.stringify(e && [e.stimmen.codex.abgegeben, e.stimmen.codex.grund]))
|
||||
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)] })
|
||||
|
||||
@@ -123,7 +123,30 @@ MUTANTEN = [
|
||||
("quorum", "ungepruefter blockierender Befund sperrt nicht", "(gilt(g) || g.status === 'nicht_geprueft')", "gilt(g)"),
|
||||
("quorum", "Urteil schon mit einer Stimme", "if (abgegeben.length < 2) {", "if (abgegeben.length < 1) {"),
|
||||
("quorum", "Bericht ohne Urteil", "return ergebnis('unvollstaendig', 'kein_urteil', grund, null,", "return ergebnis('unvollstaendig', 'kein_urteil', grund, 'x',"),
|
||||
("quorum", "jobId aus der ganzen Ausgabe samt Status", "start.slice(start.lastIndexOf('EXIT status:'))", "start"),
|
||||
("quorum", "jobId aus der ganzen Ausgabe samt Status", " const job = taskIds.size === 1 ? taskJobs[0] : null\n", ' const job = /"jobId": "([^"]+)"/.exec(start)\n'),
|
||||
("quorum", "Erkennung vertauschter laeufe entfernt", " const job = taskIds.size === 1 ? taskJobs[0] : null\n", " const job = /\"jobId\": \"([^\"]+)\"/.exec(start.slice(start.lastIndexOf('EXIT status:')))\n"),
|
||||
("quorum", "alte jobId als Ersatz zugelassen", " const job = taskIds.size === 1 ? taskJobs[0] : null\n", ' const job = taskJobs[0] || /"jobId": "([^"]+)"/.exec(start)\n'),
|
||||
("quorum", "Hinweis auf vertauschte laeufe fehlt", " hinweis('Codex-Start: Ausgabe nicht in Befehlsreihenfolge; jobId aus der task-Ausgabe (' + job[1] + ')')\n", ""),
|
||||
("quorum", "erster Kandidat gewinnt", " const job = taskIds.size === 1 ? taskJobs[0] : null\n", " const job = taskJobs[0] || null\n"),
|
||||
("quorum", "Warnung vor ungesehenem Auftrag fehlt", " ? 'jobId nicht sicher bestimmbar; Codex-Auftrag läuft womöglich ungesehen weiter'\n", " ? 'Codex-Auftrag nicht gestartet'\n"),
|
||||
("quorum", "Vertauschung nur an EXIT-Reihenfolge erkannt", " if (!(statusEnde < job.index && job.index < taskEnde)) {\n", " if (taskEnde < statusEnde) {\n"),
|
||||
("quorum", "Warnung bei fehlendem Status fehlt", " z.grund = 'Status vor dem Lauf nicht gelesen' + (taskOK ? '; Codex-Auftrag läuft womöglich ungesehen weiter' : '')\n", " z.grund = 'Status vor dem Lauf nicht gelesen'\n"),
|
||||
("quorum", "Grenze vor EXIT task nicht geprüft", " if (!(statusEnde < job.index && job.index < taskEnde)) {\n", " if (!(statusEnde < job.index)) {\n"),
|
||||
("quorum", "eingerückte Zeile mit queued als Task-Herkunft zugelassen", r' const laufJobs = [...laufText.matchAll(/\{\s*"jobId"\s*:\s*"([^"\\\r\n]+)"\s*,\s*"status"\s*:\s*"queued"/g)]' + "\n", r' const laufJobs = [...laufText.matchAll(/(?:\{\s*|^[ \t]+)"jobId"\s*:\s*"([^"\\\r\n]+)"\s*,\s*"status"\s*:\s*"queued"/gm)]' + "\n"),
|
||||
("quorum", "beliebige verschachtelte jobId zugelassen", r' const laufJobs = [...laufText.matchAll(/\{\s*"jobId"\s*:\s*"([^"\\\r\n]+)"\s*,\s*"status"\s*:\s*"queued"/g)]' + "\n", r' const laufJobs = [...laufText.matchAll(/"jobId"\s*:\s*"([^"\\\r\n]+)"/g)]' + "\n"),
|
||||
("quorum", "Objektöffnung vor jobId nicht verlangt", r' const laufJobs = [...laufText.matchAll(/\{\s*"jobId"\s*:\s*"([^"\\\r\n]+)"\s*,\s*"status"\s*:\s*"queued"/g)]' + "\n", r' const laufJobs = [...laufText.matchAll(/"jobId"\s*:\s*"([^"\\\r\n]+)"\s*,\s*"status"\s*:\s*"queued"/g)]' + "\n"),
|
||||
("quorum", "Komma nach jobId nicht verlangt", r' const laufJobs = [...laufText.matchAll(/\{\s*"jobId"\s*:\s*"([^"\\\r\n]+)"\s*,\s*"status"\s*:\s*"queued"/g)]' + "\n", r' const laufJobs = [...laufText.matchAll(/\{\s*"jobId"\s*:\s*"([^"\\\r\n]+)"\s*,?\s*"status"\s*:\s*"queued"/g)]' + "\n"),
|
||||
("quorum", "queued nicht verlangt", r' const laufJobs = [...laufText.matchAll(/\{\s*"jobId"\s*:\s*"([^"\\\r\n]+)"\s*,\s*"status"\s*:\s*"queued"/g)]' + "\n", r' const laufJobs = [...laufText.matchAll(/\{\s*"jobId"\s*:\s*"([^"\\\r\n]+)"\s*,\s*"status"\s*:\s*"[^"]+"/g)]' + "\n"),
|
||||
("quorum", "Treffer statt verschiedener IDs gezählt", " const job = taskIds.size === 1 ? taskJobs[0] : null\n", " const job = taskJobs.length === 1 ? taskJobs[0] : null\n"),
|
||||
("quorum", "Rohzeile wieder als Merkmal zugelassen", r' const laufJobs = [...laufText.matchAll(/\{\s*"jobId"\s*:\s*"([^"\\\r\n]+)"\s*,\s*"status"\s*:\s*"queued"/g)]' + "\n", r' const laufJobs = [...laufText.matchAll(/^ "jobId": "([^"\\\r\n]+)",?\r?$/gm), ...laufText.matchAll(/\{\s*"jobId"\s*:\s*"([^"\\\r\n]+)"\s*,\s*"status"\s*:\s*"queued"/g)]' + "\n"),
|
||||
("quorum", "Merkmal über Laufgrenzen zugelassen", " for (const laufText of laufTexte(startAntwort)) {\n", " for (const laufText of [start]) {\n"),
|
||||
("quorum", "Task-EXIT-Suche nicht am Zeilenanfang verankert", r' const taskEnde = start.search(/^EXIT task: 0(?!\d)/m)' + "\n", r' const taskEnde = start.search(/EXIT task: 0(?!\d)/m)' + "\n"),
|
||||
("quorum", "Letzte statt erster Fundstelle gewählt", " const job = taskIds.size === 1 ? taskJobs[0] : null\n", " const job = taskIds.size === 1 ? taskJobs[taskJobs.length - 1] : null\n"),
|
||||
("quorum", "Fundstellen nicht auf den verbundenen Text umgerechnet", " treffer.index += startIndex\n", ""),
|
||||
("quorum", "Trennzeichen beim Umrechnen nicht mitgezählt", " startIndex += laufText.length + 1\n", " startIndex += laufText.length\n"),
|
||||
("quorum", "Task-Exit nicht verlangt bei gefundener jobId", " if (!taskOK || !job) {\n", " if (!job) {\n"),
|
||||
("quorum", "Status-EXIT-Suche nicht am Zeilenanfang verankert", r" const statusEnde = start.search(/^EXIT status: 0(?!\d)/m)" + "\n", r" const statusEnde = start.search(/EXIT status: 0(?!\d)/m)" + "\n"),
|
||||
("quorum", "Warnung auch ohne gelaufenen task", " z.grund = 'Status vor dem Lauf nicht gelesen' + (taskOK ? '; Codex-Auftrag läuft womöglich ungesehen weiter' : '')\n", " z.grund = 'Status vor dem Lauf nicht gelesen' + '; Codex-Auftrag läuft womöglich ungesehen weiter'\n"),
|
||||
("quorum", "Teilgruppen ignoriert, Gruppe bleibt ganz", "teile.forEach(t => gruppen.push(t))", "gruppen.push(mitglieder)"),
|
||||
("quorum", "Gruppe bei Aufteilung ganz aufgeloest", "teile.forEach(t => gruppen.push(t))", "(teile.length > 1 ? mitglieder.map(b => [b]) : teile).forEach(t => gruppen.push(t))"),
|
||||
("quorum", "Aufteilung ohne Hinweis", "if (teile.length > 1) meldungen.push(", "if (false) meldungen.push("),
|
||||
@@ -187,6 +210,46 @@ MUTANTEN = [
|
||||
("quorum", "Klartext: Hinweise roh", "'- ' + klartext(h)", "'- ' + h"),
|
||||
("quorum", "Klartext: Stimmentabelle, Grund roh", "'nein: ' + klartext(s.grund)", "'nein: ' + s.grund"),
|
||||
("quorum", "Klartext: ganzer Bericht, Hub-Zeile entschaerft", r"return z.join('\n')", r"return klartext(z.join('\n'))"),
|
||||
# Exit 2 der Planwerkzeuge als Befund (2026-11-02)
|
||||
("welle-mit-pruefung", "Planbefund haelt nicht", " if (planBefunde.length) {", " if (false) {"),
|
||||
("welle-mit-pruefung", "Befundzeile ohne Zeilennummer", r"const BEFUNDZEILE = /^BEFUND [^\s:]+:\d+:/", r"const BEFUNDZEILE = /^BEFUND /"),
|
||||
("welle-mit-pruefung", "CR bleibt in der Befundzeile", r".map(z => z.replace(/\r$/, ''))", ""),
|
||||
("welle-mit-pruefung", "Halt nur bei Exit 2", " const planBefunde = befundZeilen(aus)", " const planBefunde = exitsAller(r).includes(2) ? befundZeilen(aus) : []"),
|
||||
("welle-mit-pruefung", "Befund nur im ersten Lauf gesucht", " const planBefunde = befundZeilen(aus)", " const planBefunde = befundZeilen(r && r.laeufe && r.laeufe[0] ? String(r.laeufe[0].ausgabe || '') : '')"),
|
||||
("welle-mit-pruefung", "Halt ohne Temp-Ordner", " temp_ordner: tempHinweis(temp),\n welle: null,", " temp_ordner: null,\n welle: null,"),
|
||||
("plankritik", "Exit 2 mit Befund gilt nicht als gelaufen", "(exit === 2 && befunde.length > 0) || ", ""),
|
||||
("plankritik", "Exit 2 ohne Befund gilt als gelaufen", "(exit === 2 && befunde.length > 0)", "exit === 2"),
|
||||
("plankritik", "Exit 3 mit Marke gilt nicht als gelaufen", " || (name === 'plan_bloecke' && exit === 3 && /^Nichts geprueft:/m.test(abschnitt))", ""),
|
||||
("plankritik", "Exit 3 ohne Marke gilt als gelaufen", "exit === 3 && /^Nichts geprueft:/m.test(abschnitt)", "exit === 3"),
|
||||
("plankritik", "Befunde dem falschen Werkzeug zugeordnet", "werkzeugStand(aus, 'plan_zeichen', grenze >= 0 ? aus.slice(0, grenze) : aus)", "werkzeugStand(aus, 'plan_zeichen', aus)"),
|
||||
("plankritik", "Hilfsagent-Meldung fuer gescheiterte Werkzeuge", " else log('Planwerkzeuge: ' + [zeichen, bloecke]", " else log('Planwerkzeuge: Hilfsagent ohne Ergebnis; ' + [zeichen, bloecke]"),
|
||||
("plankritik", "Ausgabe trotz gescheitertem Werkzeug an die Linse", " if (zeichen.gelaufen && bloecke.gelaufen) ctx.werkzeuge = aus", " if (zeichen.gelaufen || bloecke.gelaufen) ctx.werkzeuge = aus"),
|
||||
("plankritik", "materialisieren: Planbefund als Werkzeugfehler", " if (befunde.length) {\n befunde.forEach(", " if (false) {\n befunde.forEach("),
|
||||
("plankritik", "materialisieren: Planbefund nicht erfasst", " befunde.forEach(befund => ctx.befunde.push({ werkzeug: 'plan_materialisiere', befund }))\n", ""),
|
||||
("plankritik", "materialisieren: Temp-Ordner nicht genannt", " if (m) ctx.temp = m[1].replace(/\\/mat$/, '')\n log(", " log("),
|
||||
("plankritik", "Befundzeile ohne Zeilennummer", r"const BEFUNDZEILE = /^BEFUND [^\s:]+:\d+:/", r"const BEFUNDZEILE = /^BEFUND /"),
|
||||
("plankritik", "CR bleibt in der Befundzeile", r".map(z => z.replace(/\r$/, ''))", ""),
|
||||
("plankritik", "gebremst ohne werkzeug_befunde", " werkzeug_befunde: ctx.befunde,\n ohne_ergebnis: gewaehlt,", " ohne_ergebnis: gewaehlt,"),
|
||||
("plankritik", "fertig ohne werkzeug_befunde", " werkzeug_befunde: ctx.befunde,\n ohne_ergebnis: ohneErgebnis,", " ohne_ergebnis: ohneErgebnis,"),
|
||||
("plankritik", "Planwerkzeuge nur im ersten Lauf gelesen", " const aus = ausgabeAller(r)\n const grenze", " const aus = String(lauf.ausgabe || '')\n const grenze"),
|
||||
("plankritik", "materialisieren: Befund nur im ersten Lauf", " const befunde = befundZeilen(aus)", " const befunde = befundZeilen(r.laeufe[0].ausgabe)"),
|
||||
# Alle laeufe an fuenf weiteren Stellen (2026-11-02, Folgeauftrag): je Stelle wieder nur laeufe[0], fuer die Kopie auch das Exit nur eines Laufs
|
||||
("welle-mit-pruefung", "Kopie: KOPIE-Zeile nur im ersten Lauf", r" const m = /^KOPIE=(.+?)\s*$/m.exec(aus)", r" const m = /^KOPIE=(.+?)\s*$/m.exec(r && r.laeufe && r.laeufe[0] ? String(r.laeufe[0].ausgabe || '') : '')"),
|
||||
("welle-mit-pruefung", "Kopie: angewendete Bloecke nur im ersten Lauf", " const angewendet = /'(neu|ersetzt|angehaengt)': [1-9]/.test(aus)", " const angewendet = /'(neu|ersetzt|angehaengt)': [1-9]/.test(r && r.laeufe && r.laeufe[0] ? String(r.laeufe[0].ausgabe || '') : '')"),
|
||||
("welle-mit-pruefung", "Kopie: nur das Exit des ersten Laufs", "const alleExitNull = r => exitsAller(r).length > 0 && exitsAller(r).every(x => x === 0)", "const alleExitNull = r => exitsAller(r)[0] === 0"),
|
||||
("welle-mit-pruefung", "Kopie: nur das Exit des letzten Laufs", "const alleExitNull = r => exitsAller(r).length > 0 && exitsAller(r).every(x => x === 0)", "const alleExitNull = r => exitsAller(r).slice(-1)[0] === 0"),
|
||||
("welle-mit-pruefung", "Vergleich nur aus dem ersten Lauf", "kopfUndEnde(ausgabeAller(belege.vergleich), 1500, 2500)", "kopfUndEnde(belege.vergleich && belege.vergleich.laeufe[0] ? belege.vergleich.laeufe[0].ausgabe : '', 1500, 2500)"),
|
||||
("plankritik", "materialisieren: KOPIE-Zeile nur im ersten Lauf", r" const m = /^KOPIE=(.+?)\s*$/m.exec(aus)", r" const m = /^KOPIE=(.+?)\s*$/m.exec(r.laeufe[0].ausgabe)"),
|
||||
("plankritik", "materialisieren: angewendete Bloecke nur im ersten Lauf", " const angewendet = /'(neu|ersetzt|angehaengt)': [1-9]/.test(aus)", " const angewendet = /'(neu|ersetzt|angehaengt)': [1-9]/.test(r.laeufe[0].ausgabe)"),
|
||||
("plankritik", "materialisieren: nur das Exit des ersten Laufs", "const alleExitNull = r => exitsAller(r).length > 0 && exitsAller(r).every(x => x === 0)", "const alleExitNull = r => exitsAller(r)[0] === 0"),
|
||||
("plankritik", "materialisieren: nur das Exit des letzten Laufs", "const alleExitNull = r => exitsAller(r).length > 0 && exitsAller(r).every(x => x === 0)", "const alleExitNull = r => exitsAller(r).slice(-1)[0] === 0"),
|
||||
("plankritik", "Mutantenlauf nur aus dem ersten Lauf", " const aus = ausgabeAller(r)\n const exitMatch", " const aus = r && r.laeufe && r.laeufe[0] ? String(r.laeufe[0].ausgabe || '') : ''\n const exitMatch"),
|
||||
("quorum", "Hilfsagenten-Ausgabe nur aus dem ersten Lauf", r" return laufTexte(r).join('\n')", r" return laufTexte(r).slice(0, 1).join('\n')"),
|
||||
("plankritik", "KEINE_MUTANTEN_TOML unverankert", r"if (/^KEINE_MUTANTEN_TOML\s*$/m.test(aus))", "if (/KEINE_MUTANTEN_TOML/.test(aus))"),
|
||||
("welle-mit-pruefung", "Kopie: Lauf ohne exit zaehlt als 0", "const alleExitNull = r => exitsAller(r).length > 0 && exitsAller(r).every(x => x === 0)", "const alleExitNull = r => exitsAller(r).length > 0 && exitsAller(r).every(x => x === 0 || x == null)"),
|
||||
("plankritik", "materialisieren: Lauf ohne exit zaehlt als 0", "const alleExitNull = r => exitsAller(r).length > 0 && exitsAller(r).every(x => x === 0)", "const alleExitNull = r => exitsAller(r).length > 0 && exitsAller(r).every(x => x === 0 || x == null)"),
|
||||
("welle-mit-pruefung", "Vergleich umgekehrt verbunden", "kopfUndEnde(ausgabeAller(belege.vergleich), 1500, 2500)", "kopfUndEnde(ausgabeAller({ laeufe: [...((belege.vergleich && belege.vergleich.laeufe) || [])].reverse() }), 1500, 2500)"),
|
||||
("plankritik", "materialisieren: ohne-Ergebnis-Wache fehlt", " if (!exits.length) { log(`${wozu}: Hilfsagent ohne Ergebnis, keine Kopie`); return null }\n", ""),
|
||||
]
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user