@vue-skuilder/db 0.1.23 → 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 +36 -11
- package/dist/index.d.ts +36 -11
- package/dist/index.js +2410 -806
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2112 -529
- 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 +107 -0
- package/src/factory.ts +6 -0
- package/src/impl/common/BaseUserDB.ts +16 -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
|
@@ -53,7 +53,6 @@ interface HierarchyConfig {
|
|
|
53
53
|
prerequisites: {
|
|
54
54
|
[tagId: string]: TagPrerequisite[];
|
|
55
55
|
};
|
|
56
|
-
delegateStrategy?: string; // default: "elo"
|
|
57
56
|
}
|
|
58
57
|
|
|
59
58
|
interface TagPrerequisite {
|
|
@@ -75,8 +74,7 @@ interface TagPrerequisite {
|
|
|
75
74
|
"blends": [
|
|
76
75
|
{ "tag": "cvc-words", "masteryThreshold": { "minCount": 20 } }
|
|
77
76
|
]
|
|
78
|
-
}
|
|
79
|
-
"delegateStrategy": "elo"
|
|
77
|
+
}
|
|
80
78
|
}
|
|
81
79
|
```
|
|
82
80
|
|
|
@@ -99,7 +97,6 @@ interface InterferenceConfig {
|
|
|
99
97
|
minElapsedDays?: number;
|
|
100
98
|
};
|
|
101
99
|
defaultDecay?: number;
|
|
102
|
-
delegateStrategy?: string;
|
|
103
100
|
}
|
|
104
101
|
|
|
105
102
|
interface InterferenceGroup {
|
|
@@ -137,7 +134,6 @@ interface RelativePriorityConfig {
|
|
|
137
134
|
defaultPriority?: number;
|
|
138
135
|
combineMode?: 'max' | 'average' | 'min';
|
|
139
136
|
priorityInfluence?: number;
|
|
140
|
-
delegateStrategy?: string;
|
|
141
137
|
}
|
|
142
138
|
```
|
|
143
139
|
|
|
@@ -398,4 +394,10 @@ The NavigationStrategy editor is accessible in:
|
|
|
398
394
|
- `packages/db/src/core/navigators/hierarchyDefinition.ts` — Config interface reference
|
|
399
395
|
- `packages/db/src/core/navigators/interferenceMitigator.ts` — Config interface reference
|
|
400
396
|
- `packages/db/src/core/navigators/relativePriority.ts` — Config interface reference
|
|
401
|
-
- `packages/edit-ui/src/components/NavigationStrategy/` — UI components (all forms)
|
|
397
|
+
- `packages/edit-ui/src/components/NavigationStrategy/` — UI components (all forms)
|
|
398
|
+
|
|
399
|
+
## Related Documentation
|
|
400
|
+
|
|
401
|
+
- `navigators-architecture.md` — Pipeline architecture and strategy framework
|
|
402
|
+
- `future-orchestration-vision.md` — Long-term adaptive strategy vision
|
|
403
|
+
- `devlog/1032-orchestrator` — Evolutionary orchestration implementation details
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
7
|
-
"version": "0.1.
|
|
7
|
+
"version": "0.1.24",
|
|
8
8
|
"description": "Database layer for vue-skuilder",
|
|
9
9
|
"main": "dist/index.js",
|
|
10
10
|
"module": "dist/index.mjs",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"@nilock2/pouchdb-authentication": "^1.0.2",
|
|
51
|
-
"@vue-skuilder/common": "0.1.
|
|
51
|
+
"@vue-skuilder/common": "0.1.24",
|
|
52
52
|
"cross-fetch": "^4.1.0",
|
|
53
53
|
"moment": "^2.29.4",
|
|
54
54
|
"pouchdb": "^9.0.0",
|
|
@@ -62,5 +62,5 @@
|
|
|
62
62
|
"vite": "^7.0.0",
|
|
63
63
|
"vitest": "^4.0.15"
|
|
64
64
|
},
|
|
65
|
-
"stableVersion": "0.1.
|
|
65
|
+
"stableVersion": "0.1.24"
|
|
66
66
|
}
|
package/src/core/index.ts
CHANGED
|
@@ -4,7 +4,9 @@ export * from './interfaces';
|
|
|
4
4
|
export * from './types/types-legacy';
|
|
5
5
|
export * from './types/user';
|
|
6
6
|
export * from './types/strategyState';
|
|
7
|
+
export * from './types/userOutcome';
|
|
7
8
|
export * from '../util/Loggable';
|
|
8
9
|
export * from './util';
|
|
9
10
|
export * from './navigators';
|
|
10
11
|
export * from './bulkImport';
|
|
12
|
+
export * from './orchestration';
|
|
@@ -4,6 +4,7 @@ import { StudentClassroomDB } from '../../impl/couch/classroomDB';
|
|
|
4
4
|
import { WeightedCard } from '../navigators';
|
|
5
5
|
import { TagFilter, hasActiveFilter } from '@vue-skuilder/common';
|
|
6
6
|
import { TagFilteredContentSource } from '../../study/TagFilteredContentSource';
|
|
7
|
+
import { OrchestrationContext } from '../orchestration';
|
|
7
8
|
|
|
8
9
|
export type StudySessionFailedItem = StudySessionFailedNewItem | StudySessionFailedReviewItem;
|
|
9
10
|
|
|
@@ -72,6 +73,12 @@ export interface StudyContentSource {
|
|
|
72
73
|
* @returns Cards sorted by score descending
|
|
73
74
|
*/
|
|
74
75
|
getWeightedCards(limit: number): Promise<WeightedCard[]>;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Get the orchestration context for this source.
|
|
79
|
+
* Used for recording learning outcomes.
|
|
80
|
+
*/
|
|
81
|
+
getOrchestrationContext?(): Promise<OrchestrationContext>;
|
|
75
82
|
}
|
|
76
83
|
// #endregion docs_StudyContentSource
|
|
77
84
|
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
import { CourseElo, Status } from '@vue-skuilder/common';
|
|
8
8
|
import { Moment } from 'moment';
|
|
9
9
|
import { CardHistory, CardRecord, QualifiedCardID } from '../types/types-legacy';
|
|
10
|
+
import { UserOutcomeRecord } from '../types/userOutcome';
|
|
10
11
|
import { UserConfig } from '../types/user';
|
|
11
12
|
import { DocumentUpdater } from '@db/study';
|
|
12
13
|
|
|
@@ -164,6 +165,11 @@ export interface UserDBWriter extends DocumentUpdater {
|
|
|
164
165
|
* @param strategyKey - Unique key identifying the strategy (typically class name)
|
|
165
166
|
*/
|
|
166
167
|
deleteStrategyState(courseId: string, strategyKey: string): Promise<void>;
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Record a user learning outcome for evolutionary orchestration.
|
|
171
|
+
*/
|
|
172
|
+
putUserOutcome(record: UserOutcomeRecord): Promise<void>;
|
|
167
173
|
}
|
|
168
174
|
|
|
169
175
|
/**
|
|
@@ -6,6 +6,7 @@ import type { WeightedCard } from './index';
|
|
|
6
6
|
import type { CardFilter, FilterContext } from './filters/types';
|
|
7
7
|
import type { CardGenerator, GeneratorContext } from './generators/types';
|
|
8
8
|
import { logger } from '../../util/logger';
|
|
9
|
+
import { createOrchestrationContext, OrchestrationContext } from '../orchestration';
|
|
9
10
|
|
|
10
11
|
// ============================================================================
|
|
11
12
|
// PIPELINE LOGGING HELPERS
|
|
@@ -273,10 +274,14 @@ export class Pipeline extends ContentNavigator {
|
|
|
273
274
|
logger.debug(`[Pipeline] Could not get user ELO, using default: ${e}`);
|
|
274
275
|
}
|
|
275
276
|
|
|
277
|
+
// Initialize orchestration context (used for evolutionary weighting)
|
|
278
|
+
const orchestration = await createOrchestrationContext(this.user!, this.course!);
|
|
279
|
+
|
|
276
280
|
return {
|
|
277
281
|
user: this.user!,
|
|
278
282
|
course: this.course!,
|
|
279
283
|
userElo,
|
|
284
|
+
orchestration,
|
|
280
285
|
};
|
|
281
286
|
}
|
|
282
287
|
|
|
@@ -286,4 +291,45 @@ export class Pipeline extends ContentNavigator {
|
|
|
286
291
|
getCourseID(): string {
|
|
287
292
|
return this.course!.getCourseID();
|
|
288
293
|
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Get orchestration context for outcome recording.
|
|
297
|
+
*/
|
|
298
|
+
async getOrchestrationContext(): Promise<OrchestrationContext> {
|
|
299
|
+
return createOrchestrationContext(this.user!, this.course!);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Get IDs of all strategies in this pipeline.
|
|
304
|
+
* Used to record which strategies contributed to an outcome.
|
|
305
|
+
*/
|
|
306
|
+
getStrategyIds(): string[] {
|
|
307
|
+
const ids: string[] = [];
|
|
308
|
+
|
|
309
|
+
const extractId = (obj: any): string | null => {
|
|
310
|
+
// Check for strategyId property (ContentNavigator, WeightedFilter)
|
|
311
|
+
if (obj.strategyId) return obj.strategyId;
|
|
312
|
+
return null;
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
// Generator(s)
|
|
316
|
+
const genId = extractId(this.generator);
|
|
317
|
+
if (genId) ids.push(genId);
|
|
318
|
+
|
|
319
|
+
// Inspect CompositeGenerator children (accessing private field via cast)
|
|
320
|
+
if ((this.generator as any).generators && Array.isArray((this.generator as any).generators)) {
|
|
321
|
+
(this.generator as any).generators.forEach((g: any) => {
|
|
322
|
+
const subId = extractId(g);
|
|
323
|
+
if (subId) ids.push(subId);
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Filters
|
|
328
|
+
for (const filter of this.filters) {
|
|
329
|
+
const fId = extractId(filter);
|
|
330
|
+
if (fId) ids.push(fId);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return [...new Set(ids)];
|
|
334
|
+
}
|
|
289
335
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ContentNavigationStrategyData } from '../types/contentNavigationStrategy';
|
|
2
2
|
import { ContentNavigator, isGenerator, isFilter, Navigators } from './index';
|
|
3
3
|
import type { CardFilter } from './filters/types';
|
|
4
|
+
import { WeightedFilter } from './filters/WeightedFilter';
|
|
4
5
|
import type { CardGenerator } from './generators/types';
|
|
5
6
|
import { Pipeline } from './Pipeline';
|
|
6
7
|
import { DocType } from '../types/types-legacy';
|
|
@@ -149,7 +150,19 @@ export class PipelineAssembler {
|
|
|
149
150
|
const nav = await ContentNavigator.create(user, course, filterStrategy);
|
|
150
151
|
// The navigator implements CardFilter
|
|
151
152
|
if ('transform' in nav && typeof nav.transform === 'function') {
|
|
152
|
-
|
|
153
|
+
let filter = nav as unknown as CardFilter;
|
|
154
|
+
|
|
155
|
+
// Apply evolutionary weighting wrapper if configured
|
|
156
|
+
if (filterStrategy.learnable) {
|
|
157
|
+
filter = new WeightedFilter(
|
|
158
|
+
filter,
|
|
159
|
+
filterStrategy.learnable,
|
|
160
|
+
filterStrategy.staticWeight,
|
|
161
|
+
filterStrategy._id
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
filters.push(filter);
|
|
153
166
|
logger.debug(`[PipelineAssembler] Added filter: ${filterStrategy.name}`);
|
|
154
167
|
} else {
|
|
155
168
|
warnings.push(
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { CardFilter, FilterContext } from './types';
|
|
2
|
+
import { WeightedCard } from '../index';
|
|
3
|
+
import { LearnableWeight, DEFAULT_LEARNABLE_WEIGHT } from '../../types/contentNavigationStrategy';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Wraps a CardFilter to apply evolutionary weighting to its effects.
|
|
7
|
+
*
|
|
8
|
+
* If a filter applies a multiplier M (score * M), and the strategy has
|
|
9
|
+
* an effective weight W, the final multiplier becomes M^W.
|
|
10
|
+
*
|
|
11
|
+
* - W=1.0: Original behavior
|
|
12
|
+
* - W>1.0: Amplifies the filter's opinion (stronger boost/penalty)
|
|
13
|
+
* - W<1.0: Dampens the filter's opinion (weaker boost/penalty)
|
|
14
|
+
* - W=0.0: Nullifies the filter (identity)
|
|
15
|
+
*
|
|
16
|
+
* This wrapper handles the math of scaling the filter's impact and updating
|
|
17
|
+
* the provenance trail with the effective weight used.
|
|
18
|
+
*/
|
|
19
|
+
export class WeightedFilter implements CardFilter {
|
|
20
|
+
public name: string;
|
|
21
|
+
private inner: CardFilter;
|
|
22
|
+
private learnable: LearnableWeight;
|
|
23
|
+
private staticWeight: boolean;
|
|
24
|
+
private strategyId?: string;
|
|
25
|
+
|
|
26
|
+
constructor(
|
|
27
|
+
inner: CardFilter,
|
|
28
|
+
learnable: LearnableWeight = DEFAULT_LEARNABLE_WEIGHT,
|
|
29
|
+
staticWeight: boolean = false,
|
|
30
|
+
strategyId?: string
|
|
31
|
+
) {
|
|
32
|
+
this.inner = inner;
|
|
33
|
+
this.name = inner.name;
|
|
34
|
+
this.learnable = learnable;
|
|
35
|
+
this.staticWeight = staticWeight;
|
|
36
|
+
this.strategyId = strategyId;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Apply the inner filter, then scale its effect by the configured weight.
|
|
41
|
+
*/
|
|
42
|
+
async transform(cards: WeightedCard[], context: FilterContext): Promise<WeightedCard[]> {
|
|
43
|
+
// ========================================================================
|
|
44
|
+
// 1. DETERMINE EFFECTIVE WEIGHT
|
|
45
|
+
// ========================================================================
|
|
46
|
+
|
|
47
|
+
// Determine effective weight using orchestration context if available
|
|
48
|
+
let effectiveWeight = this.learnable.weight;
|
|
49
|
+
let deviation: number | undefined;
|
|
50
|
+
|
|
51
|
+
if (!this.staticWeight && context.orchestration) {
|
|
52
|
+
// ContentNavigator instances have a strategyId property (protected/private)
|
|
53
|
+
// We assume inner filter is a ContentNavigator or has a strategyId property.
|
|
54
|
+
// Fallback to name if not present.
|
|
55
|
+
const strategyId = this.strategyId || (this.inner as any).strategyId || this.name;
|
|
56
|
+
effectiveWeight = context.orchestration.getEffectiveWeight(strategyId, this.learnable);
|
|
57
|
+
deviation = context.orchestration.getDeviation(strategyId);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Optimization: If weight is 1.0, the scaling is an identity operation.
|
|
61
|
+
// Just run the inner filter directly.
|
|
62
|
+
if (Math.abs(effectiveWeight - 1.0) < 0.001) {
|
|
63
|
+
return this.inner.transform(cards, context);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ========================================================================
|
|
67
|
+
// 2. CAPTURE STATE BEFORE FILTER
|
|
68
|
+
// ========================================================================
|
|
69
|
+
|
|
70
|
+
// We need original scores to calculate what the filter did (M = new/old)
|
|
71
|
+
const originalScores = new Map<string, number>();
|
|
72
|
+
for (const card of cards) {
|
|
73
|
+
originalScores.set(card.cardId, card.score);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ========================================================================
|
|
77
|
+
// 3. RUN INNER FILTER
|
|
78
|
+
// ========================================================================
|
|
79
|
+
|
|
80
|
+
const transformedCards = await this.inner.transform(cards, context);
|
|
81
|
+
|
|
82
|
+
// ========================================================================
|
|
83
|
+
// 4. APPLY WEIGHT SCALING
|
|
84
|
+
// ========================================================================
|
|
85
|
+
|
|
86
|
+
return transformedCards.map((card) => {
|
|
87
|
+
const originalScore = originalScores.get(card.cardId);
|
|
88
|
+
|
|
89
|
+
// Edge cases where we can't or shouldn't scale:
|
|
90
|
+
// - Original score missing (shouldn't happen)
|
|
91
|
+
// - Original score 0 (was already excluded)
|
|
92
|
+
// - New score 0 (filter excluded it / vetoed) - we treat vetoes as absolute
|
|
93
|
+
if (originalScore === undefined || originalScore === 0 || card.score === 0) {
|
|
94
|
+
return card;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Calculate raw effect multiplier: M = new / old
|
|
98
|
+
const rawEffect = card.score / originalScore;
|
|
99
|
+
|
|
100
|
+
// If filter didn't change this card, nothing to scale
|
|
101
|
+
if (Math.abs(rawEffect - 1.0) < 0.0001) {
|
|
102
|
+
return card;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Apply weight: scaled = M ^ W
|
|
106
|
+
// Example: 0.5 penalty ^ 2.0 weight = 0.25 (stronger penalty)
|
|
107
|
+
// Example: 0.5 penalty ^ 0.5 weight = 0.707 (weaker penalty)
|
|
108
|
+
const weightedEffect = Math.pow(rawEffect, effectiveWeight);
|
|
109
|
+
const newScore = originalScore * weightedEffect;
|
|
110
|
+
|
|
111
|
+
// Update provenance
|
|
112
|
+
// The inner filter just added the last entry. We need to update it
|
|
113
|
+
// to reflect the weighted score and record the effective weight.
|
|
114
|
+
const lastProvIndex = card.provenance.length - 1;
|
|
115
|
+
const lastProv = card.provenance[lastProvIndex];
|
|
116
|
+
|
|
117
|
+
if (lastProv) {
|
|
118
|
+
const updatedProvenance = [...card.provenance];
|
|
119
|
+
updatedProvenance[lastProvIndex] = {
|
|
120
|
+
...lastProv,
|
|
121
|
+
score: newScore,
|
|
122
|
+
effectiveWeight: effectiveWeight,
|
|
123
|
+
deviation: deviation,
|
|
124
|
+
// We can optionally append to the reason, but the structured field is key
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
...card,
|
|
129
|
+
score: newScore,
|
|
130
|
+
provenance: updatedProvenance,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Fallback if no provenance found (rare)
|
|
135
|
+
return {
|
|
136
|
+
...card,
|
|
137
|
+
score: newScore,
|
|
138
|
+
};
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { WeightedCard } from '../index';
|
|
2
2
|
import type { CourseDBInterface } from '../../interfaces/courseDB';
|
|
3
3
|
import type { UserDBInterface } from '../../interfaces/userDB';
|
|
4
|
+
import type { OrchestrationContext } from '../../orchestration';
|
|
4
5
|
|
|
5
6
|
// ============================================================================
|
|
6
7
|
// CARD FILTER INTERFACE
|
|
@@ -40,6 +41,9 @@ export interface FilterContext {
|
|
|
40
41
|
/** User's global ELO score for this course */
|
|
41
42
|
userElo: number;
|
|
42
43
|
|
|
44
|
+
/** Orchestration context for evolutionary weighting */
|
|
45
|
+
orchestration?: OrchestrationContext;
|
|
46
|
+
|
|
43
47
|
// Future extensions:
|
|
44
48
|
// - hydrated tags for all cards (batch lookup)
|
|
45
49
|
// - user's tag-level ELO data
|
|
@@ -113,20 +113,45 @@ export default class CompositeGenerator extends ContentNavigator implements Card
|
|
|
113
113
|
this.generators.map((g) => g.getWeightedCards(limit, context))
|
|
114
114
|
);
|
|
115
115
|
|
|
116
|
-
// Group by cardId
|
|
117
|
-
|
|
118
|
-
|
|
116
|
+
// Group by cardId, tracking the weight of the generator that produced each instance
|
|
117
|
+
type WeightedResult = { card: WeightedCard; weight: number };
|
|
118
|
+
const byCardId = new Map<string, WeightedResult[]>();
|
|
119
|
+
|
|
120
|
+
results.forEach((cards, index) => {
|
|
121
|
+
// Access learnable weight if available
|
|
122
|
+
const gen = this.generators[index] as unknown as ContentNavigator;
|
|
123
|
+
|
|
124
|
+
// Determine effective weight
|
|
125
|
+
let weight = gen.learnable?.weight ?? 1.0;
|
|
126
|
+
let deviation: number | undefined;
|
|
127
|
+
|
|
128
|
+
if (gen.learnable && !gen.staticWeight && context.orchestration) {
|
|
129
|
+
// Access strategyId (protected field) via type assertion
|
|
130
|
+
const strategyId = (gen as any).strategyId;
|
|
131
|
+
if (strategyId) {
|
|
132
|
+
weight = context.orchestration.getEffectiveWeight(strategyId, gen.learnable);
|
|
133
|
+
deviation = context.orchestration.getDeviation(strategyId);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
119
137
|
for (const card of cards) {
|
|
138
|
+
// Record effective weight in provenance for transparency
|
|
139
|
+
if (card.provenance.length > 0) {
|
|
140
|
+
card.provenance[0].effectiveWeight = weight;
|
|
141
|
+
card.provenance[0].deviation = deviation;
|
|
142
|
+
}
|
|
143
|
+
|
|
120
144
|
const existing = byCardId.get(card.cardId) || [];
|
|
121
|
-
existing.push(card);
|
|
145
|
+
existing.push({ card, weight });
|
|
122
146
|
byCardId.set(card.cardId, existing);
|
|
123
147
|
}
|
|
124
|
-
}
|
|
148
|
+
});
|
|
125
149
|
|
|
126
150
|
// Aggregate scores
|
|
127
151
|
const merged: WeightedCard[] = [];
|
|
128
|
-
for (const [,
|
|
129
|
-
const
|
|
152
|
+
for (const [, items] of byCardId) {
|
|
153
|
+
const cards = items.map((i) => i.card);
|
|
154
|
+
const aggregatedScore = this.aggregateScores(items);
|
|
130
155
|
const finalScore = Math.min(1.0, aggregatedScore); // Clamp to [0, 1]
|
|
131
156
|
|
|
132
157
|
// Merge provenance from all generators that produced this card
|
|
@@ -138,7 +163,7 @@ export default class CompositeGenerator extends ContentNavigator implements Card
|
|
|
138
163
|
finalScore > initialScore ? 'boosted' : finalScore < initialScore ? 'penalized' : 'passed';
|
|
139
164
|
|
|
140
165
|
// Build reason explaining the aggregation
|
|
141
|
-
const reason = this.buildAggregationReason(
|
|
166
|
+
const reason = this.buildAggregationReason(items, finalScore);
|
|
142
167
|
|
|
143
168
|
// Append composite provenance entry
|
|
144
169
|
merged.push({
|
|
@@ -165,12 +190,18 @@ export default class CompositeGenerator extends ContentNavigator implements Card
|
|
|
165
190
|
/**
|
|
166
191
|
* Build human-readable reason for score aggregation.
|
|
167
192
|
*/
|
|
168
|
-
private buildAggregationReason(
|
|
193
|
+
private buildAggregationReason(
|
|
194
|
+
items: { card: WeightedCard; weight: number }[],
|
|
195
|
+
finalScore: number
|
|
196
|
+
): string {
|
|
197
|
+
const cards = items.map((i) => i.card);
|
|
169
198
|
const count = cards.length;
|
|
170
199
|
const scores = cards.map((c) => c.score.toFixed(2)).join(', ');
|
|
171
200
|
|
|
172
201
|
if (count === 1) {
|
|
173
|
-
|
|
202
|
+
const weightMsg =
|
|
203
|
+
Math.abs(items[0].weight - 1.0) > 0.001 ? ` (w=${items[0].weight.toFixed(2)})` : '';
|
|
204
|
+
return `Single generator, score ${finalScore.toFixed(2)}${weightMsg}`;
|
|
174
205
|
}
|
|
175
206
|
|
|
176
207
|
const strategies = cards.map((c) => c.provenance[0]?.strategy || 'unknown').join(', ');
|
|
@@ -180,12 +211,16 @@ export default class CompositeGenerator extends ContentNavigator implements Card
|
|
|
180
211
|
return `Max of ${count} generators (${strategies}): scores [${scores}] → ${finalScore.toFixed(2)}`;
|
|
181
212
|
|
|
182
213
|
case AggregationMode.AVERAGE:
|
|
183
|
-
return `
|
|
214
|
+
return `Weighted Avg of ${count} generators (${strategies}): scores [${scores}] → ${finalScore.toFixed(2)}`;
|
|
184
215
|
|
|
185
216
|
case AggregationMode.FREQUENCY_BOOST: {
|
|
186
|
-
|
|
217
|
+
// Recalculate basic weighted avg for display
|
|
218
|
+
const totalWeight = items.reduce((sum, i) => sum + i.weight, 0);
|
|
219
|
+
const weightedSum = items.reduce((sum, i) => sum + i.card.score * i.weight, 0);
|
|
220
|
+
const avg = totalWeight > 0 ? weightedSum / totalWeight : 0;
|
|
221
|
+
|
|
187
222
|
const boost = 1 + FREQUENCY_BOOST_FACTOR * (count - 1);
|
|
188
|
-
return `Frequency boost from ${count} generators (${strategies}): avg ${avg.toFixed(2)} × ${boost.toFixed(2)} → ${finalScore.toFixed(2)}`;
|
|
223
|
+
return `Frequency boost from ${count} generators (${strategies}): w-avg ${avg.toFixed(2)} × ${boost.toFixed(2)} → ${finalScore.toFixed(2)}`;
|
|
189
224
|
}
|
|
190
225
|
|
|
191
226
|
default:
|
|
@@ -196,19 +231,26 @@ export default class CompositeGenerator extends ContentNavigator implements Card
|
|
|
196
231
|
/**
|
|
197
232
|
* Aggregate scores from multiple generators for the same card.
|
|
198
233
|
*/
|
|
199
|
-
private aggregateScores(
|
|
200
|
-
const scores =
|
|
234
|
+
private aggregateScores(items: { card: WeightedCard; weight: number }[]): number {
|
|
235
|
+
const scores = items.map((i) => i.card.score);
|
|
201
236
|
|
|
202
237
|
switch (this.aggregationMode) {
|
|
203
238
|
case AggregationMode.MAX:
|
|
204
239
|
return Math.max(...scores);
|
|
205
240
|
|
|
206
|
-
case AggregationMode.AVERAGE:
|
|
207
|
-
|
|
241
|
+
case AggregationMode.AVERAGE: {
|
|
242
|
+
const totalWeight = items.reduce((sum, i) => sum + i.weight, 0);
|
|
243
|
+
if (totalWeight === 0) return 0;
|
|
244
|
+
const weightedSum = items.reduce((sum, i) => sum + i.card.score * i.weight, 0);
|
|
245
|
+
return weightedSum / totalWeight;
|
|
246
|
+
}
|
|
208
247
|
|
|
209
248
|
case AggregationMode.FREQUENCY_BOOST: {
|
|
210
|
-
const
|
|
211
|
-
const
|
|
249
|
+
const totalWeight = items.reduce((sum, i) => sum + i.weight, 0);
|
|
250
|
+
const weightedSum = items.reduce((sum, i) => sum + i.card.score * i.weight, 0);
|
|
251
|
+
const avg = totalWeight > 0 ? weightedSum / totalWeight : 0;
|
|
252
|
+
|
|
253
|
+
const frequencyBoost = 1 + FREQUENCY_BOOST_FACTOR * (items.length - 1);
|
|
212
254
|
return avg * frequencyBoost;
|
|
213
255
|
}
|
|
214
256
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { WeightedCard } from '../index';
|
|
2
2
|
import type { CourseDBInterface } from '../../interfaces/courseDB';
|
|
3
3
|
import type { UserDBInterface } from '../../interfaces/userDB';
|
|
4
|
+
import type { OrchestrationContext } from '../../orchestration';
|
|
4
5
|
|
|
5
6
|
// ============================================================================
|
|
6
7
|
// CARD GENERATOR INTERFACE
|
|
@@ -34,6 +35,9 @@ export interface GeneratorContext {
|
|
|
34
35
|
/** User's global ELO score for this course */
|
|
35
36
|
userElo: number;
|
|
36
37
|
|
|
38
|
+
/** Orchestration context for evolutionary weighting */
|
|
39
|
+
orchestration?: OrchestrationContext;
|
|
40
|
+
|
|
37
41
|
// Future extensions:
|
|
38
42
|
// - user's tag-level ELO data
|
|
39
43
|
// - course config
|