# -*- coding: utf-8 -*- """Wertet die erzeugten Anforderungen eines Laufs aus und schreibt einen Protokollabschnitt. Aufruf: python analyse-anforderungen.py [--print] Erzeugt `/_meta/anforderungen.md` (der fertige Abschnitt) und `/_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'), 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, ''] 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()