@vue-skuilder/db 0.1.18 → 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 (59) hide show
  1. package/dist/{classroomDB-BgfrVb8d.d.ts → classroomDB-CZdMBiTU.d.ts} +71 -2
  2. package/dist/{classroomDB-CTOenngH.d.cts → classroomDB-PxDZTky3.d.cts} +71 -2
  3. package/dist/core/index.d.cts +80 -6
  4. package/dist/core/index.d.ts +80 -6
  5. package/dist/core/index.js +370 -52
  6. package/dist/core/index.js.map +1 -1
  7. package/dist/core/index.mjs +369 -52
  8. package/dist/core/index.mjs.map +1 -1
  9. package/dist/{dataLayerProvider-D6PoCwS6.d.cts → dataLayerProvider-D0MoZMjH.d.cts} +1 -1
  10. package/dist/{dataLayerProvider-CZxC9GtB.d.ts → dataLayerProvider-D8o6ZnKW.d.ts} +1 -1
  11. package/dist/impl/couch/index.d.cts +4 -3
  12. package/dist/impl/couch/index.d.ts +4 -3
  13. package/dist/impl/couch/index.js +371 -55
  14. package/dist/impl/couch/index.js.map +1 -1
  15. package/dist/impl/couch/index.mjs +371 -55
  16. package/dist/impl/couch/index.mjs.map +1 -1
  17. package/dist/impl/static/index.d.cts +5 -4
  18. package/dist/impl/static/index.d.ts +5 -4
  19. package/dist/impl/static/index.js +356 -44
  20. package/dist/impl/static/index.js.map +1 -1
  21. package/dist/impl/static/index.mjs +356 -44
  22. package/dist/impl/static/index.mjs.map +1 -1
  23. package/dist/{index-D-Fa4Smt.d.cts → 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.cts +10 -10
  26. package/dist/index.d.ts +10 -10
  27. package/dist/index.js +382 -55
  28. package/dist/index.js.map +1 -1
  29. package/dist/index.mjs +381 -55
  30. package/dist/index.mjs.map +1 -1
  31. package/dist/{types-CzPDLAK6.d.cts → types-Bn0itutr.d.cts} +1 -1
  32. package/dist/{types-CewsN87z.d.ts → types-DQaXnuoc.d.ts} +1 -1
  33. package/dist/{types-legacy-6ettoclI.d.cts → types-legacy-DDY4N-Uq.d.cts} +3 -1
  34. package/dist/{types-legacy-6ettoclI.d.ts → types-legacy-DDY4N-Uq.d.ts} +3 -1
  35. package/dist/util/packer/index.d.cts +3 -3
  36. package/dist/util/packer/index.d.ts +3 -3
  37. package/docs/navigators-architecture.md +115 -10
  38. package/package.json +4 -4
  39. package/src/core/index.ts +1 -0
  40. package/src/core/interfaces/courseDB.ts +13 -0
  41. package/src/core/interfaces/userDB.ts +32 -0
  42. package/src/core/navigators/Pipeline.ts +127 -14
  43. package/src/core/navigators/filters/index.ts +3 -0
  44. package/src/core/navigators/filters/userTagPreference.ts +232 -0
  45. package/src/core/navigators/hierarchyDefinition.ts +4 -4
  46. package/src/core/navigators/index.ts +59 -0
  47. package/src/core/navigators/inferredPreference.ts +107 -0
  48. package/src/core/navigators/interferenceMitigator.ts +1 -13
  49. package/src/core/navigators/relativePriority.ts +2 -14
  50. package/src/core/navigators/userGoal.ts +136 -0
  51. package/src/core/types/strategyState.ts +84 -0
  52. package/src/core/types/types-legacy.ts +2 -0
  53. package/src/impl/common/BaseUserDB.ts +74 -7
  54. package/src/impl/couch/adminDB.ts +1 -2
  55. package/src/impl/couch/courseDB.ts +30 -10
  56. package/src/impl/static/courseDB.ts +11 -0
  57. package/tests/core/navigators/Pipeline.test.ts +1 -0
  58. package/docs/todo-pipeline-optimization.md +0 -117
  59. package/docs/todo-strategy-state-storage.md +0 -278
@@ -0,0 +1,232 @@
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 './types';
9
+
10
+ // ============================================================================
11
+ // USER TAG PREFERENCE FILTER
12
+ // ============================================================================
13
+ //
14
+ // Allows users to personalize their learning experience by specifying:
15
+ // - Tags to boost/penalize (score multiplied by boost factor)
16
+ //
17
+ // User preferences are stored in STRATEGY_STATE documents in the user's
18
+ // database, enabling persistence across sessions and sync across devices.
19
+ //
20
+ // Use cases:
21
+ // - Goal-based learning: "I want to learn piano by ear, skip sight-reading"
22
+ // - Selective focus: "I only want to practice chess endgames"
23
+ // - Accessibility: "Skip text-heavy cards, prefer visual content"
24
+ // - Difficulty customization: "Skip beginner content I already know"
25
+ //
26
+ // ============================================================================
27
+
28
+ /**
29
+ * User's tag preference state, stored in STRATEGY_STATE document.
30
+ *
31
+ * This interface defines what gets persisted to the user's database.
32
+ * UI components write to this structure, and the filter reads from it.
33
+ *
34
+ * ## Preferences vs Goals
35
+ *
36
+ * Preferences are **path constraints** — they affect HOW the user learns,
37
+ * not WHAT they're trying to learn. Examples:
38
+ * - "Skip text-heavy cards" (accessibility)
39
+ * - "Prefer visual content"
40
+ *
41
+ * For **goal-based** filtering (defining WHAT to learn), see the separate
42
+ * UserGoalNavigator (stub). Goals affect progress tracking and completion
43
+ * criteria; preferences only affect card selection.
44
+ *
45
+ * ## Slider Semantics
46
+ *
47
+ * Each tag maps to a multiplier value in the `boost` record:
48
+ * - `0` = banish/exclude (card score = 0)
49
+ * - `0.5` = penalize by 50%
50
+ * - `1.0` = neutral/no effect (default when tag added)
51
+ * - `2.0` = 2x preference boost
52
+ * - Higher values = stronger preference
53
+ *
54
+ * If multiple tags on a card have preferences, the maximum multiplier wins.
55
+ */
56
+ export interface UserTagPreferenceState {
57
+ /**
58
+ * Tag-specific multipliers.
59
+ * Maps tag name to score multiplier (0 = exclude, 1 = neutral, >1 = boost).
60
+ */
61
+ boost: Record<string, number>;
62
+
63
+ /**
64
+ * ISO timestamp of last update.
65
+ * Use `moment.utc(updatedAt)` to parse into a Moment object.
66
+ */
67
+ updatedAt: string;
68
+ }
69
+
70
+ /**
71
+ * A filter that applies user-configured tag preferences.
72
+ *
73
+ * Reads preferences from STRATEGY_STATE document in user's database.
74
+ * If no preferences exist, passes through unchanged (no-op).
75
+ *
76
+ * Implements CardFilter for use in Pipeline architecture.
77
+ * Also extends ContentNavigator for compatibility with dynamic loading.
78
+ */
79
+ export default class UserTagPreferenceFilter extends ContentNavigator implements CardFilter {
80
+ private _strategyData: ContentNavigationStrategyData;
81
+
82
+ /** Human-readable name for CardFilter interface */
83
+ name: string;
84
+
85
+ constructor(
86
+ user: UserDBInterface,
87
+ course: CourseDBInterface,
88
+ strategyData: ContentNavigationStrategyData
89
+ ) {
90
+ super(user, course, strategyData);
91
+ this._strategyData = strategyData;
92
+ this.name = strategyData.name || 'User Tag Preferences';
93
+ }
94
+
95
+ /**
96
+ * Compute multiplier for a card based on its tags and user preferences.
97
+ * Returns the maximum multiplier among all matching tags, or 1.0 if no matches.
98
+ */
99
+ private computeMultiplier(cardTags: string[], boostMap: Record<string, number>): number {
100
+ const multipliers = cardTags
101
+ .map((tag) => boostMap[tag])
102
+ .filter((val) => val !== undefined);
103
+
104
+ if (multipliers.length === 0) {
105
+ return 1.0;
106
+ }
107
+
108
+ // Use max multiplier among matching tags
109
+ return Math.max(...multipliers);
110
+ }
111
+
112
+ /**
113
+ * Build human-readable reason for the filter's decision.
114
+ */
115
+ private buildReason(
116
+ cardTags: string[],
117
+ boostMap: Record<string, number>,
118
+ multiplier: number
119
+ ): string {
120
+ // Find which tag(s) contributed to the multiplier
121
+ const matchingTags = cardTags.filter((tag) => boostMap[tag] === multiplier);
122
+
123
+ if (multiplier === 0) {
124
+ return `Excluded by user preference: ${matchingTags.join(', ')} (${multiplier}x)`;
125
+ }
126
+
127
+ if (multiplier < 1.0) {
128
+ return `Penalized by user preference: ${matchingTags.join(', ')} (${multiplier.toFixed(2)}x)`;
129
+ }
130
+
131
+ if (multiplier > 1.0) {
132
+ return `Boosted by user preference: ${matchingTags.join(', ')} (${multiplier.toFixed(2)}x)`;
133
+ }
134
+
135
+ return 'No matching user preferences';
136
+ }
137
+
138
+ /**
139
+ * CardFilter.transform implementation.
140
+ *
141
+ * Apply user tag preferences:
142
+ * 1. Read preferences from strategy state
143
+ * 2. If no preferences, pass through unchanged
144
+ * 3. For each card:
145
+ * - Look up tag in boost record
146
+ * - If tag found: apply multiplier (0 = exclude, 1 = neutral, >1 = boost)
147
+ * - If multiple tags match: use max multiplier
148
+ * - Append provenance with clear reason
149
+ */
150
+ async transform(cards: WeightedCard[], _context: FilterContext): Promise<WeightedCard[]> {
151
+ // Read user preferences from strategy state
152
+ const prefs = await this.getStrategyState<UserTagPreferenceState>();
153
+
154
+
155
+ // No preferences configured → pass through unchanged
156
+ if (!prefs || Object.keys(prefs.boost).length === 0) {
157
+ return cards.map((card) => ({
158
+ ...card,
159
+ provenance: [
160
+ ...card.provenance,
161
+ {
162
+ strategy: 'userTagPreference',
163
+ strategyName: this.strategyName || this.name,
164
+ strategyId: this.strategyId || this._strategyData._id,
165
+ action: 'passed' as const,
166
+ score: card.score,
167
+ reason: 'No user tag preferences configured',
168
+ },
169
+ ],
170
+ }));
171
+ }
172
+
173
+ // Process each card
174
+ const adjusted: WeightedCard[] = await Promise.all(
175
+ cards.map(async (card) => {
176
+ const cardTags = card.tags ?? [];
177
+
178
+ // Compute multiplier based on card tags and user preferences
179
+ const multiplier = this.computeMultiplier(cardTags, prefs.boost);
180
+ const finalScore = Math.min(1, card.score * multiplier);
181
+
182
+ // Determine action for provenance
183
+ let action: 'passed' | 'boosted' | 'penalized';
184
+ if (multiplier === 0 || multiplier < 1.0) {
185
+ action = 'penalized';
186
+ } else if (multiplier > 1.0) {
187
+ action = 'boosted';
188
+ } else {
189
+ action = 'passed';
190
+ }
191
+
192
+ return {
193
+ ...card,
194
+ score: finalScore,
195
+ provenance: [
196
+ ...card.provenance,
197
+ {
198
+ strategy: 'userTagPreference',
199
+ strategyName: this.strategyName || this.name,
200
+ strategyId: this.strategyId || this._strategyData._id,
201
+ action,
202
+ score: finalScore,
203
+ reason: this.buildReason(cardTags, prefs.boost, multiplier),
204
+ },
205
+ ],
206
+ };
207
+ })
208
+ );
209
+
210
+ return adjusted;
211
+ }
212
+
213
+ /**
214
+ * Legacy getWeightedCards - throws as filters should not be used as generators.
215
+ */
216
+ async getWeightedCards(_limit: number): Promise<WeightedCard[]> {
217
+ throw new Error(
218
+ 'UserTagPreferenceFilter is a filter and should not be used as a generator. ' +
219
+ 'Use Pipeline with a generator and this filter via transform().'
220
+ );
221
+ }
222
+
223
+ // Legacy methods - stub implementations since filters don't generate cards
224
+
225
+ async getNewCards(_n?: number): Promise<StudySessionNewItem[]> {
226
+ return [];
227
+ }
228
+
229
+ async getPendingReviews(): Promise<(StudySessionReviewItem & ScheduledCard)[]> {
230
+ return [];
231
+ }
232
+ }
@@ -158,14 +158,14 @@ export default class HierarchyDefinitionNavigator extends ContentNavigator imple
158
158
  * Check if a card is unlocked and generate reason.
159
159
  */
160
160
  private async checkCardUnlock(
161
- cardId: string,
161
+ card: WeightedCard,
162
162
  course: CourseDBInterface,
163
163
  unlockedTags: Set<string>,
164
164
  masteredTags: Set<string>
165
165
  ): Promise<{ isUnlocked: boolean; reason: string }> {
166
166
  try {
167
- const tagResponse = await course.getAppliedTags(cardId);
168
- const cardTags = tagResponse.rows.map((row) => row.value?.name || row.key);
167
+ // Pipeline hydrates tags before filters run
168
+ const cardTags = card.tags ?? [];
169
169
 
170
170
  // Check each tag's prerequisite status
171
171
  const lockedTags = cardTags.filter(
@@ -214,7 +214,7 @@ export default class HierarchyDefinitionNavigator extends ContentNavigator imple
214
214
 
215
215
  for (const card of cards) {
216
216
  const { isUnlocked, reason } = await this.checkCardUnlock(
217
- card.cardId,
217
+ card,
218
218
  context.course,
219
219
  unlockedTags,
220
220
  masteredTags
@@ -137,6 +137,11 @@ export interface WeightedCard {
137
137
  * First entry is from the generator, subsequent entries from filters.
138
138
  */
139
139
  provenance: StrategyContribution[];
140
+ /**
141
+ * Pre-fetched tags. Populated by Pipeline before filters run.
142
+ * Filters should use this instead of querying getAppliedTags() individually.
143
+ */
144
+ tags?: string[];
140
145
  }
141
146
 
142
147
  /**
@@ -173,6 +178,7 @@ export enum Navigators {
173
178
  HIERARCHY = 'hierarchyDefinition',
174
179
  INTERFERENCE = 'interferenceMitigator',
175
180
  RELATIVE_PRIORITY = 'relativePriority',
181
+ USER_TAG_PREFERENCE = 'userTagPreference',
176
182
  }
177
183
 
178
184
  // ============================================================================
@@ -211,6 +217,7 @@ export const NavigatorRoles: Record<Navigators, NavigatorRole> = {
211
217
  [Navigators.HIERARCHY]: NavigatorRole.FILTER,
212
218
  [Navigators.INTERFERENCE]: NavigatorRole.FILTER,
213
219
  [Navigators.RELATIVE_PRIORITY]: NavigatorRole.FILTER,
220
+ [Navigators.USER_TAG_PREFERENCE]: NavigatorRole.FILTER,
214
221
  };
215
222
 
216
223
  /**
@@ -275,6 +282,58 @@ export abstract class ContentNavigator implements StudyContentSource {
275
282
  }
276
283
  }
277
284
 
285
+ // ============================================================================
286
+ // STRATEGY STATE HELPERS
287
+ // ============================================================================
288
+ //
289
+ // These methods allow strategies to persist their own state (user preferences,
290
+ // learned patterns, temporal tracking) in the user database.
291
+ //
292
+ // ============================================================================
293
+
294
+ /**
295
+ * Unique key identifying this strategy for state storage.
296
+ *
297
+ * Defaults to the constructor name (e.g., "UserTagPreferenceFilter").
298
+ * Override in subclasses if multiple instances of the same strategy type
299
+ * need separate state storage.
300
+ */
301
+ protected get strategyKey(): string {
302
+ return this.constructor.name;
303
+ }
304
+
305
+ /**
306
+ * Get this strategy's persisted state for the current course.
307
+ *
308
+ * @returns The strategy's data payload, or null if no state exists
309
+ * @throws Error if user or course is not initialized
310
+ */
311
+ protected async getStrategyState<T>(): Promise<T | null> {
312
+ if (!this.user || !this.course) {
313
+ throw new Error(
314
+ `Cannot get strategy state: navigator not properly initialized. ` +
315
+ `Ensure user and course are provided to constructor.`
316
+ );
317
+ }
318
+ return this.user.getStrategyState<T>(this.course.getCourseID(), this.strategyKey);
319
+ }
320
+
321
+ /**
322
+ * Persist this strategy's state for the current course.
323
+ *
324
+ * @param data - The strategy's data payload to store
325
+ * @throws Error if user or course is not initialized
326
+ */
327
+ protected async putStrategyState<T>(data: T): Promise<void> {
328
+ if (!this.user || !this.course) {
329
+ throw new Error(
330
+ `Cannot put strategy state: navigator not properly initialized. ` +
331
+ `Ensure user and course are provided to constructor.`
332
+ );
333
+ }
334
+ return this.user.putStrategyState<T>(this.course.getCourseID(), this.strategyKey, data);
335
+ }
336
+
278
337
  /**
279
338
  * Factory method to create navigator instances dynamically.
280
339
  *
@@ -0,0 +1,107 @@
1
+ // ============================================================================
2
+ // INFERRED PREFERENCE NAVIGATOR — STUB
3
+ // ============================================================================
4
+ //
5
+ // STATUS: NOT IMPLEMENTED — This file documents architectural intent only.
6
+ //
7
+ // ============================================================================
8
+ //
9
+ // ## Purpose
10
+ //
11
+ // Inferred preferences are learned from user behavior, as opposed to explicit
12
+ // preferences which are configured via UI. The system observes patterns in
13
+ // user interactions and adjusts card selection accordingly.
14
+ //
15
+ // ## Inference Signals
16
+ //
17
+ // Potential signals to learn from:
18
+ //
19
+ // 1. **Card dismissal patterns**: User consistently skips certain card types
20
+ // 2. **Time-on-card**: User spends less time on certain content (boredom?)
21
+ // 3. **Error patterns**: User struggles with certain presentation styles
22
+ // 4. **Session timing**: User performs better at certain times of day
23
+ // 5. **Tag success rates**: User masters some tags faster than others
24
+ //
25
+ // ## Inferred State (Proposed)
26
+ //
27
+ // ```typescript
28
+ // interface InferredPreferenceState {
29
+ // // Learned tag affinities (positive = user does well, negative = struggles)
30
+ // tagAffinities: Record<string, number>;
31
+ //
32
+ // // Presentation style preferences
33
+ // preferredStyles: {
34
+ // visualVsText: number; // -1 to 1 (negative = text, positive = visual)
35
+ // shortVsLong: number; // -1 to 1 (negative = long, positive = short)
36
+ // };
37
+ //
38
+ // // Temporal patterns
39
+ // optimalSessionLength: number; // minutes
40
+ // optimalTimeOfDay: number; // hour (0-23)
41
+ //
42
+ // // Confidence in inferences
43
+ // sampleSize: number;
44
+ // lastUpdated: string;
45
+ // }
46
+ // ```
47
+ //
48
+ // ## Relationship to Explicit Preferences
49
+ //
50
+ // - Explicit preferences (UserTagPreferenceFilter) always take precedence
51
+ // - Inferred preferences act as soft suggestions when no explicit pref exists
52
+ // - User can "lock in" an inference as an explicit preference via UI
53
+ // - User can dismiss/override an inference ("I actually like text cards")
54
+ //
55
+ // ## Transparency Requirements
56
+ //
57
+ // Inferred preferences must be:
58
+ //
59
+ // 1. **Visible**: User can see what the system has inferred
60
+ // 2. **Explainable**: "We noticed you master visual cards faster"
61
+ // 3. **Overridable**: User can disable or invert any inference
62
+ // 4. **Forgettable**: User can reset inferences and start fresh
63
+ //
64
+ // ## Implementation Considerations
65
+ //
66
+ // 1. **Cold start**: Need minimum sample size before inferring
67
+ // 2. **Drift**: Preferences may change over time; use decay/recency weighting
68
+ // 3. **Privacy**: Inference data is personal; handle with care
69
+ // 4. **Bias**: Avoid reinforcing accidental patterns as permanent preferences
70
+ //
71
+ // ## Related Files
72
+ //
73
+ // - `filters/userTagPreference.ts` — Explicit preferences (takes precedence)
74
+ // - `userGoal.ts` — Goals (destination, not path)
75
+ // - `../types/strategyState.ts` — Storage mechanism
76
+ //
77
+ // ## Next Steps
78
+ //
79
+ // 1. Define minimum viable inference signals
80
+ // 2. Design inference algorithms (simple heuristics vs ML)
81
+ // 3. Build transparency UI ("Here's what we learned about you")
82
+ // 4. Implement override/dismiss mechanism
83
+ // 5. Add to card record collection for inference input
84
+ //
85
+ // ============================================================================
86
+
87
+ // Placeholder export to make this a valid module
88
+ export const INFERRED_PREFERENCE_NAVIGATOR_STUB = true;
89
+
90
+ /**
91
+ * @stub InferredPreferenceNavigator
92
+ *
93
+ * A navigator that learns user preferences from behavior patterns.
94
+ * See module-level documentation for architectural intent.
95
+ *
96
+ * NOT IMPLEMENTED — This is a design placeholder.
97
+ */
98
+ export interface InferredPreferenceState {
99
+ /** Learned affinity scores per tag (-1 to 1) */
100
+ tagAffinities: Record<string, number>;
101
+
102
+ /** Number of card interactions used to build inferences */
103
+ sampleSize: number;
104
+
105
+ /** ISO timestamp of last inference update */
106
+ updatedAt: string;
107
+ }
@@ -234,18 +234,6 @@ export default class InterferenceMitigatorNavigator extends ContentNavigator imp
234
234
  return avoid;
235
235
  }
236
236
 
237
- /**
238
- * Get tags for a single card
239
- */
240
- private async getCardTags(cardId: string, course: CourseDBInterface): Promise<string[]> {
241
- try {
242
- const tagResponse = await course.getAppliedTags(cardId);
243
- return tagResponse.rows.map((row) => row.value?.name || row.key).filter(Boolean);
244
- } catch {
245
- return [];
246
- }
247
- }
248
-
249
237
  /**
250
238
  * Compute interference score reduction for a card.
251
239
  * Returns: { multiplier, interfering tags, reason }
@@ -313,7 +301,7 @@ export default class InterferenceMitigatorNavigator extends ContentNavigator imp
313
301
  const adjusted: WeightedCard[] = [];
314
302
 
315
303
  for (const card of cards) {
316
- const cardTags = await this.getCardTags(card.cardId, context.course);
304
+ const cardTags = card.tags ?? [];
317
305
  const { multiplier, reason } = this.computeInterferenceEffect(
318
306
  cardTags,
319
307
  tagsToAvoid,
@@ -190,28 +190,16 @@ export default class RelativePriorityNavigator extends ContentNavigator implemen
190
190
  }
191
191
  }
192
192
 
193
- /**
194
- * Get tags for a single card.
195
- */
196
- private async getCardTags(cardId: string, course: CourseDBInterface): Promise<string[]> {
197
- try {
198
- const tagResponse = await course.getAppliedTags(cardId);
199
- return tagResponse.rows.map((r) => r.doc?.name).filter((x): x is string => !!x);
200
- } catch {
201
- return [];
202
- }
203
- }
204
-
205
193
  /**
206
194
  * CardFilter.transform implementation.
207
195
  *
208
196
  * Apply priority-adjusted scoring. Cards with high-priority tags get boosted,
209
197
  * cards with low-priority tags get reduced scores.
210
198
  */
211
- async transform(cards: WeightedCard[], context: FilterContext): Promise<WeightedCard[]> {
199
+ async transform(cards: WeightedCard[], _context: FilterContext): Promise<WeightedCard[]> {
212
200
  const adjusted: WeightedCard[] = await Promise.all(
213
201
  cards.map(async (card) => {
214
- const cardTags = await this.getCardTags(card.cardId, context.course);
202
+ const cardTags = card.tags ?? [];
215
203
  const priority = this.computeCardPriority(cardTags);
216
204
  const boostFactor = this.computeBoostFactor(priority);
217
205
  const finalScore = Math.max(0, Math.min(1, card.score * boostFactor));
@@ -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
+ }