/** * Datentypen des amtlichen Fragenkatalogs. * * Spiegelt exakt das Format von `content/katalog/katalog.json`, das die * Datenpipeline (`data-pipeline/parse_catalog.py`) aus dem BVA-PDF erzeugt. * Der Fragenwortlaut ist amtlicher Inhalt und wird unverändert dargestellt; * eigene didaktische Ergänzungen liegen getrennt davon. */ /** * Ein Textabschnitt mit optionaler Hervorhebung. * * Im Katalog sind die Kernelemente der Musterantworten unterstrichen – also * jene Bestandteile, die eine Antwort in der Prüfung enthalten muss. Sie * werden hier als `h: true` geführt und tragen später das Freitext-Training. */ export interface TextSegment { /** Textinhalt des Abschnitts. */ readonly t: string; /** * `true`, wenn die Stelle im amtlichen Katalog unterstrichen ist. * * Was das Amt damit meint, sagt es nirgends. In Musterantworten liest die * Anwendung es als Hervorhebung, in Fragetexten als Betonung (meist einer * Verneinung) – dieselbe Auszeichnung in zwei Rollen. Was sie NICHT ist: * eine Vorgabe, was eine richtige Antwort enthalten muss. */ readonly h?: boolean; } /** Text mit Auszeichnung; `text` ist dieselbe Zeichenfolge ohne Auszeichnung. */ export interface RichText { /** Reiner Text – für Suche, Vorlesefunktion und Textvergleich. */ readonly text: string; /** Derselbe Text, zerlegt in ausgezeichnete Abschnitte. */ readonly segmente: readonly TextSegment[]; } /** Abbildung im Katalog – ausschließlich amtliche Prüf- und Zulassungszeichen. */ export interface KatalogBild { readonly id: string; readonly datei: string; readonly breite: number; readonly hoehe: number; /** * Alternativtext. Redaktioneller Inhalt aus `content/alttexte.json`. * Bei Fragen, in denen die Zeichen selbst die Antwortoptionen sind, * beschreibt er nur die Form und verrät die Lösung nicht. */ readonly alt: string | null; /** * Erklärende Bedeutung des Zeichens – zweite Stufe des Bildkonzepts aus * dem Prüfplan (Szenario S4). Ebenfalls redaktioneller Inhalt aus * `content/alttexte.json`. Weil sie die Lösung verraten kann, zeigt die * Oberfläche sie erst NACH dem Beantworten und nie im Prüfungslauf. */ readonly beschreibung: string | null; } /** Eine Antwortmöglichkeit einer Multiple-Choice-Frage. */ export interface Antwortoption { /** Amtliches Label, „a“ bis „h“. */ readonly label: string; readonly inhalt: RichText; readonly korrekt: boolean; /** Bild-IDs; bei einigen Fragen ist das Zeichen selbst die Antwort. */ readonly bilder: readonly string[]; } /** * Fragetyp. * - `mc`: Multiple Choice. Achtung: 114 der 471 MC-Fragen haben mehr als eine * richtige Antwort, die Auswahl muss also mehrfach zulassen. * - `freitext`: offene Frage, wird gegen eine Musterantwort selbst bewertet. * - `lueckentext`: Sonderfall (nur Frage 5.01). */ export type Fragetyp = 'mc' | 'freitext' | 'lueckentext'; export interface Frage { /** Stabile ID, z. B. „I.1-01“ oder „IV-89“. */ readonly id: string; /** Nummer wie im Katalog, z. B. „1.01“ – bleibt für Nutzer sichtbar. */ readonly amtliche_nummer: string; readonly kapitel: string; readonly abschnitt: string | null; readonly typ: Fragetyp; /** Seite im Original-PDF – erlaubt das Nachschlagen in der Vorlage. */ readonly seite: number; readonly frage: RichText; readonly bilder: readonly string[]; /** Nur bei `typ === 'mc'`. */ readonly optionen?: readonly Antwortoption[]; /** Nur bei offenen Fragen und beim Lückentext. */ readonly musterantwort?: RichText; /** Hinweise aus der Extraktion, die eine Sichtprüfung verdienen. */ readonly warnungen?: readonly string[]; } export interface Abschnitt { readonly id: string; readonly titel: string; } export interface Kapitel { readonly id: string; readonly titel: string; readonly abschnitte: readonly Abschnitt[]; } export interface KatalogMeta { readonly titel: string; readonly herausgeber: string; /** Katalogstand als ISO-Datum, z. B. „2024-12-16“. */ readonly stand: string; /** Pflichtangabe, die in der Oberfläche sichtbar geführt wird. */ readonly quellenangabe: string; readonly quelle_url: string; readonly quelldatei_sha256: string; readonly fragen_gesamt: number; } export interface Katalog { readonly meta: KatalogMeta; readonly kapitel: readonly Kapitel[]; readonly bilder: readonly KatalogBild[]; readonly fragen: readonly Frage[]; } // ─── Hilfsfunktionen ──────────────────────────────────────────────────────── /** Ist bei dieser Frage mehr als eine Antwort richtig? */ export function istMehrfachauswahl(frage: Frage): boolean { return anzahlRichtiger(frage) > 1; } export function anzahlRichtiger(frage: Frage): number { return frage.optionen?.filter((o) => o.korrekt).length ?? 0; } /** Labels aller richtigen Optionen, aufsteigend sortiert. */ export function richtigeLabels(frage: Frage): string[] { return (frage.optionen ?? []).filter((o) => o.korrekt).map((o) => o.label); } /** * Bewertet eine Multiple-Choice-Auswahl. * * Eine Antwort gilt nur als richtig, wenn genau die richtigen Optionen gewählt * wurden – nicht weniger und nicht mehr. Das entspricht der Prüfungspraxis: * eine unvollständige Auswahl ist keine richtige Antwort. */ export function bewerteAuswahl( frage: Frage, auswahl: readonly string[], ): { richtig: boolean; fehlend: string[]; zuviel: string[] } { const soll = new Set(richtigeLabels(frage)); const ist = new Set(auswahl); const fehlend = [...soll].filter((l) => !ist.has(l)).sort(); const zuviel = [...ist].filter((l) => !soll.has(l)).sort(); return { richtig: fehlend.length === 0 && zuviel.length === 0, fehlend, zuviel }; } /** * Die im amtlichen Katalog unterstrichenen Stellen einer Musterantwort. * * Hieß bis Fassung 0.17.0 „Kernelemente – die Bestandteile, die enthalten * sein müssen“. Der Name blieb, die Behauptung ist weg: Nachgemessen * schwankt der unterstrichene Anteil zwischen 3 und 100 Prozent der * Musterantwort, und bei 63 der 104 offenen Fragen ist gar nichts * unterstrichen. Als Pflichtinhalt taugt das nicht, als Lesehilfe schon. */ export function kernelemente(text: RichText | undefined): string[] { if (!text) return []; return text.segmente .filter((s) => s.h) .map((s) => s.t.trim()) .filter((s) => s.length > 0); } /** Menschlich lesbare Bezeichnung eines Fragetyps. */ export function fragetypBezeichnung(typ: Fragetyp): string { switch (typ) { case 'mc': return 'Multiple Choice'; case 'freitext': return 'Frage zum Ausformulieren'; case 'lueckentext': return 'Lückentext'; } }