@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.
Files changed (66) 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 +36 -11
  26. package/dist/index.d.ts +36 -11
  27. package/dist/index.js +2410 -806
  28. package/dist/index.js.map +1 -1
  29. package/dist/index.mjs +2112 -529
  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 +107 -0
  62. package/src/factory.ts +6 -0
  63. package/src/impl/common/BaseUserDB.ts +16 -0
  64. package/src/study/SessionController.ts +64 -1
  65. package/tests/core/navigators/Pipeline.test.ts +2 -0
  66. 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.23",
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.23",
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.23"
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
- filters.push(nav as unknown as CardFilter);
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
- const byCardId = new Map<string, WeightedCard[]>();
118
- for (const cards of results) {
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 [, cards] of byCardId) {
129
- const aggregatedScore = this.aggregateScores(cards);
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(cards, finalScore);
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(cards: WeightedCard[], finalScore: number): string {
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
- return `Single generator, score ${finalScore.toFixed(2)}`;
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 `Average of ${count} generators (${strategies}): scores [${scores}] → ${finalScore.toFixed(2)}`;
214
+ return `Weighted Avg of ${count} generators (${strategies}): scores [${scores}] → ${finalScore.toFixed(2)}`;
184
215
 
185
216
  case AggregationMode.FREQUENCY_BOOST: {
186
- const avg = cards.reduce((sum, c) => sum + c.score, 0) / count;
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(cards: WeightedCard[]): number {
200
- const scores = cards.map((c) => c.score);
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
- return scores.reduce((sum, s) => sum + s, 0) / scores.length;
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 avg = scores.reduce((sum, s) => sum + s, 0) / scores.length;
211
- const frequencyBoost = 1 + FREQUENCY_BOOST_FACTOR * (cards.length - 1);
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