Compare commits

..
22 Commits
Author SHA1 Message Date
Christoph SchwörerandClaude Opus 5 08d90f1ccb Effortvergleich high gegen max: Effort wirkt ueber Delegation
Dieselbe TensorX-Matrix ein zweites Mal bei hoechstem Effort. Zwoelf
gueltige Laeufe, 2.116 Anforderungen, 127,6 Mio. Tokens, hochgerechnet
$10,53 aus der Preisliste vom 02.09.2026.

Befund: In den nicht-delegierenden Zellen bewegt sich der Ertrag zwischen
minus 18 und plus 44 Prozent ohne erkennbare Richtung - in der
Groessenordnung der Streuung. Der eine deutliche Ausschlag ist GLM in
custom mit plus 122 Prozent, erreicht mit 78 statt 30 Subagenten. Der
hoehere Denkaufwand schlaegt sich in mehr Zerlegung nieder, und die traegt
den Ertrag, nicht der Denkaufwand als solcher.

Qwens custom-Zelle hat sich qualitativ erholt: bei high 61 Prozent ohne
Beleg und 40 Prozent Hypothesen, bei max 3 Prozent ohne Beleg und 96
Prozent Primaerbeleg. Der Einbruch war ein Laufmerkmal, kein
Modellmerkmal - ein weiterer Beleg, dass n gleich 1 je Zelle nicht traegt.

Skill 13.2.0: analyse-anforderungen.py toleriert jetzt vier
Markdown-Fassungen der Feldvorgabe. Jedes der vier eingesetzten Modelle
formatierte sie anders, und jede Fassung wurde zunaechst mit null
Anforderungen gezaehlt, obwohl Belege und Pruefideen vollstaendig
vorlagen. Das ist ein Befund ueber den Versuchsaufbau: Die Formatvorgabe
ist fuer Menschen eindeutig, fuer maschinelle Auswertung nicht.
Regressionsprobe an sieben Laeufen unveraendert.

_matrix.ps1 nimmt zusaetzlich -Effort und -Modi fuer einzelne Zellen.
Ein Lauf fiel durch Standby des Rechners aus und wurde wiederholt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 13:41:01 +02:00
Christoph SchwörerandClaude Opus 5 e2c3a0e8f8 TensorX-Matrix: erste vollstaendig besetzte Matrix, 918 Anforderungen
Sechs Zellen - z-ai/glm-5.3-flash und qwen/qwen3.8-flash-next je solo,
builtin und custom - alle mit dem kompletten Artefaktsatz von sieben
Dateien. 47,1 Mio. Tokens in 6,4 Stunden, hochgerechnet rund $3,25.

Das Matrixskript heisst jetzt _matrix.ps1 und nimmt -Provider und
-Effort; die LM-Studio-Ladeparameter werden nur noch lokal uebergeben.
Vor dem Start bestaetigte ein Smoke-Test den TensorX-Pfad unter den
seither geaenderten Bedingungen (Denylist, Spiegel, Freigabemuster):
Anmeldung, Modellkontrolle, wirksame Effort-Variante und Dateiuebernahme.

Befund: Die Anforderungsanzahl haette in die Irre gefuehrt. Qwens
custom-Lauf liegt mit 157 Anforderungen im Mittelfeld, ist aber
qualitativ zusammengebrochen - 61 Prozent ohne jeden Beleg, 17 Prozent
mit Primaerbeleg, 40 Prozent Hypothesen, gegenueber 0 Prozent ohne Beleg
und 79 bis 98 Prozent Primaerbelegen in den uebrigen fuenf Laeufen. Er
lieferte zugleich weniger als builtin bei 37 Prozent mehr Tokens.

Der Moduseffekt ist modellabhaengig: Bei GLM steigt der Ertrag monoton
von 126 ueber 139 auf 216 bei durchgaengig hoher Belegqualitaet, bei Qwen
ist builtin das Optimum. Die Annahme, rollenspezialisierte Agenten seien
generell ueberlegen, traegt damit nicht.

Qwens custom-Lauf meldet exit_code 1 bei finish_reason stop und ohne
Timeout, nachdem alle 24 Subagenten zurueckkamen und sieben Dateien
entstanden. Er ist als gueltig mit Vorbehalt gefuehrt, die Ursache offen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 16:21:31 +02:00
Christoph SchwörerandClaude Opus 5 6c5c26a2e4 Lokale Matrix liefert Ergebnisse: 119 Anforderungen aus Gemma und Qwen
Skill 13.0.0/13.1.0, Adapter 2.5.2.

Spiegel-Arbeitsverzeichnis: OpenCode laeuft nicht mehr direkt im
Codebasis-Root, sondern in einem Verzeichnis aus Junctions auf die
Top-Level-Eintraege, mit einem echten Ergebnisverzeichnis, dessen Inhalt
nach dem Lauf uebernommen wird. Damit landen relative wie absolute
Ausgabepfade am richtigen Ort, ohne dass der Prompt vom Wortlaut der
Claude-Laeufe abweichen muss. Der Spiegel liegt in _meta des Laufs; die
Codebasis bleibt unberuehrt.

Der Spiegel allein genuegte nicht. Sechs Laeufe schrieben mit korrektem
absolutem Pfad und wurden dennoch abgewiesen, weil OpenCode Ziele im
Repository gegen den worktree-relativen Pfad abgleicht - ein Befund, der
seit Skill 10.0.2 dokumentiert war und erst sichtbar wurde, als der
Spiegel vom Temp-Verzeichnis ins Projekt wanderte.

analyse-anforderungen.py erkennt Kennungen als Markdown-Ueberschrift, auch
ohne Feldnamen, sofern die Pflichtfelder folgen. Regressionsprobe an fuenf
Claude-Laeufen unveraendert.

Ergebnisse der gueltigen Laeufe: Qwen 3.5-9B liefert 107 der 119
Anforderungen, davon 75 im Modus custom mit nur drei Subagenten. Gemma
kommt auf 12. Der Modus builtin fiel bei beiden Modellen aus - drei von
drei Laeufen endeten nach einem Turn ohne einen Werkzeugaufruf. Die 13
Laeufe mit Adapter 2.3.0 bis 2.5.1 sind Artefakte der Fehlersuche und
als adapterbedingte Fehlmessungen gekennzeichnet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 22:49:53 +02:00
Christoph SchwörerandClaude Opus 5 28e927013b LM-Studio-Matrix mit gemma-4-e4b: zwoelf Laeufe und vier Befunde
Skill 12.0.0 bis 12.2.0 und Adapter 2.0.0 bis 2.2.0.

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

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

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

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 11:02:04 +02:00
Christoph Schwörer 654339464e neues modell 2026-09-01 08:12:07 +02:00
Christoph SchwörerandClaude Opus 5 611fd0a80c Lokaler LM-Studio-Adapter fuer Gemma und Qwen (Skill 10.1.0)
Der TensorX-Wrapper wird providerneutral: opencode-tensorx-adapter.py heisst
jetzt opencode-adapter.py und waehlt ueber --provider {tensorx,lmstudio}
Gateway und Modellvorlage. Der TensorX-Pfad bleibt unveraendert; die vier
bestehenden Regressionstests laufen durch.

Neu fuer den lokalen Betrieb:
- opencode-lmstudio.json fuer google/gemma-4-e4b und qwen/qwen3.8-27b
- Preflight ueber /api/v0/models: Servererreichbarkeit, Modellverfuegbarkeit,
  tool_use-Faehigkeit, geladenes Kontextfenster (--min-context, Standard 32768)
  und genau eine geladene Instanz; --lmstudio-autoload stellt das selbst her
- local_runtime in RawResult.json (Quantisierung, Architektur, Runtime,
  lms-Version, Instanzbezeichner, Kontextfenster) fuer Kap. 4.3
- effort_applied, da der lokale Endpunkt keinen Thinking-Level annimmt

Drei Befunde aus der Inbetriebnahme, alle im Adapter abgefangen: LM Studio
laedt standardmaessig nur 8192 Kontexttokens; ein erneutes lms load erzeugt
eine zweite Instanz und macht das Routing mehrdeutig; Effort ist lokal
wirkungslos. Dazu zwei Korrekturen am gemeinsamen Pfad (Abbruchgrund nur
einmal in errors, saubere lms-Versionskennung).

Enthaelt ausserdem die bislang nicht committeten Laeufe der Iterationen 8
und 9 sowie Versuch 2 (Iterationen 1 bis 3). Der laufende Lauf unter
Iteration 10 ist bewusst nicht enthalten.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 20:19:33 +02:00
Christoph Schwörer b369e6115e Add OpenCode TensorX experiment adapter 2026-08-31 18:42:25 +02:00
Christoph Schwörer ca52aa4701 Add TensorX models for Versuch 2 2026-08-31 16:51:35 +02:00
Christoph Schwörer 8a22d586f1 iteration 8 2026-08-28 19:41:13 +02:00
Christoph Schwörer 37275c96d6 tensorx adapter 2026-08-28 07:38:12 +02:00
Christoph Schwörer ea1f3caff7 more runs before cline 2026-08-27 19:22:37 +02:00
Christoph SchwörerandClaude Opus 5 3d5b691bfa Iteration 3: Modell- und Modusraster erweitert, Skill 7.0.0, V2/V3 vorbereitet
Neue gueltige Zellen in Iteration 3
- claude-opus-5/solo/high: 363 Anforderungen, 99,7 % mit Primaerbeleg,
  Belege je Anforderung Median 2,0, 38,1 Mio. Tokens
- claude-fable-5/solo/high: 241 Anforderungen, 98,3 % mit Primaerbeleg,
  89,0 % PRIMAER-Anteil, vollstaendig regelkonform, 30,6 Mio. Tokens

Damit sind 6 von 12 Zellen des Rasters belegt. Zwei Befunde daraus:

Die Belegdichte folgt dem Modell, nicht dem Effort. Opus erreicht Median 2,0
auch auf high; alle 44 Sonnet-Laeufe lagen bei 1,0. max hebt Opus auf 3,0.
Die frueher dem Effort zugeschriebene Verdopplung ist damit eingegrenzt.

Die Fable-Modellverletzung ist reproduziert und abgegrenzt. Bei builtin laufen
die Subagenten auf claude-opus-5[1m] statt Fable (zweiter Fall nach Iteration 1),
bei solo dagegen sauber. Nicht das Modell ist die Ursache, sondern Fable in
Kombination mit Delegation.

Fehlmessungen, vollstaendig protokolliert
- vier 429-Abbrueche (Session-Kontingent) aus dem Parallelblock 19:59;
  drei davon mit Teilbestand, einer ohne Ergebnis
- opus-5/builtin/high zum dritten Mal gescheitert: 790,7 Mio. Tokens ueber drei
  Anlaeufe ohne Artefakt. Zelle mit dieser Prompt-Version nicht messbar.

Skill 7.0.0 (MAJOR)
- Isolationsmechanismus modusabhaengig: --safe-mode schaltet MCP-Server und
  Custom-Agenten ab und ist mit V2/V3 unvereinbar. Smoke-Test verifiziert:
  mit Flag spawned=0, ohne Flag spawned=2. Ersatz fuer custom/MCP:
  --strict-mcp-config plus --disallowedTools Skill WebSearch WebFetch SlashCommand.
- 6.1.0: Pflichtpruefung leeres Ergebnisverzeichnis = Fehlmessung unabhaengig von
  is_error; CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS=0; Protokollfeld Gueltigkeit
- extract-subagenten.py: Start-Quittung wird nicht mehr als Ertragsmass ausgewiesen

Versuch 2 und 3 vorbereitet
- Prompt-Kette V1 -> V2 (02-A, angepasst an Agentendateien) -> V3 (02-B, MCP)
- V2: acht Rollen inkl. nicht delegierendem ISO-29148-Orchestrator
- V3: elf Rollen, fuenf Werkzeugserver, neue Belegklasse LAUFZEIT

Ablaufprotokoll um Phase 6 und 7 sowie die Vorbereitung von V2/V3 ergaenzt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 08:03:00 +02:00
Christoph Schwörer affde3a45f gpt skill ergünzt 2026-08-26 16:54:32 +02:00
Christoph Schwörer 844b4e5569 Lots of runs 2026-08-26 16:37:24 +02:00
Christoph SchwörerandClaude Opus 5 f349d189c7 Codebasis-Snapshot: DB-Schema als SQL-Dump ergaenzt
SSMS_DB_SCHEMA.sql (3.266.626 B, 76.793 Zeilen) - Schema-Dump der Datenbank
CentronVOED2: 1.558 Tabellen, 182 Views, 63 Prozeduren, 30 Funktionen,
134 Fremdschluessel.

Aenderung der Versuchsbedingung: Der Prompt fordert Datenbankschemata in
Schritt 2 (Artefakterhebung) ausdruecklich als Quelle. Laeufe mit und ohne
diese Datei untersuchen einen anderen Gegenstand und sind nicht poolbar.

Zeitpunkt im Arbeitsverzeichnis: 2026-08-26 10:28:08.
- Laeufe ohne die Datei: Tag 2, 02_Lauf_..._084301-d6f9 sowie 094249/094250
  (3983, f631, c69e); 4840 lief zum Zeitpunkt des Hinzufuegens noch.
- Laeufe mit der Datei: die fuenf 102932-Laeufe, abgelegt unter Tag 3.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 10:34:44 +02:00
Christoph Schwörer 7df384f6d2 Tag 1 abgeschlossen 2026-08-26 08:19:33 +02:00
Christoph Schwörer f045b99a25 Codebasis als Dateien ins Arbeitsrepo statt als Gitlink
QuellCode/CentronERP war nur als Gitlink (Submodul-Referenz auf 79c1142)
getrackt, ohne .gitmodules und ohne erreichbares Remote. Der
Untersuchungsgegenstand der Versuchsreihe war damit nicht reproduzierbar
gesichert: Ein Klon haette ein leeres Verzeichnis erhalten, und die Belege
der 3.287 Anforderungen waeren nicht ueberpruefbar gewesen.

Umstellung:
- Historie nach c:\DEV\CentronERP_git_snapshot_79c1142 ausgelagert
  (vollstaendig lesbar, enthaelt 79c1142 und Vorgaenger 89ccfd6)
- Gitlink aus dem Index entfernt
- Dateiinhalt aufgenommen: 24.557 Dateien, rund 333 MB

Die verschachtelte .gitignore der Codebasis gilt weiter, Build-Artefakte
bleiben ausgeschlossen. Details in Versuche/Versuch_01/_Codebasis-Nachweis.md
2026-08-26 07:43:51 +02:00
Christoph Schwörer 18edae75b6 V01 Erste Runs 2026-08-26 07:34:29 +02:00
ChristophSchwoerer 0ec86aed8c lit.bib fixed 2026-08-25 10:39:25 +02:00
Gowler 714dab3986 sync 2026-08-25 10:20:31 +02:00
Gowler 376972453b Merge branch 'master' of http://192.168.0.99:3011/Gowler/Masterarbeit 2026-08-25 10:18:41 +02:00
Gowler 8f204bc330 reorganize Kapitel 2026-08-25 10:18:38 +02:00
26961 changed files with 6923569 additions and 27330 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,305 @@
# -*- coding: utf-8 -*-
"""Wertet die erzeugten Anforderungen eines Laufs aus und schreibt einen Protokollabschnitt.
Aufruf: python analyse-anforderungen.py <laufverzeichnis> [--print]
Erzeugt `<laufverzeichnis>/_meta/anforderungen.md` (der fertige Abschnitt) und
`<laufverzeichnis>/_meta/anforderungen.json` (maschinenlesbar).
Geparst wird das im Prompt vorgegebene Blockformat (`ID:`, `Typ:`, `Belege:`, `Status:` ...).
"""
import io, json, os, re, sys, collections
EBENEN = ('StRS', 'SyRS', 'SwRS')
# Anforderungen, fuer die der Prompt mindestens einen PRIMAER-Beleg verlangt
RISIKO = re.compile(r'sicherheit|abrechnung|fakturier|berechtigung|recht|zugriff|authentifiz|'
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 = normalisiere(io.open(pfad, encoding='utf-8').read())
teile = re.split(r'(?m)^ID:', text)
for t in teile[1:]:
yield 'ID:' + t
def feld(block, name):
m = re.search(r'(?m)^%s:[ \t]*(.*)$' % re.escape(name), block)
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_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:
norm.append('PRIMÄR' if x.startswith('PRIM')
else 'SEKUNDÄR' if x.startswith('SEKUND') else 'KONTEXT')
status = feld(b, 'Status')
anf.append(dict(
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(),
tracelinks=feld(b, 'Tracelinks'),
konsolidierung=feld(b, 'Konsolidierung'),
pruefidee=feld(b, 'Prüfidee') or feld(b, 'Pruefidee'),
qm=feld(b, 'Qualitätsmerkmal'),
uebernahme=feld(b, 'Übernahmewürdigkeit') or feld(b, 'Uebernahmewuerdigkeit'),
))
return anf
def de(n):
return '{:,}'.format(int(n)).replace(',', '.')
def pct(a, b):
if not b:
return '–'
return ('%.1f' % (100.0 * a / b)).replace('.', ',') + ' %'
def abschnitt(anf):
n = len(anf)
if not n:
return '## Gefundene Anforderungen\n\nKeine Anforderungen im vorgegebenen Format gefunden.\n'
je_ebene = collections.Counter(a['ebene'] for a in anf)
typen = collections.Counter(a['typ'] for a in anf)
bel = collections.Counter()
for a in anf:
bel.update(a['belege'])
bel_ges = sum(bel.values())
ohne_beleg = [a for a in anf if not a['belege']]
hyp = [a for a in anf if a['hypothese']]
work = [a for a in anf if a['workaround']]
ohne_trace = [a for a in anf if not a['tracelinks'] or a['tracelinks'].lower() in ('-', '–', 'keine')]
ohne_pruef = [a for a in anf if not a['pruefidee']]
kons = [a for a in anf if a['konsolidierung'] and a['konsolidierung'].lower() != 'nein']
mit_qm = [a for a in anf if a['qm']]
# Uebernahmewuerdigkeit: erst ab Prompt-Fassung 2026-08-26; aeltere Laeufe kennen das Feld nicht
uebern = collections.Counter()
for a in anf:
v = a['uebernahme'].lower()
if not v:
continue
for schl in ('übernehmen', 'workaround', 'sonderfall', 'veraltet'):
if schl in v:
uebern[schl] += 1
break
else:
uebern['(sonstige Angabe)'] += 1
mit_uebern = sum(uebern.values())
risiko = [a for a in anf if RISIKO.search(a['typ'] + ' ' + a['titel'])]
risiko_ungedeckt = [a for a in risiko if 'PRIMÄR' not in a['belege'] and not a['hypothese']]
zahlen = sorted(len(a['belege']) for a in anf)
median = zahlen[n // 2] if n % 2 else (zahlen[n // 2 - 1] + zahlen[n // 2]) / 2.0
z = ['## Gefundene Anforderungen', '',
'Maschinell aus `Ergebnisse\\StRS.md`, `SyRS.md` und `SwRS.md` ausgewertet '
'(Blockformat des Prompts). Erzeugt von `analyse-anforderungen.py`.', '',
'Die Kenngrößen decken die **maschinell prüfbare** Hälfte des Evaluationsrahmens aus '
'Kapitel 4.3 ab: Belegqualität und Übernahmewürdigkeit gehören zur *Statement-Qualität*, '
'Verteilung und Konsolidierungskandidaten zur *Set-Qualität*, Tracelinks und '
'Belegklassifikation zur *Traceability-Qualität*. Die Expertenbewertung nach '
'Likert-Skala tritt daneben und wird hier nicht ersetzt.', '',
'### Verteilung über die Ebenen', '',
'| Ebene | Anzahl | Anteil |', '|---|---:|---:|']
for eb in EBENEN:
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)))
if len(typen) > 10:
rest = sum(c for _, c in typen.most_common()[10:])
z.append('| (%d weitere) | %d | %s |' % (len(typen) - 10, rest, pct(rest, n)))
z.append('')
z += ['### Belegqualität', '', '| Messgröße | Wert |', '|---|---:|',
'| Belege gesamt | %s |' % de(bel_ges),
'| davon `PRIMÄR` | %s (%s) |' % (de(bel['PRIMÄR']), pct(bel['PRIMÄR'], bel_ges)),
'| davon `SEKUNDÄR` | %s (%s) |' % (de(bel['SEKUNDÄR']), pct(bel['SEKUNDÄR'], bel_ges)),
'| davon `KONTEXT` | %s (%s) |' % (de(bel['KONTEXT']), pct(bel['KONTEXT'], bel_ges)),
'| Belege je Anforderung (Median) | %s |' % str(median).replace('.', ','),
'| Anforderungen mit mindestens einem `PRIMÄR`-Beleg | %d (%s) |'
% (sum(1 for a in anf if 'PRIMÄR' in a['belege']),
pct(sum(1 for a in anf if 'PRIMÄR' in a['belege']), n)),
'']
z += ['### Übernahmewürdigkeit', '']
if not mit_uebern:
z += ['**nicht erhoben** – das Feld `Übernahmewürdigkeit` wurde erst mit der '
'Prompt-Fassung vom 2026-08-26 eingeführt und liegt für diesen Lauf nicht vor. '
'Hinweise auf Workarounds stecken ersatzweise im Feld `Status`.', '']
else:
z += ['| Einstufung | Anzahl | Anteil |', '|---|---:|---:|']
for k in ('übernehmen', 'workaround', 'sonderfall', 'veraltet', '(sonstige Angabe)'):
if uebern.get(k):
z.append('| %s | %d | %s |' % (k, uebern[k], pct(uebern[k], n)))
if n - mit_uebern:
z.append('| **ohne Angabe** | %d | %s |' % (n - mit_uebern, pct(n - mit_uebern, n)))
z.append('')
z += ['### Status', '', '| Kategorie | Anzahl | Anteil |', '|---|---:|---:|',
'| belegt | %d | %s |' % (n - len(hyp), pct(n - len(hyp), n)),
'| als `HYPOTHESE` gekennzeichnet | %d | %s |' % (len(hyp), pct(len(hyp), n)),
'| als Workaround vermerkt | %d | %s |' % (len(work), pct(len(work), n)),
'| Konsolidierungskandidaten | %d | %s |' % (len(kons), pct(len(kons), n)),
'| mit ISO-25010-Qualitätsmerkmal | %d | %s |' % (len(mit_qm), pct(len(mit_qm), n)),
'']
z += ['### Regelkonformität (Prüfung gegen die Vorgaben des Prompts)', '',
'| Vorgabe | Ergebnis |', '|---|---|']
z.append('| **Belegpflicht** – jede Anforderung mindestens ein Artefaktbeleg | %s |'
% ('**erfüllt** (0 Anforderungen ohne Beleg)' if not ohne_beleg
else '**verletzt** – %d ohne Beleg: %s' % (len(ohne_beleg),
', '.join(a['id'] for a in ohne_beleg[:8]) + (' …' if len(ohne_beleg) > 8 else ''))))
z.append('| **Risikobasierte Priorisierung** – Sicherheit, Abrechnung, Berechtigungen brauchen '
'einen `PRIMÄR`-Beleg oder die Kennzeichnung `[HYPOTHESE]` | %s |'
% ('**erfüllt** (%d risikorelevante Anforderungen, alle gedeckt)' % len(risiko)
if not risiko_ungedeckt
else '**verletzt** – %d von %d ungedeckt: %s' % (len(risiko_ungedeckt), len(risiko),
', '.join(a['id'] for a in risiko_ungedeckt[:8]) + (' …' if len(risiko_ungedeckt) > 8 else ''))))
z.append('| **Verifizierbarkeit** – jede Anforderung mit Prüfidee oder Akzeptanzkriterium | %s |'
% ('**erfüllt**' if not ohne_pruef
else '**verletzt** – %d ohne Prüfidee' % len(ohne_pruef)))
z.append('| **Übernahmewürdigkeit** – Einstufung für die Migrationsperspektive | %s |'
% ('*nicht erhoben* (Feld erst ab Prompt-Fassung 2026-08-26)' if not mit_uebern
else ('**erfüllt** (alle %d Anforderungen eingestuft)' % n if mit_uebern == n
else '**verletzt** – %d von %d ohne Angabe' % (n - mit_uebern, n))))
z.append('| **Traceability** – Verknüpfung zwischen den Ebenen | %d von %d mit Tracelinks (%s) |'
% (n - len(ohne_trace), n, pct(n - len(ohne_trace), n)))
z.append('')
return '\n'.join(z)
def main():
lauf = sys.argv[1]
anf = analysiere(lauf)
meta = os.path.join(lauf, '_meta')
os.makedirs(meta, exist_ok=True)
io.open(os.path.join(meta, 'anforderungen.json'), 'w', encoding='utf-8').write(
json.dumps(anf, indent=1, ensure_ascii=False))
text = abschnitt(anf)
io.open(os.path.join(meta, 'anforderungen.md'), 'w', encoding='utf-8').write(text + '\n')
if '--print' in sys.argv:
print(text)
else:
print('%s: %d Anforderungen ausgewertet -> _meta/anforderungen.md'
% (os.path.basename(os.path.normpath(lauf))[8:], len(anf)))
if __name__ == '__main__':
main()
@@ -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
}
@@ -0,0 +1,125 @@
"""Extrahiert Subagenten-Aufrufe (Prompt, Typ, Ergebnislaenge) aus dem persistierten
Session-Transkript eines Headless-Laufs und legt sie im Laufverzeichnis unter _meta ab.
Aufruf: python subagenten.py <laufverzeichnis> [<transkript-wurzel>]
Die Session-ID wird aus <laufverzeichnis>/RawResult.json gelesen.
"""
import io, json, os, sys, glob
lauf = sys.argv[1]
wurzel = sys.argv[2] if len(sys.argv) > 2 else os.path.expandvars(r'%USERPROFILE%\.claude\projects')
roh = json.load(io.open(os.path.join(lauf, 'RawResult.json'), encoding='utf-8-sig'))
sid = roh.get('session_id')
stats = roh.get('subagent_stats') or {}
gesamt = stats.get('spawned', 0)
verschachtelt = stats.get('spawned_by_subagents', 0)
# Im Haupttranskript stehen nur die direkt vom Hauptagenten gestarteten Subagenten.
# Von Subagenten gestartete liegen in deren eigenen Transkripten.
erwartet = gesamt - verschachtelt
treffer = glob.glob(os.path.join(wurzel, '*', sid + '.jsonl'))
if not treffer:
print('KEIN TRANSKRIPT gefunden fuer Session', sid)
sys.exit(1)
pfad = treffer[0]
aufrufe, ergebnisse = [], {}
for zeile in io.open(pfad, encoding='utf-8'):
try:
o = json.loads(zeile)
except Exception:
continue
cont = (o.get('message') or {}).get('content')
if not isinstance(cont, list):
continue
for c in cont:
if not isinstance(c, dict):
continue
if c.get('type') == 'tool_use' and c.get('name') in ('Task', 'Agent'):
ein = c.get('input') or {}
aufrufe.append({
'id': c.get('id'),
'werkzeug': c.get('name'),
'subagent_type': ein.get('subagent_type'),
'description': ein.get('description'),
'run_in_background': ein.get('run_in_background'),
'model': ein.get('model'),
'prompt': ein.get('prompt') or '',
})
elif c.get('type') == 'tool_result':
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')] = 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:
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)
io.open(os.path.join(meta, 'subagenten.json'), 'w', encoding='utf-8').write(
json.dumps(aufrufe, indent=1, ensure_ascii=False))
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** 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 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(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 **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 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()
+4
View File
@@ -1 +1,5 @@
# Spiegel-Arbeitsverzeichnisse der OpenCode-Laeufe (Junctions, laufzeitlokal)
_meta/spiegel/
**/_meta/spiegel/
@@ -1,2 +0,0 @@
#heading(level: 1, numbering: "0")[Abstract]
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ergänze hier die Zusammenfassung der Arbeit.
+1 -1
View File
@@ -1,2 +1,2 @@
#heading(level: 1, numbering: none)[Abstract (ca. 1 Seite)]
#heading(level: 1, numbering: none)[Abstract]
Zusammenfassung der Arbeit.
+1 -1
View File
@@ -1,4 +1,4 @@
#heading(level: 1)[Einleitung (ca. 8 Seiten)]
#heading(level: 1)[Einleitung]
#heading(level: 2)[Ausgangssituation und Motivation]
In den vergangenen Jahren hat die digitale Transformation mittelständische Softwareanbieter gezwungen, ihre Produkte neu zu bewerten. Betroffen sind vor allem Systeme, die über lange Jahre ausschließlich in Windows-Umgebungen vertrieben wurden. Diese stoßen bei Cloud-, Web- und Mobile-Szenarien an technische sowie organisatorische Grenzen und häufen zunehmend technische Schulden an. Eine technologische Weiterentwicklung wird zusehends schwieriger, und an einer Neuentwicklung führt oft kein Weg vorbei. Dokumentierte Anforderungen oder eine systematische Code-Dokumentation sind allerdings selten. Der Großteil des Wissens steckt implizit im Code oder in den Köpfen der verbleibenden Entwickler.
+1 -1
View File
@@ -4,7 +4,7 @@
#hide(bibliography("../literatur.bib", style: "apa"))
]
#heading(level: 1)[Theoretische Grundlagen (ca. 12 Seiten)]
#heading(level: 1)[Theoretische Grundlagen]
Dieses Kapitel beschreibt die theoretischen Grundlagen, die für die Konzeption und Bewertung eines KI-gestützten Reverse Requirements Engineering in Legacy-Umgebungen benötigt werden. Zunächst werden zentrale Themen des Requirements Engineerings sowie die Idee des reverse Requirements Engineerings auf Basis bestehender Systeme eingeordnet. Anschließend werden Large Language Models und deren Einsatz im Software Engineering inklusive typischer Leistungsgrenzen und Absicherungsmechanismen beschrieben. Abschließend werden Grundlagen der Legacy-Modernisierung sowie etablierte Migrationsstrategien zusammengefasst, um den Kontext der Fallstudie und die Zielrichtung einzuordnen.
@@ -71,9 +71,9 @@ Methodisch lassen sich dabei grob zwei Analysestränge unterscheiden:
/ Statische Analyse: Ableitung von Struktur- und Datenflussinformationen aus Code und Artefakten ohne Ausführung (z. B. Abhängigkeiten, SQL-Statements, Aufrufketten). Statische Analyse skaliert gut, erkennt aber nicht zuverlässig Laufzeitbedingungen (z. B. Feature Flags, Konfigurationsvarianten).
/ Dynamische Analyse: Beobachtung von Laufzeitverhalten durch Logging, Tracing oder instrumentierte Tests (z. B. welche Regeln bei bestimmten Eingaben greifen). Dynamische Analyse ist näher am realen Verhalten, benötigt aber reproduzierbare Szenarien und Testdaten.
Reverse Requirements Engineering in einem Migrationsprojekt profitiert typischerweise von einer Kombination beider Stränge. Ohne dynamische Belege steigt das Risiko, dass nicht offensichtliche Bedingungen (z. B. kundenspezifische Schalter) übersehen werden; ohne statische Analyse bleibt die Abdeckung häufig zu gering.
Reverse Requirements Engineering in einem Migrationsprojekt profitiert typischerweise von einer Kombination beider Stränge. Ohne dynamische Belege steigt das Risiko, dass nicht offensichtliche Bedingungen wie kundenspezifische Schalter übersehen werden. Ohne statische Analyse bleibt die Abdeckung häufig zu gering.
_Da eine Dynamische Analyse durch ein LLM mit den gegebenen technischen Möglichkeiten derzeit unpraktikabel ist, fokussiert sich diese Arbeit auf die statische Analyse von Artefakten, ergänzt manuell erstellte Artefakte zur Laufzeit (z.B. Screenshots)._
_Eine vollständige dynamische Analyse durch ein LLM ist mit den gegebenen technischen Möglichkeiten derzeit nicht praktikabel. Diese Arbeit fokussiert sich daher auf die statische Analyse von Artefakten und ergänzt sie um manuell erstellte Laufzeit-Artefakte wie Screenshots. Mit einem MCP-Server zur GUI-Beobachtung ist darüber hinaus eine teilweise dynamische Analyse möglich. Dieser Ansatz wird im Versuchsaufbau optional vorgesehen._
#heading(level: 3)[Typische Methodenkette für Requirements-Rückgewinnung aus Code]
+1 -1
View File
@@ -4,7 +4,7 @@
#hide(bibliography("../literatur.bib", style: "apa"))
]
#heading(level: 1)[Fallstudie c-entron GmbH (ca. 6 Seiten)]
#heading(level: 1)[Fallstudie c-entron GmbH]
Dieses Kapitel beschreibt die Anwendung auf die sich diese Arbeit bezieht. Betrachtet wird die Windows-basierte ERP-Software der c-entron GmbH, die auf eine webbasierte Plattform überführt werden soll. Ziel ist es, die fachlichen und technischen Rahmenbedingungen so darzustellen, dass die Anforderungen an ein KI-gestütztes Reverse Requirements Engineering nachvollziehbar werden. Zunächst werden Unternehmenskontext und Legacy-Software eingeordnet, anschließend folgen Migrationsstrategie und spezifische Herausforderungen.
+4 -388
View File
@@ -1,400 +1,16 @@
#import "@preview/cetz:0.4.2"
#set text(lang: "de")
#let __is_thesis = context { query(<__thesis_document>).len() > 0 }
#if __is_thesis == false [
#set cite(style: "apa")
#hide(bibliography("../literatur.bib", style: "apa"))
]
#heading(level: 1)[Konzeption und methodisches Vorgehen (ca. 12 Seiten)]
#heading(level: 1)[Konzeption und methodisches Vorgehen]
Dieses Kapitel beschreibt die Methodik, mit der die in Kapitel 1 beschriebenen Ziele und Forschungsleitfragen beantwortet werden sollen. Ausgangspunkt ist das methodische Design. Aus diesem Design leiten sich alle weiteren methodischen Entscheidungen ab. Vorausgegangene Proof-of-Concept-Läufe haben einzelne Aspekte des Vorgehens informell erprobt und das hier dargestellte Vorgehen geprägt, sie sind aber nicht Gegenstand der Auswertung. Die eigentliche Untersuchung wird in den folgenden Abschnitten geplant und in den folgenden Kapiteln durchgeführt und bewertet.
#heading(level: 2)[Werkzeug-Grundlagen: Agentische CLIs, Agenten, MCP und lokale Inferenz]
#include "04_konzeption_methodisches_vorgehen/04_01_methodisches_design.typ"
Bevor der eigentliche Versuchsaufbau beschrieben wird, werden die zentralen Werkzeug-Begriffe geklärt, an denen die spätere Versuchsreihe variiert. Sie sind nicht nur Hilfsmittel der Durchführung, sondern definieren genau die Stellschrauben, die in den Versuchen V1 bis V3 systematisch hinzugenommen werden. Die folgende Darstellung beschränkt sich auf das für diese Arbeit relevante Maß und ersetzt keine vollständige Werkzeugdokumentation. Das Zusammenspiel der vier Bausteine ist in @abb_werkzeug_grundlagen skizziert.
#include "04_konzeption_methodisches_vorgehen/04_02_versuchsdesign.typ"
/ Agentische CLIs: Eine LLM CLI ist ein Kommandozeilen-Werkzeug, das einem LLM den direkten Zugriff auf die lokale Arbeitsumgebung erlaubt. Es kann nicht nur online Artefakte erzeugen, sondern auch lokale Dateien lesen und bearbeiten sowie Shell-Befehle ausführen oder lokal installierte Tools aufrufen. Innerhalb einer Ausführungsschleife entscheidet das LLM selbst, welches Werkzeug es auf dem lokalen Computer aufrufen möchte. Die CLI stellt dafür lediglich die Laufzeitumgebung und den Zugriff auf die lokalen Werkzeuge bereit. In dieser Arbeit kommen Claude Code , die Codex-CLI sowie die Qwen Code CLI zum Einsatz.
#include "04_konzeption_methodisches_vorgehen/04_03_evaluation_und_absicherung.typ"
/ Agenten und Agentendateien: LLM Agenten sind rollenspezialisierte Konfigurationen (Prompts), die innerhalb einer agentischen CLI als Sub-Prozesse aufgerufen werden. Sie werden typischerweise als Markdown-Dateien abgelegt und enthalten drei Bestandteile: einen Auftrag (was tut der Agent), eine eigene Systemanweisung (wie tut er es) und ein begrenztes Toolset (womit darf er es tun). Die aufrufende CLI startet einen solchen Agenten mit eigenem Kontextfenster, sodass dessen Zwischenergebnisse den Hauptkontext nicht überfluten.
/ Model Context Protocol (MCP): Das Model Context Protocol ist ein 2024 von Anthropic veröffentlichter offener Standard zur Anbindung externer Werkzeuge und Datenquellen an LLM-Clients @claudecode_mcp_2026. Ein MCP-Server kapselt konkrete Fähigkeiten, etwa Symbol-Navigation im Quellcode, Datenbankzugriff oder GUI-Manipulation, hinter einer einheitlichen Schnittstelle. Ein MCP-Client wie Claude Code entdeckt die angebotenen Werkzeuge zur Laufzeit und ruft sie kontrolliert auf, um ein deterministischer Ergebnis zu erhalten, anstatt den Aufruf direkt durch das LLM zu generieren. So stellt etwa ein MCP-Server für lesenden Datenbankzugriff eine fest definierte SQL-Schnittstelle bereit, über die das LLM Tabellenstrukturen und Datensätze reproduzierbar abruft, anstatt sie aus dem allgemeinen Modellwissen zu rekonstruieren.
/ Lokale Inferenz-Runtimes: Eine lokale Inferenz-Runtime ist eine Software, die LLMs auf eigener Hardware ausführt und ihre Funktion über einen OpenAI-kompatiblen HTTP-Endpunkt bereitstellt. In dieser Arbeit kommt LM Studio zum Einsatz, das Modelle wie Qwen 3.5 Coder lokal ausführen kann.
#figure(
cetz.canvas({
import cetz.draw: *
let draw-box(x, y, w, h, label) = {
rect(
(x - w/2, y - h/2),
(x + w/2, y + h/2),
stroke: black + 0.6pt, fill: luma(240),
)
content((x, y), text(size: 9pt, weight: "bold")[#label])
}
let draw-arrow(from, to) = {
line(from, to, mark: (end: ">"), stroke: black + 0.6pt)
}
let h-half = 0.4
let gap = 0.15
draw-box(0, 5, 2.6, 0.8, "Benutzer")
draw-box(0, 3.5, 4.0, 0.8, "Agentische CLI")
draw-box(5, 3.5, 4.0, 0.8, "LLM (Cloud / LM Studio)")
draw-box(-4, 1, 3.6, 0.8, "Agentendateien")
draw-box(4, 1, 3.6, 0.8, "MCP-Server")
draw-box(0, -1, 3.6, 0.8, "Codebasis")
draw-box(4, -1, 3.6, 0.8, "Datenbank")
draw-arrow((0, 5 - h-half - gap), (0, 3.5 + h-half + gap))
line(
(0 + 4.0/2 + gap, 3.5),
(5 - 4.0/2 - gap, 3.5),
mark: (start: ">", end: ">"),
stroke: black + 0.6pt,
)
draw-arrow((0, 3.5 - h-half - gap), (-4, 1 + h-half + gap))
draw-arrow((0, 3.5 - h-half - gap), (4, 1 + h-half + gap))
draw-arrow((0, 3.5 - h-half - gap), (0, -1 + h-half + gap))
draw-arrow((4, 1 - h-half - gap), (4, -1 + h-half + gap))
}),
caption: [Hierarchie der Werkzeuge. Der Anwender ruft die agentische CLI auf. Diese übergibt das initiale Prompt und das verfügbare Toolset an das LLM und erhält dessen Antworten samt Tool-Aufrufen zurück. Die CLI führt die vom LLM erbetenen Aufrufe aus. Die Codebasis ist direkt über die CLI zugänglich. Der Zugriff auf die Datenbank erfolgt ausschließlich über einen MCP-Server.],
) <abb_werkzeug_grundlagen>
#heading(level: 2)[Methodisches Design im Überblick]
Das Vorgehen ist entlang der vier Forschungsleitfragen aus Kapitel 1 strukturiert. Diese werden im Folgenden mit Frage 1 (Steuerung und Reproduzierbarkeit), Frage 2 (KI-Extraktion und Stakeholder-Input), Frage 3 (Qualitätsbewertung) und Frage 4 (Chancen, Grenzen und Risiken) bezeichnet. Aus jeder Leitfrage folgt unmittelbar eine Datenquelle und ein Auswertungsweg. Damit ist sichergestellt, dass die methodischen Bausteine nicht nachträglich auf die Fragen abgebildet werden, sondern aus ihnen hervorgehen.
#figure(
cetz.canvas({
import cetz.draw: *
let stages = (
(y: 4.0, label: "Codebasis", q: none),
(y: 2.0, label: "KI-Extraktion", q: (num: "Frage 1", text: "Welche Steuerungsmechanismen und Kontrollpunkte sind notwendig, um LLMs reproduzierbar einzusetzen?")),
(y: 0.0, label: "Strukturierung", q: (num: "Frage 2", text: "Welche Anforderungen lassen sich aus Code extrahieren, welche müssen über Interviews ergänzt werden?")),
(y: -2.0, label: "Validierung", q: (num: "Frage 3", text: "Wie beurteilen Fachexperten Vollständigkeit, Verständlichkeit und Nützlichkeit der KI-Ergebnisse?")),
(y: -4.0, label: "Bewertung", q: (num: "Frage 4", text: "Welche Effizienzgewinne, Limitierungen und Risiken sind realistisch und müssen adressiert werden?")),
)
let box-half-w = 1.5
let box-half-h = 0.4
let stage-x = -5.0
let arrow-gap = 0.1
for s in stages {
rect(
(stage-x - box-half-w, s.y - box-half-h),
(stage-x + box-half-w, s.y + box-half-h),
stroke: black + 0.6pt, fill: luma(240),
)
content((stage-x, s.y), text(size: 9pt, weight: "bold")[#s.label])
}
for i in range(stages.len() - 1) {
let s1 = stages.at(i)
let s2 = stages.at(i + 1)
line(
(stage-x, s1.y - box-half-h - arrow-gap),
(stage-x, s2.y + box-half-h + arrow-gap),
mark: (end: ">"),
stroke: black + 0.6pt,
)
}
let q-x = stage-x + box-half-w + 0.6
for s in stages {
if s.q != none {
content(
(q-x, s.y),
anchor: "west",
box(
width: 9cm,
text(size: 9pt)[*#s.q.num:* #s.q.text],
),
)
}
}
}),
caption: [Methodisches Design im Überblick. Die vertikale Sequenz zeigt den Ablauf von der Codebasis bis zur Bewertung. Pro Phase ist die zugeordnete Forschungsleitfrage angeben.],
) <abb_forschungsdesign>
Der untersuchte Prozess folgt einer durchgehenden Kette von der Codebasis bis zum Requirement. Auf der Codebasis setzt eine KI-gestützte Extraktion auf. Die Ergebnisse werden in eine konsistente Spezifikationsform überführt und durch Fachexperten validiert. Die abschließende Bewertung erfolgt mit Hilfe vordefinierter Qalitätskriterien.
Aus diesem Ablauf ergeben sich drei methodische Module, die in den folgenden Abschnitten ausgearbeitet werden. Erstens der *kontrollierte Tooling-Vergleich*. Es ist eine Versuchsreihe vorgesehen, die auf derselben Codebasis und mit demselben Grundprompt arbeitet und sich gezielt nur in einzelnen Werkzeugkomponenten unterscheidet. Die konkrete Anzahl und Zuschnitt der Versuche werden im Untersuchungsdesign festgelegt. Zweitens die *strukturierte Stakeholder-Validierung*. Jede extrahierte Anforderung soll durch Domänenexperten geprüft, anhand einer Likert-Skala bewertet und durch halbstrukturierte Interviews ergänzt werden. Drittens die *RE-Qualitätsbewertung*. Die Bewertungskriterien werden vor der Durchführung definiert, sodass eine nachträgliche Kriterienwahl ausgeschlossen ist. Die Bewertung schließt neben der klassischen RE-Qualität auch die Migrations- und Konsolidierungsperspektive ein, also die Frage, ob eine extrahierte Anforderung im Zielsystem in dieser Form erhalten bleiben oder mit anderen zusammengeführt werden sollte.
#heading(level: 2)[Der RRE-Prozess als Rahmen]
Das in dieser Arbeit untersuchte Vorgehen folgt der in Kapitel 2 hergeleiteten siebenstufigen Methodenkette für Reverse Requirements Engineering. Die Schritte bauen aufeinander auf und decken den Weg von der ersten Abgrenzung des Untersuchungsgegenstands bis zur Validierung der gewonnenen Anforderungen ab.
1. *Scope und Domänenabgrenzung:* Auswahl relevanter Module, Datenobjekte und Prozesse.
2. *Artefakterhebung:* Quellcode, Konfiguration, UI-Texte, Datenbankschemata, Schnittstellenbeschreibungen und Change-Historie.
3. *Technische Analyse:* Struktur- und Abhängigkeitsanalyse sowie Identifikation von Kernkomponenten, Regeln und Integrationspunkten.
4. *Semantische Interpretation:* Ableitung fachlicher Aussagen aus technischen Implementierungen.
5. *Formalisierung:* Überführung in klare, testbare Anforderungen mit Kontext, Vorbedingung und Ergebnis.
6. *Traceability-Anreicherung:* Verknüpfung jedes Requirements mit Artefaktbelegen.
7. *Validierung:* Review durch Fachexperten und Abgleich mit Laufzeitverhalten oder Tickets.
In dieser Arbeit werden lediglich Schritt 1 und Schritt 7 manuell durchgeführt. Die dazwischenliegenden Schritte 2 bis 6 sollen KI-gestützt automatisiert werden. Der Untersuchungsschwerpunkt liegt damit nicht nur auf der Anforderungsbeschreibung, sondern vor allem auch auf der zuverlässigen Erzeugung dieser Beschreibung durch ein LLM.
Damit das Vorgehen belastbar bleibt, sind in jedem Versuchsdurchlauf drei Eigenschaften sicherzustellen:
/ Belegpflicht: Jede extrahierte Anforderung muss auf ein konkretes Artefakt wie eine Datei, ein Modul, ein Datenobjekt oder einen UI-Text zurückführbar sein.
/ Explizite Hypothesenmarkierung: Aussagen, die nicht eindeutig aus Artefakten ableitbar sind, werden als Hypothesen markiert und gesondert validiert.
/ Human-in-the-loop: Die fachliche Validierung durch Domänenexperten ist nicht optional. Plausibel formulierte LLM-Ausgaben sind kein hinreichender Beweis für sachliche Korrektheit.
Mit der Festlegung der Schrittfolge, der Aufteilung zwischen Mensch und KI sowie den drei Pflicht-Eigenschaften ist der Bezugsrahmen geklärt, in dem die folgenden Abschnitte ihre Detailfragen verorten.
#heading(level: 2)[Auswahl des LLM]
Die Wahl des konkret eingesetzten LLM bestimmt maßgeblich, welche Steuerungsmechanismen praktisch umgesetzt werden können und wie reproduzierbar die Ergebnisse erzeugt werden. Aus diesem Grund wird die Werkzeugauswahl nicht implizit vorausgesetzt, sondern entlang der fünf Kriterien aus der Zielsetzung begründet. Diese Kriterien sind Kontextfenster, Codeverständnis, Steuerbarkeit, Kosten und Datenschutz.
Zur Auswahl stehen vier aktuell verfügbare Optionen. Anthropic Claude wird über die CLI-Variante Claude Code eingebunden, die agentisches Arbeiten und MCP-Integration nativ unterstützt. OpenAI bietet mit GPT-5 und der Codex-CLI eine vergleichbare agentische Schnittstelle. Als weitere Optionen kommen Qwen 3.5 Coder mit der Möglichkeit zur lokalen Ausführung ohne Cloud-Versand sowie DeepSeek R1 als cloudbasierte Alternative infrage.
#figure(
table(
columns: (1.4fr, 1fr, 1fr, 1fr, 1fr),
align: (left, left, left, left, left),
stroke: 0.4pt,
[*Kriterium*], [*Claude (Claude Code)*], [*GPT-5 (Codex)*], [*DeepSeek R1 (Cloud)*], [*Qwen 3.5 Coder (lokal)*],
[Kontextfenster], [bis 1 M Tokens], [bis 400 k Tokens], [bis 128 k Tokens], [bis 256 k Tokens],
[Codeverständnis], [hoch], [hoch], [hoch], [hoch],
[Steuerbarkeit (Agenten, MCP)], [nativ], [über Codex-CLI], [über API], [über Qwen Code CLI],
[Kosten], [API-Abrechnung], [API-Abrechnung], [API-Abrechnung], [Eigenbetrieb],
[Datenschutz], [Cloud-Versand], [Cloud-Versand], [Cloud-Versand], [On-Premise],
),
caption: [Vergleich der LLM-Optionen entlang der fünf Auswahlkriterien.],
) <tab_llm_vergleich>
Für diese Arbeit fällt die Entscheidung auf Claude Code als primäres Werkzeug. Ausschlaggebend sind das große Kontextfenster, die native Unterstützung von Agenten und MCP-Servern sowie eine offen dokumentierte CLI für reproduzierbare Aufrufe. Die kostenseitigen und datenschutzrechtlichen Nachteile gegenüber lokalen Modellen werden durch gezielte Konfigurationsmaßnahmen adressiert. Qwen 3.5 Coder und DeepSeek R1 werden für einen optionalen LLM-Querschnitt offengehalten, sind aber nicht das primäre Werkzeug. Qwen 3.5 Coder wird dabei lokal über LM Studio als Inferenz-Runtime betrieben. Die Anbindung erfolgt über den OpenAI-kompatiblen HTTP-Endpunkt von LM Studio, an den die Qwen Code CLI und gegebenenfalls MCP-Clients adressiert werden. Damit bleibt der Versuchsaufbau ohne Cloud-Versand und ohne Übertragung kundenbezogener Codeausschnitte an externe Anbieter.
#heading(level: 2)[Versuchsaufbau]
Der Versuchsaufbau folgt einer schrittweise aufbauenden Vergleichslogik. Versuch 1 bis 3 bilden den Kern der Reihe und werden ausschließlich auf Claude Code durchgeführt. Ausgehend von einer Baseline werden in jedem weiteren Versuch zusätzliche Werkzeugkomponenten hinzugefügt, sodass der Effekt jeder Komponente isoliert beobachtbar ist. Alle drei Versuche arbeiten auf derselben Codebasis und mit demselben Grundprompt. Variiert wird ausschließlich die Werkzeugkonfiguration. Der optionale Versuch 4 kehrt die Logik um. Die in Versuch 1 bis 3 wirksamste Konfiguration wird fixiert und auf den drei alternativen Modellen wiederholt, um modellabhängige von werkzeugabhängigen Effekten trennen zu können.
/ Versuch 1 (Baseline, Prompt-only): Reine Prompt-Steuerung ohne Agentendateien und ohne externe Tools. Die Hypothese lautet, dass eine formal strukturierte Anforderungsmenge bereits ohne Spezialisierung erreichbar ist, allerdings mit begrenzter Discovery-Breite und ohne dynamische Code- oder Datenbeobachtung.
/ Versuch 2 (Spezialisierung über Agenten): Wie Versuch 1, ergänzt um rollenspezialisierte Agentendateien für Stakeholder-Analyse, System-Requirements, Software-Requirements und einen ISO-29148-Orchestrator. Die Hypothese lautet, dass Spezialisierung die Strukturierungstiefe und die Normkonformität erhöht, ohne die Discovery-Breite signifikant zu verschlechtern.
/ Versuch 3 (Toolzugriff über MCP-Server): Wie Versuch 2, ergänzt um strukturierten Tool-Zugriff über MCP. Vorgesehen sind drei Server für Symbol-Navigation auf Code-Ebene, für Datenbank-Inspektion auf Schema- und Datensatzebene sowie optional für GUI-Beobachtung. Die Hypothese lautet, dass strukturierter Tool-Zugriff die Discovery-Breite vergrößert und zuvor undokumentierte Use Cases sichtbar macht, allerdings zu Lasten erhöhter Steuerungskomplexität.
/ Versuch 4 (optional, LLM-Querschnitt): Die in den ersten drei Versuchen wirksamste Konfiguration wird auf allen drei alternativen Modellen wiederholt: GPT-5 über die Codex-CLI, DeepSeek R1 über die Cloud-API sowie Qwen 3.5 Coder lokal über LM Studio. Wo einzelne Werkzeugkomponenten in der jeweiligen Umgebung nicht eins zu eins verfügbar sind, wird ein funktional äquivalenter Ersatz gewählt und dokumentiert. Ziel ist eine Einschätzung, in welchem Maße die in Versuch 1 bis 3 beobachteten Effekte modellabhängig oder werkzeugabhängig sind.
#figure(
table(
columns: (auto, 1fr, 1.4fr),
align: (left + top, left + top, left + top),
stroke: 0.4pt,
[*Versuch*], [*Werkzeugkonfiguration*], [*Hypothese*],
[V1 Baseline],
[
- Prompt-only
- keine Agentendateien
- keine externen Tools
],
[
- Formal strukturierte Spezifikation erreichbar
- Discovery-Breite begrenzt
],
[V2 Agenten],
[
- Wie V1
- rollenspezialisierte Agentendateien
],
[
- Höhere Strukturierungstiefe und Normkonformität
- Vergleichbare Discovery-Breite
],
[V3 MCP-Tools],
[
- Wie V2
- MCP-Server für Code
- MCP-Server für Datenbank
- optional MCP-Server für GUI
],
[
- Größere Discovery-Breite
- Höhere Steuerungskomplexität
],
[V4 (optional)],
[
- beste Konfiguration aus V1–V3
- GPT-5 über Codex-CLI
- DeepSeek R1 über Cloud-API
- Qwen 3.5 Coder lokal über LM Studio
],
[Trennung modell- gegenüber werkzeugabhängiger Effekte],
),
caption: [Übersicht der geplanten Versuche mit Werkzeugkonfiguration und Arbeitshypothese.],
) <tab_versuchsreihe>
#heading(level: 3)[Iteratives Vorgehen innerhalb eines Versuchs]
Innerhalb jedes Versuchs läuft ein iterativer Loop ab, in dem der Prompt schrittweise verfeinert wird. Der Begriff Iteration bezeichnet in dieser Arbeit ausschließlich diesen inneren Loop. Der schrittweise Aufbau über V1, V2 und V3 wird hingegen als Versuchsreihe bezeichnet.
Der Ablauf folgt fünf Schritten. Ausgangspunkt ist ein Initial-Prompt. In Versuch 1 wird dieser zu Versuchsbeginn neu formuliert. In Versuch 2 und Versuch 3 wird stattdessen der finale Prompt des vorhergehenden Versuchs übernommen und vor dem ersten Lauf an die neu hinzukommende Werkzeugkomponente angepasst (Agentendateien beziehungsweise MCP-Server). Der Initial-Prompt wird gegen die Codebasis ausgeführt. Anschließend begutachtet der Autor die Ausgabe stichprobenartig auf offensichtliche Lücken, formale Probleme oder fehlende Belege. Auf Basis dieser Begutachtung wird der Prompt mit dokumentiertem Änderungsgrund angepasst und erneut ausgeführt. Der Loop wiederholt sich, bis der Autor das Ergebnis als ausreichend einstuft.
Diese innere Begutachtung ersetzt nicht die Stakeholder-Validierung. Die Stakeholder-Validierung greift erst, wenn der Autor den Versuch als abgeschlossen erklärt. Ihr Urteil bezieht sich ausschließlich auf das End-Set des Versuchs. Damit fließt der Validatoren-Aufwand nicht in jede Zwischenversion ein.
Jede Prompt-Version wird im Versuchsordner mit Zeitstempel und kurzer Notiz zum Änderungsgrund abgelegt, sodass die Iterations-Historie reproduzierbar bleibt und später als Lerneffekt diskutiert werden kann. Der Ablauf ist in @abb_versuchsablauf zusammengefasst.
#figure(
cetz.canvas({
import cetz.draw: *
let draw-box(x, y, w, h, label, dashed: false) = {
rect(
(x - w/2, y - h/2),
(x + w/2, y + h/2),
stroke: if dashed { (dash: "dashed", thickness: 0.6pt, paint: black) } else { black + 0.6pt },
fill: luma(240),
)
content((x, y), text(size: 9pt, weight: "bold")[#label])
}
let draw-mini-box(x, y, w, h, label) = {
rect(
(x - w/2, y - h/2),
(x + w/2, y + h/2),
stroke: black + 0.4pt,
fill: luma(248),
)
content((x, y), text(size: 8pt)[#label])
}
let draw-arrow(from, to) = {
line(from, to, mark: (end: ">"), stroke: black + 0.6pt)
}
let h-half = 0.4
let mh-half = 0.25
let gap = 0.1
let top-y = 5
let versuche = (
(x: -5, label: "V1 Baseline"),
(x: -1.5, label: "V2 Agenten"),
(x: 2, label: "V3 MCP-Tools"),
)
for v in versuche {
draw-box(v.x, top-y, 3.0, 0.8, v.label)
}
draw-box(5.5, top-y, 3.0, 0.8, "V4 (optional)", dashed: true)
draw-arrow((-5 + 1.5, top-y), (-1.5 - 1.5, top-y))
draw-arrow((-1.5 + 1.5, top-y), ( 2 - 1.5, top-y))
line(
(2 + 1.5, top-y), (5.5 - 1.5, top-y),
mark: (end: ">"),
stroke: (dash: "dashed", thickness: 0.6pt, paint: black),
)
for v in versuche {
let cx = v.x
let py = 3.5
let ly = 2.5
let by = 1.5
draw-mini-box(cx, py, 1.7, 0.5, "Prompt")
draw-mini-box(cx, ly, 1.7, 0.5, "Lauf")
draw-mini-box(cx, by, 1.9, 0.5, "Begutachtung")
draw-arrow((cx, top-y - h-half - gap), (cx, py + mh-half + gap))
draw-arrow((cx, py - mh-half - gap), (cx, ly + mh-half + gap))
draw-arrow((cx, ly - mh-half - gap), (cx, by + mh-half + gap))
let lx = cx + 1.25
line((cx + 0.95, by), (lx, by), stroke: black + 0.6pt)
line((lx, by), (lx, py), stroke: black + 0.6pt)
line((lx, py), (cx + 0.85, py),
mark: (end: ">"), stroke: black + 0.6pt)
content((lx + 0.25, (py + by) / 2), text(size: 7pt, fill: luma(80))[nein])
}
}),
caption: [Zustandsgraph des Versuchsablaufs. Die Versuche bauen schrittweise aufeinander auf, V4 ist optional. Der finale Prompt eines Versuchs wird zu Beginn des Folgeversuchs an die neu hinzukommende Werkzeugkomponente angepasst und dient dann als Startprompt. Innerhalb jedes Versuchs verfeinert ein Iterations-Loop den Prompt: Lauf → Begutachtung durch den Autor → bei „nein" Prompt-Anpassung, bei „ja" Übergang zum nächsten Versuch.],
) <abb_versuchsablauf>
Konstanten und Variablen sind in jedem Versuch klar dokumentiert. In Versuch 1 bis 3 umfassen die Konstanten Codebasis, Iterations-Vorgehen, Modellfamilie, Validierungsstichprobe und Bewertungskriterien. Variabel sind die Werkzeugkonfiguration und der Startprompt jedes Versuchs. Versuch 1 startet mit einem zu Versuchsbeginn neu formulierten Prompt. Versuch 2 und Versuch 3 übernehmen jeweils den finalen Prompt des vorhergehenden Versuchs und passen ihn zu Beginn an die neu hinzukommende Werkzeugkomponente an (Agentendateien in Versuch 2, MCP-Server in Versuch 3). Erst dieser angepasste Prompt bildet den Ausgangspunkt für den Iterations-Loop des Folgeversuchs. In Versuch 4 wird die Logik umgekehrt. Die Werkzeugkonfiguration ist die Konstante, das Modell die Variable. Damit lassen sich Unterschiede in Versuch 1 bis 3 ursächlich der Werkzeugvariation und in Versuch 4 ursächlich der Modellwahl zuordnen.
#heading(level: 2)[Stakeholder-Validierung als Verifikationsverfahren]
Die Stakeholder-Validierung ist das zentrale Verifikationsverfahren dieser Arbeit. Sie ist nicht als nachgelagerter Schritt gedacht, sondern bildet das Maß, an dem die KI-Ergebnisse gemessen werden. Plausibel formulierte LLM-Ausgaben sind nicht hinreichend. Eine Anforderung gilt erst dann als belastbar, wenn sie durch einen Domänenexperten geprüft und bestätigt wurde.
Vorgesehen sind bis zu drei Validatoren mit jeweils mehrjähriger Erfahrung in der c-entron-Codebasis und in den fachlich abgedeckten Geschäftsprozessen. Da im Unternehmen nicht mehr für jeden Fachbereich ein dedizierter Experte verfügbar ist, kann eine vollständige modulweise Abdeckung durch bereichsspezifische Validatoren nicht garantiert werden. Die Validatorengruppe deckt die Codebasis stattdessen in Summe ab. Die Teilnehmer sind bereits identifiziert. Der Interview-Leitfaden ist in @anh_interview_validierung im Anhang dokumentiert.
Für die Validierung wird pro Versuchslauf eine Zufallsstichprobe aus den extrahierten Anforderungen gezogen und auf die bis zu drei Teilnehmer verteilt. Die Stichprobengröße wird vor Versuchsbeginn festgelegt und im Versuchsprotokoll dokumentiert. Eine bereichs- oder risikoklassenspezifische Stratifizierung ist nicht vorgesehen, da die personelle Verfügbarkeit der Fachexperten eine flächendeckende Modulabdeckung nicht zulässt.
Jede Anforderung wird entlang von sechs Dimensionen bewertet:
/ Sachliche Korrektheit: Beschreibt die Anforderung das tatsächliche Systemverhalten?
/ Vollständigkeit: Sind Akteur, Vorbedingung und Ergebnis ausreichend spezifiziert?
/ Verständlichkeit: Lässt sich die Anforderung ohne Rückfrage interpretieren?
/ Redundanzfreiheit: Ist die Anforderung von anderen klar abgegrenzt?
/ Übernahmewürdigkeit: Soll die Anforderung im Zielsystem in ihrer Funktion erhalten bleiben, oder ist sie ein historisch gewachsener Workaround, ein Sonderfall oder eine veraltete Logik, die im Zuge der Neuimplementierung entfallen sollte?
/ Konsolidierungsbedarf: Bestehen andere Anforderungen, die dieselbe fachliche Funktion abbilden und im Zielsystem zu einer gemeinsamen Anforderung zusammengeführt werden sollten? Ein typisches Beispiel sind die in der bestehenden c-entron-Codebasis getrennt geführten Datensätze für Drucker („Stammblätter") und sonstige Hardware („Assets"), die im Zielsystem zu einem konsolidierten Asset-Konzept zusammengeführt werden sollen.
Die Bewertung erfolgt auf einer fünfstufigen Likert-Skala mit definierten Ankern an den Polen, wobei 1 für „trifft nicht zu" und 5 für „trifft voll zu" steht.
Ergänzend zur itemweisen Bewertung werden mit den Validatoren halbstrukturierte Interviews geführt. Themen sind hierbei vor allem migrationsspezifische Risiken, der Konsolidierungsbedarf gegenüber der Zielarchitektur und die Nützlichkeit der KI-Ergebnisse im Vergleich zu einer hypothetischen manuellen Analyse.
Eine Requirement gilt im Sinne dieser Arbeit als belastbar, wenn drei Quellen sie stützen: ein KI generiertes Requirement, ein konkreter Artefakt-Beleg und eine Expertenbestätigung.
#heading(level: 2)[Evaluationsrahmen]
Der Evaluationsrahmen wird vor der Durchführung der Versuche definiert. Damit wird eine nachträgliche Anpassung der Kriterien an die Ergebnisse ausgeschlossen. Die Bewertung orientiert sich an den drei in @sec_re_qualitaet hergeleiteten Qualitätsdimensionen.
/ Statement-Qualität: Pro einzelner Anforderung wird gemessen, ob sie eindeutig formuliert, vollständig im Satzbau, frei von unbelegten Annahmen und mit Akzeptanzkriterium oder Prüfidee versehen ist. Die Messung erfolgt über die zuvor beschriebene Likert-Skala.
/ Set-Qualität: Pro Versuch wird die gesamte erzeugte Anforderungsmenge als ein Set bewertet. Gemessen wird, ob sie die relevanten Prozesse und Varianten vollständig abdeckt, in sich konsistent ist und keine Doubletten enthält. Im Vordergrund steht dabei die Vollständigkeit, weil ein fehlendes Requirement im Migrationskontext zu Funktionsverlust führen kann. Die Bewertung erfolgt qualitativ durch Expertenbewertung und ergänzend durch maschinelle Konsistenzprüfungen wie doppelte IDs oder fehlende Belege.
/ Traceability-Qualität: Pro Beleg-Verknüpfung wird gemessen, ob der Beleg reproduzierbar auffindbar ist, etwa über Dateipfad, Methode oder SQL-Query, und ob die Ableitung vom Beleg zur Anforderung nachvollziehbar bleibt.\ \
Ergänzend zur Qualitätsbewertung wird der Aufwand erhoben. Dazu werden während der KI-Läufe Indikatoren wie Tokenkosten un Bearbeitungsdauer protokolliert. Parallel wird grob abgeschätzt, wie viele Stunden ein erfahrener Analyst für dieselben Module ohne KI-Unterstützung gebraucht hätte. Beide Größen zusammen erlauben einen Vergleich der Größenordnung, nicht jedoch einen exakten Effizienzfaktor.
#heading(level: 2)[Reproduzierbarkeit und Risikomanagement]
Reproduzierbarkeit und Risikomanagement sind als querschnittliche Aspekte angelegt. Sie betreffen alle Versuchsdurchläufe gleichermaßen und werden hier zusammengefasst.
Alle steuerungsrelevanten Artefakte werden versioniert vorgehalten. Hierzu zählen die verwendeten Prompts in ihrer Textfassung inklusive aller Iterationsversionen mit Zeitstempel und Änderungsgrund, die Agentendateien mit ihren Rollenbeschreibungen, die MCP-Server-Konfigurationen sowie die Angaben zu Modellversion und Kontextfenstergröße. Für lokal betriebene Modelle werden zusätzlich die Inferenz-Runtime mit Version (LM Studio), die Quantisierungsstufe des verwendeten Modell-Builds sowie weitere Sampling-Parameter dokumentiert, da diese Stellgrößen die Reproduzierbarkeit der Ausgaben spürbar beeinflussen. Jeder Versuchsordner enthält die vollständige Konfiguration als Single Source. Wo möglich, werden deterministische Einstellungen gewählt.
Da die Codebasis kundenbezogene Strukturen enthält, werden datenschutzkritische Werkzeuge bewusst eingegrenzt. MCP-Server für Datenbank-Inspektion und Symbol-Navigation werden lokal betrieben. An externe LLM-Anbieter werden nur diejenigen Codeausschnitte gesendet, die für den jeweiligen Analyseschritt notwendig sind. Personenbezogene Daten oder vollständige Datenexporte sind ausgeschlossen.
Die folgenden vier Risikokategorien werden adressiert:
/ Halluzinationen: Begegnet durch Belegpflicht und Stakeholder-Validierung. Jede Anforderung ohne nachvollziehbaren Beleg wird als Hypothese markiert.
/ Reproduzierbarkeitsverlust: Begegnet durch versionierte Prompts und deterministische Einstellungen, soweit das Modell sie unterstützt. Da die innere Iteration zudem die Gefahr eines Autoren-Bias birgt, etwa eines unbewussten Hinsteuerns auf bestimmte Module, werden die Prompt-Änderungen pro Iteration mit Änderungsgrund dokumentiert und sind im Versuchsordner nachvollziehbar.
/ Domänen- und Datenbias: Begegnet durch eine Stichprobenwahl, die alle relevanten Module abdeckt und nicht nur die in der KI-Ausgabe häufig auftauchenden.
/ Datenschutzverletzungen: Begegnet durch On-Premise-MCP, kontrollierten Versand und Logging der externen Aufrufe.
#heading(level: 2)[Konkrete Konfigurationen der geplanten Versuche]
Dieser Abschnitt konkretisiert die zuvor beschriebene Versuchsreihe auf Konfigurationsebene. Jeder Versuch ist durch seinen Prompt, seine Agentenliste und seine MCP-Server-Liste vollständig beschrieben. Modellversion, Kontextfenster und Temperatur werden im Versuchsordner protokolliert.
#figure(
table(
columns: (auto, 1fr, 1fr, 1fr),
align: (left, left, left, left),
stroke: 0.4pt,
[*Element*], [*V1 Baseline*], [*V2 Agenten*], [*V3 MCP-Tools*],
[Modell], [Claude (Claude Code)], [Claude (Claude Code)], [Claude (Claude Code)],
[Grundprompt], [Standard-Extraktionsprompt], [Standard-Extraktionsprompt], [Standard-Extraktionsprompt],
[Agentendateien], [keine], [Stakeholder, System, Software, ISO-29148-Orchestrator], [Wie V2, ergänzt um codebasis-spezifische Reviewer],
[MCP-Server], [keine], [keine], [Symbol-Navigation, Datenbank-Inspektion, optional GUI-Interaktion],
[Validierungsstichprobe], [Stratifiziert], [Stratifiziert], [Stratifiziert],
),
caption: [Detail-Konfiguration der drei Kernversuche.],
) <tab_versuchskonfiguration>
Die Versuchsordner-Struktur folgt einer einheitlichen Konvention. Pro Versuch existiert ein Unterordner mit den Konfigurationsartefakten, ein Eingangsprotokoll mit Modell- und Werkzeugangaben, ein Ergebnis-Unterordner sowie eine Verlaufsdokumentation für die Validierungsschritte. Damit ist jeder Versuch eigenständig reproduzierbar.
/*
#heading(level: 2)[Überleitung]
Mit der vorangegangenen Methodikbeschreibung ist das Untersuchungsdesign vollständig dokumentiert. Das folgende Kapitel beschreibt die Durchführung der geplanten Versuche und stellt die erzeugten Ergebnisartefakte vor. Daran schließt sich die Anwendung des hier definierten Evaluationsrahmens auf die Ergebnisse an. Den Abschluss bildet die Diskussion der gewonnenen Erkenntnisse im Hinblick auf die vier Forschungsleitfragen.
*/
@@ -0,0 +1,142 @@
#import "@preview/cetz:0.4.2"
#set text(lang: "de")
#heading(level: 2)[Werkzeug-Grundlagen: Agentische CLIs, Agenten, MCP und lokale Inferenz]
Bevor der eigentliche Versuchsaufbau beschrieben wird, werden die zentralen Werkzeug-Begriffe geklärt, an denen die spätere Versuchsreihe variiert. Sie sind nicht nur Hilfsmittel der Durchführung, sondern definieren genau die Stellschrauben, die in den Versuchen V1 bis V3 systematisch hinzugenommen werden. Die folgende Darstellung beschränkt sich auf das für diese Arbeit relevante Maß und ersetzt keine vollständige Werkzeugdokumentation. Das Zusammenspiel der vier Bausteine ist in @abb_werkzeug_grundlagen skizziert.
/ Agentische CLIs: Eine LLM CLI ist ein Kommandozeilen-Werkzeug, das einem LLM den direkten Zugriff auf die lokale Arbeitsumgebung erlaubt. Es kann nicht nur online Artefakte erzeugen, sondern auch lokale Dateien lesen und bearbeiten sowie Shell-Befehle ausführen oder lokal installierte Tools aufrufen. Innerhalb einer Ausführungsschleife entscheidet das LLM selbst, welches Werkzeug es auf dem lokalen Computer aufrufen möchte. Die CLI stellt dafür lediglich die Laufzeitumgebung und den Zugriff auf die lokalen Werkzeuge bereit. In dieser Arbeit kommen Claude Code , die Codex-CLI sowie die Qwen Code CLI zum Einsatz.
/ Agenten und Agentendateien: LLM Agenten sind rollenspezialisierte Konfigurationen (Prompts), die innerhalb einer agentischen CLI als Sub-Prozesse aufgerufen werden. Sie werden typischerweise als Markdown-Dateien abgelegt und enthalten drei Bestandteile: einen Auftrag (was tut der Agent), eine eigene Systemanweisung (wie tut er es) und ein begrenztes Toolset (womit darf er es tun). Die aufrufende CLI startet einen solchen Agenten mit eigenem Kontextfenster, sodass dessen Zwischenergebnisse den Hauptkontext nicht überfluten.
/ Model Context Protocol (MCP): Das Model Context Protocol ist ein 2024 von Anthropic veröffentlichter offener Standard zur Anbindung externer Werkzeuge und Datenquellen an LLM-Clients @claudecode_mcp_2026. Ein MCP-Server kapselt konkrete Fähigkeiten, etwa Symbol-Navigation im Quellcode, Datenbankzugriff oder GUI-Manipulation, hinter einer einheitlichen Schnittstelle. Ein MCP-Client wie Claude Code entdeckt die angebotenen Werkzeuge zur Laufzeit und ruft sie kontrolliert auf, um ein deterministischer Ergebnis zu erhalten, anstatt den Aufruf direkt durch das LLM zu generieren. So stellt etwa ein MCP-Server für lesenden Datenbankzugriff eine fest definierte SQL-Schnittstelle bereit, über die das LLM Tabellenstrukturen und Datensätze reproduzierbar abruft, anstatt sie aus dem allgemeinen Modellwissen zu rekonstruieren.
/ Lokale Inferenz-Runtimes: Eine lokale Inferenz-Runtime ist eine Software, die LLMs auf eigener Hardware ausführt und ihre Funktion über einen OpenAI-kompatiblen HTTP-Endpunkt bereitstellt. In dieser Arbeit kommt LM Studio zum Einsatz, das Modelle wie Qwen 3.5 Coder lokal ausführen kann.
#figure(
cetz.canvas({
import cetz.draw: *
let draw-box(x, y, w, h, label) = {
rect(
(x - w/2, y - h/2),
(x + w/2, y + h/2),
stroke: black + 0.6pt, fill: luma(240),
)
content((x, y), text(size: 9pt, weight: "bold")[#label])
}
let draw-arrow(from, to) = {
line(from, to, mark: (end: ">"), stroke: black + 0.6pt)
}
let h-half = 0.4
let gap = 0.15
draw-box(0, 5, 2.6, 0.8, "Benutzer")
draw-box(0, 3.5, 4.0, 0.8, "Agentische CLI")
draw-box(5, 3.5, 4.0, 0.8, "LLM (Cloud / LM Studio)")
draw-box(-4, 1, 3.6, 0.8, "Agentendateien")
draw-box(4, 1, 3.6, 0.8, "MCP-Server")
draw-box(0, -1, 3.6, 0.8, "Codebasis")
draw-box(4, -1, 3.6, 0.8, "Datenbank")
draw-arrow((0, 5 - h-half - gap), (0, 3.5 + h-half + gap))
line(
(0 + 4.0/2 + gap, 3.5),
(5 - 4.0/2 - gap, 3.5),
mark: (start: ">", end: ">"),
stroke: black + 0.6pt,
)
draw-arrow((0, 3.5 - h-half - gap), (-4, 1 + h-half + gap))
draw-arrow((0, 3.5 - h-half - gap), (4, 1 + h-half + gap))
draw-arrow((0, 3.5 - h-half - gap), (0, -1 + h-half + gap))
draw-arrow((4, 1 - h-half - gap), (4, -1 + h-half + gap))
}),
caption: [Hierarchie der Werkzeuge. Der Anwender ruft die agentische CLI auf. Diese übergibt das initiale Prompt und das verfügbare Toolset an das LLM und erhält dessen Antworten samt Tool-Aufrufen zurück. Die CLI führt die vom LLM erbetenen Aufrufe aus. Die Codebasis ist direkt über die CLI zugänglich. Der Zugriff auf die Datenbank erfolgt ausschließlich über einen MCP-Server.],
) <abb_werkzeug_grundlagen>
#heading(level: 2)[Methodisches Design im Überblick]
Das Vorgehen ist entlang der vier Forschungsleitfragen aus Kapitel 1 strukturiert. Diese werden im Folgenden mit Frage 1 (Steuerung und Reproduzierbarkeit), Frage 2 (KI-Extraktion und Stakeholder-Input), Frage 3 (Qualitätsbewertung) und Frage 4 (Chancen, Grenzen und Risiken) bezeichnet. Aus jeder Leitfrage folgt unmittelbar eine Datenquelle und ein Auswertungsweg. Damit ist sichergestellt, dass die methodischen Bausteine nicht nachträglich auf die Fragen abgebildet werden, sondern aus ihnen hervorgehen.
#figure(
cetz.canvas({
import cetz.draw: *
let stages = (
(y: 4.0, label: "Codebasis", q: none),
(y: 2.0, label: "KI-Extraktion", q: (num: "Frage 1", text: "Welche Steuerungsmechanismen und Kontrollpunkte sind notwendig, um LLMs reproduzierbar einzusetzen?")),
(y: 0.0, label: "Strukturierung", q: (num: "Frage 2", text: "Welche Anforderungen lassen sich aus Code extrahieren, welche müssen über Interviews ergänzt werden?")),
(y: -2.0, label: "Validierung", q: (num: "Frage 3", text: "Wie beurteilen Fachexperten Vollständigkeit, Verständlichkeit und Nützlichkeit der KI-Ergebnisse?")),
(y: -4.0, label: "Bewertung", q: (num: "Frage 4", text: "Welche Effizienzgewinne, Limitierungen und Risiken sind realistisch und müssen adressiert werden?")),
)
let box-half-w = 1.5
let box-half-h = 0.4
let stage-x = -5.0
let arrow-gap = 0.1
for s in stages {
rect(
(stage-x - box-half-w, s.y - box-half-h),
(stage-x + box-half-w, s.y + box-half-h),
stroke: black + 0.6pt, fill: luma(240),
)
content((stage-x, s.y), text(size: 9pt, weight: "bold")[#s.label])
}
for i in range(stages.len() - 1) {
let s1 = stages.at(i)
let s2 = stages.at(i + 1)
line(
(stage-x, s1.y - box-half-h - arrow-gap),
(stage-x, s2.y + box-half-h + arrow-gap),
mark: (end: ">"),
stroke: black + 0.6pt,
)
}
let q-x = stage-x + box-half-w + 0.6
for s in stages {
if s.q != none {
content(
(q-x, s.y),
anchor: "west",
box(
width: 9cm,
text(size: 9pt)[*#s.q.num:* #s.q.text],
),
)
}
}
}),
caption: [Methodisches Design im Überblick. Die vertikale Sequenz zeigt den Ablauf von der Codebasis bis zur Bewertung. Pro Phase ist die zugeordnete Forschungsleitfrage angeben.],
) <abb_forschungsdesign>
Der untersuchte Prozess folgt einer durchgehenden Kette von der Codebasis bis zum Requirement. Auf der Codebasis setzt eine KI-gestützte Extraktion auf. Die Ergebnisse werden in eine konsistente Spezifikationsform überführt und durch Fachexperten validiert. Die abschließende Bewertung erfolgt mit Hilfe vordefinierter Qalitätskriterien.
Aus diesem Ablauf ergeben sich drei methodische Module, die in den folgenden Abschnitten ausgearbeitet werden. Erstens der *kontrollierte Tooling-Vergleich*. Es ist eine Versuchsreihe vorgesehen, die auf derselben Codebasis und mit demselben Grundprompt arbeitet und sich gezielt nur in einzelnen Werkzeugkomponenten unterscheidet. Die konkrete Anzahl und Zuschnitt der Versuche werden im Untersuchungsdesign festgelegt. Zweitens die *strukturierte Stakeholder-Validierung*. Jede extrahierte Anforderung soll durch Domänenexperten geprüft, anhand einer Likert-Skala bewertet und durch halbstrukturierte Interviews ergänzt werden. Drittens die *RE-Qualitätsbewertung*. Die Bewertungskriterien werden vor der Durchführung definiert, sodass eine nachträgliche Kriterienwahl ausgeschlossen ist. Die Bewertung schließt neben der klassischen RE-Qualität auch die Migrations- und Konsolidierungsperspektive ein, also die Frage, ob eine extrahierte Anforderung im Zielsystem in dieser Form erhalten bleiben oder mit anderen zusammengeführt werden sollte.
#heading(level: 2)[Der RRE-Prozess als Rahmen]
Das in dieser Arbeit untersuchte Vorgehen folgt der in Kapitel 2 hergeleiteten siebenstufigen Methodenkette für Reverse Requirements Engineering. Die Schritte bauen aufeinander auf und decken den Weg von der ersten Abgrenzung des Untersuchungsgegenstands bis zur Validierung der gewonnenen Anforderungen ab.
1. *Scope und Domänenabgrenzung:* Auswahl relevanter Module, Datenobjekte und Prozesse.
2. *Artefakterhebung:* Quellcode, Konfiguration, UI-Texte, Datenbankschemata, Schnittstellenbeschreibungen und Change-Historie.
3. *Technische Analyse:* Struktur- und Abhängigkeitsanalyse sowie Identifikation von Kernkomponenten, Regeln und Integrationspunkten.
4. *Semantische Interpretation:* Ableitung fachlicher Aussagen aus technischen Implementierungen.
5. *Formalisierung:* Überführung in klare, testbare Anforderungen mit Kontext, Vorbedingung und Ergebnis.
6. *Traceability-Anreicherung:* Verknüpfung jedes Requirements mit Artefaktbelegen.
7. *Validierung:* Review durch Fachexperten und Abgleich mit Laufzeitverhalten oder Tickets.
In dieser Arbeit werden lediglich Schritt 1 und Schritt 7 manuell durchgeführt. Die dazwischenliegenden Schritte 2 bis 6 sollen KI-gestützt automatisiert werden. Der Untersuchungsschwerpunkt liegt damit nicht nur auf der Anforderungsbeschreibung, sondern vor allem auch auf der zuverlässigen Erzeugung dieser Beschreibung durch ein LLM.
Damit das Vorgehen belastbar bleibt, sind in jedem Versuchsdurchlauf drei Eigenschaften sicherzustellen:
/ Belegpflicht: Jede extrahierte Anforderung muss auf ein konkretes Artefakt wie eine Datei, ein Modul, ein Datenobjekt oder einen UI-Text zurückführbar sein.
/ Explizite Hypothesenmarkierung: Aussagen, die nicht eindeutig aus Artefakten ableitbar sind, werden als Hypothesen markiert und gesondert validiert.
/ Human-in-the-loop: Die fachliche Validierung durch Domänenexperten ist nicht optional. Plausibel formulierte LLM-Ausgaben sind kein hinreichender Beweis für sachliche Korrektheit.
Mit der Festlegung der Schrittfolge, der Aufteilung zwischen Mensch und KI sowie den drei Pflicht-Eigenschaften ist der Bezugsrahmen geklärt, in dem die folgenden Abschnitte ihre Detailfragen verorten.
@@ -0,0 +1,189 @@
#import "@preview/cetz:0.4.2"
#set text(lang: "de")
#heading(level: 2)[Auswahl des LLM]
Die Wahl des konkret eingesetzten LLM bestimmt maßgeblich, welche Steuerungsmechanismen praktisch umgesetzt werden können und wie reproduzierbar die Ergebnisse erzeugt werden. Aus diesem Grund wird die Werkzeugauswahl nicht implizit vorausgesetzt, sondern entlang der fünf Kriterien aus der Zielsetzung begründet. Diese Kriterien sind Kontextfenster, Codeverständnis, Steuerbarkeit, Kosten und Datenschutz.
Zur Auswahl stehen vier aktuell verfügbare Optionen. Anthropic Claude wird über die CLI-Variante Claude Code eingebunden, die agentisches Arbeiten und MCP-Integration nativ unterstützt. OpenAI bietet mit GPT-5 und der Codex-CLI eine vergleichbare agentische Schnittstelle. Als weitere Optionen kommen Qwen 3.5 Coder mit der Möglichkeit zur lokalen Ausführung ohne Cloud-Versand sowie DeepSeek R1 als cloudbasierte Alternative infrage.
#figure(
table(
columns: (1.4fr, 1fr, 1fr, 1fr, 1fr),
align: (left, left, left, left, left),
stroke: 0.4pt,
[*Kriterium*], [*Claude (Claude Code)*], [*GPT-5 (Codex)*], [*DeepSeek R1 (Cloud)*], [*Qwen 3.5 Coder (lokal)*],
[Kontextfenster], [bis 1 M Tokens], [bis 400 k Tokens], [bis 128 k Tokens], [bis 256 k Tokens],
[Codeverständnis], [hoch], [hoch], [hoch], [hoch],
[Steuerbarkeit (Agenten, MCP)], [nativ], [über Codex-CLI], [über API], [über Qwen Code CLI],
[Kosten], [API-Abrechnung], [API-Abrechnung], [API-Abrechnung], [Eigenbetrieb],
[Datenschutz], [Cloud-Versand], [Cloud-Versand], [Cloud-Versand], [On-Premise],
),
caption: [Vergleich der LLM-Optionen entlang der fünf Auswahlkriterien.],
) <tab_llm_vergleich>
Für diese Arbeit fällt die Entscheidung auf Claude Code als primäres Werkzeug. Ausschlaggebend sind das große Kontextfenster, die native Unterstützung von Agenten und MCP-Servern sowie eine offen dokumentierte CLI für reproduzierbare Aufrufe. Die kostenseitigen und datenschutzrechtlichen Nachteile gegenüber lokalen Modellen werden durch gezielte Konfigurationsmaßnahmen adressiert. Qwen 3.5 Coder und DeepSeek R1 werden für einen optionalen LLM-Querschnitt offengehalten, sind aber nicht das primäre Werkzeug. Qwen 3.5 Coder wird dabei lokal über LM Studio als Inferenz-Runtime betrieben. Die Anbindung erfolgt über den OpenAI-kompatiblen HTTP-Endpunkt von LM Studio, an den die Qwen Code CLI und gegebenenfalls MCP-Clients adressiert werden. Damit bleibt der Versuchsaufbau ohne Cloud-Versand und ohne Übertragung kundenbezogener Codeausschnitte an externe Anbieter.
#heading(level: 2)[Versuchsaufbau]
Der Versuchsaufbau folgt einer schrittweise aufbauenden Vergleichslogik. Versuch 1 bis 3 bilden den Kern der Reihe und werden ausschließlich auf Claude Code durchgeführt. Ausgehend von einer Baseline werden in jedem weiteren Versuch zusätzliche Werkzeugkomponenten hinzugefügt, sodass der Effekt jeder Komponente isoliert beobachtbar ist. Alle drei Versuche arbeiten auf derselben Codebasis und mit demselben Grundprompt. Variiert wird ausschließlich die Werkzeugkonfiguration. Der optionale Versuch 4 kehrt die Logik um. Die in Versuch 1 bis 3 wirksamste Konfiguration wird fixiert und auf den drei alternativen Modellen wiederholt, um modellabhängige von werkzeugabhängigen Effekten trennen zu können.
/ Versuch 1 (Baseline, Prompt-only): Reine Prompt-Steuerung ohne Agentendateien und ohne externe Tools. Die Hypothese lautet, dass eine formal strukturierte Anforderungsmenge bereits ohne Spezialisierung erreichbar ist, allerdings mit begrenzter Discovery-Breite und ohne dynamische Code- oder Datenbeobachtung.
/ Versuch 1b (Baseline mit werkzeugeigenen Agenten): Wie Versuch 1, jedoch mit den Subagenten, die das eingesetzte Werkzeug von sich aus mitbringt. Diese Subagenten sind keine Agentendateien im Sinne von Versuch 2, sondern eine Funktion der CLI. Der Versuch trennt damit den Effekt werkzeugeigener Delegation vom Effekt rollenspezialisierter Agentendateien. Die Hypothese lautet, dass die Delegation die Discovery-Breite erhöht, zugleich aber die Streuung zwischen Läufen deutlich vergrößert, weil das Werkzeug die Zerlegung der Analyse selbst wählt.
/ Versuch 2 (Spezialisierung über Agenten): Wie Versuch 1, ergänzt um rollenspezialisierte Agentendateien für Stakeholder-Analyse, System-Requirements, Software-Requirements und einen ISO-29148-Orchestrator. Die Hypothese lautet, dass Spezialisierung die Strukturierungstiefe und die Normkonformität erhöht, ohne die Discovery-Breite signifikant zu verschlechtern.
/ Versuch 3 (Toolzugriff über MCP-Server): Wie Versuch 2, ergänzt um strukturierten Tool-Zugriff über MCP. Vorgesehen sind drei Server für Symbol-Navigation auf Code-Ebene, für Datenbank-Inspektion auf Schema- und Datensatzebene sowie optional für GUI-Beobachtung. Die Hypothese lautet, dass strukturierter Tool-Zugriff die Discovery-Breite vergrößert und zuvor undokumentierte Use Cases sichtbar macht, allerdings zu Lasten erhöhter Steuerungskomplexität.
/ Versuch 4 (optional, LLM-Querschnitt): Die in den ersten drei Versuchen wirksamste Konfiguration wird auf allen drei alternativen Modellen wiederholt: GPT-5 über die Codex-CLI, DeepSeek R1 über die Cloud-API sowie Qwen 3.5 Coder lokal über LM Studio. Wo einzelne Werkzeugkomponenten in der jeweiligen Umgebung nicht eins zu eins verfügbar sind, wird ein funktional äquivalenter Ersatz gewählt und dokumentiert. Ziel ist eine Einschätzung, in welchem Maße die in Versuch 1 bis 3 beobachteten Effekte modellabhängig oder werkzeugabhängig sind.
#figure(
table(
columns: (auto, 1fr, 1.4fr),
align: (left + top, left + top, left + top),
stroke: 0.4pt,
[*Versuch*], [*Werkzeugkonfiguration*], [*Hypothese*],
[V1 Baseline],
[
- Prompt-only
- keine Agentendateien
- keine externen Tools
],
[
- Formal strukturierte Spezifikation erreichbar
- Discovery-Breite begrenzt
],
[V1b Werkzeug-Agenten],
[
- Wie V1
- werkzeugeigene Subagenten zugelassen
- keine Agentendateien
],
[
- Höhere Discovery-Breite
- Deutlich größere Streuung zwischen Läufen
],
[V2 Agenten],
[
- Wie V1b
- rollenspezialisierte Agentendateien
],
[
- Höhere Strukturierungstiefe und Normkonformität
- Vergleichbare Discovery-Breite
],
[V3 MCP-Tools],
[
- Wie V2
- MCP-Server für Code
- MCP-Server für Datenbank
- optional MCP-Server für GUI
],
[
- Größere Discovery-Breite
- Höhere Steuerungskomplexität
],
[V4 (optional)],
[
- beste Konfiguration aus V1–V3
- GPT-5 über Codex-CLI
- DeepSeek R1 über Cloud-API
- Qwen 3.5 Coder lokal über LM Studio
],
[- Trennung modell- gegenüber werkzeugabhängiger Effekte],
),
caption: [Übersicht der geplanten Versuche mit Werkzeugkonfiguration und Arbeitshypothese.],
) <tab_versuchsreihe>
#heading(level: 3)[Iteratives Vorgehen innerhalb eines Versuchs]
Innerhalb jedes Versuchs läuft ein iterativer Loop ab, in dem der Prompt schrittweise verfeinert wird. Der Begriff Iteration bezeichnet in dieser Arbeit ausschließlich diesen inneren Loop. Der schrittweise Aufbau über V1, V2 und V3 wird hingegen als Versuchsreihe bezeichnet.
Der Ablauf folgt fünf Schritten. Ausgangspunkt ist ein Initial-Prompt. In Versuch 1 wird dieser zu Versuchsbeginn neu formuliert. In Versuch 2 und Versuch 3 wird stattdessen der finale Prompt des vorhergehenden Versuchs übernommen und vor dem ersten Lauf an die neu hinzukommende Werkzeugkomponente angepasst (Agentendateien beziehungsweise MCP-Server). Der Initial-Prompt wird gegen die Codebasis ausgeführt. Anschließend begutachtet der Autor die Ausgabe stichprobenartig auf offensichtliche Lücken, formale Probleme oder fehlende Belege. Auf Basis dieser Begutachtung wird der Prompt mit dokumentiertem Änderungsgrund angepasst und erneut ausgeführt. Der Loop wiederholt sich, bis der Autor das Ergebnis als ausreichend einstuft.
Diese innere Begutachtung ersetzt nicht die Stakeholder-Validierung. Die Stakeholder-Validierung greift erst, wenn der Autor den Versuch als abgeschlossen erklärt. Ihr Urteil bezieht sich ausschließlich auf das End-Set des Versuchs. Damit fließt der Validatoren-Aufwand nicht in jede Zwischenversion ein.
Jede Prompt-Version wird im Versuchsordner mit Zeitstempel und kurzer Notiz zum Änderungsgrund abgelegt, sodass die Iterations-Historie reproduzierbar bleibt und später als Lerneffekt diskutiert werden kann. Der Ablauf ist in @abb_versuchsablauf zusammengefasst.
#figure(
cetz.canvas({
import cetz.draw: *
let draw-box(x, y, w, h, label, dashed: false) = {
rect(
(x - w/2, y - h/2),
(x + w/2, y + h/2),
stroke: if dashed { (dash: "dashed", thickness: 0.6pt, paint: black) } else { black + 0.6pt },
fill: luma(240),
)
content((x, y), text(size: 9pt, weight: "bold")[#label])
}
let draw-mini-box(x, y, w, h, label) = {
rect(
(x - w/2, y - h/2),
(x + w/2, y + h/2),
stroke: black + 0.4pt,
fill: luma(248),
)
content((x, y), text(size: 8pt)[#label])
}
let draw-arrow(from, to) = {
line(from, to, mark: (end: ">"), stroke: black + 0.6pt)
}
let h-half = 0.4
let mh-half = 0.25
let gap = 0.1
let top-y = 5
let versuche = (
(x: -5, label: "V1 Baseline"),
(x: -1.5, label: "V2 Agenten"),
(x: 2, label: "V3 MCP-Tools"),
)
for v in versuche {
draw-box(v.x, top-y, 3.0, 0.8, v.label)
}
draw-box(5.5, top-y, 3.0, 0.8, "V4 (optional)", dashed: true)
draw-arrow((-5 + 1.5, top-y), (-1.5 - 1.5, top-y))
draw-arrow((-1.5 + 1.5, top-y), ( 2 - 1.5, top-y))
line(
(2 + 1.5, top-y), (5.5 - 1.5, top-y),
mark: (end: ">"),
stroke: (dash: "dashed", thickness: 0.6pt, paint: black),
)
for v in versuche {
let cx = v.x
let py = 3.5
let ly = 2.5
let by = 1.5
draw-mini-box(cx, py, 1.7, 0.5, "Prompt")
draw-mini-box(cx, ly, 1.7, 0.5, "Lauf")
draw-mini-box(cx, by, 1.9, 0.5, "Begutachtung")
draw-arrow((cx, top-y - h-half - gap), (cx, py + mh-half + gap))
draw-arrow((cx, py - mh-half - gap), (cx, ly + mh-half + gap))
draw-arrow((cx, ly - mh-half - gap), (cx, by + mh-half + gap))
let lx = cx + 1.25
line((cx + 0.95, by), (lx, by), stroke: black + 0.6pt)
line((lx, by), (lx, py), stroke: black + 0.6pt)
line((lx, py), (cx + 0.85, py),
mark: (end: ">"), stroke: black + 0.6pt)
content((lx + 0.25, (py + by) / 2), text(size: 7pt, fill: luma(80))[nein])
}
}),
caption: [Zustandsgraph des Versuchsablaufs. Die Versuche bauen schrittweise aufeinander auf, V4 ist optional. Der finale Prompt eines Versuchs wird zu Beginn des Folgeversuchs an die neu hinzukommende Werkzeugkomponente angepasst und dient dann als Startprompt. Innerhalb jedes Versuchs verfeinert ein Iterations-Loop den Prompt: Lauf → Begutachtung durch den Autor → bei „nein" Prompt-Anpassung, bei „ja" Übergang zum nächsten Versuch.],
) <abb_versuchsablauf>
Abweichend von dieser Aufteilung wurde ein Teil des Modellvergleichs bereits innerhalb von Versuch 1 durchgeführt. Nachdem sich in den Wiederholungsläufen eine unerwartet große Streuung zeigte, wurde derselbe Prompt unter sonst identischer Bedingung zusätzlich mit zwei weiteren Modellen ausgeführt, um zu prüfen, ob diese Streuung modellabhängig ist. Diese Läufe liegen deshalb im Versuchsordner von Versuch 1 und nicht in Versuch 4. Sie ersetzen den dort geplanten LLM-Querschnitt nicht, da sie nur die Baseline-Konfiguration abdecken und nicht die in Versuch 1 bis 3 wirksamste Konfiguration. Ihr Zweck ist die Absicherung der Varianzaussage, nicht der Werkzeugvergleich.
Konstanten und Variablen sind in jedem Versuch klar dokumentiert. In Versuch 1 bis 3 umfassen die Konstanten Codebasis, Iterations-Vorgehen, Modellfamilie, Validierungsstichprobe und Bewertungskriterien. Variabel sind die Werkzeugkonfiguration und der Startprompt jedes Versuchs. Versuch 1 startet mit einem zu Versuchsbeginn neu formulierten Prompt. Versuch 2 und Versuch 3 übernehmen jeweils den finalen Prompt des vorhergehenden Versuchs und passen ihn zu Beginn an die neu hinzukommende Werkzeugkomponente an (Agentendateien in Versuch 2, MCP-Server in Versuch 3). Erst dieser angepasste Prompt bildet den Ausgangspunkt für den Iterations-Loop des Folgeversuchs. In Versuch 4 wird die Logik umgekehrt. Die Werkzeugkonfiguration ist die Konstante, das Modell die Variable. Damit lassen sich Unterschiede in Versuch 1 bis 3 ursächlich der Werkzeugvariation und in Versuch 4 ursächlich der Modellwahl zuordnen.
@@ -0,0 +1,72 @@
#set text(lang: "de")
#heading(level: 2)[Stakeholder-Validierung als Verifikationsverfahren]
Die Stakeholder-Validierung ist das zentrale Verifikationsverfahren dieser Arbeit. Sie ist nicht als nachgelagerter Schritt gedacht, sondern bildet das Maß, an dem die KI-Ergebnisse gemessen werden. Plausibel formulierte LLM-Ausgaben sind nicht hinreichend. Eine Anforderung gilt erst dann als belastbar, wenn sie durch einen Domänenexperten geprüft und bestätigt wurde.
Vorgesehen sind bis zu drei Validatoren mit jeweils mehrjähriger Erfahrung in der c-entron-Codebasis und in den fachlich abgedeckten Geschäftsprozessen. Da im Unternehmen nicht mehr für jeden Fachbereich ein dedizierter Experte verfügbar ist, kann eine vollständige modulweise Abdeckung durch bereichsspezifische Validatoren nicht garantiert werden. Die Validatorengruppe deckt die Codebasis stattdessen in Summe ab. Die Teilnehmer sind bereits identifiziert. Der Interview-Leitfaden ist in @anh_interview_validierung im Anhang dokumentiert.
Für die Validierung wird pro Versuchslauf eine Zufallsstichprobe aus den extrahierten Anforderungen gezogen und auf die bis zu drei Teilnehmer verteilt. Die Stichprobengröße wird vor Versuchsbeginn festgelegt und im Versuchsprotokoll dokumentiert. Eine bereichs- oder risikoklassenspezifische Stratifizierung ist nicht vorgesehen, da die personelle Verfügbarkeit der Fachexperten eine flächendeckende Modulabdeckung nicht zulässt.
Jede Anforderung wird entlang von sechs Dimensionen bewertet:
/ Sachliche Korrektheit: Beschreibt die Anforderung das tatsächliche Systemverhalten?
/ Vollständigkeit: Sind Akteur, Vorbedingung und Ergebnis ausreichend spezifiziert?
/ Verständlichkeit: Lässt sich die Anforderung ohne Rückfrage interpretieren?
/ Redundanzfreiheit: Ist die Anforderung von anderen klar abgegrenzt?
/ Übernahmewürdigkeit: Soll die Anforderung im Zielsystem in ihrer Funktion erhalten bleiben, oder ist sie ein historisch gewachsener Workaround, ein Sonderfall oder eine veraltete Logik, die im Zuge der Neuimplementierung entfallen sollte?
/ Konsolidierungsbedarf: Bestehen andere Anforderungen, die dieselbe fachliche Funktion abbilden und im Zielsystem zu einer gemeinsamen Anforderung zusammengeführt werden sollten? Ein typisches Beispiel sind die in der bestehenden c-entron-Codebasis getrennt geführten Datensätze für Drucker („Stammblätter") und sonstige Hardware („Assets"), die im Zielsystem zu einem konsolidierten Asset-Konzept zusammengeführt werden sollen.
Die Bewertung erfolgt auf einer fünfstufigen Likert-Skala mit definierten Ankern an den Polen, wobei 1 für „trifft nicht zu" und 5 für „trifft voll zu" steht.
Ergänzend zur itemweisen Bewertung werden mit den Validatoren halbstrukturierte Interviews geführt. Themen sind hierbei vor allem migrationsspezifische Risiken, der Konsolidierungsbedarf gegenüber der Zielarchitektur und die Nützlichkeit der KI-Ergebnisse im Vergleich zu einer hypothetischen manuellen Analyse.
Eine Requirement gilt im Sinne dieser Arbeit als belastbar, wenn drei Quellen sie stützen: ein KI generiertes Requirement, ein konkreter Artefakt-Beleg und eine Expertenbestätigung.
#heading(level: 2)[Evaluationsrahmen]
Der Evaluationsrahmen wird vor der Durchführung der Versuche definiert. Damit wird eine nachträgliche Anpassung der Kriterien an die Ergebnisse ausgeschlossen. Die Bewertung orientiert sich an den drei in @sec_re_qualitaet hergeleiteten Qualitätsdimensionen.
/ Statement-Qualität: Pro einzelner Anforderung wird gemessen, ob sie eindeutig formuliert, vollständig im Satzbau, frei von unbelegten Annahmen und mit Akzeptanzkriterium oder Prüfidee versehen ist. Die Messung erfolgt über die zuvor beschriebene Likert-Skala.
/ Set-Qualität: Pro Versuch wird die gesamte erzeugte Anforderungsmenge als ein Set bewertet. Gemessen wird, ob sie die relevanten Prozesse und Varianten vollständig abdeckt, in sich konsistent ist und keine Doubletten enthält. Im Vordergrund steht dabei die Vollständigkeit, weil ein fehlendes Requirement im Migrationskontext zu Funktionsverlust führen kann. Die Bewertung erfolgt qualitativ durch Expertenbewertung und ergänzend durch maschinelle Konsistenzprüfungen wie doppelte IDs oder fehlende Belege.
/ Traceability-Qualität: Pro Beleg-Verknüpfung wird gemessen, ob der Beleg reproduzierbar auffindbar ist, etwa über Dateipfad, Methode oder SQL-Query, und ob die Ableitung vom Beleg zur Anforderung nachvollziehbar bleibt.\ \
Ergänzend zur Qualitätsbewertung wird der Aufwand erhoben. Dazu werden während der KI-Läufe Indikatoren wie Tokenkosten un Bearbeitungsdauer protokolliert. Parallel wird grob abgeschätzt, wie viele Stunden ein erfahrener Analyst für dieselben Module ohne KI-Unterstützung gebraucht hätte. Beide Größen zusammen erlauben einen Vergleich der Größenordnung, nicht jedoch einen exakten Effizienzfaktor.
#heading(level: 2)[Reproduzierbarkeit und Risikomanagement]
Reproduzierbarkeit und Risikomanagement sind als querschnittliche Aspekte angelegt. Sie betreffen alle Versuchsdurchläufe gleichermaßen und werden hier zusammengefasst.
Alle steuerungsrelevanten Artefakte werden versioniert vorgehalten. Hierzu zählen die verwendeten Prompts in ihrer Textfassung inklusive aller Iterationsversionen mit Zeitstempel und Änderungsgrund, die Agentendateien mit ihren Rollenbeschreibungen, die MCP-Server-Konfigurationen sowie die Angaben zu Modellversion und Kontextfenstergröße. Für lokal betriebene Modelle werden zusätzlich die Inferenz-Runtime mit Version (LM Studio), die Quantisierungsstufe des verwendeten Modell-Builds sowie weitere Sampling-Parameter dokumentiert, da diese Stellgrößen die Reproduzierbarkeit der Ausgaben spürbar beeinflussen. Jeder Versuchsordner enthält die vollständige Konfiguration als Single Source. Wo möglich, werden deterministische Einstellungen gewählt.
Da die Codebasis kundenbezogene Strukturen enthält, werden datenschutzkritische Werkzeuge bewusst eingegrenzt. MCP-Server für Datenbank-Inspektion und Symbol-Navigation werden lokal betrieben. An externe LLM-Anbieter werden nur diejenigen Codeausschnitte gesendet, die für den jeweiligen Analyseschritt notwendig sind. Personenbezogene Daten oder vollständige Datenexporte sind ausgeschlossen.
Die folgenden vier Risikokategorien werden adressiert:
/ Halluzinationen: Begegnet durch Belegpflicht und Stakeholder-Validierung. Jede Anforderung ohne nachvollziehbaren Beleg wird als Hypothese markiert.
/ Reproduzierbarkeitsverlust: Begegnet durch versionierte Prompts und deterministische Einstellungen, soweit das Modell sie unterstützt. Da die innere Iteration zudem die Gefahr eines Autoren-Bias birgt, etwa eines unbewussten Hinsteuerns auf bestimmte Module, werden die Prompt-Änderungen pro Iteration mit Änderungsgrund dokumentiert und sind im Versuchsordner nachvollziehbar.
/ Domänen- und Datenbias: Begegnet durch eine Stichprobenwahl, die alle relevanten Module abdeckt und nicht nur die in der KI-Ausgabe häufig auftauchenden.
/ Datenschutzverletzungen: Begegnet durch On-Premise-MCP, kontrollierten Versand und Logging der externen Aufrufe.
#heading(level: 2)[Konkrete Konfigurationen der geplanten Versuche]
Dieser Abschnitt konkretisiert die zuvor beschriebene Versuchsreihe auf Konfigurationsebene. Jeder Versuch ist durch seinen Prompt, seine Agentenliste und seine MCP-Server-Liste vollständig beschrieben. Modellversion, Kontextfenster und Temperatur werden im Versuchsordner protokolliert.
#figure(
table(
columns: (auto, 1fr, 1fr, 1fr),
align: (left, left, left, left),
stroke: 0.4pt,
[*Element*], [*V1 Baseline*], [*V2 Agenten*], [*V3 MCP-Tools*],
[Modell], [Claude (Claude Code)], [Claude (Claude Code)], [Claude (Claude Code)],
[Startprompt], [Zu Versuchsbeginn neu formuliert], [Finaler Prompt aus V1, angepasst an Agentendateien], [Finaler Prompt aus V2, angepasst an MCP-Server],
[Agentendateien], [keine], [Stakeholder, System, Software, ISO-29148-Orchestrator], [wie V2],
[MCP-Server], [keine], [keine], [Symbol-Navigation, Datenbank-Inspektion, optional GUI-Beobachtung],
[Validierungsstichprobe], [Zufallsstichprobe], [Zufallsstichprobe], [Zufallsstichprobe],
),
caption: [Detail-Konfiguration der drei Kernversuche.],
) <tab_versuchskonfiguration>
Die Versuchsordner-Struktur folgt einer einheitlichen Konvention. Pro Versuch existiert ein Unterordner mit den Konfigurationsartefakten, ein Eingangsprotokoll mit Modell- und Werkzeugangaben, ein Ergebnis-Unterordner sowie eine Verlaufsdokumentation für die Validierungsschritte. Damit ist jeder Versuch eigenständig reproduzierbar.
+1 -1
View File
@@ -4,7 +4,7 @@
#hide(bibliography("../literatur.bib", style: "apa"))
]
#heading(level: 1)[Ergebnisse (ca. 10 Seiten)]
#heading(level: 1)[Durchführung und Ergebnisse]
// TODO Variante B – Inhalte aus 05_prototypische_umsetzung_VarianteA.typ übernehmen
// und explizit als „Ergebnisse der Versuche" strukturieren (keine Versuchs-Logbuch-Optik).
+1 -1
View File
@@ -4,7 +4,7 @@
#hide(bibliography("../literatur.bib", style: "apa"))
]
#heading(level: 1)[Evaluation (ca. 12 Seiten)]
#heading(level: 1)[Evaluation]
// TODO Variante B – Anwendung des in Kap 4.6 definierten Evaluationsrahmens auf die
// Ergebnisse aus Kap 5. Keine Erst-Definition von Kriterien hier.
+1 -33
View File
@@ -4,46 +4,14 @@
#hide(bibliography("../literatur.bib", style: "apa"))
]
#heading(level: 1)[Diskussion (ca. 8 Seiten)]
#heading(level: 1)[Diskussion ]
#heading(level: 2)[Interpretation der Ergebnisse]
Die Ergebnisse zeigen einen klaren methodischen Lerneffekt ueber die drei Versuche. Der Verlauf von V01 ueber V02 zu V03 ist nicht als Widerspruch, sondern als komplementaere Reifung zu interpretieren:
- V01 demonstriert, dass bereits mit einfacher Konfiguration formal strukturierte Requirements ableitbar sind.
- V02 zeigt, dass eine agentengestuetzte ISO-Konsolidierung methodisch sauber, aber fuer den Gesamtumfang zu rigide sein kann.
- V03 zeigt, dass die MCP-Erweiterung die funktionale Breite massiv erhoeht und Discovery-Luecken schliesst.
In Summe entsteht ein zweistufiges Zielbild fuer Reverse Requirements Engineering in Legacy-Projekten: zuerst *formal konsolidieren*, danach *gezielt in die Breite erweitern*.
#heading(level: 2)[Chancen und Grenzen]
Die wesentlichen Chancen des Ansatzes liegen in:
- hoher Skalierbarkeit bei grossen Legacy-Artefakten,
- schneller Sichtbarmachung undokumentierter Funktionalitaet,
- strukturierter Ueberfuehrung in reviewbare Requirements-Artefakte.
Die zentralen Grenzen bleiben:
- keine belastbare Vollstaendigkeit ohne Zusatzquellen (insbesondere Nutzungs- und Prozesssicht),
- Halluzinations- und Fehlinterpretationsrisiken ohne Beleg- und Reviewpflicht,
- hoher Konsolidierungsaufwand zwischen Discovery-Artefakten und abnahmefaehiger Spezifikation.
Damit bestaetigt die Fallstudie, dass LLMs Requirements Engineering nicht ersetzen, aber als beschleunigendes Analyseinstrument mit klaren Governance-Regeln substantiellen Mehrwert liefern.
#heading(level: 2)[Implikationen fuer Forschung und Praxis]
Fuer die Praxis folgt daraus ein umsetzbarer Einfuehrungspfad:
1. Schrittweise aufeinander aufbauende Versuchsreihe statt einmaliger "Big-Bang"-Extraktion.
2. Trennung von Discovery- und Konsolidierungsphase als Standard.
3. Traceability als verpflichtendes Abnahmekriterium fuer LLM-Ergebnisse.
Fuer die Forschung ergeben sich drei Anschlussfragen:
1. Wie laesst sich die Triangulation aus Code-, Video- und Stakeholderdaten automatisiert zusammenfuehren?
2. Welche Metriken messen Qualitaet von Requirements-Artefakten robuster als reine Umfangszahlen?
3. Wie kann Human-in-the-loop-Validierung mit vertretbarem Aufwand skaliert werden?
Die vorliegende Arbeit liefert dafuer eine belastbare methodische Ausgangsbasis, zeigt aber zugleich, dass die letzte Meile zur fachlich finalen Spezifikation weiterhin ein kooperativer Mensch-KI-Prozess bleibt.
+1 -34
View File
@@ -1,35 +1,2 @@
#heading(level: 1)[Fazit und Ausblick (ca. 4 Seiten)]
#heading(level: 1)[Fazit und Ausblick]
#heading(level: 2)[Zusammenfassung und Beantwortung der Forschungsfragen]
Die Arbeit zeigt, dass KI-gestuetztes Reverse Requirements Engineering im untersuchten Legacy-ERP-Kontext praktikabel ist, wenn der Prozess schrittweise und kontrolliert aufgebaut wird. Die drei durchgefuehrten Versuche liefern dabei komplementaere Staerken:
- V01 liefert eine formale Baseline mit klarer Requirements-Struktur.
- V02 konsolidiert die Erkenntnisse in eine ISO-29148-nahe, traceability-starke Spezifikation.
- V03 erweitert die Discovery-Breite per MCP und deckt einen hohen dokumentationsbezogenen Gap auf.
Damit ist F1 (prozessuale Einsetzbarkeit von LLMs) positiv beantwortet. F4 (Chancen und Grenzen) ist ebenfalls klar beantwortbar: Hohe Effizienz- und Strukturgewinne stehen einem weiterhin relevanten Validierungs- und Konsolidierungsbedarf gegenueber. F2 und F3 sind teilweise beantwortet, da video- und interviewbasierte Endvalidierung noch nicht vollstaendig abgeschlossen ist.
#heading(level: 2)[Handlungsempfehlungen fuer c-entron GmbH]
Aus den Ergebnissen lassen sich folgende priorisierte Handlungsschritte ableiten:
1. **V02 als Spezifikationsbasis verwenden:** Die 220 konsolidierten Requirements mit hoher Traceability als Arbeitsgrundlage fuer die Web-Migration etablieren.
2. **V03 als Discovery-Backlog nutzen:** Die 1720 identifizierten Faehigkeiten systematisch gegen V02 mappen, um potenzielle Luecken sichtbar zu halten.
3. **Review-Governance fest verankern:** Fachliche Freigaben und Aenderungsentscheidungen pro Requirement dokumentieren (kein unreviewter LLM-Output im Zielbacklog).
4. **Toolchain standardisieren:** Prompt-/Agentenkonfigurationen versionieren, damit Folgeanalysen reproduzierbar bleiben.
#heading(level: 2)[Ausblick und naechste Schritte]
Die naechste Arbeitsphase erweitert die bisher codezentrierte Evidenz um die noch offenen Schritte aus dem Protokoll:
1. **Vollstaendige Videoanalyse:** Alle vorhandenen Schulungsvideos KI-gestuetzt transkribieren und strukturiert auf Use Cases auswerten.
2. **Abgleich Video vs. Codeanalyse:** Systematischer Vergleich, ob und wo sich beide Sichten decken bzw. welche Use Cases nur in einer Quelle auftauchen.
3. **Clusterung in abstrakte Konzepte:** Die identifizierten Use Cases in die bereits vorbereiteten 101 abstrahierten Konzepte ueberfuehren (vgl. `A_Videoanalyse_Uebersicht.csv`).
4. **Manuelle Fachklassifikation pro Cluster:** Bewertung in die Kategorien
- **ja:** unveraenderte Uebernahme,
- **nein:** Entfall in ERP Web,
- **neu:** fachlich vorhanden, aber neu zu konzipieren,
- **TBD:** vorlaeufig offen.
Erst mit dieser finalen Triangulation aus Code, Video und Fachbewertung ist eine belastbare Vollstaendigkeitsaussage fuer die Migrationsplanung moeglich.
+1 -1
View File
@@ -1,3 +1,3 @@
#heading(level: 1)[Literaturverzeichnis (ca. 3 Seiten)]
#heading(level: 1)[Literaturverzeichnis]
#bibliography("../literatur.bib", style: "apa")
+2 -4
View File
@@ -1,11 +1,9 @@
#heading(level: 1)[Anhang (ca. 6 Seiten)]
#heading(level: 1)[Anhang]
#heading(level: 2)[Interviewleitfäden]
#heading(level: 3)[Stakeholder-Validierung der KI-extrahierten Anforderungen] <anh_interview_validierung>
_Ausarbeitung folgt._
#heading(level: 2)[Zusätzliches Datenmaterial]
#heading(level: 2)[Konfigurationsdetails des Prototyps]
#heading(level: 2)[Konfigurationsdetails]
File diff suppressed because it is too large Load Diff
-26780
View File
File diff suppressed because it is too large Load Diff
+8 -4
View File
@@ -7,7 +7,7 @@
"Masterarbeit an der Hochschule Neu-Ulm",
"Christoph Schwörer",
"Master of Business Administration",
"Prof. Dr. Daniel Schallmö",
"Prof. Dr. Daniel Schallmo",
"XX 2026"
)
@@ -15,6 +15,10 @@
#pagebreak()
#(thesis.confidentiality)(meta)
#pagebreak()
#(thesis.declaration)(meta)
#pagebreak()
@@ -38,15 +42,15 @@
#outline(depth: 2, title: "Inhaltsverzeichnis")
#pagebreak()
#include "Kapitel/00_abstract.typ"
#include "Kapitel/01_einleitung.typ"
#include "Kapitel/02_theoretischer_hintergrund.typ"
#include "Kapitel/03_fallstudie.typ"
#pagebreak()
#include "Kapitel/04_konzeption_methodisches_vorgehen.typ"
#pagebreak()
#include "Kapitel/05_prototypische_umsetzung.typ"
#include "Kapitel/05_durchfuehrung_und_ergebnisse.typ"
#pagebreak()
#include "Kapitel/06_evaluation.typ"
#include "Kapitel/07_diskussion.typ"
+14
View File
@@ -0,0 +1,14 @@
**/.git
**/.idea
**/app.config
**/bin
**/obj
./.editorconfig
./azure
./deployment
./docker
./docs
./scripts
./src/centron/
./tests
bin
+64
View File
@@ -0,0 +1,64 @@
# Remove the line below if you want to inherit .editorconfig settings from higher directories
root = true
# C# and XAML files
[*.{cs,xaml}]
charset = utf-8-bom
# C# files
[*.cs]
#### Naming styles ####
# Naming rules
dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.types_should_be_pascal_case.symbols = types
dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
dotnet_naming_rule.private_or_internal_field_should_be_camel_case_with___prefix.severity = warning
dotnet_naming_rule.private_or_internal_field_should_be_camel_case_with___prefix.symbols = private_or_internal_field
dotnet_naming_rule.private_or_internal_field_should_be_camel_case_with___prefix.style = camel_case_with___prefix
# Symbol specifications
dotnet_naming_symbols.interface.applicable_kinds = interface
dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal
dotnet_naming_symbols.interface.required_modifiers =
dotnet_naming_symbols.private_or_internal_field.applicable_kinds = field
dotnet_naming_symbols.private_or_internal_field.applicable_accessibilities = internal, private
dotnet_naming_symbols.private_or_internal_field.required_modifiers =
dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal
dotnet_naming_symbols.types.required_modifiers =
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal
dotnet_naming_symbols.non_field_members.required_modifiers =
# Naming styles
dotnet_naming_style.pascal_case.required_prefix =
dotnet_naming_style.pascal_case.required_suffix =
dotnet_naming_style.pascal_case.word_separator =
dotnet_naming_style.pascal_case.capitalization = pascal_case
dotnet_naming_style.begins_with_i.required_prefix = I
dotnet_naming_style.begins_with_i.required_suffix =
dotnet_naming_style.begins_with_i.word_separator =
dotnet_naming_style.begins_with_i.capitalization = pascal_case
dotnet_naming_style.camel_case_with___prefix.required_prefix = _
dotnet_naming_style.camel_case_with___prefix.required_suffix =
dotnet_naming_style.camel_case_with___prefix.word_separator =
dotnet_naming_style.camel_case_with___prefix.capitalization = camel_case
+63
View File
@@ -0,0 +1,63 @@
###############################################################################
# Set default behavior to automatically normalize line endings.
###############################################################################
* text=auto eol=lf
###############################################################################
# Set default behavior for command prompt diff.
#
# This is need for earlier builds of msysgit that does not have it on by
# default for csharp files.
# Note: This is only used by command line
###############################################################################
#*.cs diff=csharp
###############################################################################
# Set the merge driver for project and solution files
#
# Merging from the command prompt will add diff markers to the files if there
# are conflicts (Merging from VS is not affected by the settings below, in VS
# the diff markers are never inserted). Diff markers may cause the following
# file extensions to fail to load in VS. An alternative would be to treat
# these files as binary and thus will always conflict and require user
# intervention with every merge. To do so, just uncomment the entries below
###############################################################################
#*.sln merge=binary
#*.csproj merge=binary
#*.vbproj merge=binary
#*.vcxproj merge=binary
#*.vcproj merge=binary
#*.dbproj merge=binary
#*.fsproj merge=binary
#*.lsproj merge=binary
#*.wixproj merge=binary
#*.modelproj merge=binary
#*.sqlproj merge=binary
#*.wwaproj merge=binary
###############################################################################
# behavior for image files
#
# image files are treated as binary by default.
###############################################################################
#*.jpg binary
#*.png binary
#*.gif binary
###############################################################################
# diff behavior for common document formats
#
# Convert binary document formats to text before diffing them. This feature
# is only available from the command line. Turn it on by uncommenting the
# entries below.
###############################################################################
#*.doc diff=astextplain
#*.DOC diff=astextplain
#*.docx diff=astextplain
#*.DOCX diff=astextplain
#*.dot diff=astextplain
#*.DOT diff=astextplain
#*.pdf diff=astextplain
#*.PDF diff=astextplain
#*.rtf diff=astextplain
#*.RTF diff=astextplain
@@ -0,0 +1,149 @@
name: Sign artifacts
description: >
Signs files via azure/artifact-signing-action with up to three attempts.
The Microsoft timestamp server (timestamp.acs.microsoft.com) fails
intermittently; re-signing already-signed files is safe because signtool
replaces existing signatures, so failed batches can simply be retried.
inputs:
azure-tenant-id:
description: Azure tenant id used for authentication.
required: true
azure-client-id:
description: Azure client id used for authentication.
required: true
azure-client-secret:
description: Azure client secret used for authentication.
required: true
files:
description: Newline-separated list of files to sign.
required: false
default: ''
files-folder:
description: Folder containing the files to sign.
required: false
default: ''
files-folder-filter:
description: Comma-separated file extensions to sign within files-folder.
required: false
default: ''
files-folder-recurse:
description: Whether to search files-folder recursively.
required: false
default: 'false'
endpoint:
description: Artifact Signing endpoint.
required: false
default: https://weu.codesigning.azure.net/
signing-account-name:
description: Artifact Signing account name.
required: false
default: CentronCodesigning
certificate-profile-name:
description: Certificate profile name.
required: false
default: centroncert
runs:
using: composite
steps:
- name: Sign (attempt 1)
id: attempt1
continue-on-error: true
uses: azure/artifact-signing-action@c7ab2a863ab5f9a846ddb8265964877ef296ee82 # v2.0.0
with:
azure-tenant-id: ${{ inputs.azure-tenant-id }}
azure-client-id: ${{ inputs.azure-client-id }}
azure-client-secret: ${{ inputs.azure-client-secret }}
endpoint: ${{ inputs.endpoint }}
signing-account-name: ${{ inputs.signing-account-name }}
certificate-profile-name: ${{ inputs.certificate-profile-name }}
files: ${{ inputs.files }}
files-folder: ${{ inputs.files-folder }}
files-folder-filter: ${{ inputs.files-folder-filter }}
files-folder-recurse: ${{ inputs.files-folder-recurse }}
file-digest: SHA256
timestamp-rfc3161: http://timestamp.acs.microsoft.com
timestamp-digest: SHA256
exclude-environment-credential: false
exclude-workload-identity-credential: true
exclude-managed-identity-credential: true
exclude-shared-token-cache-credential: true
exclude-visual-studio-credential: true
exclude-visual-studio-code-credential: true
exclude-azure-cli-credential: true
exclude-azure-powershell-credential: true
exclude-azure-developer-cli-credential: true
exclude-interactive-browser-credential: true
- name: Wait before retry (attempt 2)
if: steps.attempt1.outcome == 'failure'
shell: pwsh
run: |
Write-Host 'Signing failed, retrying in 30 seconds...'
Start-Sleep -Seconds 30
- name: Sign (attempt 2)
id: attempt2
if: steps.attempt1.outcome == 'failure'
continue-on-error: true
uses: azure/artifact-signing-action@c7ab2a863ab5f9a846ddb8265964877ef296ee82 # v2.0.0
with:
azure-tenant-id: ${{ inputs.azure-tenant-id }}
azure-client-id: ${{ inputs.azure-client-id }}
azure-client-secret: ${{ inputs.azure-client-secret }}
endpoint: ${{ inputs.endpoint }}
signing-account-name: ${{ inputs.signing-account-name }}
certificate-profile-name: ${{ inputs.certificate-profile-name }}
files: ${{ inputs.files }}
files-folder: ${{ inputs.files-folder }}
files-folder-filter: ${{ inputs.files-folder-filter }}
files-folder-recurse: ${{ inputs.files-folder-recurse }}
file-digest: SHA256
timestamp-rfc3161: http://timestamp.acs.microsoft.com
timestamp-digest: SHA256
exclude-environment-credential: false
exclude-workload-identity-credential: true
exclude-managed-identity-credential: true
exclude-shared-token-cache-credential: true
exclude-visual-studio-credential: true
exclude-visual-studio-code-credential: true
exclude-azure-cli-credential: true
exclude-azure-powershell-credential: true
exclude-azure-developer-cli-credential: true
exclude-interactive-browser-credential: true
- name: Wait before retry (attempt 3)
if: steps.attempt1.outcome == 'failure' && steps.attempt2.outcome == 'failure'
shell: pwsh
run: |
Write-Host 'Signing failed again, retrying in 90 seconds...'
Start-Sleep -Seconds 90
- name: Sign (attempt 3)
if: steps.attempt1.outcome == 'failure' && steps.attempt2.outcome == 'failure'
uses: azure/artifact-signing-action@c7ab2a863ab5f9a846ddb8265964877ef296ee82 # v2.0.0
with:
azure-tenant-id: ${{ inputs.azure-tenant-id }}
azure-client-id: ${{ inputs.azure-client-id }}
azure-client-secret: ${{ inputs.azure-client-secret }}
endpoint: ${{ inputs.endpoint }}
signing-account-name: ${{ inputs.signing-account-name }}
certificate-profile-name: ${{ inputs.certificate-profile-name }}
files: ${{ inputs.files }}
files-folder: ${{ inputs.files-folder }}
files-folder-filter: ${{ inputs.files-folder-filter }}
files-folder-recurse: ${{ inputs.files-folder-recurse }}
file-digest: SHA256
timestamp-rfc3161: http://timestamp.acs.microsoft.com
timestamp-digest: SHA256
exclude-environment-credential: false
exclude-workload-identity-credential: true
exclude-managed-identity-credential: true
exclude-shared-token-cache-credential: true
exclude-visual-studio-credential: true
exclude-visual-studio-code-credential: true
exclude-azure-cli-credential: true
exclude-azure-powershell-credential: true
exclude-azure-developer-cli-credential: true
exclude-interactive-browser-credential: true
+431
View File
@@ -0,0 +1,431 @@
name: Build, sign, and publish
on:
workflow_dispatch:
pull_request:
branches:
- main
- 'release/**'
types:
- opened
- synchronize
- reopened
- ready_for_review
push:
branches:
- main
- 'release/**'
permissions:
contents: read
# Supersede in-flight runs of this workflow for the same pull request. Pushes to main and
# release branches are excluded so every commit there still gets a full, recorded result.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
build:
name: Build and sign
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.draft == false && github.event.pull_request.head.repo.full_name == github.repository) }}
runs-on:
group: self-hosted
labels: [self-hosted, Windows, X64]
timeout-minutes: 180
permissions:
contents: read
outputs:
version: ${{ steps.version.outputs.version }}
main_version: ${{ steps.version.outputs.main_version }}
env:
DOTNET_NOLOGO: true
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
DOTNET_CLI_TELEMETRY_OPTOUT: true
CENTRON_BUILD_RUNNING_IN_AZURE_PIPELINE: true
CENTRON_BUILD_IS_DEV_BUILD: false
steps:
- name: Show runner information
shell: pwsh
run: |
Write-Host "Runner: $env:RUNNER_NAME"
Write-Host "Computer: $env:COMPUTERNAME"
Write-Host "PowerShell: $($PSVersionTable.PSVersion)"
- name: Check out repository
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Normalize Git repository format
shell: pwsh
run: |
$workspace = [IO.Path]::GetFullPath($env:GITHUB_WORKSPACE)
$safeDirectories = @(git config --global --get-all safe.directory)
$safeDirectoriesExitCode = $LASTEXITCODE
if ($safeDirectoriesExitCode -notin 0, 1) {
throw 'Could not determine the configured Git safe directories.'
}
if ($safeDirectories -notcontains $workspace) {
git config --global --add safe.directory $workspace
if ($LASTEXITCODE -ne 0) {
throw "Could not register the GitHub workspace as a safe directory: $workspace"
}
}
$repositoryFormatVersion = git config --local --get core.repositoryFormatVersion
if ($LASTEXITCODE -ne 0) {
throw 'Could not determine the Git repository format version.'
}
if ($repositoryFormatVersion -eq '1') {
$extensions = @(git config --local --name-only --get-regexp '^extensions\.')
$extensionsExitCode = $LASTEXITCODE
if ($extensionsExitCode -notin 0, 1) {
throw 'Could not determine the active Git repository extensions.'
}
if ($extensions.Count -gt 0) {
throw "Cannot normalize the Git repository while extensions are active: $($extensions -join ', ')"
}
git config --local core.repositoryFormatVersion 0
if ($LASTEXITCODE -ne 0) {
throw 'Could not normalize the Git repository format for Nerdbank.GitVersioning.'
}
}
- name: Verify build environment
shell: pwsh
run: |
$sdkVersion = dotnet --version
if ($LASTEXITCODE -ne 0) {
throw "The .NET SDK could not be resolved using global.json."
}
if ($sdkVersion -notmatch '^10\.0\.') {
throw "Expected a .NET 10 SDK, but resolved '$sdkVersion'."
}
if ((git rev-parse --is-shallow-repository) -ne 'false') {
throw 'Repository was checked out without its full history.'
}
Write-Host "Resolved .NET SDK: $sdkVersion"
dotnet --info
- name: Set version and create NuGet packages
shell: pwsh
run: |
dotnet run `
--project ".\scripts\Centron.Scripts\Centron.Scripts.csproj" `
-- create-nuget-packages
- name: Determine build version
id: version
shell: pwsh
run: |
$versionText = [string](
& ".\dotnettools\nbgv.exe" get-version -v Version
)
if ($LASTEXITCODE -ne 0) {
throw "Nerdbank.GitVersioning failed with exit code $LASTEXITCODE."
}
$version = [Version]$versionText
$buildVersion = $version.ToString()
$mainVersion = $version.ToString(3)
Write-Host "Build version: $buildVersion"
Write-Host "Main version: $mainVersion"
"version=$buildVersion" >> $env:GITHUB_OUTPUT
"main_version=$mainVersion" >> $env:GITHUB_OUTPUT
$metadataDirectory = Join-Path $env:RUNNER_TEMP 'centron-build-metadata'
New-Item -Path $metadataDirectory -ItemType Directory -Force | Out-Null
Set-Content `
-LiteralPath (Join-Path $metadataDirectory 'version.txt') `
-Value $buildVersion `
-Encoding utf8NoBOM
- name: Prepare Artifact Signing dependencies
shell: powershell
run: |
if ((Get-PackageProvider -Name NuGet -ErrorAction Ignore) -eq $null) {
Install-PackageProvider -Name NuGet -Force -Scope CurrentUser
}
if ((Get-InstalledModule -Name PowerShellGet -MinimumVersion 2.2.1 -ErrorAction Ignore) -eq $null) {
Install-Module -Name PowerShellGet -MinimumVersion 2.2.1 -Scope CurrentUser -Force -AllowClobber
}
- name: Build Web Service
shell: pwsh
run: |
dotnet run `
--project ".\scripts\Centron.Scripts\Centron.Scripts.csproj" `
-- build-web-service-only
- name: Sign Web Service
uses: ./.github/actions/sign-artifacts
with:
azure-tenant-id: ${{ vars.ARTIFACT_SIGNING_TENANT_ID }}
azure-client-id: ${{ vars.ARTIFACT_SIGNING_CLIENT_ID }}
azure-client-secret: ${{ secrets.ARTIFACT_SIGNING_CLIENT_SECRET }}
files: |
${{ github.workspace }}\src\webservice\Centron.Host.WindowsService\bin\Release\net10.0-windows\win-x64\publish\Centron.Host.WindowsService.exe
${{ github.workspace }}\src\webservice\Centron.Host.WindowsService\bin\Release\net10.0-windows\win-x64\publish\Centron.Host.WindowsService.dll
${{ github.workspace }}\src\webservice\Centron.Host.WindowsService\bin\Release\net10.0-windows\win-x64\publish\ConnectionManager\c-entron Connection Manager.exe
${{ github.workspace }}\src\webservice\Centron.Host.WindowsService\bin\Release\net10.0-windows\win-x64\publish\ConnectionManager\c-entron Connection Manager.dll
${{ github.workspace }}\src\webservice\Centron.Host.WindowsService\bin\Release\net10.0-windows\win-x64\publish\Centron.Interfaces.dll
${{ github.workspace }}\src\webservice\Centron.Host.WindowsService\bin\Release\net10.0-windows\win-x64\publish\Centron.WebServices.Core.dll
${{ github.workspace }}\src\webservice\Centron.Host.WindowsService\bin\Release\net10.0-windows\win-x64\publish\Centron.Core.dll
- name: Create Web Service installer
shell: pwsh
run: |
dotnet run `
--project ".\scripts\Centron.Scripts\Centron.Scripts.csproj" `
-- set-dependencies-web-service build-web-service-installer-only
- name: Sign Web Service installer
uses: ./.github/actions/sign-artifacts
with:
azure-tenant-id: ${{ vars.ARTIFACT_SIGNING_TENANT_ID }}
azure-client-id: ${{ vars.ARTIFACT_SIGNING_CLIENT_ID }}
azure-client-secret: ${{ secrets.ARTIFACT_SIGNING_CLIENT_SECRET }}
files-folder: ${{ github.workspace }}\deployment\centron\WebServiceSetupProject\bin\Release
files-folder-filter: exe,dll,msi
files-folder-recurse: true
- name: Package Web Service installer
shell: pwsh
run: |
dotnet run `
--project ".\scripts\Centron.Scripts\Centron.Scripts.csproj" `
-- set-zip-directory-web-service
- name: Build c-entron.NET
shell: pwsh
run: |
dotnet run `
--project ".\scripts\Centron.Scripts\Centron.Scripts.csproj" `
-- build-centron-net-only
- name: Sign c-entron.NET
uses: ./.github/actions/sign-artifacts
with:
azure-tenant-id: ${{ vars.ARTIFACT_SIGNING_TENANT_ID }}
azure-client-id: ${{ vars.ARTIFACT_SIGNING_CLIENT_ID }}
azure-client-secret: ${{ secrets.ARTIFACT_SIGNING_CLIENT_SECRET }}
files: |
${{ github.workspace }}\src\centron\Centron.WPF.UI\bin\Release\net10.0-windows\win-x64\publish\c-entron 2.0.exe
${{ github.workspace }}\src\centron\Centron.WPF.UI\bin\Release\net10.0-windows\win-x64\publish\c-entron 2.0.dll
- name: Create c-entron.NET installer
shell: pwsh
run: |
dotnet run `
--project ".\scripts\Centron.Scripts\Centron.Scripts.csproj" `
-- set-dependencies-centron-net build-centron-net-installer-only
- name: Sign c-entron.NET installer
uses: ./.github/actions/sign-artifacts
with:
azure-tenant-id: ${{ vars.ARTIFACT_SIGNING_TENANT_ID }}
azure-client-id: ${{ vars.ARTIFACT_SIGNING_CLIENT_ID }}
azure-client-secret: ${{ secrets.ARTIFACT_SIGNING_CLIENT_SECRET }}
files-folder: ${{ github.workspace }}\deployment\centron\CentronSetupProject\bin\Release
files-folder-filter: exe,dll,msi
files-folder-recurse: true
- name: Package c-entron.NET installer
shell: pwsh
run: |
dotnet run `
--project ".\scripts\Centron.Scripts\Centron.Scripts.csproj" `
-- set-zip-directory-centron-net
- name: Build Nexus
shell: pwsh
run: |
dotnet run `
--project ".\scripts\Scripts\Scripts.csproj" `
-- build-nexus
- name: Sign Nexus
uses: ./.github/actions/sign-artifacts
with:
azure-tenant-id: ${{ vars.ARTIFACT_SIGNING_TENANT_ID }}
azure-client-id: ${{ vars.ARTIFACT_SIGNING_CLIENT_ID }}
azure-client-secret: ${{ secrets.ARTIFACT_SIGNING_CLIENT_SECRET }}
files: |
${{ github.workspace }}\src\nexus\CentronNexus.Host\bin\Release\net10.0\win-x64\publish\CentronNexus.Host.exe
${{ github.workspace }}\src\nexus\CentronNexus.Host\bin\Release\net10.0\win-x64\publish\CentronNexus.Host.dll
${{ github.workspace }}\src\nexus\CentronNexus.Host\bin\Release\net10.0\win-x64\publish\CentronNexus.dll
${{ github.workspace }}\src\nexus\CentronNexus.Host\bin\Release\net10.0\win-x64\publish\CentronNexus.OutlookAddIn.dll
${{ github.workspace }}\src\nexus\CentronNexus.Host\bin\Release\net10.0\win-x64\publish\Centron.Core.dll
${{ github.workspace }}\src\nexus\CentronNexus.Host\bin\Release\net10.0\win-x64\publish\Centron.WebServices.Core.dll
${{ github.workspace }}\src\nexus\CentronNexus.Host\bin\Release\net10.0\win-x64\publish\Centron.Interfaces.dll
${{ github.workspace }}\src\nexus\CentronNexus.Host\bin\Release\net10.0\win-x64\publish\Centron.Office.Client.dll
- name: Prepare Nexus installer
shell: pwsh
run: |
dotnet run `
--project ".\scripts\Scripts\Scripts.csproj" `
-- zip-nexus-directory
dotnet tool update --global wix --version 5.0.2 --allow-downgrade
if ($LASTEXITCODE -ne 0) {
dotnet tool install --global wix --version 5.0.2
}
wix extension add -g WixToolset.UI.wixext/5.0.2
dotnet run `
--project ".\scripts\Scripts\Scripts.csproj" `
-- build-nexus-installer
- name: Sign Nexus installer
uses: ./.github/actions/sign-artifacts
with:
azure-tenant-id: ${{ vars.ARTIFACT_SIGNING_TENANT_ID }}
azure-client-id: ${{ vars.ARTIFACT_SIGNING_CLIENT_ID }}
azure-client-secret: ${{ secrets.ARTIFACT_SIGNING_CLIENT_SECRET }}
files: ${{ github.workspace }}\deployment\WixSharpInstaller\bin\Release\net10.0-windows\c-entron Nexus.msi
- name: Verify signed installers
shell: pwsh
run: |
$files = @(
'.\deployment\centron\WebServiceSetupProject\bin\Release\c-entron Web-Service Installer.msi'
'.\deployment\centron\CentronSetupProject\bin\Release\c-entron.NET Installer.msi'
'.\deployment\WixSharpInstaller\bin\Release\net10.0-windows\c-entron Nexus.msi'
)
foreach ($file in $files) {
if (-not (Test-Path -LiteralPath $file -PathType Leaf)) {
throw "Signed installer not found: $file"
}
$signature = Get-AuthenticodeSignature -LiteralPath $file
if ($signature.Status -ne 'Valid') {
throw "Invalid signature for '$file': $($signature.StatusMessage)"
}
Write-Host "Valid signature: $file"
}
- name: Package Nexus installer
shell: pwsh
run: |
dotnet run `
--project ".\scripts\Scripts\Scripts.csproj" `
-- zip-nexus-singleFile
- name: Upload build artifacts
id: build-artifact
uses: actions/upload-artifact@v7
with:
name: centron-build
path: artifacts/
if-no-files-found: error
retention-days: 14
- name: Upload build version metadata
uses: actions/upload-artifact@v7
with:
name: centron-build-version-${{ steps.version.outputs.version }}
path: ${{ runner.temp }}/centron-build-metadata/version.txt
if-no-files-found: error
retention-days: 14
- name: Add artifact download link
shell: pwsh
run: |
"### Signed build artifacts" >> $env:GITHUB_STEP_SUMMARY
"Build version: ${{ steps.version.outputs.version }}" >> $env:GITHUB_STEP_SUMMARY
"[Download centron-build](${{ steps.build-artifact.outputs.artifact-url }})" >> $env:GITHUB_STEP_SUMMARY
upload-centron-net:
name: Upload c-entron.NET
needs: build
if: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/') }}
permissions:
actions: read
contents: read
id-token: write
uses: NEXOWARE-Systems/ci-cd-reusable-workflows/.github/workflows/upload-software-build.yml@main
with:
artifact_name: centron-build
source_path: c-entron.NET Installer.zip
destination_folder: c-entron.NET
destination_file: c-entron.NET Installer.zip
version: ${{ needs.build.outputs.version }}
main_version: ${{ needs.build.outputs.main_version }}
environment_name: SoftwareBuilds
azure_client_id: ${{ vars.AZURE_CLIENT_ID }}
azure_tenant_id: ${{ vars.AZURE_TENANT_ID }}
upload-web-service:
name: Upload c-entron Web Service
needs: build
if: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/') }}
permissions:
actions: read
contents: read
id-token: write
uses: NEXOWARE-Systems/ci-cd-reusable-workflows/.github/workflows/upload-software-build.yml@main
with:
artifact_name: centron-build
source_path: c-entron Web-Service Installer.zip
destination_folder: c-entron Web-Service
destination_file: c-entron Web-Service Installer.zip
version: ${{ needs.build.outputs.version }}
main_version: ${{ needs.build.outputs.main_version }}
environment_name: SoftwareBuilds
azure_client_id: ${{ vars.AZURE_CLIENT_ID }}
azure_tenant_id: ${{ vars.AZURE_TENANT_ID }}
upload-nexus:
name: Upload c-entron Nexus
needs: build
if: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/') }}
permissions:
actions: read
contents: read
id-token: write
uses: NEXOWARE-Systems/ci-cd-reusable-workflows/.github/workflows/upload-software-build.yml@main
with:
artifact_name: centron-build
source_path: c-entron Nexus Installer.zip
destination_folder: c-entron Nexus
destination_file: c-entron Nexus Installer.zip
version: ${{ needs.build.outputs.version }}
main_version: ${{ needs.build.outputs.main_version }}
environment_name: SoftwareBuilds
azure_client_id: ${{ vars.AZURE_CLIENT_ID }}
azure_tenant_id: ${{ vars.AZURE_TENANT_ID }}
@@ -0,0 +1,117 @@
name: Clean up closed PR artifacts
on:
pull_request_target:
types:
- closed
permissions:
actions: write
pull-requests: read
jobs:
delete-artifacts:
name: Delete PR artifacts
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Delete artifacts from PR workflow runs
uses: actions/github-script@v9
with:
script: |
const { owner, repo } = context.repo;
const pullRequest = context.payload.pull_request;
const workflowRuns = await github.paginate(
github.rest.actions.listWorkflowRunsForRepo,
{
owner,
repo,
event: 'pull_request',
branch: pullRequest.head.ref,
per_page: 100
}
);
const pullRequestRuns = [];
const associatedPullRequestsBySha = new Map();
for (const run of workflowRuns) {
if (!associatedPullRequestsBySha.has(run.head_sha)) {
const associatedPullRequests = await github.paginate(
github.rest.repos.listPullRequestsAssociatedWithCommit,
{
owner,
repo,
commit_sha: run.head_sha,
per_page: 100
}
);
associatedPullRequestsBySha.set(run.head_sha, associatedPullRequests);
}
const belongsToPullRequest = associatedPullRequestsBySha
.get(run.head_sha)
.some(pr => pr.number === pullRequest.number);
if (belongsToPullRequest) {
pullRequestRuns.push(run);
}
}
let deletedArtifacts = 0;
let deletedBytes = 0;
for (const run of pullRequestRuns) {
const artifacts = await github.paginate(
github.rest.actions.listWorkflowRunArtifacts,
{
owner,
repo,
run_id: run.id,
per_page: 100
}
);
for (const artifact of artifacts) {
if (artifact.expired) {
core.info(`Skipping expired artifact ${artifact.name} (${artifact.id}).`);
continue;
}
try {
await github.rest.actions.deleteArtifact({
owner,
repo,
artifact_id: artifact.id
});
deletedArtifacts += 1;
deletedBytes += artifact.size_in_bytes;
core.info(`Deleted artifact ${artifact.name} (${artifact.id}).`);
} catch (error) {
if (error.status === 404) {
core.info(`Artifact ${artifact.name} (${artifact.id}) was already deleted.`);
continue;
}
throw error;
}
}
}
const deletedMiB = (deletedBytes / 1024 / 1024).toFixed(1);
core.info(
`Deleted ${deletedArtifacts} artifact(s) (${deletedMiB} MiB) ` +
`from ${pullRequestRuns.length} workflow run(s) for PR #${pullRequest.number}.`
);
await core.summary
.addHeading('PR artifact cleanup')
.addRaw(`Pull request: #${pullRequest.number}`, true)
.addRaw(`Workflow runs inspected: ${pullRequestRuns.length}`, true)
.addRaw(`Artifacts deleted: ${deletedArtifacts}`, true)
.addRaw(`Storage released: ${deletedMiB} MiB`, true)
.write();
@@ -0,0 +1,269 @@
name: Regression tests
on:
workflow_dispatch:
pull_request:
branches:
- main
- 'release/**'
push:
branches:
- main
- 'release/**'
permissions:
contents: read
# Supersede in-flight runs of this workflow for the same pull request. Pushes to main and
# release branches are excluded so every commit there still gets a full, recorded result.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
regression-tests:
name: End-to-end regression tests
runs-on: ubuntu-24.04
timeout-minutes: 180
env:
DOTNET_NOLOGO: true
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
DOTNET_CLI_TELEMETRY_OPTOUT: true
DevExpress_License: ${{ secrets.DEVEXPRESS_LICENSE }}
ACR_USERNAME: ${{ secrets.ACR_USERNAME }}
ACR_PASSWORD: ${{ secrets.ACR_PASSWORD }}
DB_USERNAME: sa
DB_PASSWORD: SA!password
DB_PORT: 1433
steps:
- name: Check out repository
uses: actions/checkout@v7
with:
fetch-depth: 0
# Verifier.OverrideExpectedFiles = true rewrites the expected files instead of comparing
# against them, so a test that keeps the flag silently verifies nothing. It is only meant
# to be set temporarily while regenerating snapshots and must never be committed.
- name: Check for committed OverrideExpectedFiles
shell: pwsh
run: |
$violations = foreach ($file in Get-ChildItem -Path ./tests -Recurse -Filter *.cs) {
$segments = $file.FullName -split '[\\/]'
if ($segments -contains 'bin' -or $segments -contains 'obj') { continue }
$text = Get-Content -LiteralPath $file.FullName -Raw
if ([string]::IsNullOrEmpty($text)) { continue }
# Blank out block comments, keeping newlines so reported line numbers stay correct.
$text = [regex]::Replace($text, '(?s)/\*.*?\*/', { param($m) $m.Value -replace '[^\r\n]', '' })
$lineNumber = 0
foreach ($line in $text -split '\r?\n') {
$lineNumber++
# The assignment must start the line, optionally behind a dotted receiver such as
# this.Verifier. - anything else in front (other code, // or /// ) means it is not a
# statement. Deliberately not stripping // comments: that would also cut a real
# assignment that follows a string containing '//', turning a false positive into a
# false negative.
if ($line -match '^\s*(?:[A-Za-z_][A-Za-z0-9_.]*\.)?OverrideExpectedFiles\s*=\s*true') {
[pscustomobject]@{ Path = $file.FullName; Line = $lineNumber }
}
}
}
if ($violations) {
foreach ($violation in $violations) {
Write-Host "::error file=$($violation.Path),line=$($violation.Line)::Remove 'OverrideExpectedFiles = true' before committing - the test compares nothing while it is set."
}
throw "Found $(@($violations).Count) committed 'OverrideExpectedFiles = true' assignment(s). Regenerate the expected files locally, then remove the flag."
}
Write-Host 'No committed OverrideExpectedFiles assignments found.'
- name: Set up .NET SDK
uses: actions/setup-dotnet@v5
with:
dotnet-version: 10.0.x
- name: Verify test environment
shell: pwsh
run: |
$sdkVersion = dotnet --version
if ($LASTEXITCODE -ne 0 -or $sdkVersion -notmatch '^10\.0\.') {
throw "Expected a .NET 10 SDK, but resolved '$sdkVersion'."
}
docker version
if ($LASTEXITCODE -ne 0) {
throw 'Docker is unavailable on the self-hosted runner.'
}
- name: Sign in to Azure Container Registry
shell: pwsh
run: |
if ([string]::IsNullOrWhiteSpace($env:ACR_USERNAME) -or
[string]::IsNullOrWhiteSpace($env:ACR_PASSWORD)) {
throw 'ACR_USERNAME and ACR_PASSWORD secrets are required on GitHub-hosted runners.'
}
$env:ACR_PASSWORD | docker login centron.azurecr.io --username $env:ACR_USERNAME --password-stdin
if ($LASTEXITCODE -ne 0) {
throw 'Azure Container Registry login failed.'
}
# Superseded runs are now cancelled mid-test. "Stop regression database" below uses
# if: always() and therefore still runs on cancellation, but a hard runner failure can
# leave a container behind. Drop anything older than the job timeout so leftovers cannot
# pile up on the self-hosted runner. Younger containers may belong to a concurrent run of
# another pull request and are left alone.
- name: Remove stale regression containers
continue-on-error: true
shell: pwsh
run: |
$cutoff = (Get-Date).ToUniversalTime().AddHours(-4)
# The docker name filter is a regex over a substring, so an unanchored pattern would
# also match something like backup-centron-regression-db. '^/?' anchors it and works
# whether the daemon matches the bare name or the internal '/name'.
foreach ($id in @(docker ps --all --quiet --filter 'name=^/?centron-regression-')) {
if ([string]::IsNullOrWhiteSpace($id)) { continue }
$parts = (docker inspect --format '{{.Name}}|{{.Created}}' $id) -split '\|', 2
if ($parts.Count -ne 2) { continue }
$name = $parts[0].TrimStart('/')
$created = $parts[1]
# Second guard: this force-removes containers, so never act on a name that does not
# actually carry the prefix, whatever the daemon's filter semantics happen to be.
if (-not $name.StartsWith('centron-regression-')) { continue }
$parsed = [datetime]::MinValue
$isParsed = [datetime]::TryParse(
$created,
[cultureinfo]::InvariantCulture,
[System.Globalization.DateTimeStyles]::AdjustToUniversal,
[ref] $parsed)
if ($isParsed -and $parsed -lt $cutoff) {
Write-Host "Removing stale regression container $name created at $created."
docker rm --force $id | Out-Null
}
}
$global:LASTEXITCODE = 0
- name: Start regression database
shell: pwsh
run: |
$containerName = "centron-regression-$env:GITHUB_RUN_ID-$env:GITHUB_RUN_ATTEMPT".ToLowerInvariant()
"DB_CONTAINER_NAME=$containerName" >> $env:GITHUB_ENV
docker pull centron.azurecr.io/centron_db/regression_tests:latest
if ($LASTEXITCODE -ne 0) {
throw 'Could not pull the regression database image.'
}
docker run --detach `
--name $containerName `
--env "MSSQL_SA_PASSWORD=$env:DB_PASSWORD" `
--env ACCEPT_EULA=Y `
--env MSSQL_PID=Standard `
--publish "${env:DB_PORT}:1433" `
centron.azurecr.io/centron_db/regression_tests:latest | Out-Null
if ($LASTEXITCODE -ne 0) {
throw 'Could not start the regression database container.'
}
- name: Wait for regression database
shell: pwsh
run: |
for ($attempt = 1; $attempt -le 120; $attempt++) {
$client = [Net.Sockets.TcpClient]::new()
try {
$connected = $client.ConnectAsync('localhost', [int]$env:DB_PORT).Wait(1000)
if ($connected -and $client.Connected) {
Write-Host 'Regression database is reachable.'
exit 0
}
}
catch {
# Database is still starting.
}
finally {
$client.Dispose()
}
Start-Sleep -Seconds 2
}
throw 'Regression database did not become reachable within four minutes.'
- name: Build regression tests
shell: pwsh
run: |
dotnet build `
"./tests/Centron.Tests.EndToEnd/Centron.Tests.EndToEnd.csproj" `
--configuration Release `
--framework net10.0 `
-nodeReuse:false
- name: Run regression tests
shell: pwsh
env:
CENTRON_TESTS_DATABASE_SERVER: localhost,1433
CENTRON_TESTS_DATABASE_USERNAME: sa
CENTRON_TESTS_DATABASE_PASSWORD: SA!password
DATABASE_BACKUP_PATH: /var/opt/mssql/backup/DatabaseBackup.bak
run: |
dotnet test `
"./tests/Centron.Tests.EndToEnd/Centron.Tests.EndToEnd.csproj" `
--configuration Release `
--framework net10.0 `
--no-build `
--logger "trx;LogFileName=TestResults.trx" `
--results-directory "./artifacts/EndToEndTests" `
-nodeReuse:false
- name: Capture database logs
if: always()
continue-on-error: true
shell: pwsh
run: |
New-Item -ItemType Directory -Force -Path './artifacts/EndToEndTests' | Out-Null
docker logs $env:DB_CONTAINER_NAME *>&1 |
Set-Content -LiteralPath './artifacts/EndToEndTests/database.log'
- name: Stop regression database
if: always()
continue-on-error: true
shell: pwsh
run: |
if (-not [string]::IsNullOrWhiteSpace($env:DB_CONTAINER_NAME)) {
docker rm --force $env:DB_CONTAINER_NAME 2>$null
}
$global:LASTEXITCODE = 0
- name: Upload regression results
if: always()
uses: actions/upload-artifact@v7
with:
name: regression-test-results
path: artifacts/EndToEndTests/
if-no-files-found: warn
retention-days: 14
+105
View File
@@ -0,0 +1,105 @@
name: Unit tests
on:
workflow_dispatch:
pull_request:
branches:
- main
- 'release/**'
push:
branches:
- main
- 'release/**'
permissions:
contents: read
# Supersede in-flight runs of this workflow for the same pull request. Pushes to main and
# release branches are excluded so every commit there still gets a full, recorded result.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
test:
name: ${{ matrix.display_name }}
runs-on: windows-2025
timeout-minutes: 120
strategy:
fail-fast: false
matrix:
include:
- name: backend-bl
display_name: Backend BL tests
project: .\tests\backend\Centron.Tests.BL\Centron.Tests.BL.csproj
results_directory: artifacts/tests/backend/bl
- name: backend-dao
display_name: Backend DAO tests
project: .\tests\backend\Centron.Tests.DAO\Centron.Tests.DAO.csproj
results_directory: artifacts/tests/backend/dao
- name: shared-core
display_name: Shared Core tests
project: .\tests\shared\Centron.Tests.Core\Centron.Tests.Core.csproj
results_directory: artifacts/tests/shared/core
- name: nexus
display_name: Nexus tests
project: .\tests\CentronNexusTests\CentronNexusTests.csproj
results_directory: artifacts/tests/nexus
env:
DOTNET_NOLOGO: true
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
DOTNET_CLI_TELEMETRY_OPTOUT: true
CENTRON_BUILD_IS_DEV_BUILD: true
DevExpress_License: ${{ secrets.DEVEXPRESS_LICENSE }}
steps:
- name: Check out repository
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Set up .NET SDK
uses: actions/setup-dotnet@v5
with:
dotnet-version: 10.0.x
- name: Verify .NET SDK
shell: pwsh
run: |
$sdkVersion = dotnet --version
if ($LASTEXITCODE -ne 0 -or $sdkVersion -notmatch '^10\.0\.') {
throw "Expected a .NET 10 SDK, but resolved '$sdkVersion'."
}
Write-Host "Resolved .NET SDK: $sdkVersion"
- name: Set build version
shell: pwsh
run: |
dotnet run `
--project ".\scripts\Centron.Scripts\Centron.Scripts.csproj" `
-- setup-versioning
- name: Run tests
shell: pwsh
run: |
dotnet test "${{ matrix.project }}" `
--configuration Release `
--framework net10.0 `
--logger "trx;LogFileName=TestResults.trx" `
--results-directory "${{ matrix.results_directory }}" `
-nodeReuse:false
- name: Upload test results
if: always()
uses: actions/upload-artifact@v7
with:
name: test-results-${{ matrix.name }}
path: ${{ matrix.results_directory }}/TestResults.trx
if-no-files-found: warn
retention-days: 14
+404
View File
@@ -0,0 +1,404 @@
# Created by https://www.gitignore.io/api/visualstudio
# Edit at https://www.gitignore.io/?templates=visualstudio
### VisualStudio ###
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
*.lscache
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Oo]ut/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# JustCode is a .NET coding add-in
.JustCode
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# End of https://www.gitignore.io/api/visualstudio
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
/.idea
# c-entron Custom Ignores
!src/backend/Centron.Entities/Entities/Import/Log
/deployment/centron/CentronSetupProject/Files
/deployment/centron/WebServiceSetupProject/Files
/deployment/riverbird/RiverbirdWebserviceSetup/Files
/dotnettools
*actual.txt
*AnalyticsUserId.txt
!/nugets/*.nupkg
/.claude/settings.local.json
kind-pike
/.claude/worktrees
BlazorServer/wwwroot/js/index.bundle.js
BlazorServer/NpmJS/src/*.js
BlazorServer/NpmJS/src/*.js.map
BlazorServer/NpmJS/package-lock.json
src/CentronNexus/ProjectManagement/NpmJS/src/*.js
src/CentronNexus/ProjectManagement/NpmJS/src/*.js.map
src/CentronNexus/ProjectManagement/NpmJS/package-lock.json
src/CentronNexus/wwwroot/js/*.bundle.js
src/CentronNexus/Shared/Scripts/src/*.js
src/CentronNexus/Shared/Scripts/src/*.js.map
src/CentronNexus/Shared/Scripts/package-lock.json
/deployment/WixSharpInstaller/Files
/src/CentronNexus/.config/dotnet-tools.json
/src/CentronNexus/Microsoft.CodeAnalysis.Razor.Compiler/Microsoft.NET.Sdk.Razor.SourceGenerators.RazorSourceGenerator/
/.windows-mcp
+26
View File
@@ -0,0 +1,26 @@
{
"version": "0.2.0",
"configurations": [
{
// Use IntelliSense to find out which attributes exist for C# debugging
// Use hover for the description of the existing attributes
// For further information visit https://github.com/dotnet/vscode-csharp/blob/main/debugger-launchjson.md
"name": ".NET Core Launch (console)",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
// If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/src/centron/Centron.WPF.UI/bin/Debug/net8.0-windows/c-entron 2.0.dll",
"args": [],
"cwd": "${workspaceFolder}/src/centron/Centron.WPF.UI",
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
"console": "internalConsole",
"stopAtEntry": false
},
{
"name": ".NET Core Attach",
"type": "coreclr",
"request": "attach"
}
]
}
+41
View File
@@ -0,0 +1,41 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "build",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/src/centron/Centron.WPF.UI/Centron.WPF.UI.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary;ForceNoAlign"
],
"problemMatcher": "$msCompile"
},
{
"label": "publish",
"command": "dotnet",
"type": "process",
"args": [
"publish",
"${workspaceFolder}/src/centron/Centron.WPF.UI/Centron.WPF.UI.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary;ForceNoAlign"
],
"problemMatcher": "$msCompile"
},
{
"label": "watch",
"command": "dotnet",
"type": "process",
"args": [
"watch",
"run",
"--project",
"${workspaceFolder}/src/centron/Centron.WPF.UI/Centron.WPF.UI.csproj"
],
"problemMatcher": "$msCompile"
}
]
}
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\src\backend\Centron.Interfaces\Centron.Interfaces.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,131 @@
using Centron.Api.docuFORM.Helper;
using Centron.Api.docuFORM.Models;
using Centron.Api.docuFORM.Models.Swagger;
using Centron.Api.docuFORM.Models.Swagger.Responses;
using Centron.Core;
using Centron.Core.Extensions;
using Centron.Interfaces.BL;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Reflection;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Centron.Api.docuFORM
{
public class DocuFormRestApiClient : IDocuFormApiClient, IDisposable
{
#region Fields
private readonly HttpClient _httpClient;
private bool _disposed = false;
#endregion
#region Ctor
public DocuFormRestApiClient(string serverAddress)
{
try
{
var trimmedAddress = serverAddress.TrimEnd('/');
var handler = new SocketsHttpHandler() { MaxConnectionsPerServer = 20 };
this._httpClient = new HttpClient(handler) { BaseAddress = new Uri(trimmedAddress) };
this._httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
}
catch(Exception ex)
{
throw new Exception($"Fehler beim Initalisieren des HTTP-Clients für folgende Adresse \"{serverAddress}\"!{Environment.NewLine}{ex.Message}");
}
}
#endregion
#region Methods
public async Task<string> RequestAuthorization(AuthCodeRequest authCodeRequest)
{
Guard.NotNull(authCodeRequest, nameof(authCodeRequest));
var requestUrl = DocuFormRequestHelper.CreateAuthorizationRequestURI(authCodeRequest);
var response = await this._httpClient.GetAsync(requestUrl);
var contentString = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
throw new HttpRequestException($"Fehler während der Authorisierung bei docuFORM: {contentString}");
return contentString;
}
public async Task<AuthTokenResponse> RequestToken(AuthTokenRequest tokenRequest)
{
Guard.NotNull(tokenRequest, nameof(tokenRequest));
var paremeters = DocuFormRequestHelper.RequestToParameters(tokenRequest);
var request = new HttpRequestMessage(HttpMethod.Post, DocuFormRestApiConstants._endpointAuthToken)
{
Content = new FormUrlEncodedContent(paremeters)
};
var response = await this._httpClient.SendAsync(request);
var responseContentString = await response.ReadContentAndThrowIfError();
var authToken = JsonSerializer.Deserialize<AuthTokenResponse>(responseContentString);
if(authToken == null)
throw new HttpRequestException($"No auth token was received!");
return authToken;
}
public async Task<Result<DevicesResponse>> GetAllDevices(string token)
{
var requestUri = DocuFormRestApiConstants._endpointDevices;
var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
var response = await this._httpClient.SendRequestWithToken(request, token);
return await response.CheckResponseAndDeserializeContent<DevicesResponse>();
}
public async Task<Result<DeviceCountersResponse>> GetDeviceCounters(string token, int deviceId, DateTime? requestedDate = null)
{
var requestUri = DocuFormRestApiConstants._endpointDevices + $"/{deviceId}/counters";
if (requestedDate.HasValue)
{
var utcDate = requestedDate.Value.ToUniversalTime();
requestUri += $"?date={utcDate.ToString("yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture)}";
}
var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
var response = await this._httpClient.SendRequestWithToken(request, token);
return await response.CheckResponseAndDeserializeContent<DeviceCountersResponse>();
}
#endregion
#region IDisposable Support
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed && disposing)
{
_httpClient?.Dispose();
_disposed = true;
}
}
#endregion
}
}
@@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Centron.Api.docuFORM
{
internal static class DocuFormRestApiConstants
{
internal static readonly string _redirectURI = "http://127.0.0.1"; // Don't ad trailing /
internal static readonly string _endpointAuthToken = "/auth/v2/token";
internal static readonly string _endpointAuthCode = "/auth/v2/authorize";
internal static readonly string _endpointDevices = "/dfmserver/v2/devices";
}
}
@@ -0,0 +1,70 @@
using Centron.Api.docuFORM.Models;
using System;
using System.Collections.Generic;
using System.Net;
using System.Reflection;
using System.Text;
using System.Text.Json.Serialization;
namespace Centron.Api.docuFORM.Helper
{
public static class DocuFormRequestHelper
{
public static string CreateAuthorizationRequestURI(AuthCodeRequest authCodeRequest, string? baseUriToAdd = null)
{
var parameters = RequestToParameterString(authCodeRequest);
var endPointAndParameters = DocuFormRestApiConstants._endpointAuthCode + "?" + parameters;
var baseUri = baseUriToAdd != null
? new Uri(baseUriToAdd)
: null;
return baseUri != null
? new Uri(baseUri, endPointAndParameters).ToString()
: endPointAndParameters;
}
public static string RequestToParameterString<T>(T request)
{
var parameters = RequestToParameters(request);
var parameterString = string.Join("&", parameters.Select(f => $"{WebUtility.UrlEncode(f.Key)}={WebUtility.UrlEncode(f.Value)}"));
return parameterString;
}
public static Dictionary<string, string> RequestToParameters<T>(T request, bool removeParameterWithoutValue = true)
{
var dict = new Dictionary<string, string>();
var props = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
foreach (var prop in props)
{
var attr = prop.GetCustomAttribute<JsonPropertyNameAttribute>();
var key = attr?.Name ?? prop.Name;
var value = prop.GetValue(request)?.ToString();
if(removeParameterWithoutValue && string.IsNullOrWhiteSpace(value))
continue;
dict[key] = value ?? string.Empty;
}
return dict;
}
public static string CreateRedirectURI(int? portNumber = null)
{
var uriBuilder = new UriBuilder(DocuFormRestApiConstants._redirectURI);
uriBuilder.Port = portNumber.HasValue && portNumber.Value > 0
? portNumber.Value
: -1;
var uri = uriBuilder.ToString();
while(uri.EndsWith("/"))
uri = uri.Substring(0, uri.Length - 1);
return uri;
}
}
}
@@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Net.Http.Headers;
using System.Text;
namespace Centron.Api.docuFORM.Helper
{
public static class HttpClientExtensions
{
public static async Task<HttpResponseMessage> SendRequestWithToken(this HttpClient httpClient, HttpRequestMessage httpRequest, string token)
{
httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
if (httpRequest.Headers.Accept.Count == 0)
httpRequest.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
return await httpClient.SendAsync(httpRequest).ConfigureAwait(false);
}
}
}
@@ -0,0 +1,50 @@
using Centron.Api.docuFORM.Models.Swagger.Responses;
using Centron.Interfaces.BL;
using System;
using System.Collections.Generic;
using System.Net;
using System.Text;
using System.Text.Json;
namespace Centron.Api.docuFORM.Helper
{
public static class HttpResponseMessageExtensions
{
public static async Task<string> ReadContentAndThrowIfError(this HttpResponseMessage? response)
{
if(response == null)
throw new HttpRequestException ($"No http response received!");
var content = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
throw new HttpRequestException ($"An error occured during the http request: {content}");
return content;
}
public static async Task<Result<T>> CheckResponseAndDeserializeContent<T>(this HttpResponseMessage? response)
{
try
{
if(response == null)
throw new HttpRequestException ($"No http response received!");
var content = await ReadContentAndThrowIfError(response);
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
var obj = JsonSerializer.Deserialize<T>(content, options);
if(obj != null)
return Result<T>.AsSuccess(obj);
return Result<T>.AsError("Unable to deserialize object!");
}
catch (Exception ex)
{
return Result<T>.FromException(ex);
}
}
}
}
@@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
namespace Centron.Api.docuFORM.Helper
{
public static class OAuthHelper
{
// Can be used to generate a "state" or the codeVerifier for the "code challenge"
public static string GenerateRandomBase64String(int byteLength = 32)
{
byte[] randomBytes = new byte[byteLength];
using (var rng = RandomNumberGenerator.Create())
{
rng.GetBytes(randomBytes);
}
// Base64 URL-safe encoding
string state = Convert.ToBase64String(randomBytes)
.Replace("+", "-")
.Replace("/", "_")
.Replace("=", "");
return state;
}
public static string GenerateCodeChallenge(string codeVerifier)
{
using (var sha256 = SHA256.Create())
{
byte[] hash = sha256.ComputeHash(Encoding.ASCII.GetBytes(codeVerifier));
string codeChallenge = Convert.ToBase64String(hash)
.Replace("+", "-")
.Replace("/", "_")
.Replace("=", "");
return codeChallenge;
}
}
}
}
@@ -0,0 +1,21 @@
using Centron.Api.docuFORM.Models;
using Centron.Api.docuFORM.Models.Swagger;
using Centron.Api.docuFORM.Models.Swagger.Responses;
using Centron.Interfaces.BL;
using System;
using System.Collections.Generic;
using System.Text;
namespace Centron.Api.docuFORM
{
public interface IDocuFormApiClient
{
public Task<string> RequestAuthorization(AuthCodeRequest authCodeRequest);
public Task<AuthTokenResponse> RequestToken(AuthTokenRequest tokenRequest);
public Task<Result<DevicesResponse>> GetAllDevices(string token);
public Task<Result<DeviceCountersResponse>> GetDeviceCounters(string token, int deviceId, DateTime? requestedDate);
}
}
@@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.Json.Serialization;
namespace Centron.Api.docuFORM.Models
{
public class AuthCodeRequest
{
[JsonPropertyName("client_id")]
public string ClientId { get; set; } = string.Empty;
[JsonPropertyName("response_type")]
public string ResponseType { get; set; } = "code";
[JsonPropertyName("scope")]
public string Scope { get; set; } = string.Empty;
[JsonPropertyName("redirect_uri")]
public string RedirectUri { get; set; } = "http://127.0.0.1";
[JsonPropertyName("authMode")]
public string AuthMode { get; set; } = string.Empty;
[JsonPropertyName("state")]
public string State { get; set; } = string.Empty;
[JsonPropertyName("code_challenge_method")]
public string CodeChallengeMethod { get; set; } = "S256";
[JsonPropertyName("code_challenge")]
public string CodeChallenge { get; set; } = string.Empty;
[JsonPropertyName("infotext")]
public string InfoText { get; set; } = string.Empty;
[JsonPropertyName("username")]
public string UserName { get; set; } = string.Empty;
}
}
@@ -0,0 +1,39 @@
using System.Text.Json.Serialization;
namespace Centron.Api.docuFORM.Models.Swagger;
public class AuthTokenRequest
{
[JsonPropertyName("grant_type")]
public string GrantType { get; set; } = string.Empty;
[JsonPropertyName("client_id")]
public string ClientId { get; set; } = string.Empty;
[JsonPropertyName("client_secret")]
public string? ClientSecret { get; set; }
[JsonPropertyName("username")]
public string? Username { get; set; }
[JsonPropertyName("password")]
public string? Password { get; set; }
[JsonPropertyName("refresh_token")]
public string? RefreshToken { get; set; }
[JsonPropertyName("code")]
public string? Code { get; set; }
[JsonPropertyName("code_verifier")]
public string? CodeVerifier { get; set; }
[JsonPropertyName("scope")]
public string? Scope { get; set; }
[JsonPropertyName("redirect_uri")]
public string? RedirectUri { get; set; }
[JsonPropertyName("client_deviceid")]
public string? ClientDeviceId { get; set; }
}
@@ -0,0 +1,27 @@
using System.Text.Json.Serialization;
namespace Centron.Api.docuFORM.Models.Swagger;
public class AuthTokenResponse
{
[JsonPropertyName("access_token")]
public string? AccessToken { get; set; }
[JsonPropertyName("refresh_token")]
public string? RefreshToken { get; set; }
[JsonPropertyName("expires_in")]
public long ExpiresIn { get; set; }
[JsonPropertyName("token_type")]
public string? TokenType { get; set; }
[JsonPropertyName("server_api_url")]
public string? ServerApiUrl { get; set; }
[JsonPropertyName("client_api_url")]
public string? ClientApiUrl { get; set; }
[JsonPropertyName("user_id")]
public int UserId { get; set; }
}
@@ -0,0 +1,30 @@
using System.Text.Json.Serialization;
namespace Centron.Api.docuFORM.Models.Swagger;
public class Customer : CustomerCreate
{
[JsonPropertyName("id")]
public int Id { get; set; }
[JsonPropertyName("uuid")]
public string? Uuid { get; set; }
[JsonPropertyName("dealerUuid")]
public string? DealerUuid { get; set; }
[JsonPropertyName("created")]
public DateTime? Created { get; set; }
[JsonPropertyName("createdBy")]
public string? CreatedBy { get; set; }
[JsonPropertyName("restApiClientUuid")]
public string? RestApiClientUuid { get; set; }
[JsonPropertyName("lastLogin")]
public DateTime? LastLogin { get; set; }
[JsonPropertyName("productVersion")]
public string? ProductVersion { get; set; }
}
@@ -0,0 +1,13 @@
using System.Text.Json.Serialization;
namespace Centron.Api.docuFORM.Models.Swagger;
public class CustomerCreate : CustomerUpdate
{
[JsonPropertyName("dealerId")]
public int DealerId { get; set; }
/// <summary>Customer name. Shadows <see cref="UserProperties.Name"/> (the user login name).</summary>
[JsonPropertyName("name")]
public new string Name { get; set; } = string.Empty;
}
@@ -0,0 +1,15 @@
using System.Text.Json.Serialization;
namespace Centron.Api.docuFORM.Models.Swagger;
public class CustomerDeployment
{
[JsonPropertyName("mcsCollectionUuid")]
public string? McsCollectionUuid { get; set; }
[JsonPropertyName("mcsPassword")]
public string? McsPassword { get; set; }
[JsonPropertyName("mcsOauthHost")]
public string? McsOauthHost { get; set; }
}
@@ -0,0 +1,30 @@
using System.Text.Json.Serialization;
namespace Centron.Api.docuFORM.Models.Swagger;
public class CustomerRights
{
[JsonPropertyName("dashboard")]
public bool? Dashboard { get; set; }
[JsonPropertyName("reports")]
public bool? Reports { get; set; }
[JsonPropertyName("eshop")]
public bool? Eshop { get; set; }
[JsonPropertyName("customerThresholds")]
public bool? CustomerThresholds { get; set; }
[JsonPropertyName("customerAlerts")]
public bool? CustomerAlerts { get; set; }
[JsonPropertyName("deletePrinters")]
public bool? DeletePrinters { get; set; }
[JsonPropertyName("contractManagement")]
public bool? ContractManagement { get; set; }
[JsonPropertyName("deployment")]
public bool? Deployment { get; set; }
}
@@ -0,0 +1,21 @@
using System.Text.Json.Serialization;
namespace Centron.Api.docuFORM.Models.Swagger;
public class CustomerUpdate : UserProperties
{
[JsonPropertyName("password")]
public string? Password { get; set; }
[JsonPropertyName("submitPassword")]
public string? SubmitPassword { get; set; }
[JsonPropertyName("f2pControl")]
public string? F2pControl { get; set; }
[JsonPropertyName("rights")]
public CustomerRights? Rights { get; set; }
[JsonPropertyName("deployment")]
public CustomerDeployment? Deployment { get; set; }
}
@@ -0,0 +1,24 @@
using System.Text.Json.Serialization;
namespace Centron.Api.docuFORM.Models.Swagger;
public class Dealer : UserProperties
{
[JsonPropertyName("id")]
public int Id { get; set; }
[JsonPropertyName("uuid")]
public string? Uuid { get; set; }
[JsonPropertyName("created")]
public DateTime? Created { get; set; }
[JsonPropertyName("createdBy")]
public string? CreatedBy { get; set; }
[JsonPropertyName("restApiClientUuid")]
public string? RestApiClientUuid { get; set; }
[JsonPropertyName("lastLogin")]
public DateTime? LastLogin { get; set; }
}
@@ -0,0 +1,73 @@
using System.Text.Json.Serialization;
namespace Centron.Api.docuFORM.Models.Swagger;
public class Device : DeviceCreate
{
// DeviceProperties (server-populated read fields)
[JsonPropertyName("id")]
public int Id { get; set; }
[JsonPropertyName("uuid")]
public string? Uuid { get; set; }
[JsonPropertyName("macAddress")]
public string? MacAddress { get; set; }
[JsonPropertyName("serialNumber")]
public string? SerialNumber { get; set; }
[JsonPropertyName("ipAddress")]
public string? IpAddress { get; set; }
[JsonPropertyName("hostAddressDate")]
public DateTime? HostAddressDate { get; set; }
[JsonPropertyName("contractId")]
public int? ContractId { get; set; }
[JsonPropertyName("createdOn")]
public DeviceCreatedOn? CreatedOn { get; set; }
[JsonPropertyName("firmware")]
public string? Firmware { get; set; }
[JsonPropertyName("mibFile")]
public DeviceMibFile? MibFile { get; set; }
[JsonPropertyName("modelName")]
public string? ModelName { get; set; }
[JsonPropertyName("vendorName")]
public string? VendorName { get; set; }
[JsonPropertyName("vendorId")]
public int? VendorId { get; set; }
[JsonPropertyName("systemName")]
public string? SystemName { get; set; }
[JsonPropertyName("pageCount")]
public int? PageCount { get; set; }
[JsonPropertyName("properties")]
public List<string>? Properties { get; set; }
[JsonPropertyName("outputDimensions")]
public DeviceFeatureOutputDimensions? OutputDimensions { get; set; }
[JsonPropertyName("technicalSpecs")]
public DeviceFeatureTecSpecs? TechnicalSpecs { get; set; }
[JsonPropertyName("numOutputTrays")]
public int? NumOutputTrays { get; set; }
[JsonPropertyName("numPaperTrays")]
public int? NumPaperTrays { get; set; }
[JsonPropertyName("resolution")]
public string? Resolution { get; set; }
[JsonPropertyName("paperTrays")]
public List<DevicePaperTray>? PaperTrays { get; set; }
}
@@ -0,0 +1,24 @@
using System.Text.Json.Serialization;
namespace Centron.Api.docuFORM.Models.Swagger;
public class DeviceCiData
{
[JsonPropertyName("articleNumber")]
public string? ArticleNumber { get; set; }
[JsonPropertyName("ciSerial")]
public string? CiSerial { get; set; }
[JsonPropertyName("exceptionFleet")]
public bool ExceptionFleet { get; set; }
[JsonPropertyName("mpsClientId")]
public string? MpsClientId { get; set; }
[JsonPropertyName("npsServerId")]
public string? NpsServerId { get; set; }
[JsonPropertyName("sla")]
public string? Sla { get; set; }
}
@@ -0,0 +1,18 @@
using System.Text.Json.Serialization;
namespace Centron.Api.docuFORM.Models.Swagger;
public class DeviceCounters
{
[JsonPropertyName("deviceId")]
public int DeviceId { get; set; }
[JsonPropertyName("timestamp")]
public DateTime? Timestamp { get; set; }
[JsonPropertyName("standardCounters")]
public Dictionary<string, int>? StandardCounters { get; set; }
[JsonPropertyName("extendedCounters")]
public Dictionary<string, int>? ExtendedCounters { get; set; }
}
@@ -0,0 +1,15 @@
using System.Text.Json.Serialization;
namespace Centron.Api.docuFORM.Models.Swagger;
public class DeviceCreate : DeviceUpdate
{
[JsonPropertyName("dealerId")]
public int DealerId { get; set; }
[JsonPropertyName("customerId")]
public int CustomerId { get; set; }
[JsonPropertyName("hostAddress")]
public string HostAddress { get; set; } = string.Empty;
}
@@ -0,0 +1,12 @@
using System.Text.Json.Serialization;
namespace Centron.Api.docuFORM.Models.Swagger;
public class DeviceCreatedOn
{
[JsonPropertyName("client")]
public DateTime? Client { get; set; }
[JsonPropertyName("server")]
public DateTime? Server { get; set; }
}
@@ -0,0 +1,42 @@
using System.Text.Json.Serialization;
namespace Centron.Api.docuFORM.Models.Swagger;
public class DeviceEvent
{
[JsonPropertyName("id")]
public int? Id { get; set; }
[JsonPropertyName("eventId")]
public string? EventId { get; set; }
[JsonPropertyName("time")]
public DateTime? Time { get; set; }
[JsonPropertyName("notified")]
public bool? Notified { get; set; }
[JsonPropertyName("clearDate")]
public DateTime? ClearDate { get; set; }
[JsonPropertyName("contractId")]
public int? ContractId { get; set; }
[JsonPropertyName("deviceId")]
public int DeviceId { get; set; }
[JsonPropertyName("message")]
public string Message { get; set; } = string.Empty;
[JsonPropertyName("key")]
public string? Key { get; set; }
[JsonPropertyName("pageCount")]
public int? PageCount { get; set; }
[JsonPropertyName("severity")]
public string? Severity { get; set; }
[JsonPropertyName("source")]
public string? Source { get; set; }
}
@@ -0,0 +1,15 @@
using System.Text.Json.Serialization;
namespace Centron.Api.docuFORM.Models.Swagger;
public class DeviceEvents
{
[JsonPropertyName("deviceId")]
public int DeviceId { get; set; }
[JsonPropertyName("events")]
public List<DeviceEvent>? Events { get; set; }
[JsonPropertyName("nextPageToken")]
public string? NextPageToken { get; set; }
}
@@ -0,0 +1,16 @@
using System.Text.Json.Serialization;
namespace Centron.Api.docuFORM.Models.Swagger;
/// <summary>Media feed dimensions of the device.</summary>
public class DeviceFeatureOutputDimensions
{
[JsonPropertyName("maxMediaFeedDimension")]
public long? MaxMediaFeedDimension { get; set; }
[JsonPropertyName("maxMediaXFeedDimension")]
public long? MaxMediaXFeedDimension { get; set; }
[JsonPropertyName("northMargin")]
public long? NorthMargin { get; set; }
}
@@ -0,0 +1,28 @@
using System.Text.Json.Serialization;
namespace Centron.Api.docuFORM.Models.Swagger;
/// <summary>Technical specifications of the device.</summary>
public class DeviceFeatureTecSpecs
{
[JsonPropertyName("copiesByDriver")]
public int? CopiesByDriver { get; set; }
[JsonPropertyName("externalPSGeneration")]
public int? ExternalPsGeneration { get; set; }
[JsonPropertyName("feedResolution")]
public int? FeedResolution { get; set; }
[JsonPropertyName("paperTrayFormatIds")]
public string? PaperTrayFormatIds { get; set; }
[JsonPropertyName("securePinPrinting")]
public string? SecurePinPrinting { get; set; }
[JsonPropertyName("supportedPDL")]
public string? SupportedPdl { get; set; }
[JsonPropertyName("xFeedResolution")]
public int? XFeedResolution { get; set; }
}
@@ -0,0 +1,27 @@
using System.Text.Json.Serialization;
namespace Centron.Api.docuFORM.Models.Swagger;
public class DeviceFeatures
{
[JsonPropertyName("deviceId")]
public int DeviceId { get; set; }
[JsonPropertyName("properties")]
public List<string>? Properties { get; set; }
[JsonPropertyName("outputDimensions")]
public DeviceFeatureOutputDimensions? OutputDimensions { get; set; }
[JsonPropertyName("technicalSpecs")]
public DeviceFeatureTecSpecs? TechnicalSpecs { get; set; }
[JsonPropertyName("outputTrays")]
public int? OutputTrays { get; set; }
[JsonPropertyName("paperTrays")]
public int? PaperTrays { get; set; }
[JsonPropertyName("resolution")]
public string? Resolution { get; set; }
}
@@ -0,0 +1,24 @@
using System.Text.Json.Serialization;
namespace Centron.Api.docuFORM.Models.Swagger;
public class DeviceItem
{
[JsonPropertyName("supplyIndex")]
public int SupplyIndex { get; set; }
[JsonPropertyName("installationIncluded")]
public bool? InstallationIncluded { get; set; }
[JsonPropertyName("isRecommended")]
public bool? IsRecommended { get; set; }
[JsonPropertyName("isAttached")]
public bool? IsAttached { get; set; }
[JsonPropertyName("isOriginal")]
public bool? IsOriginal { get; set; }
[JsonPropertyName("item")]
public Item? Item { get; set; }
}
@@ -0,0 +1,12 @@
using System.Text.Json.Serialization;
namespace Centron.Api.docuFORM.Models.Swagger;
public class DeviceItemCreate : ItemCreateProperties
{
[JsonPropertyName("supplyIndex")]
public int SupplyIndex { get; set; }
[JsonPropertyName("installationIncluded")]
public bool? InstallationIncluded { get; set; }
}
@@ -0,0 +1,12 @@
using System.Text.Json.Serialization;
namespace Centron.Api.docuFORM.Models.Swagger;
public class DeviceMibFile
{
[JsonPropertyName("name")]
public string? Name { get; set; }
[JsonPropertyName("version")]
public string? Version { get; set; }
}
@@ -0,0 +1,22 @@
using System.Text.Json.Serialization;
namespace Centron.Api.docuFORM.Models.Swagger;
public class DeviceNotifications
{
[JsonPropertyName("alertName")]
public string? AlertName { get; set; }
[JsonPropertyName("thresholdName")]
public string? ThresholdName { get; set; }
[JsonPropertyName("thresholdAction")]
public int? ThresholdAction { get; set; }
/// <summary>Note: the API uses "tresholdReceiver" (typo) in the write schema but "thresholdReceiver" in read.</summary>
[JsonPropertyName("thresholdReceiver")]
public string? ThresholdReceiver { get; set; }
[JsonPropertyName("thresholdSubject")]
public string? ThresholdSubject { get; set; }
}
@@ -0,0 +1,30 @@
using System.Text.Json.Serialization;
namespace Centron.Api.docuFORM.Models.Swagger;
public class DevicePaperTray
{
[JsonPropertyName("index")]
public int Index { get; set; }
[JsonPropertyName("name")]
public string? Name { get; set; }
[JsonPropertyName("description")]
public string? Description { get; set; }
[JsonPropertyName("formatId")]
public int? FormatId { get; set; }
[JsonPropertyName("formatName")]
public string? FormatName { get; set; }
[JsonPropertyName("mediaDimX")]
public int? MediaDimX { get; set; }
[JsonPropertyName("mediaDimY")]
public int? MediaDimY { get; set; }
[JsonPropertyName("mediaLevelPcnt")]
public int? MediaLevelPcnt { get; set; }
}
@@ -0,0 +1,12 @@
using System.Text.Json.Serialization;
namespace Centron.Api.docuFORM.Models.Swagger;
public class DevicePaperTrays
{
[JsonPropertyName("deviceId")]
public int DeviceId { get; set; }
[JsonPropertyName("paperTrays")]
public List<DevicePaperTray>? PaperTrays { get; set; }
}
@@ -0,0 +1,22 @@
using System.Text.Json.Serialization;
namespace Centron.Api.docuFORM.Models.Swagger;
/// <summary>SNMP configuration as returned in Device read responses.</summary>
public class DeviceSnmp
{
[JsonPropertyName("community")]
public string? Community { get; set; }
[JsonPropertyName("contextName")]
public string? ContextName { get; set; }
[JsonPropertyName("portNumber")]
public int? PortNumber { get; set; }
[JsonPropertyName("securityName")]
public string? SecurityName { get; set; }
[JsonPropertyName("version")]
public int? Version { get; set; }
}
@@ -0,0 +1,18 @@
using System.Text.Json.Serialization;
namespace Centron.Api.docuFORM.Models.Swagger;
public class DeviceSnmpData
{
[JsonPropertyName("contact")]
public string? Contact { get; set; }
[JsonPropertyName("location")]
public string? Location { get; set; }
[JsonPropertyName("overwritable")]
public bool Overwritable { get; set; }
[JsonPropertyName("queriedOn")]
public DateTime? QueriedOn { get; set; }
}
@@ -0,0 +1,18 @@
using System.Text.Json.Serialization;
namespace Centron.Api.docuFORM.Models.Swagger;
public class DeviceSummary
{
[JsonPropertyName("name")]
public string? Name { get; set; }
[JsonPropertyName("lastUpdate")]
public DateTime? LastUpdate { get; set; }
[JsonPropertyName("warning")]
public bool Warning { get; set; }
[JsonPropertyName("groups")]
public List<DeviceSummaryGroup>? Groups { get; set; }
}
@@ -0,0 +1,16 @@
using System.Text.Json.Serialization;
namespace Centron.Api.docuFORM.Models.Swagger;
public class DeviceSummaryCounterElement
{
/// <summary>Type discriminator — value is 2 for counter elements.</summary>
[JsonPropertyName("type")]
public int? Type { get; set; }
[JsonPropertyName("title")]
public string? Title { get; set; }
[JsonPropertyName("value")]
public string? Value { get; set; }
}
@@ -0,0 +1,17 @@
using System.Text.Json.Serialization;
namespace Centron.Api.docuFORM.Models.Swagger;
public class DeviceSummaryGroup
{
[JsonPropertyName("title")]
public string? Title { get; set; }
/// <summary>
/// Each element is either a <see cref="DeviceSummarySupplyElement"/> (type=1) or a
/// <see cref="DeviceSummaryCounterElement"/> (type=2). Deserialized as generic objects;
/// check the "type" field to cast accordingly.
/// </summary>
[JsonPropertyName("elements")]
public List<object>? Elements { get; set; }
}
@@ -0,0 +1,25 @@
using System.Text.Json.Serialization;
namespace Centron.Api.docuFORM.Models.Swagger;
public class DeviceSummarySupplyElement
{
/// <summary>Type discriminator — value is 1 for supply elements.</summary>
[JsonPropertyName("type")]
public int? Type { get; set; }
[JsonPropertyName("hex")]
public string? Hex { get; set; }
[JsonPropertyName("title")]
public string? Title { get; set; }
[JsonPropertyName("subtitle")]
public string? Subtitle { get; set; }
[JsonPropertyName("warning")]
public bool? Warning { get; set; }
[JsonPropertyName("value")]
public string? Value { get; set; }
}
@@ -0,0 +1,12 @@
using System.Text.Json.Serialization;
namespace Centron.Api.docuFORM.Models.Swagger;
public class DeviceSupplies
{
[JsonPropertyName("deviceId")]
public int DeviceId { get; set; }
[JsonPropertyName("supplies")]
public List<DeviceSupply>? Supplies { get; set; }
}
@@ -0,0 +1,60 @@
using System.Text.Json.Serialization;
namespace Centron.Api.docuFORM.Models.Swagger;
public class DeviceSupply
{
[JsonPropertyName("attachedItemId")]
public int? AttachedItemId { get; set; }
[JsonPropertyName("capacity")]
public int? Capacity { get; set; }
[JsonPropertyName("class")]
public int? Class { get; set; }
[JsonPropertyName("colorant")]
public string? Colorant { get; set; }
[JsonPropertyName("index")]
public int Index { get; set; }
[JsonPropertyName("level")]
public int? Level { get; set; }
[JsonPropertyName("levelCorrectionMode")]
public string? LevelCorrectionMode { get; set; }
[JsonPropertyName("name")]
public string? Name { get; set; }
[JsonPropertyName("orderState")]
public string? OrderState { get; set; }
[JsonPropertyName("originalPartNo")]
public string? OriginalPartNo { get; set; }
[JsonPropertyName("percent")]
public int? Percent { get; set; }
[JsonPropertyName("remainingPages")]
public int? RemainingPages { get; set; }
[JsonPropertyName("remainingDays")]
public int? RemainingDays { get; set; }
[JsonPropertyName("thresholdReached")]
public bool? ThresholdReached { get; set; }
[JsonPropertyName("timestamp")]
public DateTime? Timestamp { get; set; }
[JsonPropertyName("type")]
public int? Type { get; set; }
[JsonPropertyName("unit")]
public int? Unit { get; set; }
[JsonPropertyName("value")]
public string? Value { get; set; }
}
@@ -0,0 +1,78 @@
using System.Text.Json.Serialization;
namespace Centron.Api.docuFORM.Models.Swagger;
public class DeviceUpdate
{
[JsonPropertyName("isActive")]
public bool? IsActive { get; set; }
[JsonPropertyName("isManaged")]
public bool? IsManaged { get; set; }
[JsonPropertyName("isLicensed")]
public bool? IsLicensed { get; set; }
[JsonPropertyName("assetNumber")]
public string? AssetNumber { get; set; }
[JsonPropertyName("cardReaderAddress")]
public string? CardReaderAddress { get; set; }
[JsonPropertyName("ciid")]
public string? Ciid { get; set; }
[JsonPropertyName("ciData")]
public DeviceCiData? CiData { get; set; }
[JsonPropertyName("costCenter")]
public string? CostCenter { get; set; }
[JsonPropertyName("description")]
public string? Description { get; set; }
[JsonPropertyName("follow2Print")]
public bool? Follow2Print { get; set; }
[JsonPropertyName("information")]
public List<string>? Information { get; set; }
[JsonPropertyName("inventoryNumber")]
public string? InventoryNumber { get; set; }
[JsonPropertyName("itemDeliveryAddress")]
public string? ItemDeliveryAddress { get; set; }
[JsonPropertyName("itemDeliveryNote")]
public string? ItemDeliveryNote { get; set; }
[JsonPropertyName("itemDeliveryInformation")]
public List<string>? ItemDeliveryInformation { get; set; }
[JsonPropertyName("locationDescription")]
public string? LocationDescription { get; set; }
[JsonPropertyName("locationLevels")]
public List<string>? LocationLevels { get; set; }
[JsonPropertyName("notifications")]
public DeviceNotifications? Notifications { get; set; }
[JsonPropertyName("officePrint")]
public bool? OfficePrint { get; set; }
[JsonPropertyName("productionProcess")]
public bool? ProductionProcess { get; set; }
[JsonPropertyName("securePrint")]
public bool? SecurePrint { get; set; }
[JsonPropertyName("snmp")]
public DeviceSnmp? Snmp { get; set; }
[JsonPropertyName("snmpData")]
public DeviceSnmpData? SnmpData { get; set; }
[JsonPropertyName("warranty")]
public DeviceWarranty? Warranty { get; set; }
}
@@ -0,0 +1,15 @@
using System.Text.Json.Serialization;
namespace Centron.Api.docuFORM.Models.Swagger;
public class DeviceWarranty
{
[JsonPropertyName("start")]
public DateTime? Start { get; set; }
[JsonPropertyName("months")]
public int? Months { get; set; }
[JsonPropertyName("clicks")]
public int? Clicks { get; set; }
}
@@ -0,0 +1,18 @@
using System.Text.Json.Serialization;
namespace Centron.Api.docuFORM.Models.Swagger;
public class Item : ItemCreateProperties
{
[JsonPropertyName("id")]
public int Id { get; set; }
[JsonPropertyName("colorant")]
public string? Colorant { get; set; }
[JsonPropertyName("supplyType")]
public int? SupplyType { get; set; }
[JsonPropertyName("isColorMarker")]
public bool? IsColorMarker { get; set; }
}
@@ -0,0 +1,42 @@
using System.Text.Json.Serialization;
namespace Centron.Api.docuFORM.Models.Swagger;
public class ItemCreateProperties
{
[JsonPropertyName("name")]
public string Name { get; set; } = string.Empty;
[JsonPropertyName("description")]
public string? Description { get; set; }
[JsonPropertyName("manufacturer")]
public string? Manufacturer { get; set; }
[JsonPropertyName("partNo")]
public string PartNo { get; set; } = string.Empty;
[JsonPropertyName("articleNo")]
public string ArticleNo { get; set; } = string.Empty;
[JsonPropertyName("originalPartNo")]
public string OriginalPartNo { get; set; } = string.Empty;
[JsonPropertyName("isKit")]
public bool? IsKit { get; set; }
[JsonPropertyName("pages")]
public int? Pages { get; set; }
[JsonPropertyName("percentage")]
public double? Percentage { get; set; }
[JsonPropertyName("price")]
public double? Price { get; set; }
[JsonPropertyName("currency")]
public string? Currency { get; set; }
[JsonPropertyName("unit")]
public string? Unit { get; set; }
}
@@ -0,0 +1,35 @@
using System.Text.Json.Serialization;
namespace Centron.Api.docuFORM.Models.Swagger;
public class JobCreate
{
[JsonPropertyName("ipAddress")]
public string IpAddress { get; set; } = string.Empty;
[JsonPropertyName("cardId")]
public string CardId { get; set; } = string.Empty;
/// <summary>1=SmartCard, 2=PIN, 3=Fingerprint, 4=Username.</summary>
[JsonPropertyName("cardType")]
public int CardType { get; set; }
[JsonPropertyName("jobName")]
public string JobName { get; set; } = string.Empty;
[JsonPropertyName("billingCode")]
public int? BillingCode { get; set; }
/// <summary>20=Copies, 21=Scan, 22=Fax.</summary>
[JsonPropertyName("dataType")]
public int DataType { get; set; }
[JsonPropertyName("stapled")]
public bool? Stapled { get; set; }
[JsonPropertyName("punched")]
public bool? Punched { get; set; }
[JsonPropertyName("paperInfos")]
public List<JobCreatePaperInfo>? PaperInfos { get; set; }
}
@@ -0,0 +1,30 @@
using System.Text.Json.Serialization;
namespace Centron.Api.docuFORM.Models.Swagger;
public class JobCreatePaperInfo
{
[JsonPropertyName("paperTray")]
public int? PaperTray { get; set; }
[JsonPropertyName("windowsPaperSize")]
public int? WindowsPaperSize { get; set; }
[JsonPropertyName("paperWidth")]
public int? PaperWidth { get; set; }
[JsonPropertyName("paperHeight")]
public int? PaperHeight { get; set; }
[JsonPropertyName("pagesSimplexBw")]
public int PagesSimplexBw { get; set; }
[JsonPropertyName("pagesDuplexBw")]
public int PagesDuplexBw { get; set; }
[JsonPropertyName("pagesSimplexColor")]
public int PagesSimplexColor { get; set; }
[JsonPropertyName("pagesDuplexColor")]
public int PagesDuplexColor { get; set; }
}
@@ -0,0 +1,51 @@
using System.Text.Json.Serialization;
namespace Centron.Api.docuFORM.Models.Swagger;
public class JobRead
{
[JsonPropertyName("access")]
public bool Access { get; set; }
[JsonPropertyName("balance")]
public double? Balance { get; set; }
[JsonPropertyName("billing")]
public bool? Billing { get; set; }
[JsonPropertyName("billingId")]
public int? BillingId { get; set; }
[JsonPropertyName("budget")]
public double? Budget { get; set; }
[JsonPropertyName("clickTimeout")]
public int? ClickTimeout { get; set; }
[JsonPropertyName("currency")]
public string? Currency { get; set; }
[JsonPropertyName("email")]
public string? Email { get; set; }
[JsonPropertyName("fullName")]
public string? FullName { get; set; }
[JsonPropertyName("maxTotalCount")]
public int? MaxTotalCount { get; set; }
[JsonPropertyName("maxColorCount")]
public int? MaxColorCount { get; set; }
[JsonPropertyName("private")]
public bool? Private { get; set; }
[JsonPropertyName("senseTimeout")]
public int? SenseTimeout { get; set; }
[JsonPropertyName("userId")]
public int? UserId { get; set; }
[JsonPropertyName("userName")]
public string? UserName { get; set; }
}
@@ -0,0 +1,27 @@
using System.Text.Json.Serialization;
namespace Centron.Api.docuFORM.Models.Swagger;
public class MpsClient
{
[JsonPropertyName("id")]
public int Id { get; set; }
[JsonPropertyName("uuid")]
public string? Uuid { get; set; }
[JsonPropertyName("dealerId")]
public int DealerId { get; set; }
[JsonPropertyName("clientApiUrl")]
public string? ClientApiUrl { get; set; }
[JsonPropertyName("lastTransfer")]
public DateTime? LastTransfer { get; set; }
[JsonPropertyName("lastUpdate")]
public DateTime? LastUpdate { get; set; }
[JsonPropertyName("lastUpdateQuery")]
public DateTime? LastUpdateQuery { get; set; }
}
@@ -0,0 +1,67 @@
using System.Text.Json.Serialization;
namespace Centron.Api.docuFORM.Models.Swagger;
public class Order
{
[JsonPropertyName("id")]
public int Id { get; set; }
[JsonPropertyName("orderNumber")]
public string? OrderNumber { get; set; }
[JsonPropertyName("deviceId")]
public int DeviceId { get; set; }
[JsonPropertyName("supplyIndex")]
public int? SupplyIndex { get; set; }
[JsonPropertyName("quantity")]
public int? Quantity { get; set; }
[JsonPropertyName("purchaseRequisitionNumber")]
public string? PurchaseRequisitionNumber { get; set; }
[JsonPropertyName("installationIncluded")]
public bool? InstallationIncluded { get; set; }
[JsonPropertyName("note")]
public string? Note { get; set; }
[JsonPropertyName("orderDate")]
public DateTime? OrderDate { get; set; }
[JsonPropertyName("orderedBy")]
public string? OrderedBy { get; set; }
[JsonPropertyName("deliveryDate")]
public DateTime? DeliveryDate { get; set; }
[JsonPropertyName("address")]
public string? Address { get; set; }
[JsonPropertyName("deliveryNotes")]
public List<string>? DeliveryNotes { get; set; }
[JsonPropertyName("supplierInfoUrl")]
public string? SupplierInfoUrl { get; set; }
[JsonPropertyName("item")]
public Item? Item { get; set; }
/// <summary>Current order state: notAvailable, proposed, placed, confirmed, shipped, delivered, completed.</summary>
[JsonPropertyName("state")]
public string? State { get; set; }
[JsonPropertyName("requestDate")]
public DateTime? RequestDate { get; set; }
[JsonPropertyName("requestedBy")]
public string? RequestedBy { get; set; }
[JsonPropertyName("mountDate")]
public DateTime? MountDate { get; set; }
[JsonPropertyName("shipmentInfo")]
public OrderShipmentInfo? ShipmentInfo { get; set; }
}
@@ -0,0 +1,64 @@
using System.Text.Json.Serialization;
namespace Centron.Api.docuFORM.Models.Swagger;
public class OrderCreate
{
// Required fields
[JsonPropertyName("deviceId")]
public int DeviceId { get; set; }
[JsonPropertyName("supplyIndex")]
public int SupplyIndex { get; set; }
[JsonPropertyName("quantity")]
public int Quantity { get; set; }
/// <summary>Required. Allowed values: placed, confirmed, shipped, delivered.</summary>
[JsonPropertyName("state")]
public string State { get; set; } = string.Empty;
// Item identification (at least one required)
[JsonPropertyName("itemId")]
public int? ItemId { get; set; }
[JsonPropertyName("articleNo")]
public string? ArticleNo { get; set; }
[JsonPropertyName("partNo")]
public string? PartNo { get; set; }
// Order details
[JsonPropertyName("orderNumber")]
public string? OrderNumber { get; set; }
[JsonPropertyName("purchaseRequisitionNumber")]
public string? PurchaseRequisitionNumber { get; set; }
[JsonPropertyName("installationIncluded")]
public bool? InstallationIncluded { get; set; }
[JsonPropertyName("note")]
public string? Note { get; set; }
[JsonPropertyName("orderDate")]
public DateTime? OrderDate { get; set; }
[JsonPropertyName("orderedBy")]
public string? OrderedBy { get; set; }
[JsonPropertyName("deliveryDate")]
public DateTime? DeliveryDate { get; set; }
[JsonPropertyName("address")]
public string? Address { get; set; }
[JsonPropertyName("deliveryNotes")]
public List<string>? DeliveryNotes { get; set; }
[JsonPropertyName("supplierInfoUrl")]
public string? SupplierInfoUrl { get; set; }
[JsonPropertyName("shipmentInfo")]
public OrderShipmentInfo? ShipmentInfo { get; set; }
}

Some files were not shown because too many files have changed in this diff Show More