# QA: Control Quality Pipeline

## Übersicht

Die Control Quality Pipeline prüft und verbessert die Canonical Controls der Compliance-Bibliothek. Sie nutzt **PDF-basierte Verifizierung** als Ground Truth — jeder Control-Originaltext wird direkt im Quelldokument (PDF) lokalisiert.

Alle Scripts liegen in **`scripts/qa/`**. Starten auf dem Mac Mini via Runner-Script:

```bash
# Job starten (laedt .env automatisch, PID-Lock, unbuffered output)
ssh macmini "bash ~/Projekte/breakpilot-compliance/scripts/qa/run_job.sh <script.py> [args...]"

# Status aller Jobs
ssh macmini "bash ~/Projekte/breakpilot-compliance/scripts/qa/run_job.sh --status"

# Log ansehen
ssh macmini "bash ~/Projekte/breakpilot-compliance/scripts/qa/run_job.sh --log <script.py>"

# Job stoppen
ssh macmini "bash ~/Projekte/breakpilot-compliance/scripts/qa/run_job.sh --kill <script.py>"
```

## Architektur

```
Original-PDFs (~/rag-ingestion/pdfs/)
        ↓
  PDF Text-Extraktion (PyMuPDF)
        ↓
  Artikel-Index aufbauen
  (Erwägungsgründe → Artikel → Anhänge)
        ↓
  source_original_text im PDF suchen
  (Substring-Match, normalisiert)
        ↓
  Artikel/§/Section zuordnen + article_type setzen
        ↓
  Duplikat-Erkennung
  (Preamble vs. Artikel: Artikel hat Vorrang)
```

## PDF-basierte Artikelzuordnung

### Konzept

Jeder Control hat ein Feld `source_original_text` — der Chunk-Text aus dem Quelldokument. Statt über Qdrant-Hashes wird dieser Text **direkt im Original-PDF gesucht**. Die Position im PDF bestimmt den Artikel.

### Dokumenttypen

| Typ | Struktur | Beispiel |
|-----|----------|---------|
| EU-Verordnung | Erwägungsgründe → Artikel → Anhänge | DSGVO, KI-VO, CRA |
| Deutsches Gesetz | §-Paragraphen | BDSG, GewO, HGB |
| NIST | Control Families (AC-1, SC-7) | SP 800-53, CSF 2.0 |
| OWASP | Kategorien (A01:2021, V1.1) | Top 10, ASVS, MASVS |
| EDPB/ENISA | Nummerierte Abschnitte | Leitlinien, Guidelines |

### Article Types

| article_type | Bedeutung | Beispiel |
|---|---|---|
| `article` | Gesetzesartikel / Paragraph | Artikel 25 DSGVO |
| `preamble` | Erwägungsgrund | Erwägungsgrund (78) |
| `annex` | Anhang | Anhang III |
| `control` | NIST Control Family | AC-6, SA-7 |
| `section` | Nummerierter Abschnitt | Section 2.1 |
| `category` | OWASP Kategorie | A01:2021 |
| `requirement` | OWASP Requirement | V1.1, MASVS-STORAGE-1 |

### Ergebnisse (Stand 2026-03-20)

| Metrik | Wert |
|---|---|
| Controls mit source_original_text | 5.751 (86%) |
| Im PDF lokalisiert | **5.063 (88%)** |
| Nicht gefunden | 649 |
| Kein PDF vorhanden | 29 |
| Recital_suspect markiert | 648 |
| 100% Match-Rate | 20+ Regulations (inkl. DSGVO, KI-VO, NIS2, NIST 800-53, Blue Guide) |

**Verlauf:** v1 (4.110, 52%) → v2 (6.091, 77%) → v3 (6.259, 79%) → v4 +Blue Guide EN (6.803, 86%) → v5 nach Cleanup (5.063/5.741, 88%)

### Nicht-matchende Controls

| Ursache | Controls | Status |
|---|---|---|
| ~~Blue Guide EN vs. DE PDF~~ | ~~562~~ | ✅ Gelöst — EN-PDF beschafft, 544/544 gematcht |
| ~~OWASP Top 10 multilingual~~ | ~~324~~ | ✅ Als duplicate markiert — Übersetzungen ohne Mehrwert |
| CRA Encoding | ~76 | PDF-Ligaturen/Sonderzeichen-Differenzen |
| CISA Secure by Design | ~113 | Falsches PDF (ENISA statt CISA) |
| OWASP ASVS | ~173 | PDF-Matching-Problem (meist EN) |

## Brute-Force-Suche

Für Controls mit unbekannter Quelle: Text gegen **alle ~100 PDFs** suchen. Findet:
- Korrekte Quelldokument-Zuordnung
- Falsche Source-Zuordnungen (44 entdeckt)

## Erwägungsgrund-Controls (Wettbewerbsvorteil)

Controls aus Erwägungsgründen (`article_type = preamble`) sind **kein Nachteil**. Sie decken Aspekte ab, die reine Artikel-basierte Compliance-Tools übersehen.

**Duplikat-Regel:** Wenn ein Preamble-Control das gleiche Thema wie ein Artikel-Control behandelt (Jaccard-Ähnlichkeit ≥ 0.40), hat der **Artikel Vorrang**. Das Preamble-Control wird als `duplicate` markiert.

### Ergebnis der Preamble-Dedup

| Metrik | Wert |
|---|---|
| Preamble-Controls geprüft | 838 |
| Als Duplikat markiert | 190 |
| **Unique Preamble-Controls** | **648** |

## Pipeline-Versionen

| Version | Controls | Mit Originaltext | Beschreibung |
|---|---|---|---|
| v1 | 5.332 | 4.137 (78%) | Automatisch + manuell erstellt |
| v2 | 2.258 | 2.258 (100%) | Automatisch aus Chunks |
| v3 | 1.570 | 1.548 (99%) | Neueste Pipeline |

Die 1.195 v1-Controls **ohne** Originaltext sind manuell erstellt (`strategy=ungrouped`) und haben keine Chunk-Referenz.

## OWASP Cleanup (2026-03-20)

- **324 OWASP Top 10 multilingual Controls** → `duplicate` markiert (ZH, AR, ID, FR, ES, PT — Übersetzungen derselben 10 Kategorien)
- **47 Controls** mit falscher Quellenzuordnung korrigiert (z.B. als "OWASP Top 10" getaggt, aber tatsächlich aus ASVS/SAMM/API/MASVS)
- **~200 OWASP ASVS/SAMM/MASVS EN Controls** behalten — unique Content aus GitHub/Website, nicht im PDF auffindbar

## NIST OSCAL Import (2026-03-20)

**776 neue Controls** aus NIST SP 800-53 Rev 5 OSCAL (Public Domain, maschinenlesbar):

- Quelle: `usnistgov/oscal-content` (JSON Catalog)
- Vor allem **Control Enhancements** (z.B. AC-2(3), SC-7(8)) — die atomaren Unteranforderungen
- Jeder Control enthält: Statement + Guidance + Assessment-Methoden + Cross-References + Parameters
- `pipeline_version = 4`, `generation_strategy = 'oscal_import'`
- Kein Pass 0a/0b nötig — Controls sind **bereits atomar**

| Metrik | Vorher | Nachher |
|---|---|---|
| SP 800-53 Controls (aktiv) | 1.107 | **1.883** |
| OSCAL-Abdeckung | 238/1.014 (23%) | **1.014/1.014 (100%)** |

## Phase 5: RAG-Deduplizierung + Normalisierung (2026-03-20)

### Durchgeführte Schritte

| Schritt | Beschreibung | Controls |
|---|---|---|
| 5.1 | OSCAL Controls: `source_regulation` in generation_metadata gesetzt | 776 |
| 5.2 | v3 Controls ohne Source → `needs_review` mit `missing_source` Flag | 20 |
| 5.3 | Leerer Source-Name korrigiert (AT TKG) | 1 |
| 5.4 | OWASP regulation_code Fehlzuordnungen korrigiert | 47 |
| 5.5 | **duplicate/too_close Controls hart gelöscht** | **3.301** |
| 5.6 | Processed Chunks bereinigt (gelöschte Control-IDs entfernt) | 2.520 |

### Ergebnis

- **Vorher:** 9.936 Controls (6.635 aktiv, 2.998 duplicate, 303 too_close)
- **Nachher:** 6.635 Controls, **alle aktiv** (0 duplicate/too_close)
- Alle regulation_codes haben jetzt einheitliche Source-Namen
- OWASP-Controls sind korrekt ihren Quellen zugeordnet

## DB-Status (Stand 2026-03-20, nach Phase 7.4)

| release_state | Count |
|---|---|
| draft | ~6.030 |
| needs_review | 838 |
| **Gesamt** | **6.868** |

## Scripts (`scripts/qa/`)

### Kern-QA (PDF-Matching)

| Script | Beschreibung |
|---|---|
| `pdf_qa_all.py` | **Haupt-QA**: Controls gegen PDFs matchen, Artikel-Index aufbauen. Enthaelt `SOURCE_FILE_MAP`, alle Index-Builder (EU, DE, NIST, OWASP, generic). 526 Zeilen. |
| `pdf_qa_inventory.py` | Inventar: Welche Regulations haben Controls, wie viele, welche PDFs existieren |
| `apply_pdf_qa_results.py` | Ergebnisse aus `pdf_qa_all.py` in DB schreiben (`article_type`, `recital_suspect`) |
| `pdf_article_lookup_poc.py` | POC: Control-Text in PDF lokalisieren, Headings von Cross-Refs unterscheiden |

### Lueckenanalyse + Control-Generierung

| Script | Beschreibung |
|---|---|
| `gap_analysis.py` | **Phase 7.3**: Artikel im PDF vs. Controls in DB vergleichen, Luecken identifizieren |
| `phase74_generate_gap_controls.py` | **Phase 7.4**: Neue Controls fuer Luecken via Anthropic API generieren. `pipeline_version=5`. 624 Zeilen. |
| `benchmark_llm_controls.py` | LLM-Vergleich: gpt-oss-120b vs. Claude Sonnet fuer Control-Generierung |
| `test_pass0a.py` | **Pass 0a Test**: Obligation Extraction + 3-Tier-Klassifizierung (Pflicht/Empfehlung/Kann). Standalone, speichert JSON. |

### Deduplizierung + Normalisierung

| Script | Beschreibung |
|---|---|
| `preamble_dedup.py` | Preamble vs. Artikel Duplikat-Erkennung (Jaccard >= 0.40) |
| `qa_dedup_controls.py` | Jaccard-basierte Titel-Deduplizierung |
| `qa_apply_and_dedup.py` | Ergebnisse anwenden + Duplikate in einem Schritt markieren |
| `qa_normalize_sources.py` | Source-Namen normalisieren (kanonische Namen) |
| `phase5_normalize_and_cleanup.py` | **Phase 5**: Normalisierung + 3.301 Duplikate hart loeschen |
| `qa_delete_gpsr_dupe.py` | GPSR-Duplikate loeschen |
| `delete_gpsr_prod.py` | GPSR-Duplikate aus Production-Qdrant entfernen |

### Quellen-spezifische Scripts

| Script | Beschreibung |
|---|---|
| `blue_guide_en_match.py` | Blue Guide EN-PDF matchen (544/544 Erfolg) |
| `owasp_cleanup.py` | OWASP multilingual Cleanup (324 Duplikate) + Source-Fix (47 korrigiert) |
| `owasp_github_match.py` | OWASP ASVS/SAMM/MASVS gegen GitHub-Markdown matchen |
| `oscal_import.py` | NIST OSCAL Import (776 Controls aus JSON Catalog) |
| `oscal_analysis.py` | NIST OSCAL Analyse: Abdeckung, fehlende Controls |

### Diagnose + Utilities

| Script | Beschreibung |
|---|---|
| `db_status.py` | DB-Status: release_state Counts, pipeline_version, source Verteilung |
| `debug_low_match.py` | Debugging: Warum matchen Blue Guide / OWASP / CISA schlecht? |
| `qa_article_map_all_chunks.py` | Alle Chunks Artikel-Nummern zuordnen (Bulk) |
| `backfill_job_66228863.py` | Einmaliger Backfill-Job |
| `sync_controls_to_prod.py` | Controls von Dev nach Production synchronisieren |

### Runner

| Script | Beschreibung |
|---|---|
| `run_job.sh` | **Job-Runner**: Laedt `.env`, PID-Lock, Monitoring (`--status`, `--log`, `--kill`) |

## Phase 7: PDF-Validierung + Enrichment (2026-03-20)

### 7.1 + 7.2: Controls gegen PDFs validiert + Ergebnisse angewendet ✅

- 5.063 Controls erfolgreich im Original-PDF lokalisiert (88%)
- `article_type` fuer alle gematchten Controls gesetzt
- 648 Preamble-Controls als `recital_suspect` in `generation_metadata` markiert
- 332 Controls nicht matchbar (OWASP ASVS 132, CISA 72, ENISA 38, OWASP SAMM 31, CRA 28)

### 7.3: Lueckenanalyse ✅

**494 Artikel-Luecken** in 15 Quellen identifiziert. Geschaetzt ~300 davon actionable.

| Source | Luecken | Coverage | Bemerkung |
|---|---:|---:|---|
| AML-Verordnung | 91 | 5% | Kaum ingestiert |
| MiCA | 71 | 52% | Grosse Verordnung |
| NIST SP 800-53 | 59 | 83% | Meist Section-Header, nur SA-15 fehlt |
| OWASP ASVS 4.0 | 47 | 35% | Requirement-Gruppen fehlen |
| Batterieverordnung | 41 | 58% | |
| DSGVO | 35 | 65% | Einige Governance/Aufsicht-Artikel |
| ENISA ICS/SCADA | 34 | 31% | |
| ENISA Supply Chain | 26 | 7% | |
| CRA | 23 | 68% | |
| NIS2 | 16 | 65% | |
| KI-Verordnung | 15 | 87% | Fast komplett |
| Maschinenverordnung | 5 | 91% | Fast komplett |

### 7.4: Neue Controls fuer Luecken generieren ✅ (2026-03-20)

Script: `phase74_generate_gap_controls.py --resume`

- **494 Artikel-Luecken** in 15 Quellen → Anthropic Claude Sonnet 4.6
- `pipeline_version = 5`, `generation_strategy = 'phase74_gap_fill'`
- Direkt PDF-Text als Input (nicht RAG-Chunks)
- Starten via: `run_job.sh phase74_generate_gap_controls.py --resume`

**Ergebnis:**

| Source | Luecken | Generiert |
|---|---:|---:|
| AML-Verordnung | 91 | 97 |
| MiCA | 71 | 68 |
| NIST SP 800-53 | 59 | 19 |
| KI-Verordnung | 15 | 15 |
| OWASP ASVS 4.0 | 47 | 11 |
| Batterieverordnung | 41 | 9 |
| DSGVO | 35 | 4 |
| OWASP Top 10 | 12 | 3 |
| NIS2 | 16 | 3 |
| CRA | 23 | 3 |
| OECD KI-Empfehlung | 4 | 1 |
| **Gesamt** | **494** | **233** |

Nicht generiert: 75 zu kurzer Text, 29 NIST-Intros, 11 Parse-Errors, 162 ID-Konflikte (COMP-1000 etc.).
API-Kosten: ~$7,55 (109 min Laufzeit).

## Pass 0a: Obligation Extraction — 3-Tier-Klassifizierung

### Konzept

Pass 0a zerlegt Rich Controls (~6.000) in **atomare Obligations** per LLM (Claude Sonnet 4.6).
Jede Obligation wird durch den **Quality Gate** klassifiziert — nicht gefiltert:

| obligation_type | Signal | Beispiel |
|---|---|---|
| **pflicht** | müssen, muss, ist zu, hat zu, shall, must, required | "Der Betreiber muss alle Daten verschluesseln" |
| **empfehlung** | soll, sollen, should, sicherstellen, gewaehrleisten, dokumentieren | "Der Betreiber soll regelmaessige Audits durchfuehren" |
| **kann** | kann, koennen, darf, duerfen, may, optional | "Der Betreiber kann zusaetzliche Massnahmen ergreifen" |

**Wichtig:** Nichts wird mehr rejected wegen fehlendem normativem Signal. Obligations ohne Signal werden als `empfehlung` klassifiziert. Rejected werden nur noch: Evidence-Only, zu kurz (<20 Zeichen), fehlender Parent-Link.

### Warum auch Empfehlungen behalten?

Empfehlungen helfen Firmen, ihre Systeme sicherer zu machen — ueber das Pflichtprogramm hinaus. Im Frontend erhalten Kunden einen Marker, der klar anzeigt:

- **Pflicht** = gesetzlich/regulatorisch vorgeschrieben
- **Empfehlung** = Best Practice, freiwillig, aber wertvoll
- **Kann** = optional, weitergehende Massnahme

### Quality Gate — Kritische Flags

| Flag | Kritisch? | Beschreibung |
|---|---|---|
| `has_normative_signal` | Nein | Informativer Check, kein Ablehnungsgrund |
| `obligation_type` | — | Klassifizierung (pflicht/empfehlung/kann) |
| `not_evidence_only` | **Ja** | Kein reiner Nachweis-Eintrag |
| `min_length` | **Ja** | Mindestens 20 Zeichen |
| `has_parent_link` | **Ja** | Verbindung zum Parent-Control |
| `single_action` | Nein | Nur ein Hauptverb (heuristisch) |
| `not_rationale` | Nein | Keine reine Begruendung |

### Normative Signal Detection — Regex-Tiers

```
Tier 1 (Pflicht): muessen, muss, ist/sind/hat/haben zu + Infinitiv,
                   Compound-Verben (festzustellen, vorzunehmen),
                   Gerundivum (mitzuteilen, bereitzustellen),
                   shall, must, required

Tier 2 (Empfehlung): soll, sollen, sollte, sollten,
                       gewaehrleisten, sicherstellen,
                       should, ensure, recommend,
                       dokumentieren, implementieren, ueberpruefen

Tier 3 (Kann): kann, koennen, darf, duerfen, may, optional
```

### Testergebnisse (3 Iterationen, 2026-03-20)

| Run | Controls | Obligations | Validated | Rejected | Kosten |
|---|---:|---:|---:|---:|---:|
| 1 (v0 Regex) | 10 | ~100 | 68% | 32% | $0,28 |
| 2 (v1 Regex) | 50 | ~530 | 78% | 22% | $1,43 |
| 3 (v2 Regex) | 50 | ~530 | 86% | 14% | $1,44 |
| 4 (3-Tier) | 60 | — | — | — | — |

Run 4 laeuft mit dem neuen Klassifizierer — statt PASS/REJECT wird jetzt PFLICHT/EMPFEHLUNG/KANN ausgegeben.

### Scripts

| Script | Beschreibung |
|---|---|
| `test_pass0a.py` | **Test-Script**: Standalone (kein SQLAlchemy), psycopg2 + Anthropic API. Speichert Ergebnisse als JSON. |

```bash
# Test mit 10 Controls
run_job.sh test_pass0a.py --limit 10

# Test mit bestimmter Quelle
run_job.sh test_pass0a.py --limit 20 --source "DSGVO"

# Ergebnisse: /tmp/pass0a_results_<N>controls.json
```

### Backend-Code

- **Klassifizierung:** `backend-compliance/compliance/services/decomposition_pass.py`
  - `classify_obligation_type()` — 3-Tier-Klassifizierung
  - `quality_gate()` — gibt `obligation_type` in Flags zurueck
  - `passes_quality_gate()` — `has_normative_signal` nicht mehr kritisch
  - `ObligationCandidate.obligation_type` — neues Feld

### Hochrechnung (basierend auf 50-Control-Runs)

| Metrik | Wert |
|---|---|
| Kosten pro Control | ~$0,029 |
| Kosten fuer ~6.000 Controls | **~$172** |
| Laufzeit (geschaetzt) | ~25h |
| Obligations pro Control | ~10,5 |

---

## Naechste Schritte

1. ~~**Phase 5 Cleanup** → 3.301 Duplikate geloescht, Source normalisiert~~ ✅
2. ~~**Phase 6 Pipeline-Haertung** → Source aus REGULATION_LICENSE_MAP~~ ✅
3. ~~**Phase 7.1-7.3** → PDF-Validierung + Enrichment + Lueckenanalyse~~ ✅
4. ~~**Phase 7.4** → 233 neue Controls fuer Luecken generiert ($7,55)~~ ✅
5. **Pass 0a** → Obligation Extraction mit 3-Tier-Klassifizierung (Tests laufen, ~$172)
6. **Pass 0b** → Atomic Control Composition aus validierten Obligations
7. **Pass 1-5** → Multi-Layer Migration (Code + 500 Tests bereits vorhanden)
8. **Phase 8** → Qdrant Re-Ingestion (Runtime-Betrieb, ZULETZT)
9. **needs_review Triage** — 838 Controls klassifizieren
10. **Frontend** — `obligation_type` (Pflicht/Empfehlung/Kann) + `article_type` anzeigen
