@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.
Files changed (59) hide show
  1. package/dist/{classroomDB-BgfrVb8d.d.ts → classroomDB-CZdMBiTU.d.ts} +71 -2
  2. package/dist/{classroomDB-CTOenngH.d.cts → classroomDB-PxDZTky3.d.cts} +71 -2
  3. package/dist/core/index.d.cts +80 -6
  4. package/dist/core/index.d.ts +80 -6
  5. package/dist/core/index.js +370 -52
  6. package/dist/core/index.js.map +1 -1
  7. package/dist/core/index.mjs +369 -52
  8. package/dist/core/index.mjs.map +1 -1
  9. package/dist/{dataLayerProvider-D6PoCwS6.d.cts → dataLayerProvider-D0MoZMjH.d.cts} +1 -1
  10. package/dist/{dataLayerProvider-CZxC9GtB.d.ts → dataLayerProvider-D8o6ZnKW.d.ts} +1 -1
  11. package/dist/impl/couch/index.d.cts +4 -3
  12. package/dist/impl/couch/index.d.ts +4 -3
  13. package/dist/impl/couch/index.js +371 -55
  14. package/dist/impl/couch/index.js.map +1 -1
  15. package/dist/impl/couch/index.mjs +371 -55
  16. package/dist/impl/couch/index.mjs.map +1 -1
  17. package/dist/impl/static/index.d.cts +5 -4
  18. package/dist/impl/static/index.d.ts +5 -4
  19. package/dist/impl/static/index.js +356 -44
  20. package/dist/impl/static/index.js.map +1 -1
  21. package/dist/impl/static/index.mjs +356 -44
  22. package/dist/impl/static/index.mjs.map +1 -1
  23. package/dist/{index-D-Fa4Smt.d.cts → index-B_j6u5E4.d.cts} +1 -1
  24. package/dist/{index-CD8BZz2k.d.ts → index-Dj0SEgk3.d.ts} +1 -1
  25. package/dist/index.d.cts +10 -10
  26. package/dist/index.d.ts +10 -10
  27. package/dist/index.js +382 -55
  28. package/dist/index.js.map +1 -1
  29. package/dist/index.mjs +381 -55
  30. package/dist/index.mjs.map +1 -1
  31. package/dist/{types-CzPDLAK6.d.cts → types-Bn0itutr.d.cts} +1 -1
  32. package/dist/{types-CewsN87z.d.ts → types-DQaXnuoc.d.ts} +1 -1
  33. package/dist/{types-legacy-6ettoclI.d.cts → types-legacy-DDY4N-Uq.d.cts} +3 -1
  34. package/dist/{types-legacy-6ettoclI.d.ts → types-legacy-DDY4N-Uq.d.ts} +3 -1
  35. package/dist/util/packer/index.d.cts +3 -3
  36. package/dist/util/packer/index.d.ts +3 -3
  37. package/docs/navigators-architecture.md +115 -10
  38. package/package.json +4 -4
  39. package/src/core/index.ts +1 -0
  40. package/src/core/interfaces/courseDB.ts +13 -0
  41. package/src/core/interfaces/userDB.ts +32 -0
  42. package/src/core/navigators/Pipeline.ts +127 -14
  43. package/src/core/navigators/filters/index.ts +3 -0
  44. package/src/core/navigators/filters/userTagPreference.ts +232 -0
  45. package/src/core/navigators/hierarchyDefinition.ts +4 -4
  46. package/src/core/navigators/index.ts +59 -0
  47. package/src/core/navigators/inferredPreference.ts +107 -0
  48. package/src/core/navigators/interferenceMitigator.ts +1 -13
  49. package/src/core/navigators/relativePriority.ts +2 -14
  50. package/src/core/navigators/userGoal.ts +136 -0
  51. package/src/core/types/strategyState.ts +84 -0
  52. package/src/core/types/types-legacy.ts +2 -0
  53. package/src/impl/common/BaseUserDB.ts +74 -7
  54. package/src/impl/couch/adminDB.ts +1 -2
  55. package/src/impl/couch/courseDB.ts +30 -10
  56. package/src/impl/static/courseDB.ts +11 -0
  57. package/tests/core/navigators/Pipeline.test.ts +1 -0
  58. package/docs/todo-pipeline-optimization.md +0 -117
  59. package/docs/todo-strategy-state-storage.md +0 -278
@@ -258,7 +258,8 @@ var init_types_legacy = __esm({
258
258
  ["QUESTION" /* QUESTIONTYPE */]: "QUESTION",
259
259
  ["VIEW" /* VIEW */]: "VIEW",
260
260
  ["PEDAGOGY" /* PEDAGOGY */]: "PEDAGOGY",
261
- ["NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */]: "NAVIGATION_STRATEGY"
261
+ ["NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */]: "NAVIGATION_STRATEGY",
262
+ ["STRATEGY_STATE" /* STRATEGY_STATE */]: "STRATEGY_STATE"
262
263
  };
263
264
  }
264
265
  });
@@ -789,6 +790,41 @@ __export(Pipeline_exports, {
789
790
  Pipeline: () => Pipeline
790
791
  });
791
792
  import { toCourseElo as toCourseElo2 } from "@vue-skuilder/common";
793
+ function logPipelineConfig(generator, filters) {
794
+ const filterList = filters.length > 0 ? "\n - " + filters.map((f) => f.name).join("\n - ") : " none";
795
+ logger.info(
796
+ `[Pipeline] Configuration:
797
+ Generator: ${generator.name}
798
+ Filters:${filterList}`
799
+ );
800
+ }
801
+ function logTagHydration(cards, tagsByCard) {
802
+ const totalTags = Array.from(tagsByCard.values()).reduce((sum, tags) => sum + tags.length, 0);
803
+ const cardsWithTags = Array.from(tagsByCard.values()).filter((tags) => tags.length > 0).length;
804
+ logger.debug(
805
+ `[Pipeline] Tag hydration: ${cards.length} cards, ${cardsWithTags} have tags (${totalTags} total tags) - single batch query`
806
+ );
807
+ }
808
+ function logExecutionSummary(generatorName, generatedCount, filterCount, finalCount, topScores) {
809
+ const scoreDisplay = topScores.length > 0 ? topScores.map((s) => s.toFixed(2)).join(", ") : "none";
810
+ logger.info(
811
+ `[Pipeline] Execution: ${generatorName} produced ${generatedCount} \u2192 ${filterCount} filters \u2192 ${finalCount} results (top scores: ${scoreDisplay})`
812
+ );
813
+ }
814
+ function logCardProvenance(cards, maxCards = 3) {
815
+ const cardsToLog = cards.slice(0, maxCards);
816
+ logger.debug(`[Pipeline] Provenance for top ${cardsToLog.length} cards:`);
817
+ for (const card of cardsToLog) {
818
+ logger.debug(`[Pipeline] ${card.cardId} (final score: ${card.score.toFixed(3)}):`);
819
+ for (const entry of card.provenance) {
820
+ const scoreChange = entry.score.toFixed(3);
821
+ const action = entry.action.padEnd(9);
822
+ logger.debug(
823
+ `[Pipeline] ${action} ${scoreChange} - ${entry.strategyName}: ${entry.reason}`
824
+ );
825
+ }
826
+ }
827
+ }
792
828
  var Pipeline;
793
829
  var init_Pipeline = __esm({
794
830
  "src/core/navigators/Pipeline.ts"() {
@@ -812,19 +848,18 @@ var init_Pipeline = __esm({
812
848
  this.filters = filters;
813
849
  this.user = user;
814
850
  this.course = course;
815
- logger.debug(
816
- `[Pipeline] Created with generator '${generator.name}' and ${filters.length} filters: ${filters.map((f) => f.name).join(", ")}`
817
- );
851
+ logPipelineConfig(generator, filters);
818
852
  }
819
853
  /**
820
854
  * Get weighted cards by running generator and applying filters.
821
855
  *
822
856
  * 1. Build shared context (user ELO, etc.)
823
857
  * 2. Get candidates from generator (passing context)
824
- * 3. Apply each filter sequentially
825
- * 4. Remove zero-score cards
826
- * 5. Sort by score descending
827
- * 6. Return top N
858
+ * 3. Batch hydrate tags for all candidates
859
+ * 4. Apply each filter sequentially
860
+ * 5. Remove zero-score cards
861
+ * 6. Sort by score descending
862
+ * 7. Return top N
828
863
  *
829
864
  * @param limit - Maximum number of cards to return
830
865
  * @returns Cards sorted by score descending
@@ -837,7 +872,9 @@ var init_Pipeline = __esm({
837
872
  `[Pipeline] Fetching ${fetchLimit} candidates from generator '${this.generator.name}'`
838
873
  );
839
874
  let cards = await this.generator.getWeightedCards(fetchLimit, context);
840
- logger.debug(`[Pipeline] Generator returned ${cards.length} candidates`);
875
+ const generatedCount = cards.length;
876
+ logger.debug(`[Pipeline] Generator returned ${generatedCount} candidates`);
877
+ cards = await this.hydrateTags(cards);
841
878
  for (const filter of this.filters) {
842
879
  const beforeCount = cards.length;
843
880
  cards = await filter.transform(cards, context);
@@ -846,11 +883,33 @@ var init_Pipeline = __esm({
846
883
  cards = cards.filter((c) => c.score > 0);
847
884
  cards.sort((a, b) => b.score - a.score);
848
885
  const result = cards.slice(0, limit);
849
- logger.debug(
850
- `[Pipeline] Returning ${result.length} cards (top scores: ${result.slice(0, 3).map((c) => c.score.toFixed(2)).join(", ")}...)`
851
- );
886
+ const topScores = result.slice(0, 3).map((c) => c.score);
887
+ logExecutionSummary(this.generator.name, generatedCount, this.filters.length, result.length, topScores);
888
+ logCardProvenance(result, 3);
852
889
  return result;
853
890
  }
891
+ /**
892
+ * Batch hydrate tags for all cards.
893
+ *
894
+ * Fetches tags for all cards in a single database query and attaches them
895
+ * to the WeightedCard objects. Filters can then use card.tags instead of
896
+ * making individual getAppliedTags() calls.
897
+ *
898
+ * @param cards - Cards to hydrate
899
+ * @returns Cards with tags populated
900
+ */
901
+ async hydrateTags(cards) {
902
+ if (cards.length === 0) {
903
+ return cards;
904
+ }
905
+ const cardIds = cards.map((c) => c.cardId);
906
+ const tagsByCard = await this.course.getAppliedTagsBatch(cardIds);
907
+ logTagHydration(cards, tagsByCard);
908
+ return cards.map((card) => ({
909
+ ...card,
910
+ tags: tagsByCard.get(card.cardId) ?? []
911
+ }));
912
+ }
854
913
  /**
855
914
  * Build shared context for generator and filters.
856
915
  *
@@ -1210,15 +1269,144 @@ var init_eloDistance = __esm({
1210
1269
  }
1211
1270
  });
1212
1271
 
1272
+ // src/core/navigators/filters/userTagPreference.ts
1273
+ var userTagPreference_exports = {};
1274
+ __export(userTagPreference_exports, {
1275
+ default: () => UserTagPreferenceFilter
1276
+ });
1277
+ var UserTagPreferenceFilter;
1278
+ var init_userTagPreference = __esm({
1279
+ "src/core/navigators/filters/userTagPreference.ts"() {
1280
+ "use strict";
1281
+ init_navigators();
1282
+ UserTagPreferenceFilter = class extends ContentNavigator {
1283
+ _strategyData;
1284
+ /** Human-readable name for CardFilter interface */
1285
+ name;
1286
+ constructor(user, course, strategyData) {
1287
+ super(user, course, strategyData);
1288
+ this._strategyData = strategyData;
1289
+ this.name = strategyData.name || "User Tag Preferences";
1290
+ }
1291
+ /**
1292
+ * Compute multiplier for a card based on its tags and user preferences.
1293
+ * Returns the maximum multiplier among all matching tags, or 1.0 if no matches.
1294
+ */
1295
+ computeMultiplier(cardTags, boostMap) {
1296
+ const multipliers = cardTags.map((tag) => boostMap[tag]).filter((val) => val !== void 0);
1297
+ if (multipliers.length === 0) {
1298
+ return 1;
1299
+ }
1300
+ return Math.max(...multipliers);
1301
+ }
1302
+ /**
1303
+ * Build human-readable reason for the filter's decision.
1304
+ */
1305
+ buildReason(cardTags, boostMap, multiplier) {
1306
+ const matchingTags = cardTags.filter((tag) => boostMap[tag] === multiplier);
1307
+ if (multiplier === 0) {
1308
+ return `Excluded by user preference: ${matchingTags.join(", ")} (${multiplier}x)`;
1309
+ }
1310
+ if (multiplier < 1) {
1311
+ return `Penalized by user preference: ${matchingTags.join(", ")} (${multiplier.toFixed(2)}x)`;
1312
+ }
1313
+ if (multiplier > 1) {
1314
+ return `Boosted by user preference: ${matchingTags.join(", ")} (${multiplier.toFixed(2)}x)`;
1315
+ }
1316
+ return "No matching user preferences";
1317
+ }
1318
+ /**
1319
+ * CardFilter.transform implementation.
1320
+ *
1321
+ * Apply user tag preferences:
1322
+ * 1. Read preferences from strategy state
1323
+ * 2. If no preferences, pass through unchanged
1324
+ * 3. For each card:
1325
+ * - Look up tag in boost record
1326
+ * - If tag found: apply multiplier (0 = exclude, 1 = neutral, >1 = boost)
1327
+ * - If multiple tags match: use max multiplier
1328
+ * - Append provenance with clear reason
1329
+ */
1330
+ async transform(cards, _context) {
1331
+ const prefs = await this.getStrategyState();
1332
+ if (!prefs || Object.keys(prefs.boost).length === 0) {
1333
+ return cards.map((card) => ({
1334
+ ...card,
1335
+ provenance: [
1336
+ ...card.provenance,
1337
+ {
1338
+ strategy: "userTagPreference",
1339
+ strategyName: this.strategyName || this.name,
1340
+ strategyId: this.strategyId || this._strategyData._id,
1341
+ action: "passed",
1342
+ score: card.score,
1343
+ reason: "No user tag preferences configured"
1344
+ }
1345
+ ]
1346
+ }));
1347
+ }
1348
+ const adjusted = await Promise.all(
1349
+ cards.map(async (card) => {
1350
+ const cardTags = card.tags ?? [];
1351
+ const multiplier = this.computeMultiplier(cardTags, prefs.boost);
1352
+ const finalScore = Math.min(1, card.score * multiplier);
1353
+ let action;
1354
+ if (multiplier === 0 || multiplier < 1) {
1355
+ action = "penalized";
1356
+ } else if (multiplier > 1) {
1357
+ action = "boosted";
1358
+ } else {
1359
+ action = "passed";
1360
+ }
1361
+ return {
1362
+ ...card,
1363
+ score: finalScore,
1364
+ provenance: [
1365
+ ...card.provenance,
1366
+ {
1367
+ strategy: "userTagPreference",
1368
+ strategyName: this.strategyName || this.name,
1369
+ strategyId: this.strategyId || this._strategyData._id,
1370
+ action,
1371
+ score: finalScore,
1372
+ reason: this.buildReason(cardTags, prefs.boost, multiplier)
1373
+ }
1374
+ ]
1375
+ };
1376
+ })
1377
+ );
1378
+ return adjusted;
1379
+ }
1380
+ /**
1381
+ * Legacy getWeightedCards - throws as filters should not be used as generators.
1382
+ */
1383
+ async getWeightedCards(_limit) {
1384
+ throw new Error(
1385
+ "UserTagPreferenceFilter is a filter and should not be used as a generator. Use Pipeline with a generator and this filter via transform()."
1386
+ );
1387
+ }
1388
+ // Legacy methods - stub implementations since filters don't generate cards
1389
+ async getNewCards(_n) {
1390
+ return [];
1391
+ }
1392
+ async getPendingReviews() {
1393
+ return [];
1394
+ }
1395
+ };
1396
+ }
1397
+ });
1398
+
1213
1399
  // src/core/navigators/filters/index.ts
1214
1400
  var filters_exports = {};
1215
1401
  __export(filters_exports, {
1402
+ UserTagPreferenceFilter: () => UserTagPreferenceFilter,
1216
1403
  createEloDistanceFilter: () => createEloDistanceFilter
1217
1404
  });
1218
1405
  var init_filters = __esm({
1219
1406
  "src/core/navigators/filters/index.ts"() {
1220
1407
  "use strict";
1221
1408
  init_eloDistance();
1409
+ init_userTagPreference();
1222
1410
  }
1223
1411
  });
1224
1412
 
@@ -1451,10 +1639,9 @@ var init_hierarchyDefinition = __esm({
1451
1639
  /**
1452
1640
  * Check if a card is unlocked and generate reason.
1453
1641
  */
1454
- async checkCardUnlock(cardId, course, unlockedTags, masteredTags) {
1642
+ async checkCardUnlock(card, course, unlockedTags, masteredTags) {
1455
1643
  try {
1456
- const tagResponse = await course.getAppliedTags(cardId);
1457
- const cardTags = tagResponse.rows.map((row) => row.value?.name || row.key);
1644
+ const cardTags = card.tags ?? [];
1458
1645
  const lockedTags = cardTags.filter(
1459
1646
  (tag) => this.hasPrerequisites(tag) && !unlockedTags.has(tag)
1460
1647
  );
@@ -1491,7 +1678,7 @@ var init_hierarchyDefinition = __esm({
1491
1678
  const gated = [];
1492
1679
  for (const card of cards) {
1493
1680
  const { isUnlocked, reason } = await this.checkCardUnlock(
1494
- card.cardId,
1681
+ card,
1495
1682
  context.course,
1496
1683
  unlockedTags,
1497
1684
  masteredTags
@@ -1537,6 +1724,19 @@ var init_hierarchyDefinition = __esm({
1537
1724
  }
1538
1725
  });
1539
1726
 
1727
+ // src/core/navigators/inferredPreference.ts
1728
+ var inferredPreference_exports = {};
1729
+ __export(inferredPreference_exports, {
1730
+ INFERRED_PREFERENCE_NAVIGATOR_STUB: () => INFERRED_PREFERENCE_NAVIGATOR_STUB
1731
+ });
1732
+ var INFERRED_PREFERENCE_NAVIGATOR_STUB;
1733
+ var init_inferredPreference = __esm({
1734
+ "src/core/navigators/inferredPreference.ts"() {
1735
+ "use strict";
1736
+ INFERRED_PREFERENCE_NAVIGATOR_STUB = true;
1737
+ }
1738
+ });
1739
+
1540
1740
  // src/core/navigators/interferenceMitigator.ts
1541
1741
  var interferenceMitigator_exports = {};
1542
1742
  __export(interferenceMitigator_exports, {
@@ -1668,17 +1868,6 @@ var init_interferenceMitigator = __esm({
1668
1868
  }
1669
1869
  return avoid;
1670
1870
  }
1671
- /**
1672
- * Get tags for a single card
1673
- */
1674
- async getCardTags(cardId, course) {
1675
- try {
1676
- const tagResponse = await course.getAppliedTags(cardId);
1677
- return tagResponse.rows.map((row) => row.value?.name || row.key).filter(Boolean);
1678
- } catch {
1679
- return [];
1680
- }
1681
- }
1682
1871
  /**
1683
1872
  * Compute interference score reduction for a card.
1684
1873
  * Returns: { multiplier, interfering tags, reason }
@@ -1730,7 +1919,7 @@ var init_interferenceMitigator = __esm({
1730
1919
  const tagsToAvoid = this.getTagsToAvoid(immatureTags);
1731
1920
  const adjusted = [];
1732
1921
  for (const card of cards) {
1733
- const cardTags = await this.getCardTags(card.cardId, context.course);
1922
+ const cardTags = card.tags ?? [];
1734
1923
  const { multiplier, reason } = this.computeInterferenceEffect(
1735
1924
  cardTags,
1736
1925
  tagsToAvoid,
@@ -1875,27 +2064,16 @@ var init_relativePriority = __esm({
1875
2064
  return `Low-priority tags: ${tagList}${more} (priority ${priority.toFixed(2)} \u2192 reduce ${boostFactor.toFixed(2)}x \u2192 ${finalScore.toFixed(2)})`;
1876
2065
  }
1877
2066
  }
1878
- /**
1879
- * Get tags for a single card.
1880
- */
1881
- async getCardTags(cardId, course) {
1882
- try {
1883
- const tagResponse = await course.getAppliedTags(cardId);
1884
- return tagResponse.rows.map((r) => r.doc?.name).filter((x) => !!x);
1885
- } catch {
1886
- return [];
1887
- }
1888
- }
1889
2067
  /**
1890
2068
  * CardFilter.transform implementation.
1891
2069
  *
1892
2070
  * Apply priority-adjusted scoring. Cards with high-priority tags get boosted,
1893
2071
  * cards with low-priority tags get reduced scores.
1894
2072
  */
1895
- async transform(cards, context) {
2073
+ async transform(cards, _context) {
1896
2074
  const adjusted = await Promise.all(
1897
2075
  cards.map(async (card) => {
1898
- const cardTags = await this.getCardTags(card.cardId, context.course);
2076
+ const cardTags = card.tags ?? [];
1899
2077
  const priority = this.computeCardPriority(cardTags);
1900
2078
  const boostFactor = this.computeBoostFactor(priority);
1901
2079
  const finalScore = Math.max(0, Math.min(1, card.score * boostFactor));
@@ -2062,6 +2240,19 @@ var init_srs = __esm({
2062
2240
  }
2063
2241
  });
2064
2242
 
2243
+ // src/core/navigators/userGoal.ts
2244
+ var userGoal_exports = {};
2245
+ __export(userGoal_exports, {
2246
+ USER_GOAL_NAVIGATOR_STUB: () => USER_GOAL_NAVIGATOR_STUB
2247
+ });
2248
+ var USER_GOAL_NAVIGATOR_STUB;
2249
+ var init_userGoal = __esm({
2250
+ "src/core/navigators/userGoal.ts"() {
2251
+ "use strict";
2252
+ USER_GOAL_NAVIGATOR_STUB = true;
2253
+ }
2254
+ });
2255
+
2065
2256
  // import("./**/*") in src/core/navigators/index.ts
2066
2257
  var globImport;
2067
2258
  var init_ = __esm({
@@ -2074,14 +2265,17 @@ var init_ = __esm({
2074
2265
  "./filters/eloDistance.ts": () => Promise.resolve().then(() => (init_eloDistance(), eloDistance_exports)),
2075
2266
  "./filters/index.ts": () => Promise.resolve().then(() => (init_filters(), filters_exports)),
2076
2267
  "./filters/types.ts": () => Promise.resolve().then(() => (init_types(), types_exports)),
2268
+ "./filters/userTagPreference.ts": () => Promise.resolve().then(() => (init_userTagPreference(), userTagPreference_exports)),
2077
2269
  "./generators/index.ts": () => Promise.resolve().then(() => (init_generators(), generators_exports)),
2078
2270
  "./generators/types.ts": () => Promise.resolve().then(() => (init_types2(), types_exports2)),
2079
2271
  "./hardcodedOrder.ts": () => Promise.resolve().then(() => (init_hardcodedOrder(), hardcodedOrder_exports)),
2080
2272
  "./hierarchyDefinition.ts": () => Promise.resolve().then(() => (init_hierarchyDefinition(), hierarchyDefinition_exports)),
2081
2273
  "./index.ts": () => Promise.resolve().then(() => (init_navigators(), navigators_exports)),
2274
+ "./inferredPreference.ts": () => Promise.resolve().then(() => (init_inferredPreference(), inferredPreference_exports)),
2082
2275
  "./interferenceMitigator.ts": () => Promise.resolve().then(() => (init_interferenceMitigator(), interferenceMitigator_exports)),
2083
2276
  "./relativePriority.ts": () => Promise.resolve().then(() => (init_relativePriority(), relativePriority_exports)),
2084
- "./srs.ts": () => Promise.resolve().then(() => (init_srs(), srs_exports))
2277
+ "./srs.ts": () => Promise.resolve().then(() => (init_srs(), srs_exports)),
2278
+ "./userGoal.ts": () => Promise.resolve().then(() => (init_userGoal(), userGoal_exports))
2085
2279
  });
2086
2280
  }
2087
2281
  });
@@ -2130,6 +2324,7 @@ var init_navigators = __esm({
2130
2324
  Navigators2["HIERARCHY"] = "hierarchyDefinition";
2131
2325
  Navigators2["INTERFERENCE"] = "interferenceMitigator";
2132
2326
  Navigators2["RELATIVE_PRIORITY"] = "relativePriority";
2327
+ Navigators2["USER_TAG_PREFERENCE"] = "userTagPreference";
2133
2328
  return Navigators2;
2134
2329
  })(Navigators || {});
2135
2330
  NavigatorRole = /* @__PURE__ */ ((NavigatorRole2) => {
@@ -2143,7 +2338,8 @@ var init_navigators = __esm({
2143
2338
  ["hardcodedOrder" /* HARDCODED */]: "generator" /* GENERATOR */,
2144
2339
  ["hierarchyDefinition" /* HIERARCHY */]: "filter" /* FILTER */,
2145
2340
  ["interferenceMitigator" /* INTERFERENCE */]: "filter" /* FILTER */,
2146
- ["relativePriority" /* RELATIVE_PRIORITY */]: "filter" /* FILTER */
2341
+ ["relativePriority" /* RELATIVE_PRIORITY */]: "filter" /* FILTER */,
2342
+ ["userTagPreference" /* USER_TAG_PREFERENCE */]: "filter" /* FILTER */
2147
2343
  };
2148
2344
  ContentNavigator = class {
2149
2345
  /** User interface for this navigation session */
@@ -2168,6 +2364,52 @@ var init_navigators = __esm({
2168
2364
  this.strategyId = strategyData._id;
2169
2365
  }
2170
2366
  }
2367
+ // ============================================================================
2368
+ // STRATEGY STATE HELPERS
2369
+ // ============================================================================
2370
+ //
2371
+ // These methods allow strategies to persist their own state (user preferences,
2372
+ // learned patterns, temporal tracking) in the user database.
2373
+ //
2374
+ // ============================================================================
2375
+ /**
2376
+ * Unique key identifying this strategy for state storage.
2377
+ *
2378
+ * Defaults to the constructor name (e.g., "UserTagPreferenceFilter").
2379
+ * Override in subclasses if multiple instances of the same strategy type
2380
+ * need separate state storage.
2381
+ */
2382
+ get strategyKey() {
2383
+ return this.constructor.name;
2384
+ }
2385
+ /**
2386
+ * Get this strategy's persisted state for the current course.
2387
+ *
2388
+ * @returns The strategy's data payload, or null if no state exists
2389
+ * @throws Error if user or course is not initialized
2390
+ */
2391
+ async getStrategyState() {
2392
+ if (!this.user || !this.course) {
2393
+ throw new Error(
2394
+ `Cannot get strategy state: navigator not properly initialized. Ensure user and course are provided to constructor.`
2395
+ );
2396
+ }
2397
+ return this.user.getStrategyState(this.course.getCourseID(), this.strategyKey);
2398
+ }
2399
+ /**
2400
+ * Persist this strategy's state for the current course.
2401
+ *
2402
+ * @param data - The strategy's data payload to store
2403
+ * @throws Error if user or course is not initialized
2404
+ */
2405
+ async putStrategyState(data) {
2406
+ if (!this.user || !this.course) {
2407
+ throw new Error(
2408
+ `Cannot put strategy state: navigator not properly initialized. Ensure user and course are provided to constructor.`
2409
+ );
2410
+ }
2411
+ return this.user.putStrategyState(this.course.getCourseID(), this.strategyKey, data);
2412
+ }
2171
2413
  /**
2172
2414
  * Factory method to create navigator instances dynamically.
2173
2415
  *
@@ -2584,15 +2826,6 @@ var init_courseDB = __esm({
2584
2826
  ret[r.id] = r.doc.id_displayable_data;
2585
2827
  }
2586
2828
  });
2587
- await Promise.all(
2588
- cards.rows.map((r) => {
2589
- return async () => {
2590
- if (isSuccessRow(r)) {
2591
- ret[r.id] = r.doc.id_displayable_data;
2592
- }
2593
- };
2594
- })
2595
- );
2596
2829
  return ret;
2597
2830
  }
2598
2831
  async getCardsByELO(elo, cardLimit) {
@@ -2677,6 +2910,28 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
2677
2910
  throw new Error(`Failed to find tags for card ${this.id}-${cardId}`);
2678
2911
  }
2679
2912
  }
2913
+ async getAppliedTagsBatch(cardIds) {
2914
+ if (cardIds.length === 0) {
2915
+ return /* @__PURE__ */ new Map();
2916
+ }
2917
+ const db = getCourseDB2(this.id);
2918
+ const result = await db.query("getTags", {
2919
+ keys: cardIds,
2920
+ include_docs: false
2921
+ });
2922
+ const tagsByCard = /* @__PURE__ */ new Map();
2923
+ for (const cardId of cardIds) {
2924
+ tagsByCard.set(cardId, []);
2925
+ }
2926
+ for (const row of result.rows) {
2927
+ const cardId = row.key;
2928
+ const tagName = row.value?.name;
2929
+ if (tagName && tagsByCard.has(cardId)) {
2930
+ tagsByCard.get(cardId).push(tagName);
2931
+ }
2932
+ }
2933
+ return tagsByCard;
2934
+ }
2680
2935
  async addTagToCard(cardId, tagId, updateELO) {
2681
2936
  return await addTagToCard(
2682
2937
  this.id,
@@ -3601,6 +3856,16 @@ var init_user = __esm({
3601
3856
  }
3602
3857
  });
3603
3858
 
3859
+ // src/core/types/strategyState.ts
3860
+ function buildStrategyStateId(courseId, strategyKey) {
3861
+ return `STRATEGY_STATE::${courseId}::${strategyKey}`;
3862
+ }
3863
+ var init_strategyState = __esm({
3864
+ "src/core/types/strategyState.ts"() {
3865
+ "use strict";
3866
+ }
3867
+ });
3868
+
3604
3869
  // src/core/util/index.ts
3605
3870
  function getCardHistoryID(courseID, cardID) {
3606
3871
  return `${DocTypePrefixes["CARDRECORD" /* CARDRECORD */]}-${courseID}-${cardID}`;
@@ -3644,6 +3909,7 @@ var init_core = __esm({
3644
3909
  init_interfaces();
3645
3910
  init_types_legacy();
3646
3911
  init_user();
3912
+ init_strategyState();
3647
3913
  init_Loggable();
3648
3914
  init_util();
3649
3915
  init_navigators();
@@ -3832,7 +4098,9 @@ import moment5 from "moment";
3832
4098
  function accomodateGuest() {
3833
4099
  logger.log("[funnel] accomodateGuest() called");
3834
4100
  if (typeof localStorage === "undefined") {
3835
- logger.log("[funnel] localStorage not available (Node.js environment), returning default guest");
4101
+ logger.log(
4102
+ "[funnel] localStorage not available (Node.js environment), returning default guest"
4103
+ );
3836
4104
  return {
3837
4105
  username: GuestUsername + "nodejs-test",
3838
4106
  firstVisit: true
@@ -4810,6 +5078,55 @@ Currently logged-in as ${this._username}.`
4810
5078
  async updateUserElo(courseId, elo) {
4811
5079
  return updateUserElo(this._username, courseId, elo);
4812
5080
  }
5081
+ async getStrategyState(courseId, strategyKey) {
5082
+ const docId = buildStrategyStateId(courseId, strategyKey);
5083
+ try {
5084
+ const doc = await this.localDB.get(docId);
5085
+ return doc.data;
5086
+ } catch (e) {
5087
+ const err = e;
5088
+ if (err.status === 404) {
5089
+ return null;
5090
+ }
5091
+ throw e;
5092
+ }
5093
+ }
5094
+ async putStrategyState(courseId, strategyKey, data) {
5095
+ const docId = buildStrategyStateId(courseId, strategyKey);
5096
+ let existingRev;
5097
+ try {
5098
+ const existing = await this.localDB.get(docId);
5099
+ existingRev = existing._rev;
5100
+ } catch (e) {
5101
+ const err = e;
5102
+ if (err.status !== 404) {
5103
+ throw e;
5104
+ }
5105
+ }
5106
+ const doc = {
5107
+ _id: docId,
5108
+ _rev: existingRev,
5109
+ docType: "STRATEGY_STATE" /* STRATEGY_STATE */,
5110
+ courseId,
5111
+ strategyKey,
5112
+ data,
5113
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
5114
+ };
5115
+ await this.localDB.put(doc);
5116
+ }
5117
+ async deleteStrategyState(courseId, strategyKey) {
5118
+ const docId = buildStrategyStateId(courseId, strategyKey);
5119
+ try {
5120
+ const doc = await this.localDB.get(docId);
5121
+ await this.localDB.remove(doc);
5122
+ } catch (e) {
5123
+ const err = e;
5124
+ if (err.status === 404) {
5125
+ return;
5126
+ }
5127
+ throw e;
5128
+ }
5129
+ }
4813
5130
  };
4814
5131
  userCoursesDoc = "CourseRegistrations";
4815
5132
  userClassroomsDoc = "ClassroomRegistrations";
@@ -4910,8 +5227,7 @@ var init_adminDB2 = __esm({
4910
5227
  }
4911
5228
  }
4912
5229
  }
4913
- const dbs = await Promise.all(promisedCRDbs);
4914
- return dbs.map((db) => {
5230
+ return promisedCRDbs.map((db) => {
4915
5231
  return {
4916
5232
  ...db.getConfig(),
4917
5233
  _id: db._id