learngraph 0.4.0 → 0.5.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.
package/dist/cjs/index.js CHANGED
@@ -53,7 +53,7 @@ __exportStar(require("./types/index.js"), exports);
53
53
  /**
54
54
  * Package version
55
55
  */
56
- exports.VERSION = '0.1.1';
56
+ exports.VERSION = '0.5.0';
57
57
  /**
58
58
  * Package name
59
59
  */
@@ -2,11 +2,30 @@
2
2
  /**
3
3
  * Query and traversal engines
4
4
  *
5
+ * This module provides intelligent query engines for:
6
+ * - Zone of Proximal Development (ZPD) calculation
7
+ * - Learning path generation
8
+ * - Spaced repetition scheduling
9
+ *
5
10
  * @packageDocumentation
6
11
  */
7
12
  Object.defineProperty(exports, "__esModule", { value: true });
8
- // Query engines will be implemented in Phase 6
9
- // export { ZPDCalculator } from './zpd.js';
10
- // export { PathGenerator } from './path.js';
11
- // export { SkillQueryBuilder } from './builder.js';
13
+ exports.SM2_CONSTANTS = exports.REVIEW_DEFAULTS = exports.calculateSM2 = exports.createSpacedRepetitionScheduler = exports.SpacedRepetitionScheduler = exports.PATH_DEFAULTS = exports.createPathGenerator = exports.PathGenerator = exports.ZPD_DEFAULTS = exports.createZPDCalculator = exports.ZPDCalculator = void 0;
14
+ // ZPD Calculator
15
+ var zpd_js_1 = require("./zpd.js");
16
+ Object.defineProperty(exports, "ZPDCalculator", { enumerable: true, get: function () { return zpd_js_1.ZPDCalculator; } });
17
+ Object.defineProperty(exports, "createZPDCalculator", { enumerable: true, get: function () { return zpd_js_1.createZPDCalculator; } });
18
+ Object.defineProperty(exports, "ZPD_DEFAULTS", { enumerable: true, get: function () { return zpd_js_1.ZPD_DEFAULTS; } });
19
+ // Learning Path Generator
20
+ var path_js_1 = require("./path.js");
21
+ Object.defineProperty(exports, "PathGenerator", { enumerable: true, get: function () { return path_js_1.PathGenerator; } });
22
+ Object.defineProperty(exports, "createPathGenerator", { enumerable: true, get: function () { return path_js_1.createPathGenerator; } });
23
+ Object.defineProperty(exports, "PATH_DEFAULTS", { enumerable: true, get: function () { return path_js_1.PATH_DEFAULTS; } });
24
+ // Spaced Repetition Scheduler
25
+ var spaced_repetition_js_1 = require("./spaced-repetition.js");
26
+ Object.defineProperty(exports, "SpacedRepetitionScheduler", { enumerable: true, get: function () { return spaced_repetition_js_1.SpacedRepetitionScheduler; } });
27
+ Object.defineProperty(exports, "createSpacedRepetitionScheduler", { enumerable: true, get: function () { return spaced_repetition_js_1.createSpacedRepetitionScheduler; } });
28
+ Object.defineProperty(exports, "calculateSM2", { enumerable: true, get: function () { return spaced_repetition_js_1.calculateSM2; } });
29
+ Object.defineProperty(exports, "REVIEW_DEFAULTS", { enumerable: true, get: function () { return spaced_repetition_js_1.REVIEW_DEFAULTS; } });
30
+ Object.defineProperty(exports, "SM2_CONSTANTS", { enumerable: true, get: function () { return spaced_repetition_js_1.SM2_CONSTANTS; } });
12
31
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/query/index.ts"],"names":[],"mappings":";AAAA;;;;GAIG;;AAmBH,+CAA+C;AAC/C,4CAA4C;AAC5C,6CAA6C;AAC7C,oDAAoD"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/query/index.ts"],"names":[],"mappings":";AAAA;;;;;;;;;GASG;;;AAmBH,iBAAiB;AACjB,mCAKkB;AAJhB,uGAAA,aAAa,OAAA;AACb,6GAAA,mBAAmB,OAAA;AACnB,sGAAA,YAAY,OAAA;AAId,0BAA0B;AAC1B,qCAKmB;AAJjB,wGAAA,aAAa,OAAA;AACb,8GAAA,mBAAmB,OAAA;AACnB,wGAAA,aAAa,OAAA;AAIf,8BAA8B;AAC9B,+DASgC;AAR9B,iIAAA,yBAAyB,OAAA;AACzB,uIAAA,+BAA+B,OAAA;AAC/B,oHAAA,YAAY,OAAA;AACZ,uHAAA,eAAe,OAAA;AACf,qHAAA,aAAa,OAAA"}
@@ -0,0 +1,313 @@
1
+ "use strict";
2
+ /**
3
+ * Learning Path Generator
4
+ *
5
+ * Generates personalized learning paths from current state to target skills.
6
+ * Uses topological sorting and groups skills into manageable sessions.
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.PathGenerator = exports.PATH_DEFAULTS = void 0;
12
+ exports.createPathGenerator = createPathGenerator;
13
+ /**
14
+ * Default options for path generation
15
+ */
16
+ exports.PATH_DEFAULTS = {
17
+ sessionMinutes: 30,
18
+ maxPathLength: 100,
19
+ includeReview: false,
20
+ };
21
+ /**
22
+ * Learning Path Generator
23
+ *
24
+ * Generates optimal learning paths to reach target skills by:
25
+ * 1. Finding all unmastered prerequisites
26
+ * 2. Topologically sorting based on dependencies
27
+ * 3. Grouping into sessions based on cognitive load
28
+ *
29
+ * @example
30
+ * ```typescript
31
+ * const generator = new PathGenerator(storage);
32
+ *
33
+ * const path = await generator.generatePath(
34
+ * 'advanced-skill',
35
+ * learnerState,
36
+ * { sessionMinutes: 45 }
37
+ * );
38
+ *
39
+ * console.log(`Path has ${path.skills.length} skills`);
40
+ * console.log(`Estimated time: ${path.totalMinutes} minutes`);
41
+ * ```
42
+ */
43
+ class PathGenerator {
44
+ storage;
45
+ constructor(storage) {
46
+ this.storage = storage;
47
+ }
48
+ /**
49
+ * Generate a learning path to a target skill
50
+ *
51
+ * @param targetId - The skill to learn
52
+ * @param learner - Current learner state
53
+ * @param options - Path generation options
54
+ * @returns Learning path with skills grouped into sessions
55
+ */
56
+ async generatePath(targetId, learner, options = {}) {
57
+ const opts = { ...exports.PATH_DEFAULTS, ...options };
58
+ const target = await this.storage.getSkill(targetId);
59
+ if (!target) {
60
+ return null;
61
+ }
62
+ // Check if already mastered
63
+ const targetMastery = learner.masteryStates.get(targetId);
64
+ if (targetMastery && targetMastery.mastery >= target.masteryThreshold) {
65
+ // Already mastered - return empty path
66
+ return {
67
+ target,
68
+ skills: [],
69
+ totalMinutes: 0,
70
+ sessions: [],
71
+ checkpoints: [],
72
+ };
73
+ }
74
+ // Get all prerequisites (transitive)
75
+ const allPrereqs = await this.getTransitivePrerequisites(targetId);
76
+ // Filter to unmastered skills
77
+ const skillsToLearn = await this.filterUnmastered([...allPrereqs, target], learner, opts.includeReview);
78
+ // Topologically sort
79
+ const sorted = await this.topologicalSort(skillsToLearn);
80
+ // Apply max length limit
81
+ const limited = opts.maxPathLength
82
+ ? sorted.slice(0, opts.maxPathLength)
83
+ : sorted;
84
+ // Calculate total time
85
+ const totalMinutes = limited.reduce((sum, skill) => sum + skill.estimatedMinutes, 0);
86
+ // Group into sessions
87
+ const sessions = this.groupIntoSessions(limited, opts.sessionMinutes);
88
+ // Identify checkpoints (threshold concepts)
89
+ const checkpoints = limited.filter((skill) => skill.isThresholdConcept);
90
+ return {
91
+ target,
92
+ skills: limited,
93
+ totalMinutes,
94
+ sessions,
95
+ checkpoints,
96
+ };
97
+ }
98
+ /**
99
+ * Generate paths to multiple target skills
100
+ *
101
+ * @param targetIds - Skills to learn
102
+ * @param learner - Current learner state
103
+ * @param options - Path generation options
104
+ * @returns Array of learning paths
105
+ */
106
+ async generatePaths(targetIds, learner, options = {}) {
107
+ const paths = [];
108
+ for (const targetId of targetIds) {
109
+ const path = await this.generatePath(targetId, learner, options);
110
+ if (path) {
111
+ paths.push(path);
112
+ }
113
+ }
114
+ return paths;
115
+ }
116
+ /**
117
+ * Get a merged learning path for multiple targets
118
+ * Combines all required skills and eliminates duplicates
119
+ */
120
+ async generateMergedPath(targetIds, learner, options = {}) {
121
+ const opts = { ...exports.PATH_DEFAULTS, ...options };
122
+ // Get all target skills
123
+ const targets = [];
124
+ for (const id of targetIds) {
125
+ const skill = await this.storage.getSkill(id);
126
+ if (skill) {
127
+ targets.push(skill);
128
+ }
129
+ }
130
+ if (targets.length === 0) {
131
+ return null;
132
+ }
133
+ // Collect all unmastered prerequisites from all targets
134
+ const allSkillsSet = new Set();
135
+ const allSkills = [];
136
+ for (const target of targets) {
137
+ const prereqs = await this.getTransitivePrerequisites(target.id);
138
+ const allForTarget = [...prereqs, target];
139
+ for (const skill of allForTarget) {
140
+ if (!allSkillsSet.has(skill.id)) {
141
+ allSkillsSet.add(skill.id);
142
+ allSkills.push(skill);
143
+ }
144
+ }
145
+ }
146
+ // Filter to unmastered
147
+ const skillsToLearn = await this.filterUnmastered(allSkills, learner, opts.includeReview);
148
+ // Topologically sort
149
+ const sorted = await this.topologicalSort(skillsToLearn);
150
+ // Apply max length
151
+ const limited = opts.maxPathLength
152
+ ? sorted.slice(0, opts.maxPathLength)
153
+ : sorted;
154
+ const totalMinutes = limited.reduce((sum, skill) => sum + skill.estimatedMinutes, 0);
155
+ const sessions = this.groupIntoSessions(limited, opts.sessionMinutes);
156
+ const checkpoints = limited.filter((skill) => skill.isThresholdConcept);
157
+ // Use first target as the primary target
158
+ return {
159
+ target: targets[0],
160
+ skills: limited,
161
+ totalMinutes,
162
+ sessions,
163
+ checkpoints,
164
+ };
165
+ }
166
+ /**
167
+ * Get all transitive prerequisites of a skill
168
+ */
169
+ async getTransitivePrerequisites(skillId) {
170
+ const visited = new Set();
171
+ const result = [];
172
+ const visit = async (id) => {
173
+ if (visited.has(id)) {
174
+ return;
175
+ }
176
+ visited.add(id);
177
+ const prereqs = await this.storage.getPrerequisitesOf(id);
178
+ for (const prereq of prereqs) {
179
+ await visit(prereq.id);
180
+ if (!result.some((s) => s.id === prereq.id)) {
181
+ result.push(prereq);
182
+ }
183
+ }
184
+ };
185
+ await visit(skillId);
186
+ return result;
187
+ }
188
+ /**
189
+ * Filter skills to only unmastered ones
190
+ */
191
+ async filterUnmastered(skills, learner, includeReview) {
192
+ return skills.filter((skill) => {
193
+ const mastery = learner.masteryStates.get(skill.id);
194
+ const level = mastery?.mastery ?? 0;
195
+ if (includeReview) {
196
+ // Include if below 100% mastery
197
+ return level < 1.0;
198
+ }
199
+ // Include if below mastery threshold
200
+ return level < skill.masteryThreshold;
201
+ });
202
+ }
203
+ /**
204
+ * Topologically sort skills based on prerequisites
205
+ * Skills with no unmastered prerequisites come first
206
+ */
207
+ async topologicalSort(skills) {
208
+ const skillMap = new Map(skills.map((s) => [s.id, s]));
209
+ const result = [];
210
+ const visited = new Set();
211
+ const visiting = new Set(); // For cycle detection
212
+ const visit = async (skill) => {
213
+ if (visited.has(skill.id)) {
214
+ return;
215
+ }
216
+ if (visiting.has(skill.id)) {
217
+ // Cycle detected - skip to avoid infinite loop
218
+ return;
219
+ }
220
+ visiting.add(skill.id);
221
+ // Visit prerequisites first (only those in our skill set)
222
+ const prereqs = await this.storage.getPrerequisitesOf(skill.id);
223
+ for (const prereq of prereqs) {
224
+ const prereqSkill = skillMap.get(prereq.id);
225
+ if (prereqSkill && !visited.has(prereq.id)) {
226
+ await visit(prereqSkill);
227
+ }
228
+ }
229
+ visiting.delete(skill.id);
230
+ visited.add(skill.id);
231
+ result.push(skill);
232
+ };
233
+ // Visit all skills
234
+ for (const skill of skills) {
235
+ if (!visited.has(skill.id)) {
236
+ await visit(skill);
237
+ }
238
+ }
239
+ return result;
240
+ }
241
+ /**
242
+ * Group skills into learning sessions based on target duration
243
+ */
244
+ groupIntoSessions(skills, targetMinutes) {
245
+ const sessions = [];
246
+ let currentSession = [];
247
+ let currentDuration = 0;
248
+ for (const skill of skills) {
249
+ // Start new session if current would exceed target
250
+ if (currentDuration + skill.estimatedMinutes > targetMinutes &&
251
+ currentSession.length > 0) {
252
+ sessions.push(this.createSession(sessions.length + 1, currentSession));
253
+ currentSession = [];
254
+ currentDuration = 0;
255
+ }
256
+ currentSession.push(skill);
257
+ currentDuration += skill.estimatedMinutes;
258
+ }
259
+ // Add final session if not empty
260
+ if (currentSession.length > 0) {
261
+ sessions.push(this.createSession(sessions.length + 1, currentSession));
262
+ }
263
+ return sessions;
264
+ }
265
+ /**
266
+ * Create a learning session from a group of skills
267
+ */
268
+ createSession(sessionNumber, skills) {
269
+ const durationMinutes = skills.reduce((sum, s) => sum + s.estimatedMinutes, 0);
270
+ // Determine focus from most common tag or domain
271
+ const focus = this.determineFocus(skills);
272
+ // Build session object, only include focus if defined
273
+ const session = {
274
+ sessionNumber,
275
+ skills,
276
+ durationMinutes,
277
+ };
278
+ if (focus !== undefined) {
279
+ session.focus = focus;
280
+ }
281
+ return session;
282
+ }
283
+ /**
284
+ * Determine the focus/theme of a session
285
+ */
286
+ determineFocus(skills) {
287
+ // Count tag occurrences
288
+ const tagCounts = new Map();
289
+ for (const skill of skills) {
290
+ for (const tag of skill.tags) {
291
+ tagCounts.set(tag, (tagCounts.get(tag) ?? 0) + 1);
292
+ }
293
+ }
294
+ // Find most common tag
295
+ let maxCount = 0;
296
+ let focus;
297
+ for (const [tag, count] of tagCounts) {
298
+ if (count > maxCount) {
299
+ maxCount = count;
300
+ focus = tag;
301
+ }
302
+ }
303
+ return focus;
304
+ }
305
+ }
306
+ exports.PathGenerator = PathGenerator;
307
+ /**
308
+ * Create a path generator
309
+ */
310
+ function createPathGenerator(storage) {
311
+ return new PathGenerator(storage);
312
+ }
313
+ //# sourceMappingURL=path.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"path.js","sourceRoot":"","sources":["../../../src/query/path.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,298 @@
1
+ "use strict";
2
+ /**
3
+ * Spaced Repetition Scheduler (SM-2 Algorithm)
4
+ *
5
+ * Implements the SuperMemo SM-2 algorithm for optimal review scheduling.
6
+ * Schedules reviews at increasing intervals based on performance.
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.SpacedRepetitionScheduler = exports.SM2_CONSTANTS = exports.REVIEW_DEFAULTS = void 0;
12
+ exports.createSpacedRepetitionScheduler = createSpacedRepetitionScheduler;
13
+ exports.calculateSM2 = calculateSM2;
14
+ /**
15
+ * Default options for review scheduling
16
+ */
17
+ exports.REVIEW_DEFAULTS = {
18
+ includeUpcoming: true,
19
+ upcomingDays: 7,
20
+ maxReviews: 50,
21
+ };
22
+ /**
23
+ * SM-2 algorithm constants
24
+ */
25
+ exports.SM2_CONSTANTS = {
26
+ /** Minimum easiness factor */
27
+ MIN_EASINESS: 1.3,
28
+ /** Default easiness factor */
29
+ DEFAULT_EASINESS: 2.5,
30
+ /** Maximum easiness factor */
31
+ MAX_EASINESS: 3.5,
32
+ /** Initial interval (days) after first review */
33
+ INITIAL_INTERVAL: 1,
34
+ /** Second interval (days) after second review */
35
+ SECOND_INTERVAL: 6,
36
+ };
37
+ /**
38
+ * Spaced Repetition Scheduler
39
+ *
40
+ * Uses the SM-2 algorithm to schedule optimal review times.
41
+ * The algorithm adjusts intervals based on recall quality:
42
+ * - Quality 5: Perfect response
43
+ * - Quality 4: Correct with hesitation
44
+ * - Quality 3: Correct with difficulty
45
+ * - Quality 2: Incorrect but easily recalled
46
+ * - Quality 1: Incorrect, remembered when shown
47
+ * - Quality 0: Complete blackout
48
+ *
49
+ * @example
50
+ * ```typescript
51
+ * const scheduler = new SpacedRepetitionScheduler(storage);
52
+ *
53
+ * // Get review schedule
54
+ * const schedule = await scheduler.getSchedule(learnerState);
55
+ * console.log(`${schedule.dueNow.length} reviews due now`);
56
+ *
57
+ * // Update after a review
58
+ * const result = scheduler.calculateNextReview(masteryState, 4); // quality 4
59
+ * console.log(`Next review in ${result.intervalDays} days`);
60
+ * ```
61
+ */
62
+ class SpacedRepetitionScheduler {
63
+ storage;
64
+ constructor(storage) {
65
+ this.storage = storage;
66
+ }
67
+ /**
68
+ * Get the review schedule for a learner
69
+ *
70
+ * @param learner - Current learner state
71
+ * @param options - Schedule options
72
+ * @returns Review schedule with due and upcoming items
73
+ */
74
+ async getSchedule(learner, options = {}) {
75
+ const opts = { ...exports.REVIEW_DEFAULTS, ...options };
76
+ const now = new Date();
77
+ const upcomingCutoff = new Date(now.getTime() + opts.upcomingDays * 24 * 60 * 60 * 1000);
78
+ // Get all skills that have been practiced
79
+ const allSkills = await this.storage.findSkills({});
80
+ const dueNow = [];
81
+ const upcoming = [];
82
+ for (const skill of allSkills) {
83
+ const mastery = learner.masteryStates.get(skill.id);
84
+ if (!mastery) {
85
+ continue; // Never practiced
86
+ }
87
+ // Only schedule reviews for skills with some mastery
88
+ if (mastery.mastery < 0.1) {
89
+ continue;
90
+ }
91
+ const nextReview = this.calculateNextReviewDate(mastery);
92
+ const priority = this.calculatePriority(mastery, nextReview, now);
93
+ const reviewItem = {
94
+ skill,
95
+ state: {
96
+ mastery: mastery.mastery,
97
+ lastAttempt: mastery.lastAttempt,
98
+ streak: mastery.streak,
99
+ },
100
+ nextReview,
101
+ priority,
102
+ };
103
+ if (nextReview <= now) {
104
+ dueNow.push(reviewItem);
105
+ }
106
+ else if (opts.includeUpcoming && nextReview <= upcomingCutoff) {
107
+ upcoming.push(reviewItem);
108
+ }
109
+ }
110
+ // Sort by priority (highest first)
111
+ dueNow.sort((a, b) => b.priority - a.priority);
112
+ upcoming.sort((a, b) => a.nextReview.getTime() - b.nextReview.getTime());
113
+ // Apply limits
114
+ const limitedDue = opts.maxReviews ? dueNow.slice(0, opts.maxReviews) : dueNow;
115
+ const remainingLimit = opts.maxReviews
116
+ ? Math.max(0, opts.maxReviews - limitedDue.length)
117
+ : undefined;
118
+ const limitedUpcoming = remainingLimit !== undefined
119
+ ? upcoming.slice(0, remainingLimit)
120
+ : upcoming;
121
+ // Calculate total review time
122
+ const totalReviewMinutes = [...limitedDue, ...limitedUpcoming].reduce((sum, item) => sum + Math.ceil(item.skill.estimatedMinutes * 0.3), // Reviews are ~30% of initial learning time
123
+ 0);
124
+ return {
125
+ dueNow: limitedDue,
126
+ upcoming: limitedUpcoming,
127
+ totalReviewMinutes,
128
+ };
129
+ }
130
+ /**
131
+ * Calculate the next review interval using SM-2 algorithm
132
+ *
133
+ * @param currentState - Current mastery state
134
+ * @param quality - Review quality (0-5)
135
+ * @returns Updated SM-2 parameters
136
+ */
137
+ calculateNextReview(currentState, quality) {
138
+ const now = new Date();
139
+ // Get current SM-2 state
140
+ let easiness = currentState.easinessFactor ?? exports.SM2_CONSTANTS.DEFAULT_EASINESS;
141
+ let repetitions = currentState.streak;
142
+ let interval;
143
+ // Quality must be >= 3 for successful recall
144
+ if (quality >= 3) {
145
+ // Successful recall
146
+ if (repetitions === 0) {
147
+ interval = exports.SM2_CONSTANTS.INITIAL_INTERVAL;
148
+ }
149
+ else if (repetitions === 1) {
150
+ interval = exports.SM2_CONSTANTS.SECOND_INTERVAL;
151
+ }
152
+ else {
153
+ // Get previous interval from last attempt
154
+ const lastInterval = this.estimatePreviousInterval(currentState);
155
+ interval = Math.round(lastInterval * easiness);
156
+ }
157
+ repetitions++;
158
+ }
159
+ else {
160
+ // Failed recall - reset to beginning
161
+ repetitions = 0;
162
+ interval = exports.SM2_CONSTANTS.INITIAL_INTERVAL;
163
+ }
164
+ // Update easiness factor
165
+ // EF = EF + (0.1 - (5 - q) * (0.08 + (5 - q) * 0.02))
166
+ const easinessChange = 0.1 - (5 - quality) * (0.08 + (5 - quality) * 0.02);
167
+ easiness = Math.max(exports.SM2_CONSTANTS.MIN_EASINESS, Math.min(exports.SM2_CONSTANTS.MAX_EASINESS, easiness + easinessChange));
168
+ // Calculate next review date
169
+ const nextReview = new Date(now.getTime() + interval * 24 * 60 * 60 * 1000);
170
+ return {
171
+ easinessFactor: easiness,
172
+ intervalDays: interval,
173
+ nextReview,
174
+ repetitions,
175
+ };
176
+ }
177
+ /**
178
+ * Convert review quality to mastery update
179
+ *
180
+ * @param quality - Review quality (0-5)
181
+ * @returns Mastery level adjustment
182
+ */
183
+ qualityToMasteryAdjustment(quality) {
184
+ // Map quality to mastery change
185
+ switch (quality) {
186
+ case 5: return 0.1; // Perfect - increase mastery
187
+ case 4: return 0.05; // Correct with hesitation
188
+ case 3: return 0.0; // Correct with difficulty - maintain
189
+ case 2: return -0.1; // Incorrect but close
190
+ case 1: return -0.2; // Poor recall
191
+ case 0: return -0.3; // Complete failure
192
+ }
193
+ }
194
+ /**
195
+ * Calculate next review date based on current mastery state
196
+ */
197
+ calculateNextReviewDate(mastery) {
198
+ if (!mastery.lastAttempt) {
199
+ return new Date(); // Due immediately
200
+ }
201
+ const easiness = mastery.easinessFactor ?? exports.SM2_CONSTANTS.DEFAULT_EASINESS;
202
+ const streak = mastery.streak;
203
+ // Calculate interval based on streak
204
+ let interval;
205
+ if (streak === 0) {
206
+ interval = exports.SM2_CONSTANTS.INITIAL_INTERVAL;
207
+ }
208
+ else if (streak === 1) {
209
+ interval = exports.SM2_CONSTANTS.SECOND_INTERVAL;
210
+ }
211
+ else {
212
+ // Exponential growth based on streak and easiness
213
+ interval = Math.round(exports.SM2_CONSTANTS.SECOND_INTERVAL * Math.pow(easiness, streak - 1));
214
+ }
215
+ // Clamp to reasonable bounds (1 day to 1 year)
216
+ interval = Math.max(1, Math.min(365, interval));
217
+ return new Date(mastery.lastAttempt.getTime() + interval * 24 * 60 * 60 * 1000);
218
+ }
219
+ /**
220
+ * Estimate previous interval from mastery state
221
+ */
222
+ estimatePreviousInterval(mastery) {
223
+ const streak = mastery.streak;
224
+ const easiness = mastery.easinessFactor ?? exports.SM2_CONSTANTS.DEFAULT_EASINESS;
225
+ if (streak <= 1) {
226
+ return exports.SM2_CONSTANTS.INITIAL_INTERVAL;
227
+ }
228
+ else if (streak === 2) {
229
+ return exports.SM2_CONSTANTS.SECOND_INTERVAL;
230
+ }
231
+ else {
232
+ return Math.round(exports.SM2_CONSTANTS.SECOND_INTERVAL * Math.pow(easiness, streak - 2));
233
+ }
234
+ }
235
+ /**
236
+ * Calculate review priority
237
+ * Higher priority = more urgent to review
238
+ */
239
+ calculatePriority(mastery, nextReview, now) {
240
+ // Base priority on how overdue the review is
241
+ const overdueDays = (now.getTime() - nextReview.getTime()) / (24 * 60 * 60 * 1000);
242
+ // Higher mastery items get slightly lower priority (we want to maintain them)
243
+ const masteryFactor = 1 - mastery.mastery * 0.3;
244
+ // Combine factors
245
+ // Overdue items get positive priority, upcoming items get negative
246
+ return overdueDays * masteryFactor * 10;
247
+ }
248
+ }
249
+ exports.SpacedRepetitionScheduler = SpacedRepetitionScheduler;
250
+ /**
251
+ * Create a spaced repetition scheduler
252
+ */
253
+ function createSpacedRepetitionScheduler(storage) {
254
+ return new SpacedRepetitionScheduler(storage);
255
+ }
256
+ /**
257
+ * Standalone SM-2 calculation (for use without storage)
258
+ *
259
+ * @param quality - Review quality (0-5)
260
+ * @param previousEasiness - Previous easiness factor
261
+ * @param previousInterval - Previous interval in days
262
+ * @param repetitions - Number of previous successful repetitions
263
+ * @returns Updated SM-2 parameters
264
+ */
265
+ function calculateSM2(quality, previousEasiness = exports.SM2_CONSTANTS.DEFAULT_EASINESS, previousInterval = 0, repetitions = 0) {
266
+ const now = new Date();
267
+ let easiness = previousEasiness;
268
+ let interval;
269
+ let newRepetitions = repetitions;
270
+ if (quality >= 3) {
271
+ // Successful recall
272
+ if (newRepetitions === 0) {
273
+ interval = exports.SM2_CONSTANTS.INITIAL_INTERVAL;
274
+ }
275
+ else if (newRepetitions === 1) {
276
+ interval = exports.SM2_CONSTANTS.SECOND_INTERVAL;
277
+ }
278
+ else {
279
+ interval = Math.round(previousInterval * easiness);
280
+ }
281
+ newRepetitions++;
282
+ }
283
+ else {
284
+ // Failed - reset
285
+ interval = exports.SM2_CONSTANTS.INITIAL_INTERVAL;
286
+ newRepetitions = 0;
287
+ }
288
+ // Update easiness
289
+ const easinessChange = 0.1 - (5 - quality) * (0.08 + (5 - quality) * 0.02);
290
+ easiness = Math.max(exports.SM2_CONSTANTS.MIN_EASINESS, Math.min(exports.SM2_CONSTANTS.MAX_EASINESS, easiness + easinessChange));
291
+ return {
292
+ easinessFactor: easiness,
293
+ intervalDays: interval,
294
+ nextReview: new Date(now.getTime() + interval * 24 * 60 * 60 * 1000),
295
+ repetitions: newRepetitions,
296
+ };
297
+ }
298
+ //# sourceMappingURL=spaced-repetition.js.map