@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.
- package/CLAUDE.md +2 -2
- package/dist/{classroomDB-BgfrVb8d.d.ts → contentSource-BP9hznNV.d.ts} +220 -197
- package/dist/{classroomDB-CTOenngH.d.cts → contentSource-DsJadoBU.d.cts} +220 -197
- package/dist/core/index.d.cts +80 -6
- package/dist/core/index.d.ts +80 -6
- package/dist/core/index.js +735 -1560
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +708 -1539
- package/dist/core/index.mjs.map +1 -1
- package/dist/{dataLayerProvider-D6PoCwS6.d.cts → dataLayerProvider-CHYrQ5pB.d.cts} +1 -1
- package/dist/{dataLayerProvider-CZxC9GtB.d.ts → dataLayerProvider-MDTxXq2l.d.ts} +1 -1
- package/dist/impl/couch/index.d.cts +8 -23
- package/dist/impl/couch/index.d.ts +8 -23
- package/dist/impl/couch/index.js +723 -1578
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +692 -1552
- package/dist/impl/couch/index.mjs.map +1 -1
- package/dist/impl/static/index.d.cts +25 -8
- package/dist/impl/static/index.d.ts +25 -8
- package/dist/impl/static/index.js +700 -1400
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +688 -1393
- package/dist/impl/static/index.mjs.map +1 -1
- package/dist/{index-D-Fa4Smt.d.cts → index-B_j6u5E4.d.cts} +1 -1
- package/dist/{index-CD8BZz2k.d.ts → index-Dj0SEgk3.d.ts} +1 -1
- package/dist/index.d.cts +71 -63
- package/dist/index.d.ts +71 -63
- package/dist/index.js +1162 -1996
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1124 -1955
- package/dist/index.mjs.map +1 -1
- package/dist/pouch/index.js +3 -0
- package/dist/pouch/index.js.map +1 -1
- package/dist/pouch/index.mjs +3 -0
- package/dist/pouch/index.mjs.map +1 -1
- package/dist/{types-CzPDLAK6.d.cts → types-Bn0itutr.d.cts} +1 -1
- package/dist/{types-CewsN87z.d.ts → types-DQaXnuoc.d.ts} +1 -1
- package/dist/{types-legacy-6ettoclI.d.cts → types-legacy-DDY4N-Uq.d.cts} +3 -1
- package/dist/{types-legacy-6ettoclI.d.ts → types-legacy-DDY4N-Uq.d.ts} +3 -1
- package/dist/util/packer/index.d.cts +3 -3
- package/dist/util/packer/index.d.ts +3 -3
- package/docs/navigators-architecture.md +115 -17
- package/package.json +4 -4
- package/src/core/index.ts +1 -0
- package/src/core/interfaces/classroomDB.ts +5 -13
- package/src/core/interfaces/contentSource.ts +6 -66
- package/src/core/interfaces/courseDB.ts +15 -7
- package/src/core/interfaces/userDB.ts +32 -0
- package/src/core/navigators/Pipeline.ts +136 -52
- package/src/core/navigators/PipelineAssembler.ts +1 -1
- package/src/core/navigators/defaults.ts +84 -0
- package/src/core/navigators/{hierarchyDefinition.ts → filters/hierarchyDefinition.ts} +15 -29
- package/src/core/navigators/filters/index.ts +3 -0
- package/src/core/navigators/filters/inferredPreferenceStub.ts +107 -0
- package/src/core/navigators/{interferenceMitigator.ts → filters/interferenceMitigator.ts} +11 -37
- package/src/core/navigators/{relativePriority.ts → filters/relativePriority.ts} +12 -38
- package/src/core/navigators/filters/userGoalStub.ts +136 -0
- package/src/core/navigators/filters/userTagPreference.ts +217 -0
- package/src/core/navigators/{CompositeGenerator.ts → generators/CompositeGenerator.ts} +15 -64
- package/src/core/navigators/{elo.ts → generators/elo.ts} +13 -63
- package/src/core/navigators/{srs.ts → generators/srs.ts} +11 -40
- package/src/core/navigators/generators/types.ts +1 -1
- package/src/core/navigators/index.ts +95 -91
- package/src/core/types/strategyState.ts +84 -0
- package/src/core/types/types-legacy.ts +2 -0
- package/src/impl/common/BaseUserDB.ts +74 -7
- package/src/impl/couch/adminDB.ts +1 -2
- package/src/impl/couch/classroomDB.ts +100 -103
- package/src/impl/couch/courseDB.ts +35 -91
- package/src/impl/couch/pouchdb-setup.ts +7 -0
- package/src/impl/static/StaticDataUnpacker.ts +50 -1
- package/src/impl/static/courseDB.ts +87 -37
- package/src/study/SessionController.ts +122 -202
- package/src/study/SourceMixer.ts +65 -0
- package/src/study/TagFilteredContentSource.ts +49 -92
- package/src/study/index.ts +1 -0
- package/src/study/services/CardHydrationService.ts +165 -81
- package/src/util/dataDirectory.ts +1 -1
- package/src/util/index.ts +0 -1
- package/tests/core/navigators/CompositeGenerator.test.ts +44 -168
- package/tests/core/navigators/Pipeline.test.ts +6 -72
- package/tests/core/navigators/PipelineAssembler.test.ts +8 -58
- package/tests/core/navigators/navigators.test.ts +118 -151
- package/docs/todo-pipeline-optimization.md +0 -117
- package/docs/todo-strategy-state-storage.md +0 -278
- package/src/core/navigators/hardcodedOrder.ts +0 -163
- package/src/util/tuiLogger.ts +0 -139
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import moment from 'moment';
|
|
2
|
-
import type { ScheduledCard } from '
|
|
3
|
-
import type { CourseDBInterface } from '
|
|
4
|
-
import type { UserDBInterface } from '
|
|
5
|
-
import { ContentNavigator } from '
|
|
6
|
-
import type { WeightedCard } from '
|
|
7
|
-
import type { ContentNavigationStrategyData } from '
|
|
8
|
-
import type {
|
|
9
|
-
import
|
|
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)
|
|
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
|
|
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:
|
|
@@ -137,6 +130,17 @@ export interface WeightedCard {
|
|
|
137
130
|
* First entry is from the generator, subsequent entries from filters.
|
|
138
131
|
*/
|
|
139
132
|
provenance: StrategyContribution[];
|
|
133
|
+
/**
|
|
134
|
+
* Pre-fetched tags. Populated by Pipeline before filters run.
|
|
135
|
+
* Filters should use this instead of querying getAppliedTags() individually.
|
|
136
|
+
*/
|
|
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;
|
|
140
144
|
}
|
|
141
145
|
|
|
142
146
|
/**
|
|
@@ -169,10 +173,10 @@ export function getCardOrigin(card: WeightedCard): 'new' | 'review' | 'failed' {
|
|
|
169
173
|
export enum Navigators {
|
|
170
174
|
ELO = 'elo',
|
|
171
175
|
SRS = 'srs',
|
|
172
|
-
HARDCODED = 'hardcodedOrder',
|
|
173
176
|
HIERARCHY = 'hierarchyDefinition',
|
|
174
177
|
INTERFERENCE = 'interferenceMitigator',
|
|
175
178
|
RELATIVE_PRIORITY = 'relativePriority',
|
|
179
|
+
USER_TAG_PREFERENCE = 'userTagPreference',
|
|
176
180
|
}
|
|
177
181
|
|
|
178
182
|
// ============================================================================
|
|
@@ -180,7 +184,7 @@ export enum Navigators {
|
|
|
180
184
|
// ============================================================================
|
|
181
185
|
//
|
|
182
186
|
// Navigators are classified as either generators or filters:
|
|
183
|
-
// - Generators: Produce candidate cards (ELO, SRS
|
|
187
|
+
// - Generators: Produce candidate cards (ELO, SRS)
|
|
184
188
|
// - Filters: Transform/score candidates (Hierarchy, Interference, RelativePriority)
|
|
185
189
|
//
|
|
186
190
|
// This classification is used by PipelineAssembler to build pipelines:
|
|
@@ -207,10 +211,10 @@ export enum NavigatorRole {
|
|
|
207
211
|
export const NavigatorRoles: Record<Navigators, NavigatorRole> = {
|
|
208
212
|
[Navigators.ELO]: NavigatorRole.GENERATOR,
|
|
209
213
|
[Navigators.SRS]: NavigatorRole.GENERATOR,
|
|
210
|
-
[Navigators.HARDCODED]: NavigatorRole.GENERATOR,
|
|
211
214
|
[Navigators.HIERARCHY]: NavigatorRole.FILTER,
|
|
212
215
|
[Navigators.INTERFERENCE]: NavigatorRole.FILTER,
|
|
213
216
|
[Navigators.RELATIVE_PRIORITY]: NavigatorRole.FILTER,
|
|
217
|
+
[Navigators.USER_TAG_PREFERENCE]: NavigatorRole.FILTER,
|
|
214
218
|
};
|
|
215
219
|
|
|
216
220
|
/**
|
|
@@ -245,10 +249,10 @@ export function isFilter(impl: string): boolean {
|
|
|
245
249
|
*/
|
|
246
250
|
export abstract class ContentNavigator implements StudyContentSource {
|
|
247
251
|
/** User interface for this navigation session */
|
|
248
|
-
protected user
|
|
252
|
+
protected user: UserDBInterface;
|
|
249
253
|
|
|
250
254
|
/** Course interface for this navigation session */
|
|
251
|
-
protected course
|
|
255
|
+
protected course: CourseDBInterface;
|
|
252
256
|
|
|
253
257
|
/** Human-readable name for this strategy instance (from ContentNavigationStrategyData.name) */
|
|
254
258
|
protected strategyName?: string;
|
|
@@ -260,21 +264,74 @@ export abstract class ContentNavigator implements StudyContentSource {
|
|
|
260
264
|
* Constructor for standard navigators.
|
|
261
265
|
* Call this from subclass constructors to initialize common fields.
|
|
262
266
|
*
|
|
263
|
-
* Note: CompositeGenerator
|
|
267
|
+
* Note: CompositeGenerator and Pipeline call super() without args, then set
|
|
268
|
+
* user/course fields directly if needed.
|
|
264
269
|
*/
|
|
265
270
|
constructor(
|
|
266
271
|
user?: UserDBInterface,
|
|
267
272
|
course?: CourseDBInterface,
|
|
268
273
|
strategyData?: ContentNavigationStrategyData
|
|
269
274
|
) {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
275
|
+
this.user = user!;
|
|
276
|
+
this.course = course!;
|
|
277
|
+
if (strategyData) {
|
|
273
278
|
this.strategyName = strategyData.name;
|
|
274
279
|
this.strategyId = strategyData._id;
|
|
275
280
|
}
|
|
276
281
|
}
|
|
277
282
|
|
|
283
|
+
// ============================================================================
|
|
284
|
+
// STRATEGY STATE HELPERS
|
|
285
|
+
// ============================================================================
|
|
286
|
+
//
|
|
287
|
+
// These methods allow strategies to persist their own state (user preferences,
|
|
288
|
+
// learned patterns, temporal tracking) in the user database.
|
|
289
|
+
//
|
|
290
|
+
// ============================================================================
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Unique key identifying this strategy for state storage.
|
|
294
|
+
*
|
|
295
|
+
* Defaults to the constructor name (e.g., "UserTagPreferenceFilter").
|
|
296
|
+
* Override in subclasses if multiple instances of the same strategy type
|
|
297
|
+
* need separate state storage.
|
|
298
|
+
*/
|
|
299
|
+
protected get strategyKey(): string {
|
|
300
|
+
return this.constructor.name;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Get this strategy's persisted state for the current course.
|
|
305
|
+
*
|
|
306
|
+
* @returns The strategy's data payload, or null if no state exists
|
|
307
|
+
* @throws Error if user or course is not initialized
|
|
308
|
+
*/
|
|
309
|
+
protected async getStrategyState<T>(): Promise<T | null> {
|
|
310
|
+
if (!this.user || !this.course) {
|
|
311
|
+
throw new Error(
|
|
312
|
+
`Cannot get strategy state: navigator not properly initialized. ` +
|
|
313
|
+
`Ensure user and course are provided to constructor.`
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
return this.user.getStrategyState<T>(this.course.getCourseID(), this.strategyKey);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Persist this strategy's state for the current course.
|
|
321
|
+
*
|
|
322
|
+
* @param data - The strategy's data payload to store
|
|
323
|
+
* @throws Error if user or course is not initialized
|
|
324
|
+
*/
|
|
325
|
+
protected async putStrategyState<T>(data: T): Promise<void> {
|
|
326
|
+
if (!this.user || !this.course) {
|
|
327
|
+
throw new Error(
|
|
328
|
+
`Cannot put strategy state: navigator not properly initialized. ` +
|
|
329
|
+
`Ensure user and course are provided to constructor.`
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
return this.user.putStrategyState<T>(this.course.getCourseID(), this.strategyKey, data);
|
|
333
|
+
}
|
|
334
|
+
|
|
278
335
|
/**
|
|
279
336
|
* Factory method to create navigator instances dynamically.
|
|
280
337
|
*
|
|
@@ -293,15 +350,19 @@ export abstract class ContentNavigator implements StudyContentSource {
|
|
|
293
350
|
|
|
294
351
|
// Try different extension variations
|
|
295
352
|
const variations = ['.ts', '.js', ''];
|
|
353
|
+
const dirs = ['filters', 'generators'];
|
|
296
354
|
|
|
297
355
|
for (const ext of variations) {
|
|
298
|
-
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
+
}
|
|
305
366
|
}
|
|
306
367
|
}
|
|
307
368
|
|
|
@@ -312,24 +373,6 @@ export abstract class ContentNavigator implements StudyContentSource {
|
|
|
312
373
|
return new NavigatorImpl(user, course, strategyData);
|
|
313
374
|
}
|
|
314
375
|
|
|
315
|
-
/**
|
|
316
|
-
* Get cards scheduled for review.
|
|
317
|
-
*
|
|
318
|
-
* @deprecated This method is part of the legacy StudyContentSource interface.
|
|
319
|
-
* New strategies should focus on implementing CardGenerator.getWeightedCards() instead.
|
|
320
|
-
*/
|
|
321
|
-
abstract getPendingReviews(): Promise<(StudySessionReviewItem & ScheduledCard)[]>;
|
|
322
|
-
|
|
323
|
-
/**
|
|
324
|
-
* Get new cards for introduction.
|
|
325
|
-
*
|
|
326
|
-
* @deprecated This method is part of the legacy StudyContentSource interface.
|
|
327
|
-
* New strategies should focus on implementing CardGenerator.getWeightedCards() instead.
|
|
328
|
-
*
|
|
329
|
-
* @param n - Maximum number of new cards to return
|
|
330
|
-
*/
|
|
331
|
-
abstract getNewCards(n?: number): Promise<StudySessionNewItem[]>;
|
|
332
|
-
|
|
333
376
|
/**
|
|
334
377
|
* Get cards with suitability scores and provenance trails.
|
|
335
378
|
*
|
|
@@ -339,62 +382,23 @@ export abstract class ContentNavigator implements StudyContentSource {
|
|
|
339
382
|
* better candidates for presentation. Each card includes a provenance trail
|
|
340
383
|
* documenting how strategies contributed to the final score.
|
|
341
384
|
*
|
|
385
|
+
* ## Implementation Required
|
|
386
|
+
* All navigation strategies MUST override this method. The base class does
|
|
387
|
+
* not provide a default implementation.
|
|
388
|
+
*
|
|
342
389
|
* ## For Generators
|
|
343
390
|
* Override this method to generate candidates and compute scores based on
|
|
344
391
|
* your strategy's logic (e.g., ELO proximity, review urgency). Create the
|
|
345
392
|
* initial provenance entry with action='generated'.
|
|
346
393
|
*
|
|
347
|
-
* ##
|
|
348
|
-
*
|
|
349
|
-
*
|
|
350
|
-
* 2. Assigns score=1.0 to all cards
|
|
351
|
-
* 3. Creates minimal provenance from legacy methods
|
|
352
|
-
* 4. Returns combined results up to limit
|
|
353
|
-
*
|
|
354
|
-
* This allows existing strategies to work without modification while
|
|
355
|
-
* 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().
|
|
356
397
|
*
|
|
357
398
|
* @param limit - Maximum cards to return
|
|
358
399
|
* @returns Cards sorted by score descending, with provenance trails
|
|
359
400
|
*/
|
|
360
|
-
async getWeightedCards(
|
|
361
|
-
|
|
362
|
-
const newCards = await this.getNewCards(limit);
|
|
363
|
-
const reviews = await this.getPendingReviews();
|
|
364
|
-
|
|
365
|
-
const weighted: WeightedCard[] = [
|
|
366
|
-
...newCards.map((c) => ({
|
|
367
|
-
cardId: c.cardID,
|
|
368
|
-
courseId: c.courseID,
|
|
369
|
-
score: 1.0,
|
|
370
|
-
provenance: [
|
|
371
|
-
{
|
|
372
|
-
strategy: 'legacy',
|
|
373
|
-
strategyName: this.strategyName || 'Legacy API',
|
|
374
|
-
strategyId: this.strategyId || 'legacy-fallback',
|
|
375
|
-
action: 'generated' as const,
|
|
376
|
-
score: 1.0,
|
|
377
|
-
reason: 'Generated via legacy getNewCards(), new card',
|
|
378
|
-
},
|
|
379
|
-
],
|
|
380
|
-
})),
|
|
381
|
-
...reviews.map((r) => ({
|
|
382
|
-
cardId: r.cardID,
|
|
383
|
-
courseId: r.courseID,
|
|
384
|
-
score: 1.0,
|
|
385
|
-
provenance: [
|
|
386
|
-
{
|
|
387
|
-
strategy: 'legacy',
|
|
388
|
-
strategyName: this.strategyName || 'Legacy API',
|
|
389
|
-
strategyId: this.strategyId || 'legacy-fallback',
|
|
390
|
-
action: 'generated' as const,
|
|
391
|
-
score: 1.0,
|
|
392
|
-
reason: 'Generated via legacy getPendingReviews(), review',
|
|
393
|
-
},
|
|
394
|
-
],
|
|
395
|
-
})),
|
|
396
|
-
];
|
|
397
|
-
|
|
398
|
-
return weighted.slice(0, limit);
|
|
401
|
+
async getWeightedCards(_limit: number): Promise<WeightedCard[]> {
|
|
402
|
+
throw new Error(`${this.constructor.name} must implement getWeightedCards(). `);
|
|
399
403
|
}
|
|
400
404
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { DocType, DocTypePrefixes } from './types-legacy';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Template literal type for strategy state document IDs.
|
|
5
|
+
*
|
|
6
|
+
* Format: `STRATEGY_STATE-{courseId}-{strategyKey}`
|
|
7
|
+
*/
|
|
8
|
+
export type StrategyStateId =
|
|
9
|
+
`${(typeof DocTypePrefixes)[DocType.STRATEGY_STATE]}::${string}::${string}`;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Document storing strategy-specific state in the user database.
|
|
13
|
+
*
|
|
14
|
+
* Each strategy can persist its own state (user preferences, learned patterns,
|
|
15
|
+
* temporal tracking, etc.) using this document type. The state is scoped to
|
|
16
|
+
* a (user, course, strategy) tuple.
|
|
17
|
+
*
|
|
18
|
+
* ## Use Cases
|
|
19
|
+
*
|
|
20
|
+
* 1. **Explicit user preferences**: User configures tag filters, difficulty
|
|
21
|
+
* preferences, or learning goals. UI writes to strategy state.
|
|
22
|
+
*
|
|
23
|
+
* 2. **Learned/temporal state**: Strategy tracks patterns over time, e.g.,
|
|
24
|
+
* "when did I last introduce confusable concepts together?"
|
|
25
|
+
*
|
|
26
|
+
* 3. **Adaptive personalization**: Strategy infers user preferences from
|
|
27
|
+
* behavior and stores them for future sessions.
|
|
28
|
+
*
|
|
29
|
+
* ## Storage Location
|
|
30
|
+
*
|
|
31
|
+
* These documents live in the **user database**, not the course database.
|
|
32
|
+
* They sync with the user's data across devices.
|
|
33
|
+
*
|
|
34
|
+
* ## Document ID Format
|
|
35
|
+
*
|
|
36
|
+
* `STRATEGY_STATE::{courseId}::{strategyKey}`
|
|
37
|
+
*
|
|
38
|
+
* Example: `STRATEGY_STATE::piano-basics::UserTagPreferenceFilter`
|
|
39
|
+
*
|
|
40
|
+
* @template T - The shape of the strategy-specific data payload
|
|
41
|
+
*/
|
|
42
|
+
export interface StrategyStateDoc<T = unknown> {
|
|
43
|
+
_id: StrategyStateId;
|
|
44
|
+
_rev?: string;
|
|
45
|
+
docType: DocType.STRATEGY_STATE;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The course this state applies to.
|
|
49
|
+
*/
|
|
50
|
+
courseId: string;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Unique key identifying the strategy instance.
|
|
54
|
+
* Typically the strategy class name (e.g., "UserTagPreferenceFilter",
|
|
55
|
+
* "InterferenceMitigatorNavigator").
|
|
56
|
+
*
|
|
57
|
+
* If a course has multiple instances of the same strategy type with
|
|
58
|
+
* different configurations, use a more specific key.
|
|
59
|
+
*/
|
|
60
|
+
strategyKey: string;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Strategy-specific data payload.
|
|
64
|
+
* Each strategy defines its own schema for this field.
|
|
65
|
+
*/
|
|
66
|
+
data: T;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* ISO timestamp of last update.
|
|
70
|
+
* Use `moment.utc(updatedAt)` to parse into a Moment object.
|
|
71
|
+
*/
|
|
72
|
+
updatedAt: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Build the document ID for a strategy state document.
|
|
77
|
+
*
|
|
78
|
+
* @param courseId - The course ID
|
|
79
|
+
* @param strategyKey - The strategy key (typically class name)
|
|
80
|
+
* @returns The document ID in format `STRATEGY_STATE::{courseId}::{strategyKey}`
|
|
81
|
+
*/
|
|
82
|
+
export function buildStrategyStateId(courseId: string, strategyKey: string): StrategyStateId {
|
|
83
|
+
return `STRATEGY_STATE::${courseId}::${strategyKey}`;
|
|
84
|
+
}
|
|
@@ -19,6 +19,7 @@ export enum DocType {
|
|
|
19
19
|
SCHEDULED_CARD = 'SCHEDULED_CARD',
|
|
20
20
|
TAG = 'TAG',
|
|
21
21
|
NAVIGATION_STRATEGY = 'NAVIGATION_STRATEGY',
|
|
22
|
+
STRATEGY_STATE = 'STRATEGY_STATE',
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
export interface QualifiedCardID {
|
|
@@ -103,6 +104,7 @@ export const DocTypePrefixes = {
|
|
|
103
104
|
[DocType.VIEW]: 'VIEW',
|
|
104
105
|
[DocType.PEDAGOGY]: 'PEDAGOGY',
|
|
105
106
|
[DocType.NAVIGATION_STRATEGY]: 'NAVIGATION_STRATEGY',
|
|
107
|
+
[DocType.STRATEGY_STATE]: 'STRATEGY_STATE',
|
|
106
108
|
} as const;
|
|
107
109
|
|
|
108
110
|
export interface CardHistory<T extends CardRecord> {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DocType, DocTypePrefixes } from '@db/core';
|
|
1
|
+
import { DocType, DocTypePrefixes, StrategyStateDoc, buildStrategyStateId } from '@db/core';
|
|
2
2
|
import { getCardHistoryID } from '@db/core/util';
|
|
3
3
|
import { CourseElo, Status } from '@vue-skuilder/common';
|
|
4
4
|
import moment, { Moment } from 'moment';
|
|
@@ -1046,6 +1046,61 @@ Currently logged-in as ${this._username}.`
|
|
|
1046
1046
|
public async updateUserElo(courseId: string, elo: CourseElo): Promise<PouchDB.Core.Response> {
|
|
1047
1047
|
return updateUserElo(this._username, courseId, elo);
|
|
1048
1048
|
}
|
|
1049
|
+
|
|
1050
|
+
public async getStrategyState<T>(courseId: string, strategyKey: string): Promise<T | null> {
|
|
1051
|
+
const docId = buildStrategyStateId(courseId, strategyKey);
|
|
1052
|
+
try {
|
|
1053
|
+
const doc = await this.localDB.get<StrategyStateDoc<T>>(docId);
|
|
1054
|
+
return doc.data;
|
|
1055
|
+
} catch (e) {
|
|
1056
|
+
const err = e as PouchError;
|
|
1057
|
+
if (err.status === 404) {
|
|
1058
|
+
return null;
|
|
1059
|
+
}
|
|
1060
|
+
throw e;
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
public async putStrategyState<T>(courseId: string, strategyKey: string, data: T): Promise<void> {
|
|
1065
|
+
const docId = buildStrategyStateId(courseId, strategyKey);
|
|
1066
|
+
let existingRev: string | undefined;
|
|
1067
|
+
|
|
1068
|
+
try {
|
|
1069
|
+
const existing = await this.localDB.get<StrategyStateDoc<T>>(docId);
|
|
1070
|
+
existingRev = existing._rev;
|
|
1071
|
+
} catch (e) {
|
|
1072
|
+
const err = e as PouchError;
|
|
1073
|
+
if (err.status !== 404) {
|
|
1074
|
+
throw e;
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
const doc: StrategyStateDoc<T> = {
|
|
1079
|
+
_id: docId,
|
|
1080
|
+
_rev: existingRev,
|
|
1081
|
+
docType: DocType.STRATEGY_STATE,
|
|
1082
|
+
courseId,
|
|
1083
|
+
strategyKey,
|
|
1084
|
+
data,
|
|
1085
|
+
updatedAt: new Date().toISOString(),
|
|
1086
|
+
};
|
|
1087
|
+
|
|
1088
|
+
await this.localDB.put(doc);
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
public async deleteStrategyState(courseId: string, strategyKey: string): Promise<void> {
|
|
1092
|
+
const docId = buildStrategyStateId(courseId, strategyKey);
|
|
1093
|
+
try {
|
|
1094
|
+
const doc = await this.localDB.get(docId);
|
|
1095
|
+
await this.localDB.remove(doc);
|
|
1096
|
+
} catch (e) {
|
|
1097
|
+
const err = e as PouchError;
|
|
1098
|
+
if (err.status === 404) {
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
throw e;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1049
1104
|
}
|
|
1050
1105
|
|
|
1051
1106
|
export function accomodateGuest(): {
|
|
@@ -1056,7 +1111,9 @@ export function accomodateGuest(): {
|
|
|
1056
1111
|
|
|
1057
1112
|
// Check if localStorage is available (browser environment)
|
|
1058
1113
|
if (typeof localStorage === 'undefined') {
|
|
1059
|
-
logger.log(
|
|
1114
|
+
logger.log(
|
|
1115
|
+
'[funnel] localStorage not available (Node.js environment), returning default guest'
|
|
1116
|
+
);
|
|
1060
1117
|
return {
|
|
1061
1118
|
username: GuestUsername + 'nodejs-test',
|
|
1062
1119
|
firstVisit: true,
|
|
@@ -1125,11 +1182,21 @@ export function accomodateGuest(): {
|
|
|
1125
1182
|
bytes[8] = (bytes[8] & 0x3f) | 0x80; // Variant 10
|
|
1126
1183
|
|
|
1127
1184
|
const uuid = [
|
|
1128
|
-
Array.from(bytes.slice(0, 4))
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
Array.from(bytes.slice(
|
|
1132
|
-
|
|
1185
|
+
Array.from(bytes.slice(0, 4))
|
|
1186
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
1187
|
+
.join(''),
|
|
1188
|
+
Array.from(bytes.slice(4, 6))
|
|
1189
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
1190
|
+
.join(''),
|
|
1191
|
+
Array.from(bytes.slice(6, 8))
|
|
1192
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
1193
|
+
.join(''),
|
|
1194
|
+
Array.from(bytes.slice(8, 10))
|
|
1195
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
1196
|
+
.join(''),
|
|
1197
|
+
Array.from(bytes.slice(10, 16))
|
|
1198
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
1199
|
+
.join(''),
|
|
1133
1200
|
].join('-');
|
|
1134
1201
|
|
|
1135
1202
|
logger.log('[funnel] Generated UUID using crypto.getRandomValues():', uuid);
|