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 | /** Gesamtüberblick für den Startbildschirm. */ |
| 88 | export interface Lernuebersicht { |
| 89 | readonly fragenGesamt: number; |
| 90 | readonly beantwortet: number; |
| 91 | /** Siehe {@link BereichStatistik.belegt}. */ |
| 92 | readonly belegt: number; |
| 93 | readonly reifegrad: number; |
| 94 | /** Gesamtstufe, gedeckelt durch einen zurückliegenden K.-o.-Bereich. */ |
| 95 | readonly stufe: Reifestufe; |
| 96 | /** |
| 97 | * Bereiche, die das Gesamturteil deckeln – heute nur Notwehr und Notstand. |
| 98 | * Leer, wenn keiner zurückliegt. Die Oberfläche nennt sie beim Namen, |
| 99 | * statt nur eine Stufe tiefer zu zeigen. |
| 100 | */ |
| 101 | readonly deckelnd: readonly string[]; |
| 102 | readonly faellig: number; |
| 103 | readonly gemerkt: number; |
| 104 | /** |
| 105 | * Offene Fragen im Lernumfang – die auszuformulierenden. |
| 106 | * |
| 107 | * Zählt wie {@link Lernuebersicht.fragenGesamt} nur, was das Zielprofil |
| 108 | * einschließt: Ohne Kapitel IV sind es 75 statt 104. Deshalb steht die |
| 109 | * Zahl hier und wird nicht in der Oberfläche aus dem Katalog gerechnet – |
| 110 | * dort wäre die Kapitelabwahl ein zweites Mal nachzubilden. |
| 111 | */ |
| 112 | readonly offen: number; |
| 113 | /** |
| 114 | * Fragen, die zuletzt falsch beantwortet wurden. |
| 115 | * |
| 116 | * Dieselbe Menge, die der Einstieg „Nur Fehler“ vorlegt und die das |
| 117 | * Fehlerprotokoll druckt – gebildet aus derselben Regel, damit alle drei |
| 118 | * dasselbe sagen. Zählt wie {@link Lernuebersicht.fragenGesamt} nur, was |
| 119 | * das Zielprofil einschließt. |
| 120 | */ |
| 121 | readonly fehler: number; |
| 122 | readonly heuteRichtig: number; |
| 123 | readonly heuteFalsch: number; |
| 124 | /** |
| 125 | * Fragen des Lernumfangs, die heute bearbeitet wurden. |
| 126 | * |
| 127 | * Eine Bestandszahl aus `frage_stand`, kein Protokoll: Sie zählt |
| 128 | * **verschiedene Fragen**, nicht Zeilen. Eine mit „Nicht gewusst“ bewertete |
| 129 | * Frage ist sofort wieder fällig und käme sonst zweimal vor. |
| 130 | */ |
| 131 | readonly heuteBearbeitet: number; |
| 132 | /** |
| 133 | * Volle Kalendertage seit der letzten Antwort; `null`, solange nie |
| 134 | * beantwortet. |
| 135 | * |
| 136 | * Positiv für Vergangenes – anders als `tageBisTermin` im Lernplan, das |
| 137 | * vorwärts rechnet. Wer nach zwei Wochen zurückkommt, sieht einen |
| 138 | * gefallenen Reifegrad und ein gewachsenes Pensum; diese Zahl ist die |
| 139 | * Tatsache dazu. |
| 140 | */ |
| 141 | readonly tageSeitLetzterAntwort: number | null; |
| 142 | /** |
| 143 | * Wie oft Wiederholungen nach mindestens einem Tag Abstand wirklich saßen. |
| 144 | * |
| 145 | * **Wozu.** Der Lernplan steuert auf eine Zielquote (0,90 bis 0,97), und die |
| 146 | * Reife-Ampel zeigt modellierte Abrufwahrscheinlichkeiten. Nirgends stand |
| 147 | * bis 0.22.0, wie oft der Lernende fällige Wiederholungen **tatsächlich** |
| 148 | * trifft. Diese Zahl schließt die Lücke zwischen der Behauptung des Modells |
| 149 | * und dem Befund am Menschen — im Geist des Projekts: Messung statt |
| 150 | * Behauptung. |
| 151 | * |
| 152 | * Eine **gemessene Vergangenheitszahl, keine Vorhersage**: Die abgelehnte |
| 153 | * Bestehenswahrscheinlichkeit bleibt außen vor. Gezählt werden Antworten, |
| 154 | * deren Vorgänger zur selben Frage mindestens einen Tag zurücklag — |
| 155 | * dieselbe Abstandsregel, die die Belegrechnung benutzt. |
| 156 | * |
| 157 | * `null`, solange zu wenige solcher Wiederholungen vorliegen: Eine Quote |
| 158 | * aus drei Antworten wäre eine Zahl ohne Aussage. |
| 159 | */ |
| 160 | readonly behaltensquote?: Behaltensquote | null; |
| 161 | readonly bereiche: readonly BereichStatistik[]; |
| 162 | } |
| 163 | |
| 164 | /** Die gemessene Trefferquote bei Wiederholungen mit Abstand. */ |
| 165 | export interface Behaltensquote { |
| 166 | /** Wiederholungen mit mindestens einem Tag Abstand. */ |
| 167 | readonly gesamt: number; |
| 168 | /** Davon richtig beantwortet. */ |
| 169 | readonly richtig: number; |
| 170 | } |
| 171 | |
| 172 | /** Filter für die Zusammenstellung einer Lernsitzung. */ |
| 173 | export interface SitzungsFilter { |
| 174 | /** Nur Fragen dieser Kapitel; leer bedeutet alle. */ |
| 175 | readonly kapitel?: readonly string[]; |
| 176 | /** Nur Fragen dieser Abschnitte; leer bedeutet alle. */ |
| 177 | readonly abschnitte?: readonly string[]; |
| 178 | /** Nur gemerkte Fragen. */ |
| 179 | readonly nurGemerkte?: boolean; |
| 180 | /** Nur Fragen, die zuletzt falsch beantwortet wurden. */ |
| 181 | readonly nurFehler?: boolean; |
| 182 | /** |
| 183 | * Nur hartnäckige Fragen – solche, die wiederholt danebengingen. |
| 184 | * |
| 185 | * **Warum das neben {@link SitzungsFilter.nurFehler} steht und nicht an |
| 186 | * seiner Stelle.** „Nur Fehler“ fragt die **letzte** Antwort: Wer eine |
| 187 | * Frage gestern zufällig richtig hatte, sieht sie dort nicht mehr – und |
| 188 | * das ist richtig so, das gedruckte Fehlerprotokoll sagt es ausdrücklich |
| 189 | * zu („Sobald Sie eine davon wieder richtig beantworten, verschwindet sie |
| 190 | * aus dieser Liste“). Dieser Filter fragt die **Historie**: Eine Frage, die |
| 191 | * über Wochen viermal durchfiel und einmal saß, ist nicht gekonnt. |
| 192 | * |
| 193 | * Die Schwelle steht in `shared/hartnaeckig.ts`. Rein deskriptiv – gezählte |
| 194 | * Fehlschläge aus dem Protokoll, keine erfundene Kennzahl. |
| 195 | */ |
| 196 | readonly nurHartnaeckige?: boolean; |
| 197 | /** Nur noch nie beantwortete Fragen. */ |
| 198 | readonly nurNeue?: boolean; |
| 199 | /** |
| 200 | * Nur offene Fragen – solche, die auszuformulieren sind. |
| 201 | * |
| 202 | * Der amtliche Katalog enthält davon 104 von 575, sehr ungleich verteilt: |
| 203 | * Kapitel I 61, II 13, III 1, IV 29. Wer Kapitel IV abgewählt hat, behält |
| 204 | * 75. Sie sind der Teil der Prüfung, den ein Mensch bewertet, und der |
| 205 | * einzige, den man nicht durch Ankreuzen erraten kann. |
| 206 | * |
| 207 | * Anders als die drei übrigen Filter fragt dieser nicht den Lernstand, |
| 208 | * sondern den Katalog: Der Fragetyp steht in `Frage.typ`, nicht in der |
| 209 | * Datenbank. |
| 210 | */ |
| 211 | readonly nurOffene?: boolean; |
| 212 | /** Höchstzahl der Fragen in der Sitzung. */ |
| 213 | readonly anzahl?: number; |
| 214 | /** Reihenfolge mischen (Vorgabe) oder Katalogreihenfolge beibehalten. */ |
| 215 | readonly mischen?: boolean; |
| 216 | /** |
| 217 | * Antwortmöglichkeiten innerhalb der Frage mischen. Vorgabe ist `false`. |
| 218 | * |
| 219 | * Vollständig getrennt von {@link SitzungsFilter.mischen}, weil beides |
| 220 | * Unterschiedliches leistet: Die Fragenreihenfolge ist eine Frage der |
| 221 | * Abwechslung und folgenlos. Die Optionsreihenfolge kostet den Gleichlauf |
| 222 | * mit dem amtlichen Katalog – 83 % der Auswahlfragen erscheinen dann |
| 223 | * anders als dort – und nimmt jedem den Halt, der sich die Antworten über |
| 224 | * ihre Stelle merkt; bei einer Gedächtnis- oder Konzentrationsbeeinträchtigung |
| 225 | * ein üblicher Weg. Ein Lernvorteil, der das aufwöge, ist nicht belegt. |
| 226 | * |
| 227 | * Fehlt der Wert, wird **nicht** gemischt. Früher galt hier |
| 228 | * {@link SitzungsFilter.mischen} – diese Kopplung mischte die Antworten |
| 229 | * still mit, sobald ein Aufrufer nur die Fragen mischen wollte. |
| 230 | */ |
| 231 | readonly optionenMischen?: boolean; |
| 232 | } |
| 233 | |
| 234 | /** Ein Lernprofil. Mehrere Profile teilen sich ein Gerät. */ |
| 235 | export interface Profil { |
| 236 | readonly id: number; |
| 237 | readonly name: string; |
| 238 | /** Prüfungstermin als ISO-Datum; steuert später den Lernplan. */ |
| 239 | readonly pruefungstermin: string | null; |
| 240 | /** |
| 241 | * Kapitel, die dieses Profil dauerhaft nicht lernt – Kennungen wie „IV“. |
| 242 | * |
| 243 | * Nicht jede Prüfungsstelle prüft Kapitel IV („Not- und |
| 244 | * Seenotsignalmittel“). Wer es nie braucht, schleppte sonst 89 der 575 |
| 245 | * Fragen durch jede Zahl: Fortschritt, Tagespensum, Prognose, |
| 246 | * Prüfungsreife. Die Abwahl je Simulationslauf gibt es davon getrennt und |
| 247 | * wirkt nur auf den gezogenen Bogen. |
| 248 | * |
| 249 | * Eine **Liste**, kein Wahrheitswert: Heute ist nur Kapitel IV abwählbar, |
| 250 | * aber ein `boolean` müsste beim nächsten Kapitel wieder migriert werden. |
| 251 | * |
| 252 | * Ausgeblendet, nicht gelöscht: Was in Kapitel IV bereits gelernt wurde, |
| 253 | * bleibt im Lernstand stehen und kehrt bei Wiederwahl vollständig zurück. |
| 254 | */ |
| 255 | readonly kapitelAusschluss: readonly string[]; |
| 256 | readonly erstelltAm: string; |
| 257 | } |
| 258 | |
| 259 | /** |
| 260 | * Die Antwortoptionen einer Frage in Anzeigereihenfolge – im Regelfall die |
| 261 | * des amtlichen Katalogs, gemischt nur auf ausdrücklichen Wunsch. Die |
| 262 | * Zuordnung bleibt in jedem Fall über die Labels erhalten: Der Buchstabe |
| 263 | * gehört zum Inhalt, nicht zur Stelle. |
| 264 | */ |
| 265 | export interface SitzungsFrage { |
| 266 | readonly frageId: string; |
| 267 | /** Reihenfolge der Optionslabels für diese Darstellung. */ |
| 268 | readonly optionsReihenfolge: readonly string[]; |
| 269 | /** |
| 270 | * Ist die Frage gemerkt? |
| 271 | * |
| 272 | * Kommt mit der Sitzung mit, weil die Oberfläche den Stern sonst erst |
| 273 | * kennt, nachdem die Frage beantwortet wurde: Bis Fassung 0.24.1 begann |
| 274 | * jede Sitzung mit einer leeren Merkliste, und der Stern stand an jeder |
| 275 | * Frage auf „nicht gemerkt“ – auch in einer Sitzung mit dem Filter |
| 276 | * „nur Gemerkte“, in der jede einzelne Frage gemerkt ist. |
| 277 | */ |
| 278 | readonly gemerkt: boolean; |
| 279 | /** |
| 280 | * Was der Lernende bei dieser offenen Frage zuletzt geschrieben hat. |
| 281 | * |
| 282 | * Fehlt, wenn es keine offene Frage ist, wenn sie noch nie beantwortet |
| 283 | * wurde oder wenn das Feld leer blieb – Letzteres ist ausdrücklich erlaubt |
| 284 | * und soll nicht als Vorhaltung wiederkehren. |
| 285 | * |
| 286 | * **Wird erst nach dem Bestätigen gezeigt.** Vorher wäre es eine Vorlage |
| 287 | * zum Abschreiben und machte aus dem Ausformulieren ein Kopieren. |
| 288 | * |
| 289 | * Der Text stand bis Fassung 0.20.0 in `antwort_log.freitext` und wurde von |
| 290 | * keiner einzigen Abfrage gelesen – geschrieben, aufbewahrt, nie gezeigt. |
| 291 | */ |
| 292 | readonly letzterFreitext?: LetzterFreitext; |
| 293 | } |
| 294 | |
| 295 | /** Eine frühere eigene Antwort, mit dem Tag, an dem sie entstand. */ |
| 296 | export interface LetzterFreitext { |
| 297 | readonly text: string; |
| 298 | /** Zeitpunkt als ISO-Zeichenkette. */ |
| 299 | readonly zeitpunkt: string; |
| 300 | } |