@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,4 +1,4 @@
1
- import { U as UserDBInterface, a as UserDBReader, C as CourseDBInterface, b as CoursesDBInterface, c as ClassroomDBInterface, A as AdminDBInterface } from './classroomDB-CTOenngH.cjs';
1
+ import { U as UserDBInterface, a as UserDBReader, C as CourseDBInterface, b as CoursesDBInterface, c as ClassroomDBInterface, A as AdminDBInterface } from './classroomDB-PxDZTky3.cjs';
2
2
 
3
3
  /**
4
4
  * Main factory interface for data access
@@ -1,4 +1,4 @@
1
- import { U as UserDBInterface, a as UserDBReader, C as CourseDBInterface, b as CoursesDBInterface, c as ClassroomDBInterface, A as AdminDBInterface } from './classroomDB-BgfrVb8d.js';
1
+ import { U as UserDBInterface, a as UserDBReader, C as CourseDBInterface, b as CoursesDBInterface, c as ClassroomDBInterface, A as AdminDBInterface } from './classroomDB-CZdMBiTU.js';
2
2
 
3
3
  /**
4
4
  * Main factory interface for data access
@@ -1,7 +1,7 @@
1
- import { T as TagStub, a as Tag, S as SkuilderCourseData, Q as QualifiedCardID } from '../../types-legacy-6ettoclI.cjs';
1
+ import { T as TagStub, a as Tag, S as SkuilderCourseData, Q as QualifiedCardID } from '../../types-legacy-DDY4N-Uq.cjs';
2
2
  import { Moment } from 'moment';
3
- import { A as AdminDBInterface, h as AssignedContent, i as StudyContentSource, j as StudentClassroomDBInterface, U as UserDBInterface, f as StudySessionReviewItem, g as ScheduledCard, S as StudySessionNewItem, W as WeightedCard, T as TeacherClassroomDBInterface, b as CoursesDBInterface, C as CourseDBInterface, d as CourseInfo, D as DataLayerResult, e as ContentNavigationStrategyData, k as ContentNavigator, l as StudySessionItem } from '../../classroomDB-CTOenngH.cjs';
4
- export { q as ContentSourceID, m as StudySessionFailedItem, n as StudySessionFailedNewItem, o as StudySessionFailedReviewItem, r as getStudySource, p as isReview } from '../../classroomDB-CTOenngH.cjs';
3
+ import { A as AdminDBInterface, h as AssignedContent, i as StudyContentSource, j as StudentClassroomDBInterface, U as UserDBInterface, f as StudySessionReviewItem, g as ScheduledCard, S as StudySessionNewItem, W as WeightedCard, T as TeacherClassroomDBInterface, b as CoursesDBInterface, C as CourseDBInterface, d as CourseInfo, D as DataLayerResult, e as ContentNavigationStrategyData, k as ContentNavigator, l as StudySessionItem } from '../../classroomDB-PxDZTky3.cjs';
4
+ export { q as ContentSourceID, m as StudySessionFailedItem, n as StudySessionFailedNewItem, o as StudySessionFailedReviewItem, r as getStudySource, p as isReview } from '../../classroomDB-PxDZTky3.cjs';
5
5
  import * as _vue_skuilder_common from '@vue-skuilder/common';
6
6
  import { ClassroomConfig, DataShape, CourseElo, CourseConfig as CourseConfig$1 } from '@vue-skuilder/common';
7
7
  import { S as SyncStrategy, A as AccountCreationResult, a as AuthenticationResult } from '../../SyncStrategy-CyATpyLQ.cjs';
@@ -191,6 +191,7 @@ declare class CourseDB implements StudyContentSource, CourseDBInterface {
191
191
  updateCourseConfig(cfg: CourseConfig$1): Promise<PouchDB.Core.Response>;
192
192
  updateCardElo(cardId: string, elo: CourseElo): Promise<PouchDB.Core.Response>;
193
193
  getAppliedTags(cardId: string): Promise<PouchDB.Query.Response<TagStub>>;
194
+ getAppliedTagsBatch(cardIds: string[]): Promise<Map<string, string[]>>;
194
195
  addTagToCard(cardId: string, tagId: string, updateELO?: boolean): Promise<PouchDB.Core.Response>;
195
196
  removeTagFromCard(cardId: string, tagId: string): Promise<PouchDB.Core.Response>;
196
197
  createTag(name: string, author: string): Promise<PouchDB.Core.Response>;
@@ -1,7 +1,7 @@
1
- import { T as TagStub, a as Tag, S as SkuilderCourseData, Q as QualifiedCardID } from '../../types-legacy-6ettoclI.js';
1
+ import { T as TagStub, a as Tag, S as SkuilderCourseData, Q as QualifiedCardID } from '../../types-legacy-DDY4N-Uq.js';
2
2
  import { Moment } from 'moment';
3
- import { A as AdminDBInterface, h as AssignedContent, i as StudyContentSource, j as StudentClassroomDBInterface, U as UserDBInterface, f as StudySessionReviewItem, g as ScheduledCard, S as StudySessionNewItem, W as WeightedCard, T as TeacherClassroomDBInterface, b as CoursesDBInterface, C as CourseDBInterface, d as CourseInfo, D as DataLayerResult, e as ContentNavigationStrategyData, k as ContentNavigator, l as StudySessionItem } from '../../classroomDB-BgfrVb8d.js';
4
- export { q as ContentSourceID, m as StudySessionFailedItem, n as StudySessionFailedNewItem, o as StudySessionFailedReviewItem, r as getStudySource, p as isReview } from '../../classroomDB-BgfrVb8d.js';
3
+ import { A as AdminDBInterface, h as AssignedContent, i as StudyContentSource, j as StudentClassroomDBInterface, U as UserDBInterface, f as StudySessionReviewItem, g as ScheduledCard, S as StudySessionNewItem, W as WeightedCard, T as TeacherClassroomDBInterface, b as CoursesDBInterface, C as CourseDBInterface, d as CourseInfo, D as DataLayerResult, e as ContentNavigationStrategyData, k as ContentNavigator, l as StudySessionItem } from '../../classroomDB-CZdMBiTU.js';
4
+ export { q as ContentSourceID, m as StudySessionFailedItem, n as StudySessionFailedNewItem, o as StudySessionFailedReviewItem, r as getStudySource, p as isReview } from '../../classroomDB-CZdMBiTU.js';
5
5
  import * as _vue_skuilder_common from '@vue-skuilder/common';
6
6
  import { ClassroomConfig, DataShape, CourseElo, CourseConfig as CourseConfig$1 } from '@vue-skuilder/common';
7
7
  import { S as SyncStrategy, A as AccountCreationResult, a as AuthenticationResult } from '../../SyncStrategy-CyATpyLQ.js';
@@ -191,6 +191,7 @@ declare class CourseDB implements StudyContentSource, CourseDBInterface {
191
191
  updateCourseConfig(cfg: CourseConfig$1): Promise<PouchDB.Core.Response>;
192
192
  updateCardElo(cardId: string, elo: CourseElo): Promise<PouchDB.Core.Response>;
193
193
  getAppliedTags(cardId: string): Promise<PouchDB.Query.Response<TagStub>>;
194
+ getAppliedTagsBatch(cardIds: string[]): Promise<Map<string, string[]>>;
194
195
  addTagToCard(cardId: string, tagId: string, updateELO?: boolean): Promise<PouchDB.Core.Response>;
195
196
  removeTagFromCard(cardId: string, tagId: string): Promise<PouchDB.Core.Response>;
196
197
  createTag(name: string, author: string): Promise<PouchDB.Core.Response>;
@@ -280,7 +280,8 @@ var init_types_legacy = __esm({
280
280
  ["QUESTION" /* QUESTIONTYPE */]: "QUESTION",
281
281
  ["VIEW" /* VIEW */]: "VIEW",
282
282
  ["PEDAGOGY" /* PEDAGOGY */]: "PEDAGOGY",
283
- ["NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */]: "NAVIGATION_STRATEGY"
283
+ ["NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */]: "NAVIGATION_STRATEGY",
284
+ ["STRATEGY_STATE" /* STRATEGY_STATE */]: "STRATEGY_STATE"
284
285
  };
285
286
  }
286
287
  });
@@ -810,6 +811,41 @@ var Pipeline_exports = {};
810
811
  __export(Pipeline_exports, {
811
812
  Pipeline: () => Pipeline
812
813
  });
814
+ function logPipelineConfig(generator, filters) {
815
+ const filterList = filters.length > 0 ? "\n - " + filters.map((f) => f.name).join("\n - ") : " none";
816
+ logger.info(
817
+ `[Pipeline] Configuration:
818
+ Generator: ${generator.name}
819
+ Filters:${filterList}`
820
+ );
821
+ }
822
+ function logTagHydration(cards, tagsByCard) {
823
+ const totalTags = Array.from(tagsByCard.values()).reduce((sum, tags) => sum + tags.length, 0);
824
+ const cardsWithTags = Array.from(tagsByCard.values()).filter((tags) => tags.length > 0).length;
825
+ logger.debug(
826
+ `[Pipeline] Tag hydration: ${cards.length} cards, ${cardsWithTags} have tags (${totalTags} total tags) - single batch query`
827
+ );
828
+ }
829
+ function logExecutionSummary(generatorName, generatedCount, filterCount, finalCount, topScores) {
830
+ const scoreDisplay = topScores.length > 0 ? topScores.map((s) => s.toFixed(2)).join(", ") : "none";
831
+ logger.info(
832
+ `[Pipeline] Execution: ${generatorName} produced ${generatedCount} \u2192 ${filterCount} filters \u2192 ${finalCount} results (top scores: ${scoreDisplay})`
833
+ );
834
+ }
835
+ function logCardProvenance(cards, maxCards = 3) {
836
+ const cardsToLog = cards.slice(0, maxCards);
837
+ logger.debug(`[Pipeline] Provenance for top ${cardsToLog.length} cards:`);
838
+ for (const card of cardsToLog) {
839
+ logger.debug(`[Pipeline] ${card.cardId} (final score: ${card.score.toFixed(3)}):`);
840
+ for (const entry of card.provenance) {
841
+ const scoreChange = entry.score.toFixed(3);
842
+ const action = entry.action.padEnd(9);
843
+ logger.debug(
844
+ `[Pipeline] ${action} ${scoreChange} - ${entry.strategyName}: ${entry.reason}`
845
+ );
846
+ }
847
+ }
848
+ }
813
849
  var import_common5, Pipeline;
814
850
  var init_Pipeline = __esm({
815
851
  "src/core/navigators/Pipeline.ts"() {
@@ -834,19 +870,18 @@ var init_Pipeline = __esm({
834
870
  this.filters = filters;
835
871
  this.user = user;
836
872
  this.course = course;
837
- logger.debug(
838
- `[Pipeline] Created with generator '${generator.name}' and ${filters.length} filters: ${filters.map((f) => f.name).join(", ")}`
839
- );
873
+ logPipelineConfig(generator, filters);
840
874
  }
841
875
  /**
842
876
  * Get weighted cards by running generator and applying filters.
843
877
  *
844
878
  * 1. Build shared context (user ELO, etc.)
845
879
  * 2. Get candidates from generator (passing context)
846
- * 3. Apply each filter sequentially
847
- * 4. Remove zero-score cards
848
- * 5. Sort by score descending
849
- * 6. Return top N
880
+ * 3. Batch hydrate tags for all candidates
881
+ * 4. Apply each filter sequentially
882
+ * 5. Remove zero-score cards
883
+ * 6. Sort by score descending
884
+ * 7. Return top N
850
885
  *
851
886
  * @param limit - Maximum number of cards to return
852
887
  * @returns Cards sorted by score descending
@@ -859,7 +894,9 @@ var init_Pipeline = __esm({
859
894
  `[Pipeline] Fetching ${fetchLimit} candidates from generator '${this.generator.name}'`
860
895
  );
861
896
  let cards = await this.generator.getWeightedCards(fetchLimit, context);
862
- logger.debug(`[Pipeline] Generator returned ${cards.length} candidates`);
897
+ const generatedCount = cards.length;
898
+ logger.debug(`[Pipeline] Generator returned ${generatedCount} candidates`);
899
+ cards = await this.hydrateTags(cards);
863
900
  for (const filter of this.filters) {
864
901
  const beforeCount = cards.length;
865
902
  cards = await filter.transform(cards, context);
@@ -868,11 +905,33 @@ var init_Pipeline = __esm({
868
905
  cards = cards.filter((c) => c.score > 0);
869
906
  cards.sort((a, b) => b.score - a.score);
870
907
  const result = cards.slice(0, limit);
871
- logger.debug(
872
- `[Pipeline] Returning ${result.length} cards (top scores: ${result.slice(0, 3).map((c) => c.score.toFixed(2)).join(", ")}...)`
873
- );
908
+ const topScores = result.slice(0, 3).map((c) => c.score);
909
+ logExecutionSummary(this.generator.name, generatedCount, this.filters.length, result.length, topScores);
910
+ logCardProvenance(result, 3);
874
911
  return result;
875
912
  }
913
+ /**
914
+ * Batch hydrate tags for all cards.
915
+ *
916
+ * Fetches tags for all cards in a single database query and attaches them
917
+ * to the WeightedCard objects. Filters can then use card.tags instead of
918
+ * making individual getAppliedTags() calls.
919
+ *
920
+ * @param cards - Cards to hydrate
921
+ * @returns Cards with tags populated
922
+ */
923
+ async hydrateTags(cards) {
924
+ if (cards.length === 0) {
925
+ return cards;
926
+ }
927
+ const cardIds = cards.map((c) => c.cardId);
928
+ const tagsByCard = await this.course.getAppliedTagsBatch(cardIds);
929
+ logTagHydration(cards, tagsByCard);
930
+ return cards.map((card) => ({
931
+ ...card,
932
+ tags: tagsByCard.get(card.cardId) ?? []
933
+ }));
934
+ }
876
935
  /**
877
936
  * Build shared context for generator and filters.
878
937
  *
@@ -1232,15 +1291,144 @@ var init_eloDistance = __esm({
1232
1291
  }
1233
1292
  });
1234
1293
 
1294
+ // src/core/navigators/filters/userTagPreference.ts
1295
+ var userTagPreference_exports = {};
1296
+ __export(userTagPreference_exports, {
1297
+ default: () => UserTagPreferenceFilter
1298
+ });
1299
+ var UserTagPreferenceFilter;
1300
+ var init_userTagPreference = __esm({
1301
+ "src/core/navigators/filters/userTagPreference.ts"() {
1302
+ "use strict";
1303
+ init_navigators();
1304
+ UserTagPreferenceFilter = class extends ContentNavigator {
1305
+ _strategyData;
1306
+ /** Human-readable name for CardFilter interface */
1307
+ name;
1308
+ constructor(user, course, strategyData) {
1309
+ super(user, course, strategyData);
1310
+ this._strategyData = strategyData;
1311
+ this.name = strategyData.name || "User Tag Preferences";
1312
+ }
1313
+ /**
1314
+ * Compute multiplier for a card based on its tags and user preferences.
1315
+ * Returns the maximum multiplier among all matching tags, or 1.0 if no matches.
1316
+ */
1317
+ computeMultiplier(cardTags, boostMap) {
1318
+ const multipliers = cardTags.map((tag) => boostMap[tag]).filter((val) => val !== void 0);
1319
+ if (multipliers.length === 0) {
1320
+ return 1;
1321
+ }
1322
+ return Math.max(...multipliers);
1323
+ }
1324
+ /**
1325
+ * Build human-readable reason for the filter's decision.
1326
+ */
1327
+ buildReason(cardTags, boostMap, multiplier) {
1328
+ const matchingTags = cardTags.filter((tag) => boostMap[tag] === multiplier);
1329
+ if (multiplier === 0) {
1330
+ return `Excluded by user preference: ${matchingTags.join(", ")} (${multiplier}x)`;
1331
+ }
1332
+ if (multiplier < 1) {
1333
+ return `Penalized by user preference: ${matchingTags.join(", ")} (${multiplier.toFixed(2)}x)`;
1334
+ }
1335
+ if (multiplier > 1) {
1336
+ return `Boosted by user preference: ${matchingTags.join(", ")} (${multiplier.toFixed(2)}x)`;
1337
+ }
1338
+ return "No matching user preferences";
1339
+ }
1340
+ /**
1341
+ * CardFilter.transform implementation.
1342
+ *
1343
+ * Apply user tag preferences:
1344
+ * 1. Read preferences from strategy state
1345
+ * 2. If no preferences, pass through unchanged
1346
+ * 3. For each card:
1347
+ * - Look up tag in boost record
1348
+ * - If tag found: apply multiplier (0 = exclude, 1 = neutral, >1 = boost)
1349
+ * - If multiple tags match: use max multiplier
1350
+ * - Append provenance with clear reason
1351
+ */
1352
+ async transform(cards, _context) {
1353
+ const prefs = await this.getStrategyState();
1354
+ if (!prefs || Object.keys(prefs.boost).length === 0) {
1355
+ return cards.map((card) => ({
1356
+ ...card,
1357
+ provenance: [
1358
+ ...card.provenance,
1359
+ {
1360
+ strategy: "userTagPreference",
1361
+ strategyName: this.strategyName || this.name,
1362
+ strategyId: this.strategyId || this._strategyData._id,
1363
+ action: "passed",
1364
+ score: card.score,
1365
+ reason: "No user tag preferences configured"
1366
+ }
1367
+ ]
1368
+ }));
1369
+ }
1370
+ const adjusted = await Promise.all(
1371
+ cards.map(async (card) => {
1372
+ const cardTags = card.tags ?? [];
1373
+ const multiplier = this.computeMultiplier(cardTags, prefs.boost);
1374
+ const finalScore = Math.min(1, card.score * multiplier);
1375
+ let action;
1376
+ if (multiplier === 0 || multiplier < 1) {
1377
+ action = "penalized";
1378
+ } else if (multiplier > 1) {
1379
+ action = "boosted";
1380
+ } else {
1381
+ action = "passed";
1382
+ }
1383
+ return {
1384
+ ...card,
1385
+ score: finalScore,
1386
+ provenance: [
1387
+ ...card.provenance,
1388
+ {
1389
+ strategy: "userTagPreference",
1390
+ strategyName: this.strategyName || this.name,
1391
+ strategyId: this.strategyId || this._strategyData._id,
1392
+ action,
1393
+ score: finalScore,
1394
+ reason: this.buildReason(cardTags, prefs.boost, multiplier)
1395
+ }
1396
+ ]
1397
+ };
1398
+ })
1399
+ );
1400
+ return adjusted;
1401
+ }
1402
+ /**
1403
+ * Legacy getWeightedCards - throws as filters should not be used as generators.
1404
+ */
1405
+ async getWeightedCards(_limit) {
1406
+ throw new Error(
1407
+ "UserTagPreferenceFilter is a filter and should not be used as a generator. Use Pipeline with a generator and this filter via transform()."
1408
+ );
1409
+ }
1410
+ // Legacy methods - stub implementations since filters don't generate cards
1411
+ async getNewCards(_n) {
1412
+ return [];
1413
+ }
1414
+ async getPendingReviews() {
1415
+ return [];
1416
+ }
1417
+ };
1418
+ }
1419
+ });
1420
+
1235
1421
  // src/core/navigators/filters/index.ts
1236
1422
  var filters_exports = {};
1237
1423
  __export(filters_exports, {
1424
+ UserTagPreferenceFilter: () => UserTagPreferenceFilter,
1238
1425
  createEloDistanceFilter: () => createEloDistanceFilter
1239
1426
  });
1240
1427
  var init_filters = __esm({
1241
1428
  "src/core/navigators/filters/index.ts"() {
1242
1429
  "use strict";
1243
1430
  init_eloDistance();
1431
+ init_userTagPreference();
1244
1432
  }
1245
1433
  });
1246
1434
 
@@ -1473,10 +1661,9 @@ var init_hierarchyDefinition = __esm({
1473
1661
  /**
1474
1662
  * Check if a card is unlocked and generate reason.
1475
1663
  */
1476
- async checkCardUnlock(cardId, course, unlockedTags, masteredTags) {
1664
+ async checkCardUnlock(card, course, unlockedTags, masteredTags) {
1477
1665
  try {
1478
- const tagResponse = await course.getAppliedTags(cardId);
1479
- const cardTags = tagResponse.rows.map((row) => row.value?.name || row.key);
1666
+ const cardTags = card.tags ?? [];
1480
1667
  const lockedTags = cardTags.filter(
1481
1668
  (tag) => this.hasPrerequisites(tag) && !unlockedTags.has(tag)
1482
1669
  );
@@ -1513,7 +1700,7 @@ var init_hierarchyDefinition = __esm({
1513
1700
  const gated = [];
1514
1701
  for (const card of cards) {
1515
1702
  const { isUnlocked, reason } = await this.checkCardUnlock(
1516
- card.cardId,
1703
+ card,
1517
1704
  context.course,
1518
1705
  unlockedTags,
1519
1706
  masteredTags
@@ -1559,6 +1746,19 @@ var init_hierarchyDefinition = __esm({
1559
1746
  }
1560
1747
  });
1561
1748
 
1749
+ // src/core/navigators/inferredPreference.ts
1750
+ var inferredPreference_exports = {};
1751
+ __export(inferredPreference_exports, {
1752
+ INFERRED_PREFERENCE_NAVIGATOR_STUB: () => INFERRED_PREFERENCE_NAVIGATOR_STUB
1753
+ });
1754
+ var INFERRED_PREFERENCE_NAVIGATOR_STUB;
1755
+ var init_inferredPreference = __esm({
1756
+ "src/core/navigators/inferredPreference.ts"() {
1757
+ "use strict";
1758
+ INFERRED_PREFERENCE_NAVIGATOR_STUB = true;
1759
+ }
1760
+ });
1761
+
1562
1762
  // src/core/navigators/interferenceMitigator.ts
1563
1763
  var interferenceMitigator_exports = {};
1564
1764
  __export(interferenceMitigator_exports, {
@@ -1690,17 +1890,6 @@ var init_interferenceMitigator = __esm({
1690
1890
  }
1691
1891
  return avoid;
1692
1892
  }
1693
- /**
1694
- * Get tags for a single card
1695
- */
1696
- async getCardTags(cardId, course) {
1697
- try {
1698
- const tagResponse = await course.getAppliedTags(cardId);
1699
- return tagResponse.rows.map((row) => row.value?.name || row.key).filter(Boolean);
1700
- } catch {
1701
- return [];
1702
- }
1703
- }
1704
1893
  /**
1705
1894
  * Compute interference score reduction for a card.
1706
1895
  * Returns: { multiplier, interfering tags, reason }
@@ -1752,7 +1941,7 @@ var init_interferenceMitigator = __esm({
1752
1941
  const tagsToAvoid = this.getTagsToAvoid(immatureTags);
1753
1942
  const adjusted = [];
1754
1943
  for (const card of cards) {
1755
- const cardTags = await this.getCardTags(card.cardId, context.course);
1944
+ const cardTags = card.tags ?? [];
1756
1945
  const { multiplier, reason } = this.computeInterferenceEffect(
1757
1946
  cardTags,
1758
1947
  tagsToAvoid,
@@ -1897,27 +2086,16 @@ var init_relativePriority = __esm({
1897
2086
  return `Low-priority tags: ${tagList}${more} (priority ${priority.toFixed(2)} \u2192 reduce ${boostFactor.toFixed(2)}x \u2192 ${finalScore.toFixed(2)})`;
1898
2087
  }
1899
2088
  }
1900
- /**
1901
- * Get tags for a single card.
1902
- */
1903
- async getCardTags(cardId, course) {
1904
- try {
1905
- const tagResponse = await course.getAppliedTags(cardId);
1906
- return tagResponse.rows.map((r) => r.doc?.name).filter((x) => !!x);
1907
- } catch {
1908
- return [];
1909
- }
1910
- }
1911
2089
  /**
1912
2090
  * CardFilter.transform implementation.
1913
2091
  *
1914
2092
  * Apply priority-adjusted scoring. Cards with high-priority tags get boosted,
1915
2093
  * cards with low-priority tags get reduced scores.
1916
2094
  */
1917
- async transform(cards, context) {
2095
+ async transform(cards, _context) {
1918
2096
  const adjusted = await Promise.all(
1919
2097
  cards.map(async (card) => {
1920
- const cardTags = await this.getCardTags(card.cardId, context.course);
2098
+ const cardTags = card.tags ?? [];
1921
2099
  const priority = this.computeCardPriority(cardTags);
1922
2100
  const boostFactor = this.computeBoostFactor(priority);
1923
2101
  const finalScore = Math.max(0, Math.min(1, card.score * boostFactor));
@@ -2084,6 +2262,19 @@ var init_srs = __esm({
2084
2262
  }
2085
2263
  });
2086
2264
 
2265
+ // src/core/navigators/userGoal.ts
2266
+ var userGoal_exports = {};
2267
+ __export(userGoal_exports, {
2268
+ USER_GOAL_NAVIGATOR_STUB: () => USER_GOAL_NAVIGATOR_STUB
2269
+ });
2270
+ var USER_GOAL_NAVIGATOR_STUB;
2271
+ var init_userGoal = __esm({
2272
+ "src/core/navigators/userGoal.ts"() {
2273
+ "use strict";
2274
+ USER_GOAL_NAVIGATOR_STUB = true;
2275
+ }
2276
+ });
2277
+
2087
2278
  // import("./**/*") in src/core/navigators/index.ts
2088
2279
  var globImport;
2089
2280
  var init_ = __esm({
@@ -2096,14 +2287,17 @@ var init_ = __esm({
2096
2287
  "./filters/eloDistance.ts": () => Promise.resolve().then(() => (init_eloDistance(), eloDistance_exports)),
2097
2288
  "./filters/index.ts": () => Promise.resolve().then(() => (init_filters(), filters_exports)),
2098
2289
  "./filters/types.ts": () => Promise.resolve().then(() => (init_types(), types_exports)),
2290
+ "./filters/userTagPreference.ts": () => Promise.resolve().then(() => (init_userTagPreference(), userTagPreference_exports)),
2099
2291
  "./generators/index.ts": () => Promise.resolve().then(() => (init_generators(), generators_exports)),
2100
2292
  "./generators/types.ts": () => Promise.resolve().then(() => (init_types2(), types_exports2)),
2101
2293
  "./hardcodedOrder.ts": () => Promise.resolve().then(() => (init_hardcodedOrder(), hardcodedOrder_exports)),
2102
2294
  "./hierarchyDefinition.ts": () => Promise.resolve().then(() => (init_hierarchyDefinition(), hierarchyDefinition_exports)),
2103
2295
  "./index.ts": () => Promise.resolve().then(() => (init_navigators(), navigators_exports)),
2296
+ "./inferredPreference.ts": () => Promise.resolve().then(() => (init_inferredPreference(), inferredPreference_exports)),
2104
2297
  "./interferenceMitigator.ts": () => Promise.resolve().then(() => (init_interferenceMitigator(), interferenceMitigator_exports)),
2105
2298
  "./relativePriority.ts": () => Promise.resolve().then(() => (init_relativePriority(), relativePriority_exports)),
2106
- "./srs.ts": () => Promise.resolve().then(() => (init_srs(), srs_exports))
2299
+ "./srs.ts": () => Promise.resolve().then(() => (init_srs(), srs_exports)),
2300
+ "./userGoal.ts": () => Promise.resolve().then(() => (init_userGoal(), userGoal_exports))
2107
2301
  });
2108
2302
  }
2109
2303
  });
@@ -2152,6 +2346,7 @@ var init_navigators = __esm({
2152
2346
  Navigators2["HIERARCHY"] = "hierarchyDefinition";
2153
2347
  Navigators2["INTERFERENCE"] = "interferenceMitigator";
2154
2348
  Navigators2["RELATIVE_PRIORITY"] = "relativePriority";
2349
+ Navigators2["USER_TAG_PREFERENCE"] = "userTagPreference";
2155
2350
  return Navigators2;
2156
2351
  })(Navigators || {});
2157
2352
  NavigatorRole = /* @__PURE__ */ ((NavigatorRole2) => {
@@ -2165,7 +2360,8 @@ var init_navigators = __esm({
2165
2360
  ["hardcodedOrder" /* HARDCODED */]: "generator" /* GENERATOR */,
2166
2361
  ["hierarchyDefinition" /* HIERARCHY */]: "filter" /* FILTER */,
2167
2362
  ["interferenceMitigator" /* INTERFERENCE */]: "filter" /* FILTER */,
2168
- ["relativePriority" /* RELATIVE_PRIORITY */]: "filter" /* FILTER */
2363
+ ["relativePriority" /* RELATIVE_PRIORITY */]: "filter" /* FILTER */,
2364
+ ["userTagPreference" /* USER_TAG_PREFERENCE */]: "filter" /* FILTER */
2169
2365
  };
2170
2366
  ContentNavigator = class {
2171
2367
  /** User interface for this navigation session */
@@ -2190,6 +2386,52 @@ var init_navigators = __esm({
2190
2386
  this.strategyId = strategyData._id;
2191
2387
  }
2192
2388
  }
2389
+ // ============================================================================
2390
+ // STRATEGY STATE HELPERS
2391
+ // ============================================================================
2392
+ //
2393
+ // These methods allow strategies to persist their own state (user preferences,
2394
+ // learned patterns, temporal tracking) in the user database.
2395
+ //
2396
+ // ============================================================================
2397
+ /**
2398
+ * Unique key identifying this strategy for state storage.
2399
+ *
2400
+ * Defaults to the constructor name (e.g., "UserTagPreferenceFilter").
2401
+ * Override in subclasses if multiple instances of the same strategy type
2402
+ * need separate state storage.
2403
+ */
2404
+ get strategyKey() {
2405
+ return this.constructor.name;
2406
+ }
2407
+ /**
2408
+ * Get this strategy's persisted state for the current course.
2409
+ *
2410
+ * @returns The strategy's data payload, or null if no state exists
2411
+ * @throws Error if user or course is not initialized
2412
+ */
2413
+ async getStrategyState() {
2414
+ if (!this.user || !this.course) {
2415
+ throw new Error(
2416
+ `Cannot get strategy state: navigator not properly initialized. Ensure user and course are provided to constructor.`
2417
+ );
2418
+ }
2419
+ return this.user.getStrategyState(this.course.getCourseID(), this.strategyKey);
2420
+ }
2421
+ /**
2422
+ * Persist this strategy's state for the current course.
2423
+ *
2424
+ * @param data - The strategy's data payload to store
2425
+ * @throws Error if user or course is not initialized
2426
+ */
2427
+ async putStrategyState(data) {
2428
+ if (!this.user || !this.course) {
2429
+ throw new Error(
2430
+ `Cannot put strategy state: navigator not properly initialized. Ensure user and course are provided to constructor.`
2431
+ );
2432
+ }
2433
+ return this.user.putStrategyState(this.course.getCourseID(), this.strategyKey, data);
2434
+ }
2193
2435
  /**
2194
2436
  * Factory method to create navigator instances dynamically.
2195
2437
  *
@@ -2601,15 +2843,6 @@ var init_courseDB = __esm({
2601
2843
  ret[r.id] = r.doc.id_displayable_data;
2602
2844
  }
2603
2845
  });
2604
- await Promise.all(
2605
- cards.rows.map((r) => {
2606
- return async () => {
2607
- if (isSuccessRow(r)) {
2608
- ret[r.id] = r.doc.id_displayable_data;
2609
- }
2610
- };
2611
- })
2612
- );
2613
2846
  return ret;
2614
2847
  }
2615
2848
  async getCardsByELO(elo, cardLimit) {
@@ -2694,6 +2927,28 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
2694
2927
  throw new Error(`Failed to find tags for card ${this.id}-${cardId}`);
2695
2928
  }
2696
2929
  }
2930
+ async getAppliedTagsBatch(cardIds) {
2931
+ if (cardIds.length === 0) {
2932
+ return /* @__PURE__ */ new Map();
2933
+ }
2934
+ const db = getCourseDB2(this.id);
2935
+ const result = await db.query("getTags", {
2936
+ keys: cardIds,
2937
+ include_docs: false
2938
+ });
2939
+ const tagsByCard = /* @__PURE__ */ new Map();
2940
+ for (const cardId of cardIds) {
2941
+ tagsByCard.set(cardId, []);
2942
+ }
2943
+ for (const row of result.rows) {
2944
+ const cardId = row.key;
2945
+ const tagName = row.value?.name;
2946
+ if (tagName && tagsByCard.has(cardId)) {
2947
+ tagsByCard.get(cardId).push(tagName);
2948
+ }
2949
+ }
2950
+ return tagsByCard;
2951
+ }
2697
2952
  async addTagToCard(cardId, tagId, updateELO) {
2698
2953
  return await addTagToCard(
2699
2954
  this.id,
@@ -3619,6 +3874,16 @@ var init_user = __esm({
3619
3874
  }
3620
3875
  });
3621
3876
 
3877
+ // src/core/types/strategyState.ts
3878
+ function buildStrategyStateId(courseId, strategyKey) {
3879
+ return `STRATEGY_STATE::${courseId}::${strategyKey}`;
3880
+ }
3881
+ var init_strategyState = __esm({
3882
+ "src/core/types/strategyState.ts"() {
3883
+ "use strict";
3884
+ }
3885
+ });
3886
+
3622
3887
  // src/core/util/index.ts
3623
3888
  function getCardHistoryID(courseID, cardID) {
3624
3889
  return `${DocTypePrefixes["CARDRECORD" /* CARDRECORD */]}-${courseID}-${cardID}`;
@@ -3663,6 +3928,7 @@ var init_core = __esm({
3663
3928
  init_interfaces();
3664
3929
  init_types_legacy();
3665
3930
  init_user();
3931
+ init_strategyState();
3666
3932
  init_Loggable();
3667
3933
  init_util();
3668
3934
  init_navigators();
@@ -3850,7 +4116,9 @@ var init_user_course_relDB = __esm({
3850
4116
  function accomodateGuest() {
3851
4117
  logger.log("[funnel] accomodateGuest() called");
3852
4118
  if (typeof localStorage === "undefined") {
3853
- logger.log("[funnel] localStorage not available (Node.js environment), returning default guest");
4119
+ logger.log(
4120
+ "[funnel] localStorage not available (Node.js environment), returning default guest"
4121
+ );
3854
4122
  return {
3855
4123
  username: GuestUsername + "nodejs-test",
3856
4124
  firstVisit: true
@@ -4830,6 +5098,55 @@ Currently logged-in as ${this._username}.`
4830
5098
  async updateUserElo(courseId, elo) {
4831
5099
  return updateUserElo(this._username, courseId, elo);
4832
5100
  }
5101
+ async getStrategyState(courseId, strategyKey) {
5102
+ const docId = buildStrategyStateId(courseId, strategyKey);
5103
+ try {
5104
+ const doc = await this.localDB.get(docId);
5105
+ return doc.data;
5106
+ } catch (e) {
5107
+ const err = e;
5108
+ if (err.status === 404) {
5109
+ return null;
5110
+ }
5111
+ throw e;
5112
+ }
5113
+ }
5114
+ async putStrategyState(courseId, strategyKey, data) {
5115
+ const docId = buildStrategyStateId(courseId, strategyKey);
5116
+ let existingRev;
5117
+ try {
5118
+ const existing = await this.localDB.get(docId);
5119
+ existingRev = existing._rev;
5120
+ } catch (e) {
5121
+ const err = e;
5122
+ if (err.status !== 404) {
5123
+ throw e;
5124
+ }
5125
+ }
5126
+ const doc = {
5127
+ _id: docId,
5128
+ _rev: existingRev,
5129
+ docType: "STRATEGY_STATE" /* STRATEGY_STATE */,
5130
+ courseId,
5131
+ strategyKey,
5132
+ data,
5133
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
5134
+ };
5135
+ await this.localDB.put(doc);
5136
+ }
5137
+ async deleteStrategyState(courseId, strategyKey) {
5138
+ const docId = buildStrategyStateId(courseId, strategyKey);
5139
+ try {
5140
+ const doc = await this.localDB.get(docId);
5141
+ await this.localDB.remove(doc);
5142
+ } catch (e) {
5143
+ const err = e;
5144
+ if (err.status === 404) {
5145
+ return;
5146
+ }
5147
+ throw e;
5148
+ }
5149
+ }
4833
5150
  };
4834
5151
  userCoursesDoc = "CourseRegistrations";
4835
5152
  userClassroomsDoc = "ClassroomRegistrations";
@@ -4930,8 +5247,7 @@ var init_adminDB2 = __esm({
4930
5247
  }
4931
5248
  }
4932
5249
  }
4933
- const dbs = await Promise.all(promisedCRDbs);
4934
- return dbs.map((db) => {
5250
+ return promisedCRDbs.map((db) => {
4935
5251
  return {
4936
5252
  ...db.getConfig(),
4937
5253
  _id: db._id