@contractspec/module.learning-journey 3.7.16 → 3.7.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/dist/browser/contracts/index.js +1 -578
  2. package/dist/browser/contracts/models.js +1 -193
  3. package/dist/browser/contracts/onboarding.js +1 -417
  4. package/dist/browser/contracts/operations.js +1 -326
  5. package/dist/browser/contracts/shared.js +1 -5
  6. package/dist/browser/docs/index.js +7 -51
  7. package/dist/browser/docs/learning-journey.docblock.js +7 -51
  8. package/dist/browser/engines/index.js +1 -675
  9. package/dist/browser/engines/srs.js +1 -198
  10. package/dist/browser/engines/streak.js +1 -159
  11. package/dist/browser/engines/xp.js +1 -320
  12. package/dist/browser/entities/ai.js +1 -343
  13. package/dist/browser/entities/course.js +1 -276
  14. package/dist/browser/entities/flashcard.js +1 -222
  15. package/dist/browser/entities/gamification.js +1 -340
  16. package/dist/browser/entities/index.js +1 -2140
  17. package/dist/browser/entities/learner.js +1 -333
  18. package/dist/browser/entities/onboarding.js +1 -301
  19. package/dist/browser/entities/quiz.js +1 -304
  20. package/dist/browser/events.js +1 -423
  21. package/dist/browser/i18n/catalogs/en.js +1 -43
  22. package/dist/browser/i18n/catalogs/es.js +1 -43
  23. package/dist/browser/i18n/catalogs/fr.js +1 -43
  24. package/dist/browser/i18n/catalogs/index.js +1 -127
  25. package/dist/browser/i18n/index.js +1 -169
  26. package/dist/browser/i18n/keys.js +1 -16
  27. package/dist/browser/i18n/locale.js +1 -13
  28. package/dist/browser/i18n/messages.js +1 -139
  29. package/dist/browser/index.js +7 -3914
  30. package/dist/browser/learning-journey.capability.js +1 -43
  31. package/dist/browser/learning-journey.feature.js +1 -56
  32. package/dist/contracts/index.js +1 -578
  33. package/dist/contracts/models.js +1 -193
  34. package/dist/contracts/onboarding.js +1 -417
  35. package/dist/contracts/operations.js +1 -326
  36. package/dist/contracts/shared.js +1 -5
  37. package/dist/docs/index.js +7 -51
  38. package/dist/docs/learning-journey.docblock.js +7 -51
  39. package/dist/engines/index.js +1 -675
  40. package/dist/engines/srs.js +1 -198
  41. package/dist/engines/streak.js +1 -159
  42. package/dist/engines/xp.js +1 -320
  43. package/dist/entities/ai.js +1 -343
  44. package/dist/entities/course.js +1 -276
  45. package/dist/entities/flashcard.js +1 -222
  46. package/dist/entities/gamification.js +1 -340
  47. package/dist/entities/index.js +1 -2140
  48. package/dist/entities/learner.js +1 -333
  49. package/dist/entities/onboarding.js +1 -301
  50. package/dist/entities/quiz.js +1 -304
  51. package/dist/events.js +1 -423
  52. package/dist/i18n/catalogs/en.js +1 -43
  53. package/dist/i18n/catalogs/es.js +1 -43
  54. package/dist/i18n/catalogs/fr.js +1 -43
  55. package/dist/i18n/catalogs/index.js +1 -127
  56. package/dist/i18n/index.js +1 -169
  57. package/dist/i18n/keys.js +1 -16
  58. package/dist/i18n/locale.js +1 -13
  59. package/dist/i18n/messages.js +1 -139
  60. package/dist/index.js +7 -3914
  61. package/dist/learning-journey.capability.js +1 -43
  62. package/dist/learning-journey.feature.js +1 -56
  63. package/dist/node/contracts/index.js +1 -578
  64. package/dist/node/contracts/models.js +1 -193
  65. package/dist/node/contracts/onboarding.js +1 -417
  66. package/dist/node/contracts/operations.js +1 -326
  67. package/dist/node/contracts/shared.js +1 -5
  68. package/dist/node/docs/index.js +7 -51
  69. package/dist/node/docs/learning-journey.docblock.js +7 -51
  70. package/dist/node/engines/index.js +1 -675
  71. package/dist/node/engines/srs.js +1 -198
  72. package/dist/node/engines/streak.js +1 -159
  73. package/dist/node/engines/xp.js +1 -320
  74. package/dist/node/entities/ai.js +1 -343
  75. package/dist/node/entities/course.js +1 -276
  76. package/dist/node/entities/flashcard.js +1 -222
  77. package/dist/node/entities/gamification.js +1 -340
  78. package/dist/node/entities/index.js +1 -2140
  79. package/dist/node/entities/learner.js +1 -333
  80. package/dist/node/entities/onboarding.js +1 -301
  81. package/dist/node/entities/quiz.js +1 -304
  82. package/dist/node/events.js +1 -423
  83. package/dist/node/i18n/catalogs/en.js +1 -43
  84. package/dist/node/i18n/catalogs/es.js +1 -43
  85. package/dist/node/i18n/catalogs/fr.js +1 -43
  86. package/dist/node/i18n/catalogs/index.js +1 -127
  87. package/dist/node/i18n/index.js +1 -169
  88. package/dist/node/i18n/keys.js +1 -16
  89. package/dist/node/i18n/locale.js +1 -13
  90. package/dist/node/i18n/messages.js +1 -139
  91. package/dist/node/index.js +7 -3914
  92. package/dist/node/learning-journey.capability.js +1 -43
  93. package/dist/node/learning-journey.feature.js +1 -56
  94. package/package.json +5 -5
@@ -1,676 +1,2 @@
1
1
  // @bun
2
- // src/engines/srs.ts
3
- var DEFAULT_SRS_CONFIG = {
4
- learningSteps: [1, 10],
5
- graduatingInterval: 1,
6
- easyInterval: 4,
7
- relearningSteps: [10],
8
- minEaseFactor: 1.3,
9
- maxInterval: 365,
10
- intervalModifier: 1,
11
- newIntervalModifier: 0.5,
12
- hardIntervalModifier: 1.2,
13
- easyBonus: 1.3
14
- };
15
-
16
- class SRSEngine {
17
- config;
18
- constructor(config = {}) {
19
- this.config = { ...DEFAULT_SRS_CONFIG, ...config };
20
- }
21
- calculateNextReview(state, rating, now = new Date) {
22
- if (!state.isGraduated && !state.isRelearning) {
23
- return this.handleLearningCard(state, rating, now);
24
- }
25
- if (state.isRelearning) {
26
- return this.handleRelearningCard(state, rating, now);
27
- }
28
- return this.handleReviewCard(state, rating, now);
29
- }
30
- getInitialState() {
31
- return {
32
- interval: 0,
33
- easeFactor: 2.5,
34
- repetitions: 0,
35
- learningStep: 0,
36
- isGraduated: false,
37
- isRelearning: false,
38
- lapses: 0
39
- };
40
- }
41
- isDue(nextReviewAt, now = new Date) {
42
- return nextReviewAt <= now;
43
- }
44
- getOverdueDays(nextReviewAt, now = new Date) {
45
- const diff = now.getTime() - nextReviewAt.getTime();
46
- return Math.floor(diff / (1000 * 60 * 60 * 24));
47
- }
48
- handleLearningCard(state, rating, now) {
49
- const steps = this.config.learningSteps;
50
- let newStep = state.learningStep;
51
- let isGraduated = false;
52
- let interval = 0;
53
- let nextReviewAt;
54
- switch (rating) {
55
- case "AGAIN":
56
- newStep = 0;
57
- interval = steps[0] ?? 1;
58
- nextReviewAt = this.addMinutes(now, interval);
59
- break;
60
- case "HARD":
61
- interval = steps[newStep] ?? steps[0] ?? 1;
62
- nextReviewAt = this.addMinutes(now, interval);
63
- break;
64
- case "GOOD":
65
- newStep++;
66
- if (newStep >= steps.length) {
67
- isGraduated = true;
68
- interval = this.config.graduatingInterval;
69
- nextReviewAt = this.addDays(now, interval);
70
- } else {
71
- interval = steps[newStep] ?? 10;
72
- nextReviewAt = this.addMinutes(now, interval);
73
- }
74
- break;
75
- case "EASY":
76
- isGraduated = true;
77
- interval = this.config.easyInterval;
78
- nextReviewAt = this.addDays(now, interval);
79
- break;
80
- }
81
- return {
82
- interval: isGraduated ? interval : 0,
83
- easeFactor: state.easeFactor,
84
- repetitions: isGraduated ? 1 : 0,
85
- nextReviewAt,
86
- learningStep: newStep,
87
- isGraduated,
88
- isRelearning: false,
89
- lapses: state.lapses
90
- };
91
- }
92
- handleRelearningCard(state, rating, now) {
93
- const steps = this.config.relearningSteps;
94
- let newStep = state.learningStep;
95
- let isRelearning = true;
96
- let interval = 0;
97
- let nextReviewAt;
98
- switch (rating) {
99
- case "AGAIN":
100
- newStep = 0;
101
- interval = steps[0] ?? 10;
102
- nextReviewAt = this.addMinutes(now, interval);
103
- break;
104
- case "HARD":
105
- interval = steps[newStep] ?? steps[0] ?? 10;
106
- nextReviewAt = this.addMinutes(now, interval);
107
- break;
108
- case "GOOD":
109
- newStep++;
110
- if (newStep >= steps.length) {
111
- isRelearning = false;
112
- interval = Math.max(1, Math.floor(state.interval * this.config.newIntervalModifier));
113
- nextReviewAt = this.addDays(now, interval);
114
- } else {
115
- interval = steps[newStep] ?? 10;
116
- nextReviewAt = this.addMinutes(now, interval);
117
- }
118
- break;
119
- case "EASY":
120
- isRelearning = false;
121
- interval = Math.max(1, Math.floor(state.interval * this.config.newIntervalModifier * 1.5));
122
- nextReviewAt = this.addDays(now, interval);
123
- break;
124
- }
125
- return {
126
- interval: isRelearning ? state.interval : interval,
127
- easeFactor: state.easeFactor,
128
- repetitions: isRelearning ? state.repetitions : state.repetitions + 1,
129
- nextReviewAt,
130
- learningStep: newStep,
131
- isGraduated: true,
132
- isRelearning,
133
- lapses: state.lapses
134
- };
135
- }
136
- handleReviewCard(state, rating, now) {
137
- let newInterval;
138
- let newEaseFactor = state.easeFactor;
139
- let repetitions = state.repetitions;
140
- let isRelearning = false;
141
- let learningStep = 0;
142
- let lapses = state.lapses;
143
- switch (rating) {
144
- case "AGAIN":
145
- lapses++;
146
- isRelearning = true;
147
- learningStep = 0;
148
- newEaseFactor = Math.max(this.config.minEaseFactor, newEaseFactor - 0.2);
149
- newInterval = state.interval;
150
- return {
151
- interval: newInterval,
152
- easeFactor: newEaseFactor,
153
- repetitions,
154
- nextReviewAt: this.addMinutes(now, this.config.relearningSteps[0] ?? 10),
155
- learningStep,
156
- isGraduated: true,
157
- isRelearning: true,
158
- lapses
159
- };
160
- case "HARD":
161
- newEaseFactor = Math.max(this.config.minEaseFactor, newEaseFactor - 0.15);
162
- newInterval = Math.max(state.interval + 1, state.interval * this.config.hardIntervalModifier);
163
- break;
164
- case "GOOD":
165
- newInterval = state.interval * newEaseFactor * this.config.intervalModifier;
166
- repetitions++;
167
- break;
168
- case "EASY":
169
- newEaseFactor = newEaseFactor + 0.15;
170
- newInterval = state.interval * newEaseFactor * this.config.easyBonus * this.config.intervalModifier;
171
- repetitions++;
172
- break;
173
- }
174
- newInterval = Math.min(Math.round(newInterval), this.config.maxInterval);
175
- newInterval = Math.max(1, newInterval);
176
- return {
177
- interval: newInterval,
178
- easeFactor: newEaseFactor,
179
- repetitions,
180
- nextReviewAt: this.addDays(now, newInterval),
181
- learningStep,
182
- isGraduated: true,
183
- isRelearning,
184
- lapses
185
- };
186
- }
187
- addMinutes(date, minutes) {
188
- return new Date(date.getTime() + minutes * 60 * 1000);
189
- }
190
- addDays(date, days) {
191
- return new Date(date.getTime() + days * 24 * 60 * 60 * 1000);
192
- }
193
- }
194
- var srsEngine = new SRSEngine;
195
-
196
- // src/engines/streak.ts
197
- var DEFAULT_STREAK_CONFIG = {
198
- timezone: "UTC",
199
- freezesPerMonth: 2,
200
- maxFreezes: 5,
201
- gracePeriodHours: 4
202
- };
203
-
204
- class StreakEngine {
205
- config;
206
- constructor(config = {}) {
207
- this.config = { ...DEFAULT_STREAK_CONFIG, ...config };
208
- }
209
- update(state, now = new Date) {
210
- const todayDate = this.getDateString(now);
211
- const result = {
212
- state: { ...state },
213
- streakMaintained: false,
214
- streakLost: false,
215
- freezeUsed: false,
216
- newStreak: false,
217
- daysMissed: 0
218
- };
219
- if (!state.lastActivityDate) {
220
- result.state.currentStreak = 1;
221
- result.state.longestStreak = Math.max(1, state.longestStreak);
222
- result.state.lastActivityAt = now;
223
- result.state.lastActivityDate = todayDate;
224
- result.newStreak = true;
225
- result.streakMaintained = true;
226
- return result;
227
- }
228
- if (state.lastActivityDate === todayDate) {
229
- result.state.lastActivityAt = now;
230
- result.streakMaintained = true;
231
- return result;
232
- }
233
- const daysSinceActivity = this.getDaysBetween(state.lastActivityDate, todayDate);
234
- if (daysSinceActivity === 1) {
235
- result.state.currentStreak = state.currentStreak + 1;
236
- result.state.longestStreak = Math.max(result.state.currentStreak, state.longestStreak);
237
- result.state.lastActivityAt = now;
238
- result.state.lastActivityDate = todayDate;
239
- result.streakMaintained = true;
240
- return result;
241
- }
242
- result.daysMissed = daysSinceActivity - 1;
243
- const freezesNeeded = result.daysMissed;
244
- if (freezesNeeded <= state.freezesRemaining) {
245
- result.state.freezesRemaining = state.freezesRemaining - freezesNeeded;
246
- result.state.freezeUsedAt = now;
247
- result.state.currentStreak = state.currentStreak + 1;
248
- result.state.longestStreak = Math.max(result.state.currentStreak, state.longestStreak);
249
- result.state.lastActivityAt = now;
250
- result.state.lastActivityDate = todayDate;
251
- result.freezeUsed = true;
252
- result.streakMaintained = true;
253
- return result;
254
- }
255
- result.streakLost = true;
256
- result.state.currentStreak = 1;
257
- result.state.lastActivityAt = now;
258
- result.state.lastActivityDate = todayDate;
259
- result.newStreak = true;
260
- return result;
261
- }
262
- checkStatus(state, now = new Date) {
263
- if (!state.lastActivityDate) {
264
- return {
265
- isActive: false,
266
- willExpireAt: null,
267
- canUseFreeze: false,
268
- daysUntilExpiry: 0
269
- };
270
- }
271
- const todayDate = this.getDateString(now);
272
- const daysSinceActivity = this.getDaysBetween(state.lastActivityDate, todayDate);
273
- if (daysSinceActivity === 0) {
274
- const tomorrow = this.addDays(now, 1);
275
- tomorrow.setHours(23, 59, 59, 999);
276
- return {
277
- isActive: true,
278
- willExpireAt: tomorrow,
279
- canUseFreeze: state.freezesRemaining > 0,
280
- daysUntilExpiry: 1
281
- };
282
- }
283
- if (daysSinceActivity === 1) {
284
- const endOfDay = new Date(now);
285
- endOfDay.setHours(23 + this.config.gracePeriodHours, 59, 59, 999);
286
- return {
287
- isActive: true,
288
- willExpireAt: endOfDay,
289
- canUseFreeze: state.freezesRemaining > 0,
290
- daysUntilExpiry: 0
291
- };
292
- }
293
- const missedDays = daysSinceActivity - 1;
294
- return {
295
- isActive: missedDays <= state.freezesRemaining,
296
- willExpireAt: null,
297
- canUseFreeze: missedDays <= state.freezesRemaining,
298
- daysUntilExpiry: -missedDays
299
- };
300
- }
301
- useFreeze(state, now = new Date) {
302
- if (state.freezesRemaining <= 0) {
303
- return null;
304
- }
305
- return {
306
- ...state,
307
- freezesRemaining: state.freezesRemaining - 1,
308
- freezeUsedAt: now
309
- };
310
- }
311
- awardMonthlyFreezes(state) {
312
- return {
313
- ...state,
314
- freezesRemaining: Math.min(state.freezesRemaining + this.config.freezesPerMonth, this.config.maxFreezes)
315
- };
316
- }
317
- getInitialState() {
318
- return {
319
- currentStreak: 0,
320
- longestStreak: 0,
321
- lastActivityAt: null,
322
- lastActivityDate: null,
323
- freezesRemaining: this.config.freezesPerMonth,
324
- freezeUsedAt: null
325
- };
326
- }
327
- getMilestones(currentStreak) {
328
- const milestones = [3, 7, 14, 30, 60, 90, 180, 365, 500, 1000];
329
- const achieved = milestones.filter((m) => currentStreak >= m);
330
- const next = milestones.find((m) => currentStreak < m) ?? null;
331
- return { achieved, next };
332
- }
333
- getDateString(date) {
334
- const year = date.getFullYear();
335
- const month = String(date.getMonth() + 1).padStart(2, "0");
336
- const day = String(date.getDate()).padStart(2, "0");
337
- return `${year}-${month}-${day}`;
338
- }
339
- getDaysBetween(dateStr1, dateStr2) {
340
- const date1 = new Date(dateStr1);
341
- const date2 = new Date(dateStr2);
342
- const diffTime = date2.getTime() - date1.getTime();
343
- return Math.floor(diffTime / (1000 * 60 * 60 * 24));
344
- }
345
- addDays(date, days) {
346
- return new Date(date.getTime() + days * 24 * 60 * 60 * 1000);
347
- }
348
- }
349
- var streakEngine = new StreakEngine;
350
-
351
- // src/i18n/catalogs/en.ts
352
- import { defineTranslation } from "@contractspec/lib.contracts-spec/translations";
353
- var enMessages = defineTranslation({
354
- meta: {
355
- key: "learning-journey.messages",
356
- version: "1.0.0",
357
- domain: "learning-journey",
358
- description: "XP source labels for the learning-journey module",
359
- owners: ["platform"],
360
- stability: "experimental"
361
- },
362
- locale: "en",
363
- fallback: "en",
364
- messages: {
365
- "xp.source.base": {
366
- value: "Base",
367
- description: "XP breakdown label for base XP"
368
- },
369
- "xp.source.scoreBonus": {
370
- value: "Score Bonus",
371
- description: "XP breakdown label for score-based bonus"
372
- },
373
- "xp.source.perfectScore": {
374
- value: "Perfect Score",
375
- description: "XP breakdown label for perfect score bonus"
376
- },
377
- "xp.source.firstAttempt": {
378
- value: "First Attempt",
379
- description: "XP breakdown label for first attempt bonus"
380
- },
381
- "xp.source.retryPenalty": {
382
- value: "Retry Penalty",
383
- description: "XP breakdown label for retry penalty"
384
- },
385
- "xp.source.streakBonus": {
386
- value: "Streak Bonus",
387
- description: "XP breakdown label for streak bonus"
388
- }
389
- }
390
- });
391
-
392
- // src/i18n/catalogs/es.ts
393
- import { defineTranslation as defineTranslation2 } from "@contractspec/lib.contracts-spec/translations";
394
- var esMessages = defineTranslation2({
395
- meta: {
396
- key: "learning-journey.messages",
397
- version: "1.0.0",
398
- domain: "learning-journey",
399
- description: "XP source labels (Spanish)",
400
- owners: ["platform"],
401
- stability: "experimental"
402
- },
403
- locale: "es",
404
- fallback: "en",
405
- messages: {
406
- "xp.source.base": {
407
- value: "Base",
408
- description: "XP breakdown label for base XP"
409
- },
410
- "xp.source.scoreBonus": {
411
- value: "Bonificaci\xF3n por puntuaci\xF3n",
412
- description: "XP breakdown label for score-based bonus"
413
- },
414
- "xp.source.perfectScore": {
415
- value: "Puntuaci\xF3n perfecta",
416
- description: "XP breakdown label for perfect score bonus"
417
- },
418
- "xp.source.firstAttempt": {
419
- value: "Primer intento",
420
- description: "XP breakdown label for first attempt bonus"
421
- },
422
- "xp.source.retryPenalty": {
423
- value: "Penalizaci\xF3n por reintento",
424
- description: "XP breakdown label for retry penalty"
425
- },
426
- "xp.source.streakBonus": {
427
- value: "Bonificaci\xF3n por racha",
428
- description: "XP breakdown label for streak bonus"
429
- }
430
- }
431
- });
432
-
433
- // src/i18n/catalogs/fr.ts
434
- import { defineTranslation as defineTranslation3 } from "@contractspec/lib.contracts-spec/translations";
435
- var frMessages = defineTranslation3({
436
- meta: {
437
- key: "learning-journey.messages",
438
- version: "1.0.0",
439
- domain: "learning-journey",
440
- description: "XP source labels (French)",
441
- owners: ["platform"],
442
- stability: "experimental"
443
- },
444
- locale: "fr",
445
- fallback: "en",
446
- messages: {
447
- "xp.source.base": {
448
- value: "Base",
449
- description: "XP breakdown label for base XP"
450
- },
451
- "xp.source.scoreBonus": {
452
- value: "Bonus de score",
453
- description: "XP breakdown label for score-based bonus"
454
- },
455
- "xp.source.perfectScore": {
456
- value: "Score parfait",
457
- description: "XP breakdown label for perfect score bonus"
458
- },
459
- "xp.source.firstAttempt": {
460
- value: "Premier essai",
461
- description: "XP breakdown label for first attempt bonus"
462
- },
463
- "xp.source.retryPenalty": {
464
- value: "P\xE9nalit\xE9 de r\xE9essai",
465
- description: "XP breakdown label for retry penalty"
466
- },
467
- "xp.source.streakBonus": {
468
- value: "Bonus de s\xE9rie",
469
- description: "XP breakdown label for streak bonus"
470
- }
471
- }
472
- });
473
-
474
- // src/i18n/messages.ts
475
- import {
476
- createI18nFactory
477
- } from "@contractspec/lib.contracts-spec/translations";
478
- var factory = createI18nFactory({
479
- specKey: "learning-journey.messages",
480
- catalogs: [enMessages, frMessages, esMessages]
481
- });
482
- var createLearningJourneyI18n = factory.create;
483
- var getDefaultI18n = factory.getDefault;
484
- var resetI18nRegistry = factory.resetRegistry;
485
-
486
- // src/engines/xp.ts
487
- var DEFAULT_XP_CONFIG = {
488
- baseValues: {
489
- lesson_complete: 10,
490
- quiz_pass: 20,
491
- quiz_perfect: 50,
492
- flashcard_review: 1,
493
- course_complete: 200,
494
- module_complete: 50,
495
- streak_bonus: 5,
496
- achievement_unlock: 0,
497
- daily_goal_complete: 15,
498
- first_lesson: 25,
499
- onboarding_step: 5,
500
- onboarding_complete: 50
501
- },
502
- scoreThresholds: [
503
- { min: 90, multiplier: 1.5 },
504
- { min: 80, multiplier: 1.25 },
505
- { min: 70, multiplier: 1 },
506
- { min: 60, multiplier: 0.75 },
507
- { min: 0, multiplier: 0.5 }
508
- ],
509
- streakTiers: [
510
- { days: 365, bonus: 50 },
511
- { days: 180, bonus: 30 },
512
- { days: 90, bonus: 20 },
513
- { days: 30, bonus: 15 },
514
- { days: 14, bonus: 10 },
515
- { days: 7, bonus: 5 },
516
- { days: 3, bonus: 2 },
517
- { days: 1, bonus: 0 }
518
- ],
519
- perfectScoreMultiplier: 1.5,
520
- firstAttemptBonus: 10,
521
- retryPenalty: 0.5,
522
- speedBonusMultiplier: 1.2,
523
- speedBonusThreshold: 0.8
524
- };
525
-
526
- class XPEngine {
527
- config;
528
- constructor(config = {}) {
529
- this.config = {
530
- ...DEFAULT_XP_CONFIG,
531
- ...config,
532
- baseValues: { ...DEFAULT_XP_CONFIG.baseValues, ...config.baseValues },
533
- scoreThresholds: config.scoreThresholds || DEFAULT_XP_CONFIG.scoreThresholds,
534
- streakTiers: config.streakTiers || DEFAULT_XP_CONFIG.streakTiers
535
- };
536
- }
537
- calculate(input) {
538
- const breakdown = [];
539
- const baseXp = input.baseXp ?? this.config.baseValues[input.activity];
540
- let totalXp = baseXp;
541
- breakdown.push({
542
- source: "base",
543
- amount: baseXp
544
- });
545
- if (input.score !== undefined) {
546
- const scoreMultiplier = this.getScoreMultiplier(input.score);
547
- if (scoreMultiplier !== 1) {
548
- const scoreBonus = Math.round(baseXp * (scoreMultiplier - 1));
549
- totalXp += scoreBonus;
550
- breakdown.push({
551
- source: "score_bonus",
552
- amount: scoreBonus,
553
- multiplier: scoreMultiplier
554
- });
555
- }
556
- if (input.score === 100) {
557
- const perfectBonus = Math.round(baseXp * (this.config.perfectScoreMultiplier - 1));
558
- totalXp += perfectBonus;
559
- breakdown.push({
560
- source: "perfect_score",
561
- amount: perfectBonus,
562
- multiplier: this.config.perfectScoreMultiplier
563
- });
564
- }
565
- }
566
- if (input.attemptNumber === 1 && !input.isRetry) {
567
- totalXp += this.config.firstAttemptBonus;
568
- breakdown.push({
569
- source: "first_attempt",
570
- amount: this.config.firstAttemptBonus
571
- });
572
- }
573
- if (input.isRetry) {
574
- const penalty = Math.round(totalXp * (1 - this.config.retryPenalty));
575
- totalXp -= penalty;
576
- breakdown.push({
577
- source: "retry_penalty",
578
- amount: -penalty,
579
- multiplier: this.config.retryPenalty
580
- });
581
- }
582
- if (input.currentStreak && input.currentStreak > 0) {
583
- const streakBonus = this.getStreakBonus(input.currentStreak);
584
- if (streakBonus > 0) {
585
- totalXp += streakBonus;
586
- breakdown.push({
587
- source: "streak_bonus",
588
- amount: streakBonus
589
- });
590
- }
591
- }
592
- if (baseXp > 0) {
593
- totalXp = Math.max(1, totalXp);
594
- }
595
- return {
596
- totalXp: Math.round(totalXp),
597
- baseXp,
598
- breakdown
599
- };
600
- }
601
- calculateStreakBonus(currentStreak) {
602
- const bonus = this.getStreakBonus(currentStreak);
603
- return {
604
- totalXp: bonus,
605
- baseXp: bonus,
606
- breakdown: [
607
- {
608
- source: "streak_bonus",
609
- amount: bonus
610
- }
611
- ]
612
- };
613
- }
614
- getXpForLevel(level) {
615
- if (level <= 1)
616
- return 0;
617
- return Math.round(100 * Math.pow(level - 1, 1.5));
618
- }
619
- getLevelFromXp(totalXp) {
620
- let level = 1;
621
- let xpRequired = this.getXpForLevel(level + 1);
622
- while (totalXp >= xpRequired && level < 1000) {
623
- level++;
624
- xpRequired = this.getXpForLevel(level + 1);
625
- }
626
- const xpForCurrentLevel = this.getXpForLevel(level);
627
- const xpForNextLevel = this.getXpForLevel(level + 1);
628
- return {
629
- level,
630
- xpInLevel: totalXp - xpForCurrentLevel,
631
- xpForNextLevel: xpForNextLevel - xpForCurrentLevel
632
- };
633
- }
634
- getScoreMultiplier(score) {
635
- for (const threshold of this.config.scoreThresholds) {
636
- if (score >= threshold.min) {
637
- return threshold.multiplier;
638
- }
639
- }
640
- return 1;
641
- }
642
- getStreakBonus(streak) {
643
- for (const tier of this.config.streakTiers) {
644
- if (streak >= tier.days) {
645
- return tier.bonus;
646
- }
647
- }
648
- return 0;
649
- }
650
- }
651
- var SOURCE_KEY_MAP = {
652
- base: "xp.source.base",
653
- score_bonus: "xp.source.scoreBonus",
654
- perfect_score: "xp.source.perfectScore",
655
- first_attempt: "xp.source.firstAttempt",
656
- retry_penalty: "xp.source.retryPenalty",
657
- streak_bonus: "xp.source.streakBonus"
658
- };
659
- function getXpSourceLabel(source, locale) {
660
- const i18n = createLearningJourneyI18n(locale);
661
- const i18nKey = SOURCE_KEY_MAP[source];
662
- return i18nKey ? i18n.t(i18nKey) : source;
663
- }
664
- var xpEngine = new XPEngine;
665
- export {
666
- xpEngine,
667
- streakEngine,
668
- srsEngine,
669
- getXpSourceLabel,
670
- XPEngine,
671
- StreakEngine,
672
- SRSEngine,
673
- DEFAULT_XP_CONFIG,
674
- DEFAULT_STREAK_CONFIG,
675
- DEFAULT_SRS_CONFIG
676
- };
2
+ var N={learningSteps:[1,10],graduatingInterval:1,easyInterval:4,relearningSteps:[10],minEaseFactor:1.3,maxInterval:365,intervalModifier:1,newIntervalModifier:0.5,hardIntervalModifier:1.2,easyBonus:1.3};class K{config;constructor(j={}){this.config={...N,...j}}calculateNextReview(j,Q,W=new Date){if(!j.isGraduated&&!j.isRelearning)return this.handleLearningCard(j,Q,W);if(j.isRelearning)return this.handleRelearningCard(j,Q,W);return this.handleReviewCard(j,Q,W)}getInitialState(){return{interval:0,easeFactor:2.5,repetitions:0,learningStep:0,isGraduated:!1,isRelearning:!1,lapses:0}}isDue(j,Q=new Date){return j<=Q}getOverdueDays(j,Q=new Date){let W=Q.getTime()-j.getTime();return Math.floor(W/86400000)}handleLearningCard(j,Q,W){let H=this.config.learningSteps,V=j.learningStep,Z=!1,$=0,J;switch(Q){case"AGAIN":V=0,$=H[0]??1,J=this.addMinutes(W,$);break;case"HARD":$=H[V]??H[0]??1,J=this.addMinutes(W,$);break;case"GOOD":if(V++,V>=H.length)Z=!0,$=this.config.graduatingInterval,J=this.addDays(W,$);else $=H[V]??10,J=this.addMinutes(W,$);break;case"EASY":Z=!0,$=this.config.easyInterval,J=this.addDays(W,$);break}return{interval:Z?$:0,easeFactor:j.easeFactor,repetitions:Z?1:0,nextReviewAt:J,learningStep:V,isGraduated:Z,isRelearning:!1,lapses:j.lapses}}handleRelearningCard(j,Q,W){let H=this.config.relearningSteps,V=j.learningStep,Z=!0,$=0,J;switch(Q){case"AGAIN":V=0,$=H[0]??10,J=this.addMinutes(W,$);break;case"HARD":$=H[V]??H[0]??10,J=this.addMinutes(W,$);break;case"GOOD":if(V++,V>=H.length)Z=!1,$=Math.max(1,Math.floor(j.interval*this.config.newIntervalModifier)),J=this.addDays(W,$);else $=H[V]??10,J=this.addMinutes(W,$);break;case"EASY":Z=!1,$=Math.max(1,Math.floor(j.interval*this.config.newIntervalModifier*1.5)),J=this.addDays(W,$);break}return{interval:Z?j.interval:$,easeFactor:j.easeFactor,repetitions:Z?j.repetitions:j.repetitions+1,nextReviewAt:J,learningStep:V,isGraduated:!0,isRelearning:Z,lapses:j.lapses}}handleReviewCard(j,Q,W){let H,V=j.easeFactor,Z=j.repetitions,$=!1,J=0,q=j.lapses;switch(Q){case"AGAIN":return q++,$=!0,J=0,V=Math.max(this.config.minEaseFactor,V-0.2),H=j.interval,{interval:H,easeFactor:V,repetitions:Z,nextReviewAt:this.addMinutes(W,this.config.relearningSteps[0]??10),learningStep:J,isGraduated:!0,isRelearning:!0,lapses:q};case"HARD":V=Math.max(this.config.minEaseFactor,V-0.15),H=Math.max(j.interval+1,j.interval*this.config.hardIntervalModifier);break;case"GOOD":H=j.interval*V*this.config.intervalModifier,Z++;break;case"EASY":V=V+0.15,H=j.interval*V*this.config.easyBonus*this.config.intervalModifier,Z++;break}return H=Math.min(Math.round(H),this.config.maxInterval),H=Math.max(1,H),{interval:H,easeFactor:V,repetitions:Z,nextReviewAt:this.addDays(W,H),learningStep:J,isGraduated:!0,isRelearning:$,lapses:q}}addMinutes(j,Q){return new Date(j.getTime()+Q*60*1000)}addDays(j,Q){return new Date(j.getTime()+Q*24*60*60*1000)}}var I=new K;var O={timezone:"UTC",freezesPerMonth:2,maxFreezes:5,gracePeriodHours:4};class h{config;constructor(j={}){this.config={...O,...j}}update(j,Q=new Date){let W=this.getDateString(Q),H={state:{...j},streakMaintained:!1,streakLost:!1,freezeUsed:!1,newStreak:!1,daysMissed:0};if(!j.lastActivityDate)return H.state.currentStreak=1,H.state.longestStreak=Math.max(1,j.longestStreak),H.state.lastActivityAt=Q,H.state.lastActivityDate=W,H.newStreak=!0,H.streakMaintained=!0,H;if(j.lastActivityDate===W)return H.state.lastActivityAt=Q,H.streakMaintained=!0,H;let V=this.getDaysBetween(j.lastActivityDate,W);if(V===1)return H.state.currentStreak=j.currentStreak+1,H.state.longestStreak=Math.max(H.state.currentStreak,j.longestStreak),H.state.lastActivityAt=Q,H.state.lastActivityDate=W,H.streakMaintained=!0,H;H.daysMissed=V-1;let Z=H.daysMissed;if(Z<=j.freezesRemaining)return H.state.freezesRemaining=j.freezesRemaining-Z,H.state.freezeUsedAt=Q,H.state.currentStreak=j.currentStreak+1,H.state.longestStreak=Math.max(H.state.currentStreak,j.longestStreak),H.state.lastActivityAt=Q,H.state.lastActivityDate=W,H.freezeUsed=!0,H.streakMaintained=!0,H;return H.streakLost=!0,H.state.currentStreak=1,H.state.lastActivityAt=Q,H.state.lastActivityDate=W,H.newStreak=!0,H}checkStatus(j,Q=new Date){if(!j.lastActivityDate)return{isActive:!1,willExpireAt:null,canUseFreeze:!1,daysUntilExpiry:0};let W=this.getDateString(Q),H=this.getDaysBetween(j.lastActivityDate,W);if(H===0){let Z=this.addDays(Q,1);return Z.setHours(23,59,59,999),{isActive:!0,willExpireAt:Z,canUseFreeze:j.freezesRemaining>0,daysUntilExpiry:1}}if(H===1){let Z=new Date(Q);return Z.setHours(23+this.config.gracePeriodHours,59,59,999),{isActive:!0,willExpireAt:Z,canUseFreeze:j.freezesRemaining>0,daysUntilExpiry:0}}let V=H-1;return{isActive:V<=j.freezesRemaining,willExpireAt:null,canUseFreeze:V<=j.freezesRemaining,daysUntilExpiry:-V}}useFreeze(j,Q=new Date){if(j.freezesRemaining<=0)return null;return{...j,freezesRemaining:j.freezesRemaining-1,freezeUsedAt:Q}}awardMonthlyFreezes(j){return{...j,freezesRemaining:Math.min(j.freezesRemaining+this.config.freezesPerMonth,this.config.maxFreezes)}}getInitialState(){return{currentStreak:0,longestStreak:0,lastActivityAt:null,lastActivityDate:null,freezesRemaining:this.config.freezesPerMonth,freezeUsedAt:null}}getMilestones(j){let Q=[3,7,14,30,60,90,180,365,500,1000],W=Q.filter((V)=>j>=V),H=Q.find((V)=>j<V)??null;return{achieved:W,next:H}}getDateString(j){let Q=j.getFullYear(),W=String(j.getMonth()+1).padStart(2,"0"),H=String(j.getDate()).padStart(2,"0");return`${Q}-${W}-${H}`}getDaysBetween(j,Q){let W=new Date(j),V=new Date(Q).getTime()-W.getTime();return Math.floor(V/86400000)}addDays(j,Q){return new Date(j.getTime()+Q*24*60*60*1000)}}var x=new h;import{defineTranslation as L}from"@contractspec/lib.contracts-spec/translations";var B=L({meta:{key:"learning-journey.messages",version:"1.0.0",domain:"learning-journey",description:"XP source labels for the learning-journey module",owners:["platform"],stability:"experimental"},locale:"en",fallback:"en",messages:{"xp.source.base":{value:"Base",description:"XP breakdown label for base XP"},"xp.source.scoreBonus":{value:"Score Bonus",description:"XP breakdown label for score-based bonus"},"xp.source.perfectScore":{value:"Perfect Score",description:"XP breakdown label for perfect score bonus"},"xp.source.firstAttempt":{value:"First Attempt",description:"XP breakdown label for first attempt bonus"},"xp.source.retryPenalty":{value:"Retry Penalty",description:"XP breakdown label for retry penalty"},"xp.source.streakBonus":{value:"Streak Bonus",description:"XP breakdown label for streak bonus"}}});import{defineTranslation as G}from"@contractspec/lib.contracts-spec/translations";var M=G({meta:{key:"learning-journey.messages",version:"1.0.0",domain:"learning-journey",description:"XP source labels (Spanish)",owners:["platform"],stability:"experimental"},locale:"es",fallback:"en",messages:{"xp.source.base":{value:"Base",description:"XP breakdown label for base XP"},"xp.source.scoreBonus":{value:"Bonificaci\xF3n por puntuaci\xF3n",description:"XP breakdown label for score-based bonus"},"xp.source.perfectScore":{value:"Puntuaci\xF3n perfecta",description:"XP breakdown label for perfect score bonus"},"xp.source.firstAttempt":{value:"Primer intento",description:"XP breakdown label for first attempt bonus"},"xp.source.retryPenalty":{value:"Penalizaci\xF3n por reintento",description:"XP breakdown label for retry penalty"},"xp.source.streakBonus":{value:"Bonificaci\xF3n por racha",description:"XP breakdown label for streak bonus"}}});import{defineTranslation as T}from"@contractspec/lib.contracts-spec/translations";var U=T({meta:{key:"learning-journey.messages",version:"1.0.0",domain:"learning-journey",description:"XP source labels (French)",owners:["platform"],stability:"experimental"},locale:"fr",fallback:"en",messages:{"xp.source.base":{value:"Base",description:"XP breakdown label for base XP"},"xp.source.scoreBonus":{value:"Bonus de score",description:"XP breakdown label for score-based bonus"},"xp.source.perfectScore":{value:"Score parfait",description:"XP breakdown label for perfect score bonus"},"xp.source.firstAttempt":{value:"Premier essai",description:"XP breakdown label for first attempt bonus"},"xp.source.retryPenalty":{value:"P\xE9nalit\xE9 de r\xE9essai",description:"XP breakdown label for retry penalty"},"xp.source.streakBonus":{value:"Bonus de s\xE9rie",description:"XP breakdown label for streak bonus"}}});import{createI18nFactory as m}from"@contractspec/lib.contracts-spec/translations";var z=m({specKey:"learning-journey.messages",catalogs:[B,U,M]}),P=z.create,v=z.getDefault,d=z.resetRegistry;var Y={baseValues:{lesson_complete:10,quiz_pass:20,quiz_perfect:50,flashcard_review:1,course_complete:200,module_complete:50,streak_bonus:5,achievement_unlock:0,daily_goal_complete:15,first_lesson:25,onboarding_step:5,onboarding_complete:50},scoreThresholds:[{min:90,multiplier:1.5},{min:80,multiplier:1.25},{min:70,multiplier:1},{min:60,multiplier:0.75},{min:0,multiplier:0.5}],streakTiers:[{days:365,bonus:50},{days:180,bonus:30},{days:90,bonus:20},{days:30,bonus:15},{days:14,bonus:10},{days:7,bonus:5},{days:3,bonus:2},{days:1,bonus:0}],perfectScoreMultiplier:1.5,firstAttemptBonus:10,retryPenalty:0.5,speedBonusMultiplier:1.2,speedBonusThreshold:0.8};class C{config;constructor(j={}){this.config={...Y,...j,baseValues:{...Y.baseValues,...j.baseValues},scoreThresholds:j.scoreThresholds||Y.scoreThresholds,streakTiers:j.streakTiers||Y.streakTiers}}calculate(j){let Q=[],W=j.baseXp??this.config.baseValues[j.activity],H=W;if(Q.push({source:"base",amount:W}),j.score!==void 0){let V=this.getScoreMultiplier(j.score);if(V!==1){let Z=Math.round(W*(V-1));H+=Z,Q.push({source:"score_bonus",amount:Z,multiplier:V})}if(j.score===100){let Z=Math.round(W*(this.config.perfectScoreMultiplier-1));H+=Z,Q.push({source:"perfect_score",amount:Z,multiplier:this.config.perfectScoreMultiplier})}}if(j.attemptNumber===1&&!j.isRetry)H+=this.config.firstAttemptBonus,Q.push({source:"first_attempt",amount:this.config.firstAttemptBonus});if(j.isRetry){let V=Math.round(H*(1-this.config.retryPenalty));H-=V,Q.push({source:"retry_penalty",amount:-V,multiplier:this.config.retryPenalty})}if(j.currentStreak&&j.currentStreak>0){let V=this.getStreakBonus(j.currentStreak);if(V>0)H+=V,Q.push({source:"streak_bonus",amount:V})}if(W>0)H=Math.max(1,H);return{totalXp:Math.round(H),baseXp:W,breakdown:Q}}calculateStreakBonus(j){let Q=this.getStreakBonus(j);return{totalXp:Q,baseXp:Q,breakdown:[{source:"streak_bonus",amount:Q}]}}getXpForLevel(j){if(j<=1)return 0;return Math.round(100*Math.pow(j-1,1.5))}getLevelFromXp(j){let Q=1,W=this.getXpForLevel(Q+1);while(j>=W&&Q<1000)Q++,W=this.getXpForLevel(Q+1);let H=this.getXpForLevel(Q),V=this.getXpForLevel(Q+1);return{level:Q,xpInLevel:j-H,xpForNextLevel:V-H}}getScoreMultiplier(j){for(let Q of this.config.scoreThresholds)if(j>=Q.min)return Q.multiplier;return 1}getStreakBonus(j){for(let Q of this.config.streakTiers)if(j>=Q.days)return Q.bonus;return 0}}var _={base:"xp.source.base",score_bonus:"xp.source.scoreBonus",perfect_score:"xp.source.perfectScore",first_attempt:"xp.source.firstAttempt",retry_penalty:"xp.source.retryPenalty",streak_bonus:"xp.source.streakBonus"};function u(j,Q){let W=P(Q),H=_[j];return H?W.t(H):j}var w=new C;export{w as xpEngine,x as streakEngine,I as srsEngine,u as getXpSourceLabel,C as XPEngine,h as StreakEngine,K as SRSEngine,Y as DEFAULT_XP_CONFIG,O as DEFAULT_STREAK_CONFIG,N as DEFAULT_SRS_CONFIG};