@vue-skuilder/db 0.1.29 → 0.1.30

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,6 +374,45 @@ 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
+
377
416
  ### Generator
378
417
 
379
418
  ```typescript
@@ -382,7 +421,7 @@ class MyGenerator extends ContentNavigator implements CardGenerator {
382
421
 
383
422
  async getWeightedCards(limit: number, context?: GeneratorContext): Promise<WeightedCard[]> {
384
423
  const candidates = await this.findCandidates(limit);
385
-
424
+
386
425
  return candidates.map(c => ({
387
426
  cardId: c.id,
388
427
  courseId: this.course.getCourseID(),
@@ -400,8 +439,6 @@ class MyGenerator extends ContentNavigator implements CardGenerator {
400
439
  }
401
440
  ```
402
441
 
403
- Register in `NavigatorRoles` as `NavigatorRole.GENERATOR`.
404
-
405
442
  ### Filter
406
443
 
407
444
  ```typescript
@@ -413,7 +450,7 @@ class MyFilter extends ContentNavigator implements CardFilter {
413
450
  const multiplier = this.computeMultiplier(card, context);
414
451
  const newScore = card.score * multiplier;
415
452
  const action = multiplier < 1 ? 'penalized' : multiplier > 1 ? 'boosted' : 'passed';
416
-
453
+
417
454
  return {
418
455
  ...card,
419
456
  score: newScore,
@@ -434,7 +471,29 @@ class MyFilter extends ContentNavigator implements CardFilter {
434
471
  }
435
472
  ```
436
473
 
437
- Register in `NavigatorRoles` as `NavigatorRole.FILTER`.
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.
438
497
 
439
498
  ---
440
499
 
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
7
- "version": "0.1.29",
7
+ "version": "0.1.30",
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.29",
51
+ "@vue-skuilder/common": "0.1.30",
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.29"
65
+ "stableVersion": "0.1.30"
66
66
  }
@@ -46,10 +46,20 @@ export type NavigatorConstructor = new (
46
46
  ) => ContentNavigator;
47
47
 
48
48
  /**
49
- * Registry mapping implementingClass names to navigator constructors.
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.
50
60
  * Populated by registerNavigator() and used by ContentNavigator.create().
51
61
  */
52
- const navigatorRegistry = new Map<string, NavigatorConstructor>();
62
+ const navigatorRegistry = new Map<string, NavigatorRegistryEntry>();
53
63
 
54
64
  /**
55
65
  * Register a navigator implementation.
@@ -57,15 +67,21 @@ const navigatorRegistry = new Map<string, NavigatorConstructor>();
57
67
  * Call this to make a navigator available for instantiation by
58
68
  * ContentNavigator.create() without relying on dynamic imports.
59
69
  *
60
- * @param implementingClass - The class name (e.g., 'elo', 'hierarchyDefinition')
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')
61
75
  * @param constructor - The navigator class constructor
76
+ * @param role - Optional pipeline role (GENERATOR or FILTER)
62
77
  */
63
78
  export function registerNavigator(
64
79
  implementingClass: string,
65
- constructor: NavigatorConstructor
80
+ constructor: NavigatorConstructor,
81
+ role?: NavigatorRole
66
82
  ): void {
67
- navigatorRegistry.set(implementingClass, constructor);
68
- logger.debug(`[NavigatorRegistry] Registered: ${implementingClass}`);
83
+ navigatorRegistry.set(implementingClass, { constructor, role });
84
+ logger.debug(`[NavigatorRegistry] Registered: ${implementingClass}${role ? ` (${role})` : ''}`);
69
85
  }
70
86
 
71
87
  /**
@@ -75,7 +91,7 @@ export function registerNavigator(
75
91
  * @returns The constructor, or undefined if not registered
76
92
  */
77
93
  export function getRegisteredNavigator(implementingClass: string): NavigatorConstructor | undefined {
78
- return navigatorRegistry.get(implementingClass);
94
+ return navigatorRegistry.get(implementingClass)?.constructor;
79
95
  }
80
96
 
81
97
  /**
@@ -88,6 +104,16 @@ export function hasRegisteredNavigator(implementingClass: string): boolean {
88
104
  return navigatorRegistry.has(implementingClass);
89
105
  }
90
106
 
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
+
91
117
  /**
92
118
  * Get all registered navigator names.
93
119
  * Useful for debugging and testing.
@@ -371,17 +397,24 @@ export const NavigatorRoles: Record<Navigators, NavigatorRole> = {
371
397
  * @returns true if the navigator is a generator, false otherwise
372
398
  */
373
399
  export function isGenerator(impl: string): boolean {
374
- return NavigatorRoles[impl as Navigators] === NavigatorRole.GENERATOR;
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;
375
403
  }
376
404
 
377
405
  /**
378
406
  * Check if a navigator implementation is a filter.
379
407
  *
380
- * @param impl - Navigator implementation name (e.g., 'elo', 'hierarchyDefinition')
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')
381
412
  * @returns true if the navigator is a filter, false otherwise
382
413
  */
383
414
  export function isFilter(impl: string): boolean {
384
- return NavigatorRoles[impl as Navigators] === NavigatorRole.FILTER;
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;
385
418
  }
386
419
 
387
420
  /**