@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,14 +1,90 @@
1
1
  import { toCourseElo } from '@vue-skuilder/common';
2
2
  import type { CourseDBInterface } from '../interfaces/courseDB';
3
3
  import type { UserDBInterface } from '../interfaces/userDB';
4
- import type { ScheduledCard } from '../types/user';
5
4
  import { ContentNavigator } from './index';
6
5
  import type { WeightedCard } from './index';
7
6
  import type { CardFilter, FilterContext } from './filters/types';
8
7
  import type { CardGenerator, GeneratorContext } from './generators/types';
9
- import type { StudySessionNewItem, StudySessionReviewItem } from '../interfaces/contentSource';
10
8
  import { logger } from '../../util/logger';
11
9
 
10
+ // ============================================================================
11
+ // PIPELINE LOGGING HELPERS
12
+ // ============================================================================
13
+ //
14
+ // Focused logging functions that can be toggled by commenting single lines.
15
+ // Use these to inspect pipeline behavior in development/production.
16
+ //
17
+
18
+ /**
19
+ * Log pipeline configuration on construction.
20
+ * Shows generator and filter chain structure.
21
+ */
22
+ function logPipelineConfig(generator: CardGenerator, filters: CardFilter[]): void {
23
+ const filterList =
24
+ filters.length > 0 ? '\n - ' + filters.map((f) => f.name).join('\n - ') : ' none';
25
+
26
+ logger.info(
27
+ `[Pipeline] Configuration:\n` + ` Generator: ${generator.name}\n` + ` Filters:${filterList}`
28
+ );
29
+ }
30
+
31
+ /**
32
+ * Log tag hydration results.
33
+ * Shows effectiveness of batch query (how many cards/tags were hydrated).
34
+ */
35
+ function logTagHydration(cards: WeightedCard[], tagsByCard: Map<string, string[]>): void {
36
+ const totalTags = Array.from(tagsByCard.values()).reduce((sum, tags) => sum + tags.length, 0);
37
+ const cardsWithTags = Array.from(tagsByCard.values()).filter((tags) => tags.length > 0).length;
38
+
39
+ logger.debug(
40
+ `[Pipeline] Tag hydration: ${cards.length} cards, ` +
41
+ `${cardsWithTags} have tags (${totalTags} total tags) - single batch query`
42
+ );
43
+ }
44
+
45
+ /**
46
+ * Log pipeline execution summary.
47
+ * Shows complete flow from generator through filters to final results.
48
+ */
49
+ function logExecutionSummary(
50
+ generatorName: string,
51
+ generatedCount: number,
52
+ filterCount: number,
53
+ finalCount: number,
54
+ topScores: number[]
55
+ ): void {
56
+ const scoreDisplay =
57
+ topScores.length > 0 ? topScores.map((s) => s.toFixed(2)).join(', ') : 'none';
58
+
59
+ logger.info(
60
+ `[Pipeline] Execution: ${generatorName} produced ${generatedCount} → ` +
61
+ `${filterCount} filters → ${finalCount} results (top scores: ${scoreDisplay})`
62
+ );
63
+ }
64
+
65
+ /**
66
+ * Log provenance trails for cards.
67
+ * Shows the complete scoring history for each card through the pipeline.
68
+ * Useful for debugging why cards scored the way they did.
69
+ */
70
+ function logCardProvenance(cards: WeightedCard[], maxCards: number = 3): void {
71
+ const cardsToLog = cards.slice(0, maxCards);
72
+
73
+ logger.debug(`[Pipeline] Provenance for top ${cardsToLog.length} cards:`);
74
+
75
+ for (const card of cardsToLog) {
76
+ logger.debug(`[Pipeline] ${card.cardId} (final score: ${card.score.toFixed(3)}):`);
77
+
78
+ for (const entry of card.provenance) {
79
+ const scoreChange = entry.score.toFixed(3);
80
+ const action = entry.action.padEnd(9); // Align columns
81
+ logger.debug(
82
+ `[Pipeline] ${action} ${scoreChange} - ${entry.strategyName}: ${entry.reason}`
83
+ );
84
+ }
85
+ }
86
+ }
87
+
12
88
  // ============================================================================
13
89
  // PIPELINE
14
90
  // ============================================================================
@@ -72,9 +148,16 @@ export class Pipeline extends ContentNavigator {
72
148
  this.user = user;
73
149
  this.course = course;
74
150
 
75
- logger.debug(
76
- `[Pipeline] Created with generator '${generator.name}' and ${filters.length} filters: ${filters.map((f) => f.name).join(', ')}`
77
- );
151
+ course
152
+ .getCourseConfig()
153
+ .then((cfg) => {
154
+ logger.debug(`[pipeline] Crated pipeline for ${cfg.name}`);
155
+ })
156
+ .catch((e) => {
157
+ logger.error(`[pipeline] Failed to lookup courseCfg: ${e}`);
158
+ });
159
+ // Toggle pipeline configuration logging:
160
+ logPipelineConfig(generator, filters);
78
161
  }
79
162
 
80
163
  /**
@@ -82,10 +165,11 @@ export class Pipeline extends ContentNavigator {
82
165
  *
83
166
  * 1. Build shared context (user ELO, etc.)
84
167
  * 2. Get candidates from generator (passing context)
85
- * 3. Apply each filter sequentially
86
- * 4. Remove zero-score cards
87
- * 5. Sort by score descending
88
- * 6. Return top N
168
+ * 3. Batch hydrate tags for all candidates
169
+ * 4. Apply each filter sequentially
170
+ * 5. Remove zero-score cards
171
+ * 6. Sort by score descending
172
+ * 7. Return top N
89
173
  *
90
174
  * @param limit - Maximum number of cards to return
91
175
  * @returns Cards sorted by score descending
@@ -104,8 +188,12 @@ export class Pipeline extends ContentNavigator {
104
188
 
105
189
  // Get candidates from generator, passing context
106
190
  let cards = await this.generator.getWeightedCards(fetchLimit, context);
191
+ const generatedCount = cards.length;
192
+
193
+ logger.debug(`[Pipeline] Generator returned ${generatedCount} candidates`);
107
194
 
108
- logger.debug(`[Pipeline] Generator returned ${cards.length} candidates`);
195
+ // Batch hydrate tags before filters run
196
+ cards = await this.hydrateTags(cards);
109
197
 
110
198
  // Apply filters sequentially
111
199
  for (const filter of this.filters) {
@@ -123,16 +211,49 @@ export class Pipeline extends ContentNavigator {
123
211
  // Return top N
124
212
  const result = cards.slice(0, limit);
125
213
 
126
- logger.debug(
127
- `[Pipeline] Returning ${result.length} cards (top scores: ${result
128
- .slice(0, 3)
129
- .map((c) => c.score.toFixed(2))
130
- .join(', ')}...)`
214
+ // Toggle execution summary logging:
215
+ const topScores = result.slice(0, 3).map((c) => c.score);
216
+ logExecutionSummary(
217
+ this.generator.name,
218
+ generatedCount,
219
+ this.filters.length,
220
+ result.length,
221
+ topScores
131
222
  );
132
223
 
224
+ // Toggle provenance logging (shows scoring history for top cards):
225
+ logCardProvenance(result, 3);
226
+
133
227
  return result;
134
228
  }
135
229
 
230
+ /**
231
+ * Batch hydrate tags for all cards.
232
+ *
233
+ * Fetches tags for all cards in a single database query and attaches them
234
+ * to the WeightedCard objects. Filters can then use card.tags instead of
235
+ * making individual getAppliedTags() calls.
236
+ *
237
+ * @param cards - Cards to hydrate
238
+ * @returns Cards with tags populated
239
+ */
240
+ private async hydrateTags(cards: WeightedCard[]): Promise<WeightedCard[]> {
241
+ if (cards.length === 0) {
242
+ return cards;
243
+ }
244
+
245
+ const cardIds = cards.map((c) => c.cardId);
246
+ const tagsByCard = await this.course!.getAppliedTagsBatch(cardIds);
247
+
248
+ // Toggle tag hydration logging:
249
+ logTagHydration(cards, tagsByCard);
250
+
251
+ return cards.map((card) => ({
252
+ ...card,
253
+ tags: tagsByCard.get(card.cardId) ?? [],
254
+ }));
255
+ }
256
+
136
257
  /**
137
258
  * Build shared context for generator and filters.
138
259
  *
@@ -159,43 +280,6 @@ export class Pipeline extends ContentNavigator {
159
280
  };
160
281
  }
161
282
 
162
- // ===========================================================================
163
- // Legacy StudyContentSource methods
164
- // ===========================================================================
165
- //
166
- // These delegate to the generator for backward compatibility.
167
- // Eventually SessionController will use getWeightedCards() exclusively.
168
- //
169
-
170
- /**
171
- * Get new cards via legacy API.
172
- * Delegates to the generator if it supports the legacy interface.
173
- */
174
- async getNewCards(n?: number): Promise<StudySessionNewItem[]> {
175
- // Check if generator has legacy method (ContentNavigator-based generators do)
176
- if ('getNewCards' in this.generator && typeof this.generator.getNewCards === 'function') {
177
- return (this.generator as ContentNavigator).getNewCards(n);
178
- }
179
- // Pure CardGenerator without legacy support - return empty
180
- return [];
181
- }
182
-
183
- /**
184
- * Get pending reviews via legacy API.
185
- * Delegates to the generator if it supports the legacy interface.
186
- */
187
- async getPendingReviews(): Promise<(StudySessionReviewItem & ScheduledCard)[]> {
188
- // Check if generator has legacy method (ContentNavigator-based generators do)
189
- if (
190
- 'getPendingReviews' in this.generator &&
191
- typeof this.generator.getPendingReviews === 'function'
192
- ) {
193
- return (this.generator as ContentNavigator).getPendingReviews();
194
- }
195
- // Pure CardGenerator without legacy support - return empty
196
- return [];
197
- }
198
-
199
283
  /**
200
284
  * Get the course ID for this pipeline.
201
285
  */
@@ -7,7 +7,7 @@ import { DocType } from '../types/types-legacy';
7
7
  import { logger } from '../../util/logger';
8
8
  import type { CourseDBInterface } from '../interfaces/courseDB';
9
9
  import type { UserDBInterface } from '../interfaces/userDB';
10
- import CompositeGenerator from './CompositeGenerator';
10
+ import CompositeGenerator from './generators/CompositeGenerator';
11
11
 
12
12
  // ============================================================================
13
13
  // PIPELINE ASSEMBLER
@@ -0,0 +1,84 @@
1
+ import { Navigators } from './index';
2
+ import { Pipeline } from './Pipeline';
3
+ import CompositeGenerator from './generators/CompositeGenerator';
4
+ import ELONavigator from './generators/elo';
5
+ import SRSNavigator from './generators/srs';
6
+ import { createEloDistanceFilter } from './filters/eloDistance';
7
+ import type { ContentNavigationStrategyData } from '../types/contentNavigationStrategy';
8
+ import { DocType } from '../types/types-legacy';
9
+ import type { CourseDBInterface, UserDBInterface } from '../interfaces';
10
+
11
+ /**
12
+ * Default navigation pipeline configuration.
13
+ *
14
+ * This module provides factory functions for creating the canonical default
15
+ * navigation pipeline used by both CouchDB and static course implementations.
16
+ */
17
+
18
+ /**
19
+ * Create default ELO navigation strategy data.
20
+ * Used when no custom strategies are configured.
21
+ *
22
+ * @param courseId - The course ID to associate with this strategy
23
+ * @returns Strategy data for default ELO navigation
24
+ */
25
+ export function createDefaultEloStrategy(courseId: string): ContentNavigationStrategyData {
26
+ return {
27
+ _id: 'NAVIGATION_STRATEGY-ELO-default',
28
+ docType: DocType.NAVIGATION_STRATEGY,
29
+ name: 'ELO (default)',
30
+ description: 'Default ELO-based navigation strategy for new cards',
31
+ implementingClass: Navigators.ELO,
32
+ course: courseId,
33
+ serializedData: '',
34
+ };
35
+ }
36
+
37
+ /**
38
+ * Create default SRS navigation strategy data.
39
+ * Used when no custom strategies are configured.
40
+ *
41
+ * @param courseId - The course ID to associate with this strategy
42
+ * @returns Strategy data for default SRS navigation
43
+ */
44
+ export function createDefaultSrsStrategy(courseId: string): ContentNavigationStrategyData {
45
+ return {
46
+ _id: 'NAVIGATION_STRATEGY-SRS-default',
47
+ docType: DocType.NAVIGATION_STRATEGY,
48
+ name: 'SRS (default)',
49
+ description: 'Default SRS-based navigation strategy for reviews',
50
+ implementingClass: Navigators.SRS,
51
+ course: courseId,
52
+ serializedData: '',
53
+ };
54
+ }
55
+
56
+ /**
57
+ * Creates the default navigation pipeline for courses with no configured strategies.
58
+ *
59
+ * Default: Pipeline(Composite(ELO, SRS), [eloDistanceFilter])
60
+ * - ELO generator: scores new cards by skill proximity
61
+ * - SRS generator: scores reviews by overdueness and interval recency
62
+ * - ELO distance filter: penalizes cards far from user's current level
63
+ *
64
+ * This is the canonical default configuration used when:
65
+ * - No navigation strategy documents exist in the course
66
+ * - PipelineAssembler fails to build from strategy documents
67
+ *
68
+ * @param user - User database interface for accessing user state
69
+ * @param course - Course database interface for accessing course data
70
+ * @returns Configured Pipeline ready for use
71
+ */
72
+ export function createDefaultPipeline(
73
+ user: UserDBInterface,
74
+ course: CourseDBInterface
75
+ ): Pipeline {
76
+ const courseId = course.getCourseID();
77
+ const eloNavigator = new ELONavigator(user, course, createDefaultEloStrategy(courseId));
78
+ const srsNavigator = new SRSNavigator(user, course, createDefaultSrsStrategy(courseId));
79
+
80
+ const compositeGenerator = new CompositeGenerator([eloNavigator, srsNavigator]);
81
+ const eloDistanceFilter = createEloDistanceFilter();
82
+
83
+ return new Pipeline(compositeGenerator, [eloDistanceFilter], user, course);
84
+ }
@@ -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
  import { toCourseElo } from '@vue-skuilder/common';
10
8
 
11
9
  /**
@@ -53,7 +51,6 @@ const DEFAULT_MIN_COUNT = 3;
53
51
  */
54
52
  export default class HierarchyDefinitionNavigator extends ContentNavigator implements CardFilter {
55
53
  private config: HierarchyConfig;
56
- private _strategyData: ContentNavigationStrategyData;
57
54
 
58
55
  /** Human-readable name for CardFilter interface */
59
56
  name: string;
@@ -61,12 +58,11 @@ export default class HierarchyDefinitionNavigator extends ContentNavigator imple
61
58
  constructor(
62
59
  user: UserDBInterface,
63
60
  course: CourseDBInterface,
64
- _strategyData: ContentNavigationStrategyData
61
+ strategyData: ContentNavigationStrategyData
65
62
  ) {
66
- super(user, course, _strategyData);
67
- this._strategyData = _strategyData;
68
- this.config = this.parseConfig(_strategyData.serializedData);
69
- this.name = _strategyData.name || 'Hierarchy Definition';
63
+ super(user, course, strategyData);
64
+ this.config = this.parseConfig(strategyData.serializedData);
65
+ this.name = strategyData.name || 'Hierarchy Definition';
70
66
  }
71
67
 
72
68
  private parseConfig(serializedData: string): HierarchyConfig {
@@ -158,14 +154,14 @@ export default class HierarchyDefinitionNavigator extends ContentNavigator imple
158
154
  * Check if a card is unlocked and generate reason.
159
155
  */
160
156
  private async checkCardUnlock(
161
- cardId: string,
162
- course: CourseDBInterface,
157
+ card: WeightedCard,
158
+ _course: CourseDBInterface,
163
159
  unlockedTags: Set<string>,
164
160
  masteredTags: Set<string>
165
161
  ): Promise<{ isUnlocked: boolean; reason: string }> {
166
162
  try {
167
- const tagResponse = await course.getAppliedTags(cardId);
168
- const cardTags = tagResponse.rows.map((row) => row.value?.name || row.key);
163
+ // Pipeline hydrates tags before filters run
164
+ const cardTags = card.tags ?? [];
169
165
 
170
166
  // Check each tag's prerequisite status
171
167
  const lockedTags = cardTags.filter(
@@ -214,7 +210,7 @@ export default class HierarchyDefinitionNavigator extends ContentNavigator imple
214
210
 
215
211
  for (const card of cards) {
216
212
  const { isUnlocked, reason } = await this.checkCardUnlock(
217
- card.cardId,
213
+ card,
218
214
  context.course,
219
215
  unlockedTags,
220
216
  masteredTags
@@ -253,14 +249,4 @@ export default class HierarchyDefinitionNavigator extends ContentNavigator imple
253
249
  'Use Pipeline with a generator and this filter via transform().'
254
250
  );
255
251
  }
256
-
257
- // Legacy methods - stub implementations since filters don't generate cards
258
-
259
- async getNewCards(_n?: number): Promise<StudySessionNewItem[]> {
260
- return [];
261
- }
262
-
263
- async getPendingReviews(): Promise<(StudySessionReviewItem & ScheduledCard)[]> {
264
- return [];
265
- }
266
252
  }
@@ -4,3 +4,6 @@ export type { CardFilter, FilterContext, CardFilterFactory } from './types';
4
4
  // Filter implementations
5
5
  export { createEloDistanceFilter } from './eloDistance';
6
6
  export type { EloDistanceConfig } from './eloDistance';
7
+
8
+ export { default as UserTagPreferenceFilter } from './userTagPreference';
9
+ export type { UserTagPreferenceState } from './userTagPreference';
@@ -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
+ }
@@ -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
  import { toCourseElo } from '@vue-skuilder/common';
10
8
 
11
9
  /**
@@ -80,7 +78,6 @@ const DEFAULT_INTERFERENCE_DECAY = 0.8;
80
78
  */
81
79
  export default class InterferenceMitigatorNavigator extends ContentNavigator implements CardFilter {
82
80
  private config: InterferenceConfig;
83
- private _strategyData: ContentNavigationStrategyData;
84
81
 
85
82
  /** Human-readable name for CardFilter interface */
86
83
  name: string;
@@ -91,13 +88,12 @@ export default class InterferenceMitigatorNavigator extends ContentNavigator imp
91
88
  constructor(
92
89
  user: UserDBInterface,
93
90
  course: CourseDBInterface,
94
- _strategyData: ContentNavigationStrategyData
91
+ strategyData: ContentNavigationStrategyData
95
92
  ) {
96
- super(user, course, _strategyData);
97
- this._strategyData = _strategyData;
98
- this.config = this.parseConfig(_strategyData.serializedData);
93
+ super(user, course, strategyData);
94
+ this.config = this.parseConfig(strategyData.serializedData);
99
95
  this.interferenceMap = this.buildInterferenceMap();
100
- this.name = _strategyData.name || 'Interference Mitigator';
96
+ this.name = strategyData.name || 'Interference Mitigator';
101
97
  }
102
98
 
103
99
  private parseConfig(serializedData: string): InterferenceConfig {
@@ -234,18 +230,6 @@ export default class InterferenceMitigatorNavigator extends ContentNavigator imp
234
230
  return avoid;
235
231
  }
236
232
 
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
233
  /**
250
234
  * Compute interference score reduction for a card.
251
235
  * Returns: { multiplier, interfering tags, reason }
@@ -313,7 +297,7 @@ export default class InterferenceMitigatorNavigator extends ContentNavigator imp
313
297
  const adjusted: WeightedCard[] = [];
314
298
 
315
299
  for (const card of cards) {
316
- const cardTags = await this.getCardTags(card.cardId, context.course);
300
+ const cardTags = card.tags ?? [];
317
301
  const { multiplier, reason } = this.computeInterferenceEffect(
318
302
  cardTags,
319
303
  tagsToAvoid,
@@ -354,14 +338,4 @@ export default class InterferenceMitigatorNavigator extends ContentNavigator imp
354
338
  'Use Pipeline with a generator and this filter via transform().'
355
339
  );
356
340
  }
357
-
358
- // Legacy methods - stub implementations since filters don't generate cards
359
-
360
- async getNewCards(_n?: number): Promise<StudySessionNewItem[]> {
361
- return [];
362
- }
363
-
364
- async getPendingReviews(): Promise<(StudySessionReviewItem & ScheduledCard)[]> {
365
- return [];
366
- }
367
341
  }