@contractspec/module.learning-journey 3.7.5 → 3.7.7
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.
- package/README.md +65 -188
- package/dist/browser/contracts/index.js +148 -148
- package/dist/browser/contracts/models.js +1 -1
- package/dist/browser/contracts/onboarding.js +5 -5
- package/dist/browser/contracts/operations.js +4 -4
- package/dist/browser/engines/index.js +173 -173
- package/dist/browser/engines/xp.js +18 -18
- package/dist/browser/events.js +1 -1
- package/dist/browser/i18n/catalogs/index.js +18 -18
- package/dist/browser/i18n/index.js +26 -26
- package/dist/browser/i18n/locale.js +2 -2
- package/dist/browser/i18n/messages.js +18 -18
- package/dist/browser/index.js +336 -335
- package/dist/contracts/index.d.ts +2 -2
- package/dist/contracts/index.js +148 -148
- package/dist/contracts/models.js +1 -1
- package/dist/contracts/onboarding.js +5 -5
- package/dist/contracts/operations.js +4 -4
- package/dist/engines/index.d.ts +1 -1
- package/dist/engines/index.js +173 -173
- package/dist/engines/xp.js +18 -18
- package/dist/events.js +1 -1
- package/dist/i18n/catalogs/index.d.ts +1 -1
- package/dist/i18n/catalogs/index.js +18 -18
- package/dist/i18n/index.d.ts +7 -7
- package/dist/i18n/index.js +26 -26
- package/dist/i18n/locale.d.ts +1 -1
- package/dist/i18n/locale.js +2 -2
- package/dist/i18n/messages.js +18 -18
- package/dist/index.d.ts +3 -3
- package/dist/index.js +336 -335
- package/dist/node/contracts/index.js +148 -148
- package/dist/node/contracts/models.js +1 -1
- package/dist/node/contracts/onboarding.js +5 -5
- package/dist/node/contracts/operations.js +4 -4
- package/dist/node/engines/index.js +173 -173
- package/dist/node/engines/xp.js +18 -18
- package/dist/node/events.js +1 -1
- package/dist/node/i18n/catalogs/index.js +18 -18
- package/dist/node/i18n/index.js +26 -26
- package/dist/node/i18n/locale.js +2 -2
- package/dist/node/i18n/messages.js +18 -18
- package/dist/node/index.js +336 -335
- package/package.json +7 -7
package/dist/index.js
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
// src/contracts/shared.ts
|
|
3
|
-
var LEARNING_JOURNEY_OWNERS = ["modules.learning-journey"];
|
|
4
|
-
|
|
5
2
|
// src/contracts/models.ts
|
|
6
|
-
import {
|
|
3
|
+
import { defineSchemaModel, ScalarTypeEnum } from "@contractspec/lib.schema";
|
|
7
4
|
var CourseModel = defineSchemaModel({
|
|
8
5
|
name: "Course",
|
|
9
6
|
description: "A learning course",
|
|
@@ -179,149 +176,12 @@ var SuccessOutput = defineSchemaModel({
|
|
|
179
176
|
}
|
|
180
177
|
});
|
|
181
178
|
|
|
182
|
-
// src/contracts/
|
|
183
|
-
|
|
184
|
-
var EnrollInCourseContract = defineCommand({
|
|
185
|
-
meta: {
|
|
186
|
-
key: "learning.enroll",
|
|
187
|
-
version: "1.0.0",
|
|
188
|
-
stability: "stable",
|
|
189
|
-
owners: [...LEARNING_JOURNEY_OWNERS],
|
|
190
|
-
tags: ["learning", "enrollment"],
|
|
191
|
-
description: "Enroll in a course.",
|
|
192
|
-
goal: "Start learning a new course.",
|
|
193
|
-
context: "Called when a learner wants to start a course."
|
|
194
|
-
},
|
|
195
|
-
io: {
|
|
196
|
-
input: EnrollInCourseInput,
|
|
197
|
-
output: EnrollmentModel,
|
|
198
|
-
errors: {
|
|
199
|
-
COURSE_NOT_FOUND: {
|
|
200
|
-
description: "Course does not exist",
|
|
201
|
-
http: 404,
|
|
202
|
-
gqlCode: "COURSE_NOT_FOUND",
|
|
203
|
-
when: "Course ID is invalid"
|
|
204
|
-
},
|
|
205
|
-
ALREADY_ENROLLED: {
|
|
206
|
-
description: "Already enrolled in course",
|
|
207
|
-
http: 409,
|
|
208
|
-
gqlCode: "ALREADY_ENROLLED",
|
|
209
|
-
when: "Learner is already enrolled"
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
},
|
|
213
|
-
policy: {
|
|
214
|
-
auth: "user"
|
|
215
|
-
}
|
|
216
|
-
});
|
|
217
|
-
var CompleteLessonContract = defineCommand({
|
|
218
|
-
meta: {
|
|
219
|
-
key: "learning.completeLesson",
|
|
220
|
-
version: "1.0.0",
|
|
221
|
-
stability: "stable",
|
|
222
|
-
owners: [...LEARNING_JOURNEY_OWNERS],
|
|
223
|
-
tags: ["learning", "progress"],
|
|
224
|
-
description: "Mark a lesson as completed.",
|
|
225
|
-
goal: "Record lesson completion and earn XP.",
|
|
226
|
-
context: "Called when a learner finishes a lesson."
|
|
227
|
-
},
|
|
228
|
-
io: {
|
|
229
|
-
input: CompleteLessonInput,
|
|
230
|
-
output: SuccessOutput,
|
|
231
|
-
errors: {
|
|
232
|
-
LESSON_NOT_FOUND: {
|
|
233
|
-
description: "Lesson does not exist",
|
|
234
|
-
http: 404,
|
|
235
|
-
gqlCode: "LESSON_NOT_FOUND",
|
|
236
|
-
when: "Lesson ID is invalid"
|
|
237
|
-
},
|
|
238
|
-
NOT_ENROLLED: {
|
|
239
|
-
description: "Not enrolled in course",
|
|
240
|
-
http: 403,
|
|
241
|
-
gqlCode: "NOT_ENROLLED",
|
|
242
|
-
when: "Learner is not enrolled in the course"
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
},
|
|
246
|
-
policy: {
|
|
247
|
-
auth: "user"
|
|
248
|
-
}
|
|
249
|
-
});
|
|
250
|
-
var SubmitCardReviewContract = defineCommand({
|
|
251
|
-
meta: {
|
|
252
|
-
key: "learning.submitCardReview",
|
|
253
|
-
version: "1.0.0",
|
|
254
|
-
stability: "stable",
|
|
255
|
-
owners: [...LEARNING_JOURNEY_OWNERS],
|
|
256
|
-
tags: ["learning", "flashcards"],
|
|
257
|
-
description: "Submit a flashcard review.",
|
|
258
|
-
goal: "Record review and update SRS schedule.",
|
|
259
|
-
context: "Called when reviewing flashcards."
|
|
260
|
-
},
|
|
261
|
-
io: {
|
|
262
|
-
input: SubmitCardReviewInput,
|
|
263
|
-
output: SuccessOutput,
|
|
264
|
-
errors: {
|
|
265
|
-
CARD_NOT_FOUND: {
|
|
266
|
-
description: "Card does not exist",
|
|
267
|
-
http: 404,
|
|
268
|
-
gqlCode: "CARD_NOT_FOUND",
|
|
269
|
-
when: "Card ID is invalid"
|
|
270
|
-
},
|
|
271
|
-
INVALID_RATING: {
|
|
272
|
-
description: "Invalid rating",
|
|
273
|
-
http: 400,
|
|
274
|
-
gqlCode: "INVALID_RATING",
|
|
275
|
-
when: "Rating must be AGAIN, HARD, GOOD, or EASY"
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
},
|
|
279
|
-
policy: {
|
|
280
|
-
auth: "user"
|
|
281
|
-
}
|
|
282
|
-
});
|
|
283
|
-
var GetDueCardsContract = defineQuery({
|
|
284
|
-
meta: {
|
|
285
|
-
key: "learning.getDueCards",
|
|
286
|
-
version: "1.0.0",
|
|
287
|
-
stability: "stable",
|
|
288
|
-
owners: [...LEARNING_JOURNEY_OWNERS],
|
|
289
|
-
tags: ["learning", "flashcards"],
|
|
290
|
-
description: "Get flashcards due for review.",
|
|
291
|
-
goal: "Get the next batch of cards to review.",
|
|
292
|
-
context: "Called when starting a review session."
|
|
293
|
-
},
|
|
294
|
-
io: {
|
|
295
|
-
input: GetDueCardsInput,
|
|
296
|
-
output: GetDueCardsOutput
|
|
297
|
-
},
|
|
298
|
-
policy: {
|
|
299
|
-
auth: "user"
|
|
300
|
-
}
|
|
301
|
-
});
|
|
302
|
-
var GetLearnerDashboardContract = defineQuery({
|
|
303
|
-
meta: {
|
|
304
|
-
key: "learning.getDashboard",
|
|
305
|
-
version: "1.0.0",
|
|
306
|
-
stability: "stable",
|
|
307
|
-
owners: [...LEARNING_JOURNEY_OWNERS],
|
|
308
|
-
tags: ["learning", "dashboard"],
|
|
309
|
-
description: "Get learner dashboard data.",
|
|
310
|
-
goal: "Display learner progress and stats.",
|
|
311
|
-
context: "Called when viewing the learning dashboard."
|
|
312
|
-
},
|
|
313
|
-
io: {
|
|
314
|
-
input: GetLearnerDashboardInput,
|
|
315
|
-
output: LearnerDashboardModel
|
|
316
|
-
},
|
|
317
|
-
policy: {
|
|
318
|
-
auth: "user"
|
|
319
|
-
}
|
|
320
|
-
});
|
|
179
|
+
// src/contracts/shared.ts
|
|
180
|
+
var LEARNING_JOURNEY_OWNERS = ["modules.learning-journey"];
|
|
321
181
|
|
|
322
182
|
// src/contracts/onboarding.ts
|
|
323
|
-
import {
|
|
324
|
-
import {
|
|
183
|
+
import { defineCommand, defineQuery } from "@contractspec/lib.contracts-spec";
|
|
184
|
+
import { defineSchemaModel as defineSchemaModel2, ScalarTypeEnum as ScalarTypeEnum2 } from "@contractspec/lib.schema";
|
|
325
185
|
var OnboardingStepConditionModel = defineSchemaModel2({
|
|
326
186
|
name: "OnboardingStepCondition",
|
|
327
187
|
description: "Structured completion condition for onboarding steps.",
|
|
@@ -476,7 +336,7 @@ var RecordOnboardingEventInput = defineSchemaModel2({
|
|
|
476
336
|
occurredAt: { type: ScalarTypeEnum2.DateTime(), isOptional: true }
|
|
477
337
|
}
|
|
478
338
|
});
|
|
479
|
-
var ListOnboardingTracksContract =
|
|
339
|
+
var ListOnboardingTracksContract = defineQuery({
|
|
480
340
|
meta: {
|
|
481
341
|
key: "learning.onboarding.listTracks",
|
|
482
342
|
version: "1.0.0",
|
|
@@ -495,7 +355,7 @@ var ListOnboardingTracksContract = defineQuery2({
|
|
|
495
355
|
auth: "user"
|
|
496
356
|
}
|
|
497
357
|
});
|
|
498
|
-
var GetOnboardingProgressContract =
|
|
358
|
+
var GetOnboardingProgressContract = defineQuery({
|
|
499
359
|
meta: {
|
|
500
360
|
key: "learning.onboarding.getProgress",
|
|
501
361
|
version: "1.0.0",
|
|
@@ -514,7 +374,7 @@ var GetOnboardingProgressContract = defineQuery2({
|
|
|
514
374
|
auth: "user"
|
|
515
375
|
}
|
|
516
376
|
});
|
|
517
|
-
var RecordOnboardingEventContract =
|
|
377
|
+
var RecordOnboardingEventContract = defineCommand({
|
|
518
378
|
meta: {
|
|
519
379
|
key: "learning.onboarding.recordEvent",
|
|
520
380
|
version: "1.0.0",
|
|
@@ -547,6 +407,146 @@ var RecordOnboardingEventContract = defineCommand2({
|
|
|
547
407
|
auth: "user"
|
|
548
408
|
}
|
|
549
409
|
});
|
|
410
|
+
|
|
411
|
+
// src/contracts/operations.ts
|
|
412
|
+
import { defineCommand as defineCommand2, defineQuery as defineQuery2 } from "@contractspec/lib.contracts-spec";
|
|
413
|
+
var EnrollInCourseContract = defineCommand2({
|
|
414
|
+
meta: {
|
|
415
|
+
key: "learning.enroll",
|
|
416
|
+
version: "1.0.0",
|
|
417
|
+
stability: "stable",
|
|
418
|
+
owners: [...LEARNING_JOURNEY_OWNERS],
|
|
419
|
+
tags: ["learning", "enrollment"],
|
|
420
|
+
description: "Enroll in a course.",
|
|
421
|
+
goal: "Start learning a new course.",
|
|
422
|
+
context: "Called when a learner wants to start a course."
|
|
423
|
+
},
|
|
424
|
+
io: {
|
|
425
|
+
input: EnrollInCourseInput,
|
|
426
|
+
output: EnrollmentModel,
|
|
427
|
+
errors: {
|
|
428
|
+
COURSE_NOT_FOUND: {
|
|
429
|
+
description: "Course does not exist",
|
|
430
|
+
http: 404,
|
|
431
|
+
gqlCode: "COURSE_NOT_FOUND",
|
|
432
|
+
when: "Course ID is invalid"
|
|
433
|
+
},
|
|
434
|
+
ALREADY_ENROLLED: {
|
|
435
|
+
description: "Already enrolled in course",
|
|
436
|
+
http: 409,
|
|
437
|
+
gqlCode: "ALREADY_ENROLLED",
|
|
438
|
+
when: "Learner is already enrolled"
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
},
|
|
442
|
+
policy: {
|
|
443
|
+
auth: "user"
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
var CompleteLessonContract = defineCommand2({
|
|
447
|
+
meta: {
|
|
448
|
+
key: "learning.completeLesson",
|
|
449
|
+
version: "1.0.0",
|
|
450
|
+
stability: "stable",
|
|
451
|
+
owners: [...LEARNING_JOURNEY_OWNERS],
|
|
452
|
+
tags: ["learning", "progress"],
|
|
453
|
+
description: "Mark a lesson as completed.",
|
|
454
|
+
goal: "Record lesson completion and earn XP.",
|
|
455
|
+
context: "Called when a learner finishes a lesson."
|
|
456
|
+
},
|
|
457
|
+
io: {
|
|
458
|
+
input: CompleteLessonInput,
|
|
459
|
+
output: SuccessOutput,
|
|
460
|
+
errors: {
|
|
461
|
+
LESSON_NOT_FOUND: {
|
|
462
|
+
description: "Lesson does not exist",
|
|
463
|
+
http: 404,
|
|
464
|
+
gqlCode: "LESSON_NOT_FOUND",
|
|
465
|
+
when: "Lesson ID is invalid"
|
|
466
|
+
},
|
|
467
|
+
NOT_ENROLLED: {
|
|
468
|
+
description: "Not enrolled in course",
|
|
469
|
+
http: 403,
|
|
470
|
+
gqlCode: "NOT_ENROLLED",
|
|
471
|
+
when: "Learner is not enrolled in the course"
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
},
|
|
475
|
+
policy: {
|
|
476
|
+
auth: "user"
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
var SubmitCardReviewContract = defineCommand2({
|
|
480
|
+
meta: {
|
|
481
|
+
key: "learning.submitCardReview",
|
|
482
|
+
version: "1.0.0",
|
|
483
|
+
stability: "stable",
|
|
484
|
+
owners: [...LEARNING_JOURNEY_OWNERS],
|
|
485
|
+
tags: ["learning", "flashcards"],
|
|
486
|
+
description: "Submit a flashcard review.",
|
|
487
|
+
goal: "Record review and update SRS schedule.",
|
|
488
|
+
context: "Called when reviewing flashcards."
|
|
489
|
+
},
|
|
490
|
+
io: {
|
|
491
|
+
input: SubmitCardReviewInput,
|
|
492
|
+
output: SuccessOutput,
|
|
493
|
+
errors: {
|
|
494
|
+
CARD_NOT_FOUND: {
|
|
495
|
+
description: "Card does not exist",
|
|
496
|
+
http: 404,
|
|
497
|
+
gqlCode: "CARD_NOT_FOUND",
|
|
498
|
+
when: "Card ID is invalid"
|
|
499
|
+
},
|
|
500
|
+
INVALID_RATING: {
|
|
501
|
+
description: "Invalid rating",
|
|
502
|
+
http: 400,
|
|
503
|
+
gqlCode: "INVALID_RATING",
|
|
504
|
+
when: "Rating must be AGAIN, HARD, GOOD, or EASY"
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
},
|
|
508
|
+
policy: {
|
|
509
|
+
auth: "user"
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
var GetDueCardsContract = defineQuery2({
|
|
513
|
+
meta: {
|
|
514
|
+
key: "learning.getDueCards",
|
|
515
|
+
version: "1.0.0",
|
|
516
|
+
stability: "stable",
|
|
517
|
+
owners: [...LEARNING_JOURNEY_OWNERS],
|
|
518
|
+
tags: ["learning", "flashcards"],
|
|
519
|
+
description: "Get flashcards due for review.",
|
|
520
|
+
goal: "Get the next batch of cards to review.",
|
|
521
|
+
context: "Called when starting a review session."
|
|
522
|
+
},
|
|
523
|
+
io: {
|
|
524
|
+
input: GetDueCardsInput,
|
|
525
|
+
output: GetDueCardsOutput
|
|
526
|
+
},
|
|
527
|
+
policy: {
|
|
528
|
+
auth: "user"
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
var GetLearnerDashboardContract = defineQuery2({
|
|
532
|
+
meta: {
|
|
533
|
+
key: "learning.getDashboard",
|
|
534
|
+
version: "1.0.0",
|
|
535
|
+
stability: "stable",
|
|
536
|
+
owners: [...LEARNING_JOURNEY_OWNERS],
|
|
537
|
+
tags: ["learning", "dashboard"],
|
|
538
|
+
description: "Get learner dashboard data.",
|
|
539
|
+
goal: "Display learner progress and stats.",
|
|
540
|
+
context: "Called when viewing the learning dashboard."
|
|
541
|
+
},
|
|
542
|
+
io: {
|
|
543
|
+
input: GetLearnerDashboardInput,
|
|
544
|
+
output: LearnerDashboardModel
|
|
545
|
+
},
|
|
546
|
+
policy: {
|
|
547
|
+
auth: "user"
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
550
|
// src/docs/learning-journey.docblock.ts
|
|
551
551
|
import { registerDocBlocks } from "@contractspec/lib.contracts-spec/docs";
|
|
552
552
|
var learningJourneyDocBlocks = [
|
|
@@ -843,27 +843,182 @@ class SRSEngine {
|
|
|
843
843
|
repetitions++;
|
|
844
844
|
break;
|
|
845
845
|
}
|
|
846
|
-
newInterval = Math.min(Math.round(newInterval), this.config.maxInterval);
|
|
847
|
-
newInterval = Math.max(1, newInterval);
|
|
846
|
+
newInterval = Math.min(Math.round(newInterval), this.config.maxInterval);
|
|
847
|
+
newInterval = Math.max(1, newInterval);
|
|
848
|
+
return {
|
|
849
|
+
interval: newInterval,
|
|
850
|
+
easeFactor: newEaseFactor,
|
|
851
|
+
repetitions,
|
|
852
|
+
nextReviewAt: this.addDays(now, newInterval),
|
|
853
|
+
learningStep,
|
|
854
|
+
isGraduated: true,
|
|
855
|
+
isRelearning,
|
|
856
|
+
lapses
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
addMinutes(date, minutes) {
|
|
860
|
+
return new Date(date.getTime() + minutes * 60 * 1000);
|
|
861
|
+
}
|
|
862
|
+
addDays(date, days) {
|
|
863
|
+
return new Date(date.getTime() + days * 24 * 60 * 60 * 1000);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
var srsEngine = new SRSEngine;
|
|
867
|
+
|
|
868
|
+
// src/engines/streak.ts
|
|
869
|
+
var DEFAULT_STREAK_CONFIG = {
|
|
870
|
+
timezone: "UTC",
|
|
871
|
+
freezesPerMonth: 2,
|
|
872
|
+
maxFreezes: 5,
|
|
873
|
+
gracePeriodHours: 4
|
|
874
|
+
};
|
|
875
|
+
|
|
876
|
+
class StreakEngine {
|
|
877
|
+
config;
|
|
878
|
+
constructor(config = {}) {
|
|
879
|
+
this.config = { ...DEFAULT_STREAK_CONFIG, ...config };
|
|
880
|
+
}
|
|
881
|
+
update(state, now = new Date) {
|
|
882
|
+
const todayDate = this.getDateString(now);
|
|
883
|
+
const result = {
|
|
884
|
+
state: { ...state },
|
|
885
|
+
streakMaintained: false,
|
|
886
|
+
streakLost: false,
|
|
887
|
+
freezeUsed: false,
|
|
888
|
+
newStreak: false,
|
|
889
|
+
daysMissed: 0
|
|
890
|
+
};
|
|
891
|
+
if (!state.lastActivityDate) {
|
|
892
|
+
result.state.currentStreak = 1;
|
|
893
|
+
result.state.longestStreak = Math.max(1, state.longestStreak);
|
|
894
|
+
result.state.lastActivityAt = now;
|
|
895
|
+
result.state.lastActivityDate = todayDate;
|
|
896
|
+
result.newStreak = true;
|
|
897
|
+
result.streakMaintained = true;
|
|
898
|
+
return result;
|
|
899
|
+
}
|
|
900
|
+
if (state.lastActivityDate === todayDate) {
|
|
901
|
+
result.state.lastActivityAt = now;
|
|
902
|
+
result.streakMaintained = true;
|
|
903
|
+
return result;
|
|
904
|
+
}
|
|
905
|
+
const daysSinceActivity = this.getDaysBetween(state.lastActivityDate, todayDate);
|
|
906
|
+
if (daysSinceActivity === 1) {
|
|
907
|
+
result.state.currentStreak = state.currentStreak + 1;
|
|
908
|
+
result.state.longestStreak = Math.max(result.state.currentStreak, state.longestStreak);
|
|
909
|
+
result.state.lastActivityAt = now;
|
|
910
|
+
result.state.lastActivityDate = todayDate;
|
|
911
|
+
result.streakMaintained = true;
|
|
912
|
+
return result;
|
|
913
|
+
}
|
|
914
|
+
result.daysMissed = daysSinceActivity - 1;
|
|
915
|
+
const freezesNeeded = result.daysMissed;
|
|
916
|
+
if (freezesNeeded <= state.freezesRemaining) {
|
|
917
|
+
result.state.freezesRemaining = state.freezesRemaining - freezesNeeded;
|
|
918
|
+
result.state.freezeUsedAt = now;
|
|
919
|
+
result.state.currentStreak = state.currentStreak + 1;
|
|
920
|
+
result.state.longestStreak = Math.max(result.state.currentStreak, state.longestStreak);
|
|
921
|
+
result.state.lastActivityAt = now;
|
|
922
|
+
result.state.lastActivityDate = todayDate;
|
|
923
|
+
result.freezeUsed = true;
|
|
924
|
+
result.streakMaintained = true;
|
|
925
|
+
return result;
|
|
926
|
+
}
|
|
927
|
+
result.streakLost = true;
|
|
928
|
+
result.state.currentStreak = 1;
|
|
929
|
+
result.state.lastActivityAt = now;
|
|
930
|
+
result.state.lastActivityDate = todayDate;
|
|
931
|
+
result.newStreak = true;
|
|
932
|
+
return result;
|
|
933
|
+
}
|
|
934
|
+
checkStatus(state, now = new Date) {
|
|
935
|
+
if (!state.lastActivityDate) {
|
|
936
|
+
return {
|
|
937
|
+
isActive: false,
|
|
938
|
+
willExpireAt: null,
|
|
939
|
+
canUseFreeze: false,
|
|
940
|
+
daysUntilExpiry: 0
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
const todayDate = this.getDateString(now);
|
|
944
|
+
const daysSinceActivity = this.getDaysBetween(state.lastActivityDate, todayDate);
|
|
945
|
+
if (daysSinceActivity === 0) {
|
|
946
|
+
const tomorrow = this.addDays(now, 1);
|
|
947
|
+
tomorrow.setHours(23, 59, 59, 999);
|
|
948
|
+
return {
|
|
949
|
+
isActive: true,
|
|
950
|
+
willExpireAt: tomorrow,
|
|
951
|
+
canUseFreeze: state.freezesRemaining > 0,
|
|
952
|
+
daysUntilExpiry: 1
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
if (daysSinceActivity === 1) {
|
|
956
|
+
const endOfDay = new Date(now);
|
|
957
|
+
endOfDay.setHours(23 + this.config.gracePeriodHours, 59, 59, 999);
|
|
958
|
+
return {
|
|
959
|
+
isActive: true,
|
|
960
|
+
willExpireAt: endOfDay,
|
|
961
|
+
canUseFreeze: state.freezesRemaining > 0,
|
|
962
|
+
daysUntilExpiry: 0
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
const missedDays = daysSinceActivity - 1;
|
|
848
966
|
return {
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
learningStep,
|
|
854
|
-
isGraduated: true,
|
|
855
|
-
isRelearning,
|
|
856
|
-
lapses
|
|
967
|
+
isActive: missedDays <= state.freezesRemaining,
|
|
968
|
+
willExpireAt: null,
|
|
969
|
+
canUseFreeze: missedDays <= state.freezesRemaining,
|
|
970
|
+
daysUntilExpiry: -missedDays
|
|
857
971
|
};
|
|
858
972
|
}
|
|
859
|
-
|
|
860
|
-
|
|
973
|
+
useFreeze(state, now = new Date) {
|
|
974
|
+
if (state.freezesRemaining <= 0) {
|
|
975
|
+
return null;
|
|
976
|
+
}
|
|
977
|
+
return {
|
|
978
|
+
...state,
|
|
979
|
+
freezesRemaining: state.freezesRemaining - 1,
|
|
980
|
+
freezeUsedAt: now
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
awardMonthlyFreezes(state) {
|
|
984
|
+
return {
|
|
985
|
+
...state,
|
|
986
|
+
freezesRemaining: Math.min(state.freezesRemaining + this.config.freezesPerMonth, this.config.maxFreezes)
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
getInitialState() {
|
|
990
|
+
return {
|
|
991
|
+
currentStreak: 0,
|
|
992
|
+
longestStreak: 0,
|
|
993
|
+
lastActivityAt: null,
|
|
994
|
+
lastActivityDate: null,
|
|
995
|
+
freezesRemaining: this.config.freezesPerMonth,
|
|
996
|
+
freezeUsedAt: null
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
getMilestones(currentStreak) {
|
|
1000
|
+
const milestones = [3, 7, 14, 30, 60, 90, 180, 365, 500, 1000];
|
|
1001
|
+
const achieved = milestones.filter((m) => currentStreak >= m);
|
|
1002
|
+
const next = milestones.find((m) => currentStreak < m) ?? null;
|
|
1003
|
+
return { achieved, next };
|
|
1004
|
+
}
|
|
1005
|
+
getDateString(date) {
|
|
1006
|
+
const year = date.getFullYear();
|
|
1007
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
1008
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
1009
|
+
return `${year}-${month}-${day}`;
|
|
1010
|
+
}
|
|
1011
|
+
getDaysBetween(dateStr1, dateStr2) {
|
|
1012
|
+
const date1 = new Date(dateStr1);
|
|
1013
|
+
const date2 = new Date(dateStr2);
|
|
1014
|
+
const diffTime = date2.getTime() - date1.getTime();
|
|
1015
|
+
return Math.floor(diffTime / (1000 * 60 * 60 * 24));
|
|
861
1016
|
}
|
|
862
1017
|
addDays(date, days) {
|
|
863
1018
|
return new Date(date.getTime() + days * 24 * 60 * 60 * 1000);
|
|
864
1019
|
}
|
|
865
1020
|
}
|
|
866
|
-
var
|
|
1021
|
+
var streakEngine = new StreakEngine;
|
|
867
1022
|
|
|
868
1023
|
// src/i18n/catalogs/en.ts
|
|
869
1024
|
import { defineTranslation } from "@contractspec/lib.contracts-spec/translations";
|
|
@@ -906,18 +1061,18 @@ var enMessages = defineTranslation({
|
|
|
906
1061
|
}
|
|
907
1062
|
});
|
|
908
1063
|
|
|
909
|
-
// src/i18n/catalogs/
|
|
1064
|
+
// src/i18n/catalogs/es.ts
|
|
910
1065
|
import { defineTranslation as defineTranslation2 } from "@contractspec/lib.contracts-spec/translations";
|
|
911
|
-
var
|
|
1066
|
+
var esMessages = defineTranslation2({
|
|
912
1067
|
meta: {
|
|
913
1068
|
key: "learning-journey.messages",
|
|
914
1069
|
version: "1.0.0",
|
|
915
1070
|
domain: "learning-journey",
|
|
916
|
-
description: "XP source labels (
|
|
1071
|
+
description: "XP source labels (Spanish)",
|
|
917
1072
|
owners: ["platform"],
|
|
918
1073
|
stability: "experimental"
|
|
919
1074
|
},
|
|
920
|
-
locale: "
|
|
1075
|
+
locale: "es",
|
|
921
1076
|
fallback: "en",
|
|
922
1077
|
messages: {
|
|
923
1078
|
"xp.source.base": {
|
|
@@ -925,40 +1080,40 @@ var frMessages = defineTranslation2({
|
|
|
925
1080
|
description: "XP breakdown label for base XP"
|
|
926
1081
|
},
|
|
927
1082
|
"xp.source.scoreBonus": {
|
|
928
|
-
value: "
|
|
1083
|
+
value: "Bonificaci\xF3n por puntuaci\xF3n",
|
|
929
1084
|
description: "XP breakdown label for score-based bonus"
|
|
930
1085
|
},
|
|
931
1086
|
"xp.source.perfectScore": {
|
|
932
|
-
value: "
|
|
1087
|
+
value: "Puntuaci\xF3n perfecta",
|
|
933
1088
|
description: "XP breakdown label for perfect score bonus"
|
|
934
1089
|
},
|
|
935
1090
|
"xp.source.firstAttempt": {
|
|
936
|
-
value: "
|
|
1091
|
+
value: "Primer intento",
|
|
937
1092
|
description: "XP breakdown label for first attempt bonus"
|
|
938
1093
|
},
|
|
939
1094
|
"xp.source.retryPenalty": {
|
|
940
|
-
value: "
|
|
1095
|
+
value: "Penalizaci\xF3n por reintento",
|
|
941
1096
|
description: "XP breakdown label for retry penalty"
|
|
942
1097
|
},
|
|
943
1098
|
"xp.source.streakBonus": {
|
|
944
|
-
value: "
|
|
1099
|
+
value: "Bonificaci\xF3n por racha",
|
|
945
1100
|
description: "XP breakdown label for streak bonus"
|
|
946
1101
|
}
|
|
947
1102
|
}
|
|
948
1103
|
});
|
|
949
1104
|
|
|
950
|
-
// src/i18n/catalogs/
|
|
1105
|
+
// src/i18n/catalogs/fr.ts
|
|
951
1106
|
import { defineTranslation as defineTranslation3 } from "@contractspec/lib.contracts-spec/translations";
|
|
952
|
-
var
|
|
1107
|
+
var frMessages = defineTranslation3({
|
|
953
1108
|
meta: {
|
|
954
1109
|
key: "learning-journey.messages",
|
|
955
1110
|
version: "1.0.0",
|
|
956
1111
|
domain: "learning-journey",
|
|
957
|
-
description: "XP source labels (
|
|
1112
|
+
description: "XP source labels (French)",
|
|
958
1113
|
owners: ["platform"],
|
|
959
1114
|
stability: "experimental"
|
|
960
1115
|
},
|
|
961
|
-
locale: "
|
|
1116
|
+
locale: "fr",
|
|
962
1117
|
fallback: "en",
|
|
963
1118
|
messages: {
|
|
964
1119
|
"xp.source.base": {
|
|
@@ -966,23 +1121,23 @@ var esMessages = defineTranslation3({
|
|
|
966
1121
|
description: "XP breakdown label for base XP"
|
|
967
1122
|
},
|
|
968
1123
|
"xp.source.scoreBonus": {
|
|
969
|
-
value: "
|
|
1124
|
+
value: "Bonus de score",
|
|
970
1125
|
description: "XP breakdown label for score-based bonus"
|
|
971
1126
|
},
|
|
972
1127
|
"xp.source.perfectScore": {
|
|
973
|
-
value: "
|
|
1128
|
+
value: "Score parfait",
|
|
974
1129
|
description: "XP breakdown label for perfect score bonus"
|
|
975
1130
|
},
|
|
976
1131
|
"xp.source.firstAttempt": {
|
|
977
|
-
value: "
|
|
1132
|
+
value: "Premier essai",
|
|
978
1133
|
description: "XP breakdown label for first attempt bonus"
|
|
979
1134
|
},
|
|
980
1135
|
"xp.source.retryPenalty": {
|
|
981
|
-
value: "
|
|
1136
|
+
value: "P\xE9nalit\xE9 de r\xE9essai",
|
|
982
1137
|
description: "XP breakdown label for retry penalty"
|
|
983
1138
|
},
|
|
984
1139
|
"xp.source.streakBonus": {
|
|
985
|
-
value: "
|
|
1140
|
+
value: "Bonus de s\xE9rie",
|
|
986
1141
|
description: "XP breakdown label for streak bonus"
|
|
987
1142
|
}
|
|
988
1143
|
}
|
|
@@ -1179,161 +1334,6 @@ function getXpSourceLabel(source, locale) {
|
|
|
1179
1334
|
return i18nKey ? i18n.t(i18nKey) : source;
|
|
1180
1335
|
}
|
|
1181
1336
|
var xpEngine = new XPEngine;
|
|
1182
|
-
|
|
1183
|
-
// src/engines/streak.ts
|
|
1184
|
-
var DEFAULT_STREAK_CONFIG = {
|
|
1185
|
-
timezone: "UTC",
|
|
1186
|
-
freezesPerMonth: 2,
|
|
1187
|
-
maxFreezes: 5,
|
|
1188
|
-
gracePeriodHours: 4
|
|
1189
|
-
};
|
|
1190
|
-
|
|
1191
|
-
class StreakEngine {
|
|
1192
|
-
config;
|
|
1193
|
-
constructor(config = {}) {
|
|
1194
|
-
this.config = { ...DEFAULT_STREAK_CONFIG, ...config };
|
|
1195
|
-
}
|
|
1196
|
-
update(state, now = new Date) {
|
|
1197
|
-
const todayDate = this.getDateString(now);
|
|
1198
|
-
const result = {
|
|
1199
|
-
state: { ...state },
|
|
1200
|
-
streakMaintained: false,
|
|
1201
|
-
streakLost: false,
|
|
1202
|
-
freezeUsed: false,
|
|
1203
|
-
newStreak: false,
|
|
1204
|
-
daysMissed: 0
|
|
1205
|
-
};
|
|
1206
|
-
if (!state.lastActivityDate) {
|
|
1207
|
-
result.state.currentStreak = 1;
|
|
1208
|
-
result.state.longestStreak = Math.max(1, state.longestStreak);
|
|
1209
|
-
result.state.lastActivityAt = now;
|
|
1210
|
-
result.state.lastActivityDate = todayDate;
|
|
1211
|
-
result.newStreak = true;
|
|
1212
|
-
result.streakMaintained = true;
|
|
1213
|
-
return result;
|
|
1214
|
-
}
|
|
1215
|
-
if (state.lastActivityDate === todayDate) {
|
|
1216
|
-
result.state.lastActivityAt = now;
|
|
1217
|
-
result.streakMaintained = true;
|
|
1218
|
-
return result;
|
|
1219
|
-
}
|
|
1220
|
-
const daysSinceActivity = this.getDaysBetween(state.lastActivityDate, todayDate);
|
|
1221
|
-
if (daysSinceActivity === 1) {
|
|
1222
|
-
result.state.currentStreak = state.currentStreak + 1;
|
|
1223
|
-
result.state.longestStreak = Math.max(result.state.currentStreak, state.longestStreak);
|
|
1224
|
-
result.state.lastActivityAt = now;
|
|
1225
|
-
result.state.lastActivityDate = todayDate;
|
|
1226
|
-
result.streakMaintained = true;
|
|
1227
|
-
return result;
|
|
1228
|
-
}
|
|
1229
|
-
result.daysMissed = daysSinceActivity - 1;
|
|
1230
|
-
const freezesNeeded = result.daysMissed;
|
|
1231
|
-
if (freezesNeeded <= state.freezesRemaining) {
|
|
1232
|
-
result.state.freezesRemaining = state.freezesRemaining - freezesNeeded;
|
|
1233
|
-
result.state.freezeUsedAt = now;
|
|
1234
|
-
result.state.currentStreak = state.currentStreak + 1;
|
|
1235
|
-
result.state.longestStreak = Math.max(result.state.currentStreak, state.longestStreak);
|
|
1236
|
-
result.state.lastActivityAt = now;
|
|
1237
|
-
result.state.lastActivityDate = todayDate;
|
|
1238
|
-
result.freezeUsed = true;
|
|
1239
|
-
result.streakMaintained = true;
|
|
1240
|
-
return result;
|
|
1241
|
-
}
|
|
1242
|
-
result.streakLost = true;
|
|
1243
|
-
result.state.currentStreak = 1;
|
|
1244
|
-
result.state.lastActivityAt = now;
|
|
1245
|
-
result.state.lastActivityDate = todayDate;
|
|
1246
|
-
result.newStreak = true;
|
|
1247
|
-
return result;
|
|
1248
|
-
}
|
|
1249
|
-
checkStatus(state, now = new Date) {
|
|
1250
|
-
if (!state.lastActivityDate) {
|
|
1251
|
-
return {
|
|
1252
|
-
isActive: false,
|
|
1253
|
-
willExpireAt: null,
|
|
1254
|
-
canUseFreeze: false,
|
|
1255
|
-
daysUntilExpiry: 0
|
|
1256
|
-
};
|
|
1257
|
-
}
|
|
1258
|
-
const todayDate = this.getDateString(now);
|
|
1259
|
-
const daysSinceActivity = this.getDaysBetween(state.lastActivityDate, todayDate);
|
|
1260
|
-
if (daysSinceActivity === 0) {
|
|
1261
|
-
const tomorrow = this.addDays(now, 1);
|
|
1262
|
-
tomorrow.setHours(23, 59, 59, 999);
|
|
1263
|
-
return {
|
|
1264
|
-
isActive: true,
|
|
1265
|
-
willExpireAt: tomorrow,
|
|
1266
|
-
canUseFreeze: state.freezesRemaining > 0,
|
|
1267
|
-
daysUntilExpiry: 1
|
|
1268
|
-
};
|
|
1269
|
-
}
|
|
1270
|
-
if (daysSinceActivity === 1) {
|
|
1271
|
-
const endOfDay = new Date(now);
|
|
1272
|
-
endOfDay.setHours(23 + this.config.gracePeriodHours, 59, 59, 999);
|
|
1273
|
-
return {
|
|
1274
|
-
isActive: true,
|
|
1275
|
-
willExpireAt: endOfDay,
|
|
1276
|
-
canUseFreeze: state.freezesRemaining > 0,
|
|
1277
|
-
daysUntilExpiry: 0
|
|
1278
|
-
};
|
|
1279
|
-
}
|
|
1280
|
-
const missedDays = daysSinceActivity - 1;
|
|
1281
|
-
return {
|
|
1282
|
-
isActive: missedDays <= state.freezesRemaining,
|
|
1283
|
-
willExpireAt: null,
|
|
1284
|
-
canUseFreeze: missedDays <= state.freezesRemaining,
|
|
1285
|
-
daysUntilExpiry: -missedDays
|
|
1286
|
-
};
|
|
1287
|
-
}
|
|
1288
|
-
useFreeze(state, now = new Date) {
|
|
1289
|
-
if (state.freezesRemaining <= 0) {
|
|
1290
|
-
return null;
|
|
1291
|
-
}
|
|
1292
|
-
return {
|
|
1293
|
-
...state,
|
|
1294
|
-
freezesRemaining: state.freezesRemaining - 1,
|
|
1295
|
-
freezeUsedAt: now
|
|
1296
|
-
};
|
|
1297
|
-
}
|
|
1298
|
-
awardMonthlyFreezes(state) {
|
|
1299
|
-
return {
|
|
1300
|
-
...state,
|
|
1301
|
-
freezesRemaining: Math.min(state.freezesRemaining + this.config.freezesPerMonth, this.config.maxFreezes)
|
|
1302
|
-
};
|
|
1303
|
-
}
|
|
1304
|
-
getInitialState() {
|
|
1305
|
-
return {
|
|
1306
|
-
currentStreak: 0,
|
|
1307
|
-
longestStreak: 0,
|
|
1308
|
-
lastActivityAt: null,
|
|
1309
|
-
lastActivityDate: null,
|
|
1310
|
-
freezesRemaining: this.config.freezesPerMonth,
|
|
1311
|
-
freezeUsedAt: null
|
|
1312
|
-
};
|
|
1313
|
-
}
|
|
1314
|
-
getMilestones(currentStreak) {
|
|
1315
|
-
const milestones = [3, 7, 14, 30, 60, 90, 180, 365, 500, 1000];
|
|
1316
|
-
const achieved = milestones.filter((m) => currentStreak >= m);
|
|
1317
|
-
const next = milestones.find((m) => currentStreak < m) ?? null;
|
|
1318
|
-
return { achieved, next };
|
|
1319
|
-
}
|
|
1320
|
-
getDateString(date) {
|
|
1321
|
-
const year = date.getFullYear();
|
|
1322
|
-
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
1323
|
-
const day = String(date.getDate()).padStart(2, "0");
|
|
1324
|
-
return `${year}-${month}-${day}`;
|
|
1325
|
-
}
|
|
1326
|
-
getDaysBetween(dateStr1, dateStr2) {
|
|
1327
|
-
const date1 = new Date(dateStr1);
|
|
1328
|
-
const date2 = new Date(dateStr2);
|
|
1329
|
-
const diffTime = date2.getTime() - date1.getTime();
|
|
1330
|
-
return Math.floor(diffTime / (1000 * 60 * 60 * 24));
|
|
1331
|
-
}
|
|
1332
|
-
addDays(date, days) {
|
|
1333
|
-
return new Date(date.getTime() + days * 24 * 60 * 60 * 1000);
|
|
1334
|
-
}
|
|
1335
|
-
}
|
|
1336
|
-
var streakEngine = new StreakEngine;
|
|
1337
1337
|
// src/entities/ai.ts
|
|
1338
1338
|
import {
|
|
1339
1339
|
defineEntity,
|
|
@@ -3408,8 +3408,8 @@ var learningJourneySchemaContribution = {
|
|
|
3408
3408
|
};
|
|
3409
3409
|
|
|
3410
3410
|
// src/events.ts
|
|
3411
|
-
import { ScalarTypeEnum as ScalarTypeEnum3, defineSchemaModel as defineSchemaModel3 } from "@contractspec/lib.schema";
|
|
3412
3411
|
import { defineEvent } from "@contractspec/lib.contracts-spec";
|
|
3412
|
+
import { defineSchemaModel as defineSchemaModel3, ScalarTypeEnum as ScalarTypeEnum3 } from "@contractspec/lib.schema";
|
|
3413
3413
|
var CoursePublishedPayload = defineSchemaModel3({
|
|
3414
3414
|
name: "CoursePublishedEventPayload",
|
|
3415
3415
|
description: "Payload when a course is published",
|
|
@@ -3808,6 +3808,7 @@ var LearningJourneyEvents = {
|
|
|
3808
3808
|
DailyGoalCompletedEvent,
|
|
3809
3809
|
CertificateIssuedEvent
|
|
3810
3810
|
};
|
|
3811
|
+
|
|
3811
3812
|
// src/learning-journey.feature.ts
|
|
3812
3813
|
import { defineFeature } from "@contractspec/lib.contracts-spec";
|
|
3813
3814
|
var LearningJourneyFeature = defineFeature({
|