@vue-skuilder/db 0.1.18 → 0.1.21

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 (87) hide show
  1. package/CLAUDE.md +2 -2
  2. package/dist/{classroomDB-BgfrVb8d.d.ts → contentSource-BP9hznNV.d.ts} +220 -197
  3. package/dist/{classroomDB-CTOenngH.d.cts → contentSource-DsJadoBU.d.cts} +220 -197
  4. package/dist/core/index.d.cts +80 -6
  5. package/dist/core/index.d.ts +80 -6
  6. package/dist/core/index.js +735 -1560
  7. package/dist/core/index.js.map +1 -1
  8. package/dist/core/index.mjs +708 -1539
  9. package/dist/core/index.mjs.map +1 -1
  10. package/dist/{dataLayerProvider-D6PoCwS6.d.cts → dataLayerProvider-CHYrQ5pB.d.cts} +1 -1
  11. package/dist/{dataLayerProvider-CZxC9GtB.d.ts → dataLayerProvider-MDTxXq2l.d.ts} +1 -1
  12. package/dist/impl/couch/index.d.cts +8 -23
  13. package/dist/impl/couch/index.d.ts +8 -23
  14. package/dist/impl/couch/index.js +723 -1578
  15. package/dist/impl/couch/index.js.map +1 -1
  16. package/dist/impl/couch/index.mjs +692 -1552
  17. package/dist/impl/couch/index.mjs.map +1 -1
  18. package/dist/impl/static/index.d.cts +25 -8
  19. package/dist/impl/static/index.d.ts +25 -8
  20. package/dist/impl/static/index.js +700 -1400
  21. package/dist/impl/static/index.js.map +1 -1
  22. package/dist/impl/static/index.mjs +688 -1393
  23. package/dist/impl/static/index.mjs.map +1 -1
  24. package/dist/{index-D-Fa4Smt.d.cts → index-B_j6u5E4.d.cts} +1 -1
  25. package/dist/{index-CD8BZz2k.d.ts → index-Dj0SEgk3.d.ts} +1 -1
  26. package/dist/index.d.cts +71 -63
  27. package/dist/index.d.ts +71 -63
  28. package/dist/index.js +1162 -1996
  29. package/dist/index.js.map +1 -1
  30. package/dist/index.mjs +1124 -1955
  31. package/dist/index.mjs.map +1 -1
  32. package/dist/pouch/index.js +3 -0
  33. package/dist/pouch/index.js.map +1 -1
  34. package/dist/pouch/index.mjs +3 -0
  35. package/dist/pouch/index.mjs.map +1 -1
  36. package/dist/{types-CzPDLAK6.d.cts → types-Bn0itutr.d.cts} +1 -1
  37. package/dist/{types-CewsN87z.d.ts → types-DQaXnuoc.d.ts} +1 -1
  38. package/dist/{types-legacy-6ettoclI.d.cts → types-legacy-DDY4N-Uq.d.cts} +3 -1
  39. package/dist/{types-legacy-6ettoclI.d.ts → types-legacy-DDY4N-Uq.d.ts} +3 -1
  40. package/dist/util/packer/index.d.cts +3 -3
  41. package/dist/util/packer/index.d.ts +3 -3
  42. package/docs/navigators-architecture.md +115 -17
  43. package/package.json +4 -4
  44. package/src/core/index.ts +1 -0
  45. package/src/core/interfaces/classroomDB.ts +5 -13
  46. package/src/core/interfaces/contentSource.ts +6 -66
  47. package/src/core/interfaces/courseDB.ts +15 -7
  48. package/src/core/interfaces/userDB.ts +32 -0
  49. package/src/core/navigators/Pipeline.ts +136 -52
  50. package/src/core/navigators/PipelineAssembler.ts +1 -1
  51. package/src/core/navigators/defaults.ts +84 -0
  52. package/src/core/navigators/{hierarchyDefinition.ts → filters/hierarchyDefinition.ts} +15 -29
  53. package/src/core/navigators/filters/index.ts +3 -0
  54. package/src/core/navigators/filters/inferredPreferenceStub.ts +107 -0
  55. package/src/core/navigators/{interferenceMitigator.ts → filters/interferenceMitigator.ts} +11 -37
  56. package/src/core/navigators/{relativePriority.ts → filters/relativePriority.ts} +12 -38
  57. package/src/core/navigators/filters/userGoalStub.ts +136 -0
  58. package/src/core/navigators/filters/userTagPreference.ts +217 -0
  59. package/src/core/navigators/{CompositeGenerator.ts → generators/CompositeGenerator.ts} +15 -64
  60. package/src/core/navigators/{elo.ts → generators/elo.ts} +13 -63
  61. package/src/core/navigators/{srs.ts → generators/srs.ts} +11 -40
  62. package/src/core/navigators/generators/types.ts +1 -1
  63. package/src/core/navigators/index.ts +95 -91
  64. package/src/core/types/strategyState.ts +84 -0
  65. package/src/core/types/types-legacy.ts +2 -0
  66. package/src/impl/common/BaseUserDB.ts +74 -7
  67. package/src/impl/couch/adminDB.ts +1 -2
  68. package/src/impl/couch/classroomDB.ts +100 -103
  69. package/src/impl/couch/courseDB.ts +35 -91
  70. package/src/impl/couch/pouchdb-setup.ts +7 -0
  71. package/src/impl/static/StaticDataUnpacker.ts +50 -1
  72. package/src/impl/static/courseDB.ts +87 -37
  73. package/src/study/SessionController.ts +122 -202
  74. package/src/study/SourceMixer.ts +65 -0
  75. package/src/study/TagFilteredContentSource.ts +49 -92
  76. package/src/study/index.ts +1 -0
  77. package/src/study/services/CardHydrationService.ts +165 -81
  78. package/src/util/dataDirectory.ts +1 -1
  79. package/src/util/index.ts +0 -1
  80. package/tests/core/navigators/CompositeGenerator.test.ts +44 -168
  81. package/tests/core/navigators/Pipeline.test.ts +6 -72
  82. package/tests/core/navigators/PipelineAssembler.test.ts +8 -58
  83. package/tests/core/navigators/navigators.test.ts +118 -151
  84. package/docs/todo-pipeline-optimization.md +0 -117
  85. package/docs/todo-strategy-state-storage.md +0 -278
  86. package/src/core/navigators/hardcodedOrder.ts +0 -163
  87. package/src/util/tuiLogger.ts +0 -139
@@ -40,6 +40,9 @@ var import_pouchdb_find = __toESM(require("pouchdb-find"), 1);
40
40
  var import_pouchdb_authentication = __toESM(require("@nilock2/pouchdb-authentication"), 1);
41
41
  import_pouchdb.default.plugin(import_pouchdb_find.default);
42
42
  import_pouchdb.default.plugin(import_pouchdb_authentication.default);
43
+ if (typeof import_pouchdb.default.debug !== "undefined") {
44
+ import_pouchdb.default.debug.disable();
45
+ }
43
46
  import_pouchdb.default.defaults({
44
47
  // ajax: {
45
48
  // timeout: 60000,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/pouch/index.ts","../../src/impl/couch/pouchdb-setup.ts"],"sourcesContent":["// Export configured PouchDB instance\nexport { default } from '../impl/couch/pouchdb-setup.js';","import PouchDB from 'pouchdb';\nimport PouchDBFind from 'pouchdb-find';\nimport PouchDBAuth from '@nilock2/pouchdb-authentication';\n\n// Register plugins\nPouchDB.plugin(PouchDBFind);\nPouchDB.plugin(PouchDBAuth);\n\n// Configure PouchDB globally\nPouchDB.defaults({\n // ajax: {\n // timeout: 60000,\n // },\n});\n\nexport default PouchDB;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,qBAAoB;AACpB,0BAAwB;AACxB,oCAAwB;AAGxB,eAAAA,QAAQ,OAAO,oBAAAC,OAAW;AAC1B,eAAAD,QAAQ,OAAO,8BAAAE,OAAW;AAG1B,eAAAF,QAAQ,SAAS;AAAA;AAAA;AAAA;AAIjB,CAAC;AAED,IAAO,wBAAQ,eAAAA;","names":["PouchDB","PouchDBFind","PouchDBAuth"]}
1
+ {"version":3,"sources":["../../src/pouch/index.ts","../../src/impl/couch/pouchdb-setup.ts"],"sourcesContent":["// Export configured PouchDB instance\nexport { default } from '../impl/couch/pouchdb-setup.js';","import PouchDB from 'pouchdb';\nimport PouchDBFind from 'pouchdb-find';\nimport PouchDBAuth from '@nilock2/pouchdb-authentication';\n\n// Register plugins\nPouchDB.plugin(PouchDBFind);\nPouchDB.plugin(PouchDBAuth);\n\n// Disable PouchDB debug logging to prevent interference with CLI prompts\n// Debug logging (like DerivedLogger.emit) will still go to the TUI log file\n// if initializeTuiLogging() has been called, but won't clutter terminal output\nif (typeof PouchDB.debug !== 'undefined') {\n PouchDB.debug.disable();\n}\n\n// Configure PouchDB globally\nPouchDB.defaults({\n // ajax: {\n // timeout: 60000,\n // },\n});\n\nexport default PouchDB;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,qBAAoB;AACpB,0BAAwB;AACxB,oCAAwB;AAGxB,eAAAA,QAAQ,OAAO,oBAAAC,OAAW;AAC1B,eAAAD,QAAQ,OAAO,8BAAAE,OAAW;AAK1B,IAAI,OAAO,eAAAF,QAAQ,UAAU,aAAa;AACxC,iBAAAA,QAAQ,MAAM,QAAQ;AACxB;AAGA,eAAAA,QAAQ,SAAS;AAAA;AAAA;AAAA;AAIjB,CAAC;AAED,IAAO,wBAAQ,eAAAA;","names":["PouchDB","PouchDBFind","PouchDBAuth"]}
@@ -4,6 +4,9 @@ import PouchDBFind from "pouchdb-find";
4
4
  import PouchDBAuth from "@nilock2/pouchdb-authentication";
5
5
  PouchDB.plugin(PouchDBFind);
6
6
  PouchDB.plugin(PouchDBAuth);
7
+ if (typeof PouchDB.debug !== "undefined") {
8
+ PouchDB.debug.disable();
9
+ }
7
10
  PouchDB.defaults({
8
11
  // ajax: {
9
12
  // timeout: 60000,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/impl/couch/pouchdb-setup.ts"],"sourcesContent":["import PouchDB from 'pouchdb';\nimport PouchDBFind from 'pouchdb-find';\nimport PouchDBAuth from '@nilock2/pouchdb-authentication';\n\n// Register plugins\nPouchDB.plugin(PouchDBFind);\nPouchDB.plugin(PouchDBAuth);\n\n// Configure PouchDB globally\nPouchDB.defaults({\n // ajax: {\n // timeout: 60000,\n // },\n});\n\nexport default PouchDB;\n"],"mappings":";AAAA,OAAO,aAAa;AACpB,OAAO,iBAAiB;AACxB,OAAO,iBAAiB;AAGxB,QAAQ,OAAO,WAAW;AAC1B,QAAQ,OAAO,WAAW;AAG1B,QAAQ,SAAS;AAAA;AAAA;AAAA;AAIjB,CAAC;AAED,IAAO,wBAAQ;","names":[]}
1
+ {"version":3,"sources":["../../src/impl/couch/pouchdb-setup.ts"],"sourcesContent":["import PouchDB from 'pouchdb';\nimport PouchDBFind from 'pouchdb-find';\nimport PouchDBAuth from '@nilock2/pouchdb-authentication';\n\n// Register plugins\nPouchDB.plugin(PouchDBFind);\nPouchDB.plugin(PouchDBAuth);\n\n// Disable PouchDB debug logging to prevent interference with CLI prompts\n// Debug logging (like DerivedLogger.emit) will still go to the TUI log file\n// if initializeTuiLogging() has been called, but won't clutter terminal output\nif (typeof PouchDB.debug !== 'undefined') {\n PouchDB.debug.disable();\n}\n\n// Configure PouchDB globally\nPouchDB.defaults({\n // ajax: {\n // timeout: 60000,\n // },\n});\n\nexport default PouchDB;\n"],"mappings":";AAAA,OAAO,aAAa;AACpB,OAAO,iBAAiB;AACxB,OAAO,iBAAiB;AAGxB,QAAQ,OAAO,WAAW;AAC1B,QAAQ,OAAO,WAAW;AAK1B,IAAI,OAAO,QAAQ,UAAU,aAAa;AACxC,UAAQ,MAAM,QAAQ;AACxB;AAGA,QAAQ,SAAS;AAAA;AAAA;AAAA;AAIjB,CAAC;AAED,IAAO,wBAAQ;","names":[]}
@@ -1,5 +1,5 @@
1
1
  import { CourseConfig } from '@vue-skuilder/common';
2
- import { D as DocType } from './types-legacy-6ettoclI.cjs';
2
+ import { D as DocType } from './types-legacy-DDY4N-Uq.cjs';
3
3
 
4
4
  interface StaticCourseManifest {
5
5
  version: string;
@@ -1,5 +1,5 @@
1
1
  import { CourseConfig } from '@vue-skuilder/common';
2
- import { D as DocType } from './types-legacy-6ettoclI.js';
2
+ import { D as DocType } from './types-legacy-DDY4N-Uq.js';
3
3
 
4
4
  interface StaticCourseManifest {
5
5
  version: string;
@@ -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-CzPDLAK6.cjs';
2
- export { C as CouchDBToStaticPacker } from '../../index-D-Fa4Smt.cjs';
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-6ettoclI.cjs';
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-CewsN87z.js';
2
- export { C as CouchDBToStaticPacker } from '../../index-CD8BZz2k.js';
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-6ettoclI.js';
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 audit trail:
12
+ A card with a suitability score, audit trail, and pre-fetched metadata:
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:
@@ -192,10 +210,6 @@ class MyGenerator extends ContentNavigator implements CardGenerator {
192
210
  }]
193
211
  }));
194
212
  }
195
-
196
- // Legacy methods - stub or implement for backward compat
197
- async getNewCards() { return []; }
198
- async getPendingReviews() { return []; }
199
213
  }
200
214
  ```
201
215
 
@@ -228,15 +242,96 @@ class MyFilter extends ContentNavigator implements CardFilter {
228
242
  });
229
243
  }
230
244
 
231
- // Legacy methods - filters don't generate cards
245
+ // Legacy method - filters don't generate cards
232
246
  async getWeightedCards() { throw new Error('Use transform() via Pipeline'); }
233
- async getNewCards() { return []; }
234
- async getPendingReviews() { return []; }
235
247
  }
236
248
  ```
237
249
 
238
250
  Register in `NavigatorRoles` as `NavigatorRole.FILTER`.
239
251
 
252
+ ## Strategy State Storage
253
+
254
+ Strategies can persist user-scoped state (preferences, learned patterns, temporal tracking)
255
+ using the `STRATEGY_STATE` document type in the user database.
256
+
257
+ ### Goals vs Preferences vs Inferred
258
+
259
+ The system distinguishes three types of user-scoped navigation data:
260
+
261
+ | Type | Defines | Example | Affects ELO | Implementation |
262
+ |------|---------|---------|-------------|----------------|
263
+ | **Goal** | Destination (what to learn) | "Master ear-training" | Yes | `userGoal.ts` (stub) |
264
+ | **Preference** | Path (how to learn) | "Skip text-heavy cards" | No | `filters/userTagPreference.ts` |
265
+ | **Inferred** | Learned patterns | "User prefers visual" | No | `inferredPreference.ts` (stub) |
266
+
267
+ - **Goals** redefine the optimization target — they scope which content matters for progress
268
+ - **Preferences** constrain the path — they affect card selection without changing progress tracking
269
+ - **Inferred** preferences are learned from behavior — they act as soft suggestions
270
+
271
+ See stub files for detailed architectural intent on goals and inferred preferences.
272
+
273
+ ### Storage API
274
+
275
+ `ContentNavigator` provides protected helper methods:
276
+
277
+ ```typescript
278
+ // Get this strategy's persisted state for the current course
279
+ protected async getStrategyState<T>(): Promise<T | null>
280
+
281
+ // Persist this strategy's state for the current course
282
+ protected async putStrategyState<T>(data: T): Promise<void>
283
+
284
+ // Override to customize the storage key (default: constructor name)
285
+ protected get strategyKey(): string
286
+ ```
287
+
288
+ ### Document Format
289
+
290
+ ```typescript
291
+ interface StrategyStateDoc<T> {
292
+ _id: StrategyStateId; // "STRATEGY_STATE::{courseId}::{strategyKey}"
293
+ docType: DocType.STRATEGY_STATE;
294
+ courseId: string;
295
+ strategyKey: string;
296
+ data: T; // Strategy-specific payload
297
+ updatedAt: string; // ISO timestamp
298
+ }
299
+ ```
300
+
301
+ ### Example: User Tag Preferences
302
+
303
+ `UserTagPreferenceFilter` reads user preferences from strategy state:
304
+
305
+ ```typescript
306
+ interface UserTagPreferenceState {
307
+ /**
308
+ * Tag-specific multipliers.
309
+ * - 0 = exclude (card score = 0)
310
+ * - 0.5 = penalize by 50%
311
+ * - 1.0 = neutral/no effect
312
+ * - 2.0 = 2x preference boost
313
+ * - Higher = stronger preference
314
+ */
315
+ boost: Record<string, number>;
316
+ updatedAt: string;
317
+ }
318
+
319
+ // In filter's transform():
320
+ const prefs = await this.getStrategyState<UserTagPreferenceState>();
321
+ if (!prefs || Object.keys(prefs.boost).length === 0) {
322
+ return cards; // No preferences configured
323
+ }
324
+
325
+ // Apply multipliers (max wins when multiple tags match)
326
+ const multiplier = computeMultiplier(cardTags, prefs.boost);
327
+ return { ...card, score: card.score * multiplier };
328
+ ```
329
+
330
+ **UI Component**: `packages/common-ui/src/components/UserTagPreferences.vue`
331
+ - Slider-based interface (0-2 default range, expandable to 10)
332
+ - All sliders share global max for consistent visual comparison
333
+ - Writes to strategy state via `userDB.putStrategyState()`
334
+
240
335
  ## File Reference
241
336
 
242
337
  | File | Purpose |
@@ -254,12 +349,15 @@ Register in `NavigatorRoles` as `NavigatorRole.FILTER`.
254
349
  | `core/navigators/interferenceMitigator.ts` | Interference filter |
255
350
  | `core/navigators/relativePriority.ts` | Priority filter |
256
351
  | `core/navigators/filters/eloDistance.ts` | ELO distance filter |
352
+ | `core/navigators/filters/userTagPreference.ts` | User tag preference filter |
353
+ | `common-ui/.../UserTagPreferences.vue` | UI for tag preference sliders |
354
+ | `core/navigators/userGoal.ts` | User goal navigator (stub) |
355
+ | `core/navigators/inferredPreference.ts` | Inferred preference navigator (stub) |
356
+ | `core/types/strategyState.ts` | `StrategyStateDoc`, `StrategyStateId` |
257
357
  | `impl/couch/courseDB.ts` | `createNavigator()` entry point |
258
358
 
259
- ## Related TODOs
359
+ ## Related Documentation
260
360
 
261
- - `todo-pipeline-optimization.md` - Batch tag hydration for filter efficiency
262
- - `todo-strategy-authoring.md` - ux and dx for authoring strategies
263
- - todo-pipeline-optimization.md -
264
- - todo-strategy-state-storage
265
- - todo-evolutionary-orchestration
361
+ - `todo-strategy-authoring.md` UX and DX for authoring strategies
362
+ - `todo-evolutionary-orchestration.md` — Long-term adaptive strategy vision
363
+ - `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.18",
7
+ "version": "0.1.21",
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.18",
51
+ "@vue-skuilder/common": "0.1.21",
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.14"
63
+ "vitest": "^4.0.15"
64
64
  },
65
- "stableVersion": "0.1.18"
65
+ "stableVersion": "0.1.21"
66
66
  }
package/src/core/index.ts CHANGED
@@ -3,6 +3,7 @@
3
3
  export * from './interfaces';
4
4
  export * from './types/types-legacy';
5
5
  export * from './types/user';
6
+ export * from './types/strategyState';
6
7
  export * from '../util/Loggable';
7
8
  export * from './util';
8
9
  export * from './navigators';
@@ -1,6 +1,4 @@
1
1
  import { ClassroomConfig } from '@vue-skuilder/common';
2
- import { ScheduledCard } from '../types/user';
3
- import { StudySessionNewItem, StudySessionReviewItem } from './contentSource';
4
2
 
5
3
  /**
6
4
  * Classroom management
@@ -29,17 +27,11 @@ export interface TeacherClassroomDBInterface extends ClassroomDBInterface {
29
27
  removeContent?(content: AssignedContent): Promise<void>;
30
28
  }
31
29
 
32
- export interface StudentClassroomDBInterface extends ClassroomDBInterface {
33
- /**
34
- * For student interfaces: get pending reviews
35
- */
36
- getPendingReviews?(): Promise<(StudySessionReviewItem & ScheduledCard)[]>;
37
-
38
- /**
39
- * For student interfaces: get new cards
40
- */
41
- getNewCards?(limit?: number): Promise<StudySessionNewItem[]>;
42
- }
30
+ /**
31
+ * Student-facing classroom interface.
32
+ * Content is accessed via StudyContentSource.getWeightedCards().
33
+ */
34
+ export type StudentClassroomDBInterface = ClassroomDBInterface;
43
35
 
44
36
  export type AssignedContent = AssignedCourse | AssignedTag | AssignedCard;
45
37
 
@@ -1,43 +1,10 @@
1
1
  import { getDataLayer } from '@db/factory';
2
2
  import { UserDBInterface } from '..';
3
3
  import { StudentClassroomDB } from '../../impl/couch/classroomDB';
4
- import { ScheduledCard } from '@db/core/types/user';
5
4
  import { WeightedCard } from '../navigators';
6
5
  import { TagFilter, hasActiveFilter } from '@vue-skuilder/common';
7
6
  import { TagFilteredContentSource } from '../../study/TagFilteredContentSource';
8
7
 
9
- // ============================================================================
10
- // API MIGRATION NOTICE
11
- // ============================================================================
12
- //
13
- // The StudyContentSource interface is being superseded by the ContentNavigator
14
- // class and its getWeightedCards() API. See:
15
- // packages/db/src/core/navigators/ARCHITECTURE.md
16
- //
17
- // HISTORICAL CONTEXT:
18
- // - This interface was designed to abstract 'classrooms' and 'courses' as
19
- // content sources for study sessions.
20
- // - getNewCards() and getPendingReviews() were artifacts of two hard-coded
21
- // navigation strategies: ELO proximity (new) and SRS scheduling (reviews).
22
- // - The new/review split reflected implementation details, not fundamentals.
23
- //
24
- // THE PROBLEM:
25
- // - "What does 'get reviews' mean for an interference mitigator?" - it doesn't.
26
- // - SRS is just one strategy that could express review urgency as scores.
27
- // - Some strategies generate candidates, others filter/score them.
28
- //
29
- // THE SOLUTION:
30
- // - ContentNavigator.getWeightedCards() returns unified scored candidates.
31
- // - WeightedCard.source field distinguishes new/review/failed (metadata, not API).
32
- // - Strategies compose via delegate pattern (filter wraps generator).
33
- //
34
- // MIGRATION PATH:
35
- // 1. ContentNavigator implements StudyContentSource for backward compat
36
- // 2. SessionController will migrate to call getWeightedCards()
37
- // 3. Legacy methods will be deprecated, then removed
38
- //
39
- // ============================================================================
40
-
41
8
  export type StudySessionFailedItem = StudySessionFailedNewItem | StudySessionFailedReviewItem;
42
9
 
43
10
  export interface StudySessionFailedNewItem extends StudySessionItem {
@@ -89,49 +56,22 @@ export interface ContentSourceID {
89
56
  /**
90
57
  * Interface for sources that provide study content to SessionController.
91
58
  *
92
- * @deprecated This interface will be superseded by ContentNavigator.getWeightedCards().
93
- * The getNewCards/getPendingReviews split was an artifact of hard-coded ELO and SRS
94
- * strategies. The new API returns unified WeightedCard[] with scores.
95
- *
96
- * MIGRATION:
97
- * - Implement ContentNavigator instead of StudyContentSource directly
98
- * - Override getWeightedCards() as the primary method
99
- * - Legacy methods can delegate to getWeightedCards() or be left as-is
59
+ * Content sources return scored candidates via getWeightedCards(), which
60
+ * SessionController uses to populate study queues.
100
61
  *
101
- * See: packages/db/src/core/navigators/ARCHITECTURE.md
62
+ * See: packages/db/docs/navigators-architecture.md
102
63
  */
103
64
  export interface StudyContentSource {
104
- /**
105
- * Get cards scheduled for review based on SRS algorithm.
106
- *
107
- * @deprecated Will be replaced by getWeightedCards() which returns scored candidates.
108
- * Review urgency will be expressed as a score rather than a separate method.
109
- */
110
- getPendingReviews(): Promise<(StudySessionReviewItem & ScheduledCard)[]>;
111
-
112
- /**
113
- * Get new cards for introduction, typically ordered by ELO proximity.
114
- *
115
- * @deprecated Will be replaced by getWeightedCards() which returns scored candidates.
116
- * New card selection and scoring will be unified with review scoring.
117
- *
118
- * @param n - Maximum number of new cards to return
119
- */
120
- getNewCards(n?: number): Promise<StudySessionNewItem[]>;
121
-
122
65
  /**
123
66
  * Get cards with suitability scores for presentation.
124
67
  *
125
- * This is the PRIMARY API for content sources going forward. Returns unified
126
- * scored candidates that can be sorted and selected by SessionController.
127
- *
128
- * The `source` field on WeightedCard indicates origin ('new' | 'review' | 'failed')
129
- * for queue routing purposes during the migration period.
68
+ * Returns unified scored candidates that can be sorted and selected by SessionController.
69
+ * The card origin ('new' | 'review' | 'failed') is determined by provenance metadata.
130
70
  *
131
71
  * @param limit - Maximum number of cards to return
132
72
  * @returns Cards sorted by score descending
133
73
  */
134
- getWeightedCards?(limit: number): Promise<WeightedCard[]>;
74
+ getWeightedCards(limit: number): Promise<WeightedCard[]>;
135
75
  }
136
76
  // #endregion docs_StudyContentSource
137
77
 
@@ -1,5 +1,5 @@
1
1
  import { CourseConfig, CourseElo, DataShape, SkuilderCourseData } from '@vue-skuilder/common';
2
- import { StudySessionNewItem, StudySessionItem } from './contentSource';
2
+ import { StudyContentSource, StudySessionItem } from './contentSource';
3
3
  import { TagStub, Tag, QualifiedCardID } from '../types/types-legacy';
4
4
  import { DataLayerResult } from '../types/db';
5
5
  import { NavigationStrategyManager } from './navigationStrategyManager';
@@ -26,7 +26,7 @@ export interface CourseInfo {
26
26
  registeredUsers: number;
27
27
  }
28
28
 
29
- export interface CourseDBInterface extends NavigationStrategyManager {
29
+ export interface CourseDBInterface extends NavigationStrategyManager, StudyContentSource {
30
30
  /**
31
31
  * Get course config
32
32
  */
@@ -74,11 +74,6 @@ export interface CourseDBInterface extends NavigationStrategyManager {
74
74
  */
75
75
  updateCardElo(cardId: string, elo: CourseElo): Promise<PouchDB.Core.Response>;
76
76
 
77
- /**
78
- * Get new cards for study
79
- */
80
- getNewCards(limit?: number): Promise<StudySessionNewItem[]>;
81
-
82
77
  /**
83
78
  * Get cards centered at a particular ELO rating
84
79
  */
@@ -92,6 +87,19 @@ export interface CourseDBInterface extends NavigationStrategyManager {
92
87
  */
93
88
  getAppliedTags(cardId: string): Promise<PouchDB.Query.Response<TagStub>>;
94
89
 
90
+ /**
91
+ * Get tags for multiple cards in a single batch query.
92
+ * More efficient than calling getAppliedTags() for each card.
93
+ *
94
+ * This method reduces redundant database operations when multiple filters
95
+ * need tag data for the same cards. The Pipeline uses this to pre-hydrate
96
+ * tags on WeightedCard objects before filters run.
97
+ *
98
+ * @param cardIds - Array of card IDs to fetch tags for
99
+ * @returns Map from cardId to array of tag names
100
+ */
101
+ getAppliedTagsBatch(cardIds: string[]): Promise<Map<string, string[]>>;
102
+
95
103
  /**
96
104
  * Add a tag to a card
97
105
  */
@@ -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
  /**