@vue-skuilder/db 0.1.20 → 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 (70) hide show
  1. package/CLAUDE.md +2 -2
  2. package/dist/{classroomDB-CZdMBiTU.d.ts → contentSource-BP9hznNV.d.ts} +150 -196
  3. package/dist/{classroomDB-PxDZTky3.d.cts → contentSource-DsJadoBU.d.cts} +150 -196
  4. package/dist/core/index.d.cts +3 -3
  5. package/dist/core/index.d.ts +3 -3
  6. package/dist/core/index.js +615 -1758
  7. package/dist/core/index.js.map +1 -1
  8. package/dist/core/index.mjs +579 -1727
  9. package/dist/core/index.mjs.map +1 -1
  10. package/dist/{dataLayerProvider-D8o6ZnKW.d.ts → dataLayerProvider-CHYrQ5pB.d.cts} +1 -1
  11. package/dist/{dataLayerProvider-D0MoZMjH.d.cts → dataLayerProvider-MDTxXq2l.d.ts} +1 -1
  12. package/dist/impl/couch/index.d.cts +6 -22
  13. package/dist/impl/couch/index.d.ts +6 -22
  14. package/dist/impl/couch/index.js +598 -1769
  15. package/dist/impl/couch/index.js.map +1 -1
  16. package/dist/impl/couch/index.mjs +579 -1755
  17. package/dist/impl/couch/index.mjs.map +1 -1
  18. package/dist/impl/static/index.d.cts +22 -6
  19. package/dist/impl/static/index.d.ts +22 -6
  20. package/dist/impl/static/index.js +617 -1629
  21. package/dist/impl/static/index.js.map +1 -1
  22. package/dist/impl/static/index.mjs +607 -1624
  23. package/dist/impl/static/index.mjs.map +1 -1
  24. package/dist/index.d.cts +64 -56
  25. package/dist/index.d.ts +64 -56
  26. package/dist/index.js +1000 -2161
  27. package/dist/index.js.map +1 -1
  28. package/dist/index.mjs +970 -2127
  29. package/dist/index.mjs.map +1 -1
  30. package/dist/pouch/index.js +3 -0
  31. package/dist/pouch/index.js.map +1 -1
  32. package/dist/pouch/index.mjs +3 -0
  33. package/dist/pouch/index.mjs.map +1 -1
  34. package/docs/navigators-architecture.md +2 -9
  35. package/package.json +3 -3
  36. package/src/core/interfaces/classroomDB.ts +5 -13
  37. package/src/core/interfaces/contentSource.ts +6 -66
  38. package/src/core/interfaces/courseDB.ts +2 -7
  39. package/src/core/navigators/Pipeline.ts +24 -53
  40. package/src/core/navigators/PipelineAssembler.ts +1 -1
  41. package/src/core/navigators/defaults.ts +84 -0
  42. package/src/core/navigators/{hierarchyDefinition.ts → filters/hierarchyDefinition.ts} +11 -25
  43. package/src/core/navigators/{interferenceMitigator.ts → filters/interferenceMitigator.ts} +10 -24
  44. package/src/core/navigators/{relativePriority.ts → filters/relativePriority.ts} +10 -24
  45. package/src/core/navigators/filters/userTagPreference.ts +1 -16
  46. package/src/core/navigators/{CompositeGenerator.ts → generators/CompositeGenerator.ts} +15 -64
  47. package/src/core/navigators/{elo.ts → generators/elo.ts} +13 -63
  48. package/src/core/navigators/{srs.ts → generators/srs.ts} +11 -40
  49. package/src/core/navigators/generators/types.ts +1 -1
  50. package/src/core/navigators/index.ts +36 -91
  51. package/src/impl/couch/classroomDB.ts +100 -103
  52. package/src/impl/couch/courseDB.ts +5 -81
  53. package/src/impl/couch/pouchdb-setup.ts +7 -0
  54. package/src/impl/static/StaticDataUnpacker.ts +50 -1
  55. package/src/impl/static/courseDB.ts +76 -37
  56. package/src/study/SessionController.ts +122 -202
  57. package/src/study/SourceMixer.ts +65 -0
  58. package/src/study/TagFilteredContentSource.ts +49 -92
  59. package/src/study/index.ts +1 -0
  60. package/src/study/services/CardHydrationService.ts +165 -81
  61. package/src/util/dataDirectory.ts +1 -1
  62. package/src/util/index.ts +0 -1
  63. package/tests/core/navigators/CompositeGenerator.test.ts +44 -168
  64. package/tests/core/navigators/Pipeline.test.ts +5 -72
  65. package/tests/core/navigators/PipelineAssembler.test.ts +8 -58
  66. package/tests/core/navigators/navigators.test.ts +118 -151
  67. package/src/core/navigators/hardcodedOrder.ts +0 -163
  68. package/src/util/tuiLogger.ts +0 -139
  69. /package/src/core/navigators/{inferredPreference.ts → filters/inferredPreferenceStub.ts} +0 -0
  70. /package/src/core/navigators/{userGoal.ts → filters/userGoalStub.ts} +0 -0
@@ -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 {
@@ -242,14 +238,4 @@ export default class RelativePriorityNavigator extends ContentNavigator implemen
242
238
  'Use Pipeline with a generator and this filter via transform().'
243
239
  );
244
240
  }
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
241
  }
@@ -1,10 +1,8 @@
1
- import type { ScheduledCard } from '../../types/user';
2
1
  import type { CourseDBInterface } from '../../interfaces/courseDB';
3
2
  import type { UserDBInterface } from '../../interfaces/userDB';
4
3
  import { ContentNavigator } from '../index';
5
4
  import type { WeightedCard } from '../index';
6
5
  import type { ContentNavigationStrategyData } from '../../types/contentNavigationStrategy';
7
- import type { StudySessionReviewItem, StudySessionNewItem } from '../..';
8
6
  import type { CardFilter, FilterContext } from './types';
9
7
 
10
8
  // ============================================================================
@@ -97,9 +95,7 @@ export default class UserTagPreferenceFilter extends ContentNavigator implements
97
95
  * Returns the maximum multiplier among all matching tags, or 1.0 if no matches.
98
96
  */
99
97
  private computeMultiplier(cardTags: string[], boostMap: Record<string, number>): number {
100
- const multipliers = cardTags
101
- .map((tag) => boostMap[tag])
102
- .filter((val) => val !== undefined);
98
+ const multipliers = cardTags.map((tag) => boostMap[tag]).filter((val) => val !== undefined);
103
99
 
104
100
  if (multipliers.length === 0) {
105
101
  return 1.0;
@@ -150,7 +146,6 @@ export default class UserTagPreferenceFilter extends ContentNavigator implements
150
146
  async transform(cards: WeightedCard[], _context: FilterContext): Promise<WeightedCard[]> {
151
147
  // Read user preferences from strategy state
152
148
  const prefs = await this.getStrategyState<UserTagPreferenceState>();
153
-
154
149
 
155
150
  // No preferences configured → pass through unchanged
156
151
  if (!prefs || Object.keys(prefs.boost).length === 0) {
@@ -219,14 +214,4 @@ export default class UserTagPreferenceFilter extends ContentNavigator implements
219
214
  'Use Pipeline with a generator and this filter via transform().'
220
215
  );
221
216
  }
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
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);
@@ -1,12 +1,12 @@
1
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';
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 { CardGenerator, GeneratorContext } from './types';
9
+ import { logger } from '@db/util/logger';
10
10
 
11
11
  // ============================================================================
12
12
  // SRS NAVIGATOR
@@ -95,6 +95,7 @@ export default class SRSNavigator extends ContentNavigator implements CardGenera
95
95
  cardId: review.cardId,
96
96
  courseId: review.courseId,
97
97
  score,
98
+ reviewID: review._id,
98
99
  provenance: [
99
100
  {
100
101
  strategy: 'srs',
@@ -108,6 +109,8 @@ export default class SRSNavigator extends ContentNavigator implements CardGenera
108
109
  };
109
110
  });
110
111
 
112
+ logger.debug(`[srsNav] got ${scored.length} weighted cards`);
113
+
111
114
  // Sort by score descending and limit
112
115
  return scored.sort((a, b) => b.score - a.score).slice(0, limit);
113
116
  }
@@ -160,36 +163,4 @@ export default class SRSNavigator extends ContentNavigator implements CardGenera
160
163
 
161
164
  return { score, reason };
162
165
  }
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
166
  }
@@ -9,7 +9,7 @@ import type { UserDBInterface } from '../../interfaces/userDB';
9
9
  // Generators produce candidate cards with initial scores.
10
10
  // They are the "source" stage of a navigation pipeline.
11
11
  //
12
- // Examples: ELO (skill proximity), SRS (review scheduling), HardcodedOrder
12
+ // Examples: ELO (skill proximity), SRS (review scheduling)
13
13
  //
14
14
  // Generators differ from filters:
15
15
  // - Generators: produce candidates from DB queries, assign initial scores
@@ -1,10 +1,4 @@
1
- import {
2
- StudyContentSource,
3
- UserDBInterface,
4
- CourseDBInterface,
5
- StudySessionReviewItem,
6
- StudySessionNewItem,
7
- } from '..';
1
+ import { StudyContentSource, UserDBInterface, CourseDBInterface } from '..';
8
2
 
9
3
  // Re-export filter types
10
4
  export type { CardFilter, FilterContext, CardFilterFactory } from './filters/types';
@@ -13,7 +7,6 @@ export type { CardFilter, FilterContext, CardFilterFactory } from './filters/typ
13
7
  export type { CardGenerator, GeneratorContext, CardGeneratorFactory } from './generators/types';
14
8
 
15
9
  import { ContentNavigationStrategyData } from '../types/contentNavigationStrategy';
16
- import { ScheduledCard } from '../types/user';
17
10
  import { logger } from '../../util/logger';
18
11
 
19
12
  // ============================================================================
@@ -33,7 +26,7 @@ import { logger } from '../../util/logger';
33
26
  // New code should use CardGenerator or CardFilter interfaces directly.
34
27
  //
35
28
  // 3. CardGenerator vs CardFilter:
36
- // - Generators (ELO, SRS, HardcodedOrder) produce candidate cards with scores
29
+ // - Generators (ELO, SRS) produce candidate cards with scores
37
30
  // - Filters (Hierarchy, Interference, Priority, EloDistance) transform scores
38
31
  //
39
32
  // 4. Pipeline architecture:
@@ -142,6 +135,12 @@ export interface WeightedCard {
142
135
  * Filters should use this instead of querying getAppliedTags() individually.
143
136
  */
144
137
  tags?: string[];
138
+ /**
139
+ * Review document ID (_id from ScheduledCard).
140
+ * Present when this card originated from SRS review scheduling.
141
+ * Used by SessionController to track review outcomes and maintain review state.
142
+ */
143
+ reviewID?: string;
145
144
  }
146
145
 
147
146
  /**
@@ -174,7 +173,6 @@ export function getCardOrigin(card: WeightedCard): 'new' | 'review' | 'failed' {
174
173
  export enum Navigators {
175
174
  ELO = 'elo',
176
175
  SRS = 'srs',
177
- HARDCODED = 'hardcodedOrder',
178
176
  HIERARCHY = 'hierarchyDefinition',
179
177
  INTERFERENCE = 'interferenceMitigator',
180
178
  RELATIVE_PRIORITY = 'relativePriority',
@@ -186,7 +184,7 @@ export enum Navigators {
186
184
  // ============================================================================
187
185
  //
188
186
  // Navigators are classified as either generators or filters:
189
- // - Generators: Produce candidate cards (ELO, SRS, HardcodedOrder)
187
+ // - Generators: Produce candidate cards (ELO, SRS)
190
188
  // - Filters: Transform/score candidates (Hierarchy, Interference, RelativePriority)
191
189
  //
192
190
  // This classification is used by PipelineAssembler to build pipelines:
@@ -213,7 +211,6 @@ export enum NavigatorRole {
213
211
  export const NavigatorRoles: Record<Navigators, NavigatorRole> = {
214
212
  [Navigators.ELO]: NavigatorRole.GENERATOR,
215
213
  [Navigators.SRS]: NavigatorRole.GENERATOR,
216
- [Navigators.HARDCODED]: NavigatorRole.GENERATOR,
217
214
  [Navigators.HIERARCHY]: NavigatorRole.FILTER,
218
215
  [Navigators.INTERFERENCE]: NavigatorRole.FILTER,
219
216
  [Navigators.RELATIVE_PRIORITY]: NavigatorRole.FILTER,
@@ -252,10 +249,10 @@ export function isFilter(impl: string): boolean {
252
249
  */
253
250
  export abstract class ContentNavigator implements StudyContentSource {
254
251
  /** User interface for this navigation session */
255
- protected user?: UserDBInterface;
252
+ protected user: UserDBInterface;
256
253
 
257
254
  /** Course interface for this navigation session */
258
- protected course?: CourseDBInterface;
255
+ protected course: CourseDBInterface;
259
256
 
260
257
  /** Human-readable name for this strategy instance (from ContentNavigationStrategyData.name) */
261
258
  protected strategyName?: string;
@@ -267,16 +264,17 @@ export abstract class ContentNavigator implements StudyContentSource {
267
264
  * Constructor for standard navigators.
268
265
  * Call this from subclass constructors to initialize common fields.
269
266
  *
270
- * Note: CompositeGenerator doesn't use this pattern and should call super() without args.
267
+ * Note: CompositeGenerator and Pipeline call super() without args, then set
268
+ * user/course fields directly if needed.
271
269
  */
272
270
  constructor(
273
271
  user?: UserDBInterface,
274
272
  course?: CourseDBInterface,
275
273
  strategyData?: ContentNavigationStrategyData
276
274
  ) {
277
- if (user && course && strategyData) {
278
- this.user = user;
279
- this.course = course;
275
+ this.user = user!;
276
+ this.course = course!;
277
+ if (strategyData) {
280
278
  this.strategyName = strategyData.name;
281
279
  this.strategyId = strategyData._id;
282
280
  }
@@ -352,15 +350,19 @@ export abstract class ContentNavigator implements StudyContentSource {
352
350
 
353
351
  // Try different extension variations
354
352
  const variations = ['.ts', '.js', ''];
353
+ const dirs = ['filters', 'generators'];
355
354
 
356
355
  for (const ext of variations) {
357
- try {
358
- const module = await import(`./${implementingClass}${ext}`);
359
- NavigatorImpl = module.default;
360
- break; // Break the loop if loading succeeds
361
- } catch (e) {
362
- // Continue to next variation if this one fails
363
- logger.debug(`Failed to load with extension ${ext}:`, e);
356
+ for (const dir of dirs) {
357
+ const loadFrom = `./${dir}/${implementingClass}${ext}`;
358
+ try {
359
+ const module = await import(loadFrom);
360
+ NavigatorImpl = module.default;
361
+ break; // Break the loop if loading succeeds
362
+ } catch (e) {
363
+ // Continue to next variation if this one fails
364
+ logger.debug(`Failed to load extension from ${loadFrom}:`, e);
365
+ }
364
366
  }
365
367
  }
366
368
 
@@ -371,24 +373,6 @@ export abstract class ContentNavigator implements StudyContentSource {
371
373
  return new NavigatorImpl(user, course, strategyData);
372
374
  }
373
375
 
374
- /**
375
- * Get cards scheduled for review.
376
- *
377
- * @deprecated This method is part of the legacy StudyContentSource interface.
378
- * New strategies should focus on implementing CardGenerator.getWeightedCards() instead.
379
- */
380
- abstract getPendingReviews(): Promise<(StudySessionReviewItem & ScheduledCard)[]>;
381
-
382
- /**
383
- * Get new cards for introduction.
384
- *
385
- * @deprecated This method is part of the legacy StudyContentSource interface.
386
- * New strategies should focus on implementing CardGenerator.getWeightedCards() instead.
387
- *
388
- * @param n - Maximum number of new cards to return
389
- */
390
- abstract getNewCards(n?: number): Promise<StudySessionNewItem[]>;
391
-
392
376
  /**
393
377
  * Get cards with suitability scores and provenance trails.
394
378
  *
@@ -398,62 +382,23 @@ export abstract class ContentNavigator implements StudyContentSource {
398
382
  * better candidates for presentation. Each card includes a provenance trail
399
383
  * documenting how strategies contributed to the final score.
400
384
  *
385
+ * ## Implementation Required
386
+ * All navigation strategies MUST override this method. The base class does
387
+ * not provide a default implementation.
388
+ *
401
389
  * ## For Generators
402
390
  * Override this method to generate candidates and compute scores based on
403
391
  * your strategy's logic (e.g., ELO proximity, review urgency). Create the
404
392
  * initial provenance entry with action='generated'.
405
393
  *
406
- * ## Default Implementation
407
- * The base class provides a backward-compatible default that:
408
- * 1. Calls legacy getNewCards() and getPendingReviews()
409
- * 2. Assigns score=1.0 to all cards
410
- * 3. Creates minimal provenance from legacy methods
411
- * 4. Returns combined results up to limit
412
- *
413
- * This allows existing strategies to work without modification while
414
- * new strategies can override with proper scoring and provenance.
394
+ * ## For Filters
395
+ * Filters should implement the CardFilter interface instead and be composed
396
+ * via Pipeline. Filters do not directly implement getWeightedCards().
415
397
  *
416
398
  * @param limit - Maximum cards to return
417
399
  * @returns Cards sorted by score descending, with provenance trails
418
400
  */
419
- async getWeightedCards(limit: number): Promise<WeightedCard[]> {
420
- // Default implementation: delegate to legacy methods, assign score=1.0
421
- const newCards = await this.getNewCards(limit);
422
- const reviews = await this.getPendingReviews();
423
-
424
- const weighted: WeightedCard[] = [
425
- ...newCards.map((c) => ({
426
- cardId: c.cardID,
427
- courseId: c.courseID,
428
- score: 1.0,
429
- provenance: [
430
- {
431
- strategy: 'legacy',
432
- strategyName: this.strategyName || 'Legacy API',
433
- strategyId: this.strategyId || 'legacy-fallback',
434
- action: 'generated' as const,
435
- score: 1.0,
436
- reason: 'Generated via legacy getNewCards(), new card',
437
- },
438
- ],
439
- })),
440
- ...reviews.map((r) => ({
441
- cardId: r.cardID,
442
- courseId: r.courseID,
443
- score: 1.0,
444
- provenance: [
445
- {
446
- strategy: 'legacy',
447
- strategyName: this.strategyName || 'Legacy API',
448
- strategyId: this.strategyId || 'legacy-fallback',
449
- action: 'generated' as const,
450
- score: 1.0,
451
- reason: 'Generated via legacy getPendingReviews(), review',
452
- },
453
- ],
454
- })),
455
- ];
456
-
457
- return weighted.slice(0, limit);
401
+ async getWeightedCards(_limit: number): Promise<WeightedCard[]> {
402
+ throw new Error(`${this.constructor.name} must implement getWeightedCards(). `);
458
403
  }
459
404
  }