@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.js CHANGED
@@ -122,6 +122,7 @@ var init_types_legacy = __esm({
122
122
  DocType3["SCHEDULED_CARD"] = "SCHEDULED_CARD";
123
123
  DocType3["TAG"] = "TAG";
124
124
  DocType3["NAVIGATION_STRATEGY"] = "NAVIGATION_STRATEGY";
125
+ DocType3["STRATEGY_STATE"] = "STRATEGY_STATE";
125
126
  return DocType3;
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
  });
@@ -1134,6 +1136,41 @@ var Pipeline_exports = {};
1134
1136
  __export(Pipeline_exports, {
1135
1137
  Pipeline: () => Pipeline
1136
1138
  });
1139
+ function logPipelineConfig(generator, filters) {
1140
+ const filterList = filters.length > 0 ? "\n - " + filters.map((f) => f.name).join("\n - ") : " none";
1141
+ logger.info(
1142
+ `[Pipeline] Configuration:
1143
+ Generator: ${generator.name}
1144
+ Filters:${filterList}`
1145
+ );
1146
+ }
1147
+ function logTagHydration(cards, tagsByCard) {
1148
+ const totalTags = Array.from(tagsByCard.values()).reduce((sum, tags) => sum + tags.length, 0);
1149
+ const cardsWithTags = Array.from(tagsByCard.values()).filter((tags) => tags.length > 0).length;
1150
+ logger.debug(
1151
+ `[Pipeline] Tag hydration: ${cards.length} cards, ${cardsWithTags} have tags (${totalTags} total tags) - single batch query`
1152
+ );
1153
+ }
1154
+ function logExecutionSummary(generatorName, generatedCount, filterCount, finalCount, topScores) {
1155
+ const scoreDisplay = topScores.length > 0 ? topScores.map((s) => s.toFixed(2)).join(", ") : "none";
1156
+ logger.info(
1157
+ `[Pipeline] Execution: ${generatorName} produced ${generatedCount} \u2192 ${filterCount} filters \u2192 ${finalCount} results (top scores: ${scoreDisplay})`
1158
+ );
1159
+ }
1160
+ function logCardProvenance(cards, maxCards = 3) {
1161
+ const cardsToLog = cards.slice(0, maxCards);
1162
+ logger.debug(`[Pipeline] Provenance for top ${cardsToLog.length} cards:`);
1163
+ for (const card of cardsToLog) {
1164
+ logger.debug(`[Pipeline] ${card.cardId} (final score: ${card.score.toFixed(3)}):`);
1165
+ for (const entry of card.provenance) {
1166
+ const scoreChange = entry.score.toFixed(3);
1167
+ const action = entry.action.padEnd(9);
1168
+ logger.debug(
1169
+ `[Pipeline] ${action} ${scoreChange} - ${entry.strategyName}: ${entry.reason}`
1170
+ );
1171
+ }
1172
+ }
1173
+ }
1137
1174
  var import_common5, Pipeline;
1138
1175
  var init_Pipeline = __esm({
1139
1176
  "src/core/navigators/Pipeline.ts"() {
@@ -1158,19 +1195,18 @@ var init_Pipeline = __esm({
1158
1195
  this.filters = filters;
1159
1196
  this.user = user;
1160
1197
  this.course = course;
1161
- logger.debug(
1162
- `[Pipeline] Created with generator '${generator.name}' and ${filters.length} filters: ${filters.map((f) => f.name).join(", ")}`
1163
- );
1198
+ logPipelineConfig(generator, filters);
1164
1199
  }
1165
1200
  /**
1166
1201
  * Get weighted cards by running generator and applying filters.
1167
1202
  *
1168
1203
  * 1. Build shared context (user ELO, etc.)
1169
1204
  * 2. Get candidates from generator (passing context)
1170
- * 3. Apply each filter sequentially
1171
- * 4. Remove zero-score cards
1172
- * 5. Sort by score descending
1173
- * 6. Return top N
1205
+ * 3. Batch hydrate tags for all candidates
1206
+ * 4. Apply each filter sequentially
1207
+ * 5. Remove zero-score cards
1208
+ * 6. Sort by score descending
1209
+ * 7. Return top N
1174
1210
  *
1175
1211
  * @param limit - Maximum number of cards to return
1176
1212
  * @returns Cards sorted by score descending
@@ -1183,7 +1219,9 @@ var init_Pipeline = __esm({
1183
1219
  `[Pipeline] Fetching ${fetchLimit} candidates from generator '${this.generator.name}'`
1184
1220
  );
1185
1221
  let cards = await this.generator.getWeightedCards(fetchLimit, context);
1186
- logger.debug(`[Pipeline] Generator returned ${cards.length} candidates`);
1222
+ const generatedCount = cards.length;
1223
+ logger.debug(`[Pipeline] Generator returned ${generatedCount} candidates`);
1224
+ cards = await this.hydrateTags(cards);
1187
1225
  for (const filter of this.filters) {
1188
1226
  const beforeCount = cards.length;
1189
1227
  cards = await filter.transform(cards, context);
@@ -1192,11 +1230,33 @@ var init_Pipeline = __esm({
1192
1230
  cards = cards.filter((c) => c.score > 0);
1193
1231
  cards.sort((a, b) => b.score - a.score);
1194
1232
  const result = cards.slice(0, limit);
1195
- logger.debug(
1196
- `[Pipeline] Returning ${result.length} cards (top scores: ${result.slice(0, 3).map((c) => c.score.toFixed(2)).join(", ")}...)`
1197
- );
1233
+ const topScores = result.slice(0, 3).map((c) => c.score);
1234
+ logExecutionSummary(this.generator.name, generatedCount, this.filters.length, result.length, topScores);
1235
+ logCardProvenance(result, 3);
1198
1236
  return result;
1199
1237
  }
1238
+ /**
1239
+ * Batch hydrate tags for all cards.
1240
+ *
1241
+ * Fetches tags for all cards in a single database query and attaches them
1242
+ * to the WeightedCard objects. Filters can then use card.tags instead of
1243
+ * making individual getAppliedTags() calls.
1244
+ *
1245
+ * @param cards - Cards to hydrate
1246
+ * @returns Cards with tags populated
1247
+ */
1248
+ async hydrateTags(cards) {
1249
+ if (cards.length === 0) {
1250
+ return cards;
1251
+ }
1252
+ const cardIds = cards.map((c) => c.cardId);
1253
+ const tagsByCard = await this.course.getAppliedTagsBatch(cardIds);
1254
+ logTagHydration(cards, tagsByCard);
1255
+ return cards.map((card) => ({
1256
+ ...card,
1257
+ tags: tagsByCard.get(card.cardId) ?? []
1258
+ }));
1259
+ }
1200
1260
  /**
1201
1261
  * Build shared context for generator and filters.
1202
1262
  *
@@ -1556,15 +1616,144 @@ var init_eloDistance = __esm({
1556
1616
  }
1557
1617
  });
1558
1618
 
1619
+ // src/core/navigators/filters/userTagPreference.ts
1620
+ var userTagPreference_exports = {};
1621
+ __export(userTagPreference_exports, {
1622
+ default: () => UserTagPreferenceFilter
1623
+ });
1624
+ var UserTagPreferenceFilter;
1625
+ var init_userTagPreference = __esm({
1626
+ "src/core/navigators/filters/userTagPreference.ts"() {
1627
+ "use strict";
1628
+ init_navigators();
1629
+ UserTagPreferenceFilter = class extends ContentNavigator {
1630
+ _strategyData;
1631
+ /** Human-readable name for CardFilter interface */
1632
+ name;
1633
+ constructor(user, course, strategyData) {
1634
+ super(user, course, strategyData);
1635
+ this._strategyData = strategyData;
1636
+ this.name = strategyData.name || "User Tag Preferences";
1637
+ }
1638
+ /**
1639
+ * Compute multiplier for a card based on its tags and user preferences.
1640
+ * Returns the maximum multiplier among all matching tags, or 1.0 if no matches.
1641
+ */
1642
+ computeMultiplier(cardTags, boostMap) {
1643
+ const multipliers = cardTags.map((tag) => boostMap[tag]).filter((val) => val !== void 0);
1644
+ if (multipliers.length === 0) {
1645
+ return 1;
1646
+ }
1647
+ return Math.max(...multipliers);
1648
+ }
1649
+ /**
1650
+ * Build human-readable reason for the filter's decision.
1651
+ */
1652
+ buildReason(cardTags, boostMap, multiplier) {
1653
+ const matchingTags = cardTags.filter((tag) => boostMap[tag] === multiplier);
1654
+ if (multiplier === 0) {
1655
+ return `Excluded by user preference: ${matchingTags.join(", ")} (${multiplier}x)`;
1656
+ }
1657
+ if (multiplier < 1) {
1658
+ return `Penalized by user preference: ${matchingTags.join(", ")} (${multiplier.toFixed(2)}x)`;
1659
+ }
1660
+ if (multiplier > 1) {
1661
+ return `Boosted by user preference: ${matchingTags.join(", ")} (${multiplier.toFixed(2)}x)`;
1662
+ }
1663
+ return "No matching user preferences";
1664
+ }
1665
+ /**
1666
+ * CardFilter.transform implementation.
1667
+ *
1668
+ * Apply user tag preferences:
1669
+ * 1. Read preferences from strategy state
1670
+ * 2. If no preferences, pass through unchanged
1671
+ * 3. For each card:
1672
+ * - Look up tag in boost record
1673
+ * - If tag found: apply multiplier (0 = exclude, 1 = neutral, >1 = boost)
1674
+ * - If multiple tags match: use max multiplier
1675
+ * - Append provenance with clear reason
1676
+ */
1677
+ async transform(cards, _context) {
1678
+ const prefs = await this.getStrategyState();
1679
+ if (!prefs || Object.keys(prefs.boost).length === 0) {
1680
+ return cards.map((card) => ({
1681
+ ...card,
1682
+ provenance: [
1683
+ ...card.provenance,
1684
+ {
1685
+ strategy: "userTagPreference",
1686
+ strategyName: this.strategyName || this.name,
1687
+ strategyId: this.strategyId || this._strategyData._id,
1688
+ action: "passed",
1689
+ score: card.score,
1690
+ reason: "No user tag preferences configured"
1691
+ }
1692
+ ]
1693
+ }));
1694
+ }
1695
+ const adjusted = await Promise.all(
1696
+ cards.map(async (card) => {
1697
+ const cardTags = card.tags ?? [];
1698
+ const multiplier = this.computeMultiplier(cardTags, prefs.boost);
1699
+ const finalScore = Math.min(1, card.score * multiplier);
1700
+ let action;
1701
+ if (multiplier === 0 || multiplier < 1) {
1702
+ action = "penalized";
1703
+ } else if (multiplier > 1) {
1704
+ action = "boosted";
1705
+ } else {
1706
+ action = "passed";
1707
+ }
1708
+ return {
1709
+ ...card,
1710
+ score: finalScore,
1711
+ provenance: [
1712
+ ...card.provenance,
1713
+ {
1714
+ strategy: "userTagPreference",
1715
+ strategyName: this.strategyName || this.name,
1716
+ strategyId: this.strategyId || this._strategyData._id,
1717
+ action,
1718
+ score: finalScore,
1719
+ reason: this.buildReason(cardTags, prefs.boost, multiplier)
1720
+ }
1721
+ ]
1722
+ };
1723
+ })
1724
+ );
1725
+ return adjusted;
1726
+ }
1727
+ /**
1728
+ * Legacy getWeightedCards - throws as filters should not be used as generators.
1729
+ */
1730
+ async getWeightedCards(_limit) {
1731
+ throw new Error(
1732
+ "UserTagPreferenceFilter is a filter and should not be used as a generator. Use Pipeline with a generator and this filter via transform()."
1733
+ );
1734
+ }
1735
+ // Legacy methods - stub implementations since filters don't generate cards
1736
+ async getNewCards(_n) {
1737
+ return [];
1738
+ }
1739
+ async getPendingReviews() {
1740
+ return [];
1741
+ }
1742
+ };
1743
+ }
1744
+ });
1745
+
1559
1746
  // src/core/navigators/filters/index.ts
1560
1747
  var filters_exports = {};
1561
1748
  __export(filters_exports, {
1749
+ UserTagPreferenceFilter: () => UserTagPreferenceFilter,
1562
1750
  createEloDistanceFilter: () => createEloDistanceFilter
1563
1751
  });
1564
1752
  var init_filters = __esm({
1565
1753
  "src/core/navigators/filters/index.ts"() {
1566
1754
  "use strict";
1567
1755
  init_eloDistance();
1756
+ init_userTagPreference();
1568
1757
  }
1569
1758
  });
1570
1759
 
@@ -1797,10 +1986,9 @@ var init_hierarchyDefinition = __esm({
1797
1986
  /**
1798
1987
  * Check if a card is unlocked and generate reason.
1799
1988
  */
1800
- async checkCardUnlock(cardId, course, unlockedTags, masteredTags) {
1989
+ async checkCardUnlock(card, course, unlockedTags, masteredTags) {
1801
1990
  try {
1802
- const tagResponse = await course.getAppliedTags(cardId);
1803
- const cardTags = tagResponse.rows.map((row) => row.value?.name || row.key);
1991
+ const cardTags = card.tags ?? [];
1804
1992
  const lockedTags = cardTags.filter(
1805
1993
  (tag) => this.hasPrerequisites(tag) && !unlockedTags.has(tag)
1806
1994
  );
@@ -1837,7 +2025,7 @@ var init_hierarchyDefinition = __esm({
1837
2025
  const gated = [];
1838
2026
  for (const card of cards) {
1839
2027
  const { isUnlocked, reason } = await this.checkCardUnlock(
1840
- card.cardId,
2028
+ card,
1841
2029
  context.course,
1842
2030
  unlockedTags,
1843
2031
  masteredTags
@@ -1883,6 +2071,19 @@ var init_hierarchyDefinition = __esm({
1883
2071
  }
1884
2072
  });
1885
2073
 
2074
+ // src/core/navigators/inferredPreference.ts
2075
+ var inferredPreference_exports = {};
2076
+ __export(inferredPreference_exports, {
2077
+ INFERRED_PREFERENCE_NAVIGATOR_STUB: () => INFERRED_PREFERENCE_NAVIGATOR_STUB
2078
+ });
2079
+ var INFERRED_PREFERENCE_NAVIGATOR_STUB;
2080
+ var init_inferredPreference = __esm({
2081
+ "src/core/navigators/inferredPreference.ts"() {
2082
+ "use strict";
2083
+ INFERRED_PREFERENCE_NAVIGATOR_STUB = true;
2084
+ }
2085
+ });
2086
+
1886
2087
  // src/core/navigators/interferenceMitigator.ts
1887
2088
  var interferenceMitigator_exports = {};
1888
2089
  __export(interferenceMitigator_exports, {
@@ -2014,17 +2215,6 @@ var init_interferenceMitigator = __esm({
2014
2215
  }
2015
2216
  return avoid;
2016
2217
  }
2017
- /**
2018
- * Get tags for a single card
2019
- */
2020
- async getCardTags(cardId, course) {
2021
- try {
2022
- const tagResponse = await course.getAppliedTags(cardId);
2023
- return tagResponse.rows.map((row) => row.value?.name || row.key).filter(Boolean);
2024
- } catch {
2025
- return [];
2026
- }
2027
- }
2028
2218
  /**
2029
2219
  * Compute interference score reduction for a card.
2030
2220
  * Returns: { multiplier, interfering tags, reason }
@@ -2076,7 +2266,7 @@ var init_interferenceMitigator = __esm({
2076
2266
  const tagsToAvoid = this.getTagsToAvoid(immatureTags);
2077
2267
  const adjusted = [];
2078
2268
  for (const card of cards) {
2079
- const cardTags = await this.getCardTags(card.cardId, context.course);
2269
+ const cardTags = card.tags ?? [];
2080
2270
  const { multiplier, reason } = this.computeInterferenceEffect(
2081
2271
  cardTags,
2082
2272
  tagsToAvoid,
@@ -2221,27 +2411,16 @@ var init_relativePriority = __esm({
2221
2411
  return `Low-priority tags: ${tagList}${more} (priority ${priority.toFixed(2)} \u2192 reduce ${boostFactor.toFixed(2)}x \u2192 ${finalScore.toFixed(2)})`;
2222
2412
  }
2223
2413
  }
2224
- /**
2225
- * Get tags for a single card.
2226
- */
2227
- async getCardTags(cardId, course) {
2228
- try {
2229
- const tagResponse = await course.getAppliedTags(cardId);
2230
- return tagResponse.rows.map((r) => r.doc?.name).filter((x) => !!x);
2231
- } catch {
2232
- return [];
2233
- }
2234
- }
2235
2414
  /**
2236
2415
  * CardFilter.transform implementation.
2237
2416
  *
2238
2417
  * Apply priority-adjusted scoring. Cards with high-priority tags get boosted,
2239
2418
  * cards with low-priority tags get reduced scores.
2240
2419
  */
2241
- async transform(cards, context) {
2420
+ async transform(cards, _context) {
2242
2421
  const adjusted = await Promise.all(
2243
2422
  cards.map(async (card) => {
2244
- const cardTags = await this.getCardTags(card.cardId, context.course);
2423
+ const cardTags = card.tags ?? [];
2245
2424
  const priority = this.computeCardPriority(cardTags);
2246
2425
  const boostFactor = this.computeBoostFactor(priority);
2247
2426
  const finalScore = Math.max(0, Math.min(1, card.score * boostFactor));
@@ -2408,6 +2587,19 @@ var init_srs = __esm({
2408
2587
  }
2409
2588
  });
2410
2589
 
2590
+ // src/core/navigators/userGoal.ts
2591
+ var userGoal_exports = {};
2592
+ __export(userGoal_exports, {
2593
+ USER_GOAL_NAVIGATOR_STUB: () => USER_GOAL_NAVIGATOR_STUB
2594
+ });
2595
+ var USER_GOAL_NAVIGATOR_STUB;
2596
+ var init_userGoal = __esm({
2597
+ "src/core/navigators/userGoal.ts"() {
2598
+ "use strict";
2599
+ USER_GOAL_NAVIGATOR_STUB = true;
2600
+ }
2601
+ });
2602
+
2411
2603
  // import("./**/*") in src/core/navigators/index.ts
2412
2604
  var globImport;
2413
2605
  var init_ = __esm({
@@ -2420,14 +2612,17 @@ var init_ = __esm({
2420
2612
  "./filters/eloDistance.ts": () => Promise.resolve().then(() => (init_eloDistance(), eloDistance_exports)),
2421
2613
  "./filters/index.ts": () => Promise.resolve().then(() => (init_filters(), filters_exports)),
2422
2614
  "./filters/types.ts": () => Promise.resolve().then(() => (init_types(), types_exports)),
2615
+ "./filters/userTagPreference.ts": () => Promise.resolve().then(() => (init_userTagPreference(), userTagPreference_exports)),
2423
2616
  "./generators/index.ts": () => Promise.resolve().then(() => (init_generators(), generators_exports)),
2424
2617
  "./generators/types.ts": () => Promise.resolve().then(() => (init_types2(), types_exports2)),
2425
2618
  "./hardcodedOrder.ts": () => Promise.resolve().then(() => (init_hardcodedOrder(), hardcodedOrder_exports)),
2426
2619
  "./hierarchyDefinition.ts": () => Promise.resolve().then(() => (init_hierarchyDefinition(), hierarchyDefinition_exports)),
2427
2620
  "./index.ts": () => Promise.resolve().then(() => (init_navigators(), navigators_exports)),
2621
+ "./inferredPreference.ts": () => Promise.resolve().then(() => (init_inferredPreference(), inferredPreference_exports)),
2428
2622
  "./interferenceMitigator.ts": () => Promise.resolve().then(() => (init_interferenceMitigator(), interferenceMitigator_exports)),
2429
2623
  "./relativePriority.ts": () => Promise.resolve().then(() => (init_relativePriority(), relativePriority_exports)),
2430
- "./srs.ts": () => Promise.resolve().then(() => (init_srs(), srs_exports))
2624
+ "./srs.ts": () => Promise.resolve().then(() => (init_srs(), srs_exports)),
2625
+ "./userGoal.ts": () => Promise.resolve().then(() => (init_userGoal(), userGoal_exports))
2431
2626
  });
2432
2627
  }
2433
2628
  });
@@ -2476,6 +2671,7 @@ var init_navigators = __esm({
2476
2671
  Navigators2["HIERARCHY"] = "hierarchyDefinition";
2477
2672
  Navigators2["INTERFERENCE"] = "interferenceMitigator";
2478
2673
  Navigators2["RELATIVE_PRIORITY"] = "relativePriority";
2674
+ Navigators2["USER_TAG_PREFERENCE"] = "userTagPreference";
2479
2675
  return Navigators2;
2480
2676
  })(Navigators || {});
2481
2677
  NavigatorRole = /* @__PURE__ */ ((NavigatorRole2) => {
@@ -2489,7 +2685,8 @@ var init_navigators = __esm({
2489
2685
  ["hardcodedOrder" /* HARDCODED */]: "generator" /* GENERATOR */,
2490
2686
  ["hierarchyDefinition" /* HIERARCHY */]: "filter" /* FILTER */,
2491
2687
  ["interferenceMitigator" /* INTERFERENCE */]: "filter" /* FILTER */,
2492
- ["relativePriority" /* RELATIVE_PRIORITY */]: "filter" /* FILTER */
2688
+ ["relativePriority" /* RELATIVE_PRIORITY */]: "filter" /* FILTER */,
2689
+ ["userTagPreference" /* USER_TAG_PREFERENCE */]: "filter" /* FILTER */
2493
2690
  };
2494
2691
  ContentNavigator = class {
2495
2692
  /** User interface for this navigation session */
@@ -2514,6 +2711,52 @@ var init_navigators = __esm({
2514
2711
  this.strategyId = strategyData._id;
2515
2712
  }
2516
2713
  }
2714
+ // ============================================================================
2715
+ // STRATEGY STATE HELPERS
2716
+ // ============================================================================
2717
+ //
2718
+ // These methods allow strategies to persist their own state (user preferences,
2719
+ // learned patterns, temporal tracking) in the user database.
2720
+ //
2721
+ // ============================================================================
2722
+ /**
2723
+ * Unique key identifying this strategy for state storage.
2724
+ *
2725
+ * Defaults to the constructor name (e.g., "UserTagPreferenceFilter").
2726
+ * Override in subclasses if multiple instances of the same strategy type
2727
+ * need separate state storage.
2728
+ */
2729
+ get strategyKey() {
2730
+ return this.constructor.name;
2731
+ }
2732
+ /**
2733
+ * Get this strategy's persisted state for the current course.
2734
+ *
2735
+ * @returns The strategy's data payload, or null if no state exists
2736
+ * @throws Error if user or course is not initialized
2737
+ */
2738
+ async getStrategyState() {
2739
+ if (!this.user || !this.course) {
2740
+ throw new Error(
2741
+ `Cannot get strategy state: navigator not properly initialized. Ensure user and course are provided to constructor.`
2742
+ );
2743
+ }
2744
+ return this.user.getStrategyState(this.course.getCourseID(), this.strategyKey);
2745
+ }
2746
+ /**
2747
+ * Persist this strategy's state for the current course.
2748
+ *
2749
+ * @param data - The strategy's data payload to store
2750
+ * @throws Error if user or course is not initialized
2751
+ */
2752
+ async putStrategyState(data) {
2753
+ if (!this.user || !this.course) {
2754
+ throw new Error(
2755
+ `Cannot put strategy state: navigator not properly initialized. Ensure user and course are provided to constructor.`
2756
+ );
2757
+ }
2758
+ return this.user.putStrategyState(this.course.getCourseID(), this.strategyKey, data);
2759
+ }
2517
2760
  /**
2518
2761
  * Factory method to create navigator instances dynamically.
2519
2762
  *
@@ -2883,15 +3126,6 @@ var init_courseDB = __esm({
2883
3126
  ret[r.id] = r.doc.id_displayable_data;
2884
3127
  }
2885
3128
  });
2886
- await Promise.all(
2887
- cards.rows.map((r) => {
2888
- return async () => {
2889
- if (isSuccessRow(r)) {
2890
- ret[r.id] = r.doc.id_displayable_data;
2891
- }
2892
- };
2893
- })
2894
- );
2895
3129
  return ret;
2896
3130
  }
2897
3131
  async getCardsByELO(elo, cardLimit) {
@@ -2976,6 +3210,28 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
2976
3210
  throw new Error(`Failed to find tags for card ${this.id}-${cardId}`);
2977
3211
  }
2978
3212
  }
3213
+ async getAppliedTagsBatch(cardIds) {
3214
+ if (cardIds.length === 0) {
3215
+ return /* @__PURE__ */ new Map();
3216
+ }
3217
+ const db = getCourseDB2(this.id);
3218
+ const result = await db.query("getTags", {
3219
+ keys: cardIds,
3220
+ include_docs: false
3221
+ });
3222
+ const tagsByCard = /* @__PURE__ */ new Map();
3223
+ for (const cardId of cardIds) {
3224
+ tagsByCard.set(cardId, []);
3225
+ }
3226
+ for (const row of result.rows) {
3227
+ const cardId = row.key;
3228
+ const tagName = row.value?.name;
3229
+ if (tagName && tagsByCard.has(cardId)) {
3230
+ tagsByCard.get(cardId).push(tagName);
3231
+ }
3232
+ }
3233
+ return tagsByCard;
3234
+ }
2979
3235
  async addTagToCard(cardId, tagId, updateELO) {
2980
3236
  return await addTagToCard(
2981
3237
  this.id,
@@ -3677,8 +3933,7 @@ var init_adminDB2 = __esm({
3677
3933
  }
3678
3934
  }
3679
3935
  }
3680
- const dbs = await Promise.all(promisedCRDbs);
3681
- return dbs.map((db) => {
3936
+ return promisedCRDbs.map((db) => {
3682
3937
  return {
3683
3938
  ...db.getConfig(),
3684
3939
  _id: db._id
@@ -4050,7 +4305,9 @@ var init_couch = __esm({
4050
4305
  function accomodateGuest() {
4051
4306
  logger.log("[funnel] accomodateGuest() called");
4052
4307
  if (typeof localStorage === "undefined") {
4053
- logger.log("[funnel] localStorage not available (Node.js environment), returning default guest");
4308
+ logger.log(
4309
+ "[funnel] localStorage not available (Node.js environment), returning default guest"
4310
+ );
4054
4311
  return {
4055
4312
  username: GuestUsername + "nodejs-test",
4056
4313
  firstVisit: true
@@ -5030,6 +5287,55 @@ Currently logged-in as ${this._username}.`
5030
5287
  async updateUserElo(courseId, elo) {
5031
5288
  return updateUserElo(this._username, courseId, elo);
5032
5289
  }
5290
+ async getStrategyState(courseId, strategyKey) {
5291
+ const docId = buildStrategyStateId(courseId, strategyKey);
5292
+ try {
5293
+ const doc = await this.localDB.get(docId);
5294
+ return doc.data;
5295
+ } catch (e) {
5296
+ const err = e;
5297
+ if (err.status === 404) {
5298
+ return null;
5299
+ }
5300
+ throw e;
5301
+ }
5302
+ }
5303
+ async putStrategyState(courseId, strategyKey, data) {
5304
+ const docId = buildStrategyStateId(courseId, strategyKey);
5305
+ let existingRev;
5306
+ try {
5307
+ const existing = await this.localDB.get(docId);
5308
+ existingRev = existing._rev;
5309
+ } catch (e) {
5310
+ const err = e;
5311
+ if (err.status !== 404) {
5312
+ throw e;
5313
+ }
5314
+ }
5315
+ const doc = {
5316
+ _id: docId,
5317
+ _rev: existingRev,
5318
+ docType: "STRATEGY_STATE" /* STRATEGY_STATE */,
5319
+ courseId,
5320
+ strategyKey,
5321
+ data,
5322
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
5323
+ };
5324
+ await this.localDB.put(doc);
5325
+ }
5326
+ async deleteStrategyState(courseId, strategyKey) {
5327
+ const docId = buildStrategyStateId(courseId, strategyKey);
5328
+ try {
5329
+ const doc = await this.localDB.get(docId);
5330
+ await this.localDB.remove(doc);
5331
+ } catch (e) {
5332
+ const err = e;
5333
+ if (err.status === 404) {
5334
+ return;
5335
+ }
5336
+ throw e;
5337
+ }
5338
+ }
5033
5339
  };
5034
5340
  userCoursesDoc = "CourseRegistrations";
5035
5341
  userClassroomsDoc = "ClassroomRegistrations";
@@ -5697,6 +6003,14 @@ var init_courseDB2 = __esm({
5697
6003
  };
5698
6004
  }
5699
6005
  }
6006
+ async getAppliedTagsBatch(cardIds) {
6007
+ const tagsIndex = await this.unpacker.getTagsIndex();
6008
+ const tagsByCard = /* @__PURE__ */ new Map();
6009
+ for (const cardId of cardIds) {
6010
+ tagsByCard.set(cardId, tagsIndex.byCard[cardId] || []);
6011
+ }
6012
+ return tagsByCard;
6013
+ }
5700
6014
  async addTagToCard(_cardId, _tagId) {
5701
6015
  throw new Error("Cannot modify tags in static mode");
5702
6016
  }
@@ -6404,6 +6718,16 @@ var init_user = __esm({
6404
6718
  }
6405
6719
  });
6406
6720
 
6721
+ // src/core/types/strategyState.ts
6722
+ function buildStrategyStateId(courseId, strategyKey) {
6723
+ return `STRATEGY_STATE::${courseId}::${strategyKey}`;
6724
+ }
6725
+ var init_strategyState = __esm({
6726
+ "src/core/types/strategyState.ts"() {
6727
+ "use strict";
6728
+ }
6729
+ });
6730
+
6407
6731
  // src/core/bulkImport/cardProcessor.ts
6408
6732
  async function importParsedCards(parsedCards, courseDB, config) {
6409
6733
  const results = [];
@@ -6548,6 +6872,7 @@ var init_core = __esm({
6548
6872
  init_interfaces();
6549
6873
  init_types_legacy();
6550
6874
  init_user();
6875
+ init_strategyState();
6551
6876
  init_Loggable();
6552
6877
  init_util();
6553
6878
  init_navigators();
@@ -6576,6 +6901,7 @@ __export(index_exports, {
6576
6901
  TagFilteredContentSource: () => TagFilteredContentSource,
6577
6902
  _resetDataLayer: () => _resetDataLayer,
6578
6903
  areQuestionRecords: () => areQuestionRecords,
6904
+ buildStrategyStateId: () => buildStrategyStateId,
6579
6905
  docIsDeleted: () => docIsDeleted,
6580
6906
  ensureAppDataDirectory: () => ensureAppDataDirectory,
6581
6907
  getAppDataDirectory: () => getAppDataDirectory,
@@ -9037,6 +9363,7 @@ init_factory();
9037
9363
  TagFilteredContentSource,
9038
9364
  _resetDataLayer,
9039
9365
  areQuestionRecords,
9366
+ buildStrategyStateId,
9040
9367
  docIsDeleted,
9041
9368
  ensureAppDataDirectory,
9042
9369
  getAppDataDirectory,