@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
@@ -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
 
@@ -62,6 +63,11 @@ export interface UserDBReader {
62
63
  * Strategies use this to persist preferences, learned patterns, or temporal
63
64
  * tracking data across sessions. Each strategy owns its own namespace.
64
65
  *
66
+ * @deprecated Use `getCourseInterface(courseId).getStrategyState(strategyKey)` instead.
67
+ * Direct use bypasses course-scoping safety — the courseId parameter is unguarded,
68
+ * allowing accidental cross-course data access. The course-scoped interface binds
69
+ * courseId once at construction.
70
+ *
65
71
  * @param courseId - The course this state applies to
66
72
  * @param strategyKey - Unique key identifying the strategy (typically class name)
67
73
  * @returns The strategy's data payload, or null if no state exists
@@ -151,6 +157,11 @@ export interface UserDBWriter extends DocumentUpdater {
151
157
  * Strategies use this to persist preferences, learned patterns, or temporal
152
158
  * tracking data across sessions. Each strategy owns its own namespace.
153
159
  *
160
+ * @deprecated Use `getCourseInterface(courseId).putStrategyState(strategyKey, data)` instead.
161
+ * Direct use bypasses course-scoping safety — the courseId parameter is unguarded,
162
+ * allowing accidental cross-course data writes. The course-scoped interface binds
163
+ * courseId once at construction.
164
+ *
154
165
  * @param courseId - The course this state applies to
155
166
  * @param strategyKey - Unique key identifying the strategy (typically class name)
156
167
  * @param data - The strategy's data payload to store
@@ -160,10 +171,18 @@ export interface UserDBWriter extends DocumentUpdater {
160
171
  /**
161
172
  * Delete strategy-specific state for a course.
162
173
  *
174
+ * @deprecated Use `getCourseInterface(courseId).deleteStrategyState(strategyKey)` instead.
175
+ * Direct use bypasses course-scoping safety.
176
+ *
163
177
  * @param courseId - The course this state applies to
164
178
  * @param strategyKey - Unique key identifying the strategy (typically class name)
165
179
  */
166
180
  deleteStrategyState(courseId: string, strategyKey: string): Promise<void>;
181
+
182
+ /**
183
+ * Record a user learning outcome for evolutionary orchestration.
184
+ */
185
+ putUserOutcome(record: UserOutcomeRecord): Promise<void>;
167
186
  }
168
187
 
169
188
  /**
@@ -222,6 +241,37 @@ export interface UsrCrsDataInterface {
222
241
  getCourseSettings(): Promise<UserCourseSettings>;
223
242
  updateCourseSettings(updates: UserCourseSetting[]): void; // [ ] return a result of some sort?
224
243
  // getRegistrationDoc(): Promise<CourseRegistration>;
244
+
245
+ /**
246
+ * Get strategy-specific state for this course.
247
+ *
248
+ * Course-scoped alternative to `UserDBInterface.getStrategyState()`.
249
+ * The courseId is bound at construction via `getCourseInterface(courseId)`,
250
+ * so callers cannot accidentally access another course's state.
251
+ *
252
+ * @param strategyKey - Unique key identifying the state document
253
+ * @returns The state payload, or null if no state exists
254
+ */
255
+ getStrategyState<T>(strategyKey: string): Promise<T | null>;
256
+
257
+ /**
258
+ * Store strategy-specific state for this course.
259
+ *
260
+ * Course-scoped alternative to `UserDBInterface.putStrategyState()`.
261
+ *
262
+ * @param strategyKey - Unique key identifying the state document
263
+ * @param data - The state payload to store
264
+ */
265
+ putStrategyState<T>(strategyKey: string, data: T): Promise<void>;
266
+
267
+ /**
268
+ * Delete strategy-specific state for this course.
269
+ *
270
+ * Course-scoped alternative to `UserDBInterface.deleteStrategyState()`.
271
+ *
272
+ * @param strategyKey - Unique key identifying the state document
273
+ */
274
+ deleteStrategyState(strategyKey: string): Promise<void>;
225
275
  }
226
276
 
227
277
  export type ClassroomRegistrationDesignation = 'student' | 'teacher' | 'aide' | 'admin';
@@ -6,6 +6,8 @@ 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';
10
+ import { captureRun, buildRunReport, type GeneratorSummary, type FilterImpact } from './PipelineDebugger';
9
11
 
10
12
  // ============================================================================
11
13
  // PIPELINE LOGGING HELPERS
@@ -51,14 +53,29 @@ function logExecutionSummary(
51
53
  generatedCount: number,
52
54
  filterCount: number,
53
55
  finalCount: number,
54
- topScores: number[]
56
+ topScores: number[],
57
+ filterImpacts: Array<{ name: string; boosted: number; penalized: number; passed: number }>
55
58
  ): void {
56
59
  const scoreDisplay =
57
60
  topScores.length > 0 ? topScores.map((s) => s.toFixed(2)).join(', ') : 'none';
58
61
 
62
+ let filterSummary = '';
63
+ if (filterImpacts.length > 0) {
64
+ const impacts = filterImpacts.map((f) => {
65
+ const parts: string[] = [];
66
+ if (f.boosted > 0) parts.push(`+${f.boosted}`);
67
+ if (f.penalized > 0) parts.push(`-${f.penalized}`);
68
+ if (f.passed > 0) parts.push(`=${f.passed}`);
69
+ return `${f.name}: ${parts.join('/')}`;
70
+ });
71
+ filterSummary = `\n Filter impact: ${impacts.join(', ')}`;
72
+ }
73
+
59
74
  logger.info(
60
75
  `[Pipeline] Execution: ${generatorName} produced ${generatedCount} → ` +
61
- `${filterCount} filters → ${finalCount} results (top scores: ${scoreDisplay})`
76
+ `${filterCount} filters → ${finalCount} results (top scores: ${scoreDisplay})` +
77
+ filterSummary +
78
+ `\n 💡 Inspect: window.skuilder.pipeline`
62
79
  );
63
80
  }
64
81
 
@@ -189,17 +206,63 @@ export class Pipeline extends ContentNavigator {
189
206
  // Get candidates from generator, passing context
190
207
  let cards = await this.generator.getWeightedCards(fetchLimit, context);
191
208
  const generatedCount = cards.length;
209
+
210
+ // Capture generator breakdown for debugging (if CompositeGenerator)
211
+ let generatorSummaries: GeneratorSummary[] | undefined;
212
+ if ((this.generator as any).generators) {
213
+ // This is a CompositeGenerator - extract per-generator info from provenance
214
+ const genMap = new Map<string, { cards: WeightedCard[] }>();
215
+ for (const card of cards) {
216
+ const firstProv = card.provenance[0];
217
+ if (firstProv) {
218
+ const genName = firstProv.strategyName;
219
+ if (!genMap.has(genName)) {
220
+ genMap.set(genName, { cards: [] });
221
+ }
222
+ genMap.get(genName)!.cards.push(card);
223
+ }
224
+ }
225
+ generatorSummaries = Array.from(genMap.entries()).map(([name, data]) => {
226
+ const newCards = data.cards.filter((c) => c.provenance[0]?.reason?.includes('new card'));
227
+ const reviewCards = data.cards.filter((c) => c.provenance[0]?.reason?.includes('review'));
228
+ return {
229
+ name,
230
+ cardCount: data.cards.length,
231
+ newCount: newCards.length,
232
+ reviewCount: reviewCards.length,
233
+ topScore: Math.max(...data.cards.map((c) => c.score), 0),
234
+ };
235
+ });
236
+ }
192
237
 
193
238
  logger.debug(`[Pipeline] Generator returned ${generatedCount} candidates`);
194
239
 
195
240
  // Batch hydrate tags before filters run
196
241
  cards = await this.hydrateTags(cards);
242
+
243
+ // Keep a copy of all cards for debug capture (before filtering removes any)
244
+ const allCardsBeforeFiltering = [...cards];
197
245
 
198
- // Apply filters sequentially
246
+ // Apply filters sequentially, tracking impact
247
+ const filterImpacts: FilterImpact[] = [];
199
248
  for (const filter of this.filters) {
200
249
  const beforeCount = cards.length;
250
+ const beforeScores = new Map(cards.map((c) => [c.cardId, c.score]));
201
251
  cards = await filter.transform(cards, context);
202
- logger.debug(`[Pipeline] Filter '${filter.name}': ${beforeCount} → ${cards.length} cards`);
252
+
253
+ // Count boost/penalize/pass/removed for this filter
254
+ let boosted = 0, penalized = 0, passed = 0;
255
+ const removed = beforeCount - cards.length;
256
+
257
+ for (const card of cards) {
258
+ const before = beforeScores.get(card.cardId) ?? 0;
259
+ if (card.score > before) boosted++;
260
+ else if (card.score < before) penalized++;
261
+ else passed++;
262
+ }
263
+ filterImpacts.push({ name: filter.name, boosted, penalized, passed, removed });
264
+
265
+ logger.debug(`[Pipeline] Filter '${filter.name}': ${beforeScores.size} → ${cards.length} cards (↑${boosted} ↓${penalized} =${passed})`);
203
266
  }
204
267
 
205
268
  // Remove zero-score cards (hard filtered)
@@ -218,12 +281,31 @@ export class Pipeline extends ContentNavigator {
218
281
  generatedCount,
219
282
  this.filters.length,
220
283
  result.length,
221
- topScores
284
+ topScores,
285
+ filterImpacts
222
286
  );
223
287
 
224
288
  // Toggle provenance logging (shows scoring history for top cards):
225
289
  logCardProvenance(result, 3);
226
290
 
291
+ // Capture run for debug API
292
+ try {
293
+ const courseName = await this.course?.getCourseConfig().then((c) => c.name).catch(() => undefined);
294
+ const report = buildRunReport(
295
+ this.course?.getCourseID() || 'unknown',
296
+ courseName,
297
+ this.generator.name,
298
+ generatorSummaries,
299
+ generatedCount,
300
+ filterImpacts,
301
+ allCardsBeforeFiltering,
302
+ result
303
+ );
304
+ captureRun(report);
305
+ } catch (e) {
306
+ logger.debug(`[Pipeline] Failed to capture debug run: ${e}`);
307
+ }
308
+
227
309
  return result;
228
310
  }
229
311
 
@@ -273,10 +355,14 @@ export class Pipeline extends ContentNavigator {
273
355
  logger.debug(`[Pipeline] Could not get user ELO, using default: ${e}`);
274
356
  }
275
357
 
358
+ // Initialize orchestration context (used for evolutionary weighting)
359
+ const orchestration = await createOrchestrationContext(this.user!, this.course!);
360
+
276
361
  return {
277
362
  user: this.user!,
278
363
  course: this.course!,
279
364
  userElo,
365
+ orchestration,
280
366
  };
281
367
  }
282
368
 
@@ -286,4 +372,45 @@ export class Pipeline extends ContentNavigator {
286
372
  getCourseID(): string {
287
373
  return this.course!.getCourseID();
288
374
  }
375
+
376
+ /**
377
+ * Get orchestration context for outcome recording.
378
+ */
379
+ async getOrchestrationContext(): Promise<OrchestrationContext> {
380
+ return createOrchestrationContext(this.user!, this.course!);
381
+ }
382
+
383
+ /**
384
+ * Get IDs of all strategies in this pipeline.
385
+ * Used to record which strategies contributed to an outcome.
386
+ */
387
+ getStrategyIds(): string[] {
388
+ const ids: string[] = [];
389
+
390
+ const extractId = (obj: any): string | null => {
391
+ // Check for strategyId property (ContentNavigator, WeightedFilter)
392
+ if (obj.strategyId) return obj.strategyId;
393
+ return null;
394
+ };
395
+
396
+ // Generator(s)
397
+ const genId = extractId(this.generator);
398
+ if (genId) ids.push(genId);
399
+
400
+ // Inspect CompositeGenerator children (accessing private field via cast)
401
+ if ((this.generator as any).generators && Array.isArray((this.generator as any).generators)) {
402
+ (this.generator as any).generators.forEach((g: any) => {
403
+ const subId = extractId(g);
404
+ if (subId) ids.push(subId);
405
+ });
406
+ }
407
+
408
+ // Filters
409
+ for (const filter of this.filters) {
410
+ const fId = extractId(filter);
411
+ if (fId) ids.push(fId);
412
+ }
413
+
414
+ return [...new Set(ids)];
415
+ }
289
416
  }
@@ -1,13 +1,14 @@
1
1
  import type { ContentNavigationStrategyData } from '../types/contentNavigationStrategy';
2
- import { ContentNavigator, isGenerator, isFilter, Navigators } from './index';
2
+ import { ContentNavigator, isGenerator, isFilter } 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
- import { DocType } from '../types/types-legacy';
7
7
  import { logger } from '../../util/logger';
8
8
  import type { CourseDBInterface } from '../interfaces/courseDB';
9
9
  import type { UserDBInterface } from '../interfaces/userDB';
10
10
  import CompositeGenerator from './generators/CompositeGenerator';
11
+ import { createDefaultEloStrategy, createDefaultSrsStrategy } from './defaults';
11
12
 
12
13
  // ============================================================================
13
14
  // PIPELINE ASSEMBLER
@@ -102,13 +103,15 @@ export class PipelineAssembler {
102
103
  }
103
104
  }
104
105
 
105
- // If no generator but filters exist, use default ELO generator
106
+ // If no generator but filters exist, use default ELO and SRS generators
106
107
  if (generatorStrategies.length === 0) {
107
108
  if (filterStrategies.length > 0) {
108
109
  logger.debug(
109
- '[PipelineAssembler] No generator found, using default ELO with configured filters'
110
+ '[PipelineAssembler] No generator found, using default ELO and SRS with configured filters'
110
111
  );
111
- generatorStrategies.push(this.makeDefaultEloStrategy(course.getCourseID()));
112
+ const courseId = course.getCourseID();
113
+ generatorStrategies.push(createDefaultEloStrategy(courseId));
114
+ generatorStrategies.push(createDefaultSrsStrategy(courseId));
112
115
  } else {
113
116
  warnings.push('No generator strategy found');
114
117
  return {
@@ -149,7 +152,19 @@ export class PipelineAssembler {
149
152
  const nav = await ContentNavigator.create(user, course, filterStrategy);
150
153
  // The navigator implements CardFilter
151
154
  if ('transform' in nav && typeof nav.transform === 'function') {
152
- filters.push(nav as unknown as CardFilter);
155
+ let filter = nav as unknown as CardFilter;
156
+
157
+ // Apply evolutionary weighting wrapper if configured
158
+ if (filterStrategy.learnable) {
159
+ filter = new WeightedFilter(
160
+ filter,
161
+ filterStrategy.learnable,
162
+ filterStrategy.staticWeight,
163
+ filterStrategy._id
164
+ );
165
+ }
166
+
167
+ filters.push(filter);
153
168
  logger.debug(`[PipelineAssembler] Added filter: ${filterStrategy.name}`);
154
169
  } else {
155
170
  warnings.push(
@@ -175,20 +190,4 @@ export class PipelineAssembler {
175
190
  warnings,
176
191
  };
177
192
  }
178
-
179
- /**
180
- * Creates a default ELO generator strategy.
181
- * Used when filters are configured but no generator is specified.
182
- */
183
- private makeDefaultEloStrategy(courseId: string): ContentNavigationStrategyData {
184
- return {
185
- _id: 'NAVIGATION_STRATEGY-ELO-default',
186
- course: courseId,
187
- docType: DocType.NAVIGATION_STRATEGY,
188
- name: 'ELO (default)',
189
- description: 'Default ELO-based generator',
190
- implementingClass: Navigators.ELO,
191
- serializedData: '',
192
- };
193
- }
194
193
  }