/** * Erklärungen zu den Fragen des amtlichen Katalogs. * * Der Fragenkatalog des Bundesverwaltungsamtes nennt bei Freitextfragen eine * Musterantwort und markiert bei Auswahlfragen die zutreffenden Antworten. Was * er nicht sagt: **warum**. Genau das leisten die Erklärungen – und das ist * der eine Punkt, an dem kein Wettbewerber etwas anbietet. * * ## Trennung vom amtlichen Werk * * Die Erklärungen sind eigener redaktioneller Inhalt und stehen deshalb in * einer eigenen Datei, nicht im Katalog. Der amtliche Katalog ist ein * amtliches Werk nach § 5 Abs. 2 UrhG und wird unverändert wiedergegeben * (§ 62 UrhG); eigene Zusätze dürfen ihn nicht vermischen. In der Oberfläche * sind sie sichtbar als Ergänzung gekennzeichnet. * * ## Zusage an die Richtigkeit * * Jede Fundstelle ist **strukturiert** angegeben, nicht als Fließtext. Nur so * lässt sie sich maschinell gegen den amtlichen Gesetzestext prüfen: * `data-pipeline/pruefe_erklaerungen.py` schlägt jede einzelne Angabe im Index * aus `content/gesetze/index.json` nach und bricht ab, wenn ein Paragraf, ein * Absatz, eine Nummer oder ein Buchstabe nicht existiert. Eine erfundene * Fundstelle kommt damit nicht in die Auslieferung. * * Was die Prüfung **nicht** leisten kann: ob die zitierte Norm die Aussage * auch trägt. Dafür gibt es die redaktionelle Durchsicht – und die * ausdrückliche Angabe {@link Erklaerung.ohneFundstelleGrund}, die verlangt, * das Fehlen einer Fundstelle zu begründen, statt eine zu erfinden. */ /** Gesetze, auf die sich eine Fundstelle beziehen darf. */ export const GESETZE = [ 'WaffG', 'AWaffV', 'BeschG', 'BeschussV', 'StGB', 'SprengG', '1. SprengV', ] as const; export type Gesetzeskuerzel = (typeof GESETZE)[number]; /** Ausgeschriebene Bezeichnung für die Anzeige. */ export const GESETZ_BEZEICHNUNG: Readonly> = Object.freeze({ WaffG: 'Waffengesetz', AWaffV: 'Allgemeine Waffengesetz-Verordnung', BeschG: 'Beschussgesetz', BeschussV: 'Beschussverordnung', StGB: 'Strafgesetzbuch', SprengG: 'Sprengstoffgesetz', '1. SprengV': 'Erste Verordnung zum Sprengstoffgesetz', }); /** * Eine Fundstelle im Gesetz – strukturiert, damit sie prüfbar bleibt. * * Bewusst kein Fließtext: „siehe § 12 WaffG" ließe sich nicht nachschlagen, * ohne den Satz zu zerlegen, und jede Abweichung in der Schreibweise wäre ein * neues Sonderproblem. */ export interface Fundstelle { readonly gesetz: Gesetzeskuerzel; /** „§ 12" oder „Anlage 1". */ readonly norm: string; readonly absatz?: string; readonly nummer?: string; readonly buchstabe?: string; readonly satz?: string; /** * Nur bei Anlagen: die Stelle innerhalb der Anlage, etwa * „Abschnitt 1 Unterabschnitt 1 Nr. 1.1". Anlagen sind nicht in Absätze * gegliedert; geprüft wird deshalb durch Textsuche. */ readonly stelle?: string; } /** Erklärung zu genau einer Frage. */ export interface Erklaerung { /** * Ein Satz, der die richtige Antwort trägt. Steht unmittelbar unter der * Rückmeldung und muss für sich allein verständlich sein. */ readonly kurz: string; /** * Ausführliche Begründung: warum die richtige Antwort richtig ist und – * wo es hilft – warum die anderen es nicht sind. */ readonly text: string; /** Belege im Gesetz. Darf leer sein, dann ist {@link ohneFundstelleGrund} nötig. */ readonly fundstellen: readonly Fundstelle[]; /** * Warum es zu dieser Frage keine Fundstelle gibt. * * Nicht jede Prüfungsfrage hat eine gesetzliche Grundlage: Die * Sicherheitsregeln zur Handhabung und weite Teile der Waffentechnik sind * fachliche Praxis, nicht Gesetzestext. Dieses Feld verlangt, das * auszusprechen – die Alternative wäre, eine Fundstelle zu erfinden. */ readonly ohneFundstelleGrund?: string; /** Optionale Eselsbrücke. Nur, wo sie wirklich trägt. */ readonly merksatz?: string; /** * Fragen, die denselben Gegenstand behandeln. * * **Wozu.** Der amtliche Katalog kennt keine Frage-zu-Frage-Beziehung, obwohl * die Bündel auf der Hand liegen: II-34 fragt, was ein Schalldämpfer bewirkt, * II-36 fragt nach der Umkehrung – die Erklärung zu II-36 sagt das selbst im * Text. Wer eine Frage gerade verstanden hat, ist im besten Moment, die * Nachbarfrage mitzunehmen; bis 0.22.0 erfuhr er nie, dass es sie gibt. * * **Redaktionell, nicht amtlich.** Die Zuordnung ist eine Einschätzung dieser * Software. Sie ist bewusst **beidseitig**: Steht B bei A, muss A bei B * stehen. Eine einseitige Verknüpfung wäre fast immer ein Versehen beim * Schreiben, und sie zeigte die Verbindung nur dem, der zufällig von der * richtigen Seite kommt. `pruefe_erklaerungen.py` besteht darauf. * * **Keine Wertung.** Die Liste sagt „das gehört zusammen“, nicht „das musst * du auch können“ – der Prüfungsstoff steht im Katalog, nicht hier. */ readonly verwandt?: readonly string[]; /** * Woran sich eine vollständige Antwort erkennen lässt – bei offenen Fragen. * * **Wozu.** Bei 63 der 104 offenen Fragen ist in der amtlichen * Musterantwort nichts unterstrichen, und die Anwendung gibt die * Unterstreichungen seit 0.17.0 zu Recht nicht mehr als Pflichtinhalt aus * (`docs/entscheidung-freitext.md`). Damit stand der Lernende bei der * vierstufigen Selbstbewertung – die unmittelbar die Wiedervorlage steuert * – ohne jedes Kriterium da: Die Hilfe warnt vor zu milder Bewertung, gab * aber kein Werkzeug, das Wesentliche zu erkennen. * * **Was das Feld nicht ist.** Es sagt **nicht**, was eine Antwort enthalten * muss. Über einer Liste wie dieser stand bis 0.17.0 „Diese Kernelemente * muss Ihre Antwort enthalten“, und genau das war unbelegt. Die Punkte sind * eine Einschätzung dieser Software, in eigenen Worten aus der amtlichen * Musterantwort erarbeitet – eine Erkennungshilfe, kein * Bewertungsversprechen. Wer prüft, ist weiterhin der Prüfungsausschuss; * wer die eigene Antwort bewertet, bleibt der Lernende. * * **Warum nicht überall.** Wo die Musterantwort aus einer einzigen Tatsache * besteht („Unbefristet“, „Nein“, „Mindestens alle drei Jahre“), fehlt das * Feld. Eine Prüfliste mit einem Punkt wäre kein Werkzeug, sondern Beiwerk. */ readonly kernpunkte?: readonly string[]; } export interface ErklaerungenMeta { readonly version: number; /** Tag, an dem der Bestand zuletzt geprüft wurde (ISO-Datum). */ readonly stand: string; /** * Stand der Gesetze, gegen die geprüft wurde – wörtlich aus dem amtlichen * XML. Ändert sich ein Gesetz, ist erkennbar, worauf sich die Erklärungen * bezogen haben. */ readonly gesetzesstand: Readonly>; readonly hinweis: string; } export interface Erklaerungen { readonly meta: ErklaerungenMeta; /** Frage-ID aus dem Katalog -> Erklärung. Nicht jede Frage muss eine haben. */ readonly zuFrage: Readonly>; } /** * Sammelt die verwandten Fragen zu einer Reihe von Fragen. * * Fragen, die schon in der Eingabe stehen, fallen heraus: Wer sie eben * bearbeitet hat, braucht sie nicht als „verwandt“ vorgeschlagen zu bekommen. * Die Reihenfolge bleibt die, in der man ihnen begegnet ist – nach Kennung zu * sortieren ginge schief, weil „I.2-99“ vor „I.2-135“ kommt, lexikografisch * aber dahinter steht. * * Der Zugriff kommt als Funktion herein und nicht als ganzer Bestand: Die * Oberfläche hält die Erklärungen ohnehin nur hinter einem Nachschlager, und * so lässt sich die Sammlung ohne Attrappe des vollen Vertrags prüfen. */ export function verwandteSammeln( zu: (frageId: string) => Erklaerung | null, frageIds: readonly string[], ): string[] { const schonDa = new Set(frageIds); const gesammelt: string[] = []; const gesehen = new Set(); for (const frageId of frageIds) { for (const verwandt of zu(frageId)?.verwandt ?? []) { if (!schonDa.has(verwandt) && !gesehen.has(verwandt)) { gesehen.add(verwandt); gesammelt.push(verwandt); } } } return gesammelt; } /** * Fundstelle als Zitat, wie es in der Oberfläche erscheint. * * Eine einzige Stelle, an der die Schreibweise festgelegt wird – sonst hätte * jede Erklärung ihre eigene. */ export function fundstelleText(fundstelle: Fundstelle): string { const teile: string[] = [fundstelle.norm]; if (fundstelle.stelle !== undefined && fundstelle.stelle.length > 0) { teile.push(fundstelle.stelle); } if (fundstelle.absatz !== undefined) { teile.push(`Abs. ${fundstelle.absatz}`); } if (fundstelle.satz !== undefined) { teile.push(`Satz ${fundstelle.satz}`); } if (fundstelle.nummer !== undefined) { teile.push(`Nr. ${fundstelle.nummer}`); } if (fundstelle.buchstabe !== undefined) { teile.push(`Buchst. ${fundstelle.buchstabe}`); } return `${teile.join(' ')} ${fundstelle.gesetz}`; } /** Vollständiger Name für Bildschirmleser: Abkürzungen ausgeschrieben. */ export function fundstelleVorlesetext(fundstelle: Fundstelle): string { const teile: string[] = [fundstelle.norm.replace('§', 'Paragraf')]; if (fundstelle.stelle !== undefined && fundstelle.stelle.length > 0) { teile.push(fundstelle.stelle); } if (fundstelle.absatz !== undefined) { teile.push(`Absatz ${fundstelle.absatz}`); } if (fundstelle.satz !== undefined) { teile.push(`Satz ${fundstelle.satz}`); } if (fundstelle.nummer !== undefined) { teile.push(`Nummer ${fundstelle.nummer}`); } if (fundstelle.buchstabe !== undefined) { teile.push(`Buchstabe ${fundstelle.buchstabe}`); } return `${teile.join(' ')} ${GESETZ_BEZEICHNUNG[fundstelle.gesetz]}`; } /** * Die ganze Erklärung als vorlesbarer Text. * * **Wozu.** Die ausführliche Begründung ist der längste Text der Anwendung – * gesprochen über eine Minute. Bis 0.22.0 gab es keinen Weg, sie auf Abruf zu * hören: Die Vorlesetaste der Sitzung liest sie bewusst nicht mit (die * nächste Frage wartet, siehe `shared/ipc.ts`), und die Automatik greift nur * bei falscher Antwort. Wer bei einer richtigen Antwort trotzdem wissen * wollte, warum – oder wer sie in der Prüfungsauswertung nachlas –, war aufs * stille Lesen zurückgeworfen. Ausgerechnet in der Nachbereitung, dem * lernwirksamsten Moment. * * Die Fundstellen kommen in der ausgeschriebenen Form mit: „§ 12 Abs. 1“ * würde sonst buchstabiert oder falsch betont. Der Herkunftssatz bleibt * draußen – er steht am Bildschirm und ist keine Auskunft zur Sache. */ export function erklaerungVorlesetext(erklaerung: Erklaerung): string { const teile: string[] = [erklaerung.kurz, erklaerung.text]; if (erklaerung.merksatz !== undefined) { teile.push(`Merksatz: ${erklaerung.merksatz}`); } if (erklaerung.fundstellen.length > 0) { const liste = erklaerung.fundstellen.map((fundstelle) => fundstelleVorlesetext(fundstelle)); teile.push(`Im Gesetz nachlesen: ${liste.join('; ')}.`); } if (erklaerung.ohneFundstelleGrund !== undefined) { teile.push(erklaerung.ohneFundstelleGrund); } return teile.join('\n'); }