V01 Erste Runs
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
# -*- 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 analysiere(lauf):
|
||||
erg = os.path.join(lauf, 'Ergebnisse')
|
||||
anf = []
|
||||
for eb in EBENEN:
|
||||
for b in bloecke(os.path.join(erg, eb + '.md')):
|
||||
aid = feld(b, 'ID')
|
||||
if not aid:
|
||||
continue
|
||||
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, 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'),
|
||||
))
|
||||
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']]
|
||||
|
||||
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`.', '',
|
||||
'### 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, '']
|
||||
|
||||
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 += ['### 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('| **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()
|
||||
Reference in New Issue
Block a user