Files
Masterarbeit/.claude/skills/run-experiment/protokoll-geruest.py
T
Christoph SchwörerandClaude Opus 5 28e927013b LM-Studio-Matrix mit gemma-4-e4b: zwoelf Laeufe und vier Befunde
Skill 12.0.0 bis 12.2.0 und Adapter 2.0.0 bis 2.2.0.

Werkzeugfreigabe: Die Shell-Rechte des OpenCode-Adapters sind jetzt eine
Denylist wie beim Claude-Adapter statt einer Allowlist. Ausloeser war der
erste Qwen-Lauf, dessen einzige beide Werkzeugaufrufe an einer Pipeline
scheiterten. Kontrolltest belegt beide Richtungen: Get-ChildItem | Format-Table
laeuft durch, rm wird verweigert, die Datei bleibt bestehen.

Lokaler Betrieb: Der Preflight entlaedt alle Modelle vor jedem Lauf und laedt
den Kontext auf das Modellmaximum. Gemessen waren zuvor beide Modelle
gleichzeitig geladen - 168 MiB frei von 16,3 GB. Qwen 27B passt auf dieser
Karte nicht und wurde durch qwen3.5-9b ersetzt, in Q4_K_M wie Gemma.

Messinstrument: analyse-anforderungen.py erkennt Feldnamen in Markdown-
Fettschrift und Modulpraefixe in IDs. Der erste lokale Lauf mit Artefakten
wurde sonst mit null Anforderungen gezaehlt statt mit neun. Regressionsprobe
an vier Claude-Laeufen unveraendert.

Befunde: Der Standard-Ausgabeblock, mit dem Claude sieben Artefakte erzeugt,
liefert bei gemma-4-e4b null von sechs Laeufen ein Ergebnis - das Modell loest
den Pfad relativ zum Arbeitsverzeichnis auf. Ein Lauf startete alle sieben
vorgesehenen Rollen und lieferte neun formkonforme Anforderungen. Ein anderer
erzeugte sieben richtig benannte Dateien ohne eine einzige formkonforme
Anforderung. Und eine Shell-Umleitung schrieb an der Denylist vorbei in den
eingefrorenen Snapshot - gefunden vom Vorher/Nachher-Vergleich, nicht von der
Regel.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 11:02:04 +02:00

173 lines
7.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Erzeugt das Messprotokoll-Geruest eines Laufs aus seinen Rohdaten.
Fuellt ausschliesslich Felder, die aus `RawResult.json`, `_meta` und dem
Dateibestand **belegbar** sind. Alles Uebrige bleibt als Platzhalter stehen und
ist von Hand zu ergaenzen - insbesondere der Abschnitt *Anmerkungen*, der die
Deutung enthaelt und nicht generierbar ist.
Nicht ermittelbare Groessen werden als `nicht erfasst` ausgewiesen, niemals
geschaetzt (Randbedingung des Skills).
python protokoll-geruest.py "<Laufverzeichnis>" [--ueberschreiben]
"""
from __future__ import annotations
import argparse
import hashlib
import json
import subprocess
from pathlib import Path
EBENEN_DATEIEN = ("StRS.md", "SyRS.md", "SwRS.md")
def sha256(pfad: Path) -> str:
return hashlib.sha256(pfad.read_bytes()).hexdigest().upper()
def lies(pfad: Path, standard: str = "nicht erfasst") -> str:
try:
return pfad.read_text(encoding="utf-8").strip() or standard
except OSError:
return standard
def zahl(n) -> str:
"""Tausenderpunkte nach deutscher Schreibweise."""
try:
return f"{int(n):,}".replace(",", ".")
except (TypeError, ValueError):
return "nicht erfasst"
def dauer(ms: int) -> str:
s = ms // 1000
return f"{s // 3600:02d}:{(s % 3600) // 60:02d}:{s % 60:02d}"
def git_kopf(repo: Path) -> str:
try:
r = subprocess.run(
["git", "-C", str(repo), "rev-parse", "HEAD"],
capture_output=True, text=True, check=False,
)
return r.stdout.strip() or "nicht erfasst"
except OSError:
return "nicht erfasst"
def main() -> int:
parser = argparse.ArgumentParser(description="Protokollgeruest erzeugen")
parser.add_argument("lauf")
parser.add_argument("--repo", default="C:/DEV/MasterArbeit")
parser.add_argument("--ueberschreiben", action="store_true")
args = parser.parse_args()
lauf = Path(args.lauf).resolve()
ziel = lauf / "Protokoll.md"
if ziel.exists() and not args.ueberschreiben:
print(f"{ziel} existiert bereits - mit --ueberschreiben erzwingen.")
return 1
roh = json.loads((lauf / "RawResult.json").read_text(encoding="utf-8"))
meta = lauf / "_meta"
u = roh.get("usage", {})
lr = roh.get("local_runtime") or {}
erfasst = roh.get("usage_captured", True)
def tok(feld: str) -> str:
return zahl(u.get(feld, 0)) if erfasst else "nicht erfasst"
# Ebene und Iteration aus der Ablagestruktur ableiten
teile = lauf.parts
iteration = next((p for p in teile if p.startswith("Iteration")), "?")
zelle = "/".join(teile[teile.index(iteration):-1]) if iteration in teile else "?"
# Die Prompt-Datei liegt im Versuchsordner, also oberhalb der
# Bedingungsebenen; deren Anzahl schwankt mit der Modell-ID (ein- oder
# zweiteilig). Deshalb aufwaerts suchen statt eine feste Tiefe annehmen.
prompt_datei = None
for eltern in lauf.parents[:8]:
treffer = sorted(eltern.glob("*_Prompt.md"))
if treffer:
prompt_datei = treffer[-1]
break
anforderungen = lies(meta / "anforderungen.md", "*(nicht ausgewertet)*")
dateien = sorted(p.name for p in (lauf / "Ergebnisse").glob("*") if p.is_file())
vorher, nachher = lies(meta / "before.txt", ""), lies(meta / "after.txt", "")
text = f"""# Messprotokoll – {zelle}
> **GERUEST** – maschinell aus den Rohdaten erzeugt. Die Abschnitte *Anmerkungen*
> und *Gueltigkeit* sind von Hand zu pruefen und zu ergaenzen.
## Lauf
- **Prompt-Datei:** `{prompt_datei.name if prompt_datei else 'nicht erfasst'}`
- **SHA-256 (Prompt):** `{sha256(prompt_datei) if prompt_datei else 'nicht erfasst'}`
- **Startzeit:** {lies(meta / 'startzeit.txt')}
- **Endzeit:** {lies(meta / 'endzeit.txt')}
- **Dauer gesamt:** {dauer(roh.get('duration_ms', 0))} (API: nicht erfasst – OpenCode liefert keine separate API-Zeit)
- **Root-Verzeichnis:** `{args.repo}/QuellCode/CentronERP`
- **Codebasis-Commit:** `{git_kopf(Path(args.repo))}` (vor dem Lauf dirty: {'nein' if not vorher else 'ja – ' + vorher.replace(chr(10), '; ')})
- **Snapshot-Zustand:** bereinigt von KI-Konfigurationen: ja; Remote entkoppelt: ja
## Werkzeugkonfiguration
- **Skill-Version:** {lauf.name.split('_v')[-1].split('-')[0] if '_v' in lauf.name else 'nicht erfasst'}
- **Werkzeugadapter:** OpenCode, `opencode-adapter.py --provider {roh.get('provider')}` (Adapter-Version {roh.get('adapter_version')})
- **CLI-Version:** OpenCode {roh.get('opencode_version', 'nicht erfasst')}
- **Modell (angefordert):** `{roh.get('model_requested')}`
- **Modell (tatsaechlich):** `{roh.get('model')}`
- **Kontrolle Modell:** {'bestanden' if roh.get('model') == roh.get('model_requested') else '**verletzt**'}
- **Effort:** `{roh.get('effort')}` angefordert, **{'wirksam' if roh.get('effort_applied') else 'nicht wirksam'}** (`effort_applied: {str(roh.get('effort_applied')).lower()}`)
- **Ablage:** `{zelle}/`
- **Agentenmodus:** `{roh.get('mode')}`
- **Kontextfenster:** {zahl(roh.get('context_window', 0))} Tokens geladen (Modellmaximum {zahl(lr.get('max_context_length', 0))})
- **Sampling-Parameter:** nicht steuerbar
- **Lokaler Modellbetrieb:** Runtime `{lr.get('compatibility_type', '?')}`, `lms` {lr.get('lms_version', '?')}, Architektur `{lr.get('arch', '?')}`, **Quantisierung `{lr.get('quantization', '?')}`**, {lr.get('parallel_slots', '?')} Slots, GPU-Offload `{lr.get('gpu_offload', '?')}`, alleiniges Modell: {str(lr.get('alleiniges_modell', '?')).lower()}
- **Abbruchsicherungen:** `--stall-timeout 0`, `--max-runtime` siehe Matrixskript
- **Subagenten:** `spawned` = {roh.get('subagent_stats', {}).get('spawned', 0)}, `completed` = {roh.get('subagent_stats', {}).get('completed', 0)}, `failed` = {roh.get('subagent_stats', {}).get('failed', 0)}
- **Rollen:** {json.dumps(roh.get('subagent_stats', {}).get('by_type', {}), ensure_ascii=False)}
## Validierungsstichprobe
- **Stand:** entfaellt
## Verbrauch
| Messgroesse | Wert |
|---|---|
| Input-Tokens | {tok('prompt_tokens')} |
| Output-Tokens | {tok('completion_tokens')} |
| Reasoning-Tokens | {tok('reasoning_tokens')} |
| Cache-Write-/Cache-Read-Tokens | nicht erfasst – der lokale Server liefert keine |
| Agent-Turns | {roh.get('num_turns', 0)} |
**Tokens gesamt: {tok('total_tokens')}.** Kosten `0` – lokaler Betrieb ({roh.get('cost_source', '')}).
{'' if erfasst else chr(10) + '**Achtung:** ' + roh.get('usage_note', '') + chr(10)}
## Gefundene Anforderungen
{anforderungen}
## Ergebnis
- **Status:** `is_error: {str(roh.get('is_error')).lower()}`, `subtype: {roh.get('subtype')}`, `exit_code: {roh.get('exit_code')}`, `timed_out: {str(roh.get('timed_out')).lower()}`
- **Session-ID:** `{roh.get('session_id')}`
- **Werkzeugaufrufe:** {roh.get('tool_call_count', 0)} – {json.dumps(roh.get('tool_call_types', {}), ensure_ascii=False)}
- **Kontrolle Agentenmodus:** `subagent_stats.spawned` = {roh.get('subagent_stats', {}).get('spawned', 0)}{' – korrekt fuer solo' if roh.get('mode') == 'solo' else ''}
- **Gueltigkeit:** *(pruefen)* {'**Fehlmessung** – Ergebnisverzeichnis leer.' if not dateien else 'Ergebnisdateien vorhanden.'}
- **Erzeugte Dateien:** {', '.join(dateien) if dateien else 'keine'}
- **Root unveraendert:** {'ja' if vorher == nachher else '**nein – pruefen**'}
- **Fehlermeldungen:** {json.dumps(roh.get('errors', []), ensure_ascii=False)}
## Anmerkungen/Auffaelligkeiten
*(von Hand zu ergaenzen)*
"""
ziel.write_text(text, encoding="utf-8")
print(f"{ziel}")
return 0
if __name__ == "__main__":
raise SystemExit(main())