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