@vue-skuilder/db 0.1.22 → 0.1.24

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 (67) hide show
  1. package/dist/{contentSource-BP9hznNV.d.ts → contentSource-BotbOOfX.d.ts} +227 -3
  2. package/dist/{contentSource-DsJadoBU.d.cts → contentSource-C90LH-OH.d.cts} +227 -3
  3. package/dist/core/index.d.cts +220 -6
  4. package/dist/core/index.d.ts +220 -6
  5. package/dist/core/index.js +2052 -559
  6. package/dist/core/index.js.map +1 -1
  7. package/dist/core/index.mjs +2035 -555
  8. package/dist/core/index.mjs.map +1 -1
  9. package/dist/{dataLayerProvider-CHYrQ5pB.d.cts → dataLayerProvider-DGKp4zFB.d.cts} +1 -1
  10. package/dist/{dataLayerProvider-MDTxXq2l.d.ts → dataLayerProvider-SBpz9jQf.d.ts} +1 -1
  11. package/dist/impl/couch/index.d.cts +11 -3
  12. package/dist/impl/couch/index.d.ts +11 -3
  13. package/dist/impl/couch/index.js +1811 -574
  14. package/dist/impl/couch/index.js.map +1 -1
  15. package/dist/impl/couch/index.mjs +1792 -550
  16. package/dist/impl/couch/index.mjs.map +1 -1
  17. package/dist/impl/static/index.d.cts +4 -4
  18. package/dist/impl/static/index.d.ts +4 -4
  19. package/dist/impl/static/index.js +1797 -560
  20. package/dist/impl/static/index.js.map +1 -1
  21. package/dist/impl/static/index.mjs +1789 -547
  22. package/dist/impl/static/index.mjs.map +1 -1
  23. package/dist/{index-Dj0SEgk3.d.ts → index-BWvO-_rJ.d.ts} +1 -1
  24. package/dist/{index-B_j6u5E4.d.cts → index-Ba7hYbHj.d.cts} +1 -1
  25. package/dist/index.d.cts +150 -12
  26. package/dist/index.d.ts +150 -12
  27. package/dist/index.js +2658 -791
  28. package/dist/index.js.map +1 -1
  29. package/dist/index.mjs +2584 -747
  30. package/dist/index.mjs.map +1 -1
  31. package/dist/{types-DQaXnuoc.d.ts → types-CJrLM1Ew.d.ts} +1 -1
  32. package/dist/{types-Bn0itutr.d.cts → types-W8n-B6HG.d.cts} +1 -1
  33. package/dist/{types-legacy-DDY4N-Uq.d.cts → types-legacy-JXDxinpU.d.cts} +5 -1
  34. package/dist/{types-legacy-DDY4N-Uq.d.ts → types-legacy-JXDxinpU.d.ts} +5 -1
  35. package/dist/util/packer/index.d.cts +3 -3
  36. package/dist/util/packer/index.d.ts +3 -3
  37. package/docs/brainstorm-navigation-paradigm.md +40 -34
  38. package/docs/future-orchestration-vision.md +216 -0
  39. package/docs/navigators-architecture.md +188 -5
  40. package/docs/todo-strategy-authoring.md +8 -6
  41. package/package.json +3 -3
  42. package/src/core/index.ts +2 -0
  43. package/src/core/interfaces/contentSource.ts +7 -0
  44. package/src/core/interfaces/userDB.ts +6 -0
  45. package/src/core/navigators/Pipeline.ts +46 -0
  46. package/src/core/navigators/PipelineAssembler.ts +14 -1
  47. package/src/core/navigators/filters/WeightedFilter.ts +141 -0
  48. package/src/core/navigators/filters/types.ts +4 -0
  49. package/src/core/navigators/generators/CompositeGenerator.ts +61 -19
  50. package/src/core/navigators/generators/types.ts +4 -0
  51. package/src/core/navigators/index.ts +194 -13
  52. package/src/core/orchestration/gradient.ts +133 -0
  53. package/src/core/orchestration/index.ts +210 -0
  54. package/src/core/orchestration/learning.ts +250 -0
  55. package/src/core/orchestration/recording.ts +92 -0
  56. package/src/core/orchestration/signal.ts +67 -0
  57. package/src/core/types/contentNavigationStrategy.ts +38 -0
  58. package/src/core/types/learningState.ts +77 -0
  59. package/src/core/types/types-legacy.ts +4 -0
  60. package/src/core/types/userOutcome.ts +51 -0
  61. package/src/courseConfigRegistration.ts +546 -0
  62. package/src/factory.ts +6 -0
  63. package/src/impl/common/BaseUserDB.ts +16 -0
  64. package/src/index.ts +2 -0
  65. package/src/study/SessionController.ts +64 -1
  66. package/tests/core/navigators/Pipeline.test.ts +2 -0
  67. package/docs/todo-evolutionary-orchestration.md +0 -310
@@ -5,6 +5,11 @@ 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
+ };
8
13
  var __esm = (fn, res) => function __init() {
9
14
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
10
15
  };
@@ -279,7 +284,9 @@ var init_types_legacy = __esm({
279
284
  ["VIEW" /* VIEW */]: "VIEW",
280
285
  ["PEDAGOGY" /* PEDAGOGY */]: "PEDAGOGY",
281
286
  ["NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */]: "NAVIGATION_STRATEGY",
282
- ["STRATEGY_STATE" /* STRATEGY_STATE */]: "STRATEGY_STATE"
287
+ ["STRATEGY_STATE" /* STRATEGY_STATE */]: "STRATEGY_STATE",
288
+ ["USER_OUTCOME" /* USER_OUTCOME */]: "USER_OUTCOME",
289
+ ["STRATEGY_LEARNING_STATE" /* STRATEGY_LEARNING_STATE */]: "STRATEGY_LEARNING_STATE"
283
290
  };
284
291
  }
285
292
  });
@@ -620,175 +627,1384 @@ var init_courseLookupDB = __esm({
620
627
  }
621
628
  });
622
629
 
623
- // src/core/navigators/index.ts
624
- function isGenerator(impl) {
625
- return NavigatorRoles[impl] === "generator" /* GENERATOR */;
626
- }
627
- function isFilter(impl) {
628
- return NavigatorRoles[impl] === "filter" /* FILTER */;
629
- }
630
- var NavigatorRoles, ContentNavigator;
631
- var init_navigators = __esm({
632
- "src/core/navigators/index.ts"() {
630
+ // src/core/navigators/generators/CompositeGenerator.ts
631
+ var CompositeGenerator_exports = {};
632
+ __export(CompositeGenerator_exports, {
633
+ AggregationMode: () => AggregationMode,
634
+ default: () => CompositeGenerator
635
+ });
636
+ var AggregationMode, DEFAULT_AGGREGATION_MODE, FREQUENCY_BOOST_FACTOR, CompositeGenerator;
637
+ var init_CompositeGenerator = __esm({
638
+ "src/core/navigators/generators/CompositeGenerator.ts"() {
633
639
  "use strict";
640
+ init_navigators();
634
641
  init_logger();
635
- NavigatorRoles = {
636
- ["elo" /* ELO */]: "generator" /* GENERATOR */,
637
- ["srs" /* SRS */]: "generator" /* GENERATOR */,
638
- ["hierarchyDefinition" /* HIERARCHY */]: "filter" /* FILTER */,
639
- ["interferenceMitigator" /* INTERFERENCE */]: "filter" /* FILTER */,
640
- ["relativePriority" /* RELATIVE_PRIORITY */]: "filter" /* FILTER */,
641
- ["userTagPreference" /* USER_TAG_PREFERENCE */]: "filter" /* FILTER */
642
- };
643
- ContentNavigator = class {
644
- /** User interface for this navigation session */
645
- user;
646
- /** Course interface for this navigation session */
647
- course;
648
- /** Human-readable name for this strategy instance (from ContentNavigationStrategyData.name) */
649
- strategyName;
650
- /** Unique document ID for this strategy instance (from ContentNavigationStrategyData._id) */
651
- strategyId;
652
- /**
653
- * Constructor for standard navigators.
654
- * Call this from subclass constructors to initialize common fields.
655
- *
656
- * Note: CompositeGenerator and Pipeline call super() without args, then set
657
- * user/course fields directly if needed.
658
- */
659
- constructor(user, course, strategyData) {
660
- this.user = user;
661
- this.course = course;
662
- if (strategyData) {
663
- this.strategyName = strategyData.name;
664
- this.strategyId = strategyData._id;
642
+ AggregationMode = /* @__PURE__ */ ((AggregationMode2) => {
643
+ AggregationMode2["MAX"] = "max";
644
+ AggregationMode2["AVERAGE"] = "average";
645
+ AggregationMode2["FREQUENCY_BOOST"] = "frequencyBoost";
646
+ return AggregationMode2;
647
+ })(AggregationMode || {});
648
+ DEFAULT_AGGREGATION_MODE = "frequencyBoost" /* FREQUENCY_BOOST */;
649
+ FREQUENCY_BOOST_FACTOR = 0.1;
650
+ CompositeGenerator = class _CompositeGenerator extends ContentNavigator {
651
+ /** Human-readable name for CardGenerator interface */
652
+ name = "Composite Generator";
653
+ generators;
654
+ aggregationMode;
655
+ constructor(generators, aggregationMode = DEFAULT_AGGREGATION_MODE) {
656
+ super();
657
+ this.generators = generators;
658
+ this.aggregationMode = aggregationMode;
659
+ if (generators.length === 0) {
660
+ throw new Error("CompositeGenerator requires at least one generator");
665
661
  }
662
+ logger.debug(
663
+ `[CompositeGenerator] Created with ${generators.length} generators, mode: ${aggregationMode}`
664
+ );
666
665
  }
667
- // ============================================================================
668
- // STRATEGY STATE HELPERS
669
- // ============================================================================
670
- //
671
- // These methods allow strategies to persist their own state (user preferences,
672
- // learned patterns, temporal tracking) in the user database.
673
- //
674
- // ============================================================================
675
666
  /**
676
- * Unique key identifying this strategy for state storage.
667
+ * Creates a CompositeGenerator from strategy data.
677
668
  *
678
- * Defaults to the constructor name (e.g., "UserTagPreferenceFilter").
679
- * Override in subclasses if multiple instances of the same strategy type
680
- * need separate state storage.
669
+ * This is a convenience factory for use by PipelineAssembler.
681
670
  */
682
- get strategyKey() {
683
- return this.constructor.name;
671
+ static async fromStrategies(user, course, strategies, aggregationMode = DEFAULT_AGGREGATION_MODE) {
672
+ const generators = await Promise.all(
673
+ strategies.map((s) => ContentNavigator.create(user, course, s))
674
+ );
675
+ return new _CompositeGenerator(generators, aggregationMode);
684
676
  }
685
677
  /**
686
- * Get this strategy's persisted state for the current course.
678
+ * Get weighted cards from all generators, merge and deduplicate.
687
679
  *
688
- * @returns The strategy's data payload, or null if no state exists
689
- * @throws Error if user or course is not initialized
680
+ * Cards appearing in multiple generators receive a score boost.
681
+ * Provenance tracks which generators produced each card and how scores were aggregated.
682
+ *
683
+ * This method supports both the legacy signature (limit only) and the
684
+ * CardGenerator interface signature (limit, context).
685
+ *
686
+ * @param limit - Maximum number of cards to return
687
+ * @param context - GeneratorContext passed to child generators (required when called via Pipeline)
690
688
  */
691
- async getStrategyState() {
692
- if (!this.user || !this.course) {
689
+ async getWeightedCards(limit, context) {
690
+ if (!context) {
693
691
  throw new Error(
694
- `Cannot get strategy state: navigator not properly initialized. Ensure user and course are provided to constructor.`
692
+ "CompositeGenerator.getWeightedCards requires a GeneratorContext. It should be called via Pipeline, not directly."
695
693
  );
696
694
  }
697
- return this.user.getStrategyState(this.course.getCourseID(), this.strategyKey);
695
+ const results = await Promise.all(
696
+ this.generators.map((g) => g.getWeightedCards(limit, context))
697
+ );
698
+ const byCardId = /* @__PURE__ */ new Map();
699
+ results.forEach((cards, index) => {
700
+ const gen = this.generators[index];
701
+ let weight = gen.learnable?.weight ?? 1;
702
+ let deviation;
703
+ if (gen.learnable && !gen.staticWeight && context.orchestration) {
704
+ const strategyId = gen.strategyId;
705
+ if (strategyId) {
706
+ weight = context.orchestration.getEffectiveWeight(strategyId, gen.learnable);
707
+ deviation = context.orchestration.getDeviation(strategyId);
708
+ }
709
+ }
710
+ for (const card of cards) {
711
+ if (card.provenance.length > 0) {
712
+ card.provenance[0].effectiveWeight = weight;
713
+ card.provenance[0].deviation = deviation;
714
+ }
715
+ const existing = byCardId.get(card.cardId) || [];
716
+ existing.push({ card, weight });
717
+ byCardId.set(card.cardId, existing);
718
+ }
719
+ });
720
+ const merged = [];
721
+ for (const [, items] of byCardId) {
722
+ const cards = items.map((i) => i.card);
723
+ const aggregatedScore = this.aggregateScores(items);
724
+ const finalScore = Math.min(1, aggregatedScore);
725
+ const mergedProvenance = cards.flatMap((c) => c.provenance);
726
+ const initialScore = cards[0].score;
727
+ const action = finalScore > initialScore ? "boosted" : finalScore < initialScore ? "penalized" : "passed";
728
+ const reason = this.buildAggregationReason(items, finalScore);
729
+ merged.push({
730
+ ...cards[0],
731
+ score: finalScore,
732
+ provenance: [
733
+ ...mergedProvenance,
734
+ {
735
+ strategy: "composite",
736
+ strategyName: "Composite Generator",
737
+ strategyId: "COMPOSITE_GENERATOR",
738
+ action,
739
+ score: finalScore,
740
+ reason
741
+ }
742
+ ]
743
+ });
744
+ }
745
+ return merged.sort((a, b) => b.score - a.score).slice(0, limit);
698
746
  }
699
747
  /**
700
- * Persist this strategy's state for the current course.
701
- *
702
- * @param data - The strategy's data payload to store
703
- * @throws Error if user or course is not initialized
748
+ * Build human-readable reason for score aggregation.
704
749
  */
705
- async putStrategyState(data) {
706
- if (!this.user || !this.course) {
707
- throw new Error(
708
- `Cannot put strategy state: navigator not properly initialized. Ensure user and course are provided to constructor.`
709
- );
750
+ buildAggregationReason(items, finalScore) {
751
+ const cards = items.map((i) => i.card);
752
+ const count = cards.length;
753
+ const scores = cards.map((c) => c.score.toFixed(2)).join(", ");
754
+ if (count === 1) {
755
+ const weightMsg = Math.abs(items[0].weight - 1) > 1e-3 ? ` (w=${items[0].weight.toFixed(2)})` : "";
756
+ return `Single generator, score ${finalScore.toFixed(2)}${weightMsg}`;
757
+ }
758
+ const strategies = cards.map((c) => c.provenance[0]?.strategy || "unknown").join(", ");
759
+ switch (this.aggregationMode) {
760
+ case "max" /* MAX */:
761
+ return `Max of ${count} generators (${strategies}): scores [${scores}] \u2192 ${finalScore.toFixed(2)}`;
762
+ case "average" /* AVERAGE */:
763
+ return `Weighted Avg of ${count} generators (${strategies}): scores [${scores}] \u2192 ${finalScore.toFixed(2)}`;
764
+ case "frequencyBoost" /* FREQUENCY_BOOST */: {
765
+ const totalWeight = items.reduce((sum, i) => sum + i.weight, 0);
766
+ const weightedSum = items.reduce((sum, i) => sum + i.card.score * i.weight, 0);
767
+ const avg = totalWeight > 0 ? weightedSum / totalWeight : 0;
768
+ const boost = 1 + FREQUENCY_BOOST_FACTOR * (count - 1);
769
+ return `Frequency boost from ${count} generators (${strategies}): w-avg ${avg.toFixed(2)} \xD7 ${boost.toFixed(2)} \u2192 ${finalScore.toFixed(2)}`;
770
+ }
771
+ default:
772
+ return `Aggregated from ${count} generators: ${finalScore.toFixed(2)}`;
710
773
  }
711
- return this.user.putStrategyState(this.course.getCourseID(), this.strategyKey, data);
712
774
  }
713
775
  /**
714
- * Factory method to create navigator instances dynamically.
715
- *
716
- * @param user - User interface
717
- * @param course - Course interface
718
- * @param strategyData - Strategy configuration document
719
- * @returns the runtime object used to steer a study session.
776
+ * Aggregate scores from multiple generators for the same card.
720
777
  */
721
- static async create(user, course, strategyData) {
722
- const implementingClass = strategyData.implementingClass;
723
- let NavigatorImpl;
724
- const variations = [".ts", ".js", ""];
725
- const dirs = ["filters", "generators"];
726
- for (const ext of variations) {
727
- for (const dir of dirs) {
728
- const loadFrom = `./${dir}/${implementingClass}${ext}`;
729
- try {
730
- const module2 = await import(loadFrom);
731
- NavigatorImpl = module2.default;
732
- break;
733
- } catch (e) {
734
- logger.debug(`Failed to load extension from ${loadFrom}:`, e);
735
- }
778
+ aggregateScores(items) {
779
+ const scores = items.map((i) => i.card.score);
780
+ switch (this.aggregationMode) {
781
+ case "max" /* MAX */:
782
+ return Math.max(...scores);
783
+ case "average" /* AVERAGE */: {
784
+ const totalWeight = items.reduce((sum, i) => sum + i.weight, 0);
785
+ if (totalWeight === 0) return 0;
786
+ const weightedSum = items.reduce((sum, i) => sum + i.card.score * i.weight, 0);
787
+ return weightedSum / totalWeight;
736
788
  }
789
+ case "frequencyBoost" /* FREQUENCY_BOOST */: {
790
+ const totalWeight = items.reduce((sum, i) => sum + i.weight, 0);
791
+ const weightedSum = items.reduce((sum, i) => sum + i.card.score * i.weight, 0);
792
+ const avg = totalWeight > 0 ? weightedSum / totalWeight : 0;
793
+ const frequencyBoost = 1 + FREQUENCY_BOOST_FACTOR * (items.length - 1);
794
+ return avg * frequencyBoost;
795
+ }
796
+ default:
797
+ return scores[0];
737
798
  }
738
- if (!NavigatorImpl) {
739
- throw new Error(`Could not load navigator implementation for: ${implementingClass}`);
740
- }
741
- return new NavigatorImpl(user, course, strategyData);
799
+ }
800
+ };
801
+ }
802
+ });
803
+
804
+ // src/core/navigators/generators/elo.ts
805
+ var elo_exports = {};
806
+ __export(elo_exports, {
807
+ default: () => ELONavigator
808
+ });
809
+ var import_common5, ELONavigator;
810
+ var init_elo = __esm({
811
+ "src/core/navigators/generators/elo.ts"() {
812
+ "use strict";
813
+ init_navigators();
814
+ import_common5 = require("@vue-skuilder/common");
815
+ ELONavigator = class extends ContentNavigator {
816
+ /** Human-readable name for CardGenerator interface */
817
+ name;
818
+ constructor(user, course, strategyData) {
819
+ super(user, course, strategyData);
820
+ this.name = strategyData?.name || "ELO";
742
821
  }
743
822
  /**
744
- * Get cards with suitability scores and provenance trails.
745
- *
746
- * **This is the PRIMARY API for navigation strategies.**
747
- *
748
- * Returns cards ranked by suitability score (0-1). Higher scores indicate
749
- * better candidates for presentation. Each card includes a provenance trail
750
- * documenting how strategies contributed to the final score.
823
+ * Get new cards with suitability scores based on ELO distance.
751
824
  *
752
- * ## Implementation Required
753
- * All navigation strategies MUST override this method. The base class does
754
- * not provide a default implementation.
825
+ * Cards closer to user's ELO get higher scores.
826
+ * Score formula: max(0, 1 - distance / 500)
755
827
  *
756
- * ## For Generators
757
- * Override this method to generate candidates and compute scores based on
758
- * your strategy's logic (e.g., ELO proximity, review urgency). Create the
759
- * initial provenance entry with action='generated'.
828
+ * NOTE: This generator only handles NEW cards. Reviews are handled by
829
+ * SRSNavigator. Use CompositeGenerator to combine both.
760
830
  *
761
- * ## For Filters
762
- * Filters should implement the CardFilter interface instead and be composed
763
- * via Pipeline. Filters do not directly implement getWeightedCards().
831
+ * This method supports both the legacy signature (limit only) and the
832
+ * CardGenerator interface signature (limit, context).
764
833
  *
765
- * @param limit - Maximum cards to return
766
- * @returns Cards sorted by score descending, with provenance trails
834
+ * @param limit - Maximum number of cards to return
835
+ * @param context - Optional GeneratorContext (used when called via Pipeline)
767
836
  */
768
- async getWeightedCards(_limit) {
769
- throw new Error(`${this.constructor.name} must implement getWeightedCards(). `);
770
- }
771
- };
772
- }
773
- });
774
-
775
- // src/core/navigators/Pipeline.ts
776
- function logPipelineConfig(generator, filters) {
777
- const filterList = filters.length > 0 ? "\n - " + filters.map((f) => f.name).join("\n - ") : " none";
778
- logger.info(
779
- `[Pipeline] Configuration:
780
- Generator: ${generator.name}
781
- Filters:${filterList}`
782
- );
783
- }
784
- function logTagHydration(cards, tagsByCard) {
785
- const totalTags = Array.from(tagsByCard.values()).reduce((sum, tags) => sum + tags.length, 0);
786
- const cardsWithTags = Array.from(tagsByCard.values()).filter((tags) => tags.length > 0).length;
787
- logger.debug(
788
- `[Pipeline] Tag hydration: ${cards.length} cards, ${cardsWithTags} have tags (${totalTags} total tags) - single batch query`
789
- );
790
- }
791
- function logExecutionSummary(generatorName, generatedCount, filterCount, finalCount, topScores) {
837
+ async getWeightedCards(limit, context) {
838
+ let userGlobalElo;
839
+ if (context?.userElo !== void 0) {
840
+ userGlobalElo = context.userElo;
841
+ } else {
842
+ const courseReg = await this.user.getCourseRegDoc(this.course.getCourseID());
843
+ const userElo = (0, import_common5.toCourseElo)(courseReg.elo);
844
+ userGlobalElo = userElo.global.score;
845
+ }
846
+ const activeCards = await this.user.getActiveCards();
847
+ const newCards = (await this.course.getCardsCenteredAtELO(
848
+ { limit, elo: "user" },
849
+ (c) => !activeCards.some((ac) => c.cardID === ac.cardID)
850
+ )).map((c) => ({ ...c, status: "new" }));
851
+ const cardIds = newCards.map((c) => c.cardID);
852
+ const cardEloData = await this.course.getCardEloData(cardIds);
853
+ const scored = newCards.map((c, i) => {
854
+ const cardElo = cardEloData[i]?.global?.score ?? 1e3;
855
+ const distance = Math.abs(cardElo - userGlobalElo);
856
+ const score = Math.max(0, 1 - distance / 500);
857
+ return {
858
+ cardId: c.cardID,
859
+ courseId: c.courseID,
860
+ score,
861
+ provenance: [
862
+ {
863
+ strategy: "elo",
864
+ strategyName: this.strategyName || this.name,
865
+ strategyId: this.strategyId || "NAVIGATION_STRATEGY-ELO-default",
866
+ action: "generated",
867
+ score,
868
+ reason: `ELO distance ${Math.round(distance)} (card: ${Math.round(cardElo)}, user: ${Math.round(userGlobalElo)}), new card`
869
+ }
870
+ ]
871
+ };
872
+ });
873
+ scored.sort((a, b) => b.score - a.score);
874
+ return scored.slice(0, limit);
875
+ }
876
+ };
877
+ }
878
+ });
879
+
880
+ // src/core/navigators/generators/index.ts
881
+ var generators_exports = {};
882
+ var init_generators = __esm({
883
+ "src/core/navigators/generators/index.ts"() {
884
+ "use strict";
885
+ }
886
+ });
887
+
888
+ // src/core/navigators/generators/srs.ts
889
+ var srs_exports = {};
890
+ __export(srs_exports, {
891
+ default: () => SRSNavigator
892
+ });
893
+ var import_moment, SRSNavigator;
894
+ var init_srs = __esm({
895
+ "src/core/navigators/generators/srs.ts"() {
896
+ "use strict";
897
+ import_moment = __toESM(require("moment"), 1);
898
+ init_navigators();
899
+ init_logger();
900
+ SRSNavigator = class extends ContentNavigator {
901
+ /** Human-readable name for CardGenerator interface */
902
+ name;
903
+ constructor(user, course, strategyData) {
904
+ super(user, course, strategyData);
905
+ this.name = strategyData?.name || "SRS";
906
+ }
907
+ /**
908
+ * Get review cards scored by urgency.
909
+ *
910
+ * Score formula combines:
911
+ * - Relative overdueness: hoursOverdue / intervalHours
912
+ * - Interval recency: exponential decay favoring shorter intervals
913
+ *
914
+ * Cards not yet due are excluded (not scored as 0).
915
+ *
916
+ * This method supports both the legacy signature (limit only) and the
917
+ * CardGenerator interface signature (limit, context).
918
+ *
919
+ * @param limit - Maximum number of cards to return
920
+ * @param _context - Optional GeneratorContext (currently unused, but required for interface)
921
+ */
922
+ async getWeightedCards(limit, _context) {
923
+ if (!this.user || !this.course) {
924
+ throw new Error("SRSNavigator requires user and course to be set");
925
+ }
926
+ const reviews = await this.user.getPendingReviews(this.course.getCourseID());
927
+ const now = import_moment.default.utc();
928
+ const dueReviews = reviews.filter((r) => now.isAfter(import_moment.default.utc(r.reviewTime)));
929
+ const scored = dueReviews.map((review) => {
930
+ const { score, reason } = this.computeUrgencyScore(review, now);
931
+ return {
932
+ cardId: review.cardId,
933
+ courseId: review.courseId,
934
+ score,
935
+ reviewID: review._id,
936
+ provenance: [
937
+ {
938
+ strategy: "srs",
939
+ strategyName: this.strategyName || this.name,
940
+ strategyId: this.strategyId || "NAVIGATION_STRATEGY-SRS-default",
941
+ action: "generated",
942
+ score,
943
+ reason
944
+ }
945
+ ]
946
+ };
947
+ });
948
+ logger.debug(`[srsNav] got ${scored.length} weighted cards`);
949
+ return scored.sort((a, b) => b.score - a.score).slice(0, limit);
950
+ }
951
+ /**
952
+ * Compute urgency score for a review card.
953
+ *
954
+ * Two factors:
955
+ * 1. Relative overdueness = hoursOverdue / intervalHours
956
+ * - 2 days overdue on 3-day interval = 0.67 (urgent)
957
+ * - 2 days overdue on 180-day interval = 0.01 (not urgent)
958
+ *
959
+ * 2. Interval recency factor = 0.3 + 0.7 * exp(-intervalHours / 720)
960
+ * - 24h interval → ~1.0 (very recent learning)
961
+ * - 30 days (720h) → ~0.56
962
+ * - 180 days → ~0.30
963
+ *
964
+ * Combined: base 0.5 + weighted average of factors * 0.45
965
+ * Result range: approximately 0.5 to 0.95
966
+ */
967
+ computeUrgencyScore(review, now) {
968
+ const scheduledAt = import_moment.default.utc(review.scheduledAt);
969
+ const due = import_moment.default.utc(review.reviewTime);
970
+ const intervalHours = Math.max(1, due.diff(scheduledAt, "hours"));
971
+ const hoursOverdue = now.diff(due, "hours");
972
+ const relativeOverdue = hoursOverdue / intervalHours;
973
+ const recencyFactor = 0.3 + 0.7 * Math.exp(-intervalHours / 720);
974
+ const overdueContribution = Math.min(1, Math.max(0, relativeOverdue));
975
+ const urgency = overdueContribution * 0.5 + recencyFactor * 0.5;
976
+ const score = Math.min(0.95, 0.5 + urgency * 0.45);
977
+ const reason = `${Math.round(hoursOverdue)}h overdue (interval: ${Math.round(intervalHours)}h, relative: ${relativeOverdue.toFixed(2)}), recency: ${recencyFactor.toFixed(2)}, review`;
978
+ return { score, reason };
979
+ }
980
+ };
981
+ }
982
+ });
983
+
984
+ // src/core/navigators/generators/types.ts
985
+ var types_exports = {};
986
+ var init_types = __esm({
987
+ "src/core/navigators/generators/types.ts"() {
988
+ "use strict";
989
+ }
990
+ });
991
+
992
+ // import("./generators/**/*") in src/core/navigators/index.ts
993
+ var globImport_generators;
994
+ var init_ = __esm({
995
+ 'import("./generators/**/*") in src/core/navigators/index.ts'() {
996
+ globImport_generators = __glob({
997
+ "./generators/CompositeGenerator.ts": () => Promise.resolve().then(() => (init_CompositeGenerator(), CompositeGenerator_exports)),
998
+ "./generators/elo.ts": () => Promise.resolve().then(() => (init_elo(), elo_exports)),
999
+ "./generators/index.ts": () => Promise.resolve().then(() => (init_generators(), generators_exports)),
1000
+ "./generators/srs.ts": () => Promise.resolve().then(() => (init_srs(), srs_exports)),
1001
+ "./generators/types.ts": () => Promise.resolve().then(() => (init_types(), types_exports))
1002
+ });
1003
+ }
1004
+ });
1005
+
1006
+ // src/core/types/contentNavigationStrategy.ts
1007
+ var DEFAULT_LEARNABLE_WEIGHT;
1008
+ var init_contentNavigationStrategy = __esm({
1009
+ "src/core/types/contentNavigationStrategy.ts"() {
1010
+ "use strict";
1011
+ DEFAULT_LEARNABLE_WEIGHT = {
1012
+ weight: 1,
1013
+ confidence: 0.1,
1014
+ // Low confidence initially = wide exploration
1015
+ sampleSize: 0
1016
+ };
1017
+ }
1018
+ });
1019
+
1020
+ // src/core/navigators/filters/WeightedFilter.ts
1021
+ var WeightedFilter_exports = {};
1022
+ __export(WeightedFilter_exports, {
1023
+ WeightedFilter: () => WeightedFilter
1024
+ });
1025
+ var WeightedFilter;
1026
+ var init_WeightedFilter = __esm({
1027
+ "src/core/navigators/filters/WeightedFilter.ts"() {
1028
+ "use strict";
1029
+ init_contentNavigationStrategy();
1030
+ WeightedFilter = class {
1031
+ name;
1032
+ inner;
1033
+ learnable;
1034
+ staticWeight;
1035
+ strategyId;
1036
+ constructor(inner, learnable = DEFAULT_LEARNABLE_WEIGHT, staticWeight = false, strategyId) {
1037
+ this.inner = inner;
1038
+ this.name = inner.name;
1039
+ this.learnable = learnable;
1040
+ this.staticWeight = staticWeight;
1041
+ this.strategyId = strategyId;
1042
+ }
1043
+ /**
1044
+ * Apply the inner filter, then scale its effect by the configured weight.
1045
+ */
1046
+ async transform(cards, context) {
1047
+ let effectiveWeight = this.learnable.weight;
1048
+ let deviation;
1049
+ if (!this.staticWeight && context.orchestration) {
1050
+ const strategyId = this.strategyId || this.inner.strategyId || this.name;
1051
+ effectiveWeight = context.orchestration.getEffectiveWeight(strategyId, this.learnable);
1052
+ deviation = context.orchestration.getDeviation(strategyId);
1053
+ }
1054
+ if (Math.abs(effectiveWeight - 1) < 1e-3) {
1055
+ return this.inner.transform(cards, context);
1056
+ }
1057
+ const originalScores = /* @__PURE__ */ new Map();
1058
+ for (const card of cards) {
1059
+ originalScores.set(card.cardId, card.score);
1060
+ }
1061
+ const transformedCards = await this.inner.transform(cards, context);
1062
+ return transformedCards.map((card) => {
1063
+ const originalScore = originalScores.get(card.cardId);
1064
+ if (originalScore === void 0 || originalScore === 0 || card.score === 0) {
1065
+ return card;
1066
+ }
1067
+ const rawEffect = card.score / originalScore;
1068
+ if (Math.abs(rawEffect - 1) < 1e-4) {
1069
+ return card;
1070
+ }
1071
+ const weightedEffect = Math.pow(rawEffect, effectiveWeight);
1072
+ const newScore = originalScore * weightedEffect;
1073
+ const lastProvIndex = card.provenance.length - 1;
1074
+ const lastProv = card.provenance[lastProvIndex];
1075
+ if (lastProv) {
1076
+ const updatedProvenance = [...card.provenance];
1077
+ updatedProvenance[lastProvIndex] = {
1078
+ ...lastProv,
1079
+ score: newScore,
1080
+ effectiveWeight,
1081
+ deviation
1082
+ // We can optionally append to the reason, but the structured field is key
1083
+ };
1084
+ return {
1085
+ ...card,
1086
+ score: newScore,
1087
+ provenance: updatedProvenance
1088
+ };
1089
+ }
1090
+ return {
1091
+ ...card,
1092
+ score: newScore
1093
+ };
1094
+ });
1095
+ }
1096
+ };
1097
+ }
1098
+ });
1099
+
1100
+ // src/core/navigators/filters/eloDistance.ts
1101
+ var eloDistance_exports = {};
1102
+ __export(eloDistance_exports, {
1103
+ DEFAULT_HALF_LIFE: () => DEFAULT_HALF_LIFE,
1104
+ DEFAULT_MAX_MULTIPLIER: () => DEFAULT_MAX_MULTIPLIER,
1105
+ DEFAULT_MIN_MULTIPLIER: () => DEFAULT_MIN_MULTIPLIER,
1106
+ createEloDistanceFilter: () => createEloDistanceFilter
1107
+ });
1108
+ function computeMultiplier(distance, halfLife, minMultiplier, maxMultiplier) {
1109
+ const normalizedDistance = distance / halfLife;
1110
+ const decay = Math.exp(-(normalizedDistance * normalizedDistance));
1111
+ return minMultiplier + (maxMultiplier - minMultiplier) * decay;
1112
+ }
1113
+ function createEloDistanceFilter(config) {
1114
+ const halfLife = config?.halfLife ?? DEFAULT_HALF_LIFE;
1115
+ const minMultiplier = config?.minMultiplier ?? DEFAULT_MIN_MULTIPLIER;
1116
+ const maxMultiplier = config?.maxMultiplier ?? DEFAULT_MAX_MULTIPLIER;
1117
+ return {
1118
+ name: "ELO Distance Filter",
1119
+ async transform(cards, context) {
1120
+ const { course, userElo } = context;
1121
+ const cardIds = cards.map((c) => c.cardId);
1122
+ const cardElos = await course.getCardEloData(cardIds);
1123
+ return cards.map((card, i) => {
1124
+ const cardElo = cardElos[i]?.global?.score ?? 1e3;
1125
+ const distance = Math.abs(cardElo - userElo);
1126
+ const multiplier = computeMultiplier(distance, halfLife, minMultiplier, maxMultiplier);
1127
+ const newScore = card.score * multiplier;
1128
+ const action = multiplier < maxMultiplier - 0.01 ? "penalized" : "passed";
1129
+ return {
1130
+ ...card,
1131
+ score: newScore,
1132
+ provenance: [
1133
+ ...card.provenance,
1134
+ {
1135
+ strategy: "eloDistance",
1136
+ strategyName: "ELO Distance Filter",
1137
+ strategyId: "ELO_DISTANCE_FILTER",
1138
+ action,
1139
+ score: newScore,
1140
+ reason: `ELO distance ${Math.round(distance)} (card: ${Math.round(cardElo)}, user: ${Math.round(userElo)}) \u2192 ${multiplier.toFixed(2)}x`
1141
+ }
1142
+ ]
1143
+ };
1144
+ });
1145
+ }
1146
+ };
1147
+ }
1148
+ var DEFAULT_HALF_LIFE, DEFAULT_MIN_MULTIPLIER, DEFAULT_MAX_MULTIPLIER;
1149
+ var init_eloDistance = __esm({
1150
+ "src/core/navigators/filters/eloDistance.ts"() {
1151
+ "use strict";
1152
+ DEFAULT_HALF_LIFE = 200;
1153
+ DEFAULT_MIN_MULTIPLIER = 0.3;
1154
+ DEFAULT_MAX_MULTIPLIER = 1;
1155
+ }
1156
+ });
1157
+
1158
+ // src/core/navigators/filters/hierarchyDefinition.ts
1159
+ var hierarchyDefinition_exports = {};
1160
+ __export(hierarchyDefinition_exports, {
1161
+ default: () => HierarchyDefinitionNavigator
1162
+ });
1163
+ var import_common6, DEFAULT_MIN_COUNT, HierarchyDefinitionNavigator;
1164
+ var init_hierarchyDefinition = __esm({
1165
+ "src/core/navigators/filters/hierarchyDefinition.ts"() {
1166
+ "use strict";
1167
+ init_navigators();
1168
+ import_common6 = require("@vue-skuilder/common");
1169
+ DEFAULT_MIN_COUNT = 3;
1170
+ HierarchyDefinitionNavigator = class extends ContentNavigator {
1171
+ config;
1172
+ /** Human-readable name for CardFilter interface */
1173
+ name;
1174
+ constructor(user, course, strategyData) {
1175
+ super(user, course, strategyData);
1176
+ this.config = this.parseConfig(strategyData.serializedData);
1177
+ this.name = strategyData.name || "Hierarchy Definition";
1178
+ }
1179
+ parseConfig(serializedData) {
1180
+ try {
1181
+ const parsed = JSON.parse(serializedData);
1182
+ return {
1183
+ prerequisites: parsed.prerequisites || {}
1184
+ };
1185
+ } catch {
1186
+ return {
1187
+ prerequisites: {}
1188
+ };
1189
+ }
1190
+ }
1191
+ /**
1192
+ * Check if a specific prerequisite is satisfied
1193
+ */
1194
+ isPrerequisiteMet(prereq, userTagElo, userGlobalElo) {
1195
+ if (!userTagElo) return false;
1196
+ const minCount = prereq.masteryThreshold?.minCount ?? DEFAULT_MIN_COUNT;
1197
+ if (userTagElo.count < minCount) return false;
1198
+ if (prereq.masteryThreshold?.minElo !== void 0) {
1199
+ return userTagElo.score >= prereq.masteryThreshold.minElo;
1200
+ } else {
1201
+ return userTagElo.score >= userGlobalElo;
1202
+ }
1203
+ }
1204
+ /**
1205
+ * Get the set of tags the user has mastered.
1206
+ * A tag is "mastered" if it appears as a prerequisite somewhere and meets its threshold.
1207
+ */
1208
+ async getMasteredTags(context) {
1209
+ const mastered = /* @__PURE__ */ new Set();
1210
+ try {
1211
+ const courseReg = await context.user.getCourseRegDoc(context.course.getCourseID());
1212
+ const userElo = (0, import_common6.toCourseElo)(courseReg.elo);
1213
+ for (const prereqs of Object.values(this.config.prerequisites)) {
1214
+ for (const prereq of prereqs) {
1215
+ const tagElo = userElo.tags[prereq.tag];
1216
+ if (this.isPrerequisiteMet(prereq, tagElo, userElo.global.score)) {
1217
+ mastered.add(prereq.tag);
1218
+ }
1219
+ }
1220
+ }
1221
+ } catch {
1222
+ }
1223
+ return mastered;
1224
+ }
1225
+ /**
1226
+ * Get the set of tags that are unlocked (prerequisites met)
1227
+ */
1228
+ getUnlockedTags(masteredTags) {
1229
+ const unlocked = /* @__PURE__ */ new Set();
1230
+ for (const [tagId, prereqs] of Object.entries(this.config.prerequisites)) {
1231
+ const allPrereqsMet = prereqs.every((prereq) => masteredTags.has(prereq.tag));
1232
+ if (allPrereqsMet) {
1233
+ unlocked.add(tagId);
1234
+ }
1235
+ }
1236
+ return unlocked;
1237
+ }
1238
+ /**
1239
+ * Check if a tag has prerequisites defined in config
1240
+ */
1241
+ hasPrerequisites(tagId) {
1242
+ return tagId in this.config.prerequisites;
1243
+ }
1244
+ /**
1245
+ * Check if a card is unlocked and generate reason.
1246
+ */
1247
+ async checkCardUnlock(card, _course, unlockedTags, masteredTags) {
1248
+ try {
1249
+ const cardTags = card.tags ?? [];
1250
+ const lockedTags = cardTags.filter(
1251
+ (tag) => this.hasPrerequisites(tag) && !unlockedTags.has(tag)
1252
+ );
1253
+ if (lockedTags.length === 0) {
1254
+ const tagList = cardTags.length > 0 ? cardTags.join(", ") : "none";
1255
+ return {
1256
+ isUnlocked: true,
1257
+ reason: `Prerequisites met, tags: ${tagList}`
1258
+ };
1259
+ }
1260
+ const missingPrereqs = lockedTags.flatMap((tag) => {
1261
+ const prereqs = this.config.prerequisites[tag] || [];
1262
+ return prereqs.filter((p) => !masteredTags.has(p.tag)).map((p) => p.tag);
1263
+ });
1264
+ return {
1265
+ isUnlocked: false,
1266
+ reason: `Blocked: missing prerequisites ${missingPrereqs.join(", ")} for tags ${lockedTags.join(", ")}`
1267
+ };
1268
+ } catch {
1269
+ return {
1270
+ isUnlocked: true,
1271
+ reason: "Prerequisites check skipped (tag lookup failed)"
1272
+ };
1273
+ }
1274
+ }
1275
+ /**
1276
+ * CardFilter.transform implementation.
1277
+ *
1278
+ * Apply prerequisite gating to cards. Cards with locked tags receive score: 0.
1279
+ */
1280
+ async transform(cards, context) {
1281
+ const masteredTags = await this.getMasteredTags(context);
1282
+ const unlockedTags = this.getUnlockedTags(masteredTags);
1283
+ const gated = [];
1284
+ for (const card of cards) {
1285
+ const { isUnlocked, reason } = await this.checkCardUnlock(
1286
+ card,
1287
+ context.course,
1288
+ unlockedTags,
1289
+ masteredTags
1290
+ );
1291
+ const finalScore = isUnlocked ? card.score : 0;
1292
+ const action = isUnlocked ? "passed" : "penalized";
1293
+ gated.push({
1294
+ ...card,
1295
+ score: finalScore,
1296
+ provenance: [
1297
+ ...card.provenance,
1298
+ {
1299
+ strategy: "hierarchyDefinition",
1300
+ strategyName: this.strategyName || this.name,
1301
+ strategyId: this.strategyId || "NAVIGATION_STRATEGY-hierarchy",
1302
+ action,
1303
+ score: finalScore,
1304
+ reason
1305
+ }
1306
+ ]
1307
+ });
1308
+ }
1309
+ return gated;
1310
+ }
1311
+ /**
1312
+ * Legacy getWeightedCards - now throws as filters should not be used as generators.
1313
+ *
1314
+ * Use transform() via Pipeline instead.
1315
+ */
1316
+ async getWeightedCards(_limit) {
1317
+ throw new Error(
1318
+ "HierarchyDefinitionNavigator is a filter and should not be used as a generator. Use Pipeline with a generator and this filter via transform()."
1319
+ );
1320
+ }
1321
+ };
1322
+ }
1323
+ });
1324
+
1325
+ // src/core/navigators/filters/userTagPreference.ts
1326
+ var userTagPreference_exports = {};
1327
+ __export(userTagPreference_exports, {
1328
+ default: () => UserTagPreferenceFilter
1329
+ });
1330
+ var UserTagPreferenceFilter;
1331
+ var init_userTagPreference = __esm({
1332
+ "src/core/navigators/filters/userTagPreference.ts"() {
1333
+ "use strict";
1334
+ init_navigators();
1335
+ UserTagPreferenceFilter = class extends ContentNavigator {
1336
+ _strategyData;
1337
+ /** Human-readable name for CardFilter interface */
1338
+ name;
1339
+ constructor(user, course, strategyData) {
1340
+ super(user, course, strategyData);
1341
+ this._strategyData = strategyData;
1342
+ this.name = strategyData.name || "User Tag Preferences";
1343
+ }
1344
+ /**
1345
+ * Compute multiplier for a card based on its tags and user preferences.
1346
+ * Returns the maximum multiplier among all matching tags, or 1.0 if no matches.
1347
+ */
1348
+ computeMultiplier(cardTags, boostMap) {
1349
+ const multipliers = cardTags.map((tag) => boostMap[tag]).filter((val) => val !== void 0);
1350
+ if (multipliers.length === 0) {
1351
+ return 1;
1352
+ }
1353
+ return Math.max(...multipliers);
1354
+ }
1355
+ /**
1356
+ * Build human-readable reason for the filter's decision.
1357
+ */
1358
+ buildReason(cardTags, boostMap, multiplier) {
1359
+ const matchingTags = cardTags.filter((tag) => boostMap[tag] === multiplier);
1360
+ if (multiplier === 0) {
1361
+ return `Excluded by user preference: ${matchingTags.join(", ")} (${multiplier}x)`;
1362
+ }
1363
+ if (multiplier < 1) {
1364
+ return `Penalized by user preference: ${matchingTags.join(", ")} (${multiplier.toFixed(2)}x)`;
1365
+ }
1366
+ if (multiplier > 1) {
1367
+ return `Boosted by user preference: ${matchingTags.join(", ")} (${multiplier.toFixed(2)}x)`;
1368
+ }
1369
+ return "No matching user preferences";
1370
+ }
1371
+ /**
1372
+ * CardFilter.transform implementation.
1373
+ *
1374
+ * Apply user tag preferences:
1375
+ * 1. Read preferences from strategy state
1376
+ * 2. If no preferences, pass through unchanged
1377
+ * 3. For each card:
1378
+ * - Look up tag in boost record
1379
+ * - If tag found: apply multiplier (0 = exclude, 1 = neutral, >1 = boost)
1380
+ * - If multiple tags match: use max multiplier
1381
+ * - Append provenance with clear reason
1382
+ */
1383
+ async transform(cards, _context) {
1384
+ const prefs = await this.getStrategyState();
1385
+ if (!prefs || Object.keys(prefs.boost).length === 0) {
1386
+ return cards.map((card) => ({
1387
+ ...card,
1388
+ provenance: [
1389
+ ...card.provenance,
1390
+ {
1391
+ strategy: "userTagPreference",
1392
+ strategyName: this.strategyName || this.name,
1393
+ strategyId: this.strategyId || this._strategyData._id,
1394
+ action: "passed",
1395
+ score: card.score,
1396
+ reason: "No user tag preferences configured"
1397
+ }
1398
+ ]
1399
+ }));
1400
+ }
1401
+ const adjusted = await Promise.all(
1402
+ cards.map(async (card) => {
1403
+ const cardTags = card.tags ?? [];
1404
+ const multiplier = this.computeMultiplier(cardTags, prefs.boost);
1405
+ const finalScore = Math.min(1, card.score * multiplier);
1406
+ let action;
1407
+ if (multiplier === 0 || multiplier < 1) {
1408
+ action = "penalized";
1409
+ } else if (multiplier > 1) {
1410
+ action = "boosted";
1411
+ } else {
1412
+ action = "passed";
1413
+ }
1414
+ return {
1415
+ ...card,
1416
+ score: finalScore,
1417
+ provenance: [
1418
+ ...card.provenance,
1419
+ {
1420
+ strategy: "userTagPreference",
1421
+ strategyName: this.strategyName || this.name,
1422
+ strategyId: this.strategyId || this._strategyData._id,
1423
+ action,
1424
+ score: finalScore,
1425
+ reason: this.buildReason(cardTags, prefs.boost, multiplier)
1426
+ }
1427
+ ]
1428
+ };
1429
+ })
1430
+ );
1431
+ return adjusted;
1432
+ }
1433
+ /**
1434
+ * Legacy getWeightedCards - throws as filters should not be used as generators.
1435
+ */
1436
+ async getWeightedCards(_limit) {
1437
+ throw new Error(
1438
+ "UserTagPreferenceFilter is a filter and should not be used as a generator. Use Pipeline with a generator and this filter via transform()."
1439
+ );
1440
+ }
1441
+ };
1442
+ }
1443
+ });
1444
+
1445
+ // src/core/navigators/filters/index.ts
1446
+ var filters_exports = {};
1447
+ __export(filters_exports, {
1448
+ UserTagPreferenceFilter: () => UserTagPreferenceFilter,
1449
+ createEloDistanceFilter: () => createEloDistanceFilter
1450
+ });
1451
+ var init_filters = __esm({
1452
+ "src/core/navigators/filters/index.ts"() {
1453
+ "use strict";
1454
+ init_eloDistance();
1455
+ init_userTagPreference();
1456
+ }
1457
+ });
1458
+
1459
+ // src/core/navigators/filters/inferredPreferenceStub.ts
1460
+ var inferredPreferenceStub_exports = {};
1461
+ __export(inferredPreferenceStub_exports, {
1462
+ INFERRED_PREFERENCE_NAVIGATOR_STUB: () => INFERRED_PREFERENCE_NAVIGATOR_STUB
1463
+ });
1464
+ var INFERRED_PREFERENCE_NAVIGATOR_STUB;
1465
+ var init_inferredPreferenceStub = __esm({
1466
+ "src/core/navigators/filters/inferredPreferenceStub.ts"() {
1467
+ "use strict";
1468
+ INFERRED_PREFERENCE_NAVIGATOR_STUB = true;
1469
+ }
1470
+ });
1471
+
1472
+ // src/core/navigators/filters/interferenceMitigator.ts
1473
+ var interferenceMitigator_exports = {};
1474
+ __export(interferenceMitigator_exports, {
1475
+ default: () => InterferenceMitigatorNavigator
1476
+ });
1477
+ var import_common7, DEFAULT_MIN_COUNT2, DEFAULT_MIN_ELAPSED_DAYS, DEFAULT_INTERFERENCE_DECAY, InterferenceMitigatorNavigator;
1478
+ var init_interferenceMitigator = __esm({
1479
+ "src/core/navigators/filters/interferenceMitigator.ts"() {
1480
+ "use strict";
1481
+ init_navigators();
1482
+ import_common7 = require("@vue-skuilder/common");
1483
+ DEFAULT_MIN_COUNT2 = 10;
1484
+ DEFAULT_MIN_ELAPSED_DAYS = 3;
1485
+ DEFAULT_INTERFERENCE_DECAY = 0.8;
1486
+ InterferenceMitigatorNavigator = class extends ContentNavigator {
1487
+ config;
1488
+ /** Human-readable name for CardFilter interface */
1489
+ name;
1490
+ /** Precomputed map: tag -> set of { partner, decay } it interferes with */
1491
+ interferenceMap;
1492
+ constructor(user, course, strategyData) {
1493
+ super(user, course, strategyData);
1494
+ this.config = this.parseConfig(strategyData.serializedData);
1495
+ this.interferenceMap = this.buildInterferenceMap();
1496
+ this.name = strategyData.name || "Interference Mitigator";
1497
+ }
1498
+ parseConfig(serializedData) {
1499
+ try {
1500
+ const parsed = JSON.parse(serializedData);
1501
+ let sets = parsed.interferenceSets || [];
1502
+ if (sets.length > 0 && Array.isArray(sets[0])) {
1503
+ sets = sets.map((tags) => ({ tags }));
1504
+ }
1505
+ return {
1506
+ interferenceSets: sets,
1507
+ maturityThreshold: {
1508
+ minCount: parsed.maturityThreshold?.minCount ?? DEFAULT_MIN_COUNT2,
1509
+ minElo: parsed.maturityThreshold?.minElo,
1510
+ minElapsedDays: parsed.maturityThreshold?.minElapsedDays ?? DEFAULT_MIN_ELAPSED_DAYS
1511
+ },
1512
+ defaultDecay: parsed.defaultDecay ?? DEFAULT_INTERFERENCE_DECAY
1513
+ };
1514
+ } catch {
1515
+ return {
1516
+ interferenceSets: [],
1517
+ maturityThreshold: {
1518
+ minCount: DEFAULT_MIN_COUNT2,
1519
+ minElapsedDays: DEFAULT_MIN_ELAPSED_DAYS
1520
+ },
1521
+ defaultDecay: DEFAULT_INTERFERENCE_DECAY
1522
+ };
1523
+ }
1524
+ }
1525
+ /**
1526
+ * Build a map from each tag to its interference partners with decay coefficients.
1527
+ * If tags A, B, C are in an interference group with decay 0.8, then:
1528
+ * - A interferes with B (decay 0.8) and C (decay 0.8)
1529
+ * - B interferes with A (decay 0.8) and C (decay 0.8)
1530
+ * - etc.
1531
+ */
1532
+ buildInterferenceMap() {
1533
+ const map = /* @__PURE__ */ new Map();
1534
+ for (const group of this.config.interferenceSets) {
1535
+ const decay = group.decay ?? this.config.defaultDecay ?? DEFAULT_INTERFERENCE_DECAY;
1536
+ for (const tag of group.tags) {
1537
+ if (!map.has(tag)) {
1538
+ map.set(tag, []);
1539
+ }
1540
+ const partners = map.get(tag);
1541
+ for (const other of group.tags) {
1542
+ if (other !== tag) {
1543
+ const existing = partners.find((p) => p.partner === other);
1544
+ if (existing) {
1545
+ existing.decay = Math.max(existing.decay, decay);
1546
+ } else {
1547
+ partners.push({ partner: other, decay });
1548
+ }
1549
+ }
1550
+ }
1551
+ }
1552
+ }
1553
+ return map;
1554
+ }
1555
+ /**
1556
+ * Get the set of tags that are currently immature for this user.
1557
+ * A tag is immature if the user has interacted with it but hasn't
1558
+ * reached the maturity threshold.
1559
+ */
1560
+ async getImmatureTags(context) {
1561
+ const immature = /* @__PURE__ */ new Set();
1562
+ try {
1563
+ const courseReg = await context.user.getCourseRegDoc(context.course.getCourseID());
1564
+ const userElo = (0, import_common7.toCourseElo)(courseReg.elo);
1565
+ const minCount = this.config.maturityThreshold?.minCount ?? DEFAULT_MIN_COUNT2;
1566
+ const minElo = this.config.maturityThreshold?.minElo;
1567
+ const minElapsedDays = this.config.maturityThreshold?.minElapsedDays ?? DEFAULT_MIN_ELAPSED_DAYS;
1568
+ const minCountForElapsed = minElapsedDays * 2;
1569
+ for (const [tagId, tagElo] of Object.entries(userElo.tags)) {
1570
+ if (tagElo.count === 0) continue;
1571
+ const belowCount = tagElo.count < minCount;
1572
+ const belowElo = minElo !== void 0 && tagElo.score < minElo;
1573
+ const belowElapsed = tagElo.count < minCountForElapsed;
1574
+ if (belowCount || belowElo || belowElapsed) {
1575
+ immature.add(tagId);
1576
+ }
1577
+ }
1578
+ } catch {
1579
+ }
1580
+ return immature;
1581
+ }
1582
+ /**
1583
+ * Get all tags that interfere with any immature tag, along with their decay coefficients.
1584
+ * These are the tags we want to avoid introducing.
1585
+ */
1586
+ getTagsToAvoid(immatureTags) {
1587
+ const avoid = /* @__PURE__ */ new Map();
1588
+ for (const immatureTag of immatureTags) {
1589
+ const partners = this.interferenceMap.get(immatureTag);
1590
+ if (partners) {
1591
+ for (const { partner, decay } of partners) {
1592
+ if (!immatureTags.has(partner)) {
1593
+ const existing = avoid.get(partner) ?? 0;
1594
+ avoid.set(partner, Math.max(existing, decay));
1595
+ }
1596
+ }
1597
+ }
1598
+ }
1599
+ return avoid;
1600
+ }
1601
+ /**
1602
+ * Compute interference score reduction for a card.
1603
+ * Returns: { multiplier, interfering tags, reason }
1604
+ */
1605
+ computeInterferenceEffect(cardTags, tagsToAvoid, immatureTags) {
1606
+ if (tagsToAvoid.size === 0) {
1607
+ return {
1608
+ multiplier: 1,
1609
+ interferingTags: [],
1610
+ reason: "No interference detected"
1611
+ };
1612
+ }
1613
+ let multiplier = 1;
1614
+ const interferingTags = [];
1615
+ for (const tag of cardTags) {
1616
+ const decay = tagsToAvoid.get(tag);
1617
+ if (decay !== void 0) {
1618
+ interferingTags.push(tag);
1619
+ multiplier *= 1 - decay;
1620
+ }
1621
+ }
1622
+ if (interferingTags.length === 0) {
1623
+ return {
1624
+ multiplier: 1,
1625
+ interferingTags: [],
1626
+ reason: "No interference detected"
1627
+ };
1628
+ }
1629
+ const causingTags = /* @__PURE__ */ new Set();
1630
+ for (const tag of interferingTags) {
1631
+ for (const immatureTag of immatureTags) {
1632
+ const partners = this.interferenceMap.get(immatureTag);
1633
+ if (partners?.some((p) => p.partner === tag)) {
1634
+ causingTags.add(immatureTag);
1635
+ }
1636
+ }
1637
+ }
1638
+ const reason = `Interferes with immature tags ${Array.from(causingTags).join(", ")} (tags: ${interferingTags.join(", ")}, multiplier: ${multiplier.toFixed(2)})`;
1639
+ return { multiplier, interferingTags, reason };
1640
+ }
1641
+ /**
1642
+ * CardFilter.transform implementation.
1643
+ *
1644
+ * Apply interference-aware scoring. Cards with tags that interfere with
1645
+ * immature learnings get reduced scores.
1646
+ */
1647
+ async transform(cards, context) {
1648
+ const immatureTags = await this.getImmatureTags(context);
1649
+ const tagsToAvoid = this.getTagsToAvoid(immatureTags);
1650
+ const adjusted = [];
1651
+ for (const card of cards) {
1652
+ const cardTags = card.tags ?? [];
1653
+ const { multiplier, reason } = this.computeInterferenceEffect(
1654
+ cardTags,
1655
+ tagsToAvoid,
1656
+ immatureTags
1657
+ );
1658
+ const finalScore = card.score * multiplier;
1659
+ const action = multiplier < 1 ? "penalized" : multiplier > 1 ? "boosted" : "passed";
1660
+ adjusted.push({
1661
+ ...card,
1662
+ score: finalScore,
1663
+ provenance: [
1664
+ ...card.provenance,
1665
+ {
1666
+ strategy: "interferenceMitigator",
1667
+ strategyName: this.strategyName || this.name,
1668
+ strategyId: this.strategyId || "NAVIGATION_STRATEGY-interference",
1669
+ action,
1670
+ score: finalScore,
1671
+ reason
1672
+ }
1673
+ ]
1674
+ });
1675
+ }
1676
+ return adjusted;
1677
+ }
1678
+ /**
1679
+ * Legacy getWeightedCards - now throws as filters should not be used as generators.
1680
+ *
1681
+ * Use transform() via Pipeline instead.
1682
+ */
1683
+ async getWeightedCards(_limit) {
1684
+ throw new Error(
1685
+ "InterferenceMitigatorNavigator is a filter and should not be used as a generator. Use Pipeline with a generator and this filter via transform()."
1686
+ );
1687
+ }
1688
+ };
1689
+ }
1690
+ });
1691
+
1692
+ // src/core/navigators/filters/relativePriority.ts
1693
+ var relativePriority_exports = {};
1694
+ __export(relativePriority_exports, {
1695
+ default: () => RelativePriorityNavigator
1696
+ });
1697
+ var DEFAULT_PRIORITY, DEFAULT_PRIORITY_INFLUENCE, DEFAULT_COMBINE_MODE, RelativePriorityNavigator;
1698
+ var init_relativePriority = __esm({
1699
+ "src/core/navigators/filters/relativePriority.ts"() {
1700
+ "use strict";
1701
+ init_navigators();
1702
+ DEFAULT_PRIORITY = 0.5;
1703
+ DEFAULT_PRIORITY_INFLUENCE = 0.5;
1704
+ DEFAULT_COMBINE_MODE = "max";
1705
+ RelativePriorityNavigator = class extends ContentNavigator {
1706
+ config;
1707
+ /** Human-readable name for CardFilter interface */
1708
+ name;
1709
+ constructor(user, course, strategyData) {
1710
+ super(user, course, strategyData);
1711
+ this.config = this.parseConfig(strategyData.serializedData);
1712
+ this.name = strategyData.name || "Relative Priority";
1713
+ }
1714
+ parseConfig(serializedData) {
1715
+ try {
1716
+ const parsed = JSON.parse(serializedData);
1717
+ return {
1718
+ tagPriorities: parsed.tagPriorities || {},
1719
+ defaultPriority: parsed.defaultPriority ?? DEFAULT_PRIORITY,
1720
+ combineMode: parsed.combineMode ?? DEFAULT_COMBINE_MODE,
1721
+ priorityInfluence: parsed.priorityInfluence ?? DEFAULT_PRIORITY_INFLUENCE
1722
+ };
1723
+ } catch {
1724
+ return {
1725
+ tagPriorities: {},
1726
+ defaultPriority: DEFAULT_PRIORITY,
1727
+ combineMode: DEFAULT_COMBINE_MODE,
1728
+ priorityInfluence: DEFAULT_PRIORITY_INFLUENCE
1729
+ };
1730
+ }
1731
+ }
1732
+ /**
1733
+ * Look up the priority for a tag.
1734
+ */
1735
+ getTagPriority(tagId) {
1736
+ return this.config.tagPriorities[tagId] ?? this.config.defaultPriority ?? DEFAULT_PRIORITY;
1737
+ }
1738
+ /**
1739
+ * Compute combined priority for a card based on its tags.
1740
+ */
1741
+ computeCardPriority(cardTags) {
1742
+ if (cardTags.length === 0) {
1743
+ return this.config.defaultPriority ?? DEFAULT_PRIORITY;
1744
+ }
1745
+ const priorities = cardTags.map((tag) => this.getTagPriority(tag));
1746
+ switch (this.config.combineMode) {
1747
+ case "max":
1748
+ return Math.max(...priorities);
1749
+ case "min":
1750
+ return Math.min(...priorities);
1751
+ case "average":
1752
+ return priorities.reduce((sum, p) => sum + p, 0) / priorities.length;
1753
+ default:
1754
+ return Math.max(...priorities);
1755
+ }
1756
+ }
1757
+ /**
1758
+ * Compute boost factor based on priority.
1759
+ *
1760
+ * The formula: 1 + (priority - 0.5) * priorityInfluence
1761
+ *
1762
+ * This creates a multiplier centered around 1.0:
1763
+ * - Priority 1.0 with influence 0.5 → 1.25 (25% boost)
1764
+ * - Priority 0.5 with any influence → 1.00 (neutral)
1765
+ * - Priority 0.0 with influence 0.5 → 0.75 (25% reduction)
1766
+ */
1767
+ computeBoostFactor(priority) {
1768
+ const influence = this.config.priorityInfluence ?? DEFAULT_PRIORITY_INFLUENCE;
1769
+ return 1 + (priority - 0.5) * influence;
1770
+ }
1771
+ /**
1772
+ * Build human-readable reason for priority adjustment.
1773
+ */
1774
+ buildPriorityReason(cardTags, priority, boostFactor, finalScore) {
1775
+ if (cardTags.length === 0) {
1776
+ return `No tags, neutral priority (${priority.toFixed(2)})`;
1777
+ }
1778
+ const tagList = cardTags.slice(0, 3).join(", ");
1779
+ const more = cardTags.length > 3 ? ` (+${cardTags.length - 3} more)` : "";
1780
+ if (boostFactor === 1) {
1781
+ return `Neutral priority (${priority.toFixed(2)}) for tags: ${tagList}${more}`;
1782
+ } else if (boostFactor > 1) {
1783
+ return `High-priority tags: ${tagList}${more} (priority ${priority.toFixed(2)} \u2192 boost ${boostFactor.toFixed(2)}x \u2192 ${finalScore.toFixed(2)})`;
1784
+ } else {
1785
+ return `Low-priority tags: ${tagList}${more} (priority ${priority.toFixed(2)} \u2192 reduce ${boostFactor.toFixed(2)}x \u2192 ${finalScore.toFixed(2)})`;
1786
+ }
1787
+ }
1788
+ /**
1789
+ * CardFilter.transform implementation.
1790
+ *
1791
+ * Apply priority-adjusted scoring. Cards with high-priority tags get boosted,
1792
+ * cards with low-priority tags get reduced scores.
1793
+ */
1794
+ async transform(cards, _context) {
1795
+ const adjusted = await Promise.all(
1796
+ cards.map(async (card) => {
1797
+ const cardTags = card.tags ?? [];
1798
+ const priority = this.computeCardPriority(cardTags);
1799
+ const boostFactor = this.computeBoostFactor(priority);
1800
+ const finalScore = Math.max(0, Math.min(1, card.score * boostFactor));
1801
+ const action = boostFactor > 1 ? "boosted" : boostFactor < 1 ? "penalized" : "passed";
1802
+ const reason = this.buildPriorityReason(cardTags, priority, boostFactor, finalScore);
1803
+ return {
1804
+ ...card,
1805
+ score: finalScore,
1806
+ provenance: [
1807
+ ...card.provenance,
1808
+ {
1809
+ strategy: "relativePriority",
1810
+ strategyName: this.strategyName || this.name,
1811
+ strategyId: this.strategyId || "NAVIGATION_STRATEGY-priority",
1812
+ action,
1813
+ score: finalScore,
1814
+ reason
1815
+ }
1816
+ ]
1817
+ };
1818
+ })
1819
+ );
1820
+ return adjusted;
1821
+ }
1822
+ /**
1823
+ * Legacy getWeightedCards - now throws as filters should not be used as generators.
1824
+ *
1825
+ * Use transform() via Pipeline instead.
1826
+ */
1827
+ async getWeightedCards(_limit) {
1828
+ throw new Error(
1829
+ "RelativePriorityNavigator is a filter and should not be used as a generator. Use Pipeline with a generator and this filter via transform()."
1830
+ );
1831
+ }
1832
+ };
1833
+ }
1834
+ });
1835
+
1836
+ // src/core/navigators/filters/types.ts
1837
+ var types_exports2 = {};
1838
+ var init_types2 = __esm({
1839
+ "src/core/navigators/filters/types.ts"() {
1840
+ "use strict";
1841
+ }
1842
+ });
1843
+
1844
+ // src/core/navigators/filters/userGoalStub.ts
1845
+ var userGoalStub_exports = {};
1846
+ __export(userGoalStub_exports, {
1847
+ USER_GOAL_NAVIGATOR_STUB: () => USER_GOAL_NAVIGATOR_STUB
1848
+ });
1849
+ var USER_GOAL_NAVIGATOR_STUB;
1850
+ var init_userGoalStub = __esm({
1851
+ "src/core/navigators/filters/userGoalStub.ts"() {
1852
+ "use strict";
1853
+ USER_GOAL_NAVIGATOR_STUB = true;
1854
+ }
1855
+ });
1856
+
1857
+ // import("./filters/**/*") in src/core/navigators/index.ts
1858
+ var globImport_filters;
1859
+ var init_2 = __esm({
1860
+ 'import("./filters/**/*") in src/core/navigators/index.ts'() {
1861
+ globImport_filters = __glob({
1862
+ "./filters/WeightedFilter.ts": () => Promise.resolve().then(() => (init_WeightedFilter(), WeightedFilter_exports)),
1863
+ "./filters/eloDistance.ts": () => Promise.resolve().then(() => (init_eloDistance(), eloDistance_exports)),
1864
+ "./filters/hierarchyDefinition.ts": () => Promise.resolve().then(() => (init_hierarchyDefinition(), hierarchyDefinition_exports)),
1865
+ "./filters/index.ts": () => Promise.resolve().then(() => (init_filters(), filters_exports)),
1866
+ "./filters/inferredPreferenceStub.ts": () => Promise.resolve().then(() => (init_inferredPreferenceStub(), inferredPreferenceStub_exports)),
1867
+ "./filters/interferenceMitigator.ts": () => Promise.resolve().then(() => (init_interferenceMitigator(), interferenceMitigator_exports)),
1868
+ "./filters/relativePriority.ts": () => Promise.resolve().then(() => (init_relativePriority(), relativePriority_exports)),
1869
+ "./filters/types.ts": () => Promise.resolve().then(() => (init_types2(), types_exports2)),
1870
+ "./filters/userGoalStub.ts": () => Promise.resolve().then(() => (init_userGoalStub(), userGoalStub_exports)),
1871
+ "./filters/userTagPreference.ts": () => Promise.resolve().then(() => (init_userTagPreference(), userTagPreference_exports))
1872
+ });
1873
+ }
1874
+ });
1875
+
1876
+ // src/core/orchestration/gradient.ts
1877
+ var init_gradient = __esm({
1878
+ "src/core/orchestration/gradient.ts"() {
1879
+ "use strict";
1880
+ init_logger();
1881
+ }
1882
+ });
1883
+
1884
+ // src/core/orchestration/learning.ts
1885
+ var init_learning = __esm({
1886
+ "src/core/orchestration/learning.ts"() {
1887
+ "use strict";
1888
+ init_contentNavigationStrategy();
1889
+ init_types_legacy();
1890
+ init_logger();
1891
+ }
1892
+ });
1893
+
1894
+ // src/core/orchestration/signal.ts
1895
+ var init_signal = __esm({
1896
+ "src/core/orchestration/signal.ts"() {
1897
+ "use strict";
1898
+ }
1899
+ });
1900
+
1901
+ // src/core/orchestration/recording.ts
1902
+ var init_recording = __esm({
1903
+ "src/core/orchestration/recording.ts"() {
1904
+ "use strict";
1905
+ init_signal();
1906
+ init_types_legacy();
1907
+ init_logger();
1908
+ }
1909
+ });
1910
+
1911
+ // src/core/orchestration/index.ts
1912
+ function fnv1a(str) {
1913
+ let hash = 2166136261;
1914
+ for (let i = 0; i < str.length; i++) {
1915
+ hash ^= str.charCodeAt(i);
1916
+ hash = Math.imul(hash, 16777619);
1917
+ }
1918
+ return hash >>> 0;
1919
+ }
1920
+ function computeDeviation(userId, strategyId, salt) {
1921
+ const input = `${userId}:${strategyId}:${salt}`;
1922
+ const hash = fnv1a(input);
1923
+ const normalized = hash / 4294967296;
1924
+ return normalized * 2 - 1;
1925
+ }
1926
+ function computeSpread(confidence) {
1927
+ const clampedConfidence = Math.max(0, Math.min(1, confidence));
1928
+ return MAX_SPREAD - clampedConfidence * (MAX_SPREAD - MIN_SPREAD);
1929
+ }
1930
+ function computeEffectiveWeight(learnable, userId, strategyId, salt) {
1931
+ const deviation = computeDeviation(userId, strategyId, salt);
1932
+ const spread = computeSpread(learnable.confidence);
1933
+ const adjustment = deviation * spread * learnable.weight;
1934
+ const effective = learnable.weight + adjustment;
1935
+ return Math.max(MIN_WEIGHT, Math.min(MAX_WEIGHT, effective));
1936
+ }
1937
+ async function createOrchestrationContext(user, course) {
1938
+ let courseConfig;
1939
+ try {
1940
+ courseConfig = await course.getCourseConfig();
1941
+ } catch (e) {
1942
+ logger.error(`[Orchestration] Failed to load course config: ${e}`);
1943
+ courseConfig = {
1944
+ name: "Unknown",
1945
+ description: "",
1946
+ public: false,
1947
+ deleted: false,
1948
+ creator: "",
1949
+ admins: [],
1950
+ moderators: [],
1951
+ dataShapes: [],
1952
+ questionTypes: [],
1953
+ orchestration: { salt: "default" }
1954
+ };
1955
+ }
1956
+ const userId = user.getUsername();
1957
+ const salt = courseConfig.orchestration?.salt || "default_salt";
1958
+ return {
1959
+ user,
1960
+ course,
1961
+ userId,
1962
+ courseConfig,
1963
+ getEffectiveWeight(strategyId, learnable) {
1964
+ return computeEffectiveWeight(learnable, userId, strategyId, salt);
1965
+ },
1966
+ getDeviation(strategyId) {
1967
+ return computeDeviation(userId, strategyId, salt);
1968
+ }
1969
+ };
1970
+ }
1971
+ var MIN_SPREAD, MAX_SPREAD, MIN_WEIGHT, MAX_WEIGHT;
1972
+ var init_orchestration = __esm({
1973
+ "src/core/orchestration/index.ts"() {
1974
+ "use strict";
1975
+ init_logger();
1976
+ init_gradient();
1977
+ init_learning();
1978
+ init_signal();
1979
+ init_recording();
1980
+ MIN_SPREAD = 0.1;
1981
+ MAX_SPREAD = 0.5;
1982
+ MIN_WEIGHT = 0.1;
1983
+ MAX_WEIGHT = 3;
1984
+ }
1985
+ });
1986
+
1987
+ // src/core/navigators/Pipeline.ts
1988
+ var Pipeline_exports = {};
1989
+ __export(Pipeline_exports, {
1990
+ Pipeline: () => Pipeline
1991
+ });
1992
+ function logPipelineConfig(generator, filters) {
1993
+ const filterList = filters.length > 0 ? "\n - " + filters.map((f) => f.name).join("\n - ") : " none";
1994
+ logger.info(
1995
+ `[Pipeline] Configuration:
1996
+ Generator: ${generator.name}
1997
+ Filters:${filterList}`
1998
+ );
1999
+ }
2000
+ function logTagHydration(cards, tagsByCard) {
2001
+ const totalTags = Array.from(tagsByCard.values()).reduce((sum, tags) => sum + tags.length, 0);
2002
+ const cardsWithTags = Array.from(tagsByCard.values()).filter((tags) => tags.length > 0).length;
2003
+ logger.debug(
2004
+ `[Pipeline] Tag hydration: ${cards.length} cards, ${cardsWithTags} have tags (${totalTags} total tags) - single batch query`
2005
+ );
2006
+ }
2007
+ function logExecutionSummary(generatorName, generatedCount, filterCount, finalCount, topScores) {
792
2008
  const scoreDisplay = topScores.length > 0 ? topScores.map((s) => s.toFixed(2)).join(", ") : "none";
793
2009
  logger.info(
794
2010
  `[Pipeline] Execution: ${generatorName} produced ${generatedCount} \u2192 ${filterCount} filters \u2192 ${finalCount} results (top scores: ${scoreDisplay})`
@@ -808,13 +2024,14 @@ function logCardProvenance(cards, maxCards = 3) {
808
2024
  }
809
2025
  }
810
2026
  }
811
- var import_common5, Pipeline;
2027
+ var import_common8, Pipeline;
812
2028
  var init_Pipeline = __esm({
813
2029
  "src/core/navigators/Pipeline.ts"() {
814
2030
  "use strict";
815
- import_common5 = require("@vue-skuilder/common");
2031
+ import_common8 = require("@vue-skuilder/common");
816
2032
  init_navigators();
817
2033
  init_logger();
2034
+ init_orchestration();
818
2035
  Pipeline = class extends ContentNavigator {
819
2036
  generator;
820
2037
  filters;
@@ -917,15 +2134,17 @@ var init_Pipeline = __esm({
917
2134
  let userElo = 1e3;
918
2135
  try {
919
2136
  const courseReg = await this.user.getCourseRegDoc(this.course.getCourseID());
920
- const courseElo = (0, import_common5.toCourseElo)(courseReg.elo);
2137
+ const courseElo = (0, import_common8.toCourseElo)(courseReg.elo);
921
2138
  userElo = courseElo.global.score;
922
2139
  } catch (e) {
923
2140
  logger.debug(`[Pipeline] Could not get user ELO, using default: ${e}`);
924
2141
  }
2142
+ const orchestration = await createOrchestrationContext(this.user, this.course);
925
2143
  return {
926
2144
  user: this.user,
927
2145
  course: this.course,
928
- userElo
2146
+ userElo,
2147
+ orchestration
929
2148
  };
930
2149
  }
931
2150
  /**
@@ -934,154 +2153,51 @@ var init_Pipeline = __esm({
934
2153
  getCourseID() {
935
2154
  return this.course.getCourseID();
936
2155
  }
937
- };
938
- }
939
- });
940
-
941
- // src/core/navigators/generators/CompositeGenerator.ts
942
- var DEFAULT_AGGREGATION_MODE, FREQUENCY_BOOST_FACTOR, CompositeGenerator;
943
- var init_CompositeGenerator = __esm({
944
- "src/core/navigators/generators/CompositeGenerator.ts"() {
945
- "use strict";
946
- init_navigators();
947
- init_logger();
948
- DEFAULT_AGGREGATION_MODE = "frequencyBoost" /* FREQUENCY_BOOST */;
949
- FREQUENCY_BOOST_FACTOR = 0.1;
950
- CompositeGenerator = class _CompositeGenerator extends ContentNavigator {
951
- /** Human-readable name for CardGenerator interface */
952
- name = "Composite Generator";
953
- generators;
954
- aggregationMode;
955
- constructor(generators, aggregationMode = DEFAULT_AGGREGATION_MODE) {
956
- super();
957
- this.generators = generators;
958
- this.aggregationMode = aggregationMode;
959
- if (generators.length === 0) {
960
- throw new Error("CompositeGenerator requires at least one generator");
961
- }
962
- logger.debug(
963
- `[CompositeGenerator] Created with ${generators.length} generators, mode: ${aggregationMode}`
964
- );
965
- }
966
- /**
967
- * Creates a CompositeGenerator from strategy data.
968
- *
969
- * This is a convenience factory for use by PipelineAssembler.
970
- */
971
- static async fromStrategies(user, course, strategies, aggregationMode = DEFAULT_AGGREGATION_MODE) {
972
- const generators = await Promise.all(
973
- strategies.map((s) => ContentNavigator.create(user, course, s))
974
- );
975
- return new _CompositeGenerator(generators, aggregationMode);
976
- }
977
- /**
978
- * Get weighted cards from all generators, merge and deduplicate.
979
- *
980
- * Cards appearing in multiple generators receive a score boost.
981
- * Provenance tracks which generators produced each card and how scores were aggregated.
982
- *
983
- * This method supports both the legacy signature (limit only) and the
984
- * CardGenerator interface signature (limit, context).
985
- *
986
- * @param limit - Maximum number of cards to return
987
- * @param context - GeneratorContext passed to child generators (required when called via Pipeline)
988
- */
989
- async getWeightedCards(limit, context) {
990
- if (!context) {
991
- throw new Error(
992
- "CompositeGenerator.getWeightedCards requires a GeneratorContext. It should be called via Pipeline, not directly."
993
- );
994
- }
995
- const results = await Promise.all(
996
- this.generators.map((g) => g.getWeightedCards(limit, context))
997
- );
998
- const byCardId = /* @__PURE__ */ new Map();
999
- for (const cards of results) {
1000
- for (const card of cards) {
1001
- const existing = byCardId.get(card.cardId) || [];
1002
- existing.push(card);
1003
- byCardId.set(card.cardId, existing);
1004
- }
1005
- }
1006
- const merged = [];
1007
- for (const [, cards] of byCardId) {
1008
- const aggregatedScore = this.aggregateScores(cards);
1009
- const finalScore = Math.min(1, aggregatedScore);
1010
- const mergedProvenance = cards.flatMap((c) => c.provenance);
1011
- const initialScore = cards[0].score;
1012
- const action = finalScore > initialScore ? "boosted" : finalScore < initialScore ? "penalized" : "passed";
1013
- const reason = this.buildAggregationReason(cards, finalScore);
1014
- merged.push({
1015
- ...cards[0],
1016
- score: finalScore,
1017
- provenance: [
1018
- ...mergedProvenance,
1019
- {
1020
- strategy: "composite",
1021
- strategyName: "Composite Generator",
1022
- strategyId: "COMPOSITE_GENERATOR",
1023
- action,
1024
- score: finalScore,
1025
- reason
1026
- }
1027
- ]
1028
- });
1029
- }
1030
- return merged.sort((a, b) => b.score - a.score).slice(0, limit);
1031
- }
1032
2156
  /**
1033
- * Build human-readable reason for score aggregation.
2157
+ * Get orchestration context for outcome recording.
1034
2158
  */
1035
- buildAggregationReason(cards, finalScore) {
1036
- const count = cards.length;
1037
- const scores = cards.map((c) => c.score.toFixed(2)).join(", ");
1038
- if (count === 1) {
1039
- return `Single generator, score ${finalScore.toFixed(2)}`;
1040
- }
1041
- const strategies = cards.map((c) => c.provenance[0]?.strategy || "unknown").join(", ");
1042
- switch (this.aggregationMode) {
1043
- case "max" /* MAX */:
1044
- return `Max of ${count} generators (${strategies}): scores [${scores}] \u2192 ${finalScore.toFixed(2)}`;
1045
- case "average" /* AVERAGE */:
1046
- return `Average of ${count} generators (${strategies}): scores [${scores}] \u2192 ${finalScore.toFixed(2)}`;
1047
- case "frequencyBoost" /* FREQUENCY_BOOST */: {
1048
- const avg = cards.reduce((sum, c) => sum + c.score, 0) / count;
1049
- const boost = 1 + FREQUENCY_BOOST_FACTOR * (count - 1);
1050
- return `Frequency boost from ${count} generators (${strategies}): avg ${avg.toFixed(2)} \xD7 ${boost.toFixed(2)} \u2192 ${finalScore.toFixed(2)}`;
1051
- }
1052
- default:
1053
- return `Aggregated from ${count} generators: ${finalScore.toFixed(2)}`;
1054
- }
2159
+ async getOrchestrationContext() {
2160
+ return createOrchestrationContext(this.user, this.course);
1055
2161
  }
1056
2162
  /**
1057
- * Aggregate scores from multiple generators for the same card.
1058
- */
1059
- aggregateScores(cards) {
1060
- const scores = cards.map((c) => c.score);
1061
- switch (this.aggregationMode) {
1062
- case "max" /* MAX */:
1063
- return Math.max(...scores);
1064
- case "average" /* AVERAGE */:
1065
- return scores.reduce((sum, s) => sum + s, 0) / scores.length;
1066
- case "frequencyBoost" /* FREQUENCY_BOOST */: {
1067
- const avg = scores.reduce((sum, s) => sum + s, 0) / scores.length;
1068
- const frequencyBoost = 1 + FREQUENCY_BOOST_FACTOR * (cards.length - 1);
1069
- return avg * frequencyBoost;
1070
- }
1071
- default:
1072
- return scores[0];
2163
+ * Get IDs of all strategies in this pipeline.
2164
+ * Used to record which strategies contributed to an outcome.
2165
+ */
2166
+ getStrategyIds() {
2167
+ const ids = [];
2168
+ const extractId = (obj) => {
2169
+ if (obj.strategyId) return obj.strategyId;
2170
+ return null;
2171
+ };
2172
+ const genId = extractId(this.generator);
2173
+ if (genId) ids.push(genId);
2174
+ if (this.generator.generators && Array.isArray(this.generator.generators)) {
2175
+ this.generator.generators.forEach((g) => {
2176
+ const subId = extractId(g);
2177
+ if (subId) ids.push(subId);
2178
+ });
2179
+ }
2180
+ for (const filter of this.filters) {
2181
+ const fId = extractId(filter);
2182
+ if (fId) ids.push(fId);
1073
2183
  }
2184
+ return [...new Set(ids)];
1074
2185
  }
1075
2186
  };
1076
2187
  }
1077
2188
  });
1078
2189
 
1079
2190
  // src/core/navigators/PipelineAssembler.ts
2191
+ var PipelineAssembler_exports = {};
2192
+ __export(PipelineAssembler_exports, {
2193
+ PipelineAssembler: () => PipelineAssembler
2194
+ });
1080
2195
  var PipelineAssembler;
1081
2196
  var init_PipelineAssembler = __esm({
1082
2197
  "src/core/navigators/PipelineAssembler.ts"() {
1083
2198
  "use strict";
1084
2199
  init_navigators();
2200
+ init_WeightedFilter();
1085
2201
  init_Pipeline();
1086
2202
  init_types_legacy();
1087
2203
  init_logger();
@@ -1143,276 +2259,76 @@ var init_PipelineAssembler = __esm({
1143
2259
  generator = nav;
1144
2260
  logger.debug(`[PipelineAssembler] Using single generator: ${generatorStrategies[0].name}`);
1145
2261
  } else {
1146
- logger.debug(
1147
- `[PipelineAssembler] Using CompositeGenerator for ${generatorStrategies.length} generators: ${generatorStrategies.map((g) => g.name).join(", ")}`
1148
- );
1149
- generator = await CompositeGenerator.fromStrategies(user, course, generatorStrategies);
1150
- }
1151
- const filters = [];
1152
- const sortedFilterStrategies = [...filterStrategies].sort(
1153
- (a, b) => a.name.localeCompare(b.name)
1154
- );
1155
- for (const filterStrategy of sortedFilterStrategies) {
1156
- try {
1157
- const nav = await ContentNavigator.create(user, course, filterStrategy);
1158
- if ("transform" in nav && typeof nav.transform === "function") {
1159
- filters.push(nav);
1160
- logger.debug(`[PipelineAssembler] Added filter: ${filterStrategy.name}`);
1161
- } else {
1162
- warnings.push(
1163
- `Filter '${filterStrategy.name}' does not implement CardFilter.transform(), skipping`
1164
- );
1165
- }
1166
- } catch (e) {
1167
- warnings.push(`Failed to instantiate filter '${filterStrategy.name}': ${e}`);
1168
- }
1169
- }
1170
- const pipeline = new Pipeline(generator, filters, user, course);
1171
- logger.debug(
1172
- `[PipelineAssembler] Assembled pipeline with ${generatorStrategies.length} generator(s) and ${filters.length} filter(s)`
1173
- );
1174
- return {
1175
- pipeline,
1176
- generatorStrategies,
1177
- filterStrategies: sortedFilterStrategies,
1178
- warnings
1179
- };
1180
- }
1181
- /**
1182
- * Creates a default ELO generator strategy.
1183
- * Used when filters are configured but no generator is specified.
1184
- */
1185
- makeDefaultEloStrategy(courseId) {
1186
- return {
1187
- _id: "NAVIGATION_STRATEGY-ELO-default",
1188
- course: courseId,
1189
- docType: "NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */,
1190
- name: "ELO (default)",
1191
- description: "Default ELO-based generator",
1192
- implementingClass: "elo" /* ELO */,
1193
- serializedData: ""
1194
- };
1195
- }
1196
- };
1197
- }
1198
- });
1199
-
1200
- // src/core/navigators/generators/elo.ts
1201
- var import_common6, ELONavigator;
1202
- var init_elo = __esm({
1203
- "src/core/navigators/generators/elo.ts"() {
1204
- "use strict";
1205
- init_navigators();
1206
- import_common6 = require("@vue-skuilder/common");
1207
- ELONavigator = class extends ContentNavigator {
1208
- /** Human-readable name for CardGenerator interface */
1209
- name;
1210
- constructor(user, course, strategyData) {
1211
- super(user, course, strategyData);
1212
- this.name = strategyData?.name || "ELO";
1213
- }
1214
- /**
1215
- * Get new cards with suitability scores based on ELO distance.
1216
- *
1217
- * Cards closer to user's ELO get higher scores.
1218
- * Score formula: max(0, 1 - distance / 500)
1219
- *
1220
- * NOTE: This generator only handles NEW cards. Reviews are handled by
1221
- * SRSNavigator. Use CompositeGenerator to combine both.
1222
- *
1223
- * This method supports both the legacy signature (limit only) and the
1224
- * CardGenerator interface signature (limit, context).
1225
- *
1226
- * @param limit - Maximum number of cards to return
1227
- * @param context - Optional GeneratorContext (used when called via Pipeline)
1228
- */
1229
- async getWeightedCards(limit, context) {
1230
- let userGlobalElo;
1231
- if (context?.userElo !== void 0) {
1232
- userGlobalElo = context.userElo;
1233
- } else {
1234
- const courseReg = await this.user.getCourseRegDoc(this.course.getCourseID());
1235
- const userElo = (0, import_common6.toCourseElo)(courseReg.elo);
1236
- userGlobalElo = userElo.global.score;
1237
- }
1238
- const activeCards = await this.user.getActiveCards();
1239
- const newCards = (await this.course.getCardsCenteredAtELO(
1240
- { limit, elo: "user" },
1241
- (c) => !activeCards.some((ac) => c.cardID === ac.cardID)
1242
- )).map((c) => ({ ...c, status: "new" }));
1243
- const cardIds = newCards.map((c) => c.cardID);
1244
- const cardEloData = await this.course.getCardEloData(cardIds);
1245
- const scored = newCards.map((c, i) => {
1246
- const cardElo = cardEloData[i]?.global?.score ?? 1e3;
1247
- const distance = Math.abs(cardElo - userGlobalElo);
1248
- const score = Math.max(0, 1 - distance / 500);
1249
- return {
1250
- cardId: c.cardID,
1251
- courseId: c.courseID,
1252
- score,
1253
- provenance: [
1254
- {
1255
- strategy: "elo",
1256
- strategyName: this.strategyName || this.name,
1257
- strategyId: this.strategyId || "NAVIGATION_STRATEGY-ELO-default",
1258
- action: "generated",
1259
- score,
1260
- reason: `ELO distance ${Math.round(distance)} (card: ${Math.round(cardElo)}, user: ${Math.round(userGlobalElo)}), new card`
1261
- }
1262
- ]
1263
- };
1264
- });
1265
- scored.sort((a, b) => b.score - a.score);
1266
- return scored.slice(0, limit);
1267
- }
1268
- };
1269
- }
1270
- });
1271
-
1272
- // src/core/navigators/generators/srs.ts
1273
- var import_moment, SRSNavigator;
1274
- var init_srs = __esm({
1275
- "src/core/navigators/generators/srs.ts"() {
1276
- "use strict";
1277
- import_moment = __toESM(require("moment"), 1);
1278
- init_navigators();
1279
- init_logger();
1280
- SRSNavigator = class extends ContentNavigator {
1281
- /** Human-readable name for CardGenerator interface */
1282
- name;
1283
- constructor(user, course, strategyData) {
1284
- super(user, course, strategyData);
1285
- this.name = strategyData?.name || "SRS";
1286
- }
1287
- /**
1288
- * Get review cards scored by urgency.
1289
- *
1290
- * Score formula combines:
1291
- * - Relative overdueness: hoursOverdue / intervalHours
1292
- * - Interval recency: exponential decay favoring shorter intervals
1293
- *
1294
- * Cards not yet due are excluded (not scored as 0).
1295
- *
1296
- * This method supports both the legacy signature (limit only) and the
1297
- * CardGenerator interface signature (limit, context).
1298
- *
1299
- * @param limit - Maximum number of cards to return
1300
- * @param _context - Optional GeneratorContext (currently unused, but required for interface)
1301
- */
1302
- async getWeightedCards(limit, _context) {
1303
- if (!this.user || !this.course) {
1304
- throw new Error("SRSNavigator requires user and course to be set");
1305
- }
1306
- const reviews = await this.user.getPendingReviews(this.course.getCourseID());
1307
- const now = import_moment.default.utc();
1308
- const dueReviews = reviews.filter((r) => now.isAfter(import_moment.default.utc(r.reviewTime)));
1309
- const scored = dueReviews.map((review) => {
1310
- const { score, reason } = this.computeUrgencyScore(review, now);
1311
- return {
1312
- cardId: review.cardId,
1313
- courseId: review.courseId,
1314
- score,
1315
- reviewID: review._id,
1316
- provenance: [
1317
- {
1318
- strategy: "srs",
1319
- strategyName: this.strategyName || this.name,
1320
- strategyId: this.strategyId || "NAVIGATION_STRATEGY-SRS-default",
1321
- action: "generated",
1322
- score,
1323
- reason
1324
- }
1325
- ]
1326
- };
1327
- });
1328
- logger.debug(`[srsNav] got ${scored.length} weighted cards`);
1329
- return scored.sort((a, b) => b.score - a.score).slice(0, limit);
1330
- }
1331
- /**
1332
- * Compute urgency score for a review card.
1333
- *
1334
- * Two factors:
1335
- * 1. Relative overdueness = hoursOverdue / intervalHours
1336
- * - 2 days overdue on 3-day interval = 0.67 (urgent)
1337
- * - 2 days overdue on 180-day interval = 0.01 (not urgent)
1338
- *
1339
- * 2. Interval recency factor = 0.3 + 0.7 * exp(-intervalHours / 720)
1340
- * - 24h interval → ~1.0 (very recent learning)
1341
- * - 30 days (720h) → ~0.56
1342
- * - 180 days → ~0.30
1343
- *
1344
- * Combined: base 0.5 + weighted average of factors * 0.45
1345
- * Result range: approximately 0.5 to 0.95
1346
- */
1347
- computeUrgencyScore(review, now) {
1348
- const scheduledAt = import_moment.default.utc(review.scheduledAt);
1349
- const due = import_moment.default.utc(review.reviewTime);
1350
- const intervalHours = Math.max(1, due.diff(scheduledAt, "hours"));
1351
- const hoursOverdue = now.diff(due, "hours");
1352
- const relativeOverdue = hoursOverdue / intervalHours;
1353
- const recencyFactor = 0.3 + 0.7 * Math.exp(-intervalHours / 720);
1354
- const overdueContribution = Math.min(1, Math.max(0, relativeOverdue));
1355
- const urgency = overdueContribution * 0.5 + recencyFactor * 0.5;
1356
- const score = Math.min(0.95, 0.5 + urgency * 0.45);
1357
- const reason = `${Math.round(hoursOverdue)}h overdue (interval: ${Math.round(intervalHours)}h, relative: ${relativeOverdue.toFixed(2)}), recency: ${recencyFactor.toFixed(2)}, review`;
1358
- return { score, reason };
1359
- }
1360
- };
1361
- }
1362
- });
1363
-
1364
- // src/core/navigators/filters/eloDistance.ts
1365
- function computeMultiplier(distance, halfLife, minMultiplier, maxMultiplier) {
1366
- const normalizedDistance = distance / halfLife;
1367
- const decay = Math.exp(-(normalizedDistance * normalizedDistance));
1368
- return minMultiplier + (maxMultiplier - minMultiplier) * decay;
1369
- }
1370
- function createEloDistanceFilter(config) {
1371
- const halfLife = config?.halfLife ?? DEFAULT_HALF_LIFE;
1372
- const minMultiplier = config?.minMultiplier ?? DEFAULT_MIN_MULTIPLIER;
1373
- const maxMultiplier = config?.maxMultiplier ?? DEFAULT_MAX_MULTIPLIER;
1374
- return {
1375
- name: "ELO Distance Filter",
1376
- async transform(cards, context) {
1377
- const { course, userElo } = context;
1378
- const cardIds = cards.map((c) => c.cardId);
1379
- const cardElos = await course.getCardEloData(cardIds);
1380
- return cards.map((card, i) => {
1381
- const cardElo = cardElos[i]?.global?.score ?? 1e3;
1382
- const distance = Math.abs(cardElo - userElo);
1383
- const multiplier = computeMultiplier(distance, halfLife, minMultiplier, maxMultiplier);
1384
- const newScore = card.score * multiplier;
1385
- const action = multiplier < maxMultiplier - 0.01 ? "penalized" : "passed";
1386
- return {
1387
- ...card,
1388
- score: newScore,
1389
- provenance: [
1390
- ...card.provenance,
1391
- {
1392
- strategy: "eloDistance",
1393
- strategyName: "ELO Distance Filter",
1394
- strategyId: "ELO_DISTANCE_FILTER",
1395
- action,
1396
- score: newScore,
1397
- reason: `ELO distance ${Math.round(distance)} (card: ${Math.round(cardElo)}, user: ${Math.round(userElo)}) \u2192 ${multiplier.toFixed(2)}x`
2262
+ logger.debug(
2263
+ `[PipelineAssembler] Using CompositeGenerator for ${generatorStrategies.length} generators: ${generatorStrategies.map((g) => g.name).join(", ")}`
2264
+ );
2265
+ generator = await CompositeGenerator.fromStrategies(user, course, generatorStrategies);
2266
+ }
2267
+ const filters = [];
2268
+ const sortedFilterStrategies = [...filterStrategies].sort(
2269
+ (a, b) => a.name.localeCompare(b.name)
2270
+ );
2271
+ for (const filterStrategy of sortedFilterStrategies) {
2272
+ try {
2273
+ const nav = await ContentNavigator.create(user, course, filterStrategy);
2274
+ if ("transform" in nav && typeof nav.transform === "function") {
2275
+ let filter = nav;
2276
+ if (filterStrategy.learnable) {
2277
+ filter = new WeightedFilter(
2278
+ filter,
2279
+ filterStrategy.learnable,
2280
+ filterStrategy.staticWeight,
2281
+ filterStrategy._id
2282
+ );
2283
+ }
2284
+ filters.push(filter);
2285
+ logger.debug(`[PipelineAssembler] Added filter: ${filterStrategy.name}`);
2286
+ } else {
2287
+ warnings.push(
2288
+ `Filter '${filterStrategy.name}' does not implement CardFilter.transform(), skipping`
2289
+ );
1398
2290
  }
1399
- ]
2291
+ } catch (e) {
2292
+ warnings.push(`Failed to instantiate filter '${filterStrategy.name}': ${e}`);
2293
+ }
2294
+ }
2295
+ const pipeline = new Pipeline(generator, filters, user, course);
2296
+ logger.debug(
2297
+ `[PipelineAssembler] Assembled pipeline with ${generatorStrategies.length} generator(s) and ${filters.length} filter(s)`
2298
+ );
2299
+ return {
2300
+ pipeline,
2301
+ generatorStrategies,
2302
+ filterStrategies: sortedFilterStrategies,
2303
+ warnings
1400
2304
  };
1401
- });
1402
- }
1403
- };
1404
- }
1405
- var DEFAULT_HALF_LIFE, DEFAULT_MIN_MULTIPLIER, DEFAULT_MAX_MULTIPLIER;
1406
- var init_eloDistance = __esm({
1407
- "src/core/navigators/filters/eloDistance.ts"() {
1408
- "use strict";
1409
- DEFAULT_HALF_LIFE = 200;
1410
- DEFAULT_MIN_MULTIPLIER = 0.3;
1411
- DEFAULT_MAX_MULTIPLIER = 1;
2305
+ }
2306
+ /**
2307
+ * Creates a default ELO generator strategy.
2308
+ * Used when filters are configured but no generator is specified.
2309
+ */
2310
+ makeDefaultEloStrategy(courseId) {
2311
+ return {
2312
+ _id: "NAVIGATION_STRATEGY-ELO-default",
2313
+ course: courseId,
2314
+ docType: "NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */,
2315
+ name: "ELO (default)",
2316
+ description: "Default ELO-based generator",
2317
+ implementingClass: "elo" /* ELO */,
2318
+ serializedData: ""
2319
+ };
2320
+ }
2321
+ };
1412
2322
  }
1413
2323
  });
1414
2324
 
1415
2325
  // src/core/navigators/defaults.ts
2326
+ var defaults_exports = {};
2327
+ __export(defaults_exports, {
2328
+ createDefaultEloStrategy: () => createDefaultEloStrategy,
2329
+ createDefaultPipeline: () => createDefaultPipeline,
2330
+ createDefaultSrsStrategy: () => createDefaultSrsStrategy
2331
+ });
1416
2332
  function createDefaultEloStrategy(courseId) {
1417
2333
  return {
1418
2334
  _id: "NAVIGATION_STRATEGY-ELO-default",
@@ -1456,6 +2372,304 @@ var init_defaults = __esm({
1456
2372
  }
1457
2373
  });
1458
2374
 
2375
+ // import("./**/*") in src/core/navigators/index.ts
2376
+ var globImport;
2377
+ var init_3 = __esm({
2378
+ 'import("./**/*") in src/core/navigators/index.ts'() {
2379
+ globImport = __glob({
2380
+ "./Pipeline.ts": () => Promise.resolve().then(() => (init_Pipeline(), Pipeline_exports)),
2381
+ "./PipelineAssembler.ts": () => Promise.resolve().then(() => (init_PipelineAssembler(), PipelineAssembler_exports)),
2382
+ "./defaults.ts": () => Promise.resolve().then(() => (init_defaults(), defaults_exports)),
2383
+ "./filters/WeightedFilter.ts": () => Promise.resolve().then(() => (init_WeightedFilter(), WeightedFilter_exports)),
2384
+ "./filters/eloDistance.ts": () => Promise.resolve().then(() => (init_eloDistance(), eloDistance_exports)),
2385
+ "./filters/hierarchyDefinition.ts": () => Promise.resolve().then(() => (init_hierarchyDefinition(), hierarchyDefinition_exports)),
2386
+ "./filters/index.ts": () => Promise.resolve().then(() => (init_filters(), filters_exports)),
2387
+ "./filters/inferredPreferenceStub.ts": () => Promise.resolve().then(() => (init_inferredPreferenceStub(), inferredPreferenceStub_exports)),
2388
+ "./filters/interferenceMitigator.ts": () => Promise.resolve().then(() => (init_interferenceMitigator(), interferenceMitigator_exports)),
2389
+ "./filters/relativePriority.ts": () => Promise.resolve().then(() => (init_relativePriority(), relativePriority_exports)),
2390
+ "./filters/types.ts": () => Promise.resolve().then(() => (init_types2(), types_exports2)),
2391
+ "./filters/userGoalStub.ts": () => Promise.resolve().then(() => (init_userGoalStub(), userGoalStub_exports)),
2392
+ "./filters/userTagPreference.ts": () => Promise.resolve().then(() => (init_userTagPreference(), userTagPreference_exports)),
2393
+ "./generators/CompositeGenerator.ts": () => Promise.resolve().then(() => (init_CompositeGenerator(), CompositeGenerator_exports)),
2394
+ "./generators/elo.ts": () => Promise.resolve().then(() => (init_elo(), elo_exports)),
2395
+ "./generators/index.ts": () => Promise.resolve().then(() => (init_generators(), generators_exports)),
2396
+ "./generators/srs.ts": () => Promise.resolve().then(() => (init_srs(), srs_exports)),
2397
+ "./generators/types.ts": () => Promise.resolve().then(() => (init_types(), types_exports)),
2398
+ "./index.ts": () => Promise.resolve().then(() => (init_navigators(), navigators_exports))
2399
+ });
2400
+ }
2401
+ });
2402
+
2403
+ // src/core/navigators/index.ts
2404
+ var navigators_exports = {};
2405
+ __export(navigators_exports, {
2406
+ ContentNavigator: () => ContentNavigator,
2407
+ NavigatorRole: () => NavigatorRole,
2408
+ NavigatorRoles: () => NavigatorRoles,
2409
+ Navigators: () => Navigators,
2410
+ getCardOrigin: () => getCardOrigin,
2411
+ getRegisteredNavigator: () => getRegisteredNavigator,
2412
+ getRegisteredNavigatorNames: () => getRegisteredNavigatorNames,
2413
+ hasRegisteredNavigator: () => hasRegisteredNavigator,
2414
+ initializeNavigatorRegistry: () => initializeNavigatorRegistry,
2415
+ isFilter: () => isFilter,
2416
+ isGenerator: () => isGenerator,
2417
+ registerNavigator: () => registerNavigator
2418
+ });
2419
+ function registerNavigator(implementingClass, constructor) {
2420
+ navigatorRegistry.set(implementingClass, constructor);
2421
+ logger.debug(`[NavigatorRegistry] Registered: ${implementingClass}`);
2422
+ }
2423
+ function getRegisteredNavigator(implementingClass) {
2424
+ return navigatorRegistry.get(implementingClass);
2425
+ }
2426
+ function hasRegisteredNavigator(implementingClass) {
2427
+ return navigatorRegistry.has(implementingClass);
2428
+ }
2429
+ function getRegisteredNavigatorNames() {
2430
+ return Array.from(navigatorRegistry.keys());
2431
+ }
2432
+ async function initializeNavigatorRegistry() {
2433
+ logger.debug("[NavigatorRegistry] Initializing built-in navigators...");
2434
+ const [eloModule, srsModule] = await Promise.all([
2435
+ Promise.resolve().then(() => (init_elo(), elo_exports)),
2436
+ Promise.resolve().then(() => (init_srs(), srs_exports))
2437
+ ]);
2438
+ registerNavigator("elo", eloModule.default);
2439
+ registerNavigator("srs", srsModule.default);
2440
+ const [
2441
+ hierarchyModule,
2442
+ interferenceModule,
2443
+ relativePriorityModule,
2444
+ userTagPreferenceModule
2445
+ ] = await Promise.all([
2446
+ Promise.resolve().then(() => (init_hierarchyDefinition(), hierarchyDefinition_exports)),
2447
+ Promise.resolve().then(() => (init_interferenceMitigator(), interferenceMitigator_exports)),
2448
+ Promise.resolve().then(() => (init_relativePriority(), relativePriority_exports)),
2449
+ Promise.resolve().then(() => (init_userTagPreference(), userTagPreference_exports))
2450
+ ]);
2451
+ registerNavigator("hierarchyDefinition", hierarchyModule.default);
2452
+ registerNavigator("interferenceMitigator", interferenceModule.default);
2453
+ registerNavigator("relativePriority", relativePriorityModule.default);
2454
+ registerNavigator("userTagPreference", userTagPreferenceModule.default);
2455
+ logger.debug(
2456
+ `[NavigatorRegistry] Initialized ${navigatorRegistry.size} navigators: ${getRegisteredNavigatorNames().join(", ")}`
2457
+ );
2458
+ }
2459
+ function getCardOrigin(card) {
2460
+ if (card.provenance.length === 0) {
2461
+ throw new Error("Card has no provenance - cannot determine origin");
2462
+ }
2463
+ const firstEntry = card.provenance[0];
2464
+ const reason = firstEntry.reason.toLowerCase();
2465
+ if (reason.includes("failed")) {
2466
+ return "failed";
2467
+ }
2468
+ if (reason.includes("review")) {
2469
+ return "review";
2470
+ }
2471
+ return "new";
2472
+ }
2473
+ function isGenerator(impl) {
2474
+ return NavigatorRoles[impl] === "generator" /* GENERATOR */;
2475
+ }
2476
+ function isFilter(impl) {
2477
+ return NavigatorRoles[impl] === "filter" /* FILTER */;
2478
+ }
2479
+ var navigatorRegistry, Navigators, NavigatorRole, NavigatorRoles, ContentNavigator;
2480
+ var init_navigators = __esm({
2481
+ "src/core/navigators/index.ts"() {
2482
+ "use strict";
2483
+ init_logger();
2484
+ init_();
2485
+ init_2();
2486
+ init_3();
2487
+ navigatorRegistry = /* @__PURE__ */ new Map();
2488
+ Navigators = /* @__PURE__ */ ((Navigators2) => {
2489
+ Navigators2["ELO"] = "elo";
2490
+ Navigators2["SRS"] = "srs";
2491
+ Navigators2["HIERARCHY"] = "hierarchyDefinition";
2492
+ Navigators2["INTERFERENCE"] = "interferenceMitigator";
2493
+ Navigators2["RELATIVE_PRIORITY"] = "relativePriority";
2494
+ Navigators2["USER_TAG_PREFERENCE"] = "userTagPreference";
2495
+ return Navigators2;
2496
+ })(Navigators || {});
2497
+ NavigatorRole = /* @__PURE__ */ ((NavigatorRole2) => {
2498
+ NavigatorRole2["GENERATOR"] = "generator";
2499
+ NavigatorRole2["FILTER"] = "filter";
2500
+ return NavigatorRole2;
2501
+ })(NavigatorRole || {});
2502
+ NavigatorRoles = {
2503
+ ["elo" /* ELO */]: "generator" /* GENERATOR */,
2504
+ ["srs" /* SRS */]: "generator" /* GENERATOR */,
2505
+ ["hierarchyDefinition" /* HIERARCHY */]: "filter" /* FILTER */,
2506
+ ["interferenceMitigator" /* INTERFERENCE */]: "filter" /* FILTER */,
2507
+ ["relativePriority" /* RELATIVE_PRIORITY */]: "filter" /* FILTER */,
2508
+ ["userTagPreference" /* USER_TAG_PREFERENCE */]: "filter" /* FILTER */
2509
+ };
2510
+ ContentNavigator = class {
2511
+ /** User interface for this navigation session */
2512
+ user;
2513
+ /** Course interface for this navigation session */
2514
+ course;
2515
+ /** Human-readable name for this strategy instance (from ContentNavigationStrategyData.name) */
2516
+ strategyName;
2517
+ /** Unique document ID for this strategy instance (from ContentNavigationStrategyData._id) */
2518
+ strategyId;
2519
+ /** Evolutionary weighting configuration */
2520
+ learnable;
2521
+ /** Whether to bypass deviation (manual/static weighting) */
2522
+ staticWeight;
2523
+ /**
2524
+ * Constructor for standard navigators.
2525
+ * Call this from subclass constructors to initialize common fields.
2526
+ *
2527
+ * Note: CompositeGenerator and Pipeline call super() without args, then set
2528
+ * user/course fields directly if needed.
2529
+ */
2530
+ constructor(user, course, strategyData) {
2531
+ this.user = user;
2532
+ this.course = course;
2533
+ if (strategyData) {
2534
+ this.strategyName = strategyData.name;
2535
+ this.strategyId = strategyData._id;
2536
+ this.learnable = strategyData.learnable;
2537
+ this.staticWeight = strategyData.staticWeight;
2538
+ }
2539
+ }
2540
+ // ============================================================================
2541
+ // STRATEGY STATE HELPERS
2542
+ // ============================================================================
2543
+ //
2544
+ // These methods allow strategies to persist their own state (user preferences,
2545
+ // learned patterns, temporal tracking) in the user database.
2546
+ //
2547
+ // ============================================================================
2548
+ /**
2549
+ * Unique key identifying this strategy for state storage.
2550
+ *
2551
+ * Defaults to the constructor name (e.g., "UserTagPreferenceFilter").
2552
+ * Override in subclasses if multiple instances of the same strategy type
2553
+ * need separate state storage.
2554
+ */
2555
+ get strategyKey() {
2556
+ return this.constructor.name;
2557
+ }
2558
+ /**
2559
+ * Get this strategy's persisted state for the current course.
2560
+ *
2561
+ * @returns The strategy's data payload, or null if no state exists
2562
+ * @throws Error if user or course is not initialized
2563
+ */
2564
+ async getStrategyState() {
2565
+ if (!this.user || !this.course) {
2566
+ throw new Error(
2567
+ `Cannot get strategy state: navigator not properly initialized. Ensure user and course are provided to constructor.`
2568
+ );
2569
+ }
2570
+ return this.user.getStrategyState(this.course.getCourseID(), this.strategyKey);
2571
+ }
2572
+ /**
2573
+ * Persist this strategy's state for the current course.
2574
+ *
2575
+ * @param data - The strategy's data payload to store
2576
+ * @throws Error if user or course is not initialized
2577
+ */
2578
+ async putStrategyState(data) {
2579
+ if (!this.user || !this.course) {
2580
+ throw new Error(
2581
+ `Cannot put strategy state: navigator not properly initialized. Ensure user and course are provided to constructor.`
2582
+ );
2583
+ }
2584
+ return this.user.putStrategyState(this.course.getCourseID(), this.strategyKey, data);
2585
+ }
2586
+ /**
2587
+ * Factory method to create navigator instances.
2588
+ *
2589
+ * First checks the navigator registry for a pre-registered constructor.
2590
+ * If not found, falls back to dynamic import (for custom navigators).
2591
+ *
2592
+ * For reliable operation in test environments, call initializeNavigatorRegistry()
2593
+ * before using this method.
2594
+ *
2595
+ * @param user - User interface
2596
+ * @param course - Course interface
2597
+ * @param strategyData - Strategy configuration document
2598
+ * @returns the runtime object used to steer a study session.
2599
+ */
2600
+ static async create(user, course, strategyData) {
2601
+ const implementingClass = strategyData.implementingClass;
2602
+ const RegisteredImpl = getRegisteredNavigator(implementingClass);
2603
+ if (RegisteredImpl) {
2604
+ logger.debug(`[ContentNavigator.create] Using registered navigator: ${implementingClass}`);
2605
+ return new RegisteredImpl(user, course, strategyData);
2606
+ }
2607
+ logger.debug(
2608
+ `[ContentNavigator.create] Navigator not in registry, attempting dynamic import: ${implementingClass}`
2609
+ );
2610
+ let NavigatorImpl;
2611
+ const variations = [".ts", ".js", ""];
2612
+ for (const ext of variations) {
2613
+ try {
2614
+ const module2 = await globImport_generators(`./generators/${implementingClass}${ext}`);
2615
+ NavigatorImpl = module2.default;
2616
+ if (NavigatorImpl) break;
2617
+ } catch (e) {
2618
+ logger.debug(`Failed to load generator ${implementingClass}${ext}:`, e);
2619
+ }
2620
+ try {
2621
+ const module2 = await globImport_filters(`./filters/${implementingClass}${ext}`);
2622
+ NavigatorImpl = module2.default;
2623
+ if (NavigatorImpl) break;
2624
+ } catch (e) {
2625
+ logger.debug(`Failed to load filter ${implementingClass}${ext}:`, e);
2626
+ }
2627
+ try {
2628
+ const module2 = await globImport(`./${implementingClass}${ext}`);
2629
+ NavigatorImpl = module2.default;
2630
+ if (NavigatorImpl) break;
2631
+ } catch (e) {
2632
+ logger.debug(`Failed to load legacy ${implementingClass}${ext}:`, e);
2633
+ }
2634
+ if (NavigatorImpl) break;
2635
+ }
2636
+ if (!NavigatorImpl) {
2637
+ throw new Error(`Could not load navigator implementation for: ${implementingClass}`);
2638
+ }
2639
+ return new NavigatorImpl(user, course, strategyData);
2640
+ }
2641
+ /**
2642
+ * Get cards with suitability scores and provenance trails.
2643
+ *
2644
+ * **This is the PRIMARY API for navigation strategies.**
2645
+ *
2646
+ * Returns cards ranked by suitability score (0-1). Higher scores indicate
2647
+ * better candidates for presentation. Each card includes a provenance trail
2648
+ * documenting how strategies contributed to the final score.
2649
+ *
2650
+ * ## Implementation Required
2651
+ * All navigation strategies MUST override this method. The base class does
2652
+ * not provide a default implementation.
2653
+ *
2654
+ * ## For Generators
2655
+ * Override this method to generate candidates and compute scores based on
2656
+ * your strategy's logic (e.g., ELO proximity, review urgency). Create the
2657
+ * initial provenance entry with action='generated'.
2658
+ *
2659
+ * ## For Filters
2660
+ * Filters should implement the CardFilter interface instead and be composed
2661
+ * via Pipeline. Filters do not directly implement getWeightedCards().
2662
+ *
2663
+ * @param limit - Maximum cards to return
2664
+ * @returns Cards sorted by score descending, with provenance trails
2665
+ */
2666
+ async getWeightedCards(_limit) {
2667
+ throw new Error(`${this.constructor.name} must implement getWeightedCards(). `);
2668
+ }
2669
+ };
2670
+ }
2671
+ });
2672
+
1459
2673
  // src/impl/couch/courseDB.ts
1460
2674
  function randIntWeightedTowardZero(n) {
1461
2675
  return Math.floor(Math.random() * Math.random() * Math.random() * n);
@@ -1574,11 +2788,11 @@ ${JSON.stringify(config)}
1574
2788
  function isSuccessRow(row) {
1575
2789
  return "doc" in row && row.doc !== null && row.doc !== void 0;
1576
2790
  }
1577
- var import_common7, CoursesDB, CourseDB;
2791
+ var import_common9, CoursesDB, CourseDB;
1578
2792
  var init_courseDB = __esm({
1579
2793
  "src/impl/couch/courseDB.ts"() {
1580
2794
  "use strict";
1581
- import_common7 = require("@vue-skuilder/common");
2795
+ import_common9 = require("@vue-skuilder/common");
1582
2796
  init_couch();
1583
2797
  init_updateQueue();
1584
2798
  init_types_legacy();
@@ -1700,14 +2914,14 @@ var init_courseDB = __esm({
1700
2914
  docs.rows.forEach((r) => {
1701
2915
  if (isSuccessRow(r)) {
1702
2916
  if (r.doc && r.doc.elo) {
1703
- ret.push((0, import_common7.toCourseElo)(r.doc.elo));
2917
+ ret.push((0, import_common9.toCourseElo)(r.doc.elo));
1704
2918
  } else {
1705
2919
  logger.warn("no elo data for card: " + r.id);
1706
- ret.push((0, import_common7.blankCourseElo)());
2920
+ ret.push((0, import_common9.blankCourseElo)());
1707
2921
  }
1708
2922
  } else {
1709
2923
  logger.warn("no elo data for card: " + JSON.stringify(r));
1710
- ret.push((0, import_common7.blankCourseElo)());
2924
+ ret.push((0, import_common9.blankCourseElo)());
1711
2925
  }
1712
2926
  });
1713
2927
  return ret;
@@ -1902,7 +3116,7 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
1902
3116
  async getCourseTagStubs() {
1903
3117
  return getCourseTagStubs(this.id);
1904
3118
  }
1905
- async addNote(codeCourse, shape, data, author, tags, uploads, elo = (0, import_common7.blankCourseElo)()) {
3119
+ async addNote(codeCourse, shape, data, author, tags, uploads, elo = (0, import_common9.blankCourseElo)()) {
1906
3120
  try {
1907
3121
  const resp = await addNote55(this.id, codeCourse, shape, data, author, tags, uploads, elo);
1908
3122
  if (resp.ok) {
@@ -1911,19 +3125,19 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
1911
3125
  `[courseDB.addNote] Note added but card creation failed: ${resp.cardCreationError}`
1912
3126
  );
1913
3127
  return {
1914
- status: import_common7.Status.error,
3128
+ status: import_common9.Status.error,
1915
3129
  message: `Note was added but no cards were created: ${resp.cardCreationError}`,
1916
3130
  id: resp.id
1917
3131
  };
1918
3132
  }
1919
3133
  return {
1920
- status: import_common7.Status.ok,
3134
+ status: import_common9.Status.ok,
1921
3135
  message: "",
1922
3136
  id: resp.id
1923
3137
  };
1924
3138
  } else {
1925
3139
  return {
1926
- status: import_common7.Status.error,
3140
+ status: import_common9.Status.error,
1927
3141
  message: "Unexpected error adding note"
1928
3142
  };
1929
3143
  }
@@ -1935,7 +3149,7 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
1935
3149
  message: ${err.message}`
1936
3150
  );
1937
3151
  return {
1938
- status: import_common7.Status.error,
3152
+ status: import_common9.Status.error,
1939
3153
  message: `Error adding note to course. ${e.reason || err.message}`
1940
3154
  };
1941
3155
  }
@@ -2063,7 +3277,7 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
2063
3277
  const courseDoc = (await u.getCourseRegistrationsDoc()).courses.find((c) => {
2064
3278
  return c.courseID === this.id;
2065
3279
  });
2066
- targetElo = (0, import_common7.EloToNumber)(courseDoc.elo);
3280
+ targetElo = (0, import_common9.EloToNumber)(courseDoc.elo);
2067
3281
  } catch {
2068
3282
  targetElo = 1e3;
2069
3283
  }
@@ -2475,11 +3689,11 @@ var init_classroomDB2 = __esm({
2475
3689
  });
2476
3690
 
2477
3691
  // src/study/TagFilteredContentSource.ts
2478
- var import_common8, TagFilteredContentSource;
3692
+ var import_common10, TagFilteredContentSource;
2479
3693
  var init_TagFilteredContentSource = __esm({
2480
3694
  "src/study/TagFilteredContentSource.ts"() {
2481
3695
  "use strict";
2482
- import_common8 = require("@vue-skuilder/common");
3696
+ import_common10 = require("@vue-skuilder/common");
2483
3697
  init_courseDB();
2484
3698
  init_logger();
2485
3699
  TagFilteredContentSource = class {
@@ -2565,7 +3779,7 @@ var init_TagFilteredContentSource = __esm({
2565
3779
  * @returns Cards sorted by score descending (all scores = 1.0)
2566
3780
  */
2567
3781
  async getWeightedCards(limit) {
2568
- if (!(0, import_common8.hasActiveFilter)(this.filter)) {
3782
+ if (!(0, import_common10.hasActiveFilter)(this.filter)) {
2569
3783
  logger.warn("[TagFilteredContentSource] getWeightedCards called with no active filter");
2570
3784
  return [];
2571
3785
  }
@@ -2653,19 +3867,19 @@ async function getStudySource(source, user) {
2653
3867
  if (source.type === "classroom") {
2654
3868
  return await StudentClassroomDB.factory(source.id, user);
2655
3869
  } else {
2656
- if ((0, import_common9.hasActiveFilter)(source.tagFilter)) {
3870
+ if ((0, import_common11.hasActiveFilter)(source.tagFilter)) {
2657
3871
  return new TagFilteredContentSource(source.id, source.tagFilter, user);
2658
3872
  }
2659
3873
  return getDataLayer().getCourseDB(source.id);
2660
3874
  }
2661
3875
  }
2662
- var import_common9;
3876
+ var import_common11;
2663
3877
  var init_contentSource = __esm({
2664
3878
  "src/core/interfaces/contentSource.ts"() {
2665
3879
  "use strict";
2666
3880
  init_factory();
2667
3881
  init_classroomDB2();
2668
- import_common9 = require("@vue-skuilder/common");
3882
+ import_common11 = require("@vue-skuilder/common");
2669
3883
  init_TagFilteredContentSource();
2670
3884
  }
2671
3885
  });
@@ -2721,6 +3935,13 @@ var init_strategyState = __esm({
2721
3935
  }
2722
3936
  });
2723
3937
 
3938
+ // src/core/types/userOutcome.ts
3939
+ var init_userOutcome = __esm({
3940
+ "src/core/types/userOutcome.ts"() {
3941
+ "use strict";
3942
+ }
3943
+ });
3944
+
2724
3945
  // src/core/util/index.ts
2725
3946
  function getCardHistoryID(courseID, cardID) {
2726
3947
  return `${DocTypePrefixes["CARDRECORD" /* CARDRECORD */]}-${courseID}-${cardID}`;
@@ -2733,17 +3954,17 @@ var init_util = __esm({
2733
3954
  });
2734
3955
 
2735
3956
  // src/core/bulkImport/cardProcessor.ts
2736
- var import_common10;
3957
+ var import_common12;
2737
3958
  var init_cardProcessor = __esm({
2738
3959
  "src/core/bulkImport/cardProcessor.ts"() {
2739
3960
  "use strict";
2740
- import_common10 = require("@vue-skuilder/common");
3961
+ import_common12 = require("@vue-skuilder/common");
2741
3962
  init_logger();
2742
3963
  }
2743
3964
  });
2744
3965
 
2745
3966
  // src/core/bulkImport/types.ts
2746
- var init_types = __esm({
3967
+ var init_types3 = __esm({
2747
3968
  "src/core/bulkImport/types.ts"() {
2748
3969
  "use strict";
2749
3970
  }
@@ -2754,7 +3975,7 @@ var init_bulkImport = __esm({
2754
3975
  "src/core/bulkImport/index.ts"() {
2755
3976
  "use strict";
2756
3977
  init_cardProcessor();
2757
- init_types();
3978
+ init_types3();
2758
3979
  }
2759
3980
  });
2760
3981
 
@@ -2766,10 +3987,12 @@ var init_core = __esm({
2766
3987
  init_types_legacy();
2767
3988
  init_user();
2768
3989
  init_strategyState();
3990
+ init_userOutcome();
2769
3991
  init_Loggable();
2770
3992
  init_util();
2771
3993
  init_navigators();
2772
3994
  init_bulkImport();
3995
+ init_orchestration();
2773
3996
  }
2774
3997
  });
2775
3998
 
@@ -3115,13 +4338,13 @@ async function dropUserFromClassroom(user, classID) {
3115
4338
  async function getUserClassrooms(user) {
3116
4339
  return getOrCreateClassroomRegistrationsDoc(user);
3117
4340
  }
3118
- var import_common11, import_moment5, log3, BaseUser, userCoursesDoc, userClassroomsDoc;
4341
+ var import_common13, import_moment5, log3, BaseUser, userCoursesDoc, userClassroomsDoc;
3119
4342
  var init_BaseUserDB = __esm({
3120
4343
  "src/impl/common/BaseUserDB.ts"() {
3121
4344
  "use strict";
3122
4345
  init_core();
3123
4346
  init_util();
3124
- import_common11 = require("@vue-skuilder/common");
4347
+ import_common13 = require("@vue-skuilder/common");
3125
4348
  import_moment5 = __toESM(require("moment"), 1);
3126
4349
  init_types_legacy();
3127
4350
  init_logger();
@@ -3171,7 +4394,7 @@ Currently logged-in as ${this._username}.`
3171
4394
  );
3172
4395
  }
3173
4396
  const result = await this.syncStrategy.createAccount(username, password);
3174
- if (result.status === import_common11.Status.ok) {
4397
+ if (result.status === import_common13.Status.ok) {
3175
4398
  log3(`Account created successfully, updating username to ${username}`);
3176
4399
  this._username = username;
3177
4400
  try {
@@ -3213,7 +4436,7 @@ Currently logged-in as ${this._username}.`
3213
4436
  async resetUserData() {
3214
4437
  if (this.syncStrategy.canAuthenticate()) {
3215
4438
  return {
3216
- status: import_common11.Status.error,
4439
+ status: import_common13.Status.error,
3217
4440
  error: "Reset user data is only available for local-only mode. Use logout instead for remote sync."
3218
4441
  };
3219
4442
  }
@@ -3232,11 +4455,11 @@ Currently logged-in as ${this._username}.`
3232
4455
  await localDB.bulkDocs(docsToDelete);
3233
4456
  }
3234
4457
  await this.init();
3235
- return { status: import_common11.Status.ok };
4458
+ return { status: import_common13.Status.ok };
3236
4459
  } catch (error) {
3237
4460
  logger.error("Failed to reset user data:", error);
3238
4461
  return {
3239
- status: import_common11.Status.error,
4462
+ status: import_common13.Status.error,
3240
4463
  error: error instanceof Error ? error.message : "Unknown error during reset"
3241
4464
  };
3242
4465
  }
@@ -3963,6 +5186,19 @@ Currently logged-in as ${this._username}.`
3963
5186
  };
3964
5187
  await this.localDB.put(doc);
3965
5188
  }
5189
+ async putUserOutcome(record) {
5190
+ try {
5191
+ await this.localDB.put(record);
5192
+ } catch (err) {
5193
+ if (err.status === 409) {
5194
+ const existing = await this.localDB.get(record._id);
5195
+ record._rev = existing._rev;
5196
+ await this.localDB.put(record);
5197
+ } else {
5198
+ throw err;
5199
+ }
5200
+ }
5201
+ }
3966
5202
  async deleteStrategyState(courseId, strategyKey) {
3967
5203
  const docId = buildStrategyStateId(courseId, strategyKey);
3968
5204
  try {
@@ -4005,6 +5241,7 @@ var init_factory = __esm({
4005
5241
  "use strict";
4006
5242
  init_common();
4007
5243
  init_logger();
5244
+ init_navigators();
4008
5245
  NOT_SET = "NOT_SET";
4009
5246
  ENV = {
4010
5247
  COUCHDB_SERVER_PROTOCOL: NOT_SET,
@@ -4130,14 +5367,14 @@ var init_auth = __esm({
4130
5367
  });
4131
5368
 
4132
5369
  // src/impl/couch/CouchDBSyncStrategy.ts
4133
- var import_common13, log4, CouchDBSyncStrategy;
5370
+ var import_common15, log4, CouchDBSyncStrategy;
4134
5371
  var init_CouchDBSyncStrategy = __esm({
4135
5372
  "src/impl/couch/CouchDBSyncStrategy.ts"() {
4136
5373
  "use strict";
4137
5374
  init_factory();
4138
5375
  init_types_legacy();
4139
5376
  init_logger();
4140
- import_common13 = require("@vue-skuilder/common");
5377
+ import_common15 = require("@vue-skuilder/common");
4141
5378
  init_common();
4142
5379
  init_pouchdb_setup();
4143
5380
  init_couch();
@@ -4208,32 +5445,32 @@ var init_CouchDBSyncStrategy = __esm({
4208
5445
  }
4209
5446
  }
4210
5447
  return {
4211
- status: import_common13.Status.ok,
5448
+ status: import_common15.Status.ok,
4212
5449
  error: void 0
4213
5450
  };
4214
5451
  } else {
4215
5452
  return {
4216
- status: import_common13.Status.error,
5453
+ status: import_common15.Status.error,
4217
5454
  error: "Failed to log in after account creation"
4218
5455
  };
4219
5456
  }
4220
5457
  } else {
4221
5458
  logger.warn(`Signup not OK: ${JSON.stringify(signupRequest)}`);
4222
5459
  return {
4223
- status: import_common13.Status.error,
5460
+ status: import_common15.Status.error,
4224
5461
  error: "Account creation failed"
4225
5462
  };
4226
5463
  }
4227
5464
  } catch (e) {
4228
5465
  if (e.reason === "Document update conflict.") {
4229
5466
  return {
4230
- status: import_common13.Status.error,
5467
+ status: import_common15.Status.error,
4231
5468
  error: "This username is taken!"
4232
5469
  };
4233
5470
  }
4234
5471
  logger.error(`Error on signup: ${JSON.stringify(e)}`);
4235
5472
  return {
4236
- status: import_common13.Status.error,
5473
+ status: import_common15.Status.error,
4237
5474
  error: e.message || "Unknown error during account creation"
4238
5475
  };
4239
5476
  }