@vue-skuilder/db 0.1.16 → 0.1.18

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 (80) hide show
  1. package/dist/{userDB-DNa0XPtn.d.ts → classroomDB-BgfrVb8d.d.ts} +357 -103
  2. package/dist/{userDB-BqwxtJ_7.d.mts → classroomDB-CTOenngH.d.cts} +358 -104
  3. package/dist/core/index.d.cts +230 -0
  4. package/dist/core/index.d.ts +161 -23
  5. package/dist/core/index.js +1964 -154
  6. package/dist/core/index.js.map +1 -1
  7. package/dist/core/index.mjs +1925 -121
  8. package/dist/core/index.mjs.map +1 -1
  9. package/dist/{dataLayerProvider-BV5iZqt_.d.ts → dataLayerProvider-CZxC9GtB.d.ts} +1 -1
  10. package/dist/{dataLayerProvider-VlngD19_.d.mts → dataLayerProvider-D6PoCwS6.d.cts} +1 -1
  11. package/dist/impl/couch/{index.d.mts → index.d.cts} +46 -5
  12. package/dist/impl/couch/index.d.ts +44 -3
  13. package/dist/impl/couch/index.js +1971 -171
  14. package/dist/impl/couch/index.js.map +1 -1
  15. package/dist/impl/couch/index.mjs +1933 -134
  16. package/dist/impl/couch/index.mjs.map +1 -1
  17. package/dist/impl/static/{index.d.mts → index.d.cts} +5 -6
  18. package/dist/impl/static/index.d.ts +2 -3
  19. package/dist/impl/static/index.js +1614 -119
  20. package/dist/impl/static/index.js.map +1 -1
  21. package/dist/impl/static/index.mjs +1585 -92
  22. package/dist/impl/static/index.mjs.map +1 -1
  23. package/dist/{index-Bmll7Xse.d.mts → index-D-Fa4Smt.d.cts} +1 -1
  24. package/dist/{index.d.mts → index.d.cts} +97 -13
  25. package/dist/index.d.ts +90 -6
  26. package/dist/index.js +2085 -153
  27. package/dist/index.js.map +1 -1
  28. package/dist/index.mjs +2031 -106
  29. package/dist/index.mjs.map +1 -1
  30. package/dist/pouch/index.js +3 -3
  31. package/dist/{types-Dbp5DaRR.d.mts → types-CzPDLAK6.d.cts} +1 -1
  32. package/dist/util/packer/{index.d.mts → index.d.cts} +3 -3
  33. package/dist/util/packer/index.js.map +1 -1
  34. package/dist/util/packer/index.mjs.map +1 -1
  35. package/docs/brainstorm-navigation-paradigm.md +369 -0
  36. package/docs/navigators-architecture.md +265 -0
  37. package/docs/todo-evolutionary-orchestration.md +310 -0
  38. package/docs/todo-nominal-tag-types.md +121 -0
  39. package/docs/todo-pipeline-optimization.md +117 -0
  40. package/docs/todo-strategy-authoring.md +401 -0
  41. package/docs/todo-strategy-state-storage.md +278 -0
  42. package/eslint.config.mjs +1 -1
  43. package/package.json +9 -4
  44. package/src/core/interfaces/contentSource.ts +88 -4
  45. package/src/core/interfaces/navigationStrategyManager.ts +0 -5
  46. package/src/core/navigators/CompositeGenerator.ts +268 -0
  47. package/src/core/navigators/Pipeline.ts +205 -0
  48. package/src/core/navigators/PipelineAssembler.ts +194 -0
  49. package/src/core/navigators/elo.ts +104 -15
  50. package/src/core/navigators/filters/eloDistance.ts +132 -0
  51. package/src/core/navigators/filters/index.ts +6 -0
  52. package/src/core/navigators/filters/types.ts +115 -0
  53. package/src/core/navigators/generators/index.ts +2 -0
  54. package/src/core/navigators/generators/types.ts +107 -0
  55. package/src/core/navigators/hardcodedOrder.ts +111 -12
  56. package/src/core/navigators/hierarchyDefinition.ts +266 -0
  57. package/src/core/navigators/index.ts +345 -3
  58. package/src/core/navigators/interferenceMitigator.ts +367 -0
  59. package/src/core/navigators/relativePriority.ts +267 -0
  60. package/src/core/navigators/srs.ts +195 -0
  61. package/src/impl/couch/classroomDB.ts +51 -0
  62. package/src/impl/couch/courseDB.ts +117 -39
  63. package/src/impl/static/courseDB.ts +0 -4
  64. package/src/study/SessionController.ts +149 -1
  65. package/src/study/TagFilteredContentSource.ts +255 -0
  66. package/src/study/index.ts +1 -0
  67. package/src/util/dataDirectory.test.ts +51 -22
  68. package/src/util/logger.ts +0 -1
  69. package/tests/core/navigators/CompositeGenerator.test.ts +455 -0
  70. package/tests/core/navigators/Pipeline.test.ts +405 -0
  71. package/tests/core/navigators/PipelineAssembler.test.ts +351 -0
  72. package/tests/core/navigators/SRSNavigator.test.ts +344 -0
  73. package/tests/core/navigators/eloDistanceFilter.test.ts +192 -0
  74. package/tests/core/navigators/navigators.test.ts +710 -0
  75. package/tsconfig.json +1 -1
  76. package/vitest.config.ts +29 -0
  77. package/dist/core/index.d.mts +0 -92
  78. /package/dist/{SyncStrategy-CyATpyLQ.d.mts → SyncStrategy-CyATpyLQ.d.cts} +0 -0
  79. /package/dist/pouch/{index.d.mts → index.d.cts} +0 -0
  80. /package/dist/{types-legacy-6ettoclI.d.mts → types-legacy-6ettoclI.d.cts} +0 -0
@@ -0,0 +1,405 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { Pipeline } from '../../../src/core/navigators/Pipeline';
3
+ import { WeightedCard, ContentNavigator } from '../../../src/core/navigators/index';
4
+ import { CardFilter, FilterContext } from '../../../src/core/navigators/filters/types';
5
+ import { StudySessionNewItem, StudySessionReviewItem } from '../../../src/core/interfaces/contentSource';
6
+ import { ScheduledCard } from '../../../src/core/types/user';
7
+ import { CourseDBInterface } from '../../../src/core/interfaces/courseDB';
8
+ import { UserDBInterface } from '../../../src/core/interfaces/userDB';
9
+
10
+ // ============================================================================
11
+ // MOCK IMPLEMENTATIONS
12
+ // ============================================================================
13
+
14
+ /**
15
+ * Mock generator that returns predefined weighted cards
16
+ */
17
+ class MockGenerator extends ContentNavigator {
18
+ private cards: WeightedCard[];
19
+
20
+ constructor(cards: WeightedCard[]) {
21
+ super();
22
+ this.cards = cards;
23
+ }
24
+
25
+ async getWeightedCards(limit: number): Promise<WeightedCard[]> {
26
+ return this.cards.slice(0, limit);
27
+ }
28
+
29
+ async getNewCards(_n?: number): Promise<StudySessionNewItem[]> {
30
+ return [];
31
+ }
32
+
33
+ async getPendingReviews(): Promise<(StudySessionReviewItem & ScheduledCard)[]> {
34
+ return [];
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Create a mock filter that multiplies scores by a fixed factor
40
+ */
41
+ function createMultiplierFilter(name: string, multiplier: number): CardFilter {
42
+ return {
43
+ name,
44
+ async transform(cards: WeightedCard[], _context: FilterContext): Promise<WeightedCard[]> {
45
+ return cards.map((card) => ({
46
+ ...card,
47
+ score: card.score * multiplier,
48
+ provenance: [
49
+ ...card.provenance,
50
+ {
51
+ strategy: name.toLowerCase().replace(/\s+/g, '-'),
52
+ strategyName: name,
53
+ strategyId: `FILTER-${name.toUpperCase()}`,
54
+ action: multiplier < 1 ? 'penalized' : multiplier > 1 ? 'boosted' : 'passed',
55
+ score: card.score * multiplier,
56
+ reason: `Multiplied by ${multiplier}`,
57
+ },
58
+ ],
59
+ }));
60
+ },
61
+ };
62
+ }
63
+
64
+ /**
65
+ * Create a mock filter that zeros out specific cards
66
+ */
67
+ function createBlockingFilter(name: string, blockedCardIds: string[]): CardFilter {
68
+ return {
69
+ name,
70
+ async transform(cards: WeightedCard[], _context: FilterContext): Promise<WeightedCard[]> {
71
+ return cards.map((card) => {
72
+ const blocked = blockedCardIds.includes(card.cardId);
73
+ return {
74
+ ...card,
75
+ score: blocked ? 0 : card.score,
76
+ provenance: [
77
+ ...card.provenance,
78
+ {
79
+ strategy: 'blocker',
80
+ strategyName: name,
81
+ strategyId: `FILTER-BLOCKER`,
82
+ action: blocked ? 'penalized' : 'passed',
83
+ score: blocked ? 0 : card.score,
84
+ reason: blocked ? 'Blocked' : 'Passed',
85
+ },
86
+ ],
87
+ };
88
+ });
89
+ },
90
+ };
91
+ }
92
+
93
+ /**
94
+ * Create mock user and course interfaces
95
+ */
96
+ function createMockContext(): { user: UserDBInterface; course: CourseDBInterface } {
97
+ const mockUser = {
98
+ getCourseRegDoc: vi.fn().mockResolvedValue({
99
+ elo: { global: { score: 1000, count: 10 }, tags: {} },
100
+ }),
101
+ } as unknown as UserDBInterface;
102
+
103
+ const mockCourse = {
104
+ getCourseID: vi.fn().mockReturnValue('test-course'),
105
+ } as unknown as CourseDBInterface;
106
+
107
+ return { user: mockUser, course: mockCourse };
108
+ }
109
+
110
+ /**
111
+ * Helper to create a weighted card
112
+ */
113
+ function createCard(id: string, score: number): WeightedCard {
114
+ return {
115
+ cardId: id,
116
+ courseId: 'test-course',
117
+ score,
118
+ provenance: [
119
+ {
120
+ strategy: 'test-generator',
121
+ strategyName: 'Test Generator',
122
+ strategyId: 'TEST_GENERATOR',
123
+ action: 'generated',
124
+ score,
125
+ reason: 'Test card, new',
126
+ },
127
+ ],
128
+ };
129
+ }
130
+
131
+ // ============================================================================
132
+ // TESTS
133
+ // ============================================================================
134
+
135
+ describe('Pipeline', () => {
136
+ let mockUser: UserDBInterface;
137
+ let mockCourse: CourseDBInterface;
138
+
139
+ beforeEach(() => {
140
+ const context = createMockContext();
141
+ mockUser = context.user;
142
+ mockCourse = context.course;
143
+ });
144
+
145
+ describe('basic functionality', () => {
146
+ it('should return cards from generator when no filters', async () => {
147
+ const cards = [createCard('card-1', 0.8), createCard('card-2', 0.6)];
148
+ const generator = new MockGenerator(cards);
149
+ const pipeline = new Pipeline(generator, [], mockUser, mockCourse);
150
+
151
+ const result = await pipeline.getWeightedCards(10);
152
+
153
+ expect(result).toHaveLength(2);
154
+ expect(result[0].cardId).toBe('card-1');
155
+ expect(result[1].cardId).toBe('card-2');
156
+ });
157
+
158
+ it('should sort cards by score descending', async () => {
159
+ const cards = [
160
+ createCard('low', 0.3),
161
+ createCard('high', 0.9),
162
+ createCard('mid', 0.6),
163
+ ];
164
+ const generator = new MockGenerator(cards);
165
+ const pipeline = new Pipeline(generator, [], mockUser, mockCourse);
166
+
167
+ const result = await pipeline.getWeightedCards(10);
168
+
169
+ expect(result[0].cardId).toBe('high');
170
+ expect(result[1].cardId).toBe('mid');
171
+ expect(result[2].cardId).toBe('low');
172
+ });
173
+
174
+ it('should respect limit parameter', async () => {
175
+ const cards = [
176
+ createCard('card-1', 0.9),
177
+ createCard('card-2', 0.8),
178
+ createCard('card-3', 0.7),
179
+ createCard('card-4', 0.6),
180
+ ];
181
+ const generator = new MockGenerator(cards);
182
+ const pipeline = new Pipeline(generator, [], mockUser, mockCourse);
183
+
184
+ const result = await pipeline.getWeightedCards(2);
185
+
186
+ expect(result).toHaveLength(2);
187
+ expect(result[0].cardId).toBe('card-1');
188
+ expect(result[1].cardId).toBe('card-2');
189
+ });
190
+
191
+ it('should return empty array when generator returns no cards', async () => {
192
+ const generator = new MockGenerator([]);
193
+ const pipeline = new Pipeline(generator, [], mockUser, mockCourse);
194
+
195
+ const result = await pipeline.getWeightedCards(10);
196
+
197
+ expect(result).toHaveLength(0);
198
+ });
199
+ });
200
+
201
+ describe('filter application', () => {
202
+ it('should apply single filter to all cards', async () => {
203
+ const cards = [createCard('card-1', 1.0), createCard('card-2', 0.8)];
204
+ const generator = new MockGenerator(cards);
205
+ const filter = createMultiplierFilter('Half', 0.5);
206
+ const pipeline = new Pipeline(generator, [filter], mockUser, mockCourse);
207
+
208
+ const result = await pipeline.getWeightedCards(10);
209
+
210
+ expect(result[0].score).toBe(0.5); // 1.0 * 0.5
211
+ expect(result[1].score).toBe(0.4); // 0.8 * 0.5
212
+ });
213
+
214
+ it('should apply multiple filters sequentially', async () => {
215
+ const cards = [createCard('card-1', 1.0)];
216
+ const generator = new MockGenerator(cards);
217
+ const filter1 = createMultiplierFilter('Half', 0.5);
218
+ const filter2 = createMultiplierFilter('Double', 2.0);
219
+ const pipeline = new Pipeline(generator, [filter1, filter2], mockUser, mockCourse);
220
+
221
+ const result = await pipeline.getWeightedCards(10);
222
+
223
+ // 1.0 * 0.5 * 2.0 = 1.0
224
+ expect(result[0].score).toBe(1.0);
225
+ });
226
+
227
+ it('should remove zero-score cards after filtering', async () => {
228
+ const cards = [
229
+ createCard('keep', 0.8),
230
+ createCard('block', 0.9),
231
+ ];
232
+ const generator = new MockGenerator(cards);
233
+ const filter = createBlockingFilter('Blocker', ['block']);
234
+ const pipeline = new Pipeline(generator, [filter], mockUser, mockCourse);
235
+
236
+ const result = await pipeline.getWeightedCards(10);
237
+
238
+ expect(result).toHaveLength(1);
239
+ expect(result[0].cardId).toBe('keep');
240
+ });
241
+
242
+ it('should accumulate provenance from all filters', async () => {
243
+ const cards = [createCard('card-1', 1.0)];
244
+ const generator = new MockGenerator(cards);
245
+ const filter1 = createMultiplierFilter('Filter A', 0.9);
246
+ const filter2 = createMultiplierFilter('Filter B', 0.8);
247
+ const pipeline = new Pipeline(generator, [filter1, filter2], mockUser, mockCourse);
248
+
249
+ const result = await pipeline.getWeightedCards(10);
250
+
251
+ expect(result[0].provenance).toHaveLength(3); // generator + 2 filters
252
+ expect(result[0].provenance[0].strategyName).toBe('Test Generator');
253
+ expect(result[0].provenance[1].strategyName).toBe('Filter A');
254
+ expect(result[0].provenance[2].strategyName).toBe('Filter B');
255
+ });
256
+ });
257
+
258
+ describe('filter order independence (multipliers)', () => {
259
+ it('should produce same final score regardless of filter order', async () => {
260
+ const cards = [createCard('card-1', 1.0)];
261
+
262
+ const filterA = createMultiplierFilter('A', 0.5);
263
+ const filterB = createMultiplierFilter('B', 0.8);
264
+
265
+ // Order: A then B
266
+ const generator1 = new MockGenerator([...cards]);
267
+ const pipeline1 = new Pipeline(generator1, [filterA, filterB], mockUser, mockCourse);
268
+ const result1 = await pipeline1.getWeightedCards(10);
269
+
270
+ // Order: B then A
271
+ const generator2 = new MockGenerator([...cards]);
272
+ const pipeline2 = new Pipeline(generator2, [filterB, filterA], mockUser, mockCourse);
273
+ const result2 = await pipeline2.getWeightedCards(10);
274
+
275
+ // Both should yield 1.0 * 0.5 * 0.8 = 0.4
276
+ expect(result1[0].score).toBeCloseTo(0.4);
277
+ expect(result2[0].score).toBeCloseTo(0.4);
278
+ });
279
+ });
280
+
281
+ describe('context building', () => {
282
+ it('should pass context to filters', async () => {
283
+ const cards = [createCard('card-1', 0.8)];
284
+ const generator = new MockGenerator(cards);
285
+
286
+ let capturedContext: FilterContext | null = null;
287
+ const contextCapturingFilter: CardFilter = {
288
+ name: 'Context Capturer',
289
+ async transform(cards, context) {
290
+ capturedContext = context;
291
+ return cards;
292
+ },
293
+ };
294
+
295
+ const pipeline = new Pipeline(generator, [contextCapturingFilter], mockUser, mockCourse);
296
+ await pipeline.getWeightedCards(10);
297
+
298
+ expect(capturedContext).not.toBeNull();
299
+ expect(capturedContext!.user).toBe(mockUser);
300
+ expect(capturedContext!.course).toBe(mockCourse);
301
+ expect(capturedContext!.userElo).toBe(1000);
302
+ });
303
+
304
+ it('should use default ELO when user registration fails', async () => {
305
+ const cards = [createCard('card-1', 0.8)];
306
+ const generator = new MockGenerator(cards);
307
+
308
+ const failingUser = {
309
+ getCourseRegDoc: vi.fn().mockRejectedValue(new Error('Not registered')),
310
+ } as unknown as UserDBInterface;
311
+
312
+ let capturedElo = 0;
313
+ const eloCapturingFilter: CardFilter = {
314
+ name: 'ELO Capturer',
315
+ async transform(cards, context) {
316
+ capturedElo = context.userElo;
317
+ return cards;
318
+ },
319
+ };
320
+
321
+ const pipeline = new Pipeline(generator, [eloCapturingFilter], failingUser, mockCourse);
322
+ await pipeline.getWeightedCards(10);
323
+
324
+ expect(capturedElo).toBe(1000); // Default ELO
325
+ });
326
+ });
327
+
328
+ describe('legacy API compatibility', () => {
329
+ it('should delegate getNewCards to generator', async () => {
330
+ const mockNewCards: StudySessionNewItem[] = [
331
+ {
332
+ cardID: 'new-1',
333
+ courseID: 'test-course',
334
+ contentSourceType: 'course',
335
+ contentSourceID: 'test-course',
336
+ status: 'new',
337
+ },
338
+ ];
339
+
340
+ const generator = new MockGenerator([]);
341
+ generator.getNewCards = vi.fn().mockResolvedValue(mockNewCards);
342
+
343
+ const pipeline = new Pipeline(generator, [], mockUser, mockCourse);
344
+ const result = await pipeline.getNewCards(10);
345
+
346
+ expect(generator.getNewCards).toHaveBeenCalledWith(10);
347
+ expect(result).toEqual(mockNewCards);
348
+ });
349
+
350
+ it('should delegate getPendingReviews to generator', async () => {
351
+ const mockReviews: (StudySessionReviewItem & ScheduledCard)[] = [
352
+ {
353
+ cardID: 'review-1',
354
+ courseID: 'test-course',
355
+ contentSourceType: 'course',
356
+ contentSourceID: 'test-course',
357
+ status: 'review',
358
+ qualifiedID: 'test-course-review-1',
359
+ reviewID: 'SCHEDULED_CARD-review-1',
360
+ _id: 'SCHEDULED_CARD-review-1',
361
+ cardId: 'review-1',
362
+ courseId: 'test-course',
363
+ reviewTime: '2024-01-15T12:00:00Z',
364
+ scheduledAt: '2024-01-14T12:00:00Z',
365
+ scheduledFor: 'course',
366
+ schedulingAgentId: 'test-course',
367
+ },
368
+ ];
369
+
370
+ const generator = new MockGenerator([]);
371
+ generator.getPendingReviews = vi.fn().mockResolvedValue(mockReviews);
372
+
373
+ const pipeline = new Pipeline(generator, [], mockUser, mockCourse);
374
+ const result = await pipeline.getPendingReviews();
375
+
376
+ expect(generator.getPendingReviews).toHaveBeenCalled();
377
+ expect(result).toEqual(mockReviews);
378
+ });
379
+ });
380
+
381
+ describe('over-fetching', () => {
382
+ it('should over-fetch from generator based on filter count', async () => {
383
+ const manyCards = Array.from({ length: 20 }, (_, i) =>
384
+ createCard(`card-${i}`, 0.5 + Math.random() * 0.5)
385
+ );
386
+
387
+ const generator = new MockGenerator(manyCards);
388
+ const getWeightedCardsSpy = vi.spyOn(generator, 'getWeightedCards');
389
+
390
+ const filters = [
391
+ createMultiplierFilter('F1', 0.9),
392
+ createMultiplierFilter('F2', 0.9),
393
+ createMultiplierFilter('F3', 0.9),
394
+ ];
395
+
396
+ const pipeline = new Pipeline(generator, filters, mockUser, mockCourse);
397
+ await pipeline.getWeightedCards(5);
398
+
399
+ // With 3 filters, multiplier is 2 + 3*0.5 = 3.5, so fetch 5 * 3.5 = 17.5 → 18
400
+ const fetchLimit = getWeightedCardsSpy.mock.calls[0][0];
401
+ expect(fetchLimit).toBeGreaterThan(5);
402
+ expect(fetchLimit).toBeLessThanOrEqual(20);
403
+ });
404
+ });
405
+ });