@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
package/dist/index.mjs CHANGED
@@ -100,6 +100,7 @@ var init_types_legacy = __esm({
100
100
  DocType3["SCHEDULED_CARD"] = "SCHEDULED_CARD";
101
101
  DocType3["TAG"] = "TAG";
102
102
  DocType3["NAVIGATION_STRATEGY"] = "NAVIGATION_STRATEGY";
103
+ DocType3["STRATEGY_STATE"] = "STRATEGY_STATE";
103
104
  return DocType3;
104
105
  })(DocType || {});
105
106
  DocTypePrefixes = {
@@ -113,7 +114,8 @@ var init_types_legacy = __esm({
113
114
  ["QUESTION" /* QUESTIONTYPE */]: "QUESTION",
114
115
  ["VIEW" /* VIEW */]: "VIEW",
115
116
  ["PEDAGOGY" /* PEDAGOGY */]: "PEDAGOGY",
116
- ["NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */]: "NAVIGATION_STRATEGY"
117
+ ["NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */]: "NAVIGATION_STRATEGY",
118
+ ["STRATEGY_STATE" /* STRATEGY_STATE */]: "STRATEGY_STATE"
117
119
  };
118
120
  }
119
121
  });
@@ -1112,6 +1114,41 @@ __export(Pipeline_exports, {
1112
1114
  Pipeline: () => Pipeline
1113
1115
  });
1114
1116
  import { toCourseElo as toCourseElo2 } from "@vue-skuilder/common";
1117
+ function logPipelineConfig(generator, filters) {
1118
+ const filterList = filters.length > 0 ? "\n - " + filters.map((f) => f.name).join("\n - ") : " none";
1119
+ logger.info(
1120
+ `[Pipeline] Configuration:
1121
+ Generator: ${generator.name}
1122
+ Filters:${filterList}`
1123
+ );
1124
+ }
1125
+ function logTagHydration(cards, tagsByCard) {
1126
+ const totalTags = Array.from(tagsByCard.values()).reduce((sum, tags) => sum + tags.length, 0);
1127
+ const cardsWithTags = Array.from(tagsByCard.values()).filter((tags) => tags.length > 0).length;
1128
+ logger.debug(
1129
+ `[Pipeline] Tag hydration: ${cards.length} cards, ${cardsWithTags} have tags (${totalTags} total tags) - single batch query`
1130
+ );
1131
+ }
1132
+ function logExecutionSummary(generatorName, generatedCount, filterCount, finalCount, topScores) {
1133
+ const scoreDisplay = topScores.length > 0 ? topScores.map((s) => s.toFixed(2)).join(", ") : "none";
1134
+ logger.info(
1135
+ `[Pipeline] Execution: ${generatorName} produced ${generatedCount} \u2192 ${filterCount} filters \u2192 ${finalCount} results (top scores: ${scoreDisplay})`
1136
+ );
1137
+ }
1138
+ function logCardProvenance(cards, maxCards = 3) {
1139
+ const cardsToLog = cards.slice(0, maxCards);
1140
+ logger.debug(`[Pipeline] Provenance for top ${cardsToLog.length} cards:`);
1141
+ for (const card of cardsToLog) {
1142
+ logger.debug(`[Pipeline] ${card.cardId} (final score: ${card.score.toFixed(3)}):`);
1143
+ for (const entry of card.provenance) {
1144
+ const scoreChange = entry.score.toFixed(3);
1145
+ const action = entry.action.padEnd(9);
1146
+ logger.debug(
1147
+ `[Pipeline] ${action} ${scoreChange} - ${entry.strategyName}: ${entry.reason}`
1148
+ );
1149
+ }
1150
+ }
1151
+ }
1115
1152
  var Pipeline;
1116
1153
  var init_Pipeline = __esm({
1117
1154
  "src/core/navigators/Pipeline.ts"() {
@@ -1135,19 +1172,18 @@ var init_Pipeline = __esm({
1135
1172
  this.filters = filters;
1136
1173
  this.user = user;
1137
1174
  this.course = course;
1138
- logger.debug(
1139
- `[Pipeline] Created with generator '${generator.name}' and ${filters.length} filters: ${filters.map((f) => f.name).join(", ")}`
1140
- );
1175
+ logPipelineConfig(generator, filters);
1141
1176
  }
1142
1177
  /**
1143
1178
  * Get weighted cards by running generator and applying filters.
1144
1179
  *
1145
1180
  * 1. Build shared context (user ELO, etc.)
1146
1181
  * 2. Get candidates from generator (passing context)
1147
- * 3. Apply each filter sequentially
1148
- * 4. Remove zero-score cards
1149
- * 5. Sort by score descending
1150
- * 6. Return top N
1182
+ * 3. Batch hydrate tags for all candidates
1183
+ * 4. Apply each filter sequentially
1184
+ * 5. Remove zero-score cards
1185
+ * 6. Sort by score descending
1186
+ * 7. Return top N
1151
1187
  *
1152
1188
  * @param limit - Maximum number of cards to return
1153
1189
  * @returns Cards sorted by score descending
@@ -1160,7 +1196,9 @@ var init_Pipeline = __esm({
1160
1196
  `[Pipeline] Fetching ${fetchLimit} candidates from generator '${this.generator.name}'`
1161
1197
  );
1162
1198
  let cards = await this.generator.getWeightedCards(fetchLimit, context);
1163
- logger.debug(`[Pipeline] Generator returned ${cards.length} candidates`);
1199
+ const generatedCount = cards.length;
1200
+ logger.debug(`[Pipeline] Generator returned ${generatedCount} candidates`);
1201
+ cards = await this.hydrateTags(cards);
1164
1202
  for (const filter of this.filters) {
1165
1203
  const beforeCount = cards.length;
1166
1204
  cards = await filter.transform(cards, context);
@@ -1169,11 +1207,33 @@ var init_Pipeline = __esm({
1169
1207
  cards = cards.filter((c) => c.score > 0);
1170
1208
  cards.sort((a, b) => b.score - a.score);
1171
1209
  const result = cards.slice(0, limit);
1172
- logger.debug(
1173
- `[Pipeline] Returning ${result.length} cards (top scores: ${result.slice(0, 3).map((c) => c.score.toFixed(2)).join(", ")}...)`
1174
- );
1210
+ const topScores = result.slice(0, 3).map((c) => c.score);
1211
+ logExecutionSummary(this.generator.name, generatedCount, this.filters.length, result.length, topScores);
1212
+ logCardProvenance(result, 3);
1175
1213
  return result;
1176
1214
  }
1215
+ /**
1216
+ * Batch hydrate tags for all cards.
1217
+ *
1218
+ * Fetches tags for all cards in a single database query and attaches them
1219
+ * to the WeightedCard objects. Filters can then use card.tags instead of
1220
+ * making individual getAppliedTags() calls.
1221
+ *
1222
+ * @param cards - Cards to hydrate
1223
+ * @returns Cards with tags populated
1224
+ */
1225
+ async hydrateTags(cards) {
1226
+ if (cards.length === 0) {
1227
+ return cards;
1228
+ }
1229
+ const cardIds = cards.map((c) => c.cardId);
1230
+ const tagsByCard = await this.course.getAppliedTagsBatch(cardIds);
1231
+ logTagHydration(cards, tagsByCard);
1232
+ return cards.map((card) => ({
1233
+ ...card,
1234
+ tags: tagsByCard.get(card.cardId) ?? []
1235
+ }));
1236
+ }
1177
1237
  /**
1178
1238
  * Build shared context for generator and filters.
1179
1239
  *
@@ -1533,15 +1593,144 @@ var init_eloDistance = __esm({
1533
1593
  }
1534
1594
  });
1535
1595
 
1596
+ // src/core/navigators/filters/userTagPreference.ts
1597
+ var userTagPreference_exports = {};
1598
+ __export(userTagPreference_exports, {
1599
+ default: () => UserTagPreferenceFilter
1600
+ });
1601
+ var UserTagPreferenceFilter;
1602
+ var init_userTagPreference = __esm({
1603
+ "src/core/navigators/filters/userTagPreference.ts"() {
1604
+ "use strict";
1605
+ init_navigators();
1606
+ UserTagPreferenceFilter = class extends ContentNavigator {
1607
+ _strategyData;
1608
+ /** Human-readable name for CardFilter interface */
1609
+ name;
1610
+ constructor(user, course, strategyData) {
1611
+ super(user, course, strategyData);
1612
+ this._strategyData = strategyData;
1613
+ this.name = strategyData.name || "User Tag Preferences";
1614
+ }
1615
+ /**
1616
+ * Compute multiplier for a card based on its tags and user preferences.
1617
+ * Returns the maximum multiplier among all matching tags, or 1.0 if no matches.
1618
+ */
1619
+ computeMultiplier(cardTags, boostMap) {
1620
+ const multipliers = cardTags.map((tag) => boostMap[tag]).filter((val) => val !== void 0);
1621
+ if (multipliers.length === 0) {
1622
+ return 1;
1623
+ }
1624
+ return Math.max(...multipliers);
1625
+ }
1626
+ /**
1627
+ * Build human-readable reason for the filter's decision.
1628
+ */
1629
+ buildReason(cardTags, boostMap, multiplier) {
1630
+ const matchingTags = cardTags.filter((tag) => boostMap[tag] === multiplier);
1631
+ if (multiplier === 0) {
1632
+ return `Excluded by user preference: ${matchingTags.join(", ")} (${multiplier}x)`;
1633
+ }
1634
+ if (multiplier < 1) {
1635
+ return `Penalized by user preference: ${matchingTags.join(", ")} (${multiplier.toFixed(2)}x)`;
1636
+ }
1637
+ if (multiplier > 1) {
1638
+ return `Boosted by user preference: ${matchingTags.join(", ")} (${multiplier.toFixed(2)}x)`;
1639
+ }
1640
+ return "No matching user preferences";
1641
+ }
1642
+ /**
1643
+ * CardFilter.transform implementation.
1644
+ *
1645
+ * Apply user tag preferences:
1646
+ * 1. Read preferences from strategy state
1647
+ * 2. If no preferences, pass through unchanged
1648
+ * 3. For each card:
1649
+ * - Look up tag in boost record
1650
+ * - If tag found: apply multiplier (0 = exclude, 1 = neutral, >1 = boost)
1651
+ * - If multiple tags match: use max multiplier
1652
+ * - Append provenance with clear reason
1653
+ */
1654
+ async transform(cards, _context) {
1655
+ const prefs = await this.getStrategyState();
1656
+ if (!prefs || Object.keys(prefs.boost).length === 0) {
1657
+ return cards.map((card) => ({
1658
+ ...card,
1659
+ provenance: [
1660
+ ...card.provenance,
1661
+ {
1662
+ strategy: "userTagPreference",
1663
+ strategyName: this.strategyName || this.name,
1664
+ strategyId: this.strategyId || this._strategyData._id,
1665
+ action: "passed",
1666
+ score: card.score,
1667
+ reason: "No user tag preferences configured"
1668
+ }
1669
+ ]
1670
+ }));
1671
+ }
1672
+ const adjusted = await Promise.all(
1673
+ cards.map(async (card) => {
1674
+ const cardTags = card.tags ?? [];
1675
+ const multiplier = this.computeMultiplier(cardTags, prefs.boost);
1676
+ const finalScore = Math.min(1, card.score * multiplier);
1677
+ let action;
1678
+ if (multiplier === 0 || multiplier < 1) {
1679
+ action = "penalized";
1680
+ } else if (multiplier > 1) {
1681
+ action = "boosted";
1682
+ } else {
1683
+ action = "passed";
1684
+ }
1685
+ return {
1686
+ ...card,
1687
+ score: finalScore,
1688
+ provenance: [
1689
+ ...card.provenance,
1690
+ {
1691
+ strategy: "userTagPreference",
1692
+ strategyName: this.strategyName || this.name,
1693
+ strategyId: this.strategyId || this._strategyData._id,
1694
+ action,
1695
+ score: finalScore,
1696
+ reason: this.buildReason(cardTags, prefs.boost, multiplier)
1697
+ }
1698
+ ]
1699
+ };
1700
+ })
1701
+ );
1702
+ return adjusted;
1703
+ }
1704
+ /**
1705
+ * Legacy getWeightedCards - throws as filters should not be used as generators.
1706
+ */
1707
+ async getWeightedCards(_limit) {
1708
+ throw new Error(
1709
+ "UserTagPreferenceFilter is a filter and should not be used as a generator. Use Pipeline with a generator and this filter via transform()."
1710
+ );
1711
+ }
1712
+ // Legacy methods - stub implementations since filters don't generate cards
1713
+ async getNewCards(_n) {
1714
+ return [];
1715
+ }
1716
+ async getPendingReviews() {
1717
+ return [];
1718
+ }
1719
+ };
1720
+ }
1721
+ });
1722
+
1536
1723
  // src/core/navigators/filters/index.ts
1537
1724
  var filters_exports = {};
1538
1725
  __export(filters_exports, {
1726
+ UserTagPreferenceFilter: () => UserTagPreferenceFilter,
1539
1727
  createEloDistanceFilter: () => createEloDistanceFilter
1540
1728
  });
1541
1729
  var init_filters = __esm({
1542
1730
  "src/core/navigators/filters/index.ts"() {
1543
1731
  "use strict";
1544
1732
  init_eloDistance();
1733
+ init_userTagPreference();
1545
1734
  }
1546
1735
  });
1547
1736
 
@@ -1774,10 +1963,9 @@ var init_hierarchyDefinition = __esm({
1774
1963
  /**
1775
1964
  * Check if a card is unlocked and generate reason.
1776
1965
  */
1777
- async checkCardUnlock(cardId, course, unlockedTags, masteredTags) {
1966
+ async checkCardUnlock(card, course, unlockedTags, masteredTags) {
1778
1967
  try {
1779
- const tagResponse = await course.getAppliedTags(cardId);
1780
- const cardTags = tagResponse.rows.map((row) => row.value?.name || row.key);
1968
+ const cardTags = card.tags ?? [];
1781
1969
  const lockedTags = cardTags.filter(
1782
1970
  (tag) => this.hasPrerequisites(tag) && !unlockedTags.has(tag)
1783
1971
  );
@@ -1814,7 +2002,7 @@ var init_hierarchyDefinition = __esm({
1814
2002
  const gated = [];
1815
2003
  for (const card of cards) {
1816
2004
  const { isUnlocked, reason } = await this.checkCardUnlock(
1817
- card.cardId,
2005
+ card,
1818
2006
  context.course,
1819
2007
  unlockedTags,
1820
2008
  masteredTags
@@ -1860,6 +2048,19 @@ var init_hierarchyDefinition = __esm({
1860
2048
  }
1861
2049
  });
1862
2050
 
2051
+ // src/core/navigators/inferredPreference.ts
2052
+ var inferredPreference_exports = {};
2053
+ __export(inferredPreference_exports, {
2054
+ INFERRED_PREFERENCE_NAVIGATOR_STUB: () => INFERRED_PREFERENCE_NAVIGATOR_STUB
2055
+ });
2056
+ var INFERRED_PREFERENCE_NAVIGATOR_STUB;
2057
+ var init_inferredPreference = __esm({
2058
+ "src/core/navigators/inferredPreference.ts"() {
2059
+ "use strict";
2060
+ INFERRED_PREFERENCE_NAVIGATOR_STUB = true;
2061
+ }
2062
+ });
2063
+
1863
2064
  // src/core/navigators/interferenceMitigator.ts
1864
2065
  var interferenceMitigator_exports = {};
1865
2066
  __export(interferenceMitigator_exports, {
@@ -1991,17 +2192,6 @@ var init_interferenceMitigator = __esm({
1991
2192
  }
1992
2193
  return avoid;
1993
2194
  }
1994
- /**
1995
- * Get tags for a single card
1996
- */
1997
- async getCardTags(cardId, course) {
1998
- try {
1999
- const tagResponse = await course.getAppliedTags(cardId);
2000
- return tagResponse.rows.map((row) => row.value?.name || row.key).filter(Boolean);
2001
- } catch {
2002
- return [];
2003
- }
2004
- }
2005
2195
  /**
2006
2196
  * Compute interference score reduction for a card.
2007
2197
  * Returns: { multiplier, interfering tags, reason }
@@ -2053,7 +2243,7 @@ var init_interferenceMitigator = __esm({
2053
2243
  const tagsToAvoid = this.getTagsToAvoid(immatureTags);
2054
2244
  const adjusted = [];
2055
2245
  for (const card of cards) {
2056
- const cardTags = await this.getCardTags(card.cardId, context.course);
2246
+ const cardTags = card.tags ?? [];
2057
2247
  const { multiplier, reason } = this.computeInterferenceEffect(
2058
2248
  cardTags,
2059
2249
  tagsToAvoid,
@@ -2198,27 +2388,16 @@ var init_relativePriority = __esm({
2198
2388
  return `Low-priority tags: ${tagList}${more} (priority ${priority.toFixed(2)} \u2192 reduce ${boostFactor.toFixed(2)}x \u2192 ${finalScore.toFixed(2)})`;
2199
2389
  }
2200
2390
  }
2201
- /**
2202
- * Get tags for a single card.
2203
- */
2204
- async getCardTags(cardId, course) {
2205
- try {
2206
- const tagResponse = await course.getAppliedTags(cardId);
2207
- return tagResponse.rows.map((r) => r.doc?.name).filter((x) => !!x);
2208
- } catch {
2209
- return [];
2210
- }
2211
- }
2212
2391
  /**
2213
2392
  * CardFilter.transform implementation.
2214
2393
  *
2215
2394
  * Apply priority-adjusted scoring. Cards with high-priority tags get boosted,
2216
2395
  * cards with low-priority tags get reduced scores.
2217
2396
  */
2218
- async transform(cards, context) {
2397
+ async transform(cards, _context) {
2219
2398
  const adjusted = await Promise.all(
2220
2399
  cards.map(async (card) => {
2221
- const cardTags = await this.getCardTags(card.cardId, context.course);
2400
+ const cardTags = card.tags ?? [];
2222
2401
  const priority = this.computeCardPriority(cardTags);
2223
2402
  const boostFactor = this.computeBoostFactor(priority);
2224
2403
  const finalScore = Math.max(0, Math.min(1, card.score * boostFactor));
@@ -2385,6 +2564,19 @@ var init_srs = __esm({
2385
2564
  }
2386
2565
  });
2387
2566
 
2567
+ // src/core/navigators/userGoal.ts
2568
+ var userGoal_exports = {};
2569
+ __export(userGoal_exports, {
2570
+ USER_GOAL_NAVIGATOR_STUB: () => USER_GOAL_NAVIGATOR_STUB
2571
+ });
2572
+ var USER_GOAL_NAVIGATOR_STUB;
2573
+ var init_userGoal = __esm({
2574
+ "src/core/navigators/userGoal.ts"() {
2575
+ "use strict";
2576
+ USER_GOAL_NAVIGATOR_STUB = true;
2577
+ }
2578
+ });
2579
+
2388
2580
  // import("./**/*") in src/core/navigators/index.ts
2389
2581
  var globImport;
2390
2582
  var init_ = __esm({
@@ -2397,14 +2589,17 @@ var init_ = __esm({
2397
2589
  "./filters/eloDistance.ts": () => Promise.resolve().then(() => (init_eloDistance(), eloDistance_exports)),
2398
2590
  "./filters/index.ts": () => Promise.resolve().then(() => (init_filters(), filters_exports)),
2399
2591
  "./filters/types.ts": () => Promise.resolve().then(() => (init_types(), types_exports)),
2592
+ "./filters/userTagPreference.ts": () => Promise.resolve().then(() => (init_userTagPreference(), userTagPreference_exports)),
2400
2593
  "./generators/index.ts": () => Promise.resolve().then(() => (init_generators(), generators_exports)),
2401
2594
  "./generators/types.ts": () => Promise.resolve().then(() => (init_types2(), types_exports2)),
2402
2595
  "./hardcodedOrder.ts": () => Promise.resolve().then(() => (init_hardcodedOrder(), hardcodedOrder_exports)),
2403
2596
  "./hierarchyDefinition.ts": () => Promise.resolve().then(() => (init_hierarchyDefinition(), hierarchyDefinition_exports)),
2404
2597
  "./index.ts": () => Promise.resolve().then(() => (init_navigators(), navigators_exports)),
2598
+ "./inferredPreference.ts": () => Promise.resolve().then(() => (init_inferredPreference(), inferredPreference_exports)),
2405
2599
  "./interferenceMitigator.ts": () => Promise.resolve().then(() => (init_interferenceMitigator(), interferenceMitigator_exports)),
2406
2600
  "./relativePriority.ts": () => Promise.resolve().then(() => (init_relativePriority(), relativePriority_exports)),
2407
- "./srs.ts": () => Promise.resolve().then(() => (init_srs(), srs_exports))
2601
+ "./srs.ts": () => Promise.resolve().then(() => (init_srs(), srs_exports)),
2602
+ "./userGoal.ts": () => Promise.resolve().then(() => (init_userGoal(), userGoal_exports))
2408
2603
  });
2409
2604
  }
2410
2605
  });
@@ -2453,6 +2648,7 @@ var init_navigators = __esm({
2453
2648
  Navigators2["HIERARCHY"] = "hierarchyDefinition";
2454
2649
  Navigators2["INTERFERENCE"] = "interferenceMitigator";
2455
2650
  Navigators2["RELATIVE_PRIORITY"] = "relativePriority";
2651
+ Navigators2["USER_TAG_PREFERENCE"] = "userTagPreference";
2456
2652
  return Navigators2;
2457
2653
  })(Navigators || {});
2458
2654
  NavigatorRole = /* @__PURE__ */ ((NavigatorRole2) => {
@@ -2466,7 +2662,8 @@ var init_navigators = __esm({
2466
2662
  ["hardcodedOrder" /* HARDCODED */]: "generator" /* GENERATOR */,
2467
2663
  ["hierarchyDefinition" /* HIERARCHY */]: "filter" /* FILTER */,
2468
2664
  ["interferenceMitigator" /* INTERFERENCE */]: "filter" /* FILTER */,
2469
- ["relativePriority" /* RELATIVE_PRIORITY */]: "filter" /* FILTER */
2665
+ ["relativePriority" /* RELATIVE_PRIORITY */]: "filter" /* FILTER */,
2666
+ ["userTagPreference" /* USER_TAG_PREFERENCE */]: "filter" /* FILTER */
2470
2667
  };
2471
2668
  ContentNavigator = class {
2472
2669
  /** User interface for this navigation session */
@@ -2491,6 +2688,52 @@ var init_navigators = __esm({
2491
2688
  this.strategyId = strategyData._id;
2492
2689
  }
2493
2690
  }
2691
+ // ============================================================================
2692
+ // STRATEGY STATE HELPERS
2693
+ // ============================================================================
2694
+ //
2695
+ // These methods allow strategies to persist their own state (user preferences,
2696
+ // learned patterns, temporal tracking) in the user database.
2697
+ //
2698
+ // ============================================================================
2699
+ /**
2700
+ * Unique key identifying this strategy for state storage.
2701
+ *
2702
+ * Defaults to the constructor name (e.g., "UserTagPreferenceFilter").
2703
+ * Override in subclasses if multiple instances of the same strategy type
2704
+ * need separate state storage.
2705
+ */
2706
+ get strategyKey() {
2707
+ return this.constructor.name;
2708
+ }
2709
+ /**
2710
+ * Get this strategy's persisted state for the current course.
2711
+ *
2712
+ * @returns The strategy's data payload, or null if no state exists
2713
+ * @throws Error if user or course is not initialized
2714
+ */
2715
+ async getStrategyState() {
2716
+ if (!this.user || !this.course) {
2717
+ throw new Error(
2718
+ `Cannot get strategy state: navigator not properly initialized. Ensure user and course are provided to constructor.`
2719
+ );
2720
+ }
2721
+ return this.user.getStrategyState(this.course.getCourseID(), this.strategyKey);
2722
+ }
2723
+ /**
2724
+ * Persist this strategy's state for the current course.
2725
+ *
2726
+ * @param data - The strategy's data payload to store
2727
+ * @throws Error if user or course is not initialized
2728
+ */
2729
+ async putStrategyState(data) {
2730
+ if (!this.user || !this.course) {
2731
+ throw new Error(
2732
+ `Cannot put strategy state: navigator not properly initialized. Ensure user and course are provided to constructor.`
2733
+ );
2734
+ }
2735
+ return this.user.putStrategyState(this.course.getCourseID(), this.strategyKey, data);
2736
+ }
2494
2737
  /**
2495
2738
  * Factory method to create navigator instances dynamically.
2496
2739
  *
@@ -2865,15 +3108,6 @@ var init_courseDB = __esm({
2865
3108
  ret[r.id] = r.doc.id_displayable_data;
2866
3109
  }
2867
3110
  });
2868
- await Promise.all(
2869
- cards.rows.map((r) => {
2870
- return async () => {
2871
- if (isSuccessRow(r)) {
2872
- ret[r.id] = r.doc.id_displayable_data;
2873
- }
2874
- };
2875
- })
2876
- );
2877
3111
  return ret;
2878
3112
  }
2879
3113
  async getCardsByELO(elo, cardLimit) {
@@ -2958,6 +3192,28 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
2958
3192
  throw new Error(`Failed to find tags for card ${this.id}-${cardId}`);
2959
3193
  }
2960
3194
  }
3195
+ async getAppliedTagsBatch(cardIds) {
3196
+ if (cardIds.length === 0) {
3197
+ return /* @__PURE__ */ new Map();
3198
+ }
3199
+ const db = getCourseDB2(this.id);
3200
+ const result = await db.query("getTags", {
3201
+ keys: cardIds,
3202
+ include_docs: false
3203
+ });
3204
+ const tagsByCard = /* @__PURE__ */ new Map();
3205
+ for (const cardId of cardIds) {
3206
+ tagsByCard.set(cardId, []);
3207
+ }
3208
+ for (const row of result.rows) {
3209
+ const cardId = row.key;
3210
+ const tagName = row.value?.name;
3211
+ if (tagName && tagsByCard.has(cardId)) {
3212
+ tagsByCard.get(cardId).push(tagName);
3213
+ }
3214
+ }
3215
+ return tagsByCard;
3216
+ }
2961
3217
  async addTagToCard(cardId, tagId, updateELO) {
2962
3218
  return await addTagToCard(
2963
3219
  this.id,
@@ -3659,8 +3915,7 @@ var init_adminDB2 = __esm({
3659
3915
  }
3660
3916
  }
3661
3917
  }
3662
- const dbs = await Promise.all(promisedCRDbs);
3663
- return dbs.map((db) => {
3918
+ return promisedCRDbs.map((db) => {
3664
3919
  return {
3665
3920
  ...db.getConfig(),
3666
3921
  _id: db._id
@@ -4033,7 +4288,9 @@ import moment6 from "moment";
4033
4288
  function accomodateGuest() {
4034
4289
  logger.log("[funnel] accomodateGuest() called");
4035
4290
  if (typeof localStorage === "undefined") {
4036
- logger.log("[funnel] localStorage not available (Node.js environment), returning default guest");
4291
+ logger.log(
4292
+ "[funnel] localStorage not available (Node.js environment), returning default guest"
4293
+ );
4037
4294
  return {
4038
4295
  username: GuestUsername + "nodejs-test",
4039
4296
  firstVisit: true
@@ -5011,6 +5268,55 @@ Currently logged-in as ${this._username}.`
5011
5268
  async updateUserElo(courseId, elo) {
5012
5269
  return updateUserElo(this._username, courseId, elo);
5013
5270
  }
5271
+ async getStrategyState(courseId, strategyKey) {
5272
+ const docId = buildStrategyStateId(courseId, strategyKey);
5273
+ try {
5274
+ const doc = await this.localDB.get(docId);
5275
+ return doc.data;
5276
+ } catch (e) {
5277
+ const err = e;
5278
+ if (err.status === 404) {
5279
+ return null;
5280
+ }
5281
+ throw e;
5282
+ }
5283
+ }
5284
+ async putStrategyState(courseId, strategyKey, data) {
5285
+ const docId = buildStrategyStateId(courseId, strategyKey);
5286
+ let existingRev;
5287
+ try {
5288
+ const existing = await this.localDB.get(docId);
5289
+ existingRev = existing._rev;
5290
+ } catch (e) {
5291
+ const err = e;
5292
+ if (err.status !== 404) {
5293
+ throw e;
5294
+ }
5295
+ }
5296
+ const doc = {
5297
+ _id: docId,
5298
+ _rev: existingRev,
5299
+ docType: "STRATEGY_STATE" /* STRATEGY_STATE */,
5300
+ courseId,
5301
+ strategyKey,
5302
+ data,
5303
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
5304
+ };
5305
+ await this.localDB.put(doc);
5306
+ }
5307
+ async deleteStrategyState(courseId, strategyKey) {
5308
+ const docId = buildStrategyStateId(courseId, strategyKey);
5309
+ try {
5310
+ const doc = await this.localDB.get(docId);
5311
+ await this.localDB.remove(doc);
5312
+ } catch (e) {
5313
+ const err = e;
5314
+ if (err.status === 404) {
5315
+ return;
5316
+ }
5317
+ throw e;
5318
+ }
5319
+ }
5014
5320
  };
5015
5321
  userCoursesDoc = "CourseRegistrations";
5016
5322
  userClassroomsDoc = "ClassroomRegistrations";
@@ -5678,6 +5984,14 @@ var init_courseDB2 = __esm({
5678
5984
  };
5679
5985
  }
5680
5986
  }
5987
+ async getAppliedTagsBatch(cardIds) {
5988
+ const tagsIndex = await this.unpacker.getTagsIndex();
5989
+ const tagsByCard = /* @__PURE__ */ new Map();
5990
+ for (const cardId of cardIds) {
5991
+ tagsByCard.set(cardId, tagsIndex.byCard[cardId] || []);
5992
+ }
5993
+ return tagsByCard;
5994
+ }
5681
5995
  async addTagToCard(_cardId, _tagId) {
5682
5996
  throw new Error("Cannot modify tags in static mode");
5683
5997
  }
@@ -6384,6 +6698,16 @@ var init_user = __esm({
6384
6698
  }
6385
6699
  });
6386
6700
 
6701
+ // src/core/types/strategyState.ts
6702
+ function buildStrategyStateId(courseId, strategyKey) {
6703
+ return `STRATEGY_STATE::${courseId}::${strategyKey}`;
6704
+ }
6705
+ var init_strategyState = __esm({
6706
+ "src/core/types/strategyState.ts"() {
6707
+ "use strict";
6708
+ }
6709
+ });
6710
+
6387
6711
  // src/core/bulkImport/cardProcessor.ts
6388
6712
  import { Status as Status5 } from "@vue-skuilder/common";
6389
6713
  async function importParsedCards(parsedCards, courseDB, config) {
@@ -6527,6 +6851,7 @@ var init_core = __esm({
6527
6851
  init_interfaces();
6528
6852
  init_types_legacy();
6529
6853
  init_user();
6854
+ init_strategyState();
6530
6855
  init_Loggable();
6531
6856
  init_util();
6532
6857
  init_navigators();
@@ -8971,6 +9296,7 @@ export {
8971
9296
  TagFilteredContentSource,
8972
9297
  _resetDataLayer,
8973
9298
  areQuestionRecords,
9299
+ buildStrategyStateId,
8974
9300
  docIsDeleted,
8975
9301
  ensureAppDataDirectory,
8976
9302
  getAppDataDirectory,