# Datenpipeline: BVA-Fragenkatalog → JSON

Wandelt den amtlichen „Fragenkatalog für die Sachkundeprüfung (gemäß § 7 WaffG)"
des Bundesverwaltungsamtes (Stand 16.12.2024) in eine strukturierte, barrierefrei
nutzbare Datenbasis um. Der Fragenwortlaut bleibt dabei unverändert.

## Ausführen

```bash
python data-pipeline/parse_catalog.py
python data-pipeline/validate_catalog.py
python data-pipeline/diagnose_kreuze.py
```

Der Parser liest `Fragenkatalog_sachkunde_mitAntworten.pdf` aus dem
Projektwurzelverzeichnis und schreibt nach `content/katalog/`:

| Datei | Inhalt |
|---|---|
| `katalog.json` | Alle 575 Fragen mit Optionen, Lösungen, Musterantworten |
| `assets/*.png` | 17 Prüf- und Zulassungszeichen |
| `trennstriche.txt` | Zeilenend-Trennungen, bei denen ein Bindestrich erhalten blieb (QS-Liste) |

`validate_catalog.py` prüft das Ergebnis gegen die am Original ermittelten
Sollwerte und bricht bei Abweichungen mit Exit-Code 1 ab. Er ist damit als
CI-Schritt geeignet.

`diagnose_kreuze.py` prüft die Kreuzerkennung **am Bild statt am Vektorpfad**
und ist damit die einzige Stelle, die sie unabhängig von der Logik des Parsers
belegt (siehe unten, Punkt 2). Auch er antwortet mit Exit-Code 0 oder 1; ein
voller Lauf über alle 124 Seiten dauert rund zweieinhalb Sekunden.

```bash
python data-pipeline/diagnose_kreuze.py --seiten 6-20   # Ausschnitt
python data-pipeline/diagnose_kreuze.py --selbsttest    # die Probe prüfen
```

Voraussetzung: `pip install pymupdf`

## Sollwerte (alle am Original verifiziert)

| Kennzahl | Wert |
|---|---|
| Fragen gesamt | 575 |
| davon Multiple Choice | 471 |
| davon offene Fragen | 104 (103 Freitext + 1 Lückentext) |
| Antwortoptionen | 1430 |
| angekreuzte Optionen | 629 |
| Fragen mit mehreren richtigen Antworten | 114 |
| Fragen mit **allen** Optionen richtig | 28 |
| Fragen ohne jede richtige Option | 0 |
| Fragen mit Abbildung | 23 |
| Bild-Assets | 17 |

Verteilung je Abschnitt: I.1 = 90, I.2 = 154, I.3 = 35, I.4 = 23, I.5 = 43,
II = 92, III = 49, IV = 89.

Die letzten beiden Zeilen sind die Randfälle der Kreuzerkennung und zugleich
die beiden Fehlerbilder eines Einlesefehlers: alles angekreuzt (Layout nicht
erkannt) und nichts angekreuzt (Kreuze verloren). `validate_catalog.py` hält
deshalb nicht nur die Anzahl 28 fest, sondern die **Menge** der 28 Fragen samt
amtlicher Nummer und PDF-Seite — eine Zählung bliebe grün, wenn eine Frage die
Kreuze verlöre und eine andere welche bekäme. Dass die 28 echt sind, ist am
Original belegt (siehe unten, Punkt 2).

## Wie die Vorlage gelesen wird

Das PDF ist born-digital mit fehlerfreiem Textlayer – kein OCR nötig. Die Fragen
stehen in einer unsichtbaren vierspaltigen Tabelle:

```
x  71–103   amtliche Fragennummer      (Anker für den Fragenbeginn)
x 104–292   Fragetext
x 292–540   Antwortoptionen bzw. Musterantwort
x 503–529   Ankreuzkästchen
```

Beim Parsen sind fünf Eigenheiten der Vorlage zu beachten; jede war Ursache
eines konkreten Fehlers und ist im Code kommentiert:

1. **Fragetext und erste Antwortoption stehen oft in derselben Textzeile.**
   Getrennt wird deshalb nach x-Koordinate, nicht nach Zeilen.
2. **Lösungen stehen nicht im Text**, sondern als Vektorgrafik: ein Kästchen ist
   angekreuzt, wenn zwei Diagonalsegmente vollständig darin liegen.

   Diese Erkennung steht vor einer Lücke, nicht vor einem Schwellwert:
   Nachgemessen über alle 1430 Kästchen liegen in genau 629 jeweils **zwei**
   Diagonalsegmente, in den übrigen 801 **keines** — kein einziges hat eines,
   drei oder vier. Unabhängig gegengeprüft durch Rendern derselben Kästchen bei
   8-facher Vergrößerung: Tintendichte im Inneren 0,0000 bei allen 801,
   0,1653 bis 0,1978 bei allen 629, null Widersprüche.

   Diese Gegenprobe ist als `diagnose_kreuze.py` wiederholbar und gehört
   deshalb nicht zu den Erkundungsskripten unten. Sie ist die einzige Prüfung
   des Projekts, die nicht wieder über Vektorpfade urteilt: Ein Denkfehler in
   der Geometrie stünde sonst in Umsetzung und Prüfung gleichermaßen. Beide
   Verfahren teilen genau eine Annahme — wo ein Kästchen liegt — und keine
   darüber, was ein Kreuz ist. `--selbsttest` erzeugt jedes der vier
   Fehlerbilder absichtlich (Vektor sieht ein Kreuz zu viel, eines zu wenig;
   Tinte im leeren Kästchen; angekreuztes Kästchen fast leer) und prüft, dass
   die Probe anschlägt.

   Anlass der Nachmessung waren die 28 Fragen, bei denen **alle** Optionen
   angekreuzt sind — der Verdacht, ein Layout sei nicht erkannt und
   vorsichtshalber alles genommen worden. Alle 28 Tabellenzeilen wurden
   zusätzlich als Bild betrachtet: Die Kreuze stehen so in der Vorlage. Es sind
   durchweg Aufzählungsfragen, bei denen jede Nennung richtig ist („Welche
   Munitionsarten sind vom Waffengesetz erfasst?" — Patronenmunition,
   hülsenlose Munition, pyrotechnische Munition).
3. **Kästchen sitzen vertikal mittig zur Option**, bei mehrzeiligen Optionen also
   unterhalb ihrer ersten Zeile. Die Optionsgrenzen werden daher aus den Labels
   („a)", „b)", …) am linken Spaltenrand abgeleitet; Kästchen werden anschließend
   der Reihenfolge nach zugeordnet.
4. **Kernelemente der Musterantworten sind unterstrichen** – Füllrechtecke der
   Höhe ≈ 0,84 pt, unterscheidbar von Tabellenlinien (≈ 0,48 pt). Sie werden als
   `hervorgehoben`-Segmente übernommen und tragen später das Freitext-Training.
5. **Ein Prüfzeichen beginnt oberhalb seines eigenen Labels.** Besteht eine
   Antwortmöglichkeit nur aus einem Zeichen und trägt keinen Text, setzt der
   Satz das Zeichen senkrecht mittig in seine Tabellenzeile – das Label steht
   dann *innerhalb* des Bildrechtecks. Liegt genau ein Label im Bild, gilt es;
   sonst entscheidet die Bildmitte. Nachgemessen: 43 Bilder stehen in
   Antwortspalten, bei 38 liegt kein Label im Bild.

   Bis Fassung 0.19.2 stand hier ein Zerschneider, der ein Bild an der
   leersten Bildzeile in zwei Hälften teilte, sobald eine Option leer blieb.
   Er sprang im ganzen Katalog genau einmal an – bei Frage 3.05 – und lag dort
   falsch: Er trennte das Doppelzeichen *BKA-Raute über PTB-Trapez*, das in
   Frage 2.70 ungeteilt eine einzige Antwortmöglichkeit bildet
   („Reizstoff-Sprühdosen mit dem Zeichen"), und hängte die obere Hälfte an
   die falsche Antwort. Bleibt heute eine Antwortmöglichkeit ohne Text und
   ohne Bild, rät nichts mehr – der Katalog vermerkt es als Warnung.

Zeilenend-Trennungen werden zusammengeführt: vor Bindewörtern bleibt der
Ergänzungsstrich („Kinder- und Jugendarbeit"), vor Großschreibung der echte
Bindestrich („Physikalisch-Technische"), sonst wird das Wort geschlossen
(„Signalge-" + „bung" → „Signalgebung").

## Datenformat

```jsonc
{
  "meta":   { "titel", "herausgeber", "stand", "quellenangabe",
              "quelldatei_sha256", "fragen_gesamt" },
  "kapitel": [ { "id": "I", "titel", "abschnitte": [ { "id": "I.1", "titel" } ] } ],
  "bilder":  [ { "id", "datei", "breite", "hoehe", "alt": null,
                 "beschreibung": null } ],
  "fragen":  [ {
    "id": "I.1-01",            // stabile ID: Kapitelpräfix + laufende Nummer
    "amtliche_nummer": "1.01", // Nummer wie im Katalog, bleibt sichtbar
    "kapitel": "I", "abschnitt": "I.1",
    "typ": "mc" | "freitext" | "lueckentext",
    "seite": 6,
    "frage":   { "text": "…", "segmente": [ { "t": "…", "h": true } ] },
    "bilder":  [ "zeichen-…" ],
    // bei typ == "mc":
    "optionen": [ { "label": "a", "inhalt": { "text", "segmente" },
                    "korrekt": true, "bilder": [] } ],
    // sonst:
    "musterantwort": { "text": "…", "segmente": [ … ] }
  } ]
}
```

`segmente` trägt die Auszeichnung mit: `{"t": "…"}` ist normaler Text,
`{"t": "…", "h": true}` ein hervorgehobenes Kernelement. `text` ist dieselbe
Zeichenfolge ohne Auszeichnung und dient Suche und Vorlesefunktion.

## Offene Punkte

- **Bildtexte:** liegen für alle 17 Bilder in `content/alttexte.json` und werden
  beim Erzeugen in `katalog.json` eingesetzt. Sie sind eigener, nicht-amtlicher
  Inhalt. Je Zeichen zwei Stufen (Prüfplan, Szenario S4): `alt` ist der
  Alternativtext des Bildes, `beschreibung` die erklärende Bedeutung, die die
  Anwendung erst **nach** dem Beantworten nachreicht und im Prüfungslauf gar
  nicht zeigt. Achtung bei den Fragen 3.05, 3.24 und IV-39: Dort sind die
  Zeichen selbst die Antwortoptionen – der Alt-Text beschreibt deshalb rein die
  Form („Bundesadler über dem Buchstaben N") und verrät weder Bedeutung noch
  Lösung; genau dafür gibt es die zweite Stufe. Die Formulierungen der
  `beschreibung` stammen aus den Erklärungstexten in
  `content/erklaerungen.json`, nicht aus einer eigenen Recherche.
- **Lückentext 5.01**: Die Lücken sind im Fragetext derzeit nur Leerstellen. Für
  ein echtes Lückentext-Training müssen sie noch markiert werden (Einzelfall).
- **Manuelle Sichtprüfung** aller 575 Fragen gegen das PDF steht aus; die
  automatischen Prüfungen decken Struktur und Vollständigkeit ab, nicht jeden
  Buchstaben.

## Erklärungstexte und Rechtsfundstellen

Der amtliche Katalog nennt die richtige Antwort, aber nie den Grund. Die
Erklärungen in `content/erklaerungen.json` liefern ihn nach – eigener
redaktioneller Inhalt, streng getrennt vom amtlichen Werk.

Bei Waffenrecht ist eine erfundene Fundstelle kein Schönheitsfehler: Wer sich
darauf verlässt, lernt geltendes Recht falsch. Deshalb steht vor dem Inhalt
die Prüfgrundlage.

### Ablauf

```bash
# 1. Amtliche Gesetzestexte holen (einmalig bzw. bei Gesetzesänderung)
cd content/gesetze
for g in waffg_2002 awaffv beschg beschussv stgb sprengg_1976 sprengv_1; do
  curl -sSO "https://www.gesetze-im-internet.de/$g/xml.zip" && unzip -oq xml.zip && rm xml.zip
done
cd ../..

# 2. Prüfbaren Index bauen
python data-pipeline/gesetze_index.py

# 3. Beim Schreiben nachschlagen
python data-pipeline/gesetz.py WaffG "§ 12" --gliederung
python data-pipeline/gesetz.py --suche "Anscheinswaffe"
python data-pipeline/fragen.py --abschnitt I.1 --von 0 --anzahl 10

# 4. Teilstücke zusammenführen
python data-pipeline/erklaerungen_zusammenfuehren.py <verzeichnis>

# 5. Prüfen – ohne bestandene Prüfung wird nicht ausgeliefert
python data-pipeline/pruefe_erklaerungen.py
python data-pipeline/pruefe_erklaerungen.py --selbsttest

# 6. Die zitierten Normtexte für die Auslieferung ausschneiden
python data-pipeline/normtexte_bauen.py
python data-pipeline/normtexte_bauen.py --selbsttest
```

### Warum `normtexte.json` neben dem Index steht

Der Index enthält die sieben Gesetze **vollständig** – 914 Normen, darunter
katalogfremde wie § 173 StGB. Ausgeliefert wird nur, was in Erklärungen oder
Glossar wirklich zitiert wird: 142 Paragrafen und 6 Anlagen, rund 500 KiB. Ohne
diese Datei führte „Im Gesetz nachlesen“ ins Leere – eine ausdrücklich
vollständig offline arbeitende Software forderte zu etwas auf, das offline
gerade nicht ging.

Anlagen kennen keine Absatzgliederung und sind seitenlang. Sie werden in ihre
eigenen Gliederungsblöcke zerlegt, und zwar **verlustfrei**: Die Blöcke ergeben
aneinandergehängt wieder Zeichen für Zeichen den Ausgangstext. Das Skript
rechnet es für jede Anlage nach und bricht ab, statt etwas zu schreiben. Ein
Block ist ein Sprungziel, kein Ausschnitt – ein falsch gesetzter Schnitt wäre
ein verfälschtes Gesetzeszitat in einer Lernsoftware für eine Rechtsprüfung.

### Die Themengliederung

```bash
python data-pipeline/themen_bauen.py
python data-pipeline/themen_bauen.py --selbsttest
```

Der amtliche Katalog gliedert **nur Kapitel I** in Abschnitte. Für die 230
Fragen der Kapitel II bis IV schreibt `themen_bauen.py` eine redaktionelle
Feingliederung nach `content/themen.json` – 29 Gruppen, von Hand geschrieben.

Das Skript schreibt nur, wenn **jede** Frage dieser Kapitel in genau einer
Gruppe liegt. Eine lückenhafte Gliederung wäre schlimmer als keine: „Thema
üben“ überginge dann Fragen, ohne es zu sagen. Nach jeder Katalogänderung
deshalb erneut laufen lassen – neue Fragen fallen sonst durch das Raster, und
zwar unbemerkt.

Nach jeder Gesetzesänderung: erst Schritt 2, dann Schritt 5, dann Schritt 6.
Wer Schritt 6 vergisst, liefert die Normtexte des alten Standes aus, während die
Fundstellen schon gegen den neuen geprüft wurden.

### Was die Prüfung leistet

`pruefe_erklaerungen.py` schlägt jede Fundstelle im Index nach: Gesetz,
Paragraf, Absatz, Nummer, Buchstabe. Was dort nicht steht, existiert nicht.
Bei Normen ohne Absatzgliederung (etwa § 37a WaffG) wird gegen die
Aufzählungsmarken im Normtext geprüft, Satzangaben gegen die mögliche
Satzanzahl, Anlagen-Stellen per Textsuche. Die Ausgabe unterscheidet
„vollständig geprüft“ (jede Angabe nachgewiesen) von „nur Existenz geprüft“.
Der Selbsttest erzeugt jede der 22 Fehlerarten absichtlich – eine Prüfung, die
nichts beanstandet, ist von einer funktionierenden nicht zu unterscheiden.

### Was sie nicht leisten kann

Ob die zitierte Norm die Aussage inhaltlich **trägt**. Eine Erklärung kann auf
einen existierenden Paragrafen verweisen und trotzdem falsch sein – genau das
ist beim Erproben passiert: Eine Erklärung zum Begriff „Umgang“ zitierte
§ 2 Abs. 2 WaffG, der die Erlaubnispflicht regelt und nicht den Begriff.
Richtig ist § 1 Abs. 3 WaffG. Die Fundstelle kam durch die Prüfung.

Dagegen hilft nur, dass jede Erklärung von einer zweiten, unabhängigen Stelle
gegen den Normtext gelesen wird. Fehlt eine gesetzliche Grundlage – bei
Sicherheitsregeln und Waffentechnik der Normalfall –, verlangt das Feld
`ohneFundstelleGrund`, das auszusprechen, statt eine Fundstelle zu erfinden.

### Gesetzesstand

`content/erklaerungen.json` vermerkt im Kopf den Stand jedes zitierten
Gesetzes, wörtlich aus dem amtlichen XML. Die Prüfung vergleicht ihn mit dem
Index und schlägt an, wenn beides auseinanderläuft. Ändert sich ein Gesetz,
ist damit erkennbar, worauf sich die Erklärungen bezogen haben.

Die XML-Rohdateien sind nicht versioniert (siehe `.gitignore`); versioniert
ist nur der daraus erzeugte Index.

## Glossar der Fachbegriffe

Das Waffenrecht benutzt Alltagswörter mit eigener Bedeutung. „Führen" heißt
nicht lenken, „Erwerben" hat mit Kaufen nichts zu tun. Wer das nicht weiß,
versteht die Frage nicht – unabhängig davon, wie gut er den Stoff kann. Genau
darauf zielen WCAG 3.1.3 (Ungewöhnliche Wörter) und 3.1.4 (Abkürzungen);
beide stehen im Prüfplan dieses Projekts als Zusage.

### Ablauf

```bash
# Wortformen gegen Katalog und Erklärungen prüfen
python data-pipeline/fragen.py --wortform führt geführt Führens

# Teilstücke zusammenführen
python data-pipeline/glossar_zusammenfuehren.py <verzeichnis>

# Prüfen – ohne bestandene Prüfung wird nicht ausgeliefert
python data-pipeline/pruefe_glossar.py
python data-pipeline/pruefe_glossar.py --selbsttest
```

### Wortformen

Deutsch flektiert. Ohne die Wortformen bliebe ein Begriff in genau den Fragen
unerkannt, in denen er gebraucht wird. Gesucht wird mit derselben Wortgrenze
wie in der Anwendung – `` taugt dafür nicht, weil es nur ASCII kennt und in
„Schießstätte" an jedem ß eine Grenze sähe.

Ein Eintrag **darf** ohne Wortform bleiben. Er erscheint dann nur in der
Glossaransicht und an keiner Frage. Das ist bei Abkürzungen normal, die in den
Erklärungen vorkommen, aber nicht im amtlichen Fragenwortlaut.

### Woher die Bestimmungen stammen

Wo das Gesetz einen Begriff selbst bestimmt, gibt der Eintrag diese Bestimmung
wieder – nicht eine eigene Umschreibung. Die waffenrechtlichen Begriffe stehen
in Anlage 1 Abschnitt 2 WaffG, die technischen in Abschnitt 1. Jede Fundstelle
wird mit derselben Prüfung wie bei den Erklärungen nachgewiesen;
`pruefe_glossar.py` erbt sie wörtlich, damit für beide derselbe Maßstab gilt.

### Selbsttest

`--selbsttest` verletzt jede Regel einmal absichtlich – Eindeutigkeit,
Wortformen, Querverweise, Form, redliche Lücken, Gesetzesstand – und besteht
nur, wenn jede Verletzung auch beanstandet wird. Dazu drei Gegenproben: Ein
gültiger Eintrag darf weder Befund noch Hinweis auslösen, die Wortsuche muss
eine echte Form im Katalog wirklich finden (sonst wäre „ohne Vorkommen" die
Standardantwort und keiner der Wortform-Fälle sagte etwas aus), und der
ausgelieferte Bestand muss unbeanstandet durchlaufen. Zusammen 34 Fälle.

Bis Fassung 0.21.0 war die Fahne wirkungslos: Sie stand im Modulkopf, aber
`main()` las `sys.argv` nicht. Der Aufruf mit `--selbsttest` fuhr stumm
dieselbe Bestandsprüfung und meldete Exit-Code 0 – in einer Prüfkette lief
damit dieselbe Prüfung zweimal, während die glossareigenen Regeln von keinem
Selbsttest gedeckt waren. Ein vertippter Schalter fällt heute mit Exit-Code 2
auf, statt ersatzweise die Bestandsprüfung zu fahren.

## Rechtlicher Hinweis

Der Katalog ist ein amtliches Werk; sein Wortlaut wird unverändert übernommen und
mit Quellenangabe geführt (`meta.quellenangabe`). Eigene didaktische Inhalte
(Erklärungen, Eselsbrücken) sind davon getrennt zu halten und als nicht-amtlich
zu kennzeichnen. Details siehe `PLAN.md`, Abschnitt 2.5.

## Erkundungsskripte

`explore_layout.py`, `explore_details.py` und `diagnose_columns.py` haben die
oben genannten Layout-Konstanten am Original vermessen. Sie gehören nicht zum
Produktivlauf, dokumentieren aber, woher die Zahlen stammen, und sind bei einer
neuen Katalogfassung der erste Anlaufpunkt.
