@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
@@ -1,8 +1,8 @@
1
- import { U as UserDBInterface, C as CourseDBInterface, b as CoursesDBInterface, c as ClassroomDBInterface, A as AdminDBInterface, a as UserDBReader, d as CourseInfo, S as StudySessionNewItem, D as DataLayerResult, e as ContentNavigationStrategyData, f as StudySessionReviewItem, g as ScheduledCard } from '../../classroomDB-CTOenngH.cjs';
2
- import { D as DataLayerProvider } from '../../dataLayerProvider-D6PoCwS6.cjs';
3
- import { S as StaticCourseManifest } from '../../types-CzPDLAK6.cjs';
1
+ import { U as UserDBInterface, C as CourseDBInterface, b as CoursesDBInterface, c as ClassroomDBInterface, A as AdminDBInterface, a as UserDBReader, d as CourseInfo, S as StudySessionNewItem, D as DataLayerResult, e as ContentNavigationStrategyData, f as StudySessionReviewItem, g as ScheduledCard } from '../../classroomDB-PxDZTky3.cjs';
2
+ import { D as DataLayerProvider } from '../../dataLayerProvider-D0MoZMjH.cjs';
3
+ import { S as StaticCourseManifest } from '../../types-Bn0itutr.cjs';
4
4
  import { CourseConfig, CourseElo, DataShape } from '@vue-skuilder/common';
5
- import { S as SkuilderCourseData, Q as QualifiedCardID, T as TagStub, a as Tag } from '../../types-legacy-6ettoclI.cjs';
5
+ import { S as SkuilderCourseData, Q as QualifiedCardID, T as TagStub, a as Tag } from '../../types-legacy-DDY4N-Uq.cjs';
6
6
  import { S as SyncStrategy, A as AccountCreationResult, a as AuthenticationResult } from '../../SyncStrategy-CyATpyLQ.cjs';
7
7
  import 'moment';
8
8
 
@@ -141,6 +141,7 @@ declare class StaticCourseDB implements CourseDBInterface {
141
141
  elo: 'user' | 'random' | number;
142
142
  }, filter?: (id: QualifiedCardID) => boolean): Promise<StudySessionNewItem[]>;
143
143
  getAppliedTags(cardId: string): Promise<PouchDB.Query.Response<TagStub>>;
144
+ getAppliedTagsBatch(cardIds: string[]): Promise<Map<string, string[]>>;
144
145
  addTagToCard(_cardId: string, _tagId: string): Promise<PouchDB.Core.Response>;
145
146
  removeTagFromCard(_cardId: string, _tagId: string): Promise<PouchDB.Core.Response>;
146
147
  createTag(_tagName: string): Promise<PouchDB.Core.Response>;
@@ -1,8 +1,8 @@
1
- import { U as UserDBInterface, C as CourseDBInterface, b as CoursesDBInterface, c as ClassroomDBInterface, A as AdminDBInterface, a as UserDBReader, d as CourseInfo, S as StudySessionNewItem, D as DataLayerResult, e as ContentNavigationStrategyData, f as StudySessionReviewItem, g as ScheduledCard } from '../../classroomDB-BgfrVb8d.js';
2
- import { D as DataLayerProvider } from '../../dataLayerProvider-CZxC9GtB.js';
3
- import { S as StaticCourseManifest } from '../../types-CewsN87z.js';
1
+ import { U as UserDBInterface, C as CourseDBInterface, b as CoursesDBInterface, c as ClassroomDBInterface, A as AdminDBInterface, a as UserDBReader, d as CourseInfo, S as StudySessionNewItem, D as DataLayerResult, e as ContentNavigationStrategyData, f as StudySessionReviewItem, g as ScheduledCard } from '../../classroomDB-CZdMBiTU.js';
2
+ import { D as DataLayerProvider } from '../../dataLayerProvider-D8o6ZnKW.js';
3
+ import { S as StaticCourseManifest } from '../../types-DQaXnuoc.js';
4
4
  import { CourseConfig, CourseElo, DataShape } from '@vue-skuilder/common';
5
- import { S as SkuilderCourseData, Q as QualifiedCardID, T as TagStub, a as Tag } from '../../types-legacy-6ettoclI.js';
5
+ import { S as SkuilderCourseData, Q as QualifiedCardID, T as TagStub, a as Tag } from '../../types-legacy-DDY4N-Uq.js';
6
6
  import { S as SyncStrategy, A as AccountCreationResult, a as AuthenticationResult } from '../../SyncStrategy-CyATpyLQ.js';
7
7
  import 'moment';
8
8
 
@@ -141,6 +141,7 @@ declare class StaticCourseDB implements CourseDBInterface {
141
141
  elo: 'user' | 'random' | number;
142
142
  }, filter?: (id: QualifiedCardID) => boolean): Promise<StudySessionNewItem[]>;
143
143
  getAppliedTags(cardId: string): Promise<PouchDB.Query.Response<TagStub>>;
144
+ getAppliedTagsBatch(cardIds: string[]): Promise<Map<string, string[]>>;
144
145
  addTagToCard(_cardId: string, _tagId: string): Promise<PouchDB.Core.Response>;
145
146
  removeTagFromCard(_cardId: string, _tagId: string): Promise<PouchDB.Core.Response>;
146
147
  createTag(_tagName: string): Promise<PouchDB.Core.Response>;
@@ -119,7 +119,8 @@ var init_types_legacy = __esm({
119
119
  ["QUESTION" /* QUESTIONTYPE */]: "QUESTION",
120
120
  ["VIEW" /* VIEW */]: "VIEW",
121
121
  ["PEDAGOGY" /* PEDAGOGY */]: "PEDAGOGY",
122
- ["NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */]: "NAVIGATION_STRATEGY"
122
+ ["NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */]: "NAVIGATION_STRATEGY",
123
+ ["STRATEGY_STATE" /* STRATEGY_STATE */]: "STRATEGY_STATE"
123
124
  };
124
125
  }
125
126
  });
@@ -688,6 +689,41 @@ var Pipeline_exports = {};
688
689
  __export(Pipeline_exports, {
689
690
  Pipeline: () => Pipeline
690
691
  });
692
+ function logPipelineConfig(generator, filters) {
693
+ const filterList = filters.length > 0 ? "\n - " + filters.map((f) => f.name).join("\n - ") : " none";
694
+ logger.info(
695
+ `[Pipeline] Configuration:
696
+ Generator: ${generator.name}
697
+ Filters:${filterList}`
698
+ );
699
+ }
700
+ function logTagHydration(cards, tagsByCard) {
701
+ const totalTags = Array.from(tagsByCard.values()).reduce((sum, tags) => sum + tags.length, 0);
702
+ const cardsWithTags = Array.from(tagsByCard.values()).filter((tags) => tags.length > 0).length;
703
+ logger.debug(
704
+ `[Pipeline] Tag hydration: ${cards.length} cards, ${cardsWithTags} have tags (${totalTags} total tags) - single batch query`
705
+ );
706
+ }
707
+ function logExecutionSummary(generatorName, generatedCount, filterCount, finalCount, topScores) {
708
+ const scoreDisplay = topScores.length > 0 ? topScores.map((s) => s.toFixed(2)).join(", ") : "none";
709
+ logger.info(
710
+ `[Pipeline] Execution: ${generatorName} produced ${generatedCount} \u2192 ${filterCount} filters \u2192 ${finalCount} results (top scores: ${scoreDisplay})`
711
+ );
712
+ }
713
+ function logCardProvenance(cards, maxCards = 3) {
714
+ const cardsToLog = cards.slice(0, maxCards);
715
+ logger.debug(`[Pipeline] Provenance for top ${cardsToLog.length} cards:`);
716
+ for (const card of cardsToLog) {
717
+ logger.debug(`[Pipeline] ${card.cardId} (final score: ${card.score.toFixed(3)}):`);
718
+ for (const entry of card.provenance) {
719
+ const scoreChange = entry.score.toFixed(3);
720
+ const action = entry.action.padEnd(9);
721
+ logger.debug(
722
+ `[Pipeline] ${action} ${scoreChange} - ${entry.strategyName}: ${entry.reason}`
723
+ );
724
+ }
725
+ }
726
+ }
691
727
  var import_common5, Pipeline;
692
728
  var init_Pipeline = __esm({
693
729
  "src/core/navigators/Pipeline.ts"() {
@@ -712,19 +748,18 @@ var init_Pipeline = __esm({
712
748
  this.filters = filters;
713
749
  this.user = user;
714
750
  this.course = course;
715
- logger.debug(
716
- `[Pipeline] Created with generator '${generator.name}' and ${filters.length} filters: ${filters.map((f) => f.name).join(", ")}`
717
- );
751
+ logPipelineConfig(generator, filters);
718
752
  }
719
753
  /**
720
754
  * Get weighted cards by running generator and applying filters.
721
755
  *
722
756
  * 1. Build shared context (user ELO, etc.)
723
757
  * 2. Get candidates from generator (passing context)
724
- * 3. Apply each filter sequentially
725
- * 4. Remove zero-score cards
726
- * 5. Sort by score descending
727
- * 6. Return top N
758
+ * 3. Batch hydrate tags for all candidates
759
+ * 4. Apply each filter sequentially
760
+ * 5. Remove zero-score cards
761
+ * 6. Sort by score descending
762
+ * 7. Return top N
728
763
  *
729
764
  * @param limit - Maximum number of cards to return
730
765
  * @returns Cards sorted by score descending
@@ -737,7 +772,9 @@ var init_Pipeline = __esm({
737
772
  `[Pipeline] Fetching ${fetchLimit} candidates from generator '${this.generator.name}'`
738
773
  );
739
774
  let cards = await this.generator.getWeightedCards(fetchLimit, context);
740
- logger.debug(`[Pipeline] Generator returned ${cards.length} candidates`);
775
+ const generatedCount = cards.length;
776
+ logger.debug(`[Pipeline] Generator returned ${generatedCount} candidates`);
777
+ cards = await this.hydrateTags(cards);
741
778
  for (const filter of this.filters) {
742
779
  const beforeCount = cards.length;
743
780
  cards = await filter.transform(cards, context);
@@ -746,11 +783,33 @@ var init_Pipeline = __esm({
746
783
  cards = cards.filter((c) => c.score > 0);
747
784
  cards.sort((a, b) => b.score - a.score);
748
785
  const result = cards.slice(0, limit);
749
- logger.debug(
750
- `[Pipeline] Returning ${result.length} cards (top scores: ${result.slice(0, 3).map((c) => c.score.toFixed(2)).join(", ")}...)`
751
- );
786
+ const topScores = result.slice(0, 3).map((c) => c.score);
787
+ logExecutionSummary(this.generator.name, generatedCount, this.filters.length, result.length, topScores);
788
+ logCardProvenance(result, 3);
752
789
  return result;
753
790
  }
791
+ /**
792
+ * Batch hydrate tags for all cards.
793
+ *
794
+ * Fetches tags for all cards in a single database query and attaches them
795
+ * to the WeightedCard objects. Filters can then use card.tags instead of
796
+ * making individual getAppliedTags() calls.
797
+ *
798
+ * @param cards - Cards to hydrate
799
+ * @returns Cards with tags populated
800
+ */
801
+ async hydrateTags(cards) {
802
+ if (cards.length === 0) {
803
+ return cards;
804
+ }
805
+ const cardIds = cards.map((c) => c.cardId);
806
+ const tagsByCard = await this.course.getAppliedTagsBatch(cardIds);
807
+ logTagHydration(cards, tagsByCard);
808
+ return cards.map((card) => ({
809
+ ...card,
810
+ tags: tagsByCard.get(card.cardId) ?? []
811
+ }));
812
+ }
754
813
  /**
755
814
  * Build shared context for generator and filters.
756
815
  *
@@ -1110,15 +1169,144 @@ var init_eloDistance = __esm({
1110
1169
  }
1111
1170
  });
1112
1171
 
1172
+ // src/core/navigators/filters/userTagPreference.ts
1173
+ var userTagPreference_exports = {};
1174
+ __export(userTagPreference_exports, {
1175
+ default: () => UserTagPreferenceFilter
1176
+ });
1177
+ var UserTagPreferenceFilter;
1178
+ var init_userTagPreference = __esm({
1179
+ "src/core/navigators/filters/userTagPreference.ts"() {
1180
+ "use strict";
1181
+ init_navigators();
1182
+ UserTagPreferenceFilter = class extends ContentNavigator {
1183
+ _strategyData;
1184
+ /** Human-readable name for CardFilter interface */
1185
+ name;
1186
+ constructor(user, course, strategyData) {
1187
+ super(user, course, strategyData);
1188
+ this._strategyData = strategyData;
1189
+ this.name = strategyData.name || "User Tag Preferences";
1190
+ }
1191
+ /**
1192
+ * Compute multiplier for a card based on its tags and user preferences.
1193
+ * Returns the maximum multiplier among all matching tags, or 1.0 if no matches.
1194
+ */
1195
+ computeMultiplier(cardTags, boostMap) {
1196
+ const multipliers = cardTags.map((tag) => boostMap[tag]).filter((val) => val !== void 0);
1197
+ if (multipliers.length === 0) {
1198
+ return 1;
1199
+ }
1200
+ return Math.max(...multipliers);
1201
+ }
1202
+ /**
1203
+ * Build human-readable reason for the filter's decision.
1204
+ */
1205
+ buildReason(cardTags, boostMap, multiplier) {
1206
+ const matchingTags = cardTags.filter((tag) => boostMap[tag] === multiplier);
1207
+ if (multiplier === 0) {
1208
+ return `Excluded by user preference: ${matchingTags.join(", ")} (${multiplier}x)`;
1209
+ }
1210
+ if (multiplier < 1) {
1211
+ return `Penalized by user preference: ${matchingTags.join(", ")} (${multiplier.toFixed(2)}x)`;
1212
+ }
1213
+ if (multiplier > 1) {
1214
+ return `Boosted by user preference: ${matchingTags.join(", ")} (${multiplier.toFixed(2)}x)`;
1215
+ }
1216
+ return "No matching user preferences";
1217
+ }
1218
+ /**
1219
+ * CardFilter.transform implementation.
1220
+ *
1221
+ * Apply user tag preferences:
1222
+ * 1. Read preferences from strategy state
1223
+ * 2. If no preferences, pass through unchanged
1224
+ * 3. For each card:
1225
+ * - Look up tag in boost record
1226
+ * - If tag found: apply multiplier (0 = exclude, 1 = neutral, >1 = boost)
1227
+ * - If multiple tags match: use max multiplier
1228
+ * - Append provenance with clear reason
1229
+ */
1230
+ async transform(cards, _context) {
1231
+ const prefs = await this.getStrategyState();
1232
+ if (!prefs || Object.keys(prefs.boost).length === 0) {
1233
+ return cards.map((card) => ({
1234
+ ...card,
1235
+ provenance: [
1236
+ ...card.provenance,
1237
+ {
1238
+ strategy: "userTagPreference",
1239
+ strategyName: this.strategyName || this.name,
1240
+ strategyId: this.strategyId || this._strategyData._id,
1241
+ action: "passed",
1242
+ score: card.score,
1243
+ reason: "No user tag preferences configured"
1244
+ }
1245
+ ]
1246
+ }));
1247
+ }
1248
+ const adjusted = await Promise.all(
1249
+ cards.map(async (card) => {
1250
+ const cardTags = card.tags ?? [];
1251
+ const multiplier = this.computeMultiplier(cardTags, prefs.boost);
1252
+ const finalScore = Math.min(1, card.score * multiplier);
1253
+ let action;
1254
+ if (multiplier === 0 || multiplier < 1) {
1255
+ action = "penalized";
1256
+ } else if (multiplier > 1) {
1257
+ action = "boosted";
1258
+ } else {
1259
+ action = "passed";
1260
+ }
1261
+ return {
1262
+ ...card,
1263
+ score: finalScore,
1264
+ provenance: [
1265
+ ...card.provenance,
1266
+ {
1267
+ strategy: "userTagPreference",
1268
+ strategyName: this.strategyName || this.name,
1269
+ strategyId: this.strategyId || this._strategyData._id,
1270
+ action,
1271
+ score: finalScore,
1272
+ reason: this.buildReason(cardTags, prefs.boost, multiplier)
1273
+ }
1274
+ ]
1275
+ };
1276
+ })
1277
+ );
1278
+ return adjusted;
1279
+ }
1280
+ /**
1281
+ * Legacy getWeightedCards - throws as filters should not be used as generators.
1282
+ */
1283
+ async getWeightedCards(_limit) {
1284
+ throw new Error(
1285
+ "UserTagPreferenceFilter is a filter and should not be used as a generator. Use Pipeline with a generator and this filter via transform()."
1286
+ );
1287
+ }
1288
+ // Legacy methods - stub implementations since filters don't generate cards
1289
+ async getNewCards(_n) {
1290
+ return [];
1291
+ }
1292
+ async getPendingReviews() {
1293
+ return [];
1294
+ }
1295
+ };
1296
+ }
1297
+ });
1298
+
1113
1299
  // src/core/navigators/filters/index.ts
1114
1300
  var filters_exports = {};
1115
1301
  __export(filters_exports, {
1302
+ UserTagPreferenceFilter: () => UserTagPreferenceFilter,
1116
1303
  createEloDistanceFilter: () => createEloDistanceFilter
1117
1304
  });
1118
1305
  var init_filters = __esm({
1119
1306
  "src/core/navigators/filters/index.ts"() {
1120
1307
  "use strict";
1121
1308
  init_eloDistance();
1309
+ init_userTagPreference();
1122
1310
  }
1123
1311
  });
1124
1312
 
@@ -1351,10 +1539,9 @@ var init_hierarchyDefinition = __esm({
1351
1539
  /**
1352
1540
  * Check if a card is unlocked and generate reason.
1353
1541
  */
1354
- async checkCardUnlock(cardId, course, unlockedTags, masteredTags) {
1542
+ async checkCardUnlock(card, course, unlockedTags, masteredTags) {
1355
1543
  try {
1356
- const tagResponse = await course.getAppliedTags(cardId);
1357
- const cardTags = tagResponse.rows.map((row) => row.value?.name || row.key);
1544
+ const cardTags = card.tags ?? [];
1358
1545
  const lockedTags = cardTags.filter(
1359
1546
  (tag) => this.hasPrerequisites(tag) && !unlockedTags.has(tag)
1360
1547
  );
@@ -1391,7 +1578,7 @@ var init_hierarchyDefinition = __esm({
1391
1578
  const gated = [];
1392
1579
  for (const card of cards) {
1393
1580
  const { isUnlocked, reason } = await this.checkCardUnlock(
1394
- card.cardId,
1581
+ card,
1395
1582
  context.course,
1396
1583
  unlockedTags,
1397
1584
  masteredTags
@@ -1437,6 +1624,19 @@ var init_hierarchyDefinition = __esm({
1437
1624
  }
1438
1625
  });
1439
1626
 
1627
+ // src/core/navigators/inferredPreference.ts
1628
+ var inferredPreference_exports = {};
1629
+ __export(inferredPreference_exports, {
1630
+ INFERRED_PREFERENCE_NAVIGATOR_STUB: () => INFERRED_PREFERENCE_NAVIGATOR_STUB
1631
+ });
1632
+ var INFERRED_PREFERENCE_NAVIGATOR_STUB;
1633
+ var init_inferredPreference = __esm({
1634
+ "src/core/navigators/inferredPreference.ts"() {
1635
+ "use strict";
1636
+ INFERRED_PREFERENCE_NAVIGATOR_STUB = true;
1637
+ }
1638
+ });
1639
+
1440
1640
  // src/core/navigators/interferenceMitigator.ts
1441
1641
  var interferenceMitigator_exports = {};
1442
1642
  __export(interferenceMitigator_exports, {
@@ -1568,17 +1768,6 @@ var init_interferenceMitigator = __esm({
1568
1768
  }
1569
1769
  return avoid;
1570
1770
  }
1571
- /**
1572
- * Get tags for a single card
1573
- */
1574
- async getCardTags(cardId, course) {
1575
- try {
1576
- const tagResponse = await course.getAppliedTags(cardId);
1577
- return tagResponse.rows.map((row) => row.value?.name || row.key).filter(Boolean);
1578
- } catch {
1579
- return [];
1580
- }
1581
- }
1582
1771
  /**
1583
1772
  * Compute interference score reduction for a card.
1584
1773
  * Returns: { multiplier, interfering tags, reason }
@@ -1630,7 +1819,7 @@ var init_interferenceMitigator = __esm({
1630
1819
  const tagsToAvoid = this.getTagsToAvoid(immatureTags);
1631
1820
  const adjusted = [];
1632
1821
  for (const card of cards) {
1633
- const cardTags = await this.getCardTags(card.cardId, context.course);
1822
+ const cardTags = card.tags ?? [];
1634
1823
  const { multiplier, reason } = this.computeInterferenceEffect(
1635
1824
  cardTags,
1636
1825
  tagsToAvoid,
@@ -1775,27 +1964,16 @@ var init_relativePriority = __esm({
1775
1964
  return `Low-priority tags: ${tagList}${more} (priority ${priority.toFixed(2)} \u2192 reduce ${boostFactor.toFixed(2)}x \u2192 ${finalScore.toFixed(2)})`;
1776
1965
  }
1777
1966
  }
1778
- /**
1779
- * Get tags for a single card.
1780
- */
1781
- async getCardTags(cardId, course) {
1782
- try {
1783
- const tagResponse = await course.getAppliedTags(cardId);
1784
- return tagResponse.rows.map((r) => r.doc?.name).filter((x) => !!x);
1785
- } catch {
1786
- return [];
1787
- }
1788
- }
1789
1967
  /**
1790
1968
  * CardFilter.transform implementation.
1791
1969
  *
1792
1970
  * Apply priority-adjusted scoring. Cards with high-priority tags get boosted,
1793
1971
  * cards with low-priority tags get reduced scores.
1794
1972
  */
1795
- async transform(cards, context) {
1973
+ async transform(cards, _context) {
1796
1974
  const adjusted = await Promise.all(
1797
1975
  cards.map(async (card) => {
1798
- const cardTags = await this.getCardTags(card.cardId, context.course);
1976
+ const cardTags = card.tags ?? [];
1799
1977
  const priority = this.computeCardPriority(cardTags);
1800
1978
  const boostFactor = this.computeBoostFactor(priority);
1801
1979
  const finalScore = Math.max(0, Math.min(1, card.score * boostFactor));
@@ -1962,6 +2140,19 @@ var init_srs = __esm({
1962
2140
  }
1963
2141
  });
1964
2142
 
2143
+ // src/core/navigators/userGoal.ts
2144
+ var userGoal_exports = {};
2145
+ __export(userGoal_exports, {
2146
+ USER_GOAL_NAVIGATOR_STUB: () => USER_GOAL_NAVIGATOR_STUB
2147
+ });
2148
+ var USER_GOAL_NAVIGATOR_STUB;
2149
+ var init_userGoal = __esm({
2150
+ "src/core/navigators/userGoal.ts"() {
2151
+ "use strict";
2152
+ USER_GOAL_NAVIGATOR_STUB = true;
2153
+ }
2154
+ });
2155
+
1965
2156
  // import("./**/*") in src/core/navigators/index.ts
1966
2157
  var globImport;
1967
2158
  var init_ = __esm({
@@ -1974,14 +2165,17 @@ var init_ = __esm({
1974
2165
  "./filters/eloDistance.ts": () => Promise.resolve().then(() => (init_eloDistance(), eloDistance_exports)),
1975
2166
  "./filters/index.ts": () => Promise.resolve().then(() => (init_filters(), filters_exports)),
1976
2167
  "./filters/types.ts": () => Promise.resolve().then(() => (init_types(), types_exports)),
2168
+ "./filters/userTagPreference.ts": () => Promise.resolve().then(() => (init_userTagPreference(), userTagPreference_exports)),
1977
2169
  "./generators/index.ts": () => Promise.resolve().then(() => (init_generators(), generators_exports)),
1978
2170
  "./generators/types.ts": () => Promise.resolve().then(() => (init_types2(), types_exports2)),
1979
2171
  "./hardcodedOrder.ts": () => Promise.resolve().then(() => (init_hardcodedOrder(), hardcodedOrder_exports)),
1980
2172
  "./hierarchyDefinition.ts": () => Promise.resolve().then(() => (init_hierarchyDefinition(), hierarchyDefinition_exports)),
1981
2173
  "./index.ts": () => Promise.resolve().then(() => (init_navigators(), navigators_exports)),
2174
+ "./inferredPreference.ts": () => Promise.resolve().then(() => (init_inferredPreference(), inferredPreference_exports)),
1982
2175
  "./interferenceMitigator.ts": () => Promise.resolve().then(() => (init_interferenceMitigator(), interferenceMitigator_exports)),
1983
2176
  "./relativePriority.ts": () => Promise.resolve().then(() => (init_relativePriority(), relativePriority_exports)),
1984
- "./srs.ts": () => Promise.resolve().then(() => (init_srs(), srs_exports))
2177
+ "./srs.ts": () => Promise.resolve().then(() => (init_srs(), srs_exports)),
2178
+ "./userGoal.ts": () => Promise.resolve().then(() => (init_userGoal(), userGoal_exports))
1985
2179
  });
1986
2180
  }
1987
2181
  });
@@ -2030,6 +2224,7 @@ var init_navigators = __esm({
2030
2224
  Navigators2["HIERARCHY"] = "hierarchyDefinition";
2031
2225
  Navigators2["INTERFERENCE"] = "interferenceMitigator";
2032
2226
  Navigators2["RELATIVE_PRIORITY"] = "relativePriority";
2227
+ Navigators2["USER_TAG_PREFERENCE"] = "userTagPreference";
2033
2228
  return Navigators2;
2034
2229
  })(Navigators || {});
2035
2230
  NavigatorRole = /* @__PURE__ */ ((NavigatorRole2) => {
@@ -2043,7 +2238,8 @@ var init_navigators = __esm({
2043
2238
  ["hardcodedOrder" /* HARDCODED */]: "generator" /* GENERATOR */,
2044
2239
  ["hierarchyDefinition" /* HIERARCHY */]: "filter" /* FILTER */,
2045
2240
  ["interferenceMitigator" /* INTERFERENCE */]: "filter" /* FILTER */,
2046
- ["relativePriority" /* RELATIVE_PRIORITY */]: "filter" /* FILTER */
2241
+ ["relativePriority" /* RELATIVE_PRIORITY */]: "filter" /* FILTER */,
2242
+ ["userTagPreference" /* USER_TAG_PREFERENCE */]: "filter" /* FILTER */
2047
2243
  };
2048
2244
  ContentNavigator = class {
2049
2245
  /** User interface for this navigation session */
@@ -2068,6 +2264,52 @@ var init_navigators = __esm({
2068
2264
  this.strategyId = strategyData._id;
2069
2265
  }
2070
2266
  }
2267
+ // ============================================================================
2268
+ // STRATEGY STATE HELPERS
2269
+ // ============================================================================
2270
+ //
2271
+ // These methods allow strategies to persist their own state (user preferences,
2272
+ // learned patterns, temporal tracking) in the user database.
2273
+ //
2274
+ // ============================================================================
2275
+ /**
2276
+ * Unique key identifying this strategy for state storage.
2277
+ *
2278
+ * Defaults to the constructor name (e.g., "UserTagPreferenceFilter").
2279
+ * Override in subclasses if multiple instances of the same strategy type
2280
+ * need separate state storage.
2281
+ */
2282
+ get strategyKey() {
2283
+ return this.constructor.name;
2284
+ }
2285
+ /**
2286
+ * Get this strategy's persisted state for the current course.
2287
+ *
2288
+ * @returns The strategy's data payload, or null if no state exists
2289
+ * @throws Error if user or course is not initialized
2290
+ */
2291
+ async getStrategyState() {
2292
+ if (!this.user || !this.course) {
2293
+ throw new Error(
2294
+ `Cannot get strategy state: navigator not properly initialized. Ensure user and course are provided to constructor.`
2295
+ );
2296
+ }
2297
+ return this.user.getStrategyState(this.course.getCourseID(), this.strategyKey);
2298
+ }
2299
+ /**
2300
+ * Persist this strategy's state for the current course.
2301
+ *
2302
+ * @param data - The strategy's data payload to store
2303
+ * @throws Error if user or course is not initialized
2304
+ */
2305
+ async putStrategyState(data) {
2306
+ if (!this.user || !this.course) {
2307
+ throw new Error(
2308
+ `Cannot put strategy state: navigator not properly initialized. Ensure user and course are provided to constructor.`
2309
+ );
2310
+ }
2311
+ return this.user.putStrategyState(this.course.getCourseID(), this.strategyKey, data);
2312
+ }
2071
2313
  /**
2072
2314
  * Factory method to create navigator instances dynamically.
2073
2315
  *
@@ -2295,7 +2537,9 @@ var init_couch = __esm({
2295
2537
  function accomodateGuest() {
2296
2538
  logger.log("[funnel] accomodateGuest() called");
2297
2539
  if (typeof localStorage === "undefined") {
2298
- logger.log("[funnel] localStorage not available (Node.js environment), returning default guest");
2540
+ logger.log(
2541
+ "[funnel] localStorage not available (Node.js environment), returning default guest"
2542
+ );
2299
2543
  return {
2300
2544
  username: GuestUsername + "nodejs-test",
2301
2545
  firstVisit: true
@@ -3275,6 +3519,55 @@ Currently logged-in as ${this._username}.`
3275
3519
  async updateUserElo(courseId, elo) {
3276
3520
  return updateUserElo(this._username, courseId, elo);
3277
3521
  }
3522
+ async getStrategyState(courseId, strategyKey) {
3523
+ const docId = buildStrategyStateId(courseId, strategyKey);
3524
+ try {
3525
+ const doc = await this.localDB.get(docId);
3526
+ return doc.data;
3527
+ } catch (e) {
3528
+ const err = e;
3529
+ if (err.status === 404) {
3530
+ return null;
3531
+ }
3532
+ throw e;
3533
+ }
3534
+ }
3535
+ async putStrategyState(courseId, strategyKey, data) {
3536
+ const docId = buildStrategyStateId(courseId, strategyKey);
3537
+ let existingRev;
3538
+ try {
3539
+ const existing = await this.localDB.get(docId);
3540
+ existingRev = existing._rev;
3541
+ } catch (e) {
3542
+ const err = e;
3543
+ if (err.status !== 404) {
3544
+ throw e;
3545
+ }
3546
+ }
3547
+ const doc = {
3548
+ _id: docId,
3549
+ _rev: existingRev,
3550
+ docType: "STRATEGY_STATE" /* STRATEGY_STATE */,
3551
+ courseId,
3552
+ strategyKey,
3553
+ data,
3554
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
3555
+ };
3556
+ await this.localDB.put(doc);
3557
+ }
3558
+ async deleteStrategyState(courseId, strategyKey) {
3559
+ const docId = buildStrategyStateId(courseId, strategyKey);
3560
+ try {
3561
+ const doc = await this.localDB.get(docId);
3562
+ await this.localDB.remove(doc);
3563
+ } catch (e) {
3564
+ const err = e;
3565
+ if (err.status === 404) {
3566
+ return;
3567
+ }
3568
+ throw e;
3569
+ }
3570
+ }
3278
3571
  };
3279
3572
  userCoursesDoc = "CourseRegistrations";
3280
3573
  userClassroomsDoc = "ClassroomRegistrations";
@@ -3371,6 +3664,16 @@ var init_user = __esm({
3371
3664
  }
3372
3665
  });
3373
3666
 
3667
+ // src/core/types/strategyState.ts
3668
+ function buildStrategyStateId(courseId, strategyKey) {
3669
+ return `STRATEGY_STATE::${courseId}::${strategyKey}`;
3670
+ }
3671
+ var init_strategyState = __esm({
3672
+ "src/core/types/strategyState.ts"() {
3673
+ "use strict";
3674
+ }
3675
+ });
3676
+
3374
3677
  // src/core/bulkImport/cardProcessor.ts
3375
3678
  var import_common16;
3376
3679
  var init_cardProcessor = __esm({
@@ -3404,6 +3707,7 @@ var init_core = __esm({
3404
3707
  init_interfaces();
3405
3708
  init_types_legacy();
3406
3709
  init_user();
3710
+ init_strategyState();
3407
3711
  init_Loggable();
3408
3712
  init_util();
3409
3713
  init_navigators();
@@ -3976,6 +4280,14 @@ var init_courseDB3 = __esm({
3976
4280
  };
3977
4281
  }
3978
4282
  }
4283
+ async getAppliedTagsBatch(cardIds) {
4284
+ const tagsIndex = await this.unpacker.getTagsIndex();
4285
+ const tagsByCard = /* @__PURE__ */ new Map();
4286
+ for (const cardId of cardIds) {
4287
+ tagsByCard.set(cardId, tagsIndex.byCard[cardId] || []);
4288
+ }
4289
+ return tagsByCard;
4290
+ }
3979
4291
  async addTagToCard(_cardId, _tagId) {
3980
4292
  throw new Error("Cannot modify tags in static mode");
3981
4293
  }