learngraph 0.4.0 → 0.7.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/api/routes/analytics.js +288 -0
- package/dist/cjs/api/routes/analytics.js.map +1 -0
- package/dist/cjs/api/routes/assessments.js +269 -0
- package/dist/cjs/api/routes/assessments.js.map +1 -0
- package/dist/cjs/api/routes/curriculum.js +345 -0
- package/dist/cjs/api/routes/curriculum.js.map +1 -0
- package/dist/cjs/api/routes/edges.js +162 -0
- package/dist/cjs/api/routes/edges.js.map +1 -0
- package/dist/cjs/api/routes/explore.js +224 -0
- package/dist/cjs/api/routes/explore.js.map +1 -0
- package/dist/cjs/api/routes/learners.js +324 -0
- package/dist/cjs/api/routes/learners.js.map +1 -0
- package/dist/cjs/api/routes/me.js +404 -0
- package/dist/cjs/api/routes/me.js.map +1 -0
- package/dist/cjs/api/routes/skills.js +319 -0
- package/dist/cjs/api/routes/skills.js.map +1 -0
- package/dist/cjs/api/server.js +185 -0
- package/dist/cjs/api/server.js.map +1 -0
- package/dist/cjs/api/types.js +10 -0
- package/dist/cjs/api/types.js.map +1 -0
- package/dist/cjs/assessment/adaptive.js +390 -0
- package/dist/cjs/assessment/adaptive.js.map +1 -0
- package/dist/cjs/assessment/bkt.js +362 -0
- package/dist/cjs/assessment/bkt.js.map +1 -0
- package/dist/cjs/assessment/index.js +54 -0
- package/dist/cjs/assessment/index.js.map +1 -0
- package/dist/cjs/assessment/irt.js +420 -0
- package/dist/cjs/assessment/irt.js.map +1 -0
- package/dist/cjs/assessment/mastery-engine.js +411 -0
- package/dist/cjs/assessment/mastery-engine.js.map +1 -0
- package/dist/cjs/components/LearningPathView.js +320 -0
- package/dist/cjs/components/LearningPathView.js.map +1 -0
- package/dist/cjs/components/ProgressDashboard.js +308 -0
- package/dist/cjs/components/ProgressDashboard.js.map +1 -0
- package/dist/cjs/components/SkillCard.js +264 -0
- package/dist/cjs/components/SkillCard.js.map +1 -0
- package/dist/cjs/components/SkillExplorer.js +401 -0
- package/dist/cjs/components/SkillExplorer.js.map +1 -0
- package/dist/cjs/components/SkillGraph.js +435 -0
- package/dist/cjs/components/SkillGraph.js.map +1 -0
- package/dist/cjs/components/hooks.js +510 -0
- package/dist/cjs/components/hooks.js.map +1 -0
- package/dist/cjs/components/index.js +77 -0
- package/dist/cjs/components/index.js.map +1 -0
- package/dist/cjs/components/types.js +34 -0
- package/dist/cjs/components/types.js.map +1 -0
- package/dist/cjs/index.js +36 -2
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/llm/adapters/anthropic.js +91 -3
- package/dist/cjs/llm/adapters/anthropic.js.map +1 -1
- package/dist/cjs/llm/adapters/gemini.js +101 -8
- package/dist/cjs/llm/adapters/gemini.js.map +1 -1
- package/dist/cjs/llm/adapters/index.js +42 -1
- package/dist/cjs/llm/adapters/index.js.map +1 -1
- package/dist/cjs/llm/adapters/ollama.js +120 -3
- package/dist/cjs/llm/adapters/ollama.js.map +1 -1
- package/dist/cjs/llm/adapters/openai.js +108 -2
- package/dist/cjs/llm/adapters/openai.js.map +1 -1
- package/dist/cjs/mcp/cli.js +302 -0
- package/dist/cjs/mcp/cli.js.map +1 -0
- package/dist/cjs/mcp/index.js +79 -0
- package/dist/cjs/mcp/index.js.map +1 -0
- package/dist/cjs/mcp/prompts.js +425 -0
- package/dist/cjs/mcp/prompts.js.map +1 -0
- package/dist/cjs/mcp/resources.js +371 -0
- package/dist/cjs/mcp/resources.js.map +1 -0
- package/dist/cjs/mcp/server.js +410 -0
- package/dist/cjs/mcp/server.js.map +1 -0
- package/dist/cjs/mcp/tools.js +612 -0
- package/dist/cjs/mcp/tools.js.map +1 -0
- package/dist/cjs/mcp/types.js +10 -0
- package/dist/cjs/mcp/types.js.map +1 -0
- package/dist/cjs/query/index.js +23 -4
- package/dist/cjs/query/index.js.map +1 -1
- package/dist/cjs/query/path.js +313 -0
- package/dist/cjs/query/path.js.map +1 -0
- package/dist/cjs/query/spaced-repetition.js +298 -0
- package/dist/cjs/query/spaced-repetition.js.map +1 -0
- package/dist/cjs/query/zpd.js +216 -0
- package/dist/cjs/query/zpd.js.map +1 -0
- package/dist/cjs/types/assessment.js +46 -0
- package/dist/cjs/types/assessment.js.map +1 -0
- package/dist/cjs/types/bloom.js +12 -1
- package/dist/cjs/types/bloom.js.map +1 -1
- package/dist/cjs/types/index.js +7 -1
- package/dist/cjs/types/index.js.map +1 -1
- package/dist/esm/api/routes/analytics.js +285 -0
- package/dist/esm/api/routes/analytics.js.map +1 -0
- package/dist/esm/api/routes/assessments.js +266 -0
- package/dist/esm/api/routes/assessments.js.map +1 -0
- package/dist/esm/api/routes/curriculum.js +342 -0
- package/dist/esm/api/routes/curriculum.js.map +1 -0
- package/dist/esm/api/routes/edges.js +159 -0
- package/dist/esm/api/routes/edges.js.map +1 -0
- package/dist/esm/api/routes/explore.js +221 -0
- package/dist/esm/api/routes/explore.js.map +1 -0
- package/dist/esm/api/routes/learners.js +321 -0
- package/dist/esm/api/routes/learners.js.map +1 -0
- package/dist/esm/api/routes/me.js +401 -0
- package/dist/esm/api/routes/me.js.map +1 -0
- package/dist/esm/api/routes/skills.js +316 -0
- package/dist/esm/api/routes/skills.js.map +1 -0
- package/dist/esm/api/server.js +179 -0
- package/dist/esm/api/server.js.map +1 -0
- package/dist/esm/api/types.js +9 -0
- package/dist/esm/api/types.js.map +1 -0
- package/dist/esm/assessment/adaptive.js +384 -0
- package/dist/esm/assessment/adaptive.js.map +1 -0
- package/dist/esm/assessment/bkt.js +354 -0
- package/dist/esm/assessment/bkt.js.map +1 -0
- package/dist/esm/assessment/index.js +21 -0
- package/dist/esm/assessment/index.js.map +1 -0
- package/dist/esm/assessment/irt.js +406 -0
- package/dist/esm/assessment/irt.js.map +1 -0
- package/dist/esm/assessment/mastery-engine.js +406 -0
- package/dist/esm/assessment/mastery-engine.js.map +1 -0
- package/dist/esm/components/LearningPathView.js +316 -0
- package/dist/esm/components/LearningPathView.js.map +1 -0
- package/dist/esm/components/ProgressDashboard.js +304 -0
- package/dist/esm/components/ProgressDashboard.js.map +1 -0
- package/dist/esm/components/SkillCard.js +260 -0
- package/dist/esm/components/SkillCard.js.map +1 -0
- package/dist/esm/components/SkillExplorer.js +397 -0
- package/dist/esm/components/SkillExplorer.js.map +1 -0
- package/dist/esm/components/SkillGraph.js +398 -0
- package/dist/esm/components/SkillGraph.js.map +1 -0
- package/dist/esm/components/hooks.js +502 -0
- package/dist/esm/components/hooks.js.map +1 -0
- package/dist/esm/components/index.js +61 -0
- package/dist/esm/components/index.js.map +1 -0
- package/dist/esm/components/types.js +31 -0
- package/dist/esm/components/types.js.map +1 -0
- package/dist/esm/index.js +13 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/llm/adapters/anthropic.js +88 -2
- package/dist/esm/llm/adapters/anthropic.js.map +1 -1
- package/dist/esm/llm/adapters/gemini.js +98 -7
- package/dist/esm/llm/adapters/gemini.js.map +1 -1
- package/dist/esm/llm/adapters/index.js +15 -4
- package/dist/esm/llm/adapters/index.js.map +1 -1
- package/dist/esm/llm/adapters/ollama.js +117 -2
- package/dist/esm/llm/adapters/ollama.js.map +1 -1
- package/dist/esm/llm/adapters/openai.js +105 -1
- package/dist/esm/llm/adapters/openai.js.map +1 -1
- package/dist/esm/mcp/cli.js +267 -0
- package/dist/esm/mcp/cli.js.map +1 -0
- package/dist/esm/mcp/index.js +39 -0
- package/dist/esm/mcp/index.js.map +1 -0
- package/dist/esm/mcp/prompts.js +419 -0
- package/dist/esm/mcp/prompts.js.map +1 -0
- package/dist/esm/mcp/resources.js +359 -0
- package/dist/esm/mcp/resources.js.map +1 -0
- package/dist/esm/mcp/server.js +372 -0
- package/dist/esm/mcp/server.js.map +1 -0
- package/dist/esm/mcp/tools.js +598 -0
- package/dist/esm/mcp/tools.js.map +1 -0
- package/dist/esm/mcp/types.js +9 -0
- package/dist/esm/mcp/types.js.map +1 -0
- package/dist/esm/query/index.js +11 -5
- package/dist/esm/query/index.js.map +1 -1
- package/dist/esm/query/path.js +308 -0
- package/dist/esm/query/path.js.map +1 -0
- package/dist/esm/query/spaced-repetition.js +292 -0
- package/dist/esm/query/spaced-repetition.js.map +1 -0
- package/dist/esm/query/zpd.js +211 -0
- package/dist/esm/query/zpd.js.map +1 -0
- package/dist/esm/types/assessment.js +40 -0
- package/dist/esm/types/assessment.js.map +1 -0
- package/dist/esm/types/bloom.js +11 -0
- package/dist/esm/types/bloom.js.map +1 -1
- package/dist/esm/types/index.js +2 -1
- package/dist/esm/types/index.js.map +1 -1
- package/dist/types/api/routes/analytics.d.ts +14 -0
- package/dist/types/api/routes/analytics.d.ts.map +1 -0
- package/dist/types/api/routes/assessments.d.ts +14 -0
- package/dist/types/api/routes/assessments.d.ts.map +1 -0
- package/dist/types/api/routes/curriculum.d.ts +14 -0
- package/dist/types/api/routes/curriculum.d.ts.map +1 -0
- package/dist/types/api/routes/edges.d.ts +14 -0
- package/dist/types/api/routes/edges.d.ts.map +1 -0
- package/dist/types/api/routes/explore.d.ts +14 -0
- package/dist/types/api/routes/explore.d.ts.map +1 -0
- package/dist/types/api/routes/learners.d.ts +14 -0
- package/dist/types/api/routes/learners.d.ts.map +1 -0
- package/dist/types/api/routes/me.d.ts +14 -0
- package/dist/types/api/routes/me.d.ts.map +1 -0
- package/dist/types/api/routes/skills.d.ts +14 -0
- package/dist/types/api/routes/skills.d.ts.map +1 -0
- package/dist/types/api/server.d.ts +147 -0
- package/dist/types/api/server.d.ts.map +1 -0
- package/dist/types/api/types.d.ts +443 -0
- package/dist/types/api/types.d.ts.map +1 -0
- package/dist/types/assessment/adaptive.d.ts +155 -0
- package/dist/types/assessment/adaptive.d.ts.map +1 -0
- package/dist/types/assessment/bkt.d.ts +185 -0
- package/dist/types/assessment/bkt.d.ts.map +1 -0
- package/dist/types/assessment/index.d.ts +18 -0
- package/dist/types/assessment/index.d.ts.map +1 -0
- package/dist/types/assessment/irt.d.ts +159 -0
- package/dist/types/assessment/irt.d.ts.map +1 -0
- package/dist/types/assessment/mastery-engine.d.ts +178 -0
- package/dist/types/assessment/mastery-engine.d.ts.map +1 -0
- package/dist/types/components/LearningPathView.d.ts +40 -0
- package/dist/types/components/LearningPathView.d.ts.map +1 -0
- package/dist/types/components/ProgressDashboard.d.ts +49 -0
- package/dist/types/components/ProgressDashboard.d.ts.map +1 -0
- package/dist/types/components/SkillCard.d.ts +34 -0
- package/dist/types/components/SkillCard.d.ts.map +1 -0
- package/dist/types/components/SkillExplorer.d.ts +39 -0
- package/dist/types/components/SkillExplorer.d.ts.map +1 -0
- package/dist/types/components/SkillGraph.d.ts +38 -0
- package/dist/types/components/SkillGraph.d.ts.map +1 -0
- package/dist/types/components/hooks.d.ts +187 -0
- package/dist/types/components/hooks.d.ts.map +1 -0
- package/dist/types/components/index.d.ts +59 -0
- package/dist/types/components/index.d.ts.map +1 -0
- package/dist/types/components/types.d.ts +410 -0
- package/dist/types/components/types.d.ts.map +1 -0
- package/dist/types/index.d.ts +2 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/llm/adapters/anthropic.d.ts +84 -1
- package/dist/types/llm/adapters/anthropic.d.ts.map +1 -1
- package/dist/types/llm/adapters/gemini.d.ts +93 -6
- package/dist/types/llm/adapters/gemini.d.ts.map +1 -1
- package/dist/types/llm/adapters/index.d.ts +13 -4
- package/dist/types/llm/adapters/index.d.ts.map +1 -1
- package/dist/types/llm/adapters/ollama.d.ts +126 -1
- package/dist/types/llm/adapters/ollama.d.ts.map +1 -1
- package/dist/types/llm/adapters/openai.d.ts +104 -1
- package/dist/types/llm/adapters/openai.d.ts.map +1 -1
- package/dist/types/mcp/cli.d.ts +15 -0
- package/dist/types/mcp/cli.d.ts.map +1 -0
- package/dist/types/mcp/index.d.ts +32 -0
- package/dist/types/mcp/index.d.ts.map +1 -0
- package/dist/types/mcp/prompts.d.ts +27 -0
- package/dist/types/mcp/prompts.d.ts.map +1 -0
- package/dist/types/mcp/resources.d.ts +59 -0
- package/dist/types/mcp/resources.d.ts.map +1 -0
- package/dist/types/mcp/server.d.ts +136 -0
- package/dist/types/mcp/server.d.ts.map +1 -0
- package/dist/types/mcp/tools.d.ts +344 -0
- package/dist/types/mcp/tools.d.ts.map +1 -0
- package/dist/types/mcp/types.d.ts +137 -0
- package/dist/types/mcp/types.d.ts.map +1 -0
- package/dist/types/query/index.d.ts +8 -0
- package/dist/types/query/index.d.ts.map +1 -1
- package/dist/types/query/path.d.ts +102 -0
- package/dist/types/query/path.d.ts.map +1 -0
- package/dist/types/query/spaced-repetition.d.ts +135 -0
- package/dist/types/query/spaced-repetition.d.ts.map +1 -0
- package/dist/types/query/zpd.d.ts +97 -0
- package/dist/types/query/zpd.d.ts.map +1 -0
- package/dist/types/types/assessment.d.ts +512 -0
- package/dist/types/types/assessment.d.ts.map +1 -0
- package/dist/types/types/bloom.d.ts +4 -0
- package/dist/types/types/bloom.d.ts.map +1 -1
- package/dist/types/types/index.d.ts +3 -1
- package/dist/types/types/index.d.ts.map +1 -1
- package/package.json +48 -3
package/dist/cjs/query/index.js
CHANGED
|
@@ -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
|
-
|
|
9
|
-
//
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
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
|