@vue-skuilder/db 0.1.18 → 0.1.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{classroomDB-BgfrVb8d.d.ts → classroomDB-CZdMBiTU.d.ts} +71 -2
- package/dist/{classroomDB-CTOenngH.d.cts → classroomDB-PxDZTky3.d.cts} +71 -2
- package/dist/core/index.d.cts +80 -6
- package/dist/core/index.d.ts +80 -6
- package/dist/core/index.js +370 -52
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +369 -52
- package/dist/core/index.mjs.map +1 -1
- package/dist/{dataLayerProvider-D6PoCwS6.d.cts → dataLayerProvider-D0MoZMjH.d.cts} +1 -1
- package/dist/{dataLayerProvider-CZxC9GtB.d.ts → dataLayerProvider-D8o6ZnKW.d.ts} +1 -1
- package/dist/impl/couch/index.d.cts +4 -3
- package/dist/impl/couch/index.d.ts +4 -3
- package/dist/impl/couch/index.js +371 -55
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +371 -55
- package/dist/impl/couch/index.mjs.map +1 -1
- package/dist/impl/static/index.d.cts +5 -4
- package/dist/impl/static/index.d.ts +5 -4
- package/dist/impl/static/index.js +356 -44
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +356 -44
- 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 +10 -10
- package/dist/index.d.ts +10 -10
- package/dist/index.js +382 -55
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +381 -55
- package/dist/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 -10
- package/package.json +4 -4
- package/src/core/index.ts +1 -0
- package/src/core/interfaces/courseDB.ts +13 -0
- package/src/core/interfaces/userDB.ts +32 -0
- package/src/core/navigators/Pipeline.ts +127 -14
- package/src/core/navigators/filters/index.ts +3 -0
- package/src/core/navigators/filters/userTagPreference.ts +232 -0
- package/src/core/navigators/hierarchyDefinition.ts +4 -4
- package/src/core/navigators/index.ts +59 -0
- package/src/core/navigators/inferredPreference.ts +107 -0
- package/src/core/navigators/interferenceMitigator.ts +1 -13
- package/src/core/navigators/relativePriority.ts +2 -14
- package/src/core/navigators/userGoal.ts +136 -0
- 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/courseDB.ts +30 -10
- package/src/impl/static/courseDB.ts +11 -0
- package/tests/core/navigators/Pipeline.test.ts +1 -0
- package/docs/todo-pipeline-optimization.md +0 -117
- package/docs/todo-strategy-state-storage.md +0 -278
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import type { ScheduledCard } from '../../types/user';
|
|
2
|
+
import type { CourseDBInterface } from '../../interfaces/courseDB';
|
|
3
|
+
import type { UserDBInterface } from '../../interfaces/userDB';
|
|
4
|
+
import { ContentNavigator } from '../index';
|
|
5
|
+
import type { WeightedCard } from '../index';
|
|
6
|
+
import type { ContentNavigationStrategyData } from '../../types/contentNavigationStrategy';
|
|
7
|
+
import type { StudySessionReviewItem, StudySessionNewItem } from '../..';
|
|
8
|
+
import type { CardFilter, FilterContext } from './types';
|
|
9
|
+
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// USER TAG PREFERENCE FILTER
|
|
12
|
+
// ============================================================================
|
|
13
|
+
//
|
|
14
|
+
// Allows users to personalize their learning experience by specifying:
|
|
15
|
+
// - Tags to boost/penalize (score multiplied by boost factor)
|
|
16
|
+
//
|
|
17
|
+
// User preferences are stored in STRATEGY_STATE documents in the user's
|
|
18
|
+
// database, enabling persistence across sessions and sync across devices.
|
|
19
|
+
//
|
|
20
|
+
// Use cases:
|
|
21
|
+
// - Goal-based learning: "I want to learn piano by ear, skip sight-reading"
|
|
22
|
+
// - Selective focus: "I only want to practice chess endgames"
|
|
23
|
+
// - Accessibility: "Skip text-heavy cards, prefer visual content"
|
|
24
|
+
// - Difficulty customization: "Skip beginner content I already know"
|
|
25
|
+
//
|
|
26
|
+
// ============================================================================
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* User's tag preference state, stored in STRATEGY_STATE document.
|
|
30
|
+
*
|
|
31
|
+
* This interface defines what gets persisted to the user's database.
|
|
32
|
+
* UI components write to this structure, and the filter reads from it.
|
|
33
|
+
*
|
|
34
|
+
* ## Preferences vs Goals
|
|
35
|
+
*
|
|
36
|
+
* Preferences are **path constraints** — they affect HOW the user learns,
|
|
37
|
+
* not WHAT they're trying to learn. Examples:
|
|
38
|
+
* - "Skip text-heavy cards" (accessibility)
|
|
39
|
+
* - "Prefer visual content"
|
|
40
|
+
*
|
|
41
|
+
* For **goal-based** filtering (defining WHAT to learn), see the separate
|
|
42
|
+
* UserGoalNavigator (stub). Goals affect progress tracking and completion
|
|
43
|
+
* criteria; preferences only affect card selection.
|
|
44
|
+
*
|
|
45
|
+
* ## Slider Semantics
|
|
46
|
+
*
|
|
47
|
+
* Each tag maps to a multiplier value in the `boost` record:
|
|
48
|
+
* - `0` = banish/exclude (card score = 0)
|
|
49
|
+
* - `0.5` = penalize by 50%
|
|
50
|
+
* - `1.0` = neutral/no effect (default when tag added)
|
|
51
|
+
* - `2.0` = 2x preference boost
|
|
52
|
+
* - Higher values = stronger preference
|
|
53
|
+
*
|
|
54
|
+
* If multiple tags on a card have preferences, the maximum multiplier wins.
|
|
55
|
+
*/
|
|
56
|
+
export interface UserTagPreferenceState {
|
|
57
|
+
/**
|
|
58
|
+
* Tag-specific multipliers.
|
|
59
|
+
* Maps tag name to score multiplier (0 = exclude, 1 = neutral, >1 = boost).
|
|
60
|
+
*/
|
|
61
|
+
boost: Record<string, number>;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* ISO timestamp of last update.
|
|
65
|
+
* Use `moment.utc(updatedAt)` to parse into a Moment object.
|
|
66
|
+
*/
|
|
67
|
+
updatedAt: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* A filter that applies user-configured tag preferences.
|
|
72
|
+
*
|
|
73
|
+
* Reads preferences from STRATEGY_STATE document in user's database.
|
|
74
|
+
* If no preferences exist, passes through unchanged (no-op).
|
|
75
|
+
*
|
|
76
|
+
* Implements CardFilter for use in Pipeline architecture.
|
|
77
|
+
* Also extends ContentNavigator for compatibility with dynamic loading.
|
|
78
|
+
*/
|
|
79
|
+
export default class UserTagPreferenceFilter extends ContentNavigator implements CardFilter {
|
|
80
|
+
private _strategyData: ContentNavigationStrategyData;
|
|
81
|
+
|
|
82
|
+
/** Human-readable name for CardFilter interface */
|
|
83
|
+
name: string;
|
|
84
|
+
|
|
85
|
+
constructor(
|
|
86
|
+
user: UserDBInterface,
|
|
87
|
+
course: CourseDBInterface,
|
|
88
|
+
strategyData: ContentNavigationStrategyData
|
|
89
|
+
) {
|
|
90
|
+
super(user, course, strategyData);
|
|
91
|
+
this._strategyData = strategyData;
|
|
92
|
+
this.name = strategyData.name || 'User Tag Preferences';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Compute multiplier for a card based on its tags and user preferences.
|
|
97
|
+
* Returns the maximum multiplier among all matching tags, or 1.0 if no matches.
|
|
98
|
+
*/
|
|
99
|
+
private computeMultiplier(cardTags: string[], boostMap: Record<string, number>): number {
|
|
100
|
+
const multipliers = cardTags
|
|
101
|
+
.map((tag) => boostMap[tag])
|
|
102
|
+
.filter((val) => val !== undefined);
|
|
103
|
+
|
|
104
|
+
if (multipliers.length === 0) {
|
|
105
|
+
return 1.0;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Use max multiplier among matching tags
|
|
109
|
+
return Math.max(...multipliers);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Build human-readable reason for the filter's decision.
|
|
114
|
+
*/
|
|
115
|
+
private buildReason(
|
|
116
|
+
cardTags: string[],
|
|
117
|
+
boostMap: Record<string, number>,
|
|
118
|
+
multiplier: number
|
|
119
|
+
): string {
|
|
120
|
+
// Find which tag(s) contributed to the multiplier
|
|
121
|
+
const matchingTags = cardTags.filter((tag) => boostMap[tag] === multiplier);
|
|
122
|
+
|
|
123
|
+
if (multiplier === 0) {
|
|
124
|
+
return `Excluded by user preference: ${matchingTags.join(', ')} (${multiplier}x)`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (multiplier < 1.0) {
|
|
128
|
+
return `Penalized by user preference: ${matchingTags.join(', ')} (${multiplier.toFixed(2)}x)`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (multiplier > 1.0) {
|
|
132
|
+
return `Boosted by user preference: ${matchingTags.join(', ')} (${multiplier.toFixed(2)}x)`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return 'No matching user preferences';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* CardFilter.transform implementation.
|
|
140
|
+
*
|
|
141
|
+
* Apply user tag preferences:
|
|
142
|
+
* 1. Read preferences from strategy state
|
|
143
|
+
* 2. If no preferences, pass through unchanged
|
|
144
|
+
* 3. For each card:
|
|
145
|
+
* - Look up tag in boost record
|
|
146
|
+
* - If tag found: apply multiplier (0 = exclude, 1 = neutral, >1 = boost)
|
|
147
|
+
* - If multiple tags match: use max multiplier
|
|
148
|
+
* - Append provenance with clear reason
|
|
149
|
+
*/
|
|
150
|
+
async transform(cards: WeightedCard[], _context: FilterContext): Promise<WeightedCard[]> {
|
|
151
|
+
// Read user preferences from strategy state
|
|
152
|
+
const prefs = await this.getStrategyState<UserTagPreferenceState>();
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
// No preferences configured → pass through unchanged
|
|
156
|
+
if (!prefs || Object.keys(prefs.boost).length === 0) {
|
|
157
|
+
return cards.map((card) => ({
|
|
158
|
+
...card,
|
|
159
|
+
provenance: [
|
|
160
|
+
...card.provenance,
|
|
161
|
+
{
|
|
162
|
+
strategy: 'userTagPreference',
|
|
163
|
+
strategyName: this.strategyName || this.name,
|
|
164
|
+
strategyId: this.strategyId || this._strategyData._id,
|
|
165
|
+
action: 'passed' as const,
|
|
166
|
+
score: card.score,
|
|
167
|
+
reason: 'No user tag preferences configured',
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
}));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Process each card
|
|
174
|
+
const adjusted: WeightedCard[] = await Promise.all(
|
|
175
|
+
cards.map(async (card) => {
|
|
176
|
+
const cardTags = card.tags ?? [];
|
|
177
|
+
|
|
178
|
+
// Compute multiplier based on card tags and user preferences
|
|
179
|
+
const multiplier = this.computeMultiplier(cardTags, prefs.boost);
|
|
180
|
+
const finalScore = Math.min(1, card.score * multiplier);
|
|
181
|
+
|
|
182
|
+
// Determine action for provenance
|
|
183
|
+
let action: 'passed' | 'boosted' | 'penalized';
|
|
184
|
+
if (multiplier === 0 || multiplier < 1.0) {
|
|
185
|
+
action = 'penalized';
|
|
186
|
+
} else if (multiplier > 1.0) {
|
|
187
|
+
action = 'boosted';
|
|
188
|
+
} else {
|
|
189
|
+
action = 'passed';
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
...card,
|
|
194
|
+
score: finalScore,
|
|
195
|
+
provenance: [
|
|
196
|
+
...card.provenance,
|
|
197
|
+
{
|
|
198
|
+
strategy: 'userTagPreference',
|
|
199
|
+
strategyName: this.strategyName || this.name,
|
|
200
|
+
strategyId: this.strategyId || this._strategyData._id,
|
|
201
|
+
action,
|
|
202
|
+
score: finalScore,
|
|
203
|
+
reason: this.buildReason(cardTags, prefs.boost, multiplier),
|
|
204
|
+
},
|
|
205
|
+
],
|
|
206
|
+
};
|
|
207
|
+
})
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
return adjusted;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Legacy getWeightedCards - throws as filters should not be used as generators.
|
|
215
|
+
*/
|
|
216
|
+
async getWeightedCards(_limit: number): Promise<WeightedCard[]> {
|
|
217
|
+
throw new Error(
|
|
218
|
+
'UserTagPreferenceFilter is a filter and should not be used as a generator. ' +
|
|
219
|
+
'Use Pipeline with a generator and this filter via transform().'
|
|
220
|
+
);
|
|
221
|
+
}
|
|
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
|
+
}
|
|
@@ -158,14 +158,14 @@ export default class HierarchyDefinitionNavigator extends ContentNavigator imple
|
|
|
158
158
|
* Check if a card is unlocked and generate reason.
|
|
159
159
|
*/
|
|
160
160
|
private async checkCardUnlock(
|
|
161
|
-
|
|
161
|
+
card: WeightedCard,
|
|
162
162
|
course: CourseDBInterface,
|
|
163
163
|
unlockedTags: Set<string>,
|
|
164
164
|
masteredTags: Set<string>
|
|
165
165
|
): Promise<{ isUnlocked: boolean; reason: string }> {
|
|
166
166
|
try {
|
|
167
|
-
|
|
168
|
-
const cardTags =
|
|
167
|
+
// Pipeline hydrates tags before filters run
|
|
168
|
+
const cardTags = card.tags ?? [];
|
|
169
169
|
|
|
170
170
|
// Check each tag's prerequisite status
|
|
171
171
|
const lockedTags = cardTags.filter(
|
|
@@ -214,7 +214,7 @@ export default class HierarchyDefinitionNavigator extends ContentNavigator imple
|
|
|
214
214
|
|
|
215
215
|
for (const card of cards) {
|
|
216
216
|
const { isUnlocked, reason } = await this.checkCardUnlock(
|
|
217
|
-
card
|
|
217
|
+
card,
|
|
218
218
|
context.course,
|
|
219
219
|
unlockedTags,
|
|
220
220
|
masteredTags
|
|
@@ -137,6 +137,11 @@ export interface WeightedCard {
|
|
|
137
137
|
* First entry is from the generator, subsequent entries from filters.
|
|
138
138
|
*/
|
|
139
139
|
provenance: StrategyContribution[];
|
|
140
|
+
/**
|
|
141
|
+
* Pre-fetched tags. Populated by Pipeline before filters run.
|
|
142
|
+
* Filters should use this instead of querying getAppliedTags() individually.
|
|
143
|
+
*/
|
|
144
|
+
tags?: string[];
|
|
140
145
|
}
|
|
141
146
|
|
|
142
147
|
/**
|
|
@@ -173,6 +178,7 @@ export enum Navigators {
|
|
|
173
178
|
HIERARCHY = 'hierarchyDefinition',
|
|
174
179
|
INTERFERENCE = 'interferenceMitigator',
|
|
175
180
|
RELATIVE_PRIORITY = 'relativePriority',
|
|
181
|
+
USER_TAG_PREFERENCE = 'userTagPreference',
|
|
176
182
|
}
|
|
177
183
|
|
|
178
184
|
// ============================================================================
|
|
@@ -211,6 +217,7 @@ export const NavigatorRoles: Record<Navigators, NavigatorRole> = {
|
|
|
211
217
|
[Navigators.HIERARCHY]: NavigatorRole.FILTER,
|
|
212
218
|
[Navigators.INTERFERENCE]: NavigatorRole.FILTER,
|
|
213
219
|
[Navigators.RELATIVE_PRIORITY]: NavigatorRole.FILTER,
|
|
220
|
+
[Navigators.USER_TAG_PREFERENCE]: NavigatorRole.FILTER,
|
|
214
221
|
};
|
|
215
222
|
|
|
216
223
|
/**
|
|
@@ -275,6 +282,58 @@ export abstract class ContentNavigator implements StudyContentSource {
|
|
|
275
282
|
}
|
|
276
283
|
}
|
|
277
284
|
|
|
285
|
+
// ============================================================================
|
|
286
|
+
// STRATEGY STATE HELPERS
|
|
287
|
+
// ============================================================================
|
|
288
|
+
//
|
|
289
|
+
// These methods allow strategies to persist their own state (user preferences,
|
|
290
|
+
// learned patterns, temporal tracking) in the user database.
|
|
291
|
+
//
|
|
292
|
+
// ============================================================================
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Unique key identifying this strategy for state storage.
|
|
296
|
+
*
|
|
297
|
+
* Defaults to the constructor name (e.g., "UserTagPreferenceFilter").
|
|
298
|
+
* Override in subclasses if multiple instances of the same strategy type
|
|
299
|
+
* need separate state storage.
|
|
300
|
+
*/
|
|
301
|
+
protected get strategyKey(): string {
|
|
302
|
+
return this.constructor.name;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Get this strategy's persisted state for the current course.
|
|
307
|
+
*
|
|
308
|
+
* @returns The strategy's data payload, or null if no state exists
|
|
309
|
+
* @throws Error if user or course is not initialized
|
|
310
|
+
*/
|
|
311
|
+
protected async getStrategyState<T>(): Promise<T | null> {
|
|
312
|
+
if (!this.user || !this.course) {
|
|
313
|
+
throw new Error(
|
|
314
|
+
`Cannot get strategy state: navigator not properly initialized. ` +
|
|
315
|
+
`Ensure user and course are provided to constructor.`
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
return this.user.getStrategyState<T>(this.course.getCourseID(), this.strategyKey);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Persist this strategy's state for the current course.
|
|
323
|
+
*
|
|
324
|
+
* @param data - The strategy's data payload to store
|
|
325
|
+
* @throws Error if user or course is not initialized
|
|
326
|
+
*/
|
|
327
|
+
protected async putStrategyState<T>(data: T): Promise<void> {
|
|
328
|
+
if (!this.user || !this.course) {
|
|
329
|
+
throw new Error(
|
|
330
|
+
`Cannot put strategy state: navigator not properly initialized. ` +
|
|
331
|
+
`Ensure user and course are provided to constructor.`
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
return this.user.putStrategyState<T>(this.course.getCourseID(), this.strategyKey, data);
|
|
335
|
+
}
|
|
336
|
+
|
|
278
337
|
/**
|
|
279
338
|
* Factory method to create navigator instances dynamically.
|
|
280
339
|
*
|
|
@@ -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
|
+
}
|
|
@@ -234,18 +234,6 @@ export default class InterferenceMitigatorNavigator extends ContentNavigator imp
|
|
|
234
234
|
return avoid;
|
|
235
235
|
}
|
|
236
236
|
|
|
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
237
|
/**
|
|
250
238
|
* Compute interference score reduction for a card.
|
|
251
239
|
* Returns: { multiplier, interfering tags, reason }
|
|
@@ -313,7 +301,7 @@ export default class InterferenceMitigatorNavigator extends ContentNavigator imp
|
|
|
313
301
|
const adjusted: WeightedCard[] = [];
|
|
314
302
|
|
|
315
303
|
for (const card of cards) {
|
|
316
|
-
const cardTags =
|
|
304
|
+
const cardTags = card.tags ?? [];
|
|
317
305
|
const { multiplier, reason } = this.computeInterferenceEffect(
|
|
318
306
|
cardTags,
|
|
319
307
|
tagsToAvoid,
|
|
@@ -190,28 +190,16 @@ export default class RelativePriorityNavigator extends ContentNavigator implemen
|
|
|
190
190
|
}
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
-
/**
|
|
194
|
-
* Get tags for a single card.
|
|
195
|
-
*/
|
|
196
|
-
private async getCardTags(cardId: string, course: CourseDBInterface): Promise<string[]> {
|
|
197
|
-
try {
|
|
198
|
-
const tagResponse = await course.getAppliedTags(cardId);
|
|
199
|
-
return tagResponse.rows.map((r) => r.doc?.name).filter((x): x is string => !!x);
|
|
200
|
-
} catch {
|
|
201
|
-
return [];
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
193
|
/**
|
|
206
194
|
* CardFilter.transform implementation.
|
|
207
195
|
*
|
|
208
196
|
* Apply priority-adjusted scoring. Cards with high-priority tags get boosted,
|
|
209
197
|
* cards with low-priority tags get reduced scores.
|
|
210
198
|
*/
|
|
211
|
-
async transform(cards: WeightedCard[],
|
|
199
|
+
async transform(cards: WeightedCard[], _context: FilterContext): Promise<WeightedCard[]> {
|
|
212
200
|
const adjusted: WeightedCard[] = await Promise.all(
|
|
213
201
|
cards.map(async (card) => {
|
|
214
|
-
const cardTags =
|
|
202
|
+
const cardTags = card.tags ?? [];
|
|
215
203
|
const priority = this.computeCardPriority(cardTags);
|
|
216
204
|
const boostFactor = this.computeBoostFactor(priority);
|
|
217
205
|
const finalScore = Math.max(0, Math.min(1, card.score * boostFactor));
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// USER GOAL NAVIGATOR — STUB
|
|
3
|
+
// ============================================================================
|
|
4
|
+
//
|
|
5
|
+
// STATUS: NOT IMPLEMENTED — This file documents architectural intent only.
|
|
6
|
+
//
|
|
7
|
+
// ============================================================================
|
|
8
|
+
//
|
|
9
|
+
// ## Purpose
|
|
10
|
+
//
|
|
11
|
+
// Goals define WHAT the user wants to learn, as opposed to preferences which
|
|
12
|
+
// define HOW they want to learn. Goals affect:
|
|
13
|
+
//
|
|
14
|
+
// 1. **Content scoping**: Which tags/content are relevant to this user
|
|
15
|
+
// 2. **Progress tracking**: ELO is measured against goal-relevant content
|
|
16
|
+
// 3. **Completion criteria**: User is "done" when goal mastery is achieved
|
|
17
|
+
// 4. **Curriculum composition**: Goals enable cross-curriculum dependencies
|
|
18
|
+
//
|
|
19
|
+
// ## Goals vs Preferences
|
|
20
|
+
//
|
|
21
|
+
// | Aspect | Goal | Preference |
|
|
22
|
+
// |---------------|-------------------------------|-------------------------------|
|
|
23
|
+
// | Defines | Destination (what to learn) | Path (how to learn) |
|
|
24
|
+
// | Example | "Master ear-training" | "Skip text-heavy cards" |
|
|
25
|
+
// | Affects ELO | Yes — scopes what's tracked | No — just filters cards |
|
|
26
|
+
// | Completion | Yes — defines "done" | No — persists indefinitely |
|
|
27
|
+
// | Filter impl | UserGoalNavigator | UserTagPreferenceFilter |
|
|
28
|
+
//
|
|
29
|
+
// ## Curriculum Composition
|
|
30
|
+
//
|
|
31
|
+
// Goals enable software-style composition for curricula. A physics course
|
|
32
|
+
// can teach classical mechanics without owning the calculus prerequisites.
|
|
33
|
+
//
|
|
34
|
+
// Instead, it declares a dependency:
|
|
35
|
+
//
|
|
36
|
+
// ```typescript
|
|
37
|
+
// interface CurriculumDependency {
|
|
38
|
+
// // NPM-style package resolution
|
|
39
|
+
// curriculumId: string; // e.g., "@skuilder/calculus"
|
|
40
|
+
// version: string; // e.g., "^2.0.0" (semver)
|
|
41
|
+
//
|
|
42
|
+
// // Goal within that curriculum
|
|
43
|
+
// goal: string; // e.g., "differential-calculus"
|
|
44
|
+
//
|
|
45
|
+
// // How this maps to local prerequisites
|
|
46
|
+
// satisfiesLocalTags: string[]; // e.g., ["calculus-prereq"]
|
|
47
|
+
// }
|
|
48
|
+
// ```
|
|
49
|
+
//
|
|
50
|
+
// When a physics card requires "calculus-prereq", the system:
|
|
51
|
+
// 1. Checks if user has achieved the "differential-calculus" goal in @skuilder/calculus
|
|
52
|
+
// 2. If not, defers to that curriculum to teach the prerequisite
|
|
53
|
+
// 3. Returns to physics once the goal is satisfied
|
|
54
|
+
//
|
|
55
|
+
// This allows:
|
|
56
|
+
// - Specialized curricula (calculus experts author calculus content)
|
|
57
|
+
// - Reusable prerequisites across multiple courses
|
|
58
|
+
// - User can bring their own "calculus credential" from prior learning
|
|
59
|
+
//
|
|
60
|
+
// ## User Goal State (Proposed)
|
|
61
|
+
//
|
|
62
|
+
// ```typescript
|
|
63
|
+
// interface UserGoalState {
|
|
64
|
+
// // Primary goals — what the user wants to achieve
|
|
65
|
+
// targetTags: string[];
|
|
66
|
+
//
|
|
67
|
+
// // Excluded goals — content the user explicitly doesn't care about
|
|
68
|
+
// excludedTags: string[];
|
|
69
|
+
//
|
|
70
|
+
// // Cross-curriculum goals (for composition)
|
|
71
|
+
// externalGoals?: {
|
|
72
|
+
// curriculumId: string;
|
|
73
|
+
// goal: string;
|
|
74
|
+
// status: 'not-started' | 'in-progress' | 'achieved';
|
|
75
|
+
// }[];
|
|
76
|
+
//
|
|
77
|
+
// // When this goal configuration was set
|
|
78
|
+
// updatedAt: string;
|
|
79
|
+
// }
|
|
80
|
+
// ```
|
|
81
|
+
//
|
|
82
|
+
// ## Implementation Considerations
|
|
83
|
+
//
|
|
84
|
+
// 1. **ELO Scoping**: When goals are set, user ELO tracking should focus on
|
|
85
|
+
// goal-relevant tags. This may require changes to ELO update logic.
|
|
86
|
+
//
|
|
87
|
+
// 2. **Progress Reporting**: UI should show progress toward goals, not just
|
|
88
|
+
// overall course completion.
|
|
89
|
+
//
|
|
90
|
+
// 3. **Goal Achievement**: Need to define when a goal is "achieved" —
|
|
91
|
+
// probably ELO threshold + mastery percentage on goal-tagged content.
|
|
92
|
+
//
|
|
93
|
+
// 4. **Curriculum Registry**: For cross-curriculum composition, need a
|
|
94
|
+
// registry/resolver for curriculum packages (similar to npm registry).
|
|
95
|
+
//
|
|
96
|
+
// 5. **Interaction with HierarchyDefinition**: Goals should work with
|
|
97
|
+
// prerequisite chains — user can't skip prerequisites just because
|
|
98
|
+
// they're not part of their goal.
|
|
99
|
+
//
|
|
100
|
+
// ## Related Files
|
|
101
|
+
//
|
|
102
|
+
// - `filters/userTagPreference.ts` — Preferences (path constraints)
|
|
103
|
+
// - `hierarchyDefinition.ts` — Prerequisites (enforced regardless of goals)
|
|
104
|
+
// - `../types/strategyState.ts` — Storage mechanism for user state
|
|
105
|
+
//
|
|
106
|
+
// ## Next Steps
|
|
107
|
+
//
|
|
108
|
+
// 1. Design goal state schema in detail
|
|
109
|
+
// 2. Define goal achievement criteria
|
|
110
|
+
// 3. Implement goal-scoped ELO tracking
|
|
111
|
+
// 4. Build UI for goal configuration
|
|
112
|
+
// 5. Design curriculum dependency resolution
|
|
113
|
+
//
|
|
114
|
+
// ============================================================================
|
|
115
|
+
|
|
116
|
+
// Placeholder export to make this a valid module
|
|
117
|
+
export const USER_GOAL_NAVIGATOR_STUB = true;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* @stub UserGoalNavigator
|
|
121
|
+
*
|
|
122
|
+
* A navigator that scopes learning to user-defined goals.
|
|
123
|
+
* See module-level documentation for architectural intent.
|
|
124
|
+
*
|
|
125
|
+
* NOT IMPLEMENTED — This is a design placeholder.
|
|
126
|
+
*/
|
|
127
|
+
export interface UserGoalState {
|
|
128
|
+
/** Tags the user wants to master (defines "success") */
|
|
129
|
+
targetTags: string[];
|
|
130
|
+
|
|
131
|
+
/** Tags the user explicitly doesn't care about */
|
|
132
|
+
excludedTags: string[];
|
|
133
|
+
|
|
134
|
+
/** ISO timestamp of last update */
|
|
135
|
+
updatedAt: string;
|
|
136
|
+
}
|