Repository navigation
Expand file tree
/
Copy pathde.html
More file actions
407 lines (394 loc) · 22.1 KB
/
Copy pathde.html
File metadata and controls
407 lines (394 loc) · 22.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
<!doctype html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Codequalität und Zuverlässigkeit — Python Course</title>
<meta name="robots" content="noindex, nofollow">
<style>
:root {
color-scheme: light dark;
--bg: #ffffff;
--text: #1a1a1a;
--muted: #555555;
--border: #e0e0e0;
--accent: #4f46e5;
--th-bg: #f5f5f7;
--good-bg: color-mix(in srgb, #16a34a 10%, transparent);
--good-border: color-mix(in srgb, #16a34a 35%, var(--border));
--honest-bg: color-mix(in srgb, #d97706 8%, transparent);
--honest-border: color-mix(in srgb, #d97706 30%, var(--border));
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #14141a;
--text: #f0f0f0;
--muted: #aaaaaa;
--border: #2e2e38;
--accent: #a5b4fc;
--th-bg: #1e1e28;
}
}
* {
box-sizing: border-box;
}
body {
margin: 0;
padding: 0;
background: var(--bg);
color: var(--text);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
line-height: 1.6;
}
main {
max-width: 860px;
margin: 0 auto;
padding: 2.5rem 1.5rem 4rem;
}
.lang-switch {
font-size: 0.85rem;
margin-bottom: 1.5rem;
}
.lang-switch a {
color: var(--muted);
text-decoration: none;
}
.lang-switch a:hover {
color: var(--accent);
text-decoration: underline;
}
.lang-switch__current {
color: var(--text);
font-weight: 600;
}
h1 {
font-size: 1.8rem;
margin-bottom: 0.25rem;
}
.updated {
color: var(--muted);
font-size: 0.9rem;
margin: 0.15rem 0;
}
h2 {
font-size: 1.2rem;
margin-top: 2.2rem;
margin-bottom: 0.6rem;
border-bottom: 1px solid var(--border);
padding-bottom: 0.4rem;
}
h3 {
font-size: 1.02rem;
margin-top: 1.6rem;
margin-bottom: 0.5rem;
}
p {
margin: 0.6rem 0;
}
ul {
margin: 0.6rem 0;
padding-left: 1.4rem;
}
li {
margin: 0.3rem 0;
}
a {
color: var(--accent);
}
code {
font-family: "JetBrains Mono", "Fira Code", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
background: var(--th-bg);
padding: 0.1rem 0.35rem;
border-radius: 4px;
font-size: 0.9em;
}
.summary {
background: color-mix(in srgb, var(--accent) 10%, transparent);
border: 1px solid var(--border);
border-radius: 8px;
padding: 1rem 1.2rem;
margin: 1.5rem 0 2rem;
}
.summary p {
margin: 0.3rem 0;
}
.headline-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.9rem;
margin: 1.2rem 0 1.5rem;
}
.headline-card {
background: var(--good-bg);
border: 1px solid var(--good-border);
border-radius: 8px;
padding: 1rem 1.1rem;
}
.headline-card .num {
font-size: 1.9rem;
font-weight: 700;
line-height: 1.1;
}
.headline-card .label {
color: var(--muted);
font-size: 0.85rem;
margin-top: 0.2rem;
}
@media (max-width: 520px) {
.headline-grid { grid-template-columns: 1fr; }
}
table {
width: 100%;
border-collapse: collapse;
margin: 1rem 0 1.5rem;
font-size: 0.92rem;
}
th, td {
border: 1px solid var(--border);
padding: 0.5rem 0.7rem;
text-align: left;
}
th {
background: var(--th-bg);
font-weight: 600;
}
td.num, th.num {
text-align: right;
white-space: nowrap;
}
tr.total td {
font-weight: 700;
}
.honest-box {
background: var(--honest-bg);
border: 1px solid var(--honest-border);
border-radius: 8px;
padding: 1rem 1.2rem;
margin: 1.2rem 0;
}
.honest-box p:first-child {
margin-top: 0;
}
.tools-list {
display: grid;
gap: 0.5rem;
margin: 1rem 0 1.5rem;
}
.tools-list .tool {
border: 1px solid var(--border);
border-radius: 6px;
padding: 0.6rem 0.9rem;
}
.tools-list .tool strong {
display: inline-block;
min-width: 9rem;
}
footer {
margin-top: 3rem;
padding-top: 1rem;
border-top: 1px solid var(--border);
color: var(--muted);
font-size: 0.85rem;
}
</style>
</head>
<body>
<main>
<nav class="lang-switch"><a href="index.html">УКР</a> · <a href="en.html">EN</a> · <a href="es.html">ES</a> · <a href="fr.html">FR</a> · <span class="lang-switch__current">DE</span> · <a href="pt.html">PT</a></nav>
<h1>Codequalität und Zuverlässigkeit</h1>
<p class="updated">App: Python Course · Datum: 1. September 2026</p>
<p class="updated">Umfang: der eingebaute Python-Interpreter, die verfassten Kursinhalte (2194+ Übungen, 48 Lektionen, 17 Module, ein neuer Abschnitt „Lifehacks" — 15 Karten, 6 Sprachen), der Algorithmus für die Wiederholungsplanung (Spaced Repetition) und — neu in diesem Bericht — ob die Testsuite selbst Regressionen wirklich erkennt (Mutationstests) für die 5 kritischsten Logik-Module</p>
<div class="summary">
<p><strong>Kurz gesagt:</strong> zwei getrennte Fragen, zwei Antworten. <strong>Ist der Code korrekt?</strong> 9710 Kandidaten gegen CPython und andere Orakel geprüft — <strong>0 Abweichungen</strong>. <strong>Schützen die Tests das auch wirklich?</strong> 3694 absichtlich eingeschleuste Defekte (Mutationstests — die Abdeckung wuchs mit dem Code mit) — alle 3694 erkannt. Die zweite Frage ist neu in diesem Bericht — sie macht die erste erst vertrauenswürdig, statt nur behauptet.</p>
</div>
<div class="headline-grid">
<div class="headline-card">
<div class="num">0 von 9710</div>
<div class="label">Abweichungen gegenüber CPython/Orakeln</div>
</div>
<div class="headline-card">
<div class="num">3694 von 3694</div>
<div class="label">eingeschleuste Defekte von Tests erkannt (Mutation Score 100%)</div>
</div>
</div>
<h2>Mutationstests — erkennen die Tests Bugs wirklich</h2>
<p>
Ein Coverage-Prozentsatz beantwortet nur „ist diese Zeile mindestens einmal ausgeführt worden", nicht „würde ein Test bemerken, wenn diese Zeile kaputtgeht". Mutationstests beantworten genau die zweite Frage: Ein Werkzeug schleust absichtlich Tausende kleiner, einzelner Defekte in den Code ein (ändert <code><</code> zu <code><=</code>, <code>&&</code> zu <code>||</code>, entfernt eine Grenzprüfung usw.) und prüft, ob danach mindestens ein Test fehlschlägt. Erkennt kein Test den eingeschleusten Defekt, ist das eine echte Lücke — selbst wenn die Zeilenabdeckung für sich genommen 100 % anzeigt.
</p>
<table>
<thead>
<tr>
<th>Modul</th>
<th class="num">Mutanten</th>
<th class="num">Erkannt</th>
<th class="num">Score</th>
</tr>
</thead>
<tbody>
<tr>
<td>Logik zum Freischalten von Lektionen (<code>gate-engine</code>)</td>
<td class="num">208</td>
<td class="num">208</td>
<td class="num">100.00%</td>
</tr>
<tr>
<td>Erfolge, XP, Serie, Herzen (<code>gamification-engine</code>)</td>
<td class="num">267</td>
<td class="num">267</td>
<td class="num">100.00%</td>
</tr>
<tr>
<td>Wiederholungsplaner (<code>srs-engine</code>)</td>
<td class="num">47</td>
<td class="num">47</td>
<td class="num">100.00%</td>
</tr>
<tr>
<td>Python-Interpreter (Tokenizer, Parser, Ausführung — inklusive Import-/Datei-Code aus Modul 17)</td>
<td class="num">3123</td>
<td class="num">3123</td>
<td class="num">100.00%</td>
</tr>
<tr>
<td>Abschnitt „Lifehacks" (<code>lifehacks-engine</code> + <code>manage-lifehacks</code>)</td>
<td class="num">49</td>
<td class="num">49</td>
<td class="num">100.00%</td>
</tr>
<tr class="total">
<td>Zusammen, 5 kritischste Module</td>
<td class="num">3694</td>
<td class="num">3694</td>
<td class="num">100.00%</td>
</tr>
</tbody>
</table>
<p>
„Erkannt" bedeutet hier: Der Mutant hat mindestens einen Test fehlschlagen lassen, oder eine Endlosschleife/ein Hängenbleiben verursacht (zählt ebenfalls — der Defekt wurde trotzdem eindeutig sichtbar, nur auf andere Weise; die Python-Interpreter-Zeile oben zählt genau einen solchen Fall auf diese Weise). Jeder Einzelfall, in dem ein Mutant zunächst überlebte, wurde von Hand geprüft: Entweder wurde ein neuer Test geschrieben, der das tatsächliche Verhalten dokumentiert, oder es wurde mathematisch bewiesen, dass genau diese Codeänderung das Ergebnis niemals beeinflussen kann (und in dem Fall trägt die Zeile selbst einen erklärenden Kommentar, kein stilles Übergehen), oder — wo sich das als die treffendere Antwort erwies — der Code selbst wurde so vereinfacht, dass der nicht unterstützte Logikzweig schlicht aufhörte zu existieren. Einer dieser Fälle wird weiter unten im Detail durchgegangen.
</p>
<h2>Methodik — Korrektheit des Codes (CPython-Abgleich)</h2>
<p>
Drei unabhängige Prüfstränge, ohne gemeinsamen Code und ohne Abhängigkeit der Ergebnisse voneinander. <strong>Interpreter:</strong> Die App führt Code über einen eigenen, von Grund auf neu geschriebenen Python-Interpreter aus (kein CPython, keine Drittanbieter-Bibliothek) — jeder Kandidat (eine verfasste Referenzlösung, eine aus einer Vorlage generierte Instanz oder ein zufällig generiertes Programm) wurde gleichzeitig im Interpreter der App und in einem systemseitig installierten CPython 3.12.3 ausgeführt und anschließend auf exakte Ergebnis- bzw. Fehler-Konsistenz verglichen. <strong>Kursstruktur:</strong> ein vollständiger, ungestichprobter Durchlauf über alle Inhaltsdateien. <strong>Wiederholungsplaner:</strong> seine Ausgabe wurde mit einer separat geschriebenen (nicht aus dem Planer selbst kopierten) Implementierung derselben mathematischen Formel abgeglichen. Die Validierung lief über ein temporäres, isoliertes Werkzeug außerhalb des eigentlichen App-Codes, in mehreren unabhängigen Runden, die sich nicht im Voraus auf die Ergebnisse der jeweils anderen stützten.
</p>
<h2>Ergebnisse gegen ein Orakel</h2>
<table>
<thead>
<tr>
<th>Prüfstrang</th>
<th class="num">Geprüft</th>
<th class="num">Übereinstimmend</th>
<th class="num">Abweichungen</th>
</tr>
</thead>
<tbody>
<tr>
<td>Interpreter — Kursinhalte und erweiterter Übungsmodus (Referenz- und alternative Lösungen, Lückentext-Aufgaben im Wertemodus, statische und parametrisierte Vorlagen des erweiterten Übungsmodus, die reale Ausführung von <code>import</code>/<code>open()</code> aus Modul 17) sowie der Abschnitt „Lifehacks" (Vorher-/Nachher-Code-Schnappschüsse)</td>
<td class="num">393</td>
<td class="num">393</td>
<td class="num">0</td>
</tr>
<tr>
<td>Interpreter — Zufallstests (Fuzzing) (20 Konstrukt-Kategorien)</td>
<td class="num">400</td>
<td class="num">400</td>
<td class="num">0</td>
</tr>
<tr>
<td>Wiederholungsplaner (1020 Szenarien, 8917 Wiederholungsschritte)</td>
<td class="num">8917</td>
<td class="num">8917</td>
<td class="num">0</td>
</tr>
<tr class="total">
<td>Kombiniert</td>
<td class="num">9710</td>
<td class="num">9710</td>
<td class="num">0</td>
</tr>
</tbody>
</table>
<h2>Struktur- und Inhaltsprüfungen</h2>
<table>
<thead>
<tr>
<th>Prüfung</th>
<th class="num">Abdeckung</th>
<th class="num">Gefundene Probleme</th>
</tr>
</thead>
<tbody>
<tr>
<td>Kursstruktur (Gültigkeit der Lösungen, Integrität des Voraussetzungsgraphen, Eindeutigkeit der IDs)</td>
<td class="num">2194 Übungsdateien, 48 Lektionen, 17 Module</td>
<td class="num">0</td>
</tr>
<tr>
<td>Struktur des erweiterten Übungsmodus-Pools</td>
<td class="num">64 Pool-Einträge (53 statisch + 11 Vorlagen)</td>
<td class="num">0</td>
</tr>
<tr>
<td>Abschnitt „Lifehacks" (jede Karte verweist auf ein reales Kurskonzept; Vorher-/Nachher-Code liefert im echten Interpreter identische Ausgabe)</td>
<td class="num">15 Karten, 30 Code-Schnappschüsse</td>
<td class="num">0</td>
</tr>
<tr>
<td>Lokalisierungsparität (Inhalts-Schlüssel, keine unübersetzten Platzhalter)</td>
<td class="num">12 214 Schlüssel × EN, ES, FR, DE, PT (61 070 Vergleiche)</td>
<td class="num">0</td>
</tr>
</tbody>
</table>
<h2>Werkzeuge</h2>
<div class="tools-list">
<div class="tool"><strong>Stryker Mutator</strong> — Mutationstests (Erzeugung und Ausführung Tausender absichtlicher Defekte gegen die bestehende Testsuite).</div>
<div class="tool"><strong>Vitest</strong> — Test-Runner (1112 Unit-/Integrationstests zum Zeitpunkt dieses Berichts).</div>
<div class="tool"><strong>TypeScript, strenger Modus</strong> — <code>strict</code>, <code>noUncheckedIndexedAccess</code>, <code>exactOptionalPropertyTypes</code>.</div>
<div class="tool"><strong>CPython 3.12.3</strong> — externes Orakel zur Prüfung des Interpreter-Verhaltens.</div>
<div class="tool"><strong>Eigene isolierte Skripte</strong> — Fuzz-Test-Erzeugung und Abgleich des Wiederholungsplaners, außerhalb des eigentlichen App-Codes.</div>
</div>
<h2>Wie diese Zahlen wirklich erreicht wurden — ohne Beschönigung</h2>
<div class="honest-box">
<p>
Diese Zahlen sind nicht der Ausgangspunkt gewesen. Der erste Mutationstest-Durchlauf am größten und schwierigsten Modul (dem Python-Interpreter, ~2500 Zeilen) ergab <strong>75,65 %</strong> — knapp ein Viertel der absichtlich eingeschleusten Defekte blieb also völlig unbemerkt. Diese Lücke auf 100 % zu schließen, brauchte sieben aufeinanderfolgende Durchläufe, die jeweils einen Teil des Rests schlossen und dabei neue Grenzfälle offenlegten — einschließlich Methodikfehlern, die während des Prüfprozesses selbst gemacht wurden.
</p>
<p>
<strong>Echte Anwendungsfehler gefunden und behoben</strong> (nicht nur Testlücken) — genau durch diesen gründlichen Prozess aufgedeckt: Die Punktzahl einer Lektion wurde nach einer korrigierten Falschantwort fälschlich zu niedrig angesetzt (ein einziger Fehler begrenzte die maximal mögliche Punktzahl dauerhaft, selbst wenn die Lernperson beim Wiederholen sofort richtig antwortete); der Einstufungstest vergab fälschlich die Erfolge „Fehlerfrei" und „Keine Gnade" für übersprungene, nicht wirklich abgeschlossene Lektionen; sowie drei eng begrenzte Bugs im Interpreter selbst (Ganzzahldivision, Rundung beim Restwert-Operator, Vergleich von <code>bool</code> mit <code>int</code>).
</p>
<p>
<strong>Ein Methodikfehler wurde mitten im Prozess entdeckt und korrigiert:</strong> Eine zwischenzeitliche Verifikationsmethode (Ersetzen des gesamten Zeilentexts statt Mutation an Strykers exakten Koordinaten) überschätzte, wie viele Mutanten tatsächlich „äquivalent" (also unlösbar) waren — von 35 solchen Annahmen bestätigte sich nur 1. Behoben durch Umstellung auf präzise, koordinatengenaue Verifikation für jeden Einzelfall.
</p>
<p>
<strong>Die letzten 5 Mutanten</strong> (String-Vergleich für das Sortieren von Listen) widerstanden sowohl einem Äquivalenzbeweis als auch jedem Test über die normale öffentliche Schnittstelle — der native Sortieralgorithmus von JavaScript führt redundante Vergleiche durch, die ein einzelnes falsches Paar-Ergebnis maskieren, selbst bei einer rückwärts sortierten 5-Elemente-Liste. Gelöst durch direktes Testen der internen Vergleichsfunktion, unter vollständiger Umgehung des unvorhersehbaren Verhaltens des fremden Sortieralgorithmus.
</p>
<p>
Fazit: Keine Zahl in diesem Bericht ist geschönt. Jede ist das Ergebnis iterativer, verifizierter Arbeit — einschließlich des Eingeständnisses und der Korrektur eigener Fehler unterwegs. Genau deshalb ist diesen Zahlen zu trauen: Die App verhält sich nicht nur bei den geprüften Beispielen korrekt — das Sicherheitsnetz der Tests drumherum erkennt künftige Regressionen tatsächlich, statt nur formal zu existieren.
</p>
<p>
<strong>Warum aus „2797 von 2797, 100%" kurzzeitig „3373 von 3374, 99,97%" wurde, bevor es wieder 100% waren:</strong> Nachdem zuerst 100% erreicht waren (oben), bekam der Interpreter neue Kursthemen dazu — direkte Iteration (<code>for x in iterable:</code> ohne <code>range()</code>), <code>__str__</code>/<code>__eq__</code>, <code>@staticmethod</code>/<code>@classmethod</code>, Dictionaries/Sets/Comprehensions/Generatoren — Hunderte neuer Zeilen, die noch nie durch Mutationstests gelaufen waren. Statt dieses Wachstum ungeprüft zu lassen (und stillschweigend weiter ein veraltetes „100%" anzuzeigen), wurde es genauso gründlich durch Stryker geschickt wie alles andere: 32 neue Regressionstests, jeder gegen einen konkreten überlebenden Mutanten geschrieben (erst den Code drumherum gelesen, logisch nachvollzogen, warum die bestehenden Tests ihn übersehen hatten, und erst dann den Test geschrieben) — keine Coverage-Auffüllung. Alles andere, was sich seit dem vorigen Mutationstest-Durchlauf (17. August) geändert hatte, wurde ebenfalls geprüft: der Parser (neue Grammatik für dieselben neuen Themen) und der Wiederholungsplaner (ein neuer Schwellenwert vor dem Interleaving) — beide bereits bei 100%, ohne weiteren Aufwand. Damit blieb genau ein Mutant ungelöst, und an dieser Stelle stand im Bericht kurzzeitig „3373 von 3374, 99,97%" — mit einer Erklärung, warum das ehrlicher ist als ein herbeigeführtes „100%": Eine Funktion zum Nachschlagen lokaler Variablen (<code>Scope.tryGet</code>) enthielt eine Prüfung, die theoretisch in zwei unterschiedlichen Szenarien relevant sein könnte, von denen sich eines angesichts jeder tatsächlichen Aufrufstelle in der App als mathematisch unmöglich erwies — und das andere, tatsächlich erreichbare Szenario war bereits durch einen eigenen Test abgedeckt. Beide Mutanten dieser Zeile mit einem einzigen Kommentar als „äquivalent" zu markieren, wäre der schnellste Weg zu „100%" gewesen — genau deshalb wurde es damals nicht getan: Das Werkzeug kann die Mutationsprüfung nur für eine ganze Zeile deaktivieren, nicht für einen einzelnen Mutanten, sodass diese Abkürzung auch die Abdeckung für das tatsächlich relevante Szenario weggewischt hätte.
</p>
<p>
<strong>Was danach geschah — und warum das echte 100% sind, kein Workaround:</strong> Statt sich mit 99,97% zufriedenzugeben oder den Rest still zu unterdrücken, lautete die nächste Frage, ob diese Zeile von vornherein toter Code war. Eine genauere Nachverfolgung jedes Aufrufs von <code>tryGet()</code> in der App (es gibt genau einen) und jeder Stelle, von der der von ihr geschützte Wert überhaupt stammen konnte, zeigte: ja — die Zeile war in jedem realen Programmzustand tatsächlich unerreichbar, nicht bloß schwer zu testen. Sie wurde vollständig entfernt. Das ist kein Trick: Das Löschen von wirklich totem Code kostet keine echte Abdeckung (anders als das Unterdrücken des Mutanten per Kommentar, das auch die echte, bereits verifizierte Abdeckung des Schwester-Mutanten weggeworfen hätte) — der Mutant, den diese Zeile erzeugte, hörte einfach zusammen mit ihr auf zu existieren. Die vollständige Testsuite (damals 1035 Tests) bestätigte, dass sich das Verhalten nicht im Geringsten änderte. Der nächste Stryker-Durchlauf ergab ein echtes <strong>100,00%</strong>. Die Lehre aus diesem kleinen Umweg im Bericht selbst: Die Bereitschaft, eine ehrliche Zahl knapp unter 100% zu zeigen, erwies sich als der schnellere Weg zu einem echten 100% — nicht als langsamerer, als die Lücke von Anfang an zu übertünchen.
</p>
<p>
<strong>Woher das Wachstum von 3371 auf 3694 und von 9655 auf 9710 kommt (1. September 2026):</strong> Der Kurs ist gewachsen — ein neues 17. Modul, „Import, Module und Dateiverarbeitung", brachte echte (nicht nur syntaktisch erkannte) Ausführung von <code>import</code>/<code>open()</code>/<code>with</code> gegen ein simuliertes virtuelles Dateisystem, und ein neuer Abschnitt „Lifehacks" (15 „Vorher → Nachher"-Karten mit Erklärung, warum die kürzere Schreibweise dasselbe leistet). Beide Ergänzungen durchliefen dieselbe Disziplin wie der Rest der App: <code>interpreter.ts</code> und <code>parser.ts</code> wurden erneut durch Mutationstests geschickt (+238 bzw. +36 Mutanten, alle erkannt), der neue <code>lifehacks-engine</code> und <code>manage-lifehacks</code> wurden von null auf 100% gebracht (2 echte Testlücken geschlossen, nicht überdeckt), alle 15 Karten (30 Code-Schnappschüsse) wurden gegen CPython geprüft, ebenso die 2 neuen schleifenbasierten Übungen aus Modul 17. Am selben Tag wurde außerdem der letzte Übersetzungsrückstand geschlossen: Der Abschnitt „Lifehacks" und alle 434 neuen Schlüssel aus Modul 17 wurden sofort ins Spanische, Französische, Deutsche und Portugiesische übersetzt, sodass alle 6 Kurssprachen jetzt bei 100% Inhaltsparität liegen (12 214 Schlüssel pro Sprache) — daher stammt auch das Wachstum der Lokalisierungsvergleiche von 58 635 auf 61 070.
</p>
</div>
<h2>Anmerkungen</h2>
<p>
Während der iterativen Validierung wurden drei eng begrenzte Interpreter-Bugs gefunden und behoben — keiner hat je echte Kursinhalte betroffen, und alle drei sind jetzt dauerhaft durch Tests abgesichert, die verhindern, dass sie erneut auftreten:
</p>
<ul>
<li>ganzzahlige Division wurde als Ganzzahl statt als Pythons tatsächliches Dezimalergebnis ausgegeben (<code>11</code> statt <code>11.0</code>);</li>
<li>ein seltener Gleitkomma-Rundungsfehler beim Restwert-Operator (<code>%</code>) bei gebrochenen Operanden;</li>
<li><code>bool</code> wurde in <code>==</code>/<code>!=</code>/<code>in</code>/<code>list.remove()</code> nicht als numerisch gleich zu <code>int</code> behandelt.</li>
</ul>
<p>
Die maximale Intervallobergrenze des Wiederholungsplaners griff korrekt in mehreren Hundert dafür konzipierten Schritten, ohne eine einzige Ausnahme. Die obigen Ergebnisse stammen aus dem letzten, aktuellsten Validierungsdurchlauf, ohne bekannte offene Probleme.
</p>
<footer>
Python Course — eine Offline-App zum Python-Lernen.
</footer>
</main>
</body>
</html>