@vue-skuilder/db 0.1.20 → 0.1.21

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 (70) hide show
  1. package/CLAUDE.md +2 -2
  2. package/dist/{classroomDB-CZdMBiTU.d.ts → contentSource-BP9hznNV.d.ts} +150 -196
  3. package/dist/{classroomDB-PxDZTky3.d.cts → contentSource-DsJadoBU.d.cts} +150 -196
  4. package/dist/core/index.d.cts +3 -3
  5. package/dist/core/index.d.ts +3 -3
  6. package/dist/core/index.js +615 -1758
  7. package/dist/core/index.js.map +1 -1
  8. package/dist/core/index.mjs +579 -1727
  9. package/dist/core/index.mjs.map +1 -1
  10. package/dist/{dataLayerProvider-D8o6ZnKW.d.ts → dataLayerProvider-CHYrQ5pB.d.cts} +1 -1
  11. package/dist/{dataLayerProvider-D0MoZMjH.d.cts → dataLayerProvider-MDTxXq2l.d.ts} +1 -1
  12. package/dist/impl/couch/index.d.cts +6 -22
  13. package/dist/impl/couch/index.d.ts +6 -22
  14. package/dist/impl/couch/index.js +598 -1769
  15. package/dist/impl/couch/index.js.map +1 -1
  16. package/dist/impl/couch/index.mjs +579 -1755
  17. package/dist/impl/couch/index.mjs.map +1 -1
  18. package/dist/impl/static/index.d.cts +22 -6
  19. package/dist/impl/static/index.d.ts +22 -6
  20. package/dist/impl/static/index.js +617 -1629
  21. package/dist/impl/static/index.js.map +1 -1
  22. package/dist/impl/static/index.mjs +607 -1624
  23. package/dist/impl/static/index.mjs.map +1 -1
  24. package/dist/index.d.cts +64 -56
  25. package/dist/index.d.ts +64 -56
  26. package/dist/index.js +1000 -2161
  27. package/dist/index.js.map +1 -1
  28. package/dist/index.mjs +970 -2127
  29. package/dist/index.mjs.map +1 -1
  30. package/dist/pouch/index.js +3 -0
  31. package/dist/pouch/index.js.map +1 -1
  32. package/dist/pouch/index.mjs +3 -0
  33. package/dist/pouch/index.mjs.map +1 -1
  34. package/docs/navigators-architecture.md +2 -9
  35. package/package.json +3 -3
  36. package/src/core/interfaces/classroomDB.ts +5 -13
  37. package/src/core/interfaces/contentSource.ts +6 -66
  38. package/src/core/interfaces/courseDB.ts +2 -7
  39. package/src/core/navigators/Pipeline.ts +24 -53
  40. package/src/core/navigators/PipelineAssembler.ts +1 -1
  41. package/src/core/navigators/defaults.ts +84 -0
  42. package/src/core/navigators/{hierarchyDefinition.ts → filters/hierarchyDefinition.ts} +11 -25
  43. package/src/core/navigators/{interferenceMitigator.ts → filters/interferenceMitigator.ts} +10 -24
  44. package/src/core/navigators/{relativePriority.ts → filters/relativePriority.ts} +10 -24
  45. package/src/core/navigators/filters/userTagPreference.ts +1 -16
  46. package/src/core/navigators/{CompositeGenerator.ts → generators/CompositeGenerator.ts} +15 -64
  47. package/src/core/navigators/{elo.ts → generators/elo.ts} +13 -63
  48. package/src/core/navigators/{srs.ts → generators/srs.ts} +11 -40
  49. package/src/core/navigators/generators/types.ts +1 -1
  50. package/src/core/navigators/index.ts +36 -91
  51. package/src/impl/couch/classroomDB.ts +100 -103
  52. package/src/impl/couch/courseDB.ts +5 -81
  53. package/src/impl/couch/pouchdb-setup.ts +7 -0
  54. package/src/impl/static/StaticDataUnpacker.ts +50 -1
  55. package/src/impl/static/courseDB.ts +76 -37
  56. package/src/study/SessionController.ts +122 -202
  57. package/src/study/SourceMixer.ts +65 -0
  58. package/src/study/TagFilteredContentSource.ts +49 -92
  59. package/src/study/index.ts +1 -0
  60. package/src/study/services/CardHydrationService.ts +165 -81
  61. package/src/util/dataDirectory.ts +1 -1
  62. package/src/util/index.ts +0 -1
  63. package/tests/core/navigators/CompositeGenerator.test.ts +44 -168
  64. package/tests/core/navigators/Pipeline.test.ts +5 -72
  65. package/tests/core/navigators/PipelineAssembler.test.ts +8 -58
  66. package/tests/core/navigators/navigators.test.ts +118 -151
  67. package/src/core/navigators/hardcodedOrder.ts +0 -163
  68. package/src/util/tuiLogger.ts +0 -139
  69. /package/src/core/navigators/{inferredPreference.ts → filters/inferredPreferenceStub.ts} +0 -0
  70. /package/src/core/navigators/{userGoal.ts → filters/userGoalStub.ts} +0 -0
@@ -5,11 +5,6 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- var __glob = (map) => (path2) => {
9
- var fn = map[path2];
10
- if (fn) return fn();
11
- throw new Error("Module not found in bundle: " + path2);
12
- };
13
8
  var __esm = (fn, res) => function __init() {
14
9
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
15
10
  };
@@ -188,6 +183,9 @@ var init_pouchdb_setup = __esm({
188
183
  import_pouchdb_authentication = __toESM(require("@nilock2/pouchdb-authentication"), 1);
189
184
  import_pouchdb.default.plugin(import_pouchdb_find.default);
190
185
  import_pouchdb.default.plugin(import_pouchdb_authentication.default);
186
+ if (typeof import_pouchdb.default.debug !== "undefined") {
187
+ import_pouchdb.default.debug.disable();
188
+ }
191
189
  import_pouchdb.default.defaults({
192
190
  // ajax: {
193
191
  // timeout: 60000,
@@ -197,14 +195,6 @@ var init_pouchdb_setup = __esm({
197
195
  }
198
196
  });
199
197
 
200
- // src/util/tuiLogger.ts
201
- var init_tuiLogger = __esm({
202
- "src/util/tuiLogger.ts"() {
203
- "use strict";
204
- init_dataDirectory();
205
- }
206
- });
207
-
208
198
  // src/util/dataDirectory.ts
209
199
  function getAppDataDirectory() {
210
200
  if (ENV.LOCAL_STORAGE_PREFIX) {
@@ -222,7 +212,7 @@ var init_dataDirectory = __esm({
222
212
  "use strict";
223
213
  path = __toESM(require("path"), 1);
224
214
  os = __toESM(require("os"), 1);
225
- init_tuiLogger();
215
+ init_logger();
226
216
  init_factory();
227
217
  }
228
218
  });
@@ -711,195 +701,187 @@ var init_courseLookupDB = __esm({
711
701
  }
712
702
  });
713
703
 
714
- // src/core/navigators/CompositeGenerator.ts
715
- var CompositeGenerator_exports = {};
716
- __export(CompositeGenerator_exports, {
717
- AggregationMode: () => AggregationMode,
718
- default: () => CompositeGenerator
719
- });
720
- var AggregationMode, DEFAULT_AGGREGATION_MODE, FREQUENCY_BOOST_FACTOR, CompositeGenerator;
721
- var init_CompositeGenerator = __esm({
722
- "src/core/navigators/CompositeGenerator.ts"() {
704
+ // src/core/navigators/index.ts
705
+ function getCardOrigin(card) {
706
+ if (card.provenance.length === 0) {
707
+ throw new Error("Card has no provenance - cannot determine origin");
708
+ }
709
+ const firstEntry = card.provenance[0];
710
+ const reason = firstEntry.reason.toLowerCase();
711
+ if (reason.includes("failed")) {
712
+ return "failed";
713
+ }
714
+ if (reason.includes("review")) {
715
+ return "review";
716
+ }
717
+ return "new";
718
+ }
719
+ function isGenerator(impl) {
720
+ return NavigatorRoles[impl] === "generator" /* GENERATOR */;
721
+ }
722
+ function isFilter(impl) {
723
+ return NavigatorRoles[impl] === "filter" /* FILTER */;
724
+ }
725
+ var Navigators, NavigatorRole, NavigatorRoles, ContentNavigator;
726
+ var init_navigators = __esm({
727
+ "src/core/navigators/index.ts"() {
723
728
  "use strict";
724
- init_navigators();
725
729
  init_logger();
726
- AggregationMode = /* @__PURE__ */ ((AggregationMode2) => {
727
- AggregationMode2["MAX"] = "max";
728
- AggregationMode2["AVERAGE"] = "average";
729
- AggregationMode2["FREQUENCY_BOOST"] = "frequencyBoost";
730
- return AggregationMode2;
731
- })(AggregationMode || {});
732
- DEFAULT_AGGREGATION_MODE = "frequencyBoost" /* FREQUENCY_BOOST */;
733
- FREQUENCY_BOOST_FACTOR = 0.1;
734
- CompositeGenerator = class _CompositeGenerator extends ContentNavigator {
735
- /** Human-readable name for CardGenerator interface */
736
- name = "Composite Generator";
737
- generators;
738
- aggregationMode;
739
- constructor(generators, aggregationMode = DEFAULT_AGGREGATION_MODE) {
740
- super();
741
- this.generators = generators;
742
- this.aggregationMode = aggregationMode;
743
- if (generators.length === 0) {
744
- throw new Error("CompositeGenerator requires at least one generator");
745
- }
746
- logger.debug(
747
- `[CompositeGenerator] Created with ${generators.length} generators, mode: ${aggregationMode}`
748
- );
749
- }
730
+ Navigators = /* @__PURE__ */ ((Navigators2) => {
731
+ Navigators2["ELO"] = "elo";
732
+ Navigators2["SRS"] = "srs";
733
+ Navigators2["HIERARCHY"] = "hierarchyDefinition";
734
+ Navigators2["INTERFERENCE"] = "interferenceMitigator";
735
+ Navigators2["RELATIVE_PRIORITY"] = "relativePriority";
736
+ Navigators2["USER_TAG_PREFERENCE"] = "userTagPreference";
737
+ return Navigators2;
738
+ })(Navigators || {});
739
+ NavigatorRole = /* @__PURE__ */ ((NavigatorRole2) => {
740
+ NavigatorRole2["GENERATOR"] = "generator";
741
+ NavigatorRole2["FILTER"] = "filter";
742
+ return NavigatorRole2;
743
+ })(NavigatorRole || {});
744
+ NavigatorRoles = {
745
+ ["elo" /* ELO */]: "generator" /* GENERATOR */,
746
+ ["srs" /* SRS */]: "generator" /* GENERATOR */,
747
+ ["hierarchyDefinition" /* HIERARCHY */]: "filter" /* FILTER */,
748
+ ["interferenceMitigator" /* INTERFERENCE */]: "filter" /* FILTER */,
749
+ ["relativePriority" /* RELATIVE_PRIORITY */]: "filter" /* FILTER */,
750
+ ["userTagPreference" /* USER_TAG_PREFERENCE */]: "filter" /* FILTER */
751
+ };
752
+ ContentNavigator = class {
753
+ /** User interface for this navigation session */
754
+ user;
755
+ /** Course interface for this navigation session */
756
+ course;
757
+ /** Human-readable name for this strategy instance (from ContentNavigationStrategyData.name) */
758
+ strategyName;
759
+ /** Unique document ID for this strategy instance (from ContentNavigationStrategyData._id) */
760
+ strategyId;
750
761
  /**
751
- * Creates a CompositeGenerator from strategy data.
762
+ * Constructor for standard navigators.
763
+ * Call this from subclass constructors to initialize common fields.
752
764
  *
753
- * This is a convenience factory for use by PipelineAssembler.
765
+ * Note: CompositeGenerator and Pipeline call super() without args, then set
766
+ * user/course fields directly if needed.
754
767
  */
755
- static async fromStrategies(user, course, strategies, aggregationMode = DEFAULT_AGGREGATION_MODE) {
756
- const generators = await Promise.all(
757
- strategies.map((s) => ContentNavigator.create(user, course, s))
758
- );
759
- return new _CompositeGenerator(generators, aggregationMode);
768
+ constructor(user, course, strategyData) {
769
+ this.user = user;
770
+ this.course = course;
771
+ if (strategyData) {
772
+ this.strategyName = strategyData.name;
773
+ this.strategyId = strategyData._id;
774
+ }
760
775
  }
776
+ // ============================================================================
777
+ // STRATEGY STATE HELPERS
778
+ // ============================================================================
779
+ //
780
+ // These methods allow strategies to persist their own state (user preferences,
781
+ // learned patterns, temporal tracking) in the user database.
782
+ //
783
+ // ============================================================================
761
784
  /**
762
- * Get weighted cards from all generators, merge and deduplicate.
763
- *
764
- * Cards appearing in multiple generators receive a score boost.
765
- * Provenance tracks which generators produced each card and how scores were aggregated.
766
- *
767
- * This method supports both the legacy signature (limit only) and the
768
- * CardGenerator interface signature (limit, context).
785
+ * Unique key identifying this strategy for state storage.
769
786
  *
770
- * @param limit - Maximum number of cards to return
771
- * @param context - Optional GeneratorContext passed to child generators
787
+ * Defaults to the constructor name (e.g., "UserTagPreferenceFilter").
788
+ * Override in subclasses if multiple instances of the same strategy type
789
+ * need separate state storage.
772
790
  */
773
- async getWeightedCards(limit, context) {
774
- const results = await Promise.all(
775
- this.generators.map((g) => g.getWeightedCards(limit, context))
776
- );
777
- const byCardId = /* @__PURE__ */ new Map();
778
- for (const cards of results) {
779
- for (const card of cards) {
780
- const existing = byCardId.get(card.cardId) || [];
781
- existing.push(card);
782
- byCardId.set(card.cardId, existing);
783
- }
784
- }
785
- const merged = [];
786
- for (const [, cards] of byCardId) {
787
- const aggregatedScore = this.aggregateScores(cards);
788
- const finalScore = Math.min(1, aggregatedScore);
789
- const mergedProvenance = cards.flatMap((c) => c.provenance);
790
- const initialScore = cards[0].score;
791
- const action = finalScore > initialScore ? "boosted" : finalScore < initialScore ? "penalized" : "passed";
792
- const reason = this.buildAggregationReason(cards, finalScore);
793
- merged.push({
794
- ...cards[0],
795
- score: finalScore,
796
- provenance: [
797
- ...mergedProvenance,
798
- {
799
- strategy: "composite",
800
- strategyName: "Composite Generator",
801
- strategyId: "COMPOSITE_GENERATOR",
802
- action,
803
- score: finalScore,
804
- reason
805
- }
806
- ]
807
- });
808
- }
809
- return merged.sort((a, b) => b.score - a.score).slice(0, limit);
791
+ get strategyKey() {
792
+ return this.constructor.name;
810
793
  }
811
794
  /**
812
- * Build human-readable reason for score aggregation.
795
+ * Get this strategy's persisted state for the current course.
796
+ *
797
+ * @returns The strategy's data payload, or null if no state exists
798
+ * @throws Error if user or course is not initialized
813
799
  */
814
- buildAggregationReason(cards, finalScore) {
815
- const count = cards.length;
816
- const scores = cards.map((c) => c.score.toFixed(2)).join(", ");
817
- if (count === 1) {
818
- return `Single generator, score ${finalScore.toFixed(2)}`;
819
- }
820
- const strategies = cards.map((c) => c.provenance[0]?.strategy || "unknown").join(", ");
821
- switch (this.aggregationMode) {
822
- case "max" /* MAX */:
823
- return `Max of ${count} generators (${strategies}): scores [${scores}] \u2192 ${finalScore.toFixed(2)}`;
824
- case "average" /* AVERAGE */:
825
- return `Average of ${count} generators (${strategies}): scores [${scores}] \u2192 ${finalScore.toFixed(2)}`;
826
- case "frequencyBoost" /* FREQUENCY_BOOST */: {
827
- const avg = cards.reduce((sum, c) => sum + c.score, 0) / count;
828
- const boost = 1 + FREQUENCY_BOOST_FACTOR * (count - 1);
829
- return `Frequency boost from ${count} generators (${strategies}): avg ${avg.toFixed(2)} \xD7 ${boost.toFixed(2)} \u2192 ${finalScore.toFixed(2)}`;
830
- }
831
- default:
832
- return `Aggregated from ${count} generators: ${finalScore.toFixed(2)}`;
800
+ async getStrategyState() {
801
+ if (!this.user || !this.course) {
802
+ throw new Error(
803
+ `Cannot get strategy state: navigator not properly initialized. Ensure user and course are provided to constructor.`
804
+ );
833
805
  }
806
+ return this.user.getStrategyState(this.course.getCourseID(), this.strategyKey);
834
807
  }
835
808
  /**
836
- * Aggregate scores from multiple generators for the same card.
809
+ * Persist this strategy's state for the current course.
810
+ *
811
+ * @param data - The strategy's data payload to store
812
+ * @throws Error if user or course is not initialized
837
813
  */
838
- aggregateScores(cards) {
839
- const scores = cards.map((c) => c.score);
840
- switch (this.aggregationMode) {
841
- case "max" /* MAX */:
842
- return Math.max(...scores);
843
- case "average" /* AVERAGE */:
844
- return scores.reduce((sum, s) => sum + s, 0) / scores.length;
845
- case "frequencyBoost" /* FREQUENCY_BOOST */: {
846
- const avg = scores.reduce((sum, s) => sum + s, 0) / scores.length;
847
- const frequencyBoost = 1 + FREQUENCY_BOOST_FACTOR * (cards.length - 1);
848
- return avg * frequencyBoost;
849
- }
850
- default:
851
- return scores[0];
814
+ async putStrategyState(data) {
815
+ if (!this.user || !this.course) {
816
+ throw new Error(
817
+ `Cannot put strategy state: navigator not properly initialized. Ensure user and course are provided to constructor.`
818
+ );
852
819
  }
820
+ return this.user.putStrategyState(this.course.getCourseID(), this.strategyKey, data);
853
821
  }
854
822
  /**
855
- * Get new cards from all generators, merged and deduplicated.
823
+ * Factory method to create navigator instances dynamically.
824
+ *
825
+ * @param user - User interface
826
+ * @param course - Course interface
827
+ * @param strategyData - Strategy configuration document
828
+ * @returns the runtime object used to steer a study session.
856
829
  */
857
- async getNewCards(n) {
858
- const legacyGenerators = this.generators.filter(
859
- (g) => g instanceof ContentNavigator
860
- );
861
- const results = await Promise.all(legacyGenerators.map((g) => g.getNewCards(n)));
862
- const seen = /* @__PURE__ */ new Set();
863
- const merged = [];
864
- for (const cards of results) {
865
- for (const card of cards) {
866
- if (!seen.has(card.cardID)) {
867
- seen.add(card.cardID);
868
- merged.push(card);
830
+ static async create(user, course, strategyData) {
831
+ const implementingClass = strategyData.implementingClass;
832
+ let NavigatorImpl;
833
+ const variations = [".ts", ".js", ""];
834
+ const dirs = ["filters", "generators"];
835
+ for (const ext of variations) {
836
+ for (const dir of dirs) {
837
+ const loadFrom = `./${dir}/${implementingClass}${ext}`;
838
+ try {
839
+ const module2 = await import(loadFrom);
840
+ NavigatorImpl = module2.default;
841
+ break;
842
+ } catch (e) {
843
+ logger.debug(`Failed to load extension from ${loadFrom}:`, e);
869
844
  }
870
845
  }
871
846
  }
872
- return n ? merged.slice(0, n) : merged;
847
+ if (!NavigatorImpl) {
848
+ throw new Error(`Could not load navigator implementation for: ${implementingClass}`);
849
+ }
850
+ return new NavigatorImpl(user, course, strategyData);
873
851
  }
874
852
  /**
875
- * Get pending reviews from all generators, merged and deduplicated.
853
+ * Get cards with suitability scores and provenance trails.
854
+ *
855
+ * **This is the PRIMARY API for navigation strategies.**
856
+ *
857
+ * Returns cards ranked by suitability score (0-1). Higher scores indicate
858
+ * better candidates for presentation. Each card includes a provenance trail
859
+ * documenting how strategies contributed to the final score.
860
+ *
861
+ * ## Implementation Required
862
+ * All navigation strategies MUST override this method. The base class does
863
+ * not provide a default implementation.
864
+ *
865
+ * ## For Generators
866
+ * Override this method to generate candidates and compute scores based on
867
+ * your strategy's logic (e.g., ELO proximity, review urgency). Create the
868
+ * initial provenance entry with action='generated'.
869
+ *
870
+ * ## For Filters
871
+ * Filters should implement the CardFilter interface instead and be composed
872
+ * via Pipeline. Filters do not directly implement getWeightedCards().
873
+ *
874
+ * @param limit - Maximum cards to return
875
+ * @returns Cards sorted by score descending, with provenance trails
876
876
  */
877
- async getPendingReviews() {
878
- const legacyGenerators = this.generators.filter(
879
- (g) => g instanceof ContentNavigator
880
- );
881
- const results = await Promise.all(legacyGenerators.map((g) => g.getPendingReviews()));
882
- const seen = /* @__PURE__ */ new Set();
883
- const merged = [];
884
- for (const reviews of results) {
885
- for (const review of reviews) {
886
- if (!seen.has(review.cardID)) {
887
- seen.add(review.cardID);
888
- merged.push(review);
889
- }
890
- }
891
- }
892
- return merged;
877
+ async getWeightedCards(_limit) {
878
+ throw new Error(`${this.constructor.name} must implement getWeightedCards(). `);
893
879
  }
894
880
  };
895
881
  }
896
882
  });
897
883
 
898
884
  // src/core/navigators/Pipeline.ts
899
- var Pipeline_exports = {};
900
- __export(Pipeline_exports, {
901
- Pipeline: () => Pipeline
902
- });
903
885
  function logPipelineConfig(generator, filters) {
904
886
  const filterList = filters.length > 0 ? "\n - " + filters.map((f) => f.name).join("\n - ") : " none";
905
887
  logger.info(
@@ -959,6 +941,11 @@ var init_Pipeline = __esm({
959
941
  this.filters = filters;
960
942
  this.user = user;
961
943
  this.course = course;
944
+ course.getCourseConfig().then((cfg) => {
945
+ logger.debug(`[pipeline] Crated pipeline for ${cfg.name}`);
946
+ }).catch((e) => {
947
+ logger.error(`[pipeline] Failed to lookup courseCfg: ${e}`);
948
+ });
962
949
  logPipelineConfig(generator, filters);
963
950
  }
964
951
  /**
@@ -995,7 +982,13 @@ var init_Pipeline = __esm({
995
982
  cards.sort((a, b) => b.score - a.score);
996
983
  const result = cards.slice(0, limit);
997
984
  const topScores = result.slice(0, 3).map((c) => c.score);
998
- logExecutionSummary(this.generator.name, generatedCount, this.filters.length, result.length, topScores);
985
+ logExecutionSummary(
986
+ this.generator.name,
987
+ generatedCount,
988
+ this.filters.length,
989
+ result.length,
990
+ topScores
991
+ );
999
992
  logCardProvenance(result, 3);
1000
993
  return result;
1001
994
  }
@@ -1044,48 +1037,155 @@ var init_Pipeline = __esm({
1044
1037
  userElo
1045
1038
  };
1046
1039
  }
1047
- // ===========================================================================
1048
- // Legacy StudyContentSource methods
1049
- // ===========================================================================
1050
- //
1051
- // These delegate to the generator for backward compatibility.
1052
- // Eventually SessionController will use getWeightedCards() exclusively.
1053
- //
1054
1040
  /**
1055
- * Get new cards via legacy API.
1056
- * Delegates to the generator if it supports the legacy interface.
1041
+ * Get the course ID for this pipeline.
1057
1042
  */
1058
- async getNewCards(n) {
1059
- if ("getNewCards" in this.generator && typeof this.generator.getNewCards === "function") {
1060
- return this.generator.getNewCards(n);
1043
+ getCourseID() {
1044
+ return this.course.getCourseID();
1045
+ }
1046
+ };
1047
+ }
1048
+ });
1049
+
1050
+ // src/core/navigators/generators/CompositeGenerator.ts
1051
+ var DEFAULT_AGGREGATION_MODE, FREQUENCY_BOOST_FACTOR, CompositeGenerator;
1052
+ var init_CompositeGenerator = __esm({
1053
+ "src/core/navigators/generators/CompositeGenerator.ts"() {
1054
+ "use strict";
1055
+ init_navigators();
1056
+ init_logger();
1057
+ DEFAULT_AGGREGATION_MODE = "frequencyBoost" /* FREQUENCY_BOOST */;
1058
+ FREQUENCY_BOOST_FACTOR = 0.1;
1059
+ CompositeGenerator = class _CompositeGenerator extends ContentNavigator {
1060
+ /** Human-readable name for CardGenerator interface */
1061
+ name = "Composite Generator";
1062
+ generators;
1063
+ aggregationMode;
1064
+ constructor(generators, aggregationMode = DEFAULT_AGGREGATION_MODE) {
1065
+ super();
1066
+ this.generators = generators;
1067
+ this.aggregationMode = aggregationMode;
1068
+ if (generators.length === 0) {
1069
+ throw new Error("CompositeGenerator requires at least one generator");
1061
1070
  }
1062
- return [];
1071
+ logger.debug(
1072
+ `[CompositeGenerator] Created with ${generators.length} generators, mode: ${aggregationMode}`
1073
+ );
1063
1074
  }
1064
1075
  /**
1065
- * Get pending reviews via legacy API.
1066
- * Delegates to the generator if it supports the legacy interface.
1076
+ * Creates a CompositeGenerator from strategy data.
1077
+ *
1078
+ * This is a convenience factory for use by PipelineAssembler.
1067
1079
  */
1068
- async getPendingReviews() {
1069
- if ("getPendingReviews" in this.generator && typeof this.generator.getPendingReviews === "function") {
1070
- return this.generator.getPendingReviews();
1080
+ static async fromStrategies(user, course, strategies, aggregationMode = DEFAULT_AGGREGATION_MODE) {
1081
+ const generators = await Promise.all(
1082
+ strategies.map((s) => ContentNavigator.create(user, course, s))
1083
+ );
1084
+ return new _CompositeGenerator(generators, aggregationMode);
1085
+ }
1086
+ /**
1087
+ * Get weighted cards from all generators, merge and deduplicate.
1088
+ *
1089
+ * Cards appearing in multiple generators receive a score boost.
1090
+ * Provenance tracks which generators produced each card and how scores were aggregated.
1091
+ *
1092
+ * This method supports both the legacy signature (limit only) and the
1093
+ * CardGenerator interface signature (limit, context).
1094
+ *
1095
+ * @param limit - Maximum number of cards to return
1096
+ * @param context - GeneratorContext passed to child generators (required when called via Pipeline)
1097
+ */
1098
+ async getWeightedCards(limit, context) {
1099
+ if (!context) {
1100
+ throw new Error(
1101
+ "CompositeGenerator.getWeightedCards requires a GeneratorContext. It should be called via Pipeline, not directly."
1102
+ );
1103
+ }
1104
+ const results = await Promise.all(
1105
+ this.generators.map((g) => g.getWeightedCards(limit, context))
1106
+ );
1107
+ const byCardId = /* @__PURE__ */ new Map();
1108
+ for (const cards of results) {
1109
+ for (const card of cards) {
1110
+ const existing = byCardId.get(card.cardId) || [];
1111
+ existing.push(card);
1112
+ byCardId.set(card.cardId, existing);
1113
+ }
1114
+ }
1115
+ const merged = [];
1116
+ for (const [, cards] of byCardId) {
1117
+ const aggregatedScore = this.aggregateScores(cards);
1118
+ const finalScore = Math.min(1, aggregatedScore);
1119
+ const mergedProvenance = cards.flatMap((c) => c.provenance);
1120
+ const initialScore = cards[0].score;
1121
+ const action = finalScore > initialScore ? "boosted" : finalScore < initialScore ? "penalized" : "passed";
1122
+ const reason = this.buildAggregationReason(cards, finalScore);
1123
+ merged.push({
1124
+ ...cards[0],
1125
+ score: finalScore,
1126
+ provenance: [
1127
+ ...mergedProvenance,
1128
+ {
1129
+ strategy: "composite",
1130
+ strategyName: "Composite Generator",
1131
+ strategyId: "COMPOSITE_GENERATOR",
1132
+ action,
1133
+ score: finalScore,
1134
+ reason
1135
+ }
1136
+ ]
1137
+ });
1138
+ }
1139
+ return merged.sort((a, b) => b.score - a.score).slice(0, limit);
1140
+ }
1141
+ /**
1142
+ * Build human-readable reason for score aggregation.
1143
+ */
1144
+ buildAggregationReason(cards, finalScore) {
1145
+ const count = cards.length;
1146
+ const scores = cards.map((c) => c.score.toFixed(2)).join(", ");
1147
+ if (count === 1) {
1148
+ return `Single generator, score ${finalScore.toFixed(2)}`;
1149
+ }
1150
+ const strategies = cards.map((c) => c.provenance[0]?.strategy || "unknown").join(", ");
1151
+ switch (this.aggregationMode) {
1152
+ case "max" /* MAX */:
1153
+ return `Max of ${count} generators (${strategies}): scores [${scores}] \u2192 ${finalScore.toFixed(2)}`;
1154
+ case "average" /* AVERAGE */:
1155
+ return `Average of ${count} generators (${strategies}): scores [${scores}] \u2192 ${finalScore.toFixed(2)}`;
1156
+ case "frequencyBoost" /* FREQUENCY_BOOST */: {
1157
+ const avg = cards.reduce((sum, c) => sum + c.score, 0) / count;
1158
+ const boost = 1 + FREQUENCY_BOOST_FACTOR * (count - 1);
1159
+ return `Frequency boost from ${count} generators (${strategies}): avg ${avg.toFixed(2)} \xD7 ${boost.toFixed(2)} \u2192 ${finalScore.toFixed(2)}`;
1160
+ }
1161
+ default:
1162
+ return `Aggregated from ${count} generators: ${finalScore.toFixed(2)}`;
1071
1163
  }
1072
- return [];
1073
1164
  }
1074
1165
  /**
1075
- * Get the course ID for this pipeline.
1166
+ * Aggregate scores from multiple generators for the same card.
1076
1167
  */
1077
- getCourseID() {
1078
- return this.course.getCourseID();
1168
+ aggregateScores(cards) {
1169
+ const scores = cards.map((c) => c.score);
1170
+ switch (this.aggregationMode) {
1171
+ case "max" /* MAX */:
1172
+ return Math.max(...scores);
1173
+ case "average" /* AVERAGE */:
1174
+ return scores.reduce((sum, s) => sum + s, 0) / scores.length;
1175
+ case "frequencyBoost" /* FREQUENCY_BOOST */: {
1176
+ const avg = scores.reduce((sum, s) => sum + s, 0) / scores.length;
1177
+ const frequencyBoost = 1 + FREQUENCY_BOOST_FACTOR * (cards.length - 1);
1178
+ return avg * frequencyBoost;
1179
+ }
1180
+ default:
1181
+ return scores[0];
1182
+ }
1079
1183
  }
1080
1184
  };
1081
1185
  }
1082
1186
  });
1083
1187
 
1084
1188
  // src/core/navigators/PipelineAssembler.ts
1085
- var PipelineAssembler_exports = {};
1086
- __export(PipelineAssembler_exports, {
1087
- PipelineAssembler: () => PipelineAssembler
1088
- });
1089
1189
  var PipelineAssembler;
1090
1190
  var init_PipelineAssembler = __esm({
1091
1191
  "src/core/navigators/PipelineAssembler.ts"() {
@@ -1206,14 +1306,10 @@ var init_PipelineAssembler = __esm({
1206
1306
  }
1207
1307
  });
1208
1308
 
1209
- // src/core/navigators/elo.ts
1210
- var elo_exports = {};
1211
- __export(elo_exports, {
1212
- default: () => ELONavigator
1213
- });
1309
+ // src/core/navigators/generators/elo.ts
1214
1310
  var import_common6, ELONavigator;
1215
1311
  var init_elo = __esm({
1216
- "src/core/navigators/elo.ts"() {
1312
+ "src/core/navigators/generators/elo.ts"() {
1217
1313
  "use strict";
1218
1314
  init_navigators();
1219
1315
  import_common6 = require("@vue-skuilder/common");
@@ -1224,50 +1320,6 @@ var init_elo = __esm({
1224
1320
  super(user, course, strategyData);
1225
1321
  this.name = strategyData?.name || "ELO";
1226
1322
  }
1227
- async getPendingReviews() {
1228
- const reviews = await this.user.getPendingReviews(this.course.getCourseID());
1229
- const elo = await this.course.getCardEloData(reviews.map((r) => r.cardId));
1230
- const ratedReviews = reviews.map((r, i) => {
1231
- const ratedR = {
1232
- ...r,
1233
- ...elo[i]
1234
- };
1235
- return ratedR;
1236
- });
1237
- ratedReviews.sort((a, b) => {
1238
- return a.global.score - b.global.score;
1239
- });
1240
- return ratedReviews.map((r) => {
1241
- return {
1242
- ...r,
1243
- contentSourceType: "course",
1244
- contentSourceID: this.course.getCourseID(),
1245
- cardID: r.cardId,
1246
- courseID: r.courseId,
1247
- qualifiedID: `${r.courseId}-${r.cardId}`,
1248
- reviewID: r._id,
1249
- status: "review"
1250
- };
1251
- });
1252
- }
1253
- async getNewCards(limit = 99) {
1254
- const activeCards = await this.user.getActiveCards();
1255
- return (await this.course.getCardsCenteredAtELO(
1256
- { limit, elo: "user" },
1257
- (c) => {
1258
- if (activeCards.some((ac) => c.cardID === ac.cardID)) {
1259
- return false;
1260
- } else {
1261
- return true;
1262
- }
1263
- }
1264
- )).map((c) => {
1265
- return {
1266
- ...c,
1267
- status: "new"
1268
- };
1269
- });
1270
- }
1271
1323
  /**
1272
1324
  * Get new cards with suitability scores based on ELO distance.
1273
1325
  *
@@ -1292,7 +1344,11 @@ var init_elo = __esm({
1292
1344
  const userElo = (0, import_common6.toCourseElo)(courseReg.elo);
1293
1345
  userGlobalElo = userElo.global.score;
1294
1346
  }
1295
- const newCards = await this.getNewCards(limit);
1347
+ const activeCards = await this.user.getActiveCards();
1348
+ const newCards = (await this.course.getCardsCenteredAtELO(
1349
+ { limit, elo: "user" },
1350
+ (c) => !activeCards.some((ac) => c.cardID === ac.cardID)
1351
+ )).map((c) => ({ ...c, status: "new" }));
1296
1352
  const cardIds = newCards.map((c) => c.cardID);
1297
1353
  const cardEloData = await this.course.getCardEloData(cardIds);
1298
1354
  const scored = newCards.map((c, i) => {
@@ -1322,950 +1378,39 @@ var init_elo = __esm({
1322
1378
  }
1323
1379
  });
1324
1380
 
1325
- // src/core/navigators/filters/eloDistance.ts
1326
- var eloDistance_exports = {};
1327
- __export(eloDistance_exports, {
1328
- DEFAULT_HALF_LIFE: () => DEFAULT_HALF_LIFE,
1329
- DEFAULT_MAX_MULTIPLIER: () => DEFAULT_MAX_MULTIPLIER,
1330
- DEFAULT_MIN_MULTIPLIER: () => DEFAULT_MIN_MULTIPLIER,
1331
- createEloDistanceFilter: () => createEloDistanceFilter
1332
- });
1333
- function computeMultiplier(distance, halfLife, minMultiplier, maxMultiplier) {
1334
- const normalizedDistance = distance / halfLife;
1335
- const decay = Math.exp(-(normalizedDistance * normalizedDistance));
1336
- return minMultiplier + (maxMultiplier - minMultiplier) * decay;
1337
- }
1338
- function createEloDistanceFilter(config) {
1339
- const halfLife = config?.halfLife ?? DEFAULT_HALF_LIFE;
1340
- const minMultiplier = config?.minMultiplier ?? DEFAULT_MIN_MULTIPLIER;
1341
- const maxMultiplier = config?.maxMultiplier ?? DEFAULT_MAX_MULTIPLIER;
1342
- return {
1343
- name: "ELO Distance Filter",
1344
- async transform(cards, context) {
1345
- const { course, userElo } = context;
1346
- const cardIds = cards.map((c) => c.cardId);
1347
- const cardElos = await course.getCardEloData(cardIds);
1348
- return cards.map((card, i) => {
1349
- const cardElo = cardElos[i]?.global?.score ?? 1e3;
1350
- const distance = Math.abs(cardElo - userElo);
1351
- const multiplier = computeMultiplier(distance, halfLife, minMultiplier, maxMultiplier);
1352
- const newScore = card.score * multiplier;
1353
- const action = multiplier < maxMultiplier - 0.01 ? "penalized" : "passed";
1354
- return {
1355
- ...card,
1356
- score: newScore,
1357
- provenance: [
1358
- ...card.provenance,
1359
- {
1360
- strategy: "eloDistance",
1361
- strategyName: "ELO Distance Filter",
1362
- strategyId: "ELO_DISTANCE_FILTER",
1363
- action,
1364
- score: newScore,
1365
- reason: `ELO distance ${Math.round(distance)} (card: ${Math.round(cardElo)}, user: ${Math.round(userElo)}) \u2192 ${multiplier.toFixed(2)}x`
1366
- }
1367
- ]
1368
- };
1369
- });
1370
- }
1371
- };
1372
- }
1373
- var DEFAULT_HALF_LIFE, DEFAULT_MIN_MULTIPLIER, DEFAULT_MAX_MULTIPLIER;
1374
- var init_eloDistance = __esm({
1375
- "src/core/navigators/filters/eloDistance.ts"() {
1376
- "use strict";
1377
- DEFAULT_HALF_LIFE = 200;
1378
- DEFAULT_MIN_MULTIPLIER = 0.3;
1379
- DEFAULT_MAX_MULTIPLIER = 1;
1380
- }
1381
- });
1382
-
1383
- // src/core/navigators/filters/userTagPreference.ts
1384
- var userTagPreference_exports = {};
1385
- __export(userTagPreference_exports, {
1386
- default: () => UserTagPreferenceFilter
1387
- });
1388
- var UserTagPreferenceFilter;
1389
- var init_userTagPreference = __esm({
1390
- "src/core/navigators/filters/userTagPreference.ts"() {
1381
+ // src/core/navigators/generators/srs.ts
1382
+ var import_moment3, SRSNavigator;
1383
+ var init_srs = __esm({
1384
+ "src/core/navigators/generators/srs.ts"() {
1391
1385
  "use strict";
1386
+ import_moment3 = __toESM(require("moment"), 1);
1392
1387
  init_navigators();
1393
- UserTagPreferenceFilter = class extends ContentNavigator {
1394
- _strategyData;
1395
- /** Human-readable name for CardFilter interface */
1388
+ init_logger();
1389
+ SRSNavigator = class extends ContentNavigator {
1390
+ /** Human-readable name for CardGenerator interface */
1396
1391
  name;
1397
1392
  constructor(user, course, strategyData) {
1398
1393
  super(user, course, strategyData);
1399
- this._strategyData = strategyData;
1400
- this.name = strategyData.name || "User Tag Preferences";
1401
- }
1402
- /**
1403
- * Compute multiplier for a card based on its tags and user preferences.
1404
- * Returns the maximum multiplier among all matching tags, or 1.0 if no matches.
1405
- */
1406
- computeMultiplier(cardTags, boostMap) {
1407
- const multipliers = cardTags.map((tag) => boostMap[tag]).filter((val) => val !== void 0);
1408
- if (multipliers.length === 0) {
1409
- return 1;
1410
- }
1411
- return Math.max(...multipliers);
1412
- }
1413
- /**
1414
- * Build human-readable reason for the filter's decision.
1415
- */
1416
- buildReason(cardTags, boostMap, multiplier) {
1417
- const matchingTags = cardTags.filter((tag) => boostMap[tag] === multiplier);
1418
- if (multiplier === 0) {
1419
- return `Excluded by user preference: ${matchingTags.join(", ")} (${multiplier}x)`;
1420
- }
1421
- if (multiplier < 1) {
1422
- return `Penalized by user preference: ${matchingTags.join(", ")} (${multiplier.toFixed(2)}x)`;
1423
- }
1424
- if (multiplier > 1) {
1425
- return `Boosted by user preference: ${matchingTags.join(", ")} (${multiplier.toFixed(2)}x)`;
1426
- }
1427
- return "No matching user preferences";
1394
+ this.name = strategyData?.name || "SRS";
1428
1395
  }
1429
1396
  /**
1430
- * CardFilter.transform implementation.
1397
+ * Get review cards scored by urgency.
1398
+ *
1399
+ * Score formula combines:
1400
+ * - Relative overdueness: hoursOverdue / intervalHours
1401
+ * - Interval recency: exponential decay favoring shorter intervals
1402
+ *
1403
+ * Cards not yet due are excluded (not scored as 0).
1404
+ *
1405
+ * This method supports both the legacy signature (limit only) and the
1406
+ * CardGenerator interface signature (limit, context).
1431
1407
  *
1432
- * Apply user tag preferences:
1433
- * 1. Read preferences from strategy state
1434
- * 2. If no preferences, pass through unchanged
1435
- * 3. For each card:
1436
- * - Look up tag in boost record
1437
- * - If tag found: apply multiplier (0 = exclude, 1 = neutral, >1 = boost)
1438
- * - If multiple tags match: use max multiplier
1439
- * - Append provenance with clear reason
1408
+ * @param limit - Maximum number of cards to return
1409
+ * @param _context - Optional GeneratorContext (currently unused, but required for interface)
1440
1410
  */
1441
- async transform(cards, _context) {
1442
- const prefs = await this.getStrategyState();
1443
- if (!prefs || Object.keys(prefs.boost).length === 0) {
1444
- return cards.map((card) => ({
1445
- ...card,
1446
- provenance: [
1447
- ...card.provenance,
1448
- {
1449
- strategy: "userTagPreference",
1450
- strategyName: this.strategyName || this.name,
1451
- strategyId: this.strategyId || this._strategyData._id,
1452
- action: "passed",
1453
- score: card.score,
1454
- reason: "No user tag preferences configured"
1455
- }
1456
- ]
1457
- }));
1458
- }
1459
- const adjusted = await Promise.all(
1460
- cards.map(async (card) => {
1461
- const cardTags = card.tags ?? [];
1462
- const multiplier = this.computeMultiplier(cardTags, prefs.boost);
1463
- const finalScore = Math.min(1, card.score * multiplier);
1464
- let action;
1465
- if (multiplier === 0 || multiplier < 1) {
1466
- action = "penalized";
1467
- } else if (multiplier > 1) {
1468
- action = "boosted";
1469
- } else {
1470
- action = "passed";
1471
- }
1472
- return {
1473
- ...card,
1474
- score: finalScore,
1475
- provenance: [
1476
- ...card.provenance,
1477
- {
1478
- strategy: "userTagPreference",
1479
- strategyName: this.strategyName || this.name,
1480
- strategyId: this.strategyId || this._strategyData._id,
1481
- action,
1482
- score: finalScore,
1483
- reason: this.buildReason(cardTags, prefs.boost, multiplier)
1484
- }
1485
- ]
1486
- };
1487
- })
1488
- );
1489
- return adjusted;
1490
- }
1491
- /**
1492
- * Legacy getWeightedCards - throws as filters should not be used as generators.
1493
- */
1494
- async getWeightedCards(_limit) {
1495
- throw new Error(
1496
- "UserTagPreferenceFilter is a filter and should not be used as a generator. Use Pipeline with a generator and this filter via transform()."
1497
- );
1498
- }
1499
- // Legacy methods - stub implementations since filters don't generate cards
1500
- async getNewCards(_n) {
1501
- return [];
1502
- }
1503
- async getPendingReviews() {
1504
- return [];
1505
- }
1506
- };
1507
- }
1508
- });
1509
-
1510
- // src/core/navigators/filters/index.ts
1511
- var filters_exports = {};
1512
- __export(filters_exports, {
1513
- UserTagPreferenceFilter: () => UserTagPreferenceFilter,
1514
- createEloDistanceFilter: () => createEloDistanceFilter
1515
- });
1516
- var init_filters = __esm({
1517
- "src/core/navigators/filters/index.ts"() {
1518
- "use strict";
1519
- init_eloDistance();
1520
- init_userTagPreference();
1521
- }
1522
- });
1523
-
1524
- // src/core/navigators/filters/types.ts
1525
- var types_exports = {};
1526
- var init_types = __esm({
1527
- "src/core/navigators/filters/types.ts"() {
1528
- "use strict";
1529
- }
1530
- });
1531
-
1532
- // src/core/navigators/generators/index.ts
1533
- var generators_exports = {};
1534
- var init_generators = __esm({
1535
- "src/core/navigators/generators/index.ts"() {
1536
- "use strict";
1537
- }
1538
- });
1539
-
1540
- // src/core/navigators/generators/types.ts
1541
- var types_exports2 = {};
1542
- var init_types2 = __esm({
1543
- "src/core/navigators/generators/types.ts"() {
1544
- "use strict";
1545
- }
1546
- });
1547
-
1548
- // src/core/navigators/hardcodedOrder.ts
1549
- var hardcodedOrder_exports = {};
1550
- __export(hardcodedOrder_exports, {
1551
- default: () => HardcodedOrderNavigator
1552
- });
1553
- var HardcodedOrderNavigator;
1554
- var init_hardcodedOrder = __esm({
1555
- "src/core/navigators/hardcodedOrder.ts"() {
1556
- "use strict";
1557
- init_navigators();
1558
- init_logger();
1559
- HardcodedOrderNavigator = class extends ContentNavigator {
1560
- /** Human-readable name for CardGenerator interface */
1561
- name;
1562
- orderedCardIds = [];
1563
- constructor(user, course, strategyData) {
1564
- super(user, course, strategyData);
1565
- this.name = strategyData.name || "Hardcoded Order";
1566
- if (strategyData.serializedData) {
1567
- try {
1568
- this.orderedCardIds = JSON.parse(strategyData.serializedData);
1569
- } catch (e) {
1570
- logger.error("Failed to parse serializedData for HardcodedOrderNavigator", e);
1571
- }
1572
- }
1573
- }
1574
- async getPendingReviews() {
1575
- const reviews = await this.user.getPendingReviews(this.course.getCourseID());
1576
- return reviews.map((r) => {
1577
- return {
1578
- ...r,
1579
- contentSourceType: "course",
1580
- contentSourceID: this.course.getCourseID(),
1581
- cardID: r.cardId,
1582
- courseID: r.courseId,
1583
- reviewID: r._id,
1584
- status: "review"
1585
- };
1586
- });
1587
- }
1588
- async getNewCards(limit = 99) {
1589
- const activeCardIds = (await this.user.getActiveCards()).map((c) => c.cardID);
1590
- const newCardIds = this.orderedCardIds.filter((cardId) => !activeCardIds.includes(cardId));
1591
- const cardsToReturn = newCardIds.slice(0, limit);
1592
- return cardsToReturn.map((cardId) => {
1593
- return {
1594
- cardID: cardId,
1595
- courseID: this.course.getCourseID(),
1596
- contentSourceType: "course",
1597
- contentSourceID: this.course.getCourseID(),
1598
- status: "new"
1599
- };
1600
- });
1601
- }
1602
- /**
1603
- * Get cards in hardcoded order with scores based on position.
1604
- *
1605
- * Earlier cards in the sequence get higher scores.
1606
- * Score formula: 1.0 - (position / totalCards) * 0.5
1607
- * This ensures scores range from 1.0 (first card) to 0.5+ (last card).
1608
- *
1609
- * This method supports both the legacy signature (limit only) and the
1610
- * CardGenerator interface signature (limit, context).
1611
- *
1612
- * @param limit - Maximum number of cards to return
1613
- * @param _context - Optional GeneratorContext (currently unused, but required for interface)
1614
- */
1615
- async getWeightedCards(limit, _context) {
1616
- const activeCardIds = (await this.user.getActiveCards()).map((c) => c.cardID);
1617
- const reviews = await this.getPendingReviews();
1618
- const newCardIds = this.orderedCardIds.filter((cardId) => !activeCardIds.includes(cardId));
1619
- const totalCards = newCardIds.length;
1620
- const scoredNew = newCardIds.slice(0, limit).map((cardId, index) => {
1621
- const position = index + 1;
1622
- const score = Math.max(0.5, 1 - index / totalCards * 0.5);
1623
- return {
1624
- cardId,
1625
- courseId: this.course.getCourseID(),
1626
- score,
1627
- provenance: [
1628
- {
1629
- strategy: "hardcodedOrder",
1630
- strategyName: this.strategyName || this.name,
1631
- strategyId: this.strategyId || "NAVIGATION_STRATEGY-hardcoded",
1632
- action: "generated",
1633
- score,
1634
- reason: `Position ${position} of ${totalCards} in fixed sequence, new card`
1635
- }
1636
- ]
1637
- };
1638
- });
1639
- const scoredReviews = reviews.map((r) => ({
1640
- cardId: r.cardID,
1641
- courseId: r.courseID,
1642
- score: 1,
1643
- provenance: [
1644
- {
1645
- strategy: "hardcodedOrder",
1646
- strategyName: this.strategyName || this.name,
1647
- strategyId: this.strategyId || "NAVIGATION_STRATEGY-hardcoded",
1648
- action: "generated",
1649
- score: 1,
1650
- reason: "Scheduled review, highest priority"
1651
- }
1652
- ]
1653
- }));
1654
- const all = [...scoredReviews, ...scoredNew];
1655
- all.sort((a, b) => b.score - a.score);
1656
- return all.slice(0, limit);
1657
- }
1658
- };
1659
- }
1660
- });
1661
-
1662
- // src/core/navigators/hierarchyDefinition.ts
1663
- var hierarchyDefinition_exports = {};
1664
- __export(hierarchyDefinition_exports, {
1665
- default: () => HierarchyDefinitionNavigator
1666
- });
1667
- var import_common7, DEFAULT_MIN_COUNT, HierarchyDefinitionNavigator;
1668
- var init_hierarchyDefinition = __esm({
1669
- "src/core/navigators/hierarchyDefinition.ts"() {
1670
- "use strict";
1671
- init_navigators();
1672
- import_common7 = require("@vue-skuilder/common");
1673
- DEFAULT_MIN_COUNT = 3;
1674
- HierarchyDefinitionNavigator = class extends ContentNavigator {
1675
- config;
1676
- _strategyData;
1677
- /** Human-readable name for CardFilter interface */
1678
- name;
1679
- constructor(user, course, _strategyData) {
1680
- super(user, course, _strategyData);
1681
- this._strategyData = _strategyData;
1682
- this.config = this.parseConfig(_strategyData.serializedData);
1683
- this.name = _strategyData.name || "Hierarchy Definition";
1684
- }
1685
- parseConfig(serializedData) {
1686
- try {
1687
- const parsed = JSON.parse(serializedData);
1688
- return {
1689
- prerequisites: parsed.prerequisites || {}
1690
- };
1691
- } catch {
1692
- return {
1693
- prerequisites: {}
1694
- };
1695
- }
1696
- }
1697
- /**
1698
- * Check if a specific prerequisite is satisfied
1699
- */
1700
- isPrerequisiteMet(prereq, userTagElo, userGlobalElo) {
1701
- if (!userTagElo) return false;
1702
- const minCount = prereq.masteryThreshold?.minCount ?? DEFAULT_MIN_COUNT;
1703
- if (userTagElo.count < minCount) return false;
1704
- if (prereq.masteryThreshold?.minElo !== void 0) {
1705
- return userTagElo.score >= prereq.masteryThreshold.minElo;
1706
- } else {
1707
- return userTagElo.score >= userGlobalElo;
1708
- }
1709
- }
1710
- /**
1711
- * Get the set of tags the user has mastered.
1712
- * A tag is "mastered" if it appears as a prerequisite somewhere and meets its threshold.
1713
- */
1714
- async getMasteredTags(context) {
1715
- const mastered = /* @__PURE__ */ new Set();
1716
- try {
1717
- const courseReg = await context.user.getCourseRegDoc(context.course.getCourseID());
1718
- const userElo = (0, import_common7.toCourseElo)(courseReg.elo);
1719
- for (const prereqs of Object.values(this.config.prerequisites)) {
1720
- for (const prereq of prereqs) {
1721
- const tagElo = userElo.tags[prereq.tag];
1722
- if (this.isPrerequisiteMet(prereq, tagElo, userElo.global.score)) {
1723
- mastered.add(prereq.tag);
1724
- }
1725
- }
1726
- }
1727
- } catch {
1728
- }
1729
- return mastered;
1730
- }
1731
- /**
1732
- * Get the set of tags that are unlocked (prerequisites met)
1733
- */
1734
- getUnlockedTags(masteredTags) {
1735
- const unlocked = /* @__PURE__ */ new Set();
1736
- for (const [tagId, prereqs] of Object.entries(this.config.prerequisites)) {
1737
- const allPrereqsMet = prereqs.every((prereq) => masteredTags.has(prereq.tag));
1738
- if (allPrereqsMet) {
1739
- unlocked.add(tagId);
1740
- }
1741
- }
1742
- return unlocked;
1743
- }
1744
- /**
1745
- * Check if a tag has prerequisites defined in config
1746
- */
1747
- hasPrerequisites(tagId) {
1748
- return tagId in this.config.prerequisites;
1749
- }
1750
- /**
1751
- * Check if a card is unlocked and generate reason.
1752
- */
1753
- async checkCardUnlock(card, course, unlockedTags, masteredTags) {
1754
- try {
1755
- const cardTags = card.tags ?? [];
1756
- const lockedTags = cardTags.filter(
1757
- (tag) => this.hasPrerequisites(tag) && !unlockedTags.has(tag)
1758
- );
1759
- if (lockedTags.length === 0) {
1760
- const tagList = cardTags.length > 0 ? cardTags.join(", ") : "none";
1761
- return {
1762
- isUnlocked: true,
1763
- reason: `Prerequisites met, tags: ${tagList}`
1764
- };
1765
- }
1766
- const missingPrereqs = lockedTags.flatMap((tag) => {
1767
- const prereqs = this.config.prerequisites[tag] || [];
1768
- return prereqs.filter((p) => !masteredTags.has(p.tag)).map((p) => p.tag);
1769
- });
1770
- return {
1771
- isUnlocked: false,
1772
- reason: `Blocked: missing prerequisites ${missingPrereqs.join(", ")} for tags ${lockedTags.join(", ")}`
1773
- };
1774
- } catch {
1775
- return {
1776
- isUnlocked: true,
1777
- reason: "Prerequisites check skipped (tag lookup failed)"
1778
- };
1779
- }
1780
- }
1781
- /**
1782
- * CardFilter.transform implementation.
1783
- *
1784
- * Apply prerequisite gating to cards. Cards with locked tags receive score: 0.
1785
- */
1786
- async transform(cards, context) {
1787
- const masteredTags = await this.getMasteredTags(context);
1788
- const unlockedTags = this.getUnlockedTags(masteredTags);
1789
- const gated = [];
1790
- for (const card of cards) {
1791
- const { isUnlocked, reason } = await this.checkCardUnlock(
1792
- card,
1793
- context.course,
1794
- unlockedTags,
1795
- masteredTags
1796
- );
1797
- const finalScore = isUnlocked ? card.score : 0;
1798
- const action = isUnlocked ? "passed" : "penalized";
1799
- gated.push({
1800
- ...card,
1801
- score: finalScore,
1802
- provenance: [
1803
- ...card.provenance,
1804
- {
1805
- strategy: "hierarchyDefinition",
1806
- strategyName: this.strategyName || this.name,
1807
- strategyId: this.strategyId || "NAVIGATION_STRATEGY-hierarchy",
1808
- action,
1809
- score: finalScore,
1810
- reason
1811
- }
1812
- ]
1813
- });
1814
- }
1815
- return gated;
1816
- }
1817
- /**
1818
- * Legacy getWeightedCards - now throws as filters should not be used as generators.
1819
- *
1820
- * Use transform() via Pipeline instead.
1821
- */
1822
- async getWeightedCards(_limit) {
1823
- throw new Error(
1824
- "HierarchyDefinitionNavigator is a filter and should not be used as a generator. Use Pipeline with a generator and this filter via transform()."
1825
- );
1826
- }
1827
- // Legacy methods - stub implementations since filters don't generate cards
1828
- async getNewCards(_n) {
1829
- return [];
1830
- }
1831
- async getPendingReviews() {
1832
- return [];
1833
- }
1834
- };
1835
- }
1836
- });
1837
-
1838
- // src/core/navigators/inferredPreference.ts
1839
- var inferredPreference_exports = {};
1840
- __export(inferredPreference_exports, {
1841
- INFERRED_PREFERENCE_NAVIGATOR_STUB: () => INFERRED_PREFERENCE_NAVIGATOR_STUB
1842
- });
1843
- var INFERRED_PREFERENCE_NAVIGATOR_STUB;
1844
- var init_inferredPreference = __esm({
1845
- "src/core/navigators/inferredPreference.ts"() {
1846
- "use strict";
1847
- INFERRED_PREFERENCE_NAVIGATOR_STUB = true;
1848
- }
1849
- });
1850
-
1851
- // src/core/navigators/interferenceMitigator.ts
1852
- var interferenceMitigator_exports = {};
1853
- __export(interferenceMitigator_exports, {
1854
- default: () => InterferenceMitigatorNavigator
1855
- });
1856
- var import_common8, DEFAULT_MIN_COUNT2, DEFAULT_MIN_ELAPSED_DAYS, DEFAULT_INTERFERENCE_DECAY, InterferenceMitigatorNavigator;
1857
- var init_interferenceMitigator = __esm({
1858
- "src/core/navigators/interferenceMitigator.ts"() {
1859
- "use strict";
1860
- init_navigators();
1861
- import_common8 = require("@vue-skuilder/common");
1862
- DEFAULT_MIN_COUNT2 = 10;
1863
- DEFAULT_MIN_ELAPSED_DAYS = 3;
1864
- DEFAULT_INTERFERENCE_DECAY = 0.8;
1865
- InterferenceMitigatorNavigator = class extends ContentNavigator {
1866
- config;
1867
- _strategyData;
1868
- /** Human-readable name for CardFilter interface */
1869
- name;
1870
- /** Precomputed map: tag -> set of { partner, decay } it interferes with */
1871
- interferenceMap;
1872
- constructor(user, course, _strategyData) {
1873
- super(user, course, _strategyData);
1874
- this._strategyData = _strategyData;
1875
- this.config = this.parseConfig(_strategyData.serializedData);
1876
- this.interferenceMap = this.buildInterferenceMap();
1877
- this.name = _strategyData.name || "Interference Mitigator";
1878
- }
1879
- parseConfig(serializedData) {
1880
- try {
1881
- const parsed = JSON.parse(serializedData);
1882
- let sets = parsed.interferenceSets || [];
1883
- if (sets.length > 0 && Array.isArray(sets[0])) {
1884
- sets = sets.map((tags) => ({ tags }));
1885
- }
1886
- return {
1887
- interferenceSets: sets,
1888
- maturityThreshold: {
1889
- minCount: parsed.maturityThreshold?.minCount ?? DEFAULT_MIN_COUNT2,
1890
- minElo: parsed.maturityThreshold?.minElo,
1891
- minElapsedDays: parsed.maturityThreshold?.minElapsedDays ?? DEFAULT_MIN_ELAPSED_DAYS
1892
- },
1893
- defaultDecay: parsed.defaultDecay ?? DEFAULT_INTERFERENCE_DECAY
1894
- };
1895
- } catch {
1896
- return {
1897
- interferenceSets: [],
1898
- maturityThreshold: {
1899
- minCount: DEFAULT_MIN_COUNT2,
1900
- minElapsedDays: DEFAULT_MIN_ELAPSED_DAYS
1901
- },
1902
- defaultDecay: DEFAULT_INTERFERENCE_DECAY
1903
- };
1904
- }
1905
- }
1906
- /**
1907
- * Build a map from each tag to its interference partners with decay coefficients.
1908
- * If tags A, B, C are in an interference group with decay 0.8, then:
1909
- * - A interferes with B (decay 0.8) and C (decay 0.8)
1910
- * - B interferes with A (decay 0.8) and C (decay 0.8)
1911
- * - etc.
1912
- */
1913
- buildInterferenceMap() {
1914
- const map = /* @__PURE__ */ new Map();
1915
- for (const group of this.config.interferenceSets) {
1916
- const decay = group.decay ?? this.config.defaultDecay ?? DEFAULT_INTERFERENCE_DECAY;
1917
- for (const tag of group.tags) {
1918
- if (!map.has(tag)) {
1919
- map.set(tag, []);
1920
- }
1921
- const partners = map.get(tag);
1922
- for (const other of group.tags) {
1923
- if (other !== tag) {
1924
- const existing = partners.find((p) => p.partner === other);
1925
- if (existing) {
1926
- existing.decay = Math.max(existing.decay, decay);
1927
- } else {
1928
- partners.push({ partner: other, decay });
1929
- }
1930
- }
1931
- }
1932
- }
1933
- }
1934
- return map;
1935
- }
1936
- /**
1937
- * Get the set of tags that are currently immature for this user.
1938
- * A tag is immature if the user has interacted with it but hasn't
1939
- * reached the maturity threshold.
1940
- */
1941
- async getImmatureTags(context) {
1942
- const immature = /* @__PURE__ */ new Set();
1943
- try {
1944
- const courseReg = await context.user.getCourseRegDoc(context.course.getCourseID());
1945
- const userElo = (0, import_common8.toCourseElo)(courseReg.elo);
1946
- const minCount = this.config.maturityThreshold?.minCount ?? DEFAULT_MIN_COUNT2;
1947
- const minElo = this.config.maturityThreshold?.minElo;
1948
- const minElapsedDays = this.config.maturityThreshold?.minElapsedDays ?? DEFAULT_MIN_ELAPSED_DAYS;
1949
- const minCountForElapsed = minElapsedDays * 2;
1950
- for (const [tagId, tagElo] of Object.entries(userElo.tags)) {
1951
- if (tagElo.count === 0) continue;
1952
- const belowCount = tagElo.count < minCount;
1953
- const belowElo = minElo !== void 0 && tagElo.score < minElo;
1954
- const belowElapsed = tagElo.count < minCountForElapsed;
1955
- if (belowCount || belowElo || belowElapsed) {
1956
- immature.add(tagId);
1957
- }
1958
- }
1959
- } catch {
1960
- }
1961
- return immature;
1962
- }
1963
- /**
1964
- * Get all tags that interfere with any immature tag, along with their decay coefficients.
1965
- * These are the tags we want to avoid introducing.
1966
- */
1967
- getTagsToAvoid(immatureTags) {
1968
- const avoid = /* @__PURE__ */ new Map();
1969
- for (const immatureTag of immatureTags) {
1970
- const partners = this.interferenceMap.get(immatureTag);
1971
- if (partners) {
1972
- for (const { partner, decay } of partners) {
1973
- if (!immatureTags.has(partner)) {
1974
- const existing = avoid.get(partner) ?? 0;
1975
- avoid.set(partner, Math.max(existing, decay));
1976
- }
1977
- }
1978
- }
1979
- }
1980
- return avoid;
1981
- }
1982
- /**
1983
- * Compute interference score reduction for a card.
1984
- * Returns: { multiplier, interfering tags, reason }
1985
- */
1986
- computeInterferenceEffect(cardTags, tagsToAvoid, immatureTags) {
1987
- if (tagsToAvoid.size === 0) {
1988
- return {
1989
- multiplier: 1,
1990
- interferingTags: [],
1991
- reason: "No interference detected"
1992
- };
1993
- }
1994
- let multiplier = 1;
1995
- const interferingTags = [];
1996
- for (const tag of cardTags) {
1997
- const decay = tagsToAvoid.get(tag);
1998
- if (decay !== void 0) {
1999
- interferingTags.push(tag);
2000
- multiplier *= 1 - decay;
2001
- }
2002
- }
2003
- if (interferingTags.length === 0) {
2004
- return {
2005
- multiplier: 1,
2006
- interferingTags: [],
2007
- reason: "No interference detected"
2008
- };
2009
- }
2010
- const causingTags = /* @__PURE__ */ new Set();
2011
- for (const tag of interferingTags) {
2012
- for (const immatureTag of immatureTags) {
2013
- const partners = this.interferenceMap.get(immatureTag);
2014
- if (partners?.some((p) => p.partner === tag)) {
2015
- causingTags.add(immatureTag);
2016
- }
2017
- }
2018
- }
2019
- const reason = `Interferes with immature tags ${Array.from(causingTags).join(", ")} (tags: ${interferingTags.join(", ")}, multiplier: ${multiplier.toFixed(2)})`;
2020
- return { multiplier, interferingTags, reason };
2021
- }
2022
- /**
2023
- * CardFilter.transform implementation.
2024
- *
2025
- * Apply interference-aware scoring. Cards with tags that interfere with
2026
- * immature learnings get reduced scores.
2027
- */
2028
- async transform(cards, context) {
2029
- const immatureTags = await this.getImmatureTags(context);
2030
- const tagsToAvoid = this.getTagsToAvoid(immatureTags);
2031
- const adjusted = [];
2032
- for (const card of cards) {
2033
- const cardTags = card.tags ?? [];
2034
- const { multiplier, reason } = this.computeInterferenceEffect(
2035
- cardTags,
2036
- tagsToAvoid,
2037
- immatureTags
2038
- );
2039
- const finalScore = card.score * multiplier;
2040
- const action = multiplier < 1 ? "penalized" : multiplier > 1 ? "boosted" : "passed";
2041
- adjusted.push({
2042
- ...card,
2043
- score: finalScore,
2044
- provenance: [
2045
- ...card.provenance,
2046
- {
2047
- strategy: "interferenceMitigator",
2048
- strategyName: this.strategyName || this.name,
2049
- strategyId: this.strategyId || "NAVIGATION_STRATEGY-interference",
2050
- action,
2051
- score: finalScore,
2052
- reason
2053
- }
2054
- ]
2055
- });
2056
- }
2057
- return adjusted;
2058
- }
2059
- /**
2060
- * Legacy getWeightedCards - now throws as filters should not be used as generators.
2061
- *
2062
- * Use transform() via Pipeline instead.
2063
- */
2064
- async getWeightedCards(_limit) {
2065
- throw new Error(
2066
- "InterferenceMitigatorNavigator is a filter and should not be used as a generator. Use Pipeline with a generator and this filter via transform()."
2067
- );
2068
- }
2069
- // Legacy methods - stub implementations since filters don't generate cards
2070
- async getNewCards(_n) {
2071
- return [];
2072
- }
2073
- async getPendingReviews() {
2074
- return [];
2075
- }
2076
- };
2077
- }
2078
- });
2079
-
2080
- // src/core/navigators/relativePriority.ts
2081
- var relativePriority_exports = {};
2082
- __export(relativePriority_exports, {
2083
- default: () => RelativePriorityNavigator
2084
- });
2085
- var DEFAULT_PRIORITY, DEFAULT_PRIORITY_INFLUENCE, DEFAULT_COMBINE_MODE, RelativePriorityNavigator;
2086
- var init_relativePriority = __esm({
2087
- "src/core/navigators/relativePriority.ts"() {
2088
- "use strict";
2089
- init_navigators();
2090
- DEFAULT_PRIORITY = 0.5;
2091
- DEFAULT_PRIORITY_INFLUENCE = 0.5;
2092
- DEFAULT_COMBINE_MODE = "max";
2093
- RelativePriorityNavigator = class extends ContentNavigator {
2094
- config;
2095
- _strategyData;
2096
- /** Human-readable name for CardFilter interface */
2097
- name;
2098
- constructor(user, course, _strategyData) {
2099
- super(user, course, _strategyData);
2100
- this._strategyData = _strategyData;
2101
- this.config = this.parseConfig(_strategyData.serializedData);
2102
- this.name = _strategyData.name || "Relative Priority";
2103
- }
2104
- parseConfig(serializedData) {
2105
- try {
2106
- const parsed = JSON.parse(serializedData);
2107
- return {
2108
- tagPriorities: parsed.tagPriorities || {},
2109
- defaultPriority: parsed.defaultPriority ?? DEFAULT_PRIORITY,
2110
- combineMode: parsed.combineMode ?? DEFAULT_COMBINE_MODE,
2111
- priorityInfluence: parsed.priorityInfluence ?? DEFAULT_PRIORITY_INFLUENCE
2112
- };
2113
- } catch {
2114
- return {
2115
- tagPriorities: {},
2116
- defaultPriority: DEFAULT_PRIORITY,
2117
- combineMode: DEFAULT_COMBINE_MODE,
2118
- priorityInfluence: DEFAULT_PRIORITY_INFLUENCE
2119
- };
2120
- }
2121
- }
2122
- /**
2123
- * Look up the priority for a tag.
2124
- */
2125
- getTagPriority(tagId) {
2126
- return this.config.tagPriorities[tagId] ?? this.config.defaultPriority ?? DEFAULT_PRIORITY;
2127
- }
2128
- /**
2129
- * Compute combined priority for a card based on its tags.
2130
- */
2131
- computeCardPriority(cardTags) {
2132
- if (cardTags.length === 0) {
2133
- return this.config.defaultPriority ?? DEFAULT_PRIORITY;
2134
- }
2135
- const priorities = cardTags.map((tag) => this.getTagPriority(tag));
2136
- switch (this.config.combineMode) {
2137
- case "max":
2138
- return Math.max(...priorities);
2139
- case "min":
2140
- return Math.min(...priorities);
2141
- case "average":
2142
- return priorities.reduce((sum, p) => sum + p, 0) / priorities.length;
2143
- default:
2144
- return Math.max(...priorities);
2145
- }
2146
- }
2147
- /**
2148
- * Compute boost factor based on priority.
2149
- *
2150
- * The formula: 1 + (priority - 0.5) * priorityInfluence
2151
- *
2152
- * This creates a multiplier centered around 1.0:
2153
- * - Priority 1.0 with influence 0.5 → 1.25 (25% boost)
2154
- * - Priority 0.5 with any influence → 1.00 (neutral)
2155
- * - Priority 0.0 with influence 0.5 → 0.75 (25% reduction)
2156
- */
2157
- computeBoostFactor(priority) {
2158
- const influence = this.config.priorityInfluence ?? DEFAULT_PRIORITY_INFLUENCE;
2159
- return 1 + (priority - 0.5) * influence;
2160
- }
2161
- /**
2162
- * Build human-readable reason for priority adjustment.
2163
- */
2164
- buildPriorityReason(cardTags, priority, boostFactor, finalScore) {
2165
- if (cardTags.length === 0) {
2166
- return `No tags, neutral priority (${priority.toFixed(2)})`;
2167
- }
2168
- const tagList = cardTags.slice(0, 3).join(", ");
2169
- const more = cardTags.length > 3 ? ` (+${cardTags.length - 3} more)` : "";
2170
- if (boostFactor === 1) {
2171
- return `Neutral priority (${priority.toFixed(2)}) for tags: ${tagList}${more}`;
2172
- } else if (boostFactor > 1) {
2173
- return `High-priority tags: ${tagList}${more} (priority ${priority.toFixed(2)} \u2192 boost ${boostFactor.toFixed(2)}x \u2192 ${finalScore.toFixed(2)})`;
2174
- } else {
2175
- return `Low-priority tags: ${tagList}${more} (priority ${priority.toFixed(2)} \u2192 reduce ${boostFactor.toFixed(2)}x \u2192 ${finalScore.toFixed(2)})`;
2176
- }
2177
- }
2178
- /**
2179
- * CardFilter.transform implementation.
2180
- *
2181
- * Apply priority-adjusted scoring. Cards with high-priority tags get boosted,
2182
- * cards with low-priority tags get reduced scores.
2183
- */
2184
- async transform(cards, _context) {
2185
- const adjusted = await Promise.all(
2186
- cards.map(async (card) => {
2187
- const cardTags = card.tags ?? [];
2188
- const priority = this.computeCardPriority(cardTags);
2189
- const boostFactor = this.computeBoostFactor(priority);
2190
- const finalScore = Math.max(0, Math.min(1, card.score * boostFactor));
2191
- const action = boostFactor > 1 ? "boosted" : boostFactor < 1 ? "penalized" : "passed";
2192
- const reason = this.buildPriorityReason(cardTags, priority, boostFactor, finalScore);
2193
- return {
2194
- ...card,
2195
- score: finalScore,
2196
- provenance: [
2197
- ...card.provenance,
2198
- {
2199
- strategy: "relativePriority",
2200
- strategyName: this.strategyName || this.name,
2201
- strategyId: this.strategyId || "NAVIGATION_STRATEGY-priority",
2202
- action,
2203
- score: finalScore,
2204
- reason
2205
- }
2206
- ]
2207
- };
2208
- })
2209
- );
2210
- return adjusted;
2211
- }
2212
- /**
2213
- * Legacy getWeightedCards - now throws as filters should not be used as generators.
2214
- *
2215
- * Use transform() via Pipeline instead.
2216
- */
2217
- async getWeightedCards(_limit) {
2218
- throw new Error(
2219
- "RelativePriorityNavigator is a filter and should not be used as a generator. Use Pipeline with a generator and this filter via transform()."
2220
- );
2221
- }
2222
- // Legacy methods - stub implementations since filters don't generate cards
2223
- async getNewCards(_n) {
2224
- return [];
2225
- }
2226
- async getPendingReviews() {
2227
- return [];
2228
- }
2229
- };
2230
- }
2231
- });
2232
-
2233
- // src/core/navigators/srs.ts
2234
- var srs_exports = {};
2235
- __export(srs_exports, {
2236
- default: () => SRSNavigator
2237
- });
2238
- var import_moment3, SRSNavigator;
2239
- var init_srs = __esm({
2240
- "src/core/navigators/srs.ts"() {
2241
- "use strict";
2242
- import_moment3 = __toESM(require("moment"), 1);
2243
- init_navigators();
2244
- SRSNavigator = class extends ContentNavigator {
2245
- /** Human-readable name for CardGenerator interface */
2246
- name;
2247
- constructor(user, course, strategyData) {
2248
- super(user, course, strategyData);
2249
- this.name = strategyData?.name || "SRS";
2250
- }
2251
- /**
2252
- * Get review cards scored by urgency.
2253
- *
2254
- * Score formula combines:
2255
- * - Relative overdueness: hoursOverdue / intervalHours
2256
- * - Interval recency: exponential decay favoring shorter intervals
2257
- *
2258
- * Cards not yet due are excluded (not scored as 0).
2259
- *
2260
- * This method supports both the legacy signature (limit only) and the
2261
- * CardGenerator interface signature (limit, context).
2262
- *
2263
- * @param limit - Maximum number of cards to return
2264
- * @param _context - Optional GeneratorContext (currently unused, but required for interface)
2265
- */
2266
- async getWeightedCards(limit, _context) {
2267
- if (!this.user || !this.course) {
2268
- throw new Error("SRSNavigator requires user and course to be set");
1411
+ async getWeightedCards(limit, _context) {
1412
+ if (!this.user || !this.course) {
1413
+ throw new Error("SRSNavigator requires user and course to be set");
2269
1414
  }
2270
1415
  const reviews = await this.user.getPendingReviews(this.course.getCourseID());
2271
1416
  const now = import_moment3.default.utc();
@@ -2276,6 +1421,7 @@ var init_srs = __esm({
2276
1421
  cardId: review.cardId,
2277
1422
  courseId: review.courseId,
2278
1423
  score,
1424
+ reviewID: review._id,
2279
1425
  provenance: [
2280
1426
  {
2281
1427
  strategy: "srs",
@@ -2288,333 +1434,137 @@ var init_srs = __esm({
2288
1434
  ]
2289
1435
  };
2290
1436
  });
1437
+ logger.debug(`[srsNav] got ${scored.length} weighted cards`);
2291
1438
  return scored.sort((a, b) => b.score - a.score).slice(0, limit);
2292
1439
  }
2293
1440
  /**
2294
1441
  * Compute urgency score for a review card.
2295
1442
  *
2296
1443
  * Two factors:
2297
- * 1. Relative overdueness = hoursOverdue / intervalHours
2298
- * - 2 days overdue on 3-day interval = 0.67 (urgent)
2299
- * - 2 days overdue on 180-day interval = 0.01 (not urgent)
2300
- *
2301
- * 2. Interval recency factor = 0.3 + 0.7 * exp(-intervalHours / 720)
2302
- * - 24h interval → ~1.0 (very recent learning)
2303
- * - 30 days (720h) → ~0.56
2304
- * - 180 days → ~0.30
2305
- *
2306
- * Combined: base 0.5 + weighted average of factors * 0.45
2307
- * Result range: approximately 0.5 to 0.95
2308
- */
2309
- computeUrgencyScore(review, now) {
2310
- const scheduledAt = import_moment3.default.utc(review.scheduledAt);
2311
- const due = import_moment3.default.utc(review.reviewTime);
2312
- const intervalHours = Math.max(1, due.diff(scheduledAt, "hours"));
2313
- const hoursOverdue = now.diff(due, "hours");
2314
- const relativeOverdue = hoursOverdue / intervalHours;
2315
- const recencyFactor = 0.3 + 0.7 * Math.exp(-intervalHours / 720);
2316
- const overdueContribution = Math.min(1, Math.max(0, relativeOverdue));
2317
- const urgency = overdueContribution * 0.5 + recencyFactor * 0.5;
2318
- const score = Math.min(0.95, 0.5 + urgency * 0.45);
2319
- const reason = `${Math.round(hoursOverdue)}h overdue (interval: ${Math.round(intervalHours)}h, relative: ${relativeOverdue.toFixed(2)}), recency: ${recencyFactor.toFixed(2)}, review`;
2320
- return { score, reason };
2321
- }
2322
- /**
2323
- * Get pending reviews in legacy format.
2324
- *
2325
- * Returns all pending reviews for the course, enriched with session item fields.
2326
- */
2327
- async getPendingReviews() {
2328
- if (!this.user || !this.course) {
2329
- throw new Error("SRSNavigator requires user and course to be set");
2330
- }
2331
- const reviews = await this.user.getPendingReviews(this.course.getCourseID());
2332
- return reviews.map((r) => ({
2333
- ...r,
2334
- contentSourceType: "course",
2335
- contentSourceID: this.course.getCourseID(),
2336
- cardID: r.cardId,
2337
- courseID: r.courseId,
2338
- qualifiedID: `${r.courseId}-${r.cardId}`,
2339
- reviewID: r._id,
2340
- status: "review"
2341
- }));
2342
- }
2343
- /**
2344
- * SRS does not generate new cards.
2345
- * Use ELONavigator or another generator for new cards.
2346
- */
2347
- async getNewCards(_n) {
2348
- return [];
2349
- }
2350
- };
2351
- }
2352
- });
2353
-
2354
- // src/core/navigators/userGoal.ts
2355
- var userGoal_exports = {};
2356
- __export(userGoal_exports, {
2357
- USER_GOAL_NAVIGATOR_STUB: () => USER_GOAL_NAVIGATOR_STUB
2358
- });
2359
- var USER_GOAL_NAVIGATOR_STUB;
2360
- var init_userGoal = __esm({
2361
- "src/core/navigators/userGoal.ts"() {
2362
- "use strict";
2363
- USER_GOAL_NAVIGATOR_STUB = true;
2364
- }
2365
- });
2366
-
2367
- // import("./**/*") in src/core/navigators/index.ts
2368
- var globImport;
2369
- var init_ = __esm({
2370
- 'import("./**/*") in src/core/navigators/index.ts'() {
2371
- globImport = __glob({
2372
- "./CompositeGenerator.ts": () => Promise.resolve().then(() => (init_CompositeGenerator(), CompositeGenerator_exports)),
2373
- "./Pipeline.ts": () => Promise.resolve().then(() => (init_Pipeline(), Pipeline_exports)),
2374
- "./PipelineAssembler.ts": () => Promise.resolve().then(() => (init_PipelineAssembler(), PipelineAssembler_exports)),
2375
- "./elo.ts": () => Promise.resolve().then(() => (init_elo(), elo_exports)),
2376
- "./filters/eloDistance.ts": () => Promise.resolve().then(() => (init_eloDistance(), eloDistance_exports)),
2377
- "./filters/index.ts": () => Promise.resolve().then(() => (init_filters(), filters_exports)),
2378
- "./filters/types.ts": () => Promise.resolve().then(() => (init_types(), types_exports)),
2379
- "./filters/userTagPreference.ts": () => Promise.resolve().then(() => (init_userTagPreference(), userTagPreference_exports)),
2380
- "./generators/index.ts": () => Promise.resolve().then(() => (init_generators(), generators_exports)),
2381
- "./generators/types.ts": () => Promise.resolve().then(() => (init_types2(), types_exports2)),
2382
- "./hardcodedOrder.ts": () => Promise.resolve().then(() => (init_hardcodedOrder(), hardcodedOrder_exports)),
2383
- "./hierarchyDefinition.ts": () => Promise.resolve().then(() => (init_hierarchyDefinition(), hierarchyDefinition_exports)),
2384
- "./index.ts": () => Promise.resolve().then(() => (init_navigators(), navigators_exports)),
2385
- "./inferredPreference.ts": () => Promise.resolve().then(() => (init_inferredPreference(), inferredPreference_exports)),
2386
- "./interferenceMitigator.ts": () => Promise.resolve().then(() => (init_interferenceMitigator(), interferenceMitigator_exports)),
2387
- "./relativePriority.ts": () => Promise.resolve().then(() => (init_relativePriority(), relativePriority_exports)),
2388
- "./srs.ts": () => Promise.resolve().then(() => (init_srs(), srs_exports)),
2389
- "./userGoal.ts": () => Promise.resolve().then(() => (init_userGoal(), userGoal_exports))
2390
- });
2391
- }
2392
- });
2393
-
2394
- // src/core/navigators/index.ts
2395
- var navigators_exports = {};
2396
- __export(navigators_exports, {
2397
- ContentNavigator: () => ContentNavigator,
2398
- NavigatorRole: () => NavigatorRole,
2399
- NavigatorRoles: () => NavigatorRoles,
2400
- Navigators: () => Navigators,
2401
- getCardOrigin: () => getCardOrigin,
2402
- isFilter: () => isFilter,
2403
- isGenerator: () => isGenerator
2404
- });
2405
- function getCardOrigin(card) {
2406
- if (card.provenance.length === 0) {
2407
- throw new Error("Card has no provenance - cannot determine origin");
2408
- }
2409
- const firstEntry = card.provenance[0];
2410
- const reason = firstEntry.reason.toLowerCase();
2411
- if (reason.includes("failed")) {
2412
- return "failed";
2413
- }
2414
- if (reason.includes("review")) {
2415
- return "review";
2416
- }
2417
- return "new";
2418
- }
2419
- function isGenerator(impl) {
2420
- return NavigatorRoles[impl] === "generator" /* GENERATOR */;
2421
- }
2422
- function isFilter(impl) {
2423
- return NavigatorRoles[impl] === "filter" /* FILTER */;
2424
- }
2425
- var Navigators, NavigatorRole, NavigatorRoles, ContentNavigator;
2426
- var init_navigators = __esm({
2427
- "src/core/navigators/index.ts"() {
2428
- "use strict";
2429
- init_logger();
2430
- init_();
2431
- Navigators = /* @__PURE__ */ ((Navigators2) => {
2432
- Navigators2["ELO"] = "elo";
2433
- Navigators2["SRS"] = "srs";
2434
- Navigators2["HARDCODED"] = "hardcodedOrder";
2435
- Navigators2["HIERARCHY"] = "hierarchyDefinition";
2436
- Navigators2["INTERFERENCE"] = "interferenceMitigator";
2437
- Navigators2["RELATIVE_PRIORITY"] = "relativePriority";
2438
- Navigators2["USER_TAG_PREFERENCE"] = "userTagPreference";
2439
- return Navigators2;
2440
- })(Navigators || {});
2441
- NavigatorRole = /* @__PURE__ */ ((NavigatorRole2) => {
2442
- NavigatorRole2["GENERATOR"] = "generator";
2443
- NavigatorRole2["FILTER"] = "filter";
2444
- return NavigatorRole2;
2445
- })(NavigatorRole || {});
2446
- NavigatorRoles = {
2447
- ["elo" /* ELO */]: "generator" /* GENERATOR */,
2448
- ["srs" /* SRS */]: "generator" /* GENERATOR */,
2449
- ["hardcodedOrder" /* HARDCODED */]: "generator" /* GENERATOR */,
2450
- ["hierarchyDefinition" /* HIERARCHY */]: "filter" /* FILTER */,
2451
- ["interferenceMitigator" /* INTERFERENCE */]: "filter" /* FILTER */,
2452
- ["relativePriority" /* RELATIVE_PRIORITY */]: "filter" /* FILTER */,
2453
- ["userTagPreference" /* USER_TAG_PREFERENCE */]: "filter" /* FILTER */
2454
- };
2455
- ContentNavigator = class {
2456
- /** User interface for this navigation session */
2457
- user;
2458
- /** Course interface for this navigation session */
2459
- course;
2460
- /** Human-readable name for this strategy instance (from ContentNavigationStrategyData.name) */
2461
- strategyName;
2462
- /** Unique document ID for this strategy instance (from ContentNavigationStrategyData._id) */
2463
- strategyId;
2464
- /**
2465
- * Constructor for standard navigators.
2466
- * Call this from subclass constructors to initialize common fields.
2467
- *
2468
- * Note: CompositeGenerator doesn't use this pattern and should call super() without args.
2469
- */
2470
- constructor(user, course, strategyData) {
2471
- if (user && course && strategyData) {
2472
- this.user = user;
2473
- this.course = course;
2474
- this.strategyName = strategyData.name;
2475
- this.strategyId = strategyData._id;
2476
- }
2477
- }
2478
- // ============================================================================
2479
- // STRATEGY STATE HELPERS
2480
- // ============================================================================
2481
- //
2482
- // These methods allow strategies to persist their own state (user preferences,
2483
- // learned patterns, temporal tracking) in the user database.
2484
- //
2485
- // ============================================================================
2486
- /**
2487
- * Unique key identifying this strategy for state storage.
2488
- *
2489
- * Defaults to the constructor name (e.g., "UserTagPreferenceFilter").
2490
- * Override in subclasses if multiple instances of the same strategy type
2491
- * need separate state storage.
2492
- */
2493
- get strategyKey() {
2494
- return this.constructor.name;
2495
- }
2496
- /**
2497
- * Get this strategy's persisted state for the current course.
2498
- *
2499
- * @returns The strategy's data payload, or null if no state exists
2500
- * @throws Error if user or course is not initialized
2501
- */
2502
- async getStrategyState() {
2503
- if (!this.user || !this.course) {
2504
- throw new Error(
2505
- `Cannot get strategy state: navigator not properly initialized. Ensure user and course are provided to constructor.`
2506
- );
2507
- }
2508
- return this.user.getStrategyState(this.course.getCourseID(), this.strategyKey);
2509
- }
2510
- /**
2511
- * Persist this strategy's state for the current course.
2512
- *
2513
- * @param data - The strategy's data payload to store
2514
- * @throws Error if user or course is not initialized
2515
- */
2516
- async putStrategyState(data) {
2517
- if (!this.user || !this.course) {
2518
- throw new Error(
2519
- `Cannot put strategy state: navigator not properly initialized. Ensure user and course are provided to constructor.`
2520
- );
2521
- }
2522
- return this.user.putStrategyState(this.course.getCourseID(), this.strategyKey, data);
2523
- }
2524
- /**
2525
- * Factory method to create navigator instances dynamically.
2526
- *
2527
- * @param user - User interface
2528
- * @param course - Course interface
2529
- * @param strategyData - Strategy configuration document
2530
- * @returns the runtime object used to steer a study session.
2531
- */
2532
- static async create(user, course, strategyData) {
2533
- const implementingClass = strategyData.implementingClass;
2534
- let NavigatorImpl;
2535
- const variations = [".ts", ".js", ""];
2536
- for (const ext of variations) {
2537
- try {
2538
- const module2 = await globImport(`./${implementingClass}${ext}`);
2539
- NavigatorImpl = module2.default;
2540
- break;
2541
- } catch (e) {
2542
- logger.debug(`Failed to load with extension ${ext}:`, e);
2543
- }
2544
- }
2545
- if (!NavigatorImpl) {
2546
- throw new Error(`Could not load navigator implementation for: ${implementingClass}`);
2547
- }
2548
- return new NavigatorImpl(user, course, strategyData);
2549
- }
2550
- /**
2551
- * Get cards with suitability scores and provenance trails.
2552
- *
2553
- * **This is the PRIMARY API for navigation strategies.**
2554
- *
2555
- * Returns cards ranked by suitability score (0-1). Higher scores indicate
2556
- * better candidates for presentation. Each card includes a provenance trail
2557
- * documenting how strategies contributed to the final score.
2558
- *
2559
- * ## For Generators
2560
- * Override this method to generate candidates and compute scores based on
2561
- * your strategy's logic (e.g., ELO proximity, review urgency). Create the
2562
- * initial provenance entry with action='generated'.
2563
- *
2564
- * ## Default Implementation
2565
- * The base class provides a backward-compatible default that:
2566
- * 1. Calls legacy getNewCards() and getPendingReviews()
2567
- * 2. Assigns score=1.0 to all cards
2568
- * 3. Creates minimal provenance from legacy methods
2569
- * 4. Returns combined results up to limit
2570
- *
2571
- * This allows existing strategies to work without modification while
2572
- * new strategies can override with proper scoring and provenance.
2573
- *
2574
- * @param limit - Maximum cards to return
2575
- * @returns Cards sorted by score descending, with provenance trails
2576
- */
2577
- async getWeightedCards(limit) {
2578
- const newCards = await this.getNewCards(limit);
2579
- const reviews = await this.getPendingReviews();
2580
- const weighted = [
2581
- ...newCards.map((c) => ({
2582
- cardId: c.cardID,
2583
- courseId: c.courseID,
2584
- score: 1,
2585
- provenance: [
2586
- {
2587
- strategy: "legacy",
2588
- strategyName: this.strategyName || "Legacy API",
2589
- strategyId: this.strategyId || "legacy-fallback",
2590
- action: "generated",
2591
- score: 1,
2592
- reason: "Generated via legacy getNewCards(), new card"
2593
- }
2594
- ]
2595
- })),
2596
- ...reviews.map((r) => ({
2597
- cardId: r.cardID,
2598
- courseId: r.courseID,
2599
- score: 1,
2600
- provenance: [
2601
- {
2602
- strategy: "legacy",
2603
- strategyName: this.strategyName || "Legacy API",
2604
- strategyId: this.strategyId || "legacy-fallback",
2605
- action: "generated",
2606
- score: 1,
2607
- reason: "Generated via legacy getPendingReviews(), review"
2608
- }
2609
- ]
2610
- }))
2611
- ];
2612
- return weighted.slice(0, limit);
1444
+ * 1. Relative overdueness = hoursOverdue / intervalHours
1445
+ * - 2 days overdue on 3-day interval = 0.67 (urgent)
1446
+ * - 2 days overdue on 180-day interval = 0.01 (not urgent)
1447
+ *
1448
+ * 2. Interval recency factor = 0.3 + 0.7 * exp(-intervalHours / 720)
1449
+ * - 24h interval → ~1.0 (very recent learning)
1450
+ * - 30 days (720h) → ~0.56
1451
+ * - 180 days → ~0.30
1452
+ *
1453
+ * Combined: base 0.5 + weighted average of factors * 0.45
1454
+ * Result range: approximately 0.5 to 0.95
1455
+ */
1456
+ computeUrgencyScore(review, now) {
1457
+ const scheduledAt = import_moment3.default.utc(review.scheduledAt);
1458
+ const due = import_moment3.default.utc(review.reviewTime);
1459
+ const intervalHours = Math.max(1, due.diff(scheduledAt, "hours"));
1460
+ const hoursOverdue = now.diff(due, "hours");
1461
+ const relativeOverdue = hoursOverdue / intervalHours;
1462
+ const recencyFactor = 0.3 + 0.7 * Math.exp(-intervalHours / 720);
1463
+ const overdueContribution = Math.min(1, Math.max(0, relativeOverdue));
1464
+ const urgency = overdueContribution * 0.5 + recencyFactor * 0.5;
1465
+ const score = Math.min(0.95, 0.5 + urgency * 0.45);
1466
+ const reason = `${Math.round(hoursOverdue)}h overdue (interval: ${Math.round(intervalHours)}h, relative: ${relativeOverdue.toFixed(2)}), recency: ${recencyFactor.toFixed(2)}, review`;
1467
+ return { score, reason };
2613
1468
  }
2614
1469
  };
2615
1470
  }
2616
1471
  });
2617
1472
 
1473
+ // src/core/navigators/filters/eloDistance.ts
1474
+ function computeMultiplier(distance, halfLife, minMultiplier, maxMultiplier) {
1475
+ const normalizedDistance = distance / halfLife;
1476
+ const decay = Math.exp(-(normalizedDistance * normalizedDistance));
1477
+ return minMultiplier + (maxMultiplier - minMultiplier) * decay;
1478
+ }
1479
+ function createEloDistanceFilter(config) {
1480
+ const halfLife = config?.halfLife ?? DEFAULT_HALF_LIFE;
1481
+ const minMultiplier = config?.minMultiplier ?? DEFAULT_MIN_MULTIPLIER;
1482
+ const maxMultiplier = config?.maxMultiplier ?? DEFAULT_MAX_MULTIPLIER;
1483
+ return {
1484
+ name: "ELO Distance Filter",
1485
+ async transform(cards, context) {
1486
+ const { course, userElo } = context;
1487
+ const cardIds = cards.map((c) => c.cardId);
1488
+ const cardElos = await course.getCardEloData(cardIds);
1489
+ return cards.map((card, i) => {
1490
+ const cardElo = cardElos[i]?.global?.score ?? 1e3;
1491
+ const distance = Math.abs(cardElo - userElo);
1492
+ const multiplier = computeMultiplier(distance, halfLife, minMultiplier, maxMultiplier);
1493
+ const newScore = card.score * multiplier;
1494
+ const action = multiplier < maxMultiplier - 0.01 ? "penalized" : "passed";
1495
+ return {
1496
+ ...card,
1497
+ score: newScore,
1498
+ provenance: [
1499
+ ...card.provenance,
1500
+ {
1501
+ strategy: "eloDistance",
1502
+ strategyName: "ELO Distance Filter",
1503
+ strategyId: "ELO_DISTANCE_FILTER",
1504
+ action,
1505
+ score: newScore,
1506
+ reason: `ELO distance ${Math.round(distance)} (card: ${Math.round(cardElo)}, user: ${Math.round(userElo)}) \u2192 ${multiplier.toFixed(2)}x`
1507
+ }
1508
+ ]
1509
+ };
1510
+ });
1511
+ }
1512
+ };
1513
+ }
1514
+ var DEFAULT_HALF_LIFE, DEFAULT_MIN_MULTIPLIER, DEFAULT_MAX_MULTIPLIER;
1515
+ var init_eloDistance = __esm({
1516
+ "src/core/navigators/filters/eloDistance.ts"() {
1517
+ "use strict";
1518
+ DEFAULT_HALF_LIFE = 200;
1519
+ DEFAULT_MIN_MULTIPLIER = 0.3;
1520
+ DEFAULT_MAX_MULTIPLIER = 1;
1521
+ }
1522
+ });
1523
+
1524
+ // src/core/navigators/defaults.ts
1525
+ function createDefaultEloStrategy(courseId) {
1526
+ return {
1527
+ _id: "NAVIGATION_STRATEGY-ELO-default",
1528
+ docType: "NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */,
1529
+ name: "ELO (default)",
1530
+ description: "Default ELO-based navigation strategy for new cards",
1531
+ implementingClass: "elo" /* ELO */,
1532
+ course: courseId,
1533
+ serializedData: ""
1534
+ };
1535
+ }
1536
+ function createDefaultSrsStrategy(courseId) {
1537
+ return {
1538
+ _id: "NAVIGATION_STRATEGY-SRS-default",
1539
+ docType: "NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */,
1540
+ name: "SRS (default)",
1541
+ description: "Default SRS-based navigation strategy for reviews",
1542
+ implementingClass: "srs" /* SRS */,
1543
+ course: courseId,
1544
+ serializedData: ""
1545
+ };
1546
+ }
1547
+ function createDefaultPipeline(user, course) {
1548
+ const courseId = course.getCourseID();
1549
+ const eloNavigator = new ELONavigator(user, course, createDefaultEloStrategy(courseId));
1550
+ const srsNavigator = new SRSNavigator(user, course, createDefaultSrsStrategy(courseId));
1551
+ const compositeGenerator = new CompositeGenerator([eloNavigator, srsNavigator]);
1552
+ const eloDistanceFilter = createEloDistanceFilter();
1553
+ return new Pipeline(compositeGenerator, [eloDistanceFilter], user, course);
1554
+ }
1555
+ var init_defaults = __esm({
1556
+ "src/core/navigators/defaults.ts"() {
1557
+ "use strict";
1558
+ init_navigators();
1559
+ init_Pipeline();
1560
+ init_CompositeGenerator();
1561
+ init_elo();
1562
+ init_srs();
1563
+ init_eloDistance();
1564
+ init_types_legacy();
1565
+ }
1566
+ });
1567
+
2618
1568
  // src/impl/couch/courseDB.ts
2619
1569
  function randIntWeightedTowardZero(n) {
2620
1570
  return Math.floor(Math.random() * Math.random() * Math.random() * n);
@@ -2691,11 +1641,11 @@ ${JSON.stringify(config)}
2691
1641
  function isSuccessRow(row) {
2692
1642
  return "doc" in row && row.doc !== null && row.doc !== void 0;
2693
1643
  }
2694
- var import_common9, CourseDB;
1644
+ var import_common7, CourseDB;
2695
1645
  var init_courseDB = __esm({
2696
1646
  "src/impl/couch/courseDB.ts"() {
2697
1647
  "use strict";
2698
- import_common9 = require("@vue-skuilder/common");
1648
+ import_common7 = require("@vue-skuilder/common");
2699
1649
  init_couch();
2700
1650
  init_updateQueue();
2701
1651
  init_types_legacy();
@@ -2704,12 +1654,8 @@ var init_courseDB = __esm({
2704
1654
  init_courseAPI();
2705
1655
  init_courseLookupDB();
2706
1656
  init_navigators();
2707
- init_Pipeline();
2708
1657
  init_PipelineAssembler();
2709
- init_CompositeGenerator();
2710
- init_elo();
2711
- init_srs();
2712
- init_eloDistance();
1658
+ init_defaults();
2713
1659
  CourseDB = class {
2714
1660
  // private log(msg: string): void {
2715
1661
  // log(`CourseLog: ${this.id}\n ${msg}`);
@@ -2776,14 +1722,14 @@ var init_courseDB = __esm({
2776
1722
  docs.rows.forEach((r) => {
2777
1723
  if (isSuccessRow(r)) {
2778
1724
  if (r.doc && r.doc.elo) {
2779
- ret.push((0, import_common9.toCourseElo)(r.doc.elo));
1725
+ ret.push((0, import_common7.toCourseElo)(r.doc.elo));
2780
1726
  } else {
2781
1727
  logger.warn("no elo data for card: " + r.id);
2782
- ret.push((0, import_common9.blankCourseElo)());
1728
+ ret.push((0, import_common7.blankCourseElo)());
2783
1729
  }
2784
1730
  } else {
2785
1731
  logger.warn("no elo data for card: " + JSON.stringify(r));
2786
- ret.push((0, import_common9.blankCourseElo)());
1732
+ ret.push((0, import_common7.blankCourseElo)());
2787
1733
  }
2788
1734
  });
2789
1735
  return ret;
@@ -2978,7 +1924,7 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
2978
1924
  async getCourseTagStubs() {
2979
1925
  return getCourseTagStubs(this.id);
2980
1926
  }
2981
- async addNote(codeCourse, shape, data, author, tags, uploads, elo = (0, import_common9.blankCourseElo)()) {
1927
+ async addNote(codeCourse, shape, data, author, tags, uploads, elo = (0, import_common7.blankCourseElo)()) {
2982
1928
  try {
2983
1929
  const resp = await addNote55(this.id, codeCourse, shape, data, author, tags, uploads, elo);
2984
1930
  if (resp.ok) {
@@ -2987,19 +1933,19 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
2987
1933
  `[courseDB.addNote] Note added but card creation failed: ${resp.cardCreationError}`
2988
1934
  );
2989
1935
  return {
2990
- status: import_common9.Status.error,
1936
+ status: import_common7.Status.error,
2991
1937
  message: `Note was added but no cards were created: ${resp.cardCreationError}`,
2992
1938
  id: resp.id
2993
1939
  };
2994
1940
  }
2995
1941
  return {
2996
- status: import_common9.Status.ok,
1942
+ status: import_common7.Status.ok,
2997
1943
  message: "",
2998
1944
  id: resp.id
2999
1945
  };
3000
1946
  } else {
3001
1947
  return {
3002
- status: import_common9.Status.error,
1948
+ status: import_common7.Status.error,
3003
1949
  message: "Unexpected error adding note"
3004
1950
  };
3005
1951
  }
@@ -3011,7 +1957,7 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
3011
1957
  message: ${err.message}`
3012
1958
  );
3013
1959
  return {
3014
- status: import_common9.Status.error,
1960
+ status: import_common7.Status.error,
3015
1961
  message: `Error adding note to course. ${e.reason || err.message}`
3016
1962
  };
3017
1963
  }
@@ -3078,7 +2024,7 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
3078
2024
  logger.debug(
3079
2025
  "[courseDB] No strategy documents found, using default Pipeline(Composite(ELO, SRS), [eloDistanceFilter])"
3080
2026
  );
3081
- return this.createDefaultPipeline(user);
2027
+ return createDefaultPipeline(user, this);
3082
2028
  }
3083
2029
  const assembler = new PipelineAssembler();
3084
2030
  const { pipeline, generatorStrategies, filterStrategies, warnings } = await assembler.assemble({
@@ -3091,7 +2037,7 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
3091
2037
  }
3092
2038
  if (!pipeline) {
3093
2039
  logger.debug("[courseDB] Pipeline assembly failed, using default pipeline");
3094
- return this.createDefaultPipeline(user);
2040
+ return createDefaultPipeline(user, this);
3095
2041
  }
3096
2042
  logger.debug(
3097
2043
  `[courseDB] Using assembled pipeline with ${generatorStrategies.length} generator(s) and ${filterStrategies.length} filter(s)`
@@ -3102,69 +2048,12 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
3102
2048
  throw e;
3103
2049
  }
3104
2050
  }
3105
- makeDefaultEloStrategy() {
3106
- return {
3107
- _id: "NAVIGATION_STRATEGY-ELO-default",
3108
- docType: "NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */,
3109
- name: "ELO (default)",
3110
- description: "Default ELO-based navigation strategy for new cards",
3111
- implementingClass: "elo" /* ELO */,
3112
- course: this.id,
3113
- serializedData: ""
3114
- };
3115
- }
3116
- makeDefaultSrsStrategy() {
3117
- return {
3118
- _id: "NAVIGATION_STRATEGY-SRS-default",
3119
- docType: "NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */,
3120
- name: "SRS (default)",
3121
- description: "Default SRS-based navigation strategy for reviews",
3122
- implementingClass: "srs" /* SRS */,
3123
- course: this.id,
3124
- serializedData: ""
3125
- };
3126
- }
3127
- /**
3128
- * Creates the default navigation pipeline for courses with no configured strategies.
3129
- *
3130
- * Default: Pipeline(Composite(ELO, SRS), [eloDistanceFilter])
3131
- * - ELO generator: scores new cards by skill proximity
3132
- * - SRS generator: scores reviews by overdueness and interval recency
3133
- * - ELO distance filter: penalizes cards far from user's current level
3134
- */
3135
- createDefaultPipeline(user) {
3136
- const eloNavigator = new ELONavigator(user, this, this.makeDefaultEloStrategy());
3137
- const srsNavigator = new SRSNavigator(user, this, this.makeDefaultSrsStrategy());
3138
- const compositeGenerator = new CompositeGenerator([eloNavigator, srsNavigator]);
3139
- const eloDistanceFilter = createEloDistanceFilter();
3140
- return new Pipeline(compositeGenerator, [eloDistanceFilter], user, this);
3141
- }
3142
2051
  ////////////////////////////////////
3143
2052
  // END NavigationStrategyManager implementation
3144
2053
  ////////////////////////////////////
3145
2054
  ////////////////////////////////////
3146
2055
  // StudyContentSource implementation
3147
2056
  ////////////////////////////////////
3148
- async getNewCards(limit = 99) {
3149
- const u = await this._getCurrentUser();
3150
- try {
3151
- const navigator = await this.createNavigator(u);
3152
- return navigator.getNewCards(limit);
3153
- } catch (e) {
3154
- logger.error(`[courseDB] Error in getNewCards: ${e}`);
3155
- throw e;
3156
- }
3157
- }
3158
- async getPendingReviews() {
3159
- const u = await this._getCurrentUser();
3160
- try {
3161
- const navigator = await this.createNavigator(u);
3162
- return navigator.getPendingReviews();
3163
- } catch (e) {
3164
- logger.error(`[courseDB] Error in getPendingReviews: ${e}`);
3165
- throw e;
3166
- }
3167
- }
3168
2057
  /**
3169
2058
  * Get cards with suitability scores for presentation.
3170
2059
  *
@@ -3196,7 +2085,7 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
3196
2085
  const courseDoc = (await u.getCourseRegistrationsDoc()).courses.find((c) => {
3197
2086
  return c.courseID === this.id;
3198
2087
  });
3199
- targetElo = (0, import_common9.EloToNumber)(courseDoc.elo);
2088
+ targetElo = (0, import_common7.EloToNumber)(courseDoc.elo);
3200
2089
  } catch {
3201
2090
  targetElo = 1e3;
3202
2091
  }
@@ -3403,79 +2292,27 @@ var init_classroomDB2 = __esm({
3403
2292
  setChangeFcn(f) {
3404
2293
  void this.userMessages.on("change", f);
3405
2294
  }
3406
- async getPendingReviews() {
3407
- const u = this._user;
3408
- return (await u.getPendingReviews()).filter((r) => r.scheduledFor === "classroom" && r.schedulingAgentId === this._id).map((r) => {
3409
- return {
3410
- ...r,
3411
- qualifiedID: `${r.courseId}-${r.cardId}`,
3412
- courseID: r.courseId,
3413
- cardID: r.cardId,
3414
- contentSourceType: "classroom",
3415
- contentSourceID: this._id,
3416
- reviewID: r._id,
3417
- status: "review"
3418
- };
3419
- });
3420
- }
3421
- async getNewCards() {
3422
- const activeCards = await this._user.getActiveCards();
3423
- const now = import_moment4.default.utc();
3424
- const assigned = await this.getAssignedContent();
3425
- const due = assigned.filter((c) => now.isAfter(import_moment4.default.utc(c.activeOn, REVIEW_TIME_FORMAT2)));
3426
- logger.info(`Due content: ${JSON.stringify(due)}`);
3427
- let ret = [];
3428
- for (let i = 0; i < due.length; i++) {
3429
- const content = due[i];
3430
- if (content.type === "course") {
3431
- const db = new CourseDB(content.courseID, async () => this._user);
3432
- ret = ret.concat(await db.getNewCards());
3433
- } else if (content.type === "tag") {
3434
- const tagDoc = await getTag(content.courseID, content.tagID);
3435
- ret = ret.concat(
3436
- tagDoc.taggedCards.map((c) => {
3437
- return {
3438
- courseID: content.courseID,
3439
- cardID: c,
3440
- qualifiedID: `${content.courseID}-${c}`,
3441
- contentSourceType: "classroom",
3442
- contentSourceID: this._id,
3443
- status: "new"
3444
- };
3445
- })
3446
- );
3447
- } else if (content.type === "card") {
3448
- ret.push(await getCourseDB2(content.courseID).get(content.cardID));
3449
- }
3450
- }
3451
- logger.info(
3452
- `New Cards from classroom ${this._cfg.name}: ${ret.map((c) => `${c.courseID}-${c.cardID}`)}`
3453
- );
3454
- return ret.filter((c) => {
3455
- if (activeCards.some((ac) => c.cardID === ac.cardID)) {
3456
- return false;
3457
- } else {
3458
- return true;
3459
- }
3460
- });
3461
- }
3462
2295
  /**
3463
2296
  * Get cards with suitability scores for presentation.
3464
2297
  *
3465
- * This implementation wraps the legacy getNewCards/getPendingReviews methods,
3466
- * assigning score=1.0 to all cards. StudentClassroomDB does not currently
3467
- * support pluggable navigation strategies.
2298
+ * Gathers new cards from assigned content (courses, tags, cards) and
2299
+ * pending reviews scheduled for this classroom. Assigns score=1.0 to all.
3468
2300
  *
3469
2301
  * @param limit - Maximum number of cards to return
3470
2302
  * @returns Cards sorted by score descending (all scores = 1.0)
3471
2303
  */
3472
2304
  async getWeightedCards(limit) {
3473
- const [newCards, reviews] = await Promise.all([this.getNewCards(), this.getPendingReviews()]);
3474
- const weighted = [
3475
- ...newCards.map((c) => ({
3476
- cardId: c.cardID,
3477
- courseId: c.courseID,
2305
+ const weighted = [];
2306
+ const allUserReviews = await this._user.getPendingReviews();
2307
+ const classroomReviews = allUserReviews.filter(
2308
+ (r) => r.scheduledFor === "classroom" && r.schedulingAgentId === this._id
2309
+ );
2310
+ for (const r of classroomReviews) {
2311
+ weighted.push({
2312
+ cardId: r.cardId,
2313
+ courseId: r.courseId,
3478
2314
  score: 1,
2315
+ reviewID: r._id,
3479
2316
  provenance: [
3480
2317
  {
3481
2318
  strategy: "classroom",
@@ -3483,27 +2320,84 @@ var init_classroomDB2 = __esm({
3483
2320
  strategyId: "CLASSROOM",
3484
2321
  action: "generated",
3485
2322
  score: 1,
3486
- reason: "Classroom legacy getNewCards(), new card"
2323
+ reason: "Classroom scheduled review"
3487
2324
  }
3488
2325
  ]
3489
- })),
3490
- ...reviews.map((r) => ({
3491
- cardId: r.cardID,
3492
- courseId: r.courseID,
3493
- score: 1,
3494
- provenance: [
3495
- {
3496
- strategy: "classroom",
3497
- strategyName: "Classroom",
3498
- strategyId: "CLASSROOM",
3499
- action: "generated",
3500
- score: 1,
3501
- reason: "Classroom legacy getPendingReviews(), review"
2326
+ });
2327
+ }
2328
+ const activeCards = await this._user.getActiveCards();
2329
+ const activeCardIds = new Set(activeCards.map((ac) => ac.cardID));
2330
+ const now = import_moment4.default.utc();
2331
+ const assigned = await this.getAssignedContent();
2332
+ const due = assigned.filter((c) => now.isAfter(import_moment4.default.utc(c.activeOn, REVIEW_TIME_FORMAT2)));
2333
+ logger.info(`[StudentClassroomDB] Due content: ${JSON.stringify(due)}`);
2334
+ for (const content of due) {
2335
+ if (content.type === "course") {
2336
+ const db = new CourseDB(content.courseID, async () => this._user);
2337
+ const courseCards = await db.getWeightedCards(limit);
2338
+ for (const card of courseCards) {
2339
+ if (!activeCardIds.has(card.cardId)) {
2340
+ weighted.push({
2341
+ ...card,
2342
+ provenance: [
2343
+ ...card.provenance,
2344
+ {
2345
+ strategy: "classroom",
2346
+ strategyName: "Classroom",
2347
+ strategyId: "CLASSROOM",
2348
+ action: "passed",
2349
+ score: card.score,
2350
+ reason: `Assigned via classroom from course ${content.courseID}`
2351
+ }
2352
+ ]
2353
+ });
3502
2354
  }
3503
- ]
3504
- }))
3505
- ];
3506
- return weighted.slice(0, limit);
2355
+ }
2356
+ } else if (content.type === "tag") {
2357
+ const tagDoc = await getTag(content.courseID, content.tagID);
2358
+ for (const cardId of tagDoc.taggedCards) {
2359
+ if (!activeCardIds.has(cardId)) {
2360
+ weighted.push({
2361
+ cardId,
2362
+ courseId: content.courseID,
2363
+ score: 1,
2364
+ provenance: [
2365
+ {
2366
+ strategy: "classroom",
2367
+ strategyName: "Classroom",
2368
+ strategyId: "CLASSROOM",
2369
+ action: "generated",
2370
+ score: 1,
2371
+ reason: `Classroom assigned tag: ${content.tagID}, new card`
2372
+ }
2373
+ ]
2374
+ });
2375
+ }
2376
+ }
2377
+ } else if (content.type === "card") {
2378
+ if (!activeCardIds.has(content.cardID)) {
2379
+ weighted.push({
2380
+ cardId: content.cardID,
2381
+ courseId: content.courseID,
2382
+ score: 1,
2383
+ provenance: [
2384
+ {
2385
+ strategy: "classroom",
2386
+ strategyName: "Classroom",
2387
+ strategyId: "CLASSROOM",
2388
+ action: "generated",
2389
+ score: 1,
2390
+ reason: "Classroom assigned card, new card"
2391
+ }
2392
+ ]
2393
+ });
2394
+ }
2395
+ }
2396
+ }
2397
+ logger.info(
2398
+ `[StudentClassroomDB] New cards from classroom ${this._cfg.name}: ${weighted.length} total (reviews + new)`
2399
+ );
2400
+ return weighted.sort((a, b) => b.score - a.score).slice(0, limit);
3507
2401
  }
3508
2402
  };
3509
2403
  }
@@ -3534,14 +2428,14 @@ var init_auth = __esm({
3534
2428
  });
3535
2429
 
3536
2430
  // src/impl/couch/CouchDBSyncStrategy.ts
3537
- var import_common10;
2431
+ var import_common8;
3538
2432
  var init_CouchDBSyncStrategy = __esm({
3539
2433
  "src/impl/couch/CouchDBSyncStrategy.ts"() {
3540
2434
  "use strict";
3541
2435
  init_factory();
3542
2436
  init_types_legacy();
3543
2437
  init_logger();
3544
- import_common10 = require("@vue-skuilder/common");
2438
+ import_common8 = require("@vue-skuilder/common");
3545
2439
  init_common();
3546
2440
  init_pouchdb_setup();
3547
2441
  init_couch();
@@ -3727,13 +2621,13 @@ async function dropUserFromClassroom(user, classID) {
3727
2621
  async function getUserClassrooms(user) {
3728
2622
  return getOrCreateClassroomRegistrationsDoc(user);
3729
2623
  }
3730
- var import_common12, import_moment6, log3, BaseUser, userCoursesDoc, userClassroomsDoc;
2624
+ var import_common10, import_moment6, log3, BaseUser, userCoursesDoc, userClassroomsDoc;
3731
2625
  var init_BaseUserDB = __esm({
3732
2626
  "src/impl/common/BaseUserDB.ts"() {
3733
2627
  "use strict";
3734
2628
  init_core();
3735
2629
  init_util();
3736
- import_common12 = require("@vue-skuilder/common");
2630
+ import_common10 = require("@vue-skuilder/common");
3737
2631
  import_moment6 = __toESM(require("moment"), 1);
3738
2632
  init_types_legacy();
3739
2633
  init_logger();
@@ -3783,7 +2677,7 @@ Currently logged-in as ${this._username}.`
3783
2677
  );
3784
2678
  }
3785
2679
  const result = await this.syncStrategy.createAccount(username, password);
3786
- if (result.status === import_common12.Status.ok) {
2680
+ if (result.status === import_common10.Status.ok) {
3787
2681
  log3(`Account created successfully, updating username to ${username}`);
3788
2682
  this._username = username;
3789
2683
  try {
@@ -3825,7 +2719,7 @@ Currently logged-in as ${this._username}.`
3825
2719
  async resetUserData() {
3826
2720
  if (this.syncStrategy.canAuthenticate()) {
3827
2721
  return {
3828
- status: import_common12.Status.error,
2722
+ status: import_common10.Status.error,
3829
2723
  error: "Reset user data is only available for local-only mode. Use logout instead for remote sync."
3830
2724
  };
3831
2725
  }
@@ -3844,11 +2738,11 @@ Currently logged-in as ${this._username}.`
3844
2738
  await localDB.bulkDocs(docsToDelete);
3845
2739
  }
3846
2740
  await this.init();
3847
- return { status: import_common12.Status.ok };
2741
+ return { status: import_common10.Status.ok };
3848
2742
  } catch (error) {
3849
2743
  logger.error("Failed to reset user data:", error);
3850
2744
  return {
3851
- status: import_common12.Status.error,
2745
+ status: import_common10.Status.error,
3852
2746
  error: error instanceof Error ? error.message : "Unknown error during reset"
3853
2747
  };
3854
2748
  }
@@ -4628,11 +3522,11 @@ var init_factory = __esm({
4628
3522
  });
4629
3523
 
4630
3524
  // src/study/TagFilteredContentSource.ts
4631
- var import_common14, TagFilteredContentSource;
3525
+ var import_common12, TagFilteredContentSource;
4632
3526
  var init_TagFilteredContentSource = __esm({
4633
3527
  "src/study/TagFilteredContentSource.ts"() {
4634
3528
  "use strict";
4635
- import_common14 = require("@vue-skuilder/common");
3529
+ import_common12 = require("@vue-skuilder/common");
4636
3530
  init_courseDB();
4637
3531
  init_logger();
4638
3532
  TagFilteredContentSource = class {
@@ -4708,108 +3602,71 @@ var init_TagFilteredContentSource = __esm({
4708
3602
  return finalCardIds;
4709
3603
  }
4710
3604
  /**
4711
- * Gets new cards that match the tag filter and are not already active for the user.
3605
+ * Get cards with suitability scores for presentation.
3606
+ *
3607
+ * Filters cards by tag inclusion/exclusion and assigns score=1.0 to all.
3608
+ * TagFilteredContentSource does not currently support pluggable navigation
3609
+ * strategies - it returns flat-scored candidates.
3610
+ *
3611
+ * @param limit - Maximum number of cards to return
3612
+ * @returns Cards sorted by score descending (all scores = 1.0)
4712
3613
  */
4713
- async getNewCards(limit) {
4714
- if (!(0, import_common14.hasActiveFilter)(this.filter)) {
4715
- logger.warn("[TagFilteredContentSource] getNewCards called with no active filter");
3614
+ async getWeightedCards(limit) {
3615
+ if (!(0, import_common12.hasActiveFilter)(this.filter)) {
3616
+ logger.warn("[TagFilteredContentSource] getWeightedCards called with no active filter");
4716
3617
  return [];
4717
3618
  }
4718
3619
  const eligibleCardIds = await this.resolveFilteredCardIds();
4719
3620
  const activeCards = await this.user.getActiveCards();
4720
3621
  const activeCardIds = new Set(activeCards.map((c) => c.cardID));
4721
- const newItems = [];
3622
+ const newCardWeighted = [];
4722
3623
  for (const cardId of eligibleCardIds) {
4723
3624
  if (!activeCardIds.has(cardId)) {
4724
- newItems.push({
4725
- courseID: this.courseId,
4726
- cardID: cardId,
4727
- contentSourceType: "course",
4728
- contentSourceID: this.courseId,
4729
- status: "new"
3625
+ newCardWeighted.push({
3626
+ cardId,
3627
+ courseId: this.courseId,
3628
+ score: 1,
3629
+ provenance: [
3630
+ {
3631
+ strategy: "tagFilter",
3632
+ strategyName: "Tag Filter",
3633
+ strategyId: "TAG_FILTER",
3634
+ action: "generated",
3635
+ score: 1,
3636
+ reason: `Tag-filtered new card (tags: ${this.filter.include.join(", ")})`
3637
+ }
3638
+ ]
4730
3639
  });
4731
3640
  }
4732
- if (limit !== void 0 && newItems.length >= limit) {
3641
+ if (newCardWeighted.length >= limit) {
4733
3642
  break;
4734
3643
  }
4735
3644
  }
4736
- logger.info(`[TagFilteredContentSource] Found ${newItems.length} new cards matching filter`);
4737
- return newItems;
4738
- }
4739
- /**
4740
- * Gets pending reviews, filtered to only include cards that match the tag filter.
4741
- */
4742
- async getPendingReviews() {
4743
- if (!(0, import_common14.hasActiveFilter)(this.filter)) {
4744
- logger.warn("[TagFilteredContentSource] getPendingReviews called with no active filter");
4745
- return [];
4746
- }
4747
- const eligibleCardIds = await this.resolveFilteredCardIds();
3645
+ logger.info(
3646
+ `[TagFilteredContentSource] Found ${newCardWeighted.length} new cards matching filter`
3647
+ );
4748
3648
  const allReviews = await this.user.getPendingReviews(this.courseId);
4749
- const filteredReviews = allReviews.filter((review) => {
4750
- return eligibleCardIds.has(review.cardId);
4751
- });
3649
+ const filteredReviews = allReviews.filter((review) => eligibleCardIds.has(review.cardId));
4752
3650
  logger.info(
4753
3651
  `[TagFilteredContentSource] Found ${filteredReviews.length} pending reviews matching filter (of ${allReviews.length} total)`
4754
3652
  );
4755
- return filteredReviews.map((r) => ({
4756
- ...r,
4757
- courseID: r.courseId,
4758
- cardID: r.cardId,
4759
- contentSourceType: "course",
4760
- contentSourceID: this.courseId,
3653
+ const reviewWeighted = filteredReviews.map((r) => ({
3654
+ cardId: r.cardId,
3655
+ courseId: r.courseId,
3656
+ score: 1,
4761
3657
  reviewID: r._id,
4762
- status: "review"
3658
+ provenance: [
3659
+ {
3660
+ strategy: "tagFilter",
3661
+ strategyName: "Tag Filter",
3662
+ strategyId: "TAG_FILTER",
3663
+ action: "generated",
3664
+ score: 1,
3665
+ reason: `Tag-filtered review (tags: ${this.filter.include.join(", ")})`
3666
+ }
3667
+ ]
4763
3668
  }));
4764
- }
4765
- /**
4766
- * Get cards with suitability scores for presentation.
4767
- *
4768
- * This implementation wraps the legacy getNewCards/getPendingReviews methods,
4769
- * assigning score=1.0 to all cards. TagFilteredContentSource does not currently
4770
- * support pluggable navigation strategies - it returns flat-scored candidates.
4771
- *
4772
- * @param limit - Maximum number of cards to return
4773
- * @returns Cards sorted by score descending (all scores = 1.0)
4774
- */
4775
- async getWeightedCards(limit) {
4776
- const [newCards, reviews] = await Promise.all([
4777
- this.getNewCards(limit),
4778
- this.getPendingReviews()
4779
- ]);
4780
- const weighted = [
4781
- ...reviews.map((r) => ({
4782
- cardId: r.cardID,
4783
- courseId: r.courseID,
4784
- score: 1,
4785
- provenance: [
4786
- {
4787
- strategy: "tagFilter",
4788
- strategyName: "Tag Filter",
4789
- strategyId: "TAG_FILTER",
4790
- action: "generated",
4791
- score: 1,
4792
- reason: `Tag-filtered review (tags: ${this.filter.include.join(", ")})`
4793
- }
4794
- ]
4795
- })),
4796
- ...newCards.map((c) => ({
4797
- cardId: c.cardID,
4798
- courseId: c.courseID,
4799
- score: 1,
4800
- provenance: [
4801
- {
4802
- strategy: "tagFilter",
4803
- strategyName: "Tag Filter",
4804
- strategyId: "TAG_FILTER",
4805
- action: "generated",
4806
- score: 1,
4807
- reason: `Tag-filtered new card (tags: ${this.filter.include.join(", ")})`
4808
- }
4809
- ]
4810
- }))
4811
- ];
4812
- return weighted.slice(0, limit);
3669
+ return [...reviewWeighted, ...newCardWeighted].slice(0, limit);
4813
3670
  }
4814
3671
  /**
4815
3672
  * Clears the cached resolved card IDs.
@@ -4843,19 +3700,19 @@ async function getStudySource(source, user) {
4843
3700
  if (source.type === "classroom") {
4844
3701
  return await StudentClassroomDB.factory(source.id, user);
4845
3702
  } else {
4846
- if ((0, import_common15.hasActiveFilter)(source.tagFilter)) {
3703
+ if ((0, import_common13.hasActiveFilter)(source.tagFilter)) {
4847
3704
  return new TagFilteredContentSource(source.id, source.tagFilter, user);
4848
3705
  }
4849
3706
  return getDataLayer().getCourseDB(source.id);
4850
3707
  }
4851
3708
  }
4852
- var import_common15;
3709
+ var import_common13;
4853
3710
  var init_contentSource = __esm({
4854
3711
  "src/core/interfaces/contentSource.ts"() {
4855
3712
  "use strict";
4856
3713
  init_factory();
4857
3714
  init_classroomDB2();
4858
- import_common15 = require("@vue-skuilder/common");
3715
+ import_common13 = require("@vue-skuilder/common");
4859
3716
  init_TagFilteredContentSource();
4860
3717
  }
4861
3718
  });
@@ -4979,7 +3836,7 @@ elo: ${elo}`;
4979
3836
  misc: {}
4980
3837
  } : void 0
4981
3838
  );
4982
- if (result.status === import_common16.Status.ok) {
3839
+ if (result.status === import_common14.Status.ok) {
4983
3840
  return {
4984
3841
  originalText,
4985
3842
  status: "success",
@@ -5023,17 +3880,17 @@ function validateProcessorConfig(config) {
5023
3880
  }
5024
3881
  return { isValid: true };
5025
3882
  }
5026
- var import_common16;
3883
+ var import_common14;
5027
3884
  var init_cardProcessor = __esm({
5028
3885
  "src/core/bulkImport/cardProcessor.ts"() {
5029
3886
  "use strict";
5030
- import_common16 = require("@vue-skuilder/common");
3887
+ import_common14 = require("@vue-skuilder/common");
5031
3888
  init_logger();
5032
3889
  }
5033
3890
  });
5034
3891
 
5035
3892
  // src/core/bulkImport/types.ts
5036
- var init_types3 = __esm({
3893
+ var init_types = __esm({
5037
3894
  "src/core/bulkImport/types.ts"() {
5038
3895
  "use strict";
5039
3896
  }
@@ -5044,7 +3901,7 @@ var init_bulkImport = __esm({
5044
3901
  "src/core/bulkImport/index.ts"() {
5045
3902
  "use strict";
5046
3903
  init_cardProcessor();
5047
- init_types3();
3904
+ init_types();
5048
3905
  }
5049
3906
  });
5050
3907