@vue-skuilder/db 0.1.22 → 0.1.24
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/{contentSource-BP9hznNV.d.ts → contentSource-BotbOOfX.d.ts} +227 -3
- package/dist/{contentSource-DsJadoBU.d.cts → contentSource-C90LH-OH.d.cts} +227 -3
- package/dist/core/index.d.cts +220 -6
- package/dist/core/index.d.ts +220 -6
- package/dist/core/index.js +2052 -559
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +2035 -555
- package/dist/core/index.mjs.map +1 -1
- package/dist/{dataLayerProvider-CHYrQ5pB.d.cts → dataLayerProvider-DGKp4zFB.d.cts} +1 -1
- package/dist/{dataLayerProvider-MDTxXq2l.d.ts → dataLayerProvider-SBpz9jQf.d.ts} +1 -1
- package/dist/impl/couch/index.d.cts +11 -3
- package/dist/impl/couch/index.d.ts +11 -3
- package/dist/impl/couch/index.js +1811 -574
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +1792 -550
- package/dist/impl/couch/index.mjs.map +1 -1
- package/dist/impl/static/index.d.cts +4 -4
- package/dist/impl/static/index.d.ts +4 -4
- package/dist/impl/static/index.js +1797 -560
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +1789 -547
- package/dist/impl/static/index.mjs.map +1 -1
- package/dist/{index-Dj0SEgk3.d.ts → index-BWvO-_rJ.d.ts} +1 -1
- package/dist/{index-B_j6u5E4.d.cts → index-Ba7hYbHj.d.cts} +1 -1
- package/dist/index.d.cts +150 -12
- package/dist/index.d.ts +150 -12
- package/dist/index.js +2658 -791
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2584 -747
- package/dist/index.mjs.map +1 -1
- package/dist/{types-DQaXnuoc.d.ts → types-CJrLM1Ew.d.ts} +1 -1
- package/dist/{types-Bn0itutr.d.cts → types-W8n-B6HG.d.cts} +1 -1
- package/dist/{types-legacy-DDY4N-Uq.d.cts → types-legacy-JXDxinpU.d.cts} +5 -1
- package/dist/{types-legacy-DDY4N-Uq.d.ts → types-legacy-JXDxinpU.d.ts} +5 -1
- package/dist/util/packer/index.d.cts +3 -3
- package/dist/util/packer/index.d.ts +3 -3
- package/docs/brainstorm-navigation-paradigm.md +40 -34
- package/docs/future-orchestration-vision.md +216 -0
- package/docs/navigators-architecture.md +188 -5
- package/docs/todo-strategy-authoring.md +8 -6
- package/package.json +3 -3
- package/src/core/index.ts +2 -0
- package/src/core/interfaces/contentSource.ts +7 -0
- package/src/core/interfaces/userDB.ts +6 -0
- package/src/core/navigators/Pipeline.ts +46 -0
- package/src/core/navigators/PipelineAssembler.ts +14 -1
- package/src/core/navigators/filters/WeightedFilter.ts +141 -0
- package/src/core/navigators/filters/types.ts +4 -0
- package/src/core/navigators/generators/CompositeGenerator.ts +61 -19
- package/src/core/navigators/generators/types.ts +4 -0
- package/src/core/navigators/index.ts +194 -13
- package/src/core/orchestration/gradient.ts +133 -0
- package/src/core/orchestration/index.ts +210 -0
- package/src/core/orchestration/learning.ts +250 -0
- package/src/core/orchestration/recording.ts +92 -0
- package/src/core/orchestration/signal.ts +67 -0
- package/src/core/types/contentNavigationStrategy.ts +38 -0
- package/src/core/types/learningState.ts +77 -0
- package/src/core/types/types-legacy.ts +4 -0
- package/src/core/types/userOutcome.ts +51 -0
- package/src/courseConfigRegistration.ts +546 -0
- package/src/factory.ts +6 -0
- package/src/impl/common/BaseUserDB.ts +16 -0
- package/src/index.ts +2 -0
- package/src/study/SessionController.ts +64 -1
- package/tests/core/navigators/Pipeline.test.ts +2 -0
- package/docs/todo-evolutionary-orchestration.md +0 -310
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { LearnableWeight, DEFAULT_LEARNABLE_WEIGHT } from '../types/contentNavigationStrategy';
|
|
2
|
+
import { StrategyLearningState, GradientResult } from '../types/learningState';
|
|
3
|
+
import { DocType } from '../types/types-legacy';
|
|
4
|
+
import { logger } from '../../util/logger';
|
|
5
|
+
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// CONSTANTS
|
|
8
|
+
// ============================================================================
|
|
9
|
+
|
|
10
|
+
/** Minimum observations required before adjusting weight */
|
|
11
|
+
const MIN_OBSERVATIONS_FOR_UPDATE = 10;
|
|
12
|
+
|
|
13
|
+
/** How much to adjust weight per gradient unit */
|
|
14
|
+
const LEARNING_RATE = 0.1;
|
|
15
|
+
|
|
16
|
+
/** Maximum weight adjustment per update cycle */
|
|
17
|
+
const MAX_WEIGHT_DELTA = 0.3;
|
|
18
|
+
|
|
19
|
+
/** R-squared threshold below which we consider gradient unreliable */
|
|
20
|
+
const MIN_R_SQUARED_FOR_GRADIENT = 0.05;
|
|
21
|
+
|
|
22
|
+
/** Gradient magnitude below which we consider it "flat" (near optimal) */
|
|
23
|
+
const FLAT_GRADIENT_THRESHOLD = 0.02;
|
|
24
|
+
|
|
25
|
+
/** Maximum history entries to retain */
|
|
26
|
+
const MAX_HISTORY_LENGTH = 100;
|
|
27
|
+
|
|
28
|
+
// ============================================================================
|
|
29
|
+
// WEIGHT UPDATE
|
|
30
|
+
// ============================================================================
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Compute updated weight based on gradient result.
|
|
34
|
+
*
|
|
35
|
+
* The update logic:
|
|
36
|
+
* - Positive gradient: users with higher weight did better → increase weight
|
|
37
|
+
* - Negative gradient: users with higher weight did worse → decrease weight
|
|
38
|
+
* - Flat gradient: weight doesn't affect outcome → increase confidence
|
|
39
|
+
*
|
|
40
|
+
* @param current - Current learnable weight
|
|
41
|
+
* @param gradient - Computed gradient result
|
|
42
|
+
* @returns Updated learnable weight
|
|
43
|
+
*/
|
|
44
|
+
export function updateStrategyWeight(
|
|
45
|
+
current: LearnableWeight,
|
|
46
|
+
gradient: GradientResult
|
|
47
|
+
): LearnableWeight {
|
|
48
|
+
// Not enough data to make reliable updates
|
|
49
|
+
if (gradient.sampleSize < MIN_OBSERVATIONS_FOR_UPDATE) {
|
|
50
|
+
logger.debug(
|
|
51
|
+
`[Orchestration] Insufficient samples (${gradient.sampleSize} < ${MIN_OBSERVATIONS_FOR_UPDATE}), ` +
|
|
52
|
+
`keeping current weight`
|
|
53
|
+
);
|
|
54
|
+
return {
|
|
55
|
+
...current,
|
|
56
|
+
sampleSize: current.sampleSize + gradient.sampleSize,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Check if gradient is reliable (R² threshold)
|
|
61
|
+
const isReliable = gradient.rSquared >= MIN_R_SQUARED_FOR_GRADIENT;
|
|
62
|
+
const isFlat = Math.abs(gradient.gradient) < FLAT_GRADIENT_THRESHOLD;
|
|
63
|
+
|
|
64
|
+
let newWeight = current.weight;
|
|
65
|
+
let newConfidence = current.confidence;
|
|
66
|
+
|
|
67
|
+
if (!isReliable || isFlat) {
|
|
68
|
+
// Gradient is unreliable or flat - we're likely near optimal
|
|
69
|
+
// Increase confidence (narrow the exploration spread)
|
|
70
|
+
const confidenceGain = 0.05 * (1 - current.confidence);
|
|
71
|
+
newConfidence = Math.min(1.0, current.confidence + confidenceGain);
|
|
72
|
+
|
|
73
|
+
logger.debug(
|
|
74
|
+
`[Orchestration] Flat/unreliable gradient (|g|=${Math.abs(gradient.gradient).toFixed(4)}, ` +
|
|
75
|
+
`R²=${gradient.rSquared.toFixed(4)}). Increasing confidence: ${current.confidence.toFixed(3)} → ${newConfidence.toFixed(3)}`
|
|
76
|
+
);
|
|
77
|
+
} else {
|
|
78
|
+
// Reliable gradient - adjust weight in gradient direction
|
|
79
|
+
// Scale by learning rate and clamp to max delta
|
|
80
|
+
let delta = gradient.gradient * LEARNING_RATE;
|
|
81
|
+
delta = Math.max(-MAX_WEIGHT_DELTA, Math.min(MAX_WEIGHT_DELTA, delta));
|
|
82
|
+
|
|
83
|
+
newWeight = current.weight + delta;
|
|
84
|
+
|
|
85
|
+
// Clamp weight to reasonable bounds
|
|
86
|
+
newWeight = Math.max(0.1, Math.min(3.0, newWeight));
|
|
87
|
+
|
|
88
|
+
// Slight confidence increase for having made an observation
|
|
89
|
+
const confidenceGain = 0.02 * (1 - current.confidence);
|
|
90
|
+
newConfidence = Math.min(1.0, current.confidence + confidenceGain);
|
|
91
|
+
|
|
92
|
+
logger.debug(
|
|
93
|
+
`[Orchestration] Adjusting weight: ${current.weight.toFixed(3)} → ${newWeight.toFixed(3)} ` +
|
|
94
|
+
`(gradient=${gradient.gradient.toFixed(4)}, delta=${delta.toFixed(4)})`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
weight: newWeight,
|
|
100
|
+
confidence: newConfidence,
|
|
101
|
+
sampleSize: current.sampleSize + gradient.sampleSize,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ============================================================================
|
|
106
|
+
// LEARNING STATE MANAGEMENT
|
|
107
|
+
// ============================================================================
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Create or update a StrategyLearningState document.
|
|
111
|
+
*
|
|
112
|
+
* @param courseId - Course ID
|
|
113
|
+
* @param strategyId - Strategy ID
|
|
114
|
+
* @param currentWeight - Current learned weight
|
|
115
|
+
* @param gradient - Gradient result from recent computation
|
|
116
|
+
* @param existing - Existing learning state (if any)
|
|
117
|
+
* @returns Updated learning state document
|
|
118
|
+
*/
|
|
119
|
+
export function updateLearningState(
|
|
120
|
+
courseId: string,
|
|
121
|
+
strategyId: string,
|
|
122
|
+
currentWeight: LearnableWeight,
|
|
123
|
+
gradient: GradientResult,
|
|
124
|
+
existing?: StrategyLearningState
|
|
125
|
+
): StrategyLearningState {
|
|
126
|
+
const now = new Date().toISOString();
|
|
127
|
+
const id = `STRATEGY_LEARNING_STATE::${courseId}::${strategyId}`;
|
|
128
|
+
|
|
129
|
+
// Build history entry
|
|
130
|
+
const historyEntry = {
|
|
131
|
+
timestamp: now,
|
|
132
|
+
weight: currentWeight.weight,
|
|
133
|
+
confidence: currentWeight.confidence,
|
|
134
|
+
gradient: gradient.gradient,
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// Append to existing history or start fresh
|
|
138
|
+
let history = existing?.history ?? [];
|
|
139
|
+
history = [...history, historyEntry];
|
|
140
|
+
|
|
141
|
+
// Trim history if too long
|
|
142
|
+
if (history.length > MAX_HISTORY_LENGTH) {
|
|
143
|
+
history = history.slice(history.length - MAX_HISTORY_LENGTH);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const state: StrategyLearningState = {
|
|
147
|
+
_id: id,
|
|
148
|
+
_rev: existing?._rev,
|
|
149
|
+
docType: DocType.STRATEGY_LEARNING_STATE,
|
|
150
|
+
courseId,
|
|
151
|
+
strategyId,
|
|
152
|
+
currentWeight,
|
|
153
|
+
regression: {
|
|
154
|
+
gradient: gradient.gradient,
|
|
155
|
+
intercept: gradient.intercept,
|
|
156
|
+
rSquared: gradient.rSquared,
|
|
157
|
+
sampleSize: gradient.sampleSize,
|
|
158
|
+
computedAt: now,
|
|
159
|
+
},
|
|
160
|
+
history,
|
|
161
|
+
updatedAt: now,
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
return state;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ============================================================================
|
|
168
|
+
// PERIOD UPDATE ORCHESTRATOR
|
|
169
|
+
// ============================================================================
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Input data for running a period update on a single strategy.
|
|
173
|
+
*/
|
|
174
|
+
export interface PeriodUpdateInput {
|
|
175
|
+
courseId: string;
|
|
176
|
+
strategyId: string;
|
|
177
|
+
currentWeight: LearnableWeight;
|
|
178
|
+
gradient: GradientResult;
|
|
179
|
+
existingState?: StrategyLearningState;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Result of a period update for a single strategy.
|
|
184
|
+
*/
|
|
185
|
+
export interface PeriodUpdateResult {
|
|
186
|
+
strategyId: string;
|
|
187
|
+
previousWeight: LearnableWeight;
|
|
188
|
+
newWeight: LearnableWeight;
|
|
189
|
+
gradient: GradientResult;
|
|
190
|
+
learningState: StrategyLearningState;
|
|
191
|
+
updated: boolean;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Run a period update for a single strategy.
|
|
196
|
+
*
|
|
197
|
+
* This function:
|
|
198
|
+
* 1. Takes the computed gradient
|
|
199
|
+
* 2. Computes the new weight
|
|
200
|
+
* 3. Generates the updated learning state
|
|
201
|
+
*
|
|
202
|
+
* Note: Actual persistence (updating strategy doc, saving learning state)
|
|
203
|
+
* must be done by the caller with appropriate DB access.
|
|
204
|
+
*
|
|
205
|
+
* @param input - Update input data
|
|
206
|
+
* @returns Update result with new weight and learning state
|
|
207
|
+
*/
|
|
208
|
+
export function runPeriodUpdate(input: PeriodUpdateInput): PeriodUpdateResult {
|
|
209
|
+
const { courseId, strategyId, currentWeight, gradient, existingState } = input;
|
|
210
|
+
|
|
211
|
+
logger.info(
|
|
212
|
+
`[Orchestration] Running period update for strategy ${strategyId} ` +
|
|
213
|
+
`(${gradient.sampleSize} observations)`
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
// Compute new weight
|
|
217
|
+
const newWeight = updateStrategyWeight(currentWeight, gradient);
|
|
218
|
+
const updated = newWeight.weight !== currentWeight.weight;
|
|
219
|
+
|
|
220
|
+
// Generate learning state
|
|
221
|
+
const learningState = updateLearningState(
|
|
222
|
+
courseId,
|
|
223
|
+
strategyId,
|
|
224
|
+
newWeight,
|
|
225
|
+
gradient,
|
|
226
|
+
existingState
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
logger.info(
|
|
230
|
+
`[Orchestration] Period update complete for ${strategyId}: ` +
|
|
231
|
+
`weight ${currentWeight.weight.toFixed(3)} → ${newWeight.weight.toFixed(3)}, ` +
|
|
232
|
+
`confidence ${currentWeight.confidence.toFixed(3)} → ${newWeight.confidence.toFixed(3)}`
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
strategyId,
|
|
237
|
+
previousWeight: currentWeight,
|
|
238
|
+
newWeight,
|
|
239
|
+
gradient,
|
|
240
|
+
learningState,
|
|
241
|
+
updated,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Create a default LearnableWeight for strategies that don't have one.
|
|
247
|
+
*/
|
|
248
|
+
export function getDefaultLearnableWeight(): LearnableWeight {
|
|
249
|
+
return { ...DEFAULT_LEARNABLE_WEIGHT };
|
|
250
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { OrchestrationContext } from './index';
|
|
2
|
+
import { computeOutcomeSignal, SignalConfig } from './signal';
|
|
3
|
+
import { UserOutcomeRecord } from '../types/userOutcome';
|
|
4
|
+
import { DocType, QuestionRecord } from '../types/types-legacy';
|
|
5
|
+
import { logger } from '../../util/logger';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Records a learning outcome for a specific period of time.
|
|
9
|
+
*
|
|
10
|
+
* This function:
|
|
11
|
+
* 1. Computes a scalar "success" signal from the provided question records
|
|
12
|
+
* 2. Re-computes the deviations that were active for this user/course
|
|
13
|
+
* 3. Persists a UserOutcomeRecord to the user's database
|
|
14
|
+
*
|
|
15
|
+
* This record is later used by the optimization job to correlate
|
|
16
|
+
* deviations with outcomes (Evolutionary Orchestration).
|
|
17
|
+
*
|
|
18
|
+
* @param context - Orchestration context (user, course, etc.)
|
|
19
|
+
* @param periodStart - ISO timestamp of period start
|
|
20
|
+
* @param periodEnd - ISO timestamp of period end (now)
|
|
21
|
+
* @param records - Question records generated during this period
|
|
22
|
+
* @param activeStrategyIds - IDs of strategies active in this course
|
|
23
|
+
* @param eloStart - User's ELO at start of period (optional)
|
|
24
|
+
* @param eloEnd - User's ELO at end of period (optional)
|
|
25
|
+
* @param config - Optional configuration for signal computation
|
|
26
|
+
*/
|
|
27
|
+
export async function recordUserOutcome(
|
|
28
|
+
context: OrchestrationContext,
|
|
29
|
+
periodStart: string,
|
|
30
|
+
periodEnd: string,
|
|
31
|
+
records: QuestionRecord[],
|
|
32
|
+
activeStrategyIds: string[],
|
|
33
|
+
eloStart: number = 0,
|
|
34
|
+
eloEnd: number = 0,
|
|
35
|
+
config?: SignalConfig
|
|
36
|
+
): Promise<void> {
|
|
37
|
+
const { user, course, userId } = context;
|
|
38
|
+
const courseId = course.getCourseID();
|
|
39
|
+
|
|
40
|
+
// 1. Compute Signal
|
|
41
|
+
// If we have no records, we can't determine an outcome.
|
|
42
|
+
const outcomeValue = computeOutcomeSignal(records, config);
|
|
43
|
+
|
|
44
|
+
if (outcomeValue === null) {
|
|
45
|
+
logger.debug(
|
|
46
|
+
`[Orchestration] No outcome signal computed for ${userId} (insufficient data). Skipping record.`
|
|
47
|
+
);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 2. Capture Deviations
|
|
52
|
+
// We re-compute the deterministic deviations for all active strategies.
|
|
53
|
+
// This tells the learning algorithm "what parameter adjustments were active
|
|
54
|
+
// when this outcome was achieved".
|
|
55
|
+
const deviations: Record<string, number> = {};
|
|
56
|
+
for (const strategyId of activeStrategyIds) {
|
|
57
|
+
deviations[strategyId] = context.getDeviation(strategyId);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 3. Construct Record
|
|
61
|
+
// ID format: USER_OUTCOME::{courseId}::{userId}::{periodEnd}
|
|
62
|
+
// This ensures uniqueness per user/course/time-period.
|
|
63
|
+
const id = `USER_OUTCOME::${courseId}::${userId}::${periodEnd}`;
|
|
64
|
+
|
|
65
|
+
const record: UserOutcomeRecord = {
|
|
66
|
+
_id: id,
|
|
67
|
+
docType: DocType.USER_OUTCOME,
|
|
68
|
+
courseId,
|
|
69
|
+
userId,
|
|
70
|
+
periodStart,
|
|
71
|
+
periodEnd,
|
|
72
|
+
outcomeValue,
|
|
73
|
+
deviations,
|
|
74
|
+
metadata: {
|
|
75
|
+
sessionsCount: 1, // Assumes recording is triggered per-session currently
|
|
76
|
+
cardsSeen: records.length,
|
|
77
|
+
eloStart,
|
|
78
|
+
eloEnd,
|
|
79
|
+
signalType: 'accuracy_in_zone',
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// 4. Persist
|
|
84
|
+
try {
|
|
85
|
+
await user.putUserOutcome(record);
|
|
86
|
+
logger.debug(
|
|
87
|
+
`[Orchestration] Recorded outcome ${outcomeValue.toFixed(3)} for ${userId} (doc: ${id})`
|
|
88
|
+
);
|
|
89
|
+
} catch (e) {
|
|
90
|
+
logger.error(`[Orchestration] Failed to record outcome: ${e}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { QuestionRecord } from '../types/types-legacy';
|
|
2
|
+
|
|
3
|
+
export interface SignalConfig {
|
|
4
|
+
/** Target accuracy for "in the zone" learning (default: 0.85) */
|
|
5
|
+
targetAccuracy?: number;
|
|
6
|
+
/** Width of the peak plateau (default: 0.05) */
|
|
7
|
+
tolerance?: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Computes a scalar signal (0-1) representing the quality of the learning outcome.
|
|
12
|
+
*
|
|
13
|
+
* Current implementation focuses on "accuracy within Zone of Proximal Development".
|
|
14
|
+
* Future versions should include ELO gain rate.
|
|
15
|
+
*
|
|
16
|
+
* @param records - List of question attempts in the period
|
|
17
|
+
* @param config - Configuration for the signal function
|
|
18
|
+
* @returns Score 0.0-1.0, or null if insufficient data
|
|
19
|
+
*/
|
|
20
|
+
export function computeOutcomeSignal(
|
|
21
|
+
records: QuestionRecord[],
|
|
22
|
+
config: SignalConfig = {}
|
|
23
|
+
): number | null {
|
|
24
|
+
if (!records || records.length === 0) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const target = config.targetAccuracy ?? 0.85;
|
|
29
|
+
const tolerance = config.tolerance ?? 0.05;
|
|
30
|
+
|
|
31
|
+
let correct = 0;
|
|
32
|
+
for (const r of records) {
|
|
33
|
+
// Cast to any to avoid type error if Evaluation interface is not correctly propagated
|
|
34
|
+
|
|
35
|
+
if ((r as any).isCorrect) correct++;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const accuracy = correct / records.length;
|
|
39
|
+
|
|
40
|
+
return scoreAccuracyInZone(accuracy, target, tolerance);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Scores an accuracy value based on how close it is to the target "sweet spot".
|
|
45
|
+
*
|
|
46
|
+
* The function defines a plateau of width (2 * tolerance) around the target
|
|
47
|
+
* where score is 1.0. Outside this plateau, it falls off linearly.
|
|
48
|
+
*
|
|
49
|
+
* @param accuracy - Observed accuracy (0-1)
|
|
50
|
+
* @param target - Target accuracy (e.g. 0.85)
|
|
51
|
+
* @param tolerance - +/- range allowed for max score
|
|
52
|
+
*/
|
|
53
|
+
export function scoreAccuracyInZone(accuracy: number, target: number, tolerance: number): number {
|
|
54
|
+
const dist = Math.abs(accuracy - target);
|
|
55
|
+
|
|
56
|
+
// Inside the sweet spot
|
|
57
|
+
if (dist <= tolerance) {
|
|
58
|
+
return 1.0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Outside, fall off.
|
|
62
|
+
// We apply a linear penalty for deviation from the tolerance edge.
|
|
63
|
+
const excess = dist - tolerance;
|
|
64
|
+
const slope = 2.5; // Falloff rate (0.4 deviation = 0 score)
|
|
65
|
+
|
|
66
|
+
return Math.max(0, 1.0 - excess * slope);
|
|
67
|
+
}
|
|
@@ -1,6 +1,31 @@
|
|
|
1
1
|
import { DocType, SkuilderCourseData } from './types-legacy';
|
|
2
2
|
import type { DocTypePrefixes } from './types-legacy';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Configuration for an evolutionarily-weighted strategy.
|
|
6
|
+
*
|
|
7
|
+
* This structure tracks the "learned" weight of a strategy, representing the
|
|
8
|
+
* system's confidence in its utility.
|
|
9
|
+
*
|
|
10
|
+
* - weight: The best-known multiplier (peak of the bell curve)
|
|
11
|
+
* - confidence: How certain we are (inverse of variance / spread)
|
|
12
|
+
* - sampleSize: How many data points informed this weight
|
|
13
|
+
*/
|
|
14
|
+
export interface LearnableWeight {
|
|
15
|
+
/** The current best estimate of optimal weight (multiplier) */
|
|
16
|
+
weight: number;
|
|
17
|
+
/** Confidence in this weight (0-1). Higher = narrower exploration spread. */
|
|
18
|
+
confidence: number;
|
|
19
|
+
/** Number of outcome observations that contributed to this weight */
|
|
20
|
+
sampleSize: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const DEFAULT_LEARNABLE_WEIGHT: LearnableWeight = {
|
|
24
|
+
weight: 1.0,
|
|
25
|
+
confidence: 0.1, // Low confidence initially = wide exploration
|
|
26
|
+
sampleSize: 0,
|
|
27
|
+
};
|
|
28
|
+
|
|
4
29
|
/**
|
|
5
30
|
*
|
|
6
31
|
*/
|
|
@@ -19,4 +44,17 @@ export interface ContentNavigationStrategyData extends SkuilderCourseData {
|
|
|
19
44
|
by the implementing class's constructor at runtime.
|
|
20
45
|
*/
|
|
21
46
|
serializedData: string;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Evolutionary weighting configuration.
|
|
50
|
+
* If present, the strategy's influence is scaled by this weight.
|
|
51
|
+
* If omitted, weight defaults to 1.0.
|
|
52
|
+
*/
|
|
53
|
+
learnable?: LearnableWeight;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* If true, the weight is applied exactly as configured, without
|
|
57
|
+
* per-user deviation. Used for manual tuning or A/B testing.
|
|
58
|
+
*/
|
|
59
|
+
staticWeight?: boolean;
|
|
22
60
|
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { DocType } from './types-legacy';
|
|
2
|
+
import { LearnableWeight } from './contentNavigationStrategy';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Snapshot of the learning state for a strategy.
|
|
6
|
+
*
|
|
7
|
+
* Stored in the CourseDB for observability and debugging.
|
|
8
|
+
* Updated periodically by the gradient learning system.
|
|
9
|
+
*/
|
|
10
|
+
export interface StrategyLearningState {
|
|
11
|
+
/**
|
|
12
|
+
* Unique ID: "STRATEGY_LEARNING_STATE::{courseId}::{strategyId}"
|
|
13
|
+
*/
|
|
14
|
+
_id: string;
|
|
15
|
+
|
|
16
|
+
/** Allow CouchDB to manage revisions */
|
|
17
|
+
_rev?: string;
|
|
18
|
+
|
|
19
|
+
docType: DocType.STRATEGY_LEARNING_STATE;
|
|
20
|
+
|
|
21
|
+
courseId: string;
|
|
22
|
+
strategyId: string;
|
|
23
|
+
|
|
24
|
+
/** Current learned weight (mirrors the strategy doc, for convenience) */
|
|
25
|
+
currentWeight: LearnableWeight;
|
|
26
|
+
|
|
27
|
+
/** Most recent regression statistics */
|
|
28
|
+
regression: {
|
|
29
|
+
/** Slope of the linear regression (deviation vs outcome) */
|
|
30
|
+
gradient: number;
|
|
31
|
+
/** Y-intercept of the regression line */
|
|
32
|
+
intercept: number;
|
|
33
|
+
/** R-squared value (0-1), measure of fit quality */
|
|
34
|
+
rSquared: number;
|
|
35
|
+
/** Number of observations used in this regression */
|
|
36
|
+
sampleSize: number;
|
|
37
|
+
/** ISO timestamp of when this regression was computed */
|
|
38
|
+
computedAt: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** Historical weight snapshots for visualization */
|
|
42
|
+
history: Array<{
|
|
43
|
+
timestamp: string;
|
|
44
|
+
weight: number;
|
|
45
|
+
confidence: number;
|
|
46
|
+
gradient: number;
|
|
47
|
+
}>;
|
|
48
|
+
|
|
49
|
+
/** ISO timestamp of last update */
|
|
50
|
+
updatedAt: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Data point for gradient computation: (deviation, outcome) pair.
|
|
55
|
+
*/
|
|
56
|
+
export interface GradientObservation {
|
|
57
|
+
/** User's deviation for this strategy [-1, 1] */
|
|
58
|
+
deviation: number;
|
|
59
|
+
/** User's outcome value [0, 1] */
|
|
60
|
+
outcomeValue: number;
|
|
61
|
+
/** Optional: weight for this observation (default 1.0) */
|
|
62
|
+
weight?: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Result of linear regression on (deviation, outcome) pairs.
|
|
67
|
+
*/
|
|
68
|
+
export interface GradientResult {
|
|
69
|
+
/** Slope: positive = higher deviation correlates with better outcomes */
|
|
70
|
+
gradient: number;
|
|
71
|
+
/** Y-intercept */
|
|
72
|
+
intercept: number;
|
|
73
|
+
/** R-squared: 0-1, how well the line fits */
|
|
74
|
+
rSquared: number;
|
|
75
|
+
/** Number of observations */
|
|
76
|
+
sampleSize: number;
|
|
77
|
+
}
|
|
@@ -20,6 +20,8 @@ export enum DocType {
|
|
|
20
20
|
TAG = 'TAG',
|
|
21
21
|
NAVIGATION_STRATEGY = 'NAVIGATION_STRATEGY',
|
|
22
22
|
STRATEGY_STATE = 'STRATEGY_STATE',
|
|
23
|
+
USER_OUTCOME = 'USER_OUTCOME',
|
|
24
|
+
STRATEGY_LEARNING_STATE = 'STRATEGY_LEARNING_STATE',
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
export interface QualifiedCardID {
|
|
@@ -105,6 +107,8 @@ export const DocTypePrefixes = {
|
|
|
105
107
|
[DocType.PEDAGOGY]: 'PEDAGOGY',
|
|
106
108
|
[DocType.NAVIGATION_STRATEGY]: 'NAVIGATION_STRATEGY',
|
|
107
109
|
[DocType.STRATEGY_STATE]: 'STRATEGY_STATE',
|
|
110
|
+
[DocType.USER_OUTCOME]: 'USER_OUTCOME',
|
|
111
|
+
[DocType.STRATEGY_LEARNING_STATE]: 'STRATEGY_LEARNING_STATE',
|
|
108
112
|
} as const;
|
|
109
113
|
|
|
110
114
|
export interface CardHistory<T extends CardRecord> {
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { DocType } from './types-legacy';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Record of a user's learning outcome over a specific period.
|
|
5
|
+
*
|
|
6
|
+
* Used by the evolutionary orchestration system to correlate strategy
|
|
7
|
+
* deviations with learning success.
|
|
8
|
+
*
|
|
9
|
+
* Stored in the UserDB.
|
|
10
|
+
*/
|
|
11
|
+
export interface UserOutcomeRecord {
|
|
12
|
+
/**
|
|
13
|
+
* Unique ID: "USER_OUTCOME::{courseId}::{userId}::{timestamp}"
|
|
14
|
+
* Timestamp corresponds to periodEnd.
|
|
15
|
+
*/
|
|
16
|
+
_id: string;
|
|
17
|
+
|
|
18
|
+
docType: DocType.USER_OUTCOME;
|
|
19
|
+
|
|
20
|
+
courseId: string;
|
|
21
|
+
userId: string;
|
|
22
|
+
|
|
23
|
+
/** Start of the measurement period (ISO timestamp) */
|
|
24
|
+
periodStart: string;
|
|
25
|
+
/** End of the measurement period (ISO timestamp) */
|
|
26
|
+
periodEnd: string;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The computed signal value (e.g., 0.85 for 85% accuracy).
|
|
30
|
+
* This is the 'Y' in the regression analysis.
|
|
31
|
+
*
|
|
32
|
+
* Higher values indicate better learning outcomes.
|
|
33
|
+
*/
|
|
34
|
+
outcomeValue: number;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Snapshot of active deviations during this period.
|
|
38
|
+
* Maps strategyId -> deviation value used [-1.0, 1.0].
|
|
39
|
+
* This provides the 'X' values for regression analysis.
|
|
40
|
+
*/
|
|
41
|
+
deviations: Record<string, number>;
|
|
42
|
+
|
|
43
|
+
metadata: {
|
|
44
|
+
sessionsCount: number;
|
|
45
|
+
cardsSeen: number;
|
|
46
|
+
eloStart: number;
|
|
47
|
+
eloEnd: number;
|
|
48
|
+
/** The algorithm used to compute outcomeValue (e.g. "accuracy_in_zone") */
|
|
49
|
+
signalType: string;
|
|
50
|
+
};
|
|
51
|
+
}
|