@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
@@ -100,6 +100,7 @@ var init_types_legacy = __esm({
100
100
  DocType2["SCHEDULED_CARD"] = "SCHEDULED_CARD";
101
101
  DocType2["TAG"] = "TAG";
102
102
  DocType2["NAVIGATION_STRATEGY"] = "NAVIGATION_STRATEGY";
103
+ DocType2["STRATEGY_STATE"] = "STRATEGY_STATE";
103
104
  return DocType2;
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
  });
@@ -876,6 +878,41 @@ __export(Pipeline_exports, {
876
878
  Pipeline: () => Pipeline
877
879
  });
878
880
  import { toCourseElo as toCourseElo2 } from "@vue-skuilder/common";
881
+ function logPipelineConfig(generator, filters) {
882
+ const filterList = filters.length > 0 ? "\n - " + filters.map((f) => f.name).join("\n - ") : " none";
883
+ logger.info(
884
+ `[Pipeline] Configuration:
885
+ Generator: ${generator.name}
886
+ Filters:${filterList}`
887
+ );
888
+ }
889
+ function logTagHydration(cards, tagsByCard) {
890
+ const totalTags = Array.from(tagsByCard.values()).reduce((sum, tags) => sum + tags.length, 0);
891
+ const cardsWithTags = Array.from(tagsByCard.values()).filter((tags) => tags.length > 0).length;
892
+ logger.debug(
893
+ `[Pipeline] Tag hydration: ${cards.length} cards, ${cardsWithTags} have tags (${totalTags} total tags) - single batch query`
894
+ );
895
+ }
896
+ function logExecutionSummary(generatorName, generatedCount, filterCount, finalCount, topScores) {
897
+ const scoreDisplay = topScores.length > 0 ? topScores.map((s) => s.toFixed(2)).join(", ") : "none";
898
+ logger.info(
899
+ `[Pipeline] Execution: ${generatorName} produced ${generatedCount} \u2192 ${filterCount} filters \u2192 ${finalCount} results (top scores: ${scoreDisplay})`
900
+ );
901
+ }
902
+ function logCardProvenance(cards, maxCards = 3) {
903
+ const cardsToLog = cards.slice(0, maxCards);
904
+ logger.debug(`[Pipeline] Provenance for top ${cardsToLog.length} cards:`);
905
+ for (const card of cardsToLog) {
906
+ logger.debug(`[Pipeline] ${card.cardId} (final score: ${card.score.toFixed(3)}):`);
907
+ for (const entry of card.provenance) {
908
+ const scoreChange = entry.score.toFixed(3);
909
+ const action = entry.action.padEnd(9);
910
+ logger.debug(
911
+ `[Pipeline] ${action} ${scoreChange} - ${entry.strategyName}: ${entry.reason}`
912
+ );
913
+ }
914
+ }
915
+ }
879
916
  var Pipeline;
880
917
  var init_Pipeline = __esm({
881
918
  "src/core/navigators/Pipeline.ts"() {
@@ -899,19 +936,18 @@ var init_Pipeline = __esm({
899
936
  this.filters = filters;
900
937
  this.user = user;
901
938
  this.course = course;
902
- logger.debug(
903
- `[Pipeline] Created with generator '${generator.name}' and ${filters.length} filters: ${filters.map((f) => f.name).join(", ")}`
904
- );
939
+ logPipelineConfig(generator, filters);
905
940
  }
906
941
  /**
907
942
  * Get weighted cards by running generator and applying filters.
908
943
  *
909
944
  * 1. Build shared context (user ELO, etc.)
910
945
  * 2. Get candidates from generator (passing context)
911
- * 3. Apply each filter sequentially
912
- * 4. Remove zero-score cards
913
- * 5. Sort by score descending
914
- * 6. Return top N
946
+ * 3. Batch hydrate tags for all candidates
947
+ * 4. Apply each filter sequentially
948
+ * 5. Remove zero-score cards
949
+ * 6. Sort by score descending
950
+ * 7. Return top N
915
951
  *
916
952
  * @param limit - Maximum number of cards to return
917
953
  * @returns Cards sorted by score descending
@@ -924,7 +960,9 @@ var init_Pipeline = __esm({
924
960
  `[Pipeline] Fetching ${fetchLimit} candidates from generator '${this.generator.name}'`
925
961
  );
926
962
  let cards = await this.generator.getWeightedCards(fetchLimit, context);
927
- logger.debug(`[Pipeline] Generator returned ${cards.length} candidates`);
963
+ const generatedCount = cards.length;
964
+ logger.debug(`[Pipeline] Generator returned ${generatedCount} candidates`);
965
+ cards = await this.hydrateTags(cards);
928
966
  for (const filter of this.filters) {
929
967
  const beforeCount = cards.length;
930
968
  cards = await filter.transform(cards, context);
@@ -933,11 +971,33 @@ var init_Pipeline = __esm({
933
971
  cards = cards.filter((c) => c.score > 0);
934
972
  cards.sort((a, b) => b.score - a.score);
935
973
  const result = cards.slice(0, limit);
936
- logger.debug(
937
- `[Pipeline] Returning ${result.length} cards (top scores: ${result.slice(0, 3).map((c) => c.score.toFixed(2)).join(", ")}...)`
938
- );
974
+ const topScores = result.slice(0, 3).map((c) => c.score);
975
+ logExecutionSummary(this.generator.name, generatedCount, this.filters.length, result.length, topScores);
976
+ logCardProvenance(result, 3);
939
977
  return result;
940
978
  }
979
+ /**
980
+ * Batch hydrate tags for all cards.
981
+ *
982
+ * Fetches tags for all cards in a single database query and attaches them
983
+ * to the WeightedCard objects. Filters can then use card.tags instead of
984
+ * making individual getAppliedTags() calls.
985
+ *
986
+ * @param cards - Cards to hydrate
987
+ * @returns Cards with tags populated
988
+ */
989
+ async hydrateTags(cards) {
990
+ if (cards.length === 0) {
991
+ return cards;
992
+ }
993
+ const cardIds = cards.map((c) => c.cardId);
994
+ const tagsByCard = await this.course.getAppliedTagsBatch(cardIds);
995
+ logTagHydration(cards, tagsByCard);
996
+ return cards.map((card) => ({
997
+ ...card,
998
+ tags: tagsByCard.get(card.cardId) ?? []
999
+ }));
1000
+ }
941
1001
  /**
942
1002
  * Build shared context for generator and filters.
943
1003
  *
@@ -1297,15 +1357,144 @@ var init_eloDistance = __esm({
1297
1357
  }
1298
1358
  });
1299
1359
 
1360
+ // src/core/navigators/filters/userTagPreference.ts
1361
+ var userTagPreference_exports = {};
1362
+ __export(userTagPreference_exports, {
1363
+ default: () => UserTagPreferenceFilter
1364
+ });
1365
+ var UserTagPreferenceFilter;
1366
+ var init_userTagPreference = __esm({
1367
+ "src/core/navigators/filters/userTagPreference.ts"() {
1368
+ "use strict";
1369
+ init_navigators();
1370
+ UserTagPreferenceFilter = class extends ContentNavigator {
1371
+ _strategyData;
1372
+ /** Human-readable name for CardFilter interface */
1373
+ name;
1374
+ constructor(user, course, strategyData) {
1375
+ super(user, course, strategyData);
1376
+ this._strategyData = strategyData;
1377
+ this.name = strategyData.name || "User Tag Preferences";
1378
+ }
1379
+ /**
1380
+ * Compute multiplier for a card based on its tags and user preferences.
1381
+ * Returns the maximum multiplier among all matching tags, or 1.0 if no matches.
1382
+ */
1383
+ computeMultiplier(cardTags, boostMap) {
1384
+ const multipliers = cardTags.map((tag) => boostMap[tag]).filter((val) => val !== void 0);
1385
+ if (multipliers.length === 0) {
1386
+ return 1;
1387
+ }
1388
+ return Math.max(...multipliers);
1389
+ }
1390
+ /**
1391
+ * Build human-readable reason for the filter's decision.
1392
+ */
1393
+ buildReason(cardTags, boostMap, multiplier) {
1394
+ const matchingTags = cardTags.filter((tag) => boostMap[tag] === multiplier);
1395
+ if (multiplier === 0) {
1396
+ return `Excluded by user preference: ${matchingTags.join(", ")} (${multiplier}x)`;
1397
+ }
1398
+ if (multiplier < 1) {
1399
+ return `Penalized by user preference: ${matchingTags.join(", ")} (${multiplier.toFixed(2)}x)`;
1400
+ }
1401
+ if (multiplier > 1) {
1402
+ return `Boosted by user preference: ${matchingTags.join(", ")} (${multiplier.toFixed(2)}x)`;
1403
+ }
1404
+ return "No matching user preferences";
1405
+ }
1406
+ /**
1407
+ * CardFilter.transform implementation.
1408
+ *
1409
+ * Apply user tag preferences:
1410
+ * 1. Read preferences from strategy state
1411
+ * 2. If no preferences, pass through unchanged
1412
+ * 3. For each card:
1413
+ * - Look up tag in boost record
1414
+ * - If tag found: apply multiplier (0 = exclude, 1 = neutral, >1 = boost)
1415
+ * - If multiple tags match: use max multiplier
1416
+ * - Append provenance with clear reason
1417
+ */
1418
+ async transform(cards, _context) {
1419
+ const prefs = await this.getStrategyState();
1420
+ if (!prefs || Object.keys(prefs.boost).length === 0) {
1421
+ return cards.map((card) => ({
1422
+ ...card,
1423
+ provenance: [
1424
+ ...card.provenance,
1425
+ {
1426
+ strategy: "userTagPreference",
1427
+ strategyName: this.strategyName || this.name,
1428
+ strategyId: this.strategyId || this._strategyData._id,
1429
+ action: "passed",
1430
+ score: card.score,
1431
+ reason: "No user tag preferences configured"
1432
+ }
1433
+ ]
1434
+ }));
1435
+ }
1436
+ const adjusted = await Promise.all(
1437
+ cards.map(async (card) => {
1438
+ const cardTags = card.tags ?? [];
1439
+ const multiplier = this.computeMultiplier(cardTags, prefs.boost);
1440
+ const finalScore = Math.min(1, card.score * multiplier);
1441
+ let action;
1442
+ if (multiplier === 0 || multiplier < 1) {
1443
+ action = "penalized";
1444
+ } else if (multiplier > 1) {
1445
+ action = "boosted";
1446
+ } else {
1447
+ action = "passed";
1448
+ }
1449
+ return {
1450
+ ...card,
1451
+ score: finalScore,
1452
+ provenance: [
1453
+ ...card.provenance,
1454
+ {
1455
+ strategy: "userTagPreference",
1456
+ strategyName: this.strategyName || this.name,
1457
+ strategyId: this.strategyId || this._strategyData._id,
1458
+ action,
1459
+ score: finalScore,
1460
+ reason: this.buildReason(cardTags, prefs.boost, multiplier)
1461
+ }
1462
+ ]
1463
+ };
1464
+ })
1465
+ );
1466
+ return adjusted;
1467
+ }
1468
+ /**
1469
+ * Legacy getWeightedCards - throws as filters should not be used as generators.
1470
+ */
1471
+ async getWeightedCards(_limit) {
1472
+ throw new Error(
1473
+ "UserTagPreferenceFilter is a filter and should not be used as a generator. Use Pipeline with a generator and this filter via transform()."
1474
+ );
1475
+ }
1476
+ // Legacy methods - stub implementations since filters don't generate cards
1477
+ async getNewCards(_n) {
1478
+ return [];
1479
+ }
1480
+ async getPendingReviews() {
1481
+ return [];
1482
+ }
1483
+ };
1484
+ }
1485
+ });
1486
+
1300
1487
  // src/core/navigators/filters/index.ts
1301
1488
  var filters_exports = {};
1302
1489
  __export(filters_exports, {
1490
+ UserTagPreferenceFilter: () => UserTagPreferenceFilter,
1303
1491
  createEloDistanceFilter: () => createEloDistanceFilter
1304
1492
  });
1305
1493
  var init_filters = __esm({
1306
1494
  "src/core/navigators/filters/index.ts"() {
1307
1495
  "use strict";
1308
1496
  init_eloDistance();
1497
+ init_userTagPreference();
1309
1498
  }
1310
1499
  });
1311
1500
 
@@ -1538,10 +1727,9 @@ var init_hierarchyDefinition = __esm({
1538
1727
  /**
1539
1728
  * Check if a card is unlocked and generate reason.
1540
1729
  */
1541
- async checkCardUnlock(cardId, course, unlockedTags, masteredTags) {
1730
+ async checkCardUnlock(card, course, unlockedTags, masteredTags) {
1542
1731
  try {
1543
- const tagResponse = await course.getAppliedTags(cardId);
1544
- const cardTags = tagResponse.rows.map((row) => row.value?.name || row.key);
1732
+ const cardTags = card.tags ?? [];
1545
1733
  const lockedTags = cardTags.filter(
1546
1734
  (tag) => this.hasPrerequisites(tag) && !unlockedTags.has(tag)
1547
1735
  );
@@ -1578,7 +1766,7 @@ var init_hierarchyDefinition = __esm({
1578
1766
  const gated = [];
1579
1767
  for (const card of cards) {
1580
1768
  const { isUnlocked, reason } = await this.checkCardUnlock(
1581
- card.cardId,
1769
+ card,
1582
1770
  context.course,
1583
1771
  unlockedTags,
1584
1772
  masteredTags
@@ -1624,6 +1812,19 @@ var init_hierarchyDefinition = __esm({
1624
1812
  }
1625
1813
  });
1626
1814
 
1815
+ // src/core/navigators/inferredPreference.ts
1816
+ var inferredPreference_exports = {};
1817
+ __export(inferredPreference_exports, {
1818
+ INFERRED_PREFERENCE_NAVIGATOR_STUB: () => INFERRED_PREFERENCE_NAVIGATOR_STUB
1819
+ });
1820
+ var INFERRED_PREFERENCE_NAVIGATOR_STUB;
1821
+ var init_inferredPreference = __esm({
1822
+ "src/core/navigators/inferredPreference.ts"() {
1823
+ "use strict";
1824
+ INFERRED_PREFERENCE_NAVIGATOR_STUB = true;
1825
+ }
1826
+ });
1827
+
1627
1828
  // src/core/navigators/interferenceMitigator.ts
1628
1829
  var interferenceMitigator_exports = {};
1629
1830
  __export(interferenceMitigator_exports, {
@@ -1755,17 +1956,6 @@ var init_interferenceMitigator = __esm({
1755
1956
  }
1756
1957
  return avoid;
1757
1958
  }
1758
- /**
1759
- * Get tags for a single card
1760
- */
1761
- async getCardTags(cardId, course) {
1762
- try {
1763
- const tagResponse = await course.getAppliedTags(cardId);
1764
- return tagResponse.rows.map((row) => row.value?.name || row.key).filter(Boolean);
1765
- } catch {
1766
- return [];
1767
- }
1768
- }
1769
1959
  /**
1770
1960
  * Compute interference score reduction for a card.
1771
1961
  * Returns: { multiplier, interfering tags, reason }
@@ -1817,7 +2007,7 @@ var init_interferenceMitigator = __esm({
1817
2007
  const tagsToAvoid = this.getTagsToAvoid(immatureTags);
1818
2008
  const adjusted = [];
1819
2009
  for (const card of cards) {
1820
- const cardTags = await this.getCardTags(card.cardId, context.course);
2010
+ const cardTags = card.tags ?? [];
1821
2011
  const { multiplier, reason } = this.computeInterferenceEffect(
1822
2012
  cardTags,
1823
2013
  tagsToAvoid,
@@ -1962,27 +2152,16 @@ var init_relativePriority = __esm({
1962
2152
  return `Low-priority tags: ${tagList}${more} (priority ${priority.toFixed(2)} \u2192 reduce ${boostFactor.toFixed(2)}x \u2192 ${finalScore.toFixed(2)})`;
1963
2153
  }
1964
2154
  }
1965
- /**
1966
- * Get tags for a single card.
1967
- */
1968
- async getCardTags(cardId, course) {
1969
- try {
1970
- const tagResponse = await course.getAppliedTags(cardId);
1971
- return tagResponse.rows.map((r) => r.doc?.name).filter((x) => !!x);
1972
- } catch {
1973
- return [];
1974
- }
1975
- }
1976
2155
  /**
1977
2156
  * CardFilter.transform implementation.
1978
2157
  *
1979
2158
  * Apply priority-adjusted scoring. Cards with high-priority tags get boosted,
1980
2159
  * cards with low-priority tags get reduced scores.
1981
2160
  */
1982
- async transform(cards, context) {
2161
+ async transform(cards, _context) {
1983
2162
  const adjusted = await Promise.all(
1984
2163
  cards.map(async (card) => {
1985
- const cardTags = await this.getCardTags(card.cardId, context.course);
2164
+ const cardTags = card.tags ?? [];
1986
2165
  const priority = this.computeCardPriority(cardTags);
1987
2166
  const boostFactor = this.computeBoostFactor(priority);
1988
2167
  const finalScore = Math.max(0, Math.min(1, card.score * boostFactor));
@@ -2149,6 +2328,19 @@ var init_srs = __esm({
2149
2328
  }
2150
2329
  });
2151
2330
 
2331
+ // src/core/navigators/userGoal.ts
2332
+ var userGoal_exports = {};
2333
+ __export(userGoal_exports, {
2334
+ USER_GOAL_NAVIGATOR_STUB: () => USER_GOAL_NAVIGATOR_STUB
2335
+ });
2336
+ var USER_GOAL_NAVIGATOR_STUB;
2337
+ var init_userGoal = __esm({
2338
+ "src/core/navigators/userGoal.ts"() {
2339
+ "use strict";
2340
+ USER_GOAL_NAVIGATOR_STUB = true;
2341
+ }
2342
+ });
2343
+
2152
2344
  // import("./**/*") in src/core/navigators/index.ts
2153
2345
  var globImport;
2154
2346
  var init_ = __esm({
@@ -2161,14 +2353,17 @@ var init_ = __esm({
2161
2353
  "./filters/eloDistance.ts": () => Promise.resolve().then(() => (init_eloDistance(), eloDistance_exports)),
2162
2354
  "./filters/index.ts": () => Promise.resolve().then(() => (init_filters(), filters_exports)),
2163
2355
  "./filters/types.ts": () => Promise.resolve().then(() => (init_types(), types_exports)),
2356
+ "./filters/userTagPreference.ts": () => Promise.resolve().then(() => (init_userTagPreference(), userTagPreference_exports)),
2164
2357
  "./generators/index.ts": () => Promise.resolve().then(() => (init_generators(), generators_exports)),
2165
2358
  "./generators/types.ts": () => Promise.resolve().then(() => (init_types2(), types_exports2)),
2166
2359
  "./hardcodedOrder.ts": () => Promise.resolve().then(() => (init_hardcodedOrder(), hardcodedOrder_exports)),
2167
2360
  "./hierarchyDefinition.ts": () => Promise.resolve().then(() => (init_hierarchyDefinition(), hierarchyDefinition_exports)),
2168
2361
  "./index.ts": () => Promise.resolve().then(() => (init_navigators(), navigators_exports)),
2362
+ "./inferredPreference.ts": () => Promise.resolve().then(() => (init_inferredPreference(), inferredPreference_exports)),
2169
2363
  "./interferenceMitigator.ts": () => Promise.resolve().then(() => (init_interferenceMitigator(), interferenceMitigator_exports)),
2170
2364
  "./relativePriority.ts": () => Promise.resolve().then(() => (init_relativePriority(), relativePriority_exports)),
2171
- "./srs.ts": () => Promise.resolve().then(() => (init_srs(), srs_exports))
2365
+ "./srs.ts": () => Promise.resolve().then(() => (init_srs(), srs_exports)),
2366
+ "./userGoal.ts": () => Promise.resolve().then(() => (init_userGoal(), userGoal_exports))
2172
2367
  });
2173
2368
  }
2174
2369
  });
@@ -2217,6 +2412,7 @@ var init_navigators = __esm({
2217
2412
  Navigators2["HIERARCHY"] = "hierarchyDefinition";
2218
2413
  Navigators2["INTERFERENCE"] = "interferenceMitigator";
2219
2414
  Navigators2["RELATIVE_PRIORITY"] = "relativePriority";
2415
+ Navigators2["USER_TAG_PREFERENCE"] = "userTagPreference";
2220
2416
  return Navigators2;
2221
2417
  })(Navigators || {});
2222
2418
  NavigatorRole = /* @__PURE__ */ ((NavigatorRole2) => {
@@ -2230,7 +2426,8 @@ var init_navigators = __esm({
2230
2426
  ["hardcodedOrder" /* HARDCODED */]: "generator" /* GENERATOR */,
2231
2427
  ["hierarchyDefinition" /* HIERARCHY */]: "filter" /* FILTER */,
2232
2428
  ["interferenceMitigator" /* INTERFERENCE */]: "filter" /* FILTER */,
2233
- ["relativePriority" /* RELATIVE_PRIORITY */]: "filter" /* FILTER */
2429
+ ["relativePriority" /* RELATIVE_PRIORITY */]: "filter" /* FILTER */,
2430
+ ["userTagPreference" /* USER_TAG_PREFERENCE */]: "filter" /* FILTER */
2234
2431
  };
2235
2432
  ContentNavigator = class {
2236
2433
  /** User interface for this navigation session */
@@ -2255,6 +2452,52 @@ var init_navigators = __esm({
2255
2452
  this.strategyId = strategyData._id;
2256
2453
  }
2257
2454
  }
2455
+ // ============================================================================
2456
+ // STRATEGY STATE HELPERS
2457
+ // ============================================================================
2458
+ //
2459
+ // These methods allow strategies to persist their own state (user preferences,
2460
+ // learned patterns, temporal tracking) in the user database.
2461
+ //
2462
+ // ============================================================================
2463
+ /**
2464
+ * Unique key identifying this strategy for state storage.
2465
+ *
2466
+ * Defaults to the constructor name (e.g., "UserTagPreferenceFilter").
2467
+ * Override in subclasses if multiple instances of the same strategy type
2468
+ * need separate state storage.
2469
+ */
2470
+ get strategyKey() {
2471
+ return this.constructor.name;
2472
+ }
2473
+ /**
2474
+ * Get this strategy's persisted state for the current course.
2475
+ *
2476
+ * @returns The strategy's data payload, or null if no state exists
2477
+ * @throws Error if user or course is not initialized
2478
+ */
2479
+ async getStrategyState() {
2480
+ if (!this.user || !this.course) {
2481
+ throw new Error(
2482
+ `Cannot get strategy state: navigator not properly initialized. Ensure user and course are provided to constructor.`
2483
+ );
2484
+ }
2485
+ return this.user.getStrategyState(this.course.getCourseID(), this.strategyKey);
2486
+ }
2487
+ /**
2488
+ * Persist this strategy's state for the current course.
2489
+ *
2490
+ * @param data - The strategy's data payload to store
2491
+ * @throws Error if user or course is not initialized
2492
+ */
2493
+ async putStrategyState(data) {
2494
+ if (!this.user || !this.course) {
2495
+ throw new Error(
2496
+ `Cannot put strategy state: navigator not properly initialized. Ensure user and course are provided to constructor.`
2497
+ );
2498
+ }
2499
+ return this.user.putStrategyState(this.course.getCourseID(), this.strategyKey, data);
2500
+ }
2258
2501
  /**
2259
2502
  * Factory method to create navigator instances dynamically.
2260
2503
  *
@@ -2584,15 +2827,6 @@ var init_courseDB = __esm({
2584
2827
  ret[r.id] = r.doc.id_displayable_data;
2585
2828
  }
2586
2829
  });
2587
- await Promise.all(
2588
- cards.rows.map((r) => {
2589
- return async () => {
2590
- if (isSuccessRow(r)) {
2591
- ret[r.id] = r.doc.id_displayable_data;
2592
- }
2593
- };
2594
- })
2595
- );
2596
2830
  return ret;
2597
2831
  }
2598
2832
  async getCardsByELO(elo, cardLimit) {
@@ -2677,6 +2911,28 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
2677
2911
  throw new Error(`Failed to find tags for card ${this.id}-${cardId}`);
2678
2912
  }
2679
2913
  }
2914
+ async getAppliedTagsBatch(cardIds) {
2915
+ if (cardIds.length === 0) {
2916
+ return /* @__PURE__ */ new Map();
2917
+ }
2918
+ const db = getCourseDB2(this.id);
2919
+ const result = await db.query("getTags", {
2920
+ keys: cardIds,
2921
+ include_docs: false
2922
+ });
2923
+ const tagsByCard = /* @__PURE__ */ new Map();
2924
+ for (const cardId of cardIds) {
2925
+ tagsByCard.set(cardId, []);
2926
+ }
2927
+ for (const row of result.rows) {
2928
+ const cardId = row.key;
2929
+ const tagName = row.value?.name;
2930
+ if (tagName && tagsByCard.has(cardId)) {
2931
+ tagsByCard.get(cardId).push(tagName);
2932
+ }
2933
+ }
2934
+ return tagsByCard;
2935
+ }
2680
2936
  async addTagToCard(cardId, tagId, updateELO) {
2681
2937
  return await addTagToCard(
2682
2938
  this.id,
@@ -4263,6 +4519,55 @@ Currently logged-in as ${this._username}.`
4263
4519
  async updateUserElo(courseId, elo) {
4264
4520
  return updateUserElo(this._username, courseId, elo);
4265
4521
  }
4522
+ async getStrategyState(courseId, strategyKey) {
4523
+ const docId = buildStrategyStateId(courseId, strategyKey);
4524
+ try {
4525
+ const doc = await this.localDB.get(docId);
4526
+ return doc.data;
4527
+ } catch (e) {
4528
+ const err = e;
4529
+ if (err.status === 404) {
4530
+ return null;
4531
+ }
4532
+ throw e;
4533
+ }
4534
+ }
4535
+ async putStrategyState(courseId, strategyKey, data) {
4536
+ const docId = buildStrategyStateId(courseId, strategyKey);
4537
+ let existingRev;
4538
+ try {
4539
+ const existing = await this.localDB.get(docId);
4540
+ existingRev = existing._rev;
4541
+ } catch (e) {
4542
+ const err = e;
4543
+ if (err.status !== 404) {
4544
+ throw e;
4545
+ }
4546
+ }
4547
+ const doc = {
4548
+ _id: docId,
4549
+ _rev: existingRev,
4550
+ docType: "STRATEGY_STATE" /* STRATEGY_STATE */,
4551
+ courseId,
4552
+ strategyKey,
4553
+ data,
4554
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
4555
+ };
4556
+ await this.localDB.put(doc);
4557
+ }
4558
+ async deleteStrategyState(courseId, strategyKey) {
4559
+ const docId = buildStrategyStateId(courseId, strategyKey);
4560
+ try {
4561
+ const doc = await this.localDB.get(docId);
4562
+ await this.localDB.remove(doc);
4563
+ } catch (e) {
4564
+ const err = e;
4565
+ if (err.status === 404) {
4566
+ return;
4567
+ }
4568
+ throw e;
4569
+ }
4570
+ }
4266
4571
  };
4267
4572
  userCoursesDoc = "CourseRegistrations";
4268
4573
  userClassroomsDoc = "ClassroomRegistrations";
@@ -4575,6 +4880,16 @@ var init_user = __esm({
4575
4880
  }
4576
4881
  });
4577
4882
 
4883
+ // src/core/types/strategyState.ts
4884
+ function buildStrategyStateId(courseId, strategyKey) {
4885
+ return `STRATEGY_STATE::${courseId}::${strategyKey}`;
4886
+ }
4887
+ var init_strategyState = __esm({
4888
+ "src/core/types/strategyState.ts"() {
4889
+ "use strict";
4890
+ }
4891
+ });
4892
+
4578
4893
  // src/core/bulkImport/cardProcessor.ts
4579
4894
  import { Status as Status4 } from "@vue-skuilder/common";
4580
4895
  async function importParsedCards(parsedCards, courseDB, config) {
@@ -4717,6 +5032,7 @@ var init_core = __esm({
4717
5032
  init_interfaces();
4718
5033
  init_types_legacy();
4719
5034
  init_user();
5035
+ init_strategyState();
4720
5036
  init_Loggable();
4721
5037
  init_util();
4722
5038
  init_navigators();
@@ -4734,6 +5050,7 @@ export {
4734
5050
  NavigatorRoles,
4735
5051
  Navigators,
4736
5052
  areQuestionRecords,
5053
+ buildStrategyStateId,
4737
5054
  docIsDeleted,
4738
5055
  getCardHistoryID,
4739
5056
  getCardOrigin,