waffensachkunde

Waffensachkunde – Lernsoftware für die Sachkundeprüfung nach § 7 WaffG. Barrierefrei, offline, EUPL-1.2.

/ app src shared lernstand.ts

12,0 KB Rohdatei
app/src/shared/lernstand.ts — 300 Zeilen
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 }