@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
@@ -122,6 +122,7 @@ var init_types_legacy = __esm({
122
122
  DocType2["SCHEDULED_CARD"] = "SCHEDULED_CARD";
123
123
  DocType2["TAG"] = "TAG";
124
124
  DocType2["NAVIGATION_STRATEGY"] = "NAVIGATION_STRATEGY";
125
+ DocType2["STRATEGY_STATE"] = "STRATEGY_STATE";
125
126
  return DocType2;
126
127
  })(DocType || {});
127
128
  DocTypePrefixes = {
@@ -135,7 +136,8 @@ var init_types_legacy = __esm({
135
136
  ["QUESTION" /* QUESTIONTYPE */]: "QUESTION",
136
137
  ["VIEW" /* VIEW */]: "VIEW",
137
138
  ["PEDAGOGY" /* PEDAGOGY */]: "PEDAGOGY",
138
- ["NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */]: "NAVIGATION_STRATEGY"
139
+ ["NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */]: "NAVIGATION_STRATEGY",
140
+ ["STRATEGY_STATE" /* STRATEGY_STATE */]: "STRATEGY_STATE"
139
141
  };
140
142
  }
141
143
  });
@@ -898,6 +900,41 @@ var Pipeline_exports = {};
898
900
  __export(Pipeline_exports, {
899
901
  Pipeline: () => Pipeline
900
902
  });
903
+ function logPipelineConfig(generator, filters) {
904
+ const filterList = filters.length > 0 ? "\n - " + filters.map((f) => f.name).join("\n - ") : " none";
905
+ logger.info(
906
+ `[Pipeline] Configuration:
907
+ Generator: ${generator.name}
908
+ Filters:${filterList}`
909
+ );
910
+ }
911
+ function logTagHydration(cards, tagsByCard) {
912
+ const totalTags = Array.from(tagsByCard.values()).reduce((sum, tags) => sum + tags.length, 0);
913
+ const cardsWithTags = Array.from(tagsByCard.values()).filter((tags) => tags.length > 0).length;
914
+ logger.debug(
915
+ `[Pipeline] Tag hydration: ${cards.length} cards, ${cardsWithTags} have tags (${totalTags} total tags) - single batch query`
916
+ );
917
+ }
918
+ function logExecutionSummary(generatorName, generatedCount, filterCount, finalCount, topScores) {
919
+ const scoreDisplay = topScores.length > 0 ? topScores.map((s) => s.toFixed(2)).join(", ") : "none";
920
+ logger.info(
921
+ `[Pipeline] Execution: ${generatorName} produced ${generatedCount} \u2192 ${filterCount} filters \u2192 ${finalCount} results (top scores: ${scoreDisplay})`
922
+ );
923
+ }
924
+ function logCardProvenance(cards, maxCards = 3) {
925
+ const cardsToLog = cards.slice(0, maxCards);
926
+ logger.debug(`[Pipeline] Provenance for top ${cardsToLog.length} cards:`);
927
+ for (const card of cardsToLog) {
928
+ logger.debug(`[Pipeline] ${card.cardId} (final score: ${card.score.toFixed(3)}):`);
929
+ for (const entry of card.provenance) {
930
+ const scoreChange = entry.score.toFixed(3);
931
+ const action = entry.action.padEnd(9);
932
+ logger.debug(
933
+ `[Pipeline] ${action} ${scoreChange} - ${entry.strategyName}: ${entry.reason}`
934
+ );
935
+ }
936
+ }
937
+ }
901
938
  var import_common5, Pipeline;
902
939
  var init_Pipeline = __esm({
903
940
  "src/core/navigators/Pipeline.ts"() {
@@ -922,19 +959,18 @@ var init_Pipeline = __esm({
922
959
  this.filters = filters;
923
960
  this.user = user;
924
961
  this.course = course;
925
- logger.debug(
926
- `[Pipeline] Created with generator '${generator.name}' and ${filters.length} filters: ${filters.map((f) => f.name).join(", ")}`
927
- );
962
+ logPipelineConfig(generator, filters);
928
963
  }
929
964
  /**
930
965
  * Get weighted cards by running generator and applying filters.
931
966
  *
932
967
  * 1. Build shared context (user ELO, etc.)
933
968
  * 2. Get candidates from generator (passing context)
934
- * 3. Apply each filter sequentially
935
- * 4. Remove zero-score cards
936
- * 5. Sort by score descending
937
- * 6. Return top N
969
+ * 3. Batch hydrate tags for all candidates
970
+ * 4. Apply each filter sequentially
971
+ * 5. Remove zero-score cards
972
+ * 6. Sort by score descending
973
+ * 7. Return top N
938
974
  *
939
975
  * @param limit - Maximum number of cards to return
940
976
  * @returns Cards sorted by score descending
@@ -947,7 +983,9 @@ var init_Pipeline = __esm({
947
983
  `[Pipeline] Fetching ${fetchLimit} candidates from generator '${this.generator.name}'`
948
984
  );
949
985
  let cards = await this.generator.getWeightedCards(fetchLimit, context);
950
- logger.debug(`[Pipeline] Generator returned ${cards.length} candidates`);
986
+ const generatedCount = cards.length;
987
+ logger.debug(`[Pipeline] Generator returned ${generatedCount} candidates`);
988
+ cards = await this.hydrateTags(cards);
951
989
  for (const filter of this.filters) {
952
990
  const beforeCount = cards.length;
953
991
  cards = await filter.transform(cards, context);
@@ -956,11 +994,33 @@ var init_Pipeline = __esm({
956
994
  cards = cards.filter((c) => c.score > 0);
957
995
  cards.sort((a, b) => b.score - a.score);
958
996
  const result = cards.slice(0, limit);
959
- logger.debug(
960
- `[Pipeline] Returning ${result.length} cards (top scores: ${result.slice(0, 3).map((c) => c.score.toFixed(2)).join(", ")}...)`
961
- );
997
+ const topScores = result.slice(0, 3).map((c) => c.score);
998
+ logExecutionSummary(this.generator.name, generatedCount, this.filters.length, result.length, topScores);
999
+ logCardProvenance(result, 3);
962
1000
  return result;
963
1001
  }
1002
+ /**
1003
+ * Batch hydrate tags for all cards.
1004
+ *
1005
+ * Fetches tags for all cards in a single database query and attaches them
1006
+ * to the WeightedCard objects. Filters can then use card.tags instead of
1007
+ * making individual getAppliedTags() calls.
1008
+ *
1009
+ * @param cards - Cards to hydrate
1010
+ * @returns Cards with tags populated
1011
+ */
1012
+ async hydrateTags(cards) {
1013
+ if (cards.length === 0) {
1014
+ return cards;
1015
+ }
1016
+ const cardIds = cards.map((c) => c.cardId);
1017
+ const tagsByCard = await this.course.getAppliedTagsBatch(cardIds);
1018
+ logTagHydration(cards, tagsByCard);
1019
+ return cards.map((card) => ({
1020
+ ...card,
1021
+ tags: tagsByCard.get(card.cardId) ?? []
1022
+ }));
1023
+ }
964
1024
  /**
965
1025
  * Build shared context for generator and filters.
966
1026
  *
@@ -1320,15 +1380,144 @@ var init_eloDistance = __esm({
1320
1380
  }
1321
1381
  });
1322
1382
 
1383
+ // src/core/navigators/filters/userTagPreference.ts
1384
+ var userTagPreference_exports = {};
1385
+ __export(userTagPreference_exports, {
1386
+ default: () => UserTagPreferenceFilter
1387
+ });
1388
+ var UserTagPreferenceFilter;
1389
+ var init_userTagPreference = __esm({
1390
+ "src/core/navigators/filters/userTagPreference.ts"() {
1391
+ "use strict";
1392
+ init_navigators();
1393
+ UserTagPreferenceFilter = class extends ContentNavigator {
1394
+ _strategyData;
1395
+ /** Human-readable name for CardFilter interface */
1396
+ name;
1397
+ constructor(user, course, strategyData) {
1398
+ super(user, course, strategyData);
1399
+ this._strategyData = strategyData;
1400
+ this.name = strategyData.name || "User Tag Preferences";
1401
+ }
1402
+ /**
1403
+ * Compute multiplier for a card based on its tags and user preferences.
1404
+ * Returns the maximum multiplier among all matching tags, or 1.0 if no matches.
1405
+ */
1406
+ computeMultiplier(cardTags, boostMap) {
1407
+ const multipliers = cardTags.map((tag) => boostMap[tag]).filter((val) => val !== void 0);
1408
+ if (multipliers.length === 0) {
1409
+ return 1;
1410
+ }
1411
+ return Math.max(...multipliers);
1412
+ }
1413
+ /**
1414
+ * Build human-readable reason for the filter's decision.
1415
+ */
1416
+ buildReason(cardTags, boostMap, multiplier) {
1417
+ const matchingTags = cardTags.filter((tag) => boostMap[tag] === multiplier);
1418
+ if (multiplier === 0) {
1419
+ return `Excluded by user preference: ${matchingTags.join(", ")} (${multiplier}x)`;
1420
+ }
1421
+ if (multiplier < 1) {
1422
+ return `Penalized by user preference: ${matchingTags.join(", ")} (${multiplier.toFixed(2)}x)`;
1423
+ }
1424
+ if (multiplier > 1) {
1425
+ return `Boosted by user preference: ${matchingTags.join(", ")} (${multiplier.toFixed(2)}x)`;
1426
+ }
1427
+ return "No matching user preferences";
1428
+ }
1429
+ /**
1430
+ * CardFilter.transform implementation.
1431
+ *
1432
+ * Apply user tag preferences:
1433
+ * 1. Read preferences from strategy state
1434
+ * 2. If no preferences, pass through unchanged
1435
+ * 3. For each card:
1436
+ * - Look up tag in boost record
1437
+ * - If tag found: apply multiplier (0 = exclude, 1 = neutral, >1 = boost)
1438
+ * - If multiple tags match: use max multiplier
1439
+ * - Append provenance with clear reason
1440
+ */
1441
+ async transform(cards, _context) {
1442
+ const prefs = await this.getStrategyState();
1443
+ if (!prefs || Object.keys(prefs.boost).length === 0) {
1444
+ return cards.map((card) => ({
1445
+ ...card,
1446
+ provenance: [
1447
+ ...card.provenance,
1448
+ {
1449
+ strategy: "userTagPreference",
1450
+ strategyName: this.strategyName || this.name,
1451
+ strategyId: this.strategyId || this._strategyData._id,
1452
+ action: "passed",
1453
+ score: card.score,
1454
+ reason: "No user tag preferences configured"
1455
+ }
1456
+ ]
1457
+ }));
1458
+ }
1459
+ const adjusted = await Promise.all(
1460
+ cards.map(async (card) => {
1461
+ const cardTags = card.tags ?? [];
1462
+ const multiplier = this.computeMultiplier(cardTags, prefs.boost);
1463
+ const finalScore = Math.min(1, card.score * multiplier);
1464
+ let action;
1465
+ if (multiplier === 0 || multiplier < 1) {
1466
+ action = "penalized";
1467
+ } else if (multiplier > 1) {
1468
+ action = "boosted";
1469
+ } else {
1470
+ action = "passed";
1471
+ }
1472
+ return {
1473
+ ...card,
1474
+ score: finalScore,
1475
+ provenance: [
1476
+ ...card.provenance,
1477
+ {
1478
+ strategy: "userTagPreference",
1479
+ strategyName: this.strategyName || this.name,
1480
+ strategyId: this.strategyId || this._strategyData._id,
1481
+ action,
1482
+ score: finalScore,
1483
+ reason: this.buildReason(cardTags, prefs.boost, multiplier)
1484
+ }
1485
+ ]
1486
+ };
1487
+ })
1488
+ );
1489
+ return adjusted;
1490
+ }
1491
+ /**
1492
+ * Legacy getWeightedCards - throws as filters should not be used as generators.
1493
+ */
1494
+ async getWeightedCards(_limit) {
1495
+ throw new Error(
1496
+ "UserTagPreferenceFilter is a filter and should not be used as a generator. Use Pipeline with a generator and this filter via transform()."
1497
+ );
1498
+ }
1499
+ // Legacy methods - stub implementations since filters don't generate cards
1500
+ async getNewCards(_n) {
1501
+ return [];
1502
+ }
1503
+ async getPendingReviews() {
1504
+ return [];
1505
+ }
1506
+ };
1507
+ }
1508
+ });
1509
+
1323
1510
  // src/core/navigators/filters/index.ts
1324
1511
  var filters_exports = {};
1325
1512
  __export(filters_exports, {
1513
+ UserTagPreferenceFilter: () => UserTagPreferenceFilter,
1326
1514
  createEloDistanceFilter: () => createEloDistanceFilter
1327
1515
  });
1328
1516
  var init_filters = __esm({
1329
1517
  "src/core/navigators/filters/index.ts"() {
1330
1518
  "use strict";
1331
1519
  init_eloDistance();
1520
+ init_userTagPreference();
1332
1521
  }
1333
1522
  });
1334
1523
 
@@ -1561,10 +1750,9 @@ var init_hierarchyDefinition = __esm({
1561
1750
  /**
1562
1751
  * Check if a card is unlocked and generate reason.
1563
1752
  */
1564
- async checkCardUnlock(cardId, course, unlockedTags, masteredTags) {
1753
+ async checkCardUnlock(card, course, unlockedTags, masteredTags) {
1565
1754
  try {
1566
- const tagResponse = await course.getAppliedTags(cardId);
1567
- const cardTags = tagResponse.rows.map((row) => row.value?.name || row.key);
1755
+ const cardTags = card.tags ?? [];
1568
1756
  const lockedTags = cardTags.filter(
1569
1757
  (tag) => this.hasPrerequisites(tag) && !unlockedTags.has(tag)
1570
1758
  );
@@ -1601,7 +1789,7 @@ var init_hierarchyDefinition = __esm({
1601
1789
  const gated = [];
1602
1790
  for (const card of cards) {
1603
1791
  const { isUnlocked, reason } = await this.checkCardUnlock(
1604
- card.cardId,
1792
+ card,
1605
1793
  context.course,
1606
1794
  unlockedTags,
1607
1795
  masteredTags
@@ -1647,6 +1835,19 @@ var init_hierarchyDefinition = __esm({
1647
1835
  }
1648
1836
  });
1649
1837
 
1838
+ // src/core/navigators/inferredPreference.ts
1839
+ var inferredPreference_exports = {};
1840
+ __export(inferredPreference_exports, {
1841
+ INFERRED_PREFERENCE_NAVIGATOR_STUB: () => INFERRED_PREFERENCE_NAVIGATOR_STUB
1842
+ });
1843
+ var INFERRED_PREFERENCE_NAVIGATOR_STUB;
1844
+ var init_inferredPreference = __esm({
1845
+ "src/core/navigators/inferredPreference.ts"() {
1846
+ "use strict";
1847
+ INFERRED_PREFERENCE_NAVIGATOR_STUB = true;
1848
+ }
1849
+ });
1850
+
1650
1851
  // src/core/navigators/interferenceMitigator.ts
1651
1852
  var interferenceMitigator_exports = {};
1652
1853
  __export(interferenceMitigator_exports, {
@@ -1778,17 +1979,6 @@ var init_interferenceMitigator = __esm({
1778
1979
  }
1779
1980
  return avoid;
1780
1981
  }
1781
- /**
1782
- * Get tags for a single card
1783
- */
1784
- async getCardTags(cardId, course) {
1785
- try {
1786
- const tagResponse = await course.getAppliedTags(cardId);
1787
- return tagResponse.rows.map((row) => row.value?.name || row.key).filter(Boolean);
1788
- } catch {
1789
- return [];
1790
- }
1791
- }
1792
1982
  /**
1793
1983
  * Compute interference score reduction for a card.
1794
1984
  * Returns: { multiplier, interfering tags, reason }
@@ -1840,7 +2030,7 @@ var init_interferenceMitigator = __esm({
1840
2030
  const tagsToAvoid = this.getTagsToAvoid(immatureTags);
1841
2031
  const adjusted = [];
1842
2032
  for (const card of cards) {
1843
- const cardTags = await this.getCardTags(card.cardId, context.course);
2033
+ const cardTags = card.tags ?? [];
1844
2034
  const { multiplier, reason } = this.computeInterferenceEffect(
1845
2035
  cardTags,
1846
2036
  tagsToAvoid,
@@ -1985,27 +2175,16 @@ var init_relativePriority = __esm({
1985
2175
  return `Low-priority tags: ${tagList}${more} (priority ${priority.toFixed(2)} \u2192 reduce ${boostFactor.toFixed(2)}x \u2192 ${finalScore.toFixed(2)})`;
1986
2176
  }
1987
2177
  }
1988
- /**
1989
- * Get tags for a single card.
1990
- */
1991
- async getCardTags(cardId, course) {
1992
- try {
1993
- const tagResponse = await course.getAppliedTags(cardId);
1994
- return tagResponse.rows.map((r) => r.doc?.name).filter((x) => !!x);
1995
- } catch {
1996
- return [];
1997
- }
1998
- }
1999
2178
  /**
2000
2179
  * CardFilter.transform implementation.
2001
2180
  *
2002
2181
  * Apply priority-adjusted scoring. Cards with high-priority tags get boosted,
2003
2182
  * cards with low-priority tags get reduced scores.
2004
2183
  */
2005
- async transform(cards, context) {
2184
+ async transform(cards, _context) {
2006
2185
  const adjusted = await Promise.all(
2007
2186
  cards.map(async (card) => {
2008
- const cardTags = await this.getCardTags(card.cardId, context.course);
2187
+ const cardTags = card.tags ?? [];
2009
2188
  const priority = this.computeCardPriority(cardTags);
2010
2189
  const boostFactor = this.computeBoostFactor(priority);
2011
2190
  const finalScore = Math.max(0, Math.min(1, card.score * boostFactor));
@@ -2172,6 +2351,19 @@ var init_srs = __esm({
2172
2351
  }
2173
2352
  });
2174
2353
 
2354
+ // src/core/navigators/userGoal.ts
2355
+ var userGoal_exports = {};
2356
+ __export(userGoal_exports, {
2357
+ USER_GOAL_NAVIGATOR_STUB: () => USER_GOAL_NAVIGATOR_STUB
2358
+ });
2359
+ var USER_GOAL_NAVIGATOR_STUB;
2360
+ var init_userGoal = __esm({
2361
+ "src/core/navigators/userGoal.ts"() {
2362
+ "use strict";
2363
+ USER_GOAL_NAVIGATOR_STUB = true;
2364
+ }
2365
+ });
2366
+
2175
2367
  // import("./**/*") in src/core/navigators/index.ts
2176
2368
  var globImport;
2177
2369
  var init_ = __esm({
@@ -2184,14 +2376,17 @@ var init_ = __esm({
2184
2376
  "./filters/eloDistance.ts": () => Promise.resolve().then(() => (init_eloDistance(), eloDistance_exports)),
2185
2377
  "./filters/index.ts": () => Promise.resolve().then(() => (init_filters(), filters_exports)),
2186
2378
  "./filters/types.ts": () => Promise.resolve().then(() => (init_types(), types_exports)),
2379
+ "./filters/userTagPreference.ts": () => Promise.resolve().then(() => (init_userTagPreference(), userTagPreference_exports)),
2187
2380
  "./generators/index.ts": () => Promise.resolve().then(() => (init_generators(), generators_exports)),
2188
2381
  "./generators/types.ts": () => Promise.resolve().then(() => (init_types2(), types_exports2)),
2189
2382
  "./hardcodedOrder.ts": () => Promise.resolve().then(() => (init_hardcodedOrder(), hardcodedOrder_exports)),
2190
2383
  "./hierarchyDefinition.ts": () => Promise.resolve().then(() => (init_hierarchyDefinition(), hierarchyDefinition_exports)),
2191
2384
  "./index.ts": () => Promise.resolve().then(() => (init_navigators(), navigators_exports)),
2385
+ "./inferredPreference.ts": () => Promise.resolve().then(() => (init_inferredPreference(), inferredPreference_exports)),
2192
2386
  "./interferenceMitigator.ts": () => Promise.resolve().then(() => (init_interferenceMitigator(), interferenceMitigator_exports)),
2193
2387
  "./relativePriority.ts": () => Promise.resolve().then(() => (init_relativePriority(), relativePriority_exports)),
2194
- "./srs.ts": () => Promise.resolve().then(() => (init_srs(), srs_exports))
2388
+ "./srs.ts": () => Promise.resolve().then(() => (init_srs(), srs_exports)),
2389
+ "./userGoal.ts": () => Promise.resolve().then(() => (init_userGoal(), userGoal_exports))
2195
2390
  });
2196
2391
  }
2197
2392
  });
@@ -2240,6 +2435,7 @@ var init_navigators = __esm({
2240
2435
  Navigators2["HIERARCHY"] = "hierarchyDefinition";
2241
2436
  Navigators2["INTERFERENCE"] = "interferenceMitigator";
2242
2437
  Navigators2["RELATIVE_PRIORITY"] = "relativePriority";
2438
+ Navigators2["USER_TAG_PREFERENCE"] = "userTagPreference";
2243
2439
  return Navigators2;
2244
2440
  })(Navigators || {});
2245
2441
  NavigatorRole = /* @__PURE__ */ ((NavigatorRole2) => {
@@ -2253,7 +2449,8 @@ var init_navigators = __esm({
2253
2449
  ["hardcodedOrder" /* HARDCODED */]: "generator" /* GENERATOR */,
2254
2450
  ["hierarchyDefinition" /* HIERARCHY */]: "filter" /* FILTER */,
2255
2451
  ["interferenceMitigator" /* INTERFERENCE */]: "filter" /* FILTER */,
2256
- ["relativePriority" /* RELATIVE_PRIORITY */]: "filter" /* FILTER */
2452
+ ["relativePriority" /* RELATIVE_PRIORITY */]: "filter" /* FILTER */,
2453
+ ["userTagPreference" /* USER_TAG_PREFERENCE */]: "filter" /* FILTER */
2257
2454
  };
2258
2455
  ContentNavigator = class {
2259
2456
  /** User interface for this navigation session */
@@ -2278,6 +2475,52 @@ var init_navigators = __esm({
2278
2475
  this.strategyId = strategyData._id;
2279
2476
  }
2280
2477
  }
2478
+ // ============================================================================
2479
+ // STRATEGY STATE HELPERS
2480
+ // ============================================================================
2481
+ //
2482
+ // These methods allow strategies to persist their own state (user preferences,
2483
+ // learned patterns, temporal tracking) in the user database.
2484
+ //
2485
+ // ============================================================================
2486
+ /**
2487
+ * Unique key identifying this strategy for state storage.
2488
+ *
2489
+ * Defaults to the constructor name (e.g., "UserTagPreferenceFilter").
2490
+ * Override in subclasses if multiple instances of the same strategy type
2491
+ * need separate state storage.
2492
+ */
2493
+ get strategyKey() {
2494
+ return this.constructor.name;
2495
+ }
2496
+ /**
2497
+ * Get this strategy's persisted state for the current course.
2498
+ *
2499
+ * @returns The strategy's data payload, or null if no state exists
2500
+ * @throws Error if user or course is not initialized
2501
+ */
2502
+ async getStrategyState() {
2503
+ if (!this.user || !this.course) {
2504
+ throw new Error(
2505
+ `Cannot get strategy state: navigator not properly initialized. Ensure user and course are provided to constructor.`
2506
+ );
2507
+ }
2508
+ return this.user.getStrategyState(this.course.getCourseID(), this.strategyKey);
2509
+ }
2510
+ /**
2511
+ * Persist this strategy's state for the current course.
2512
+ *
2513
+ * @param data - The strategy's data payload to store
2514
+ * @throws Error if user or course is not initialized
2515
+ */
2516
+ async putStrategyState(data) {
2517
+ if (!this.user || !this.course) {
2518
+ throw new Error(
2519
+ `Cannot put strategy state: navigator not properly initialized. Ensure user and course are provided to constructor.`
2520
+ );
2521
+ }
2522
+ return this.user.putStrategyState(this.course.getCourseID(), this.strategyKey, data);
2523
+ }
2281
2524
  /**
2282
2525
  * Factory method to create navigator instances dynamically.
2283
2526
  *
@@ -2602,15 +2845,6 @@ var init_courseDB = __esm({
2602
2845
  ret[r.id] = r.doc.id_displayable_data;
2603
2846
  }
2604
2847
  });
2605
- await Promise.all(
2606
- cards.rows.map((r) => {
2607
- return async () => {
2608
- if (isSuccessRow(r)) {
2609
- ret[r.id] = r.doc.id_displayable_data;
2610
- }
2611
- };
2612
- })
2613
- );
2614
2848
  return ret;
2615
2849
  }
2616
2850
  async getCardsByELO(elo, cardLimit) {
@@ -2695,6 +2929,28 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
2695
2929
  throw new Error(`Failed to find tags for card ${this.id}-${cardId}`);
2696
2930
  }
2697
2931
  }
2932
+ async getAppliedTagsBatch(cardIds) {
2933
+ if (cardIds.length === 0) {
2934
+ return /* @__PURE__ */ new Map();
2935
+ }
2936
+ const db = getCourseDB2(this.id);
2937
+ const result = await db.query("getTags", {
2938
+ keys: cardIds,
2939
+ include_docs: false
2940
+ });
2941
+ const tagsByCard = /* @__PURE__ */ new Map();
2942
+ for (const cardId of cardIds) {
2943
+ tagsByCard.set(cardId, []);
2944
+ }
2945
+ for (const row of result.rows) {
2946
+ const cardId = row.key;
2947
+ const tagName = row.value?.name;
2948
+ if (tagName && tagsByCard.has(cardId)) {
2949
+ tagsByCard.get(cardId).push(tagName);
2950
+ }
2951
+ }
2952
+ return tagsByCard;
2953
+ }
2698
2954
  async addTagToCard(cardId, tagId, updateELO) {
2699
2955
  return await addTagToCard(
2700
2956
  this.id,
@@ -4283,6 +4539,55 @@ Currently logged-in as ${this._username}.`
4283
4539
  async updateUserElo(courseId, elo) {
4284
4540
  return updateUserElo(this._username, courseId, elo);
4285
4541
  }
4542
+ async getStrategyState(courseId, strategyKey) {
4543
+ const docId = buildStrategyStateId(courseId, strategyKey);
4544
+ try {
4545
+ const doc = await this.localDB.get(docId);
4546
+ return doc.data;
4547
+ } catch (e) {
4548
+ const err = e;
4549
+ if (err.status === 404) {
4550
+ return null;
4551
+ }
4552
+ throw e;
4553
+ }
4554
+ }
4555
+ async putStrategyState(courseId, strategyKey, data) {
4556
+ const docId = buildStrategyStateId(courseId, strategyKey);
4557
+ let existingRev;
4558
+ try {
4559
+ const existing = await this.localDB.get(docId);
4560
+ existingRev = existing._rev;
4561
+ } catch (e) {
4562
+ const err = e;
4563
+ if (err.status !== 404) {
4564
+ throw e;
4565
+ }
4566
+ }
4567
+ const doc = {
4568
+ _id: docId,
4569
+ _rev: existingRev,
4570
+ docType: "STRATEGY_STATE" /* STRATEGY_STATE */,
4571
+ courseId,
4572
+ strategyKey,
4573
+ data,
4574
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
4575
+ };
4576
+ await this.localDB.put(doc);
4577
+ }
4578
+ async deleteStrategyState(courseId, strategyKey) {
4579
+ const docId = buildStrategyStateId(courseId, strategyKey);
4580
+ try {
4581
+ const doc = await this.localDB.get(docId);
4582
+ await this.localDB.remove(doc);
4583
+ } catch (e) {
4584
+ const err = e;
4585
+ if (err.status === 404) {
4586
+ return;
4587
+ }
4588
+ throw e;
4589
+ }
4590
+ }
4286
4591
  };
4287
4592
  userCoursesDoc = "CourseRegistrations";
4288
4593
  userClassroomsDoc = "ClassroomRegistrations";
@@ -4596,6 +4901,16 @@ var init_user = __esm({
4596
4901
  }
4597
4902
  });
4598
4903
 
4904
+ // src/core/types/strategyState.ts
4905
+ function buildStrategyStateId(courseId, strategyKey) {
4906
+ return `STRATEGY_STATE::${courseId}::${strategyKey}`;
4907
+ }
4908
+ var init_strategyState = __esm({
4909
+ "src/core/types/strategyState.ts"() {
4910
+ "use strict";
4911
+ }
4912
+ });
4913
+
4599
4914
  // src/core/bulkImport/cardProcessor.ts
4600
4915
  async function importParsedCards(parsedCards, courseDB, config) {
4601
4916
  const results = [];
@@ -4745,6 +5060,7 @@ __export(core_exports, {
4745
5060
  NavigatorRoles: () => NavigatorRoles,
4746
5061
  Navigators: () => Navigators,
4747
5062
  areQuestionRecords: () => areQuestionRecords,
5063
+ buildStrategyStateId: () => buildStrategyStateId,
4748
5064
  docIsDeleted: () => docIsDeleted,
4749
5065
  getCardHistoryID: () => getCardHistoryID,
4750
5066
  getCardOrigin: () => getCardOrigin,
@@ -4764,6 +5080,7 @@ var init_core = __esm({
4764
5080
  init_interfaces();
4765
5081
  init_types_legacy();
4766
5082
  init_user();
5083
+ init_strategyState();
4767
5084
  init_Loggable();
4768
5085
  init_util();
4769
5086
  init_navigators();
@@ -4782,6 +5099,7 @@ init_core();
4782
5099
  NavigatorRoles,
4783
5100
  Navigators,
4784
5101
  areQuestionRecords,
5102
+ buildStrategyStateId,
4785
5103
  docIsDeleted,
4786
5104
  getCardHistoryID,
4787
5105
  getCardOrigin,