@vue-skuilder/db 0.1.17 → 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/{userDB-BqwxtJ_7.d.mts → classroomDB-CZdMBiTU.d.ts} +427 -104
- package/dist/{userDB-DNa0XPtn.d.ts → classroomDB-PxDZTky3.d.cts} +427 -104
- package/dist/core/index.d.cts +304 -0
- package/dist/core/index.d.ts +237 -25
- package/dist/core/index.js +2246 -118
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +2235 -114
- package/dist/core/index.mjs.map +1 -1
- package/dist/{dataLayerProvider-VlngD19_.d.mts → dataLayerProvider-D0MoZMjH.d.cts} +1 -1
- package/dist/{dataLayerProvider-BV5iZqt_.d.ts → dataLayerProvider-D8o6ZnKW.d.ts} +1 -1
- package/dist/impl/couch/{index.d.mts → index.d.cts} +47 -5
- package/dist/impl/couch/index.d.ts +46 -4
- package/dist/impl/couch/index.js +2250 -134
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +2212 -97
- package/dist/impl/couch/index.mjs.map +1 -1
- package/dist/impl/static/{index.d.mts → index.d.cts} +6 -6
- package/dist/impl/static/index.d.ts +5 -5
- package/dist/impl/static/index.js +1950 -143
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +1922 -117
- package/dist/impl/static/index.mjs.map +1 -1
- package/dist/{index-Bmll7Xse.d.mts → index-B_j6u5E4.d.cts} +1 -1
- package/dist/{index-CD8BZz2k.d.ts → index-Dj0SEgk3.d.ts} +1 -1
- package/dist/{index.d.mts → index.d.cts} +97 -13
- package/dist/index.d.ts +96 -12
- package/dist/index.js +2439 -180
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2386 -135
- package/dist/index.mjs.map +1 -1
- package/dist/pouch/index.js +3 -3
- package/dist/{types-Dbp5DaRR.d.mts → types-Bn0itutr.d.cts} +1 -1
- package/dist/{types-CewsN87z.d.ts → types-DQaXnuoc.d.ts} +1 -1
- package/dist/{types-legacy-6ettoclI.d.ts → types-legacy-DDY4N-Uq.d.cts} +3 -1
- package/dist/{types-legacy-6ettoclI.d.mts → types-legacy-DDY4N-Uq.d.ts} +3 -1
- package/dist/util/packer/{index.d.mts → index.d.cts} +3 -3
- package/dist/util/packer/index.d.ts +3 -3
- package/dist/util/packer/index.js.map +1 -1
- package/dist/util/packer/index.mjs.map +1 -1
- package/docs/brainstorm-navigation-paradigm.md +369 -0
- package/docs/navigators-architecture.md +370 -0
- package/docs/todo-evolutionary-orchestration.md +310 -0
- package/docs/todo-nominal-tag-types.md +121 -0
- package/docs/todo-strategy-authoring.md +401 -0
- package/eslint.config.mjs +1 -1
- package/package.json +9 -4
- package/src/core/index.ts +1 -0
- package/src/core/interfaces/contentSource.ts +88 -4
- package/src/core/interfaces/courseDB.ts +13 -0
- package/src/core/interfaces/navigationStrategyManager.ts +0 -5
- package/src/core/interfaces/userDB.ts +32 -0
- package/src/core/navigators/CompositeGenerator.ts +268 -0
- package/src/core/navigators/Pipeline.ts +318 -0
- package/src/core/navigators/PipelineAssembler.ts +194 -0
- package/src/core/navigators/elo.ts +104 -15
- package/src/core/navigators/filters/eloDistance.ts +132 -0
- package/src/core/navigators/filters/index.ts +9 -0
- package/src/core/navigators/filters/types.ts +115 -0
- package/src/core/navigators/filters/userTagPreference.ts +232 -0
- package/src/core/navigators/generators/index.ts +2 -0
- package/src/core/navigators/generators/types.ts +107 -0
- package/src/core/navigators/hardcodedOrder.ts +111 -12
- package/src/core/navigators/hierarchyDefinition.ts +266 -0
- package/src/core/navigators/index.ts +404 -3
- package/src/core/navigators/inferredPreference.ts +107 -0
- package/src/core/navigators/interferenceMitigator.ts +355 -0
- package/src/core/navigators/relativePriority.ts +255 -0
- package/src/core/navigators/srs.ts +195 -0
- 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/classroomDB.ts +51 -0
- package/src/impl/couch/courseDB.ts +147 -49
- package/src/impl/static/courseDB.ts +11 -4
- package/src/study/SessionController.ts +149 -1
- package/src/study/TagFilteredContentSource.ts +255 -0
- package/src/study/index.ts +1 -0
- package/src/util/dataDirectory.test.ts +51 -22
- package/src/util/logger.ts +0 -1
- package/tests/core/navigators/CompositeGenerator.test.ts +455 -0
- package/tests/core/navigators/Pipeline.test.ts +406 -0
- package/tests/core/navigators/PipelineAssembler.test.ts +351 -0
- package/tests/core/navigators/SRSNavigator.test.ts +344 -0
- package/tests/core/navigators/eloDistanceFilter.test.ts +192 -0
- package/tests/core/navigators/navigators.test.ts +710 -0
- package/tsconfig.json +1 -1
- package/vitest.config.ts +29 -0
- package/dist/core/index.d.mts +0 -92
- /package/dist/{SyncStrategy-CyATpyLQ.d.mts → SyncStrategy-CyATpyLQ.d.cts} +0 -0
- /package/dist/pouch/{index.d.mts → index.d.cts} +0 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,355 @@
|
|
|
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 './filters/types';
|
|
9
|
+
import { toCourseElo } from '@vue-skuilder/common';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Configuration for the InterferenceMitigator strategy.
|
|
13
|
+
*
|
|
14
|
+
* Course authors define explicit interference relationships between tags.
|
|
15
|
+
* The mitigator discourages introducing new concepts that interfere with
|
|
16
|
+
* currently immature learnings.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* A single interference group with its own decay coefficient.
|
|
20
|
+
*/
|
|
21
|
+
export interface InterferenceGroup {
|
|
22
|
+
/** Tags that interfere with each other in this group */
|
|
23
|
+
tags: string[];
|
|
24
|
+
/** How strongly these tags interfere (0-1, default: 0.8). Higher = stronger avoidance. */
|
|
25
|
+
decay?: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface InterferenceConfig {
|
|
29
|
+
/**
|
|
30
|
+
* Groups of tags that interfere with each other.
|
|
31
|
+
* Each group can have its own decay coefficient.
|
|
32
|
+
*
|
|
33
|
+
* Example: [
|
|
34
|
+
* { tags: ["b", "d", "p"], decay: 0.9 }, // visual similarity - strong
|
|
35
|
+
* { tags: ["d", "t"], decay: 0.7 }, // phonetic similarity - moderate
|
|
36
|
+
* { tags: ["m", "n"], decay: 0.8 }
|
|
37
|
+
* ]
|
|
38
|
+
*/
|
|
39
|
+
interferenceSets: InterferenceGroup[];
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Threshold below which a tag is considered "immature" (still being learned).
|
|
43
|
+
* Immature tags trigger interference avoidance for their interference partners.
|
|
44
|
+
*/
|
|
45
|
+
maturityThreshold?: {
|
|
46
|
+
/** Minimum interaction count to be considered mature (default: 10) */
|
|
47
|
+
minCount?: number;
|
|
48
|
+
/** Minimum ELO score to be considered mature (optional) */
|
|
49
|
+
minElo?: number;
|
|
50
|
+
/**
|
|
51
|
+
* Minimum elapsed time (in days) since first interaction to be considered mature.
|
|
52
|
+
* Prevents recent cramming success from indicating maturity.
|
|
53
|
+
* The skill should be "lindy" — maintained over time.
|
|
54
|
+
*/
|
|
55
|
+
minElapsedDays?: number;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Default decay for groups that don't specify their own (0-1, default: 0.8).
|
|
60
|
+
*/
|
|
61
|
+
defaultDecay?: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const DEFAULT_MIN_COUNT = 10;
|
|
65
|
+
const DEFAULT_MIN_ELAPSED_DAYS = 3;
|
|
66
|
+
const DEFAULT_INTERFERENCE_DECAY = 0.8;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* A filter strategy that avoids introducing confusable concepts simultaneously.
|
|
70
|
+
*
|
|
71
|
+
* When a user is learning a concept (tag is "immature"), this strategy reduces
|
|
72
|
+
* scores for cards tagged with interfering concepts. This encourages the system
|
|
73
|
+
* to introduce new content that is maximally distant from current learning focus.
|
|
74
|
+
*
|
|
75
|
+
* Example: While learning 'd', prefer introducing 'x' over 'b' (visual interference)
|
|
76
|
+
* or 't' (phonetic interference).
|
|
77
|
+
*
|
|
78
|
+
* Implements CardFilter for use in Pipeline architecture.
|
|
79
|
+
* Also extends ContentNavigator for backward compatibility.
|
|
80
|
+
*/
|
|
81
|
+
export default class InterferenceMitigatorNavigator extends ContentNavigator implements CardFilter {
|
|
82
|
+
private config: InterferenceConfig;
|
|
83
|
+
private _strategyData: ContentNavigationStrategyData;
|
|
84
|
+
|
|
85
|
+
/** Human-readable name for CardFilter interface */
|
|
86
|
+
name: string;
|
|
87
|
+
|
|
88
|
+
/** Precomputed map: tag -> set of { partner, decay } it interferes with */
|
|
89
|
+
private interferenceMap: Map<string, Array<{ partner: string; decay: number }>>;
|
|
90
|
+
|
|
91
|
+
constructor(
|
|
92
|
+
user: UserDBInterface,
|
|
93
|
+
course: CourseDBInterface,
|
|
94
|
+
_strategyData: ContentNavigationStrategyData
|
|
95
|
+
) {
|
|
96
|
+
super(user, course, _strategyData);
|
|
97
|
+
this._strategyData = _strategyData;
|
|
98
|
+
this.config = this.parseConfig(_strategyData.serializedData);
|
|
99
|
+
this.interferenceMap = this.buildInterferenceMap();
|
|
100
|
+
this.name = _strategyData.name || 'Interference Mitigator';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private parseConfig(serializedData: string): InterferenceConfig {
|
|
104
|
+
try {
|
|
105
|
+
const parsed = JSON.parse(serializedData);
|
|
106
|
+
// Normalize legacy format (string[][]) to new format (InterferenceGroup[])
|
|
107
|
+
let sets: InterferenceGroup[] = parsed.interferenceSets || [];
|
|
108
|
+
if (sets.length > 0 && Array.isArray(sets[0])) {
|
|
109
|
+
// Legacy format: convert string[][] to InterferenceGroup[]
|
|
110
|
+
sets = (sets as unknown as string[][]).map((tags) => ({ tags }));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
interferenceSets: sets,
|
|
115
|
+
maturityThreshold: {
|
|
116
|
+
minCount: parsed.maturityThreshold?.minCount ?? DEFAULT_MIN_COUNT,
|
|
117
|
+
minElo: parsed.maturityThreshold?.minElo,
|
|
118
|
+
minElapsedDays: parsed.maturityThreshold?.minElapsedDays ?? DEFAULT_MIN_ELAPSED_DAYS,
|
|
119
|
+
},
|
|
120
|
+
defaultDecay: parsed.defaultDecay ?? DEFAULT_INTERFERENCE_DECAY,
|
|
121
|
+
};
|
|
122
|
+
} catch {
|
|
123
|
+
return {
|
|
124
|
+
interferenceSets: [],
|
|
125
|
+
maturityThreshold: {
|
|
126
|
+
minCount: DEFAULT_MIN_COUNT,
|
|
127
|
+
minElapsedDays: DEFAULT_MIN_ELAPSED_DAYS,
|
|
128
|
+
},
|
|
129
|
+
defaultDecay: DEFAULT_INTERFERENCE_DECAY,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Build a map from each tag to its interference partners with decay coefficients.
|
|
136
|
+
* If tags A, B, C are in an interference group with decay 0.8, then:
|
|
137
|
+
* - A interferes with B (decay 0.8) and C (decay 0.8)
|
|
138
|
+
* - B interferes with A (decay 0.8) and C (decay 0.8)
|
|
139
|
+
* - etc.
|
|
140
|
+
*/
|
|
141
|
+
private buildInterferenceMap(): Map<string, Array<{ partner: string; decay: number }>> {
|
|
142
|
+
const map = new Map<string, Array<{ partner: string; decay: number }>>();
|
|
143
|
+
|
|
144
|
+
for (const group of this.config.interferenceSets) {
|
|
145
|
+
const decay = group.decay ?? this.config.defaultDecay ?? DEFAULT_INTERFERENCE_DECAY;
|
|
146
|
+
|
|
147
|
+
for (const tag of group.tags) {
|
|
148
|
+
if (!map.has(tag)) {
|
|
149
|
+
map.set(tag, []);
|
|
150
|
+
}
|
|
151
|
+
const partners = map.get(tag)!;
|
|
152
|
+
for (const other of group.tags) {
|
|
153
|
+
if (other !== tag) {
|
|
154
|
+
// Check if partner already exists (from overlapping groups)
|
|
155
|
+
const existing = partners.find((p) => p.partner === other);
|
|
156
|
+
if (existing) {
|
|
157
|
+
// Use the stronger (higher) decay
|
|
158
|
+
existing.decay = Math.max(existing.decay, decay);
|
|
159
|
+
} else {
|
|
160
|
+
partners.push({ partner: other, decay });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return map;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Get the set of tags that are currently immature for this user.
|
|
172
|
+
* A tag is immature if the user has interacted with it but hasn't
|
|
173
|
+
* reached the maturity threshold.
|
|
174
|
+
*/
|
|
175
|
+
private async getImmatureTags(context: FilterContext): Promise<Set<string>> {
|
|
176
|
+
const immature = new Set<string>();
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
const courseReg = await context.user.getCourseRegDoc(context.course.getCourseID());
|
|
180
|
+
const userElo = toCourseElo(courseReg.elo);
|
|
181
|
+
|
|
182
|
+
const minCount = this.config.maturityThreshold?.minCount ?? DEFAULT_MIN_COUNT;
|
|
183
|
+
const minElo = this.config.maturityThreshold?.minElo;
|
|
184
|
+
|
|
185
|
+
// TODO: To properly check elapsed time, we need access to first interaction timestamp.
|
|
186
|
+
// For now, we use count as a proxy (more interactions = more time elapsed).
|
|
187
|
+
// Future: query card history for earliest timestamp per tag.
|
|
188
|
+
const minElapsedDays =
|
|
189
|
+
this.config.maturityThreshold?.minElapsedDays ?? DEFAULT_MIN_ELAPSED_DAYS;
|
|
190
|
+
const minCountForElapsed = minElapsedDays * 2; // Rough proxy: ~2 interactions per day
|
|
191
|
+
|
|
192
|
+
for (const [tagId, tagElo] of Object.entries(userElo.tags)) {
|
|
193
|
+
// Only consider tags that have been started (count > 0)
|
|
194
|
+
if (tagElo.count === 0) continue;
|
|
195
|
+
|
|
196
|
+
// Check if below maturity threshold
|
|
197
|
+
const belowCount = tagElo.count < minCount;
|
|
198
|
+
const belowElo = minElo !== undefined && tagElo.score < minElo;
|
|
199
|
+
const belowElapsed = tagElo.count < minCountForElapsed; // Proxy for time
|
|
200
|
+
|
|
201
|
+
if (belowCount || belowElo || belowElapsed) {
|
|
202
|
+
immature.add(tagId);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
} catch {
|
|
206
|
+
// If we can't get user data, assume no immature tags
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return immature;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Get all tags that interfere with any immature tag, along with their decay coefficients.
|
|
214
|
+
* These are the tags we want to avoid introducing.
|
|
215
|
+
*/
|
|
216
|
+
private getTagsToAvoid(immatureTags: Set<string>): Map<string, number> {
|
|
217
|
+
const avoid = new Map<string, number>();
|
|
218
|
+
|
|
219
|
+
for (const immatureTag of immatureTags) {
|
|
220
|
+
const partners = this.interferenceMap.get(immatureTag);
|
|
221
|
+
if (partners) {
|
|
222
|
+
for (const { partner, decay } of partners) {
|
|
223
|
+
// Avoid the partner, but not if it's also immature
|
|
224
|
+
// (if both are immature, we're already learning both)
|
|
225
|
+
if (!immatureTags.has(partner)) {
|
|
226
|
+
// Use the strongest (highest) decay if partner appears multiple times
|
|
227
|
+
const existing = avoid.get(partner) ?? 0;
|
|
228
|
+
avoid.set(partner, Math.max(existing, decay));
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return avoid;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Compute interference score reduction for a card.
|
|
239
|
+
* Returns: { multiplier, interfering tags, reason }
|
|
240
|
+
*/
|
|
241
|
+
private computeInterferenceEffect(
|
|
242
|
+
cardTags: string[],
|
|
243
|
+
tagsToAvoid: Map<string, number>,
|
|
244
|
+
immatureTags: Set<string>
|
|
245
|
+
): { multiplier: number; interferingTags: string[]; reason: string } {
|
|
246
|
+
if (tagsToAvoid.size === 0) {
|
|
247
|
+
return {
|
|
248
|
+
multiplier: 1.0,
|
|
249
|
+
interferingTags: [],
|
|
250
|
+
reason: 'No interference detected',
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
let multiplier = 1.0;
|
|
255
|
+
const interferingTags: string[] = [];
|
|
256
|
+
|
|
257
|
+
for (const tag of cardTags) {
|
|
258
|
+
const decay = tagsToAvoid.get(tag);
|
|
259
|
+
if (decay !== undefined) {
|
|
260
|
+
interferingTags.push(tag);
|
|
261
|
+
multiplier *= 1.0 - decay;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (interferingTags.length === 0) {
|
|
266
|
+
return {
|
|
267
|
+
multiplier: 1.0,
|
|
268
|
+
interferingTags: [],
|
|
269
|
+
reason: 'No interference detected',
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Find which immature tags these interfere with
|
|
274
|
+
const causingTags = new Set<string>();
|
|
275
|
+
for (const tag of interferingTags) {
|
|
276
|
+
for (const immatureTag of immatureTags) {
|
|
277
|
+
const partners = this.interferenceMap.get(immatureTag);
|
|
278
|
+
if (partners?.some((p) => p.partner === tag)) {
|
|
279
|
+
causingTags.add(immatureTag);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const reason = `Interferes with immature tags ${Array.from(causingTags).join(', ')} (tags: ${interferingTags.join(', ')}, multiplier: ${multiplier.toFixed(2)})`;
|
|
285
|
+
|
|
286
|
+
return { multiplier, interferingTags, reason };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* CardFilter.transform implementation.
|
|
291
|
+
*
|
|
292
|
+
* Apply interference-aware scoring. Cards with tags that interfere with
|
|
293
|
+
* immature learnings get reduced scores.
|
|
294
|
+
*/
|
|
295
|
+
async transform(cards: WeightedCard[], context: FilterContext): Promise<WeightedCard[]> {
|
|
296
|
+
// Identify what to avoid
|
|
297
|
+
const immatureTags = await this.getImmatureTags(context);
|
|
298
|
+
const tagsToAvoid = this.getTagsToAvoid(immatureTags);
|
|
299
|
+
|
|
300
|
+
// Adjust scores based on interference
|
|
301
|
+
const adjusted: WeightedCard[] = [];
|
|
302
|
+
|
|
303
|
+
for (const card of cards) {
|
|
304
|
+
const cardTags = card.tags ?? [];
|
|
305
|
+
const { multiplier, reason } = this.computeInterferenceEffect(
|
|
306
|
+
cardTags,
|
|
307
|
+
tagsToAvoid,
|
|
308
|
+
immatureTags
|
|
309
|
+
);
|
|
310
|
+
const finalScore = card.score * multiplier;
|
|
311
|
+
|
|
312
|
+
const action = multiplier < 1.0 ? 'penalized' : multiplier > 1.0 ? 'boosted' : 'passed';
|
|
313
|
+
|
|
314
|
+
adjusted.push({
|
|
315
|
+
...card,
|
|
316
|
+
score: finalScore,
|
|
317
|
+
provenance: [
|
|
318
|
+
...card.provenance,
|
|
319
|
+
{
|
|
320
|
+
strategy: 'interferenceMitigator',
|
|
321
|
+
strategyName: this.strategyName || this.name,
|
|
322
|
+
strategyId: this.strategyId || 'NAVIGATION_STRATEGY-interference',
|
|
323
|
+
action,
|
|
324
|
+
score: finalScore,
|
|
325
|
+
reason,
|
|
326
|
+
},
|
|
327
|
+
],
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return adjusted;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Legacy getWeightedCards - now throws as filters should not be used as generators.
|
|
336
|
+
*
|
|
337
|
+
* Use transform() via Pipeline instead.
|
|
338
|
+
*/
|
|
339
|
+
async getWeightedCards(_limit: number): Promise<WeightedCard[]> {
|
|
340
|
+
throw new Error(
|
|
341
|
+
'InterferenceMitigatorNavigator is a filter and should not be used as a generator. ' +
|
|
342
|
+
'Use Pipeline with a generator and this filter via transform().'
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Legacy methods - stub implementations since filters don't generate cards
|
|
347
|
+
|
|
348
|
+
async getNewCards(_n?: number): Promise<StudySessionNewItem[]> {
|
|
349
|
+
return [];
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async getPendingReviews(): Promise<(StudySessionReviewItem & ScheduledCard)[]> {
|
|
353
|
+
return [];
|
|
354
|
+
}
|
|
355
|
+
}
|