@vue-skuilder/db 0.1.17 → 0.1.20

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 (92) hide show
  1. package/dist/{userDB-BqwxtJ_7.d.mts → classroomDB-CZdMBiTU.d.ts} +427 -104
  2. package/dist/{userDB-DNa0XPtn.d.ts → classroomDB-PxDZTky3.d.cts} +427 -104
  3. package/dist/core/index.d.cts +304 -0
  4. package/dist/core/index.d.ts +237 -25
  5. package/dist/core/index.js +2246 -118
  6. package/dist/core/index.js.map +1 -1
  7. package/dist/core/index.mjs +2235 -114
  8. package/dist/core/index.mjs.map +1 -1
  9. package/dist/{dataLayerProvider-VlngD19_.d.mts → dataLayerProvider-D0MoZMjH.d.cts} +1 -1
  10. package/dist/{dataLayerProvider-BV5iZqt_.d.ts → dataLayerProvider-D8o6ZnKW.d.ts} +1 -1
  11. package/dist/impl/couch/{index.d.mts → index.d.cts} +47 -5
  12. package/dist/impl/couch/index.d.ts +46 -4
  13. package/dist/impl/couch/index.js +2250 -134
  14. package/dist/impl/couch/index.js.map +1 -1
  15. package/dist/impl/couch/index.mjs +2212 -97
  16. package/dist/impl/couch/index.mjs.map +1 -1
  17. package/dist/impl/static/{index.d.mts → index.d.cts} +6 -6
  18. package/dist/impl/static/index.d.ts +5 -5
  19. package/dist/impl/static/index.js +1950 -143
  20. package/dist/impl/static/index.js.map +1 -1
  21. package/dist/impl/static/index.mjs +1922 -117
  22. package/dist/impl/static/index.mjs.map +1 -1
  23. package/dist/{index-Bmll7Xse.d.mts → index-B_j6u5E4.d.cts} +1 -1
  24. package/dist/{index-CD8BZz2k.d.ts → index-Dj0SEgk3.d.ts} +1 -1
  25. package/dist/{index.d.mts → index.d.cts} +97 -13
  26. package/dist/index.d.ts +96 -12
  27. package/dist/index.js +2439 -180
  28. package/dist/index.js.map +1 -1
  29. package/dist/index.mjs +2386 -135
  30. package/dist/index.mjs.map +1 -1
  31. package/dist/pouch/index.js +3 -3
  32. package/dist/{types-Dbp5DaRR.d.mts → types-Bn0itutr.d.cts} +1 -1
  33. package/dist/{types-CewsN87z.d.ts → types-DQaXnuoc.d.ts} +1 -1
  34. package/dist/{types-legacy-6ettoclI.d.ts → types-legacy-DDY4N-Uq.d.cts} +3 -1
  35. package/dist/{types-legacy-6ettoclI.d.mts → types-legacy-DDY4N-Uq.d.ts} +3 -1
  36. package/dist/util/packer/{index.d.mts → index.d.cts} +3 -3
  37. package/dist/util/packer/index.d.ts +3 -3
  38. package/dist/util/packer/index.js.map +1 -1
  39. package/dist/util/packer/index.mjs.map +1 -1
  40. package/docs/brainstorm-navigation-paradigm.md +369 -0
  41. package/docs/navigators-architecture.md +370 -0
  42. package/docs/todo-evolutionary-orchestration.md +310 -0
  43. package/docs/todo-nominal-tag-types.md +121 -0
  44. package/docs/todo-strategy-authoring.md +401 -0
  45. package/eslint.config.mjs +1 -1
  46. package/package.json +9 -4
  47. package/src/core/index.ts +1 -0
  48. package/src/core/interfaces/contentSource.ts +88 -4
  49. package/src/core/interfaces/courseDB.ts +13 -0
  50. package/src/core/interfaces/navigationStrategyManager.ts +0 -5
  51. package/src/core/interfaces/userDB.ts +32 -0
  52. package/src/core/navigators/CompositeGenerator.ts +268 -0
  53. package/src/core/navigators/Pipeline.ts +318 -0
  54. package/src/core/navigators/PipelineAssembler.ts +194 -0
  55. package/src/core/navigators/elo.ts +104 -15
  56. package/src/core/navigators/filters/eloDistance.ts +132 -0
  57. package/src/core/navigators/filters/index.ts +9 -0
  58. package/src/core/navigators/filters/types.ts +115 -0
  59. package/src/core/navigators/filters/userTagPreference.ts +232 -0
  60. package/src/core/navigators/generators/index.ts +2 -0
  61. package/src/core/navigators/generators/types.ts +107 -0
  62. package/src/core/navigators/hardcodedOrder.ts +111 -12
  63. package/src/core/navigators/hierarchyDefinition.ts +266 -0
  64. package/src/core/navigators/index.ts +404 -3
  65. package/src/core/navigators/inferredPreference.ts +107 -0
  66. package/src/core/navigators/interferenceMitigator.ts +355 -0
  67. package/src/core/navigators/relativePriority.ts +255 -0
  68. package/src/core/navigators/srs.ts +195 -0
  69. package/src/core/navigators/userGoal.ts +136 -0
  70. package/src/core/types/strategyState.ts +84 -0
  71. package/src/core/types/types-legacy.ts +2 -0
  72. package/src/impl/common/BaseUserDB.ts +74 -7
  73. package/src/impl/couch/adminDB.ts +1 -2
  74. package/src/impl/couch/classroomDB.ts +51 -0
  75. package/src/impl/couch/courseDB.ts +147 -49
  76. package/src/impl/static/courseDB.ts +11 -4
  77. package/src/study/SessionController.ts +149 -1
  78. package/src/study/TagFilteredContentSource.ts +255 -0
  79. package/src/study/index.ts +1 -0
  80. package/src/util/dataDirectory.test.ts +51 -22
  81. package/src/util/logger.ts +0 -1
  82. package/tests/core/navigators/CompositeGenerator.test.ts +455 -0
  83. package/tests/core/navigators/Pipeline.test.ts +406 -0
  84. package/tests/core/navigators/PipelineAssembler.test.ts +351 -0
  85. package/tests/core/navigators/SRSNavigator.test.ts +344 -0
  86. package/tests/core/navigators/eloDistanceFilter.test.ts +192 -0
  87. package/tests/core/navigators/navigators.test.ts +710 -0
  88. package/tsconfig.json +1 -1
  89. package/vitest.config.ts +29 -0
  90. package/dist/core/index.d.mts +0 -92
  91. /package/dist/{SyncStrategy-CyATpyLQ.d.mts → SyncStrategy-CyATpyLQ.d.cts} +0 -0
  92. /package/dist/pouch/{index.d.mts → index.d.cts} +0 -0
@@ -0,0 +1,255 @@
1
+ import type { ScheduledCard } from '../types/user';
2
+ import type { CourseDBInterface } from '../interfaces/courseDB';
3
+ import type { UserDBInterface } from '../interfaces/userDB';
4
+ import { ContentNavigator } from './index';
5
+ import type { WeightedCard } from './index';
6
+ import type { ContentNavigationStrategyData } from '../types/contentNavigationStrategy';
7
+ import type { StudySessionReviewItem, StudySessionNewItem } from '..';
8
+ import type { CardFilter, FilterContext } from './filters/types';
9
+
10
+ /**
11
+ * Configuration for the RelativePriority strategy.
12
+ *
13
+ * Course authors define priority weights for tags, allowing the system
14
+ * to prefer high-utility content (common, well-behaved patterns) over
15
+ * lower-utility content (rare, irregular patterns).
16
+ *
17
+ * Example use case: In phonics, prefer teaching 's' (common, consistent)
18
+ * before 'x' or 'z' (rare, sometimes irregular).
19
+ */
20
+ export interface RelativePriorityConfig {
21
+ /**
22
+ * Map of tag ID to priority weight (0-1).
23
+ *
24
+ * 1.0 = highest priority (present first)
25
+ * 0.5 = neutral
26
+ * 0.0 = lowest priority (defer until later)
27
+ *
28
+ * Example:
29
+ * {
30
+ * "letter-s": 0.95,
31
+ * "letter-t": 0.90,
32
+ * "letter-x": 0.10,
33
+ * "letter-z": 0.05
34
+ * }
35
+ */
36
+ tagPriorities: { [tagId: string]: number };
37
+
38
+ /**
39
+ * Priority for tags not explicitly listed (default: 0.5).
40
+ * 0.5 means unlisted tags have neutral effect on scoring.
41
+ */
42
+ defaultPriority?: number;
43
+
44
+ /**
45
+ * How to combine priorities when a card has multiple tags.
46
+ *
47
+ * - 'max': Use the highest priority among the card's tags (default)
48
+ * - 'average': Average all tag priorities
49
+ * - 'min': Use the lowest priority (conservative)
50
+ */
51
+ combineMode?: 'max' | 'average' | 'min';
52
+
53
+ /**
54
+ * How strongly priority influences the final score (0-1, default: 0.5).
55
+ *
56
+ * At 0.0: Priority has no effect (pure delegate scoring)
57
+ * At 0.5: Priority can boost/reduce scores by up to 25%
58
+ * At 1.0: Priority can boost/reduce scores by up to 50%
59
+ *
60
+ * The boost factor formula: 1 + (priority - 0.5) * priorityInfluence
61
+ * - Priority 1.0 with influence 0.5 → boost of 1.25
62
+ * - Priority 0.5 with influence 0.5 → boost of 1.00 (neutral)
63
+ * - Priority 0.0 with influence 0.5 → boost of 0.75
64
+ */
65
+ priorityInfluence?: number;
66
+ }
67
+
68
+ const DEFAULT_PRIORITY = 0.5;
69
+ const DEFAULT_PRIORITY_INFLUENCE = 0.5;
70
+ const DEFAULT_COMBINE_MODE: 'max' | 'average' | 'min' = 'max';
71
+
72
+ /**
73
+ * A filter strategy that boosts scores for high-utility content.
74
+ *
75
+ * Course authors assign priority weights to tags. Cards with high-priority
76
+ * tags get boosted scores, making them more likely to be presented first.
77
+ * This allows teaching the most useful, well-behaved concepts before
78
+ * moving on to rarer or more irregular ones.
79
+ *
80
+ * Example: When teaching phonics, prioritize common letters (s, t, a) over
81
+ * rare ones (x, z, q) by assigning higher priority weights to common letters.
82
+ *
83
+ * Implements CardFilter for use in Pipeline architecture.
84
+ * Also extends ContentNavigator for backward compatibility.
85
+ */
86
+ export default class RelativePriorityNavigator extends ContentNavigator implements CardFilter {
87
+ private config: RelativePriorityConfig;
88
+ private _strategyData: ContentNavigationStrategyData;
89
+
90
+ /** Human-readable name for CardFilter interface */
91
+ name: string;
92
+
93
+ constructor(
94
+ user: UserDBInterface,
95
+ course: CourseDBInterface,
96
+ _strategyData: ContentNavigationStrategyData
97
+ ) {
98
+ super(user, course, _strategyData);
99
+ this._strategyData = _strategyData;
100
+ this.config = this.parseConfig(_strategyData.serializedData);
101
+ this.name = _strategyData.name || 'Relative Priority';
102
+ }
103
+
104
+ private parseConfig(serializedData: string): RelativePriorityConfig {
105
+ try {
106
+ const parsed = JSON.parse(serializedData);
107
+ return {
108
+ tagPriorities: parsed.tagPriorities || {},
109
+ defaultPriority: parsed.defaultPriority ?? DEFAULT_PRIORITY,
110
+ combineMode: parsed.combineMode ?? DEFAULT_COMBINE_MODE,
111
+ priorityInfluence: parsed.priorityInfluence ?? DEFAULT_PRIORITY_INFLUENCE,
112
+ };
113
+ } catch {
114
+ // Return safe defaults if parsing fails
115
+ return {
116
+ tagPriorities: {},
117
+ defaultPriority: DEFAULT_PRIORITY,
118
+ combineMode: DEFAULT_COMBINE_MODE,
119
+ priorityInfluence: DEFAULT_PRIORITY_INFLUENCE,
120
+ };
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Look up the priority for a tag.
126
+ */
127
+ private getTagPriority(tagId: string): number {
128
+ return this.config.tagPriorities[tagId] ?? this.config.defaultPriority ?? DEFAULT_PRIORITY;
129
+ }
130
+
131
+ /**
132
+ * Compute combined priority for a card based on its tags.
133
+ */
134
+ private computeCardPriority(cardTags: string[]): number {
135
+ if (cardTags.length === 0) {
136
+ return this.config.defaultPriority ?? DEFAULT_PRIORITY;
137
+ }
138
+
139
+ const priorities = cardTags.map((tag) => this.getTagPriority(tag));
140
+
141
+ switch (this.config.combineMode) {
142
+ case 'max':
143
+ return Math.max(...priorities);
144
+ case 'min':
145
+ return Math.min(...priorities);
146
+ case 'average':
147
+ return priorities.reduce((sum, p) => sum + p, 0) / priorities.length;
148
+ default:
149
+ return Math.max(...priorities);
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Compute boost factor based on priority.
155
+ *
156
+ * The formula: 1 + (priority - 0.5) * priorityInfluence
157
+ *
158
+ * This creates a multiplier centered around 1.0:
159
+ * - Priority 1.0 with influence 0.5 → 1.25 (25% boost)
160
+ * - Priority 0.5 with any influence → 1.00 (neutral)
161
+ * - Priority 0.0 with influence 0.5 → 0.75 (25% reduction)
162
+ */
163
+ private computeBoostFactor(priority: number): number {
164
+ const influence = this.config.priorityInfluence ?? DEFAULT_PRIORITY_INFLUENCE;
165
+ return 1 + (priority - 0.5) * influence;
166
+ }
167
+
168
+ /**
169
+ * Build human-readable reason for priority adjustment.
170
+ */
171
+ private buildPriorityReason(
172
+ cardTags: string[],
173
+ priority: number,
174
+ boostFactor: number,
175
+ finalScore: number
176
+ ): string {
177
+ if (cardTags.length === 0) {
178
+ return `No tags, neutral priority (${priority.toFixed(2)})`;
179
+ }
180
+
181
+ const tagList = cardTags.slice(0, 3).join(', ');
182
+ const more = cardTags.length > 3 ? ` (+${cardTags.length - 3} more)` : '';
183
+
184
+ if (boostFactor === 1.0) {
185
+ return `Neutral priority (${priority.toFixed(2)}) for tags: ${tagList}${more}`;
186
+ } else if (boostFactor > 1.0) {
187
+ return `High-priority tags: ${tagList}${more} (priority ${priority.toFixed(2)} → boost ${boostFactor.toFixed(2)}x → ${finalScore.toFixed(2)})`;
188
+ } else {
189
+ return `Low-priority tags: ${tagList}${more} (priority ${priority.toFixed(2)} → reduce ${boostFactor.toFixed(2)}x → ${finalScore.toFixed(2)})`;
190
+ }
191
+ }
192
+
193
+ /**
194
+ * CardFilter.transform implementation.
195
+ *
196
+ * Apply priority-adjusted scoring. Cards with high-priority tags get boosted,
197
+ * cards with low-priority tags get reduced scores.
198
+ */
199
+ async transform(cards: WeightedCard[], _context: FilterContext): Promise<WeightedCard[]> {
200
+ const adjusted: WeightedCard[] = await Promise.all(
201
+ cards.map(async (card) => {
202
+ const cardTags = card.tags ?? [];
203
+ const priority = this.computeCardPriority(cardTags);
204
+ const boostFactor = this.computeBoostFactor(priority);
205
+ const finalScore = Math.max(0, Math.min(1, card.score * boostFactor));
206
+
207
+ // Determine action based on boost factor
208
+ const action = boostFactor > 1.0 ? 'boosted' : boostFactor < 1.0 ? 'penalized' : 'passed';
209
+
210
+ // Build reason explaining priority adjustment
211
+ const reason = this.buildPriorityReason(cardTags, priority, boostFactor, finalScore);
212
+
213
+ return {
214
+ ...card,
215
+ score: finalScore,
216
+ provenance: [
217
+ ...card.provenance,
218
+ {
219
+ strategy: 'relativePriority',
220
+ strategyName: this.strategyName || this.name,
221
+ strategyId: this.strategyId || 'NAVIGATION_STRATEGY-priority',
222
+ action,
223
+ score: finalScore,
224
+ reason,
225
+ },
226
+ ],
227
+ };
228
+ })
229
+ );
230
+
231
+ return adjusted;
232
+ }
233
+
234
+ /**
235
+ * Legacy getWeightedCards - now throws as filters should not be used as generators.
236
+ *
237
+ * Use transform() via Pipeline instead.
238
+ */
239
+ async getWeightedCards(_limit: number): Promise<WeightedCard[]> {
240
+ throw new Error(
241
+ 'RelativePriorityNavigator is a filter and should not be used as a generator. ' +
242
+ 'Use Pipeline with a generator and this filter via transform().'
243
+ );
244
+ }
245
+
246
+ // Legacy methods - stub implementations since filters don't generate cards
247
+
248
+ async getNewCards(_n?: number): Promise<StudySessionNewItem[]> {
249
+ return [];
250
+ }
251
+
252
+ async getPendingReviews(): Promise<(StudySessionReviewItem & ScheduledCard)[]> {
253
+ return [];
254
+ }
255
+ }
@@ -0,0 +1,195 @@
1
+ import moment from 'moment';
2
+ import type { ScheduledCard } from '../types/user';
3
+ import type { CourseDBInterface } from '../interfaces/courseDB';
4
+ import type { UserDBInterface } from '../interfaces/userDB';
5
+ import { ContentNavigator } from './index';
6
+ import type { WeightedCard } from './index';
7
+ import type { ContentNavigationStrategyData } from '../types/contentNavigationStrategy';
8
+ import type { StudySessionReviewItem, StudySessionNewItem } from '../interfaces/contentSource';
9
+ import type { CardGenerator, GeneratorContext } from './generators/types';
10
+
11
+ // ============================================================================
12
+ // SRS NAVIGATOR
13
+ // ============================================================================
14
+ //
15
+ // A generator strategy that scores review cards by urgency.
16
+ //
17
+ // Urgency is determined by two factors:
18
+ // 1. Overdueness - how far past the scheduled review time
19
+ // 2. Interval recency - shorter scheduled intervals indicate "novel content in progress"
20
+ //
21
+ // A card with a 3-day interval that's 2 days overdue is more urgent than a card
22
+ // with a 6-month interval that's 2 days overdue. The shorter interval represents
23
+ // active learning at higher resolution.
24
+ //
25
+ // This navigator only handles reviews - it does not generate new cards.
26
+ // For new cards, use ELONavigator or another generator via CompositeGenerator.
27
+ //
28
+ // ============================================================================
29
+
30
+ /**
31
+ * Configuration for the SRS strategy.
32
+ * Currently minimal - the algorithm is not parameterized.
33
+ */
34
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
35
+ export interface SRSConfig {
36
+ // Future: configurable urgency curves, thresholds, etc.
37
+ }
38
+
39
+ /**
40
+ * A navigation strategy that scores review cards by urgency.
41
+ *
42
+ * Implements CardGenerator for use in Pipeline architecture.
43
+ * Also extends ContentNavigator for backward compatibility with legacy code.
44
+ *
45
+ * Higher scores indicate more urgent reviews:
46
+ * - Cards that are more overdue (relative to their interval) score higher
47
+ * - Cards with shorter intervals (recent learning) score higher
48
+ *
49
+ * Only returns cards that are actually due (reviewTime has passed).
50
+ * Does not generate new cards - use with CompositeGenerator for mixed content.
51
+ */
52
+ export default class SRSNavigator extends ContentNavigator implements CardGenerator {
53
+ /** Human-readable name for CardGenerator interface */
54
+ name: string;
55
+
56
+ constructor(
57
+ user: UserDBInterface,
58
+ course: CourseDBInterface,
59
+ strategyData?: ContentNavigationStrategyData
60
+ ) {
61
+ super(user, course, strategyData as ContentNavigationStrategyData);
62
+ this.name = strategyData?.name || 'SRS';
63
+ }
64
+
65
+ /**
66
+ * Get review cards scored by urgency.
67
+ *
68
+ * Score formula combines:
69
+ * - Relative overdueness: hoursOverdue / intervalHours
70
+ * - Interval recency: exponential decay favoring shorter intervals
71
+ *
72
+ * Cards not yet due are excluded (not scored as 0).
73
+ *
74
+ * This method supports both the legacy signature (limit only) and the
75
+ * CardGenerator interface signature (limit, context).
76
+ *
77
+ * @param limit - Maximum number of cards to return
78
+ * @param _context - Optional GeneratorContext (currently unused, but required for interface)
79
+ */
80
+ async getWeightedCards(limit: number, _context?: GeneratorContext): Promise<WeightedCard[]> {
81
+ if (!this.user || !this.course) {
82
+ throw new Error('SRSNavigator requires user and course to be set');
83
+ }
84
+
85
+ const reviews = await this.user.getPendingReviews(this.course.getCourseID());
86
+ const now = moment.utc();
87
+
88
+ // Filter to only cards that are actually due
89
+ const dueReviews = reviews.filter((r) => now.isAfter(moment.utc(r.reviewTime)));
90
+
91
+ const scored = dueReviews.map((review) => {
92
+ const { score, reason } = this.computeUrgencyScore(review, now);
93
+
94
+ return {
95
+ cardId: review.cardId,
96
+ courseId: review.courseId,
97
+ score,
98
+ provenance: [
99
+ {
100
+ strategy: 'srs',
101
+ strategyName: this.strategyName || this.name,
102
+ strategyId: this.strategyId || 'NAVIGATION_STRATEGY-SRS-default',
103
+ action: 'generated' as const,
104
+ score,
105
+ reason,
106
+ },
107
+ ],
108
+ };
109
+ });
110
+
111
+ // Sort by score descending and limit
112
+ return scored.sort((a, b) => b.score - a.score).slice(0, limit);
113
+ }
114
+
115
+ /**
116
+ * Compute urgency score for a review card.
117
+ *
118
+ * Two factors:
119
+ * 1. Relative overdueness = hoursOverdue / intervalHours
120
+ * - 2 days overdue on 3-day interval = 0.67 (urgent)
121
+ * - 2 days overdue on 180-day interval = 0.01 (not urgent)
122
+ *
123
+ * 2. Interval recency factor = 0.3 + 0.7 * exp(-intervalHours / 720)
124
+ * - 24h interval → ~1.0 (very recent learning)
125
+ * - 30 days (720h) → ~0.56
126
+ * - 180 days → ~0.30
127
+ *
128
+ * Combined: base 0.5 + weighted average of factors * 0.45
129
+ * Result range: approximately 0.5 to 0.95
130
+ */
131
+ private computeUrgencyScore(
132
+ review: ScheduledCard,
133
+ now: moment.Moment
134
+ ): { score: number; reason: string } {
135
+ const scheduledAt = moment.utc(review.scheduledAt);
136
+ const due = moment.utc(review.reviewTime);
137
+
138
+ // Interval = time between scheduling and due date (minimum 1 hour to avoid division issues)
139
+ const intervalHours = Math.max(1, due.diff(scheduledAt, 'hours'));
140
+ const hoursOverdue = now.diff(due, 'hours');
141
+
142
+ // Relative overdueness: how late relative to the interval
143
+ const relativeOverdue = hoursOverdue / intervalHours;
144
+
145
+ // Interval recency factor: shorter intervals = more urgent
146
+ // Exponential decay with 720h (30 days) as the characteristic time
147
+ const recencyFactor = 0.3 + 0.7 * Math.exp(-intervalHours / 720);
148
+
149
+ // Combined urgency: weighted average of relative overdue and recency
150
+ // Clamp relative overdue contribution to [0, 1] to avoid runaway scores
151
+ const overdueContribution = Math.min(1.0, Math.max(0, relativeOverdue));
152
+ const urgency = overdueContribution * 0.5 + recencyFactor * 0.5;
153
+
154
+ // Final score: base 0.5 + urgency contribution, capped at 0.95
155
+ const score = Math.min(0.95, 0.5 + urgency * 0.45);
156
+
157
+ const reason =
158
+ `${Math.round(hoursOverdue)}h overdue (interval: ${Math.round(intervalHours)}h, ` +
159
+ `relative: ${relativeOverdue.toFixed(2)}), recency: ${recencyFactor.toFixed(2)}, review`;
160
+
161
+ return { score, reason };
162
+ }
163
+
164
+ /**
165
+ * Get pending reviews in legacy format.
166
+ *
167
+ * Returns all pending reviews for the course, enriched with session item fields.
168
+ */
169
+ async getPendingReviews(): Promise<(StudySessionReviewItem & ScheduledCard)[]> {
170
+ if (!this.user || !this.course) {
171
+ throw new Error('SRSNavigator requires user and course to be set');
172
+ }
173
+
174
+ const reviews = await this.user.getPendingReviews(this.course.getCourseID());
175
+
176
+ return reviews.map((r) => ({
177
+ ...r,
178
+ contentSourceType: 'course' as const,
179
+ contentSourceID: this.course!.getCourseID(),
180
+ cardID: r.cardId,
181
+ courseID: r.courseId,
182
+ qualifiedID: `${r.courseId}-${r.cardId}`,
183
+ reviewID: r._id,
184
+ status: 'review' as const,
185
+ }));
186
+ }
187
+
188
+ /**
189
+ * SRS does not generate new cards.
190
+ * Use ELONavigator or another generator for new cards.
191
+ */
192
+ async getNewCards(_n?: number): Promise<StudySessionNewItem[]> {
193
+ return [];
194
+ }
195
+ }
@@ -0,0 +1,136 @@
1
+ // ============================================================================
2
+ // USER GOAL NAVIGATOR — STUB
3
+ // ============================================================================
4
+ //
5
+ // STATUS: NOT IMPLEMENTED — This file documents architectural intent only.
6
+ //
7
+ // ============================================================================
8
+ //
9
+ // ## Purpose
10
+ //
11
+ // Goals define WHAT the user wants to learn, as opposed to preferences which
12
+ // define HOW they want to learn. Goals affect:
13
+ //
14
+ // 1. **Content scoping**: Which tags/content are relevant to this user
15
+ // 2. **Progress tracking**: ELO is measured against goal-relevant content
16
+ // 3. **Completion criteria**: User is "done" when goal mastery is achieved
17
+ // 4. **Curriculum composition**: Goals enable cross-curriculum dependencies
18
+ //
19
+ // ## Goals vs Preferences
20
+ //
21
+ // | Aspect | Goal | Preference |
22
+ // |---------------|-------------------------------|-------------------------------|
23
+ // | Defines | Destination (what to learn) | Path (how to learn) |
24
+ // | Example | "Master ear-training" | "Skip text-heavy cards" |
25
+ // | Affects ELO | Yes — scopes what's tracked | No — just filters cards |
26
+ // | Completion | Yes — defines "done" | No — persists indefinitely |
27
+ // | Filter impl | UserGoalNavigator | UserTagPreferenceFilter |
28
+ //
29
+ // ## Curriculum Composition
30
+ //
31
+ // Goals enable software-style composition for curricula. A physics course
32
+ // can teach classical mechanics without owning the calculus prerequisites.
33
+ //
34
+ // Instead, it declares a dependency:
35
+ //
36
+ // ```typescript
37
+ // interface CurriculumDependency {
38
+ // // NPM-style package resolution
39
+ // curriculumId: string; // e.g., "@skuilder/calculus"
40
+ // version: string; // e.g., "^2.0.0" (semver)
41
+ //
42
+ // // Goal within that curriculum
43
+ // goal: string; // e.g., "differential-calculus"
44
+ //
45
+ // // How this maps to local prerequisites
46
+ // satisfiesLocalTags: string[]; // e.g., ["calculus-prereq"]
47
+ // }
48
+ // ```
49
+ //
50
+ // When a physics card requires "calculus-prereq", the system:
51
+ // 1. Checks if user has achieved the "differential-calculus" goal in @skuilder/calculus
52
+ // 2. If not, defers to that curriculum to teach the prerequisite
53
+ // 3. Returns to physics once the goal is satisfied
54
+ //
55
+ // This allows:
56
+ // - Specialized curricula (calculus experts author calculus content)
57
+ // - Reusable prerequisites across multiple courses
58
+ // - User can bring their own "calculus credential" from prior learning
59
+ //
60
+ // ## User Goal State (Proposed)
61
+ //
62
+ // ```typescript
63
+ // interface UserGoalState {
64
+ // // Primary goals — what the user wants to achieve
65
+ // targetTags: string[];
66
+ //
67
+ // // Excluded goals — content the user explicitly doesn't care about
68
+ // excludedTags: string[];
69
+ //
70
+ // // Cross-curriculum goals (for composition)
71
+ // externalGoals?: {
72
+ // curriculumId: string;
73
+ // goal: string;
74
+ // status: 'not-started' | 'in-progress' | 'achieved';
75
+ // }[];
76
+ //
77
+ // // When this goal configuration was set
78
+ // updatedAt: string;
79
+ // }
80
+ // ```
81
+ //
82
+ // ## Implementation Considerations
83
+ //
84
+ // 1. **ELO Scoping**: When goals are set, user ELO tracking should focus on
85
+ // goal-relevant tags. This may require changes to ELO update logic.
86
+ //
87
+ // 2. **Progress Reporting**: UI should show progress toward goals, not just
88
+ // overall course completion.
89
+ //
90
+ // 3. **Goal Achievement**: Need to define when a goal is "achieved" —
91
+ // probably ELO threshold + mastery percentage on goal-tagged content.
92
+ //
93
+ // 4. **Curriculum Registry**: For cross-curriculum composition, need a
94
+ // registry/resolver for curriculum packages (similar to npm registry).
95
+ //
96
+ // 5. **Interaction with HierarchyDefinition**: Goals should work with
97
+ // prerequisite chains — user can't skip prerequisites just because
98
+ // they're not part of their goal.
99
+ //
100
+ // ## Related Files
101
+ //
102
+ // - `filters/userTagPreference.ts` — Preferences (path constraints)
103
+ // - `hierarchyDefinition.ts` — Prerequisites (enforced regardless of goals)
104
+ // - `../types/strategyState.ts` — Storage mechanism for user state
105
+ //
106
+ // ## Next Steps
107
+ //
108
+ // 1. Design goal state schema in detail
109
+ // 2. Define goal achievement criteria
110
+ // 3. Implement goal-scoped ELO tracking
111
+ // 4. Build UI for goal configuration
112
+ // 5. Design curriculum dependency resolution
113
+ //
114
+ // ============================================================================
115
+
116
+ // Placeholder export to make this a valid module
117
+ export const USER_GOAL_NAVIGATOR_STUB = true;
118
+
119
+ /**
120
+ * @stub UserGoalNavigator
121
+ *
122
+ * A navigator that scopes learning to user-defined goals.
123
+ * See module-level documentation for architectural intent.
124
+ *
125
+ * NOT IMPLEMENTED — This is a design placeholder.
126
+ */
127
+ export interface UserGoalState {
128
+ /** Tags the user wants to master (defines "success") */
129
+ targetTags: string[];
130
+
131
+ /** Tags the user explicitly doesn't care about */
132
+ excludedTags: string[];
133
+
134
+ /** ISO timestamp of last update */
135
+ updatedAt: string;
136
+ }
@@ -0,0 +1,84 @@
1
+ import { DocType, DocTypePrefixes } from './types-legacy';
2
+
3
+ /**
4
+ * Template literal type for strategy state document IDs.
5
+ *
6
+ * Format: `STRATEGY_STATE-{courseId}-{strategyKey}`
7
+ */
8
+ export type StrategyStateId =
9
+ `${(typeof DocTypePrefixes)[DocType.STRATEGY_STATE]}::${string}::${string}`;
10
+
11
+ /**
12
+ * Document storing strategy-specific state in the user database.
13
+ *
14
+ * Each strategy can persist its own state (user preferences, learned patterns,
15
+ * temporal tracking, etc.) using this document type. The state is scoped to
16
+ * a (user, course, strategy) tuple.
17
+ *
18
+ * ## Use Cases
19
+ *
20
+ * 1. **Explicit user preferences**: User configures tag filters, difficulty
21
+ * preferences, or learning goals. UI writes to strategy state.
22
+ *
23
+ * 2. **Learned/temporal state**: Strategy tracks patterns over time, e.g.,
24
+ * "when did I last introduce confusable concepts together?"
25
+ *
26
+ * 3. **Adaptive personalization**: Strategy infers user preferences from
27
+ * behavior and stores them for future sessions.
28
+ *
29
+ * ## Storage Location
30
+ *
31
+ * These documents live in the **user database**, not the course database.
32
+ * They sync with the user's data across devices.
33
+ *
34
+ * ## Document ID Format
35
+ *
36
+ * `STRATEGY_STATE::{courseId}::{strategyKey}`
37
+ *
38
+ * Example: `STRATEGY_STATE::piano-basics::UserTagPreferenceFilter`
39
+ *
40
+ * @template T - The shape of the strategy-specific data payload
41
+ */
42
+ export interface StrategyStateDoc<T = unknown> {
43
+ _id: StrategyStateId;
44
+ _rev?: string;
45
+ docType: DocType.STRATEGY_STATE;
46
+
47
+ /**
48
+ * The course this state applies to.
49
+ */
50
+ courseId: string;
51
+
52
+ /**
53
+ * Unique key identifying the strategy instance.
54
+ * Typically the strategy class name (e.g., "UserTagPreferenceFilter",
55
+ * "InterferenceMitigatorNavigator").
56
+ *
57
+ * If a course has multiple instances of the same strategy type with
58
+ * different configurations, use a more specific key.
59
+ */
60
+ strategyKey: string;
61
+
62
+ /**
63
+ * Strategy-specific data payload.
64
+ * Each strategy defines its own schema for this field.
65
+ */
66
+ data: T;
67
+
68
+ /**
69
+ * ISO timestamp of last update.
70
+ * Use `moment.utc(updatedAt)` to parse into a Moment object.
71
+ */
72
+ updatedAt: string;
73
+ }
74
+
75
+ /**
76
+ * Build the document ID for a strategy state document.
77
+ *
78
+ * @param courseId - The course ID
79
+ * @param strategyKey - The strategy key (typically class name)
80
+ * @returns The document ID in format `STRATEGY_STATE::{courseId}::{strategyKey}`
81
+ */
82
+ export function buildStrategyStateId(courseId: string, strategyKey: string): StrategyStateId {
83
+ return `STRATEGY_STATE::${courseId}::${strategyKey}`;
84
+ }
@@ -19,6 +19,7 @@ export enum DocType {
19
19
  SCHEDULED_CARD = 'SCHEDULED_CARD',
20
20
  TAG = 'TAG',
21
21
  NAVIGATION_STRATEGY = 'NAVIGATION_STRATEGY',
22
+ STRATEGY_STATE = 'STRATEGY_STATE',
22
23
  }
23
24
 
24
25
  export interface QualifiedCardID {
@@ -103,6 +104,7 @@ export const DocTypePrefixes = {
103
104
  [DocType.VIEW]: 'VIEW',
104
105
  [DocType.PEDAGOGY]: 'PEDAGOGY',
105
106
  [DocType.NAVIGATION_STRATEGY]: 'NAVIGATION_STRATEGY',
107
+ [DocType.STRATEGY_STATE]: 'STRATEGY_STATE',
106
108
  } as const;
107
109
 
108
110
  export interface CardHistory<T extends CardRecord> {