Files
Masterarbeit/.claude/skills/run-experiment/analyse-anforderungen.py
T
2026-08-26 16:37:24 +02:00

247 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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)
def bloecke(pfad):
"""Zerlegt eine Anforderungsdatei in Bloecke ab jeder ID:-Zeile."""
if not os.path.exists(pfad):
return
text = io.open(pfad, encoding='utf-8').read()
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
for eb in EBENEN:
if aid.strip().upper().startswith(eb.upper()):
return eb, aid.strip().upper()[:4] != datei_ebene.upper()[:4]
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()