/** * Datentypen des Lernstands. * * Der Lernstand liegt ausschließlich lokal in einer SQLite-Datei je Profil. * Es gibt kein Konto, keine Cloud und keine Telemetrie. */ import type { Reifestufe } from './reife'; /** * Selbst- bzw. Systembewertung einer Antwort. * * Die vier Stufen entsprechen den Bewertungen des FSRS-Verfahrens, das später * die Wiedervorlage plant. Bei Multiple Choice werden sie automatisch * vergeben, bei offenen Fragen wählt der Lernende selbst. */ export type Bewertung = 'nochmal' | 'schwer' | 'gut' | 'leicht'; export const BEWERTUNGEN: readonly Bewertung[] = ['nochmal', 'schwer', 'gut', 'leicht']; export const BEWERTUNG_BEZEICHNUNG: Readonly> = Object.freeze({ nochmal: 'Nicht gewusst', schwer: 'Mit Mühe gewusst', gut: 'Gewusst', leicht: 'Sicher gewusst', }); /** * Welche Bewertung als gewusst zählt. * * „Nicht gewusst“ ist der einzige Fehlschlag – „mit Mühe gewusst“ ist eine * erfolgreiche, wenn auch mühsame Erinnerung (so rechnet auch FSRS). * * **Steht seit 0.22.0 hier und nicht mehr nur im Renderer**: Der * Anwendungskern braucht dieselbe Grenze für den Reife-Beleg, seit sich eine * richtige Auswahlantwort als geraten eingestehen lässt. Zwei Fassungen * derselben Regel liefen auseinander, und eine davon wäre dann die falsche. */ export function giltAlsRichtig(bewertung: Bewertung): boolean { return bewertung !== 'nochmal'; } /** Eine beantwortete Frage, wie sie protokolliert wird. */ export interface Antwortprotokoll { readonly frageId: string; /** Bei Multiple Choice die gewählten Labels, sonst leer. */ readonly auswahl: readonly string[]; /** Bei offenen Fragen der eingegebene Text (kann leer bleiben). */ readonly freitext?: string; readonly richtig: boolean; readonly bewertung: Bewertung; /** Bearbeitungsdauer in Millisekunden. */ readonly dauerMs: number; } /** Lernstand einer einzelnen Frage. */ export interface FrageStand { readonly frageId: string; readonly versuche: number; readonly richtige: number; /** Zeitpunkt der letzten Antwort als ISO-Zeichenkette. */ readonly zuletztBeantwortet: string | null; /** Fällig ab diesem Zeitpunkt; `null`, solange nie beantwortet. */ readonly faelligAb: string | null; readonly gemerkt: boolean; /** Zuletzt vergebene Bewertung. */ readonly letzteBewertung: Bewertung | null; } /** Zusammenfassung für einen Kapitel- oder Abschnittsbereich. */ export interface BereichStatistik { /** Kapitel- oder Abschnitts-ID, z. B. „I.2“. */ readonly id: string; readonly titel: string; readonly fragenGesamt: number; readonly beantwortet: number; /** * Fragen, deren Abruf belegt ist und noch frisch – siehe `shared/reife.ts`. * Eine Untergrenze, keine Prognose. */ readonly belegt: number; /** Reifegrad, 0 bis 1. `belegt` ist dieser Wert auf ganze Fragen gerundet. */ readonly reifegrad: number; readonly stufe: Reifestufe; } /** * Dasselbe für eine Themengruppe dieser Software. * * **Warum es das zusätzlich gibt.** Der amtliche Katalog gliedert nur * Kapitel I in Abschnitte. Die 230 Fragen der Kapitel II bis IV standen in * der Aufschlüsselung deshalb als **drei** Balken – „Kapitel IV, 61 von 88“ * sagt einem Lernenden nicht, was er üben soll. Die Feingliederung in * `content/themen.json` sagt es: 29 Gruppen, jede mit einem Titel wie * „Notwehr und Notstand“ oder „Munitionsarten“. * * **Warum getrennt und nicht in {@link BereichStatistik} gemischt.** Die * Gruppen sind **redaktionell**, nicht amtlich. In einer Liste mit den * amtlichen Abschnitten sähen sie aus wie deren Geschwister. Getrennt kann * die Oberfläche sagen, woher die Gliederung kommt – und die amtlichen * Zahlen bleiben Zeichen für Zeichen dieselben wie vorher. */ export interface Themengruppenstatistik extends BereichStatistik { /** Kapitel, unter dem die Gruppe steht – „II“, „III“ oder „IV“. */ readonly kapitel: string; } /** Gesamtüberblick für den Startbildschirm. */ export interface Lernuebersicht { readonly fragenGesamt: number; readonly beantwortet: number; /** Siehe {@link BereichStatistik.belegt}. */ readonly belegt: number; readonly reifegrad: number; /** Gesamtstufe, gedeckelt durch einen zurückliegenden K.-o.-Bereich. */ readonly stufe: Reifestufe; /** * Bereiche, die das Gesamturteil deckeln – heute nur Notwehr und Notstand. * Leer, wenn keiner zurückliegt. Die Oberfläche nennt sie beim Namen, * statt nur eine Stufe tiefer zu zeigen. */ readonly deckelnd: readonly string[]; readonly faellig: number; readonly gemerkt: number; /** * Offene Fragen im Lernumfang – die auszuformulierenden. * * Zählt wie {@link Lernuebersicht.fragenGesamt} nur, was das Zielprofil * einschließt: Ohne Kapitel IV sind es 75 statt 104. Deshalb steht die * Zahl hier und wird nicht in der Oberfläche aus dem Katalog gerechnet – * dort wäre die Kapitelabwahl ein zweites Mal nachzubilden. */ readonly offen: number; /** * Fragen, die zuletzt falsch beantwortet wurden. * * Dieselbe Menge, die der Einstieg „Nur Fehler“ vorlegt und die das * Fehlerprotokoll druckt – gebildet aus derselben Regel, damit alle drei * dasselbe sagen. Zählt wie {@link Lernuebersicht.fragenGesamt} nur, was * das Zielprofil einschließt. */ readonly fehler: number; readonly heuteRichtig: number; readonly heuteFalsch: number; /** * Fragen des Lernumfangs, die heute bearbeitet wurden. * * Eine Bestandszahl aus `frage_stand`, kein Protokoll: Sie zählt * **verschiedene Fragen**, nicht Zeilen. Eine mit „Nicht gewusst“ bewertete * Frage ist sofort wieder fällig und käme sonst zweimal vor. */ readonly heuteBearbeitet: number; /** * Volle Kalendertage seit der letzten Antwort; `null`, solange nie * beantwortet. * * Positiv für Vergangenes – anders als `tageBisTermin` im Lernplan, das * vorwärts rechnet. Wer nach zwei Wochen zurückkommt, sieht einen * gefallenen Reifegrad und ein gewachsenes Pensum; diese Zahl ist die * Tatsache dazu. */ readonly tageSeitLetzterAntwort: number | null; /** * Wie oft Wiederholungen nach mindestens einem Tag Abstand wirklich saßen. * * **Wozu.** Der Lernplan steuert auf eine Zielquote (0,90 bis 0,97), und die * Reife-Ampel zeigt modellierte Abrufwahrscheinlichkeiten. Nirgends stand * bis 0.22.0, wie oft der Lernende fällige Wiederholungen **tatsächlich** * trifft. Diese Zahl schließt die Lücke zwischen der Behauptung des Modells * und dem Befund am Menschen — im Geist des Projekts: Messung statt * Behauptung. * * Eine **gemessene Vergangenheitszahl, keine Vorhersage**: Die abgelehnte * Bestehenswahrscheinlichkeit bleibt außen vor. Gezählt werden Antworten, * deren Vorgänger zur selben Frage mindestens einen Tag zurücklag — * dieselbe Abstandsregel, die die Belegrechnung benutzt. * * `null`, solange zu wenige solcher Wiederholungen vorliegen: Eine Quote * aus drei Antworten wäre eine Zahl ohne Aussage. */ readonly behaltensquote?: Behaltensquote | null; readonly bereiche: readonly BereichStatistik[]; /** * Die Feingliederung der Kapitel II bis IV; leer, wenn keine vorliegt. * * Sie **ersetzt** die Kapitelzeilen in {@link bereiche} nicht, sondern * steht darunter. Nachgemessen am Katalogstand 16.12.2024: Die 29 Gruppen * decken alle 230 Fragen dieser Kapitel, jede genau einmal. */ readonly themengruppen: readonly Themengruppenstatistik[]; } /** Die gemessene Trefferquote bei Wiederholungen mit Abstand. */ export interface Behaltensquote { /** Wiederholungen mit mindestens einem Tag Abstand. */ readonly gesamt: number; /** Davon richtig beantwortet. */ readonly richtig: number; } /** Filter für die Zusammenstellung einer Lernsitzung. */ export interface SitzungsFilter { /** Nur Fragen dieser Kapitel; leer bedeutet alle. */ readonly kapitel?: readonly string[]; /** Nur Fragen dieser Abschnitte; leer bedeutet alle. */ readonly abschnitte?: readonly string[]; /** Nur gemerkte Fragen. */ readonly nurGemerkte?: boolean; /** Nur Fragen, die zuletzt falsch beantwortet wurden. */ readonly nurFehler?: boolean; /** * Nur hartnäckige Fragen – solche, die wiederholt danebengingen. * * **Warum das neben {@link SitzungsFilter.nurFehler} steht und nicht an * seiner Stelle.** „Nur Fehler“ fragt die **letzte** Antwort: Wer eine * Frage gestern zufällig richtig hatte, sieht sie dort nicht mehr – und * das ist richtig so, das gedruckte Fehlerprotokoll sagt es ausdrücklich * zu („Sobald Sie eine davon wieder richtig beantworten, verschwindet sie * aus dieser Liste“). Dieser Filter fragt die **Historie**: Eine Frage, die * über Wochen viermal durchfiel und einmal saß, ist nicht gekonnt. * * Die Schwelle steht in `shared/hartnaeckig.ts`. Rein deskriptiv – gezählte * Fehlschläge aus dem Protokoll, keine erfundene Kennzahl. */ readonly nurHartnaeckige?: boolean; /** Nur noch nie beantwortete Fragen. */ readonly nurNeue?: boolean; /** * Nur offene Fragen – solche, die auszuformulieren sind. * * Der amtliche Katalog enthält davon 104 von 575, sehr ungleich verteilt: * Kapitel I 61, II 13, III 1, IV 29. Wer Kapitel IV abgewählt hat, behält * 75. Sie sind der Teil der Prüfung, den ein Mensch bewertet, und der * einzige, den man nicht durch Ankreuzen erraten kann. * * Anders als die drei übrigen Filter fragt dieser nicht den Lernstand, * sondern den Katalog: Der Fragetyp steht in `Frage.typ`, nicht in der * Datenbank. */ readonly nurOffene?: boolean; /** Höchstzahl der Fragen in der Sitzung. */ readonly anzahl?: number; /** Reihenfolge mischen (Vorgabe) oder Katalogreihenfolge beibehalten. */ readonly mischen?: boolean; /** * Antwortmöglichkeiten innerhalb der Frage mischen. Vorgabe ist `false`. * * Vollständig getrennt von {@link SitzungsFilter.mischen}, weil beides * Unterschiedliches leistet: Die Fragenreihenfolge ist eine Frage der * Abwechslung und folgenlos. Die Optionsreihenfolge kostet den Gleichlauf * mit dem amtlichen Katalog – 83 % der Auswahlfragen erscheinen dann * anders als dort – und nimmt jedem den Halt, der sich die Antworten über * ihre Stelle merkt; bei einer Gedächtnis- oder Konzentrationsbeeinträchtigung * ein üblicher Weg. Ein Lernvorteil, der das aufwöge, ist nicht belegt. * * Fehlt der Wert, wird **nicht** gemischt. Früher galt hier * {@link SitzungsFilter.mischen} – diese Kopplung mischte die Antworten * still mit, sobald ein Aufrufer nur die Fragen mischen wollte. */ readonly optionenMischen?: boolean; } /** Ein Lernprofil. Mehrere Profile teilen sich ein Gerät. */ export interface Profil { readonly id: number; readonly name: string; /** Prüfungstermin als ISO-Datum; steuert später den Lernplan. */ readonly pruefungstermin: string | null; /** * Kapitel, die dieses Profil dauerhaft nicht lernt – Kennungen wie „IV“. * * Nicht jede Prüfungsstelle prüft Kapitel IV („Not- und * Seenotsignalmittel“). Wer es nie braucht, schleppte sonst 89 der 575 * Fragen durch jede Zahl: Fortschritt, Tagespensum, Prognose, * Prüfungsreife. Die Abwahl je Simulationslauf gibt es davon getrennt und * wirkt nur auf den gezogenen Bogen. * * Eine **Liste**, kein Wahrheitswert: Heute ist nur Kapitel IV abwählbar, * aber ein `boolean` müsste beim nächsten Kapitel wieder migriert werden. * * Ausgeblendet, nicht gelöscht: Was in Kapitel IV bereits gelernt wurde, * bleibt im Lernstand stehen und kehrt bei Wiederwahl vollständig zurück. */ readonly kapitelAusschluss: readonly string[]; readonly erstelltAm: string; } /** * Die Antwortoptionen einer Frage in Anzeigereihenfolge – im Regelfall die * des amtlichen Katalogs, gemischt nur auf ausdrücklichen Wunsch. Die * Zuordnung bleibt in jedem Fall über die Labels erhalten: Der Buchstabe * gehört zum Inhalt, nicht zur Stelle. */ export interface SitzungsFrage { readonly frageId: string; /** Reihenfolge der Optionslabels für diese Darstellung. */ readonly optionsReihenfolge: readonly string[]; /** * Ist die Frage gemerkt? * * Kommt mit der Sitzung mit, weil die Oberfläche den Stern sonst erst * kennt, nachdem die Frage beantwortet wurde: Bis Fassung 0.24.1 begann * jede Sitzung mit einer leeren Merkliste, und der Stern stand an jeder * Frage auf „nicht gemerkt“ – auch in einer Sitzung mit dem Filter * „nur Gemerkte“, in der jede einzelne Frage gemerkt ist. */ readonly gemerkt: boolean; /** * Was der Lernende bei dieser offenen Frage zuletzt geschrieben hat. * * Fehlt, wenn es keine offene Frage ist, wenn sie noch nie beantwortet * wurde oder wenn das Feld leer blieb – Letzteres ist ausdrücklich erlaubt * und soll nicht als Vorhaltung wiederkehren. * * **Wird erst nach dem Bestätigen gezeigt.** Vorher wäre es eine Vorlage * zum Abschreiben und machte aus dem Ausformulieren ein Kopieren. * * Der Text stand bis Fassung 0.20.0 in `antwort_log.freitext` und wurde von * keiner einzigen Abfrage gelesen – geschrieben, aufbewahrt, nie gezeigt. */ readonly letzterFreitext?: LetzterFreitext; } /** Eine frühere eigene Antwort, mit dem Tag, an dem sie entstand. */ export interface LetzterFreitext { readonly text: string; /** Zeitpunkt als ISO-Zeichenkette. */ readonly zeitpunkt: string; }