@contractspec/module.learning-journey 1.57.0 → 1.58.0

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 (140) hide show
  1. package/dist/browser/contracts/index.js +578 -0
  2. package/dist/browser/contracts/models.js +193 -0
  3. package/dist/browser/contracts/onboarding.js +417 -0
  4. package/dist/browser/contracts/operations.js +326 -0
  5. package/dist/browser/contracts/shared.js +5 -0
  6. package/dist/browser/docs/index.js +124 -0
  7. package/dist/browser/docs/learning-journey.docblock.js +124 -0
  8. package/dist/browser/engines/index.js +526 -0
  9. package/dist/browser/engines/srs.js +198 -0
  10. package/dist/browser/engines/streak.js +159 -0
  11. package/dist/browser/engines/xp.js +171 -0
  12. package/dist/browser/entities/ai.js +343 -0
  13. package/dist/browser/entities/course.js +276 -0
  14. package/dist/browser/entities/flashcard.js +222 -0
  15. package/dist/browser/entities/gamification.js +340 -0
  16. package/dist/browser/entities/index.js +2136 -0
  17. package/dist/browser/entities/learner.js +329 -0
  18. package/dist/browser/entities/onboarding.js +301 -0
  19. package/dist/browser/entities/quiz.js +304 -0
  20. package/dist/browser/events.js +423 -0
  21. package/dist/browser/index.js +3833 -0
  22. package/dist/browser/learning-journey.capability.js +40 -0
  23. package/dist/browser/learning-journey.feature.js +56 -0
  24. package/dist/browser/track-spec.js +0 -0
  25. package/dist/contracts/index.d.ts +5 -5
  26. package/dist/contracts/index.d.ts.map +1 -0
  27. package/dist/contracts/index.js +578 -5
  28. package/dist/contracts/models.d.ts +426 -431
  29. package/dist/contracts/models.d.ts.map +1 -1
  30. package/dist/contracts/models.js +178 -372
  31. package/dist/contracts/onboarding.d.ts +621 -627
  32. package/dist/contracts/onboarding.d.ts.map +1 -1
  33. package/dist/contracts/onboarding.js +404 -388
  34. package/dist/contracts/operations.d.ts +243 -249
  35. package/dist/contracts/operations.d.ts.map +1 -1
  36. package/dist/contracts/operations.js +324 -148
  37. package/dist/contracts/shared.d.ts +1 -4
  38. package/dist/contracts/shared.d.ts.map +1 -1
  39. package/dist/contracts/shared.js +6 -6
  40. package/dist/docs/index.d.ts +2 -1
  41. package/dist/docs/index.d.ts.map +1 -0
  42. package/dist/docs/index.js +125 -1
  43. package/dist/docs/learning-journey.docblock.d.ts +2 -1
  44. package/dist/docs/learning-journey.docblock.d.ts.map +1 -0
  45. package/dist/docs/learning-journey.docblock.js +47 -58
  46. package/dist/engines/index.d.ts +4 -4
  47. package/dist/engines/index.d.ts.map +1 -0
  48. package/dist/engines/index.js +526 -4
  49. package/dist/engines/srs.d.ts +89 -92
  50. package/dist/engines/srs.d.ts.map +1 -1
  51. package/dist/engines/srs.js +197 -217
  52. package/dist/engines/streak.d.ts +84 -87
  53. package/dist/engines/streak.d.ts.map +1 -1
  54. package/dist/engines/streak.js +158 -192
  55. package/dist/engines/xp.d.ts +80 -83
  56. package/dist/engines/xp.d.ts.map +1 -1
  57. package/dist/engines/xp.js +170 -211
  58. package/dist/entities/ai.d.ts +199 -204
  59. package/dist/entities/ai.d.ts.map +1 -1
  60. package/dist/entities/ai.js +336 -368
  61. package/dist/entities/course.d.ts +149 -154
  62. package/dist/entities/course.d.ts.map +1 -1
  63. package/dist/entities/course.js +267 -306
  64. package/dist/entities/flashcard.d.ts +144 -149
  65. package/dist/entities/flashcard.d.ts.map +1 -1
  66. package/dist/entities/flashcard.js +217 -243
  67. package/dist/entities/gamification.d.ts +197 -202
  68. package/dist/entities/gamification.d.ts.map +1 -1
  69. package/dist/entities/gamification.js +331 -382
  70. package/dist/entities/index.d.ts +613 -618
  71. package/dist/entities/index.d.ts.map +1 -1
  72. package/dist/entities/index.js +2135 -43
  73. package/dist/entities/learner.d.ts +191 -196
  74. package/dist/entities/learner.d.ts.map +1 -1
  75. package/dist/entities/learner.js +322 -357
  76. package/dist/entities/onboarding.d.ts +164 -169
  77. package/dist/entities/onboarding.d.ts.map +1 -1
  78. package/dist/entities/onboarding.js +296 -301
  79. package/dist/entities/quiz.d.ts +184 -189
  80. package/dist/entities/quiz.d.ts.map +1 -1
  81. package/dist/entities/quiz.js +296 -361
  82. package/dist/events.d.ts +608 -614
  83. package/dist/events.d.ts.map +1 -1
  84. package/dist/events.js +421 -687
  85. package/dist/index.d.ts +8 -20
  86. package/dist/index.d.ts.map +1 -0
  87. package/dist/index.js +3834 -22
  88. package/dist/learning-journey.capability.d.ts +3 -8
  89. package/dist/learning-journey.capability.d.ts.map +1 -1
  90. package/dist/learning-journey.capability.js +41 -46
  91. package/dist/learning-journey.feature.d.ts +1 -6
  92. package/dist/learning-journey.feature.d.ts.map +1 -1
  93. package/dist/learning-journey.feature.js +55 -155
  94. package/dist/node/contracts/index.js +578 -0
  95. package/dist/node/contracts/models.js +193 -0
  96. package/dist/node/contracts/onboarding.js +417 -0
  97. package/dist/node/contracts/operations.js +326 -0
  98. package/dist/node/contracts/shared.js +5 -0
  99. package/dist/node/docs/index.js +124 -0
  100. package/dist/node/docs/learning-journey.docblock.js +124 -0
  101. package/dist/node/engines/index.js +526 -0
  102. package/dist/node/engines/srs.js +198 -0
  103. package/dist/node/engines/streak.js +159 -0
  104. package/dist/node/engines/xp.js +171 -0
  105. package/dist/node/entities/ai.js +343 -0
  106. package/dist/node/entities/course.js +276 -0
  107. package/dist/node/entities/flashcard.js +222 -0
  108. package/dist/node/entities/gamification.js +340 -0
  109. package/dist/node/entities/index.js +2136 -0
  110. package/dist/node/entities/learner.js +329 -0
  111. package/dist/node/entities/onboarding.js +301 -0
  112. package/dist/node/entities/quiz.js +304 -0
  113. package/dist/node/events.js +423 -0
  114. package/dist/node/index.js +3833 -0
  115. package/dist/node/learning-journey.capability.js +40 -0
  116. package/dist/node/learning-journey.feature.js +56 -0
  117. package/dist/node/track-spec.js +0 -0
  118. package/dist/track-spec.d.ts +115 -118
  119. package/dist/track-spec.d.ts.map +1 -1
  120. package/dist/track-spec.js +1 -0
  121. package/package.json +237 -60
  122. package/dist/contracts/models.js.map +0 -1
  123. package/dist/contracts/onboarding.js.map +0 -1
  124. package/dist/contracts/operations.js.map +0 -1
  125. package/dist/contracts/shared.js.map +0 -1
  126. package/dist/docs/learning-journey.docblock.js.map +0 -1
  127. package/dist/engines/srs.js.map +0 -1
  128. package/dist/engines/streak.js.map +0 -1
  129. package/dist/engines/xp.js.map +0 -1
  130. package/dist/entities/ai.js.map +0 -1
  131. package/dist/entities/course.js.map +0 -1
  132. package/dist/entities/flashcard.js.map +0 -1
  133. package/dist/entities/gamification.js.map +0 -1
  134. package/dist/entities/index.js.map +0 -1
  135. package/dist/entities/learner.js.map +0 -1
  136. package/dist/entities/onboarding.js.map +0 -1
  137. package/dist/entities/quiz.js.map +0 -1
  138. package/dist/events.js.map +0 -1
  139. package/dist/learning-journey.capability.js.map +0 -1
  140. package/dist/learning-journey.feature.js.map +0 -1
@@ -0,0 +1,159 @@
1
+ // src/engines/streak.ts
2
+ var DEFAULT_STREAK_CONFIG = {
3
+ timezone: "UTC",
4
+ freezesPerMonth: 2,
5
+ maxFreezes: 5,
6
+ gracePeriodHours: 4
7
+ };
8
+
9
+ class StreakEngine {
10
+ config;
11
+ constructor(config = {}) {
12
+ this.config = { ...DEFAULT_STREAK_CONFIG, ...config };
13
+ }
14
+ update(state, now = new Date) {
15
+ const todayDate = this.getDateString(now);
16
+ const result = {
17
+ state: { ...state },
18
+ streakMaintained: false,
19
+ streakLost: false,
20
+ freezeUsed: false,
21
+ newStreak: false,
22
+ daysMissed: 0
23
+ };
24
+ if (!state.lastActivityDate) {
25
+ result.state.currentStreak = 1;
26
+ result.state.longestStreak = Math.max(1, state.longestStreak);
27
+ result.state.lastActivityAt = now;
28
+ result.state.lastActivityDate = todayDate;
29
+ result.newStreak = true;
30
+ result.streakMaintained = true;
31
+ return result;
32
+ }
33
+ if (state.lastActivityDate === todayDate) {
34
+ result.state.lastActivityAt = now;
35
+ result.streakMaintained = true;
36
+ return result;
37
+ }
38
+ const daysSinceActivity = this.getDaysBetween(state.lastActivityDate, todayDate);
39
+ if (daysSinceActivity === 1) {
40
+ result.state.currentStreak = state.currentStreak + 1;
41
+ result.state.longestStreak = Math.max(result.state.currentStreak, state.longestStreak);
42
+ result.state.lastActivityAt = now;
43
+ result.state.lastActivityDate = todayDate;
44
+ result.streakMaintained = true;
45
+ return result;
46
+ }
47
+ result.daysMissed = daysSinceActivity - 1;
48
+ const freezesNeeded = result.daysMissed;
49
+ if (freezesNeeded <= state.freezesRemaining) {
50
+ result.state.freezesRemaining = state.freezesRemaining - freezesNeeded;
51
+ result.state.freezeUsedAt = now;
52
+ result.state.currentStreak = state.currentStreak + 1;
53
+ result.state.longestStreak = Math.max(result.state.currentStreak, state.longestStreak);
54
+ result.state.lastActivityAt = now;
55
+ result.state.lastActivityDate = todayDate;
56
+ result.freezeUsed = true;
57
+ result.streakMaintained = true;
58
+ return result;
59
+ }
60
+ result.streakLost = true;
61
+ result.state.currentStreak = 1;
62
+ result.state.lastActivityAt = now;
63
+ result.state.lastActivityDate = todayDate;
64
+ result.newStreak = true;
65
+ return result;
66
+ }
67
+ checkStatus(state, now = new Date) {
68
+ if (!state.lastActivityDate) {
69
+ return {
70
+ isActive: false,
71
+ willExpireAt: null,
72
+ canUseFreeze: false,
73
+ daysUntilExpiry: 0
74
+ };
75
+ }
76
+ const todayDate = this.getDateString(now);
77
+ const daysSinceActivity = this.getDaysBetween(state.lastActivityDate, todayDate);
78
+ if (daysSinceActivity === 0) {
79
+ const tomorrow = this.addDays(now, 1);
80
+ tomorrow.setHours(23, 59, 59, 999);
81
+ return {
82
+ isActive: true,
83
+ willExpireAt: tomorrow,
84
+ canUseFreeze: state.freezesRemaining > 0,
85
+ daysUntilExpiry: 1
86
+ };
87
+ }
88
+ if (daysSinceActivity === 1) {
89
+ const endOfDay = new Date(now);
90
+ endOfDay.setHours(23 + this.config.gracePeriodHours, 59, 59, 999);
91
+ return {
92
+ isActive: true,
93
+ willExpireAt: endOfDay,
94
+ canUseFreeze: state.freezesRemaining > 0,
95
+ daysUntilExpiry: 0
96
+ };
97
+ }
98
+ const missedDays = daysSinceActivity - 1;
99
+ return {
100
+ isActive: missedDays <= state.freezesRemaining,
101
+ willExpireAt: null,
102
+ canUseFreeze: missedDays <= state.freezesRemaining,
103
+ daysUntilExpiry: -missedDays
104
+ };
105
+ }
106
+ useFreeze(state, now = new Date) {
107
+ if (state.freezesRemaining <= 0) {
108
+ return null;
109
+ }
110
+ return {
111
+ ...state,
112
+ freezesRemaining: state.freezesRemaining - 1,
113
+ freezeUsedAt: now
114
+ };
115
+ }
116
+ awardMonthlyFreezes(state) {
117
+ return {
118
+ ...state,
119
+ freezesRemaining: Math.min(state.freezesRemaining + this.config.freezesPerMonth, this.config.maxFreezes)
120
+ };
121
+ }
122
+ getInitialState() {
123
+ return {
124
+ currentStreak: 0,
125
+ longestStreak: 0,
126
+ lastActivityAt: null,
127
+ lastActivityDate: null,
128
+ freezesRemaining: this.config.freezesPerMonth,
129
+ freezeUsedAt: null
130
+ };
131
+ }
132
+ getMilestones(currentStreak) {
133
+ const milestones = [3, 7, 14, 30, 60, 90, 180, 365, 500, 1000];
134
+ const achieved = milestones.filter((m) => currentStreak >= m);
135
+ const next = milestones.find((m) => currentStreak < m) ?? null;
136
+ return { achieved, next };
137
+ }
138
+ getDateString(date) {
139
+ const year = date.getFullYear();
140
+ const month = String(date.getMonth() + 1).padStart(2, "0");
141
+ const day = String(date.getDate()).padStart(2, "0");
142
+ return `${year}-${month}-${day}`;
143
+ }
144
+ getDaysBetween(dateStr1, dateStr2) {
145
+ const date1 = new Date(dateStr1);
146
+ const date2 = new Date(dateStr2);
147
+ const diffTime = date2.getTime() - date1.getTime();
148
+ return Math.floor(diffTime / (1000 * 60 * 60 * 24));
149
+ }
150
+ addDays(date, days) {
151
+ return new Date(date.getTime() + days * 24 * 60 * 60 * 1000);
152
+ }
153
+ }
154
+ var streakEngine = new StreakEngine;
155
+ export {
156
+ streakEngine,
157
+ StreakEngine,
158
+ DEFAULT_STREAK_CONFIG
159
+ };
@@ -0,0 +1,171 @@
1
+ // src/engines/xp.ts
2
+ var DEFAULT_XP_CONFIG = {
3
+ baseValues: {
4
+ lesson_complete: 10,
5
+ quiz_pass: 20,
6
+ quiz_perfect: 50,
7
+ flashcard_review: 1,
8
+ course_complete: 200,
9
+ module_complete: 50,
10
+ streak_bonus: 5,
11
+ achievement_unlock: 0,
12
+ daily_goal_complete: 15,
13
+ first_lesson: 25,
14
+ onboarding_step: 5,
15
+ onboarding_complete: 50
16
+ },
17
+ scoreThresholds: [
18
+ { min: 90, multiplier: 1.5 },
19
+ { min: 80, multiplier: 1.25 },
20
+ { min: 70, multiplier: 1 },
21
+ { min: 60, multiplier: 0.75 },
22
+ { min: 0, multiplier: 0.5 }
23
+ ],
24
+ streakTiers: [
25
+ { days: 365, bonus: 50 },
26
+ { days: 180, bonus: 30 },
27
+ { days: 90, bonus: 20 },
28
+ { days: 30, bonus: 15 },
29
+ { days: 14, bonus: 10 },
30
+ { days: 7, bonus: 5 },
31
+ { days: 3, bonus: 2 },
32
+ { days: 1, bonus: 0 }
33
+ ],
34
+ perfectScoreMultiplier: 1.5,
35
+ firstAttemptBonus: 10,
36
+ retryPenalty: 0.5,
37
+ speedBonusMultiplier: 1.2,
38
+ speedBonusThreshold: 0.8
39
+ };
40
+
41
+ class XPEngine {
42
+ config;
43
+ constructor(config = {}) {
44
+ this.config = {
45
+ ...DEFAULT_XP_CONFIG,
46
+ ...config,
47
+ baseValues: { ...DEFAULT_XP_CONFIG.baseValues, ...config.baseValues },
48
+ scoreThresholds: config.scoreThresholds || DEFAULT_XP_CONFIG.scoreThresholds,
49
+ streakTiers: config.streakTiers || DEFAULT_XP_CONFIG.streakTiers
50
+ };
51
+ }
52
+ calculate(input) {
53
+ const breakdown = [];
54
+ const baseXp = input.baseXp ?? this.config.baseValues[input.activity];
55
+ let totalXp = baseXp;
56
+ breakdown.push({
57
+ source: "base",
58
+ amount: baseXp
59
+ });
60
+ if (input.score !== undefined) {
61
+ const scoreMultiplier = this.getScoreMultiplier(input.score);
62
+ if (scoreMultiplier !== 1) {
63
+ const scoreBonus = Math.round(baseXp * (scoreMultiplier - 1));
64
+ totalXp += scoreBonus;
65
+ breakdown.push({
66
+ source: "score_bonus",
67
+ amount: scoreBonus,
68
+ multiplier: scoreMultiplier
69
+ });
70
+ }
71
+ if (input.score === 100) {
72
+ const perfectBonus = Math.round(baseXp * (this.config.perfectScoreMultiplier - 1));
73
+ totalXp += perfectBonus;
74
+ breakdown.push({
75
+ source: "perfect_score",
76
+ amount: perfectBonus,
77
+ multiplier: this.config.perfectScoreMultiplier
78
+ });
79
+ }
80
+ }
81
+ if (input.attemptNumber === 1 && !input.isRetry) {
82
+ totalXp += this.config.firstAttemptBonus;
83
+ breakdown.push({
84
+ source: "first_attempt",
85
+ amount: this.config.firstAttemptBonus
86
+ });
87
+ }
88
+ if (input.isRetry) {
89
+ const penalty = Math.round(totalXp * (1 - this.config.retryPenalty));
90
+ totalXp -= penalty;
91
+ breakdown.push({
92
+ source: "retry_penalty",
93
+ amount: -penalty,
94
+ multiplier: this.config.retryPenalty
95
+ });
96
+ }
97
+ if (input.currentStreak && input.currentStreak > 0) {
98
+ const streakBonus = this.getStreakBonus(input.currentStreak);
99
+ if (streakBonus > 0) {
100
+ totalXp += streakBonus;
101
+ breakdown.push({
102
+ source: "streak_bonus",
103
+ amount: streakBonus
104
+ });
105
+ }
106
+ }
107
+ if (baseXp > 0) {
108
+ totalXp = Math.max(1, totalXp);
109
+ }
110
+ return {
111
+ totalXp: Math.round(totalXp),
112
+ baseXp,
113
+ breakdown
114
+ };
115
+ }
116
+ calculateStreakBonus(currentStreak) {
117
+ const bonus = this.getStreakBonus(currentStreak);
118
+ return {
119
+ totalXp: bonus,
120
+ baseXp: bonus,
121
+ breakdown: [
122
+ {
123
+ source: "streak_bonus",
124
+ amount: bonus
125
+ }
126
+ ]
127
+ };
128
+ }
129
+ getXpForLevel(level) {
130
+ if (level <= 1)
131
+ return 0;
132
+ return Math.round(100 * Math.pow(level - 1, 1.5));
133
+ }
134
+ getLevelFromXp(totalXp) {
135
+ let level = 1;
136
+ let xpRequired = this.getXpForLevel(level + 1);
137
+ while (totalXp >= xpRequired && level < 1000) {
138
+ level++;
139
+ xpRequired = this.getXpForLevel(level + 1);
140
+ }
141
+ const xpForCurrentLevel = this.getXpForLevel(level);
142
+ const xpForNextLevel = this.getXpForLevel(level + 1);
143
+ return {
144
+ level,
145
+ xpInLevel: totalXp - xpForCurrentLevel,
146
+ xpForNextLevel: xpForNextLevel - xpForCurrentLevel
147
+ };
148
+ }
149
+ getScoreMultiplier(score) {
150
+ for (const threshold of this.config.scoreThresholds) {
151
+ if (score >= threshold.min) {
152
+ return threshold.multiplier;
153
+ }
154
+ }
155
+ return 1;
156
+ }
157
+ getStreakBonus(streak) {
158
+ for (const tier of this.config.streakTiers) {
159
+ if (streak >= tier.days) {
160
+ return tier.bonus;
161
+ }
162
+ }
163
+ return 0;
164
+ }
165
+ }
166
+ var xpEngine = new XPEngine;
167
+ export {
168
+ xpEngine,
169
+ XPEngine,
170
+ DEFAULT_XP_CONFIG
171
+ };
@@ -0,0 +1,343 @@
1
+ // src/entities/ai.ts
2
+ import {
3
+ defineEntity,
4
+ defineEntityEnum,
5
+ field,
6
+ index
7
+ } from "@contractspec/lib.schema";
8
+ var LearningStyleEnum = defineEntityEnum({
9
+ name: "LearningStyle",
10
+ values: ["VISUAL", "AUDITORY", "READING", "KINESTHETIC", "MIXED"],
11
+ schema: "lssm_learning",
12
+ description: "Preferred learning style."
13
+ });
14
+ var RecommendationTypeEnum = defineEntityEnum({
15
+ name: "RecommendationType",
16
+ values: [
17
+ "COURSE",
18
+ "LESSON",
19
+ "REVIEW",
20
+ "PRACTICE",
21
+ "ASSESSMENT",
22
+ "DECK"
23
+ ],
24
+ schema: "lssm_learning",
25
+ description: "Type of learning recommendation."
26
+ });
27
+ var LearnerProfileEntity = defineEntity({
28
+ name: "LearnerProfile",
29
+ description: "AI personalization profile for a learner.",
30
+ schema: "lssm_learning",
31
+ map: "learner_profile",
32
+ fields: {
33
+ id: field.id({ description: "Unique profile identifier" }),
34
+ learnerId: field.foreignKey({ description: "Learner" }),
35
+ learningStyle: field.enum("LearningStyle", {
36
+ default: "MIXED",
37
+ description: "Preferred learning style"
38
+ }),
39
+ preferredDifficulty: field.string({
40
+ default: '"adaptive"',
41
+ description: "Difficulty preference"
42
+ }),
43
+ preferredSessionLength: field.int({
44
+ default: 30,
45
+ description: "Preferred session length in minutes"
46
+ }),
47
+ interests: field.json({ isOptional: true, description: "Topic interests" }),
48
+ goals: field.json({ isOptional: true, description: "Learning goals" }),
49
+ pacePreference: field.string({
50
+ default: '"normal"',
51
+ description: "Learning pace: slow, normal, fast"
52
+ }),
53
+ bestTimeOfDay: field.string({
54
+ isOptional: true,
55
+ description: "Best time for learning"
56
+ }),
57
+ averageSessionLength: field.int({
58
+ isOptional: true,
59
+ description: "Average session length"
60
+ }),
61
+ daysActivePerWeek: field.int({
62
+ isOptional: true,
63
+ description: "Days active per week"
64
+ }),
65
+ avgQuizScore: field.int({
66
+ isOptional: true,
67
+ description: "Average quiz score"
68
+ }),
69
+ avgLessonCompletionTime: field.int({
70
+ isOptional: true,
71
+ description: "Avg lesson completion time"
72
+ }),
73
+ strengths: field.json({
74
+ isOptional: true,
75
+ description: "Identified strengths"
76
+ }),
77
+ weaknesses: field.json({
78
+ isOptional: true,
79
+ description: "Areas for improvement"
80
+ }),
81
+ lastAnalyzedAt: field.dateTime({
82
+ isOptional: true,
83
+ description: "Last AI analysis"
84
+ }),
85
+ metadata: field.json({
86
+ isOptional: true,
87
+ description: "Additional metadata"
88
+ }),
89
+ createdAt: field.createdAt(),
90
+ updatedAt: field.updatedAt(),
91
+ learner: field.belongsTo("Learner", ["learnerId"], ["id"], {
92
+ onDelete: "Cascade"
93
+ })
94
+ },
95
+ indexes: [
96
+ index.unique(["learnerId"], { name: "learner_profile_unique" }),
97
+ index.on(["learningStyle"])
98
+ ],
99
+ enums: [LearningStyleEnum]
100
+ });
101
+ var SkillMapEntity = defineEntity({
102
+ name: "SkillMap",
103
+ description: "Maps learner proficiency across skills.",
104
+ schema: "lssm_learning",
105
+ map: "skill_map",
106
+ fields: {
107
+ id: field.id({ description: "Unique skill map identifier" }),
108
+ learnerId: field.foreignKey({ description: "Learner" }),
109
+ skillId: field.string({ description: "Skill identifier" }),
110
+ skillName: field.string({ description: "Skill name" }),
111
+ skillCategory: field.string({
112
+ isOptional: true,
113
+ description: "Skill category"
114
+ }),
115
+ level: field.int({ default: 0, description: "Proficiency level (0-100)" }),
116
+ confidence: field.decimal({
117
+ default: 0.5,
118
+ description: "Confidence in assessment"
119
+ }),
120
+ lessonsCompleted: field.int({
121
+ default: 0,
122
+ description: "Related lessons completed"
123
+ }),
124
+ quizzesCompleted: field.int({
125
+ default: 0,
126
+ description: "Related quizzes completed"
127
+ }),
128
+ practiceTime: field.int({
129
+ default: 0,
130
+ description: "Practice time in minutes"
131
+ }),
132
+ lastPracticedAt: field.dateTime({
133
+ isOptional: true,
134
+ description: "Last practice time"
135
+ }),
136
+ learningVelocity: field.decimal({
137
+ isOptional: true,
138
+ description: "Learning speed for this skill"
139
+ }),
140
+ predictedTimeToMastery: field.int({
141
+ isOptional: true,
142
+ description: "Predicted time to mastery (minutes)"
143
+ }),
144
+ createdAt: field.createdAt(),
145
+ updatedAt: field.updatedAt(),
146
+ learner: field.belongsTo("Learner", ["learnerId"], ["id"], {
147
+ onDelete: "Cascade"
148
+ })
149
+ },
150
+ indexes: [
151
+ index.unique(["learnerId", "skillId"], { name: "skill_map_unique" }),
152
+ index.on(["skillId", "level"]),
153
+ index.on(["learnerId", "level"])
154
+ ]
155
+ });
156
+ var LearningPathEntity = defineEntity({
157
+ name: "LearningPath",
158
+ description: "AI-generated personalized learning path.",
159
+ schema: "lssm_learning",
160
+ map: "learning_path",
161
+ fields: {
162
+ id: field.id({ description: "Unique path identifier" }),
163
+ learnerId: field.foreignKey({ description: "Learner" }),
164
+ name: field.string({ description: "Path name" }),
165
+ description: field.string({
166
+ isOptional: true,
167
+ description: "Path description"
168
+ }),
169
+ goal: field.string({ isOptional: true, description: "Path goal" }),
170
+ steps: field.json({ description: "Ordered list of learning steps" }),
171
+ currentStepIndex: field.int({
172
+ default: 0,
173
+ description: "Current step index"
174
+ }),
175
+ progress: field.int({ default: 0, description: "Completion percentage" }),
176
+ completedSteps: field.int({ default: 0, description: "Steps completed" }),
177
+ totalSteps: field.int({ default: 0, description: "Total steps" }),
178
+ generatedAt: field.dateTime({ description: "When path was generated" }),
179
+ adaptedFrom: field.string({
180
+ isOptional: true,
181
+ description: "Original path ID if adapted"
182
+ }),
183
+ generationParams: field.json({
184
+ isOptional: true,
185
+ description: "AI generation parameters"
186
+ }),
187
+ adaptationHistory: field.json({
188
+ isOptional: true,
189
+ description: "Path adaptation history"
190
+ }),
191
+ isActive: field.boolean({
192
+ default: true,
193
+ description: "Whether path is active"
194
+ }),
195
+ isCompleted: field.boolean({
196
+ default: false,
197
+ description: "Whether path is completed"
198
+ }),
199
+ startedAt: field.dateTime({
200
+ isOptional: true,
201
+ description: "When started"
202
+ }),
203
+ completedAt: field.dateTime({
204
+ isOptional: true,
205
+ description: "When completed"
206
+ }),
207
+ estimatedCompletionDate: field.dateTime({
208
+ isOptional: true,
209
+ description: "Estimated completion"
210
+ }),
211
+ createdAt: field.createdAt(),
212
+ updatedAt: field.updatedAt(),
213
+ learner: field.belongsTo("Learner", ["learnerId"], ["id"], {
214
+ onDelete: "Cascade"
215
+ })
216
+ },
217
+ indexes: [index.on(["learnerId", "isActive"]), index.on(["generatedAt"])]
218
+ });
219
+ var RecommendationEntity = defineEntity({
220
+ name: "Recommendation",
221
+ description: "AI-powered learning recommendation.",
222
+ schema: "lssm_learning",
223
+ map: "recommendation",
224
+ fields: {
225
+ id: field.id({ description: "Unique recommendation identifier" }),
226
+ learnerId: field.foreignKey({ description: "Learner" }),
227
+ type: field.enum("RecommendationType", {
228
+ description: "Recommendation type"
229
+ }),
230
+ itemId: field.string({ description: "Recommended item ID" }),
231
+ itemType: field.string({
232
+ description: "Item type (course, lesson, deck, etc.)"
233
+ }),
234
+ score: field.decimal({ description: "Recommendation score (0-1)" }),
235
+ confidence: field.decimal({ description: "Confidence in recommendation" }),
236
+ reason: field.string({ description: "Human-readable reason" }),
237
+ factors: field.json({
238
+ isOptional: true,
239
+ description: "Factors that contributed to recommendation"
240
+ }),
241
+ status: field.string({
242
+ default: '"pending"',
243
+ description: "Status: pending, viewed, accepted, dismissed"
244
+ }),
245
+ viewedAt: field.dateTime({ isOptional: true, description: "When viewed" }),
246
+ acceptedAt: field.dateTime({
247
+ isOptional: true,
248
+ description: "When accepted"
249
+ }),
250
+ dismissedAt: field.dateTime({
251
+ isOptional: true,
252
+ description: "When dismissed"
253
+ }),
254
+ feedback: field.string({ isOptional: true, description: "User feedback" }),
255
+ feedbackRating: field.int({
256
+ isOptional: true,
257
+ description: "Feedback rating (1-5)"
258
+ }),
259
+ expiresAt: field.dateTime({
260
+ isOptional: true,
261
+ description: "When recommendation expires"
262
+ }),
263
+ createdAt: field.createdAt(),
264
+ updatedAt: field.updatedAt(),
265
+ learner: field.belongsTo("Learner", ["learnerId"], ["id"], {
266
+ onDelete: "Cascade"
267
+ })
268
+ },
269
+ indexes: [
270
+ index.on(["learnerId", "status", "score"]),
271
+ index.on(["type", "status"]),
272
+ index.on(["expiresAt"])
273
+ ],
274
+ enums: [RecommendationTypeEnum]
275
+ });
276
+ var LearningGapEntity = defineEntity({
277
+ name: "LearningGap",
278
+ description: "Identified learning gap.",
279
+ schema: "lssm_learning",
280
+ map: "learning_gap",
281
+ fields: {
282
+ id: field.id({ description: "Unique gap identifier" }),
283
+ learnerId: field.foreignKey({ description: "Learner" }),
284
+ skillId: field.string({ description: "Skill with gap" }),
285
+ skillName: field.string({ description: "Skill name" }),
286
+ severity: field.string({
287
+ default: '"moderate"',
288
+ description: "Gap severity: minor, moderate, major"
289
+ }),
290
+ confidence: field.decimal({ description: "Confidence in gap detection" }),
291
+ evidence: field.json({ isOptional: true, description: "Evidence for gap" }),
292
+ relatedQuestions: field.json({
293
+ isOptional: true,
294
+ description: "Questions that revealed gap"
295
+ }),
296
+ suggestedRemediation: field.json({
297
+ isOptional: true,
298
+ description: "Suggested remediation"
299
+ }),
300
+ remediationProgress: field.int({
301
+ default: 0,
302
+ description: "Remediation progress"
303
+ }),
304
+ status: field.string({
305
+ default: '"open"',
306
+ description: "Status: open, in_progress, resolved"
307
+ }),
308
+ resolvedAt: field.dateTime({
309
+ isOptional: true,
310
+ description: "When resolved"
311
+ }),
312
+ detectedAt: field.dateTime({ description: "When gap was detected" }),
313
+ createdAt: field.createdAt(),
314
+ updatedAt: field.updatedAt(),
315
+ learner: field.belongsTo("Learner", ["learnerId"], ["id"], {
316
+ onDelete: "Cascade"
317
+ })
318
+ },
319
+ indexes: [
320
+ index.on(["learnerId", "status"]),
321
+ index.on(["skillId", "status"]),
322
+ index.on(["severity", "status"])
323
+ ]
324
+ });
325
+ var aiEntities = [
326
+ LearnerProfileEntity,
327
+ SkillMapEntity,
328
+ LearningPathEntity,
329
+ RecommendationEntity,
330
+ LearningGapEntity
331
+ ];
332
+ var aiEnums = [LearningStyleEnum, RecommendationTypeEnum];
333
+ export {
334
+ aiEnums,
335
+ aiEntities,
336
+ SkillMapEntity,
337
+ RecommendationTypeEnum,
338
+ RecommendationEntity,
339
+ LearningStyleEnum,
340
+ LearningPathEntity,
341
+ LearningGapEntity,
342
+ LearnerProfileEntity
343
+ };