@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.
Files changed (67) hide show
  1. package/dist/{contentSource-BP9hznNV.d.ts → contentSource-BotbOOfX.d.ts} +227 -3
  2. package/dist/{contentSource-DsJadoBU.d.cts → contentSource-C90LH-OH.d.cts} +227 -3
  3. package/dist/core/index.d.cts +220 -6
  4. package/dist/core/index.d.ts +220 -6
  5. package/dist/core/index.js +2052 -559
  6. package/dist/core/index.js.map +1 -1
  7. package/dist/core/index.mjs +2035 -555
  8. package/dist/core/index.mjs.map +1 -1
  9. package/dist/{dataLayerProvider-CHYrQ5pB.d.cts → dataLayerProvider-DGKp4zFB.d.cts} +1 -1
  10. package/dist/{dataLayerProvider-MDTxXq2l.d.ts → dataLayerProvider-SBpz9jQf.d.ts} +1 -1
  11. package/dist/impl/couch/index.d.cts +11 -3
  12. package/dist/impl/couch/index.d.ts +11 -3
  13. package/dist/impl/couch/index.js +1811 -574
  14. package/dist/impl/couch/index.js.map +1 -1
  15. package/dist/impl/couch/index.mjs +1792 -550
  16. package/dist/impl/couch/index.mjs.map +1 -1
  17. package/dist/impl/static/index.d.cts +4 -4
  18. package/dist/impl/static/index.d.ts +4 -4
  19. package/dist/impl/static/index.js +1797 -560
  20. package/dist/impl/static/index.js.map +1 -1
  21. package/dist/impl/static/index.mjs +1789 -547
  22. package/dist/impl/static/index.mjs.map +1 -1
  23. package/dist/{index-Dj0SEgk3.d.ts → index-BWvO-_rJ.d.ts} +1 -1
  24. package/dist/{index-B_j6u5E4.d.cts → index-Ba7hYbHj.d.cts} +1 -1
  25. package/dist/index.d.cts +150 -12
  26. package/dist/index.d.ts +150 -12
  27. package/dist/index.js +2658 -791
  28. package/dist/index.js.map +1 -1
  29. package/dist/index.mjs +2584 -747
  30. package/dist/index.mjs.map +1 -1
  31. package/dist/{types-DQaXnuoc.d.ts → types-CJrLM1Ew.d.ts} +1 -1
  32. package/dist/{types-Bn0itutr.d.cts → types-W8n-B6HG.d.cts} +1 -1
  33. package/dist/{types-legacy-DDY4N-Uq.d.cts → types-legacy-JXDxinpU.d.cts} +5 -1
  34. package/dist/{types-legacy-DDY4N-Uq.d.ts → types-legacy-JXDxinpU.d.ts} +5 -1
  35. package/dist/util/packer/index.d.cts +3 -3
  36. package/dist/util/packer/index.d.ts +3 -3
  37. package/docs/brainstorm-navigation-paradigm.md +40 -34
  38. package/docs/future-orchestration-vision.md +216 -0
  39. package/docs/navigators-architecture.md +188 -5
  40. package/docs/todo-strategy-authoring.md +8 -6
  41. package/package.json +3 -3
  42. package/src/core/index.ts +2 -0
  43. package/src/core/interfaces/contentSource.ts +7 -0
  44. package/src/core/interfaces/userDB.ts +6 -0
  45. package/src/core/navigators/Pipeline.ts +46 -0
  46. package/src/core/navigators/PipelineAssembler.ts +14 -1
  47. package/src/core/navigators/filters/WeightedFilter.ts +141 -0
  48. package/src/core/navigators/filters/types.ts +4 -0
  49. package/src/core/navigators/generators/CompositeGenerator.ts +61 -19
  50. package/src/core/navigators/generators/types.ts +4 -0
  51. package/src/core/navigators/index.ts +194 -13
  52. package/src/core/orchestration/gradient.ts +133 -0
  53. package/src/core/orchestration/index.ts +210 -0
  54. package/src/core/orchestration/learning.ts +250 -0
  55. package/src/core/orchestration/recording.ts +92 -0
  56. package/src/core/orchestration/signal.ts +67 -0
  57. package/src/core/types/contentNavigationStrategy.ts +38 -0
  58. package/src/core/types/learningState.ts +77 -0
  59. package/src/core/types/types-legacy.ts +4 -0
  60. package/src/core/types/userOutcome.ts +51 -0
  61. package/src/courseConfigRegistration.ts +546 -0
  62. package/src/factory.ts +6 -0
  63. package/src/impl/common/BaseUserDB.ts +16 -0
  64. package/src/index.ts +2 -0
  65. package/src/study/SessionController.ts +64 -1
  66. package/tests/core/navigators/Pipeline.test.ts +2 -0
  67. 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
+ }