@vue-skuilder/db 0.1.20 → 0.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +2 -2
- package/dist/{classroomDB-CZdMBiTU.d.ts → contentSource-BP9hznNV.d.ts} +150 -196
- package/dist/{classroomDB-PxDZTky3.d.cts → contentSource-DsJadoBU.d.cts} +150 -196
- package/dist/core/index.d.cts +3 -3
- package/dist/core/index.d.ts +3 -3
- package/dist/core/index.js +615 -1758
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +579 -1727
- package/dist/core/index.mjs.map +1 -1
- package/dist/{dataLayerProvider-D8o6ZnKW.d.ts → dataLayerProvider-CHYrQ5pB.d.cts} +1 -1
- package/dist/{dataLayerProvider-D0MoZMjH.d.cts → dataLayerProvider-MDTxXq2l.d.ts} +1 -1
- package/dist/impl/couch/index.d.cts +6 -22
- package/dist/impl/couch/index.d.ts +6 -22
- package/dist/impl/couch/index.js +598 -1769
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +579 -1755
- package/dist/impl/couch/index.mjs.map +1 -1
- package/dist/impl/static/index.d.cts +22 -6
- package/dist/impl/static/index.d.ts +22 -6
- package/dist/impl/static/index.js +617 -1629
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +607 -1624
- package/dist/impl/static/index.mjs.map +1 -1
- package/dist/index.d.cts +64 -56
- package/dist/index.d.ts +64 -56
- package/dist/index.js +1000 -2161
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +970 -2127
- 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/docs/navigators-architecture.md +2 -9
- package/package.json +3 -3
- package/src/core/interfaces/classroomDB.ts +5 -13
- package/src/core/interfaces/contentSource.ts +6 -66
- package/src/core/interfaces/courseDB.ts +2 -7
- package/src/core/navigators/Pipeline.ts +24 -53
- 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} +11 -25
- package/src/core/navigators/{interferenceMitigator.ts → filters/interferenceMitigator.ts} +10 -24
- package/src/core/navigators/{relativePriority.ts → filters/relativePriority.ts} +10 -24
- package/src/core/navigators/filters/userTagPreference.ts +1 -16
- 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 +36 -91
- package/src/impl/couch/classroomDB.ts +100 -103
- package/src/impl/couch/courseDB.ts +5 -81
- package/src/impl/couch/pouchdb-setup.ts +7 -0
- package/src/impl/static/StaticDataUnpacker.ts +50 -1
- package/src/impl/static/courseDB.ts +76 -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 +5 -72
- package/tests/core/navigators/PipelineAssembler.test.ts +8 -58
- package/tests/core/navigators/navigators.test.ts +118 -151
- package/src/core/navigators/hardcodedOrder.ts +0 -163
- package/src/util/tuiLogger.ts +0 -139
- /package/src/core/navigators/{inferredPreference.ts → filters/inferredPreferenceStub.ts} +0 -0
- /package/src/core/navigators/{userGoal.ts → filters/userGoalStub.ts} +0 -0
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type {
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import type {
|
|
6
|
-
import type {
|
|
7
|
-
import type { StudySessionReviewItem, StudySessionNewItem } from '..';
|
|
8
|
-
import type { CardFilter, FilterContext } from './filters/types';
|
|
1
|
+
import type { CourseDBInterface } from '../../interfaces/courseDB';
|
|
2
|
+
import type { UserDBInterface } from '../../interfaces/userDB';
|
|
3
|
+
import { ContentNavigator } from '../index';
|
|
4
|
+
import type { WeightedCard } from '../index';
|
|
5
|
+
import type { ContentNavigationStrategyData } from '../../types/contentNavigationStrategy';
|
|
6
|
+
import type { CardFilter, FilterContext } from './types';
|
|
9
7
|
|
|
10
8
|
/**
|
|
11
9
|
* Configuration for the RelativePriority strategy.
|
|
@@ -85,7 +83,6 @@ const DEFAULT_COMBINE_MODE: 'max' | 'average' | 'min' = 'max';
|
|
|
85
83
|
*/
|
|
86
84
|
export default class RelativePriorityNavigator extends ContentNavigator implements CardFilter {
|
|
87
85
|
private config: RelativePriorityConfig;
|
|
88
|
-
private _strategyData: ContentNavigationStrategyData;
|
|
89
86
|
|
|
90
87
|
/** Human-readable name for CardFilter interface */
|
|
91
88
|
name: string;
|
|
@@ -93,12 +90,11 @@ export default class RelativePriorityNavigator extends ContentNavigator implemen
|
|
|
93
90
|
constructor(
|
|
94
91
|
user: UserDBInterface,
|
|
95
92
|
course: CourseDBInterface,
|
|
96
|
-
|
|
93
|
+
strategyData: ContentNavigationStrategyData
|
|
97
94
|
) {
|
|
98
|
-
super(user, course,
|
|
99
|
-
this.
|
|
100
|
-
this.
|
|
101
|
-
this.name = _strategyData.name || 'Relative Priority';
|
|
95
|
+
super(user, course, strategyData);
|
|
96
|
+
this.config = this.parseConfig(strategyData.serializedData);
|
|
97
|
+
this.name = strategyData.name || 'Relative Priority';
|
|
102
98
|
}
|
|
103
99
|
|
|
104
100
|
private parseConfig(serializedData: string): RelativePriorityConfig {
|
|
@@ -242,14 +238,4 @@ export default class RelativePriorityNavigator extends ContentNavigator implemen
|
|
|
242
238
|
'Use Pipeline with a generator and this filter via transform().'
|
|
243
239
|
);
|
|
244
240
|
}
|
|
245
|
-
|
|
246
|
-
// Legacy methods - stub implementations since filters don't generate cards
|
|
247
|
-
|
|
248
|
-
async getNewCards(_n?: number): Promise<StudySessionNewItem[]> {
|
|
249
|
-
return [];
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
async getPendingReviews(): Promise<(StudySessionReviewItem & ScheduledCard)[]> {
|
|
253
|
-
return [];
|
|
254
|
-
}
|
|
255
241
|
}
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import type { ScheduledCard } from '../../types/user';
|
|
2
1
|
import type { CourseDBInterface } from '../../interfaces/courseDB';
|
|
3
2
|
import type { UserDBInterface } from '../../interfaces/userDB';
|
|
4
3
|
import { ContentNavigator } from '../index';
|
|
5
4
|
import type { WeightedCard } from '../index';
|
|
6
5
|
import type { ContentNavigationStrategyData } from '../../types/contentNavigationStrategy';
|
|
7
|
-
import type { StudySessionReviewItem, StudySessionNewItem } from '../..';
|
|
8
6
|
import type { CardFilter, FilterContext } from './types';
|
|
9
7
|
|
|
10
8
|
// ============================================================================
|
|
@@ -97,9 +95,7 @@ export default class UserTagPreferenceFilter extends ContentNavigator implements
|
|
|
97
95
|
* Returns the maximum multiplier among all matching tags, or 1.0 if no matches.
|
|
98
96
|
*/
|
|
99
97
|
private computeMultiplier(cardTags: string[], boostMap: Record<string, number>): number {
|
|
100
|
-
const multipliers = cardTags
|
|
101
|
-
.map((tag) => boostMap[tag])
|
|
102
|
-
.filter((val) => val !== undefined);
|
|
98
|
+
const multipliers = cardTags.map((tag) => boostMap[tag]).filter((val) => val !== undefined);
|
|
103
99
|
|
|
104
100
|
if (multipliers.length === 0) {
|
|
105
101
|
return 1.0;
|
|
@@ -150,7 +146,6 @@ export default class UserTagPreferenceFilter extends ContentNavigator implements
|
|
|
150
146
|
async transform(cards: WeightedCard[], _context: FilterContext): Promise<WeightedCard[]> {
|
|
151
147
|
// Read user preferences from strategy state
|
|
152
148
|
const prefs = await this.getStrategyState<UserTagPreferenceState>();
|
|
153
|
-
|
|
154
149
|
|
|
155
150
|
// No preferences configured → pass through unchanged
|
|
156
151
|
if (!prefs || Object.keys(prefs.boost).length === 0) {
|
|
@@ -219,14 +214,4 @@ export default class UserTagPreferenceFilter extends ContentNavigator implements
|
|
|
219
214
|
'Use Pipeline with a generator and this filter via transform().'
|
|
220
215
|
);
|
|
221
216
|
}
|
|
222
|
-
|
|
223
|
-
// Legacy methods - stub implementations since filters don't generate cards
|
|
224
|
-
|
|
225
|
-
async getNewCards(_n?: number): Promise<StudySessionNewItem[]> {
|
|
226
|
-
return [];
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
async getPendingReviews(): Promise<(StudySessionReviewItem & ScheduledCard)[]> {
|
|
230
|
-
return [];
|
|
231
|
-
}
|
|
232
217
|
}
|
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
import { ContentNavigator } from '
|
|
2
|
-
import type { WeightedCard } from '
|
|
3
|
-
import type { ContentNavigationStrategyData } from '
|
|
4
|
-
import type { CourseDBInterface } from '
|
|
5
|
-
import type { UserDBInterface } from '
|
|
6
|
-
import type {
|
|
7
|
-
import
|
|
8
|
-
import type { CardGenerator, GeneratorContext } from './generators/types';
|
|
9
|
-
import { logger } from '../../util/logger';
|
|
1
|
+
import { ContentNavigator } from '../index';
|
|
2
|
+
import type { WeightedCard } from '../index';
|
|
3
|
+
import type { ContentNavigationStrategyData } from '../../types/contentNavigationStrategy';
|
|
4
|
+
import type { CourseDBInterface } from '../../interfaces/courseDB';
|
|
5
|
+
import type { UserDBInterface } from '../../interfaces/userDB';
|
|
6
|
+
import type { CardGenerator, GeneratorContext } from './types';
|
|
7
|
+
import { logger } from '../../../util/logger';
|
|
10
8
|
|
|
11
9
|
// ============================================================================
|
|
12
10
|
// COMPOSITE GENERATOR
|
|
@@ -100,9 +98,16 @@ export default class CompositeGenerator extends ContentNavigator implements Card
|
|
|
100
98
|
* CardGenerator interface signature (limit, context).
|
|
101
99
|
*
|
|
102
100
|
* @param limit - Maximum number of cards to return
|
|
103
|
-
* @param context -
|
|
101
|
+
* @param context - GeneratorContext passed to child generators (required when called via Pipeline)
|
|
104
102
|
*/
|
|
105
103
|
async getWeightedCards(limit: number, context?: GeneratorContext): Promise<WeightedCard[]> {
|
|
104
|
+
if (!context) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
'CompositeGenerator.getWeightedCards requires a GeneratorContext. ' +
|
|
107
|
+
'It should be called via Pipeline, not directly.'
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
106
111
|
// Fetch from all generators in parallel
|
|
107
112
|
const results = await Promise.all(
|
|
108
113
|
this.generators.map((g) => g.getWeightedCards(limit, context))
|
|
@@ -211,58 +216,4 @@ export default class CompositeGenerator extends ContentNavigator implements Card
|
|
|
211
216
|
return scores[0];
|
|
212
217
|
}
|
|
213
218
|
}
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* Get new cards from all generators, merged and deduplicated.
|
|
217
|
-
*/
|
|
218
|
-
async getNewCards(n?: number): Promise<StudySessionNewItem[]> {
|
|
219
|
-
// For legacy method, need to filter to generators that have getNewCards
|
|
220
|
-
const legacyGenerators = this.generators.filter(
|
|
221
|
-
(g): g is CardGenerator & ContentNavigator => g instanceof ContentNavigator
|
|
222
|
-
);
|
|
223
|
-
|
|
224
|
-
const results = await Promise.all(legacyGenerators.map((g) => g.getNewCards(n)));
|
|
225
|
-
|
|
226
|
-
// Deduplicate by cardID
|
|
227
|
-
const seen = new Set<string>();
|
|
228
|
-
const merged: StudySessionNewItem[] = [];
|
|
229
|
-
|
|
230
|
-
for (const cards of results) {
|
|
231
|
-
for (const card of cards) {
|
|
232
|
-
if (!seen.has(card.cardID)) {
|
|
233
|
-
seen.add(card.cardID);
|
|
234
|
-
merged.push(card);
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
return n ? merged.slice(0, n) : merged;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
* Get pending reviews from all generators, merged and deduplicated.
|
|
244
|
-
*/
|
|
245
|
-
async getPendingReviews(): Promise<(StudySessionReviewItem & ScheduledCard)[]> {
|
|
246
|
-
// For legacy method, need to filter to generators that have getPendingReviews
|
|
247
|
-
const legacyGenerators = this.generators.filter(
|
|
248
|
-
(g): g is CardGenerator & ContentNavigator => g instanceof ContentNavigator
|
|
249
|
-
);
|
|
250
|
-
|
|
251
|
-
const results = await Promise.all(legacyGenerators.map((g) => g.getPendingReviews()));
|
|
252
|
-
|
|
253
|
-
// Deduplicate by cardID
|
|
254
|
-
const seen = new Set<string>();
|
|
255
|
-
const merged: (StudySessionReviewItem & ScheduledCard)[] = [];
|
|
256
|
-
|
|
257
|
-
for (const reviews of results) {
|
|
258
|
-
for (const review of reviews) {
|
|
259
|
-
if (!seen.has(review.cardID)) {
|
|
260
|
-
seen.add(review.cardID);
|
|
261
|
-
merged.push(review);
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
return merged;
|
|
267
|
-
}
|
|
268
219
|
}
|
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type {
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import type { WeightedCard } from './index';
|
|
6
|
-
import type { CourseElo } from '@vue-skuilder/common';
|
|
1
|
+
import type { CourseDBInterface } from '../../interfaces/courseDB';
|
|
2
|
+
import type { UserDBInterface } from '../../interfaces/userDB';
|
|
3
|
+
import { ContentNavigator } from '../index';
|
|
4
|
+
import type { WeightedCard } from '../index';
|
|
7
5
|
import { toCourseElo } from '@vue-skuilder/common';
|
|
8
|
-
import type {
|
|
9
|
-
import type { CardGenerator, GeneratorContext } from './
|
|
6
|
+
import type { QualifiedCardID } from '../..';
|
|
7
|
+
import type { CardGenerator, GeneratorContext } from './types';
|
|
10
8
|
|
|
11
9
|
// ============================================================================
|
|
12
10
|
// ELO NAVIGATOR
|
|
@@ -51,59 +49,6 @@ export default class ELONavigator extends ContentNavigator implements CardGenera
|
|
|
51
49
|
this.name = strategyData?.name || 'ELO';
|
|
52
50
|
}
|
|
53
51
|
|
|
54
|
-
async getPendingReviews(): Promise<(StudySessionReviewItem & ScheduledCard)[]> {
|
|
55
|
-
type ratedReview = ScheduledCard & CourseElo;
|
|
56
|
-
|
|
57
|
-
const reviews = await this.user.getPendingReviews(this.course.getCourseID()); // todo: this adds a db round trip - should be server side
|
|
58
|
-
const elo = await this.course.getCardEloData(reviews.map((r) => r.cardId));
|
|
59
|
-
|
|
60
|
-
const ratedReviews = reviews.map((r, i) => {
|
|
61
|
-
const ratedR: ratedReview = {
|
|
62
|
-
...r,
|
|
63
|
-
...elo[i],
|
|
64
|
-
};
|
|
65
|
-
return ratedR;
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
ratedReviews.sort((a, b) => {
|
|
69
|
-
return a.global.score - b.global.score;
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
return ratedReviews.map((r) => {
|
|
73
|
-
return {
|
|
74
|
-
...r,
|
|
75
|
-
contentSourceType: 'course',
|
|
76
|
-
contentSourceID: this.course.getCourseID(),
|
|
77
|
-
cardID: r.cardId,
|
|
78
|
-
courseID: r.courseId,
|
|
79
|
-
qualifiedID: `${r.courseId}-${r.cardId}`,
|
|
80
|
-
reviewID: r._id,
|
|
81
|
-
status: 'review',
|
|
82
|
-
};
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
async getNewCards(limit: number = 99): Promise<StudySessionNewItem[]> {
|
|
87
|
-
const activeCards = await this.user.getActiveCards();
|
|
88
|
-
return (
|
|
89
|
-
await this.course.getCardsCenteredAtELO(
|
|
90
|
-
{ limit: limit, elo: 'user' },
|
|
91
|
-
(c: QualifiedCardID) => {
|
|
92
|
-
if (activeCards.some((ac) => c.cardID === ac.cardID)) {
|
|
93
|
-
return false;
|
|
94
|
-
} else {
|
|
95
|
-
return true;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
)
|
|
99
|
-
).map((c) => {
|
|
100
|
-
return {
|
|
101
|
-
...c,
|
|
102
|
-
status: 'new',
|
|
103
|
-
};
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
|
|
107
52
|
/**
|
|
108
53
|
* Get new cards with suitability scores based on ELO distance.
|
|
109
54
|
*
|
|
@@ -130,8 +75,13 @@ export default class ELONavigator extends ContentNavigator implements CardGenera
|
|
|
130
75
|
userGlobalElo = userElo.global.score;
|
|
131
76
|
}
|
|
132
77
|
|
|
133
|
-
|
|
134
|
-
const newCards =
|
|
78
|
+
const activeCards = await this.user.getActiveCards();
|
|
79
|
+
const newCards = (
|
|
80
|
+
await this.course.getCardsCenteredAtELO(
|
|
81
|
+
{ limit, elo: 'user' },
|
|
82
|
+
(c: QualifiedCardID) => !activeCards.some((ac) => c.cardID === ac.cardID)
|
|
83
|
+
)
|
|
84
|
+
).map((c) => ({ ...c, status: 'new' as const }));
|
|
135
85
|
|
|
136
86
|
// Get ELO data for all cards in one batch
|
|
137
87
|
const cardIds = newCards.map((c) => c.cardID);
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import moment from 'moment';
|
|
2
|
-
import type { ScheduledCard } from '
|
|
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:
|
|
@@ -142,6 +135,12 @@ export interface WeightedCard {
|
|
|
142
135
|
* Filters should use this instead of querying getAppliedTags() individually.
|
|
143
136
|
*/
|
|
144
137
|
tags?: string[];
|
|
138
|
+
/**
|
|
139
|
+
* Review document ID (_id from ScheduledCard).
|
|
140
|
+
* Present when this card originated from SRS review scheduling.
|
|
141
|
+
* Used by SessionController to track review outcomes and maintain review state.
|
|
142
|
+
*/
|
|
143
|
+
reviewID?: string;
|
|
145
144
|
}
|
|
146
145
|
|
|
147
146
|
/**
|
|
@@ -174,7 +173,6 @@ export function getCardOrigin(card: WeightedCard): 'new' | 'review' | 'failed' {
|
|
|
174
173
|
export enum Navigators {
|
|
175
174
|
ELO = 'elo',
|
|
176
175
|
SRS = 'srs',
|
|
177
|
-
HARDCODED = 'hardcodedOrder',
|
|
178
176
|
HIERARCHY = 'hierarchyDefinition',
|
|
179
177
|
INTERFERENCE = 'interferenceMitigator',
|
|
180
178
|
RELATIVE_PRIORITY = 'relativePriority',
|
|
@@ -186,7 +184,7 @@ export enum Navigators {
|
|
|
186
184
|
// ============================================================================
|
|
187
185
|
//
|
|
188
186
|
// Navigators are classified as either generators or filters:
|
|
189
|
-
// - Generators: Produce candidate cards (ELO, SRS
|
|
187
|
+
// - Generators: Produce candidate cards (ELO, SRS)
|
|
190
188
|
// - Filters: Transform/score candidates (Hierarchy, Interference, RelativePriority)
|
|
191
189
|
//
|
|
192
190
|
// This classification is used by PipelineAssembler to build pipelines:
|
|
@@ -213,7 +211,6 @@ export enum NavigatorRole {
|
|
|
213
211
|
export const NavigatorRoles: Record<Navigators, NavigatorRole> = {
|
|
214
212
|
[Navigators.ELO]: NavigatorRole.GENERATOR,
|
|
215
213
|
[Navigators.SRS]: NavigatorRole.GENERATOR,
|
|
216
|
-
[Navigators.HARDCODED]: NavigatorRole.GENERATOR,
|
|
217
214
|
[Navigators.HIERARCHY]: NavigatorRole.FILTER,
|
|
218
215
|
[Navigators.INTERFERENCE]: NavigatorRole.FILTER,
|
|
219
216
|
[Navigators.RELATIVE_PRIORITY]: NavigatorRole.FILTER,
|
|
@@ -252,10 +249,10 @@ export function isFilter(impl: string): boolean {
|
|
|
252
249
|
*/
|
|
253
250
|
export abstract class ContentNavigator implements StudyContentSource {
|
|
254
251
|
/** User interface for this navigation session */
|
|
255
|
-
protected user
|
|
252
|
+
protected user: UserDBInterface;
|
|
256
253
|
|
|
257
254
|
/** Course interface for this navigation session */
|
|
258
|
-
protected course
|
|
255
|
+
protected course: CourseDBInterface;
|
|
259
256
|
|
|
260
257
|
/** Human-readable name for this strategy instance (from ContentNavigationStrategyData.name) */
|
|
261
258
|
protected strategyName?: string;
|
|
@@ -267,16 +264,17 @@ export abstract class ContentNavigator implements StudyContentSource {
|
|
|
267
264
|
* Constructor for standard navigators.
|
|
268
265
|
* Call this from subclass constructors to initialize common fields.
|
|
269
266
|
*
|
|
270
|
-
* Note: CompositeGenerator
|
|
267
|
+
* Note: CompositeGenerator and Pipeline call super() without args, then set
|
|
268
|
+
* user/course fields directly if needed.
|
|
271
269
|
*/
|
|
272
270
|
constructor(
|
|
273
271
|
user?: UserDBInterface,
|
|
274
272
|
course?: CourseDBInterface,
|
|
275
273
|
strategyData?: ContentNavigationStrategyData
|
|
276
274
|
) {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
275
|
+
this.user = user!;
|
|
276
|
+
this.course = course!;
|
|
277
|
+
if (strategyData) {
|
|
280
278
|
this.strategyName = strategyData.name;
|
|
281
279
|
this.strategyId = strategyData._id;
|
|
282
280
|
}
|
|
@@ -352,15 +350,19 @@ export abstract class ContentNavigator implements StudyContentSource {
|
|
|
352
350
|
|
|
353
351
|
// Try different extension variations
|
|
354
352
|
const variations = ['.ts', '.js', ''];
|
|
353
|
+
const dirs = ['filters', 'generators'];
|
|
355
354
|
|
|
356
355
|
for (const ext of variations) {
|
|
357
|
-
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
356
|
+
for (const dir of dirs) {
|
|
357
|
+
const loadFrom = `./${dir}/${implementingClass}${ext}`;
|
|
358
|
+
try {
|
|
359
|
+
const module = await import(loadFrom);
|
|
360
|
+
NavigatorImpl = module.default;
|
|
361
|
+
break; // Break the loop if loading succeeds
|
|
362
|
+
} catch (e) {
|
|
363
|
+
// Continue to next variation if this one fails
|
|
364
|
+
logger.debug(`Failed to load extension from ${loadFrom}:`, e);
|
|
365
|
+
}
|
|
364
366
|
}
|
|
365
367
|
}
|
|
366
368
|
|
|
@@ -371,24 +373,6 @@ export abstract class ContentNavigator implements StudyContentSource {
|
|
|
371
373
|
return new NavigatorImpl(user, course, strategyData);
|
|
372
374
|
}
|
|
373
375
|
|
|
374
|
-
/**
|
|
375
|
-
* Get cards scheduled for review.
|
|
376
|
-
*
|
|
377
|
-
* @deprecated This method is part of the legacy StudyContentSource interface.
|
|
378
|
-
* New strategies should focus on implementing CardGenerator.getWeightedCards() instead.
|
|
379
|
-
*/
|
|
380
|
-
abstract getPendingReviews(): Promise<(StudySessionReviewItem & ScheduledCard)[]>;
|
|
381
|
-
|
|
382
|
-
/**
|
|
383
|
-
* Get new cards for introduction.
|
|
384
|
-
*
|
|
385
|
-
* @deprecated This method is part of the legacy StudyContentSource interface.
|
|
386
|
-
* New strategies should focus on implementing CardGenerator.getWeightedCards() instead.
|
|
387
|
-
*
|
|
388
|
-
* @param n - Maximum number of new cards to return
|
|
389
|
-
*/
|
|
390
|
-
abstract getNewCards(n?: number): Promise<StudySessionNewItem[]>;
|
|
391
|
-
|
|
392
376
|
/**
|
|
393
377
|
* Get cards with suitability scores and provenance trails.
|
|
394
378
|
*
|
|
@@ -398,62 +382,23 @@ export abstract class ContentNavigator implements StudyContentSource {
|
|
|
398
382
|
* better candidates for presentation. Each card includes a provenance trail
|
|
399
383
|
* documenting how strategies contributed to the final score.
|
|
400
384
|
*
|
|
385
|
+
* ## Implementation Required
|
|
386
|
+
* All navigation strategies MUST override this method. The base class does
|
|
387
|
+
* not provide a default implementation.
|
|
388
|
+
*
|
|
401
389
|
* ## For Generators
|
|
402
390
|
* Override this method to generate candidates and compute scores based on
|
|
403
391
|
* your strategy's logic (e.g., ELO proximity, review urgency). Create the
|
|
404
392
|
* initial provenance entry with action='generated'.
|
|
405
393
|
*
|
|
406
|
-
* ##
|
|
407
|
-
*
|
|
408
|
-
*
|
|
409
|
-
* 2. Assigns score=1.0 to all cards
|
|
410
|
-
* 3. Creates minimal provenance from legacy methods
|
|
411
|
-
* 4. Returns combined results up to limit
|
|
412
|
-
*
|
|
413
|
-
* This allows existing strategies to work without modification while
|
|
414
|
-
* new strategies can override with proper scoring and provenance.
|
|
394
|
+
* ## For Filters
|
|
395
|
+
* Filters should implement the CardFilter interface instead and be composed
|
|
396
|
+
* via Pipeline. Filters do not directly implement getWeightedCards().
|
|
415
397
|
*
|
|
416
398
|
* @param limit - Maximum cards to return
|
|
417
399
|
* @returns Cards sorted by score descending, with provenance trails
|
|
418
400
|
*/
|
|
419
|
-
async getWeightedCards(
|
|
420
|
-
|
|
421
|
-
const newCards = await this.getNewCards(limit);
|
|
422
|
-
const reviews = await this.getPendingReviews();
|
|
423
|
-
|
|
424
|
-
const weighted: WeightedCard[] = [
|
|
425
|
-
...newCards.map((c) => ({
|
|
426
|
-
cardId: c.cardID,
|
|
427
|
-
courseId: c.courseID,
|
|
428
|
-
score: 1.0,
|
|
429
|
-
provenance: [
|
|
430
|
-
{
|
|
431
|
-
strategy: 'legacy',
|
|
432
|
-
strategyName: this.strategyName || 'Legacy API',
|
|
433
|
-
strategyId: this.strategyId || 'legacy-fallback',
|
|
434
|
-
action: 'generated' as const,
|
|
435
|
-
score: 1.0,
|
|
436
|
-
reason: 'Generated via legacy getNewCards(), new card',
|
|
437
|
-
},
|
|
438
|
-
],
|
|
439
|
-
})),
|
|
440
|
-
...reviews.map((r) => ({
|
|
441
|
-
cardId: r.cardID,
|
|
442
|
-
courseId: r.courseID,
|
|
443
|
-
score: 1.0,
|
|
444
|
-
provenance: [
|
|
445
|
-
{
|
|
446
|
-
strategy: 'legacy',
|
|
447
|
-
strategyName: this.strategyName || 'Legacy API',
|
|
448
|
-
strategyId: this.strategyId || 'legacy-fallback',
|
|
449
|
-
action: 'generated' as const,
|
|
450
|
-
score: 1.0,
|
|
451
|
-
reason: 'Generated via legacy getPendingReviews(), review',
|
|
452
|
-
},
|
|
453
|
-
],
|
|
454
|
-
})),
|
|
455
|
-
];
|
|
456
|
-
|
|
457
|
-
return weighted.slice(0, limit);
|
|
401
|
+
async getWeightedCards(_limit: number): Promise<WeightedCard[]> {
|
|
402
|
+
throw new Error(`${this.constructor.name} must implement getWeightedCards(). `);
|
|
458
403
|
}
|
|
459
404
|
}
|