@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.
- package/dist/{contentSource-BP9hznNV.d.ts → contentSource-BmnmvH8C.d.ts} +268 -3
- package/dist/{contentSource-DsJadoBU.d.cts → contentSource-DfBbaLA-.d.cts} +268 -3
- package/dist/core/index.d.cts +310 -6
- package/dist/core/index.d.ts +310 -6
- package/dist/core/index.js +2606 -666
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +2564 -639
- package/dist/core/index.mjs.map +1 -1
- package/dist/{dataLayerProvider-CHYrQ5pB.d.cts → dataLayerProvider-BeRXVMs5.d.cts} +1 -1
- package/dist/{dataLayerProvider-MDTxXq2l.d.ts → dataLayerProvider-CG9GfaAY.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 +2336 -656
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +2316 -631
- 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 +2312 -632
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +2315 -630
- 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 +278 -20
- package/dist/index.d.ts +278 -20
- package/dist/index.js +3603 -720
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3529 -674
- 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 +210 -9
- package/docs/todo-review-urgency-adaptation.md +205 -0
- 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 +50 -0
- package/src/core/navigators/Pipeline.ts +132 -5
- package/src/core/navigators/PipelineAssembler.ts +21 -22
- package/src/core/navigators/PipelineDebugger.ts +426 -0
- 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 +82 -19
- package/src/core/navigators/generators/elo.ts +14 -1
- package/src/core/navigators/generators/srs.ts +146 -18
- package/src/core/navigators/generators/types.ts +4 -0
- package/src/core/navigators/index.ts +203 -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 +107 -0
- package/src/factory.ts +6 -0
- package/src/impl/common/BaseUserDB.ts +16 -0
- package/src/impl/couch/user-course-relDB.ts +12 -0
- package/src/study/MixerDebugger.ts +555 -0
- package/src/study/SessionController.ts +159 -20
- package/src/study/SessionDebugger.ts +442 -0
- package/src/study/SourceMixer.ts +36 -17
- package/src/study/TODO-session-scheduling.md +133 -0
- package/src/study/index.ts +2 -0
- package/src/study/services/EloService.ts +79 -4
- package/src/study/services/ResponseProcessor.ts +130 -72
- package/src/study/services/SrsService.ts +9 -0
- package/tests/core/navigators/Pipeline.test.ts +2 -0
- package/tests/core/navigators/PipelineAssembler.test.ts +4 -4
- 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
|
-
|
|
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
|
|
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
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
|
|
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
|
+
}
|