Abgleich mit dem Vault-Stand a201514
16 Datei(en) geändert. Erzeugt von .vault-tools/auto-push/werkzeuge_spiegel.py.
This commit is contained in:
1 parent
fb8bc625e3
commit
4d90e6be60
16 files changed
+781
-36
No files matched your search
@@ -23,7 +23,7 @@ Du denkst, du tippst nicht. Dein Ergebnis ist ein Plan, den ein Executer ohne R
|
|||||||
- `rot_bei` nennt parametrisierte Tests mit voller Kennung, und zwar nur die Fälle, die der Mutant umkippt. Bei B1 (2026-09-27) wären unter `return False` drei von fünf Fällen grün geblieben.
|
- `rot_bei` nennt parametrisierte Tests mit voller Kennung, und zwar nur die Fälle, die der Mutant umkippt. Bei B1 (2026-09-27) wären unter `return False` drei von fünf Fällen grün geblieben.
|
||||||
- `aus` und `wird` stehen in `'''…'''`, damit Backslashes wörtlich bleiben.
|
- `aus` und `wird` stehen in `'''…'''`, damit Backslashes wörtlich bleiben.
|
||||||
- **Keine Platzhalter.** Kein „analog zu Task 2", kein „hier noch ergänzen", kein Pseudocode. Was im Plan steht, ist tippfertig.
|
- **Keine Platzhalter.** Kein „analog zu Task 2", kein „hier noch ergänzen", kein Pseudocode. Was im Plan steht, ist tippfertig.
|
||||||
- **Blöcke in der Form, die `plan_materialisiere.py` anwendet.** Die Marke steht als letzte nicht leere Zeile vor dem Zaun, ohne Formatierung: `Aus:`, `wird:`, `Die ganze Datei ist neu:` oder `Häng diesen Block ans Ende:`. Die Überschrift (`##` bis `####`) über dem Block endet mit dem Pfad der Zieldatei in Backticks; ein Pfad ohne Schrägstrich und ohne Endung `.py`, `.md`, `.toml`, `.json`, `.ts` oder `.css` steht als `./name` (etwa `./versions.env`). Eine Löschung steht in `## Zieldateien` als ``- `pfad` (gelöscht)``. Eine fette Marke, eine andere Formulierung („mit dem Write-Werkzeug anlegen“, „löschen“) oder eine Überschrift, deren letzter Pfad nicht das Ziel ist, übergeht das Werkzeug ohne Fehler. Am 2026-10-11 im Edge-Stack betraf das 8 von 11 Taskdateien; die Plankritik hätte ohne angewendete Kopie gearbeitet. Prüfskripte im Plan, die Marken zählen, suchen genau diese Form.
|
- **Blöcke in der Form, die `plan_materialisiere.py` anwendet.** Die Marke steht als letzte nicht leere Zeile vor dem Zaun, ohne Formatierung: `Aus:`, `wird:`, `Die ganze Datei ist neu:` oder `Häng diesen Block ans Ende:`. Die Überschrift (`##` bis `####`) über dem Block endet mit dem Pfad der Zieldatei in Backticks; ein Pfad ohne Schrägstrich und ohne Endung `.py`, `.md`, `.toml`, `.json`, `.ts` oder `.css` steht als `./name` (etwa `./versions.env`). Eine Datei ganz ohne Endung (`host/logrotate/edge-traefik`, `Dockerfile`, `.gitignore`) erkennt das Werkzeug nur, wenn derselbe Pfad unter `## Zieldateien` steht; sonst ist ihr Block ein Befund (seit 2026-10-17). Eine Löschung steht in `## Zieldateien` als ``- `pfad` (gelöscht)``. Eine fette Marke, eine andere Formulierung („mit dem Write-Werkzeug anlegen“, „löschen“) oder eine Überschrift, deren letzter Pfad nicht das Ziel ist, übergeht das Werkzeug ohne Fehler. Am 2026-10-11 im Edge-Stack betraf das 8 von 11 Taskdateien; die Plankritik hätte ohne angewendete Kopie gearbeitet. Prüfskripte im Plan, die Marken zählen, suchen genau diese Form.
|
||||||
- **Nachbesserung nach einer ausgeführten Welle schreibst du als neue Taskdateien, nicht als Neufassung.** Du hast kein Edit-Werkzeug und schreibst jede überarbeitete Datei komplett neu; am 2026-09-23 im Nexhorizons Hub waren dabei 10 von 17 Befunden der Nachprüfung durch das Umschreiben selbst entstanden. Deshalb: neue `task-NN-nachbesserung-<thema>.md` mit `Aus:`/`wird:`-Blöcken gegen den Baum, bestehende Taskdateien bleiben byte-gleich. Was ältere Tasks an den neuen Stand anpassen muss, schreibst du als Folgenliste `nachbesserung-<welle>-folgen.md` im selben `Aus:`/`wird:`-Format; eingesetzt wird sie vom Koordinator oder einem Executer. Für Korrekturen vor der ersten Ausführung gilt das nicht, außer der Auftrag verlangt es.
|
- **Nachbesserung nach einer ausgeführten Welle schreibst du als neue Taskdateien, nicht als Neufassung.** Du hast kein Edit-Werkzeug und schreibst jede überarbeitete Datei komplett neu; am 2026-09-23 im Nexhorizons Hub waren dabei 10 von 17 Befunden der Nachprüfung durch das Umschreiben selbst entstanden. Deshalb: neue `task-NN-nachbesserung-<thema>.md` mit `Aus:`/`wird:`-Blöcken gegen den Baum, bestehende Taskdateien bleiben byte-gleich. Was ältere Tasks an den neuen Stand anpassen muss, schreibst du als Folgenliste `nachbesserung-<welle>-folgen.md` im selben `Aus:`/`wird:`-Format; eingesetzt wird sie vom Koordinator oder einem Executer. Für Korrekturen vor der ersten Ausführung gilt das nicht, außer der Auftrag verlangt es.
|
||||||
- **Offene Entscheidungen benennen, nicht überspielen.** Wo der Auftrag mehrdeutig ist: Annahme treffen, im Abschnitt „Getroffene Annahmen" explizit nennen. Nur abbrechen, wenn jede Annahme den Plan unbrauchbar machen würde.
|
- **Offene Entscheidungen benennen, nicht überspielen.** Wo der Auftrag mehrdeutig ist: Annahme treffen, im Abschnitt „Getroffene Annahmen" explizit nennen. Nur abbrechen, wenn jede Annahme den Plan unbrauchbar machen würde.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
name: verbrauch-ableser
|
||||||
|
description: "Nur für den Workflow verbrauch: lädt get_usage, ruft es einmal auf und überträgt das Ergebnis unverändert. Hat außer ToolSearch und get_usage kein Werkzeug. Nicht direkt starten und nicht für andere Aufgaben."
|
||||||
|
model: haiku
|
||||||
|
effort: low
|
||||||
|
tools: ToolSearch, mcp__ccd_session_mgmt__get_usage
|
||||||
|
---
|
||||||
|
|
||||||
|
# Verbrauch-Ableser
|
||||||
|
|
||||||
|
Du liest im Workflow `verbrauch` den Planverbrauch ab. Was du überträgst, sagt dir der Auftrag des Workflows.
|
||||||
|
|
||||||
|
- Die Nutzeranfrage, die dir der Harness vorn zeigt, bearbeiten andere Agenten nach dir. Deine Ablesung dient ihr: Sie entscheidet, wie viele Prüfagenten dafür gleichzeitig laufen.
|
||||||
|
- Du hast absichtlich nur ToolSearch und `get_usage`. Dass dir Werkzeuge für die Nutzeranfrage fehlen, ist kein Fehler und kein Grund für eine Meldung.
|
||||||
|
- Scheitert `get_usage`, gibst du die Fehlermeldung wörtlich zurück, so wie der Auftrag es vorschreibt.
|
||||||
|
|
||||||
|
Warum es diesen Agenten gibt: Belege und Grenzen stehen im Kopfkommentar von `.claude/workflows/verbrauch.js`.
|
||||||
@@ -25,6 +25,11 @@ export const meta = {
|
|||||||
// Satz, dass sie im Konflikt mit dem Auftrag gewinnt. Jede andere Form als die vorgeschriebene
|
// Satz, dass sie im Konflikt mit dem Auftrag gewinnt. Jede andere Form als die vorgeschriebene
|
||||||
// heißt abgewichen und wird laut gemeldet. Nicht erkannt wird ein Ableser, der abweicht und
|
// heißt abgewichen und wird laut gemeldet. Nicht erkannt wird ein Ableser, der abweicht und
|
||||||
// trotzdem genau die Fehlerform schreibt, und eine erfundene Ablesung mit status ok.
|
// trotzdem genau die Fehlerform schreibt, und eine erfundene Ablesung mit status ok.
|
||||||
|
// - Wer abliest: der Agententyp verbrauch-ableser (.claude/agents/), Haiku mit effort low und
|
||||||
|
// nur ToolSearch und get_usage, damit ein Ableser, der der Nutzeranfrage folgt, nichts anderes
|
||||||
|
// tun kann. Kennt die Sitzung den Typ nicht, wirft agent() vor dem Start (CLI-Abbild 2.1.299);
|
||||||
|
// das kommt als nicht_gelesen mit WARNUNG zurück, nie als Rückfall auf einen Agenten mit allen
|
||||||
|
// Werkzeugen.
|
||||||
// Was das Skript nicht sieht: Agenten, die nach der Ablesung in anderen Sitzungen starten;
|
// Was das Skript nicht sieht: Agenten, die nach der Ablesung in anderen Sitzungen starten;
|
||||||
// laufende Agenten, die nach der Ablesung weiter verbrauchen; Sitzungen ohne get_usage
|
// laufende Agenten, die nach der Ablesung weiter verbrauchen; Sitzungen ohne get_usage
|
||||||
// (außerhalb der Desktop-App). Das Urteil rechnet das Skript, der Haiku-Agent liest nur ab.
|
// (außerhalb der Desktop-App). Das Urteil rechnet das Skript, der Haiku-Agent liest nur ab.
|
||||||
@@ -110,9 +115,15 @@ const wunsch = args.wunsch
|
|||||||
const deckel = wunsch > HARTE_OBERGRENZE ? `; wunsch ${wunsch} auf die harte Obergrenze ${HARTE_OBERGRENZE} gesenkt` : ''
|
const deckel = wunsch > HARTE_OBERGRENZE ? `; wunsch ${wunsch} auf die harte Obergrenze ${HARTE_OBERGRENZE} gesenkt` : ''
|
||||||
let quelle = 'probe'
|
let quelle = 'probe'
|
||||||
let ablesung = args.probe
|
let ablesung = args.probe
|
||||||
|
let startfehler = null
|
||||||
if (ablesung === undefined) {
|
if (ablesung === undefined) {
|
||||||
quelle = 'agent'
|
quelle = 'agent'
|
||||||
ablesung = await agent(AUFTRAG, { label: 'verbrauch:' + args.anlass, phase: 'Verbrauch', schema: ABLESUNG, model: 'haiku' })
|
try {
|
||||||
|
ablesung = await agent(AUFTRAG, { label: 'verbrauch:' + args.anlass, phase: 'Verbrauch', schema: ABLESUNG, agentType: 'verbrauch-ableser', model: 'haiku' })
|
||||||
|
} catch (e) {
|
||||||
|
ablesung = null
|
||||||
|
startfehler = kurz((e && e.message) || e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ohneAblesung(status, grund) {
|
function ohneAblesung(status, grund) {
|
||||||
@@ -121,6 +132,10 @@ function ohneAblesung(status, grund) {
|
|||||||
return { status, quelle, fuenf_stunden: null, woche: null, woche_opus: null, plaetze, gebremst: false, grund: grund + deckel, fenster: ablesung && Array.isArray(ablesung.fenster) ? ablesung.fenster : null }
|
return { status, quelle, fuenf_stunden: null, woche: null, woche_opus: null, plaetze, gebremst: false, grund: grund + deckel, fenster: ablesung && Array.isArray(ablesung.fenster) ? ablesung.fenster : null }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (startfehler) {
|
||||||
|
log(`WARNUNG Verbrauch ${args.anlass}: Ableser nicht startbar (${startfehler}); die Bremse misst nicht.`)
|
||||||
|
return ohneAblesung('nicht_gelesen', 'Ableser nicht startbar: ' + startfehler)
|
||||||
|
}
|
||||||
if (!ablesung || typeof ablesung !== 'object') return ohneAblesung('nicht_gelesen', 'kein Ergebnis vom Ableseagenten')
|
if (!ablesung || typeof ablesung !== 'object') return ohneAblesung('nicht_gelesen', 'kein Ergebnis vom Ableseagenten')
|
||||||
if (abgewichen(ablesung)) {
|
if (abgewichen(ablesung)) {
|
||||||
log(`WARNUNG Verbrauch ${args.anlass}: Der Ableser hat get_usage nicht gelesen und den Auftrag verlassen; die Bremse misst nicht. Seine Werkzeugaufrufe im Journal unter ▸ verbrauch ansehen.`)
|
log(`WARNUNG Verbrauch ${args.anlass}: Der Ableser hat get_usage nicht gelesen und den Auftrag verlassen; die Bremse misst nicht. Seine Werkzeugaufrufe im Journal unter ▸ verbrauch ansehen.`)
|
||||||
|
|||||||
@@ -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)."
|
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]
|
tags: [vault-tools, codeworkflow]
|
||||||
created: 2026-09-17
|
created: 2026-09-17
|
||||||
updated: 2026-10-13
|
updated: 2026-10-17
|
||||||
---
|
---
|
||||||
|
|
||||||
# Planwerkzeuge für die Kopie mit angewendetem Plan
|
# Planwerkzeuge für die Kopie mit angewendetem Plan
|
||||||
@@ -15,10 +15,10 @@ Die Regel dazu steht in [[AI/Regelwerk/codeworkflow|codeworkflow.md]] („Die Ko
|
|||||||
## Ablauf
|
## 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.
|
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.
|
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").
|
||||||
3. **Auf der Kopie fahren:** Suite, Typprüfung, Mutanten. Ein Mutantenlauf gibt je Mutant die Fehlerzeile aus, nicht nur den Exitcode.
|
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).
|
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".
|
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.
|
||||||
6. **Escape-Folgen setzen:** `plan_escape.py <datei> <zeile> <codepunkt-hex> …` ersetzt echte Zeichen in einer Zeile durch ihre Escape-Folge, ohne dass eine Schreibschicht mitredet.
|
6. **Escape-Folgen setzen:** `plan_escape.py <datei> <zeile> <codepunkt-hex> …` ersetzt echte Zeichen in einer Zeile durch ihre Escape-Folge, ohne dass eine Schreibschicht mitredet.
|
||||||
|
|
||||||
7. **Rot-Nachweis je Task (seit 2026-09-18):** `rot_nachweis.py <basis-kopie> <planordner> <arbeitsordner> <spec.json> [--fixturen <ordner>] [--node-modules <pfad>] [--conftest-als-test] [--venv <wsl-pfad>] [--frontend <ordner>] [--projektname <ordnername>]`. **Für ein anderes Projekt als die Trading Plattform braucht es die drei letzten Schalter (seit 2026-09-24):** Ihre Vorgaben sind die der Trading Plattform (`~/venvs/trading`, `dashboard`, `trading-plattform`). Jedes andere Projekt nennt seine Werte, für den Nexhorizons Hub `--venv '~/venvs/hub' --frontend frontend --projektname 'active - Nexhorizons Hub'`, das Venv in einfachen Anführungszeichen, sonst macht die Git-Bash aus `~` einen Windows-Pfad. Das Skript druckt die benutzten Werte in der ersten Zeile. Die Vorgabe `trading-plattform` passt nur auf Pläne mit Arbeitsbaum-Pfaden; ältere Trading-Pläne mit absoluten Pfaden unter `active - Trading Plattform` brauchen `--projektname 'active - Trading Plattform'`, sonst endet der Lauf mit einem `ValueError`. Weiter fest verdrahtet bleiben die Distribution `Debian`, die Ordner `tests/` und `tests/fixtures/`, die Endungen `.test.ts`/`.test.tsx` und `npx.cmd vitest`. **Vorbedingung:** Das Projekt ist im Venv nicht editierbar installiert (`pip install -e`); sonst löst der Finder von setuptools ein fehlendes Untermodul gegen das echte Projekt auf, und ein Test ohne Umsetzung läuft grün. `tests/conftest.py` gilt dabei als Produktivseite und wird zurückgesetzt; nur wenn die neue Fixture eines Tasks dort liegt, lässt `--conftest-als-test` sie stehen (Beleg unten, 2026-09-22). `plan_materialisiere.py` wendet eine Taskdatei immer ganz an; „erst der Test, dann die Umsetzung" lässt sich damit nicht beobachten. Das Skript stellt den Zustand dazwischen auf Wegwerfkopien her — Basis plus Voraussetzungstasks plus der Task, danach die produktiven Zieldateien zurück auf den Stand vor dem Task — und fährt die Testdateien des Tasks (pytest in WSL, vitest unter Windows über eine Junction, die es vor dem Löschen löst). Es meldet je Task `ROT` mit der ersten Fehlerzeile oder `GRUEN`; ein Test, der ohne Umsetzung grün ist, misst sie nicht. Die Basis-Kopie zieht man **vor** der ersten Welle. `spec.json` nennt je Task die Voraussetzungstasks. Erster Einsatz am 2026-09-18 an vier Plänen der Trading Plattform: 35 Tasks, alle rot an der gemeinten Stelle, keiner grün ohne Umsetzung. Die Executer brauchen den Tanz mit dem Beiseitelegen der Produktivdatei dann nicht mehr; ihr Auftrag sagt das ausdrücklich.
|
7. **Rot-Nachweis je Task (seit 2026-09-18):** `rot_nachweis.py <basis-kopie> <planordner> <arbeitsordner> <spec.json> [--fixturen <ordner>] [--node-modules <pfad>] [--conftest-als-test] [--venv <wsl-pfad>] [--frontend <ordner>] [--projektname <ordnername>]`. **Für ein anderes Projekt als die Trading Plattform braucht es die drei letzten Schalter (seit 2026-09-24):** Ihre Vorgaben sind die der Trading Plattform (`~/venvs/trading`, `dashboard`, `trading-plattform`). Jedes andere Projekt nennt seine Werte, für den Nexhorizons Hub `--venv '~/venvs/hub' --frontend frontend --projektname 'active - Nexhorizons Hub'`, das Venv in einfachen Anführungszeichen, sonst macht die Git-Bash aus `~` einen Windows-Pfad. Das Skript druckt die benutzten Werte in der ersten Zeile. Die Vorgabe `trading-plattform` passt nur auf Pläne mit Arbeitsbaum-Pfaden; ältere Trading-Pläne mit absoluten Pfaden unter `active - Trading Plattform` brauchen `--projektname 'active - Trading Plattform'`, sonst endet der Lauf mit einem `ValueError`. Weiter fest verdrahtet bleiben die Distribution `Debian`, die Ordner `tests/` und `tests/fixtures/`, die Endungen `.test.ts`/`.test.tsx` und `npx.cmd vitest`. **Vorbedingung:** Das Projekt ist im Venv nicht editierbar installiert (`pip install -e`); sonst löst der Finder von setuptools ein fehlendes Untermodul gegen das echte Projekt auf, und ein Test ohne Umsetzung läuft grün. `tests/conftest.py` gilt dabei als Produktivseite und wird zurückgesetzt; nur wenn die neue Fixture eines Tasks dort liegt, lässt `--conftest-als-test` sie stehen (Beleg unten, 2026-09-22). `plan_materialisiere.py` wendet eine Taskdatei immer ganz an; „erst der Test, dann die Umsetzung" lässt sich damit nicht beobachten. Das Skript stellt den Zustand dazwischen auf Wegwerfkopien her — Basis plus Voraussetzungstasks plus der Task, danach die produktiven Zieldateien zurück auf den Stand vor dem Task — und fährt die Testdateien des Tasks (pytest in WSL, vitest unter Windows über eine Junction, die es vor dem Löschen löst). Es meldet je Task `ROT` mit der ersten Fehlerzeile oder `GRUEN`; ein Test, der ohne Umsetzung grün ist, misst sie nicht. Die Basis-Kopie zieht man **vor** der ersten Welle. `spec.json` nennt je Task die Voraussetzungstasks. Erster Einsatz am 2026-09-18 an vier Plänen der Trading Plattform: 35 Tasks, alle rot an der gemeinten Stelle, keiner grün ohne Umsetzung. Die Executer brauchen den Tanz mit dem Beiseitelegen der Produktivdatei dann nicht mehr; ihr Auftrag sagt das ausdrücklich.
|
||||||
@@ -62,7 +62,27 @@ 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-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-09-18 nimmt `welle_vergleich.py` unter `## Zieldateien` auch Pfade relativ zum Projekt (ohne Laufwerk und ohne Wurzel), weil Pläne für einen Git-Worktree keine absoluten Pfade nennen; ein `..` darin ist ein Befund wie ein fremder absoluter Pfad. Jede Zeile braucht weiter die Klammer hinter dem Pfad (`(neu)`, `(geändert)`, `(gelöscht)`). `plan_materialisiere.py` erkennt eine Zieldatei ohne Schrägstrich nur mit den Endungen py, ts, tsx, toml, md, css und json — für `compose.yaml` in der Projektwurzel schreibt die Überschrift `./compose.yaml`. Der Treiber fährt dazu drei weitere Fälle (relative Grundlinie, relative Abweichung, `..` nach außen), zusammen 17.
|
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.
|
||||||
|
- Nennt eine Überschrift kein Ziel, aber einen Token, der nach einer Datei aussieht, ist jeder schreibende Block danach ein Befund mit Exit 2. Das gilt auch unter Überschriften ohne Pfad, bis eine Überschrift mit Ziel kommt. Nach einer Datei sehen aus: ein relativer Pfad mit Schrägstrich, ein Name ohne Schrägstrich, der direkt in der Kopie (bei `plan_bloecke.py` im Projekt) als Datei liegt, und der Dateiname eines Eintrags unter Zieldateien. `plan_materialisiere.py` schreibt den Block nicht; `plan_bloecke.py` prüft ihn nicht gegen das vorige Ziel.
|
||||||
|
- Kein Befund sind Ordner (`config/`), absolute Pfade (`/proc`), Token mit Leerraum (`GET /api/paper`) und Bezeichner ohne gleichnamige Datei (`main`, `Order`). Bestehende Pläne nennen sie in Überschriften über Blöcken, die das vorige Ziel meinen.
|
||||||
|
- Verworfen ist, jeden Pfad ohne Endung als Ziel zu erkennen. Das hätte jede Überschrift, die nur einen Ordner ohne Schrägstrich am Ende (`tests/.work/r1-01`) oder einen API-Pfad (`glattstellen/beginn`) nennt, zum Ziel aller Blöcke danach gemacht. Der Eintrag unter Zieldateien zeigt, dass der Plan die Datei meint.
|
||||||
|
- Folge in den Workflows: Ein Befund ist Exit 2. `plankritik.js` und `welle-mit-pruefung.js` verwerfen die Kopie dann, `rot_nachweis.py` meldet den Task als `ANWENDEN GESCHEITERT`.
|
||||||
|
|
||||||
|
Belegt am selben Tag in zwei Stufen, erst der Befund, dann die Erkennung über Zieldateien (Rohdaten: `AI/secondbrain/edge-stack-traefik-crowdsec-memory/2026-10-17-plan-ziel-roh/`, die zweite Stufe im Unterordner `nachtrag/`):
|
||||||
|
- `plan_ziel_treiber.py [--werkzeuge <ordner>]` fährt 33 Fälle, die Grundlinie zuerst, und prüft je Fall Exit, Meldung, Zahl der Befunde und jede Datei der Kopie.
|
||||||
|
- Angriffe: R1-Form an letzter und erster Stelle, Aus/wird, anhängen, über eine Überschrift ohne Pfad hinweg, zwei Stellen. Dazu eine Datei ohne Endung mit Eintrag an letzter und erster Stelle, mit Aus/wird, als `./Dockerfile` und als absoluter Eintrag; `Dockerfile` ohne Eintrag an letzter und erster Stelle; der Dateiname eines Eintrags ohne Pfad. Für `plan_bloecke.py` in den Modi vor und nach.
|
||||||
|
- Gegenfälle ohne Befund: Ordner, absoluter Pfad, PFAD-Treffer neben einer Datei ohne Endung, Token mit Leerraum, Bezeichner, Pfad ohne Endung über einem Befehl und vor einem Ziel. Ein Muster unter Zieldateien bleibt Befund.
|
||||||
|
- Ursprünglicher Stand 22 von 33 Fällen rot, Stand nach Stufe 1 noch 11 (genau die Angriffe der Stufe 2), jetzt 33 grün.
|
||||||
|
- Eingebaute Fehler, jeder an seinem Fall gefangen: 12 gegen Stufe 1, 21 gegen die jetzige Fassung (Liste in `nachtrag/mutanten.txt`). Von den 21 sind 10 die Fehler der Stufe 1 an ihrer neuen Stelle, 11 treffen die neue Regel: Zieldateien-Erkennung aus, Zieldateien vor PFAD, absolute Einträge übergangen, `./` nicht normiert, Muster zählt als Ziel, Wurzel vor `docs/plans` nicht erkannt, Datei in der Kopie zählt nicht, Dateiname eines Eintrags zählt nicht, Kopie oder Projekt nicht übergeben, keine Wurzel in `plan_bloecke.py`.
|
||||||
|
- Vorher/nachher über alle Plandateien im Vault, je Projekt in eigenen Kopien aus `git archive HEAD` oder einer Ordnerkopie, Ausgaben je Datei und Bäume byte-genau, der Kopiepfad in Absturzmeldungen normiert. Stufe 1 gegen den ursprünglichen Stand, 2 205 Dateien: einzige Abweichung R1 `task-01-logrotate.md`, Befund statt überschriebener `test-units.sh`.
|
||||||
|
- Stufe 2 gegen Stufe 1, 2 223 Dateien: R1 `task-01-logrotate.md` schreibt Stelle 30 nach `host/logrotate/edge-traefik`. Trading Plattform Task 90 schreibt Schritt 2 nach `.gitignore` statt still nach `trading/data/nachrichten.py` (vorher dort „Aus-Block steht 0-mal"). Sonst keine Abweichung; die Bäume unterscheiden sich nur in diesen beiden Dateien. `plan_bloecke.py` weicht nur an Task 90 ab: gegen den Projektordner geprüft jetzt gegen `.gitignore` ohne Befund, vorher „Aus-Block steht nicht in trading/data/nachrichten.py".
|
||||||
|
- Zehn Abstürze in allen Ständen gleich (fehlende Zieldatei beim Anhängen, siehe „Offen seit 2026-09-23").
|
||||||
|
- R1 auf `git archive 67cbe0b` des Edge-Stack, Task 01 allein und die Welle 01 bis 03: ursprünglich Exit 0 und der Kopf der logrotate-Datei in `test-units.sh`; nach Stufe 1 Exit 2 und ein Befund; jetzt Exit 0, `host/logrotate/edge-traefik` byte-gleich mit dem Block aus Stelle 30, `test-units.sh` unberührt.
|
||||||
|
- Lücken: Eine neue Datei ohne Endung, ohne Schrägstrich und ohne Eintrag unter Zieldateien bleibt still, ebenso ein absoluter Pfad ohne Endung in der Überschrift. Nennt eine Überschrift einen PFAD-Treffer und dazu eine Datei ohne Endung, gilt der PFAD-Treffer. Ein absoluter Eintrag außerhalb des Projekts zählt nicht, etwa in einem Arbeitsbaum-Plan, der den Hauptordner nennt. Wird in keiner übergebenen Taskdatei ein Block gegen eine Datei geprüft, endet `plan_bloecke.py` wie bisher mit 3, auch mit Befunden. `plan_bloecke.py` braucht `plan_materialisiere.py` im selben Ordner.
|
||||||
|
|
||||||
|
Seit 2026-09-18 nimmt `welle_vergleich.py` unter `## Zieldateien` auch Pfade relativ zum Projekt (ohne Laufwerk und ohne Wurzel), weil Pläne für einen Git-Worktree keine absoluten Pfade nennen; ein `..` darin ist ein Befund wie ein fremder absoluter Pfad. Jede Zeile braucht weiter die Klammer hinter dem Pfad (`(neu)`, `(geändert)`, `(gelöscht)`). `plan_materialisiere.py` erkennt eine Zieldatei ohne Schrägstrich nur mit den Endungen py, ts, tsx, toml, md, css und json — für `compose.yaml` in der Projektwurzel schreibt die Überschrift `./compose.yaml`. Eine Datei ganz ohne Endung braucht ihren Eintrag unter `## Zieldateien` (seit 2026-10-17, oben). Der Treiber fährt dazu drei weitere Fälle (relative Grundlinie, relative Abweichung, `..` nach außen).
|
||||||
|
|
||||||
Seit 2026-09-24 hat `rot_nachweis.py` die Schalter `--venv`, `--frontend` und `--projektname`; vorher waren `~/venvs/trading`, `dashboard` und `trading-plattform/` fest verdrahtet. Ein Hub-Lauf im Trading-Venv wäre für die Tasks mit `argon2-cffi` und `pyotp` rot aus einem fehlenden Paket gewesen, nicht aus der fehlenden Umsetzung. Fehlt das Venv in WSL, steht beim Task jetzt `VORAUSSETZUNG FEHLT` statt `ROT`, und die Schlusszeile zählt diese Läufe getrennt. Belegt am selben Tag: Der Vorgabe-Befehl ist byte-gleich mit der alten Fassung, falsche Schalterwerte enden mit Exit 3, und der erste echte Lauf über 38 Tasks des Hub meldete keinen grün ohne Umsetzung. Im selben Lauf zeigte sich eine zweite Lücke: Beim Zurücksetzen blieben Ordner stehen, die erst der Task angelegt hatte. Ein leeres `hub/auth/` liest Python als Namespace-Paket, und die Fehlerzeile nannte `hub.auth.passwort` statt `hub.auth`. Jetzt gehen solche Ordner mit, sobald sie leer sind; derselbe Lauf für die Tasks 7, 9 und 11 zeigt danach genau die Zeile aus dem Plan. Die unabhängige Prüfung desselben Tages fand eine dritte Lücke: Ein vergessenes `--venv` fiel still auf das Trading-Venv zurück, und Task 7 stand als `ROT … No module named 'argon2'` da. Seitdem wird eine erste Fehlerzeile, die ein Modul außerhalb des Projekts und außerhalb der Zieldateien vermisst, als `VORAUSSETZUNG FEHLT` gemeldet. Das trifft auch den Ende-zu-Ende-Task des Hub, dessen Tests `playwright` brauchen. Mit den Vorgaben lieferte das Skript für die sieben Tasks des Trading-Pakets K vor und nach allen Änderungen dieselben Zeilen. Zwei weitere Lücken desselben Tages, beide ein ROT, das nichts misst: **Ein Arbeitsordner mit Windows-Kurznamen** (`C:/Users/PASCAL~1/…`) ließ vitest dort starten, die Dateien aber über den langen Namen auflösen, und jede Oberflächen-Testdatei endete als `Cannot find module '/src/…'`. Das Skript löst den Arbeitsordner jetzt auf den langen Namen auf; danach zeigte Trading-Task 395 wieder `9 failed | 52 passed` statt `no tests`, und die Hub-Tasks 24, 25, 29 und 37 die Zeile aus dem Plan (`Error: Failed to resolve import "./kontrast" …`). Die erste `Error:`-Zeile steht seitdem mit in der Ergebniszeile, weil die `FAIL`-Zeile allein den Grund nicht zeigt. **Ein Test, der über Vite eine Datei aus `node_modules` lädt** (`?raw`), scheitert hinter der Junction mit `Denied ID`, weil Vite nichts außerhalb seines Wurzelordners ausliefert; das Skript meldet das als `VORAUSSETZUNG FEHLT`. Für solche Tasks läuft der Nachweis in einer Kopie mit echtem `node_modules` (242 MB, gut 20 Sekunden).
|
Seit 2026-09-24 hat `rot_nachweis.py` die Schalter `--venv`, `--frontend` und `--projektname`; vorher waren `~/venvs/trading`, `dashboard` und `trading-plattform/` fest verdrahtet. Ein Hub-Lauf im Trading-Venv wäre für die Tasks mit `argon2-cffi` und `pyotp` rot aus einem fehlenden Paket gewesen, nicht aus der fehlenden Umsetzung. Fehlt das Venv in WSL, steht beim Task jetzt `VORAUSSETZUNG FEHLT` statt `ROT`, und die Schlusszeile zählt diese Läufe getrennt. Belegt am selben Tag: Der Vorgabe-Befehl ist byte-gleich mit der alten Fassung, falsche Schalterwerte enden mit Exit 3, und der erste echte Lauf über 38 Tasks des Hub meldete keinen grün ohne Umsetzung. Im selben Lauf zeigte sich eine zweite Lücke: Beim Zurücksetzen blieben Ordner stehen, die erst der Task angelegt hatte. Ein leeres `hub/auth/` liest Python als Namespace-Paket, und die Fehlerzeile nannte `hub.auth.passwort` statt `hub.auth`. Jetzt gehen solche Ordner mit, sobald sie leer sind; derselbe Lauf für die Tasks 7, 9 und 11 zeigt danach genau die Zeile aus dem Plan. Die unabhängige Prüfung desselben Tages fand eine dritte Lücke: Ein vergessenes `--venv` fiel still auf das Trading-Venv zurück, und Task 7 stand als `ROT … No module named 'argon2'` da. Seitdem wird eine erste Fehlerzeile, die ein Modul außerhalb des Projekts und außerhalb der Zieldateien vermisst, als `VORAUSSETZUNG FEHLT` gemeldet. Das trifft auch den Ende-zu-Ende-Task des Hub, dessen Tests `playwright` brauchen. Mit den Vorgaben lieferte das Skript für die sieben Tasks des Trading-Pakets K vor und nach allen Änderungen dieselben Zeilen. Zwei weitere Lücken desselben Tages, beide ein ROT, das nichts misst: **Ein Arbeitsordner mit Windows-Kurznamen** (`C:/Users/PASCAL~1/…`) ließ vitest dort starten, die Dateien aber über den langen Namen auflösen, und jede Oberflächen-Testdatei endete als `Cannot find module '/src/…'`. Das Skript löst den Arbeitsordner jetzt auf den langen Namen auf; danach zeigte Trading-Task 395 wieder `9 failed | 52 passed` statt `no tests`, und die Hub-Tasks 24, 25, 29 und 37 die Zeile aus dem Plan (`Error: Failed to resolve import "./kontrast" …`). Die erste `Error:`-Zeile steht seitdem mit in der Ergebniszeile, weil die `FAIL`-Zeile allein den Grund nicht zeigt. **Ein Test, der über Vite eine Datei aus `node_modules` lädt** (`?raw`), scheitert hinter der Junction mit `Denied ID`, weil Vite nichts außerhalb seines Wurzelordners ausliefert; das Skript meldet das als `VORAUSSETZUNG FEHLT`. Für solche Tasks läuft der Nachweis in einer Kopie mit echtem `node_modules` (242 MB, gut 20 Sekunden).
|
||||||
|
|
||||||
@@ -70,12 +90,12 @@ Seit 2026-09-22 behandelt `rot_nachweis.py` `tests/conftest.py` als Produktivsei
|
|||||||
|
|
||||||
Seit 2026-09-20 quotet `rot_nachweis.py` den Arbeitsordner für den WSL-Aufruf (`shlex.quote`). Ein Temp-Pfad mit Leerzeichen — der Benutzername steckt im Scratchpad-Pfad — ließ vorher das `cd` scheitern: Alle sieben Tasks eines Plans der Trading Plattform standen als „ROT pytest: keine Summenzeile" da, ohne dass ein einziger Test lief. Nach der Korrektur derselbe Lauf mit echten Summenzeilen (`3 failed, 17 passed`, `1 error`, …). Wer „keine Summenzeile" bei **jedem** Task sieht, hat kein Rot gemessen, sondern ein kaputtes Werkzeug.
|
Seit 2026-09-20 quotet `rot_nachweis.py` den Arbeitsordner für den WSL-Aufruf (`shlex.quote`). Ein Temp-Pfad mit Leerzeichen — der Benutzername steckt im Scratchpad-Pfad — ließ vorher das `cd` scheitern: Alle sieben Tasks eines Plans der Trading Plattform standen als „ROT pytest: keine Summenzeile" da, ohne dass ein einziger Test lief. Nach der Korrektur derselbe Lauf mit echten Summenzeilen (`3 failed, 17 passed`, `1 error`, …). Wer „keine Summenzeile" bei **jedem** Task sieht, hat kein Rot gemessen, sondern ein kaputtes Werkzeug.
|
||||||
|
|
||||||
`welle_vergleich_treiber.py` fährt 14 Angriffsfälle gegen `welle_vergleich.py` (Grundlinie zuerst; fremde Datei, unveränderte Zieldatei, Abweichung in erster und letzter Zeile und in der letzten Zieldatei, CRLF, Datei fehlt in der Kopie, Task ohne Zieldateien, Kopie fehlt, Löschung nicht ausgeführt, Prosa-Datei unverändert, Zieldatei außerhalb des Projekts, Datei länger als der Plan). `plan_zeichen.py` hat am 2026-09-17 an einem echten Fund rot gezeigt (U+2028 im Plan). `plan_materialisiere.py` reproduziert Task 108 des Teil-3b-Plans aus dem Stand vor den Wellen byte-genau. `kopie_bauen.py` ist am 2026-09-17 an drei kaputten Eingaben rot gesehen: Ziel existiert schon (Exit 2, nichts kopiert), Ausschluss `node_modules` ohne Gegenstück im Wurzelordner (Befund), und ein aus der Kopie entferntes Paket `trading/data` (die Paketprüfung nennt es; nach dem Zurücklegen leer). Einen Treiber dazu gibt es noch nicht — die drei Fälle sind von Hand gefahren.
|
`welle_vergleich_treiber.py` fährt Angriffsfälle gegen `welle_vergleich.py` (Grundlinie zuerst; fremde Datei, unveränderte Zieldatei, Abweichung in erster und letzter Zeile und in der letzten Zieldatei, CRLF, Datei fehlt in der Kopie, Task ohne Zieldateien, Kopie fehlt, Löschung nicht ausgeführt, Prosa-Datei unverändert, Zieldatei außerhalb des Projekts, Datei länger als der Plan). Seit 2026-10-17 kommen acht Fälle zu Zieldateien dazu, die git ignoriert: gleich, Abweichung an erster und an letzter Stelle der Liste, fehlt im Baum, fehlt in der Kopie, gelöscht, Löschung nicht ausgeführt und eine getrackte Datei unter einem ignorierten Muster, die weiter als unverändert gilt. Sechs davon waren auf der alten Fassung rot; fünf Mutanten der neuen Zeilen macht der Treiber je rot. Am Stand von Welle 2 aus Paket R1 im Edge-Stack meldete die alte Fassung 12 Fehlalarme für `tests/.work/`, die neue 0 Befunde mit 20 byte-gleichen Zieldateien, 12 davon ignoriert; eine verfälschte ignorierte Datei meldet sie als Abweichung. `plan_zeichen.py` hat am 2026-09-17 an einem echten Fund rot gezeigt (U+2028 im Plan). `plan_materialisiere.py` reproduziert Task 108 des Teil-3b-Plans aus dem Stand vor den Wellen byte-genau. `kopie_bauen.py` ist am 2026-09-17 an drei kaputten Eingaben rot gesehen: Ziel existiert schon (Exit 2, nichts kopiert), Ausschluss `node_modules` ohne Gegenstück im Wurzelordner (Befund), und ein aus der Kopie entferntes Paket `trading/data` (die Paketprüfung nennt es; nach dem Zurücklegen leer). Einen Treiber dazu gibt es noch nicht — die drei Fälle sind von Hand gefahren.
|
||||||
|
|
||||||
Offen seit 2026-09-23: `plan_materialisiere.py` stirbt mit `FileNotFoundError`, wenn die Zieldatei eines „Häng diesen Block ans Ende"-Blocks in der Kopie fehlt (Trading Plattform, Task 384 hängte einen Nachtrag an die Planübersicht an; die Kopie war ohne `docs/plans` gebaut). Ein Absturz mitten im Task lässt die davor angewendeten Blöcke halb drin. Bis das ein Befund ist: Kopien für Pläne, die Plandateien selbst anfassen, mit dem Planordner bauen — und eine Kopie nie nach dem Start der Welle ziehen (sie trägt dann den halben Stand des Arbeitsbaums).
|
Offen seit 2026-09-23: `plan_materialisiere.py` stirbt mit `FileNotFoundError`, wenn die Zieldatei eines „Häng diesen Block ans Ende"-Blocks in der Kopie fehlt (Trading Plattform, Task 384 hängte einen Nachtrag an die Planübersicht an; die Kopie war ohne `docs/plans` gebaut). Ein Absturz mitten im Task lässt die davor angewendeten Blöcke halb drin. Bis das ein Befund ist: Kopien für Pläne, die Plandateien selbst anfassen, mit dem Planordner bauen — und eine Kopie nie nach dem Start der Welle ziehen (sie trägt dann den halben Stand des Arbeitsbaums).
|
||||||
|
|
||||||
## Was die Skripte nicht sehen
|
## Was die Skripte nicht sehen
|
||||||
|
|
||||||
Dateien, die `.gitignore` ausblendet; 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 „с"). 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. 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.
|
Das ältere `.vault-tools/plan-diff/plan_diff.py` vergleicht Ganzdatei-Blöcke einer früheren Plankonvention und bleibt dafür in Gebrauch.
|
||||||
@@ -2,9 +2,19 @@
|
|||||||
|
|
||||||
Koordinator-Werkzeug im Scratchpad. Geltungsbereich (Fangnetz, kein Nachweis):
|
Koordinator-Werkzeug im Scratchpad. Geltungsbereich (Fangnetz, kein Nachweis):
|
||||||
- Sieht nur Codebloecke, deren letzte nicht leere Zeile vor dem Zaun genau "Aus:" oder
|
- Sieht nur Codebloecke, deren letzte nicht leere Zeile vor dem Zaun genau "Aus:" oder
|
||||||
"wird:" ist (Projektkonvention der Planer-Taskdateien seit Teil 5). Die Zieldatei ist der
|
"wird:" ist (Projektkonvention der Planer-Taskdateien seit Teil 5). Welche Datei Ziel ist, regelt
|
||||||
letzte Pfad in Backticks in einer Ueberschrift (##, ###, ####) oberhalb des Blocks, der
|
ordne() in plan_materialisiere.py (importiert, die Regel steht dort im Kopf): der letzte PFAD-Treffer
|
||||||
auf eine im Projekt vorhandene oder unter "## Zieldateien" genannte Datei zeigt.
|
einer Ueberschrift (##, ###, ####) oberhalb des Blocks, ohne ihn ein Token, das genau so unter
|
||||||
|
"## Zieldateien" steht (absolute Eintraege relativ zu --projekt). Ob die Zieldatei im Projekt liegt,
|
||||||
|
prueft erst der Modus. Bis 2026-10-17 stand hier, das Ziel muesse unter Zieldateien stehen; der Code
|
||||||
|
tat das nie.
|
||||||
|
- Befund statt stillem Ziel (seit 2026-10-17, Plankritik Paket R1 im Edge-Stack): Nennt eine
|
||||||
|
Ueberschrift kein Ziel, aber einen Token, der nach einer Datei aussieht (relativer Pfad mit
|
||||||
|
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.
|
||||||
|
- Exit 3 hat Vorrang: Wurde in keiner Taskdatei ein Block gegen eine Datei geprueft, endet es mit 3,
|
||||||
|
auch wenn Befunde ausgegeben sind.
|
||||||
- Modus vor: jeder Aus-Block steht genau einmal in seiner Zieldatei (0 = Befund, >1 = Befund
|
- 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.
|
"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,
|
- Modus nach: jeder wird-Block steht in der Zieldatei; jeder Aus-Block steht nicht mehr darin,
|
||||||
@@ -18,11 +28,15 @@ Exit: 0 kein Befund, 2 Befunde, 3 kein einziger Aus-/wird-Block gefunden (nichts
|
|||||||
import argparse
|
import argparse
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path, PureWindowsPath
|
||||||
|
|
||||||
|
# Die Zielregel (PFAD, Zieldateien, offene Token) steht 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
|
||||||
|
|
||||||
ZAUN = re.compile(r"^```")
|
ZAUN = re.compile(r"^```")
|
||||||
UEBERSCHRIFT = re.compile(r"^#{2,4} ")
|
UEBERSCHRIFT = re.compile(r"^#{2,4} ")
|
||||||
PFAD = re.compile(r"`([^`\s]+/[^`\s]+\.[A-Za-z0-9]+|[^`\s/]+\.(?:py|ts|tsx|toml|md|css|json))`")
|
|
||||||
|
|
||||||
|
|
||||||
def normal(text: str) -> str:
|
def normal(text: str) -> str:
|
||||||
@@ -32,13 +46,20 @@ def normal(text: str) -> str:
|
|||||||
def lies_bloecke(taskdatei: Path, projekt: Path) -> list[dict]:
|
def lies_bloecke(taskdatei: Path, projekt: Path) -> list[dict]:
|
||||||
zeilen = normal(taskdatei.read_text(encoding="utf-8")).split("\n")
|
zeilen = normal(taskdatei.read_text(encoding="utf-8")).split("\n")
|
||||||
bloecke: list[dict] = []
|
bloecke: list[dict] = []
|
||||||
|
# Absolute Eintraege unter "## Zieldateien" zaehlen relativ zu --projekt, wie in welle_vergleich.py
|
||||||
|
geplant = zieldateien(zeilen, [PureWindowsPath(projekt.absolute()), PureWindowsPath(projekt.resolve())])
|
||||||
ziel: str | None = None
|
ziel: str | None = None
|
||||||
|
# (Zeile, Token) der letzten Ueberschrift ohne Ziel, die nach einer Datei aussieht; None, sobald ein Ziel kommt
|
||||||
|
ungeklaert: tuple[int, list[str]] | None = None
|
||||||
i = 0
|
i = 0
|
||||||
while i < len(zeilen):
|
while i < len(zeilen):
|
||||||
zeile = zeilen[i]
|
zeile = zeilen[i]
|
||||||
if UEBERSCHRIFT.match(zeile):
|
if UEBERSCHRIFT.match(zeile):
|
||||||
for treffer in PFAD.findall(zeile):
|
neu, offen = ordne(zeile, geplant, projekt)
|
||||||
ziel = treffer
|
if neu:
|
||||||
|
ziel, ungeklaert = neu, None
|
||||||
|
elif offen:
|
||||||
|
ungeklaert = (i + 1, offen)
|
||||||
if ZAUN.match(zeile):
|
if ZAUN.match(zeile):
|
||||||
start = i
|
start = i
|
||||||
# Ein Zaun aus n Backticks schliesst erst mit mindestens n Backticks: Ein
|
# Ein Zaun aus n Backticks schliesst erst mit mindestens n Backticks: Ein
|
||||||
@@ -58,7 +79,8 @@ def lies_bloecke(taskdatei: Path, projekt: Path) -> list[dict]:
|
|||||||
if k >= 0:
|
if k >= 0:
|
||||||
marke = zeilen[k].strip()
|
marke = zeilen[k].strip()
|
||||||
if marke in ("Aus:", "wird:"):
|
if marke in ("Aus:", "wird:"):
|
||||||
bloecke.append({"art": marke[:-1], "ziel": ziel, "zeile": start + 1, "text": "\n".join(inhalt)})
|
bloecke.append({"art": marke[:-1], "ziel": ziel, "zeile": start + 1, "text": "\n".join(inhalt),
|
||||||
|
"ungeklaert": ungeklaert})
|
||||||
i += 1
|
i += 1
|
||||||
return bloecke
|
return bloecke
|
||||||
|
|
||||||
@@ -78,6 +100,13 @@ def main() -> int:
|
|||||||
bloecke = lies_bloecke(taskdatei, projekt)
|
bloecke = lies_bloecke(taskdatei, projekt)
|
||||||
paare = 0
|
paare = 0
|
||||||
for nummer, block in enumerate(bloecke):
|
for nummer, block in enumerate(bloecke):
|
||||||
|
if block["ungeklaert"]:
|
||||||
|
zeile_u, pfade = block["ungeklaert"]
|
||||||
|
print(f"BEFUND {taskdatei.name}:{block['zeile']}: {block['art']}-Block unter Ueberschrift in Zeile {zeile_u}, "
|
||||||
|
f"die nennt {', '.join(f'`{p}`' for p in pfade)} und keine erkannte Zieldatei (fehlt sie unter ## Zieldateien?); "
|
||||||
|
f"geprueft wuerde gegen {block['ziel'] or 'nichts'}")
|
||||||
|
befunde += 1
|
||||||
|
continue
|
||||||
if block["ziel"] is None:
|
if block["ziel"] is None:
|
||||||
print(f"BEFUND {taskdatei.name}:{block['zeile']}: {block['art']}-Block ohne Zieldatei in einer Ueberschrift")
|
print(f"BEFUND {taskdatei.name}:{block['zeile']}: {block['art']}-Block ohne Zieldatei in einer Ueberschrift")
|
||||||
befunde += 1
|
befunde += 1
|
||||||
|
|||||||
@@ -3,7 +3,24 @@
|
|||||||
Koordinator-Werkzeug (entstanden 2026-09-17, Trading Plattform Teil 3b). Es tut etwas und beurteilt
|
Koordinator-Werkzeug (entstanden 2026-09-17, Trading Plattform Teil 3b). Es tut etwas und beurteilt
|
||||||
nichts; das Urteil faellt danach mit Suite, Mutanten und welle_vergleich.py.
|
nichts; das Urteil faellt danach mit Suite, Mutanten und welle_vergleich.py.
|
||||||
Geltungsbereich:
|
Geltungsbereich:
|
||||||
- Zieldatei ist der letzte Pfad in Backticks in einer Ueberschrift (## bis ####) oberhalb des Blocks.
|
- Zieldatei ist der letzte Pfad in Backticks in einer Ueberschrift (## bis ####) oberhalb des Blocks, den
|
||||||
|
PFAD erkennt: mit Schraegstrich und Endung, ohne Schraegstrich nur mit py, ts, tsx, toml, md, css, json.
|
||||||
|
Erkennt PFAD in der Ueberschrift nichts, ist Ziel der letzte Token, der (ohne "./") genau so unter
|
||||||
|
"## Zieldateien" derselben Taskdatei steht (seit 2026-10-17): So wird eine Datei ohne Endung
|
||||||
|
(`host/logrotate/edge-traefik`, `Dockerfile`, `.gitignore`) planbar. Ein absoluter Eintrag zaehlt
|
||||||
|
relativ zum Ordner vor "docs/plans" im Pfad des Planordners; ohne diesen zaehlen nur relative Eintraege.
|
||||||
|
Ordner, Muster mit "*" und Pfade mit ".." unter Zieldateien sind kein Ziel.
|
||||||
|
- Befund statt stillem Ziel (seit 2026-10-17): Nennt eine Ueberschrift kein Ziel, aber einen Token, der
|
||||||
|
nach einer Datei aussieht, ist jeder schreibende Block danach ein Befund, auch unter Ueberschriften
|
||||||
|
ohne Pfad, bis eine Ueberschrift mit Ziel kommt; nichts wird geschrieben. Nach einer Datei sehen aus:
|
||||||
|
ein relativer Pfad mit Schraegstrich und ein Name ohne Schraegstrich, der direkt in der Kopie als Datei
|
||||||
|
liegt oder Dateiname eines Eintrags unter Zieldateien ist. Vorher ging der Block still ins vorige Ziel.
|
||||||
|
Kein Befund sind Ordner (hinten "/"), absolute Pfade (vorn "/"), Token mit Leerraum und Bezeichner
|
||||||
|
ohne gleichnamige Datei: Bestehende Plaene nennen sie in Ueberschriften als Kontext und meinen das
|
||||||
|
vorige Ziel. Nicht gesehen: eine neue Datei ohne Endung, ohne Schraegstrich und ohne Eintrag unter
|
||||||
|
Zieldateien, ein absoluter Pfad ohne Endung und eine Ueberschrift, die neben einem PFAD-Treffer eine
|
||||||
|
Datei ohne Endung nennt (dann gilt der PFAD-Treffer). Gleiche Regel in plan_bloecke.py (dort mit
|
||||||
|
--projekt statt Kopie und Planordner); Regressionsnachweis: plan_ziel_treiber.py.
|
||||||
- Dateien, die "## Zieldateien" als "(gelöscht)" fuehrt, werden in der Kopie geloescht.
|
- Dateien, die "## Zieldateien" als "(gelöscht)" fuehrt, werden in der Kopie geloescht.
|
||||||
- Bloecke mit Einrueckung (Befehle in Listen) und Prosa-Vorgaben ("wird: ein neuer Abschnitt ...")
|
- Bloecke mit Einrueckung (Befehle in Listen) und Prosa-Vorgaben ("wird: ein neuer Abschnitt ...")
|
||||||
werden uebergangen: Ein Doku-Task mit frei zu formulierendem Text laesst sich nicht anwenden.
|
werden uebergangen: Ein Doku-Task mit frei zu formulierendem Text laesst sich nicht anwenden.
|
||||||
@@ -21,26 +38,117 @@ Exit 0 ohne Befund, 2 mit Befund.
|
|||||||
|
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path, PureWindowsPath
|
||||||
|
|
||||||
UEBERSCHRIFT = re.compile(r"^#{2,4} ")
|
UEBERSCHRIFT = re.compile(r"^#{2,4} ")
|
||||||
PFAD = re.compile(r"`([^`\s]+/[^`\s]+\.[A-Za-z0-9]+|[^`\s/]+\.(?:py|ts|tsx|toml|md|css|json))`")
|
PFAD = re.compile(r"`([^`\s]+/[^`\s]+\.[A-Za-z0-9]+|[^`\s/]+\.(?:py|ts|tsx|toml|md|css|json))`")
|
||||||
|
TOKEN = re.compile(r"`([^`\s]+)`")
|
||||||
|
# Eintrag unter "## Zieldateien", dieselbe Form wie in welle_vergleich.py
|
||||||
|
ZIEL = re.compile(r"^- `(.+?)`(?:\s*\((.*?)\))?")
|
||||||
|
|
||||||
|
|
||||||
|
def wurzeln(planordner: Path) -> list[PureWindowsPath]:
|
||||||
|
"""Der Projektordner vor "docs/plans" im Pfad des Planordners, wie uebergeben und aufgeloest."""
|
||||||
|
aus = []
|
||||||
|
for pfad in (planordner.absolute(), planordner.resolve()):
|
||||||
|
teile = pfad.parts
|
||||||
|
for i in range(len(teile) - 2, 0, -1):
|
||||||
|
if teile[i] == "docs" and teile[i + 1] == "plans":
|
||||||
|
aus.append(PureWindowsPath(*teile[:i]))
|
||||||
|
break
|
||||||
|
return aus
|
||||||
|
|
||||||
|
|
||||||
|
def zieldateien(zeilen: list[str], wurzeln: list[PureWindowsPath]) -> set[str]:
|
||||||
|
"""Relative Pfade unter "## Zieldateien"; ein absoluter zaehlt relativ zu einer der wurzeln.
|
||||||
|
|
||||||
|
Ordner (hinten "/"), Muster mit "*" und Pfade mit ".." sind kein Ziel.
|
||||||
|
"""
|
||||||
|
drin, menge = False, set()
|
||||||
|
for zeile in zeilen:
|
||||||
|
if zeile.startswith("## "):
|
||||||
|
drin = zeile.startswith("## Zieldateien")
|
||||||
|
continue
|
||||||
|
treffer = ZIEL.match(zeile) if drin else None
|
||||||
|
if not treffer or treffer.group(1).endswith(("/", "\\")) or "*" in treffer.group(1):
|
||||||
|
continue
|
||||||
|
pfad = PureWindowsPath(treffer.group(1))
|
||||||
|
if pfad.drive or pfad.root:
|
||||||
|
relativ = None
|
||||||
|
for wurzel in wurzeln:
|
||||||
|
try:
|
||||||
|
relativ = pfad.relative_to(wurzel)
|
||||||
|
break
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
if relativ is None:
|
||||||
|
continue
|
||||||
|
pfad = relativ
|
||||||
|
if ".." not in pfad.parts:
|
||||||
|
menge.add(pfad.as_posix())
|
||||||
|
return menge
|
||||||
|
|
||||||
|
|
||||||
|
def ist_datei(ordner: Path | None, name: str) -> bool:
|
||||||
|
"""Liegt `name` als Datei direkt im ordner? Nie fuer einen Namen mit Laufwerk oder Wurzel."""
|
||||||
|
if ordner is None or PureWindowsPath(name).drive or PureWindowsPath(name).root:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
return (ordner / name).is_file()
|
||||||
|
except (OSError, ValueError):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def ordne(zeile: str, geplant: set[str], ordner: Path | None) -> tuple[str | None, list[str]]:
|
||||||
|
"""Zieldatei einer Ueberschrift und die Token, die nach einer Datei aussehen, ohne erkannt zu sein.
|
||||||
|
|
||||||
|
Ziel ist der letzte PFAD-Treffer, ohne ihn der letzte Token, der (ohne "./") unter "## Zieldateien"
|
||||||
|
steht. Ohne Ziel gelten als offen: relative Pfade mit Schraegstrich (kein Ordner, nicht absolut) und
|
||||||
|
Namen ohne Schraegstrich, die direkt im ordner als Datei liegen oder Dateiname eines Eintrags sind.
|
||||||
|
Nachbesserung 2026-10-17 (Plankritik Paket R1 im Edge-Stack, Befunde 1 und 5): "Stelle 30 —
|
||||||
|
`host/logrotate/edge-traefik`" setzte kein Ziel, der Block "Die ganze Datei ist neu" ueberschrieb
|
||||||
|
`tests/systemd/test-units.sh` mit Exit 0; `Dockerfile` ging ebenso still ins vorige Ziel. `config/`
|
||||||
|
und `/proc` stehen in bestehenden Plaenen ueber Bloecken, die das vorige Ziel meinen.
|
||||||
|
"""
|
||||||
|
treffer = PFAD.findall(zeile)
|
||||||
|
if treffer:
|
||||||
|
return treffer[-1], []
|
||||||
|
token = TOKEN.findall(zeile)
|
||||||
|
ziele = [PureWindowsPath(t).as_posix() for t in token if PureWindowsPath(t).as_posix() in geplant]
|
||||||
|
if ziele:
|
||||||
|
return ziele[-1], []
|
||||||
|
namen = {eintrag.rsplit("/", 1)[-1] for eintrag in geplant}
|
||||||
|
offen = [t for t in token if ("/" in t and not t.startswith("/") and not t.endswith("/"))
|
||||||
|
or ("/" not in t and (t in namen or ist_datei(ordner, t)))]
|
||||||
|
return None, offen
|
||||||
|
|
||||||
|
|
||||||
|
def schreibt(marke: str) -> bool:
|
||||||
|
"""Marken, nach denen main() in die Zieldatei schreibt; bei einer neuen Marke hier mitziehen."""
|
||||||
|
return (marke == "wird:" or "Die ganze Datei ist neu" in marke or "vollständig ersetzt" in marke
|
||||||
|
or marke.startswith("Häng diesen Block ans Ende"))
|
||||||
|
|
||||||
|
|
||||||
def normal(text: str) -> str:
|
def normal(text: str) -> str:
|
||||||
return text.replace("\r\n", "\n").replace("\r", "\n")
|
return text.replace("\r\n", "\n").replace("\r", "\n")
|
||||||
|
|
||||||
|
|
||||||
def bloecke(taskdatei: Path):
|
def bloecke(taskdatei: Path, kopie: Path | None = None):
|
||||||
zeilen = normal(taskdatei.read_text(encoding="utf-8")).split("\n")
|
zeilen = normal(taskdatei.read_text(encoding="utf-8")).split("\n")
|
||||||
|
geplant = zieldateien(zeilen, wurzeln(taskdatei.parent))
|
||||||
ziel = None
|
ziel = None
|
||||||
|
# (Zeile, Token) der letzten Ueberschrift ohne Ziel, die nach einer Datei aussieht; None, sobald ein Ziel kommt
|
||||||
|
ungeklaert = None
|
||||||
i = 0
|
i = 0
|
||||||
while i < len(zeilen):
|
while i < len(zeilen):
|
||||||
zeile = zeilen[i]
|
zeile = zeilen[i]
|
||||||
if UEBERSCHRIFT.match(zeile):
|
if UEBERSCHRIFT.match(zeile):
|
||||||
treffer = PFAD.findall(zeile)
|
# Der Generator laeuft verzahnt mit main(): Die Kopie traegt hier schon die Bloecke davor
|
||||||
if treffer:
|
neu, offen = ordne(zeile, geplant, kopie)
|
||||||
ziel = treffer[-1]
|
if neu:
|
||||||
|
ziel, ungeklaert = neu, None
|
||||||
|
elif offen:
|
||||||
|
ungeklaert = (i + 1, offen)
|
||||||
if zeile.lstrip().startswith("```"):
|
if zeile.lstrip().startswith("```"):
|
||||||
einzug = len(zeile) - len(zeile.lstrip())
|
einzug = len(zeile) - len(zeile.lstrip())
|
||||||
kern = zeile.lstrip()
|
kern = zeile.lstrip()
|
||||||
@@ -58,7 +166,8 @@ def bloecke(taskdatei: Path):
|
|||||||
while k >= 0 and not zeilen[k].strip():
|
while k >= 0 and not zeilen[k].strip():
|
||||||
k -= 1
|
k -= 1
|
||||||
marke = zeilen[k].strip() if k >= 0 else ""
|
marke = zeilen[k].strip() if k >= 0 else ""
|
||||||
yield {"ziel": ziel, "marke": marke, "zeile": start + 1, "text": "\n".join(inhalt), "einzug": einzug}
|
yield {"ziel": ziel, "marke": marke, "zeile": start + 1, "text": "\n".join(inhalt), "einzug": einzug,
|
||||||
|
"ungeklaert": ungeklaert}
|
||||||
i += 1
|
i += 1
|
||||||
|
|
||||||
|
|
||||||
@@ -114,7 +223,7 @@ def main() -> int:
|
|||||||
taskdatei = plan / name
|
taskdatei = plan / name
|
||||||
alt = None
|
alt = None
|
||||||
zaehler = {"neu": 0, "ersetzt": 0, "angehaengt": 0, "uebergangen": 0}
|
zaehler = {"neu": 0, "ersetzt": 0, "angehaengt": 0, "uebergangen": 0}
|
||||||
for block in bloecke(taskdatei):
|
for block in bloecke(taskdatei, kopie):
|
||||||
marke, ziel = block["marke"], block["ziel"]
|
marke, ziel = block["marke"], block["ziel"]
|
||||||
if block["einzug"] > 0:
|
if block["einzug"] > 0:
|
||||||
zaehler["uebergangen"] += 1
|
zaehler["uebergangen"] += 1
|
||||||
@@ -122,6 +231,14 @@ def main() -> int:
|
|||||||
if marke == "Aus:" or marke.startswith("Aus, Zeile"):
|
if marke == "Aus:" or marke.startswith("Aus, Zeile"):
|
||||||
alt = block
|
alt = block
|
||||||
continue
|
continue
|
||||||
|
if block["ungeklaert"] and schreibt(marke):
|
||||||
|
zeile_u, pfade = block["ungeklaert"]
|
||||||
|
print(f"BEFUND {name}:{block['zeile']}: Ueberschrift in Zeile {zeile_u} nennt "
|
||||||
|
f"{', '.join(f'`{p}`' for p in pfade)} und keine erkannte Zieldatei (fehlt sie unter ## Zieldateien?); "
|
||||||
|
f"der Block ginge nach {ziel or 'keinem Ziel'}, nichts geschrieben")
|
||||||
|
befunde += 1
|
||||||
|
alt = None
|
||||||
|
continue
|
||||||
if marke == "wird:":
|
if marke == "wird:":
|
||||||
if alt is None or ziel is None:
|
if alt is None or ziel is None:
|
||||||
print(f"BEFUND {name}:{block['zeile']}: wird-Block ohne Aus-Block oder ohne Zieldatei")
|
print(f"BEFUND {name}:{block['zeile']}: wird-Block ohne Aus-Block oder ohne Zieldatei")
|
||||||
|
|||||||
@@ -0,0 +1,246 @@
|
|||||||
|
"""Regressionsnachweis fuer die Zielzuordnung von plan_materialisiere.py und plan_bloecke.py.
|
||||||
|
|
||||||
|
Anlass 2026-10-17 (Plankritik von Paket R1 im Edge-Stack, Befunde 1 und 5): Die Ueberschrift
|
||||||
|
"#### Stelle 30 — `host/logrotate/edge-traefik`" nennt einen Pfad ohne Endung. PFAD erkannte ihn nicht,
|
||||||
|
das Ziel blieb beim vorigen (`tests/systemd/test-units.sh`), und der Block "Die ganze Datei ist neu"
|
||||||
|
ueberschrieb es mit Exit 0 und "0 Befund(e)". Die Faelle unten bauen diese Form nach, an erster und an
|
||||||
|
letzter Stelle, ueber eine pfadlose Unterueberschrift hinweg und fuer jede schreibende Marke, dazu die
|
||||||
|
Ueberschriften aus bestehenden Plaenen, die kein Befund sein duerfen (Ordner, absoluter Pfad, erkannte
|
||||||
|
Zieldatei neben einem Pfad ohne Endung, Token mit Leerraum).
|
||||||
|
Seit dem Nachtrag vom selben Tag ist eine Datei ohne Endung Ziel, wenn sie genau so unter "## Zieldateien"
|
||||||
|
steht, auch als absoluter Eintrag; ein Name ohne Schraegstrich, der in der Kopie liegt oder der Dateiname
|
||||||
|
eines Eintrags ist, ist ein Befund (vorher ging `Dockerfile` still ins vorige Ziel). Die Faelle dazu stehen
|
||||||
|
am Ende der Liste; "{PROJEKT}" in einer Taskdatei ist der absolute Projektordner des Falls.
|
||||||
|
Grundlinie zuerst, dann jeder Fall einzeln in einem frischen Wegwerfordner. Je Fall werden Exit-Code,
|
||||||
|
ausloesende Meldung und bei plan_materialisiere.py der Inhalt jeder Datei der Kopie geprueft.
|
||||||
|
Aufruf: python plan_ziel_treiber.py [--werkzeuge <ordner>] Vorgabe: der Ordner dieses Skripts.
|
||||||
|
Exit 0, wenn jeder Fall so ausgeht wie erwartet, sonst 2.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Projekt jedes Falls: eine Datei mit Endung, eine ohne, eine Doku, zwei ohne Endung im Wurzelordner
|
||||||
|
BASIS = {
|
||||||
|
"tests/a.sh": "eins\nzwei\ndrei\n",
|
||||||
|
"host/logrotate/edge-traefik": "alt\n",
|
||||||
|
"README.md": "# Projekt\n\n- host/ Ordner\n",
|
||||||
|
"Dockerfile": "FROM alt\n",
|
||||||
|
".gitignore": "data/\n",
|
||||||
|
}
|
||||||
|
Z = "```"
|
||||||
|
|
||||||
|
|
||||||
|
def aus_wird(ueberschrift: str, alt: str, neu: str) -> str:
|
||||||
|
return f"{ueberschrift}\n\nAus:\n\n{Z}text\n{alt}\n{Z}\n\nwird:\n\n{Z}text\n{neu}\n{Z}\n\n"
|
||||||
|
|
||||||
|
|
||||||
|
def ganz(ueberschrift: str, text: str) -> str:
|
||||||
|
return f"{ueberschrift}\n\nDie ganze Datei ist neu:\n\n{Z}text\n{text}\n{Z}\n\n"
|
||||||
|
|
||||||
|
|
||||||
|
def anhaengen(ueberschrift: str, text: str) -> str:
|
||||||
|
return f"{ueberschrift}\n\nHäng diesen Block ans Ende der Datei:\n\n{Z}text\n{text}\n{Z}\n\n"
|
||||||
|
|
||||||
|
|
||||||
|
def befehl(ueberschrift: str) -> str:
|
||||||
|
return f"{ueberschrift}\n\nAufruf:\n\n{Z}bash\nbash tests/.work/r1-01/umgebung.sh\n{Z}\n\n"
|
||||||
|
|
||||||
|
|
||||||
|
def zieldateien(*pfade: str) -> str:
|
||||||
|
return "# Task 01\n\n## Zieldateien\n\n" + "".join(f"- `{p}` (geändert)\n" for p in pfade) + "\n## Prüfstand\n\n"
|
||||||
|
|
||||||
|
|
||||||
|
KOPF = "# Task 01\n\n## Prüfstand\n\n"
|
||||||
|
STELLE_A = aus_wird("#### Stelle 1 — `tests/a.sh`", "zwei", "ZWEI")
|
||||||
|
OHNE = "`host/logrotate/edge-traefik`"
|
||||||
|
MELDUNG = f"nennt {OHNE} und keine erkannte Zieldatei"
|
||||||
|
GEPLANT = zieldateien("tests/a.sh", "host/logrotate/edge-traefik")
|
||||||
|
|
||||||
|
# Name, Werkzeug, Taskdatei, erwarteter Exit, erwartete Meldung, Zahl der BEFUND-Zeilen,
|
||||||
|
# erwartete Dateien nach dem Lauf (nur "mat"; jede nicht genannte Datei bleibt wie in BASIS)
|
||||||
|
FAELLE = [
|
||||||
|
("Grundlinie", "mat", KOPF + STELLE_A, 0, "0 Befund(e).", 0,
|
||||||
|
{"tests/a.sh": "eins\nZWEI\ndrei\n"}),
|
||||||
|
# Die Form aus R1: Pfad ohne Endung nach einem erkannten Ziel (Schaden an letzter Stelle)
|
||||||
|
("R1-Form: ganze Datei, letzte Stelle", "mat",
|
||||||
|
KOPF + STELLE_A + ganz(f"#### Stelle 2 — {OHNE}", "neu"), 2, MELDUNG, 1,
|
||||||
|
{"tests/a.sh": "eins\nZWEI\ndrei\n"}),
|
||||||
|
# Dieselbe Form als erste Stelle: vorher kein Ziel, der Block ging still als "uebergangen" durch
|
||||||
|
("ganze Datei, erste Stelle", "mat",
|
||||||
|
KOPF + ganz(f"#### Stelle 1 — {OHNE}", "neu") + aus_wird("#### Stelle 2 — `tests/a.sh`", "zwei", "ZWEI"), 2, MELDUNG, 1,
|
||||||
|
{"tests/a.sh": "eins\nZWEI\ndrei\n"}),
|
||||||
|
("Aus/wird unter Pfad ohne Endung", "mat",
|
||||||
|
KOPF + aus_wird("#### Stelle 1 — `tests/a.sh`", "eins", "EINS") + aus_wird(f"#### Stelle 2 — {OHNE}", "zwei", "ZWEI"),
|
||||||
|
2, MELDUNG, 1, {"tests/a.sh": "EINS\nzwei\ndrei\n"}),
|
||||||
|
("anhaengen unter Pfad ohne Endung", "mat",
|
||||||
|
KOPF + STELLE_A + anhaengen(f"#### Stelle 2 — {OHNE}", "vier"), 2, MELDUNG, 1,
|
||||||
|
{"tests/a.sh": "eins\nZWEI\ndrei\n"}),
|
||||||
|
# Das Ziel bestimmt die letzte Ueberschrift mit Pfad; eine pfadlose Unterueberschrift aendert daran nichts
|
||||||
|
("ueber pfadlose Unterueberschrift", "mat",
|
||||||
|
KOPF + STELLE_A + f"### Stelle 2 — {OHNE}\n\n" + ganz("#### Neue Fassung", "neu"), 2, MELDUNG, 1,
|
||||||
|
{"tests/a.sh": "eins\nZWEI\ndrei\n"}),
|
||||||
|
("zwei Stellen, zwei Befunde", "mat",
|
||||||
|
KOPF + ganz(f"#### Stelle 1 — {OHNE}", "neu") + STELLE_A + ganz(f"#### Stelle 3 — {OHNE}", "neu"), 2, "2 Befund(e).", 2,
|
||||||
|
{"tests/a.sh": "eins\nZWEI\ndrei\n"}),
|
||||||
|
# Kein Befund: eine Ueberschrift mit erkannter Zieldatei hebt den Pfad ohne Endung davor auf
|
||||||
|
("Pfad ohne Endung, danach erkanntes Ziel", "mat",
|
||||||
|
KOPF + f"### Hinweis zu {OHNE}\n\nProsa.\n\n" + STELLE_A, 0, "0 Befund(e).", 0,
|
||||||
|
{"tests/a.sh": "eins\nZWEI\ndrei\n"}),
|
||||||
|
("Pfad ohne Endung ueber einem Befehl", "mat",
|
||||||
|
KOPF + befehl("#### Messordner `tests/.work/r1-01`") + STELLE_A, 0, "0 Befund(e).", 0,
|
||||||
|
{"tests/a.sh": "eins\nZWEI\ndrei\n"}),
|
||||||
|
# Kein Befund: Ueberschriften aus bestehenden Plaenen (Paket E2 Task 367, S3 Task 298, NF Task 703)
|
||||||
|
("Ordner mit Schraegstrich", "mat",
|
||||||
|
KOPF + STELLE_A + aus_wird("#### Änderung 2 — dass `host/` nicht der einzige Ort ist", "drei", "DREI"), 0, "0 Befund(e).", 0,
|
||||||
|
{"tests/a.sh": "eins\nZWEI\nDREI\n"}),
|
||||||
|
("absoluter Pfad", "mat",
|
||||||
|
KOPF + STELLE_A + aus_wird("#### Änderung 2 — ein Wegwerf-`/proc` statt zweier Dateien", "drei", "DREI"), 0, "0 Befund(e).", 0,
|
||||||
|
{"tests/a.sh": "eins\nZWEI\nDREI\n"}),
|
||||||
|
("erkannte Zieldatei neben Pfad ohne Endung", "mat",
|
||||||
|
KOPF + STELLE_A + aus_wird("#### Änderung in `README.md`: `glattstellen/beginn` ohne Schlüssel", "host/ Ordner", "host/ Ordner, neu"),
|
||||||
|
0, "0 Befund(e).", 0, {"tests/a.sh": "eins\nZWEI\ndrei\n", "README.md": "# Projekt\n\n- host/ Ordner, neu\n"}),
|
||||||
|
("Token mit Leerraum", "mat",
|
||||||
|
KOPF + STELLE_A + aus_wird("#### Endpunkt `GET /api/paper`", "drei", "DREI"), 0, "0 Befund(e).", 0,
|
||||||
|
{"tests/a.sh": "eins\nZWEI\nDREI\n"}),
|
||||||
|
# plan_bloecke.py, Modus vor: Der Aus-Block steht zufaellig auch im vorigen Ziel, deshalb vorher still.
|
||||||
|
# Je Stelle zwei Befunde (Aus und wird); keiner davon darf zusaetzlich gegen das vorige Ziel laufen.
|
||||||
|
("bloecke: Grundlinie", "vor", KOPF + STELLE_A, 0, "0 Befund(e).", 0, None),
|
||||||
|
("bloecke: letzte Stelle", "vor",
|
||||||
|
KOPF + STELLE_A + aus_wird(f"#### Stelle 2 — {OHNE}", "drei", "DREI"), 2, MELDUNG, 2, None),
|
||||||
|
("bloecke: erste Stelle", "vor",
|
||||||
|
KOPF + aus_wird(f"#### Stelle 1 — {OHNE}", "drei", "DREI") + STELLE_A, 2, MELDUNG, 2, None),
|
||||||
|
("bloecke: ueber pfadlose Unterueberschrift", "vor",
|
||||||
|
KOPF + STELLE_A + f"### Stelle 2 — {OHNE}\n\n" + aus_wird("#### Änderung", "drei", "DREI"), 2, MELDUNG, 2, None),
|
||||||
|
("bloecke: Ordner, absolut, Leerraum, erkannte Zieldatei", "vor",
|
||||||
|
KOPF + STELLE_A + aus_wird("#### Änderung 2 — dass `host/` nicht der einzige Ort ist", "drei", "DREI")
|
||||||
|
+ aus_wird("#### Änderung 3 — Wegwerf-`/proc`", "eins", "EINS")
|
||||||
|
+ aus_wird("#### Endpunkt `GET /api/paper`", "eins", "EINS")
|
||||||
|
+ aus_wird("#### Änderung in `README.md`: `glattstellen/beginn`", "host/ Ordner", "host/ Ordner, neu"),
|
||||||
|
0, "0 Befund(e).", 0, None),
|
||||||
|
# Modus nach: Der Baum traegt schon den wird-Text im vorigen Ziel
|
||||||
|
("bloecke nach: letzte Stelle", "nach",
|
||||||
|
KOPF + aus_wird("#### Stelle 1 — `tests/a.sh`", "eins", "EINS") + aus_wird(f"#### Stelle 2 — {OHNE}", "drei", "DREI"),
|
||||||
|
2, MELDUNG, 2, None),
|
||||||
|
# Nachtrag: Datei ohne Endung unter "## Zieldateien" ist Ziel (R1 Stelle 30 mit Eintrag), letzte und erste Stelle
|
||||||
|
("Zieldateien: ganze Datei ohne Endung, letzte Stelle", "mat",
|
||||||
|
GEPLANT + STELLE_A + ganz(f"#### Stelle 2 — {OHNE}", "neu"), 0, "0 Befund(e).", 0,
|
||||||
|
{"tests/a.sh": "eins\nZWEI\ndrei\n", "host/logrotate/edge-traefik": "neu\n"}),
|
||||||
|
("Zieldateien: ganze Datei ohne Endung, erste Stelle", "mat",
|
||||||
|
GEPLANT + ganz(f"#### Stelle 1 — {OHNE}", "neu") + aus_wird("#### Stelle 2 — `tests/a.sh`", "zwei", "ZWEI"),
|
||||||
|
0, "0 Befund(e).", 0, {"tests/a.sh": "eins\nZWEI\ndrei\n", "host/logrotate/edge-traefik": "neu\n"}),
|
||||||
|
("Zieldateien: Aus/wird in Datei ohne Endung", "mat",
|
||||||
|
GEPLANT + STELLE_A + aus_wird(f"#### Stelle 2 — {OHNE}", "alt", "ALT"), 0, "0 Befund(e).", 0,
|
||||||
|
{"tests/a.sh": "eins\nZWEI\ndrei\n", "host/logrotate/edge-traefik": "ALT\n"}),
|
||||||
|
# "./Dockerfile" in der Ueberschrift, "Dockerfile" unter Zieldateien
|
||||||
|
("Zieldateien: ./Dockerfile", "mat",
|
||||||
|
zieldateien("tests/a.sh", "Dockerfile") + STELLE_A + ganz("#### Stelle 2 — `./Dockerfile`", "FROM neu"),
|
||||||
|
0, "0 Befund(e).", 0, {"tests/a.sh": "eins\nZWEI\ndrei\n", "Dockerfile": "FROM neu\n"}),
|
||||||
|
# Absoluter Eintrag: relativ zum Ordner vor docs/plans (Trading Plattform, Task 90: `.gitignore`)
|
||||||
|
("Zieldateien: absoluter Eintrag", "mat",
|
||||||
|
zieldateien("tests/a.sh", "{PROJEKT}\\.gitignore") + STELLE_A + aus_wird("### Schritt 2 — `.gitignore`", "data/", "data/\nartefakte/"),
|
||||||
|
0, "0 Befund(e).", 0, {"tests/a.sh": "eins\nZWEI\ndrei\n", ".gitignore": "data/\nartefakte/\n"}),
|
||||||
|
# Vorrang: Ein PFAD-Treffer in derselben Ueberschrift bleibt Ziel, auch neben einer Zieldatei ohne Endung
|
||||||
|
("Zieldateien: PFAD-Treffer hat Vorrang", "mat",
|
||||||
|
GEPLANT + STELLE_A + aus_wird(f"#### Änderung in `README.md`, Zeile {OHNE}", "host/ Ordner", "host/ Ordner, neu"),
|
||||||
|
0, "0 Befund(e).", 0, {"tests/a.sh": "eins\nZWEI\ndrei\n", "README.md": "# Projekt\n\n- host/ Ordner, neu\n"}),
|
||||||
|
# Ein Muster unter Zieldateien ist kein Ziel; der Pfad bleibt ein Befund
|
||||||
|
("Zieldateien: Muster ist kein Ziel", "mat",
|
||||||
|
zieldateien("tests/a.sh", "host/logrotate/*") + STELLE_A + ganz("#### Stelle 2 — `host/logrotate/*`", "neu"),
|
||||||
|
2, "nennt `host/logrotate/*` und keine erkannte Zieldatei", 1, {"tests/a.sh": "eins\nZWEI\ndrei\n"}),
|
||||||
|
# Name ohne Schraegstrich: liegt in der Kopie oder ist Dateiname eines Eintrags -> Befund statt stillem Ziel
|
||||||
|
("Dockerfile ohne Eintrag, letzte Stelle", "mat",
|
||||||
|
KOPF + STELLE_A + ganz("#### Stelle 2 — `Dockerfile`", "FROM neu"), 2, "nennt `Dockerfile` und keine erkannte Zieldatei", 1,
|
||||||
|
{"tests/a.sh": "eins\nZWEI\ndrei\n"}),
|
||||||
|
("Dockerfile ohne Eintrag, erste Stelle", "mat",
|
||||||
|
KOPF + ganz("#### Stelle 1 — `Dockerfile`", "FROM neu") + aus_wird("#### Stelle 2 — `tests/a.sh`", "zwei", "ZWEI"),
|
||||||
|
2, "nennt `Dockerfile` und keine erkannte Zieldatei", 1, {"tests/a.sh": "eins\nZWEI\ndrei\n"}),
|
||||||
|
("Dateiname eines Eintrags ohne Pfad", "mat",
|
||||||
|
GEPLANT + STELLE_A + ganz("#### Stelle 2 — `edge-traefik`", "neu"), 2, "nennt `edge-traefik` und keine erkannte Zieldatei", 1,
|
||||||
|
{"tests/a.sh": "eins\nZWEI\ndrei\n"}),
|
||||||
|
# Kein Befund: ein Bezeichner ohne Datei dieses Namens (bestehende Plaene: `main`, `pytest`, `Order`)
|
||||||
|
("Bezeichner ohne Datei", "mat",
|
||||||
|
KOPF + STELLE_A + aus_wird("#### Änderung 2 — `main` ruft `pruefe` auf", "drei", "DREI"), 0, "0 Befund(e).", 0,
|
||||||
|
{"tests/a.sh": "eins\nZWEI\nDREI\n"}),
|
||||||
|
("bloecke: Zieldateien, Datei ohne Endung", "vor",
|
||||||
|
GEPLANT + STELLE_A + aus_wird(f"#### Stelle 2 — {OHNE}", "alt", "ALT"), 0, "0 Befund(e).", 0, None),
|
||||||
|
("bloecke: Zieldateien, absoluter Eintrag", "vor",
|
||||||
|
zieldateien("tests/a.sh", "{PROJEKT}\\.gitignore") + STELLE_A + aus_wird("### Schritt 2 — `.gitignore`", "data/", "data/\nartefakte/"),
|
||||||
|
0, "0 Befund(e).", 0, None),
|
||||||
|
("bloecke: Dockerfile ohne Eintrag", "vor",
|
||||||
|
KOPF + STELLE_A + aus_wird("#### Stelle 2 — `Dockerfile`", "drei", "DREI"), 2, "nennt `Dockerfile` und keine erkannte Zieldatei", 2, None),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def schreibe(pfad: Path, text: str) -> None:
|
||||||
|
pfad.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
pfad.write_bytes(text.encode("utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
def fahre(werkzeuge: Path, art: str, wurzel: Path, plan: Path) -> subprocess.CompletedProcess:
|
||||||
|
if art == "mat":
|
||||||
|
befehl = [str(werkzeuge / "plan_materialisiere.py"), str(plan), str(wurzel / "kopie"), "task-01.md"]
|
||||||
|
else:
|
||||||
|
befehl = [str(werkzeuge / "plan_bloecke.py"), art, str(plan / "task-01.md"), "--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 main() -> int:
|
||||||
|
zerleger = argparse.ArgumentParser()
|
||||||
|
zerleger.add_argument("--werkzeuge", default=str(Path(__file__).resolve().parent))
|
||||||
|
werkzeuge = Path(zerleger.parse_args().werkzeuge)
|
||||||
|
fehler = 0
|
||||||
|
for name, art, task, exit_erwartet, meldung, anzahl, dateien in FAELLE:
|
||||||
|
wurzel = Path(tempfile.mkdtemp(prefix="pz_"))
|
||||||
|
try:
|
||||||
|
basis = dict(BASIS)
|
||||||
|
if art == "nach":
|
||||||
|
# Stand nach der Welle: Stelle 1 angewendet, Stelle 2 im vorigen Ziel statt in edge-traefik
|
||||||
|
basis["tests/a.sh"] = "EINS\nzwei\nDREI\n"
|
||||||
|
for rel, text in basis.items():
|
||||||
|
schreibe(wurzel / "kopie" / rel, text)
|
||||||
|
# {PROJEKT}: fuer plan_bloecke der Ordner hinter --projekt; fuer plan_materialisiere ein eigener
|
||||||
|
# Projektordner, unter dessen docs/plans die Taskdatei liegt (die Kopie liegt woanders)
|
||||||
|
plan = wurzel / "plan"
|
||||||
|
if "{PROJEKT}" in task:
|
||||||
|
projekt = wurzel / ("projekt" if art == "mat" else "kopie")
|
||||||
|
plan = projekt / "docs" / "plans" / "p" if art == "mat" else plan
|
||||||
|
task = task.replace("{PROJEKT}", str(projekt))
|
||||||
|
schreibe(plan / "task-01.md", task)
|
||||||
|
lauf = fahre(werkzeuge, art, wurzel, plan)
|
||||||
|
ausgabe = lauf.stdout + lauf.stderr
|
||||||
|
gruende = []
|
||||||
|
if lauf.returncode != exit_erwartet:
|
||||||
|
gruende.append(f"Exit {lauf.returncode}, erwartet {exit_erwartet}")
|
||||||
|
if meldung not in ausgabe:
|
||||||
|
gruende.append(f"Meldung fehlt: {meldung}")
|
||||||
|
if ausgabe.count("BEFUND ") != anzahl:
|
||||||
|
gruende.append(f"{ausgabe.count('BEFUND ')} BEFUND-Zeile(n), erwartet {anzahl}")
|
||||||
|
# Jede Datei der Kopie: erwarteter Inhalt, sonst unveraendert wie BASIS
|
||||||
|
for rel, text in basis.items() if dateien is not None else ():
|
||||||
|
soll = dateien.get(rel, text)
|
||||||
|
ist = (wurzel / "kopie" / rel).read_bytes().decode("utf-8", errors="replace")
|
||||||
|
if ist != soll:
|
||||||
|
gruende.append(f"{rel}: {ist!r}, erwartet {soll!r}")
|
||||||
|
print(f"{'ok ' if not gruende else 'FEHLER'} {name}: Exit {lauf.returncode}")
|
||||||
|
if gruende:
|
||||||
|
fehler += 1
|
||||||
|
for grund in gruende:
|
||||||
|
print(" " + grund)
|
||||||
|
print(" " + ausgabe.strip().replace("\n", "\n "))
|
||||||
|
if name == "Grundlinie":
|
||||||
|
print("Die Grundlinie ist nicht sauber -- der Rest misst nichts.")
|
||||||
|
return 2
|
||||||
|
finally:
|
||||||
|
shutil.rmtree(wurzel, ignore_errors=True)
|
||||||
|
print(f"{fehler} Fall/Faelle mit falschem Ausgang von {len(FAELLE)}.")
|
||||||
|
return 2 if fehler else 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -5,9 +5,15 @@ Koordinator-Werkzeug im Scratchpad. Fragen und Herkunft der Antwort:
|
|||||||
2. Welche durfte sie aendern? Quelle: Abschnitt "## Zieldateien" der uebergebenen Taskdateien.
|
2. Welche durfte sie aendern? Quelle: Abschnitt "## Zieldateien" der uebergebenen Taskdateien.
|
||||||
3. Ist jede Zieldatei byte-gleich mit dem Plan? Quelle: die Kopie `mat`, auf der plan_materialisiere.py
|
3. Ist jede Zieldatei byte-gleich mit dem Plan? Quelle: die Kopie `mat`, auf der plan_materialisiere.py
|
||||||
alle Codebloecke des Plans angewendet hat.
|
alle Codebloecke des Plans angewendet hat.
|
||||||
|
4. Welche Zieldateien blendet git aus? Quelle: `git check-ignore -z --stdin` im Projekt. Getrackte
|
||||||
|
Dateien zaehlen nicht, auch wenn ein Muster passt (git-check-ignore(1)).
|
||||||
|
Seit 2026-10-17: Eine ignorierte Zieldatei steht nie in `git status`. Frueher hiess das "unveraendert gegenueber
|
||||||
|
HEAD" und der Byte-Vergleich fiel aus (Paket R1 im Edge-Stack: 8 und 12 Fehlalarme fuer tests/.work/).
|
||||||
|
Jetzt wird sie wie jede andere gegen die Kopie verglichen; ob sie vor der Welle schon so aussah, sieht das
|
||||||
|
Werkzeug bei ihr nicht, und eine geloeschte gilt als geloescht, sobald sie im Baum fehlt.
|
||||||
Was es NICHT sieht (Fangnetz, kein Nachweis):
|
Was es NICHT sieht (Fangnetz, kein Nachweis):
|
||||||
- Dateien, die .gitignore ausblendet (data/, artifacts/, node_modules/): Ein Executer, der dorthin
|
- Dateien, die .gitignore ausblendet und die keine Zieldatei sind (data/, artifacts/, node_modules/): Ein
|
||||||
schreibt, bleibt unsichtbar.
|
Executer, der dorthin schreibt, bleibt unsichtbar.
|
||||||
- Ob die Kopie selbst sauber ist. Eine verschmutzte Kopie erzeugt Fehlalarm, kein falsches Gruen.
|
- Ob die Kopie selbst sauber ist. Eine verschmutzte Kopie erzeugt Fehlalarm, kein falsches Gruen.
|
||||||
- Prosa-Dateien (--prosa): nur Mengenpruefung, kein Byte-Vergleich; die liest der Koordinator selbst.
|
- Prosa-Dateien (--prosa): nur Mengenpruefung, kein Byte-Vergleich; die liest der Koordinator selbst.
|
||||||
- Bereits committete Aenderungen: Verglichen wird der Arbeitsbaum gegen HEAD.
|
- Bereits committete Aenderungen: Verglichen wird der Arbeitsbaum gegen HEAD.
|
||||||
@@ -99,6 +105,25 @@ def geaendert(projekt: Path, befunde: list[str]) -> dict[str, str]:
|
|||||||
return aus
|
return aus
|
||||||
|
|
||||||
|
|
||||||
|
def ignoriert(projekt: Path, pfade: list[str], befunde: list[str]) -> set[str]:
|
||||||
|
"""Die Zieldateien unter pfade, die git ignoriert. Exit 1 von check-ignore heisst: keine; alles ueber 1 ein Befund."""
|
||||||
|
if not pfade:
|
||||||
|
return set()
|
||||||
|
try:
|
||||||
|
ergebnis = subprocess.run(
|
||||||
|
["git", "-C", str(projekt), "check-ignore", "-z", "--stdin"],
|
||||||
|
input=("\0".join(pfade) + "\0").encode("utf-8"),
|
||||||
|
capture_output=True,
|
||||||
|
)
|
||||||
|
except OSError as fehler:
|
||||||
|
befunde.append(f"git check-ignore laesst sich nicht starten ({fehler})")
|
||||||
|
return set()
|
||||||
|
if ergebnis.returncode not in (0, 1):
|
||||||
|
befunde.append(f"git check-ignore endet mit {ergebnis.returncode}: {ergebnis.stderr.decode('utf-8', 'replace').strip()}")
|
||||||
|
return set()
|
||||||
|
return {p for p in ergebnis.stdout.decode("utf-8", "replace").split("\0") if p}
|
||||||
|
|
||||||
|
|
||||||
def erste_abweichung(echt: bytes, kopie: bytes) -> str:
|
def erste_abweichung(echt: bytes, kopie: bytes) -> str:
|
||||||
if echt.replace(b"\r\n", b"\n") == kopie.replace(b"\r\n", b"\n"):
|
if echt.replace(b"\r\n", b"\n") == kopie.replace(b"\r\n", b"\n"):
|
||||||
return "nur die Zeilenenden weichen ab (CRLF gegen LF)"
|
return "nur die Zeilenenden weichen ab (CRLF gegen LF)"
|
||||||
@@ -122,6 +147,7 @@ def pruefe(args: argparse.Namespace, befunde: list[str]) -> None:
|
|||||||
for name in args.taskdateien:
|
for name in args.taskdateien:
|
||||||
ziele.update(zieldateien(Path(name), projekt, befunde))
|
ziele.update(zieldateien(Path(name), projekt, befunde))
|
||||||
status = geaendert(projekt, befunde)
|
status = geaendert(projekt, befunde)
|
||||||
|
ign = ignoriert(projekt, sorted(ziele), befunde)
|
||||||
|
|
||||||
for pfad in sorted(set(status) - set(ziele)):
|
for pfad in sorted(set(status) - set(ziele)):
|
||||||
befunde.append(f"{pfad}: geaendert ({status[pfad]!r}), liegt aber ausserhalb der Zieldateien dieser Welle")
|
befunde.append(f"{pfad}: geaendert ({status[pfad]!r}), liegt aber ausserhalb der Zieldateien dieser Welle")
|
||||||
@@ -136,10 +162,10 @@ def pruefe(args: argparse.Namespace, befunde: list[str]) -> None:
|
|||||||
befunde.append(f"{pfad}: soll geloescht sein, steht aber noch im Baum")
|
befunde.append(f"{pfad}: soll geloescht sein, steht aber noch im Baum")
|
||||||
if kopie.exists():
|
if kopie.exists():
|
||||||
befunde.append(f"{pfad}: soll geloescht sein, steht aber noch in der Kopie")
|
befunde.append(f"{pfad}: soll geloescht sein, steht aber noch in der Kopie")
|
||||||
if not echt.exists() and "D" not in status.get(pfad, ""):
|
if not echt.exists() and pfad not in ign and "D" not in status.get(pfad, ""):
|
||||||
befunde.append(f"{pfad}: fehlt im Baum, git meldet aber keine Loeschung ({status.get(pfad)!r})")
|
befunde.append(f"{pfad}: fehlt im Baum, git meldet aber keine Loeschung ({status.get(pfad)!r})")
|
||||||
continue
|
continue
|
||||||
if pfad not in status:
|
if pfad not in status and pfad not in ign:
|
||||||
befunde.append(f"{pfad}: Zieldatei ist unveraendert gegenueber HEAD")
|
befunde.append(f"{pfad}: Zieldatei ist unveraendert gegenueber HEAD")
|
||||||
continue
|
continue
|
||||||
if not echt.is_file():
|
if not echt.is_file():
|
||||||
@@ -155,7 +181,7 @@ def pruefe(args: argparse.Namespace, befunde: list[str]) -> None:
|
|||||||
gleich += 1
|
gleich += 1
|
||||||
else:
|
else:
|
||||||
befunde.append(f"{pfad}: weicht vom Plan ab -- {erste_abweichung(a, b)}")
|
befunde.append(f"{pfad}: weicht vom Plan ab -- {erste_abweichung(a, b)}")
|
||||||
print(f"{len(ziele)} Zieldateien, {len(status)} geaenderte Dateien laut git, {gleich} byte-gleich mit der Kopie, {len(prosa & set(ziele))} nur als Menge geprueft.")
|
print(f"{len(ziele)} Zieldateien, {len(status)} geaenderte Dateien laut git, {gleich} byte-gleich mit der Kopie, {len(prosa & set(ziele))} nur als Menge geprueft, {len(ign)} von git ignoriert.")
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
|
|||||||
@@ -151,6 +151,64 @@ def fall_relativ_nach_aussen(projekt, mat, task, wurzel):
|
|||||||
schreibe(task, text.replace("Keine andere Datei", "- `../anderswo/x.py` (neu)\n\nKeine andere Datei"))
|
schreibe(task, text.replace("Keine andere Datei", "- `../anderswo/x.py` (neu)\n\nKeine andere Datei"))
|
||||||
|
|
||||||
|
|
||||||
|
def _ignoriert(projekt, mat, task, erst="neu", letzt="neu"):
|
||||||
|
"""Seit 2026-10-17: Zieldateien unter einem Pfad, den .gitignore ausblendet (Paket R1 im Edge-Stack: Prüfdateien
|
||||||
|
unter tests/.work/). .gitignore kommt in den Basis-Commit; work/erst.py steht als erste, work/letzt.py als letzte
|
||||||
|
Zieldatei in der Liste, beide in Baum und Kopie gleich. erst/letzt: "neu" oder "gelöscht" (dann in keinem Baum)."""
|
||||||
|
schreibe(projekt / ".gitignore", "work/\n")
|
||||||
|
git(projekt, "add", ".gitignore")
|
||||||
|
git(projekt, "commit", "-q", "-m", "ignoriert")
|
||||||
|
for name, art in (("erst.py", erst), ("letzt.py", letzt)):
|
||||||
|
if art == "neu":
|
||||||
|
schreibe(projekt / "work" / name, f"{name} eins\n{name} zwei\n")
|
||||||
|
schreibe(mat / "work" / name, f"{name} eins\n{name} zwei\n")
|
||||||
|
text = task.read_text(encoding="utf-8")
|
||||||
|
text = text.replace("## Zieldateien\n\n", f"## Zieldateien\n\n- `work/erst.py` ({erst})\n", 1)
|
||||||
|
text = text.replace("\n\nKeine andere Datei", f"\n- `work/letzt.py` ({letzt})\n\nKeine andere Datei", 1)
|
||||||
|
schreibe(task, text)
|
||||||
|
|
||||||
|
|
||||||
|
def fall_ignoriert_gleich(projekt, mat, task, wurzel):
|
||||||
|
_ignoriert(projekt, mat, task)
|
||||||
|
|
||||||
|
|
||||||
|
def fall_ignoriert_erste_weicht_ab(projekt, mat, task, wurzel):
|
||||||
|
_ignoriert(projekt, mat, task)
|
||||||
|
schreibe(projekt / "work" / "erst.py", "erst.py EINS\nerst.py zwei\n")
|
||||||
|
|
||||||
|
|
||||||
|
def fall_ignoriert_letzte_weicht_ab(projekt, mat, task, wurzel):
|
||||||
|
_ignoriert(projekt, mat, task)
|
||||||
|
schreibe(projekt / "work" / "letzt.py", "letzt.py eins\nletzt.py ZWEI\n")
|
||||||
|
|
||||||
|
|
||||||
|
def fall_ignoriert_fehlt_im_baum(projekt, mat, task, wurzel):
|
||||||
|
_ignoriert(projekt, mat, task)
|
||||||
|
(projekt / "work" / "letzt.py").unlink()
|
||||||
|
|
||||||
|
|
||||||
|
def fall_ignoriert_fehlt_in_kopie(projekt, mat, task, wurzel):
|
||||||
|
_ignoriert(projekt, mat, task)
|
||||||
|
(mat / "work" / "erst.py").unlink()
|
||||||
|
|
||||||
|
|
||||||
|
def fall_ignoriert_geloescht(projekt, mat, task, wurzel):
|
||||||
|
_ignoriert(projekt, mat, task, letzt="gelöscht")
|
||||||
|
|
||||||
|
|
||||||
|
def fall_ignoriert_nicht_geloescht(projekt, mat, task, wurzel):
|
||||||
|
_ignoriert(projekt, mat, task, letzt="gelöscht")
|
||||||
|
schreibe(projekt / "work" / "letzt.py", "bleibt\n")
|
||||||
|
|
||||||
|
|
||||||
|
def fall_ignoriert_getrackt(projekt, mat, task, wurzel):
|
||||||
|
"""Eine getrackte Datei unter einem ignorierten Muster (git add -f) ist kein ignoriertes Ziel: Unveraendert
|
||||||
|
gegenueber HEAD bleibt ein Befund."""
|
||||||
|
_ignoriert(projekt, mat, task)
|
||||||
|
git(projekt, "add", "-f", "work/erst.py")
|
||||||
|
git(projekt, "commit", "-q", "-m", "erzwungen")
|
||||||
|
|
||||||
|
|
||||||
FAELLE = [
|
FAELLE = [
|
||||||
("Grundlinie", fall_grundlinie, 0, "0 Befund(e)."),
|
("Grundlinie", fall_grundlinie, 0, "0 Befund(e)."),
|
||||||
("fremde Datei im Baum", fall_fremde_datei, 2, "fremd.py: geaendert"),
|
("fremde Datei im Baum", fall_fremde_datei, 2, "fremd.py: geaendert"),
|
||||||
@@ -174,6 +232,15 @@ FAELLE = [
|
|||||||
# Seit 2026-09-18: Die Klammer hinter dem Pfad ist wahlfrei, Text dahinter erlaubt.
|
# Seit 2026-09-18: Die Klammer hinter dem Pfad ist wahlfrei, Text dahinter erlaubt.
|
||||||
("Zieldateien ohne Klammer", fall_ohne_klammer_grundlinie, 0, "0 Befund(e)."),
|
("Zieldateien ohne Klammer", fall_ohne_klammer_grundlinie, 0, "0 Befund(e)."),
|
||||||
("Zieldatei ohne Klammer weicht ab", fall_ohne_klammer_abweichung, 2, "a.py: weicht vom Plan ab -- Zeile 1"),
|
("Zieldatei ohne Klammer weicht ab", fall_ohne_klammer_abweichung, 2, "a.py: weicht vom Plan ab -- Zeile 1"),
|
||||||
|
# Seit 2026-10-17: Zieldateien, die git ignoriert, werden byte-genau verglichen statt als unveraendert gemeldet.
|
||||||
|
("ignorierte Zieldateien gleich", fall_ignoriert_gleich, 0, "4 byte-gleich mit der Kopie, 1 nur als Menge geprueft, 2 von git ignoriert."),
|
||||||
|
("ignorierte erste Zieldatei weicht ab", fall_ignoriert_erste_weicht_ab, 2, "work/erst.py: weicht vom Plan ab -- Zeile 1"),
|
||||||
|
("ignorierte letzte Zieldatei weicht ab", fall_ignoriert_letzte_weicht_ab, 2, "work/letzt.py: weicht vom Plan ab -- Zeile 2"),
|
||||||
|
("ignorierte Zieldatei fehlt im Baum", fall_ignoriert_fehlt_im_baum, 2, "work/letzt.py: Zieldatei fehlt im Baum"),
|
||||||
|
("ignorierte Zieldatei fehlt in der Kopie", fall_ignoriert_fehlt_in_kopie, 2, "work/erst.py: fehlt in der Kopie"),
|
||||||
|
("ignorierte Zieldatei geloescht", fall_ignoriert_geloescht, 0, "0 Befund(e)."),
|
||||||
|
("ignorierte Loeschung nicht ausgefuehrt", fall_ignoriert_nicht_geloescht, 2, "work/letzt.py: soll geloescht sein, steht aber noch im Baum"),
|
||||||
|
("getrackte Datei unter ignoriertem Muster unveraendert", fall_ignoriert_getrackt, 2, "work/erst.py: Zieldatei ist unveraendert"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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."
|
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]
|
tags: [vault-tools, codeworkflow]
|
||||||
created: 2026-09-24
|
created: 2026-09-24
|
||||||
updated: 2026-10-16
|
updated: 2026-10-17
|
||||||
---
|
---
|
||||||
|
|
||||||
# Workflow-Prüfung
|
# Workflow-Prüfung
|
||||||
@@ -21,7 +21,7 @@ Die Regel dazu steht in [[AI/Regelwerk/codeworkflow|codeworkflow.md]], Abschnitt
|
|||||||
|
|
||||||
## Was die Attrappe nachbildet
|
## Was die Attrappe nachbildet
|
||||||
|
|
||||||
`attrappe.mjs` führt ein Workflow-Skript mit nachgebauten Globals aus, genauso streng wie das Laufzeitsystem: `workflow()` im Kind wirft, im Kind ist `phase()` wirkungslos und jeder Agent trägt die Phase `▸ <name>` (ab dem zweiten Aufruf desselben Kindes `▸ <name> #<n>`), ein unbekannter Workflow wirft, `Date.now()`, `new Date()` ohne Argument und `Math.random()` werfen. Ein Schema ohne `properties` wirft, ein Ergebnis ohne die `required`-Felder wird zu `null`, und ein `agent()` ohne `model` ist ein Verstoß. Aufrufe über `D:/Claude/.claude/workflows/...` leitet sie in den Ordner unter Test um. Sie zählt, wie viele Agenten je Phase und Modell gleichzeitig laufen.
|
`attrappe.mjs` führt ein Workflow-Skript mit nachgebauten Globals aus, genauso streng wie das Laufzeitsystem: `workflow()` im Kind wirft, im Kind ist `phase()` wirkungslos und jeder Agent trägt die Phase `▸ <name>` (ab dem zweiten Aufruf desselben Kindes `▸ <name> #<n>`), ein unbekannter Workflow wirft, `Date.now()`, `new Date()` ohne Argument und `Math.random()` werfen. Ein Schema ohne `properties` wirft, ein Ergebnis ohne die `required`-Felder wird zu `null`, und ein `agent()` ohne `model` ist ein Verstoß. Ein `agentType` ohne Agentendatei unter `D:/Claude/.claude/agents/` wirft vor dem Start, wie im Ziel (Echtlauf 2026-10-17); Fälle können die Liste über `optionen.agenten` vorgeben. Aufrufe über `D:/Claude/.claude/workflows/...` leitet sie in den Ordner unter Test um. Sie zählt, wie viele Agenten je Phase und Modell gleichzeitig laufen.
|
||||||
|
|
||||||
**Was sie nicht sieht:** die Obergrenze gleichzeitiger Agenten je Lauf, Budget, Resume und das Verhalten echter Modelle. Wie lange echte Linsen bei drei gleichzeitigen Opus-Agenten laufen und wie viel Verbrauch sie erzeugen, zeigt nur ein echter Lauf.
|
**Was sie nicht sieht:** die Obergrenze gleichzeitiger Agenten je Lauf, Budget, Resume und das Verhalten echter Modelle. Wie lange echte Linsen bei drei gleichzeitigen Opus-Agenten laufen und wie viel Verbrauch sie erzeugen, zeigt nur ein echter Lauf.
|
||||||
|
|
||||||
@@ -40,6 +40,7 @@ Stand 2026-09-24 nach Plankritik, Prüfung und Nachbesserung. Die Zahlen gelten
|
|||||||
- **Erster Eichwert** am Abend des 2026-09-24, Plan laut App „Pro“: `verbrauch_reihe.py --seit 2026-09-24` ergab 8,1 % des 5-Stunden-Fensters je Opus-Agentenstunde (5 Paare, 1,7 Opus-Stunden, darunter ein Plankritik-Lauf der Trading-Sitzung). Die Woche stieg in denselben Paaren um 5 Punkte. Der Verbrauch anderer Sitzungen und der Hauptsitzung steckt mit drin; die Zahl ist also eher zu hoch. Für das Nachstellen der Stufen reicht ein Abend nicht.
|
- **Erster Eichwert** am Abend des 2026-09-24, Plan laut App „Pro“: `verbrauch_reihe.py --seit 2026-09-24` ergab 8,1 % des 5-Stunden-Fensters je Opus-Agentenstunde (5 Paare, 1,7 Opus-Stunden, darunter ein Plankritik-Lauf der Trading-Sitzung). Die Woche stieg in denselben Paaren um 5 Punkte. Der Verbrauch anderer Sitzungen und der Hauptsitzung steckt mit drin; die Zahl ist also eher zu hoch. Für das Nachstellen der Stufen reicht ein Abend nicht.
|
||||||
|
|
||||||
- **Ableser verlässt den Auftrag** (2026-10-16, Plan `docs/plans/2026-10-16-ableser-abweichung/`): Zweimal bearbeitete der Haiku-Ableser die Nutzeranfrage statt `get_usage` zu lesen und schrieb seine Antwort in `status` und `plan` (wf_7ca17029-421, wf_80616e8e-0e3). `verbrauch.js` meldet diese Form jetzt als `status: abgewichen` mit einer Logzeile, die mit `WARNUNG` beginnt; Plätze wie ohne Ablesung. Neun neue Fälle, sieben davon gegen den alten Stand rot; die zwei Gegenproben (ehrlicher Fehlschlag, `not_applicable`) bleiben auf beiden Ständen grün. Acht neue Mutanten, alle gefangen; der volle Mutantenlauf brauchte knapp 50 Minuten. Die drei echten Antworten aus den Journalen ergaben `abgewichen`, `abgewichen` und `nicht_gelesen`. **Ungemessen:** ob der neue Brückensatz im Auftrag einen echten Ableser vom Abweichen abhält; der Fall misst nur, dass der Satz dasteht. Ein abweichender Ableser hat weiterhin alle Werkzeuge.
|
- **Ableser verlässt den Auftrag** (2026-10-16, Plan `docs/plans/2026-10-16-ableser-abweichung/`): Zweimal bearbeitete der Haiku-Ableser die Nutzeranfrage statt `get_usage` zu lesen und schrieb seine Antwort in `status` und `plan` (wf_7ca17029-421, wf_80616e8e-0e3). `verbrauch.js` meldet diese Form jetzt als `status: abgewichen` mit einer Logzeile, die mit `WARNUNG` beginnt; Plätze wie ohne Ablesung. Neun neue Fälle, sieben davon gegen den alten Stand rot; die zwei Gegenproben (ehrlicher Fehlschlag, `not_applicable`) bleiben auf beiden Ständen grün. Acht neue Mutanten, alle gefangen; der volle Mutantenlauf brauchte knapp 50 Minuten. Die drei echten Antworten aus den Journalen ergaben `abgewichen`, `abgewichen` und `nicht_gelesen`. **Ungemessen:** ob der neue Brückensatz im Auftrag einen echten Ableser vom Abweichen abhält; der Fall misst nur, dass der Satz dasteht. Ein abweichender Ableser hat weiterhin alle Werkzeuge.
|
||||||
|
- **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.
|
||||||
|
|
||||||
**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:** 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.
|
||||||
|
|
||||||
|
|||||||
@@ -14,16 +14,25 @@
|
|||||||
// - agent() mit schema braucht type object und properties, required muss in properties stehen.
|
// - agent() mit schema braucht type object und properties, required muss in properties stehen.
|
||||||
// Ein Ergebnis ohne die required-Felder wird wie ein gescheiterter Agent zu null.
|
// Ein Ergebnis ohne die required-Felder wird wie ein gescheiterter Agent zu null.
|
||||||
// - parallel() ist eine Barriere; ein werfender Thunk wird zu null.
|
// - parallel() ist eine Barriere; ein werfender Thunk wird zu null.
|
||||||
|
// - agent() mit einem agentType, den die Sitzung nicht kennt, wirft vor dem Start (CLI-Abbild
|
||||||
|
// 2.1.299). Bekannt sind die Agentendateien unter .claude/agents/ des Vaults oder optionen.agenten.
|
||||||
// Zusätzlich als Verstoß protokolliert (Stamm, "Agenten & Modelle"): agent() ohne model.
|
// Zusätzlich als Verstoß protokolliert (Stamm, "Agenten & Modelle"): agent() ohne model.
|
||||||
// Was die Attrappe nicht nachbildet: die Obergrenze gleichzeitiger Agenten je Lauf, Budget,
|
// Was die Attrappe nicht nachbildet: die Obergrenze gleichzeitiger Agenten je Lauf, Budget,
|
||||||
// Resume und das Verhalten echter Modelle. Sie ist ein Fangnetz für die Steuerlogik der
|
// Resume und das Verhalten echter Modelle. Sie ist ein Fangnetz für die Steuerlogik der
|
||||||
// Skripte, kein Nachweis für einen echten Lauf.
|
// Skripte, kein Nachweis für einen echten Lauf.
|
||||||
import { readFileSync, existsSync } from 'node:fs'
|
import { readFileSync, existsSync, readdirSync } from 'node:fs'
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
|
|
||||||
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor
|
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor
|
||||||
// Aufrufe über den echten Pfad werden in den Ordner unter Test umgeleitet.
|
// Aufrufe über den echten Pfad werden in den Ordner unter Test umgeleitet.
|
||||||
const ECHTER_ORDNER = 'D:/Claude/.claude/workflows/'
|
const ECHTER_ORDNER = 'D:/Claude/.claude/workflows/'
|
||||||
|
// Agententypen wie in der Sitzung: die Agentendateien des Vaults neben den Workflows. Fehlt der
|
||||||
|
// Ordner, ist jeder agentType unbekannt und jeder Fall damit rot: ein Befund, kein Durchlass.
|
||||||
|
const AGENTEN_ORDNER = path.join(ECHTER_ORDNER, '..', 'agents')
|
||||||
|
function agentenDesVaults() {
|
||||||
|
if (!existsSync(AGENTEN_ORDNER)) return []
|
||||||
|
return readdirSync(AGENTEN_ORDNER).filter(n => n.endsWith('.md')).map(n => n.slice(0, -3))
|
||||||
|
}
|
||||||
|
|
||||||
class DatumAttrappe extends Date {
|
class DatumAttrappe extends Date {
|
||||||
constructor(...a) {
|
constructor(...a) {
|
||||||
@@ -51,6 +60,7 @@ const warte = ms => new Promise(r => setTimeout(r, ms))
|
|||||||
// aufruf trägt label, phase, modell, effort, agentType, prompt, tiefe und schema.
|
// aufruf trägt label, phase, modell, effort, agentType, prompt, tiefe und schema.
|
||||||
export async function fuehreAus(datei, args, antworte, optionen = {}) {
|
export async function fuehreAus(datei, args, antworte, optionen = {}) {
|
||||||
const ordner = optionen.ordner || path.dirname(datei)
|
const ordner = optionen.ordner || path.dirname(datei)
|
||||||
|
const agenten = optionen.agenten || agentenDesVaults()
|
||||||
const protokoll = { agents: [], logs: [], phasen: [], verstoesse: [], spitze: {}, spitzeJePhase: {}, workflows: [] }
|
const protokoll = { agents: [], logs: [], phasen: [], verstoesse: [], spitze: {}, spitzeJePhase: {}, workflows: [] }
|
||||||
const laufend = {}
|
const laufend = {}
|
||||||
const laufendJePhase = {}
|
const laufendJePhase = {}
|
||||||
@@ -82,6 +92,7 @@ export async function fuehreAus(datei, args, antworte, optionen = {}) {
|
|||||||
if (!opts.schema || opts.schema.type !== 'object' || !opts.schema.properties) throw new Error('Schema braucht type object und properties: ' + opts.label)
|
if (!opts.schema || opts.schema.type !== 'object' || !opts.schema.properties) throw new Error('Schema braucht type object und properties: ' + opts.label)
|
||||||
if ((opts.schema.required || []).some(k => !(k in opts.schema.properties))) throw new Error('required nicht in properties: ' + opts.label)
|
if ((opts.schema.required || []).some(k => !(k in opts.schema.properties))) throw new Error('required nicht in properties: ' + opts.label)
|
||||||
}
|
}
|
||||||
|
if (opts.agentType != null && !agenten.includes(String(opts.agentType))) throw new Error(`agent({agentType}): agent type '${opts.agentType}' not found. Available agents: ${agenten.join(', ')}`)
|
||||||
const modell = opts.model || '(erbt)'
|
const modell = opts.model || '(erbt)'
|
||||||
const phase = kindPhase || opts.phase || aktuellePhase || '(ohne)'
|
const phase = kindPhase || opts.phase || aktuellePhase || '(ohne)'
|
||||||
const eintrag = { label: opts.label || null, phase, modell, effort: opts.effort, agentType: opts.agentType, prompt, tiefe, start: ++uhr, ende: null, ergebnis: undefined }
|
const eintrag = { label: opts.label || null, phase, modell, effort: opts.effort, agentType: opts.agentType, prompt, tiefe, start: ++uhr, ende: null, ergebnis: undefined }
|
||||||
|
|||||||
+3
-2
@@ -5,7 +5,7 @@ status: active
|
|||||||
summary: "Nach dem Plan erkennt verbrauch.js an status und plan, dass der Haiku-Ableser statt get_usage die Nutzeranfrage bearbeitet hat, und meldet das als status abgewichen mit Warnzeile; der Auftrag ordnet die Ablesung ausdrücklich der Nutzeranfrage zu. Pascals Wahl vom 2026-10-16: Möglichkeiten 1 und 2."
|
summary: "Nach dem Plan erkennt verbrauch.js an status und plan, dass der Haiku-Ableser statt get_usage die Nutzeranfrage bearbeitet hat, und meldet das als status abgewichen mit Warnzeile; der Auftrag ordnet die Ablesung ausdrücklich der Nutzeranfrage zu. Pascals Wahl vom 2026-10-16: Möglichkeiten 1 und 2."
|
||||||
tags: [plan, vault-tools, codeworkflow]
|
tags: [plan, vault-tools, codeworkflow]
|
||||||
created: 2026-10-16
|
created: 2026-10-16
|
||||||
updated: 2026-10-16
|
updated: 2026-10-17
|
||||||
---
|
---
|
||||||
|
|
||||||
Projekt: [LIESMICH](../../../LIESMICH.md)
|
Projekt: [LIESMICH](../../../LIESMICH.md)
|
||||||
@@ -42,9 +42,10 @@ Projekt: [LIESMICH](../../../LIESMICH.md)
|
|||||||
| Nr. | Datei | Thema | Modell |
|
| Nr. | Datei | Thema | Modell |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| 01 | [task-01-erkennung.md](task-01-erkennung.md) | Erkennung, Brückensatz, Fälle, Mutanten | `sonnet` |
|
| 01 | [task-01-erkennung.md](task-01-erkennung.md) | Erkennung, Brückensatz, Fälle, Mutanten | `sonnet` |
|
||||||
|
| 02 | [task-02-agententyp.md](task-02-agententyp.md) | Ableser als Agententyp `verbrauch-ableser`, Startfehler abfangen, Attrappe prüft `agentType` | `sonnet` |
|
||||||
|
|
||||||
## Nicht im Umfang
|
## Nicht im Umfang
|
||||||
|
|
||||||
- Eigener Agententyp für den Ableser mit nur ToolSearch und `get_usage`: eigener Schritt nach einem Echtlauf.
|
- Task 02 (Möglichkeit 5, Agententyp mit nur ToolSearch und `get_usage`) kam am 2026-10-17 auf Pascals Auftrag dazu. Die Agentendatei und die Zeile in der Rollentabelle legt der Koordinator an, und vor Task 02 läuft ein Echtlauf über das Workflow-Werkzeug.
|
||||||
- `LIESMICH.md`, Erinnerung und Sitzungsprotokoll schreibt der Koordinator.
|
- `LIESMICH.md`, Erinnerung und Sitzungsprotokoll schreibt der Koordinator.
|
||||||
- Kein Commit. Im Vault-Repo committet nur Pascal oder die Aufgabe um 04:00.
|
- Kein Commit. Im Vault-Repo committet nur Pascal oder die Aufgabe um 04:00.
|
||||||
+1
-1
@@ -118,7 +118,7 @@ Diese Einträge in `MUTANTEN`, direkt nach `("verbrauch", "5-Stunden-Regex unver
|
|||||||
("verbrauch", "Bericht in plan bei status fehler uebersehen", "return a.status === 'fehler' && typeof a.plan === 'string' && a.plan.trim() !== ''", "return false"),
|
("verbrauch", "Bericht in plan bei status fehler uebersehen", "return a.status === 'fehler' && typeof a.plan === 'string' && a.plan.trim() !== ''", "return false"),
|
||||||
("verbrauch", "ehrlicher Fehler gilt als Abweichung", "'unavailable', 'fehler']", "'unavailable']"),
|
("verbrauch", "ehrlicher Fehler gilt als Abweichung", "'unavailable', 'fehler']", "'unavailable']"),
|
||||||
("verbrauch", "Abweichung als nicht_gelesen", "return ohneAblesung('abgewichen',", "return ohneAblesung('nicht_gelesen',"),
|
("verbrauch", "Abweichung als nicht_gelesen", "return ohneAblesung('abgewichen',", "return ohneAblesung('nicht_gelesen',"),
|
||||||
("verbrauch", "Warnzeile fehlt", "log(`WARNUNG Verbrauch", "String(`WARNUNG Verbrauch"),
|
("verbrauch", "Warnzeile fehlt", "log(`WARNUNG Verbrauch ${args.anlass}: Der Ableser", "String(`WARNUNG Verbrauch ${args.anlass}: Der Ableser"),
|
||||||
("verbrauch", "status ungekuerzt im grund", ".trim().slice(0, 160)", ".trim()"),
|
("verbrauch", "status ungekuerzt im grund", ".trim().slice(0, 160)", ".trim()"),
|
||||||
("verbrauch", "Brueckensatz fehlt", "Diese Ablesung gehört zur Nutzeranfrage und widerspricht ihr nicht: Sie entscheidet, wie viele Prüfagenten für die Anfrage gleichzeitig laufen dürfen. Die Anfrage selbst bearbeiten andere Agenten nach dir; du bearbeitest sie nicht, auch nicht teilweise, und liest dafür keine Dateien.\n", ""),
|
("verbrauch", "Brueckensatz fehlt", "Diese Ablesung gehört zur Nutzeranfrage und widerspricht ihr nicht: Sie entscheidet, wie viele Prüfagenten für die Anfrage gleichzeitig laufen dürfen. Die Anfrage selbst bearbeiten andere Agenten nach dir; du bearbeitest sie nicht, auch nicht teilweise, und liest dafür keine Dateien.\n", ""),
|
||||||
```
|
```
|
||||||
|
|||||||
+154
@@ -0,0 +1,154 @@
|
|||||||
|
---
|
||||||
|
type: plan-task
|
||||||
|
title: "Task 02 — verbrauch.js: Ableser als Agententyp verbrauch-ableser, Startfehler abfangen, Attrappe prüft agentType"
|
||||||
|
status: draft
|
||||||
|
summary: "Der Ableser läuft als agentType verbrauch-ableser mit nur ToolSearch und get_usage; ein Startfehler, etwa ein in der Sitzung noch unbekannter Typ, kommt als nicht_gelesen mit Warnzeile zurück statt als Absturz. Die Attrappe wirft wie das Ziel bei einem unbekannten agentType."
|
||||||
|
tags: [plan-task, vault-tools, codeworkflow]
|
||||||
|
created: 2026-10-16
|
||||||
|
updated: 2026-10-17
|
||||||
|
modell: sonnet
|
||||||
|
modell_begruendung: "verbrauch.js und attrappe.mjs sind Abschrift, die Fälle brauchen Autorschaft."
|
||||||
|
---
|
||||||
|
|
||||||
|
Übersicht: [00-uebersicht.md](00-uebersicht.md)
|
||||||
|
|
||||||
|
# Task 02 — Agententyp verbrauch-ableser
|
||||||
|
|
||||||
|
Pascals Auftrag vom 2026-10-17: Möglichkeit 5 umsetzen. Die Agentendatei `D:/Claude/.claude/agents/verbrauch-ableser.md` und die Zeile in der Rollentabelle des Stamms hat der Koordinator angelegt. Voraussetzung ist der Stand von `verbrauch.js` nach Task 01 (Erkennung `abgewichen`, Brückensatz, `kurz`); er ist seit dem 2026-10-17 eingespielt.
|
||||||
|
|
||||||
|
## Arbeitsorte
|
||||||
|
|
||||||
|
Wie Task 01, `S` gleich. Du schreibst **nur**:
|
||||||
|
|
||||||
|
- `S/wf-neu/verbrauch.js`
|
||||||
|
- `S/wp-neu/attrappe.mjs`
|
||||||
|
- `S/wp-neu/faelle_workflows.mjs`
|
||||||
|
- `S/wp-neu/mutanten_workflows.py`
|
||||||
|
|
||||||
|
Nichts unter `D:/Claude/` anfassen. Keine Hilfsdateien. Kein Commit.
|
||||||
|
|
||||||
|
## Belege
|
||||||
|
|
||||||
|
- Ziel, CLI-Abbild 2.1.299 (gelesen, nicht gelaufen): `agent()` mit einem `agentType`, den die Sitzung nicht kennt, wirft `agent({agentType}): agent type '<typ>' not found. Available agents: ...`, bevor ein Agent startet.
|
||||||
|
- Stamm, „Agenten & Modelle": Mit `agentType` liefert dessen Frontmatter den Effort; ein `effort`-Parameter daneben steht nur für eine bewusste Abweichung.
|
||||||
|
- Echtlauf 2026-10-17, `wf_63cb007d-71b`: Solange die Sitzung den Typ nicht kannte, warf `agent()` genau diese Meldung, nach 145 ms und ohne Agent.
|
||||||
|
- Echtlauf 2026-10-17, `wf_1901f51f-dd3`, drei Ablesungen mit `agentType: 'verbrauch-ableser'`: Alle drei liefen als `verbrauch-ableser` auf `claude-haiku-5-5` mit Effort `low` und riefen nur ToolSearch, `get_usage` und StructuredOutput auf. Als nachladbares Werkzeug war nur `get_usage` angeboten. Die Werte stimmten mit der eigenen Ablesung des Koordinators überein (36 % und 53 %). Der Lauf startete in einem Zug ohne Nutzernachricht, der Ableser sah also keine abweichende Nutzeranfrage.
|
||||||
|
|
||||||
|
## 1 verbrauch.js
|
||||||
|
|
||||||
|
**1a, Kopfkommentar:** Vor der Zeile `// Was das Skript nicht sieht: Agenten, die nach der Ablesung in anderen Sitzungen starten;` kommen diese Zeilen:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// - Wer abliest: der Agententyp verbrauch-ableser (.claude/agents/), Haiku mit effort low und
|
||||||
|
// nur ToolSearch und get_usage, damit ein Ableser, der der Nutzeranfrage folgt, nichts anderes
|
||||||
|
// tun kann. Kennt die Sitzung den Typ nicht, wirft agent() vor dem Start (CLI-Abbild 2.1.299);
|
||||||
|
// das kommt als nicht_gelesen mit WARNUNG zurück, nie als Rückfall auf einen Agenten mit allen
|
||||||
|
// Werkzeugen.
|
||||||
|
```
|
||||||
|
|
||||||
|
**1b, Aufruf:** Die Zeile
|
||||||
|
|
||||||
|
```js
|
||||||
|
ablesung = await agent(AUFTRAG, { label: 'verbrauch:' + args.anlass, phase: 'Verbrauch', schema: ABLESUNG, model: 'haiku' })
|
||||||
|
```
|
||||||
|
|
||||||
|
wird zu
|
||||||
|
|
||||||
|
```js
|
||||||
|
try {
|
||||||
|
ablesung = await agent(AUFTRAG, { label: 'verbrauch:' + args.anlass, phase: 'Verbrauch', schema: ABLESUNG, agentType: 'verbrauch-ableser', model: 'haiku' })
|
||||||
|
} catch (e) {
|
||||||
|
ablesung = null
|
||||||
|
startfehler = kurz((e && e.message) || e)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**1c, Variable:** Die Zeile `let ablesung = args.probe` wird zu zwei Zeilen:
|
||||||
|
|
||||||
|
```js
|
||||||
|
let ablesung = args.probe
|
||||||
|
let startfehler = null
|
||||||
|
```
|
||||||
|
|
||||||
|
**1d, Rückgabe:** Vor der Zeile `if (!ablesung || typeof ablesung !== 'object') return ohneAblesung('nicht_gelesen', 'kein Ergebnis vom Ableseagenten')` kommt:
|
||||||
|
|
||||||
|
```js
|
||||||
|
if (startfehler) {
|
||||||
|
log(`WARNUNG Verbrauch ${args.anlass}: Ableser nicht startbar (${startfehler}); die Bremse misst nicht.`)
|
||||||
|
return ohneAblesung('nicht_gelesen', 'Ableser nicht startbar: ' + startfehler)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`kurz` ist in Task 01 vor `pruefeArgs(args)` angelegt und damit vor dem Aufruf definiert. Sonst änderst du an `verbrauch.js` nichts.
|
||||||
|
|
||||||
|
## 2 attrappe.mjs
|
||||||
|
|
||||||
|
Die Attrappe wirft wie das Ziel bei einem unbekannten `agentType`. Bekannt sind die Namen der Dateien `*.md` in `D:/Claude/.claude/agents/`, abgeleitet aus `ECHTER_ORDNER`, oder eine Liste aus `optionen.agenten`. Nicht relativ zur Attrappe, weil sie im Prüfstand auch als Kopie im Scratchpad läuft.
|
||||||
|
|
||||||
|
**2a, Kopfkommentar:** Nach der Zeile `// - parallel() ist eine Barriere; ein werfender Thunk wird zu null.` kommt:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// - agent() mit einem agentType, den die Sitzung nicht kennt, wirft vor dem Start (CLI-Abbild
|
||||||
|
// 2.1.299). Bekannt sind die Agentendateien unter .claude/agents/ des Vaults oder optionen.agenten.
|
||||||
|
```
|
||||||
|
|
||||||
|
**2b, Import:** `import { readFileSync, existsSync } from 'node:fs'` wird zu `import { readFileSync, existsSync, readdirSync } from 'node:fs'`.
|
||||||
|
|
||||||
|
**2c, Liste:** Nach der Zeile `const ECHTER_ORDNER = 'D:/Claude/.claude/workflows/'` kommt:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// Agententypen wie in der Sitzung: die Agentendateien des Vaults neben den Workflows. Fehlt der
|
||||||
|
// Ordner, ist jeder agentType unbekannt und jeder Fall damit rot: ein Befund, kein Durchlass.
|
||||||
|
const AGENTEN_ORDNER = path.join(ECHTER_ORDNER, '..', 'agents')
|
||||||
|
function agentenDesVaults() {
|
||||||
|
if (!existsSync(AGENTEN_ORDNER)) return []
|
||||||
|
return readdirSync(AGENTEN_ORDNER).filter(n => n.endsWith('.md')).map(n => n.slice(0, -3))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**2d, in `fuehreAus`:** Nach der Zeile `const ordner = optionen.ordner || path.dirname(datei)` kommt `const agenten = optionen.agenten || agentenDesVaults()`.
|
||||||
|
|
||||||
|
**2e, in `agent()`:** Nach dem Block, der das Schema prüft (er endet mit der schließenden `}` nach `throw new Error('required nicht in properties: ' + opts.label)`), kommt:
|
||||||
|
|
||||||
|
```js
|
||||||
|
if (opts.agentType != null && !agenten.includes(String(opts.agentType))) throw new Error(`agent({agentType}): agent type '${opts.agentType}' not found. Available agents: ${agenten.join(', ')}`)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3 faelle_workflows.mjs
|
||||||
|
|
||||||
|
| Abschnitt | Name | Eingabe | Erwartung |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `attrappe` | `unbekannter agentType wirft vor dem Start` | Skript mit `agent('x', { label: 'werkzeug:planwerkzeuge', model: 'sonnet', agentType: 'gibt-es-nicht' })` | Fehler passt auf `/agent type 'gibt-es-nicht' not found/`, `p.agents.length === 0` |
|
||||||
|
| `attrappe` | `bekannter agentType aus optionen.agenten läuft` | dasselbe mit `agentType: 'eigen'` und `fuehreAus(..., { ordner: TMP, agenten: ['eigen'] })` | kein Fehler, ein Agent mit `agentType 'eigen'` |
|
||||||
|
| `verbrauch` | Grundlinie ergänzen | bestehender Fall `Grundlinie: Agent liest 10/10, ...` | zusätzlich `p.agents[0].agentType === 'verbrauch-ableser'` und `p.agents[0].effort === undefined` |
|
||||||
|
| `verbrauch` | `Ableser-Typ unbekannt: nicht_gelesen mit Warnung, kein Absturz` | `fuehreAus(WF('verbrauch'), { anlass: 't', wunsch: 3 }, antworten({}), { ordner: ORDNER, agenten: ['planer'] })` | `fehler === null`, `status 'nicht_gelesen'`, `plaetze 2`, `grund` passt auf `/nicht startbar/` und auf `/not found/`, genau eine Warnzeile, kein Agent gestartet |
|
||||||
|
| `plankritik` | `Ableser-Typ unbekannt: Linsen zu zweit, Plankritik läuft` | `PK(PK_ARGS, {}, { agenten: ['pruefer'] })` | `e.status 'fertig'`, Spitze `Plankritik`/`opus` gleich 2, `e.verbrauch.every(v => v.status === 'nicht_gelesen')`, drei Warnzeilen |
|
||||||
|
|
||||||
|
Für den letzten Fall nimmt `PK` die Optionen schon als drittes Argument entgegen.
|
||||||
|
|
||||||
|
## 4 mutanten_workflows.py
|
||||||
|
|
||||||
|
Nach den Einträgen aus Task 01:
|
||||||
|
|
||||||
|
```python
|
||||||
|
("verbrauch", "Ableser ohne Agententyp", "agentType: 'verbrauch-ableser', ", ""),
|
||||||
|
("verbrauch", "Startfehler nicht abgefangen", " ablesung = null\n startfehler = kurz((e && e.message) || e)\n", " throw e\n"),
|
||||||
|
("verbrauch", "Startfehler ohne Warnzeile", "log(`WARNUNG Verbrauch ${args.anlass}: Ableser nicht startbar", "String(`WARNUNG Verbrauch ${args.anlass}: Ableser nicht startbar"),
|
||||||
|
("verbrauch", "Ableser erbt Sitzungseffort statt Frontmatter", "agentType: 'verbrauch-ableser', model: 'haiku' }", "agentType: 'verbrauch-ableser', model: 'haiku', effort: 'max' }"),
|
||||||
|
```
|
||||||
|
|
||||||
|
**Korrektur nach dem Lauf (2026-10-17):** Der Block 1d beginnt wie die Warnzeile aus Task 01, das Muster des Mutanten „Warnzeile fehlt“ kam dadurch zweimal vor (VERALTET). Es lautet jetzt ``log(`WARNUNG Verbrauch ${args.anlass}: Der Ableser``; Task 01 ist mitgezogen.
|
||||||
|
|
||||||
|
## Prüfen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Alle Fälle gegen den neuen Stand
|
||||||
|
node "$S/wp-neu/faelle_workflows.mjs" "$S/wf-neu"; echo "EXIT $?"
|
||||||
|
# Rotlauf der neuen verbrauch- und plankritik-Fälle gegen den Stand nach Task 01 ist nicht nötig; Rotlauf gegen den alten Stand
|
||||||
|
node "$S/wp-neu/faelle_workflows.mjs" "D:/Claude/.claude/workflows" | grep -v "^OK"; echo "EXIT ${PIPESTATUS[0]}"
|
||||||
|
# Mutanten: kein UEBERLEBT, kein VERALTET
|
||||||
|
python "$S/wp-neu/mutanten_workflows.py" "$S/wf-neu" | grep -v "^GEFANGEN"; echo "EXIT ${PIPESTATUS[0]}"
|
||||||
|
```
|
||||||
|
|
||||||
|
Melde am Ende: geänderte Dateien, die drei Ausgaben, und **was du anders gemacht hast als beauftragt und warum**.
|
||||||
|
|
||||||
@@ -146,6 +146,22 @@ fall('attrappe', 'agent() ohne model ist ein Verstoß', async () => {
|
|||||||
return fehler
|
return fehler
|
||||||
})
|
})
|
||||||
|
|
||||||
|
fall('attrappe', 'unbekannter agentType wirft vor dem Start', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { fehler: f, protokoll: p } = await A(skript('typ-unbekannt', "return await agent('x', { label: 'werkzeug:planwerkzeuge', model: 'sonnet', agentType: 'gibt-es-nicht' })"))
|
||||||
|
soll(f && /agent type 'gibt-es-nicht' not found/.test(f), 'Fehler: ' + f)
|
||||||
|
soll(p.agents.length === 0, 'Agent gestartet trotz unbekanntem Typ: ' + p.agents.length)
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
|
fall('attrappe', 'bekannter agentType aus optionen.agenten läuft', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { fehler: f, protokoll: p } = await fuehreAus(skript('typ-bekannt', "return await agent('x', { label: 'werkzeug:planwerkzeuge', model: 'sonnet', agentType: 'eigen' })"), {}, antworten({}), { ordner: TMP, agenten: ['eigen'] })
|
||||||
|
soll(f === null, 'Fehler: ' + f)
|
||||||
|
soll(p.agents.length === 1 && p.agents[0].agentType === 'eigen', 'Agenten: ' + JSON.stringify(p.agents.map(x => x.agentType)))
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
fall('attrappe', 'Ergebnis ohne required-Feld wird null und ist ein Verstoß', async () => {
|
fall('attrappe', 'Ergebnis ohne required-Feld wird null und ist ein Verstoß', async () => {
|
||||||
const { fehler, soll } = pruefe()
|
const { fehler, soll } = pruefe()
|
||||||
const schema = "{ type: 'object', properties: { a: { type: 'string' }, b: { type: 'string' } }, required: ['a', 'b'] }"
|
const schema = "{ type: 'object', properties: { a: { type: 'string' }, b: { type: 'string' } }, required: ['a', 'b'] }"
|
||||||
@@ -171,6 +187,7 @@ fall('verbrauch', 'Grundlinie: Agent liest 10/10, wunsch 3 ergibt 3 Plätze', as
|
|||||||
soll(f === null, 'Fehler: ' + f)
|
soll(f === null, 'Fehler: ' + f)
|
||||||
soll(e && e.status === 'ok' && e.plaetze === 3 && e.gebremst === false && e.quelle === 'agent', 'Ergebnis: ' + JSON.stringify(e))
|
soll(e && e.status === 'ok' && e.plaetze === 3 && e.gebremst === false && e.quelle === 'agent', 'Ergebnis: ' + JSON.stringify(e))
|
||||||
soll(p.agents.length === 1 && p.agents[0].label === 'verbrauch:test' && p.agents[0].modell === 'haiku', 'Agent: ' + JSON.stringify(p.agents.map(x => [x.label, x.modell])))
|
soll(p.agents.length === 1 && p.agents[0].label === 'verbrauch:test' && p.agents[0].modell === 'haiku', 'Agent: ' + JSON.stringify(p.agents.map(x => [x.label, x.modell])))
|
||||||
|
soll(p.agents[0].agentType === 'verbrauch-ableser' && p.agents[0].effort === undefined, 'Typ/Effort: ' + JSON.stringify([p.agents[0].agentType, p.agents[0].effort]))
|
||||||
ohneVerstoss(p, soll)
|
ohneVerstoss(p, soll)
|
||||||
return fehler
|
return fehler
|
||||||
})
|
})
|
||||||
@@ -313,6 +330,16 @@ fall('verbrauch', 'Auftrag ordnet die Ablesung der Nutzeranfrage zu', async () =
|
|||||||
return fehler
|
return fehler
|
||||||
})
|
})
|
||||||
|
|
||||||
|
fall('verbrauch', 'Ableser-Typ unbekannt: nicht_gelesen mit Warnung, kein Absturz', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { ergebnis: e, fehler: f, protokoll: p } = await fuehreAus(WF('verbrauch'), { anlass: 't', wunsch: 3 }, antworten({}), { ordner: ORDNER, agenten: ['planer'] })
|
||||||
|
soll(f === null, 'Fehler: ' + f)
|
||||||
|
soll(e && e.status === 'nicht_gelesen' && e.plaetze === 2 && /nicht startbar/.test(e.grund) && /not found/.test(e.grund), JSON.stringify(e))
|
||||||
|
soll(warnung(p).length === 1, 'Warnzeilen: ' + JSON.stringify(warnung(p)))
|
||||||
|
soll(p.agents.length === 0, 'Agenten gestartet: ' + p.agents.length)
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
for (const [name, probe] of [
|
for (const [name, probe] of [
|
||||||
['status unavailable', { verfuegbar: true, status: 'unavailable', plan: '', fenster: [], fehler: '' }],
|
['status unavailable', { verfuegbar: true, status: 'unavailable', plan: '', fenster: [], fehler: '' }],
|
||||||
['verfuegbar false', { verfuegbar: false, status: 'fehler', plan: '', fenster: [], fehler: 'Werkzeug fehlt' }],
|
['verfuegbar false', { verfuegbar: false, status: 'fehler', plan: '', fenster: [], fehler: 'Werkzeug fehlt' }],
|
||||||
@@ -479,6 +506,16 @@ fall('plankritik', 'Ablesung ohne Ergebnis: Linsen zu zweit (alte Obergrenze)',
|
|||||||
return fehler
|
return fehler
|
||||||
})
|
})
|
||||||
|
|
||||||
|
fall('plankritik', 'Ableser-Typ unbekannt: Linsen zu zweit, Plankritik läuft', async () => {
|
||||||
|
const { fehler, soll } = pruefe()
|
||||||
|
const { ergebnis: e, protokoll: p } = await PK(PK_ARGS, {}, { agenten: ['pruefer'] })
|
||||||
|
soll(e && e.status === 'fertig', 'status ' + (e && e.status))
|
||||||
|
soll(spitze(p, 'Plankritik', 'opus') === 2, 'Linsen gleichzeitig: ' + spitze(p, 'Plankritik', 'opus'))
|
||||||
|
soll(e && e.verbrauch.every(v => v.status === 'nicht_gelesen'), JSON.stringify(e && e.verbrauch.map(v => v.status)))
|
||||||
|
soll(warnung(p).length === 3, 'Warnzeilen: ' + JSON.stringify(warnung(p)))
|
||||||
|
return fehler
|
||||||
|
})
|
||||||
|
|
||||||
fall('plankritik', 'Abweichung bei der ersten Ablesung: Linsen zu zweit, Status abgewichen im Ergebnis', async () => {
|
fall('plankritik', 'Abweichung bei der ersten Ablesung: Linsen zu zweit, Status abgewichen im Ergebnis', async () => {
|
||||||
const { fehler, soll } = pruefe()
|
const { fehler, soll } = pruefe()
|
||||||
const { ergebnis: e, protokoll: p } = await PK(PK_ARGS, { ablesungen: [BEANTWORTET] })
|
const { ergebnis: e, protokoll: p } = await PK(PK_ARGS, { ablesungen: [BEANTWORTET] })
|
||||||
|
|||||||
@@ -82,9 +82,13 @@ MUTANTEN = [
|
|||||||
("verbrauch", "Bericht in plan bei status fehler uebersehen", "return a.status === 'fehler' && typeof a.plan === 'string' && a.plan.trim() !== ''", "return false"),
|
("verbrauch", "Bericht in plan bei status fehler uebersehen", "return a.status === 'fehler' && typeof a.plan === 'string' && a.plan.trim() !== ''", "return false"),
|
||||||
("verbrauch", "ehrlicher Fehler gilt als Abweichung", "'unavailable', 'fehler']", "'unavailable']"),
|
("verbrauch", "ehrlicher Fehler gilt als Abweichung", "'unavailable', 'fehler']", "'unavailable']"),
|
||||||
("verbrauch", "Abweichung als nicht_gelesen", "return ohneAblesung('abgewichen',", "return ohneAblesung('nicht_gelesen',"),
|
("verbrauch", "Abweichung als nicht_gelesen", "return ohneAblesung('abgewichen',", "return ohneAblesung('nicht_gelesen',"),
|
||||||
("verbrauch", "Warnzeile fehlt", "log(`WARNUNG Verbrauch", "String(`WARNUNG Verbrauch"),
|
("verbrauch", "Warnzeile fehlt", "log(`WARNUNG Verbrauch ${args.anlass}: Der Ableser", "String(`WARNUNG Verbrauch ${args.anlass}: Der Ableser"),
|
||||||
("verbrauch", "status ungekuerzt im grund", ".trim().slice(0, 160)", ".trim()"),
|
("verbrauch", "status ungekuerzt im grund", ".trim().slice(0, 160)", ".trim()"),
|
||||||
("verbrauch", "Brueckensatz fehlt", "Diese Ablesung gehört zur Nutzeranfrage und widerspricht ihr nicht: Sie entscheidet, wie viele Prüfagenten für die Anfrage gleichzeitig laufen dürfen. Die Anfrage selbst bearbeiten andere Agenten nach dir; du bearbeitest sie nicht, auch nicht teilweise, und liest dafür keine Dateien.\n", ""),
|
("verbrauch", "Brueckensatz fehlt", "Diese Ablesung gehört zur Nutzeranfrage und widerspricht ihr nicht: Sie entscheidet, wie viele Prüfagenten für die Anfrage gleichzeitig laufen dürfen. Die Anfrage selbst bearbeiten andere Agenten nach dir; du bearbeitest sie nicht, auch nicht teilweise, und liest dafür keine Dateien.\n", ""),
|
||||||
|
("verbrauch", "Ableser ohne Agententyp", "agentType: 'verbrauch-ableser', ", ""),
|
||||||
|
("verbrauch", "Startfehler nicht abgefangen", " ablesung = null\n startfehler = kurz((e && e.message) || e)\n", " throw e\n"),
|
||||||
|
("verbrauch", "Startfehler ohne Warnzeile", "log(`WARNUNG Verbrauch ${args.anlass}: Ableser nicht startbar", "String(`WARNUNG Verbrauch ${args.anlass}: Ableser nicht startbar"),
|
||||||
|
("verbrauch", "Ableser erbt Sitzungseffort statt Frontmatter", "agentType: 'verbrauch-ableser', model: 'haiku' }", "agentType: 'verbrauch-ableser', model: 'haiku', effort: 'max' }"),
|
||||||
("plankritik", "Muster-Satz fehlt (N5)", "musterModus\n ? ' Kein Mutant ist gefahren; OK heißt hier nur, dass das Muster genau einmal in der Datei steht; ob ein Test fällt, ist unbelegt.'\n : ", ""),
|
("plankritik", "Muster-Satz fehlt (N5)", "musterModus\n ? ' Kein Mutant ist gefahren; OK heißt hier nur, dass das Muster genau einmal in der Datei steht; ob ein Test fällt, ist unbelegt.'\n : ", ""),
|
||||||
("plankritik", "Exit 2 wird wie 0 behandelt (N7)", "exitCode === 2 || exitCode === 3", "false"),
|
("plankritik", "Exit 2 wird wie 0 behandelt (N7)", "exitCode === 2 || exitCode === 3", "false"),
|
||||||
("quorum", "Mehrheit schon bei einer Stimme", "mehrheit: beteiligt.length >= 2", "mehrheit: beteiligt.length >= 1"),
|
("quorum", "Mehrheit schon bei einer Stimme", "mehrheit: beteiligt.length >= 2", "mehrheit: beteiligt.length >= 1"),
|
||||||
|
|||||||
Reference in new issue
Block a user