@vue-skuilder/db 0.1.18 → 0.1.20
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/{classroomDB-BgfrVb8d.d.ts → classroomDB-CZdMBiTU.d.ts} +71 -2
- package/dist/{classroomDB-CTOenngH.d.cts → classroomDB-PxDZTky3.d.cts} +71 -2
- package/dist/core/index.d.cts +80 -6
- package/dist/core/index.d.ts +80 -6
- package/dist/core/index.js +370 -52
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +369 -52
- package/dist/core/index.mjs.map +1 -1
- package/dist/{dataLayerProvider-D6PoCwS6.d.cts → dataLayerProvider-D0MoZMjH.d.cts} +1 -1
- package/dist/{dataLayerProvider-CZxC9GtB.d.ts → dataLayerProvider-D8o6ZnKW.d.ts} +1 -1
- package/dist/impl/couch/index.d.cts +4 -3
- package/dist/impl/couch/index.d.ts +4 -3
- package/dist/impl/couch/index.js +371 -55
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +371 -55
- package/dist/impl/couch/index.mjs.map +1 -1
- package/dist/impl/static/index.d.cts +5 -4
- package/dist/impl/static/index.d.ts +5 -4
- package/dist/impl/static/index.js +356 -44
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +356 -44
- package/dist/impl/static/index.mjs.map +1 -1
- package/dist/{index-D-Fa4Smt.d.cts → index-B_j6u5E4.d.cts} +1 -1
- package/dist/{index-CD8BZz2k.d.ts → index-Dj0SEgk3.d.ts} +1 -1
- package/dist/index.d.cts +10 -10
- package/dist/index.d.ts +10 -10
- package/dist/index.js +382 -55
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +381 -55
- package/dist/index.mjs.map +1 -1
- package/dist/{types-CzPDLAK6.d.cts → types-Bn0itutr.d.cts} +1 -1
- package/dist/{types-CewsN87z.d.ts → types-DQaXnuoc.d.ts} +1 -1
- package/dist/{types-legacy-6ettoclI.d.cts → types-legacy-DDY4N-Uq.d.cts} +3 -1
- package/dist/{types-legacy-6ettoclI.d.ts → types-legacy-DDY4N-Uq.d.ts} +3 -1
- package/dist/util/packer/index.d.cts +3 -3
- package/dist/util/packer/index.d.ts +3 -3
- package/docs/navigators-architecture.md +115 -10
- package/package.json +4 -4
- package/src/core/index.ts +1 -0
- package/src/core/interfaces/courseDB.ts +13 -0
- package/src/core/interfaces/userDB.ts +32 -0
- package/src/core/navigators/Pipeline.ts +127 -14
- package/src/core/navigators/filters/index.ts +3 -0
- package/src/core/navigators/filters/userTagPreference.ts +232 -0
- package/src/core/navigators/hierarchyDefinition.ts +4 -4
- package/src/core/navigators/index.ts +59 -0
- package/src/core/navigators/inferredPreference.ts +107 -0
- package/src/core/navigators/interferenceMitigator.ts +1 -13
- package/src/core/navigators/relativePriority.ts +2 -14
- package/src/core/navigators/userGoal.ts +136 -0
- package/src/core/types/strategyState.ts +84 -0
- package/src/core/types/types-legacy.ts +2 -0
- package/src/impl/common/BaseUserDB.ts +74 -7
- package/src/impl/couch/adminDB.ts +1 -2
- package/src/impl/couch/courseDB.ts +30 -10
- package/src/impl/static/courseDB.ts +11 -0
- package/tests/core/navigators/Pipeline.test.ts +1 -0
- package/docs/todo-pipeline-optimization.md +0 -117
- package/docs/todo-strategy-state-storage.md +0 -278
|
@@ -13,7 +13,8 @@ declare enum DocType {
|
|
|
13
13
|
CARDRECORD = "CARDRECORD",
|
|
14
14
|
SCHEDULED_CARD = "SCHEDULED_CARD",
|
|
15
15
|
TAG = "TAG",
|
|
16
|
-
NAVIGATION_STRATEGY = "NAVIGATION_STRATEGY"
|
|
16
|
+
NAVIGATION_STRATEGY = "NAVIGATION_STRATEGY",
|
|
17
|
+
STRATEGY_STATE = "STRATEGY_STATE"
|
|
17
18
|
}
|
|
18
19
|
interface QualifiedCardID {
|
|
19
20
|
courseID: string;
|
|
@@ -89,6 +90,7 @@ declare const DocTypePrefixes: {
|
|
|
89
90
|
readonly VIEW: "VIEW";
|
|
90
91
|
readonly PEDAGOGY: "PEDAGOGY";
|
|
91
92
|
readonly NAVIGATION_STRATEGY: "NAVIGATION_STRATEGY";
|
|
93
|
+
readonly STRATEGY_STATE: "STRATEGY_STATE";
|
|
92
94
|
};
|
|
93
95
|
interface CardHistory<T extends CardRecord> {
|
|
94
96
|
_id: PouchDB.Core.DocumentId;
|
|
@@ -13,7 +13,8 @@ declare enum DocType {
|
|
|
13
13
|
CARDRECORD = "CARDRECORD",
|
|
14
14
|
SCHEDULED_CARD = "SCHEDULED_CARD",
|
|
15
15
|
TAG = "TAG",
|
|
16
|
-
NAVIGATION_STRATEGY = "NAVIGATION_STRATEGY"
|
|
16
|
+
NAVIGATION_STRATEGY = "NAVIGATION_STRATEGY",
|
|
17
|
+
STRATEGY_STATE = "STRATEGY_STATE"
|
|
17
18
|
}
|
|
18
19
|
interface QualifiedCardID {
|
|
19
20
|
courseID: string;
|
|
@@ -89,6 +90,7 @@ declare const DocTypePrefixes: {
|
|
|
89
90
|
readonly VIEW: "VIEW";
|
|
90
91
|
readonly PEDAGOGY: "PEDAGOGY";
|
|
91
92
|
readonly NAVIGATION_STRATEGY: "NAVIGATION_STRATEGY";
|
|
93
|
+
readonly STRATEGY_STATE: "STRATEGY_STATE";
|
|
92
94
|
};
|
|
93
95
|
interface CardHistory<T extends CardRecord> {
|
|
94
96
|
_id: PouchDB.Core.DocumentId;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { A as AttachmentData, C as ChunkMetadata, D as DesignDocument, I as IndexMetadata, a as PackedCourseData, P as PackerConfig, S as StaticCourseManifest } from '../../types-
|
|
2
|
-
export { C as CouchDBToStaticPacker } from '../../index-
|
|
1
|
+
export { A as AttachmentData, C as ChunkMetadata, D as DesignDocument, I as IndexMetadata, a as PackedCourseData, P as PackerConfig, S as StaticCourseManifest } from '../../types-Bn0itutr.cjs';
|
|
2
|
+
export { C as CouchDBToStaticPacker } from '../../index-B_j6u5E4.cjs';
|
|
3
3
|
import '@vue-skuilder/common';
|
|
4
|
-
import '../../types-legacy-
|
|
4
|
+
import '../../types-legacy-DDY4N-Uq.cjs';
|
|
5
5
|
import 'moment';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { A as AttachmentData, C as ChunkMetadata, D as DesignDocument, I as IndexMetadata, a as PackedCourseData, P as PackerConfig, S as StaticCourseManifest } from '../../types-
|
|
2
|
-
export { C as CouchDBToStaticPacker } from '../../index-
|
|
1
|
+
export { A as AttachmentData, C as ChunkMetadata, D as DesignDocument, I as IndexMetadata, a as PackedCourseData, P as PackerConfig, S as StaticCourseManifest } from '../../types-DQaXnuoc.js';
|
|
2
|
+
export { C as CouchDBToStaticPacker } from '../../index-Dj0SEgk3.js';
|
|
3
3
|
import '@vue-skuilder/common';
|
|
4
|
-
import '../../types-legacy-
|
|
4
|
+
import '../../types-legacy-DDY4N-Uq.js';
|
|
5
5
|
import 'moment';
|
|
@@ -9,7 +9,7 @@ The navigation strategy system selects and scores cards for study sessions. It u
|
|
|
9
9
|
|
|
10
10
|
### WeightedCard
|
|
11
11
|
|
|
12
|
-
A card with a suitability score and
|
|
12
|
+
A card with a suitability score, audit trail, and pre-fetched data:
|
|
13
13
|
|
|
14
14
|
```typescript
|
|
15
15
|
interface WeightedCard {
|
|
@@ -17,6 +17,7 @@ interface WeightedCard {
|
|
|
17
17
|
courseId: string;
|
|
18
18
|
score: number; // 0-1 suitability score
|
|
19
19
|
provenance: StrategyContribution[]; // Audit trail
|
|
20
|
+
tags?: string[]; // Pre-fetched tags (hydrated by Pipeline)
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
interface StrategyContribution {
|
|
@@ -57,15 +58,19 @@ interface CardFilter {
|
|
|
57
58
|
}
|
|
58
59
|
```
|
|
59
60
|
|
|
61
|
+
Filters receive cards with pre-hydrated data (e.g., `card.tags`) from Pipeline, eliminating
|
|
62
|
+
redundant database queries.
|
|
63
|
+
|
|
60
64
|
**Implementations:**
|
|
61
65
|
- `HierarchyDefinitionNavigator` — Gates cards by prerequisite mastery (score=0 if locked)
|
|
62
66
|
- `InterferenceMitigatorNavigator` — Reduces scores for confusable content
|
|
63
67
|
- `RelativePriorityNavigator` — Boosts scores for high-utility content
|
|
68
|
+
- `UserTagPreferenceFilter` — Applies user-configured tag preferences (path constraints)
|
|
64
69
|
- `createEloDistanceFilter()` — Penalizes cards far from user's current ELO
|
|
65
70
|
|
|
66
71
|
### Pipeline
|
|
67
72
|
|
|
68
|
-
Orchestrates generator and filters:
|
|
73
|
+
Orchestrates generator, data hydration, and filters:
|
|
69
74
|
|
|
70
75
|
```typescript
|
|
71
76
|
class Pipeline {
|
|
@@ -77,13 +82,20 @@ class Pipeline {
|
|
|
77
82
|
)
|
|
78
83
|
|
|
79
84
|
async getWeightedCards(limit: number): Promise<WeightedCard[]> {
|
|
85
|
+
// Build shared context (user ELO, etc.)
|
|
80
86
|
const context = await this.buildContext();
|
|
87
|
+
|
|
88
|
+
// Generate candidates
|
|
81
89
|
let cards = await this.generator.getWeightedCards(fetchLimit, context);
|
|
82
|
-
|
|
90
|
+
|
|
91
|
+
// Hydrate shared data (tags, etc.) in single batch query
|
|
92
|
+
cards = await this.hydrateTags(cards);
|
|
93
|
+
|
|
94
|
+
// Apply filters sequentially
|
|
83
95
|
for (const filter of this.filters) {
|
|
84
96
|
cards = await filter.transform(cards, context);
|
|
85
97
|
}
|
|
86
|
-
|
|
98
|
+
|
|
87
99
|
return cards.filter(c => c.score > 0)
|
|
88
100
|
.sort((a, b) => b.score - a.score)
|
|
89
101
|
.slice(0, limit);
|
|
@@ -91,6 +103,12 @@ class Pipeline {
|
|
|
91
103
|
}
|
|
92
104
|
```
|
|
93
105
|
|
|
106
|
+
**Responsibilities:**
|
|
107
|
+
- **Context building** — Fetches shared data (user ELO) once for all strategies
|
|
108
|
+
- **Data hydration** — Pre-fetches commonly needed data (tags) in batch queries
|
|
109
|
+
- **Filter orchestration** — Applies filters in sequence, accumulating provenance
|
|
110
|
+
- **Result selection** — Removes zero-scores, sorts, and returns top N
|
|
111
|
+
|
|
94
112
|
## Pipeline Assembly
|
|
95
113
|
|
|
96
114
|
`PipelineAssembler` builds pipelines from strategy documents:
|
|
@@ -237,6 +255,89 @@ class MyFilter extends ContentNavigator implements CardFilter {
|
|
|
237
255
|
|
|
238
256
|
Register in `NavigatorRoles` as `NavigatorRole.FILTER`.
|
|
239
257
|
|
|
258
|
+
## Strategy State Storage
|
|
259
|
+
|
|
260
|
+
Strategies can persist user-scoped state (preferences, learned patterns, temporal tracking)
|
|
261
|
+
using the `STRATEGY_STATE` document type in the user database.
|
|
262
|
+
|
|
263
|
+
### Goals vs Preferences vs Inferred
|
|
264
|
+
|
|
265
|
+
The system distinguishes three types of user-scoped navigation data:
|
|
266
|
+
|
|
267
|
+
| Type | Defines | Example | Affects ELO | Implementation |
|
|
268
|
+
|------|---------|---------|-------------|----------------|
|
|
269
|
+
| **Goal** | Destination (what to learn) | "Master ear-training" | Yes | `userGoal.ts` (stub) |
|
|
270
|
+
| **Preference** | Path (how to learn) | "Skip text-heavy cards" | No | `filters/userTagPreference.ts` |
|
|
271
|
+
| **Inferred** | Learned patterns | "User prefers visual" | No | `inferredPreference.ts` (stub) |
|
|
272
|
+
|
|
273
|
+
- **Goals** redefine the optimization target — they scope which content matters for progress
|
|
274
|
+
- **Preferences** constrain the path — they affect card selection without changing progress tracking
|
|
275
|
+
- **Inferred** preferences are learned from behavior — they act as soft suggestions
|
|
276
|
+
|
|
277
|
+
See stub files for detailed architectural intent on goals and inferred preferences.
|
|
278
|
+
|
|
279
|
+
### Storage API
|
|
280
|
+
|
|
281
|
+
`ContentNavigator` provides protected helper methods:
|
|
282
|
+
|
|
283
|
+
```typescript
|
|
284
|
+
// Get this strategy's persisted state for the current course
|
|
285
|
+
protected async getStrategyState<T>(): Promise<T | null>
|
|
286
|
+
|
|
287
|
+
// Persist this strategy's state for the current course
|
|
288
|
+
protected async putStrategyState<T>(data: T): Promise<void>
|
|
289
|
+
|
|
290
|
+
// Override to customize the storage key (default: constructor name)
|
|
291
|
+
protected get strategyKey(): string
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Document Format
|
|
295
|
+
|
|
296
|
+
```typescript
|
|
297
|
+
interface StrategyStateDoc<T> {
|
|
298
|
+
_id: StrategyStateId; // "STRATEGY_STATE::{courseId}::{strategyKey}"
|
|
299
|
+
docType: DocType.STRATEGY_STATE;
|
|
300
|
+
courseId: string;
|
|
301
|
+
strategyKey: string;
|
|
302
|
+
data: T; // Strategy-specific payload
|
|
303
|
+
updatedAt: string; // ISO timestamp
|
|
304
|
+
}
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### Example: User Tag Preferences
|
|
308
|
+
|
|
309
|
+
`UserTagPreferenceFilter` reads user preferences from strategy state:
|
|
310
|
+
|
|
311
|
+
```typescript
|
|
312
|
+
interface UserTagPreferenceState {
|
|
313
|
+
/**
|
|
314
|
+
* Tag-specific multipliers.
|
|
315
|
+
* - 0 = exclude (card score = 0)
|
|
316
|
+
* - 0.5 = penalize by 50%
|
|
317
|
+
* - 1.0 = neutral/no effect
|
|
318
|
+
* - 2.0 = 2x preference boost
|
|
319
|
+
* - Higher = stronger preference
|
|
320
|
+
*/
|
|
321
|
+
boost: Record<string, number>;
|
|
322
|
+
updatedAt: string;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// In filter's transform():
|
|
326
|
+
const prefs = await this.getStrategyState<UserTagPreferenceState>();
|
|
327
|
+
if (!prefs || Object.keys(prefs.boost).length === 0) {
|
|
328
|
+
return cards; // No preferences configured
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Apply multipliers (max wins when multiple tags match)
|
|
332
|
+
const multiplier = computeMultiplier(cardTags, prefs.boost);
|
|
333
|
+
return { ...card, score: card.score * multiplier };
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
**UI Component**: `packages/common-ui/src/components/UserTagPreferences.vue`
|
|
337
|
+
- Slider-based interface (0-2 default range, expandable to 10)
|
|
338
|
+
- All sliders share global max for consistent visual comparison
|
|
339
|
+
- Writes to strategy state via `userDB.putStrategyState()`
|
|
340
|
+
|
|
240
341
|
## File Reference
|
|
241
342
|
|
|
242
343
|
| File | Purpose |
|
|
@@ -254,12 +355,16 @@ Register in `NavigatorRoles` as `NavigatorRole.FILTER`.
|
|
|
254
355
|
| `core/navigators/interferenceMitigator.ts` | Interference filter |
|
|
255
356
|
| `core/navigators/relativePriority.ts` | Priority filter |
|
|
256
357
|
| `core/navigators/filters/eloDistance.ts` | ELO distance filter |
|
|
358
|
+
| `core/navigators/filters/userTagPreference.ts` | User tag preference filter |
|
|
359
|
+
| `common-ui/.../UserTagPreferences.vue` | UI for tag preference sliders |
|
|
360
|
+
| `core/navigators/userGoal.ts` | User goal navigator (stub) |
|
|
361
|
+
| `core/navigators/inferredPreference.ts` | Inferred preference navigator (stub) |
|
|
362
|
+
| `core/types/strategyState.ts` | `StrategyStateDoc`, `StrategyStateId` |
|
|
257
363
|
| `impl/couch/courseDB.ts` | `createNavigator()` entry point |
|
|
258
364
|
|
|
259
|
-
## Related
|
|
365
|
+
## Related Documentation
|
|
260
366
|
|
|
261
|
-
- `todo-pipeline-optimization.md`
|
|
262
|
-
- `todo-strategy-authoring.md`
|
|
263
|
-
- todo-
|
|
264
|
-
-
|
|
265
|
-
- todo-evolutionary-orchestration
|
|
367
|
+
- `todo-pipeline-optimization.md` — Batch tag hydration implementation (✅ completed)
|
|
368
|
+
- `todo-strategy-authoring.md` — UX and DX for authoring strategies
|
|
369
|
+
- `todo-evolutionary-orchestration.md` — Long-term adaptive strategy vision
|
|
370
|
+
- `devlog/1004` — Implementation details for tag hydration optimization
|
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.20",
|
|
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.20",
|
|
52
52
|
"cross-fetch": "^4.1.0",
|
|
53
53
|
"moment": "^2.29.4",
|
|
54
54
|
"pouchdb": "^9.0.0",
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"tsup": "^8.0.2",
|
|
61
61
|
"typescript": "~5.9.3",
|
|
62
62
|
"vite": "^7.0.0",
|
|
63
|
-
"vitest": "^4.0.
|
|
63
|
+
"vitest": "^4.0.15"
|
|
64
64
|
},
|
|
65
|
-
"stableVersion": "0.1.
|
|
65
|
+
"stableVersion": "0.1.20"
|
|
66
66
|
}
|
package/src/core/index.ts
CHANGED
|
@@ -92,6 +92,19 @@ export interface CourseDBInterface extends NavigationStrategyManager {
|
|
|
92
92
|
*/
|
|
93
93
|
getAppliedTags(cardId: string): Promise<PouchDB.Query.Response<TagStub>>;
|
|
94
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Get tags for multiple cards in a single batch query.
|
|
97
|
+
* More efficient than calling getAppliedTags() for each card.
|
|
98
|
+
*
|
|
99
|
+
* This method reduces redundant database operations when multiple filters
|
|
100
|
+
* need tag data for the same cards. The Pipeline uses this to pre-hydrate
|
|
101
|
+
* tags on WeightedCard objects before filters run.
|
|
102
|
+
*
|
|
103
|
+
* @param cardIds - Array of card IDs to fetch tags for
|
|
104
|
+
* @returns Map from cardId to array of tag names
|
|
105
|
+
*/
|
|
106
|
+
getAppliedTagsBatch(cardIds: string[]): Promise<Map<string, string[]>>;
|
|
107
|
+
|
|
95
108
|
/**
|
|
96
109
|
* Add a tag to a card
|
|
97
110
|
*/
|
|
@@ -56,6 +56,18 @@ export interface UserDBReader {
|
|
|
56
56
|
|
|
57
57
|
getActivityRecords(): Promise<ActivityRecord[]>;
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Get strategy-specific state for a course.
|
|
61
|
+
*
|
|
62
|
+
* Strategies use this to persist preferences, learned patterns, or temporal
|
|
63
|
+
* tracking data across sessions. Each strategy owns its own namespace.
|
|
64
|
+
*
|
|
65
|
+
* @param courseId - The course this state applies to
|
|
66
|
+
* @param strategyKey - Unique key identifying the strategy (typically class name)
|
|
67
|
+
* @returns The strategy's data payload, or null if no state exists
|
|
68
|
+
*/
|
|
69
|
+
getStrategyState<T>(courseId: string, strategyKey: string): Promise<T | null>;
|
|
70
|
+
|
|
59
71
|
/**
|
|
60
72
|
* Get user's classroom registrations
|
|
61
73
|
*/
|
|
@@ -132,6 +144,26 @@ export interface UserDBWriter extends DocumentUpdater {
|
|
|
132
144
|
* Reset all user data (progress, registrations, etc.) while preserving authentication
|
|
133
145
|
*/
|
|
134
146
|
resetUserData(): Promise<{ status: Status; error?: string }>;
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Store strategy-specific state for a course.
|
|
150
|
+
*
|
|
151
|
+
* Strategies use this to persist preferences, learned patterns, or temporal
|
|
152
|
+
* tracking data across sessions. Each strategy owns its own namespace.
|
|
153
|
+
*
|
|
154
|
+
* @param courseId - The course this state applies to
|
|
155
|
+
* @param strategyKey - Unique key identifying the strategy (typically class name)
|
|
156
|
+
* @param data - The strategy's data payload to store
|
|
157
|
+
*/
|
|
158
|
+
putStrategyState<T>(courseId: string, strategyKey: string, data: T): Promise<void>;
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Delete strategy-specific state for a course.
|
|
162
|
+
*
|
|
163
|
+
* @param courseId - The course this state applies to
|
|
164
|
+
* @param strategyKey - Unique key identifying the strategy (typically class name)
|
|
165
|
+
*/
|
|
166
|
+
deleteStrategyState(courseId: string, strategyKey: string): Promise<void>;
|
|
135
167
|
}
|
|
136
168
|
|
|
137
169
|
/**
|
|
@@ -9,6 +9,88 @@ import type { CardGenerator, GeneratorContext } from './generators/types';
|
|
|
9
9
|
import type { StudySessionNewItem, StudySessionReviewItem } from '../interfaces/contentSource';
|
|
10
10
|
import { logger } from '../../util/logger';
|
|
11
11
|
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// PIPELINE LOGGING HELPERS
|
|
14
|
+
// ============================================================================
|
|
15
|
+
//
|
|
16
|
+
// Focused logging functions that can be toggled by commenting single lines.
|
|
17
|
+
// Use these to inspect pipeline behavior in development/production.
|
|
18
|
+
//
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Log pipeline configuration on construction.
|
|
22
|
+
* Shows generator and filter chain structure.
|
|
23
|
+
*/
|
|
24
|
+
function logPipelineConfig(generator: CardGenerator, filters: CardFilter[]): void {
|
|
25
|
+
const filterList = filters.length > 0
|
|
26
|
+
? '\n - ' + filters.map(f => f.name).join('\n - ')
|
|
27
|
+
: ' none';
|
|
28
|
+
|
|
29
|
+
logger.info(
|
|
30
|
+
`[Pipeline] Configuration:\n` +
|
|
31
|
+
` Generator: ${generator.name}\n` +
|
|
32
|
+
` Filters:${filterList}`
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Log tag hydration results.
|
|
38
|
+
* Shows effectiveness of batch query (how many cards/tags were hydrated).
|
|
39
|
+
*/
|
|
40
|
+
function logTagHydration(cards: WeightedCard[], tagsByCard: Map<string, string[]>): void {
|
|
41
|
+
const totalTags = Array.from(tagsByCard.values()).reduce((sum, tags) => sum + tags.length, 0);
|
|
42
|
+
const cardsWithTags = Array.from(tagsByCard.values()).filter(tags => tags.length > 0).length;
|
|
43
|
+
|
|
44
|
+
logger.debug(
|
|
45
|
+
`[Pipeline] Tag hydration: ${cards.length} cards, ` +
|
|
46
|
+
`${cardsWithTags} have tags (${totalTags} total tags) - single batch query`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Log pipeline execution summary.
|
|
52
|
+
* Shows complete flow from generator through filters to final results.
|
|
53
|
+
*/
|
|
54
|
+
function logExecutionSummary(
|
|
55
|
+
generatorName: string,
|
|
56
|
+
generatedCount: number,
|
|
57
|
+
filterCount: number,
|
|
58
|
+
finalCount: number,
|
|
59
|
+
topScores: number[]
|
|
60
|
+
): void {
|
|
61
|
+
const scoreDisplay = topScores.length > 0
|
|
62
|
+
? topScores.map(s => s.toFixed(2)).join(', ')
|
|
63
|
+
: 'none';
|
|
64
|
+
|
|
65
|
+
logger.info(
|
|
66
|
+
`[Pipeline] Execution: ${generatorName} produced ${generatedCount} → ` +
|
|
67
|
+
`${filterCount} filters → ${finalCount} results (top scores: ${scoreDisplay})`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Log provenance trails for cards.
|
|
73
|
+
* Shows the complete scoring history for each card through the pipeline.
|
|
74
|
+
* Useful for debugging why cards scored the way they did.
|
|
75
|
+
*/
|
|
76
|
+
function logCardProvenance(cards: WeightedCard[], maxCards: number = 3): void {
|
|
77
|
+
const cardsToLog = cards.slice(0, maxCards);
|
|
78
|
+
|
|
79
|
+
logger.debug(`[Pipeline] Provenance for top ${cardsToLog.length} cards:`);
|
|
80
|
+
|
|
81
|
+
for (const card of cardsToLog) {
|
|
82
|
+
logger.debug(`[Pipeline] ${card.cardId} (final score: ${card.score.toFixed(3)}):`);
|
|
83
|
+
|
|
84
|
+
for (const entry of card.provenance) {
|
|
85
|
+
const scoreChange = entry.score.toFixed(3);
|
|
86
|
+
const action = entry.action.padEnd(9); // Align columns
|
|
87
|
+
logger.debug(
|
|
88
|
+
`[Pipeline] ${action} ${scoreChange} - ${entry.strategyName}: ${entry.reason}`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
12
94
|
// ============================================================================
|
|
13
95
|
// PIPELINE
|
|
14
96
|
// ============================================================================
|
|
@@ -72,9 +154,8 @@ export class Pipeline extends ContentNavigator {
|
|
|
72
154
|
this.user = user;
|
|
73
155
|
this.course = course;
|
|
74
156
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
);
|
|
157
|
+
// Toggle pipeline configuration logging:
|
|
158
|
+
logPipelineConfig(generator, filters);
|
|
78
159
|
}
|
|
79
160
|
|
|
80
161
|
/**
|
|
@@ -82,10 +163,11 @@ export class Pipeline extends ContentNavigator {
|
|
|
82
163
|
*
|
|
83
164
|
* 1. Build shared context (user ELO, etc.)
|
|
84
165
|
* 2. Get candidates from generator (passing context)
|
|
85
|
-
* 3.
|
|
86
|
-
* 4.
|
|
87
|
-
* 5.
|
|
88
|
-
* 6.
|
|
166
|
+
* 3. Batch hydrate tags for all candidates
|
|
167
|
+
* 4. Apply each filter sequentially
|
|
168
|
+
* 5. Remove zero-score cards
|
|
169
|
+
* 6. Sort by score descending
|
|
170
|
+
* 7. Return top N
|
|
89
171
|
*
|
|
90
172
|
* @param limit - Maximum number of cards to return
|
|
91
173
|
* @returns Cards sorted by score descending
|
|
@@ -104,8 +186,12 @@ export class Pipeline extends ContentNavigator {
|
|
|
104
186
|
|
|
105
187
|
// Get candidates from generator, passing context
|
|
106
188
|
let cards = await this.generator.getWeightedCards(fetchLimit, context);
|
|
189
|
+
const generatedCount = cards.length;
|
|
190
|
+
|
|
191
|
+
logger.debug(`[Pipeline] Generator returned ${generatedCount} candidates`);
|
|
107
192
|
|
|
108
|
-
|
|
193
|
+
// Batch hydrate tags before filters run
|
|
194
|
+
cards = await this.hydrateTags(cards);
|
|
109
195
|
|
|
110
196
|
// Apply filters sequentially
|
|
111
197
|
for (const filter of this.filters) {
|
|
@@ -123,16 +209,43 @@ export class Pipeline extends ContentNavigator {
|
|
|
123
209
|
// Return top N
|
|
124
210
|
const result = cards.slice(0, limit);
|
|
125
211
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
);
|
|
212
|
+
// Toggle execution summary logging:
|
|
213
|
+
const topScores = result.slice(0, 3).map(c => c.score);
|
|
214
|
+
logExecutionSummary(this.generator.name, generatedCount, this.filters.length, result.length, topScores);
|
|
215
|
+
|
|
216
|
+
// Toggle provenance logging (shows scoring history for top cards):
|
|
217
|
+
logCardProvenance(result, 3);
|
|
132
218
|
|
|
133
219
|
return result;
|
|
134
220
|
}
|
|
135
221
|
|
|
222
|
+
/**
|
|
223
|
+
* Batch hydrate tags for all cards.
|
|
224
|
+
*
|
|
225
|
+
* Fetches tags for all cards in a single database query and attaches them
|
|
226
|
+
* to the WeightedCard objects. Filters can then use card.tags instead of
|
|
227
|
+
* making individual getAppliedTags() calls.
|
|
228
|
+
*
|
|
229
|
+
* @param cards - Cards to hydrate
|
|
230
|
+
* @returns Cards with tags populated
|
|
231
|
+
*/
|
|
232
|
+
private async hydrateTags(cards: WeightedCard[]): Promise<WeightedCard[]> {
|
|
233
|
+
if (cards.length === 0) {
|
|
234
|
+
return cards;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const cardIds = cards.map((c) => c.cardId);
|
|
238
|
+
const tagsByCard = await this.course!.getAppliedTagsBatch(cardIds);
|
|
239
|
+
|
|
240
|
+
// Toggle tag hydration logging:
|
|
241
|
+
logTagHydration(cards, tagsByCard);
|
|
242
|
+
|
|
243
|
+
return cards.map((card) => ({
|
|
244
|
+
...card,
|
|
245
|
+
tags: tagsByCard.get(card.cardId) ?? [],
|
|
246
|
+
}));
|
|
247
|
+
}
|
|
248
|
+
|
|
136
249
|
/**
|
|
137
250
|
* Build shared context for generator and filters.
|
|
138
251
|
*
|
|
@@ -4,3 +4,6 @@ export type { CardFilter, FilterContext, CardFilterFactory } from './types';
|
|
|
4
4
|
// Filter implementations
|
|
5
5
|
export { createEloDistanceFilter } from './eloDistance';
|
|
6
6
|
export type { EloDistanceConfig } from './eloDistance';
|
|
7
|
+
|
|
8
|
+
export { default as UserTagPreferenceFilter } from './userTagPreference';
|
|
9
|
+
export type { UserTagPreferenceState } from './userTagPreference';
|