Compare commits
15
Commits
7df384f6d2
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08d90f1ccb | ||
|
|
e2c3a0e8f8 | ||
|
|
6c5c26a2e4 | ||
|
|
28e927013b | ||
|
|
654339464e | ||
|
|
611fd0a80c | ||
|
|
b369e6115e | ||
|
|
ca52aa4701 | ||
|
|
8a22d586f1 | ||
|
|
37275c96d6 | ||
|
|
ea1f3caff7 | ||
|
|
3d5b691bfa | ||
|
|
affde3a45f | ||
|
|
844b4e5569 | ||
|
|
f349d189c7 |
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -16,11 +16,66 @@ RISIKO = re.compile(r'sicherheit|abrechnung|fakturier|berechtigung|recht|zugriff
|
||||
r'passwort|rolle|lizenz|steuer|zahlung|mahn', re.I)
|
||||
|
||||
|
||||
# Feldnamen in Markdown-Fettschrift, z. B. "**ID:** StRS-01" statt "ID: StRS-01".
|
||||
# Modelle formatieren die Vorgabe des Prompts haeufig als Markdown aus; ohne
|
||||
# Normalisierung bleibt eine vollstaendig korrekt gefuellte Datei unerkannt.
|
||||
# Beobachtet am 01.09.2026 im Lauf Iteration 7/.../v12.1.0-001c: vier Dateien mit
|
||||
# regelkonformen Bloecken wurden als "keine Anforderungen" gezaehlt.
|
||||
FETTES_FELD = re.compile(r'(?m)^[ \t]*\*\*([^*:\n]{1,40}):\*\*[ \t]*')
|
||||
|
||||
|
||||
# Ueberschriftenmarker vor einem Feldnamen, z. B. "### ID: StRS-1".
|
||||
# Beobachtet am 01.09.2026 im Lauf Iteration 15/.../v13.0.0-312f: sechs Dateien
|
||||
# mit vollstaendig ausgefuellten, regelkonformen Bloecken wurden mit 0 gezaehlt,
|
||||
# weil die ID-Zeile als Markdown-Ueberschrift gesetzt war.
|
||||
UEBERSCHRIFT_FELD = re.compile(r'(?m)^[ \t]*#{1,6}[ \t]+(?=(?:\*\*)?[A-Za-zÄÖÜäöüß ]{1,40}:)')
|
||||
|
||||
|
||||
# Kennung als blosse Ueberschrift, ohne Feldnamen: "### StRS-001".
|
||||
# Nur dann als ID gewertet, wenn die Pflichtfelder unmittelbar folgen - sonst
|
||||
# wuerde jede Zwischenueberschrift zur Anforderung. Beobachtet am 01.09.2026 im
|
||||
# Lauf Iteration 15/.../v13.0.0-0b06 (qwen, solo): sieben Dateien, alle Felder
|
||||
# ausser der Kennung regelkonform beschriftet.
|
||||
UEBERSCHRIFT_KENNUNG = re.compile(
|
||||
r'(?m)^[ \t]*#{1,6}[ \t]*((?:StRS|SyRS|SwRS)[A-Za-z0-9_.\-]*)[ \t]*$'
|
||||
r'(?=(?:[ \t]*\n)*(?:[ \t]*(?:\*\*)?(?:Titel|Ebene)[:\*]))',
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
|
||||
# Felder als eingerueckte Listenpunkte, z. B. " - ID: StRS-001" unterhalb eines
|
||||
# Bullets. Beobachtet am 04.09.2026 im Lauf Iteration 9/.../v13.0.0-d345: sieben
|
||||
# Dateien, 323 kB, alle Felder korrekt gefuellt - vom Parser mit 0 gezaehlt.
|
||||
# Bewusst auf die Feldnamen des Prompts beschraenkt: Ein allgemeines Entfernen
|
||||
# der Einrueckung wuerde auch "Begruendung:" innerhalb der Belegliste treffen.
|
||||
FELDNAMEN = (
|
||||
'ID|Titel|Ebene|Typ|Qualitätsmerkmal|Qualitaetsmerkmal|Akteur|Vorbedingung|'
|
||||
'Fakt|Aussage|Ergebnis|Belege|Prüfidee|Pruefidee|Tracelinks|Konsolidierung|'
|
||||
'Übernahmewürdigkeit|Uebernahmewuerdigkeit|Status'
|
||||
)
|
||||
EINGERUECKTES_FELD = re.compile(r'(?m)^[ \t]+(?:[-*+][ \t]+)?(?=(?:\*\*)?(?:%s)[:\*])' % FELDNAMEN)
|
||||
|
||||
|
||||
def normalisiere(text):
|
||||
"""Markdown-Auszeichnung der Feldnamen auf die Klartextform zuruecknehmen.
|
||||
|
||||
Modelle setzen die Feldvorgabe des Prompts haeufig als Markdown - fett
|
||||
(``**ID:**``), als Ueberschrift (``### ID:``), ganz ohne Feldnamen
|
||||
(``### StRS-001``) oder als eingerueckte Liste (`` - ID: StRS-001``).
|
||||
Inhaltlich ist der Block dann regelkonform; ohne Normalisierung bleibt er
|
||||
unerkannt.
|
||||
"""
|
||||
text = UEBERSCHRIFT_KENNUNG.sub(lambda m: 'ID: ' + m.group(1), text)
|
||||
text = UEBERSCHRIFT_FELD.sub('', text)
|
||||
text = EINGERUECKTES_FELD.sub('', text)
|
||||
return FETTES_FELD.sub(lambda m: m.group(1) + ': ', text)
|
||||
|
||||
|
||||
def bloecke(pfad):
|
||||
"""Zerlegt eine Anforderungsdatei in Bloecke ab jeder ID:-Zeile."""
|
||||
if not os.path.exists(pfad):
|
||||
return
|
||||
text = io.open(pfad, encoding='utf-8').read()
|
||||
text = normalisiere(io.open(pfad, encoding='utf-8').read())
|
||||
teile = re.split(r'(?m)^ID:', text)
|
||||
for t in teile[1:]:
|
||||
yield 'ID:' + t
|
||||
@@ -31,14 +86,39 @@ def feld(block, name):
|
||||
return m.group(1).strip() if m else ''
|
||||
|
||||
|
||||
def ebene_von(block, aid, datei_ebene):
|
||||
"""Ebene einer Anforderung bestimmen.
|
||||
|
||||
Die Ebene wird NICHT aus dem Dateinamen abgeleitet: Agenten legen Bloecke
|
||||
regelmaessig in der Datei einer anderen Ebene ab (beobachtet im Lauf
|
||||
Iteration 2/.../094250_v4.2.1-c69e: 12 StRS- und 5 SyRS-Bloecke standen in SwRS.md).
|
||||
Massgeblich ist das Feld `Ebene:` des Blocks, hilfsweise das ID-Praefix,
|
||||
erst zuletzt die Datei.
|
||||
"""
|
||||
f = feld(block, 'Ebene')
|
||||
for eb in EBENEN:
|
||||
if f.strip().upper().startswith(eb.upper()):
|
||||
return eb, False
|
||||
# IDs tragen haeufig ein Modulpraefix ("M003-StRS-01"). Die Ebene steht dann
|
||||
# nicht am Anfang, sondern als Bestandteil der ID; ein reiner Praefixtest
|
||||
# meldet sonst eine Fremdablage, die es nicht gibt.
|
||||
kennung = aid.strip().upper()
|
||||
for eb in EBENEN:
|
||||
if re.search(r'(?:^|[^A-Z])%s' % eb.upper(), kennung):
|
||||
return eb, eb.upper() != datei_ebene.upper()
|
||||
return datei_ebene, False
|
||||
|
||||
|
||||
def analysiere(lauf):
|
||||
erg = os.path.join(lauf, 'Ergebnisse')
|
||||
anf = []
|
||||
for eb in EBENEN:
|
||||
for b in bloecke(os.path.join(erg, eb + '.md')):
|
||||
for eb_datei in EBENEN:
|
||||
for b in bloecke(os.path.join(erg, eb_datei + '.md')):
|
||||
aid = feld(b, 'ID')
|
||||
if not aid:
|
||||
continue
|
||||
eb, _ = ebene_von(b, aid, eb_datei)
|
||||
fremd = not aid.strip().upper().startswith(eb_datei.upper())
|
||||
belege = re.findall(r'\[(PRIM\w*R|SEKUND\w*R|KONTEXT)\]', b)
|
||||
norm = []
|
||||
for x in belege:
|
||||
@@ -46,7 +126,8 @@ def analysiere(lauf):
|
||||
else 'SEKUNDÄR' if x.startswith('SEKUND') else 'KONTEXT')
|
||||
status = feld(b, 'Status')
|
||||
anf.append(dict(
|
||||
id=aid, ebene=eb, titel=feld(b, 'Titel'), typ=feld(b, 'Typ') or '(ohne)',
|
||||
id=aid, ebene=eb, datei_ebene=eb_datei, fremdabgelegt=fremd,
|
||||
titel=feld(b, 'Titel'), typ=feld(b, 'Typ') or '(ohne)',
|
||||
belege=norm, status=status,
|
||||
hypothese=('HYPOTHESE' in status.upper()) or ('[HYPOTHESE]' in b),
|
||||
workaround='workaround' in status.lower(),
|
||||
@@ -123,6 +204,20 @@ def abschnitt(anf):
|
||||
z.append('| %s | %d | %s |' % (eb, je_ebene.get(eb, 0), pct(je_ebene.get(eb, 0), n)))
|
||||
z += ['| **Gesamt** | **%d** | 100 %% |' % n, '']
|
||||
|
||||
fremd = [a for a in anf if a.get('fremdabgelegt')]
|
||||
if fremd:
|
||||
nach = collections.Counter(
|
||||
'%s-Block in `%s.md`' % (a['ebene'], a['datei_ebene']) for a in fremd)
|
||||
z.append('')
|
||||
z.append('> **Auffälligkeit – Ebene weicht von der Ablagedatei ab.** %d von %d '
|
||||
'Anforderungen stehen in der Datei einer anderen Ebene: %s. Die Ebene wurde '
|
||||
'aus dem Feld `Ebene:` beziehungsweise dem ID-Präfix bestimmt, nicht aus dem '
|
||||
'Dateinamen. Für die Set-Qualität ist das relevant: Die Dreiteilung StRS / '
|
||||
'SyRS / SwRS ist dann nicht mehr an der Dateistruktur ablesbar.'
|
||||
% (len(fremd), n,
|
||||
', '.join('%d × %s' % (v, k) for k, v in sorted(nach.items()))))
|
||||
z.append('')
|
||||
|
||||
z += ['### Anforderungstypen', '', '| Typ | Anzahl | Anteil |', '|---|---:|---:|']
|
||||
for t, c in typen.most_common(10):
|
||||
z.append('| %s | %d | %s |' % (t, c, pct(c, n)))
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"files": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": { "type": "string", "minLength": 1 },
|
||||
"content": { "type": "string" }
|
||||
},
|
||||
"required": ["path", "content"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"summary": { "type": "string" }
|
||||
},
|
||||
"required": ["files", "summary"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
@@ -51,10 +51,26 @@ for zeile in io.open(pfad, encoding='utf-8'):
|
||||
inhalt = c.get('content')
|
||||
if isinstance(inhalt, list):
|
||||
inhalt = ' '.join(str(x.get('text', '')) for x in inhalt if isinstance(x, dict))
|
||||
ergebnisse[c.get('tool_use_id')] = len(str(inhalt or ''))
|
||||
ergebnisse[c.get('tool_use_id')] = str(inhalt or '')
|
||||
|
||||
# Ein Aufruf, der am Nebenlaeufigkeitslimit scheitert, erscheint im Transkript wie ein
|
||||
# regulaerer Subagenten-Start, ist aber keiner: Er liefert nur die Absage zurueck und
|
||||
# zaehlt nicht in `subagent_stats.spawned`. Ohne diese Trennung meldet der Abgleich eine
|
||||
# Abweichung, die es nicht gibt (Lauf Iteration 3/.../125032_v4.4.0-fb24: 21 gefundene
|
||||
# Aufrufe gegenueber 13 erwarteten - die Differenz waren 8 Absagen).
|
||||
ABSAGE = 'Concurrent subagent limit reached'
|
||||
|
||||
for a in aufrufe:
|
||||
a['ergebnis_zeichen'] = ergebnisse.get(a['id'])
|
||||
txt = ergebnisse.get(a['id']) or ''
|
||||
a['ergebnis_zeichen'] = len(txt)
|
||||
a['abgewiesen'] = ABSAGE in txt
|
||||
# Ein im Hintergrund gestarteter Subagent liefert sofort eine Start-Quittung
|
||||
# ('Async agent launched successfully'), nicht seine Befunde. Ihre Laenge ist
|
||||
# KEIN Ertragsmass - sie ist fuer alle Hintergrund-Subagenten praktisch gleich.
|
||||
a['nur_startquittung'] = 'Async agent launched successfully' in txt
|
||||
|
||||
abgewiesen = [a for a in aufrufe if a['abgewiesen']]
|
||||
echte = [a for a in aufrufe if not a['abgewiesen']]
|
||||
|
||||
meta = os.path.join(lauf, '_meta')
|
||||
os.makedirs(meta, exist_ok=True)
|
||||
@@ -65,26 +81,45 @@ md = ['# Subagenten-Aufrufe', '',
|
||||
'Session `%s`, Transkript `%s`.' % (sid, os.path.basename(pfad)),
|
||||
'',
|
||||
'`subagent_stats`: **%s** Subagenten gesamt, davon **%s** von Subagenten gestartet '
|
||||
'(max_depth %s). Direkt vom Hauptagenten erwartet: **%s**. Im Transkript gefunden: **%s**.'
|
||||
% (gesamt, verschachtelt, stats.get('max_depth'), erwartet, len(aufrufe)), '']
|
||||
'(max_depth %s). Direkt vom Hauptagenten erwartet: **%s**. Im Transkript gefunden: '
|
||||
'**%s** echte Starts%s.'
|
||||
% (gesamt, verschachtelt, stats.get('max_depth'), erwartet, len(echte),
|
||||
' und **%d** am Nebenlaeufigkeitslimit abgewiesene Aufrufe' % len(abgewiesen)
|
||||
if abgewiesen else ''), '']
|
||||
if verschachtelt:
|
||||
md += ['> Die %s von Subagenten gestarteten Aufrufe stehen in deren eigenen Transkripten und'
|
||||
' sind hier **nicht** enthalten.' % verschachtelt, '']
|
||||
if erwartet != len(aufrufe):
|
||||
md += ['> **Abweichung** zwischen erwarteter und gefundener Anzahl.',
|
||||
if abgewiesen:
|
||||
md += ['> **%d Aufrufe wurden am Nebenlaeufigkeitslimit abgewiesen** '
|
||||
'(`subagent_stats.refused.concurrency_limit` = %s) und sind unten **nicht** '
|
||||
'aufgefuehrt. Sie erscheinen im Transkript wie regulaere Starts, liefern aber nur '
|
||||
'die Absage zurueck und zaehlen nicht in `spawned`. Fuer die Auswertung der '
|
||||
'selbstgewaehlten Zerlegung sind sie dennoch aufschlussreich: Der Hauptagent wollte '
|
||||
'staerker parallelisieren, als das Werkzeug zuliess.'
|
||||
% (len(abgewiesen), stats.get('refused', {}).get('concurrency_limit', '?')), '']
|
||||
if erwartet != len(echte):
|
||||
md += ['> **Abweichung** zwischen erwarteter (%s) und gefundener Anzahl echter Starts (%s).'
|
||||
% (erwartet, len(echte)),
|
||||
'> Ursache pruefen, bevor die Prompts ausgewertet werden.', '']
|
||||
for i, a in enumerate(aufrufe, 1):
|
||||
for i, a in enumerate(echte, 1):
|
||||
md += ['## %d. %s' % (i, a['description'] or '(ohne Beschreibung)'),
|
||||
'',
|
||||
'- **Werkzeug:** `%s` **Typ:** `%s` **Hintergrund:** %s'
|
||||
% (a['werkzeug'], a['subagent_type'], a['run_in_background']),
|
||||
'- **Prompt-Zeichen:** %d **Ergebnis-Zeichen:** %s'
|
||||
% (len(a['prompt']), a['ergebnis_zeichen']),
|
||||
'- **Prompt-Zeichen:** %d **Rueckgabe:** %s'
|
||||
% (len(a['prompt']),
|
||||
'Start-Quittung (Befunde kommen per Benachrichtigung, nicht als '
|
||||
'Werkzeugergebnis - Laenge ist kein Ertragsmass)'
|
||||
if a.get('nur_startquittung') else '%s Zeichen' % a['ergebnis_zeichen']),
|
||||
'', '### Prompt', '', '```', a['prompt'].rstrip(), '```', '']
|
||||
io.open(os.path.join(meta, 'subagenten.md'), 'w', encoding='utf-8').write('\n'.join(md))
|
||||
|
||||
print('Subagenten gefunden: %d (direkt erwartet: %s, gesamt: %s, davon verschachtelt: %s)' % (len(aufrufe), erwartet, gesamt, verschachtelt))
|
||||
for a in aufrufe:
|
||||
print(' - %-42s %s Prompt %6d Z. Ergebnis %s Z.'
|
||||
% ((a['description'] or '')[:42], a['subagent_type'], len(a['prompt']), a['ergebnis_zeichen']))
|
||||
print('Subagenten gefunden: %d echte%s (direkt erwartet: %s, gesamt: %s, davon verschachtelt: %s)'
|
||||
% (len(echte), (', %d abgewiesen' % len(abgewiesen)) if abgewiesen else '',
|
||||
erwartet, gesamt, verschachtelt))
|
||||
for a in echte:
|
||||
print(' - %-42s %-16s Prompt %6d Z. %s'
|
||||
% ((a['description'] or '')[:42], a['subagent_type'], len(a['prompt']),
|
||||
'Hintergrund (Start-Quittung)' if a.get('nur_startquittung')
|
||||
else 'Ergebnis %s Z.' % a['ergebnis_zeichen']))
|
||||
print('geschrieben:', os.path.join(meta, 'subagenten.md'))
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Verdichtet die RawResult.json unterhalb eines Verzeichnisses zu einer Tabelle.
|
||||
|
||||
Gedacht fuer die Sichtung einer Matrix: Welche Zelle hat gemessen, welche ist
|
||||
eine Fehlmessung, und woran lag es. Schreibt nichts - reine Auswertung.
|
||||
|
||||
python lauf-uebersicht.py "<Verzeichnis>" [--details]
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def schreibziele(lauf: Path) -> list[str]:
|
||||
"""Wohin der Lauf zu schreiben versuchte, samt Ergebnis.
|
||||
|
||||
Der haeufigste Grund fuer ein leeres Ergebnisverzeichnis ist ein
|
||||
Schreibversuch an den falschen Ort - der ist ohne diese Aufstellung nicht
|
||||
zu sehen, weil ``written_files`` dann schlicht leer bleibt.
|
||||
"""
|
||||
session = lauf / "_meta" / "opencode-session.json"
|
||||
if not session.is_file():
|
||||
return []
|
||||
try:
|
||||
daten = json.loads(session.read_text(encoding="utf-8"))
|
||||
except (json.JSONDecodeError, OSError):
|
||||
return []
|
||||
ziele = []
|
||||
for nachricht in daten.get("messages", []):
|
||||
for teil in nachricht.get("parts", []):
|
||||
if teil.get("type") != "tool" or teil.get("tool") not in ("write", "edit"):
|
||||
continue
|
||||
zustand = teil.get("state", {})
|
||||
ziele.append(
|
||||
f"{teil.get('tool')} {zustand.get('status')} -> "
|
||||
f"{zustand.get('input', {}).get('filePath')}"
|
||||
)
|
||||
return ziele
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Uebersicht ueber Versuchslaeufe")
|
||||
parser.add_argument("verzeichnis")
|
||||
parser.add_argument("--details", action="store_true", help="Schreibziele zeigen")
|
||||
args = parser.parse_args()
|
||||
|
||||
wurzel = Path(args.verzeichnis)
|
||||
zeilen = []
|
||||
for datei in sorted(wurzel.rglob("RawResult.json")):
|
||||
try:
|
||||
d = json.loads(datei.read_text(encoding="utf-8"))
|
||||
except (json.JSONDecodeError, OSError):
|
||||
continue
|
||||
lauf = datei.parent
|
||||
zeilen.append(
|
||||
{
|
||||
"lauf": lauf.name,
|
||||
"modus": d.get("mode", "?"),
|
||||
"exit": d.get("exit_code"),
|
||||
"status": d.get("subtype", "?"),
|
||||
"min": round(d.get("duration_ms", 0) / 60000, 1),
|
||||
"turns": d.get("num_turns", 0),
|
||||
"tools": d.get("tool_call_count", 0),
|
||||
"sub": d.get("subagent_stats", {}).get("spawned", 0),
|
||||
"dateien": len(d.get("written_files") or []),
|
||||
# Eine 0 aus einem laufenden Subagenten ist kein Messwert.
|
||||
"tokens": (
|
||||
d["usage"]["total_tokens"]
|
||||
if d.get("usage_captured", True)
|
||||
else None
|
||||
),
|
||||
"pfad": lauf,
|
||||
}
|
||||
)
|
||||
|
||||
if not zeilen:
|
||||
print("Keine RawResult.json gefunden.")
|
||||
return 1
|
||||
|
||||
kopf = f"{'Lauf':44} {'Modus':8} {'St':3} {'Min':>6} {'Turn':>5} {'Tool':>5} {'Sub':>4} {'Dat':>4} {'Tokens':>10}"
|
||||
print(kopf)
|
||||
print("-" * len(kopf))
|
||||
for z in zeilen:
|
||||
tok = "n. erf." if z["tokens"] is None else f"{z['tokens']:,}"
|
||||
print(
|
||||
f"{z['lauf'][:44]:44} {z['modus']:8} {str(z['exit']):3} {z['min']:6.1f} "
|
||||
f"{z['turns']:5} {z['tools']:5} {z['sub']:4} {z['dateien']:4} {tok:>10}"
|
||||
)
|
||||
if args.details:
|
||||
for ziel in schreibziele(z["pfad"]):
|
||||
print(f" {ziel}")
|
||||
|
||||
gueltig = [z for z in zeilen if z["dateien"] > 0]
|
||||
print(
|
||||
f"\n{len(zeilen)} Laeufe, davon {len(gueltig)} mit Ergebnisdateien, "
|
||||
f"{len(zeilen) - len(gueltig)} Fehlmessungen."
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,165 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Materialize Codex result files and normalize its JSONL measurements."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from collections import Counter
|
||||
from datetime import datetime
|
||||
from pathlib import Path, PurePosixPath
|
||||
from typing import Any
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("run_directory", type=Path)
|
||||
parser.add_argument("--model", required=True)
|
||||
parser.add_argument("--effort", required=True)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def read_jsonl(path: Path) -> tuple[list[dict[str, Any]], list[str]]:
|
||||
events: list[dict[str, Any]] = []
|
||||
malformed: list[str] = []
|
||||
for number, line in enumerate(path.read_text(encoding="utf-8-sig").splitlines(), 1):
|
||||
if not line.strip():
|
||||
continue
|
||||
try:
|
||||
value = json.loads(line)
|
||||
except json.JSONDecodeError as exc:
|
||||
malformed.append(f"line {number}: {exc}")
|
||||
continue
|
||||
if isinstance(value, dict):
|
||||
events.append(value)
|
||||
else:
|
||||
malformed.append(f"line {number}: JSON value is not an object")
|
||||
return events, malformed
|
||||
|
||||
|
||||
def safe_result_path(results_dir: Path, raw_path: str) -> Path:
|
||||
relative = PurePosixPath(raw_path.replace("\\", "/"))
|
||||
if relative.is_absolute() or not relative.parts or ".." in relative.parts:
|
||||
raise ValueError(f"unsafe result path: {raw_path!r}")
|
||||
if any(part in ("", ".") or ":" in part for part in relative.parts):
|
||||
raise ValueError(f"invalid result path: {raw_path!r}")
|
||||
target = results_dir.joinpath(*relative.parts).resolve()
|
||||
root = results_dir.resolve()
|
||||
if root != target and root not in target.parents:
|
||||
raise ValueError(f"result path escapes Ergebnisse: {raw_path!r}")
|
||||
return target
|
||||
|
||||
|
||||
def parse_iso(path: Path) -> datetime | None:
|
||||
if not path.exists():
|
||||
return None
|
||||
value = path.read_text(encoding="utf-8-sig").strip()
|
||||
try:
|
||||
return datetime.fromisoformat(value.replace("Z", "+00:00"))
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
run_dir = args.run_directory.resolve()
|
||||
meta_dir = run_dir / "_meta"
|
||||
results_dir = run_dir / "Ergebnisse"
|
||||
events, malformed = read_jsonl(run_dir / "RawEvents.jsonl")
|
||||
envelope = json.loads((meta_dir / "final_response.json").read_text(encoding="utf-8-sig"))
|
||||
if not isinstance(envelope, dict) or not isinstance(envelope.get("files"), list):
|
||||
raise ValueError("final_response.json does not match the Codex output envelope")
|
||||
|
||||
results_dir.mkdir(parents=True, exist_ok=True)
|
||||
seen: set[str] = set()
|
||||
materialized: list[str] = []
|
||||
for entry in envelope["files"]:
|
||||
if not isinstance(entry, dict):
|
||||
raise ValueError("file entry is not an object")
|
||||
raw_path = entry.get("path")
|
||||
content = entry.get("content")
|
||||
if not isinstance(raw_path, str) or not isinstance(content, str):
|
||||
raise ValueError("file entry requires string path and content")
|
||||
target = safe_result_path(results_dir, raw_path)
|
||||
key = str(target).casefold()
|
||||
if key in seen:
|
||||
raise ValueError(f"duplicate result path: {raw_path!r}")
|
||||
seen.add(key)
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
target.write_text(content, encoding="utf-8", newline="\n")
|
||||
materialized.append(str(target.relative_to(results_dir)).replace("\\", "/"))
|
||||
|
||||
usage = Counter()
|
||||
item_types = Counter()
|
||||
errors: list[Any] = list(malformed)
|
||||
thread_id = None
|
||||
turns = 0
|
||||
for event in events:
|
||||
event_type = event.get("type")
|
||||
if event_type == "thread.started":
|
||||
thread_id = event.get("thread_id")
|
||||
elif event_type == "turn.completed":
|
||||
turns += 1
|
||||
turn_usage = event.get("usage") or {}
|
||||
for field in ("input_tokens", "cached_input_tokens", "output_tokens", "reasoning_output_tokens"):
|
||||
value = turn_usage.get(field, 0)
|
||||
if isinstance(value, int):
|
||||
usage[field] += value
|
||||
elif event_type in ("turn.failed", "error"):
|
||||
errors.append(event)
|
||||
if event_type == "item.completed":
|
||||
item = event.get("item") or {}
|
||||
if isinstance(item.get("type"), str):
|
||||
item_types[item["type"]] += 1
|
||||
|
||||
start = parse_iso(meta_dir / "startzeit.txt")
|
||||
end = parse_iso(meta_dir / "endzeit.txt")
|
||||
duration_ms = round((end - start).total_seconds() * 1000) if start and end else None
|
||||
|
||||
exit_code = None
|
||||
exit_code_path = meta_dir / "exitcode.txt"
|
||||
if exit_code_path.exists():
|
||||
try:
|
||||
exit_code = int(exit_code_path.read_text(encoding="utf-8-sig").strip())
|
||||
except ValueError:
|
||||
errors.append("invalid exitcode.txt")
|
||||
if exit_code not in (None, 0):
|
||||
errors.append({"exit_code": exit_code})
|
||||
|
||||
normalized = {
|
||||
"adapter": "codex-cli",
|
||||
"is_error": bool(errors),
|
||||
"subtype": "success" if not errors else "error",
|
||||
"session_id": thread_id,
|
||||
"duration_ms": duration_ms,
|
||||
"duration_api_ms": None,
|
||||
"num_turns": turns,
|
||||
"requested_model": args.model,
|
||||
"actual_models": None,
|
||||
"model_control": "not_verifiable_from_codex_exec_jsonl",
|
||||
"effort": args.effort,
|
||||
"usage": {
|
||||
"input_tokens": usage["input_tokens"],
|
||||
"cached_input_tokens": usage["cached_input_tokens"],
|
||||
"output_tokens": usage["output_tokens"],
|
||||
"reasoning_output_tokens": usage["reasoning_output_tokens"],
|
||||
"total_tokens": usage["input_tokens"] + usage["output_tokens"],
|
||||
"semantics": "cached_input_tokens is a subset of input_tokens and is not added again",
|
||||
},
|
||||
"item_counts": dict(sorted(item_types.items())),
|
||||
"permission_denials": None,
|
||||
"subagent_stats": {"spawned": 0, "source": "multi-agent disabled by configuration"},
|
||||
"materialized_files": materialized,
|
||||
"result": envelope.get("summary", ""),
|
||||
"errors": errors,
|
||||
}
|
||||
(run_dir / "RawResult.json").write_text(
|
||||
json.dumps(normalized, ensure_ascii=False, indent=2) + "\n",
|
||||
encoding="utf-8",
|
||||
newline="\n",
|
||||
)
|
||||
return 1 if errors else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"provider": {
|
||||
"lmstudio": {
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"name": "LM Studio (lokal)",
|
||||
"options": {
|
||||
"baseURL": "http://localhost:1234/v1",
|
||||
"apiKey": "lm-studio"
|
||||
},
|
||||
"models": {
|
||||
"google/gemma-4-e4b": {
|
||||
"name": "Gemma 4 E4B (lokal)",
|
||||
"limit": {
|
||||
"context": 131072,
|
||||
"output": 32768
|
||||
}
|
||||
},
|
||||
"qwen/qwen3.5-9b": {
|
||||
"name": "Qwen 3.5 9B (lokal)",
|
||||
"limit": {
|
||||
"context": 262144,
|
||||
"output": 32768
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"provider": {
|
||||
"tensorx": {
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"name": "TensorX",
|
||||
"options": {
|
||||
"baseURL": "https://api.tensorx.ai/v1"
|
||||
},
|
||||
"models": {
|
||||
"qwen/qwen3.8-flash-next": {
|
||||
"name": "Qwen 3.8 Flash Next",
|
||||
"limit": {
|
||||
"context": 262144,
|
||||
"output": 131072
|
||||
},
|
||||
"variants": {
|
||||
"low": {
|
||||
"thinking": {
|
||||
"type": "enabled",
|
||||
"level": "low"
|
||||
}
|
||||
},
|
||||
"medium": {
|
||||
"thinking": {
|
||||
"type": "enabled",
|
||||
"level": "medium"
|
||||
}
|
||||
},
|
||||
"high": {
|
||||
"thinking": {
|
||||
"type": "enabled",
|
||||
"level": "high"
|
||||
}
|
||||
},
|
||||
"xhigh": {
|
||||
"thinking": {
|
||||
"type": "enabled",
|
||||
"level": "xhigh"
|
||||
}
|
||||
},
|
||||
"max": {
|
||||
"thinking": {
|
||||
"type": "enabled",
|
||||
"level": "xhigh"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"z-ai/glm-5.2": {
|
||||
"name": "GLM 5.2",
|
||||
"limit": {
|
||||
"context": 1048576,
|
||||
"output": 131072
|
||||
},
|
||||
"variants": {
|
||||
"low": { "thinking": { "type": "enabled", "level": "low" } },
|
||||
"medium": { "thinking": { "type": "enabled", "level": "medium" } },
|
||||
"high": { "thinking": { "type": "enabled", "level": "high" } },
|
||||
"xhigh": { "thinking": { "type": "enabled", "level": "xhigh" } },
|
||||
"max": { "thinking": { "type": "enabled", "level": "xhigh" } }
|
||||
}
|
||||
},
|
||||
"z-ai/glm-5.3-flash": {
|
||||
"name": "GLM 5.3 Flash",
|
||||
"variants": {
|
||||
"low": { "thinking": { "type": "enabled", "level": "low" } },
|
||||
"medium": { "thinking": { "type": "enabled", "level": "medium" } },
|
||||
"high": { "thinking": { "type": "enabled", "level": "high" } },
|
||||
"xhigh": { "thinking": { "type": "enabled", "level": "xhigh" } },
|
||||
"max": { "thinking": { "type": "enabled", "level": "xhigh" } }
|
||||
}
|
||||
},
|
||||
"moonshotai/kimi-k3": {
|
||||
"name": "Kimi K3",
|
||||
"limit": {
|
||||
"context": 1048576,
|
||||
"output": 131072
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Create and verify an isolated Codex analysis workspace."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def fail(message: str) -> None:
|
||||
raise SystemExit(message)
|
||||
|
||||
|
||||
def resolved(path: str) -> Path:
|
||||
return Path(path).resolve()
|
||||
|
||||
|
||||
def native_path(path: Path) -> str:
|
||||
value = str(path)
|
||||
if os.name == "nt" and not value.startswith("\\\\?\\"):
|
||||
return "\\\\?\\" + value
|
||||
return value
|
||||
|
||||
|
||||
def allowed_workspace(workspace: Path, meta: Path) -> bool:
|
||||
temporary_root = Path(tempfile.gettempdir()).resolve()
|
||||
return (
|
||||
(workspace.parent == meta and workspace.name == "workspace")
|
||||
or (workspace.parent == temporary_root and workspace.name.startswith("codex-experiment-"))
|
||||
)
|
||||
|
||||
|
||||
def git_output(cwd: Path, *args: str) -> bytes:
|
||||
completed = subprocess.run(
|
||||
["git", "-C", str(cwd), *args],
|
||||
check=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
return completed.stdout
|
||||
|
||||
|
||||
def source_files(source: Path) -> tuple[list[Path], str | None]:
|
||||
try:
|
||||
top = resolved(git_output(source, "rev-parse", "--show-toplevel").decode().strip())
|
||||
relative_source = source.relative_to(top)
|
||||
raw = git_output(
|
||||
top,
|
||||
"ls-files",
|
||||
"-z",
|
||||
"--cached",
|
||||
"--others",
|
||||
"--exclude-standard",
|
||||
"--",
|
||||
relative_source.as_posix(),
|
||||
)
|
||||
paths = []
|
||||
for entry in raw.split(b"\0"):
|
||||
if not entry:
|
||||
continue
|
||||
candidate = resolved(top / os.fsdecode(entry))
|
||||
if candidate.is_file() and candidate.is_relative_to(source):
|
||||
paths.append(candidate)
|
||||
return sorted(set(paths), key=lambda item: item.as_posix().casefold()), str(top)
|
||||
except (subprocess.CalledProcessError, ValueError):
|
||||
paths = [item for item in source.rglob("*") if item.is_file() and ".git" not in item.parts]
|
||||
return sorted(paths, key=lambda item: item.as_posix().casefold()), None
|
||||
|
||||
|
||||
def sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with open(native_path(path), "rb") as handle:
|
||||
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest().upper()
|
||||
|
||||
|
||||
def manifest(workspace: Path) -> dict[str, dict[str, int | str]]:
|
||||
result: dict[str, dict[str, int | str]] = {}
|
||||
workspace_native = native_path(workspace)
|
||||
files: list[tuple[str, Path]] = []
|
||||
for directory, _, names in os.walk(workspace_native):
|
||||
for name in names:
|
||||
full_path = Path(directory) / name
|
||||
relative = os.path.relpath(str(full_path), workspace_native).replace("\\", "/")
|
||||
files.append((relative, full_path))
|
||||
for relative, path in sorted(files, key=lambda item: item[0].casefold()):
|
||||
result[relative] = {"size": os.stat(str(path)).st_size, "sha256": sha256(path)}
|
||||
return result
|
||||
|
||||
|
||||
def write_json(path: Path, value: object) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(value, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def create(source_arg: str, workspace_arg: str, meta_arg: str) -> None:
|
||||
source = resolved(source_arg)
|
||||
workspace = resolved(workspace_arg)
|
||||
meta = resolved(meta_arg)
|
||||
if not source.is_dir():
|
||||
fail(f"Source directory does not exist: {source}")
|
||||
if not allowed_workspace(workspace, meta):
|
||||
fail("Workspace must be _meta/workspace or a codex-experiment-* directory in system temp")
|
||||
if workspace.exists():
|
||||
before_manifest = meta / "workspace-manifest-before.json"
|
||||
if workspace.parent == meta and workspace.name == "workspace" and not before_manifest.exists():
|
||||
shutil.rmtree(native_path(workspace))
|
||||
else:
|
||||
fail(f"Workspace already exists: {workspace}")
|
||||
|
||||
files, git_root = source_files(source)
|
||||
workspace.mkdir(parents=True)
|
||||
for path in files:
|
||||
relative = path.relative_to(source)
|
||||
target = workspace / relative
|
||||
os.makedirs(native_path(target.parent), exist_ok=True)
|
||||
try:
|
||||
shutil.copy2(native_path(path), native_path(target), follow_symlinks=False)
|
||||
except OSError as error:
|
||||
fail(f"Failed to copy {path} -> {target}: {error}")
|
||||
|
||||
before = manifest(workspace)
|
||||
write_json(meta / "workspace-manifest-before.json", before)
|
||||
write_json(
|
||||
meta / "workspace-source.json",
|
||||
{
|
||||
"source": str(source),
|
||||
"workspace": str(workspace),
|
||||
"git_root": git_root,
|
||||
"file_count": len(before),
|
||||
"total_bytes": sum(int(item["size"]) for item in before.values()),
|
||||
},
|
||||
)
|
||||
print(f"Created isolated workspace with {len(before)} files: {workspace}")
|
||||
|
||||
|
||||
def verify(workspace_arg: str, meta_arg: str) -> None:
|
||||
workspace = resolved(workspace_arg)
|
||||
meta = resolved(meta_arg)
|
||||
before_path = meta / "workspace-manifest-before.json"
|
||||
if not workspace.is_dir() or not before_path.is_file():
|
||||
fail("Workspace or before-manifest is missing")
|
||||
|
||||
before = json.loads(before_path.read_text(encoding="utf-8"))
|
||||
after = manifest(workspace)
|
||||
added = sorted(set(after) - set(before), key=str.casefold)
|
||||
removed = sorted(set(before) - set(after), key=str.casefold)
|
||||
modified = sorted(
|
||||
(path for path in set(before) & set(after) if before[path] != after[path]),
|
||||
key=str.casefold,
|
||||
)
|
||||
write_json(meta / "workspace-manifest-after.json", after)
|
||||
result = {
|
||||
"unchanged": not (added or removed or modified),
|
||||
"added": added,
|
||||
"removed": removed,
|
||||
"modified": modified,
|
||||
"file_count_before": len(before),
|
||||
"file_count_after": len(after),
|
||||
}
|
||||
write_json(meta / "workspace-integrity.json", result)
|
||||
print(json.dumps(result, ensure_ascii=False))
|
||||
if not result["unchanged"]:
|
||||
raise SystemExit(3)
|
||||
|
||||
|
||||
def cleanup(workspace_arg: str, meta_arg: str) -> None:
|
||||
workspace = resolved(workspace_arg)
|
||||
meta = resolved(meta_arg)
|
||||
if not allowed_workspace(workspace, meta):
|
||||
fail("Refusing to remove a directory outside the approved experiment workspace locations")
|
||||
if workspace.exists():
|
||||
shutil.rmtree(native_path(workspace))
|
||||
print(f"Removed temporary workspace: {workspace}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
create_parser = subparsers.add_parser("create")
|
||||
create_parser.add_argument("source")
|
||||
create_parser.add_argument("workspace")
|
||||
create_parser.add_argument("meta")
|
||||
verify_parser = subparsers.add_parser("verify")
|
||||
verify_parser.add_argument("workspace")
|
||||
verify_parser.add_argument("meta")
|
||||
cleanup_parser = subparsers.add_parser("cleanup")
|
||||
cleanup_parser.add_argument("workspace")
|
||||
cleanup_parser.add_argument("meta")
|
||||
args = parser.parse_args()
|
||||
if args.command == "create":
|
||||
create(args.source, args.workspace, args.meta)
|
||||
elif args.command == "verify":
|
||||
verify(args.workspace, args.meta)
|
||||
else:
|
||||
cleanup(args.workspace, args.meta)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,172 @@
|
||||
#!/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())
|
||||
@@ -0,0 +1,305 @@
|
||||
# OpenCode-Adapter
|
||||
|
||||
Ein Wrapper, zwei Provider. `opencode-adapter.py` startet OpenCode headless und normalisiert den
|
||||
Lauf nach `RawResult.json`. Welcher Provider gilt, entscheidet `--provider`:
|
||||
|
||||
| `--provider` | Modell-IDs | Betrieb | Vorlage |
|
||||
|---|---|---|---|
|
||||
| `tensorx` (Standard) | `z-ai/*`, `qwen/qwen3.8-flash-next`, `moonshotai/*` | Remote-Gateway `https://api.tensorx.ai/v1` | `opencode-tensorx.json` |
|
||||
| `lmstudio` | `google/gemma-4-e4b`, `qwen/qwen3.8-27b` | lokaler LM-Studio-Server `http://localhost:1234/v1` | `opencode-lmstudio.json` |
|
||||
|
||||
`qwen/qwen3.8-flash-next` (TensorX) und `qwen/qwen3.8-27b` (lokal) teilen sich das Präfix `qwen/`.
|
||||
Das Präfix allein bestimmt den Adapter deshalb **nicht** – maßgeblich ist die vollständige
|
||||
Modell-ID gemäß dieser Tabelle.
|
||||
|
||||
Der Abschnitt **LM Studio** unten beschreibt alles, was nur für den lokalen Betrieb gilt.
|
||||
Alle übrigen Abschnitte gelten für beide Provider.
|
||||
|
||||
## Voraussetzungen und Authentifizierung
|
||||
|
||||
1. OpenCode installieren und die Version protokollieren:
|
||||
|
||||
```powershell
|
||||
npm install -g opencode-ai
|
||||
opencode --version
|
||||
```
|
||||
|
||||
2. Nur für `--provider tensorx`: TensorX einmal im OpenCode-Credential-Store anmelden
|
||||
(`--provider lmstudio` braucht keine Anmeldung – der lokale Server prüft keinen Key):
|
||||
|
||||
```powershell
|
||||
opencode auth login
|
||||
opencode auth list
|
||||
```
|
||||
|
||||
Die statische Datei `opencode-tensorx.json` enthält Provider, Basis-URL, Modelle und
|
||||
Effort-Varianten, aber keinen API-Key. Der Adapter liest weder Cline-Dateien noch einen
|
||||
Cline-Credential-Store. OpenCode löst das Credential selbst auf. Keys niemals in
|
||||
Laufartefakte, Prompts oder die Konfigurationsvorlage schreiben.
|
||||
|
||||
## Modell- und Effort-Mapping
|
||||
|
||||
Der Adapter ergänzt intern das Providerpräfix (`tensorx/` bzw. `lmstudio/`). Aus
|
||||
`qwen/qwen3.8-flash-next` wird daher für OpenCode
|
||||
`tensorx/qwen/qwen3.8-flash-next`; im Messprotokoll bleibt die ursprüngliche TensorX-ID.
|
||||
|
||||
Die Stufen `low`, `medium`, `high`, `xhigh` und `max` werden als OpenCode-Varianten aus der
|
||||
Providervorlage übergeben. Für Qwen und GLM über TensorX enthält die Variante
|
||||
`thinking: {type: enabled, level: ...}`; `max` wird auf `xhigh` abgebildet, falls der Provider
|
||||
keine eigene `max`-Stufe kennt. Nur in der Vorlage vorhandene Varianten werden per
|
||||
`--variant` gesetzt.
|
||||
|
||||
**Bei `lmstudio` ist Effort nicht steuerbar.** Der OpenAI-kompatible Endpunkt von LM Studio
|
||||
nimmt keinen Thinking-Level entgegen; `opencode-lmstudio.json` deklariert deshalb bewusst keine
|
||||
Varianten. Der Adapter protokolliert das im `Adapter.log` und setzt `effort_applied: false` in
|
||||
`RawResult.json`. Der übergebene `--effort`-Wert bleibt als angeforderte Bedingung erhalten, ist
|
||||
im Protokoll aber unter „Sampling-Parameter" als **nicht steuerbar** auszuweisen – niemals so
|
||||
darzustellen, als hätte er gewirkt. Reasoning-Tokens liefern die Modelle trotzdem, sofern sie
|
||||
von sich aus mit `reasoning_content` antworten.
|
||||
|
||||
## Isolierte Laufkonfiguration
|
||||
|
||||
Für jeden Lauf schreibt der Adapter `_meta/opencode-config.json` und setzt nur für den
|
||||
Kindprozess `OPENCODE_CONFIG` auf diese Datei. Der Aufruf verwendet `opencode run --pure`,
|
||||
damit keine interaktive Oberfläche benötigt wird. Der Prompt wird über stdin übergeben.
|
||||
|
||||
Die Berechtigungen beginnen mit `deny` und erlauben gezielt:
|
||||
|
||||
- Lesen, Suchen, Auflisten sowie eine Read-only-Shell-Allowlist;
|
||||
- Schreiben und externe Verzeichnisse ausschließlich für das angegebene Ergebnisverzeichnis;
|
||||
- im Modus `solo` keine Tasks;
|
||||
- im Modus `builtin` nur OpenCodes `general`- und `explore`-Subagenten;
|
||||
- im Modus `custom` nur Rollen aus der mit `--agents` übergebenen JSON-Datei.
|
||||
|
||||
Für das Ergebnisziel erzeugt der Wrapper kanonische sowie zum aktiven OpenCode-Root und zur
|
||||
Git-Worktree-Wurzel relative Allow-Patterns. Das ist unter Windows notwendig, wenn Root und
|
||||
Laufverzeichnis im selben Git-Worktree liegen, das Laufverzeichnis aber außerhalb des
|
||||
Root-Unterordners liegt.
|
||||
|
||||
Die Shell-Rechte sind seit Skill 12.0.0 eine **Denylist**, spiegelbildlich zum
|
||||
Claude-Code-Adapter: Erlaubt ist alles, gesperrt sind ausdrücklich die schreibenden und
|
||||
bauenden Kommandos – `rm`, `rmdir`, `mv`, `cp`, `dd`, `truncate`, `chmod`, `chown`, `ln`,
|
||||
`tee`, `sed -i`, die schreibenden `git`-Kommandos einschließlich `fetch`, `pull` und `remote`,
|
||||
`dotnet`, `msbuild`, `npm install`, `nuget` sowie `Remove-Item`, `Move-Item`, `Copy-Item`,
|
||||
`New-Item`, `Set-Content`, `Add-Content`, `Clear-Content`, `Out-File`, `Set-ItemProperty` und
|
||||
`Rename-Item`.
|
||||
|
||||
**Die Reihenfolge ist bedeutsam.** OpenCode wertet die Regeln der Reihe nach aus; die *zuletzt
|
||||
passende* Regel gewinnt – nicht die spezifischste, und `deny` gewinnt nicht automatisch. Das
|
||||
Catch-all `"*": "allow"` muss deshalb **zuerst** stehen, die Sperren danach. Python-Dicts
|
||||
erhalten ihre Einfügereihenfolge, `json.dump` schreibt sie unverändert.
|
||||
|
||||
Die frühere Allowlist konnte Kommandos nur als Präfix treffen und scheiterte deshalb an
|
||||
Pipelines: `Get-ChildItem ... | Format-Table ...` blieb gesperrt, obwohl `Get-ChildItem`
|
||||
erlaubt war. Zugleich war die Werkzeugfreiheit nicht mit der des Claude-Adapters vergleichbar.
|
||||
|
||||
Wie beim Claude-Adapter gilt: Mustervergleich auf Kommandozeilen ist **nicht lückenlos**. Die
|
||||
belastbare Read-only-Garantie bleibt der Vorher/Nachher-Vergleich per `git status`; die
|
||||
Denylist senkt das Risiko, sie ersetzt die Verifikation nicht.
|
||||
|
||||
Webzugriff, Skills, Rückfragen und Weiterdelegation durch Subagenten bleiben gesperrt. Bei
|
||||
`custom` übersetzt der Adapter `description` und `prompt` jeder Rolle in eine explizite
|
||||
OpenCode-Subagentenkonfiguration mit demselben Modell wie der Hauptagent.
|
||||
|
||||
## LM Studio (nur `--provider lmstudio`)
|
||||
|
||||
Lokale Läufe sind eine eigene Versuchsbedingung: kein Netzzugriff, keine Providerkosten,
|
||||
gewichtsbezogene Reproduzierbarkeitsangaben (Quantisierung, Runtime, Kontextfenster) und ein
|
||||
Kontextfenster, das der Server beim Laden festlegt.
|
||||
|
||||
### Vorbereitung
|
||||
|
||||
```powershell
|
||||
lms server start # OpenAI-kompatibler Endpunkt auf Port 1234
|
||||
lms ls # heruntergeladene Modelle
|
||||
lms get qwen/qwen3.8-27b # fehlendes Modell holen (mehrere GB)
|
||||
lms ps # geladene Instanzen samt Kontextfenster
|
||||
```
|
||||
|
||||
### Preflight des Adapters
|
||||
|
||||
Vor dem Start von OpenCode prüft der Adapter über `GET /api/v0/models` und bricht mit
|
||||
Exitcode `2` und einer Handlungsanweisung ab, wenn eine Bedingung verletzt ist:
|
||||
|
||||
| Prüfung | Abbruchgrund |
|
||||
|---|---|
|
||||
| Server erreichbar | `lms server start` fehlt |
|
||||
| Modell vorhanden | nicht heruntergeladen → `lms get <ID>` |
|
||||
| `capabilities` enthält `tool_use` | ohne Tool-Calling ist kein Analyselauf möglich |
|
||||
| `max_context_length` ≥ `--min-context` | Modell kann die Bedingung nicht erfüllen |
|
||||
| Zustand `loaded` und `loaded_context_length` ≥ `--min-context` | zu kleines Fenster schneidet die Codebasis **stillschweigend** ab |
|
||||
| genau **eine** geladene Instanz | mehrere Instanzen (`modell`, `modell:2`) beantworten dieselbe `model`-Angabe; das Routing wäre nicht reproduzierbar |
|
||||
|
||||
`--min-context` ist standardmäßig `32768`. Ein bewusst kleinerer Wert ist zulässig, gehört aber
|
||||
als abweichende Versuchsbedingung ins Protokoll.
|
||||
|
||||
`--lmstudio-autoload` stellt den Sollzustand selbst her: Es entlädt **alle** Instanzen des
|
||||
Modells und lädt genau eine mit `--min-context` neu. Ohne das Flag meldet der Preflight nur den
|
||||
exakten `lms`-Befehl. Die Standardgröße von LM Studio (häufig 8192) reicht für eine
|
||||
Codebasisanalyse nicht.
|
||||
|
||||
Das geladene Fenster wird zusätzlich als `limit.context` in die Laufkonfiguration geschrieben,
|
||||
damit OpenCode nicht mehr Kontext sendet, als der Server vorhält.
|
||||
|
||||
### Speicherhygiene und Ladeparameter
|
||||
|
||||
Der lokale Durchsatz haengt fast vollstaendig davon ab, ob Gewichte und KV-Cache in den VRAM
|
||||
passen. Gemessen am 01.09.2026 auf einer RTX 5080 Laptop GPU (16.303 MiB):
|
||||
|
||||
| Konfiguration | VRAM belegt | Generierung |
|
||||
|---|---:|---:|
|
||||
| `gemma-4-e4b`, 32k, parallel 1 | 5.162 MiB | 48,3 tok/s |
|
||||
| `gemma-4-e4b`, 32k, parallel 4 | 5.222 MiB | 47,7 tok/s |
|
||||
| `gemma-4-e4b`, **131k**, parallel 4 | 6.854 MiB | 46,8 tok/s |
|
||||
| `qwen3.8-27b`, 32k, `--gpu max` | 15.696 MiB (308 frei) | Abbruch nach 10 min |
|
||||
| beide Modelle gleichzeitig geladen | 15.836 MiB (168 frei) | 0,028 Mio. Tokens/h |
|
||||
|
||||
Daraus die drei Regeln, die der Preflight seit Adapter 2.2.0 durchsetzt:
|
||||
|
||||
1. **Genau ein Modell ist geladen.** `--lmstudio-autoload` entlaedt `--all` und laedt nur das
|
||||
angeforderte Modell; ein fremdes geladenes Modell fuehrt sonst zum Abbruch. Ein nebenher
|
||||
geladenes Modell belegt VRAM, das dem Lauf fehlt, und veraendert dessen Durchsatz um
|
||||
Groessenordnungen.
|
||||
2. **Der Kontext wird auf das Modellmaximum geladen** (`--lmstudio-context max`, Standard).
|
||||
Freier VRAM ist in Kontext besser investiert als ungenutzt: vierfaches Fenster fuer 1,7 GB
|
||||
und 1,5 tok/s. `--min-context` bleibt die Gueltigkeitsschwelle, nicht die Ladegroesse.
|
||||
3. **`--lmstudio-gpu max`** erzwingt die vollstaendige Auslagerung, **`--lmstudio-parallel 4`**
|
||||
ist messbar kostenneutral und hilft, wenn Subagenten nebenlaeufig anfragen.
|
||||
|
||||
**Modelle jenseits der VRAM-Grenze sind nicht messbar.** `qwen3.8-27b` belegt mit 17,74 GB
|
||||
Gewichten mehr, als die Karte hat; der Rest laeuft auf der CPU. Auch eine kleinere
|
||||
Quantisierung loest das nicht, weil der KV-Cache eines 27B-Modells bei brauchbarem Kontext
|
||||
mehrere GB zusaetzlich fordert. Fuer eine 16-GB-Karte ist die 9B-Klasse die groesste, die mit
|
||||
vollem Fenster hineinpasst - dann aber in hoher Quantisierung, um den Speicher zu nutzen.
|
||||
|
||||
### Zusätzliche Laufartefakte und Messfelder
|
||||
|
||||
| Datei | Inhalt |
|
||||
|---|---|
|
||||
| `_meta/lmstudio-modelle.json` | Rohantwort von `/api/v0/models` zum Zeitpunkt des Preflights |
|
||||
|
||||
`RawResult.json` enthält bei lokalen Läufen zusätzlich:
|
||||
|
||||
| Messgröße | Feld |
|
||||
|---|---|
|
||||
| Kontextfenster des Laufs | `context_window` (= `local_runtime.loaded_context_length`) |
|
||||
| Quantisierungsstufe | `local_runtime.quantization` |
|
||||
| Runtime und Architektur | `local_runtime.compatibility_type`, `local_runtime.arch` |
|
||||
| Runtime-Version | `local_runtime.lms_version` |
|
||||
| Instanzbezeichner | `local_runtime.instance_id` |
|
||||
| Endpunkt | `local_runtime.base_url` |
|
||||
| Kosten | `cost` ist `0`; `cost_source` weist „nicht erfasst (lokaler Betrieb)" aus |
|
||||
| Effortwirkung | `effort_applied` ist `false` |
|
||||
|
||||
Damit sind die von Kapitel 4.3 geforderten Angaben für lokalen Betrieb – Runtime samt Version
|
||||
und Quantisierungsstufe – vollständig erfasst.
|
||||
|
||||
### Laufzeitverhalten
|
||||
|
||||
Kleine lokale Modelle beenden eine Aufgabe nicht zuverlässig von selbst; im Smoke-Test lief
|
||||
`google/gemma-4-e4b` über 50 Schritte weiter, ohne die geforderte Datei zu schreiben. Der
|
||||
Stall-Timeout greift dabei **nicht**, weil laufend Text erzeugt wird. Für lokale Läufe deshalb
|
||||
immer ein absolutes `--max-runtime` setzen und einen Abbruch als solchen protokollieren, statt
|
||||
ihn als Ergebnis zu werten.
|
||||
|
||||
## Aufruf
|
||||
|
||||
```powershell
|
||||
$skillDir = "<Verzeichnis des Skills>"
|
||||
$lauf = "<absoluter Pfad zum Laufverzeichnis>"
|
||||
$root = "<Root-Verzeichnis der Codebasis>"
|
||||
$provider = "<tensorx|lmstudio>"
|
||||
$modell = "<Modell-ID>"
|
||||
$effort = "<low|medium|high|xhigh|max>"
|
||||
$modus = "<solo|builtin|custom>"
|
||||
$agents = "<Agenten-JSON; nur bei custom>"
|
||||
|
||||
python "$skillDir\opencode-adapter.py" `
|
||||
--prompt "$lauf\_meta\combined_prompt.md" `
|
||||
--root $root `
|
||||
--output "$lauf\Ergebnisse" `
|
||||
--provider $provider `
|
||||
--model $modell `
|
||||
--effort $effort `
|
||||
--mode $modus `
|
||||
--agents $agents `
|
||||
--stall-timeout 600 `
|
||||
--max-runtime 0 `
|
||||
--result-dir $lauf `
|
||||
--title "run-experiment $modell $modus $effort"
|
||||
```
|
||||
|
||||
Für `--provider lmstudio` kommen hinzu:
|
||||
|
||||
```powershell
|
||||
--min-context 32768 ` # Mindestgröße des geladenen Kontextfensters
|
||||
--lmstudio-autoload ` # Modell notfalls selbst neu laden
|
||||
--max-runtime 3600 # absolutes Limit; lokale Modelle terminieren nicht zuverlässig
|
||||
```
|
||||
|
||||
`--base-url` (Standard `http://localhost:1234`) und `--lms` (Pfad zur CLI) sind nur nötig, wenn
|
||||
Port oder Installationsort abweichen.
|
||||
|
||||
`--agents` bei `solo` und `builtin` weglassen. `--stall-timeout 600` beendet den gesamten
|
||||
OpenCode-Prozessbaum, wenn zehn Minuten lang weder stdout noch stderr Aktivität zeigen.
|
||||
`--stall-timeout 0` deaktiviert diese Sicherung.
|
||||
|
||||
**In den Modi `builtin` und `custom` ist `--stall-timeout 0` zwingend.** OpenCode sendet
|
||||
**keine Ereignisse, solange ein Subagent arbeitet** – der Ereignisstrom schweigt für dessen
|
||||
gesamte Laufzeit. Jeder positive Stall-Timeout bricht den Lauf deshalb ab, sobald der erste
|
||||
Subagent startet, und erzeugt eine Fehlmessung, die wie ein Hänger aussieht. Der Adapter lehnt
|
||||
diese Kombination seit Version 1.3.0 mit einer Fehlermeldung ab, statt sie stillschweigend zu
|
||||
korrigieren. Die Laufzeit wird in diesen Modi ausschließlich über `--max-runtime` begrenzt. `--max-runtime 0` setzt kein absolutes
|
||||
Laufzeitlimit. `Ctrl+C` beendet ebenfalls den Prozessbaum und persistiert soweit möglich das
|
||||
Teilergebnis. Ein leeres `Ergebnisse`-Verzeichnis macht den Lauf standardmäßig zu einem Fehler.
|
||||
Nur ein bewusst textueller Smoke-Test darf diese Prüfung mit `--allow-empty-output` abschalten.
|
||||
|
||||
## Laufartefakte und Messfelder
|
||||
|
||||
| Datei | Inhalt |
|
||||
|---|---|
|
||||
| `OpenCodeEvents.jsonl` | unveränderter, inkrementell geschriebener JSON-Ereignisstrom |
|
||||
| `OpenCode.log` | OpenCode-stderr, inkrementell geschrieben |
|
||||
| `Adapter.log` | Start, Lebenszyklus, Abbruchgrund und Abschluss des Wrappers |
|
||||
| `_meta/opencode-config.json` | tatsächlich verwendete, keyfreie Laufkonfiguration |
|
||||
| `_meta/opencode-session.json` | exportierte Session, soweit eine Session-ID vorliegt |
|
||||
| `RawResult.json` | normalisierte Metriken für das gemeinsame Messprotokoll |
|
||||
|
||||
Aus `RawResult.json` verwenden:
|
||||
|
||||
| Messgröße | Feld |
|
||||
|---|---|
|
||||
| Erfolg/Abbruch | `is_error`, `subtype`, `timed_out`, `interrupted`, `exit_code`, `errors` |
|
||||
| Modellkontrolle | `provider`, `model`, `model_requested` |
|
||||
| Zeit | `duration_ms`, `start_time`, `end_time` |
|
||||
| Tokens | `usage.prompt_tokens`, `completion_tokens`, `reasoning_tokens`, `cache_read_tokens`, `cache_creation_tokens`, `total_tokens` |
|
||||
| Turns und Ende | `num_turns`, `finish_reason` |
|
||||
| Tools | `tool_call_count`, `tool_call_types`, `tool_calls` |
|
||||
| Subagenten | `subagent_stats`, `subagent_details` |
|
||||
| Ergebnisdateien | `written_files` |
|
||||
| Abschlusstext | `result` |
|
||||
| Reproduzierbarkeit | `adapter_version`, `opencode_version`, `config_path`, `session_id`, `adapter` |
|
||||
| Effortwirkung | `effort`, `effort_applied` |
|
||||
| Lokaler Betrieb | `local_runtime`, `context_window`, `cost_source` (nur `lmstudio`) |
|
||||
|
||||
`usage.total_tokens` übernimmt nach Möglichkeit OpenCodes Gesamtwert. Fehlt dieser, addiert
|
||||
der Adapter Input, Output, Reasoning, Cache-Read und Cache-Write aus den gelieferten
|
||||
OpenCode-Feldern. Kosten werden nur protokolliert, wenn OpenCode sie liefert; nicht schätzen.
|
||||
Bei `lmstudio` entstehen keine Providerkosten – `cost` ist definitionsgemäß `0`, nicht
|
||||
„unbekannt". Cache-Read und Cache-Write liefert der lokale Server nicht; sie sind im Protokoll
|
||||
als `nicht erfasst` auszuweisen.
|
||||
|
||||
## Pflichtprüfung
|
||||
|
||||
1. `RawResult.json` existiert und `is_error` ist `false`.
|
||||
2. `timed_out` und `interrupted` sind `false`; `exit_code` ist `0`.
|
||||
3. `OpenCode.log` und `errors` enthalten keinen Provider- oder Permissionfehler.
|
||||
4. Für Analyseversuche ist `Ergebnisse/` nicht leer und `tool_call_count` größer null.
|
||||
5. `model_requested` entspricht der angeforderten Modell-ID; `provider` entspricht `--provider`.
|
||||
6. Bei `custom` sind die erwarteten Rollen in `subagent_stats.by_type` nachweisbar.
|
||||
7. Bei `lmstudio` zusätzlich: `local_runtime.loaded_context_length` ≥ `--min-context`,
|
||||
`local_runtime.quantization` und `local_runtime.lms_version` sind gefüllt, und
|
||||
`effort_applied` ist `false` (im Protokoll als nicht steuerbar vermerken).
|
||||
|
||||
Ein absichtlich textueller Smoke-Test darf von den Prüfungen 4 und 6 abweichen, muss aber
|
||||
Antwort, Exitcode, Modell, Sessionexport und Tokenmetriken bestätigen.
|
||||
@@ -0,0 +1,244 @@
|
||||
import importlib.util
|
||||
import tempfile
|
||||
import time
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
ADAPTER_PATH = Path(__file__).with_name("glm-kimi-adapter.py")
|
||||
SPEC = importlib.util.spec_from_file_location("glm_kimi_adapter", ADAPTER_PATH)
|
||||
ADAPTER = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(ADAPTER)
|
||||
|
||||
|
||||
def response(message, finish_reason="tool_calls", tokens=10):
|
||||
return {
|
||||
"model": "test/model",
|
||||
"usage": {
|
||||
"prompt_tokens": tokens,
|
||||
"completion_tokens": 1,
|
||||
"total_tokens": tokens + 1,
|
||||
"prompt_tokens_details": {"cached_tokens": 0},
|
||||
"completion_tokens_details": {"reasoning_tokens": 0},
|
||||
},
|
||||
"choices": [{"finish_reason": finish_reason, "message": message}],
|
||||
}
|
||||
|
||||
|
||||
def subagent_result(agent_id):
|
||||
return {
|
||||
"result": f"Ergebnis {agent_id}",
|
||||
"usage": {
|
||||
"prompt_tokens": 2,
|
||||
"completion_tokens": 1,
|
||||
"total_tokens": 3,
|
||||
"cached_tokens": 0,
|
||||
"reasoning_tokens": 0,
|
||||
},
|
||||
"turns": 1,
|
||||
"tool_calls": 0,
|
||||
"errors": [],
|
||||
"status": "completed",
|
||||
}
|
||||
|
||||
|
||||
class AdapterTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.provider = {
|
||||
"base_url": "https://example.invalid/v1",
|
||||
"__id": "test",
|
||||
}
|
||||
self.liveness = ADAPTER.LivenessMonitor(interval_seconds=0)
|
||||
|
||||
def test_timeout_zero_is_forwarded_as_no_requests_timeout(self):
|
||||
captured = {}
|
||||
|
||||
class FakeResponse:
|
||||
status_code = 200
|
||||
text = ""
|
||||
|
||||
@staticmethod
|
||||
def json():
|
||||
return {"ok": True}
|
||||
|
||||
def fake_post(*args, **kwargs):
|
||||
captured["timeout"] = kwargs["timeout"]
|
||||
return FakeResponse()
|
||||
|
||||
with patch.object(ADAPTER.requests, "post", side_effect=fake_post):
|
||||
result = ADAPTER.post_chat_completion(
|
||||
"https://example.invalid", {}, {}, 0, self.liveness,
|
||||
"test", "Testaufruf",
|
||||
)
|
||||
|
||||
self.assertEqual({"ok": True}, result)
|
||||
self.assertIsNone(captured["timeout"])
|
||||
|
||||
def test_requested_tensorx_models_have_explicit_effort_mapping(self):
|
||||
self.assertEqual("thinking", ADAPTER.MODEL_EFFORT_TYPE["qwen"])
|
||||
self.assertEqual("thinking", ADAPTER.MODEL_EFFORT_TYPE["z-ai"])
|
||||
|
||||
def test_custom_mode_exposes_spawn_subagent(self):
|
||||
captured = {}
|
||||
|
||||
def fake_post(url, headers, body, timeout, liveness,
|
||||
activity_id, activity_label):
|
||||
captured["tools"] = body["tools"]
|
||||
return response(
|
||||
{"role": "assistant", "content": "Fertig."},
|
||||
finish_reason="stop",
|
||||
)
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir, \
|
||||
patch.object(ADAPTER, "post_chat_completion", side_effect=fake_post):
|
||||
result = ADAPTER.run_agent_loop(
|
||||
provider=self.provider,
|
||||
model="qwen/qwen3.8-flash-next",
|
||||
system_prompt="System",
|
||||
user_prompt="Aufgabe",
|
||||
api_key="key",
|
||||
effort="high",
|
||||
root=temp_dir,
|
||||
output_dir=temp_dir,
|
||||
max_turns=0,
|
||||
temperature=1.0,
|
||||
timeout=0,
|
||||
liveness=self.liveness,
|
||||
mode="custom",
|
||||
)
|
||||
|
||||
tool_names = {
|
||||
tool["function"]["name"] for tool in captured["tools"]
|
||||
}
|
||||
self.assertIn("spawn_subagent", tool_names)
|
||||
self.assertFalse(result["is_error"])
|
||||
|
||||
def test_liveness_monitor_reports_active_main_and_subagent_operations(self):
|
||||
lines = []
|
||||
monitor = ADAPTER.LivenessMonitor(interval_seconds=0.02)
|
||||
with patch.object(ADAPTER, "log_stderr", side_effect=lines.append):
|
||||
monitor.set("main", "Hauptagent wartet auf Subagenten")
|
||||
monitor.set("subagent-1", "Subagent 1 wartet auf API-Antwort")
|
||||
monitor.start()
|
||||
time.sleep(0.06)
|
||||
monitor.stop()
|
||||
|
||||
output = "\n".join(lines)
|
||||
self.assertIn("LIFESIGN: Prozess lebt", output)
|
||||
self.assertIn("Hauptagent wartet auf Subagenten", output)
|
||||
self.assertIn("Subagent 1 wartet auf API-Antwort", output)
|
||||
self.assertIn("keinen serverseitigen Fortschritt", output)
|
||||
|
||||
def test_subagents_from_one_turn_run_in_parallel_without_count_limit(self):
|
||||
first_message = {
|
||||
"role": "assistant",
|
||||
"content": "Ich delegiere.",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": f"spawn-{index}",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "spawn_subagent",
|
||||
"arguments": (
|
||||
'{"description":"Aufgabe %d",'
|
||||
'"subagent_type":"explore"}' % index
|
||||
),
|
||||
},
|
||||
}
|
||||
for index in range(12)
|
||||
],
|
||||
}
|
||||
responses = [
|
||||
response(first_message),
|
||||
response(
|
||||
{"role": "assistant", "content": "Fertig."},
|
||||
finish_reason="stop",
|
||||
),
|
||||
]
|
||||
starts = []
|
||||
|
||||
def fake_post(*args, **kwargs):
|
||||
return responses.pop(0)
|
||||
|
||||
def fake_subagent(*args, **kwargs):
|
||||
agent_id = args[10]
|
||||
starts.append(time.monotonic())
|
||||
time.sleep(0.15)
|
||||
return subagent_result(agent_id)
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
started = time.monotonic()
|
||||
with patch.object(ADAPTER, "post_chat_completion", side_effect=fake_post), \
|
||||
patch.object(ADAPTER, "run_subagent", side_effect=fake_subagent):
|
||||
result = ADAPTER.run_agent_loop(
|
||||
provider=self.provider,
|
||||
model="test/model",
|
||||
system_prompt="System",
|
||||
user_prompt="Aufgabe",
|
||||
api_key="key",
|
||||
effort="high",
|
||||
root=temp_dir,
|
||||
output_dir=temp_dir,
|
||||
max_turns=0,
|
||||
temperature=1.0,
|
||||
timeout=0,
|
||||
liveness=self.liveness,
|
||||
mode="builtin",
|
||||
subagent_max_turns=0,
|
||||
)
|
||||
elapsed = time.monotonic() - started
|
||||
|
||||
self.assertEqual(12, result["subagent_stats"]["spawned"])
|
||||
self.assertEqual(12, result["subagent_stats"]["completed"])
|
||||
self.assertEqual(0, result["subagent_stats"]["failed"])
|
||||
self.assertLess(max(starts) - min(starts), 0.12)
|
||||
self.assertLess(elapsed, 0.6)
|
||||
|
||||
def test_api_error_is_not_masked_by_earlier_content(self):
|
||||
first_message = {
|
||||
"role": "assistant",
|
||||
"content": "Zwischenstand",
|
||||
"tool_calls": [{
|
||||
"id": "list-1",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "list_directory",
|
||||
"arguments": '{"path":""}',
|
||||
},
|
||||
}],
|
||||
}
|
||||
calls = [response(first_message), RuntimeError("Transportfehler")]
|
||||
|
||||
def fake_post(*args, **kwargs):
|
||||
item = calls.pop(0)
|
||||
if isinstance(item, Exception):
|
||||
raise item
|
||||
return item
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir, \
|
||||
patch.object(ADAPTER, "post_chat_completion", side_effect=fake_post):
|
||||
result = ADAPTER.run_agent_loop(
|
||||
provider=self.provider,
|
||||
model="test/model",
|
||||
system_prompt="System",
|
||||
user_prompt="Aufgabe",
|
||||
api_key="key",
|
||||
effort="high",
|
||||
root=temp_dir,
|
||||
output_dir=temp_dir,
|
||||
max_turns=0,
|
||||
temperature=1.0,
|
||||
timeout=0,
|
||||
liveness=self.liveness,
|
||||
mode="solo",
|
||||
)
|
||||
|
||||
self.assertEqual("Zwischenstand", result["result"])
|
||||
self.assertTrue(result["is_error"])
|
||||
self.assertEqual("error", result["subtype"])
|
||||
self.assertIn("Transportfehler", result["errors"][0])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,447 @@
|
||||
import importlib.util
|
||||
import json
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ADAPTER_PATH = Path(__file__).with_name("opencode-adapter.py")
|
||||
SPEC = importlib.util.spec_from_file_location("opencode_adapter", ADAPTER_PATH)
|
||||
ADAPTER = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(ADAPTER)
|
||||
|
||||
|
||||
class OpenCodeAdapterTests(unittest.TestCase):
|
||||
def test_model_reference_keeps_upstream_slashes(self):
|
||||
self.assertEqual(
|
||||
(
|
||||
"tensorx/qwen/qwen3.8-flash-next",
|
||||
"qwen/qwen3.8-flash-next",
|
||||
),
|
||||
ADAPTER.normalize_model("qwen/qwen3.8-flash-next"),
|
||||
)
|
||||
self.assertEqual(
|
||||
(
|
||||
"tensorx/qwen/qwen3.8-flash-next",
|
||||
"qwen/qwen3.8-flash-next",
|
||||
),
|
||||
ADAPTER.normalize_model("tensorx/qwen/qwen3.8-flash-next"),
|
||||
)
|
||||
|
||||
def test_custom_mode_translates_agents_and_restricts_delegation(self):
|
||||
base = {
|
||||
"provider": {
|
||||
"tensorx": {
|
||||
"models": {"qwen/qwen3.8-flash-next": {"name": "Qwen"}}
|
||||
}
|
||||
}
|
||||
}
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
temp = Path(temp_dir)
|
||||
root = temp / "root"
|
||||
output = root / "run" / "Ergebnisse"
|
||||
root.mkdir()
|
||||
output.mkdir(parents=True)
|
||||
agents = temp / "agents.json"
|
||||
agents.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"reviewer": {
|
||||
"description": "Prueft Fakten",
|
||||
"prompt": "Pruefe nur die zugewiesenen Fakten.",
|
||||
}
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
config = ADAPTER.build_run_config(
|
||||
base,
|
||||
"tensorx/qwen/qwen3.8-flash-next",
|
||||
"qwen/qwen3.8-flash-next",
|
||||
"custom",
|
||||
root,
|
||||
output,
|
||||
agents,
|
||||
)
|
||||
|
||||
self.assertEqual("allow", config["permission"]["task"]["reviewer"])
|
||||
self.assertEqual("deny", config["permission"]["task"]["*"])
|
||||
self.assertEqual("subagent", config["agent"]["reviewer"]["mode"])
|
||||
self.assertEqual(
|
||||
"tensorx/qwen/qwen3.8-flash-next",
|
||||
config["agent"]["reviewer"]["model"],
|
||||
)
|
||||
self.assertEqual("deny", config["agent"]["reviewer"]["permission"]["task"])
|
||||
self.assertIn(
|
||||
ADAPTER.normalized_path(output) + "/**",
|
||||
config["permission"]["edit"],
|
||||
)
|
||||
self.assertIn(
|
||||
"run/Ergebnisse/**",
|
||||
config["permission"]["edit"],
|
||||
)
|
||||
|
||||
def test_output_permissions_include_parent_relative_worktree_path(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
temp = Path(temp_dir)
|
||||
root = temp / "QuellCode" / "Product"
|
||||
output = temp / "Versuche" / "Lauf" / "Ergebnisse"
|
||||
root.mkdir(parents=True)
|
||||
output.mkdir(parents=True)
|
||||
|
||||
patterns = ADAPTER.output_permission_patterns(root, output, temp)
|
||||
|
||||
self.assertIn("Versuche/Lauf/Ergebnisse/**", patterns)
|
||||
self.assertIn("../../Versuche/Lauf/Ergebnisse", patterns)
|
||||
self.assertIn("../../Versuche/Lauf/Ergebnisse/**", patterns)
|
||||
self.assertIn(ADAPTER.normalized_path(output) + "/**", patterns)
|
||||
|
||||
def test_normalize_result_uses_exported_metrics_and_tool_calls(self):
|
||||
session = {
|
||||
"info": {
|
||||
"id": "ses_test",
|
||||
"version": "1.18.25",
|
||||
"model": {
|
||||
"id": "qwen/qwen3.8-flash-next",
|
||||
"providerID": "tensorx",
|
||||
},
|
||||
"tokens": {
|
||||
"input": 100,
|
||||
"output": 10,
|
||||
"reasoning": 5,
|
||||
"cache": {"read": 20, "write": 0},
|
||||
},
|
||||
"cost": 0.1,
|
||||
},
|
||||
"messages": [
|
||||
{
|
||||
"info": {"role": "assistant", "finish": "stop"},
|
||||
"parts": [
|
||||
{
|
||||
"type": "tool",
|
||||
"tool": "task",
|
||||
"state": {
|
||||
"status": "completed",
|
||||
"input": {
|
||||
"subagent_type": "reviewer",
|
||||
"description": "Pruefen",
|
||||
},
|
||||
},
|
||||
},
|
||||
{"type": "text", "text": "Fertig"},
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
output = Path(temp_dir)
|
||||
(output / "StRS.md").write_text("Inhalt", encoding="utf-8")
|
||||
result = ADAPTER.normalize_result(
|
||||
session=session,
|
||||
events=[],
|
||||
model_ref="tensorx/qwen/qwen3.8-flash-next",
|
||||
mode="custom",
|
||||
effort="low",
|
||||
exit_code=0,
|
||||
timed_out=False,
|
||||
interrupted=False,
|
||||
duration_s=1.5,
|
||||
output_dir=output,
|
||||
errors=[],
|
||||
)
|
||||
|
||||
self.assertFalse(result["is_error"])
|
||||
self.assertEqual(135, result["usage"]["total_tokens"])
|
||||
self.assertEqual(1, result["tool_call_count"])
|
||||
self.assertEqual(1, result["subagent_stats"]["spawned"])
|
||||
self.assertEqual({"reviewer": 1}, result["subagent_stats"]["by_type"])
|
||||
self.assertEqual("Fertig", result["result"])
|
||||
self.assertEqual(1, len(result["written_files"]))
|
||||
|
||||
|
||||
class ReadonlyShellTests(unittest.TestCase):
|
||||
"""Denylist: alles erlaubt ausser schreibenden und bauenden Kommandos."""
|
||||
|
||||
def test_catch_all_allow_comes_first(self):
|
||||
# OpenCode wertet der Reihe nach aus, die letzte passende Regel gewinnt.
|
||||
# Stuende das Catch-all hinten, waere jede Sperre wirkungslos.
|
||||
perms = ADAPTER.readonly_shell_permissions()
|
||||
self.assertEqual("allow", perms["*"])
|
||||
self.assertEqual("*", next(iter(perms)))
|
||||
|
||||
def test_writing_and_building_commands_are_denied(self):
|
||||
perms = ADAPTER.readonly_shell_permissions()
|
||||
for muster in ("rm *", "mv *", "sed -i*", "git commit*", "git push*",
|
||||
"dotnet *", "msbuild *", "npm install*",
|
||||
"Remove-Item *", "Set-Content *", "Out-File *"):
|
||||
self.assertEqual("deny", perms[muster], muster)
|
||||
|
||||
def test_reading_commands_need_no_rule(self):
|
||||
# Der Kern der Umstellung: Lesekommandos - auch als Pipeline - sind
|
||||
# erlaubt, ohne einzeln aufgefuehrt zu sein. Genau daran scheiterte
|
||||
# 'Get-ChildItem ... | Format-Table ...' unter der Allowlist.
|
||||
perms = ADAPTER.readonly_shell_permissions()
|
||||
for kommando in ("ls -la", "cat foo.cs", "rg muster",
|
||||
"Get-ChildItem -Recurse | Format-Table Name",
|
||||
"find . -name *.cs"):
|
||||
self.assertNotIn(kommando, perms)
|
||||
self.assertEqual("allow", perms["*"])
|
||||
|
||||
def test_no_reading_command_is_denied(self):
|
||||
verboten_praefixe = ("ls", "cat", "rg", "grep", "find", "head", "tail",
|
||||
"Get-ChildItem", "Get-Content", "Select-String",
|
||||
"git status", "git log", "dir", "type")
|
||||
for muster, aktion in ADAPTER.readonly_shell_permissions().items():
|
||||
if aktion != "deny":
|
||||
continue
|
||||
for praefix in verboten_praefixe:
|
||||
self.assertFalse(
|
||||
muster.lower().startswith(praefix.lower()),
|
||||
f"Lesekommando gesperrt: {muster}",
|
||||
)
|
||||
|
||||
|
||||
class SpiegelTests(unittest.TestCase):
|
||||
"""Der Spiegel loest relative Ausgabepfade, ohne die Quelle zu beruehren."""
|
||||
|
||||
def test_relativer_schreibpfad_landet_im_laufverzeichnis(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
temp = Path(temp_dir)
|
||||
quelle = temp / "codebasis"
|
||||
(quelle / "src").mkdir(parents=True)
|
||||
(quelle / "src" / "a.cs").write_text("Quellcode", encoding="utf-8")
|
||||
(quelle / "README.md").write_text("Liesmich", encoding="utf-8")
|
||||
ausgabe = temp / "lauf" / "Ergebnisse"
|
||||
ausgabe.mkdir(parents=True)
|
||||
spiegel = temp / "spiegel"
|
||||
|
||||
ADAPTER.erstelle_spiegel(quelle, spiegel, ausgabe, lambda _: None)
|
||||
try:
|
||||
self.assertTrue((spiegel / "src" / "a.cs").is_file())
|
||||
self.assertTrue((spiegel / "README.md").is_file())
|
||||
# `Ergebnisse` ist im Spiegel ein echtes Verzeichnis - ein
|
||||
# relativer Schreibpfad landet dort und wird danach uebernommen.
|
||||
(spiegel / "Ergebnisse" / "StRS.md").write_text("X", encoding="utf-8")
|
||||
self.assertFalse((ausgabe / "StRS.md").is_file())
|
||||
anzahl = ADAPTER.uebernimm_ergebnisse(spiegel, ausgabe, lambda _: None)
|
||||
self.assertEqual(1, anzahl)
|
||||
self.assertTrue((ausgabe / "StRS.md").is_file())
|
||||
self.assertEqual("X", (ausgabe / "StRS.md").read_text(encoding="utf-8"))
|
||||
finally:
|
||||
ADAPTER.entferne_spiegel(spiegel, lambda _: None)
|
||||
|
||||
# Der Abbau darf die Quelle nicht durch die Junction hindurch loeschen.
|
||||
self.assertFalse(spiegel.exists())
|
||||
self.assertTrue((quelle / "src" / "a.cs").is_file())
|
||||
self.assertEqual("Quellcode", (quelle / "src" / "a.cs").read_text(encoding="utf-8"))
|
||||
self.assertTrue((ausgabe / "StRS.md").is_file())
|
||||
|
||||
def test_freigabe_enthaelt_arbeitsverzeichnis_relative_form(self):
|
||||
"""resolve() folgt der Junction - die relative Form darf nicht verloren gehen."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
temp = Path(temp_dir)
|
||||
quelle = temp / "codebasis"
|
||||
(quelle / "src").mkdir(parents=True)
|
||||
ausgabe = temp / "lauf" / "Ergebnisse"
|
||||
ausgabe.mkdir(parents=True)
|
||||
spiegel = temp / "spiegel"
|
||||
ADAPTER.erstelle_spiegel(quelle, spiegel, ausgabe, lambda _: None)
|
||||
try:
|
||||
config = ADAPTER.build_run_config(
|
||||
{}, "lmstudio/x", "x", "solo", spiegel, ausgabe, None,
|
||||
provider="lmstudio",
|
||||
zusatz_ausgaben=[spiegel / ausgabe.name],
|
||||
)
|
||||
finally:
|
||||
ADAPTER.entferne_spiegel(spiegel, lambda _: None)
|
||||
erlaubt = [k for k, v in config["permission"]["edit"].items() if v == "allow"]
|
||||
self.assertIn("Ergebnisse", erlaubt)
|
||||
self.assertIn("Ergebnisse/**", erlaubt)
|
||||
|
||||
def test_quelle_bleibt_ohne_neue_eintraege(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
temp = Path(temp_dir)
|
||||
quelle = temp / "codebasis"
|
||||
(quelle / "src").mkdir(parents=True)
|
||||
vorher = sorted(p.name for p in quelle.iterdir())
|
||||
ausgabe = temp / "lauf" / "Ergebnisse"
|
||||
ausgabe.mkdir(parents=True)
|
||||
spiegel = temp / "spiegel"
|
||||
|
||||
ADAPTER.erstelle_spiegel(quelle, spiegel, ausgabe, lambda _: None)
|
||||
(spiegel / "Ergebnisse" / "datei.md").write_text("X", encoding="utf-8")
|
||||
ADAPTER.uebernimm_ergebnisse(spiegel, ausgabe, lambda _: None)
|
||||
ADAPTER.entferne_spiegel(spiegel, lambda _: None)
|
||||
|
||||
self.assertEqual(vorher, sorted(p.name for p in quelle.iterdir()))
|
||||
|
||||
|
||||
class UsageCapturedTests(unittest.TestCase):
|
||||
"""Eine Null aus einem laufenden Subagenten ist kein Messwert."""
|
||||
|
||||
def _result(self, session, output):
|
||||
return ADAPTER.normalize_result(
|
||||
session=session, events=[], model_ref="lmstudio/google/gemma-4-e4b",
|
||||
mode="builtin", effort="high", exit_code=1, timed_out=True,
|
||||
interrupted=False, duration_s=3600.0, output_dir=output, errors=[],
|
||||
provider="lmstudio", effort_applied=False,
|
||||
)
|
||||
|
||||
def test_running_subagent_with_zero_tokens_is_not_captured(self):
|
||||
session = {
|
||||
"info": {"model": {"id": "google/gemma-4-e4b"},
|
||||
"tokens": {"input": 0, "output": 0, "reasoning": 0,
|
||||
"cache": {"read": 0, "write": 0}}},
|
||||
"messages": [{
|
||||
"info": {"role": "assistant"},
|
||||
"parts": [{"type": "tool", "tool": "task",
|
||||
"state": {"status": "running",
|
||||
"input": {"subagent_type": "explore"}}}],
|
||||
}],
|
||||
}
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
result = self._result(session, Path(temp_dir))
|
||||
self.assertFalse(result["usage_captured"])
|
||||
self.assertIn("nicht erfasst", result["usage_note"])
|
||||
self.assertIn("1 Subagent(en)", result["usage_note"])
|
||||
|
||||
def test_real_token_counts_are_marked_captured(self):
|
||||
session = {
|
||||
"info": {"model": {"id": "google/gemma-4-e4b"},
|
||||
"tokens": {"input": 100, "output": 10, "reasoning": 0,
|
||||
"cache": {"read": 0, "write": 0}}},
|
||||
"messages": [{"info": {"role": "assistant"},
|
||||
"parts": [{"type": "text", "text": "ok"}]}],
|
||||
}
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
result = self._result(session, Path(temp_dir))
|
||||
self.assertTrue(result["usage_captured"])
|
||||
self.assertNotIn("usage_note", result)
|
||||
|
||||
|
||||
class OpenCodeLmStudioTests(unittest.TestCase):
|
||||
def test_model_reference_uses_lmstudio_provider(self):
|
||||
self.assertEqual(
|
||||
("lmstudio/google/gemma-4-e4b", "google/gemma-4-e4b"),
|
||||
ADAPTER.normalize_model("google/gemma-4-e4b", "lmstudio"),
|
||||
)
|
||||
self.assertEqual(
|
||||
("lmstudio/qwen/qwen3.5-9b", "qwen/qwen3.5-9b"),
|
||||
ADAPTER.normalize_model("lmstudio/qwen/qwen3.5-9b", "lmstudio"),
|
||||
)
|
||||
|
||||
def test_template_declares_both_local_models_without_key(self):
|
||||
template = json.loads(
|
||||
(ADAPTER_PATH.parent / "opencode-lmstudio.json").read_text(
|
||||
encoding="utf-8-sig"
|
||||
)
|
||||
)
|
||||
provider = template["provider"]["lmstudio"]
|
||||
self.assertEqual(
|
||||
"http://localhost:1234/v1", provider["options"]["baseURL"]
|
||||
)
|
||||
self.assertEqual(
|
||||
{"google/gemma-4-e4b", "qwen/qwen3.5-9b"},
|
||||
set(provider["models"]),
|
||||
)
|
||||
# Lokale Server pruefen den Key nicht; er darf nur ein Platzhalter sein.
|
||||
self.assertEqual("lm-studio", provider["options"]["apiKey"])
|
||||
|
||||
def test_build_run_config_pins_loaded_context_window(self):
|
||||
base = json.loads(
|
||||
(ADAPTER_PATH.parent / "opencode-lmstudio.json").read_text(
|
||||
encoding="utf-8-sig"
|
||||
)
|
||||
)
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
root = Path(temp_dir) / "root"
|
||||
output = root / "run" / "Ergebnisse"
|
||||
output.mkdir(parents=True)
|
||||
config = ADAPTER.build_run_config(
|
||||
base,
|
||||
"lmstudio/google/gemma-4-e4b",
|
||||
"google/gemma-4-e4b",
|
||||
"solo",
|
||||
root,
|
||||
output,
|
||||
None,
|
||||
provider="lmstudio",
|
||||
context_limit=32768,
|
||||
)
|
||||
|
||||
model = config["provider"]["lmstudio"]["models"]["google/gemma-4-e4b"]
|
||||
self.assertEqual(32768, model["limit"]["context"])
|
||||
self.assertEqual("deny", config["permission"]["task"])
|
||||
self.assertEqual("deny", config["permission"]["webfetch"])
|
||||
|
||||
def test_normalize_result_reports_local_runtime_and_zero_cost(self):
|
||||
runtime = {
|
||||
"provider": "lmstudio",
|
||||
"quantization": "Q4_K_M",
|
||||
"arch": "gemma4",
|
||||
"compatibility_type": "gguf",
|
||||
"loaded_context_length": 32768,
|
||||
"max_context_length": 131072,
|
||||
}
|
||||
session = {
|
||||
"info": {
|
||||
"id": "ses_local",
|
||||
"model": {"id": "google/gemma-4-e4b"},
|
||||
"tokens": {"input": 10, "output": 4, "reasoning": 2,
|
||||
"cache": {"read": 0, "write": 0}},
|
||||
"cost": 0,
|
||||
},
|
||||
"messages": [
|
||||
{
|
||||
"info": {"role": "assistant", "finish": "stop"},
|
||||
"parts": [{"type": "text", "text": "Fertig"}],
|
||||
}
|
||||
],
|
||||
}
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
output = Path(temp_dir)
|
||||
(output / "StRS.md").write_text("Inhalt", encoding="utf-8")
|
||||
result = ADAPTER.normalize_result(
|
||||
session=session,
|
||||
events=[],
|
||||
model_ref="lmstudio/google/gemma-4-e4b",
|
||||
mode="solo",
|
||||
effort="high",
|
||||
exit_code=0,
|
||||
timed_out=False,
|
||||
interrupted=False,
|
||||
duration_s=2.0,
|
||||
output_dir=output,
|
||||
errors=[],
|
||||
provider="lmstudio",
|
||||
effort_applied=False,
|
||||
local_runtime=runtime,
|
||||
)
|
||||
|
||||
self.assertEqual("lmstudio", result["provider"])
|
||||
self.assertEqual("opencode-lmstudio", result["adapter"])
|
||||
self.assertFalse(result["effort_applied"])
|
||||
self.assertEqual(32768, result["context_window"])
|
||||
self.assertEqual("Q4_K_M", result["local_runtime"]["quantization"])
|
||||
self.assertEqual(0, result["cost"])
|
||||
self.assertIn("lokaler Betrieb", result["cost_source"])
|
||||
self.assertEqual(16, result["usage"]["total_tokens"])
|
||||
|
||||
def test_tensorx_result_keeps_remote_shape(self):
|
||||
session = {"info": {"model": {"id": "qwen/qwen3.8-flash-next"},
|
||||
"tokens": {"input": 1, "output": 1}}, "messages": []}
|
||||
result = ADAPTER.normalize_result(
|
||||
session=session, events=[], model_ref="tensorx/qwen/qwen3.8-flash-next",
|
||||
mode="solo", effort="low", exit_code=0, timed_out=False,
|
||||
interrupted=False, duration_s=1.0, output_dir=Path("."), errors=[],
|
||||
)
|
||||
self.assertEqual("tensorx", result["provider"])
|
||||
self.assertEqual("opencode-tensorx", result["adapter"])
|
||||
self.assertTrue(result["effort_applied"])
|
||||
self.assertNotIn("local_runtime", result)
|
||||
self.assertNotIn("context_window", result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1 +1,5 @@
|
||||
|
||||
|
||||
# Spiegel-Arbeitsverzeichnisse der OpenCode-Laeufe (Junctions, laufzeitlokal)
|
||||
_meta/spiegel/
|
||||
**/_meta/spiegel/
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+1472
-24
File diff suppressed because it is too large
Load Diff
@@ -3,19 +3,17 @@
|
||||
## Metadaten
|
||||
- **Versuch:** V1 Baseline (Prompt-only)
|
||||
- **Iteration:** 01 (Initial-Prompt, zu Versuchsbeginn neu formuliert)
|
||||
- **Werkzeugkonfiguration:** Claude Code, keine Agentendateien, keine MCP-Server
|
||||
- **Codebasis:** c-entron ERP-Suite (Windows, C#/XAML, MSSQL)
|
||||
- **Modell:** Claude (Claude Code)
|
||||
- **Zeitstempel:** 2026-08-25
|
||||
- **Änderungsgrund:** Initial-Prompt für V1 Baseline (kein Vorgänger). Vor Erstlauf überarbeitet: Abgleich mit Kap. 4 (RRE-Methodenkette, Evaluationsrahmen) und Kap. 2 (Prozesskontrollen); mit der Vorfassung fand kein Lauf statt. Am 2026-08-26 werkzeugneutral gefasst: Aussagen zu Werkzeugkonfiguration, Modell und Ausgabeverzeichnis entfernt (sie werden vom Versuchsaufbau zur Laufzeit beigestellt und im Messprotokoll dokumentiert); Feld `Übernahmewürdigkeit` aus dem Evaluationsrahmen (Kap. 4.3) ergänzt.
|
||||
|
||||
> Dieser Prompt enthält ausschließlich die **Analyseanweisung** und ist damit unabhängig von einem
|
||||
> bestimmten Werkzeug oder Modell einsetzbar. Welche Werkzeuge im jeweiligen Lauf zur Verfügung
|
||||
> stehen und wohin die Ergebnisse geschrieben werden, stellt der Versuchsaufbau beim Start bei.
|
||||
- **Änderungsgrund:** Initial-Prompt für V1 Baseline (kein Vorgänger). Vor Erstlauf überarbeitet: Abgleich mit Kap. 4 (RRE-Methodenkette, Evaluationsrahmen) und Kap. 2 (Prozesskontrollen); mit der Vorfassung fand kein Lauf statt.
|
||||
|
||||
---
|
||||
|
||||
## Prompt
|
||||
|
||||
Du bist ein Requirements Engineer im Reverse Requirements Engineering eines Legacy-ERP-Systems. Erzeuge aus der vorliegenden Codebasis eine Anforderungsspezifikation nach **ISO/IEC/IEEE 29148:2018**. Arbeite ausschließlich auf den im Arbeitsverzeichnis liegenden Artefakten (Quellcode, Konfiguration, UI-Ressourcen, ggf. DB-Skripte). Nutze nur Informationen, die du aus diesen Artefakten gewinnen kannst.
|
||||
Du bist ein Requirements Engineer im Reverse Requirements Engineering eines Legacy-ERP-Systems. Erzeuge aus der vorliegenden Codebasis eine Anforderungsspezifikation nach **ISO/IEC/IEEE 29148:2018**. Arbeite ausschließlich auf den im Arbeitsverzeichnis liegenden Artefakten (Quellcode, Konfiguration, UI-Ressourcen, ggf. DB-Skripte). Es stehen weder spezialisierte Agenten noch MCP-Server zur Verfügung; nutze nur, was als Datei lesbar ist.
|
||||
|
||||
### Auftrag
|
||||
|
||||
@@ -53,8 +51,6 @@ Bearbeite die Schritte 2-6 der RRE-Methodenkette (Schritt 1 Scope ist oben vorge
|
||||
- **Hypothesenmarkierung:** Aussagen, die sich nicht eindeutig aus Artefakten ableiten lassen, kennzeichnest du explizit mit `[HYPOTHESE]` und einer kurzen Begründung, welche Information zur Bestätigung fehlt.
|
||||
- **Verifizierbarkeit:** Jede Anforderung enthält mindestens eine Prüfidee oder ein Akzeptanzkriterium.
|
||||
- **Eindeutigkeit:** Vermeide vage Begriffe ("schnell", "benutzerfreundlich"); definiere domänenspezifische Begriffe beim ersten Auftreten.
|
||||
- **Übernahmewürdigkeit:** Beurteile für jede Anforderung, ob ihre Funktion im Zielsystem erhalten bleiben soll. Unterscheide `übernehmen` (fachlich weiterhin erforderlich), `Workaround` (historisch gewachsene Behelfslösung), `Sonderfall` (Ausnahme für einen einzelnen Kunden, Mandanten oder Altbestand) und `veraltet` (durch neuere Logik abgelöst oder fachlich überholt). Begründe die Einstufung in einem Halbsatz. Diese Angabe steuert die spätere fachliche Priorisierung; eine Fehleinschätzung ist unkritisch, eine fehlende Angabe nicht.
|
||||
- **Redundanzfreiheit:** Formuliere jede Anforderung so, dass sie von den übrigen klar abgegrenzt ist. Beschreiben zwei Anforderungen dieselbe fachliche Funktion aus unterschiedlicher Perspektive, führe sie zusammen oder grenze sie im Titel und in der Aussage ausdrücklich gegeneinander ab.
|
||||
|
||||
### Formatvorgabe pro Anforderung
|
||||
|
||||
@@ -75,7 +71,6 @@ Belege:
|
||||
Prüfidee: <Akzeptanzkriterium oder Testidee>
|
||||
Tracelinks: <verwandte StRS-/SyRS-/SwRS-IDs>
|
||||
Konsolidierung: <nein | Kandidat: <IDs oder Stellen, die dieselbe fachliche Funktion abbilden>>
|
||||
Übernahmewürdigkeit: <übernehmen | Workaround | Sonderfall | veraltet> - <kurze Begründung>
|
||||
Status: <belegt | HYPOTHESE>
|
||||
```
|
||||
|
||||
@@ -108,14 +103,14 @@ Ergebnisse/
|
||||
Analysebericht.md (Modul-/Komponentenübersicht, abgedeckte Bereiche, bekannte Lücken)
|
||||
```
|
||||
|
||||
Das Ausgabeverzeichnis wird beim Start des Laufs beigestellt. Die analysierte Codebasis wird ausschließlich gelesen und nicht verändert.
|
||||
Das Ausgabeverzeichnis wird beim Start des Laufs benannt. Die analysierte Codebasis wird ausschließlich gelesen und nicht verändert.
|
||||
|
||||
### Randbedingungen
|
||||
|
||||
- **Keine Halluzinationen.** Wenn ein Artefakt nicht gelesen oder eine Aussage nicht belegt werden kann, ist das offen zu legen, nicht zu erfinden.
|
||||
- **Keine Generierung von Code.** Es sollen ausschließlich Spezifikationsartefakte entstehen.
|
||||
- **Keine Annahme über nicht beigestellte Hilfsmittel.** Arbeite mit dem, was dir in diesem Lauf zur Verfügung steht. Setze keine zusätzlichen Analysewerkzeuge, Datenbankzugriffe oder laufende Systeme voraus. Stehen für eine Aussage nur indirekte Belege zur Verfügung, ist sie als `[HYPOTHESE]` zu kennzeichnen.
|
||||
- **Migrationsperspektive berücksichtigen.** Erkennbare Workarounds, Sonderfälle und überholte Logik gehören in das Feld `Übernahmewürdigkeit`, nicht in das Feld `Status`. `Status` beschreibt ausschließlich die Belegsituation (`belegt` oder `HYPOTHESE`), `Übernahmewürdigkeit` die fachliche Zukunft der Anforderung. Beide Angaben sind unabhängig voneinander: Eine gut belegte Anforderung kann ein Workaround sein, eine Hypothese kann übernahmewürdig sein.
|
||||
- **Keine Annahme nicht vorhandener Tools.** Stehen für eine Aussage nur indirekte Belege zur Verfügung, ist die Aussage als `[HYPOTHESE]` zu kennzeichnen.
|
||||
- **Migrationsperspektive berücksichtigen.** Wenn eine Implementierung erkennbar ein historischer Workaround oder Sonderfall ist, vermerke das im Feld `Status` (`belegt; Workaround`), damit es in der späteren Validierung priorisiert geprüft werden kann.
|
||||
- **Sprache:** Deutsch für Anforderungsaussagen, technische Bezeichner (Klassen, Methoden, Spalten) bleiben in ihrer Originalsprache.
|
||||
|
||||
### Abschluss
|
||||
@@ -123,9 +118,7 @@ Das Ausgabeverzeichnis wird beim Start des Laufs beigestellt. Die analysierte Co
|
||||
Führe vor Abgabe einen **Konsistenzcheck über das gesamte Anforderungs-Set** durch und dokumentiere das Ergebnis im `Analysebericht.md`:
|
||||
- Doppelte oder mehrfach vergebene IDs
|
||||
- Anforderungen ohne Beleg
|
||||
- Anforderungen ohne Angabe zur `Übernahmewürdigkeit`
|
||||
- Tracelinks auf nicht existierende IDs
|
||||
- Inhaltlich deckungsgleiche Anforderungen, die nicht als Konsolidierungskandidat markiert sind
|
||||
|
||||
Beende den Lauf mit einer kurzen Selbstbewertung im `Analysebericht.md`:
|
||||
- Welche Module wurden vollständig analysiert, welche nur stichprobenhaft, welche gar nicht?
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"modulinventar": {
|
||||
"description": "Erstellt das vollständige Modulinventar (Schritt 0) als Bezugsgröße für die Abdeckung. Erzeugt KEINE Anforderungen.",
|
||||
"prompt": "Du erstellst das Modulinventar für ein Reverse-Requirements-Engineering-Vorhaben. Du formulierst KEINE Anforderungen – deine einzige Aufgabe ist eine vollständige, belegte Bestandsaufnahme.\n\nVorgehen:\n1. Verschaffe dir über Verzeichnisauflistungen und Projektdateien (.sln, .csproj, Ordnerstruktur) einen vollständigen Überblick über den Untersuchungsgegenstand. Arbeite von der Struktur aus, nicht von Stichproben.\n2. Erfasse JEDES fachliche Modul und JEDE technische Querschnittskomponente. Ein Modul, das du nicht erfasst, existiert für die gesamte weitere Analyse nicht.\n3. Liefere je Eintrag: laufende ID (M001, M002, …), Modulname, Pfad im Arbeitsverzeichnis, ein Satz zur fachlichen Aufgabe, Anzahl der enthaltenen Quelldateien, und ob es sich um ein fachliches Modul oder eine technische Querschnittskomponente handelt.\n\nHarte Regeln:\n- Der eine Satz zur fachlichen Aufgabe muss aus dem belegen, was du tatsächlich gelesen hast (Klassennamen, UI-Strings, Kommentare, Tabellennamen). Rate nicht aus dem Ordnernamen.\n- Kannst du die Aufgabe eines Moduls nicht bestimmen, führe es mit dem Vermerk `Aufgabe unbestimmt` und einer kurzen Begründung. Lasse es NICHT weg.\n- Nenne am Ende die Gesamtzahl der Module und die Gesamtzahl der Quelldateien, die du dem Inventar zugeordnet hast, sowie die Gesamtzahl der Quelldateien im Arbeitsverzeichnis. Weichen die Zahlen ab, benenne die nicht zugeordneten Bereiche.\n\nDeine Antwort ist das Inventar als Markdown-Tabelle plus die Abschlusszahlen. Keine Einleitung, keine Zusammenfassung."
|
||||
},
|
||||
|
||||
"faktenermittler": {
|
||||
"description": "Erhebt für einen zugewiesenen Modulausschnitt belegte technische Fakten samt der durchsetzenden Codestelle. Formuliert KEINE Anforderungen.",
|
||||
"prompt": "Du erhebst Fakten aus einer Legacy-Codebasis für ein Reverse-Requirements-Engineering-Vorhaben. Du formulierst KEINE Anforderungen und KEINE Interpretationen – die schreibt der Auftraggeber selbst aus deinen Fakten. Liefere ausschließlich zitierfähige technische Beobachtungen.\n\nJe Fakt lieferst du:\n- **Fundstelle:** Pfad, Klasse, Methode und, wenn bestimmbar, Zeilenbereich.\n- **Beobachtung:** Was der Code tatsächlich tut – Statusübergang, Validierungsregel, Berechnungsformel, Berechtigungsprüfung, Constraint, Default. Zitiere die tragende Bedingung wörtlich oder eng paraphrasiert.\n- **Einstufung:** `PRIMÄR`, wenn die genannte Stelle die Regel **durchsetzt**; `SEKUNDÄR`, wenn sie die Regel nur aufruft, konfiguriert oder anzeigt; `KONTEXT`, wenn sie das Umfeld beschreibt.\n\nHarte Regeln – sie entscheiden über die Verwertbarkeit deiner Antwort:\n- **Ein Dateiverweis ist kein Fakt.** „Diese Datei betrifft die Fakturierung\" ist wertlos. Gefordert ist die durchsetzende Stelle samt Bedingung, etwa: `InvoiceService.cs:212, FinalizeInvoice() – wirft InvalidOperationException, wenn invoice.Status == InvoiceStatus.Paid`.\n- **Arbeite nicht mit Stichproben.** Öffne nicht „ein bis drei repräsentative Dateien\". Dein zugewiesener Ausschnitt ist vollständig zu durchsuchen; nutze Suchwerkzeuge, um die tragenden Stellen zu finden, statt zu raten, welche Datei repräsentativ ist.\n- **Melde ausdrücklich, was du NICHT gefunden hast.** Wenn eine Regel offensichtlich existiert, du die durchsetzende Stelle aber nicht lokalisieren konntest, sage das mit Begründung. Diese Meldungen werden zu ausgewiesenen Hypothesen; verschwiegene Lücken werden zu falschen Anforderungen.\n- **Erfinde nichts.** Kein Fakt ohne Fundstelle. Keine Vermutung im Gewand einer Beobachtung.\n\nGehe dort in die Tiefe, wo Sicherheitsregeln, Abrechnungs- und Fakturierungslogik oder Berechtigungsprüfungen liegen. Gliedere deine Antwort nach den Untermodulen deines Ausschnitts."
|
||||
},
|
||||
|
||||
"strs-autor": {
|
||||
"description": "Formuliert Stakeholder-Anforderungen (StRS) im vorgegebenen Blockformat aus gelieferten Fakten. Arbeitet ausschließlich auf der Stakeholder-Ebene.",
|
||||
"prompt": "Du formulierst **ausschließlich Stakeholder-Anforderungen (StRS)** nach ISO/IEC/IEEE 29148. Die Ebene ist deine Zuständigkeit und deine Grenze: Du schreibst keine System- (SyRS) und keine Software-Anforderungen (SwRS), auch dann nicht, wenn die Faktenlage es nahelegt. Verweise stattdessen über Tracelinks.\n\nDie StRS-Ebene beschreibt die **fachliche Sicht**: Akteure, Geschäftsziele, Geschäftsregeln, fachliche Ergebnisse. Sie beschreibt nicht, wie das System das technisch löst.\n\nDu erhältst Fakten mit Fundstelle und Belegeinstufung. Daraus formulierst du Anforderungen im vorgegebenen Blockformat (`ID`, `Titel`, `Ebene`, `Typ`, `Qualitätsmerkmal`, `Akteur`, `Vorbedingung`, `Fakt`, `Aussage`, `Ergebnis`, `Belege`, `Prüfidee`, `Tracelinks`, `Konsolidierung`, `Übernahmewürdigkeit`, `Status`). Das Format ist verbindlich; jedes Feld wird gefüllt.\n\nHarte Regeln:\n- **Keine Anforderung ohne Beleg.** Übernimm Fundstelle und Einstufung aus den gelieferten Fakten unverändert. Erfinde keine Belege und stufe nichts als `PRIMÄR` ein, was dir nicht als `PRIMÄR` geliefert wurde.\n- **Trenne `Fakt` und `Aussage` sauber.** `Fakt` ist die belegte Beobachtung, `Aussage` die fachliche Soll-Formulierung. Vermische beides nicht.\n- **Risikorelevante Anforderungen** (Sicherheit, Abrechnung, Berechtigungen) brauchen einen `PRIMÄR`-Beleg **oder** die Kennzeichnung `[HYPOTHESE]` in `Status`. Ein dritter Weg existiert nicht.\n- **Belege mehrfach, wo möglich.** Eine Anforderung, die von mehreren Stellen getragen wird, führt mehrere Belege. Ein einzelner Beleg ist zulässig, aber kein Ziel.\n- **Prüfidee ist Pflicht** und muss ein prüfbares Kriterium nennen, keine Absichtserklärung.\n\nAntworte ausschließlich mit den Anforderungsblöcken."
|
||||
},
|
||||
|
||||
"syrs-autor": {
|
||||
"description": "Formuliert System-Anforderungen (SyRS) im vorgegebenen Blockformat aus gelieferten Fakten. Arbeitet ausschließlich auf der Systemebene.",
|
||||
"prompt": "Du formulierst **ausschließlich System-Anforderungen (SyRS)** nach ISO/IEC/IEEE 29148. Die Ebene ist deine Zuständigkeit und deine Grenze: Du schreibst keine Stakeholder- (StRS) und keine Software-Anforderungen (SwRS). Verweise stattdessen über Tracelinks.\n\nDie SyRS-Ebene beschreibt das **Systemverhalten**: beobachtbares Verhalten an den Systemgrenzen, Schnittstellen, Statusmaschinen, Validierungen, Performance- und Sicherheitsanforderungen. Sie beschreibt nicht die interne Codestruktur – das ist SwRS.\n\nDu erhältst Fakten mit Fundstelle und Belegeinstufung. Daraus formulierst du Anforderungen im vorgegebenen Blockformat (`ID`, `Titel`, `Ebene`, `Typ`, `Qualitätsmerkmal`, `Akteur`, `Vorbedingung`, `Fakt`, `Aussage`, `Ergebnis`, `Belege`, `Prüfidee`, `Tracelinks`, `Konsolidierung`, `Übernahmewürdigkeit`, `Status`). Das Format ist verbindlich; jedes Feld wird gefüllt.\n\nHarte Regeln:\n- **Keine Anforderung ohne Beleg.** Fundstelle und Einstufung werden unverändert übernommen; nichts wird zu `PRIMÄR` hochgestuft.\n- **Trenne `Fakt` und `Aussage` sauber.**\n- **Risikorelevante Anforderungen** (Sicherheit, Abrechnung, Berechtigungen) brauchen einen `PRIMÄR`-Beleg **oder** `[HYPOTHESE]` in `Status`.\n- **Nicht-funktionale Anforderungen** tragen das ISO-25010-Qualitätsmerkmal im Feld `Qualitätsmerkmal`, nicht im Feld `Typ`.\n- **Jede SyRS-Anforderung referenziert die zugehörige StRS-Anforderung** in `Tracelinks`. Existiert keine, benenne die fachliche Lücke ausdrücklich, statt den Link leer zu lassen.\n- **Prüfidee ist Pflicht** und muss ein prüfbares Kriterium nennen.\n\nAntworte ausschließlich mit den Anforderungsblöcken."
|
||||
},
|
||||
|
||||
"swrs-autor": {
|
||||
"description": "Formuliert Software-Anforderungen (SwRS) im vorgegebenen Blockformat aus gelieferten Fakten. Arbeitet ausschließlich auf der Softwareebene.",
|
||||
"prompt": "Du formulierst **ausschließlich Software-Anforderungen (SwRS)** nach ISO/IEC/IEEE 29148. Die Ebene ist deine Zuständigkeit und deine Grenze: Du schreibst keine Stakeholder- (StRS) und keine System-Anforderungen (SyRS). Verweise stattdessen über Tracelinks.\n\nDie SwRS-Ebene beschreibt die **softwareinterne Sicht**: Komponenten, Datenmodelle, Persistenzregeln, interne Algorithmen und Berechnungsvorschriften, softwareinterne Constraints.\n\nDu erhältst Fakten mit Fundstelle und Belegeinstufung. Daraus formulierst du Anforderungen im vorgegebenen Blockformat (`ID`, `Titel`, `Ebene`, `Typ`, `Qualitätsmerkmal`, `Akteur`, `Vorbedingung`, `Fakt`, `Aussage`, `Ergebnis`, `Belege`, `Prüfidee`, `Tracelinks`, `Konsolidierung`, `Übernahmewürdigkeit`, `Status`). Das Format ist verbindlich; jedes Feld wird gefüllt.\n\nHarte Regeln:\n- **Keine Anforderung ohne Beleg.** Fundstelle und Einstufung werden unverändert übernommen.\n- **Trenne `Fakt` und `Aussage` sauber.**\n- **Risikorelevante Anforderungen** (Sicherheit, Abrechnung, Berechtigungen) brauchen einen `PRIMÄR`-Beleg **oder** `[HYPOTHESE]` in `Status`.\n- **Jede SwRS-Anforderung referenziert die zugehörige SyRS-Anforderung** in `Tracelinks`.\n- **Konsolidierungsprüfung:** Gemeint sind fachlich gleichartige Konzepte in getrennten Implementierungen – etwa zwei Datenhaltungen für denselben fachlichen Gegenstand. Zwei Anforderungen, die denselben Sachverhalt aus Sicht verschiedener Ebenen beschreiben, sind **kein** Konsolidierungsfall; dafür sind die Tracelinks da.\n- **Prüfidee ist Pflicht** und muss ein prüfbares Kriterium nennen.\n\nAntworte ausschließlich mit den Anforderungsblöcken."
|
||||
},
|
||||
|
||||
"konsistenzpruefer": {
|
||||
"description": "Prüft einen fertigen Anforderungssatz gegen die Vorgaben des Auftrags und meldet Verstöße. Formuliert und korrigiert selbst KEINE Anforderungen.",
|
||||
"prompt": "Du prüfst einen fertigen Anforderungssatz gegen die Vorgaben des Auftrags. Du korrigierst nichts und formulierst nichts um – du meldest Verstöße mit Fundstelle, damit der Auftraggeber entscheidet.\n\nPrüfe vollständig und zähle absolut, nicht beispielhaft:\n\n1. **Belegpflicht:** Anforderungen ohne jeden Beleg. Liste jede betroffene ID.\n2. **Risikobasierte Priorisierung:** Anforderungen vom Typ Sicherheit, Abrechnung oder Berechtigungen, die weder einen `PRIMÄR`-Beleg noch die Kennzeichnung `[HYPOTHESE]` tragen. Liste jede betroffene ID. **Dies ist der Prüfpunkt, der in der Praxis am häufigsten verfehlt wird – geh ihn Anforderung für Anforderung durch, nicht überschlägig.**\n3. **Verifizierbarkeit:** Anforderungen ohne Prüfidee oder mit einer Prüfidee, die kein prüfbares Kriterium nennt.\n4. **Traceability:** Anforderungen ohne Tracelinks; Tracelinks, die auf nicht existierende IDs zeigen; SwRS ohne SyRS-Bezug; SyRS ohne StRS-Bezug.\n5. **Formtreue:** doppelte IDs; fehlende Pflichtfelder; nicht-funktionale Anforderungen ohne ISO-25010-Qualitätsmerkmal; Qualitätsmerkmale, die fälschlich im Feld `Typ` stehen.\n6. **Ebenentreue:** Anforderungen, deren Inhalt nicht zur angegebenen Ebene passt (etwa eine Codestruktur-Aussage auf StRS-Ebene), sowie Blöcke, die in der Datei einer anderen Ebene abgelegt sind.\n7. **Deckungsgleichheit:** Weicht die Sammeldatei der Hypothesen von den Inline-Kennzeichnungen ab?\n\nLiefere je Prüfpunkt: Anzahl der Verstöße, Gesamtzahl der geprüften Anforderungen, und die vollständige Liste der betroffenen IDs. Bei null Verstößen sage das ausdrücklich. Schätze nichts und kürze keine Liste mit „und weitere\" ab."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
# Versuch 01 - Baseline (Prompt-only) - Iteration 02
|
||||
|
||||
## Metadaten
|
||||
- **Versuch:** V1 Baseline (Prompt-only)
|
||||
- **Iteration:** 02 (erste Überarbeitung nach Auswertung von Iteration 01)
|
||||
- **Codebasis:** c-entron ERP-Suite (Windows, C#/XAML, MSSQL)
|
||||
- **Zeitstempel:** 2026-08-26
|
||||
- **Vorgänger:** `01_Prompt.md` (SHA-256 `1B0DB06B…3C02FF`), 24 Läufe an Tag 1
|
||||
- **Änderungsgrund:** Auswertung der 24 Läufe von Tag 1 über 3.287 erzeugte Anforderungen. Jede Änderung ist an einen gemessenen Befund gekoppelt:
|
||||
|
||||
| Änderung | Auslösender Befund |
|
||||
|---|---|
|
||||
| Modulinventar als Pflicht-Vorstufe, Mindestabdeckung je Modul, Vertiefung erst danach | Anforderungszahl schwankte je Lauf zwischen 42 und 325 (Faktor 5,9); 55 bis 60 von rund 85 Modulen blieben unanalysiert; die Modultabellen der Analyseberichte reichten von 0 bis 51 Zeilen |
|
||||
| Hypothesenpflicht kalibriert, `Hypothesen.md` deckungsgleich mit den Inline-Markierungen | Zwei Läufe meldeten null Hypothesen bei 71 bzw. 148 Anforderungen; der Anteil schwankte zwischen 0 % und 26,2 %; mehrfach wichen Sammeldatei und Inline-Markierungen voneinander ab |
|
||||
| Primärbeleg muss die durchsetzende Stelle benennen | 45,9 % aller Anforderungen trugen genau einen Beleg; in einem Lauf waren 5 von 31 risikorelevanten Anforderungen weder mit `PRIMÄR` noch als `[HYPOTHESE]` gedeckt |
|
||||
| Belegpflicht verschärft: ohne Beleg keine Anforderung | Eine Anforderung wurde ohne jeden Beleg geschrieben |
|
||||
| Konsolidierungsbegriff an einem Beispiel kalibriert | Anteil der Konsolidierungskandidaten schwankte je Lauf zwischen 2,4 % und 35,2 % |
|
||||
| Eigenes Feld `Qualitätsmerkmal` für die ISO-25010-Zuordnung | Die Zuordnung war gefordert, hatte aber keinen Ablageort: nur 33 von 313 nicht-funktionalen Anforderungen führten sie als eigene Angabe |
|
||||
| Risikoanforderungen im Konsistenzcheck auflisten | Verstöße gegen die risikobasierte Priorisierung fielen erst in der nachgelagerten Auswertung auf, nicht im Lauf selbst |
|
||||
|
||||
Unverändert bleiben Prüfidee, Tracelinks, Belegklassifikation und das Blockformat: Prüfidee und Tracelinks waren in **allen** 3.287 Anforderungen gesetzt, 78,6 % der Belege waren `PRIMÄR`.
|
||||
|
||||
> Dieser Prompt enthält ausschließlich die **Analyseanweisung** und ist damit unabhängig von einem
|
||||
> bestimmten Werkzeug oder Modell einsetzbar. Welche Werkzeuge im jeweiligen Lauf zur Verfügung
|
||||
> stehen und wohin die Ergebnisse geschrieben werden, stellt der Versuchsaufbau beim Start bei.
|
||||
|
||||
---
|
||||
|
||||
## Prompt
|
||||
|
||||
Du bist ein Requirements Engineer im Reverse Requirements Engineering eines Legacy-ERP-Systems. Erzeuge aus der vorliegenden Codebasis eine Anforderungsspezifikation nach **ISO/IEC/IEEE 29148:2018**. Arbeite ausschließlich auf den im Arbeitsverzeichnis liegenden Artefakten (Quellcode, Konfiguration, UI-Ressourcen, ggf. DB-Skripte). Nutze nur Informationen, die du aus diesen Artefakten gewinnen kannst.
|
||||
|
||||
### Auftrag
|
||||
|
||||
Erzeuge eine konsolidierte Spezifikation auf den drei Ebenen:
|
||||
|
||||
1. **StRS** - Stakeholder Requirements Specification (fachliche Sicht, Akteure, Geschäftsziele)
|
||||
2. **SyRS** - System Requirements Specification (Systemverhalten, Schnittstellen, Performance-, Sicherheitsanforderungen)
|
||||
3. **SwRS** - Software Requirements Specification (Komponenten, Datenmodelle, Software-interne Regeln)
|
||||
|
||||
Ziel ist eine Spezifikation, die als belastbare Basis für eine Web-/SaaS-Neuimplementierung dienen kann.
|
||||
|
||||
### Scope (Schritt 1 der RRE-Methodenkette, manuell vorgegeben)
|
||||
|
||||
Der Untersuchungsgegenstand ist die **gesamte Codebasis** im Arbeitsverzeichnis. Es gilt bewusst keine Modulbeschränkung: Alle Module, Datenobjekte und Prozesse sind gleichrangig zu erfassen.
|
||||
|
||||
**Breite geht vor Tiefe.** Ein fehlendes Requirement führt bei einer Neuimplementierung zu Funktionsverlust; eine oberflächlich erfasste Funktion lässt sich dagegen nachschärfen. Erfasse deshalb zuerst die gesamte Breite und vertiefe erst danach. Halte dich an die Reihenfolge aus dem Abschnitt **Vorgehen**: erst Inventar, dann Mindestabdeckung, dann Vertiefung.
|
||||
|
||||
### Vorgehen (statische Analyse, keine Ausführung)
|
||||
|
||||
Bearbeite die Schritte 2-6 der RRE-Methodenkette (Schritt 1 Scope ist oben vorgegeben, Schritt 7 Validierung erfolgt manuell durch Fachexperten). Vorgeschaltet ist eine verbindliche Inventarisierung:
|
||||
|
||||
**Schritt 0 - Modulinventar (vor der ersten Anforderung).** Verschaffe dir zuerst einen vollständigen Überblick über den Untersuchungsgegenstand und lege ihn im `Analysebericht.md` als Tabelle ab: fachliches Modul beziehungsweise Komponente, Pfad im Arbeitsverzeichnis, ein Satz zur fachlichen Aufgabe. Das Inventar wird erstellt, **bevor** die erste Anforderung formuliert wird. Es ist die Bezugsgröße für die Abdeckung und darf später ergänzt, aber nicht gekürzt werden.
|
||||
|
||||
**Schritt 0b - Mindestabdeckung.** Jedes Modul des Inventars erhält **mindestens eine** Anforderung, bevor irgendein Modul vertieft wird. Lässt sich für ein Modul keine belegbare Anforderung bilden, führe es im Inventar als `nicht analysiert` mit einer kurzen Begründung. Ein Modul ohne Anforderung und ohne Begründung ist unzulässig.
|
||||
|
||||
**Schritt 0c - Vertiefung nach Risiko.** Erst wenn die Mindestabdeckung steht, vertiefe einzelne Module. Beginne dort, wo Sicherheitsregeln, Abrechnungs- und Fakturierungslogik oder Berechtigungsprüfungen liegen.
|
||||
|
||||
2. **Artefakterhebung:** Erfasse Quellcode, Konfiguration, UI-Texte, Datenbankschemata, Schnittstellenbeschreibungen sowie Change-Historie und Projektartefakte (Commit-Messages, Tickets, Release Notes, Migrationsnotizen), soweit als Datei lesbar.
|
||||
3. **Technische Analyse:** Identifiziere Module, Komponenten, Abhängigkeiten, Statusmaschinen, Validierungslogik, Berechtigungsprüfungen.
|
||||
4. **Semantische Interpretation:** Leite fachliche Aussagen aus technischen Implementierungen ab (z. B. Statusübergänge → Geschäftsregel).
|
||||
5. **Formalisierung:** Überführe die Aussagen in klare, testbare Anforderungen mit Kontext, Vorbedingung und Ergebnis.
|
||||
6. **Traceability-Anreicherung:** Verknüpfe jede Anforderung mit konkreten Artefaktbelegen.
|
||||
|
||||
### Pflicht-Eigenschaften jeder Anforderung
|
||||
|
||||
- **Belegpflicht:** Jede Anforderung **muss** mindestens einen konkreten Artefaktbeleg führen (Dateipfad, Klasse/Methode, SQL-Statement, UI-String, Konfigurationseintrag). Jeder Beleg erhält eine kurze Begründung, warum er die Aussage trägt. Lässt sich eine Aussage nicht belegen, **schreibe die Anforderung nicht** - erfasse den offenen Punkt stattdessen als Hypothese. Eine Anforderung ohne Beleg ist unter keinen Umständen zulässig.
|
||||
- **Trennung von Fakt und Interpretation:** Die belegte technische Beobachtung (Feld `Fakt`) wird getrennt von der fachlichen Interpretation (Feld `Aussage`) dokumentiert, damit nachvollziehbar bleibt, was im Artefakt steht und was daraus geschlossen wurde.
|
||||
- **Risikobasierte Priorisierung:** Anforderungen zu Sicherheitsregeln, Abrechnungs-/Fakturierungslogik und Berechtigungen unterliegen strengeren Evidenzanforderungen: Sie benötigen mindestens einen `PRIMÄR`-Beleg, andernfalls sind sie zwingend als `[HYPOTHESE]` zu kennzeichnen. Ein `PRIMÄR`-Beleg benennt hier die **durchsetzende Stelle** - Datei, Klasse, Methode und die konkrete Prüfung, Bedingung oder das Constraint. Ein Verweis auf eine Datei ohne Angabe der prüfenden Stelle genügt für diese Anforderungen nicht.
|
||||
- **Belegklassifikation:** Kennzeichne jeden Beleg als
|
||||
- `PRIMÄR` (durchgesetzte Regel im Code oder DB-Constraint),
|
||||
- `SEKUNDÄR` (UI-Label, Fehlermeldung, Reportlayout, Mappingtabelle, Konfigurationsschalter),
|
||||
- `KONTEXT` (Kommentar, Commit-Message, Ticketreferenz).
|
||||
- **Hypothesenmarkierung:** Aussagen, die sich nicht eindeutig aus Artefakten ableiten lassen, kennzeichnest du explizit mit `[HYPOTHESE]` und einer kurzen Begründung, welche Information zur Bestätigung fehlt. Bei einer Codebasis dieser Größe ist eine Analyse ohne jeden offenen Punkt unplausibel: Führst du keine einzige Hypothese, begründe das ausdrücklich in der Selbstbewertung. Umgekehrt ist eine hohe Hypothesenzahl kein Mangel, sondern ein Hinweis auf ehrliche Abgrenzung.
|
||||
- **Verifizierbarkeit:** Jede Anforderung enthält mindestens eine Prüfidee oder ein Akzeptanzkriterium.
|
||||
- **Eindeutigkeit:** Vermeide vage Begriffe ("schnell", "benutzerfreundlich"); definiere domänenspezifische Begriffe beim ersten Auftreten.
|
||||
- **Übernahmewürdigkeit:** Beurteile für jede Anforderung, ob ihre Funktion im Zielsystem erhalten bleiben soll. Unterscheide `übernehmen` (fachlich weiterhin erforderlich), `Workaround` (historisch gewachsene Behelfslösung), `Sonderfall` (Ausnahme für einen einzelnen Kunden, Mandanten oder Altbestand) und `veraltet` (durch neuere Logik abgelöst oder fachlich überholt). Begründe die Einstufung in einem Halbsatz. Diese Angabe steuert die spätere fachliche Priorisierung; eine Fehleinschätzung ist unkritisch, eine fehlende Angabe nicht.
|
||||
- **Redundanzfreiheit:** Formuliere jede Anforderung so, dass sie von den übrigen klar abgegrenzt ist. Beschreiben zwei Anforderungen dieselbe fachliche Funktion aus unterschiedlicher Perspektive, führe sie zusammen oder grenze sie im Titel und in der Aussage ausdrücklich gegeneinander ab.
|
||||
|
||||
### Formatvorgabe pro Anforderung
|
||||
|
||||
```
|
||||
ID: <StRS|SyRS|SwRS>-<laufende Nummer>
|
||||
Titel: <kurzer Titel>
|
||||
Ebene: <StRS | SyRS | SwRS>
|
||||
Typ: <funktional | nicht-funktional | Schnittstelle | Daten | Sicherheit | ...>
|
||||
Qualitätsmerkmal: <nur bei nicht-funktionalen Anforderungen: ISO-25010-Merkmal, sonst leer>
|
||||
Akteur: <Rolle / System / Komponente>
|
||||
Vorbedingung: <Zustand vor Auslösen>
|
||||
Fakt: <belegte technische Beobachtung, z. B. Statusübergang, Constraint, Prüfung>
|
||||
Aussage: Das System soll <...>. (fachliche Interpretation als klare Soll-Aussage)
|
||||
Ergebnis: <erwartetes Ergebnis / Nachbedingung>
|
||||
Belege:
|
||||
- [PRIMÄR] <Pfad/Klasse/Methode/SQL/UI-String> - Begründung: <warum trägt der Beleg die Aussage>
|
||||
- [SEKUNDÄR] <...> - Begründung: <...>
|
||||
- [KONTEXT] <...> - Begründung: <...>
|
||||
Prüfidee: <Akzeptanzkriterium oder Testidee>
|
||||
Tracelinks: <verwandte StRS-/SyRS-/SwRS-IDs>
|
||||
Konsolidierung: <nein | Kandidat: <IDs oder Stellen, die dieselbe fachliche Funktion abbilden>>
|
||||
Übernahmewürdigkeit: <übernehmen | Workaround | Sonderfall | veraltet> - <kurze Begründung>
|
||||
Status: <belegt | HYPOTHESE>
|
||||
```
|
||||
|
||||
### Traceability
|
||||
|
||||
Stelle Forward- und Backward-Traceability zwischen den drei Ebenen her:
|
||||
- Jede SwRS-Anforderung referenziert die zugehörige SyRS-Anforderung.
|
||||
- Jede SyRS-Anforderung referenziert die zugehörige StRS-Anforderung.
|
||||
- Erzeuge zusätzlich eine konsolidierte **Traceability-Tabelle** (Markdown oder CSV): `StRS-ID | SyRS-ID | SwRS-ID | Artefaktbeleg`.
|
||||
|
||||
### Nicht-funktionale Anforderungen
|
||||
|
||||
- Ordne nicht-funktionale Anforderungen den Qualitätsmerkmalen der **ISO/IEC 25010** zu (z. B. Zuverlässigkeit, Performance-Effizienz, Sicherheit, Wartbarkeit, Übertragbarkeit). Trage die Zuordnung in das dafür vorgesehene Feld `Qualitätsmerkmal` ein, nicht in das Feld `Typ`.
|
||||
- Leite Betriebs- und Sicherheitsanforderungen gezielt auch aus indirekt sichtbaren Artefakten ab: Konfigurationen, Deployment-Skripte, Logging-Policies, Rechteprüfungen.
|
||||
|
||||
### Konsolidierungsbedarf
|
||||
|
||||
Die Codebasis enthält fachliche Redundanz: Dieselbe Anforderung kann auf unterschiedlichen Masken oder in unterschiedlichen Modulen mehrfach und teils unterschiedlich implementiert sein. Prüfe daher bei jeder Anforderung, ob andere Anforderungen dieselbe fachliche Funktion abbilden, und vermerke solche Fälle im Feld `Konsolidierung` als Kandidat für eine Zusammenführung im Zielsystem.
|
||||
|
||||
**Gemeint sind fachlich gleichartige Konzepte in getrennten Implementierungen**, nicht bloß ähnlich formulierte Anforderungen. Ein Beispiel aus dieser Codebasis: Drucker werden als „Stammblätter" geführt, sonstige Hardware getrennt davon als „Assets" - zwei Datenhaltungen für denselben fachlichen Gegenstand, die im Zielsystem zu einem Asset-Konzept zusammengeführt werden sollen. Zwei Anforderungen, die denselben Sachverhalt nur aus Sicht verschiedener Ebenen beschreiben (etwa StRS und SwRS), sind **kein** Konsolidierungsfall - dafür sind die Tracelinks da.
|
||||
|
||||
### Ergebnisstruktur (im vorgegebenen Ausgabeverzeichnis)
|
||||
|
||||
```
|
||||
Ergebnisse/
|
||||
StRS.md
|
||||
SyRS.md
|
||||
SwRS.md
|
||||
Traceability.md (oder Traceability.csv)
|
||||
Hypothesen.md (Sammlung aller mit [HYPOTHESE] markierten Aussagen mit offener Frage)
|
||||
Glossar.md (Domänenbegriffe, die in den Anforderungen verwendet werden)
|
||||
Analysebericht.md (Modulinventar aus Schritt 0, Abdeckungstabelle, Konsistenzcheck,
|
||||
Selbstbewertung, bekannte Lücken)
|
||||
```
|
||||
|
||||
Das Ausgabeverzeichnis wird beim Start des Laufs beigestellt. Die analysierte Codebasis wird ausschließlich gelesen und nicht verändert.
|
||||
|
||||
### Randbedingungen
|
||||
|
||||
- **Keine Halluzinationen.** Wenn ein Artefakt nicht gelesen oder eine Aussage nicht belegt werden kann, ist das offen zu legen, nicht zu erfinden.
|
||||
- **Keine Generierung von Code.** Es sollen ausschließlich Spezifikationsartefakte entstehen.
|
||||
- **Keine Annahme über nicht beigestellte Hilfsmittel.** Arbeite mit dem, was dir in diesem Lauf zur Verfügung steht. Setze keine zusätzlichen Analysewerkzeuge, Datenbankzugriffe oder laufende Systeme voraus. Stehen für eine Aussage nur indirekte Belege zur Verfügung, ist sie als `[HYPOTHESE]` zu kennzeichnen.
|
||||
- **Migrationsperspektive berücksichtigen.** Erkennbare Workarounds, Sonderfälle und überholte Logik gehören in das Feld `Übernahmewürdigkeit`, nicht in das Feld `Status`. `Status` beschreibt ausschließlich die Belegsituation (`belegt` oder `HYPOTHESE`), `Übernahmewürdigkeit` die fachliche Zukunft der Anforderung. Beide Angaben sind unabhängig voneinander: Eine gut belegte Anforderung kann ein Workaround sein, eine Hypothese kann übernahmewürdig sein.
|
||||
- **Sprache:** Deutsch für Anforderungsaussagen, technische Bezeichner (Klassen, Methoden, Spalten) bleiben in ihrer Originalsprache.
|
||||
|
||||
### Abschluss
|
||||
|
||||
Führe vor Abgabe einen **Konsistenzcheck über das gesamte Anforderungs-Set** durch und dokumentiere das Ergebnis im `Analysebericht.md`:
|
||||
- Doppelte oder mehrfach vergebene IDs
|
||||
- Anforderungen ohne Beleg
|
||||
- Anforderungen ohne Angabe zur `Übernahmewürdigkeit`
|
||||
- Tracelinks auf nicht existierende IDs
|
||||
- Inhaltlich deckungsgleiche Anforderungen, die nicht als Konsolidierungskandidat markiert sind
|
||||
- **Liste aller risikorelevanten Anforderungen** (Sicherheit, Abrechnung/Fakturierung, Berechtigungen) mit ihrer Belegsituation: ID, Titel, ob ein `PRIMÄR`-Beleg vorliegt, andernfalls die `[HYPOTHESE]`-Kennzeichnung. Diese Liste macht Verstöße gegen die risikobasierte Priorisierung im Lauf selbst sichtbar.
|
||||
- **Abgleich `Hypothesen.md` gegen die Inline-Markierungen:** Beide müssen dieselben Anforderungen nennen. `Hypothesen.md` enthält genau die Anforderungen mit `[HYPOTHESE]`-Markierung und keine zusätzlichen freien Fragen; offene Punkte ohne zugehörige Anforderung gehören in die Selbstbewertung.
|
||||
|
||||
Erstelle außerdem die **Abdeckungstabelle** auf Basis des Modulinventars aus Schritt 0: je Modul die Einstufung `tief | mittel | flach | nicht analysiert` und die Anzahl der daraus erzeugten Anforderungen. Jede Zeile des Inventars muss in der Abdeckungstabelle auftauchen.
|
||||
|
||||
Beende den Lauf mit einer kurzen Selbstbewertung im `Analysebericht.md`:
|
||||
- Wie viele Module des Inventars wurden tief, mittel, flach beziehungsweise gar nicht analysiert? Nenne absolute Zahlen, nicht nur Beispiele.
|
||||
- Wurde die Mindestabdeckung erreicht, also hat jedes Modul mindestens eine Anforderung? Falls nein: welche Module fehlen und warum?
|
||||
- An welchen Stellen war der Beleg dünn (hoher Anteil `SEKUNDÄR`/`KONTEXT` oder `[HYPOTHESE]`)?
|
||||
- Falls keine einzige Hypothese geführt wurde: Begründung, warum die Analyse ohne offene Punkte auskommt.
|
||||
- Welche Erkenntnisse legen einen Nachschlag in einer Folge-Iteration nahe?
|
||||
@@ -0,0 +1,161 @@
|
||||
# Versuch 01 - Baseline (Prompt-only) - Iteration 03
|
||||
|
||||
## Metadaten
|
||||
- **Versuch:** V1 Baseline (Prompt-only)
|
||||
- **Iteration:** 03 (zweite Überarbeitung nach Auswertung der Iteration-6-Läufe)
|
||||
- **Codebasis:** c-entron ERP-Suite (Windows, C#/XAML, MSSQL)
|
||||
- **Zeitstempel:** 2026-08-28
|
||||
- **Vorgänger:** `02_Prompt.md` (SHA-256 `F9B2A1AA…0D7849`), 4 Läufe in Iteration 6
|
||||
- **Änderungsgrund:** Auswertung der 4 Iteration-6-Läufe (GLM-solo, GLM-builtin, Kimi-solo, Kimi-builtin). Jede Änderung ist an einen gemessenen Befund gekoppelt:
|
||||
|
||||
| Änderung | Auslösender Befund |
|
||||
|---|---|
|
||||
|
||||
---
|
||||
|
||||
## Prompt
|
||||
|
||||
Du bist ein Requirements Engineer im Reverse Requirements Engineering eines Legacy-ERP-Systems. Erzeuge aus der vorliegenden Codebasis eine Anforderungsspezifikation nach **ISO/IEC/IEEE 29148:2018**. Arbeite ausschließlich auf den im Arbeitsverzeichnis liegenden Artefakten (Quellcode, Konfiguration, UI-Ressourcen, ggf. DB-Skripte). Nutze nur Informationen, die du aus diesen Artefakten gewinnen kannst.
|
||||
|
||||
### Auftrag
|
||||
|
||||
Erzeuge eine konsolidierte Spezifikation auf den drei Ebenen:
|
||||
|
||||
1. **StRS** - Stakeholder Requirements Specification (fachliche Sicht, Akteure, Geschäftsziele)
|
||||
2. **SyRS** - System Requirements Specification (Systemverhalten, Schnittstellen, Performance-, Sicherheitsanforderungen)
|
||||
3. **SwRS** - Software Requirements Specification (Komponenten, Datenmodelle, Software-interne Regeln)
|
||||
|
||||
Ziel ist eine Spezifikation, die als belastbare Basis für eine Web-/SaaS-Neuimplementierung dienen kann.
|
||||
|
||||
### Scope (Schritt 1 der RRE-Methodenkette, manuell vorgegeben)
|
||||
|
||||
Der Untersuchungsgegenstand ist die **gesamte Codebasis** im Arbeitsverzeichnis. Es gilt bewusst keine Modulbeschränkung: Alle Module, Datenobjekte und Prozesse sind gleichrangig zu erfassen.
|
||||
|
||||
**Breite geht vor Tiefe.** Ein fehlendes Requirement führt bei einer Neuimplementierung zu Funktionsverlust; eine oberflächlich erfasste Funktion lässt sich dagegen nachschärfen. Erfasse deshalb zuerst die gesamte Breite und vertiefe erst danach. Halte dich an die Reihenfolge aus dem Abschnitt **Vorgehen**: erst Inventar, dann Mindestabdeckung, dann Vertiefung.
|
||||
|
||||
### Vorgehen (statische Analyse, keine Ausführung)
|
||||
|
||||
Bearbeite die Schritte 2-6 der RRE-Methodenkette (Schritt 1 Scope ist oben vorgegeben, Schritt 7 Validierung erfolgt manuell durch Fachexperten). Vorgeschaltet ist eine verbindliche Inventarisierung:
|
||||
|
||||
**Schritt 0 - Modulinventar (vor der ersten Anforderung).** Verschaffe dir zuerst einen vollständigen Überblick über den Untersuchungsgegenstand und lege ihn im `Analysebericht.md` als Tabelle ab: fachliches Modul beziehungsweise Komponente, Pfad im Arbeitsverzeichnis, ein Satz zur fachlichen Aufgabe. Das Inventar wird erstellt, **bevor** die erste Anforderung formuliert wird. Es ist die Bezugsgröße für die Abdeckung und darf später ergänzt, aber nicht gekürzt werden.
|
||||
|
||||
**Schritt 0b - Mindestabdeckung.** Jedes Modul des Inventars erhält **mindestens eine** Anforderung, bevor irgendein Modul vertieft wird. Lässt sich für ein Modul keine belegbare Anforderung bilden, führe es im Inventar als `nicht analysiert` mit einer kurzen Begründung. Ein Modul ohne Anforderung und ohne Begründung ist unzulässig. **Mehr als 10 % der Module als `nicht analysiert` zu führen, ist ein Hinweis auf unvollständige Erkundung** – gehe zurück und lies die zugehörigen Quelldateien, bevor du mit der Vertiefung fortfährst.
|
||||
|
||||
**Schritt 0c - Vertiefung nach Risiko.** Erst wenn die Mindestabdeckung steht, vertiefe einzelne Module. Beginne dort, wo Sicherheitsregeln, Abrechnungs- und Fakturierungslogik oder Berechtigungsprüfungen liegen.
|
||||
|
||||
2. **Artefakterhebung:** Erfasse Quellcode, Konfiguration, UI-Texte, Datenbankschemata, Schnittstellenbeschreibungen sowie Change-Historie und Projektartefakte (Commit-Messages, Tickets, Release Notes, Migrationsnotizen), soweit als Datei lesbar.
|
||||
3. **Technische Analyse:** Identifiziere Module, Komponenten, Abhängigkeiten, Statusmaschinen, Validierungslogik, Berechtigungsprüfungen.
|
||||
4. **Semantische Interpretation:** Leite fachliche Aussagen aus technischen Implementierungen ab (z. B. Statusübergänge → Geschäftsregel).
|
||||
5. **Formalisierung:** Überführe die Aussagen in klare, testbare Anforderungen mit Kontext, Vorbedingung und Ergebnis.
|
||||
6. **Traceability-Anreicherung:** Verknüpfe jede Anforderung mit konkreten Artefaktbelegen.
|
||||
|
||||
### Pflicht-Eigenschaften jeder Anforderung
|
||||
|
||||
- **Belegpflicht:** Jede Anforderung **muss** mindestens einen konkreten Artefaktbeleg führen (Dateipfad, Klasse/Methode, SQL-Statement, UI-String, Konfigurationseintrag). Jeder Beleg erhält eine kurze Begründung, warum er die Aussage trägt. Lässt sich eine Aussage nicht belegen, **schreibe die Anforderung nicht** - erfasse den offenen Punkt stattdessen als Hypothese. Eine Anforderung ohne Beleg ist unter keinen Umständen zulässig.
|
||||
- **Trennung von Fakt und Interpretation:** Die belegte technische Beobachtung (Feld `Fakt`) wird getrennt von der fachlichen Interpretation (Feld `Aussage`) dokumentiert, damit nachvollziehbar bleibt, was im Artefakt steht und was daraus geschlossen wurde.
|
||||
- **Risikobasierte Priorisierung:** Anforderungen zu Sicherheitsregeln, Abrechnungs-/Fakturierungslogik und Berechtigungen unterliegen strengeren Evidenzanforderungen: Sie benötigen mindestens einen `PRIMÄR`-Beleg, andernfalls sind sie zwingend als `[HYPOTHESE]` zu kennzeichnen. Ein `PRIMÄR`-Beleg benennt hier die **durchsetzende Stelle** - Datei, Klasse, Methode und die konkrete Prüfung, Bedingung oder das Constraint. Ein Verweis auf eine Datei ohne Angabe der prüfenden Stelle genügt für diese Anforderungen nicht.
|
||||
- **Belegklassifikation:** Kennzeichne jeden Beleg als
|
||||
- `PRIMÄR` (durchgesetzte Regel im Code oder DB-Constraint),
|
||||
- `SEKUNDÄR` (UI-Label, Fehlermeldung, Reportlayout, Mappingtabelle, Konfigurationsschalter),
|
||||
- `KONTEXT` (Kommentar, Commit-Message, Ticketreferenz).
|
||||
- **Hypothesenmarkierung:** Aussagen, die sich nicht eindeutig aus Artefakten ableiten lassen, kennzeichnest du explizit mit `[HYPOTHESE]` und einer kurzen Begründung, welche Information zur Bestätigung fehlt. Bei einer Codebasis dieser Größe ist eine Analyse ohne jeden offenen Punkt unplausibel: Führst du keine einzige Hypothese, begründe das ausdrücklich in der Selbstbewertung. Umgekehrt ist eine hohe Hypothesenzahl kein Mangel, sondern ein Hinweis auf ehrliche Abgrenzung.
|
||||
- **Verifizierbarkeit:** Jede Anforderung enthält mindestens eine Prüfidee oder ein Akzeptanzkriterium.
|
||||
- **Eindeutigkeit:** Vermeide vage Begriffe ("schnell", "benutzerfreundlich"); definiere domänenspezifische Begriffe beim ersten Auftreten.
|
||||
- **Übernahmewürdigkeit:** Beurteile für jede Anforderung, ob ihre Funktion im Zielsystem erhalten bleiben soll. Unterscheide `übernehmen` (fachlich weiterhin erforderlich), `Workaround` (historisch gewachsene Behelfslösung), `Sonderfall` (Ausnahme für einen einzelnen Kunden, Mandanten oder Altbestand) und `veraltet` (durch neuere Logik abgelöst oder fachlich überholt). Begründe die Einstufung in einem Halbsatz.
|
||||
- **Redundanzfreiheit:** Formuliere jede Anforderung so, dass sie von den übrigen klar abgegrenzt ist. Beschreiben zwei Anforderungen dieselbe fachliche Funktion aus unterschiedlicher Perspektive, führe sie zusammen oder grenze sie im Titel und in der Aussage ausdrücklich gegeneinander ab.
|
||||
|
||||
### Formatvorgabe pro Anforderung
|
||||
|
||||
```
|
||||
ID: <StRS|SyRS|SwRS>-<laufende Nummer>
|
||||
Titel: <kurzer Titel>
|
||||
Ebene: <StRS | SyRS | SwRS>
|
||||
Typ: <funktional | nicht-funktional | Schnittstelle | Daten | Sicherheit | ...>
|
||||
Qualitätsmerkmal: <nur bei nicht-funktionalen Anforderungen: ISO-25010-Merkmal, sonst leer>
|
||||
Akteur: <Rolle / System / Komponente>
|
||||
Vorbedingung: <Zustand vor Auslösen>
|
||||
Fakt: <belegte technische Beobachtung, z. B. Statusübergang, Constraint, Prüfung>
|
||||
Aussage: Das System soll <...>. (fachliche Interpretation als klare Soll-Aussage)
|
||||
Ergebnis: <erwartetes Ergebnis / Nachbedingung>
|
||||
Belege:
|
||||
- [PRIMÄR] <Pfad/Klasse/Methode/SQL/UI-String> - Begründung: <warum trägt der Beleg die Aussage>
|
||||
- [SEKUNDÄR] <...> - Begründung: <...>
|
||||
- [KONTEXT] <...> - Begründung: <...>
|
||||
Prüfidee: <Akzeptanzkriterium oder Testidee>
|
||||
Tracelinks: <verwandte StRS-/SyRS-/SwRS-IDs>
|
||||
Konsolidierung: <nein | Kandidat: <IDs oder Stellen, die dieselbe fachliche Funktion abbilden>>
|
||||
Übernahmewürdigkeit: <übernehmen | Workaround | Sonderfall | veraltet> - <kurze Begründung>
|
||||
Status: <belegt | HYPOTHESE>
|
||||
```
|
||||
|
||||
### Traceability
|
||||
|
||||
Stelle Forward- und Backward-Traceability zwischen den drei Ebenen her:
|
||||
- Jede SwRS-Anforderung referenziert die zugehörige SyRS-Anforderung.
|
||||
- Jede SyRS-Anforderung referenziert die zugehörige StRS-Anforderung.
|
||||
- Erzeuge zusätzlich eine konsolidierte **Traceability-Tabelle** (Markdown oder CSV): `StRS-ID | SyRS-ID | SwRS-ID | Artefaktbeleg`.
|
||||
|
||||
### Nicht-funktionale Anforderungen
|
||||
|
||||
- Ordne nicht-funktionale Anforderungen den Qualitätsmerkmalen der **ISO/IEC 25010** zu (z. B. Zuverlässigkeit, Performance-Effizienz, Sicherheit, Wartbarkeit, Übertragbarkeit). Trage die Zuordnung in das dafür vorgesehene Feld `Qualitätsmerkmal` ein, nicht in das Feld `Typ`.
|
||||
- Leite Betriebs- und Sicherheitsanforderungen gezielt auch aus indirekt sichtbaren Artefakten ab: Konfigurationen, Deployment-Skripte, Logging-Policies, Rechteprüfungen.
|
||||
|
||||
### Konsolidierungsbedarf
|
||||
|
||||
Die Codebasis enthält fachliche Redundanz: Dieselbe Anforderung kann auf unterschiedlichen Masken oder in unterschiedlichen Modulen mehrfach und teils unterschiedlich implementiert sein. Prüfe daher bei jeder Anforderung, ob andere Anforderungen dieselbe fachliche Funktion abbilden, und vermerke solche Fälle im Feld `Konsolidierung` als Kandidat für eine Zusammenführung im Zielsystem.
|
||||
|
||||
**Gemeint sind fachlich gleichartige Konzepte in getrennten Implementierungen**, nicht bloß ähnlich formulierte Anforderungen. Ein Beispiel aus dieser Codebasis: Drucker werden als „Stammblätter" geführt, sonstige Hardware getrennt davon als „Assets" - zwei Datenhaltungen für denselben fachlichen Gegenstand, die im Zielsystem zu einem Asset-Konzept zusammengeführt werden sollen. Zwei Anforderungen, die denselben Sachverhalt nur aus Sicht verschiedener Ebenen beschreiben (etwa StRS und SwRS), sind **kein** Konsolidierungsfall - dafür sind die Tracelinks da.
|
||||
|
||||
### Ergebnisstruktur (im vorgegebenen Ausgabeverzeichnis)
|
||||
|
||||
```text
|
||||
Ergebnisse/
|
||||
StRS.md
|
||||
SyRS.md
|
||||
SwRS.md
|
||||
Traceability.md (oder Traceability.csv)
|
||||
Hypothesen.md (Sammlung aller mit [HYPOTHESE] markierten Aussagen mit offener Frage)
|
||||
Glossar.md (Domänenbegriffe, die in den Anforderungen verwendet werden)
|
||||
Analysebericht.md (Modulinventar aus Schritt 0, Abdeckungstabelle, Konsistenzcheck,
|
||||
Selbstbewertung, bekannte Lücken)
|
||||
```
|
||||
|
||||
**Erstelle ausschließlich diese 7 Dateien.** Keine Ergänzungsdateien, keine Aufteilungen wie `SwRS-Ergaenzungen.md` oder `SyRS-Teil2.md`. Wenn eine Datei zu lang wird, fahre in derselben Datei fort — die ID-Reihe macht die Reihenfolge klar. Anforderungen außerhalb dieser 7 Dateien werden von der Auswertung nicht erfasst.
|
||||
|
||||
Das Ausgabeverzeichnis wird beim Start des Laufs beigestellt. Die analysierte Codebasis wird ausschließlich gelesen und nicht verändert.
|
||||
|
||||
### Randbedingungen
|
||||
|
||||
- **Keine Halluzinationen.** Wenn ein Artefakt nicht gelesen oder eine Aussage nicht belegt werden kann, ist das offen zu legen, nicht zu erfinden.
|
||||
- **Keine Generierung von Code.** Es sollen ausschließlich Spezifikationsartefakte entstehen.
|
||||
- **Keine Annahme über nicht beigestellte Hilfsmittel.** Arbeite mit dem, was dir in diesem Lauf zur Verfügung steht. Setze keine zusätzlichen Analysewerkzeuge, Datenbankzugriffe oder laufende Systeme voraus. Stehen für eine Aussage nur indirekte Belege zur Verfügung, ist sie als `[HYPOTHESE]` zu kennzeichnen.
|
||||
- **Migrationsperspektive berücksichtigen.** Erkennbare Workarounds, Sonderfälle und überholte Logik gehören in das Feld `Übernahmewürdigkeit`, nicht in das Feld `Status`. `Status` beschreibt ausschließlich die Belegsituation (`belegt` oder `HYPOTHESE`), `Übernahmewürdigkeit` die fachliche Zukunft der Anforderung. Beide Angaben sind unabhängig voneinander: Eine gut belegte Anforderung kann ein Workaround sein, eine Hypothese kann übernahmewürdig sein.
|
||||
- **Sprache:** Deutsch für Anforderungsaussagen, technische Bezeichner (Klassen, Methoden, Spalten) bleiben in ihrer Originalsprache.
|
||||
|
||||
### Abschluss
|
||||
|
||||
Führe vor Abgabe einen **Konsistenzcheck über das gesamte Anforderungs-Set** durch und dokumentiere das Ergebnis im `Analysebericht.md`:
|
||||
- Doppelte oder mehrfach vergebene IDs
|
||||
- Anforderungen ohne Beleg
|
||||
- Anforderungen ohne Angabe zur `Übernahmewürdigkeit`
|
||||
- Tracelinks auf nicht existierende IDs
|
||||
- Inhaltlich deckungsgleiche Anforderungen, die nicht als Konsolidierungskandidat markiert sind
|
||||
- **Liste aller risikorelevanten Anforderungen** (Sicherheit, Abrechnung/Fakturierung, Berechtigungen) mit ihrer Belegsituation: ID, Titel, ob ein `PRIMÄR`-Beleg vorliegt, andernfalls die `[HYPOTHESE]`-Kennzeichnung. Diese Liste macht Verstöße gegen die risikobasierte Priorisierung im Lauf selbst sichtbar.
|
||||
- **Abgleich `Hypothesen.md` gegen die Inline-Markierungen:** Beide müssen dieselben Anforderungen nennen. `Hypothesen.md` enthält genau die Anforderungen mit `[HYPOTHESE]`-Markierung und keine zusätzlichen freien Fragen; offene Punkte ohne zugehörige Anforderung gehören in die Selbstbewertung.
|
||||
|
||||
Erstelle außerdem die **Abdeckungstabelle** auf Basis des Modulinventars aus Schritt 0: je Modul die Einstufung `tief | mittel | flach | nicht analysiert` und die Anzahl der daraus erzeugten Anforderungen. Jede Zeile des Inventars muss in der Abdeckungstabelle auftauchen.
|
||||
|
||||
Beende den Lauf mit einer kurzen Selbstbewertung im `Analysebericht.md`:
|
||||
- Wie viele Module des Inventars wurden tief, mittel, flach beziehungsweise gar nicht analysiert? Nenne absolute Zahlen, nicht nur Beispiele.
|
||||
- Wurde die Mindestabdeckung erreicht, also hat jedes Modul mindestens eine Anforderung? Falls nein: welche Module fehlen und warum?
|
||||
- An welchen Stellen war der Beleg dünn (hoher Anteil `SEKUNDÄR`/`KONTEXT` oder `[HYPOTHESE]`)?
|
||||
- Falls keine einzige Hypothese geführt wurde: Begründung, warum die Analyse ohne offene Punkte auskommt.
|
||||
- Welche Erkenntnisse legen einen Nachschlag in einer Folge-Iteration nahe?
|
||||
| Ergebnisstruktur: nur die 7 vorgegebenen Dateien, keine Ergänzungsdateien | Kimi-solo erstellte `SwRS-Ergaenzungen.md` und `SyRS-Ergaenzungen.md` — 18 Anforderungen lagen außerhalb der vorgegebenen Dateien und wurden vom Auswertungsskript nicht erfasst |
|
||||
| Modulabdeckung härter einfordern: >10 % `nicht analysiert` = unvollständige Erkundung | GLM-solo ließ 33 von 120 Modulen (27,5 %) unanalysiert; Kimi-solo kam auf 1/56 (1,8 %) — die Streuung zeigt, dass die Formulierung „nicht analysiert mit Begründung" zu weich war |
|
||||
|
||||
Unverändert bleiben: Prüfidee, Tracelinks, Belegklassifikation, Blockformat, Hypothesenpflicht, risikobasierte Priorisierung, Konsolidierungsbegriff und ISO-25010-Zuordnung. Werkzeugnutzung und Turn-Anzahl werden bewusst nicht vorgegeben — sie sind Teil der Untersuchung.
|
||||
|
||||
> Dieser Prompt enthält ausschließlich die **Analyseanweisung** und ist damit unabhängig von einem
|
||||
> bestimmten Werkzeug oder Modell einsetzbar. Welche Werkzeuge im jeweiligen Lauf zur Verfügung
|
||||
> stehen und wohin die Ergebnisse geschrieben werden, stellt der Versuchsaufbau beim Start bei.
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
# Messprotokoll – V1b-Fable (builtin, `claude-fable-5`, Effort `high`) – Iteration 01, Lauf 24 (Lauf R)
|
||||
# Messprotokoll – V1b-Fable (builtin, `claude-fable-5`, Effort `high`) – Prompt-Version 01, Lauf 24 (Lauf R)
|
||||
|
||||
> ## ⚠ Bedingungsverletzung: Die Subagenten liefen auf einem anderen Modell
|
||||
>
|
||||
@@ -28,7 +28,7 @@
|
||||
|
||||
## Werkzeugkonfiguration
|
||||
- **Laufverzeichnis-ID:** `v3.7.0-15db`
|
||||
- **Ablage:** `claude-fable-5/builtin/high/`
|
||||
- **Ablage:** `Iteration 1/claude-fable-5/builtin/high/`
|
||||
- **Parallele Läufe:** nein
|
||||
- **Skill-Version:** `3.7.0` (erster Lauf mit Effort im Verzeichnisnamen)
|
||||
- **Claude-Code-Version:** 2.1.245
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
# Messprotokoll – V1-Fable (solo, `claude-fable-5`, Effort `max`) – Iteration 01, Lauf 22 (Lauf P)
|
||||
# Messprotokoll – V1-Fable (solo, `claude-fable-5`, Effort `max`) – Prompt-Version 01, Lauf 22 (Lauf P)
|
||||
|
||||
> **Neue Messreihe mit zwei geänderten Variablen.** Gegenüber allen 21 Vorläufen wechseln
|
||||
> **gleichzeitig Modell und Effort**: `claude-fable-5` statt Sonnet/Opus, `max` statt `high`.
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
## Werkzeugkonfiguration
|
||||
- **Laufverzeichnis-ID:** `v3.6.0-7adf`
|
||||
- **Ablage:** `claude-fable-5/solo/max/`
|
||||
- **Ablage:** `Iteration 1/claude-fable-5/solo/max/`
|
||||
- **Parallele Läufe:** ja – `fable5_solo_v3.6.0-58d2`
|
||||
- **Skill-Version:** `3.6.0`
|
||||
- **Claude-Code-Version:** 2.1.245
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
# Messprotokoll – V1-Fable (solo, `claude-fable-5`, Effort `max`) – Iteration 01, Lauf 23 (Lauf Q)
|
||||
# Messprotokoll – V1-Fable (solo, `claude-fable-5`, Effort `max`) – Prompt-Version 01, Lauf 23 (Lauf Q)
|
||||
|
||||
> **Neue Messreihe mit zwei geänderten Variablen.** Gegenüber allen 21 Vorläufen wechseln
|
||||
> **gleichzeitig Modell und Effort**: `claude-fable-5` statt Sonnet/Opus, `max` statt `high`.
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
## Werkzeugkonfiguration
|
||||
- **Laufverzeichnis-ID:** `v3.6.0-58d2`
|
||||
- **Ablage:** `claude-fable-5/solo/max/`
|
||||
- **Ablage:** `Iteration 1/claude-fable-5/solo/max/`
|
||||
- **Parallele Läufe:** ja – `fable5_solo_v3.6.0-7adf`
|
||||
- **Skill-Version:** `3.6.0`
|
||||
- **Claude-Code-Version:** 2.1.245
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
# Messprotokoll – V1-Opus (Baseline solo, `claude-opus-5`) – Iteration 01, Lauf 17 (Lauf K)
|
||||
# Messprotokoll – V1-Opus (Baseline solo, `claude-opus-5`) – Prompt-Version 01, Lauf 17 (Lauf K)
|
||||
|
||||
> **Neue Messreihe.** Erster Block mit `claude-opus-5`; alle 16 Vorläufe nutzten
|
||||
> `claude-sonnet-5`. Modus, Effort, Prompt, Snapshot und Parallelitätsgrad sind identisch zur
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
## Werkzeugkonfiguration
|
||||
- **Laufverzeichnis-ID:** `v3.6.0-2603`
|
||||
- **Ablage:** `claude-opus-5/solo/high/`
|
||||
- **Ablage:** `Iteration 1/claude-opus-5/solo/high/`
|
||||
- **Parallele Läufe:** ja – die vier übrigen Läufe dieses Blocks (`opus5_solo_v3.6.0-6bfe`, `opus5_solo_v3.6.0-5070`, `opus5_solo_v3.6.0-26d8`, `opus5_solo_v3.6.0-f63e`)
|
||||
- **Skill-Version:** `3.6.0`
|
||||
- **Claude-Code-Version:** 2.1.245
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
# Messprotokoll – V1-Opus (Baseline solo, `claude-opus-5`) – Iteration 01, Lauf 18 (Lauf L)
|
||||
# Messprotokoll – V1-Opus (Baseline solo, `claude-opus-5`) – Prompt-Version 01, Lauf 18 (Lauf L)
|
||||
|
||||
> **Neue Messreihe.** Erster Block mit `claude-opus-5`; alle 16 Vorläufe nutzten
|
||||
> `claude-sonnet-5`. Modus, Effort, Prompt, Snapshot und Parallelitätsgrad sind identisch zur
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
## Werkzeugkonfiguration
|
||||
- **Laufverzeichnis-ID:** `v3.6.0-6bfe`
|
||||
- **Ablage:** `claude-opus-5/solo/high/`
|
||||
- **Ablage:** `Iteration 1/claude-opus-5/solo/high/`
|
||||
- **Parallele Läufe:** ja – die vier übrigen Läufe dieses Blocks (`opus5_solo_v3.6.0-2603`, `opus5_solo_v3.6.0-5070`, `opus5_solo_v3.6.0-26d8`, `opus5_solo_v3.6.0-f63e`)
|
||||
- **Skill-Version:** `3.6.0`
|
||||
- **Claude-Code-Version:** 2.1.245
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user