# Testinventar — was ein grüner CI-Lauf beweist und was nicht

Gemessen am Commit `7b2429a`, Läufe `31010683025` (`portability-matrix.yml`) und
`31010683096` (`rls.yml`). Beide Läufe tragen `headSha = 7b2429a`; nachprüfbar
über `gh run list --branch main --json headSha`.

Bewusst **nicht** „gemessen gegen `main`" — eine Messung gilt für einen Commit,
nicht für einen bewegten Zeiger. Die erste Fassung schrieb die Gleichung hin und
war damit falsch, sobald der nächste Merge lief. Wer die Zahlen benutzt, prüfe
mit `git log 7b2429a..origin/main --stat -- '*.test.ts' '*.test.tsx'`, ob sich
seither Tests geändert haben. Ist die Ausgabe leer, gelten die Zahlen weiter.

## Warum es dieses Dokument gibt

Ein grüner Haken an einem Pull Request liest sich wie eine Aussage über die
Software. Er ist eine Aussage über die Tests, die tatsächlich gelaufen sind.
Zwischen beiden liegt hier eine Lücke, die niemand im Kopf haben kann, weil sie
über 48 Dateien verteilt ist.

Zwei Fehler haben dieses Dokument erzwungen, beide meine:

1. Ich hatte behauptet, der Golden Corpus behebe, dass 43 QAF-Testdateien nur
   lokal laufen. Nachgemessen senkt er die Zahl um null.
2. Die erste Fassung **dieses** Dokuments war gegen den Elterncommit von `main`
   gemessen und trug trotzdem „gemessen gegen `main` = `7b2429a`" darüber. Der
   Merge dazwischen hatte die Zahlen um 6 Tests verschoben. Ein unabhängiges
   Review hat es gefunden.

Der zweite Fehler ist der lehrreichere: ein Dokument, das „nicht fortschreiben,
neu messen" fordert, war im Moment des Schreibens selbst veraltet. Wer hier
Zahlen liest, prüfe den Commit in der Kopfzeile gegen `git log -1 origin/main`.

## Änderungen seit dem Messpunkt

Die Zahlen unten gelten für Commit `7b2429a`. Seither sind Tests **hinzugekommen**,
die in CI laufen — die Zahlen sind damit veraltet, und zwar in die gute Richtung:

| Was | Tests | Quelle |
|---|---:|---|
| Golden-Corpus-Abnahme | 5 | `tests/fixtures/qaf-golden/__tests__/golden-corpus.test.ts` |
| Regressionssperre des Anonymisierers | 32 | `scripts/qaf-v2/anonymise/__tests__/leck-regression.test.ts` |
| Determinismus über die Fixtures | 6 | `tests/fixtures/qaf-golden/__tests__/determinismus.test.ts` |

Hier steht bewusst **keine neue Gesamtzahl**. Eine Summe aus Addition wäre eine
Fortschreibung, und dieses Dokument verlangt Neumessung. Der Befehl dafür steht
unten; er ist erst nach dem Merge des jeweiligen PR aussagekräftig, weil die CI
den Stand auf `main` messen muss.

Was sich **nicht** geändert hat: keine der 24 unentschiedenen Dateien wurde
umgehängt. Warum, steht gemessen in `triage-env-gated-tests.md`, seit dem
06.08. auch für die 18 Fälle, die dort als „vielleicht" geführt waren: 7 davon
verlangen Stichproben über 16 Paare bis 120 Dateien und sind damit endgültig
beantwortet, 6 hängen am blockierten Paar-Korpus, 5 sind eine offene
Messaufgabe am Detektor — für die inzwischen gemessen ist, dass drei der vier
gesuchten Struktur-Familien mit den heutigen Fixtures belegbar sind.

Die frühere Formulierung „für 79 von 80 geht es nicht" war im Ergebnis fast
richtig und in der Begründung zu grob — sie stellte 18 offene Fälle als
entschieden dar. Eine Zahl weitergereicht, ohne die Quelle zu lesen, auf die
sie verweist.

## Die Zahlen

### `portability-matrix.yml` — die vollständige vitest-Suite

| | Dateien | Tests |
|---|---:|---:|
| ausgeführt | 423 | 6776 |
| übersprungen | 57 | 316 |
| Summe | 480 | 7092 |

Beide Profile (`default`, `bmw`) überspringen **identisch dieselben** Dateien —
Datei für Datei verglichen, nicht nur der Summe nach.

In 58 Dateien kommen übersprungene Tests vor: 57 vollständig übersprungene plus
`components/qaf-differences/__tests__/qaf-upload-constants.test.ts` mit 1 von 9.
Daher steht in der Zusammenfassung 57 und in der Dateiliste 58.

### Was davon woanders abgedeckt ist

`rls.yml` fährt gegen eine echte Postgres-Instanz und hat ein exaktes
Zahlen-Gate im Workflow („Erwartet waren genau N ausgeführte Tests"):

| | Dateien | Tests |
|---|---:|---:|
| ausgeführt in `rls.yml` | 10 | 146 |

Das sind exakt die 141 übersprungenen Sicherheits-/RLS-Tests plus
`__tests__/offline/sync-updated-at-schema.test.ts` (5). Datei für Datei
abgeglichen.

### Ein dritter Workflow, der Tests startet

`claude-pr-review.yml` ruft `npm run test -- --coverage` auf. Er läuft
PR-getriggert, mit `continue-on-error: true` und ohne Gate — er kann also nichts
blockieren, und er benutzt dieselbe vitest-Konfiguration mit denselben
Skip-Bedingungen. Er verändert keine Zahl in diesem Dokument, gehört aber
genannt, damit niemand annimmt, es gäbe nur zwei.

Playwright/E2E (`e2e/`, `e2e-ct/`) ist angelegt, steht auf `test.fixme` und ist
in keinem Workflow verdrahtet. Es liefert keine verborgene Abdeckung.

### Die belastbare Zahl

> **170 Tests in 48 Dateien laufen in keinem CI-Workflow.**

316 übersprungen minus 146 anderswo abgedeckt. Das sind 2,4 % aller Tests.

## Wonach sich die 170 aufteilen

Klassifiziert nach **Familie plus Einzelfallprüfung** — genauer formuliert, als
eine frühere Fassung dieses Abschnitts behauptete:

- Die 20 Dateien unter `multi-qaf/` und `g60/` sind über ihr **Verzeichnis**
  eingeordnet. Getragen wird das von der pauschalen Produktentscheidung
  („G60 und Multi-QAF sind nicht Bestandteil", `deferred-scope.md`), nicht von
  einem Marker in der Datei. Nachgezählt: **keine einzige** dieser 20 Dateien
  nennt `deferred_by_product_owner` in ihrem eigenen Text.
- Nur `capability-detector.deferred-scope.real-files.test.ts` ist tatsächlich
  über den Inhalt eingeordnet: sie liegt in keinem der beiden Verzeichnisse und
  wäre nach Pfadmuster als „unentschieden" gezählt worden, weist sich aber im
  eigenen `describe`-Titel als zurückgestellt aus.

Die Einzelfallprüfung ist also nötig, aber sie trägt genau einen Fall. Wer
diesen Abschnitt liest, soll nicht glauben, hier prüfe eine Regel jede Datei
inhaltlich — sie prüft die Ausnahmen, die eine Verzeichnisregel verfehlt.

**Ungeklärte Spannung:** Verzeichnisregel und Register decken sich nicht
vollständig. Zwei Multi-QAF-Dateien (`detail-ui.real-files.test.tsx`,
`variant-vs-standard-detail-ui.real-files.test.tsx`, zusammen 11 Tests) fehlen
in der 18-Zeilen-Tabelle von `deferred-scope.md` und beschreiben sich im eigenen
Kopf als „mandatory deliverable per task brief — NOT a follow-up". Unter der
pauschalen Familienregel sind sie hier als zurückgestellt gezählt. Ob das der
Absicht entspricht, gehört dem Product Owner, nicht diesem Dokument.

| Gruppe | Dateien | Tests | Bewertung |
|---|---:|---:|---|
| zurückgestellt (Produktentscheidung) | 21 | 84 | entschieden — kein Mangel |
| bewusst lokal, im Datei-Kopf begründet | 3 | 6 | entschieden, aber nicht zentral erfasst |
| **unentschieden** | **24** | **80** | **das ist die Lücke** |

Die drei bewusst lokalen tragen ihre Begründung im eigenen Kopf („bewusst eine
Messung, keine Abnahme", „kein CI-Gate im eigentlichen Sinn"):

| Datei | Tests |
|---|---:|
| `audit/__tests__/hice-reproduction.test.ts` | 4 |
| `scripts/qaf-v2/anonymise/__tests__/coverage-probe.test.ts` | 1 |
| `scripts/qaf-v2/anonymise/__tests__/xml-fixture-probe.test.ts` | 1 |

Sie sind begründet, stehen aber in keinem zentralen Register. „Niemand hat das
entschieden" wäre für sie falsch; „nicht im Register" trifft es.

**Bleiben 80 Tests in 24 Dateien, für die weder eine Produktentscheidung noch
eine geschriebene Begründung existiert.**

> **Fortschreibung 06.08.2026: 75 in 23.** `capability-detector` (`#450`) und
> `blank-template-negative-anchor` laufen inzwischen ungegated in der CI. Die
> Zahlen unten gelten für den Messzeitpunkt und sind nicht nachgezogen; die
> aktuelle Einzelmessung aller Kandidaten steht in
> `triage-env-gated-tests.md`. Größte Einzelgruppe darin: 10 der 24
Dateien folgen dem Muster
`lib/qaf-differences/internal/__tests__/*-golden-case.real-files.test.ts`, die
Golden-Case-Suiten der Vergleichslogik. Das sind 42 %, nicht die Mehrheit — eine
frühere Fassung schrieb „überwiegend", was zu weit ging. Alle 24 Köpfe wurden auf
Freitext-Hinweise durchsucht, die eine Absicht belegen würden; es gibt keine.

## Die Gate-Verzeichnisse

Alle außerhalb des Repositorys, alle nicht eingecheckt:

- `/home/aria/work/qaf-corpus/incoming` samt Unterverzeichnissen
- `/home/aria/work/qaf-compare-v2-golden`
- `/root/aria/work/qaf-compare-kar824/input`

Auf dem Entwicklungsrechner existieren alle drei. Der Unterschied zwischen
„läuft bei mir" und „läuft in CI" ist genau diese Liste.

## Was der Golden Corpus daran ändert

**Derzeit nichts an den 170.** Stand 2026-08-05 liegen **vier** anonymisierte
reale Fixtures im Repository (`qaf-8.8-hice-vergabe`, `qaf-9.1-standard-de`,
`qaf-9.1-standard-en`, `qaf-9.1-zeilen-de`), gelesen von **zwei** Dateien mit
zusammen **11 Tests**, die in CI laufen, weil die Fixtures eingecheckt sind.

Keine bestehende gegatete Suite wurde umgehängt. Gezählt wurde über das
Verzeichnis `tests/fixtures/qaf-golden/__tests__/`, nicht über eine Namenssuche —
`determinismus.test.ts` bezieht seine Eingaben über `manifest.json` und nennt
keinen Dateinamen als Literal, wäre also von einer Namenssuche nicht gefunden
worden. (`golden-corpus.test.ts` nennt fünf Fixture-Namen wörtlich und wäre
gefunden worden.)

**Dieser Absatz stand zweimal falsch da, und der zweite Fehler ist der
lehrreichere.** Die erste Fassung begründete die Aussage mit einer Namenssuche.
Die Korrektur behauptete dann, *beide* Leser nennten keinen Dateinamen und der
Pfad `fixtures/qaf-golden` komme außerhalb nur in **drei** Erzeugungsskripten vor.
Beides falsch: `golden-corpus.test.ts` nennt fünf Namen, und in
`anonymise_workbook.py` ist der einzige `qaf-golden`-Treffer der Standardwert des
Seed-Arguments (`--seed default="qaf-golden-2026"`) — kein Pfadbezug. Es sind zwei
Skripte: `build_manifest.py` und `build_fixtures.sh`.

Die Korrektur eines Werkzeugbefunds hat also einen neuen Werkzeugbefund
derselben Art erzeugt: eine Substring-Trefferliste gezählt, aber nicht gelesen.
Ein Review hat beide gefunden. Die belastbare Aussage ist die schwächere: **keine
Testdatei außerhalb des Fixture-Verzeichnisses liest die Fixtures** — geprüft über
das Verzeichnis und über den Pfad-String, mit gelesener Trefferliste.

Die Fixtures sind die **Voraussetzung** dafür, dass man umhängen kann. Das
Umhängen ist die Arbeit, die die Zahl senkt, und sie steht aus.

Erreichbar wären damit vor allem die 80 unentschiedenen Tests, soweit sie mit
diesen vier Fixtures auskommen. Suiten, die gegen den vollen 374-Dateien-Korpus
messen (Abdeckungsstatistik, Vorlagen-Fingerabdruck-Kalibrierung), brauchen den
Korpus selbst und bleiben lokal.

## Für die Traceability-Matrix

Sechs der 83 Anforderungen in
`baseline-inputs/07_Requirements_Traceability_Matrix_v1.0.md` formulieren ihre
Abnahme über Tests: DET-003, FOR-002, ACC-003, AI-001, TST-002, ARC-003.

**Diese Zuordnung ist jetzt gemacht** (Stand 2026-08-05). Sie beantwortet für
jede die Frage: läuft der Test, der die Abnahme tragen soll, in CI — oder gehört
er zu den 170?

| ID | Abnahme laut Matrix | Trägt ein Test das? | Läuft er in CI? |
|---|---|---|---|
| `DET-003` | Golden Werte reproduzierbar | **nein** | — |
| `FOR-002` | Positiv- und Negativtest grün | **nein, der Produktivcode fehlt** | — |
| `ACC-003` | Color-blind test grün | teilweise | ja |
| `AI-001` | Unsicherer Lauf wird nicht zusammengefasst | ja | ja, plus eine gegatete Ergänzung |
| `TST-002` | Test schlägt bei absichtlichem Defekt fehl | ja, punktuell | ja |
| `ARC-003` | Komponenten testbar und kleiner | nicht testförmig | — |

Im Einzelnen, jeweils mit dem Befund, der die Einordnung trägt:

- **`DET-003` — „Alle Rundungen sind versioniert".** Es gibt **kein**
  Rundungsprofil: `roundingProfile` und `rounding_profile` haben null Treffer im
  gesamten `lib/`. Gerundet wird an **71 Stellen in 26 Dateien** des
  Produktivcodes von `lib/qaf-differences` (13 × `Math.round`, 54 × `toFixed`,
  4 × `maximumFractionDigits`).

  Eine frühere Fassung schrieb „81 Stellen in 26 Dateien" und mischte damit zwei
  Messungen: 81 Treffer ergeben sich nur **mit** den Testdateien, und dann sind
  es 31 Dateien, nicht 26. Beide Messungen für sich sind richtig — die
  Kombination ist es nicht, und ein Review hat sie nachgerechnet. Gezählt wird
  hier der Produktivcode, weil die Anforderung eine Aussage über das Produkt
  macht. Die von der
  Matrix als Ort genannte `format-de.ts` enthält selbst keine Rundung, sondern
  formatiert über `Intl.NumberFormat` mit einem `decimals`-Parameter (Standard 2)
  — eine Entscheidung je Aufruf, keine zentrale Politik. Die Determinismustests
  belegen, dass derselbe Lauf denselben Hash ergibt; sie belegen **nicht**, dass
  eine Rundungsänderung versioniert auffiele. Der Status „Teilweise" in der
  Matrix ist korrekt; abgesichert ist die Anforderung nicht. Gehört zu Loop 8.
- **`FOR-002` — „Formel zu Hardcode erkennen".** `D16` und `D17` existieren in
  `data-quality.ts` als Typvariante und in der Gewichtungstabelle, werden aber
  **nirgends gesetzt** — keine Zuweisung, keine Rückgabe. Ein Positiv- und
  Negativtest ist damit nicht möglich, weil der Detektor fehlt, nicht weil der
  Test fehlt. Gehört zu Loop 4.
- **`ACC-003` — „Farbe nicht allein".** `lib/ui/__tests__/contrast.test.ts`
  läuft in CI und prüft `contrastRatio`, `relativeLuminance`, `meetsWcagAA`. Das
  deckt die **Kontrast**-Achse. Die eigentliche Anforderung ist die zweite Achse
  — Labels, Icons, Muster statt Farbe als einziger Träger — und für die gibt es
  keinen Test an Charts oder Badges. Teilweise, mit benannter Lücke. Loop 11.
- **`AI-001` — `safeToSummarize`-Gate.** `ai-ready.test.ts` läuft in CI.
  Zusätzlich existiert `ai-ready-golden-case.real-files.test.ts` — env-gated und
  damit **eine der 24 unentschiedenen Dateien**. Die Kernabnahme trägt der
  CI-Test; die reale Ergänzung ist Teil der offenen Lücke. Loop 14.
- **`TST-002` — Mutationsproben.** Getragen in CI von der Regressionssperre des
  Anonymisierers (jede Schutzmaßnahme einzeln mutationsgeprüft) und den beiden
  Golden-Corpus-Dateien in `tests/fixtures/qaf-golden/__tests__/`.

  Eine frühere Fassung nannte hier zusätzlich die Regressionssperre des
  Portabilitäts-Gates (`check-qaf-core-portability`). Das war **kein zulässiger
  Beleg**: das Gate liegt in einem offenen, nicht gemergten Pull Request und
  existiert in dem Commit, gegen den dieses Dokument gemessen ist, nicht. Ein
  Review hat es gefunden. Sobald der PR gemergt ist, gehört er hier hinein — vorher
  nicht.

  Ebenfalls korrigiert: „vier Golden-Corpus-Suiten" waren tatsächlich **zwei
  Dateien**; „vier" ergab sich nur, wenn man die vier fixture-parametrisierten
  Fälle innerhalb von `determinismus.test.ts` als Suiten zählt.

  Die Matrix fordert „Mutation per Regel" — systematisiert über das ganze
  Repository ist das weiterhin nicht, belegt ist es punktuell. „Loop alle".
- **`ARC-003` — UI-Monolithen zerlegen.** Die Abnahme („Komponenten testbar und
  kleiner") ist eine Struktur-, keine Testaussage. Sie gehört zu Loop 10 und
  lässt sich nicht über die 170 beantworten.

**Was daraus folgt:** Von sechs testbasierten Anforderungen tragen zwei ihre
Abnahme in CI (`AI-001`, `TST-002` punktuell), eine teilweise (`ACC-003`), zwei
gar nicht (`DET-003`, `FOR-002` — bei `FOR-002` fehlt der Produktivcode), und
eine ist nicht testförmig (`ARC-003`). Kein Abnahmehaken ist dadurch neu
gerechtfertigt; zwei wären ohne diese Prüfung fälschlich als abgesichert
gelesen worden.

## Begriffe

| Begriff | Bedeutung |
|---|---|
| Kernabnahme | Tests, die über die Freigabe einer QAF-Familie entscheiden |
| `deferred_by_product_owner` | vom Product Owner zurückgestellt: nicht erweitern, nicht als Exit-Kriterium verwenden, Bestand nicht verschlechtern |
| G60 | eigene QAF-Dateifamilie mit nummerierten Detailreitern, eigene Pipeline unter `internal/g60/` |
| Multi-QAF | QAF-Mappen mit Variantenstruktur, eigene Pipeline unter `internal/multi-qaf/` |
| env-gated | der Test läuft nur, wenn ein lokales Verzeichnis existiert (`describe.skipIf`) |
| Golden Corpus | anonymisierte, im Repository liegende reale QAF-Mappen als Testgrundlage |

## Wie diese Zahlen neu erhoben werden

Nicht fortschreiben, neu messen — und gegen den **aktuellen** `main`-Stand, nicht
gegen den, der beim letzten Mal aktuell war:

```bash
git log -1 --format=%h origin/main                     # Ziel-Commit
gh run list --branch main --json databaseId,workflowName,headSha
# den Lauf mit passendem headSha nehmen, nicht den neuesten grünen
gh run view <id> --log > ci.log
```

Im Log die Zeilen `↓ <datei> (N tests | M skipped)` auszählen, die Zeilen mit
`skipped > 0` bei bestandenen Dateien nicht vergessen, und gegen die
Zusammenfassung `Test Files` / `Tests` gegenprüfen. Dasselbe für `rls.yml`, dann
beide Mengen Datei für Datei abgleichen.

**Nicht aus dem Repository schätzen.** Eine statische Zählung von
`describe.skipIf` trifft die Testanzahl nicht, übersieht teilweise übersprungene
Dateien und weiß nichts von einem zweiten Workflow, der dieselben Dateien
ausführt. Alle drei Fehler hätte sie hier gemacht.

**Nicht nach Pfad klassifizieren.** Siehe oben: eine Datei kann zurückgestellt
sein, ohne im zurückgestellten Verzeichnis zu liegen.
