@vue-skuilder/db 0.1.23 → 0.1.25

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 (80) hide show
  1. package/dist/{contentSource-BP9hznNV.d.ts → contentSource-BmnmvH8C.d.ts} +268 -3
  2. package/dist/{contentSource-DsJadoBU.d.cts → contentSource-DfBbaLA-.d.cts} +268 -3
  3. package/dist/core/index.d.cts +310 -6
  4. package/dist/core/index.d.ts +310 -6
  5. package/dist/core/index.js +2606 -666
  6. package/dist/core/index.js.map +1 -1
  7. package/dist/core/index.mjs +2564 -639
  8. package/dist/core/index.mjs.map +1 -1
  9. package/dist/{dataLayerProvider-CHYrQ5pB.d.cts → dataLayerProvider-BeRXVMs5.d.cts} +1 -1
  10. package/dist/{dataLayerProvider-MDTxXq2l.d.ts → dataLayerProvider-CG9GfaAY.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 +2336 -656
  14. package/dist/impl/couch/index.js.map +1 -1
  15. package/dist/impl/couch/index.mjs +2316 -631
  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 +2312 -632
  20. package/dist/impl/static/index.js.map +1 -1
  21. package/dist/impl/static/index.mjs +2315 -630
  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 +278 -20
  26. package/dist/index.d.ts +278 -20
  27. package/dist/index.js +3603 -720
  28. package/dist/index.js.map +1 -1
  29. package/dist/index.mjs +3529 -674
  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 +210 -9
  40. package/docs/todo-review-urgency-adaptation.md +205 -0
  41. package/docs/todo-strategy-authoring.md +8 -6
  42. package/package.json +3 -3
  43. package/src/core/index.ts +2 -0
  44. package/src/core/interfaces/contentSource.ts +7 -0
  45. package/src/core/interfaces/userDB.ts +50 -0
  46. package/src/core/navigators/Pipeline.ts +132 -5
  47. package/src/core/navigators/PipelineAssembler.ts +21 -22
  48. package/src/core/navigators/PipelineDebugger.ts +426 -0
  49. package/src/core/navigators/filters/WeightedFilter.ts +141 -0
  50. package/src/core/navigators/filters/types.ts +4 -0
  51. package/src/core/navigators/generators/CompositeGenerator.ts +82 -19
  52. package/src/core/navigators/generators/elo.ts +14 -1
  53. package/src/core/navigators/generators/srs.ts +146 -18
  54. package/src/core/navigators/generators/types.ts +4 -0
  55. package/src/core/navigators/index.ts +203 -13
  56. package/src/core/orchestration/gradient.ts +133 -0
  57. package/src/core/orchestration/index.ts +210 -0
  58. package/src/core/orchestration/learning.ts +250 -0
  59. package/src/core/orchestration/recording.ts +92 -0
  60. package/src/core/orchestration/signal.ts +67 -0
  61. package/src/core/types/contentNavigationStrategy.ts +38 -0
  62. package/src/core/types/learningState.ts +77 -0
  63. package/src/core/types/types-legacy.ts +4 -0
  64. package/src/core/types/userOutcome.ts +51 -0
  65. package/src/courseConfigRegistration.ts +107 -0
  66. package/src/factory.ts +6 -0
  67. package/src/impl/common/BaseUserDB.ts +16 -0
  68. package/src/impl/couch/user-course-relDB.ts +12 -0
  69. package/src/study/MixerDebugger.ts +555 -0
  70. package/src/study/SessionController.ts +159 -20
  71. package/src/study/SessionDebugger.ts +442 -0
  72. package/src/study/SourceMixer.ts +36 -17
  73. package/src/study/TODO-session-scheduling.md +133 -0
  74. package/src/study/index.ts +2 -0
  75. package/src/study/services/EloService.ts +79 -4
  76. package/src/study/services/ResponseProcessor.ts +130 -72
  77. package/src/study/services/SrsService.ts +9 -0
  78. package/tests/core/navigators/Pipeline.test.ts +2 -0
  79. package/tests/core/navigators/PipelineAssembler.test.ts +4 -4
  80. package/docs/todo-evolutionary-orchestration.md +0 -310
@@ -6,9 +6,143 @@ export type { CardFilter, FilterContext, CardFilterFactory } from './filters/typ
6
6
  // Re-export generator types
7
7
  export type { CardGenerator, GeneratorContext, CardGeneratorFactory } from './generators/types';
8
8
 
9
- import { ContentNavigationStrategyData } from '../types/contentNavigationStrategy';
9
+ // Re-export pipeline debugger API
10
+ export {
11
+ pipelineDebugAPI,
12
+ mountPipelineDebugger,
13
+ type PipelineRunReport,
14
+ type GeneratorSummary,
15
+ type FilterImpact,
16
+ } from './PipelineDebugger';
17
+
18
+ import { LearnableWeight } from '../types/contentNavigationStrategy';
19
+ export type { ContentNavigationStrategyData, LearnableWeight } from '../types/contentNavigationStrategy';
20
+ import type { ContentNavigationStrategyData } from '../types/contentNavigationStrategy';
10
21
  import { logger } from '../../util/logger';
11
22
 
23
+ // ============================================================================
24
+ // NAVIGATOR REGISTRY
25
+ // ============================================================================
26
+ //
27
+ // Static registry of navigator implementations. This allows ContentNavigator.create()
28
+ // to instantiate navigators without relying on dynamic imports, which don't work
29
+ // reliably in all environments (e.g., test runners, bundled code).
30
+ //
31
+ // Usage:
32
+ // 1. Import your navigator class
33
+ // 2. Call registerNavigator('implementingClass', YourNavigatorClass)
34
+ // 3. ContentNavigator.create() will use the registry before falling back to
35
+ // dynamic import
36
+ //
37
+ // ============================================================================
38
+
39
+ /**
40
+ * Type for navigator constructor functions.
41
+ */
42
+ export type NavigatorConstructor = new (
43
+ user: UserDBInterface,
44
+ course: CourseDBInterface,
45
+ strategyData: ContentNavigationStrategyData
46
+ ) => ContentNavigator;
47
+
48
+ /**
49
+ * Registry mapping implementingClass names to navigator constructors.
50
+ * Populated by registerNavigator() and used by ContentNavigator.create().
51
+ */
52
+ const navigatorRegistry = new Map<string, NavigatorConstructor>();
53
+
54
+ /**
55
+ * Register a navigator implementation.
56
+ *
57
+ * Call this to make a navigator available for instantiation by
58
+ * ContentNavigator.create() without relying on dynamic imports.
59
+ *
60
+ * @param implementingClass - The class name (e.g., 'elo', 'hierarchyDefinition')
61
+ * @param constructor - The navigator class constructor
62
+ */
63
+ export function registerNavigator(
64
+ implementingClass: string,
65
+ constructor: NavigatorConstructor
66
+ ): void {
67
+ navigatorRegistry.set(implementingClass, constructor);
68
+ logger.debug(`[NavigatorRegistry] Registered: ${implementingClass}`);
69
+ }
70
+
71
+ /**
72
+ * Get a navigator constructor from the registry.
73
+ *
74
+ * @param implementingClass - The class name to look up
75
+ * @returns The constructor, or undefined if not registered
76
+ */
77
+ export function getRegisteredNavigator(implementingClass: string): NavigatorConstructor | undefined {
78
+ return navigatorRegistry.get(implementingClass);
79
+ }
80
+
81
+ /**
82
+ * Check if a navigator is registered.
83
+ *
84
+ * @param implementingClass - The class name to check
85
+ * @returns true if registered, false otherwise
86
+ */
87
+ export function hasRegisteredNavigator(implementingClass: string): boolean {
88
+ return navigatorRegistry.has(implementingClass);
89
+ }
90
+
91
+ /**
92
+ * Get all registered navigator names.
93
+ * Useful for debugging and testing.
94
+ */
95
+ export function getRegisteredNavigatorNames(): string[] {
96
+ return Array.from(navigatorRegistry.keys());
97
+ }
98
+
99
+ /**
100
+ * Initialize the navigator registry with all built-in navigators.
101
+ *
102
+ * This function dynamically imports all standard navigator implementations
103
+ * and registers them. Call this once at application startup to ensure
104
+ * all navigators are available.
105
+ *
106
+ * In test environments, this may need to be called explicitly before
107
+ * using ContentNavigator.create().
108
+ */
109
+ export async function initializeNavigatorRegistry(): Promise<void> {
110
+ logger.debug('[NavigatorRegistry] Initializing built-in navigators...');
111
+
112
+ // Import and register generators
113
+ const [eloModule, srsModule] = await Promise.all([
114
+ import('./generators/elo'),
115
+ import('./generators/srs'),
116
+ ]);
117
+ registerNavigator('elo', eloModule.default);
118
+ registerNavigator('srs', srsModule.default);
119
+
120
+ // Import and register filters
121
+ const [
122
+ hierarchyModule,
123
+ interferenceModule,
124
+ relativePriorityModule,
125
+ userTagPreferenceModule,
126
+ ] = await Promise.all([
127
+ import('./filters/hierarchyDefinition'),
128
+ import('./filters/interferenceMitigator'),
129
+ import('./filters/relativePriority'),
130
+ import('./filters/userTagPreference'),
131
+ ]);
132
+ registerNavigator('hierarchyDefinition', hierarchyModule.default);
133
+ registerNavigator('interferenceMitigator', interferenceModule.default);
134
+ registerNavigator('relativePriority', relativePriorityModule.default);
135
+ registerNavigator('userTagPreference', userTagPreferenceModule.default);
136
+
137
+ // Note: eloDistance uses a factory pattern (createEloDistanceFilter) rather than
138
+ // a ContentNavigator class, so it's not registered here. It's used differently
139
+ // via Pipeline composition.
140
+
141
+ logger.debug(
142
+ `[NavigatorRegistry] Initialized ${navigatorRegistry.size} navigators: ${getRegisteredNavigatorNames().join(', ')}`
143
+ );
144
+ }
145
+
12
146
  // ============================================================================
13
147
  // NAVIGATION STRATEGY API
14
148
  // ============================================================================
@@ -92,6 +226,19 @@ export interface StrategyContribution {
92
226
  /** Score after this strategy's processing */
93
227
  score: number;
94
228
 
229
+ /**
230
+ * The effective weight applied for this strategy instance.
231
+ * If using evolutionary orchestration, this includes deviation.
232
+ * If omitted, implies weight 1.0 (legacy behavior).
233
+ */
234
+ effectiveWeight?: number;
235
+
236
+ /**
237
+ * The deviation factor applied to this user's cohort for this strategy.
238
+ * Range [-1.0, 1.0].
239
+ */
240
+ deviation?: number;
241
+
95
242
  /**
96
243
  * Human-readable explanation of the strategy's decision.
97
244
  *
@@ -260,6 +407,12 @@ export abstract class ContentNavigator implements StudyContentSource {
260
407
  /** Unique document ID for this strategy instance (from ContentNavigationStrategyData._id) */
261
408
  protected strategyId?: string;
262
409
 
410
+ /** Evolutionary weighting configuration */
411
+ public learnable?: LearnableWeight;
412
+
413
+ /** Whether to bypass deviation (manual/static weighting) */
414
+ public staticWeight?: boolean;
415
+
263
416
  /**
264
417
  * Constructor for standard navigators.
265
418
  * Call this from subclass constructors to initialize common fields.
@@ -277,6 +430,8 @@ export abstract class ContentNavigator implements StudyContentSource {
277
430
  if (strategyData) {
278
431
  this.strategyName = strategyData.name;
279
432
  this.strategyId = strategyData._id;
433
+ this.learnable = strategyData.learnable;
434
+ this.staticWeight = strategyData.staticWeight;
280
435
  }
281
436
  }
282
437
 
@@ -333,7 +488,13 @@ export abstract class ContentNavigator implements StudyContentSource {
333
488
  }
334
489
 
335
490
  /**
336
- * Factory method to create navigator instances dynamically.
491
+ * Factory method to create navigator instances.
492
+ *
493
+ * First checks the navigator registry for a pre-registered constructor.
494
+ * If not found, falls back to dynamic import (for custom navigators).
495
+ *
496
+ * For reliable operation in test environments, call initializeNavigatorRegistry()
497
+ * before using this method.
337
498
  *
338
499
  * @param user - User interface
339
500
  * @param course - Course interface
@@ -346,24 +507,53 @@ export abstract class ContentNavigator implements StudyContentSource {
346
507
  strategyData: ContentNavigationStrategyData
347
508
  ): Promise<ContentNavigator> {
348
509
  const implementingClass = strategyData.implementingClass;
510
+
511
+ // First, check the registry for a pre-registered constructor
512
+ const RegisteredImpl = getRegisteredNavigator(implementingClass);
513
+ if (RegisteredImpl) {
514
+ logger.debug(`[ContentNavigator.create] Using registered navigator: ${implementingClass}`);
515
+ return new RegisteredImpl(user, course, strategyData);
516
+ }
517
+
518
+ // Fall back to dynamic import for custom/unknown navigators
519
+ logger.debug(
520
+ `[ContentNavigator.create] Navigator not in registry, attempting dynamic import: ${implementingClass}`
521
+ );
522
+
349
523
  let NavigatorImpl;
350
524
 
351
525
  // Try different extension variations
352
526
  const variations = ['.ts', '.js', ''];
353
- const dirs = ['filters', 'generators'];
354
527
 
355
528
  for (const ext of variations) {
356
- for (const dir of dirs) {
357
- const loadFrom = `./${dir}/${implementingClass}${ext}`;
358
- try {
359
- const module = await import(loadFrom);
360
- NavigatorImpl = module.default;
361
- break; // Break the loop if loading succeeds
362
- } catch (e) {
363
- // Continue to next variation if this one fails
364
- logger.debug(`Failed to load extension from ${loadFrom}:`, e);
365
- }
529
+ // Try generators directory
530
+ try {
531
+ const module = await import(`./generators/${implementingClass}${ext}`);
532
+ NavigatorImpl = module.default;
533
+ if (NavigatorImpl) break;
534
+ } catch (e) {
535
+ logger.debug(`Failed to load generator ${implementingClass}${ext}:`, e);
536
+ }
537
+
538
+ // Try filters directory
539
+ try {
540
+ const module = await import(`./filters/${implementingClass}${ext}`);
541
+ NavigatorImpl = module.default;
542
+ if (NavigatorImpl) break;
543
+ } catch (e) {
544
+ logger.debug(`Failed to load filter ${implementingClass}${ext}:`, e);
545
+ }
546
+
547
+ // Try current directory (legacy)
548
+ try {
549
+ const module = await import(`./${implementingClass}${ext}`);
550
+ NavigatorImpl = module.default;
551
+ if (NavigatorImpl) break;
552
+ } catch (e) {
553
+ logger.debug(`Failed to load legacy ${implementingClass}${ext}:`, e);
366
554
  }
555
+
556
+ if (NavigatorImpl) break;
367
557
  }
368
558
 
369
559
  if (!NavigatorImpl) {
@@ -0,0 +1,133 @@
1
+ import { UserOutcomeRecord } from '../types/userOutcome';
2
+ import { GradientObservation, GradientResult } from '../types/learningState';
3
+ import { logger } from '../../util/logger';
4
+
5
+ /**
6
+ * Extract (deviation, outcome) observations for a specific strategy
7
+ * from a collection of UserOutcomeRecords.
8
+ *
9
+ * @param outcomes - Collection of outcome records (from multiple users)
10
+ * @param strategyId - The strategy to extract observations for
11
+ * @returns Array of gradient observations
12
+ */
13
+ export function aggregateOutcomesForGradient(
14
+ outcomes: UserOutcomeRecord[],
15
+ strategyId: string
16
+ ): GradientObservation[] {
17
+ const observations: GradientObservation[] = [];
18
+
19
+ for (const outcome of outcomes) {
20
+ // Skip if this outcome doesn't have a deviation for this strategy
21
+ const deviation = outcome.deviations[strategyId];
22
+ if (deviation === undefined) {
23
+ continue;
24
+ }
25
+
26
+ observations.push({
27
+ deviation,
28
+ outcomeValue: outcome.outcomeValue,
29
+ weight: 1.0,
30
+ });
31
+ }
32
+
33
+ logger.debug(
34
+ `[Orchestration] Aggregated ${observations.length} observations for strategy ${strategyId}`
35
+ );
36
+
37
+ return observations;
38
+ }
39
+
40
+ /**
41
+ * Compute linear regression on (deviation, outcome) pairs.
42
+ *
43
+ * Uses ordinary least squares to find the best fit line:
44
+ * outcome = gradient * deviation + intercept
45
+ *
46
+ * The gradient tells us:
47
+ * - Positive: users with higher deviation (higher weight) had better outcomes
48
+ * → we should increase the peak weight
49
+ * - Negative: users with higher deviation (higher weight) had worse outcomes
50
+ * → we should decrease the peak weight
51
+ * - Near zero: weight doesn't affect outcomes much
52
+ * → we're near optimal, increase confidence
53
+ *
54
+ * @param observations - Array of (deviation, outcome) pairs
55
+ * @returns Regression result, or null if insufficient data
56
+ */
57
+ export function computeStrategyGradient(
58
+ observations: GradientObservation[]
59
+ ): GradientResult | null {
60
+ const n = observations.length;
61
+
62
+ if (n < 3) {
63
+ logger.debug(`[Orchestration] Insufficient observations for gradient (${n} < 3)`);
64
+ return null;
65
+ }
66
+
67
+ // Compute means
68
+ let sumX = 0;
69
+ let sumY = 0;
70
+ let sumW = 0;
71
+
72
+ for (const obs of observations) {
73
+ const w = obs.weight ?? 1.0;
74
+ sumX += obs.deviation * w;
75
+ sumY += obs.outcomeValue * w;
76
+ sumW += w;
77
+ }
78
+
79
+ const meanX = sumX / sumW;
80
+ const meanY = sumY / sumW;
81
+
82
+ // Compute slope (gradient) and intercept using weighted least squares
83
+ let numerator = 0;
84
+ let denominator = 0;
85
+ let ssTotal = 0;
86
+
87
+ for (const obs of observations) {
88
+ const w = obs.weight ?? 1.0;
89
+ const dx = obs.deviation - meanX;
90
+ const dy = obs.outcomeValue - meanY;
91
+
92
+ numerator += w * dx * dy;
93
+ denominator += w * dx * dx;
94
+ ssTotal += w * dy * dy;
95
+ }
96
+
97
+ // Avoid division by zero if all deviations are the same
98
+ if (denominator < 1e-10) {
99
+ logger.debug(`[Orchestration] No variance in deviations, cannot compute gradient`);
100
+ return {
101
+ gradient: 0,
102
+ intercept: meanY,
103
+ rSquared: 0,
104
+ sampleSize: n,
105
+ };
106
+ }
107
+
108
+ const gradient = numerator / denominator;
109
+ const intercept = meanY - gradient * meanX;
110
+
111
+ // Compute R-squared
112
+ let ssResidual = 0;
113
+ for (const obs of observations) {
114
+ const w = obs.weight ?? 1.0;
115
+ const predicted = gradient * obs.deviation + intercept;
116
+ const residual = obs.outcomeValue - predicted;
117
+ ssResidual += w * residual * residual;
118
+ }
119
+
120
+ const rSquared = ssTotal > 1e-10 ? 1 - ssResidual / ssTotal : 0;
121
+
122
+ logger.debug(
123
+ `[Orchestration] Computed gradient: ${gradient.toFixed(4)}, ` +
124
+ `intercept: ${intercept.toFixed(4)}, R²: ${rSquared.toFixed(4)}, n=${n}`
125
+ );
126
+
127
+ return {
128
+ gradient,
129
+ intercept,
130
+ rSquared: Math.max(0, Math.min(1, rSquared)), // Clamp to [0,1]
131
+ sampleSize: n,
132
+ };
133
+ }
@@ -0,0 +1,210 @@
1
+ import type { UserDBInterface } from '../interfaces/userDB';
2
+ import type { CourseDBInterface } from '../interfaces/courseDB';
3
+ import type { LearnableWeight } from '../types/contentNavigationStrategy';
4
+ import type { CourseConfig } from '@vue-skuilder/common';
5
+ import { logger } from '../../util/logger';
6
+
7
+ // Re-export gradient and learning functions
8
+ export { aggregateOutcomesForGradient, computeStrategyGradient } from './gradient';
9
+ export {
10
+ updateStrategyWeight,
11
+ updateLearningState,
12
+ runPeriodUpdate,
13
+ getDefaultLearnableWeight,
14
+ } from './learning';
15
+ export type { PeriodUpdateInput, PeriodUpdateResult } from './learning';
16
+
17
+ // Re-export signal functions
18
+ export { computeOutcomeSignal, scoreAccuracyInZone } from './signal';
19
+ export type { SignalConfig } from './signal';
20
+
21
+ // Re-export recording functions
22
+ export { recordUserOutcome } from './recording';
23
+
24
+ // Re-export types
25
+ export type { GradientObservation, GradientResult, StrategyLearningState } from '../types/learningState';
26
+ export type { UserOutcomeRecord } from '../types/userOutcome';
27
+
28
+ // ============================================================================
29
+ // TYPES
30
+ // ============================================================================
31
+
32
+ /**
33
+ * Context for orchestration decisions during a session.
34
+ *
35
+ * Provides access to user/course data and helper methods for determining
36
+ * effective strategy weights based on the user's cohort assignment.
37
+ */
38
+ export interface OrchestrationContext {
39
+ user: UserDBInterface;
40
+ course: CourseDBInterface;
41
+ userId: string;
42
+ courseConfig: CourseConfig;
43
+
44
+ /**
45
+ * Calculate the effective weight for a strategy for this user.
46
+ *
47
+ * Applies deviation based on the user's cohort assignment (derived from
48
+ * userId, strategyId, and course salt).
49
+ *
50
+ * @param strategyId - Unique ID of the strategy
51
+ * @param learnable - The strategy's learning configuration
52
+ * @returns Effective weight multiplier (typically 0.1 - 3.0)
53
+ */
54
+ getEffectiveWeight(strategyId: string, learnable: LearnableWeight): number;
55
+
56
+ /**
57
+ * Get the deviation factor for this user/strategy.
58
+ * Range [-1.0, 1.0].
59
+ */
60
+ getDeviation(strategyId: string): number;
61
+ }
62
+
63
+ // ============================================================================
64
+ // DEVIATION LOGIC
65
+ // ============================================================================
66
+
67
+ const MIN_SPREAD = 0.1;
68
+ const MAX_SPREAD = 0.5;
69
+ const MIN_WEIGHT = 0.1;
70
+ const MAX_WEIGHT = 3.0;
71
+
72
+ /**
73
+ * FNV-1a hash implementation for deterministic distribution.
74
+ */
75
+ function fnv1a(str: string): number {
76
+ let hash = 2166136261;
77
+ for (let i = 0; i < str.length; i++) {
78
+ hash ^= str.charCodeAt(i);
79
+ hash = Math.imul(hash, 16777619);
80
+ }
81
+ return hash >>> 0;
82
+ }
83
+
84
+ /**
85
+ * Compute a user's deviation for a specific strategy.
86
+ *
87
+ * Returns a value in [-1, 1] that is:
88
+ * 1. Deterministic for the same (user, strategy, salt) tuple
89
+ * 2. Uniformly distributed across users
90
+ * 3. Uncorrelated between different strategies (due to strategyId in hash)
91
+ * 4. Rotatable by changing the salt
92
+ *
93
+ * @param userId - ID of the user
94
+ * @param strategyId - ID of the strategy
95
+ * @param salt - Random seed from course config
96
+ * @returns Deviation factor between -1.0 and 1.0
97
+ */
98
+ export function computeDeviation(userId: string, strategyId: string, salt: string): number {
99
+ const input = `${userId}:${strategyId}:${salt}`;
100
+ const hash = fnv1a(input);
101
+
102
+ // Normalize 32-bit unsigned integer to [0, 1]
103
+ const normalized = hash / 4294967296;
104
+
105
+ // Map [0, 1] to [-1, 1]
106
+ return (normalized * 2) - 1;
107
+ }
108
+
109
+ /**
110
+ * Compute the exploration spread based on confidence.
111
+ *
112
+ * - Low confidence (0.0) -> Max spread (Explore broadly)
113
+ * - High confidence (1.0) -> Min spread (Exploit known good weight)
114
+ *
115
+ * @param confidence - Confidence level 0-1
116
+ * @returns Spread magnitude (half-width of the distribution)
117
+ */
118
+ export function computeSpread(confidence: number): number {
119
+ // Linear interpolation: confidence 0 -> MAX_SPREAD, confidence 1 -> MIN_SPREAD
120
+ const clampedConfidence = Math.max(0, Math.min(1, confidence));
121
+ return MAX_SPREAD - (clampedConfidence * (MAX_SPREAD - MIN_SPREAD));
122
+ }
123
+
124
+ /**
125
+ * Calculate the effective weight for a strategy instance.
126
+ *
127
+ * Combines the learnable weight (peak) with the user's deviation and the
128
+ * allowed spread (based on confidence).
129
+ *
130
+ * @param learnable - Strategy learning config
131
+ * @param userId - User ID
132
+ * @param strategyId - Strategy ID
133
+ * @param salt - Course salt
134
+ * @returns Effective weight multiplier
135
+ */
136
+ export function computeEffectiveWeight(
137
+ learnable: LearnableWeight,
138
+ userId: string,
139
+ strategyId: string,
140
+ salt: string
141
+ ): number {
142
+ const deviation = computeDeviation(userId, strategyId, salt);
143
+ const spread = computeSpread(learnable.confidence);
144
+
145
+ // Apply deviation: effective = weight + (deviation * spread * weight)
146
+ // We scale the spread relative to the weight itself so it's proportional.
147
+ // e.g. weight 2.0, deviation -0.5, spread 0.2 -> 2.0 + (-0.5 * 0.2 * 2.0) = 1.8
148
+ const adjustment = deviation * spread * learnable.weight;
149
+
150
+ const effective = learnable.weight + adjustment;
151
+
152
+ // Clamp to sane bounds to prevent runaway weights or negative multipliers
153
+ return Math.max(MIN_WEIGHT, Math.min(MAX_WEIGHT, effective));
154
+ }
155
+
156
+ // ============================================================================
157
+ // CONTEXT FACTORY
158
+ // ============================================================================
159
+
160
+ /**
161
+ * Create an orchestration context for a study session.
162
+ *
163
+ * Fetches necessary configuration to enable deterministic weight calculation.
164
+ *
165
+ * @param user - User DB interface
166
+ * @param course - Course DB interface
167
+ * @returns Initialized orchestration context
168
+ */
169
+ export async function createOrchestrationContext(
170
+ user: UserDBInterface,
171
+ course: CourseDBInterface
172
+ ): Promise<OrchestrationContext> {
173
+ let courseConfig: CourseConfig;
174
+ try {
175
+ courseConfig = await course.getCourseConfig();
176
+ } catch (e) {
177
+ logger.error(`[Orchestration] Failed to load course config: ${e}`);
178
+ // Fallback stub if config load fails
179
+ courseConfig = {
180
+ name: 'Unknown',
181
+ description: '',
182
+ public: false,
183
+ deleted: false,
184
+ creator: '',
185
+ admins: [],
186
+ moderators: [],
187
+ dataShapes: [],
188
+ questionTypes: [],
189
+ orchestration: { salt: 'default' },
190
+ };
191
+ }
192
+
193
+ const userId = user.getUsername(); // Or user ID if available on interface
194
+ const salt = courseConfig.orchestration?.salt || 'default_salt';
195
+
196
+ return {
197
+ user,
198
+ course,
199
+ userId,
200
+ courseConfig,
201
+
202
+ getEffectiveWeight(strategyId: string, learnable: LearnableWeight): number {
203
+ return computeEffectiveWeight(learnable, userId, strategyId, salt);
204
+ },
205
+
206
+ getDeviation(strategyId: string): number {
207
+ return computeDeviation(userId, strategyId, salt);
208
+ }
209
+ };
210
+ }