@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
@@ -97,7 +97,8 @@ var init_types_legacy = __esm({
97
97
  ["QUESTION" /* QUESTIONTYPE */]: "QUESTION",
98
98
  ["VIEW" /* VIEW */]: "VIEW",
99
99
  ["PEDAGOGY" /* PEDAGOGY */]: "PEDAGOGY",
100
- ["NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */]: "NAVIGATION_STRATEGY"
100
+ ["NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */]: "NAVIGATION_STRATEGY",
101
+ ["STRATEGY_STATE" /* STRATEGY_STATE */]: "STRATEGY_STATE"
101
102
  };
102
103
  }
103
104
  });
@@ -665,6 +666,41 @@ __export(Pipeline_exports, {
665
666
  Pipeline: () => Pipeline
666
667
  });
667
668
  import { toCourseElo as toCourseElo2 } from "@vue-skuilder/common";
669
+ function logPipelineConfig(generator, filters) {
670
+ const filterList = filters.length > 0 ? "\n - " + filters.map((f) => f.name).join("\n - ") : " none";
671
+ logger.info(
672
+ `[Pipeline] Configuration:
673
+ Generator: ${generator.name}
674
+ Filters:${filterList}`
675
+ );
676
+ }
677
+ function logTagHydration(cards, tagsByCard) {
678
+ const totalTags = Array.from(tagsByCard.values()).reduce((sum, tags) => sum + tags.length, 0);
679
+ const cardsWithTags = Array.from(tagsByCard.values()).filter((tags) => tags.length > 0).length;
680
+ logger.debug(
681
+ `[Pipeline] Tag hydration: ${cards.length} cards, ${cardsWithTags} have tags (${totalTags} total tags) - single batch query`
682
+ );
683
+ }
684
+ function logExecutionSummary(generatorName, generatedCount, filterCount, finalCount, topScores) {
685
+ const scoreDisplay = topScores.length > 0 ? topScores.map((s) => s.toFixed(2)).join(", ") : "none";
686
+ logger.info(
687
+ `[Pipeline] Execution: ${generatorName} produced ${generatedCount} \u2192 ${filterCount} filters \u2192 ${finalCount} results (top scores: ${scoreDisplay})`
688
+ );
689
+ }
690
+ function logCardProvenance(cards, maxCards = 3) {
691
+ const cardsToLog = cards.slice(0, maxCards);
692
+ logger.debug(`[Pipeline] Provenance for top ${cardsToLog.length} cards:`);
693
+ for (const card of cardsToLog) {
694
+ logger.debug(`[Pipeline] ${card.cardId} (final score: ${card.score.toFixed(3)}):`);
695
+ for (const entry of card.provenance) {
696
+ const scoreChange = entry.score.toFixed(3);
697
+ const action = entry.action.padEnd(9);
698
+ logger.debug(
699
+ `[Pipeline] ${action} ${scoreChange} - ${entry.strategyName}: ${entry.reason}`
700
+ );
701
+ }
702
+ }
703
+ }
668
704
  var Pipeline;
669
705
  var init_Pipeline = __esm({
670
706
  "src/core/navigators/Pipeline.ts"() {
@@ -688,19 +724,18 @@ var init_Pipeline = __esm({
688
724
  this.filters = filters;
689
725
  this.user = user;
690
726
  this.course = course;
691
- logger.debug(
692
- `[Pipeline] Created with generator '${generator.name}' and ${filters.length} filters: ${filters.map((f) => f.name).join(", ")}`
693
- );
727
+ logPipelineConfig(generator, filters);
694
728
  }
695
729
  /**
696
730
  * Get weighted cards by running generator and applying filters.
697
731
  *
698
732
  * 1. Build shared context (user ELO, etc.)
699
733
  * 2. Get candidates from generator (passing context)
700
- * 3. Apply each filter sequentially
701
- * 4. Remove zero-score cards
702
- * 5. Sort by score descending
703
- * 6. Return top N
734
+ * 3. Batch hydrate tags for all candidates
735
+ * 4. Apply each filter sequentially
736
+ * 5. Remove zero-score cards
737
+ * 6. Sort by score descending
738
+ * 7. Return top N
704
739
  *
705
740
  * @param limit - Maximum number of cards to return
706
741
  * @returns Cards sorted by score descending
@@ -713,7 +748,9 @@ var init_Pipeline = __esm({
713
748
  `[Pipeline] Fetching ${fetchLimit} candidates from generator '${this.generator.name}'`
714
749
  );
715
750
  let cards = await this.generator.getWeightedCards(fetchLimit, context);
716
- logger.debug(`[Pipeline] Generator returned ${cards.length} candidates`);
751
+ const generatedCount = cards.length;
752
+ logger.debug(`[Pipeline] Generator returned ${generatedCount} candidates`);
753
+ cards = await this.hydrateTags(cards);
717
754
  for (const filter of this.filters) {
718
755
  const beforeCount = cards.length;
719
756
  cards = await filter.transform(cards, context);
@@ -722,11 +759,33 @@ var init_Pipeline = __esm({
722
759
  cards = cards.filter((c) => c.score > 0);
723
760
  cards.sort((a, b) => b.score - a.score);
724
761
  const result = cards.slice(0, limit);
725
- logger.debug(
726
- `[Pipeline] Returning ${result.length} cards (top scores: ${result.slice(0, 3).map((c) => c.score.toFixed(2)).join(", ")}...)`
727
- );
762
+ const topScores = result.slice(0, 3).map((c) => c.score);
763
+ logExecutionSummary(this.generator.name, generatedCount, this.filters.length, result.length, topScores);
764
+ logCardProvenance(result, 3);
728
765
  return result;
729
766
  }
767
+ /**
768
+ * Batch hydrate tags for all cards.
769
+ *
770
+ * Fetches tags for all cards in a single database query and attaches them
771
+ * to the WeightedCard objects. Filters can then use card.tags instead of
772
+ * making individual getAppliedTags() calls.
773
+ *
774
+ * @param cards - Cards to hydrate
775
+ * @returns Cards with tags populated
776
+ */
777
+ async hydrateTags(cards) {
778
+ if (cards.length === 0) {
779
+ return cards;
780
+ }
781
+ const cardIds = cards.map((c) => c.cardId);
782
+ const tagsByCard = await this.course.getAppliedTagsBatch(cardIds);
783
+ logTagHydration(cards, tagsByCard);
784
+ return cards.map((card) => ({
785
+ ...card,
786
+ tags: tagsByCard.get(card.cardId) ?? []
787
+ }));
788
+ }
730
789
  /**
731
790
  * Build shared context for generator and filters.
732
791
  *
@@ -1086,15 +1145,144 @@ var init_eloDistance = __esm({
1086
1145
  }
1087
1146
  });
1088
1147
 
1148
+ // src/core/navigators/filters/userTagPreference.ts
1149
+ var userTagPreference_exports = {};
1150
+ __export(userTagPreference_exports, {
1151
+ default: () => UserTagPreferenceFilter
1152
+ });
1153
+ var UserTagPreferenceFilter;
1154
+ var init_userTagPreference = __esm({
1155
+ "src/core/navigators/filters/userTagPreference.ts"() {
1156
+ "use strict";
1157
+ init_navigators();
1158
+ UserTagPreferenceFilter = class extends ContentNavigator {
1159
+ _strategyData;
1160
+ /** Human-readable name for CardFilter interface */
1161
+ name;
1162
+ constructor(user, course, strategyData) {
1163
+ super(user, course, strategyData);
1164
+ this._strategyData = strategyData;
1165
+ this.name = strategyData.name || "User Tag Preferences";
1166
+ }
1167
+ /**
1168
+ * Compute multiplier for a card based on its tags and user preferences.
1169
+ * Returns the maximum multiplier among all matching tags, or 1.0 if no matches.
1170
+ */
1171
+ computeMultiplier(cardTags, boostMap) {
1172
+ const multipliers = cardTags.map((tag) => boostMap[tag]).filter((val) => val !== void 0);
1173
+ if (multipliers.length === 0) {
1174
+ return 1;
1175
+ }
1176
+ return Math.max(...multipliers);
1177
+ }
1178
+ /**
1179
+ * Build human-readable reason for the filter's decision.
1180
+ */
1181
+ buildReason(cardTags, boostMap, multiplier) {
1182
+ const matchingTags = cardTags.filter((tag) => boostMap[tag] === multiplier);
1183
+ if (multiplier === 0) {
1184
+ return `Excluded by user preference: ${matchingTags.join(", ")} (${multiplier}x)`;
1185
+ }
1186
+ if (multiplier < 1) {
1187
+ return `Penalized by user preference: ${matchingTags.join(", ")} (${multiplier.toFixed(2)}x)`;
1188
+ }
1189
+ if (multiplier > 1) {
1190
+ return `Boosted by user preference: ${matchingTags.join(", ")} (${multiplier.toFixed(2)}x)`;
1191
+ }
1192
+ return "No matching user preferences";
1193
+ }
1194
+ /**
1195
+ * CardFilter.transform implementation.
1196
+ *
1197
+ * Apply user tag preferences:
1198
+ * 1. Read preferences from strategy state
1199
+ * 2. If no preferences, pass through unchanged
1200
+ * 3. For each card:
1201
+ * - Look up tag in boost record
1202
+ * - If tag found: apply multiplier (0 = exclude, 1 = neutral, >1 = boost)
1203
+ * - If multiple tags match: use max multiplier
1204
+ * - Append provenance with clear reason
1205
+ */
1206
+ async transform(cards, _context) {
1207
+ const prefs = await this.getStrategyState();
1208
+ if (!prefs || Object.keys(prefs.boost).length === 0) {
1209
+ return cards.map((card) => ({
1210
+ ...card,
1211
+ provenance: [
1212
+ ...card.provenance,
1213
+ {
1214
+ strategy: "userTagPreference",
1215
+ strategyName: this.strategyName || this.name,
1216
+ strategyId: this.strategyId || this._strategyData._id,
1217
+ action: "passed",
1218
+ score: card.score,
1219
+ reason: "No user tag preferences configured"
1220
+ }
1221
+ ]
1222
+ }));
1223
+ }
1224
+ const adjusted = await Promise.all(
1225
+ cards.map(async (card) => {
1226
+ const cardTags = card.tags ?? [];
1227
+ const multiplier = this.computeMultiplier(cardTags, prefs.boost);
1228
+ const finalScore = Math.min(1, card.score * multiplier);
1229
+ let action;
1230
+ if (multiplier === 0 || multiplier < 1) {
1231
+ action = "penalized";
1232
+ } else if (multiplier > 1) {
1233
+ action = "boosted";
1234
+ } else {
1235
+ action = "passed";
1236
+ }
1237
+ return {
1238
+ ...card,
1239
+ score: finalScore,
1240
+ provenance: [
1241
+ ...card.provenance,
1242
+ {
1243
+ strategy: "userTagPreference",
1244
+ strategyName: this.strategyName || this.name,
1245
+ strategyId: this.strategyId || this._strategyData._id,
1246
+ action,
1247
+ score: finalScore,
1248
+ reason: this.buildReason(cardTags, prefs.boost, multiplier)
1249
+ }
1250
+ ]
1251
+ };
1252
+ })
1253
+ );
1254
+ return adjusted;
1255
+ }
1256
+ /**
1257
+ * Legacy getWeightedCards - throws as filters should not be used as generators.
1258
+ */
1259
+ async getWeightedCards(_limit) {
1260
+ throw new Error(
1261
+ "UserTagPreferenceFilter is a filter and should not be used as a generator. Use Pipeline with a generator and this filter via transform()."
1262
+ );
1263
+ }
1264
+ // Legacy methods - stub implementations since filters don't generate cards
1265
+ async getNewCards(_n) {
1266
+ return [];
1267
+ }
1268
+ async getPendingReviews() {
1269
+ return [];
1270
+ }
1271
+ };
1272
+ }
1273
+ });
1274
+
1089
1275
  // src/core/navigators/filters/index.ts
1090
1276
  var filters_exports = {};
1091
1277
  __export(filters_exports, {
1278
+ UserTagPreferenceFilter: () => UserTagPreferenceFilter,
1092
1279
  createEloDistanceFilter: () => createEloDistanceFilter
1093
1280
  });
1094
1281
  var init_filters = __esm({
1095
1282
  "src/core/navigators/filters/index.ts"() {
1096
1283
  "use strict";
1097
1284
  init_eloDistance();
1285
+ init_userTagPreference();
1098
1286
  }
1099
1287
  });
1100
1288
 
@@ -1327,10 +1515,9 @@ var init_hierarchyDefinition = __esm({
1327
1515
  /**
1328
1516
  * Check if a card is unlocked and generate reason.
1329
1517
  */
1330
- async checkCardUnlock(cardId, course, unlockedTags, masteredTags) {
1518
+ async checkCardUnlock(card, course, unlockedTags, masteredTags) {
1331
1519
  try {
1332
- const tagResponse = await course.getAppliedTags(cardId);
1333
- const cardTags = tagResponse.rows.map((row) => row.value?.name || row.key);
1520
+ const cardTags = card.tags ?? [];
1334
1521
  const lockedTags = cardTags.filter(
1335
1522
  (tag) => this.hasPrerequisites(tag) && !unlockedTags.has(tag)
1336
1523
  );
@@ -1367,7 +1554,7 @@ var init_hierarchyDefinition = __esm({
1367
1554
  const gated = [];
1368
1555
  for (const card of cards) {
1369
1556
  const { isUnlocked, reason } = await this.checkCardUnlock(
1370
- card.cardId,
1557
+ card,
1371
1558
  context.course,
1372
1559
  unlockedTags,
1373
1560
  masteredTags
@@ -1413,6 +1600,19 @@ var init_hierarchyDefinition = __esm({
1413
1600
  }
1414
1601
  });
1415
1602
 
1603
+ // src/core/navigators/inferredPreference.ts
1604
+ var inferredPreference_exports = {};
1605
+ __export(inferredPreference_exports, {
1606
+ INFERRED_PREFERENCE_NAVIGATOR_STUB: () => INFERRED_PREFERENCE_NAVIGATOR_STUB
1607
+ });
1608
+ var INFERRED_PREFERENCE_NAVIGATOR_STUB;
1609
+ var init_inferredPreference = __esm({
1610
+ "src/core/navigators/inferredPreference.ts"() {
1611
+ "use strict";
1612
+ INFERRED_PREFERENCE_NAVIGATOR_STUB = true;
1613
+ }
1614
+ });
1615
+
1416
1616
  // src/core/navigators/interferenceMitigator.ts
1417
1617
  var interferenceMitigator_exports = {};
1418
1618
  __export(interferenceMitigator_exports, {
@@ -1544,17 +1744,6 @@ var init_interferenceMitigator = __esm({
1544
1744
  }
1545
1745
  return avoid;
1546
1746
  }
1547
- /**
1548
- * Get tags for a single card
1549
- */
1550
- async getCardTags(cardId, course) {
1551
- try {
1552
- const tagResponse = await course.getAppliedTags(cardId);
1553
- return tagResponse.rows.map((row) => row.value?.name || row.key).filter(Boolean);
1554
- } catch {
1555
- return [];
1556
- }
1557
- }
1558
1747
  /**
1559
1748
  * Compute interference score reduction for a card.
1560
1749
  * Returns: { multiplier, interfering tags, reason }
@@ -1606,7 +1795,7 @@ var init_interferenceMitigator = __esm({
1606
1795
  const tagsToAvoid = this.getTagsToAvoid(immatureTags);
1607
1796
  const adjusted = [];
1608
1797
  for (const card of cards) {
1609
- const cardTags = await this.getCardTags(card.cardId, context.course);
1798
+ const cardTags = card.tags ?? [];
1610
1799
  const { multiplier, reason } = this.computeInterferenceEffect(
1611
1800
  cardTags,
1612
1801
  tagsToAvoid,
@@ -1751,27 +1940,16 @@ var init_relativePriority = __esm({
1751
1940
  return `Low-priority tags: ${tagList}${more} (priority ${priority.toFixed(2)} \u2192 reduce ${boostFactor.toFixed(2)}x \u2192 ${finalScore.toFixed(2)})`;
1752
1941
  }
1753
1942
  }
1754
- /**
1755
- * Get tags for a single card.
1756
- */
1757
- async getCardTags(cardId, course) {
1758
- try {
1759
- const tagResponse = await course.getAppliedTags(cardId);
1760
- return tagResponse.rows.map((r) => r.doc?.name).filter((x) => !!x);
1761
- } catch {
1762
- return [];
1763
- }
1764
- }
1765
1943
  /**
1766
1944
  * CardFilter.transform implementation.
1767
1945
  *
1768
1946
  * Apply priority-adjusted scoring. Cards with high-priority tags get boosted,
1769
1947
  * cards with low-priority tags get reduced scores.
1770
1948
  */
1771
- async transform(cards, context) {
1949
+ async transform(cards, _context) {
1772
1950
  const adjusted = await Promise.all(
1773
1951
  cards.map(async (card) => {
1774
- const cardTags = await this.getCardTags(card.cardId, context.course);
1952
+ const cardTags = card.tags ?? [];
1775
1953
  const priority = this.computeCardPriority(cardTags);
1776
1954
  const boostFactor = this.computeBoostFactor(priority);
1777
1955
  const finalScore = Math.max(0, Math.min(1, card.score * boostFactor));
@@ -1938,6 +2116,19 @@ var init_srs = __esm({
1938
2116
  }
1939
2117
  });
1940
2118
 
2119
+ // src/core/navigators/userGoal.ts
2120
+ var userGoal_exports = {};
2121
+ __export(userGoal_exports, {
2122
+ USER_GOAL_NAVIGATOR_STUB: () => USER_GOAL_NAVIGATOR_STUB
2123
+ });
2124
+ var USER_GOAL_NAVIGATOR_STUB;
2125
+ var init_userGoal = __esm({
2126
+ "src/core/navigators/userGoal.ts"() {
2127
+ "use strict";
2128
+ USER_GOAL_NAVIGATOR_STUB = true;
2129
+ }
2130
+ });
2131
+
1941
2132
  // import("./**/*") in src/core/navigators/index.ts
1942
2133
  var globImport;
1943
2134
  var init_ = __esm({
@@ -1950,14 +2141,17 @@ var init_ = __esm({
1950
2141
  "./filters/eloDistance.ts": () => Promise.resolve().then(() => (init_eloDistance(), eloDistance_exports)),
1951
2142
  "./filters/index.ts": () => Promise.resolve().then(() => (init_filters(), filters_exports)),
1952
2143
  "./filters/types.ts": () => Promise.resolve().then(() => (init_types(), types_exports)),
2144
+ "./filters/userTagPreference.ts": () => Promise.resolve().then(() => (init_userTagPreference(), userTagPreference_exports)),
1953
2145
  "./generators/index.ts": () => Promise.resolve().then(() => (init_generators(), generators_exports)),
1954
2146
  "./generators/types.ts": () => Promise.resolve().then(() => (init_types2(), types_exports2)),
1955
2147
  "./hardcodedOrder.ts": () => Promise.resolve().then(() => (init_hardcodedOrder(), hardcodedOrder_exports)),
1956
2148
  "./hierarchyDefinition.ts": () => Promise.resolve().then(() => (init_hierarchyDefinition(), hierarchyDefinition_exports)),
1957
2149
  "./index.ts": () => Promise.resolve().then(() => (init_navigators(), navigators_exports)),
2150
+ "./inferredPreference.ts": () => Promise.resolve().then(() => (init_inferredPreference(), inferredPreference_exports)),
1958
2151
  "./interferenceMitigator.ts": () => Promise.resolve().then(() => (init_interferenceMitigator(), interferenceMitigator_exports)),
1959
2152
  "./relativePriority.ts": () => Promise.resolve().then(() => (init_relativePriority(), relativePriority_exports)),
1960
- "./srs.ts": () => Promise.resolve().then(() => (init_srs(), srs_exports))
2153
+ "./srs.ts": () => Promise.resolve().then(() => (init_srs(), srs_exports)),
2154
+ "./userGoal.ts": () => Promise.resolve().then(() => (init_userGoal(), userGoal_exports))
1961
2155
  });
1962
2156
  }
1963
2157
  });
@@ -2006,6 +2200,7 @@ var init_navigators = __esm({
2006
2200
  Navigators2["HIERARCHY"] = "hierarchyDefinition";
2007
2201
  Navigators2["INTERFERENCE"] = "interferenceMitigator";
2008
2202
  Navigators2["RELATIVE_PRIORITY"] = "relativePriority";
2203
+ Navigators2["USER_TAG_PREFERENCE"] = "userTagPreference";
2009
2204
  return Navigators2;
2010
2205
  })(Navigators || {});
2011
2206
  NavigatorRole = /* @__PURE__ */ ((NavigatorRole2) => {
@@ -2019,7 +2214,8 @@ var init_navigators = __esm({
2019
2214
  ["hardcodedOrder" /* HARDCODED */]: "generator" /* GENERATOR */,
2020
2215
  ["hierarchyDefinition" /* HIERARCHY */]: "filter" /* FILTER */,
2021
2216
  ["interferenceMitigator" /* INTERFERENCE */]: "filter" /* FILTER */,
2022
- ["relativePriority" /* RELATIVE_PRIORITY */]: "filter" /* FILTER */
2217
+ ["relativePriority" /* RELATIVE_PRIORITY */]: "filter" /* FILTER */,
2218
+ ["userTagPreference" /* USER_TAG_PREFERENCE */]: "filter" /* FILTER */
2023
2219
  };
2024
2220
  ContentNavigator = class {
2025
2221
  /** User interface for this navigation session */
@@ -2044,6 +2240,52 @@ var init_navigators = __esm({
2044
2240
  this.strategyId = strategyData._id;
2045
2241
  }
2046
2242
  }
2243
+ // ============================================================================
2244
+ // STRATEGY STATE HELPERS
2245
+ // ============================================================================
2246
+ //
2247
+ // These methods allow strategies to persist their own state (user preferences,
2248
+ // learned patterns, temporal tracking) in the user database.
2249
+ //
2250
+ // ============================================================================
2251
+ /**
2252
+ * Unique key identifying this strategy for state storage.
2253
+ *
2254
+ * Defaults to the constructor name (e.g., "UserTagPreferenceFilter").
2255
+ * Override in subclasses if multiple instances of the same strategy type
2256
+ * need separate state storage.
2257
+ */
2258
+ get strategyKey() {
2259
+ return this.constructor.name;
2260
+ }
2261
+ /**
2262
+ * Get this strategy's persisted state for the current course.
2263
+ *
2264
+ * @returns The strategy's data payload, or null if no state exists
2265
+ * @throws Error if user or course is not initialized
2266
+ */
2267
+ async getStrategyState() {
2268
+ if (!this.user || !this.course) {
2269
+ throw new Error(
2270
+ `Cannot get strategy state: navigator not properly initialized. Ensure user and course are provided to constructor.`
2271
+ );
2272
+ }
2273
+ return this.user.getStrategyState(this.course.getCourseID(), this.strategyKey);
2274
+ }
2275
+ /**
2276
+ * Persist this strategy's state for the current course.
2277
+ *
2278
+ * @param data - The strategy's data payload to store
2279
+ * @throws Error if user or course is not initialized
2280
+ */
2281
+ async putStrategyState(data) {
2282
+ if (!this.user || !this.course) {
2283
+ throw new Error(
2284
+ `Cannot put strategy state: navigator not properly initialized. Ensure user and course are provided to constructor.`
2285
+ );
2286
+ }
2287
+ return this.user.putStrategyState(this.course.getCourseID(), this.strategyKey, data);
2288
+ }
2047
2289
  /**
2048
2290
  * Factory method to create navigator instances dynamically.
2049
2291
  *
@@ -2274,7 +2516,9 @@ import moment6 from "moment";
2274
2516
  function accomodateGuest() {
2275
2517
  logger.log("[funnel] accomodateGuest() called");
2276
2518
  if (typeof localStorage === "undefined") {
2277
- logger.log("[funnel] localStorage not available (Node.js environment), returning default guest");
2519
+ logger.log(
2520
+ "[funnel] localStorage not available (Node.js environment), returning default guest"
2521
+ );
2278
2522
  return {
2279
2523
  username: GuestUsername + "nodejs-test",
2280
2524
  firstVisit: true
@@ -3252,6 +3496,55 @@ Currently logged-in as ${this._username}.`
3252
3496
  async updateUserElo(courseId, elo) {
3253
3497
  return updateUserElo(this._username, courseId, elo);
3254
3498
  }
3499
+ async getStrategyState(courseId, strategyKey) {
3500
+ const docId = buildStrategyStateId(courseId, strategyKey);
3501
+ try {
3502
+ const doc = await this.localDB.get(docId);
3503
+ return doc.data;
3504
+ } catch (e) {
3505
+ const err = e;
3506
+ if (err.status === 404) {
3507
+ return null;
3508
+ }
3509
+ throw e;
3510
+ }
3511
+ }
3512
+ async putStrategyState(courseId, strategyKey, data) {
3513
+ const docId = buildStrategyStateId(courseId, strategyKey);
3514
+ let existingRev;
3515
+ try {
3516
+ const existing = await this.localDB.get(docId);
3517
+ existingRev = existing._rev;
3518
+ } catch (e) {
3519
+ const err = e;
3520
+ if (err.status !== 404) {
3521
+ throw e;
3522
+ }
3523
+ }
3524
+ const doc = {
3525
+ _id: docId,
3526
+ _rev: existingRev,
3527
+ docType: "STRATEGY_STATE" /* STRATEGY_STATE */,
3528
+ courseId,
3529
+ strategyKey,
3530
+ data,
3531
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
3532
+ };
3533
+ await this.localDB.put(doc);
3534
+ }
3535
+ async deleteStrategyState(courseId, strategyKey) {
3536
+ const docId = buildStrategyStateId(courseId, strategyKey);
3537
+ try {
3538
+ const doc = await this.localDB.get(docId);
3539
+ await this.localDB.remove(doc);
3540
+ } catch (e) {
3541
+ const err = e;
3542
+ if (err.status === 404) {
3543
+ return;
3544
+ }
3545
+ throw e;
3546
+ }
3547
+ }
3255
3548
  };
3256
3549
  userCoursesDoc = "CourseRegistrations";
3257
3550
  userClassroomsDoc = "ClassroomRegistrations";
@@ -3346,6 +3639,16 @@ var init_user = __esm({
3346
3639
  }
3347
3640
  });
3348
3641
 
3642
+ // src/core/types/strategyState.ts
3643
+ function buildStrategyStateId(courseId, strategyKey) {
3644
+ return `STRATEGY_STATE::${courseId}::${strategyKey}`;
3645
+ }
3646
+ var init_strategyState = __esm({
3647
+ "src/core/types/strategyState.ts"() {
3648
+ "use strict";
3649
+ }
3650
+ });
3651
+
3349
3652
  // src/core/bulkImport/cardProcessor.ts
3350
3653
  import { Status as Status4 } from "@vue-skuilder/common";
3351
3654
  var init_cardProcessor = __esm({
@@ -3378,6 +3681,7 @@ var init_core = __esm({
3378
3681
  init_interfaces();
3379
3682
  init_types_legacy();
3380
3683
  init_user();
3684
+ init_strategyState();
3381
3685
  init_Loggable();
3382
3686
  init_util();
3383
3687
  init_navigators();
@@ -3950,6 +4254,14 @@ var init_courseDB3 = __esm({
3950
4254
  };
3951
4255
  }
3952
4256
  }
4257
+ async getAppliedTagsBatch(cardIds) {
4258
+ const tagsIndex = await this.unpacker.getTagsIndex();
4259
+ const tagsByCard = /* @__PURE__ */ new Map();
4260
+ for (const cardId of cardIds) {
4261
+ tagsByCard.set(cardId, tagsIndex.byCard[cardId] || []);
4262
+ }
4263
+ return tagsByCard;
4264
+ }
3953
4265
  async addTagToCard(_cardId, _tagId) {
3954
4266
  throw new Error("Cannot modify tags in static mode");
3955
4267
  }