@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.
- package/dist/{contentSource-ygoFw9oV.d.ts → contentSource-BmnmvH8C.d.ts} +4 -19
- package/dist/{contentSource-B7nXusjk.d.cts → contentSource-DfBbaLA-.d.cts} +4 -19
- package/dist/core/index.d.cts +3 -3
- package/dist/core/index.d.ts +3 -3
- package/dist/core/index.js +11 -18
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +11 -17
- package/dist/core/index.mjs.map +1 -1
- package/dist/{dataLayerProvider-BW7HvkMt.d.cts → dataLayerProvider-BeRXVMs5.d.cts} +1 -1
- package/dist/{dataLayerProvider-BfXUVDuG.d.ts → dataLayerProvider-CG9GfaAY.d.ts} +1 -1
- package/dist/impl/couch/index.d.cts +2 -2
- package/dist/impl/couch/index.d.ts +2 -2
- package/dist/impl/couch/index.js +11 -16
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +11 -16
- package/dist/impl/couch/index.mjs.map +1 -1
- package/dist/impl/static/index.d.cts +2 -2
- package/dist/impl/static/index.d.ts +2 -2
- package/dist/impl/static/index.js +11 -16
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +11 -16
- package/dist/impl/static/index.mjs.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +11 -18
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +11 -17
- package/dist/index.mjs.map +1 -1
- package/docs/navigators-architecture.md +5 -64
- package/package.json +3 -3
- package/src/core/navigators/generators/elo.ts +17 -6
- package/src/core/navigators/index.ts +10 -43
|
@@ -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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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)}),
|
|
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
|
|
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
|
-
*
|
|
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,
|
|
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
|
-
*
|
|
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,
|
|
84
|
-
logger.debug(`[NavigatorRegistry] Registered: ${implementingClass}
|
|
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)
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
/**
|