@vue-skuilder/db 0.1.30 → 0.1.31-a

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.
@@ -374,45 +374,6 @@ Set `staticWeight: true` for foundational strategies that should not be tuned:
374
374
 
375
375
  ## Creating New Strategies
376
376
 
377
- Strategies can be defined in two places:
378
-
379
- - **Framework-internal:** Added directly to `NavigatorRoles` in `index.ts`. Used
380
- for general-purpose strategies shipped with the framework.
381
- - **Consumer-defined:** Registered at app startup via `registerNavigator()`.
382
- Used for course-specific strategies that live in the consumer codebase.
383
-
384
- Both types participate identically in the pipeline once registered.
385
-
386
- ### Registration
387
-
388
- Framework-internal strategies are listed in the hardcoded `NavigatorRoles` record.
389
- Consumer-defined strategies use the public `registerNavigator()` API:
390
-
391
- ```typescript
392
- import { registerNavigator, NavigatorRole } from '@vue-skuilder/db';
393
- import { MyFilter } from './MyFilter';
394
-
395
- // At app init, before any study session:
396
- registerNavigator('myFilter', MyFilter, NavigatorRole.FILTER);
397
- ```
398
-
399
- The third argument (`role`) is **required** for consumer-defined strategies —
400
- without it, `PipelineAssembler` cannot classify the strategy and will skip it
401
- with a warning. For framework-internal strategies the role is already in
402
- `NavigatorRoles`, so the argument is optional.
403
-
404
- A corresponding `NAVIGATION_STRATEGY` document must exist in CouchDB with
405
- `implementingClass` matching the registered name:
406
-
407
- ```json
408
- {
409
- "_id": "NAVIGATION_STRATEGY-my-filter",
410
- "implementingClass": "myFilter",
411
- "name": "My Filter",
412
- "serializedData": "{}"
413
- }
414
- ```
415
-
416
377
  ### Generator
417
378
 
418
379
  ```typescript
@@ -421,7 +382,7 @@ class MyGenerator extends ContentNavigator implements CardGenerator {
421
382
 
422
383
  async getWeightedCards(limit: number, context?: GeneratorContext): Promise<WeightedCard[]> {
423
384
  const candidates = await this.findCandidates(limit);
424
-
385
+
425
386
  return candidates.map(c => ({
426
387
  cardId: c.id,
427
388
  courseId: this.course.getCourseID(),
@@ -439,6 +400,8 @@ class MyGenerator extends ContentNavigator implements CardGenerator {
439
400
  }
440
401
  ```
441
402
 
403
+ Register in `NavigatorRoles` as `NavigatorRole.GENERATOR`.
404
+
442
405
  ### Filter
443
406
 
444
407
  ```typescript
@@ -450,7 +413,7 @@ class MyFilter extends ContentNavigator implements CardFilter {
450
413
  const multiplier = this.computeMultiplier(card, context);
451
414
  const newScore = card.score * multiplier;
452
415
  const action = multiplier < 1 ? 'penalized' : multiplier > 1 ? 'boosted' : 'passed';
453
-
416
+
454
417
  return {
455
418
  ...card,
456
419
  score: newScore,
@@ -471,29 +434,7 @@ class MyFilter extends ContentNavigator implements CardFilter {
471
434
  }
472
435
  ```
473
436
 
474
- ### Accessing Strategy State from Consumer Filters
475
-
476
- Consumer strategies can share state with other parts of the consumer app via
477
- `getStrategyState()` / `putStrategyState()`. Override `strategyKey` to read
478
- an existing state document:
479
-
480
- ```typescript
481
- class MyFilter extends ContentNavigator implements CardFilter {
482
- // Read the same doc that another part of the app writes
483
- protected get strategyKey(): string {
484
- return 'MySharedStateKey';
485
- }
486
-
487
- async transform(cards: WeightedCard[], context: FilterContext): Promise<WeightedCard[]> {
488
- const state = await this.getStrategyState<MyStateType>();
489
- // ... use state for filtering decisions
490
- }
491
- }
492
- ```
493
-
494
- This enables **single source of truth** patterns: the consumer app writes state
495
- via `UsrCrsDataInterface.putStrategyState()`, and the consumer filter reads it
496
- via the same key. No framework changes needed.
437
+ Register in `NavigatorRoles` as `NavigatorRole.FILTER`.
497
438
 
498
439
  ---
499
440
 
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
7
- "version": "0.1.30",
7
+ "version": "0.1.31-a",
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.30",
51
+ "@vue-skuilder/common": "0.1.31-a",
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.30"
65
+ "stableVersion": "0.1.31a"
66
66
  }
@@ -88,30 +88,41 @@ export default class ELONavigator extends ContentNavigator implements CardGenera
88
88
  const cardIds = newCards.map((c) => c.cardID);
89
89
  const cardEloData = await this.course.getCardEloData(cardIds);
90
90
 
91
- // Score new cards by ELO distance
91
+ // Score new cards by ELO distance, then apply weighted sampling without
92
+ // replacement using the Efraimidis-Spirakis (A-Res) algorithm:
93
+ //
94
+ // key = U ^ (1 / rawScore) where U ~ Uniform(0, 1)
95
+ //
96
+ // Sorting by key descending produces a weighted random sample: high-score
97
+ // cards are still preferred, but cards with equal scores are shuffled
98
+ // uniformly rather than deterministically. This prevents the same failed
99
+ // cards from looping back every session when many cards share similar ELO.
100
+ //
101
+ // Edge case: rawScore=0 → key=0, never selected (correct exclusion).
92
102
  const scored: WeightedCard[] = newCards.map((c, i) => {
93
103
  const cardElo = cardEloData[i]?.global?.score ?? 1000;
94
104
  const distance = Math.abs(cardElo - userGlobalElo);
95
- const score = Math.max(0, 1 - distance / 500);
105
+ const rawScore = Math.max(0, 1 - distance / 500);
106
+ const samplingKey = rawScore > 0 ? Math.random() ** (1 / rawScore) : 0;
96
107
 
97
108
  return {
98
109
  cardId: c.cardID,
99
110
  courseId: c.courseID,
100
- score,
111
+ score: samplingKey,
101
112
  provenance: [
102
113
  {
103
114
  strategy: 'elo',
104
115
  strategyName: this.strategyName || this.name,
105
116
  strategyId: this.strategyId || 'NAVIGATION_STRATEGY-ELO-default',
106
117
  action: 'generated',
107
- score,
108
- reason: `ELO distance ${Math.round(distance)} (card: ${Math.round(cardElo)}, user: ${Math.round(userGlobalElo)}), new card`,
118
+ score: samplingKey,
119
+ reason: `ELO distance ${Math.round(distance)} (card: ${Math.round(cardElo)}, user: ${Math.round(userGlobalElo)}), raw ${rawScore.toFixed(3)}, key ${samplingKey.toFixed(3)}`,
109
120
  },
110
121
  ],
111
122
  };
112
123
  });
113
124
 
114
- // Sort by score descending
125
+ // Sort by sampling key descending (weighted sample without replacement)
115
126
  scored.sort((a, b) => b.score - a.score);
116
127
 
117
128
  const result = scored.slice(0, limit);
@@ -46,20 +46,10 @@ export type NavigatorConstructor = new (
46
46
  ) => ContentNavigator;
47
47
 
48
48
  /**
49
- * Entry in the navigator registry, storing the constructor and an optional
50
- * pipeline role. The role is used by PipelineAssembler to classify
51
- * consumer-registered navigators that aren't in the built-in Navigators enum.
52
- */
53
- interface NavigatorRegistryEntry {
54
- constructor: NavigatorConstructor;
55
- role?: NavigatorRole;
56
- }
57
-
58
- /**
59
- * Registry mapping implementingClass names to navigator entries.
49
+ * Registry mapping implementingClass names to navigator constructors.
60
50
  * Populated by registerNavigator() and used by ContentNavigator.create().
61
51
  */
62
- const navigatorRegistry = new Map<string, NavigatorRegistryEntry>();
52
+ const navigatorRegistry = new Map<string, NavigatorConstructor>();
63
53
 
64
54
  /**
65
55
  * Register a navigator implementation.
@@ -67,21 +57,15 @@ const navigatorRegistry = new Map<string, NavigatorRegistryEntry>();
67
57
  * Call this to make a navigator available for instantiation by
68
58
  * ContentNavigator.create() without relying on dynamic imports.
69
59
  *
70
- * Passing a `role` is optional for built-in navigators (whose roles are in
71
- * the hardcoded `NavigatorRoles` record), but **required** for consumer-
72
- * defined navigators that need to participate in pipeline assembly.
73
- *
74
- * @param implementingClass - The class name (e.g., 'elo', 'letterGatingFilter')
60
+ * @param implementingClass - The class name (e.g., 'elo', 'hierarchyDefinition')
75
61
  * @param constructor - The navigator class constructor
76
- * @param role - Optional pipeline role (GENERATOR or FILTER)
77
62
  */
78
63
  export function registerNavigator(
79
64
  implementingClass: string,
80
- constructor: NavigatorConstructor,
81
- role?: NavigatorRole
65
+ constructor: NavigatorConstructor
82
66
  ): void {
83
- navigatorRegistry.set(implementingClass, { constructor, role });
84
- logger.debug(`[NavigatorRegistry] Registered: ${implementingClass}${role ? ` (${role})` : ''}`);
67
+ navigatorRegistry.set(implementingClass, constructor);
68
+ logger.debug(`[NavigatorRegistry] Registered: ${implementingClass}`);
85
69
  }
86
70
 
87
71
  /**
@@ -91,7 +75,7 @@ export function registerNavigator(
91
75
  * @returns The constructor, or undefined if not registered
92
76
  */
93
77
  export function getRegisteredNavigator(implementingClass: string): NavigatorConstructor | undefined {
94
- return navigatorRegistry.get(implementingClass)?.constructor;
78
+ return navigatorRegistry.get(implementingClass);
95
79
  }
96
80
 
97
81
  /**
@@ -104,16 +88,6 @@ export function hasRegisteredNavigator(implementingClass: string): boolean {
104
88
  return navigatorRegistry.has(implementingClass);
105
89
  }
106
90
 
107
- /**
108
- * Get the registered role for a navigator, if one was provided at registration.
109
- *
110
- * @param implementingClass - The class name to look up
111
- * @returns The role, or undefined if not registered or no role was specified
112
- */
113
- export function getRegisteredNavigatorRole(implementingClass: string): NavigatorRole | undefined {
114
- return navigatorRegistry.get(implementingClass)?.role;
115
- }
116
-
117
91
  /**
118
92
  * Get all registered navigator names.
119
93
  * Useful for debugging and testing.
@@ -397,24 +371,17 @@ export const NavigatorRoles: Record<Navigators, NavigatorRole> = {
397
371
  * @returns true if the navigator is a generator, false otherwise
398
372
  */
399
373
  export function isGenerator(impl: string): boolean {
400
- if (NavigatorRoles[impl as Navigators] === NavigatorRole.GENERATOR) return true;
401
- // Fallback: check the registry for consumer-registered navigators
402
- return getRegisteredNavigatorRole(impl) === NavigatorRole.GENERATOR;
374
+ return NavigatorRoles[impl as Navigators] === NavigatorRole.GENERATOR;
403
375
  }
404
376
 
405
377
  /**
406
378
  * Check if a navigator implementation is a filter.
407
379
  *
408
- * Checks the built-in NavigatorRoles enum first, then falls back to the
409
- * navigator registry for consumer-registered navigators.
410
- *
411
- * @param impl - Navigator implementation name (e.g., 'elo', 'letterGatingFilter')
380
+ * @param impl - Navigator implementation name (e.g., 'elo', 'hierarchyDefinition')
412
381
  * @returns true if the navigator is a filter, false otherwise
413
382
  */
414
383
  export function isFilter(impl: string): boolean {
415
- if (NavigatorRoles[impl as Navigators] === NavigatorRole.FILTER) return true;
416
- // Fallback: check the registry for consumer-registered navigators
417
- return getRegisteredNavigatorRole(impl) === NavigatorRole.FILTER;
384
+ return NavigatorRoles[impl as Navigators] === NavigatorRole.FILTER;
418
385
  }
419
386
 
420
387
  /**