@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,14 +1,90 @@
|
|
|
1
1
|
import { toCourseElo } from '@vue-skuilder/common';
|
|
2
2
|
import type { CourseDBInterface } from '../interfaces/courseDB';
|
|
3
3
|
import type { UserDBInterface } from '../interfaces/userDB';
|
|
4
|
-
import type { ScheduledCard } from '../types/user';
|
|
5
4
|
import { ContentNavigator } from './index';
|
|
6
5
|
import type { WeightedCard } from './index';
|
|
7
6
|
import type { CardFilter, FilterContext } from './filters/types';
|
|
8
7
|
import type { CardGenerator, GeneratorContext } from './generators/types';
|
|
9
|
-
import type { StudySessionNewItem, StudySessionReviewItem } from '../interfaces/contentSource';
|
|
10
8
|
import { logger } from '../../util/logger';
|
|
11
9
|
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// PIPELINE LOGGING HELPERS
|
|
12
|
+
// ============================================================================
|
|
13
|
+
//
|
|
14
|
+
// Focused logging functions that can be toggled by commenting single lines.
|
|
15
|
+
// Use these to inspect pipeline behavior in development/production.
|
|
16
|
+
//
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Log pipeline configuration on construction.
|
|
20
|
+
* Shows generator and filter chain structure.
|
|
21
|
+
*/
|
|
22
|
+
function logPipelineConfig(generator: CardGenerator, filters: CardFilter[]): void {
|
|
23
|
+
const filterList =
|
|
24
|
+
filters.length > 0 ? '\n - ' + filters.map((f) => f.name).join('\n - ') : ' none';
|
|
25
|
+
|
|
26
|
+
logger.info(
|
|
27
|
+
`[Pipeline] Configuration:\n` + ` Generator: ${generator.name}\n` + ` Filters:${filterList}`
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Log tag hydration results.
|
|
33
|
+
* Shows effectiveness of batch query (how many cards/tags were hydrated).
|
|
34
|
+
*/
|
|
35
|
+
function logTagHydration(cards: WeightedCard[], tagsByCard: Map<string, string[]>): void {
|
|
36
|
+
const totalTags = Array.from(tagsByCard.values()).reduce((sum, tags) => sum + tags.length, 0);
|
|
37
|
+
const cardsWithTags = Array.from(tagsByCard.values()).filter((tags) => tags.length > 0).length;
|
|
38
|
+
|
|
39
|
+
logger.debug(
|
|
40
|
+
`[Pipeline] Tag hydration: ${cards.length} cards, ` +
|
|
41
|
+
`${cardsWithTags} have tags (${totalTags} total tags) - single batch query`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Log pipeline execution summary.
|
|
47
|
+
* Shows complete flow from generator through filters to final results.
|
|
48
|
+
*/
|
|
49
|
+
function logExecutionSummary(
|
|
50
|
+
generatorName: string,
|
|
51
|
+
generatedCount: number,
|
|
52
|
+
filterCount: number,
|
|
53
|
+
finalCount: number,
|
|
54
|
+
topScores: number[]
|
|
55
|
+
): void {
|
|
56
|
+
const scoreDisplay =
|
|
57
|
+
topScores.length > 0 ? topScores.map((s) => s.toFixed(2)).join(', ') : 'none';
|
|
58
|
+
|
|
59
|
+
logger.info(
|
|
60
|
+
`[Pipeline] Execution: ${generatorName} produced ${generatedCount} → ` +
|
|
61
|
+
`${filterCount} filters → ${finalCount} results (top scores: ${scoreDisplay})`
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Log provenance trails for cards.
|
|
67
|
+
* Shows the complete scoring history for each card through the pipeline.
|
|
68
|
+
* Useful for debugging why cards scored the way they did.
|
|
69
|
+
*/
|
|
70
|
+
function logCardProvenance(cards: WeightedCard[], maxCards: number = 3): void {
|
|
71
|
+
const cardsToLog = cards.slice(0, maxCards);
|
|
72
|
+
|
|
73
|
+
logger.debug(`[Pipeline] Provenance for top ${cardsToLog.length} cards:`);
|
|
74
|
+
|
|
75
|
+
for (const card of cardsToLog) {
|
|
76
|
+
logger.debug(`[Pipeline] ${card.cardId} (final score: ${card.score.toFixed(3)}):`);
|
|
77
|
+
|
|
78
|
+
for (const entry of card.provenance) {
|
|
79
|
+
const scoreChange = entry.score.toFixed(3);
|
|
80
|
+
const action = entry.action.padEnd(9); // Align columns
|
|
81
|
+
logger.debug(
|
|
82
|
+
`[Pipeline] ${action} ${scoreChange} - ${entry.strategyName}: ${entry.reason}`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
12
88
|
// ============================================================================
|
|
13
89
|
// PIPELINE
|
|
14
90
|
// ============================================================================
|
|
@@ -72,9 +148,16 @@ export class Pipeline extends ContentNavigator {
|
|
|
72
148
|
this.user = user;
|
|
73
149
|
this.course = course;
|
|
74
150
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
151
|
+
course
|
|
152
|
+
.getCourseConfig()
|
|
153
|
+
.then((cfg) => {
|
|
154
|
+
logger.debug(`[pipeline] Crated pipeline for ${cfg.name}`);
|
|
155
|
+
})
|
|
156
|
+
.catch((e) => {
|
|
157
|
+
logger.error(`[pipeline] Failed to lookup courseCfg: ${e}`);
|
|
158
|
+
});
|
|
159
|
+
// Toggle pipeline configuration logging:
|
|
160
|
+
logPipelineConfig(generator, filters);
|
|
78
161
|
}
|
|
79
162
|
|
|
80
163
|
/**
|
|
@@ -82,10 +165,11 @@ export class Pipeline extends ContentNavigator {
|
|
|
82
165
|
*
|
|
83
166
|
* 1. Build shared context (user ELO, etc.)
|
|
84
167
|
* 2. Get candidates from generator (passing context)
|
|
85
|
-
* 3.
|
|
86
|
-
* 4.
|
|
87
|
-
* 5.
|
|
88
|
-
* 6.
|
|
168
|
+
* 3. Batch hydrate tags for all candidates
|
|
169
|
+
* 4. Apply each filter sequentially
|
|
170
|
+
* 5. Remove zero-score cards
|
|
171
|
+
* 6. Sort by score descending
|
|
172
|
+
* 7. Return top N
|
|
89
173
|
*
|
|
90
174
|
* @param limit - Maximum number of cards to return
|
|
91
175
|
* @returns Cards sorted by score descending
|
|
@@ -104,8 +188,12 @@ export class Pipeline extends ContentNavigator {
|
|
|
104
188
|
|
|
105
189
|
// Get candidates from generator, passing context
|
|
106
190
|
let cards = await this.generator.getWeightedCards(fetchLimit, context);
|
|
191
|
+
const generatedCount = cards.length;
|
|
192
|
+
|
|
193
|
+
logger.debug(`[Pipeline] Generator returned ${generatedCount} candidates`);
|
|
107
194
|
|
|
108
|
-
|
|
195
|
+
// Batch hydrate tags before filters run
|
|
196
|
+
cards = await this.hydrateTags(cards);
|
|
109
197
|
|
|
110
198
|
// Apply filters sequentially
|
|
111
199
|
for (const filter of this.filters) {
|
|
@@ -123,16 +211,49 @@ export class Pipeline extends ContentNavigator {
|
|
|
123
211
|
// Return top N
|
|
124
212
|
const result = cards.slice(0, limit);
|
|
125
213
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
214
|
+
// Toggle execution summary logging:
|
|
215
|
+
const topScores = result.slice(0, 3).map((c) => c.score);
|
|
216
|
+
logExecutionSummary(
|
|
217
|
+
this.generator.name,
|
|
218
|
+
generatedCount,
|
|
219
|
+
this.filters.length,
|
|
220
|
+
result.length,
|
|
221
|
+
topScores
|
|
131
222
|
);
|
|
132
223
|
|
|
224
|
+
// Toggle provenance logging (shows scoring history for top cards):
|
|
225
|
+
logCardProvenance(result, 3);
|
|
226
|
+
|
|
133
227
|
return result;
|
|
134
228
|
}
|
|
135
229
|
|
|
230
|
+
/**
|
|
231
|
+
* Batch hydrate tags for all cards.
|
|
232
|
+
*
|
|
233
|
+
* Fetches tags for all cards in a single database query and attaches them
|
|
234
|
+
* to the WeightedCard objects. Filters can then use card.tags instead of
|
|
235
|
+
* making individual getAppliedTags() calls.
|
|
236
|
+
*
|
|
237
|
+
* @param cards - Cards to hydrate
|
|
238
|
+
* @returns Cards with tags populated
|
|
239
|
+
*/
|
|
240
|
+
private async hydrateTags(cards: WeightedCard[]): Promise<WeightedCard[]> {
|
|
241
|
+
if (cards.length === 0) {
|
|
242
|
+
return cards;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const cardIds = cards.map((c) => c.cardId);
|
|
246
|
+
const tagsByCard = await this.course!.getAppliedTagsBatch(cardIds);
|
|
247
|
+
|
|
248
|
+
// Toggle tag hydration logging:
|
|
249
|
+
logTagHydration(cards, tagsByCard);
|
|
250
|
+
|
|
251
|
+
return cards.map((card) => ({
|
|
252
|
+
...card,
|
|
253
|
+
tags: tagsByCard.get(card.cardId) ?? [],
|
|
254
|
+
}));
|
|
255
|
+
}
|
|
256
|
+
|
|
136
257
|
/**
|
|
137
258
|
* Build shared context for generator and filters.
|
|
138
259
|
*
|
|
@@ -159,43 +280,6 @@ export class Pipeline extends ContentNavigator {
|
|
|
159
280
|
};
|
|
160
281
|
}
|
|
161
282
|
|
|
162
|
-
// ===========================================================================
|
|
163
|
-
// Legacy StudyContentSource methods
|
|
164
|
-
// ===========================================================================
|
|
165
|
-
//
|
|
166
|
-
// These delegate to the generator for backward compatibility.
|
|
167
|
-
// Eventually SessionController will use getWeightedCards() exclusively.
|
|
168
|
-
//
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Get new cards via legacy API.
|
|
172
|
-
* Delegates to the generator if it supports the legacy interface.
|
|
173
|
-
*/
|
|
174
|
-
async getNewCards(n?: number): Promise<StudySessionNewItem[]> {
|
|
175
|
-
// Check if generator has legacy method (ContentNavigator-based generators do)
|
|
176
|
-
if ('getNewCards' in this.generator && typeof this.generator.getNewCards === 'function') {
|
|
177
|
-
return (this.generator as ContentNavigator).getNewCards(n);
|
|
178
|
-
}
|
|
179
|
-
// Pure CardGenerator without legacy support - return empty
|
|
180
|
-
return [];
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Get pending reviews via legacy API.
|
|
185
|
-
* Delegates to the generator if it supports the legacy interface.
|
|
186
|
-
*/
|
|
187
|
-
async getPendingReviews(): Promise<(StudySessionReviewItem & ScheduledCard)[]> {
|
|
188
|
-
// Check if generator has legacy method (ContentNavigator-based generators do)
|
|
189
|
-
if (
|
|
190
|
-
'getPendingReviews' in this.generator &&
|
|
191
|
-
typeof this.generator.getPendingReviews === 'function'
|
|
192
|
-
) {
|
|
193
|
-
return (this.generator as ContentNavigator).getPendingReviews();
|
|
194
|
-
}
|
|
195
|
-
// Pure CardGenerator without legacy support - return empty
|
|
196
|
-
return [];
|
|
197
|
-
}
|
|
198
|
-
|
|
199
283
|
/**
|
|
200
284
|
* Get the course ID for this pipeline.
|
|
201
285
|
*/
|
|
@@ -7,7 +7,7 @@ import { DocType } from '../types/types-legacy';
|
|
|
7
7
|
import { logger } from '../../util/logger';
|
|
8
8
|
import type { CourseDBInterface } from '../interfaces/courseDB';
|
|
9
9
|
import type { UserDBInterface } from '../interfaces/userDB';
|
|
10
|
-
import CompositeGenerator from './CompositeGenerator';
|
|
10
|
+
import CompositeGenerator from './generators/CompositeGenerator';
|
|
11
11
|
|
|
12
12
|
// ============================================================================
|
|
13
13
|
// PIPELINE ASSEMBLER
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Navigators } from './index';
|
|
2
|
+
import { Pipeline } from './Pipeline';
|
|
3
|
+
import CompositeGenerator from './generators/CompositeGenerator';
|
|
4
|
+
import ELONavigator from './generators/elo';
|
|
5
|
+
import SRSNavigator from './generators/srs';
|
|
6
|
+
import { createEloDistanceFilter } from './filters/eloDistance';
|
|
7
|
+
import type { ContentNavigationStrategyData } from '../types/contentNavigationStrategy';
|
|
8
|
+
import { DocType } from '../types/types-legacy';
|
|
9
|
+
import type { CourseDBInterface, UserDBInterface } from '../interfaces';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Default navigation pipeline configuration.
|
|
13
|
+
*
|
|
14
|
+
* This module provides factory functions for creating the canonical default
|
|
15
|
+
* navigation pipeline used by both CouchDB and static course implementations.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Create default ELO navigation strategy data.
|
|
20
|
+
* Used when no custom strategies are configured.
|
|
21
|
+
*
|
|
22
|
+
* @param courseId - The course ID to associate with this strategy
|
|
23
|
+
* @returns Strategy data for default ELO navigation
|
|
24
|
+
*/
|
|
25
|
+
export function createDefaultEloStrategy(courseId: string): ContentNavigationStrategyData {
|
|
26
|
+
return {
|
|
27
|
+
_id: 'NAVIGATION_STRATEGY-ELO-default',
|
|
28
|
+
docType: DocType.NAVIGATION_STRATEGY,
|
|
29
|
+
name: 'ELO (default)',
|
|
30
|
+
description: 'Default ELO-based navigation strategy for new cards',
|
|
31
|
+
implementingClass: Navigators.ELO,
|
|
32
|
+
course: courseId,
|
|
33
|
+
serializedData: '',
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Create default SRS navigation strategy data.
|
|
39
|
+
* Used when no custom strategies are configured.
|
|
40
|
+
*
|
|
41
|
+
* @param courseId - The course ID to associate with this strategy
|
|
42
|
+
* @returns Strategy data for default SRS navigation
|
|
43
|
+
*/
|
|
44
|
+
export function createDefaultSrsStrategy(courseId: string): ContentNavigationStrategyData {
|
|
45
|
+
return {
|
|
46
|
+
_id: 'NAVIGATION_STRATEGY-SRS-default',
|
|
47
|
+
docType: DocType.NAVIGATION_STRATEGY,
|
|
48
|
+
name: 'SRS (default)',
|
|
49
|
+
description: 'Default SRS-based navigation strategy for reviews',
|
|
50
|
+
implementingClass: Navigators.SRS,
|
|
51
|
+
course: courseId,
|
|
52
|
+
serializedData: '',
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Creates the default navigation pipeline for courses with no configured strategies.
|
|
58
|
+
*
|
|
59
|
+
* Default: Pipeline(Composite(ELO, SRS), [eloDistanceFilter])
|
|
60
|
+
* - ELO generator: scores new cards by skill proximity
|
|
61
|
+
* - SRS generator: scores reviews by overdueness and interval recency
|
|
62
|
+
* - ELO distance filter: penalizes cards far from user's current level
|
|
63
|
+
*
|
|
64
|
+
* This is the canonical default configuration used when:
|
|
65
|
+
* - No navigation strategy documents exist in the course
|
|
66
|
+
* - PipelineAssembler fails to build from strategy documents
|
|
67
|
+
*
|
|
68
|
+
* @param user - User database interface for accessing user state
|
|
69
|
+
* @param course - Course database interface for accessing course data
|
|
70
|
+
* @returns Configured Pipeline ready for use
|
|
71
|
+
*/
|
|
72
|
+
export function createDefaultPipeline(
|
|
73
|
+
user: UserDBInterface,
|
|
74
|
+
course: CourseDBInterface
|
|
75
|
+
): Pipeline {
|
|
76
|
+
const courseId = course.getCourseID();
|
|
77
|
+
const eloNavigator = new ELONavigator(user, course, createDefaultEloStrategy(courseId));
|
|
78
|
+
const srsNavigator = new SRSNavigator(user, course, createDefaultSrsStrategy(courseId));
|
|
79
|
+
|
|
80
|
+
const compositeGenerator = new CompositeGenerator([eloNavigator, srsNavigator]);
|
|
81
|
+
const eloDistanceFilter = createEloDistanceFilter();
|
|
82
|
+
|
|
83
|
+
return new Pipeline(compositeGenerator, [eloDistanceFilter], user, course);
|
|
84
|
+
}
|
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import type {
|
|
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
|
import { toCourseElo } from '@vue-skuilder/common';
|
|
10
8
|
|
|
11
9
|
/**
|
|
@@ -53,7 +51,6 @@ const DEFAULT_MIN_COUNT = 3;
|
|
|
53
51
|
*/
|
|
54
52
|
export default class HierarchyDefinitionNavigator extends ContentNavigator implements CardFilter {
|
|
55
53
|
private config: HierarchyConfig;
|
|
56
|
-
private _strategyData: ContentNavigationStrategyData;
|
|
57
54
|
|
|
58
55
|
/** Human-readable name for CardFilter interface */
|
|
59
56
|
name: string;
|
|
@@ -61,12 +58,11 @@ export default class HierarchyDefinitionNavigator extends ContentNavigator imple
|
|
|
61
58
|
constructor(
|
|
62
59
|
user: UserDBInterface,
|
|
63
60
|
course: CourseDBInterface,
|
|
64
|
-
|
|
61
|
+
strategyData: ContentNavigationStrategyData
|
|
65
62
|
) {
|
|
66
|
-
super(user, course,
|
|
67
|
-
this.
|
|
68
|
-
this.
|
|
69
|
-
this.name = _strategyData.name || 'Hierarchy Definition';
|
|
63
|
+
super(user, course, strategyData);
|
|
64
|
+
this.config = this.parseConfig(strategyData.serializedData);
|
|
65
|
+
this.name = strategyData.name || 'Hierarchy Definition';
|
|
70
66
|
}
|
|
71
67
|
|
|
72
68
|
private parseConfig(serializedData: string): HierarchyConfig {
|
|
@@ -158,14 +154,14 @@ export default class HierarchyDefinitionNavigator extends ContentNavigator imple
|
|
|
158
154
|
* Check if a card is unlocked and generate reason.
|
|
159
155
|
*/
|
|
160
156
|
private async checkCardUnlock(
|
|
161
|
-
|
|
162
|
-
|
|
157
|
+
card: WeightedCard,
|
|
158
|
+
_course: CourseDBInterface,
|
|
163
159
|
unlockedTags: Set<string>,
|
|
164
160
|
masteredTags: Set<string>
|
|
165
161
|
): Promise<{ isUnlocked: boolean; reason: string }> {
|
|
166
162
|
try {
|
|
167
|
-
|
|
168
|
-
const cardTags =
|
|
163
|
+
// Pipeline hydrates tags before filters run
|
|
164
|
+
const cardTags = card.tags ?? [];
|
|
169
165
|
|
|
170
166
|
// Check each tag's prerequisite status
|
|
171
167
|
const lockedTags = cardTags.filter(
|
|
@@ -214,7 +210,7 @@ export default class HierarchyDefinitionNavigator extends ContentNavigator imple
|
|
|
214
210
|
|
|
215
211
|
for (const card of cards) {
|
|
216
212
|
const { isUnlocked, reason } = await this.checkCardUnlock(
|
|
217
|
-
card
|
|
213
|
+
card,
|
|
218
214
|
context.course,
|
|
219
215
|
unlockedTags,
|
|
220
216
|
masteredTags
|
|
@@ -253,14 +249,4 @@ export default class HierarchyDefinitionNavigator extends ContentNavigator imple
|
|
|
253
249
|
'Use Pipeline with a generator and this filter via transform().'
|
|
254
250
|
);
|
|
255
251
|
}
|
|
256
|
-
|
|
257
|
-
// Legacy methods - stub implementations since filters don't generate cards
|
|
258
|
-
|
|
259
|
-
async getNewCards(_n?: number): Promise<StudySessionNewItem[]> {
|
|
260
|
-
return [];
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
async getPendingReviews(): Promise<(StudySessionReviewItem & ScheduledCard)[]> {
|
|
264
|
-
return [];
|
|
265
|
-
}
|
|
266
252
|
}
|
|
@@ -4,3 +4,6 @@ export type { CardFilter, FilterContext, CardFilterFactory } from './types';
|
|
|
4
4
|
// Filter implementations
|
|
5
5
|
export { createEloDistanceFilter } from './eloDistance';
|
|
6
6
|
export type { EloDistanceConfig } from './eloDistance';
|
|
7
|
+
|
|
8
|
+
export { default as UserTagPreferenceFilter } from './userTagPreference';
|
|
9
|
+
export type { UserTagPreferenceState } from './userTagPreference';
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// INFERRED PREFERENCE NAVIGATOR — STUB
|
|
3
|
+
// ============================================================================
|
|
4
|
+
//
|
|
5
|
+
// STATUS: NOT IMPLEMENTED — This file documents architectural intent only.
|
|
6
|
+
//
|
|
7
|
+
// ============================================================================
|
|
8
|
+
//
|
|
9
|
+
// ## Purpose
|
|
10
|
+
//
|
|
11
|
+
// Inferred preferences are learned from user behavior, as opposed to explicit
|
|
12
|
+
// preferences which are configured via UI. The system observes patterns in
|
|
13
|
+
// user interactions and adjusts card selection accordingly.
|
|
14
|
+
//
|
|
15
|
+
// ## Inference Signals
|
|
16
|
+
//
|
|
17
|
+
// Potential signals to learn from:
|
|
18
|
+
//
|
|
19
|
+
// 1. **Card dismissal patterns**: User consistently skips certain card types
|
|
20
|
+
// 2. **Time-on-card**: User spends less time on certain content (boredom?)
|
|
21
|
+
// 3. **Error patterns**: User struggles with certain presentation styles
|
|
22
|
+
// 4. **Session timing**: User performs better at certain times of day
|
|
23
|
+
// 5. **Tag success rates**: User masters some tags faster than others
|
|
24
|
+
//
|
|
25
|
+
// ## Inferred State (Proposed)
|
|
26
|
+
//
|
|
27
|
+
// ```typescript
|
|
28
|
+
// interface InferredPreferenceState {
|
|
29
|
+
// // Learned tag affinities (positive = user does well, negative = struggles)
|
|
30
|
+
// tagAffinities: Record<string, number>;
|
|
31
|
+
//
|
|
32
|
+
// // Presentation style preferences
|
|
33
|
+
// preferredStyles: {
|
|
34
|
+
// visualVsText: number; // -1 to 1 (negative = text, positive = visual)
|
|
35
|
+
// shortVsLong: number; // -1 to 1 (negative = long, positive = short)
|
|
36
|
+
// };
|
|
37
|
+
//
|
|
38
|
+
// // Temporal patterns
|
|
39
|
+
// optimalSessionLength: number; // minutes
|
|
40
|
+
// optimalTimeOfDay: number; // hour (0-23)
|
|
41
|
+
//
|
|
42
|
+
// // Confidence in inferences
|
|
43
|
+
// sampleSize: number;
|
|
44
|
+
// lastUpdated: string;
|
|
45
|
+
// }
|
|
46
|
+
// ```
|
|
47
|
+
//
|
|
48
|
+
// ## Relationship to Explicit Preferences
|
|
49
|
+
//
|
|
50
|
+
// - Explicit preferences (UserTagPreferenceFilter) always take precedence
|
|
51
|
+
// - Inferred preferences act as soft suggestions when no explicit pref exists
|
|
52
|
+
// - User can "lock in" an inference as an explicit preference via UI
|
|
53
|
+
// - User can dismiss/override an inference ("I actually like text cards")
|
|
54
|
+
//
|
|
55
|
+
// ## Transparency Requirements
|
|
56
|
+
//
|
|
57
|
+
// Inferred preferences must be:
|
|
58
|
+
//
|
|
59
|
+
// 1. **Visible**: User can see what the system has inferred
|
|
60
|
+
// 2. **Explainable**: "We noticed you master visual cards faster"
|
|
61
|
+
// 3. **Overridable**: User can disable or invert any inference
|
|
62
|
+
// 4. **Forgettable**: User can reset inferences and start fresh
|
|
63
|
+
//
|
|
64
|
+
// ## Implementation Considerations
|
|
65
|
+
//
|
|
66
|
+
// 1. **Cold start**: Need minimum sample size before inferring
|
|
67
|
+
// 2. **Drift**: Preferences may change over time; use decay/recency weighting
|
|
68
|
+
// 3. **Privacy**: Inference data is personal; handle with care
|
|
69
|
+
// 4. **Bias**: Avoid reinforcing accidental patterns as permanent preferences
|
|
70
|
+
//
|
|
71
|
+
// ## Related Files
|
|
72
|
+
//
|
|
73
|
+
// - `filters/userTagPreference.ts` — Explicit preferences (takes precedence)
|
|
74
|
+
// - `userGoal.ts` — Goals (destination, not path)
|
|
75
|
+
// - `../types/strategyState.ts` — Storage mechanism
|
|
76
|
+
//
|
|
77
|
+
// ## Next Steps
|
|
78
|
+
//
|
|
79
|
+
// 1. Define minimum viable inference signals
|
|
80
|
+
// 2. Design inference algorithms (simple heuristics vs ML)
|
|
81
|
+
// 3. Build transparency UI ("Here's what we learned about you")
|
|
82
|
+
// 4. Implement override/dismiss mechanism
|
|
83
|
+
// 5. Add to card record collection for inference input
|
|
84
|
+
//
|
|
85
|
+
// ============================================================================
|
|
86
|
+
|
|
87
|
+
// Placeholder export to make this a valid module
|
|
88
|
+
export const INFERRED_PREFERENCE_NAVIGATOR_STUB = true;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* @stub InferredPreferenceNavigator
|
|
92
|
+
*
|
|
93
|
+
* A navigator that learns user preferences from behavior patterns.
|
|
94
|
+
* See module-level documentation for architectural intent.
|
|
95
|
+
*
|
|
96
|
+
* NOT IMPLEMENTED — This is a design placeholder.
|
|
97
|
+
*/
|
|
98
|
+
export interface InferredPreferenceState {
|
|
99
|
+
/** Learned affinity scores per tag (-1 to 1) */
|
|
100
|
+
tagAffinities: Record<string, number>;
|
|
101
|
+
|
|
102
|
+
/** Number of card interactions used to build inferences */
|
|
103
|
+
sampleSize: number;
|
|
104
|
+
|
|
105
|
+
/** ISO timestamp of last inference update */
|
|
106
|
+
updatedAt: string;
|
|
107
|
+
}
|
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import type {
|
|
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
|
import { toCourseElo } from '@vue-skuilder/common';
|
|
10
8
|
|
|
11
9
|
/**
|
|
@@ -80,7 +78,6 @@ const DEFAULT_INTERFERENCE_DECAY = 0.8;
|
|
|
80
78
|
*/
|
|
81
79
|
export default class InterferenceMitigatorNavigator extends ContentNavigator implements CardFilter {
|
|
82
80
|
private config: InterferenceConfig;
|
|
83
|
-
private _strategyData: ContentNavigationStrategyData;
|
|
84
81
|
|
|
85
82
|
/** Human-readable name for CardFilter interface */
|
|
86
83
|
name: string;
|
|
@@ -91,13 +88,12 @@ export default class InterferenceMitigatorNavigator extends ContentNavigator imp
|
|
|
91
88
|
constructor(
|
|
92
89
|
user: UserDBInterface,
|
|
93
90
|
course: CourseDBInterface,
|
|
94
|
-
|
|
91
|
+
strategyData: ContentNavigationStrategyData
|
|
95
92
|
) {
|
|
96
|
-
super(user, course,
|
|
97
|
-
this.
|
|
98
|
-
this.config = this.parseConfig(_strategyData.serializedData);
|
|
93
|
+
super(user, course, strategyData);
|
|
94
|
+
this.config = this.parseConfig(strategyData.serializedData);
|
|
99
95
|
this.interferenceMap = this.buildInterferenceMap();
|
|
100
|
-
this.name =
|
|
96
|
+
this.name = strategyData.name || 'Interference Mitigator';
|
|
101
97
|
}
|
|
102
98
|
|
|
103
99
|
private parseConfig(serializedData: string): InterferenceConfig {
|
|
@@ -234,18 +230,6 @@ export default class InterferenceMitigatorNavigator extends ContentNavigator imp
|
|
|
234
230
|
return avoid;
|
|
235
231
|
}
|
|
236
232
|
|
|
237
|
-
/**
|
|
238
|
-
* Get tags for a single card
|
|
239
|
-
*/
|
|
240
|
-
private async getCardTags(cardId: string, course: CourseDBInterface): Promise<string[]> {
|
|
241
|
-
try {
|
|
242
|
-
const tagResponse = await course.getAppliedTags(cardId);
|
|
243
|
-
return tagResponse.rows.map((row) => row.value?.name || row.key).filter(Boolean);
|
|
244
|
-
} catch {
|
|
245
|
-
return [];
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
233
|
/**
|
|
250
234
|
* Compute interference score reduction for a card.
|
|
251
235
|
* Returns: { multiplier, interfering tags, reason }
|
|
@@ -313,7 +297,7 @@ export default class InterferenceMitigatorNavigator extends ContentNavigator imp
|
|
|
313
297
|
const adjusted: WeightedCard[] = [];
|
|
314
298
|
|
|
315
299
|
for (const card of cards) {
|
|
316
|
-
const cardTags =
|
|
300
|
+
const cardTags = card.tags ?? [];
|
|
317
301
|
const { multiplier, reason } = this.computeInterferenceEffect(
|
|
318
302
|
cardTags,
|
|
319
303
|
tagsToAvoid,
|
|
@@ -354,14 +338,4 @@ export default class InterferenceMitigatorNavigator extends ContentNavigator imp
|
|
|
354
338
|
'Use Pipeline with a generator and this filter via transform().'
|
|
355
339
|
);
|
|
356
340
|
}
|
|
357
|
-
|
|
358
|
-
// Legacy methods - stub implementations since filters don't generate cards
|
|
359
|
-
|
|
360
|
-
async getNewCards(_n?: number): Promise<StudySessionNewItem[]> {
|
|
361
|
-
return [];
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
async getPendingReviews(): Promise<(StudySessionReviewItem & ScheduledCard)[]> {
|
|
365
|
-
return [];
|
|
366
|
-
}
|
|
367
341
|
}
|