@vue-skuilder/db 0.1.17 → 0.1.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/dist/{userDB-BqwxtJ_7.d.mts → classroomDB-CZdMBiTU.d.ts} +427 -104
  2. package/dist/{userDB-DNa0XPtn.d.ts → classroomDB-PxDZTky3.d.cts} +427 -104
  3. package/dist/core/index.d.cts +304 -0
  4. package/dist/core/index.d.ts +237 -25
  5. package/dist/core/index.js +2246 -118
  6. package/dist/core/index.js.map +1 -1
  7. package/dist/core/index.mjs +2235 -114
  8. package/dist/core/index.mjs.map +1 -1
  9. package/dist/{dataLayerProvider-VlngD19_.d.mts → dataLayerProvider-D0MoZMjH.d.cts} +1 -1
  10. package/dist/{dataLayerProvider-BV5iZqt_.d.ts → dataLayerProvider-D8o6ZnKW.d.ts} +1 -1
  11. package/dist/impl/couch/{index.d.mts → index.d.cts} +47 -5
  12. package/dist/impl/couch/index.d.ts +46 -4
  13. package/dist/impl/couch/index.js +2250 -134
  14. package/dist/impl/couch/index.js.map +1 -1
  15. package/dist/impl/couch/index.mjs +2212 -97
  16. package/dist/impl/couch/index.mjs.map +1 -1
  17. package/dist/impl/static/{index.d.mts → index.d.cts} +6 -6
  18. package/dist/impl/static/index.d.ts +5 -5
  19. package/dist/impl/static/index.js +1950 -143
  20. package/dist/impl/static/index.js.map +1 -1
  21. package/dist/impl/static/index.mjs +1922 -117
  22. package/dist/impl/static/index.mjs.map +1 -1
  23. package/dist/{index-Bmll7Xse.d.mts → index-B_j6u5E4.d.cts} +1 -1
  24. package/dist/{index-CD8BZz2k.d.ts → index-Dj0SEgk3.d.ts} +1 -1
  25. package/dist/{index.d.mts → index.d.cts} +97 -13
  26. package/dist/index.d.ts +96 -12
  27. package/dist/index.js +2439 -180
  28. package/dist/index.js.map +1 -1
  29. package/dist/index.mjs +2386 -135
  30. package/dist/index.mjs.map +1 -1
  31. package/dist/pouch/index.js +3 -3
  32. package/dist/{types-Dbp5DaRR.d.mts → types-Bn0itutr.d.cts} +1 -1
  33. package/dist/{types-CewsN87z.d.ts → types-DQaXnuoc.d.ts} +1 -1
  34. package/dist/{types-legacy-6ettoclI.d.ts → types-legacy-DDY4N-Uq.d.cts} +3 -1
  35. package/dist/{types-legacy-6ettoclI.d.mts → types-legacy-DDY4N-Uq.d.ts} +3 -1
  36. package/dist/util/packer/{index.d.mts → index.d.cts} +3 -3
  37. package/dist/util/packer/index.d.ts +3 -3
  38. package/dist/util/packer/index.js.map +1 -1
  39. package/dist/util/packer/index.mjs.map +1 -1
  40. package/docs/brainstorm-navigation-paradigm.md +369 -0
  41. package/docs/navigators-architecture.md +370 -0
  42. package/docs/todo-evolutionary-orchestration.md +310 -0
  43. package/docs/todo-nominal-tag-types.md +121 -0
  44. package/docs/todo-strategy-authoring.md +401 -0
  45. package/eslint.config.mjs +1 -1
  46. package/package.json +9 -4
  47. package/src/core/index.ts +1 -0
  48. package/src/core/interfaces/contentSource.ts +88 -4
  49. package/src/core/interfaces/courseDB.ts +13 -0
  50. package/src/core/interfaces/navigationStrategyManager.ts +0 -5
  51. package/src/core/interfaces/userDB.ts +32 -0
  52. package/src/core/navigators/CompositeGenerator.ts +268 -0
  53. package/src/core/navigators/Pipeline.ts +318 -0
  54. package/src/core/navigators/PipelineAssembler.ts +194 -0
  55. package/src/core/navigators/elo.ts +104 -15
  56. package/src/core/navigators/filters/eloDistance.ts +132 -0
  57. package/src/core/navigators/filters/index.ts +9 -0
  58. package/src/core/navigators/filters/types.ts +115 -0
  59. package/src/core/navigators/filters/userTagPreference.ts +232 -0
  60. package/src/core/navigators/generators/index.ts +2 -0
  61. package/src/core/navigators/generators/types.ts +107 -0
  62. package/src/core/navigators/hardcodedOrder.ts +111 -12
  63. package/src/core/navigators/hierarchyDefinition.ts +266 -0
  64. package/src/core/navigators/index.ts +404 -3
  65. package/src/core/navigators/inferredPreference.ts +107 -0
  66. package/src/core/navigators/interferenceMitigator.ts +355 -0
  67. package/src/core/navigators/relativePriority.ts +255 -0
  68. package/src/core/navigators/srs.ts +195 -0
  69. package/src/core/navigators/userGoal.ts +136 -0
  70. package/src/core/types/strategyState.ts +84 -0
  71. package/src/core/types/types-legacy.ts +2 -0
  72. package/src/impl/common/BaseUserDB.ts +74 -7
  73. package/src/impl/couch/adminDB.ts +1 -2
  74. package/src/impl/couch/classroomDB.ts +51 -0
  75. package/src/impl/couch/courseDB.ts +147 -49
  76. package/src/impl/static/courseDB.ts +11 -4
  77. package/src/study/SessionController.ts +149 -1
  78. package/src/study/TagFilteredContentSource.ts +255 -0
  79. package/src/study/index.ts +1 -0
  80. package/src/util/dataDirectory.test.ts +51 -22
  81. package/src/util/logger.ts +0 -1
  82. package/tests/core/navigators/CompositeGenerator.test.ts +455 -0
  83. package/tests/core/navigators/Pipeline.test.ts +406 -0
  84. package/tests/core/navigators/PipelineAssembler.test.ts +351 -0
  85. package/tests/core/navigators/SRSNavigator.test.ts +344 -0
  86. package/tests/core/navigators/eloDistanceFilter.test.ts +192 -0
  87. package/tests/core/navigators/navigators.test.ts +710 -0
  88. package/tsconfig.json +1 -1
  89. package/vitest.config.ts +29 -0
  90. package/dist/core/index.d.mts +0 -92
  91. /package/dist/{SyncStrategy-CyATpyLQ.d.mts → SyncStrategy-CyATpyLQ.d.cts} +0 -0
  92. /package/dist/pouch/{index.d.mts → index.d.cts} +0 -0
@@ -0,0 +1,304 @@
1
+ import { W as WeightedCard, U as UserDBInterface, C as CourseDBInterface } from '../classroomDB-PxDZTky3.cjs';
2
+ export { J as ActivityRecord, A as AdminDBInterface, v as AssignedCard, h as AssignedContent, u as AssignedCourse, t as AssignedTag, c as ClassroomDBInterface, F as ClassroomRegistration, E as ClassroomRegistrationDesignation, G as ClassroomRegistrationDoc, k as ContentNavigator, q as ContentSourceID, d as CourseInfo, K as CourseRegistration, s as CourseRegistrationDoc, b as CoursesDBInterface, O as NavigatorRole, P as NavigatorRoles, N as Navigators, g as ScheduledCard, H as SessionTrackingData, L as StrategyContribution, j as StudentClassroomDBInterface, i as StudyContentSource, m as StudySessionFailedItem, n as StudySessionFailedNewItem, o as StudySessionFailedReviewItem, l as StudySessionItem, S as StudySessionNewItem, f as StudySessionReviewItem, T as TeacherClassroomDBInterface, I as UserConfig, z as UserCourseSetting, y as UserCourseSettings, x as UserDBAuthenticator, a as UserDBReader, w as UserDBWriter, B as UsrCrsDataInterface, M as getCardOrigin, r as getStudySource, R as isFilter, Q as isGenerator, p as isReview } from '../classroomDB-PxDZTky3.cjs';
3
+ export { D as DataLayerProvider } from '../dataLayerProvider-D0MoZMjH.cjs';
4
+ import { b as DocTypePrefixes, D as DocType, C as CardHistory, c as CardRecord, i as QuestionRecord } from '../types-legacy-DDY4N-Uq.cjs';
5
+ export { d as CardData, e as CourseListData, g as DataShapeData, f as DisplayableData, F as Field, G as GuestUsername, Q as QualifiedCardID, h as QuestionData, S as SkuilderCourseData, a as Tag, T as TagStub, l as log } from '../types-legacy-DDY4N-Uq.cjs';
6
+ import { DataShape, ParsedCard } from '@vue-skuilder/common';
7
+ import 'moment';
8
+
9
+ declare abstract class Loggable {
10
+ protected abstract readonly _className: string;
11
+ protected log(...args: unknown[]): void;
12
+ protected error(...args: unknown[]): void;
13
+ }
14
+
15
+ /**
16
+ * Shared context available to all filters in a pipeline.
17
+ *
18
+ * Built once per getWeightedCards() call and passed to each filter.
19
+ * This avoids repeated lookups for common data like user ELO.
20
+ */
21
+ interface FilterContext {
22
+ /** User database interface */
23
+ user: UserDBInterface;
24
+ /** Course database interface */
25
+ course: CourseDBInterface;
26
+ /** User's global ELO score for this course */
27
+ userElo: number;
28
+ }
29
+ /**
30
+ * A filter that transforms a list of weighted cards.
31
+ *
32
+ * Filters are pure transforms - they receive cards and context,
33
+ * and return a modified list of cards. No delegate wrapping,
34
+ * no side effects beyond provenance tracking.
35
+ *
36
+ * ## Implementation Guidelines
37
+ *
38
+ * 1. **Append provenance**: Every filter should add a StrategyContribution
39
+ * entry documenting its decision for each card.
40
+ *
41
+ * 2. **Use multipliers**: Adjust scores by multiplying, not replacing.
42
+ * This ensures filter order doesn't matter.
43
+ *
44
+ * 3. **Score 0 for exclusion**: To exclude a card, set score to 0.
45
+ * Don't filter it out - let provenance show why it was excluded.
46
+ *
47
+ * 4. **Don't sort**: The Pipeline handles final sorting.
48
+ * Filters just transform scores.
49
+ *
50
+ * ## Example Implementation
51
+ *
52
+ * ```typescript
53
+ * const myFilter: CardFilter = {
54
+ * name: 'My Filter',
55
+ * async transform(cards, context) {
56
+ * return cards.map(card => {
57
+ * const multiplier = computeMultiplier(card, context);
58
+ * const newScore = card.score * multiplier;
59
+ * return {
60
+ * ...card,
61
+ * score: newScore,
62
+ * provenance: [...card.provenance, {
63
+ * strategy: 'myFilter',
64
+ * strategyName: 'My Filter',
65
+ * strategyId: 'MY_FILTER',
66
+ * action: multiplier < 1 ? 'penalized' : 'passed',
67
+ * score: newScore,
68
+ * reason: 'Explanation of decision'
69
+ * }]
70
+ * };
71
+ * });
72
+ * }
73
+ * };
74
+ * ```
75
+ */
76
+ interface CardFilter {
77
+ /** Human-readable name for this filter */
78
+ name: string;
79
+ /**
80
+ * Transform a list of weighted cards.
81
+ *
82
+ * @param cards - Cards to transform (already scored by generator)
83
+ * @param context - Shared context (user, course, userElo, etc.)
84
+ * @returns Transformed cards with updated scores and provenance
85
+ */
86
+ transform(cards: WeightedCard[], context: FilterContext): Promise<WeightedCard[]>;
87
+ }
88
+ /**
89
+ * Factory function type for creating filters from configuration.
90
+ *
91
+ * Used by PipelineAssembler to instantiate filters from strategy documents.
92
+ */
93
+ type CardFilterFactory<TConfig = unknown> = (config: TConfig) => CardFilter;
94
+
95
+ /**
96
+ * Context available to generators when producing candidates.
97
+ *
98
+ * Built once per getWeightedCards() call by the Pipeline.
99
+ */
100
+ interface GeneratorContext {
101
+ /** User database interface */
102
+ user: UserDBInterface;
103
+ /** Course database interface */
104
+ course: CourseDBInterface;
105
+ /** User's global ELO score for this course */
106
+ userElo: number;
107
+ }
108
+ /**
109
+ * A generator that produces candidate cards with initial scores.
110
+ *
111
+ * Generators are the "source" stage of a navigation pipeline.
112
+ * They query the database for eligible cards and assign initial
113
+ * suitability scores based on their strategy (ELO proximity,
114
+ * review urgency, fixed order, etc.).
115
+ *
116
+ * ## Implementation Guidelines
117
+ *
118
+ * 1. **Create provenance**: Each card should have a provenance entry
119
+ * with action='generated' documenting why it was selected.
120
+ *
121
+ * 2. **Score semantics**: Higher scores = more suitable for presentation.
122
+ * Scores should be in [0, 1] range for composability.
123
+ *
124
+ * 3. **Limit handling**: Respect the limit parameter, but may over-fetch
125
+ * internally if needed for scoring accuracy.
126
+ *
127
+ * 4. **Sort before returning**: Return cards sorted by score descending.
128
+ *
129
+ * ## Example Implementation
130
+ *
131
+ * ```typescript
132
+ * const myGenerator: CardGenerator = {
133
+ * name: 'My Generator',
134
+ * async getWeightedCards(limit, context) {
135
+ * const candidates = await fetchCandidates(context.course, limit);
136
+ * return candidates.map(c => ({
137
+ * cardId: c.id,
138
+ * courseId: context.course.getCourseID(),
139
+ * score: computeScore(c, context),
140
+ * provenance: [{
141
+ * strategy: 'myGenerator',
142
+ * strategyName: 'My Generator',
143
+ * strategyId: 'MY_GENERATOR',
144
+ * action: 'generated',
145
+ * score: computeScore(c, context),
146
+ * reason: 'Explanation of selection'
147
+ * }]
148
+ * }));
149
+ * }
150
+ * };
151
+ * ```
152
+ */
153
+ interface CardGenerator {
154
+ /** Human-readable name for this generator */
155
+ name: string;
156
+ /**
157
+ * Produce candidate cards with initial scores.
158
+ *
159
+ * @param limit - Maximum number of cards to return
160
+ * @param context - Shared context (user, course, userElo, etc.)
161
+ * @returns Cards sorted by score descending, with provenance
162
+ */
163
+ getWeightedCards(limit: number, context: GeneratorContext): Promise<WeightedCard[]>;
164
+ }
165
+ /**
166
+ * Factory function type for creating generators from configuration.
167
+ *
168
+ * Used by PipelineAssembler to instantiate generators from strategy documents.
169
+ */
170
+ type CardGeneratorFactory<TConfig = unknown> = (config: TConfig) => CardGenerator;
171
+
172
+ /**
173
+ * Template literal type for strategy state document IDs.
174
+ *
175
+ * Format: `STRATEGY_STATE-{courseId}-{strategyKey}`
176
+ */
177
+ type StrategyStateId = `${(typeof DocTypePrefixes)[DocType.STRATEGY_STATE]}::${string}::${string}`;
178
+ /**
179
+ * Document storing strategy-specific state in the user database.
180
+ *
181
+ * Each strategy can persist its own state (user preferences, learned patterns,
182
+ * temporal tracking, etc.) using this document type. The state is scoped to
183
+ * a (user, course, strategy) tuple.
184
+ *
185
+ * ## Use Cases
186
+ *
187
+ * 1. **Explicit user preferences**: User configures tag filters, difficulty
188
+ * preferences, or learning goals. UI writes to strategy state.
189
+ *
190
+ * 2. **Learned/temporal state**: Strategy tracks patterns over time, e.g.,
191
+ * "when did I last introduce confusable concepts together?"
192
+ *
193
+ * 3. **Adaptive personalization**: Strategy infers user preferences from
194
+ * behavior and stores them for future sessions.
195
+ *
196
+ * ## Storage Location
197
+ *
198
+ * These documents live in the **user database**, not the course database.
199
+ * They sync with the user's data across devices.
200
+ *
201
+ * ## Document ID Format
202
+ *
203
+ * `STRATEGY_STATE::{courseId}::{strategyKey}`
204
+ *
205
+ * Example: `STRATEGY_STATE::piano-basics::UserTagPreferenceFilter`
206
+ *
207
+ * @template T - The shape of the strategy-specific data payload
208
+ */
209
+ interface StrategyStateDoc<T = unknown> {
210
+ _id: StrategyStateId;
211
+ _rev?: string;
212
+ docType: DocType.STRATEGY_STATE;
213
+ /**
214
+ * The course this state applies to.
215
+ */
216
+ courseId: string;
217
+ /**
218
+ * Unique key identifying the strategy instance.
219
+ * Typically the strategy class name (e.g., "UserTagPreferenceFilter",
220
+ * "InterferenceMitigatorNavigator").
221
+ *
222
+ * If a course has multiple instances of the same strategy type with
223
+ * different configurations, use a more specific key.
224
+ */
225
+ strategyKey: string;
226
+ /**
227
+ * Strategy-specific data payload.
228
+ * Each strategy defines its own schema for this field.
229
+ */
230
+ data: T;
231
+ /**
232
+ * ISO timestamp of last update.
233
+ * Use `moment.utc(updatedAt)` to parse into a Moment object.
234
+ */
235
+ updatedAt: string;
236
+ }
237
+ /**
238
+ * Build the document ID for a strategy state document.
239
+ *
240
+ * @param courseId - The course ID
241
+ * @param strategyKey - The strategy key (typically class name)
242
+ * @returns The document ID in format `STRATEGY_STATE::{courseId}::{strategyKey}`
243
+ */
244
+ declare function buildStrategyStateId(courseId: string, strategyKey: string): StrategyStateId;
245
+
246
+ declare function areQuestionRecords(h: CardHistory<CardRecord>): h is CardHistory<QuestionRecord>;
247
+ declare function isQuestionRecord(c: CardRecord): c is QuestionRecord;
248
+ declare function getCardHistoryID(courseID: string, cardID: string): PouchDB.Core.DocumentId;
249
+ declare function parseCardHistoryID(id: string): {
250
+ courseID: string;
251
+ cardID: string;
252
+ };
253
+ interface PouchDBError extends Error {
254
+ error?: string;
255
+ reason?: string;
256
+ }
257
+ declare function docIsDeleted(e: PouchDBError): boolean;
258
+
259
+ /**
260
+ * Interface representing the result of a bulk import operation for a single card
261
+ */
262
+ interface ImportResult {
263
+ /** The original text input for the card */
264
+ originalText: string;
265
+ /** Status of the import operation */
266
+ status: 'success' | 'error';
267
+ /** Message describing the result or error */
268
+ message: string;
269
+ /** ID of the newly created card (only for success) */
270
+ cardId?: string;
271
+ }
272
+ /**
273
+ * Configuration for the bulk card processor
274
+ */
275
+ interface BulkCardProcessorConfig {
276
+ /** The data shape to use for the cards */
277
+ dataShape: DataShape;
278
+ /** The course code used for adding notes */
279
+ courseCode: string;
280
+ /** The username of the current user */
281
+ userName: string;
282
+ }
283
+
284
+ /**
285
+ * Processes multiple cards from bulk text input
286
+ *
287
+ * @param parsedCards - Array of parsed cards to import
288
+ * @param courseDB - Course database interface
289
+ * @param config - Configuration for the card processor
290
+ * @returns Array of import results
291
+ */
292
+ declare function importParsedCards(parsedCards: ParsedCard[], courseDB: CourseDBInterface, config: BulkCardProcessorConfig): Promise<ImportResult[]>;
293
+ /**
294
+ * Validates the configuration for bulk card processing
295
+ *
296
+ * @param config - Configuration to validate
297
+ * @returns Object with validation result and error message if any
298
+ */
299
+ declare function validateProcessorConfig(config: Partial<BulkCardProcessorConfig>): {
300
+ isValid: boolean;
301
+ errorMessage?: string;
302
+ };
303
+
304
+ export { type BulkCardProcessorConfig, type CardFilter, type CardFilterFactory, type CardGenerator, type CardGeneratorFactory, CardHistory, CardRecord, CourseDBInterface, DocType, DocTypePrefixes, type FilterContext, type GeneratorContext, type ImportResult, Loggable, QuestionRecord, type StrategyStateDoc, type StrategyStateId, UserDBInterface, WeightedCard, areQuestionRecords, buildStrategyStateId, docIsDeleted, getCardHistoryID, importParsedCards, isQuestionRecord, parseCardHistoryID, validateProcessorConfig };
@@ -1,8 +1,8 @@
1
- import { i as StudyContentSource, U as UserDBInterface, C as CourseDBInterface, e as ContentNavigationStrategyData, f as StudySessionReviewItem, g as ScheduledCard, S as StudySessionNewItem } from '../userDB-DNa0XPtn.js';
2
- export { I as ActivityRecord, A as AdminDBInterface, u as AssignedCard, h as AssignedContent, t as AssignedCourse, s as AssignedTag, c as ClassroomDBInterface, E as ClassroomRegistration, B as ClassroomRegistrationDesignation, F as ClassroomRegistrationDoc, p as ContentSourceID, d as CourseInfo, J as CourseRegistration, r as CourseRegistrationDoc, b as CoursesDBInterface, G as SessionTrackingData, j as StudentClassroomDBInterface, l as StudySessionFailedItem, m as StudySessionFailedNewItem, n as StudySessionFailedReviewItem, k as StudySessionItem, T as TeacherClassroomDBInterface, H as UserConfig, y as UserCourseSetting, x as UserCourseSettings, w as UserDBAuthenticator, a as UserDBReader, v as UserDBWriter, z as UsrCrsDataInterface, q as getStudySource, o as isReview } from '../userDB-DNa0XPtn.js';
3
- export { D as DataLayerProvider } from '../dataLayerProvider-BV5iZqt_.js';
4
- import { C as CardHistory, c as CardRecord, i as QuestionRecord } from '../types-legacy-6ettoclI.js';
5
- export { d as CardData, e as CourseListData, g as DataShapeData, f as DisplayableData, D as DocType, b as DocTypePrefixes, F as Field, G as GuestUsername, Q as QualifiedCardID, h as QuestionData, S as SkuilderCourseData, a as Tag, T as TagStub, l as log } from '../types-legacy-6ettoclI.js';
1
+ import { W as WeightedCard, U as UserDBInterface, C as CourseDBInterface } from '../classroomDB-CZdMBiTU.js';
2
+ export { J as ActivityRecord, A as AdminDBInterface, v as AssignedCard, h as AssignedContent, u as AssignedCourse, t as AssignedTag, c as ClassroomDBInterface, F as ClassroomRegistration, E as ClassroomRegistrationDesignation, G as ClassroomRegistrationDoc, k as ContentNavigator, q as ContentSourceID, d as CourseInfo, K as CourseRegistration, s as CourseRegistrationDoc, b as CoursesDBInterface, O as NavigatorRole, P as NavigatorRoles, N as Navigators, g as ScheduledCard, H as SessionTrackingData, L as StrategyContribution, j as StudentClassroomDBInterface, i as StudyContentSource, m as StudySessionFailedItem, n as StudySessionFailedNewItem, o as StudySessionFailedReviewItem, l as StudySessionItem, S as StudySessionNewItem, f as StudySessionReviewItem, T as TeacherClassroomDBInterface, I as UserConfig, z as UserCourseSetting, y as UserCourseSettings, x as UserDBAuthenticator, a as UserDBReader, w as UserDBWriter, B as UsrCrsDataInterface, M as getCardOrigin, r as getStudySource, R as isFilter, Q as isGenerator, p as isReview } from '../classroomDB-CZdMBiTU.js';
3
+ export { D as DataLayerProvider } from '../dataLayerProvider-D8o6ZnKW.js';
4
+ import { b as DocTypePrefixes, D as DocType, C as CardHistory, c as CardRecord, i as QuestionRecord } from '../types-legacy-DDY4N-Uq.js';
5
+ export { d as CardData, e as CourseListData, g as DataShapeData, f as DisplayableData, F as Field, G as GuestUsername, Q as QualifiedCardID, h as QuestionData, S as SkuilderCourseData, a as Tag, T as TagStub, l as log } from '../types-legacy-DDY4N-Uq.js';
6
6
  import { DataShape, ParsedCard } from '@vue-skuilder/common';
7
7
  import 'moment';
8
8
 
@@ -12,6 +12,237 @@ declare abstract class Loggable {
12
12
  protected error(...args: unknown[]): void;
13
13
  }
14
14
 
15
+ /**
16
+ * Shared context available to all filters in a pipeline.
17
+ *
18
+ * Built once per getWeightedCards() call and passed to each filter.
19
+ * This avoids repeated lookups for common data like user ELO.
20
+ */
21
+ interface FilterContext {
22
+ /** User database interface */
23
+ user: UserDBInterface;
24
+ /** Course database interface */
25
+ course: CourseDBInterface;
26
+ /** User's global ELO score for this course */
27
+ userElo: number;
28
+ }
29
+ /**
30
+ * A filter that transforms a list of weighted cards.
31
+ *
32
+ * Filters are pure transforms - they receive cards and context,
33
+ * and return a modified list of cards. No delegate wrapping,
34
+ * no side effects beyond provenance tracking.
35
+ *
36
+ * ## Implementation Guidelines
37
+ *
38
+ * 1. **Append provenance**: Every filter should add a StrategyContribution
39
+ * entry documenting its decision for each card.
40
+ *
41
+ * 2. **Use multipliers**: Adjust scores by multiplying, not replacing.
42
+ * This ensures filter order doesn't matter.
43
+ *
44
+ * 3. **Score 0 for exclusion**: To exclude a card, set score to 0.
45
+ * Don't filter it out - let provenance show why it was excluded.
46
+ *
47
+ * 4. **Don't sort**: The Pipeline handles final sorting.
48
+ * Filters just transform scores.
49
+ *
50
+ * ## Example Implementation
51
+ *
52
+ * ```typescript
53
+ * const myFilter: CardFilter = {
54
+ * name: 'My Filter',
55
+ * async transform(cards, context) {
56
+ * return cards.map(card => {
57
+ * const multiplier = computeMultiplier(card, context);
58
+ * const newScore = card.score * multiplier;
59
+ * return {
60
+ * ...card,
61
+ * score: newScore,
62
+ * provenance: [...card.provenance, {
63
+ * strategy: 'myFilter',
64
+ * strategyName: 'My Filter',
65
+ * strategyId: 'MY_FILTER',
66
+ * action: multiplier < 1 ? 'penalized' : 'passed',
67
+ * score: newScore,
68
+ * reason: 'Explanation of decision'
69
+ * }]
70
+ * };
71
+ * });
72
+ * }
73
+ * };
74
+ * ```
75
+ */
76
+ interface CardFilter {
77
+ /** Human-readable name for this filter */
78
+ name: string;
79
+ /**
80
+ * Transform a list of weighted cards.
81
+ *
82
+ * @param cards - Cards to transform (already scored by generator)
83
+ * @param context - Shared context (user, course, userElo, etc.)
84
+ * @returns Transformed cards with updated scores and provenance
85
+ */
86
+ transform(cards: WeightedCard[], context: FilterContext): Promise<WeightedCard[]>;
87
+ }
88
+ /**
89
+ * Factory function type for creating filters from configuration.
90
+ *
91
+ * Used by PipelineAssembler to instantiate filters from strategy documents.
92
+ */
93
+ type CardFilterFactory<TConfig = unknown> = (config: TConfig) => CardFilter;
94
+
95
+ /**
96
+ * Context available to generators when producing candidates.
97
+ *
98
+ * Built once per getWeightedCards() call by the Pipeline.
99
+ */
100
+ interface GeneratorContext {
101
+ /** User database interface */
102
+ user: UserDBInterface;
103
+ /** Course database interface */
104
+ course: CourseDBInterface;
105
+ /** User's global ELO score for this course */
106
+ userElo: number;
107
+ }
108
+ /**
109
+ * A generator that produces candidate cards with initial scores.
110
+ *
111
+ * Generators are the "source" stage of a navigation pipeline.
112
+ * They query the database for eligible cards and assign initial
113
+ * suitability scores based on their strategy (ELO proximity,
114
+ * review urgency, fixed order, etc.).
115
+ *
116
+ * ## Implementation Guidelines
117
+ *
118
+ * 1. **Create provenance**: Each card should have a provenance entry
119
+ * with action='generated' documenting why it was selected.
120
+ *
121
+ * 2. **Score semantics**: Higher scores = more suitable for presentation.
122
+ * Scores should be in [0, 1] range for composability.
123
+ *
124
+ * 3. **Limit handling**: Respect the limit parameter, but may over-fetch
125
+ * internally if needed for scoring accuracy.
126
+ *
127
+ * 4. **Sort before returning**: Return cards sorted by score descending.
128
+ *
129
+ * ## Example Implementation
130
+ *
131
+ * ```typescript
132
+ * const myGenerator: CardGenerator = {
133
+ * name: 'My Generator',
134
+ * async getWeightedCards(limit, context) {
135
+ * const candidates = await fetchCandidates(context.course, limit);
136
+ * return candidates.map(c => ({
137
+ * cardId: c.id,
138
+ * courseId: context.course.getCourseID(),
139
+ * score: computeScore(c, context),
140
+ * provenance: [{
141
+ * strategy: 'myGenerator',
142
+ * strategyName: 'My Generator',
143
+ * strategyId: 'MY_GENERATOR',
144
+ * action: 'generated',
145
+ * score: computeScore(c, context),
146
+ * reason: 'Explanation of selection'
147
+ * }]
148
+ * }));
149
+ * }
150
+ * };
151
+ * ```
152
+ */
153
+ interface CardGenerator {
154
+ /** Human-readable name for this generator */
155
+ name: string;
156
+ /**
157
+ * Produce candidate cards with initial scores.
158
+ *
159
+ * @param limit - Maximum number of cards to return
160
+ * @param context - Shared context (user, course, userElo, etc.)
161
+ * @returns Cards sorted by score descending, with provenance
162
+ */
163
+ getWeightedCards(limit: number, context: GeneratorContext): Promise<WeightedCard[]>;
164
+ }
165
+ /**
166
+ * Factory function type for creating generators from configuration.
167
+ *
168
+ * Used by PipelineAssembler to instantiate generators from strategy documents.
169
+ */
170
+ type CardGeneratorFactory<TConfig = unknown> = (config: TConfig) => CardGenerator;
171
+
172
+ /**
173
+ * Template literal type for strategy state document IDs.
174
+ *
175
+ * Format: `STRATEGY_STATE-{courseId}-{strategyKey}`
176
+ */
177
+ type StrategyStateId = `${(typeof DocTypePrefixes)[DocType.STRATEGY_STATE]}::${string}::${string}`;
178
+ /**
179
+ * Document storing strategy-specific state in the user database.
180
+ *
181
+ * Each strategy can persist its own state (user preferences, learned patterns,
182
+ * temporal tracking, etc.) using this document type. The state is scoped to
183
+ * a (user, course, strategy) tuple.
184
+ *
185
+ * ## Use Cases
186
+ *
187
+ * 1. **Explicit user preferences**: User configures tag filters, difficulty
188
+ * preferences, or learning goals. UI writes to strategy state.
189
+ *
190
+ * 2. **Learned/temporal state**: Strategy tracks patterns over time, e.g.,
191
+ * "when did I last introduce confusable concepts together?"
192
+ *
193
+ * 3. **Adaptive personalization**: Strategy infers user preferences from
194
+ * behavior and stores them for future sessions.
195
+ *
196
+ * ## Storage Location
197
+ *
198
+ * These documents live in the **user database**, not the course database.
199
+ * They sync with the user's data across devices.
200
+ *
201
+ * ## Document ID Format
202
+ *
203
+ * `STRATEGY_STATE::{courseId}::{strategyKey}`
204
+ *
205
+ * Example: `STRATEGY_STATE::piano-basics::UserTagPreferenceFilter`
206
+ *
207
+ * @template T - The shape of the strategy-specific data payload
208
+ */
209
+ interface StrategyStateDoc<T = unknown> {
210
+ _id: StrategyStateId;
211
+ _rev?: string;
212
+ docType: DocType.STRATEGY_STATE;
213
+ /**
214
+ * The course this state applies to.
215
+ */
216
+ courseId: string;
217
+ /**
218
+ * Unique key identifying the strategy instance.
219
+ * Typically the strategy class name (e.g., "UserTagPreferenceFilter",
220
+ * "InterferenceMitigatorNavigator").
221
+ *
222
+ * If a course has multiple instances of the same strategy type with
223
+ * different configurations, use a more specific key.
224
+ */
225
+ strategyKey: string;
226
+ /**
227
+ * Strategy-specific data payload.
228
+ * Each strategy defines its own schema for this field.
229
+ */
230
+ data: T;
231
+ /**
232
+ * ISO timestamp of last update.
233
+ * Use `moment.utc(updatedAt)` to parse into a Moment object.
234
+ */
235
+ updatedAt: string;
236
+ }
237
+ /**
238
+ * Build the document ID for a strategy state document.
239
+ *
240
+ * @param courseId - The course ID
241
+ * @param strategyKey - The strategy key (typically class name)
242
+ * @returns The document ID in format `STRATEGY_STATE::{courseId}::{strategyKey}`
243
+ */
244
+ declare function buildStrategyStateId(courseId: string, strategyKey: string): StrategyStateId;
245
+
15
246
  declare function areQuestionRecords(h: CardHistory<CardRecord>): h is CardHistory<QuestionRecord>;
16
247
  declare function isQuestionRecord(c: CardRecord): c is QuestionRecord;
17
248
  declare function getCardHistoryID(courseID: string, cardID: string): PouchDB.Core.DocumentId;
@@ -25,25 +256,6 @@ interface PouchDBError extends Error {
25
256
  }
26
257
  declare function docIsDeleted(e: PouchDBError): boolean;
27
258
 
28
- declare enum Navigators {
29
- ELO = "elo",
30
- HARDCODED = "hardcodedOrder"
31
- }
32
- /**
33
- * A content-navigator provides runtime steering of study sessions.
34
- */
35
- declare abstract class ContentNavigator implements StudyContentSource {
36
- /**
37
- *
38
- * @param user
39
- * @param strategyData
40
- * @returns the runtime object used to steer a study session.
41
- */
42
- static create(user: UserDBInterface, course: CourseDBInterface, strategyData: ContentNavigationStrategyData): Promise<ContentNavigator>;
43
- abstract getPendingReviews(): Promise<(StudySessionReviewItem & ScheduledCard)[]>;
44
- abstract getNewCards(n?: number): Promise<StudySessionNewItem[]>;
45
- }
46
-
47
259
  /**
48
260
  * Interface representing the result of a bulk import operation for a single card
49
261
  */
@@ -89,4 +301,4 @@ declare function validateProcessorConfig(config: Partial<BulkCardProcessorConfig
89
301
  errorMessage?: string;
90
302
  };
91
303
 
92
- export { type BulkCardProcessorConfig, CardHistory, CardRecord, ContentNavigator, CourseDBInterface, type ImportResult, Loggable, Navigators, QuestionRecord, ScheduledCard, StudyContentSource, StudySessionNewItem, StudySessionReviewItem, UserDBInterface, areQuestionRecords, docIsDeleted, getCardHistoryID, importParsedCards, isQuestionRecord, parseCardHistoryID, validateProcessorConfig };
304
+ export { type BulkCardProcessorConfig, type CardFilter, type CardFilterFactory, type CardGenerator, type CardGeneratorFactory, CardHistory, CardRecord, CourseDBInterface, DocType, DocTypePrefixes, type FilterContext, type GeneratorContext, type ImportResult, Loggable, QuestionRecord, type StrategyStateDoc, type StrategyStateId, UserDBInterface, WeightedCard, areQuestionRecords, buildStrategyStateId, docIsDeleted, getCardHistoryID, importParsedCards, isQuestionRecord, parseCardHistoryID, validateProcessorConfig };