waffensachkunde
Waffensachkunde – Lernsoftware für die Sachkundeprüfung nach § 7 WaffG. Barrierefrei, offline, EUPL-1.2.
| 1 | /** |
| 2 | * Datentypen des Lernstands. |
| 3 | * |
| 4 | * Der Lernstand liegt ausschließlich lokal in einer SQLite-Datei je Profil. |
| 5 | * Es gibt kein Konto, keine Cloud und keine Telemetrie. |
| 6 | */ |
| 7 | |
| 8 | import type { Reifestufe } from './reife'; |
| 9 | |
| 10 | /** |
| 11 | * Selbst- bzw. Systembewertung einer Antwort. |
| 12 | * |
| 13 | * Die vier Stufen entsprechen den Bewertungen des FSRS-Verfahrens, das später |
| 14 | * die Wiedervorlage plant. Bei Multiple Choice werden sie automatisch |
| 15 | * vergeben, bei offenen Fragen wählt der Lernende selbst. |
| 16 | */ |
| 17 | export type Bewertung = 'nochmal' | 'schwer' | 'gut' | 'leicht'; |
| 18 | |
| 19 | export const BEWERTUNGEN: readonly Bewertung[] = ['nochmal', 'schwer', 'gut', 'leicht']; |
| 20 | |
| 21 | export const BEWERTUNG_BEZEICHNUNG: Readonly<Record<Bewertung, string>> = Object.freeze({ |
| 22 | nochmal: 'Nicht gewusst', |
| 23 | schwer: 'Mit Mühe gewusst', |
| 24 | gut: 'Gewusst', |
| 25 | leicht: 'Sicher gewusst', |
| 26 | }); |
| 27 | |
| 28 | /** |
| 29 | * Welche Bewertung als gewusst zählt. |
| 30 | * |
| 31 | * „Nicht gewusst“ ist der einzige Fehlschlag – „mit Mühe gewusst“ ist eine |
| 32 | * erfolgreiche, wenn auch mühsame Erinnerung (so rechnet auch FSRS). |
| 33 | * |
| 34 | * **Steht seit 0.22.0 hier und nicht mehr nur im Renderer**: Der |
| 35 | * Anwendungskern braucht dieselbe Grenze für den Reife-Beleg, seit sich eine |
| 36 | * richtige Auswahlantwort als geraten eingestehen lässt. Zwei Fassungen |
| 37 | * derselben Regel liefen auseinander, und eine davon wäre dann die falsche. |
| 38 | */ |
| 39 | export function giltAlsRichtig(bewertung: Bewertung): boolean { |
| 40 | return bewertung !== 'nochmal'; |
| 41 | } |
| 42 | |
| 43 | /** Eine beantwortete Frage, wie sie protokolliert wird. */ |
| 44 | export interface Antwortprotokoll { |
| 45 | readonly frageId: string; |
| 46 | /** Bei Multiple Choice die gewählten Labels, sonst leer. */ |
| 47 | readonly auswahl: readonly string[]; |
| 48 | /** Bei offenen Fragen der eingegebene Text (kann leer bleiben). */ |
| 49 | readonly freitext?: string; |
| 50 | readonly richtig: boolean; |
| 51 | readonly bewertung: Bewertung; |
| 52 | /** Bearbeitungsdauer in Millisekunden. */ |
| 53 | readonly dauerMs: number; |
| 54 | } |
| 55 | |
| 56 | /** Lernstand einer einzelnen Frage. */ |
| 57 | export interface FrageStand { |
| 58 | readonly frageId: string; |
| 59 | readonly versuche: number; |
| 60 | readonly richtige: number; |
| 61 | /** Zeitpunkt der letzten Antwort als ISO-Zeichenkette. */ |
| 62 | readonly zuletztBeantwortet: string | null; |
| 63 | /** Fällig ab diesem Zeitpunkt; `null`, solange nie beantwortet. */ |
| 64 | readonly faelligAb: string | null; |
| 65 | readonly gemerkt: boolean; |
| 66 | /** Zuletzt vergebene Bewertung. */ |
| 67 | readonly letzteBewertung: Bewertung | null; |
| 68 | } |
| 69 | |
| 70 | /** Zusammenfassung für einen Kapitel- oder Abschnittsbereich. */ |
| 71 | export interface BereichStatistik { |
| 72 | /** Kapitel- oder Abschnitts-ID, z. B. „I.2“. */ |
| 73 | readonly id: string; |
| 74 | readonly titel: string; |
| 75 | readonly fragenGesamt: number; |
| 76 | readonly beantwortet: number; |
| 77 | /** |
| 78 | * Fragen, deren Abruf belegt ist und noch frisch – siehe `shared/reife.ts`. |
| 79 | * Eine Untergrenze, keine Prognose. |
| 80 | */ |
| 81 | readonly belegt: number; |
| 82 | /** Reifegrad, 0 bis 1. `belegt` ist dieser Wert auf ganze Fragen gerundet. */ |
| 83 | readonly reifegrad: number; |
| 84 | readonly stufe: Reifestufe; |
| 85 | } |
| 86 | |
| 87 | /** |
| 88 | * Dasselbe für eine Themengruppe dieser Software. |
| 89 | * |
| 90 | * **Warum es das zusätzlich gibt.** Der amtliche Katalog gliedert nur |
| 91 | * Kapitel I in Abschnitte. Die 230 Fragen der Kapitel II bis IV standen in |
| 92 | * der Aufschlüsselung deshalb als **drei** Balken – „Kapitel IV, 61 von 88“ |
| 93 | * sagt einem Lernenden nicht, was er üben soll. Die Feingliederung in |
| 94 | * `content/themen.json` sagt es: 29 Gruppen, jede mit einem Titel wie |
| 95 | * „Notwehr und Notstand“ oder „Munitionsarten“. |
| 96 | * |
| 97 | * **Warum getrennt und nicht in {@link BereichStatistik} gemischt.** Die |
| 98 | * Gruppen sind **redaktionell**, nicht amtlich. In einer Liste mit den |
| 99 | * amtlichen Abschnitten sähen sie aus wie deren Geschwister. Getrennt kann |
| 100 | * die Oberfläche sagen, woher die Gliederung kommt – und die amtlichen |
| 101 | * Zahlen bleiben Zeichen für Zeichen dieselben wie vorher. |
| 102 | */ |
| 103 | export interface Themengruppenstatistik extends BereichStatistik { |
| 104 | /** Kapitel, unter dem die Gruppe steht – „II“, „III“ oder „IV“. */ |
| 105 | readonly kapitel: string; |
| 106 | } |
| 107 | |
| 108 | /** Gesamtüberblick für den Startbildschirm. */ |
| 109 | export interface Lernuebersicht { |
| 110 | readonly fragenGesamt: number; |
| 111 | readonly beantwortet: number; |
| 112 | /** Siehe {@link BereichStatistik.belegt}. */ |
| 113 | readonly belegt: number; |
| 114 | readonly reifegrad: number; |
| 115 | /** Gesamtstufe, gedeckelt durch einen zurückliegenden K.-o.-Bereich. */ |
| 116 | readonly stufe: Reifestufe; |
| 117 | /** |
| 118 | * Bereiche, die das Gesamturteil deckeln – heute nur Notwehr und Notstand. |
| 119 | * Leer, wenn keiner zurückliegt. Die Oberfläche nennt sie beim Namen, |
| 120 | * statt nur eine Stufe tiefer zu zeigen. |
| 121 | */ |
| 122 | readonly deckelnd: readonly string[]; |
| 123 | readonly faellig: number; |
| 124 | readonly gemerkt: number; |
| 125 | /** |
| 126 | * Offene Fragen im Lernumfang – die auszuformulierenden. |
| 127 | * |
| 128 | * Zählt wie {@link Lernuebersicht.fragenGesamt} nur, was das Zielprofil |
| 129 | * einschließt: Ohne Kapitel IV sind es 75 statt 104. Deshalb steht die |
| 130 | * Zahl hier und wird nicht in der Oberfläche aus dem Katalog gerechnet – |
| 131 | * dort wäre die Kapitelabwahl ein zweites Mal nachzubilden. |
| 132 | */ |
| 133 | readonly offen: number; |
| 134 | /** |
| 135 | * Fragen, die zuletzt falsch beantwortet wurden. |
| 136 | * |
| 137 | * Dieselbe Menge, die der Einstieg „Nur Fehler“ vorlegt und die das |
| 138 | * Fehlerprotokoll druckt – gebildet aus derselben Regel, damit alle drei |
| 139 | * dasselbe sagen. Zählt wie {@link Lernuebersicht.fragenGesamt} nur, was |
| 140 | * das Zielprofil einschließt. |
| 141 | */ |
| 142 | readonly fehler: number; |
| 143 | readonly heuteRichtig: number; |
| 144 | readonly heuteFalsch: number; |
| 145 | /** |
| 146 | * Fragen des Lernumfangs, die heute bearbeitet wurden. |
| 147 | * |
| 148 | * Eine Bestandszahl aus `frage_stand`, kein Protokoll: Sie zählt |
| 149 | * **verschiedene Fragen**, nicht Zeilen. Eine mit „Nicht gewusst“ bewertete |
| 150 | * Frage ist sofort wieder fällig und käme sonst zweimal vor. |
| 151 | */ |
| 152 | readonly heuteBearbeitet: number; |
| 153 | /** |
| 154 | * Volle Kalendertage seit der letzten Antwort; `null`, solange nie |
| 155 | * beantwortet. |
| 156 | * |
| 157 | * Positiv für Vergangenes – anders als `tageBisTermin` im Lernplan, das |
| 158 | * vorwärts rechnet. Wer nach zwei Wochen zurückkommt, sieht einen |
| 159 | * gefallenen Reifegrad und ein gewachsenes Pensum; diese Zahl ist die |
| 160 | * Tatsache dazu. |
| 161 | */ |
| 162 | readonly tageSeitLetzterAntwort: number | null; |
| 163 | /** |
| 164 | * Wie oft Wiederholungen nach mindestens einem Tag Abstand wirklich saßen. |
| 165 | * |
| 166 | * **Wozu.** Der Lernplan steuert auf eine Zielquote (0,90 bis 0,97), und die |
| 167 | * Reife-Ampel zeigt modellierte Abrufwahrscheinlichkeiten. Nirgends stand |
| 168 | * bis 0.22.0, wie oft der Lernende fällige Wiederholungen **tatsächlich** |
| 169 | * trifft. Diese Zahl schließt die Lücke zwischen der Behauptung des Modells |
| 170 | * und dem Befund am Menschen — im Geist des Projekts: Messung statt |
| 171 | * Behauptung. |
| 172 | * |
| 173 | * Eine **gemessene Vergangenheitszahl, keine Vorhersage**: Die abgelehnte |
| 174 | * Bestehenswahrscheinlichkeit bleibt außen vor. Gezählt werden Antworten, |
| 175 | * deren Vorgänger zur selben Frage mindestens einen Tag zurücklag — |
| 176 | * dieselbe Abstandsregel, die die Belegrechnung benutzt. |
| 177 | * |
| 178 | * `null`, solange zu wenige solcher Wiederholungen vorliegen: Eine Quote |
| 179 | * aus drei Antworten wäre eine Zahl ohne Aussage. |
| 180 | */ |
| 181 | readonly behaltensquote?: Behaltensquote | null; |
| 182 | readonly bereiche: readonly BereichStatistik[]; |
| 183 | /** |
| 184 | * Die Feingliederung der Kapitel II bis IV; leer, wenn keine vorliegt. |
| 185 | * |
| 186 | * Sie **ersetzt** die Kapitelzeilen in {@link bereiche} nicht, sondern |
| 187 | * steht darunter. Nachgemessen am Katalogstand 16.12.2024: Die 29 Gruppen |
| 188 | * decken alle 230 Fragen dieser Kapitel, jede genau einmal. |
| 189 | */ |
| 190 | readonly themengruppen: readonly Themengruppenstatistik[]; |
| 191 | } |
| 192 | |
| 193 | /** Die gemessene Trefferquote bei Wiederholungen mit Abstand. */ |
| 194 | export interface Behaltensquote { |
| 195 | /** Wiederholungen mit mindestens einem Tag Abstand. */ |
| 196 | readonly gesamt: number; |
| 197 | /** Davon richtig beantwortet. */ |
| 198 | readonly richtig: number; |
| 199 | } |
| 200 | |
| 201 | /** Filter für die Zusammenstellung einer Lernsitzung. */ |
| 202 | export interface SitzungsFilter { |
| 203 | /** Nur Fragen dieser Kapitel; leer bedeutet alle. */ |
| 204 | readonly kapitel?: readonly string[]; |
| 205 | /** Nur Fragen dieser Abschnitte; leer bedeutet alle. */ |
| 206 | readonly abschnitte?: readonly string[]; |
| 207 | /** Nur gemerkte Fragen. */ |
| 208 | readonly nurGemerkte?: boolean; |
| 209 | /** Nur Fragen, die zuletzt falsch beantwortet wurden. */ |
| 210 | readonly nurFehler?: boolean; |
| 211 | /** |
| 212 | * Nur hartnäckige Fragen – solche, die wiederholt danebengingen. |
| 213 | * |
| 214 | * **Warum das neben {@link SitzungsFilter.nurFehler} steht und nicht an |
| 215 | * seiner Stelle.** „Nur Fehler“ fragt die **letzte** Antwort: Wer eine |
| 216 | * Frage gestern zufällig richtig hatte, sieht sie dort nicht mehr – und |
| 217 | * das ist richtig so, das gedruckte Fehlerprotokoll sagt es ausdrücklich |
| 218 | * zu („Sobald Sie eine davon wieder richtig beantworten, verschwindet sie |
| 219 | * aus dieser Liste“). Dieser Filter fragt die **Historie**: Eine Frage, die |
| 220 | * über Wochen viermal durchfiel und einmal saß, ist nicht gekonnt. |
| 221 | * |
| 222 | * Die Schwelle steht in `shared/hartnaeckig.ts`. Rein deskriptiv – gezählte |
| 223 | * Fehlschläge aus dem Protokoll, keine erfundene Kennzahl. |
| 224 | */ |
| 225 | readonly nurHartnaeckige?: boolean; |
| 226 | /** Nur noch nie beantwortete Fragen. */ |
| 227 | readonly nurNeue?: boolean; |
| 228 | /** |
| 229 | * Nur offene Fragen – solche, die auszuformulieren sind. |
| 230 | * |
| 231 | * Der amtliche Katalog enthält davon 104 von 575, sehr ungleich verteilt: |
| 232 | * Kapitel I 61, II 13, III 1, IV 29. Wer Kapitel IV abgewählt hat, behält |
| 233 | * 75. Sie sind der Teil der Prüfung, den ein Mensch bewertet, und der |
| 234 | * einzige, den man nicht durch Ankreuzen erraten kann. |
| 235 | * |
| 236 | * Anders als die drei übrigen Filter fragt dieser nicht den Lernstand, |
| 237 | * sondern den Katalog: Der Fragetyp steht in `Frage.typ`, nicht in der |
| 238 | * Datenbank. |
| 239 | */ |
| 240 | readonly nurOffene?: boolean; |
| 241 | /** Höchstzahl der Fragen in der Sitzung. */ |
| 242 | readonly anzahl?: number; |
| 243 | /** Reihenfolge mischen (Vorgabe) oder Katalogreihenfolge beibehalten. */ |
| 244 | readonly mischen?: boolean; |
| 245 | /** |
| 246 | * Antwortmöglichkeiten innerhalb der Frage mischen. Vorgabe ist `false`. |
| 247 | * |
| 248 | * Vollständig getrennt von {@link SitzungsFilter.mischen}, weil beides |
| 249 | * Unterschiedliches leistet: Die Fragenreihenfolge ist eine Frage der |
| 250 | * Abwechslung und folgenlos. Die Optionsreihenfolge kostet den Gleichlauf |
| 251 | * mit dem amtlichen Katalog – 83 % der Auswahlfragen erscheinen dann |
| 252 | * anders als dort – und nimmt jedem den Halt, der sich die Antworten über |
| 253 | * ihre Stelle merkt; bei einer Gedächtnis- oder Konzentrationsbeeinträchtigung |
| 254 | * ein üblicher Weg. Ein Lernvorteil, der das aufwöge, ist nicht belegt. |
| 255 | * |
| 256 | * Fehlt der Wert, wird **nicht** gemischt. Früher galt hier |
| 257 | * {@link SitzungsFilter.mischen} – diese Kopplung mischte die Antworten |
| 258 | * still mit, sobald ein Aufrufer nur die Fragen mischen wollte. |
| 259 | */ |
| 260 | readonly optionenMischen?: boolean; |
| 261 | } |
| 262 | |
| 263 | /** Ein Lernprofil. Mehrere Profile teilen sich ein Gerät. */ |
| 264 | export interface Profil { |
| 265 | readonly id: number; |
| 266 | readonly name: string; |
| 267 | /** Prüfungstermin als ISO-Datum; steuert später den Lernplan. */ |
| 268 | readonly pruefungstermin: string | null; |
| 269 | /** |
| 270 | * Kapitel, die dieses Profil dauerhaft nicht lernt – Kennungen wie „IV“. |
| 271 | * |
| 272 | * Nicht jede Prüfungsstelle prüft Kapitel IV („Not- und |
| 273 | * Seenotsignalmittel“). Wer es nie braucht, schleppte sonst 89 der 575 |
| 274 | * Fragen durch jede Zahl: Fortschritt, Tagespensum, Prognose, |
| 275 | * Prüfungsreife. Die Abwahl je Simulationslauf gibt es davon getrennt und |
| 276 | * wirkt nur auf den gezogenen Bogen. |
| 277 | * |
| 278 | * Eine **Liste**, kein Wahrheitswert: Heute ist nur Kapitel IV abwählbar, |
| 279 | * aber ein `boolean` müsste beim nächsten Kapitel wieder migriert werden. |
| 280 | * |
| 281 | * Ausgeblendet, nicht gelöscht: Was in Kapitel IV bereits gelernt wurde, |
| 282 | * bleibt im Lernstand stehen und kehrt bei Wiederwahl vollständig zurück. |
| 283 | */ |
| 284 | readonly kapitelAusschluss: readonly string[]; |
| 285 | readonly erstelltAm: string; |
| 286 | } |
| 287 | |
| 288 | /** |
| 289 | * Die Antwortoptionen einer Frage in Anzeigereihenfolge – im Regelfall die |
| 290 | * des amtlichen Katalogs, gemischt nur auf ausdrücklichen Wunsch. Die |
| 291 | * Zuordnung bleibt in jedem Fall über die Labels erhalten: Der Buchstabe |
| 292 | * gehört zum Inhalt, nicht zur Stelle. |
| 293 | */ |
| 294 | export interface SitzungsFrage { |
| 295 | readonly frageId: string; |
| 296 | /** Reihenfolge der Optionslabels für diese Darstellung. */ |
| 297 | readonly optionsReihenfolge: readonly string[]; |
| 298 | /** |
| 299 | * Ist die Frage gemerkt? |
| 300 | * |
| 301 | * Kommt mit der Sitzung mit, weil die Oberfläche den Stern sonst erst |
| 302 | * kennt, nachdem die Frage beantwortet wurde: Bis Fassung 0.24.1 begann |
| 303 | * jede Sitzung mit einer leeren Merkliste, und der Stern stand an jeder |
| 304 | * Frage auf „nicht gemerkt“ – auch in einer Sitzung mit dem Filter |
| 305 | * „nur Gemerkte“, in der jede einzelne Frage gemerkt ist. |
| 306 | */ |
| 307 | readonly gemerkt: boolean; |
| 308 | /** |
| 309 | * Was der Lernende bei dieser offenen Frage zuletzt geschrieben hat. |
| 310 | * |
| 311 | * Fehlt, wenn es keine offene Frage ist, wenn sie noch nie beantwortet |
| 312 | * wurde oder wenn das Feld leer blieb – Letzteres ist ausdrücklich erlaubt |
| 313 | * und soll nicht als Vorhaltung wiederkehren. |
| 314 | * |
| 315 | * **Wird erst nach dem Bestätigen gezeigt.** Vorher wäre es eine Vorlage |
| 316 | * zum Abschreiben und machte aus dem Ausformulieren ein Kopieren. |
| 317 | * |
| 318 | * Der Text stand bis Fassung 0.20.0 in `antwort_log.freitext` und wurde von |
| 319 | * keiner einzigen Abfrage gelesen – geschrieben, aufbewahrt, nie gezeigt. |
| 320 | */ |
| 321 | readonly letzterFreitext?: LetzterFreitext; |
| 322 | } |
| 323 | |
| 324 | /** Eine frühere eigene Antwort, mit dem Tag, an dem sie entstand. */ |
| 325 | export interface LetzterFreitext { |
| 326 | readonly text: string; |
| 327 | /** Zeitpunkt als ISO-Zeichenkette. */ |
| 328 | readonly zeitpunkt: string; |
| 329 | } |