173 lines
7.7 KiB
Python
173 lines
7.7 KiB
Python
# -*- 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()
|