@vue-skuilder/db 0.1.18 → 0.1.21

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 (87) hide show
  1. package/CLAUDE.md +2 -2
  2. package/dist/{classroomDB-BgfrVb8d.d.ts → contentSource-BP9hznNV.d.ts} +220 -197
  3. package/dist/{classroomDB-CTOenngH.d.cts → contentSource-DsJadoBU.d.cts} +220 -197
  4. package/dist/core/index.d.cts +80 -6
  5. package/dist/core/index.d.ts +80 -6
  6. package/dist/core/index.js +735 -1560
  7. package/dist/core/index.js.map +1 -1
  8. package/dist/core/index.mjs +708 -1539
  9. package/dist/core/index.mjs.map +1 -1
  10. package/dist/{dataLayerProvider-D6PoCwS6.d.cts → dataLayerProvider-CHYrQ5pB.d.cts} +1 -1
  11. package/dist/{dataLayerProvider-CZxC9GtB.d.ts → dataLayerProvider-MDTxXq2l.d.ts} +1 -1
  12. package/dist/impl/couch/index.d.cts +8 -23
  13. package/dist/impl/couch/index.d.ts +8 -23
  14. package/dist/impl/couch/index.js +723 -1578
  15. package/dist/impl/couch/index.js.map +1 -1
  16. package/dist/impl/couch/index.mjs +692 -1552
  17. package/dist/impl/couch/index.mjs.map +1 -1
  18. package/dist/impl/static/index.d.cts +25 -8
  19. package/dist/impl/static/index.d.ts +25 -8
  20. package/dist/impl/static/index.js +700 -1400
  21. package/dist/impl/static/index.js.map +1 -1
  22. package/dist/impl/static/index.mjs +688 -1393
  23. package/dist/impl/static/index.mjs.map +1 -1
  24. package/dist/{index-D-Fa4Smt.d.cts → index-B_j6u5E4.d.cts} +1 -1
  25. package/dist/{index-CD8BZz2k.d.ts → index-Dj0SEgk3.d.ts} +1 -1
  26. package/dist/index.d.cts +71 -63
  27. package/dist/index.d.ts +71 -63
  28. package/dist/index.js +1162 -1996
  29. package/dist/index.js.map +1 -1
  30. package/dist/index.mjs +1124 -1955
  31. package/dist/index.mjs.map +1 -1
  32. package/dist/pouch/index.js +3 -0
  33. package/dist/pouch/index.js.map +1 -1
  34. package/dist/pouch/index.mjs +3 -0
  35. package/dist/pouch/index.mjs.map +1 -1
  36. package/dist/{types-CzPDLAK6.d.cts → types-Bn0itutr.d.cts} +1 -1
  37. package/dist/{types-CewsN87z.d.ts → types-DQaXnuoc.d.ts} +1 -1
  38. package/dist/{types-legacy-6ettoclI.d.cts → types-legacy-DDY4N-Uq.d.cts} +3 -1
  39. package/dist/{types-legacy-6ettoclI.d.ts → types-legacy-DDY4N-Uq.d.ts} +3 -1
  40. package/dist/util/packer/index.d.cts +3 -3
  41. package/dist/util/packer/index.d.ts +3 -3
  42. package/docs/navigators-architecture.md +115 -17
  43. package/package.json +4 -4
  44. package/src/core/index.ts +1 -0
  45. package/src/core/interfaces/classroomDB.ts +5 -13
  46. package/src/core/interfaces/contentSource.ts +6 -66
  47. package/src/core/interfaces/courseDB.ts +15 -7
  48. package/src/core/interfaces/userDB.ts +32 -0
  49. package/src/core/navigators/Pipeline.ts +136 -52
  50. package/src/core/navigators/PipelineAssembler.ts +1 -1
  51. package/src/core/navigators/defaults.ts +84 -0
  52. package/src/core/navigators/{hierarchyDefinition.ts → filters/hierarchyDefinition.ts} +15 -29
  53. package/src/core/navigators/filters/index.ts +3 -0
  54. package/src/core/navigators/filters/inferredPreferenceStub.ts +107 -0
  55. package/src/core/navigators/{interferenceMitigator.ts → filters/interferenceMitigator.ts} +11 -37
  56. package/src/core/navigators/{relativePriority.ts → filters/relativePriority.ts} +12 -38
  57. package/src/core/navigators/filters/userGoalStub.ts +136 -0
  58. package/src/core/navigators/filters/userTagPreference.ts +217 -0
  59. package/src/core/navigators/{CompositeGenerator.ts → generators/CompositeGenerator.ts} +15 -64
  60. package/src/core/navigators/{elo.ts → generators/elo.ts} +13 -63
  61. package/src/core/navigators/{srs.ts → generators/srs.ts} +11 -40
  62. package/src/core/navigators/generators/types.ts +1 -1
  63. package/src/core/navigators/index.ts +95 -91
  64. package/src/core/types/strategyState.ts +84 -0
  65. package/src/core/types/types-legacy.ts +2 -0
  66. package/src/impl/common/BaseUserDB.ts +74 -7
  67. package/src/impl/couch/adminDB.ts +1 -2
  68. package/src/impl/couch/classroomDB.ts +100 -103
  69. package/src/impl/couch/courseDB.ts +35 -91
  70. package/src/impl/couch/pouchdb-setup.ts +7 -0
  71. package/src/impl/static/StaticDataUnpacker.ts +50 -1
  72. package/src/impl/static/courseDB.ts +87 -37
  73. package/src/study/SessionController.ts +122 -202
  74. package/src/study/SourceMixer.ts +65 -0
  75. package/src/study/TagFilteredContentSource.ts +49 -92
  76. package/src/study/index.ts +1 -0
  77. package/src/study/services/CardHydrationService.ts +165 -81
  78. package/src/util/dataDirectory.ts +1 -1
  79. package/src/util/index.ts +0 -1
  80. package/tests/core/navigators/CompositeGenerator.test.ts +44 -168
  81. package/tests/core/navigators/Pipeline.test.ts +6 -72
  82. package/tests/core/navigators/PipelineAssembler.test.ts +8 -58
  83. package/tests/core/navigators/navigators.test.ts +118 -151
  84. package/docs/todo-pipeline-optimization.md +0 -117
  85. package/docs/todo-strategy-state-storage.md +0 -278
  86. package/src/core/navigators/hardcodedOrder.ts +0 -163
  87. package/src/util/tuiLogger.ts +0 -139
@@ -1,11 +1,9 @@
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';
1
+ import type { CourseDBInterface } from '../../interfaces/courseDB';
2
+ import type { UserDBInterface } from '../../interfaces/userDB';
3
+ import { ContentNavigator } from '../index';
4
+ import type { WeightedCard } from '../index';
5
+ import type { ContentNavigationStrategyData } from '../../types/contentNavigationStrategy';
6
+ import type { CardFilter, FilterContext } from './types';
9
7
 
10
8
  /**
11
9
  * Configuration for the RelativePriority strategy.
@@ -85,7 +83,6 @@ const DEFAULT_COMBINE_MODE: 'max' | 'average' | 'min' = 'max';
85
83
  */
86
84
  export default class RelativePriorityNavigator extends ContentNavigator implements CardFilter {
87
85
  private config: RelativePriorityConfig;
88
- private _strategyData: ContentNavigationStrategyData;
89
86
 
90
87
  /** Human-readable name for CardFilter interface */
91
88
  name: string;
@@ -93,12 +90,11 @@ export default class RelativePriorityNavigator extends ContentNavigator implemen
93
90
  constructor(
94
91
  user: UserDBInterface,
95
92
  course: CourseDBInterface,
96
- _strategyData: ContentNavigationStrategyData
93
+ strategyData: ContentNavigationStrategyData
97
94
  ) {
98
- super(user, course, _strategyData);
99
- this._strategyData = _strategyData;
100
- this.config = this.parseConfig(_strategyData.serializedData);
101
- this.name = _strategyData.name || 'Relative Priority';
95
+ super(user, course, strategyData);
96
+ this.config = this.parseConfig(strategyData.serializedData);
97
+ this.name = strategyData.name || 'Relative Priority';
102
98
  }
103
99
 
104
100
  private parseConfig(serializedData: string): RelativePriorityConfig {
@@ -190,28 +186,16 @@ export default class RelativePriorityNavigator extends ContentNavigator implemen
190
186
  }
191
187
  }
192
188
 
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
189
  /**
206
190
  * CardFilter.transform implementation.
207
191
  *
208
192
  * Apply priority-adjusted scoring. Cards with high-priority tags get boosted,
209
193
  * cards with low-priority tags get reduced scores.
210
194
  */
211
- async transform(cards: WeightedCard[], context: FilterContext): Promise<WeightedCard[]> {
195
+ async transform(cards: WeightedCard[], _context: FilterContext): Promise<WeightedCard[]> {
212
196
  const adjusted: WeightedCard[] = await Promise.all(
213
197
  cards.map(async (card) => {
214
- const cardTags = await this.getCardTags(card.cardId, context.course);
198
+ const cardTags = card.tags ?? [];
215
199
  const priority = this.computeCardPriority(cardTags);
216
200
  const boostFactor = this.computeBoostFactor(priority);
217
201
  const finalScore = Math.max(0, Math.min(1, card.score * boostFactor));
@@ -254,14 +238,4 @@ export default class RelativePriorityNavigator extends ContentNavigator implemen
254
238
  'Use Pipeline with a generator and this filter via transform().'
255
239
  );
256
240
  }
257
-
258
- // Legacy methods - stub implementations since filters don't generate cards
259
-
260
- async getNewCards(_n?: number): Promise<StudySessionNewItem[]> {
261
- return [];
262
- }
263
-
264
- async getPendingReviews(): Promise<(StudySessionReviewItem & ScheduledCard)[]> {
265
- return [];
266
- }
267
241
  }
@@ -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,217 @@
1
+ import type { CourseDBInterface } from '../../interfaces/courseDB';
2
+ import type { UserDBInterface } from '../../interfaces/userDB';
3
+ import { ContentNavigator } from '../index';
4
+ import type { WeightedCard } from '../index';
5
+ import type { ContentNavigationStrategyData } from '../../types/contentNavigationStrategy';
6
+ import type { CardFilter, FilterContext } from './types';
7
+
8
+ // ============================================================================
9
+ // USER TAG PREFERENCE FILTER
10
+ // ============================================================================
11
+ //
12
+ // Allows users to personalize their learning experience by specifying:
13
+ // - Tags to boost/penalize (score multiplied by boost factor)
14
+ //
15
+ // User preferences are stored in STRATEGY_STATE documents in the user's
16
+ // database, enabling persistence across sessions and sync across devices.
17
+ //
18
+ // Use cases:
19
+ // - Goal-based learning: "I want to learn piano by ear, skip sight-reading"
20
+ // - Selective focus: "I only want to practice chess endgames"
21
+ // - Accessibility: "Skip text-heavy cards, prefer visual content"
22
+ // - Difficulty customization: "Skip beginner content I already know"
23
+ //
24
+ // ============================================================================
25
+
26
+ /**
27
+ * User's tag preference state, stored in STRATEGY_STATE document.
28
+ *
29
+ * This interface defines what gets persisted to the user's database.
30
+ * UI components write to this structure, and the filter reads from it.
31
+ *
32
+ * ## Preferences vs Goals
33
+ *
34
+ * Preferences are **path constraints** — they affect HOW the user learns,
35
+ * not WHAT they're trying to learn. Examples:
36
+ * - "Skip text-heavy cards" (accessibility)
37
+ * - "Prefer visual content"
38
+ *
39
+ * For **goal-based** filtering (defining WHAT to learn), see the separate
40
+ * UserGoalNavigator (stub). Goals affect progress tracking and completion
41
+ * criteria; preferences only affect card selection.
42
+ *
43
+ * ## Slider Semantics
44
+ *
45
+ * Each tag maps to a multiplier value in the `boost` record:
46
+ * - `0` = banish/exclude (card score = 0)
47
+ * - `0.5` = penalize by 50%
48
+ * - `1.0` = neutral/no effect (default when tag added)
49
+ * - `2.0` = 2x preference boost
50
+ * - Higher values = stronger preference
51
+ *
52
+ * If multiple tags on a card have preferences, the maximum multiplier wins.
53
+ */
54
+ export interface UserTagPreferenceState {
55
+ /**
56
+ * Tag-specific multipliers.
57
+ * Maps tag name to score multiplier (0 = exclude, 1 = neutral, >1 = boost).
58
+ */
59
+ boost: Record<string, number>;
60
+
61
+ /**
62
+ * ISO timestamp of last update.
63
+ * Use `moment.utc(updatedAt)` to parse into a Moment object.
64
+ */
65
+ updatedAt: string;
66
+ }
67
+
68
+ /**
69
+ * A filter that applies user-configured tag preferences.
70
+ *
71
+ * Reads preferences from STRATEGY_STATE document in user's database.
72
+ * If no preferences exist, passes through unchanged (no-op).
73
+ *
74
+ * Implements CardFilter for use in Pipeline architecture.
75
+ * Also extends ContentNavigator for compatibility with dynamic loading.
76
+ */
77
+ export default class UserTagPreferenceFilter extends ContentNavigator implements CardFilter {
78
+ private _strategyData: ContentNavigationStrategyData;
79
+
80
+ /** Human-readable name for CardFilter interface */
81
+ name: string;
82
+
83
+ constructor(
84
+ user: UserDBInterface,
85
+ course: CourseDBInterface,
86
+ strategyData: ContentNavigationStrategyData
87
+ ) {
88
+ super(user, course, strategyData);
89
+ this._strategyData = strategyData;
90
+ this.name = strategyData.name || 'User Tag Preferences';
91
+ }
92
+
93
+ /**
94
+ * Compute multiplier for a card based on its tags and user preferences.
95
+ * Returns the maximum multiplier among all matching tags, or 1.0 if no matches.
96
+ */
97
+ private computeMultiplier(cardTags: string[], boostMap: Record<string, number>): number {
98
+ const multipliers = cardTags.map((tag) => boostMap[tag]).filter((val) => val !== undefined);
99
+
100
+ if (multipliers.length === 0) {
101
+ return 1.0;
102
+ }
103
+
104
+ // Use max multiplier among matching tags
105
+ return Math.max(...multipliers);
106
+ }
107
+
108
+ /**
109
+ * Build human-readable reason for the filter's decision.
110
+ */
111
+ private buildReason(
112
+ cardTags: string[],
113
+ boostMap: Record<string, number>,
114
+ multiplier: number
115
+ ): string {
116
+ // Find which tag(s) contributed to the multiplier
117
+ const matchingTags = cardTags.filter((tag) => boostMap[tag] === multiplier);
118
+
119
+ if (multiplier === 0) {
120
+ return `Excluded by user preference: ${matchingTags.join(', ')} (${multiplier}x)`;
121
+ }
122
+
123
+ if (multiplier < 1.0) {
124
+ return `Penalized by user preference: ${matchingTags.join(', ')} (${multiplier.toFixed(2)}x)`;
125
+ }
126
+
127
+ if (multiplier > 1.0) {
128
+ return `Boosted by user preference: ${matchingTags.join(', ')} (${multiplier.toFixed(2)}x)`;
129
+ }
130
+
131
+ return 'No matching user preferences';
132
+ }
133
+
134
+ /**
135
+ * CardFilter.transform implementation.
136
+ *
137
+ * Apply user tag preferences:
138
+ * 1. Read preferences from strategy state
139
+ * 2. If no preferences, pass through unchanged
140
+ * 3. For each card:
141
+ * - Look up tag in boost record
142
+ * - If tag found: apply multiplier (0 = exclude, 1 = neutral, >1 = boost)
143
+ * - If multiple tags match: use max multiplier
144
+ * - Append provenance with clear reason
145
+ */
146
+ async transform(cards: WeightedCard[], _context: FilterContext): Promise<WeightedCard[]> {
147
+ // Read user preferences from strategy state
148
+ const prefs = await this.getStrategyState<UserTagPreferenceState>();
149
+
150
+ // No preferences configured → pass through unchanged
151
+ if (!prefs || Object.keys(prefs.boost).length === 0) {
152
+ return cards.map((card) => ({
153
+ ...card,
154
+ provenance: [
155
+ ...card.provenance,
156
+ {
157
+ strategy: 'userTagPreference',
158
+ strategyName: this.strategyName || this.name,
159
+ strategyId: this.strategyId || this._strategyData._id,
160
+ action: 'passed' as const,
161
+ score: card.score,
162
+ reason: 'No user tag preferences configured',
163
+ },
164
+ ],
165
+ }));
166
+ }
167
+
168
+ // Process each card
169
+ const adjusted: WeightedCard[] = await Promise.all(
170
+ cards.map(async (card) => {
171
+ const cardTags = card.tags ?? [];
172
+
173
+ // Compute multiplier based on card tags and user preferences
174
+ const multiplier = this.computeMultiplier(cardTags, prefs.boost);
175
+ const finalScore = Math.min(1, card.score * multiplier);
176
+
177
+ // Determine action for provenance
178
+ let action: 'passed' | 'boosted' | 'penalized';
179
+ if (multiplier === 0 || multiplier < 1.0) {
180
+ action = 'penalized';
181
+ } else if (multiplier > 1.0) {
182
+ action = 'boosted';
183
+ } else {
184
+ action = 'passed';
185
+ }
186
+
187
+ return {
188
+ ...card,
189
+ score: finalScore,
190
+ provenance: [
191
+ ...card.provenance,
192
+ {
193
+ strategy: 'userTagPreference',
194
+ strategyName: this.strategyName || this.name,
195
+ strategyId: this.strategyId || this._strategyData._id,
196
+ action,
197
+ score: finalScore,
198
+ reason: this.buildReason(cardTags, prefs.boost, multiplier),
199
+ },
200
+ ],
201
+ };
202
+ })
203
+ );
204
+
205
+ return adjusted;
206
+ }
207
+
208
+ /**
209
+ * Legacy getWeightedCards - throws as filters should not be used as generators.
210
+ */
211
+ async getWeightedCards(_limit: number): Promise<WeightedCard[]> {
212
+ throw new Error(
213
+ 'UserTagPreferenceFilter is a filter and should not be used as a generator. ' +
214
+ 'Use Pipeline with a generator and this filter via transform().'
215
+ );
216
+ }
217
+ }
@@ -1,12 +1,10 @@
1
- import { ContentNavigator } from './index';
2
- import type { WeightedCard } from './index';
3
- import type { ContentNavigationStrategyData } from '../types/contentNavigationStrategy';
4
- import type { CourseDBInterface } from '../interfaces/courseDB';
5
- import type { UserDBInterface } from '../interfaces/userDB';
6
- import type { StudySessionNewItem, StudySessionReviewItem } from '../interfaces/contentSource';
7
- import type { ScheduledCard } from '../types/user';
8
- import type { CardGenerator, GeneratorContext } from './generators/types';
9
- import { logger } from '../../util/logger';
1
+ import { ContentNavigator } from '../index';
2
+ import type { WeightedCard } from '../index';
3
+ import type { ContentNavigationStrategyData } from '../../types/contentNavigationStrategy';
4
+ import type { CourseDBInterface } from '../../interfaces/courseDB';
5
+ import type { UserDBInterface } from '../../interfaces/userDB';
6
+ import type { CardGenerator, GeneratorContext } from './types';
7
+ import { logger } from '../../../util/logger';
10
8
 
11
9
  // ============================================================================
12
10
  // COMPOSITE GENERATOR
@@ -100,9 +98,16 @@ export default class CompositeGenerator extends ContentNavigator implements Card
100
98
  * CardGenerator interface signature (limit, context).
101
99
  *
102
100
  * @param limit - Maximum number of cards to return
103
- * @param context - Optional GeneratorContext passed to child generators
101
+ * @param context - GeneratorContext passed to child generators (required when called via Pipeline)
104
102
  */
105
103
  async getWeightedCards(limit: number, context?: GeneratorContext): Promise<WeightedCard[]> {
104
+ if (!context) {
105
+ throw new Error(
106
+ 'CompositeGenerator.getWeightedCards requires a GeneratorContext. ' +
107
+ 'It should be called via Pipeline, not directly.'
108
+ );
109
+ }
110
+
106
111
  // Fetch from all generators in parallel
107
112
  const results = await Promise.all(
108
113
  this.generators.map((g) => g.getWeightedCards(limit, context))
@@ -211,58 +216,4 @@ export default class CompositeGenerator extends ContentNavigator implements Card
211
216
  return scores[0];
212
217
  }
213
218
  }
214
-
215
- /**
216
- * Get new cards from all generators, merged and deduplicated.
217
- */
218
- async getNewCards(n?: number): Promise<StudySessionNewItem[]> {
219
- // For legacy method, need to filter to generators that have getNewCards
220
- const legacyGenerators = this.generators.filter(
221
- (g): g is CardGenerator & ContentNavigator => g instanceof ContentNavigator
222
- );
223
-
224
- const results = await Promise.all(legacyGenerators.map((g) => g.getNewCards(n)));
225
-
226
- // Deduplicate by cardID
227
- const seen = new Set<string>();
228
- const merged: StudySessionNewItem[] = [];
229
-
230
- for (const cards of results) {
231
- for (const card of cards) {
232
- if (!seen.has(card.cardID)) {
233
- seen.add(card.cardID);
234
- merged.push(card);
235
- }
236
- }
237
- }
238
-
239
- return n ? merged.slice(0, n) : merged;
240
- }
241
-
242
- /**
243
- * Get pending reviews from all generators, merged and deduplicated.
244
- */
245
- async getPendingReviews(): Promise<(StudySessionReviewItem & ScheduledCard)[]> {
246
- // For legacy method, need to filter to generators that have getPendingReviews
247
- const legacyGenerators = this.generators.filter(
248
- (g): g is CardGenerator & ContentNavigator => g instanceof ContentNavigator
249
- );
250
-
251
- const results = await Promise.all(legacyGenerators.map((g) => g.getPendingReviews()));
252
-
253
- // Deduplicate by cardID
254
- const seen = new Set<string>();
255
- const merged: (StudySessionReviewItem & ScheduledCard)[] = [];
256
-
257
- for (const reviews of results) {
258
- for (const review of reviews) {
259
- if (!seen.has(review.cardID)) {
260
- seen.add(review.cardID);
261
- merged.push(review);
262
- }
263
- }
264
- }
265
-
266
- return merged;
267
- }
268
219
  }
@@ -1,12 +1,10 @@
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 { CourseElo } from '@vue-skuilder/common';
1
+ import type { CourseDBInterface } from '../../interfaces/courseDB';
2
+ import type { UserDBInterface } from '../../interfaces/userDB';
3
+ import { ContentNavigator } from '../index';
4
+ import type { WeightedCard } from '../index';
7
5
  import { toCourseElo } from '@vue-skuilder/common';
8
- import type { StudySessionReviewItem, StudySessionNewItem, QualifiedCardID } from '..';
9
- import type { CardGenerator, GeneratorContext } from './generators/types';
6
+ import type { QualifiedCardID } from '../..';
7
+ import type { CardGenerator, GeneratorContext } from './types';
10
8
 
11
9
  // ============================================================================
12
10
  // ELO NAVIGATOR
@@ -51,59 +49,6 @@ export default class ELONavigator extends ContentNavigator implements CardGenera
51
49
  this.name = strategyData?.name || 'ELO';
52
50
  }
53
51
 
54
- async getPendingReviews(): Promise<(StudySessionReviewItem & ScheduledCard)[]> {
55
- type ratedReview = ScheduledCard & CourseElo;
56
-
57
- const reviews = await this.user.getPendingReviews(this.course.getCourseID()); // todo: this adds a db round trip - should be server side
58
- const elo = await this.course.getCardEloData(reviews.map((r) => r.cardId));
59
-
60
- const ratedReviews = reviews.map((r, i) => {
61
- const ratedR: ratedReview = {
62
- ...r,
63
- ...elo[i],
64
- };
65
- return ratedR;
66
- });
67
-
68
- ratedReviews.sort((a, b) => {
69
- return a.global.score - b.global.score;
70
- });
71
-
72
- return ratedReviews.map((r) => {
73
- return {
74
- ...r,
75
- contentSourceType: 'course',
76
- contentSourceID: this.course.getCourseID(),
77
- cardID: r.cardId,
78
- courseID: r.courseId,
79
- qualifiedID: `${r.courseId}-${r.cardId}`,
80
- reviewID: r._id,
81
- status: 'review',
82
- };
83
- });
84
- }
85
-
86
- async getNewCards(limit: number = 99): Promise<StudySessionNewItem[]> {
87
- const activeCards = await this.user.getActiveCards();
88
- return (
89
- await this.course.getCardsCenteredAtELO(
90
- { limit: limit, elo: 'user' },
91
- (c: QualifiedCardID) => {
92
- if (activeCards.some((ac) => c.cardID === ac.cardID)) {
93
- return false;
94
- } else {
95
- return true;
96
- }
97
- }
98
- )
99
- ).map((c) => {
100
- return {
101
- ...c,
102
- status: 'new',
103
- };
104
- });
105
- }
106
-
107
52
  /**
108
53
  * Get new cards with suitability scores based on ELO distance.
109
54
  *
@@ -130,8 +75,13 @@ export default class ELONavigator extends ContentNavigator implements CardGenera
130
75
  userGlobalElo = userElo.global.score;
131
76
  }
132
77
 
133
- // Get new cards (existing logic)
134
- const newCards = await this.getNewCards(limit);
78
+ const activeCards = await this.user.getActiveCards();
79
+ const newCards = (
80
+ await this.course.getCardsCenteredAtELO(
81
+ { limit, elo: 'user' },
82
+ (c: QualifiedCardID) => !activeCards.some((ac) => c.cardID === ac.cardID)
83
+ )
84
+ ).map((c) => ({ ...c, status: 'new' as const }));
135
85
 
136
86
  // Get ELO data for all cards in one batch
137
87
  const cardIds = newCards.map((c) => c.cardID);